| Jan Schär | b48174d | 2025-04-14 10:13:02 +0000 | [diff] [blame^] | 1 | // Copyright The Monogon Project Authors. |
| 2 | // SPDX-License-Identifier: Apache-2.0 |
| 3 | |
| 4 | // Package oci contains tools for handling OCI images. |
| 5 | package oci |
| 6 | |
| 7 | import ( |
| 8 | "crypto/sha256" |
| 9 | "encoding/json" |
| 10 | "fmt" |
| 11 | "io" |
| 12 | "iter" |
| 13 | "strings" |
| 14 | |
| 15 | ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" |
| 16 | |
| 17 | "source.monogon.dev/osbase/structfs" |
| 18 | ) |
| 19 | |
| 20 | // Image represents an OCI image. |
| 21 | type Image struct { |
| 22 | // Manifest contains the parsed image manifest. |
| 23 | Manifest *ocispecv1.Manifest |
| 24 | // RawManifest contains the bytes of the image manifest. |
| 25 | RawManifest []byte |
| 26 | // ManifestDigest contains the computed digest of RawManifest. |
| 27 | ManifestDigest string |
| 28 | |
| 29 | blobs Blobs |
| 30 | } |
| 31 | |
| 32 | // Blobs is the interface which image sources implement to retrieve the content |
| 33 | // of blobs. |
| 34 | type Blobs interface { |
| 35 | // Blob returns the contents of a blob from its descriptor. |
| 36 | // It does not verify the contents against the digest. |
| 37 | Blob(*ocispecv1.Descriptor) (io.ReadCloser, error) |
| 38 | } |
| 39 | |
| 40 | // NewImage verifies the manifest against the expected digest if not empty, |
| 41 | // then parses it and returns an [Image]. |
| 42 | func NewImage(rawManifest []byte, expectedDigest string, blobs Blobs) (*Image, error) { |
| 43 | digest := fmt.Sprintf("sha256:%x", sha256.Sum256(rawManifest)) |
| 44 | if expectedDigest != "" && expectedDigest != digest { |
| 45 | return nil, fmt.Errorf("failed verification of manifest: expected digest %q, computed %q", expectedDigest, digest) |
| 46 | } |
| 47 | |
| 48 | manifest := &ocispecv1.Manifest{} |
| 49 | err := json.Unmarshal(rawManifest, &manifest) |
| 50 | if err != nil { |
| 51 | return nil, fmt.Errorf("failed to parse image manifest: %w", err) |
| 52 | } |
| 53 | if manifest.MediaType != ocispecv1.MediaTypeImageManifest { |
| 54 | return nil, fmt.Errorf("unexpected manifest media type %q", manifest.MediaType) |
| 55 | } |
| 56 | image := &Image{ |
| 57 | Manifest: manifest, |
| 58 | RawManifest: rawManifest, |
| 59 | ManifestDigest: digest, |
| 60 | blobs: blobs, |
| 61 | } |
| 62 | for descriptor := range image.Descriptors() { |
| 63 | if descriptor.Size < 0 { |
| 64 | return nil, fmt.Errorf("invalid manifest: contains descriptor with negative size") |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | return image, nil |
| 69 | } |
| 70 | |
| 71 | // Descriptors returns an iterator over all descriptors in the image (config and |
| 72 | // layers). |
| 73 | func (i *Image) Descriptors() iter.Seq[*ocispecv1.Descriptor] { |
| 74 | return func(yield func(*ocispecv1.Descriptor) bool) { |
| 75 | if !yield(&i.Manifest.Config) { |
| 76 | return |
| 77 | } |
| 78 | for l := range i.Manifest.Layers { |
| 79 | if !yield(&i.Manifest.Layers[l]) { |
| 80 | return |
| 81 | } |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | // Blob returns the contents of a blob from its descriptor. |
| 87 | // It does not verify the contents against the digest. |
| 88 | func (i *Image) Blob(descriptor *ocispecv1.Descriptor) (io.ReadCloser, error) { |
| 89 | if int64(len(descriptor.Data)) == descriptor.Size { |
| 90 | return structfs.Bytes(descriptor.Data).Open() |
| 91 | } else if len(descriptor.Data) != 0 { |
| 92 | return nil, fmt.Errorf("descriptor has embedded data of wrong length") |
| 93 | } |
| 94 | return i.blobs.Blob(descriptor) |
| 95 | } |
| 96 | |
| 97 | // ReadBlobVerified reads a blob into a byte slice and verifies it against the |
| 98 | // digest. |
| 99 | func (i *Image) ReadBlobVerified(descriptor *ocispecv1.Descriptor) ([]byte, error) { |
| 100 | if descriptor.Size < 0 { |
| 101 | return nil, fmt.Errorf("invalid descriptor size %d", descriptor.Size) |
| 102 | } |
| 103 | if descriptor.Size > 50*1024*1024 { |
| 104 | return nil, fmt.Errorf("refusing to read blob of size %d into memory", descriptor.Size) |
| 105 | } |
| 106 | expectedDigest := string(descriptor.Digest) |
| 107 | if _, _, err := ParseDigest(expectedDigest); err != nil { |
| 108 | return nil, err |
| 109 | } |
| 110 | blob, err := i.Blob(descriptor) |
| 111 | if err != nil { |
| 112 | return nil, err |
| 113 | } |
| 114 | defer blob.Close() |
| 115 | content := make([]byte, descriptor.Size) |
| 116 | _, err = io.ReadFull(blob, content) |
| 117 | if err != nil { |
| 118 | return nil, err |
| 119 | } |
| 120 | digest := fmt.Sprintf("sha256:%x", sha256.Sum256(content)) |
| 121 | if expectedDigest != digest { |
| 122 | return nil, fmt.Errorf("failed verification of blob: expected digest %q, computed %q", expectedDigest, digest) |
| 123 | } |
| 124 | return content, nil |
| 125 | } |
| 126 | |
| 127 | // StructfsBlob wraps an image and descriptor into a [structfs.Blob]. |
| 128 | func (i *Image) StructfsBlob(descriptor *ocispecv1.Descriptor) structfs.Blob { |
| 129 | return &structfsBlob{ |
| 130 | image: i, |
| 131 | descriptor: descriptor, |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | type structfsBlob struct { |
| 136 | image *Image |
| 137 | descriptor *ocispecv1.Descriptor |
| 138 | } |
| 139 | |
| 140 | func (b *structfsBlob) Open() (io.ReadCloser, error) { |
| 141 | return b.image.Blob(b.descriptor) |
| 142 | } |
| 143 | |
| 144 | func (b *structfsBlob) Size() int64 { |
| 145 | return b.descriptor.Size |
| 146 | } |
| 147 | |
| 148 | // ParseDigest splits a digest into its components. It returns an error if the |
| 149 | // algorithm is not supported, or if encoded is not valid for the algorithm. |
| 150 | func ParseDigest(digest string) (algorithm string, encoded string, err error) { |
| 151 | algorithm, encoded, ok := strings.Cut(digest, ":") |
| 152 | if !ok { |
| 153 | return "", "", fmt.Errorf("invalid digest") |
| 154 | } |
| 155 | switch algorithm { |
| 156 | case "sha256": |
| 157 | rest := strings.TrimLeft(encoded, "0123456789abcdef") |
| 158 | if len(rest) != 0 { |
| 159 | return "", "", fmt.Errorf("invalid character in sha256 digest") |
| 160 | } |
| 161 | if len(encoded) != sha256.Size*2 { |
| 162 | return "", "", fmt.Errorf("invalid sha256 digest length") |
| 163 | } |
| 164 | default: |
| 165 | return "", "", fmt.Errorf("unknown digest algorithm %q", algorithm) |
| 166 | } |
| 167 | return |
| 168 | } |