blob: a62b5273060580010df0529440295a2cc6e2086a [file] [log] [blame]
Jan Schärb48174d2025-04-14 10:13:02 +00001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
4// Package oci contains tools for handling OCI images.
5package oci
6
7import (
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.
21type 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.
34type 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].
42func 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).
73func (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.
88func (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.
99func (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].
128func (i *Image) StructfsBlob(descriptor *ocispecv1.Descriptor) structfs.Blob {
129 return &structfsBlob{
130 image: i,
131 descriptor: descriptor,
132 }
133}
134
135type structfsBlob struct {
136 image *Image
137 descriptor *ocispecv1.Descriptor
138}
139
140func (b *structfsBlob) Open() (io.ReadCloser, error) {
141 return b.image.Blob(b.descriptor)
142}
143
144func (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.
150func 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}