diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a036ec1..def1c34 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1001,6 +1001,7 @@ jobs: needs: [determine-workflows-ref, goreleaser-binaries, goreleaser-windows, goreleaser-docker] outputs: merged_manifest: ${{ steps.export-manifest.outputs.merged_manifest }} + manifest_url: ${{ steps.upload-manifest.outputs.manifest_url }} permissions: id-token: write contents: read @@ -1038,16 +1039,6 @@ jobs: - name: Install cosign uses: sigstore/cosign-installer@v3 - - name: Sign manifest.json - working-directory: _workflows/_output - env: { COSIGN_EXPERIMENTAL: "1" } - shell: bash - run: | - set -euo pipefail - cosign sign-blob --yes "manifest.json" \ - --output-signature "manifest.json.sig" \ - --output-certificate "manifest.json.cert" - - name: Configure AWS credentials via OIDC uses: aws-actions/configure-aws-credentials@v5 with: @@ -1142,7 +1133,7 @@ jobs: 'if .assets.checksums then .assets.checksums.sha256 = $sha | .assets.checksums.sizeBytes = $size else . end' \ manifest.json > manifest.tmp && mv manifest.tmp manifest.json - - name: Re-sign manifest.json + - name: Sign final manifest.json working-directory: _workflows/_output env: { COSIGN_EXPERIMENTAL: "1" } shell: bash @@ -1150,7 +1141,8 @@ jobs: set -euo pipefail cosign sign-blob --yes "manifest.json" \ --output-signature "manifest.json.sig" \ - --output-certificate "manifest.json.cert" + --output-certificate "manifest.json.cert" \ + --bundle "manifest.json.sigstore.json" - name: Export final manifest for registry API id: export-manifest @@ -1213,16 +1205,21 @@ jobs: echo "::error::Failed to upload manifest.json to S3" exit 1 fi - if [ -f "manifest.json.sig" ]; then - aws s3 cp "manifest.json.sig" "s3://$BUCKET/$DIRECTORY/manifest.json.sig" \ - --cache-control "public,max-age=31536000,immutable" \ - --content-type "application/octet-stream" - fi - if [ -f "manifest.json.cert" ]; then - aws s3 cp "manifest.json.cert" "s3://$BUCKET/$DIRECTORY/manifest.json.cert" \ - --cache-control "public,max-age=31536000,immutable" \ - --content-type "application/octet-stream" - fi + for required_file in manifest.json.sig manifest.json.cert manifest.json.sigstore.json; do + if [ ! -f "$required_file" ]; then + echo "::error::$required_file not found in $(pwd)" + exit 1 + fi + done + aws s3 cp "manifest.json.sig" "s3://$BUCKET/$DIRECTORY/manifest.json.sig" \ + --cache-control "public,max-age=31536000,immutable" \ + --content-type "application/octet-stream" + aws s3 cp "manifest.json.cert" "s3://$BUCKET/$DIRECTORY/manifest.json.cert" \ + --cache-control "public,max-age=31536000,immutable" \ + --content-type "application/octet-stream" + aws s3 cp "manifest.json.sigstore.json" "s3://$BUCKET/$DIRECTORY/manifest.json.sigstore.json" \ + --cache-control "public,max-age=31536000,immutable" \ + --content-type "application/json" # ================================================================ # Registry API: record release after release manifest publication. @@ -1230,8 +1227,8 @@ jobs: # ================================================================ record-registry-api: # Use !cancelled() so the explicit needs.result check controls skipped-job behavior. - if: ${{ !cancelled() && needs.publish-release-manifest.result == 'success' }} - needs: [determine-workflows-ref, publish-release-manifest] + if: ${{ !cancelled() && needs.publish-release-manifest.result == 'success' && needs.verify-release.result == 'success' }} + needs: [determine-workflows-ref, publish-release-manifest, verify-release] permissions: id-token: write contents: read @@ -1352,6 +1349,7 @@ jobs: go run ./cmd/record-release \ -manifest _output/manifest.json \ + -manifest-url "${{ needs.publish-release-manifest.outputs.manifest_url }}" \ -org "${{ github.event.repository.owner.login }}" \ -name "${{ github.event.repository.name }}" \ -version "${{ inputs.tag }}" \ @@ -1366,8 +1364,9 @@ jobs: $RELEASED_AT_FLAG verify-release: - # Verify release artifacts and attestations after publishing - # This job is not blocking - failures trigger Datadog notification but don't fail the release + # Verify release artifacts and attestations before recording the release. + # Registry-side verification is the trust boundary; this job prevents + # obviously bad artifacts from being submitted. needs: [determine-workflows-ref, publish-release-manifest] if: always() && needs.publish-release-manifest.result == 'success' permissions: diff --git a/Makefile b/Makefile index 712d148..808ccec 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,17 @@ protofmt: buf format -w proto @echo "Protobuf formatting complete." +.PHONY: test +test: + go test ./cmd/record-release ./cmd/generate-manifest ./cmd/merge-manifests + +.PHONY: workflow-validate +workflow-validate: + yq '.' .github/workflows/release.yaml >/dev/null + +.PHONY: verify +verify: protogen test workflow-validate + .PHONY: docs docs: @echo "Generating documentation diagrams..." diff --git a/cmd/generate-manifest/main.go b/cmd/generate-manifest/main.go index ceee6c3..7ece3f7 100644 --- a/cmd/generate-manifest/main.go +++ b/cmd/generate-manifest/main.go @@ -147,16 +147,18 @@ func main() { baseURLTrimmed := strings.TrimSuffix(baseURL, "/") signatureHref := fmt.Sprintf("%s/manifest.json.sig", baseURLTrimmed) certificateHref := fmt.Sprintf("%s/manifest.json.cert", baseURLTrimmed) + signatureBundleHref := fmt.Sprintf("%s/manifest.json.sigstore.json", baseURLTrimmed) manifest := pb.Manifest_builder{ - Version: &version, - Name: &repoName, - Org: &orgName, - Semver: &tag, - ReleasedAt: timestamppb.New(now), - Assets: assets, - SignatureHref: &signatureHref, - CertificateHref: &certificateHref, + Version: &version, + Name: &repoName, + Org: &orgName, + Semver: &tag, + ReleasedAt: timestamppb.New(now), + Assets: assets, + SignatureHref: &signatureHref, + CertificateHref: &certificateHref, + SignatureBundleHref: &signatureBundleHref, }.Build() // Marshal to JSON diff --git a/cmd/record-release/main.go b/cmd/record-release/main.go index 44d6d54..67dac0c 100644 --- a/cmd/record-release/main.go +++ b/cmd/record-release/main.go @@ -19,21 +19,23 @@ import ( // RecordReleaseRequest is the JSON body sent to the registry API. type RecordReleaseRequest struct { - Org string `json:"org"` - Name string `json:"name"` - Version string `json:"version"` - RepositoryURL string `json:"repositoryUrl"` - CommitSha string `json:"commitSha"` - WorkflowRunID string `json:"workflowRunId"` - Documentation string `json:"documentation,omitempty"` - Changelog string `json:"changelog,omitempty"` - ConfigSchema string `json:"configSchema,omitempty"` - Capabilities string `json:"capabilities,omitempty"` - SignatureURL string `json:"signatureUrl,omitempty"` - CertificateURL string `json:"certificateUrl,omitempty"` - Assets map[string]*ReleaseAsset `json:"assets,omitempty"` - Images map[string]*ReleaseImage `json:"images,omitempty"` - ReleasedAt string `json:"releasedAt,omitempty"` + Org string `json:"org"` + Name string `json:"name"` + Version string `json:"version"` + RepositoryURL string `json:"repositoryUrl"` + CommitSha string `json:"commitSha"` + WorkflowRunID string `json:"workflowRunId"` + Documentation string `json:"documentation,omitempty"` + Changelog string `json:"changelog,omitempty"` + ConfigSchema string `json:"configSchema,omitempty"` + Capabilities string `json:"capabilities,omitempty"` + SignatureURL string `json:"signatureUrl,omitempty"` + CertificateURL string `json:"certificateUrl,omitempty"` + ManifestURL string `json:"manifestUrl,omitempty"` + SignatureBundleURL string `json:"signatureBundleUrl,omitempty"` + Assets map[string]*ReleaseAsset `json:"assets,omitempty"` + Images map[string]*ReleaseImage `json:"images,omitempty"` + ReleasedAt string `json:"releasedAt,omitempty"` } // ReleaseAsset is the transformed asset for the registry API. @@ -92,6 +94,7 @@ func main() { changelogPath string configSchemaPath string capabilitiesPath string + manifestURL string token string ) @@ -107,6 +110,7 @@ func main() { flag.StringVar(&changelogPath, "changelog", "", "Path to a file containing release notes (optional)") flag.StringVar(&configSchemaPath, "config-schema", "", "Path to config_schema.json file (optional)") flag.StringVar(&capabilitiesPath, "capabilities", "", "Path to baton_capabilities.json file (optional)") + flag.StringVar(&manifestURL, "manifest-url", "", "Published manifest.json URL (required)") var releasedAt string flag.StringVar(&releasedAt, "released-at", "", "Release publish timestamp in RFC 3339 format (optional, defaults to server time)") flag.StringVar(&token, "token", "", "Bearer token (or set REGISTRY_API_TOKEN env var)") @@ -138,6 +142,9 @@ func main() { if registryURL == "" { missing = append(missing, "-registry-url") } + if manifestURL == "" { + missing = append(missing, "-manifest-url") + } if len(missing) > 0 { fmt.Fprintf(os.Stderr, "record-release: error: missing required flags: %s\n", strings.Join(missing, ", ")) flag.Usage() @@ -168,6 +175,10 @@ func main() { fmt.Fprintf(os.Stderr, "record-release: error: parsing manifest: %v\n", err) os.Exit(1) } + if manifest.GetSignatureBundleHref() == "" { + fmt.Fprintf(os.Stderr, "record-release: error: manifest missing signatureBundleHref\n") + os.Exit(1) + } // Read optional documentation var documentation string @@ -220,21 +231,23 @@ func main() { // Build request body req := &RecordReleaseRequest{ - Org: org, - Name: name, - Version: version, - RepositoryURL: repositoryURL, - CommitSha: commitSha, - WorkflowRunID: workflowRunID, - Documentation: documentation, - Changelog: changelog, - ConfigSchema: configSchema, - Capabilities: capabilities, - SignatureURL: manifest.GetSignatureHref(), - CertificateURL: manifest.GetCertificateHref(), - Assets: assets, - Images: images, - ReleasedAt: releasedAt, + Org: org, + Name: name, + Version: version, + RepositoryURL: repositoryURL, + CommitSha: commitSha, + WorkflowRunID: workflowRunID, + Documentation: documentation, + Changelog: changelog, + ConfigSchema: configSchema, + Capabilities: capabilities, + SignatureURL: manifest.GetSignatureHref(), + CertificateURL: manifest.GetCertificateHref(), + ManifestURL: manifestURL, + SignatureBundleURL: manifest.GetSignatureBundleHref(), + Assets: assets, + Images: images, + ReleasedAt: releasedAt, } bodyBytes, err := json.Marshal(req) diff --git a/cmd/record-release/main_test.go b/cmd/record-release/main_test.go index 7f5027b..feb4142 100644 --- a/cmd/record-release/main_test.go +++ b/cmd/record-release/main_test.go @@ -100,9 +100,11 @@ func TestTransformImagesSkipsAttestationForNonIndexImage(t *testing.T) { func TestRecordReleaseRequestMarshalsAttestations(t *testing.T) { req := &RecordReleaseRequest{ - Org: "example", - Name: "baton-example", - Version: "v1.2.3", + Org: "example", + Name: "baton-example", + Version: "v1.2.3", + ManifestURL: "https://dist.example.com/manifest.json", + SignatureBundleURL: "https://dist.example.com/manifest.json.sigstore.json", Assets: map[string]*ReleaseAsset{ "linux-amd64": { Platform: "linux-amd64", @@ -125,7 +127,9 @@ func TestRecordReleaseRequestMarshalsAttestations(t *testing.T) { } var got struct { - Assets map[string]struct { + ManifestURL string `json:"manifestUrl"` + SignatureBundleURL string `json:"signatureBundleUrl"` + Assets map[string]struct { Attestations []ReleaseAttestation `json:"attestations"` } `json:"assets"` Images map[string]struct { @@ -136,6 +140,9 @@ func TestRecordReleaseRequestMarshalsAttestations(t *testing.T) { t.Fatalf("unmarshal request: %v", err) } + if got.ManifestURL == "" || got.SignatureBundleURL == "" { + t.Fatalf("manifest signature metadata was not marshaled: %#v", got) + } if len(got.Assets["linux-amd64"].Attestations) != 1 { t.Fatalf("asset attestations = %#v, want one entry", got.Assets["linux-amd64"].Attestations) } diff --git a/pb/artifacts/v1/manifest.pb.go b/pb/artifacts/v1/manifest.pb.go index 7764f44..9077ca7 100644 --- a/pb/artifacts/v1/manifest.pb.go +++ b/pb/artifacts/v1/manifest.pb.go @@ -25,22 +25,23 @@ const ( // Manifest represents the immutable artifact metadata for a specific release version. // This manifest is stored at the versioned path: releases/{org}/{repo}/{tag}/manifest.json type Manifest struct { - state protoimpl.MessageState `protogen:"opaque.v1"` - xxx_hidden_Version *string `protobuf:"bytes,1,opt,name=version"` - xxx_hidden_Name *string `protobuf:"bytes,2,opt,name=name"` - xxx_hidden_Org *string `protobuf:"bytes,3,opt,name=org"` - xxx_hidden_Semver *string `protobuf:"bytes,4,opt,name=semver"` - xxx_hidden_ReleasedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=released_at,json=releasedAt"` - xxx_hidden_Assets map[string]*Asset `protobuf:"bytes,6,rep,name=assets" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - xxx_hidden_Images map[string]*Image `protobuf:"bytes,7,rep,name=images" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - xxx_hidden_SignatureHref *string `protobuf:"bytes,8,opt,name=signature_href,json=signatureHref"` - xxx_hidden_CertificateHref *string `protobuf:"bytes,9,opt,name=certificate_href,json=certificateHref"` - xxx_hidden_ImageAttestation *AttestationDescriptor `protobuf:"bytes,10,opt,name=image_attestation,json=imageAttestation"` - xxx_hidden_AssetAttestation *AttestationDescriptor `protobuf:"bytes,11,opt,name=asset_attestation,json=assetAttestation"` - XXX_raceDetectHookData protoimpl.RaceDetectHookData - XXX_presence [1]uint32 - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Version *string `protobuf:"bytes,1,opt,name=version"` + xxx_hidden_Name *string `protobuf:"bytes,2,opt,name=name"` + xxx_hidden_Org *string `protobuf:"bytes,3,opt,name=org"` + xxx_hidden_Semver *string `protobuf:"bytes,4,opt,name=semver"` + xxx_hidden_ReleasedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=released_at,json=releasedAt"` + xxx_hidden_Assets map[string]*Asset `protobuf:"bytes,6,rep,name=assets" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + xxx_hidden_Images map[string]*Image `protobuf:"bytes,7,rep,name=images" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + xxx_hidden_SignatureHref *string `protobuf:"bytes,8,opt,name=signature_href,json=signatureHref"` + xxx_hidden_CertificateHref *string `protobuf:"bytes,9,opt,name=certificate_href,json=certificateHref"` + xxx_hidden_ImageAttestation *AttestationDescriptor `protobuf:"bytes,10,opt,name=image_attestation,json=imageAttestation"` + xxx_hidden_AssetAttestation *AttestationDescriptor `protobuf:"bytes,11,opt,name=asset_attestation,json=assetAttestation"` + xxx_hidden_SignatureBundleHref *string `protobuf:"bytes,12,opt,name=signature_bundle_href,json=signatureBundleHref"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Manifest) Reset() { @@ -163,24 +164,34 @@ func (x *Manifest) GetAssetAttestation() *AttestationDescriptor { return nil } +func (x *Manifest) GetSignatureBundleHref() string { + if x != nil { + if x.xxx_hidden_SignatureBundleHref != nil { + return *x.xxx_hidden_SignatureBundleHref + } + return "" + } + return "" +} + func (x *Manifest) SetVersion(v string) { x.xxx_hidden_Version = &v - protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 11) + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 12) } func (x *Manifest) SetName(v string) { x.xxx_hidden_Name = &v - protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 11) + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 12) } func (x *Manifest) SetOrg(v string) { x.xxx_hidden_Org = &v - protoimpl.X.SetPresent(&(x.XXX_presence[0]), 2, 11) + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 2, 12) } func (x *Manifest) SetSemver(v string) { x.xxx_hidden_Semver = &v - protoimpl.X.SetPresent(&(x.XXX_presence[0]), 3, 11) + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 3, 12) } func (x *Manifest) SetReleasedAt(v *timestamppb.Timestamp) { @@ -197,12 +208,12 @@ func (x *Manifest) SetImages(v map[string]*Image) { func (x *Manifest) SetSignatureHref(v string) { x.xxx_hidden_SignatureHref = &v - protoimpl.X.SetPresent(&(x.XXX_presence[0]), 7, 11) + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 7, 12) } func (x *Manifest) SetCertificateHref(v string) { x.xxx_hidden_CertificateHref = &v - protoimpl.X.SetPresent(&(x.XXX_presence[0]), 8, 11) + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 8, 12) } func (x *Manifest) SetImageAttestation(v *AttestationDescriptor) { @@ -213,6 +224,11 @@ func (x *Manifest) SetAssetAttestation(v *AttestationDescriptor) { x.xxx_hidden_AssetAttestation = v } +func (x *Manifest) SetSignatureBundleHref(v string) { + x.xxx_hidden_SignatureBundleHref = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 11, 12) +} + func (x *Manifest) HasVersion() bool { if x == nil { return false @@ -276,6 +292,13 @@ func (x *Manifest) HasAssetAttestation() bool { return x.xxx_hidden_AssetAttestation != nil } +func (x *Manifest) HasSignatureBundleHref() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 11) +} + func (x *Manifest) ClearVersion() { protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) x.xxx_hidden_Version = nil @@ -318,6 +341,11 @@ func (x *Manifest) ClearAssetAttestation() { x.xxx_hidden_AssetAttestation = nil } +func (x *Manifest) ClearSignatureBundleHref() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 11) + x.xxx_hidden_SignatureBundleHref = nil +} + type Manifest_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. @@ -349,6 +377,8 @@ type Manifest_builder struct { // Per-asset attestations (provenance, SBOM) are in Asset.attestations[]. // This field documents the types used across all assets. AssetAttestation *AttestationDescriptor + // signature_bundle_href is the URL to the Sigstore bundle file (manifest.json.sigstore.json). + SignatureBundleHref *string } func (b0 Manifest_builder) Build() *Manifest { @@ -356,34 +386,38 @@ func (b0 Manifest_builder) Build() *Manifest { b, x := &b0, m0 _, _ = b, x if b.Version != nil { - protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 11) + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 12) x.xxx_hidden_Version = b.Version } if b.Name != nil { - protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 11) + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 12) x.xxx_hidden_Name = b.Name } if b.Org != nil { - protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 2, 11) + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 2, 12) x.xxx_hidden_Org = b.Org } if b.Semver != nil { - protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 3, 11) + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 3, 12) x.xxx_hidden_Semver = b.Semver } x.xxx_hidden_ReleasedAt = b.ReleasedAt x.xxx_hidden_Assets = b.Assets x.xxx_hidden_Images = b.Images if b.SignatureHref != nil { - protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 7, 11) + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 7, 12) x.xxx_hidden_SignatureHref = b.SignatureHref } if b.CertificateHref != nil { - protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 8, 11) + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 8, 12) x.xxx_hidden_CertificateHref = b.CertificateHref } x.xxx_hidden_ImageAttestation = b.ImageAttestation x.xxx_hidden_AssetAttestation = b.AssetAttestation + if b.SignatureBundleHref != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 11, 12) + x.xxx_hidden_SignatureBundleHref = b.SignatureBundleHref + } return m0 } @@ -1103,7 +1137,7 @@ var File_artifacts_v1_manifest_proto protoreflect.FileDescriptor const file_artifacts_v1_manifest_proto_rawDesc = "" + "\n" + - "\x1bartifacts/v1/manifest.proto\x12\fartifacts.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a!google/protobuf/go_features.proto\"\xad\x05\n" + + "\x1bartifacts/v1/manifest.proto\x12\fartifacts.v1\x1a!google/protobuf/go_features.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xe1\x05\n" + "\bManifest\x12\x18\n" + "\aversion\x18\x01 \x01(\tR\aversion\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x10\n" + @@ -1117,7 +1151,8 @@ const file_artifacts_v1_manifest_proto_rawDesc = "" + "\x10certificate_href\x18\t \x01(\tR\x0fcertificateHref\x12P\n" + "\x11image_attestation\x18\n" + " \x01(\v2#.artifacts.v1.AttestationDescriptorR\x10imageAttestation\x12P\n" + - "\x11asset_attestation\x18\v \x01(\v2#.artifacts.v1.AttestationDescriptorR\x10assetAttestation\x1aN\n" + + "\x11asset_attestation\x18\v \x01(\v2#.artifacts.v1.AttestationDescriptorR\x10assetAttestation\x122\n" + + "\x15signature_bundle_href\x18\f \x01(\tR\x13signatureBundleHref\x1aN\n" + "\vAssetsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12)\n" + "\x05value\x18\x02 \x01(\v2\x13.artifacts.v1.AssetR\x05value:\x028\x01\x1aN\n" + diff --git a/proto/artifacts/v1/manifest.proto b/proto/artifacts/v1/manifest.proto index 8f08f50..bc9c137 100644 --- a/proto/artifacts/v1/manifest.proto +++ b/proto/artifacts/v1/manifest.proto @@ -4,11 +4,11 @@ edition = "2023"; package artifacts.v1; -import "google/protobuf/timestamp.proto"; import "google/protobuf/go_features.proto"; +import "google/protobuf/timestamp.proto"; -option go_package = "github.com/ConductorOne/github-workflows/pb/artifacts/v1"; option features.(pb.go).api_level = API_OPAQUE; +option go_package = "github.com/ConductorOne/github-workflows/pb/artifacts/v1"; // Manifest represents the immutable artifact metadata for a specific release version. // This manifest is stored at the versioned path: releases/{org}/{repo}/{tag}/manifest.json @@ -51,6 +51,9 @@ message Manifest { // Per-asset attestations (provenance, SBOM) are in Asset.attestations[]. // This field documents the types used across all assets. AttestationDescriptor asset_attestation = 11; + + // signature_bundle_href is the URL to the Sigstore bundle file (manifest.json.sigstore.json). + string signature_bundle_href = 12; } // Asset represents metadata for a single binary artifact. diff --git a/scripts/validate-release-artifacts.sh b/scripts/validate-release-artifacts.sh index f7e977f..76e8404 100755 --- a/scripts/validate-release-artifacts.sh +++ b/scripts/validate-release-artifacts.sh @@ -218,8 +218,19 @@ echo "" echo "=== Manifest Signature Validation ===" MANIFEST_SIG_URL="${BASE_URL}/${ORG_REPO}/${VERSION}/manifest.json.sig" MANIFEST_CERT_URL="${BASE_URL}/${ORG_REPO}/${VERSION}/manifest.json.cert" +MANIFEST_BUNDLE_URL="${BASE_URL}/${ORG_REPO}/${VERSION}/manifest.json.sigstore.json" -if curl -sfL "$MANIFEST_SIG_URL" -o "$TEMP_DIR/manifest.json.sig" 2>/dev/null && \ +if curl -sfL "$MANIFEST_BUNDLE_URL" -o "$TEMP_DIR/manifest.json.sigstore.json" 2>/dev/null; then + if cosign verify-blob \ + --bundle "$TEMP_DIR/manifest.json.sigstore.json" \ + --certificate-oidc-issuer "$CERT_OIDC_ISSUER" \ + --certificate-identity-regexp "$CERT_IDENTITY_REGEXP" \ + "$TEMP_DIR/manifest.json" > /dev/null 2>&1; then + pass "Manifest Sigstore bundle verified" + else + fail "Manifest Sigstore bundle verification failed" + fi +elif curl -sfL "$MANIFEST_SIG_URL" -o "$TEMP_DIR/manifest.json.sig" 2>/dev/null && \ curl -sfL "$MANIFEST_CERT_URL" -o "$TEMP_DIR/manifest.json.cert" 2>/dev/null; then if cosign verify-blob \ --signature "$TEMP_DIR/manifest.json.sig" \