blob: a62b5273060580010df0529440295a2cc6e2086a [file] [log] [blame]
// Copyright The Monogon Project Authors.
// SPDX-License-Identifier: Apache-2.0
// Package oci contains tools for handling OCI images.
package oci
import (
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"iter"
"strings"
ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"source.monogon.dev/osbase/structfs"
)
// Image represents an OCI image.
type Image struct {
// Manifest contains the parsed image manifest.
Manifest *ocispecv1.Manifest
// RawManifest contains the bytes of the image manifest.
RawManifest []byte
// ManifestDigest contains the computed digest of RawManifest.
ManifestDigest string
blobs Blobs
}
// Blobs is the interface which image sources implement to retrieve the content
// of blobs.
type Blobs interface {
// Blob returns the contents of a blob from its descriptor.
// It does not verify the contents against the digest.
Blob(*ocispecv1.Descriptor) (io.ReadCloser, error)
}
// NewImage verifies the manifest against the expected digest if not empty,
// then parses it and returns an [Image].
func NewImage(rawManifest []byte, expectedDigest string, blobs Blobs) (*Image, error) {
digest := fmt.Sprintf("sha256:%x", sha256.Sum256(rawManifest))
if expectedDigest != "" && expectedDigest != digest {
return nil, fmt.Errorf("failed verification of manifest: expected digest %q, computed %q", expectedDigest, digest)
}
manifest := &ocispecv1.Manifest{}
err := json.Unmarshal(rawManifest, &manifest)
if err != nil {
return nil, fmt.Errorf("failed to parse image manifest: %w", err)
}
if manifest.MediaType != ocispecv1.MediaTypeImageManifest {
return nil, fmt.Errorf("unexpected manifest media type %q", manifest.MediaType)
}
image := &Image{
Manifest: manifest,
RawManifest: rawManifest,
ManifestDigest: digest,
blobs: blobs,
}
for descriptor := range image.Descriptors() {
if descriptor.Size < 0 {
return nil, fmt.Errorf("invalid manifest: contains descriptor with negative size")
}
}
return image, nil
}
// Descriptors returns an iterator over all descriptors in the image (config and
// layers).
func (i *Image) Descriptors() iter.Seq[*ocispecv1.Descriptor] {
return func(yield func(*ocispecv1.Descriptor) bool) {
if !yield(&i.Manifest.Config) {
return
}
for l := range i.Manifest.Layers {
if !yield(&i.Manifest.Layers[l]) {
return
}
}
}
}
// Blob returns the contents of a blob from its descriptor.
// It does not verify the contents against the digest.
func (i *Image) Blob(descriptor *ocispecv1.Descriptor) (io.ReadCloser, error) {
if int64(len(descriptor.Data)) == descriptor.Size {
return structfs.Bytes(descriptor.Data).Open()
} else if len(descriptor.Data) != 0 {
return nil, fmt.Errorf("descriptor has embedded data of wrong length")
}
return i.blobs.Blob(descriptor)
}
// ReadBlobVerified reads a blob into a byte slice and verifies it against the
// digest.
func (i *Image) ReadBlobVerified(descriptor *ocispecv1.Descriptor) ([]byte, error) {
if descriptor.Size < 0 {
return nil, fmt.Errorf("invalid descriptor size %d", descriptor.Size)
}
if descriptor.Size > 50*1024*1024 {
return nil, fmt.Errorf("refusing to read blob of size %d into memory", descriptor.Size)
}
expectedDigest := string(descriptor.Digest)
if _, _, err := ParseDigest(expectedDigest); err != nil {
return nil, err
}
blob, err := i.Blob(descriptor)
if err != nil {
return nil, err
}
defer blob.Close()
content := make([]byte, descriptor.Size)
_, err = io.ReadFull(blob, content)
if err != nil {
return nil, err
}
digest := fmt.Sprintf("sha256:%x", sha256.Sum256(content))
if expectedDigest != digest {
return nil, fmt.Errorf("failed verification of blob: expected digest %q, computed %q", expectedDigest, digest)
}
return content, nil
}
// StructfsBlob wraps an image and descriptor into a [structfs.Blob].
func (i *Image) StructfsBlob(descriptor *ocispecv1.Descriptor) structfs.Blob {
return &structfsBlob{
image: i,
descriptor: descriptor,
}
}
type structfsBlob struct {
image *Image
descriptor *ocispecv1.Descriptor
}
func (b *structfsBlob) Open() (io.ReadCloser, error) {
return b.image.Blob(b.descriptor)
}
func (b *structfsBlob) Size() int64 {
return b.descriptor.Size
}
// ParseDigest splits a digest into its components. It returns an error if the
// algorithm is not supported, or if encoded is not valid for the algorithm.
func ParseDigest(digest string) (algorithm string, encoded string, err error) {
algorithm, encoded, ok := strings.Cut(digest, ":")
if !ok {
return "", "", fmt.Errorf("invalid digest")
}
switch algorithm {
case "sha256":
rest := strings.TrimLeft(encoded, "0123456789abcdef")
if len(rest) != 0 {
return "", "", fmt.Errorf("invalid character in sha256 digest")
}
if len(encoded) != sha256.Size*2 {
return "", "", fmt.Errorf("invalid sha256 digest length")
}
default:
return "", "", fmt.Errorf("unknown digest algorithm %q", algorithm)
}
return
}