|
1 | 1 | package containerregistry |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "bytes" |
5 | 4 | "context" |
| 5 | + "encoding/base64" |
6 | 6 | "encoding/json" |
| 7 | + "fmt" |
7 | 8 | "regexp" |
| 9 | + "strings" |
8 | 10 |
|
9 | 11 | "github.com/google/go-containerregistry/pkg/authn" |
10 | 12 | "github.com/google/go-containerregistry/pkg/crane" |
11 | | - cdl "github.com/sigstore/cosign/v2/cmd/cosign/cli/download" |
| 13 | + "github.com/google/go-containerregistry/pkg/name" |
| 14 | + v1 "github.com/google/go-containerregistry/pkg/v1" |
12 | 15 | "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" |
| 16 | + "github.com/sigstore/cosign/v2/pkg/cosign" |
| 17 | + "github.com/sigstore/cosign/v2/pkg/oci" |
| 18 | + ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" |
13 | 19 | ) |
14 | 20 |
|
15 | 21 | type Interface interface { |
@@ -54,41 +60,201 @@ func (c *Client) ListTagsWithConstraint(repoName string, constraint string) ([]s |
54 | 60 | return result, nil |
55 | 61 | } |
56 | 62 |
|
57 | | -func (c *Client) VersionFromSbom(mainPkg string, repo string) (string, error) { |
58 | | - type Package struct { |
59 | | - Name string `json:"name"` |
60 | | - VersionInfo string `json:"versionInfo"` |
61 | | - } |
62 | | - type Packages struct { |
63 | | - Packages []Package `json:"packages"` |
64 | | - } |
65 | | - buf := new(bytes.Buffer) |
| 63 | +func (c *Client) VersionFromSbom(mainPkg string, imageRef string) (string, error) { |
| 64 | + ctx := context.TODO() |
| 65 | + // type Package struct { |
| 66 | + // Name string `json:"name"` |
| 67 | + // VersionInfo string `json:"versionInfo"` |
| 68 | + // } |
| 69 | + // type Packages struct { |
| 70 | + // Packages []Package `json:"packages"` |
| 71 | + // } |
| 72 | + // buf := new(bytes.Buffer) |
66 | 73 | kc := authn.NewMultiKeychain( |
67 | 74 | authn.DefaultKeychain, |
68 | 75 | ) |
69 | 76 |
|
70 | | - o := &options.RegistryOptions{Keychain: kc} |
71 | | - do := &options.SBOMDownloadOptions{Platform: "linux/amd64"} |
72 | | - sboms, err := cdl.SBOMCmd(context.TODO(), *o, *do, repo, buf) |
| 77 | + regOpts := options.RegistryOptions{Keychain: kc} |
| 78 | + // do := &options.SBOMDownloadOptions{Platform: "linux/amd64"} |
| 79 | + // sboms, err := cdl.SBOMCmd(context.TODO(), *o, *do, repo, buf) |
| 80 | + // if err != nil { |
| 81 | + // return "", err |
| 82 | + // } |
| 83 | + // var ps Packages |
| 84 | + |
| 85 | + // for _, s := range sboms { |
| 86 | + // json.Unmarshal([]byte(s), &ps) |
| 87 | + // for _, v := range ps.Packages { |
| 88 | + // if v.Name == mainPkg { |
| 89 | + // return v.VersionInfo, nil |
| 90 | + // } |
| 91 | + // } |
| 92 | + // } |
| 93 | + attOptions := &options.AttestationDownloadOptions{ |
| 94 | + PredicateType: "https://slsa.dev/provenance/v1", |
| 95 | + Platform: "linux/amd64", |
| 96 | + } |
| 97 | + ref, err := name.ParseReference(imageRef, regOpts.NameOptions()...) |
| 98 | + if err != nil { |
| 99 | + return "", err |
| 100 | + } |
| 101 | + ociremoteOpts, err := regOpts.ClientOpts(ctx) |
73 | 102 | if err != nil { |
74 | 103 | return "", err |
75 | 104 | } |
76 | | - var ps Packages |
77 | 105 |
|
78 | | - for _, s := range sboms { |
79 | | - json.Unmarshal([]byte(s), &ps) |
80 | | - for _, v := range ps.Packages { |
81 | | - if v.Name == mainPkg { |
82 | | - return v.VersionInfo, nil |
83 | | - } |
| 106 | + var predicateType string |
| 107 | + predicateType, err = options.ParsePredicateType(attOptions.PredicateType) |
| 108 | + if err != nil { |
| 109 | + return "", err |
| 110 | + } |
| 111 | + |
| 112 | + se, err := ociremote.SignedEntity(ref, ociremoteOpts...) |
| 113 | + if err != nil { |
| 114 | + return "", err |
| 115 | + } |
| 116 | + |
| 117 | + idx, isIndex := se.(oci.SignedImageIndex) |
| 118 | + |
| 119 | + // We only allow --platform on multiarch indexes |
| 120 | + if attOptions.Platform != "" && !isIndex { |
| 121 | + return "", fmt.Errorf("specified reference is not a multiarch image") |
| 122 | + } |
| 123 | + |
| 124 | + if attOptions.Platform != "" && isIndex { |
| 125 | + targetPlatform, err := v1.ParsePlatform(attOptions.Platform) |
| 126 | + if err != nil { |
| 127 | + return "", fmt.Errorf("parsing platform: %w", err) |
| 128 | + } |
| 129 | + platforms, err := getIndexPlatforms(idx) |
| 130 | + if err != nil { |
| 131 | + return "", fmt.Errorf("getting available platforms: %w", err) |
| 132 | + } |
| 133 | + |
| 134 | + platforms = matchPlatform(targetPlatform, platforms) |
| 135 | + if len(platforms) == 0 { |
| 136 | + return "", fmt.Errorf("unable to find an attestation for %s", targetPlatform.String()) |
| 137 | + } |
| 138 | + if len(platforms) > 1 { |
| 139 | + return "nil", fmt.Errorf( |
| 140 | + "platform spec matches more than one image architecture: %s", |
| 141 | + platforms.String(), |
| 142 | + ) |
| 143 | + } |
| 144 | + |
| 145 | + nse, err := idx.SignedImage(platforms[0].hash) |
| 146 | + if err != nil { |
| 147 | + return "", fmt.Errorf("searching for %s image: %w", platforms[0].hash.String(), err) |
84 | 148 | } |
| 149 | + if nse == nil { |
| 150 | + return "", fmt.Errorf("unable to find image %s", platforms[0].hash.String()) |
| 151 | + } |
| 152 | + se = nse |
| 153 | + } |
| 154 | + |
| 155 | + attestations, err := cosign.FetchAttestations(se, predicateType) |
| 156 | + if err != nil { |
| 157 | + return "", err |
| 158 | + } |
| 159 | + if len(attestations) > 1 { |
| 160 | + return "", fmt.Errorf("filtered attestation list is more than one") |
85 | 161 | } |
86 | | - return "", nil |
| 162 | + var a attestationPayload |
| 163 | + att := attestations[0] |
| 164 | + pB, err := base64.StdEncoding.DecodeString(att.PayLoad) |
| 165 | + if err != nil { |
| 166 | + return "", err |
| 167 | + } |
| 168 | + if err := json.Unmarshal(pB, &a); err != nil { |
| 169 | + return "", err |
| 170 | + } |
| 171 | + return a.Predicate.BuildDefinition.InternalParameters[mainPkg], nil |
| 172 | +} |
| 173 | + |
| 174 | +type attestationPayload struct { |
| 175 | + Predicate predicate `json:"predicate"` |
| 176 | +} |
| 177 | + |
| 178 | +type predicate struct { |
| 179 | + BuildDefinition buildDefinition `json:"buildDefinition"` |
| 180 | +} |
| 181 | + |
| 182 | +type buildDefinition struct { |
| 183 | + InternalParameters map[string]string `json:"internalParameters"` |
87 | 184 | } |
88 | 185 |
|
| 186 | +// cosign download attestation cgr.dev/chainguard/redis --predicate-type https://slsa.dev/provenance/v1 | jq '.payload | @base64d | fromjson | .predicate.buildDefinition.internalParameters.redis' |
| 187 | + |
89 | 188 | func (c *Client) getAuthOpt() crane.Option { |
90 | 189 | kc := authn.NewMultiKeychain( |
91 | 190 | authn.DefaultKeychain, |
92 | 191 | ) |
93 | 192 | return crane.WithAuthFromKeychain(kc) |
94 | 193 | } |
| 194 | + |
| 195 | +func getIndexPlatforms(idx oci.SignedImageIndex) (platformList, error) { |
| 196 | + im, err := idx.IndexManifest() |
| 197 | + if err != nil { |
| 198 | + return nil, fmt.Errorf("fetching index manifest: %w", err) |
| 199 | + } |
| 200 | + |
| 201 | + platforms := platformList{} |
| 202 | + for _, m := range im.Manifests { |
| 203 | + if m.Platform == nil { |
| 204 | + continue |
| 205 | + } |
| 206 | + platforms = append(platforms, struct { |
| 207 | + hash v1.Hash |
| 208 | + platform *v1.Platform |
| 209 | + }{m.Digest, m.Platform}) |
| 210 | + } |
| 211 | + return platforms, nil |
| 212 | +} |
| 213 | + |
| 214 | +// matchPlatform filters a list of platforms returning only those matching |
| 215 | +// a base. "Based" on ko's internal equivalent while it moves to GGCR. |
| 216 | +// https://github.com/google/ko/blob/e6a7a37e26d82a8b2bb6df991c5a6cf6b2728794/pkg/build/gobuild.go#L1020 |
| 217 | +func matchPlatform(base *v1.Platform, list platformList) platformList { |
| 218 | + ret := platformList{} |
| 219 | + for _, p := range list { |
| 220 | + if base.OS != "" && base.OS != p.platform.OS { |
| 221 | + continue |
| 222 | + } |
| 223 | + if base.Architecture != "" && base.Architecture != p.platform.Architecture { |
| 224 | + continue |
| 225 | + } |
| 226 | + if base.Variant != "" && base.Variant != p.platform.Variant { |
| 227 | + continue |
| 228 | + } |
| 229 | + |
| 230 | + if base.OSVersion != "" && p.platform.OSVersion != base.OSVersion { |
| 231 | + if base.OS != "windows" { |
| 232 | + continue |
| 233 | + } else { //nolint: revive |
| 234 | + if pcount, bcount := strings.Count(base.OSVersion, "."), strings.Count(p.platform.OSVersion, "."); pcount == 2 && bcount == 3 { |
| 235 | + if base.OSVersion != p.platform.OSVersion[:strings.LastIndex(p.platform.OSVersion, ".")] { |
| 236 | + continue |
| 237 | + } |
| 238 | + } else { |
| 239 | + continue |
| 240 | + } |
| 241 | + } |
| 242 | + } |
| 243 | + ret = append(ret, p) |
| 244 | + } |
| 245 | + |
| 246 | + return ret |
| 247 | +} |
| 248 | + |
| 249 | +type platformList []struct { |
| 250 | + hash v1.Hash |
| 251 | + platform *v1.Platform |
| 252 | +} |
| 253 | + |
| 254 | +func (pl *platformList) String() string { |
| 255 | + r := []string{} |
| 256 | + for _, p := range *pl { |
| 257 | + r = append(r, p.platform.String()) |
| 258 | + } |
| 259 | + return strings.Join(r, ", ") |
| 260 | +} |
0 commit comments