diff --git a/cli/dispatcher/cmd/refresh-attestation-trusted-root/main.go b/cli/dispatcher/cmd/refresh-attestation-trusted-root/main.go new file mode 100644 index 0000000000..941475e16b --- /dev/null +++ b/cli/dispatcher/cmd/refresh-attestation-trusted-root/main.go @@ -0,0 +1,49 @@ +// Refreshes the Sigstore trusted_root.json embedded by cli/dispatcher/internal/attestation. +// +// Run this from the dispatcher module root to fetch the current Sigstore +// public-good trusted_root.json via TUF and write it as +// internal/attestation/trusted_root.json. The dispatcher then embeds that +// file so verification stays offline at runtime. Refresh cadence is +// quarterly; the attestation package has a CI guard test that fails when +// any embedded authority's validity window is within 90 days of expiring, +// so a missed refresh surfaces as a red build rather than a fleet outage. +// +// Runs from module root: +// +// go run ./cmd/refresh-attestation-trusted-root +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/sigstore/sigstore-go/pkg/tuf" +) + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, "refresh-attestation-trusted-root:", err) + os.Exit(1) + } +} + +func run() error { + client, err := tuf.DefaultClient() + if err != nil { + return fmt.Errorf("create TUF client: %w", err) + } + if err := client.Refresh(); err != nil { + return fmt.Errorf("refresh TUF metadata: %w", err) + } + target, err := client.GetTarget("trusted_root.json") + if err != nil { + return fmt.Errorf("get trusted_root.json target: %w", err) + } + outputPath := filepath.Join("internal", "attestation", "trusted_root.json") + if err := os.WriteFile(outputPath, target, 0o644); err != nil { + return fmt.Errorf("write %s: %w", outputPath, err) + } + fmt.Printf("wrote %d bytes to %s\n", len(target), outputPath) + return nil +} diff --git a/cli/dispatcher/go.mod b/cli/dispatcher/go.mod index 8c4740f600..8d4b2bb5f0 100644 --- a/cli/dispatcher/go.mod +++ b/cli/dispatcher/go.mod @@ -2,11 +2,26 @@ module github.com/hatayama/unity-cli-loop/dispatcher go 1.26 -require github.com/hatayama/unity-cli-loop/common v0.0.0-00010101000000-000000000000 +require ( + github.com/hatayama/unity-cli-loop/common v0.0.0-00010101000000-000000000000 + github.com/sigstore/sigstore-go v1.2.2 +) require ( github.com/Microsoft/go-winio v0.6.2 // indirect - golang.org/x/sys v0.10.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/google/go-containerregistry v0.21.7 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect + github.com/sigstore/protobuf-specs v0.5.1 // indirect + github.com/sigstore/sigstore v1.10.8 // indirect + github.com/theupdateframework/go-tuf/v2 v2.4.2 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/protobuf v1.36.11 // indirect ) replace github.com/hatayama/unity-cli-loop/common => ../common diff --git a/cli/dispatcher/go.sum b/cli/dispatcher/go.sum index a001052ff3..36891e830d 100644 --- a/cli/dispatcher/go.sum +++ b/cli/dispatcher/go.sum @@ -1,4 +1,44 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU= +github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs= +github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q= +github.com/sigstore/protobuf-specs v0.5.1 h1:/5OPaNuolRJmQfeZLayJGFXMpsRJEdgC6ah1/+7Px7U= +github.com/sigstore/protobuf-specs v0.5.1/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats= +github.com/sigstore/sigstore v1.10.8 h1:1Mgkxvkw4AXMfIP1DOjc6kw0GkUgA8pGVpveN/EfOq4= +github.com/sigstore/sigstore v1.10.8/go.mod h1:f9+B/4iaYimvUkySyb2mvc73n3RLqNn24grHZM/ET8M= +github.com/sigstore/sigstore-go v1.2.2 h1:xAJ8hxaoecC0HKBYVbrwUjkeAI+GJYu6vLqbxDlD2Q0= +github.com/sigstore/sigstore-go v1.2.2/go.mod h1:MIFwBxAHJD+/lKgZzt9n/4Zhq/3T2+EuGX8iGrIsZgU= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/theupdateframework/go-tuf/v2 v2.4.2 h1:w7976/W8uTwlsegP5nRymlpjPgrwSh+AXUf85is6nJk= +github.com/theupdateframework/go-tuf/v2 v2.4.2/go.mod h1:JqBrIUnNLAaNq/8GmBcEMFWfAFBbqp/MkJEJseXKbks= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/dispatcher/internal/attestation/errors.go b/cli/dispatcher/internal/attestation/errors.go new file mode 100644 index 0000000000..401c225c99 --- /dev/null +++ b/cli/dispatcher/internal/attestation/errors.go @@ -0,0 +1,25 @@ +package attestation + +import "errors" + +// Sentinel errors let callers distinguish network trouble from a real signature +// or identity mismatch so update.go and dispatcher_download.go can render +// distinct messages ("update to a newer CLI" vs "release may be compromised"). +var ( + // ErrBundleFetch reports any failure retrieving .sigstore.json + // (network error, 4xx/5xx, empty body). Fail-closed at the call site. + ErrBundleFetch = errors.New("attestation bundle fetch failed") + + // ErrTagRefFetch reports failure resolving a release tag to its commit + // SHA via the GitHub git-refs API. Fail-closed at the call site. + ErrTagRefFetch = errors.New("release tag commit SHA lookup failed") + + // ErrMalformedBundle reports a bundle that could not be parsed as a + // Sigstore protobuf JSON. + ErrMalformedBundle = errors.New("attestation bundle is malformed") + + // ErrVerificationFailed reports Sigstore signature or policy failure — + // digest mismatch, identity mismatch, tlog absent, timestamp missing. + // This is the signal that a release asset may have been tampered with. + ErrVerificationFailed = errors.New("attestation verification failed") +) diff --git a/cli/dispatcher/internal/attestation/fetcher.go b/cli/dispatcher/internal/attestation/fetcher.go new file mode 100644 index 0000000000..bbf9f15e03 --- /dev/null +++ b/cli/dispatcher/internal/attestation/fetcher.go @@ -0,0 +1,156 @@ +package attestation + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "time" +) + +// DefaultHTTPClient is the http.Client used for bundle and tag-ref fetches. +// It is a var so tests can substitute a client wired to httptest servers. +var DefaultHTTPClient = &http.Client{Timeout: 30 * time.Second} + +// githubAPIBaseURL is mutable so tests can point it at an httptest server; +// production callers never touch it directly. +var githubAPIBaseURL = "https://api.github.com" + +func githubAPIBase() string { return githubAPIBaseURL } +func setGithubAPIBase(url string) { githubAPIBaseURL = url } + +const ( + githubReleaseBaseURL = "https://github.com/%s/releases/download/%s/%s" + envAuthTokenPrimary = "GITHUB_TOKEN" + envAuthTokenSecondary = "GH_TOKEN" + acceptHeaderGitHubJSON = "application/vnd.github+json" + apiVersionHeaderValue = "2022-11-28" +) + +// BundleAssetURL builds the release download URL for .sigstore.json. +// Callers pass the base asset name (without the .sigstore.json suffix). +func BundleAssetURL(repo, tag, assetName string) string { + return fmt.Sprintf(githubReleaseBaseURL, repo, tag, url.PathEscape(assetName+".sigstore.json")) +} + +// FetchBundle downloads the .sigstore.json body from the release. It +// wraps every failure in ErrBundleFetch so the caller fails closed. 403/429 +// are treated the same way as any other non-2xx: verification is not attempted +// because we cannot distinguish rate-limit denial from an attacker denying the +// bundle to skip verification. +// +// No Authorization header is set here: release download URLs +// (github.com/.../releases/download) are not subject to the API rate limits +// that GITHUB_TOKEN would relax, and Go's http.Client strips Authorization +// across the cross-host redirect to objects.githubusercontent.com anyway, so +// leaking the token would be pointless. Least-privilege: keep the token on +// api.github.com calls only (see fetchGitRef). +func FetchBundle(ctx context.Context, bundleURL string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, bundleURL, nil) + if err != nil { + return nil, fmt.Errorf("%w: build request: %v", ErrBundleFetch, err) + } + resp, err := DefaultHTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrBundleFetch, err) + } + defer func() { + _ = resp.Body.Close() + }() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("%w: status %s from %s", ErrBundleFetch, resp.Status, bundleURL) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("%w: read body: %v", ErrBundleFetch, err) + } + if len(body) == 0 { + return nil, fmt.Errorf("%w: empty body from %s", ErrBundleFetch, bundleURL) + } + return body, nil +} + +// TagRefResponse mirrors the git ref API payload shape for the fields we +// consume. +type TagRefResponse struct { + Object struct { + SHA string `json:"sha"` + Type string `json:"type"` + } `json:"object"` +} + +// FetchTagCommitSHA resolves a release tag to the git commit SHA it points at +// via the GitHub git-refs REST API. The verifier binds this SHA to the cert's +// Source Repository Digest extension so a stolen OIDC token cannot be reused +// on a tag it did not produce. We follow one level of "tag" indirection so +// annotated tags resolve to the same commit SHA as lightweight ones. +func FetchTagCommitSHA(ctx context.Context, repo, tag string) (string, error) { + initialURL := fmt.Sprintf("%s/repos/%s/git/ref/tags/%s", githubAPIBase(), repo, url.PathEscape(tag)) + sha, objectType, err := fetchGitRef(ctx, initialURL) + if err != nil { + return "", err + } + if objectType == "tag" { + tagURL := fmt.Sprintf("%s/repos/%s/git/tags/%s", githubAPIBase(), repo, url.PathEscape(sha)) + sha, objectType, err = fetchGitRef(ctx, tagURL) + if err != nil { + return "", err + } + } + if objectType != "commit" { + return "", fmt.Errorf("%w: unexpected object type %q for tag %s", ErrTagRefFetch, objectType, tag) + } + if !isHexCommitSHA(sha) { + return "", fmt.Errorf("%w: bad commit SHA %q for tag %s", ErrTagRefFetch, sha, tag) + } + return sha, nil +} + +func fetchGitRef(ctx context.Context, apiURL string) (string, string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil) + if err != nil { + return "", "", fmt.Errorf("%w: build request: %v", ErrTagRefFetch, err) + } + req.Header.Set("Accept", acceptHeaderGitHubJSON) + req.Header.Set("X-GitHub-Api-Version", apiVersionHeaderValue) + setAuthorizationIfAvailable(req) + resp, err := DefaultHTTPClient.Do(req) + if err != nil { + return "", "", fmt.Errorf("%w: %v", ErrTagRefFetch, err) + } + defer func() { + _ = resp.Body.Close() + }() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return "", "", fmt.Errorf("%w: status %s from %s", ErrTagRefFetch, resp.Status, apiURL) + } + var payload TagRefResponse + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + return "", "", fmt.Errorf("%w: decode payload: %v", ErrTagRefFetch, err) + } + return payload.Object.SHA, payload.Object.Type, nil +} + +func setAuthorizationIfAvailable(req *http.Request) { + token := os.Getenv(envAuthTokenPrimary) + if token == "" { + token = os.Getenv(envAuthTokenSecondary) + } + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } +} + +// AssetNameFromReleaseAsset strips any query fragments and returns the base +// asset name, so callers can pass a full URL and receive the file name. +func AssetNameFromReleaseAsset(assetURL string) string { + parsed, err := url.Parse(assetURL) + if err != nil { + return path.Base(assetURL) + } + return path.Base(parsed.Path) +} diff --git a/cli/dispatcher/internal/attestation/testdata/happy_asset_digest.txt b/cli/dispatcher/internal/attestation/testdata/happy_asset_digest.txt new file mode 100644 index 0000000000..ae0300b045 --- /dev/null +++ b/cli/dispatcher/internal/attestation/testdata/happy_asset_digest.txt @@ -0,0 +1 @@ +14c15a7c132f5a3b8eb11b7e3115fff35024793368da79cb90b325a343780a28 diff --git a/cli/dispatcher/internal/attestation/testdata/happy_bundle.json b/cli/dispatcher/internal/attestation/testdata/happy_bundle.json new file mode 100644 index 0000000000..310fb1f355 --- /dev/null +++ b/cli/dispatcher/internal/attestation/testdata/happy_bundle.json @@ -0,0 +1,67 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "tlogEntries": [ + { + "logIndex": "2064602974", + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "kindVersion": { + "kind": "dsse", + "version": "0.0.1" + }, + "integratedTime": "1783123835", + "inclusionPromise": { + "signedEntryTimestamp": "MEYCIQDRQDI45IYTRoRdByBKI0yKp4Uz9NZelQEwvgJOs5K6BQIhAKUWl+o6eGILscaTlZZeimvRuOgx9MXBBSME+lZOiCrU" + }, + "inclusionProof": { + "logIndex": "1942698712", + "rootHash": "h/XYzy2m7EiyTjkEDcS8SvEJ6oAQXHWolFLhJzWYbMs=", + "treeSize": "1942698721", + "hashes": [ + "YuwdppnHHWCikgOnvkHhvrBbukuAXhXi4tPY0WOHbE8=", + "2hGAPlNsN+p/ZJVe3cM8TP+2U7F1ktOUzrrwuwvOk8Y=", + "dh3dtQP83tJywAZ59ycfA8190Xbls5ERHMvVCSPTz1k=", + "At+eCC+vaFKu2t2auwgjlP4smgRL2xkbyBE82HCDX4c=", + "H62tn3rDgucbbnC9/iTH9PHWbDNRa//Hgc6yGQCirMA=", + "ADRn5rTXSywloM0P1Hiv0UeUCRemw5EbenvPVxQHscU=", + "zuF9+aXuV8aWonTXK5QPcRyZHqI1pIV+qhHBkyw0dU4=", + "TXdrMfgKScfQz/9CkggbpNDHAWOmoxGrI4dswSHZ418=", + "TyUcqqZPPBFyDR4MxyyRz9daRRO34R7MtWX9gchYytg=", + "s1+/PhQV+6dILrjlQf8LsLPqQxc80WpKHadeJbkvlzY=", + "+y2IQ/CDKskkt3+PA+VUA/AtJN3A+yQUPP1FYzMk/Ls=", + "Rbp611iTkVhSjGTb1x6vEcbGBGUkPM7Ji6y//fGjPno=", + "I1oTnEORJwtsgPyrNUPaRrV9oPrJfWgOLOtITOBbMo0=", + "0NfyYCi9u2Iw7F7fVF1Bqxuuy0S0EewQjD3ESErETBg=", + "QK735m1ZlL+ztxDvYY2YCdj5JyEVLgBQWTaCGgdE4zw=", + "7avEtv08HZnHXt8ix9lCSATmFs0f8gaaHsayxqM3WwI=", + "RIZjwQJv61lwM5JOP8iNT/RxKPr522/etVqxVBWn7KA=", + "b69YlhA5qlDwN70us4bq9ysnikE1ulA7EwKBTb+atY0=", + "LM7F5iwbk8jG29X4FwpkCN6BqDCfuNQTlz5CyQWt87s=", + "+/VZ56MsIPxMiyLAodzKXo5TEWdQp36z89qLhpzloAo=", + "daxmZaajRpZV+JxHiOYZhJBiSKN5ucqjh2WnGbHhirw=", + "DOCeoSMovIvLExkhIvisow9AuNXgeWs4ECkyR6EcqYU=" + ], + "checkpoint": { + "envelope": "rekor.sigstore.dev - 1193050959916656506\n1942698721\nh/XYzy2m7EiyTjkEDcS8SvEJ6oAQXHWolFLhJzWYbMs=\n\n— rekor.sigstore.dev wNI9ajBFAiA/EXbL5+l5A+F25iy2OZQo8WcjYYAZYpSM75q/TUl5EgIhAK1TD/sp/ZE9GofA59TNi1dFSLqCPABfJE2U4bkFsQBD\n" + } + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiYTgwNWYwOGJjOGE2MmY4MTcxY2Y3M2U1NWExN2ZiYzc1Y2U3MTc3ZTMxZDg4NThkODAzYjczMjEwN2FmZTFjNSJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjNjNDAyMDg1M2RhODQ3MjBiNzk5OGYxMjBjMjY5NjUzNDFjNTU1ZDIzNTMzM2M1NDlmM2I4NThiMTMxOGU3N2QifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVRQ0lGWDhVZ0pSN0UrNzNGckZQNVg2WW1FSHVJVlBFMDR0aGRNWnUvV0cyTWFqQWlCKytRWFl1bVBBd0dXSEJnSHlOKy9zNThpbWtCa2grQWpMZnFaaDM5bjY3Zz09IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VoU2FrTkRRbk15WjBGM1NVSkJaMGxWVFRVeFVYVXlZVFpOVVRZM1EwRXpWa2hhZEhGSlREVkVaak4zZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwWmQwNTZRVEJOUkVGNFRVUk5NRmRvWTA1TmFsbDNUbnBCTUUxRVFYbE5SRTB3VjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVkdaSEozYzNoWVVGa3hkbkZVU1ZaRVQwd3dWRzEyY3pVeGJGSXZXa2RyV0ZoT05YVUtWbk5uUjA5UFFTOUNiMWhzVmpCVVZpOUlZMjVoYVdOQ1RYRlZVbmRvY2xCUVNXdFRkMGxXZGtjeWFqRmpiRzR2YUdGUFEwSmxkM2RuWjFodlRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVkJOazVzQ2tOUFRHSjBUM2N4TDNwdVdqTlNZM0ZpVUN0T2JtVlJkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMlJCV1VSV1VqQlNRVkZJTDBKSGIzZGhTVnB0WVVoU01HTklUVFpNZVRsdVlWaFNiMlJYU1hWWk1qbDBUREpvYUdSSFJqVlpWekZvVEROV2RRcGhXRkkxVEZkT2MyRlRNWE5pTWpsM1RIazFibUZZVW05a1YwbDJaREk1ZVdFeVduTmlNMlI2VERKU2NHTXpRbWhrUjA1dldsaEpkR05JVm1saVIyeDZDbUZETlRWaVYzaEJZMjFXYldONU9XOWFWMFpyWTNrNU1rMTVNV2xhV0ZKb1RVUnJSME5wYzBkQlVWRkNaemM0ZDBGUlJVVkxNbWd3WkVoQ2VrOXBPSFlLWkVjNWNscFhOSFZaVjA0d1lWYzVkV041Tlc1aFdGSnZaRmRLTVdNeVZubFpNamwxWkVkV2RXUkROV3BpTWpCM1JXZFpTMHQzV1VKQ1FVZEVkbnBCUWdwQloxRkZZMGhXZW1GRVFUSkNaMjl5UW1kRlJVRlpUeTlOUVVWRVFrTm5kMXBFUlhwTmJWVjVXV3BhYWxscVJYZE9hbXQ0V1RKS2EwNHlWbXRQVkVrMUNrOVhTWGhOVjBwcVRrZFNhRTlIVVhkTk1rbDRUVU5CUjBOcGMwZEJVVkZDWnpjNGQwRlJVVVZGYlZKd1l6TkNhR1JIVG05YVdFbDBZMGhXYVdKSGJIb0tZVVJCYkVKbmIzSkNaMFZGUVZsUEwwMUJSVVpDUW1SdldWaFNhR1ZYUm5SWlV6a3hZbTFzTUdWVE1XcGlSMnQwWWtjNWRtTkVRV2RDWjI5eVFtZEZSUXBCV1U4dlRVRkZSMEpDU25sYVYxcDZUREpvYkZsWFVucE1NMWw2VEZkS2JHUkhSWGRQZDFsTFMzZFpRa0pCUjBSMmVrRkNRMEZSZEVSRGRHOWtTRkozQ21ONmIzWk1NMUoyWVRKV2RVeHRSbXBrUjJ4MlltNU5kVm95YkRCaFNGWnBaRmhPYkdOdFRuWmlibEpzWW01UmRWa3lPWFJOU0ZsSFEybHpSMEZSVVVJS1p6YzRkMEZSYTBWaFFYaHRZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRESm9hR1JIUmpWWlZ6Rm9URE5XZFdGWVVqVk1WMDV6WVZNeGN3cGlNamwzVEhrMWJtRllVbTlrVjBsMlpESTVlV0V5V25OaU0yUjZUREpTY0dNelFtaGtSMDV2V2xoSmRHTklWbWxpUjJ4NllVTTFOV0pYZUVGamJWWnRDbU41T1c5YVYwWnJZM2s1TWsxNU1XbGFXRkpvVFVSblIwTnBjMGRCVVZGQ1p6YzRkMEZSYjBWTFozZHZUVWRSZUUxNlNteE5iVWt5V1RKSmVFMUVXVFVLVFZkT2FWcEVaR3hhUkd0NVQxUnNhVTFVUm1sWmVsSnJXVlJvYTAxRVRtbE5WRUZrUW1kdmNrSm5SVVZCV1U4dlRVRkZURUpCT0UxRVYyUndaRWRvTVFwWmFURnZZak5PTUZwWFVYZFBaMWxMUzNkWlFrSkJSMFIyZWtGQ1JFRlJjMFJEY0c5a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFpoUjBZd0NsbFliR2hpVjBWMlpGYzFjR1JJYTNSWk1uaHdURmQ0ZG1JelFYZFBRVmxMUzNkWlFrSkJSMFIyZWtGQ1JGRlJjVVJEWjNkYVJFVjZUVzFWZVZscVdtb0tXV3BGZDA1cWEzaFpNa3ByVGpKV2EwOVVTVFZQVjBsNFRWZEthazVIVW1oUFIxRjNUVEpKZUUxRFNVZERhWE5IUVZGUlFtYzNPSGRCVVRSRlJrRjNVd3BqYlZadFkzazViMXBYUm10amVUa3lUWGt4YVZwWVVtaE5RbTlIUTJselIwRlJVVUpuTnpoM1FWRTRSVVJCZDB0TlZFRjNUV3BSTUU1NlJYaFBSRUZ5Q2tKbmIzSkNaMFZGUVZsUEwwMUJSVkZDUWpCTlJ6Sm9NR1JJUW5wUGFUaDJXakpzTUdGSVZtbE1iVTUyWWxNNWIxbFlVbWhsVjBaMFdWUkJWMEpuYjNJS1FtZEZSVUZaVHk5TlFVVlNRa0ZuVFVKcVp6Qk5hbFUwVG5wQ01rSm5iM0pDWjBWRlFWbFBMMDFCUlZOQ1IyZE5XbTFvTUdSSVFucFBhVGgyV2pKc01BcGhTRlpwVEcxT2RtSlRPVzlaV0ZKb1pWZEdkRmxUT1RGaWJXd3daVk14YW1KSGEzUmlSemwyWTBNNGRWb3liREJoU0ZacFRETmtkbU50ZEcxaVJ6a3pDbU41T1d0aFdFNTNXVmhTYW1GSFZubE1XRUl4V1cxNGNHTXlaM1ZsVnpGelVVaEtiRnB1VFhaaFIxWm9Xa2hOZG1ScVRYUlpiVll3V1ZSQk5FSm5iM0lLUW1kRlJVRlpUeTlOUVVWVVFrTnZUVXRFUW10TlZFMTVXbFJLYVU1dFRtbE5WRUV5VDFSR2FsbHRVVE5hVjFFMVRXcHJOVmxxUlhoWmJVMHdXa2RGTkFwYVJFRjZXV3BGZDBaQldVdExkMWxDUWtGSFJIWjZRVUpHUVZGSFJFRlNkMlJZVG05TlJqUkhRMmx6UjBGUlVVSm5OemgzUVZKVlJWVkJlRTloU0ZJd0NtTklUVFpNZVRsdVlWaFNiMlJYU1hWWk1qbDBUREpvYUdSSFJqVlpWekZvVEROV2RXRllValZNVjA1ellWTXhjMkl5T1hkTU1rWnFaRWRzZG1KdVRYWUtZMjVXZFdONU9IbFBSRmswVDBSak5FNUVWVE5QUXpsb1pFaFNiR0pZUWpCamVUaDRUVUpaUjBOcGMwZEJVVkZDWnpjNGQwRlNXVVZEUVhkSFkwaFdhUXBpUjJ4cVRVVk5SME5wYzBkQlVWRkNaemM0ZDBGU1owVk9VWGQ2WTIxV2QySjZjRzlaV0ZKb1pWZEdkRmxUT1RGaWJXd3daVk14YW1KSGEzUmlSemwyQ21ORWNIbGFWMWsyWTIxV2JXTjVPVzlhVjBaclkzazVNazE1TVdsYVdGSm9UVWxIUzBKbmIzSkNaMFZGUVdSYU5VRm5VVU5DU0hkRlpXZENORUZJV1VFS00xUXdkMkZ6WWtoRlZFcHFSMUkwWTIxWFl6TkJjVXBMV0hKcVpWQkxNeTlvTkhCNVowTTRjRGR2TkVGQlFVZG1TMjVhTXpWblFVRkNRVTFCVW5wQ1JncEJhVVZCZUdaQlpsQTJTbFUyVmpobmFsVkROemhoUVd3emEwSlplbXBVY1RaR1JVdHZTa2hOWldSVU1WVllhME5KUVZwcU9HNXdVRFJsUm1oT1Z6SjBDbUUyUm5aUloxSjZlV1prY2l0M1pYUTFNVWRQZEdSdVZqQnZOVnBOUVc5SFEwTnhSMU5OTkRsQ1FVMUVRVEpqUVUxSFVVTk5Ra2hVUTJ0T1YwWXZPVGtLUVhWcVJHd3hjekkyUzFCcllubENLMGhZY21kWWIyaERTRUpKYzFCalowRTNjVXd2UzNKNE1FdFNjRk5rVUVSSGRDODNlVmwzU1hkQ2RrVTBiVVkxU3dwRWIwOVhaMlY1TUhsMVdXazFTSEIyZEd0dVZXZ3lXRGRzYzFGNVdVNTFNRUZGZVZOMGRHZFVlakpLYVVnNFMxRkVjMmxQUnpkd1pRb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENnPT0ifV19fQ==" + } + ], + "timestampVerificationData": {}, + "certificate": { + "rawBytes": "MIIHRjCCBs2gAwIBAgIUM51Qu2a6MQ67CA3VHZtqIL5Df3wwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYwNzA0MDAxMDM0WhcNMjYwNzA0MDAyMDM0WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFdrwsxXPY1vqTIVDOL0Tmvs51lR/ZGkXXN5uVsgGOOA/BoXlV0TV/HcnaicBMqURwhrPPIkSwIVvG2j1cln/haOCBewwggXoMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUA6NlCOLbtOw1/znZ3RcqbP+NneQwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wdAYDVR0RAQH/BGowaIZmaHR0cHM6Ly9naXRodWIuY29tL2hhdGF5YW1hL3VuaXR5LWNsaS1sb29wLy5naXRodWIvd29ya2Zsb3dzL2Rpc3BhdGNoZXItcHVibGlzaC55bWxAcmVmcy9oZWFkcy92My1iZXRhMDkGCisGAQQBg78wAQEEK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wEgYKKwYBBAGDvzABAgQEcHVzaDA2BgorBgEEAYO/MAEDBCgwZDEzMmUyYjZjYjEwNjkxY2JkN2VkOTI5OWIxMWJjNGRhOGQwM2IxMCAGCisGAQQBg78wAQQEEmRpc3BhdGNoZXItcHVibGlzaDAlBgorBgEEAYO/MAEFBBdoYXRheWFtYS91bml0eS1jbGktbG9vcDAgBgorBgEEAYO/MAEGBBJyZWZzL2hlYWRzL3YzLWJldGEwOwYKKwYBBAGDvzABCAQtDCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMHYGCisGAQQBg78wAQkEaAxmaHR0cHM6Ly9naXRodWIuY29tL2hhdGF5YW1hL3VuaXR5LWNsaS1sb29wLy5naXRodWIvd29ya2Zsb3dzL2Rpc3BhdGNoZXItcHVibGlzaC55bWxAcmVmcy9oZWFkcy92My1iZXRhMDgGCisGAQQBg78wAQoEKgwoMGQxMzJlMmI2Y2IxMDY5MWNiZDdlZDkyOTliMTFiYzRkYThkMDNiMTAdBgorBgEEAYO/MAELBA8MDWdpdGh1Yi1ob3N0ZWQwOgYKKwYBBAGDvzABDAQsDCpodHRwczovL2dpdGh1Yi5jb20vaGF0YXlhbWEvdW5pdHktY2xpLWxvb3AwOAYKKwYBBAGDvzABDQQqDCgwZDEzMmUyYjZjYjEwNjkxY2JkN2VkOTI5OWIxMWJjNGRhOGQwM2IxMCIGCisGAQQBg78wAQ4EFAwScmVmcy9oZWFkcy92My1iZXRhMBoGCisGAQQBg78wAQ8EDAwKMTAwMjQ0NzExODArBgorBgEEAYO/MAEQBB0MG2h0dHBzOi8vZ2l0aHViLmNvbS9oYXRheWFtYTAWBgorBgEEAYO/MAERBAgMBjg0MjU4NzB2BgorBgEEAYO/MAESBGgMZmh0dHBzOi8vZ2l0aHViLmNvbS9oYXRheWFtYS91bml0eS1jbGktbG9vcC8uZ2l0aHViL3dvcmtmbG93cy9kaXNwYXRjaGVyLXB1Ymxpc2gueW1sQHJlZnMvaGVhZHMvdjMtYmV0YTA4BgorBgEEAYO/MAETBCoMKDBkMTMyZTJiNmNiMTA2OTFjYmQ3ZWQ5Mjk5YjExYmM0ZGE4ZDAzYjEwFAYKKwYBBAGDvzABFAQGDARwdXNoMF4GCisGAQQBg78wARUEUAxOaHR0cHM6Ly9naXRodWIuY29tL2hhdGF5YW1hL3VuaXR5LWNsaS1sb29wL2FjdGlvbnMvcnVucy8yODY4ODc4NDU3OC9hdHRlbXB0cy8xMBYGCisGAQQBg78wARYECAwGcHVibGljMEMGCisGAQQBg78wARgENQwzcmVwbzpoYXRheWFtYS91bml0eS1jbGktbG9vcDpyZWY6cmVmcy9oZWFkcy92My1iZXRhMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGfKnZ35gAABAMARzBFAiEAxfAfP6JU6V8gjUC78aAl3kBYzjTq6FEKoJHMedT1UXkCIAZj8npP4eFhNW2ta6FvQgRzyfdr+wet51GOtdnV0o5ZMAoGCCqGSM49BAMDA2cAMGQCMBHTCkNWF/99AujDl1s26KPkbyB+HXrgXohCHBIsPcgA7qL/Krx0KRpSdPDGt/7yYwIwBvE4mF5KDoOWgey0yuYi5HpvtknUh2X7lsQyYNu0AEySttgTz2JiH8KQDsiOG7pe" + } + }, + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiaW5zdGFsbC5wczEiLCJkaWdlc3QiOnsic2hhMjU2IjoiYjBlOGJmZDIyZDdlMjc3ZjhhMWUwZDg1NTQ5NGQwNjg4ZTY2ODc5ZTA3ZjY4Y2FhOTUxNTczYTZhMjZkMTRhOSJ9fSx7Im5hbWUiOiJpbnN0YWxsLnBzMS5zaGEyNTYiLCJkaWdlc3QiOnsic2hhMjU2IjoiZmRmYjA4YzUxNGU0NGU1NWU2MWMwNjY4ODI1OTZiYTc3OTAwNWFlZGM0ZDNjYjhkMmNjZDFlOGI2ZDc5MWE3NCJ9fSx7Im5hbWUiOiJpbnN0YWxsLnNoIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU2NmJhZDI0YmFhN2ZhZWVkYjQ4MDQ4YmRjODJiZTNjNTVjYjIzNThkMDkwNDk3MTJmOGY0M2M0ZTQzMGJhMGMifX0seyJuYW1lIjoiaW5zdGFsbC5zaC5zaGEyNTYiLCJkaWdlc3QiOnsic2hhMjU2IjoiMTQwNDRiYWQ0OGJkMDRmZWI2MjBiNzA5ODhlYzdiYzBhYTQ0NDEyNWJmZWZkYjcwZjRhNmQ5ZGMwNzZjNWM5ZSJ9fSx7Im5hbWUiOiJ1bG9vcC1kaXNwYXRjaGVyLWRhcndpbi1hbWQ2NC50YXIuZ3oiLCJkaWdlc3QiOnsic2hhMjU2IjoiYmJjOTY2ODg1NWU1OTJjMzA5NWU0NGJiNTVmMTY2NjVmZGVmYmZlYmI0NzkyMjkxMDYzNjZmMThmZjAzMWNlMyJ9fSx7Im5hbWUiOiJ1bG9vcC1kaXNwYXRjaGVyLWRhcndpbi1hbWQ2NC50YXIuZ3ouc2hhMjU2IiwiZGlnZXN0Ijp7InNoYTI1NiI6Ijc1MTdmMDAwODA0Mjg5YjEzMWI3N2YzY2FlNWJkMTI1NDViYWI0MDQ2ZTg3YmEwMDJhMWUwMTdjMWIyZTFkZTcifX0seyJuYW1lIjoidWxvb3AtZGlzcGF0Y2hlci1kYXJ3aW4tYXJtNjQudGFyLmd6IiwiZGlnZXN0Ijp7InNoYTI1NiI6IjE0YzE1YTdjMTMyZjVhM2I4ZWIxMWI3ZTMxMTVmZmYzNTAyNDc5MzM2OGRhNzljYjkwYjMyNWEzNDM3ODBhMjgifX0seyJuYW1lIjoidWxvb3AtZGlzcGF0Y2hlci1kYXJ3aW4tYXJtNjQudGFyLmd6LnNoYTI1NiIsImRpZ2VzdCI6eyJzaGEyNTYiOiI3NWZhMjJkZGUyMjkzOGY2ZDBlZWQ4MTI5NzJiMDI3ZTI1NjFiNzVhMmRjNWJjMjM4MmQyZjc2NjcxNzNjMDc5In19LHsibmFtZSI6InVsb29wLWRpc3BhdGNoZXItd2luZG93cy1hbWQ2NC56aXAiLCJkaWdlc3QiOnsic2hhMjU2IjoiMDE5MmRkY2MxNWEzZWUxMDJlMThlYWMzZGY1MzhhZmVlNDczOGQ1ZDFkZjQyODQ4OWQxZjJiOThjZmZkOGRjMCJ9fSx7Im5hbWUiOiJ1bG9vcC1kaXNwYXRjaGVyLXdpbmRvd3MtYW1kNjQuemlwLnNoYTI1NiIsImRpZ2VzdCI6eyJzaGEyNTYiOiI2YzFjZWM0N2ZlNWI2MDhjNjNiZjY1NTljMGMyMjE2OGExMTEzMTE1YzI5MjQ0ZTU4N2FkODYzY2FmZjVmMTRlIn19XSwicHJlZGljYXRlVHlwZSI6Imh0dHBzOi8vc2xzYS5kZXYvcHJvdmVuYW5jZS92MSIsInByZWRpY2F0ZSI6eyJidWlsZERlZmluaXRpb24iOnsiYnVpbGRUeXBlIjoiaHR0cHM6Ly9hY3Rpb25zLmdpdGh1Yi5pby9idWlsZHR5cGVzL3dvcmtmbG93L3YxIiwiZXh0ZXJuYWxQYXJhbWV0ZXJzIjp7IndvcmtmbG93Ijp7InJlZiI6InJlZnMvaGVhZHMvdjMtYmV0YSIsInJlcG9zaXRvcnkiOiJodHRwczovL2dpdGh1Yi5jb20vaGF0YXlhbWEvdW5pdHktY2xpLWxvb3AiLCJwYXRoIjoiLmdpdGh1Yi93b3JrZmxvd3MvZGlzcGF0Y2hlci1wdWJsaXNoLnltbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoicHVzaCIsInJlcG9zaXRvcnlfaWQiOiIxMDAyNDQ3MTE4IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6Ijg0MjU4NyIsInJ1bm5lcl9lbnZpcm9ubWVudCI6ImdpdGh1Yi1ob3N0ZWQifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL2hhdGF5YW1hL3VuaXR5LWNsaS1sb29wQHJlZnMvaGVhZHMvdjMtYmV0YSIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiIwZDEzMmUyYjZjYjEwNjkxY2JkN2VkOTI5OWIxMWJjNGRhOGQwM2IxIn19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9oYXRheWFtYS91bml0eS1jbGktbG9vcC8uZ2l0aHViL3dvcmtmbG93cy9kaXNwYXRjaGVyLXB1Ymxpc2gueW1sQHJlZnMvaGVhZHMvdjMtYmV0YSJ9LCJtZXRhZGF0YSI6eyJpbnZvY2F0aW9uSWQiOiJodHRwczovL2dpdGh1Yi5jb20vaGF0YXlhbWEvdW5pdHktY2xpLWxvb3AvYWN0aW9ucy9ydW5zLzI4Njg4Nzg0NTc4L2F0dGVtcHRzLzEifX19fQ==", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEQCIFX8UgJR7E+73FrFP5X6YmEHuIVPE04thdMZu/WG2MajAiB++QXYumPAwGWHBgHyN+/s58imkBkh+AjLfqZh39n67g==" + } + ] + } +} diff --git a/cli/dispatcher/internal/attestation/testdata/happy_commit_sha.txt b/cli/dispatcher/internal/attestation/testdata/happy_commit_sha.txt new file mode 100644 index 0000000000..c2ae4ab34c --- /dev/null +++ b/cli/dispatcher/internal/attestation/testdata/happy_commit_sha.txt @@ -0,0 +1 @@ +0d132e2b6cb10691cbd7ed9299b11bc4da8d03b1 diff --git a/cli/dispatcher/internal/attestation/trusted_root.go b/cli/dispatcher/internal/attestation/trusted_root.go new file mode 100644 index 0000000000..7f1f1a98ee --- /dev/null +++ b/cli/dispatcher/internal/attestation/trusted_root.go @@ -0,0 +1,35 @@ +// Package attestation verifies GitHub Artifact Attestations for dispatcher +// self-update and project runner download flows. +// +// Trust anchor is embedded via go:embed at build time so verification stays +// offline and deterministic. Refresh via cmd/refresh-attestation-trusted-root. +// The CI guard test in trusted_root_test.go fails when the embedded root's +// authorities approach expiration so a missed quarterly refresh surfaces as a +// red build, not a fleet-wide self-update outage after Fulcio or Rekor rotates. +package attestation + +import ( + _ "embed" + "fmt" + + "github.com/sigstore/sigstore-go/pkg/root" +) + +//go:embed trusted_root.json +var embeddedTrustedRoot []byte + +// LoadEmbeddedTrustedMaterial parses the embedded Sigstore trusted_root.json +// into a root.TrustedMaterial suitable for verify.NewVerifier. +func LoadEmbeddedTrustedMaterial() (root.TrustedMaterial, error) { + tr, err := root.NewTrustedRootFromJSON(embeddedTrustedRoot) + if err != nil { + return nil, fmt.Errorf("parse embedded trusted_root.json: %w", err) + } + return tr, nil +} + +// EmbeddedTrustedRootBytes returns the raw embedded trusted_root.json bytes +// so tests can inspect authority validity windows without re-parsing. +func EmbeddedTrustedRootBytes() []byte { + return embeddedTrustedRoot +} diff --git a/cli/dispatcher/internal/attestation/trusted_root.json b/cli/dispatcher/internal/attestation/trusted_root.json new file mode 100644 index 0000000000..effb0a19e6 --- /dev/null +++ b/cli/dispatcher/internal/attestation/trusted_root.json @@ -0,0 +1,126 @@ +{ + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [ + { + "baseUrl": "https://rekor.sigstore.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-01-12T11:53:27Z" + } + }, + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + } + }, + { + "baseUrl": "https://log2025-1.rekor.sigstore.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MCowBQYDK2VwAyEAt8rlp1knGwjfbcXAYPYAkn0XiLz1x8O4t0YkEhie244=", + "keyDetails": "PKIX_ED25519", + "validFor": { + "start": "2025-09-23T00:00:00Z" + } + }, + "logId": { + "keyId": "zxGZFVvd0FEmjR8WrFwMdcAJ9vtaY/QXf44Y1wUeP6A=" + } + } + ], + "certificateAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstore.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==" + } + ] + }, + "validFor": { + "start": "2021-03-07T03:20:29Z", + "end": "2022-12-31T23:59:59.999Z" + } + }, + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstore.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" + }, + { + "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" + } + ] + }, + "validFor": { + "start": "2022-04-13T20:06:15Z" + } + } + ], + "ctlogs": [ + { + "baseUrl": "https://ctfe.sigstore.dev/test", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-03-14T00:00:00Z", + "end": "2022-10-31T23:59:59.999Z" + } + }, + "logId": { + "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=" + } + }, + { + "baseUrl": "https://ctfe.sigstore.dev/2022", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-10-20T00:00:00Z" + } + }, + "logId": { + "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=" + } + } + ], + "timestampAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore-tsa-selfsigned" + }, + "uri": "https://timestamp.sigstore.dev/api/v1/timestamp", + "certChain": { + "certificates": [ + { + "rawBytes": "MIICEDCCAZagAwIBAgIUOhNULwyQYe68wUMvy4qOiyojiwwwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ra2Z8hKNig2T9kFjCAToGG30jky+WQv3BzL+mKvh1SKNR/UwuwsfNCg4sryoYAd8E6isovVA3M4aoNdm9QDi50Z8nTEyvqgfDPtTIwXItfiW/AFf1V7uwkbkAoj0xxco2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFIn9eUOHz9BlRsMCRscsc1t9tOsDMB8GA1UdIwQYMBaAFJjsAe9/u1H/1JUeb4qImFMHic6/MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2gAMGUCMDtpsV/6KaO0qyF/UMsX2aSUXKQFdoGTptQGc0ftq1csulHPGG6dsmyMNd3JB+G3EQIxAOajvBcjpJmKb4Nv+2Taoj8Uc5+b6ih6FXCCKraSqupe07zqswMcXJTe1cExvHvvlw==" + }, + { + "rawBytes": "MIIB9zCCAXygAwIBAgIUV7f0GLDOoEzIh8LXSW80OJiUp14wCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQUQNtfRT/ou3YATa6wB/kKTe70cfJwyRIBovMnt8RcJph/COE82uyS6FmppLLL1VBPGcPfpQPYJNXzWwi8icwhKQ6W/Qe2h3oebBb2FHpwNJDqo+TMaC/tdfkv/ElJB72jRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSY7AHvf7tR/9SVHm+KiJhTB4nOvzAKBggqhkjOPQQDAwNpADBmAjEAwGEGrfGZR1cen1R8/DTVMI943LssZmJRtDp/i7SfGHmGRP6gRbuj9vOK3b67Z0QQAjEAuT2H673LQEaHTcyQSZrkp4mX7WwkmF+sVbkYY5mXN+RMH13KUEHHOqASaemYWK/E" + } + ] + }, + "validFor": { + "start": "2025-07-04T00:00:00Z" + } + } + ] +} diff --git a/cli/dispatcher/internal/attestation/trusted_root_test.go b/cli/dispatcher/internal/attestation/trusted_root_test.go new file mode 100644 index 0000000000..abafa593df --- /dev/null +++ b/cli/dispatcher/internal/attestation/trusted_root_test.go @@ -0,0 +1,126 @@ +package attestation + +import ( + "encoding/json" + "testing" + "time" +) + +// Verifies the embedded Sigstore trusted_root.json parses into a usable +// TrustedMaterial so a corrupted commit is caught before it can silently +// break self-update verification. +func TestLoadEmbeddedTrustedMaterial_Parses(t *testing.T) { + tr, err := LoadEmbeddedTrustedMaterial() + if err != nil { + t.Fatalf("expected embedded trusted_root to parse, got: %v", err) + } + if tr == nil { + t.Fatal("expected non-nil trusted material") + } + if len(tr.FulcioCertificateAuthorities()) == 0 { + t.Fatal("expected at least one Fulcio certificate authority") + } + if len(tr.RekorLogs()) == 0 { + t.Fatal("expected at least one Rekor log") + } +} + +// Sigstore public-good roots rotate authorities and transparency-log keys +// periodically. When every embedded authority is about to expire, offline +// verification will start failing en masse. This test fails first — in CI — +// so a maintainer can refresh trusted_root.json (via +// cli/dispatcher/cmd/refresh-attestation-trusted-root) and ship it in a +// dispatcher release before customers notice. +// +// Rule: for each category (Fulcio CAs, Rekor logs, CT logs, TSAs) that is +// present in the root, at least one authority must be usable more than the +// refresh warning horizon into the future — either its validFor.end is set +// beyond that horizon or it has no end (still active indefinitely). +const trustedRootWarningHorizon = 90 * 24 * time.Hour + +type validFor struct { + Start string `json:"start"` + End string `json:"end,omitempty"` +} + +type rawTLog struct { + PublicKey struct { + ValidFor validFor `json:"validFor"` + } `json:"publicKey"` +} + +type rawTrustedRoot struct { + CertificateAuthorities []struct { + ValidFor validFor `json:"validFor"` + } `json:"certificateAuthorities"` + TimestampAuthorities []struct { + ValidFor validFor `json:"validFor"` + } `json:"timestampAuthorities"` + TLogs []rawTLog `json:"tlogs"` + CTLogs []rawTLog `json:"ctlogs"` +} + +func TestEmbeddedTrustedRoot_HasFutureCapableAuthorityPerCategory(t *testing.T) { + var root rawTrustedRoot + if err := json.Unmarshal(EmbeddedTrustedRootBytes(), &root); err != nil { + t.Fatalf("parse embedded trusted_root: %v", err) + } + + now := time.Now().UTC() + horizon := now.Add(trustedRootWarningHorizon) + + assertHorizon := func(category string, entries []validFor) { + if len(entries) == 0 { + return // category simply not populated in this root + } + for _, entry := range entries { + if isValidBeyond(t, entry, horizon) { + return + } + } + t.Fatalf("no %s authority is usable past %s (all end before). Refresh cli/dispatcher/internal/attestation/trusted_root.json via `go run ./cmd/refresh-attestation-trusted-root` and land a fix(dispatcher) release.", category, horizon.Format(time.RFC3339)) + } + + caEntries := make([]validFor, 0, len(root.CertificateAuthorities)) + for _, e := range root.CertificateAuthorities { + caEntries = append(caEntries, e.ValidFor) + } + tsaEntries := make([]validFor, 0, len(root.TimestampAuthorities)) + for _, e := range root.TimestampAuthorities { + tsaEntries = append(tsaEntries, e.ValidFor) + } + tlogEntries := make([]validFor, 0, len(root.TLogs)) + for _, e := range root.TLogs { + tlogEntries = append(tlogEntries, e.PublicKey.ValidFor) + } + ctlogEntries := make([]validFor, 0, len(root.CTLogs)) + for _, e := range root.CTLogs { + ctlogEntries = append(ctlogEntries, e.PublicKey.ValidFor) + } + + assertHorizon("Fulcio CA", caEntries) + assertHorizon("Timestamp Authority", tsaEntries) + assertHorizon("Rekor log", tlogEntries) + assertHorizon("CT log", ctlogEntries) +} + +// isValidBeyond returns true when the entry is either unlimited (no end date) +// or its end date is on/after the given horizon. The Sigstore public-good +// root ships historical authorities with a Start value in the past, so we +// only gate on End here — a Start-in-the-future entry has never appeared in +// this root and is not part of the horizon contract this test enforces. +func isValidBeyond(t *testing.T, v validFor, horizon time.Time) bool { + t.Helper() + if v.End == "" { + return true + } + end, err := time.Parse(time.RFC3339, v.End) + if err != nil { + // Some sigstore roots use millisecond precision. Try that too. + end, err = time.Parse("2006-01-02T15:04:05.999Z07:00", v.End) + if err != nil { + t.Fatalf("cannot parse validFor.end %q: %v", v.End, err) + } + } + return !end.Before(horizon) +} diff --git a/cli/dispatcher/internal/attestation/verifier.go b/cli/dispatcher/internal/attestation/verifier.go new file mode 100644 index 0000000000..7710e9ae19 --- /dev/null +++ b/cli/dispatcher/internal/attestation/verifier.go @@ -0,0 +1,175 @@ +package attestation + +import ( + "encoding/hex" + "fmt" + + "github.com/sigstore/sigstore-go/pkg/bundle" + "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/verify" +) + +// Identity describes the GitHub Actions workflow identity that a signed release +// asset must present. Refs is the set of git refs the workflow may have run +// from — we accept a small closed allowlist (e.g. v3-beta and main branches) +// rather than a regex so a leaked OIDC token for an unrelated ref cannot forge +// releases. +type Identity struct { + Repository string + WorkflowPath string + Refs []string +} + +// SubjectAlternativeNames constructs the exact-match SAN strings that a Fulcio +// certificate must expose to pass this identity — one per allowed ref. +func (id Identity) SubjectAlternativeNames() []string { + sans := make([]string, 0, len(id.Refs)) + for _, ref := range id.Refs { + sans = append(sans, fmt.Sprintf("https://github.com/%s/%s@%s", id.Repository, id.WorkflowPath, ref)) + } + return sans +} + +// SourceRepositoryURI is the value the Fulcio cert must expose under OID +// 1.3.6.1.4.1.57264.1.12 — the top-level repository the workflow ran in. +func (id Identity) SourceRepositoryURI() string { + return "https://github.com/" + id.Repository +} + +// VerifyOptions is the input to Verify. +type VerifyOptions struct { + // AssetDigest is the SHA-256 hex digest of the local release asset the + // caller intends to trust. Must be the same digest the caller will use + // to open/execute the file. + AssetDigest string + + // BundleData is the raw JSON bytes of the .sigstore.json bundle + // downloaded from the release. The single bundle carries multiple + // subjects (one per release asset); Verify enforces that AssetDigest + // appears in the subject list. + BundleData []byte + + // ExpectedCommitSHA is the 40-char hex git commit SHA that the release + // tag points at (resolved via /repos/{owner}/{repo}/git/ref/tags/{tag}). + // The Fulcio cert's OID 1.3.6.1.4.1.57264.1.13 (SourceRepositoryDigest) + // must match — this binds the attestation to a specific tree state and + // prevents a stolen OIDC token from being reused on a different commit. + ExpectedCommitSHA string + + // Identity is the SAN allowlist + repository URI the certificate must + // match. The verifier accepts any listed ref, so releases from either + // v3-beta or main pass. + Identity Identity +} + +// Verify runs full Sigstore verification on the bundle: signature validity, +// transparency log inclusion, integrated timestamps, artifact digest match, +// certificate identity (SAN + source repo digest binding). Fail-closed on any +// step. Callers should treat a nil return as "safe to execute this file". +// +// The trustedMaterial argument is threaded in to make CI guard tests possible; +// production callers should pass LoadEmbeddedTrustedMaterial() so verification +// stays offline and deterministic across dispatcher invocations. +func Verify(trustedMaterial root.TrustedMaterial, opts VerifyOptions) error { + if err := opts.validate(); err != nil { + return err + } + + digestBytes, err := hex.DecodeString(opts.AssetDigest) + if err != nil { + return fmt.Errorf("%w: asset digest must be hex sha256: %v", ErrVerificationFailed, err) + } + if len(digestBytes) != 32 { + return fmt.Errorf("%w: asset digest must be 32 bytes (sha256), got %d", ErrVerificationFailed, len(digestBytes)) + } + + var b bundle.Bundle + if err := b.UnmarshalJSON(opts.BundleData); err != nil { + return fmt.Errorf("%w: %v", ErrMalformedBundle, err) + } + + verifier, err := verify.NewVerifier(trustedMaterial, + verify.WithTransparencyLog(1), + verify.WithIntegratedTimestamps(1), + verify.WithSignedCertificateTimestamps(1)) + if err != nil { + return fmt.Errorf("%w: build verifier: %v", ErrVerificationFailed, err) + } + + identities, err := buildCertificateIdentities(opts.Identity, opts.ExpectedCommitSHA) + if err != nil { + return fmt.Errorf("%w: build identities: %v", ErrVerificationFailed, err) + } + + policyOpts := make([]verify.PolicyOption, 0, len(identities)) + for _, id := range identities { + policyOpts = append(policyOpts, verify.WithCertificateIdentity(id)) + } + + policy := verify.NewPolicy(verify.WithArtifactDigest("sha256", digestBytes), policyOpts...) + + if _, err := verifier.Verify(&b, policy); err != nil { + return fmt.Errorf("%w: %v", ErrVerificationFailed, err) + } + return nil +} + +func (o VerifyOptions) validate() error { + if o.AssetDigest == "" { + return fmt.Errorf("%w: AssetDigest required", ErrVerificationFailed) + } + if len(o.BundleData) == 0 { + return fmt.Errorf("%w: BundleData required", ErrVerificationFailed) + } + if !isHexCommitSHA(o.ExpectedCommitSHA) { + return fmt.Errorf("%w: ExpectedCommitSHA must be 40-char hex", ErrVerificationFailed) + } + if o.Identity.Repository == "" || o.Identity.WorkflowPath == "" || len(o.Identity.Refs) == 0 { + return fmt.Errorf("%w: Identity.Repository, WorkflowPath, and at least one Ref required", ErrVerificationFailed) + } + return nil +} + +func isHexCommitSHA(s string) bool { + if len(s) != 40 { + return false + } + for i := 0; i < len(s); i++ { + c := s[i] + if (c < '0' || c > '9') && (c < 'a' || c > 'f') { + return false + } + } + return true +} + +func buildCertificateIdentities(id Identity, expectedCommitSHA string) (verify.CertificateIdentities, error) { + sans := id.SubjectAlternativeNames() + if len(sans) == 0 { + return nil, fmt.Errorf("identity has no refs") + } + identities := make(verify.CertificateIdentities, 0, len(sans)) + issuerMatcher, err := verify.NewIssuerMatcher("https://token.actions.githubusercontent.com", "") + if err != nil { + return nil, err + } + for _, san := range sans { + sanMatcher, err := verify.NewSANMatcher(san, "") + if err != nil { + return nil, fmt.Errorf("build SAN matcher: %w", err) + } + identities = append(identities, verify.CertificateIdentity{ + SubjectAlternativeName: sanMatcher, + Issuer: issuerMatcher, + Extensions: certificate.Extensions{ + SourceRepositoryURI: id.SourceRepositoryURI(), + SourceRepositoryDigest: expectedCommitSHA, + }, + }) + } + if len(identities) == 0 { + return nil, fmt.Errorf("identity produced no matchers") + } + return identities, nil +} diff --git a/cli/dispatcher/internal/attestation/verifier_test.go b/cli/dispatcher/internal/attestation/verifier_test.go new file mode 100644 index 0000000000..3cece2cb7b --- /dev/null +++ b/cli/dispatcher/internal/attestation/verifier_test.go @@ -0,0 +1,302 @@ +package attestation + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" +) + +// happyFixture returns fixture values pulled from a real backfilled +// dispatcher-v3.0.1-beta.12 arm64 tarball attestation. Verifier tests run +// offline against the embedded trusted_root.json so they stay deterministic. +type happyFixture struct { + bundle []byte + digest string + commitSHA string + identity Identity +} + +func loadHappyFixture(t *testing.T) happyFixture { + t.Helper() + fixtureDir := filepath.Join("testdata") + bundleBytes, err := os.ReadFile(filepath.Join(fixtureDir, "happy_bundle.json")) + if err != nil { + t.Fatalf("read happy_bundle.json: %v", err) + } + digest := readOneLine(t, filepath.Join(fixtureDir, "happy_asset_digest.txt")) + commitSHA := readOneLine(t, filepath.Join(fixtureDir, "happy_commit_sha.txt")) + return happyFixture{ + bundle: bundleBytes, + digest: digest, + commitSHA: commitSHA, + identity: Identity{ + Repository: "hatayama/unity-cli-loop", + WorkflowPath: ".github/workflows/dispatcher-publish.yml", + Refs: []string{"refs/heads/v3-beta", "refs/heads/main"}, + }, + } +} + +func readOneLine(t *testing.T, path string) string { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + return strings.TrimSpace(string(data)) +} + +// Verifies that a real bundle + digest + commit SHA + allowlisted SAN passes. +func TestVerify_HappyPath(t *testing.T) { + f := loadHappyFixture(t) + trusted, err := LoadEmbeddedTrustedMaterial() + if err != nil { + t.Fatalf("load trusted root: %v", err) + } + err = Verify(trusted, VerifyOptions{ + AssetDigest: f.digest, + BundleData: f.bundle, + ExpectedCommitSHA: f.commitSHA, + Identity: f.identity, + }) + if err != nil { + t.Fatalf("expected happy path to verify, got: %v", err) + } +} + +// Verifies fail-closed when the local asset digest is not one of the bundle's subjects. +func TestVerify_DigestMismatch(t *testing.T) { + f := loadHappyFixture(t) + trusted, err := LoadEmbeddedTrustedMaterial() + if err != nil { + t.Fatalf("load trusted root: %v", err) + } + wrongDigest := "0000000000000000000000000000000000000000000000000000000000000000" + err = Verify(trusted, VerifyOptions{ + AssetDigest: wrongDigest, + BundleData: f.bundle, + ExpectedCommitSHA: f.commitSHA, + Identity: f.identity, + }) + if err == nil { + t.Fatalf("expected digest mismatch to fail, got nil") + } + if !errors.Is(err, ErrVerificationFailed) { + t.Fatalf("expected ErrVerificationFailed, got: %v", err) + } +} + +// Verifies fail-closed when SAN allowlist does not include the cert's signer. +func TestVerify_IdentityMismatch_BadSAN(t *testing.T) { + f := loadHappyFixture(t) + trusted, err := LoadEmbeddedTrustedMaterial() + if err != nil { + t.Fatalf("load trusted root: %v", err) + } + badIdentity := f.identity + badIdentity.Repository = "attacker/evil-fork" + err = Verify(trusted, VerifyOptions{ + AssetDigest: f.digest, + BundleData: f.bundle, + ExpectedCommitSHA: f.commitSHA, + Identity: badIdentity, + }) + if err == nil { + t.Fatalf("expected SAN mismatch to fail, got nil") + } + if !errors.Is(err, ErrVerificationFailed) { + t.Fatalf("expected ErrVerificationFailed, got: %v", err) + } +} + +// Verifies fail-closed when the expected commit SHA does not match cert's OID .13. +func TestVerify_IdentityMismatch_BadCommitSHA(t *testing.T) { + f := loadHappyFixture(t) + trusted, err := LoadEmbeddedTrustedMaterial() + if err != nil { + t.Fatalf("load trusted root: %v", err) + } + wrongCommit := "0000000000000000000000000000000000000000" + err = Verify(trusted, VerifyOptions{ + AssetDigest: f.digest, + BundleData: f.bundle, + ExpectedCommitSHA: wrongCommit, + Identity: f.identity, + }) + if err == nil { + t.Fatalf("expected commit SHA mismatch to fail, got nil") + } + if !errors.Is(err, ErrVerificationFailed) { + t.Fatalf("expected ErrVerificationFailed, got: %v", err) + } +} + +// Verifies fail-closed when the bundle bytes are malformed. +func TestVerify_MalformedBundle(t *testing.T) { + f := loadHappyFixture(t) + trusted, err := LoadEmbeddedTrustedMaterial() + if err != nil { + t.Fatalf("load trusted root: %v", err) + } + err = Verify(trusted, VerifyOptions{ + AssetDigest: f.digest, + BundleData: []byte("this is not a sigstore bundle"), + ExpectedCommitSHA: f.commitSHA, + Identity: f.identity, + }) + if err == nil { + t.Fatalf("expected malformed bundle to fail, got nil") + } + if !errors.Is(err, ErrMalformedBundle) { + t.Fatalf("expected ErrMalformedBundle, got: %v", err) + } +} + +// Verifies FetchBundle wraps a 404 (bundle-missing) into ErrBundleFetch. +func TestFetchBundle_MissingReturns404(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.NotFound(w, r) + })) + defer server.Close() + + _, err := FetchBundle(context.Background(), server.URL+"/whatever.sigstore.json") + if err == nil { + t.Fatalf("expected 404 to fail, got nil") + } + if !errors.Is(err, ErrBundleFetch) { + t.Fatalf("expected ErrBundleFetch, got: %v", err) + } +} + +// Verifies FetchBundle fails closed on 403 (which GitHub returns during rate +// limiting and abuse detection). We must never treat 403 as "no bundle +// available so skip", or an attacker can DoS attestation lookups to strip +// verification. +func TestFetchBundle_403FailsClosed(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusForbidden) + })) + defer server.Close() + + _, err := FetchBundle(context.Background(), server.URL+"/whatever.sigstore.json") + if err == nil { + t.Fatalf("expected 403 to fail-closed, got nil") + } + if !errors.Is(err, ErrBundleFetch) { + t.Fatalf("expected ErrBundleFetch, got: %v", err) + } +} + +// Verifies FetchBundle fails closed on 429 (rate limit). +func TestFetchBundle_429FailsClosed(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTooManyRequests) + })) + defer server.Close() + + _, err := FetchBundle(context.Background(), server.URL+"/whatever.sigstore.json") + if err == nil { + t.Fatalf("expected 429 to fail-closed, got nil") + } + if !errors.Is(err, ErrBundleFetch) { + t.Fatalf("expected ErrBundleFetch, got: %v", err) + } +} + +// Verifies FetchBundle fails when the body is empty. Empty response from a +// caching CDN would otherwise let a stripped bundle look like "downloaded OK". +func TestFetchBundle_EmptyBodyFails(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + _, err := FetchBundle(context.Background(), server.URL+"/whatever.sigstore.json") + if err == nil { + t.Fatalf("expected empty body to fail, got nil") + } + if !errors.Is(err, ErrBundleFetch) { + t.Fatalf("expected ErrBundleFetch, got: %v", err) + } +} + +// Verifies FetchTagCommitSHA parses a lightweight tag ref API response. +func TestFetchTagCommitSHA_Lightweight(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprintln(w, `{"object":{"sha":"1eb1ebb9841b1bcb8fc7dec3fa282568a1c31a4f","type":"commit"}}`) + })) + defer server.Close() + + original := githubAPIBase() + setGithubAPIBase(server.URL) + defer setGithubAPIBase(original) + + sha, err := FetchTagCommitSHA(context.Background(), "hatayama/unity-cli-loop", "dispatcher-v3.0.1-beta.12") + if err != nil { + t.Fatalf("expected happy tag lookup, got: %v", err) + } + if sha != "1eb1ebb9841b1bcb8fc7dec3fa282568a1c31a4f" { + t.Fatalf("unexpected sha: %s", sha) + } +} + +// Verifies FetchTagCommitSHA follows one indirection when the ref points at +// an annotated tag object rather than a commit. +func TestFetchTagCommitSHA_Annotated(t *testing.T) { + var hits int + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + hits++ + switch hits { + case 1: + _, _ = fmt.Fprintln(w, `{"object":{"sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","type":"tag"}}`) + case 2: + _, _ = fmt.Fprintln(w, `{"object":{"sha":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","type":"commit"}}`) + default: + t.Errorf("unexpected extra call %d", hits) + } + })) + defer server.Close() + + original := githubAPIBase() + setGithubAPIBase(server.URL) + defer setGithubAPIBase(original) + + sha, err := FetchTagCommitSHA(context.Background(), "hatayama/unity-cli-loop", "annotated-tag") + if err != nil { + t.Fatalf("expected annotated tag lookup to resolve, got: %v", err) + } + if sha != "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" { + t.Fatalf("unexpected sha: %s", sha) + } + if hits != 2 { + t.Fatalf("expected 2 API hits (ref then tag), got %d", hits) + } +} + +// Verifies FetchTagCommitSHA fails closed on network / server errors. +func TestFetchTagCommitSHA_ServerError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "boom", http.StatusInternalServerError) + })) + defer server.Close() + + original := githubAPIBase() + setGithubAPIBase(server.URL) + defer setGithubAPIBase(original) + + _, err := FetchTagCommitSHA(context.Background(), "hatayama/unity-cli-loop", "any") + if err == nil { + t.Fatalf("expected server error to fail-closed") + } + if !errors.Is(err, ErrTagRefFetch) { + t.Fatalf("expected ErrTagRefFetch, got: %v", err) + } +} diff --git a/cli/go.work.sum b/cli/go.work.sum index cc90caa9bb..f9299e67e9 100644 --- a/cli/go.work.sum +++ b/cli/go.work.sum @@ -1,3 +1,113 @@ +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= +github.com/go-openapi/analysis v0.25.2 h1:I0vy4n3alz+DHTiN1PRhCb7QZxkK6g5YmswZKv2TKuw= +github.com/go-openapi/analysis v0.25.2/go.mod h1:Uhs1t/2XR10EnwONYILGEzw8gcfGIG5Xk5K2AxnhqDo= +github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I= +github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w= +github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= +github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= +github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= +github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= +github.com/go-openapi/loads v0.24.0 h1:4LLorXRPTzIN9V6ngMUZbAscsBOUBk3Oa8cClu/bFrQ= +github.com/go-openapi/loads v0.24.0/go.mod h1:xQMgX+hw5xRAhGrcDXxeMw78IFqUpIzhleu3HqPhyF4= +github.com/go-openapi/runtime v0.32.4 h1:8ElGj/3goG0itt0nBPP6Cm57ehcYyuHoI3O20nxgvkw= +github.com/go-openapi/runtime v0.32.4/go.mod h1:Bz6keOZw1NX4T6f+m42OoT1MBPDt6Re13dbccHyGH/4= +github.com/go-openapi/runtime/server-middleware v0.30.0 h1:8rPoJ/xv7JL8BsovaqboKETlpWBArVh8n+0L/GyePog= +github.com/go-openapi/runtime/server-middleware v0.30.0/go.mod h1:OYNT/TxNvB/VK5oe4htM2jDTwlEXuejVJmu0DVZfAMs= +github.com/go-openapi/spec v0.22.6 h1:Tyy1pLaNCM8GBCFLoGYLonjJi6zykqyLCjXLc19ZPic= +github.com/go-openapi/spec v0.22.6/go.mod h1:HZvTHat+iH0PALQRWhrqIHtU/PEqxqd89fu0MxGlMeM= +github.com/go-openapi/strfmt v0.26.4 h1:yI6IAEfcWow459BD5UzFY430KUwXZwBHrYusPFkhWlc= +github.com/go-openapi/strfmt v0.26.4/go.mod h1:hNJi6nb5ETD6i7A1yRo03M9S6ZoTPPoWff1iUexmfUc= +github.com/go-openapi/swag v0.26.1 h1:l5sVEyVpwj+DDYeZyo7wQI/Ebn/mKYIyGB/pFwAfGoQ= +github.com/go-openapi/swag v0.26.1/go.mod h1:yNY38BbIVthxbkDtq1UHBCGasBqjakW3lCR6ANzdBEw= +github.com/go-openapi/swag/cmdutils v0.26.1 h1:f2iE1ijYaJ3nuu5PaEMx3zpEhzhZFgivCJObWEObLIQ= +github.com/go-openapi/swag/cmdutils v0.26.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.27.0 h1:EKOH4feXrvdo8DbSsXSAqRT8fz1epEnS5O2IfXUOzE8= +github.com/go-openapi/swag/conv v0.27.0/go.mod h1:pfiv0uKQTbaGApk8Zs/lZV3uSjmSpa2FO1y183YngN8= +github.com/go-openapi/swag/fileutils v0.26.1 h1:K1XCM2CGhfNsc6YDt6v7Q5+1e59rftYWdcu/isZhvFw= +github.com/go-openapi/swag/fileutils v0.26.1/go.mod h1:mYUgxQAKX4ShS3qvvySx+/9yrlUnDhjiD1CalaQl8lQ= +github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE= +github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc= +github.com/go-openapi/swag/jsonutils v0.26.1 h1:2hdBfFkHg+7Wrz2VsCbeyR6hzkRDs7AztnMR2u84yOY= +github.com/go-openapi/swag/jsonutils v0.26.1/go.mod h1:U+RMJH3wa+6BRiphuRtIyI8fW9HPFqFQ4sHk2oRx0UQ= +github.com/go-openapi/swag/loading v0.26.1 h1:E9K4wqXeROlhjFQ13K9zMz6ojFGXIggGe+ad1odrK9w= +github.com/go-openapi/swag/loading v0.26.1/go.mod h1:3qvRIlWzWdq1HvmldwmuJ2ohpcAryN6xVt2OTKd0/7E= +github.com/go-openapi/swag/mangling v0.26.1 h1:gpYI4WuPKFJJVjV5cDLGlDVJhFIxYjQc7yN5eEb4CqM= +github.com/go-openapi/swag/mangling v0.26.1/go.mod h1:POETDH01hqAdASXfw7ISEd9bCOE6xBHOt8NHmGZRmYM= +github.com/go-openapi/swag/netutils v0.26.1 h1:BNctoc39WTAUMxyAs355fExOPzMZtPbZ0ZZ1Am2FR5M= +github.com/go-openapi/swag/netutils v0.26.1/go.mod h1:y02vByhZhQPAVwOX+0KipXFZ/hUbk6G/Enhf5rGaOkQ= +github.com/go-openapi/swag/stringutils v0.26.1 h1:f88uYyTso7TnHrKM/bUBsQ5e2wKf37cpgo6pvbzd9yU= +github.com/go-openapi/swag/stringutils v0.26.1/go.mod h1:Sc6d3bU8fgk5AyZR8/8jEQ+Is/Ald+TD/IIggPN8UJk= +github.com/go-openapi/swag/typeutils v0.27.0 h1:aCf4MSGo8NLwZP8Q6t32DWLJSvl/WwNqgmEG+xJ6v2o= +github.com/go-openapi/swag/typeutils v0.27.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= +github.com/go-openapi/swag/yamlutils v0.26.1 h1:0TSLK+lXs9vfIhAWzBeI/lOzEnIoot6WTCO1aAeWFTk= +github.com/go-openapi/swag/yamlutils v0.26.1/go.mod h1:7W5b7PRX9MxwL7TjeG7H8HkyBGRsIDRObhyMWFgBI2M= +github.com/go-openapi/validate v0.26.0 h1:dxWzQ3F+vb1SajqUxHjwb5T4mTpSHmdrtv5Bi7+ZNhw= +github.com/go-openapi/validate v0.26.0/go.mod h1:b4o00uq7fJeJA+wWhVFCJpKTctzeFwzZImGGmHsl2JA= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/certificate-transparency-go v1.3.3 h1:hq/rSxztSkXN2tx/3jQqF6Xc0O565UQPdHrOWvZwybo= +github.com/google/certificate-transparency-go v1.3.3/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/in-toto/attestation v1.2.0 h1:aPRUZ3azbqD7yEBD5fP3TD8Dszf+YHo284SOcpahjQk= +github.com/in-toto/attestation v1.2.0/go.mod h1:r79G45gOmzPismgObLSL+rZTFxUgZLOQJI6LofTZgXk= +github.com/in-toto/in-toto-golang v0.11.0 h1:nfidMYBFx+E0lnmX5KUnN2Pdm8zdNKal1ayjJuzzRoA= +github.com/in-toto/in-toto-golang v0.11.0/go.mod h1:u3PjTnwFKjp5a1YCcw8SJg0G+tMeKfVoWsWeFMDCMtw= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= +github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/sigstore/rekor v1.5.3 h1:0Tyolw3zreRgm7PUW8dccFLXGBThi08278jI8EXNSr4= +github.com/sigstore/rekor v1.5.3/go.mod h1:h3GK5dDqCcWJJZUJwdpKGSSmEV2GEjPUjJy3WTjBwzA= +github.com/sigstore/rekor-tiles/v2 v2.3.0 h1:HhMgH61UP0t899V8Fjt7pz1YdgOBptbaQdnCF+79cdc= +github.com/sigstore/rekor-tiles/v2 v2.3.0/go.mod h1:DEFiKSyQ4nF75QRVNdOPaIH3cmvMkO2B6xDZjNYngPc= +github.com/sigstore/timestamp-authority/v2 v2.1.2 h1:7DDhnknLL4w8VwomyvW2W8qblOS9LDR8oihna+jc7Ls= +github.com/sigstore/timestamp-authority/v2 v2.1.2/go.mod h1:o6rAVZceFyejClIj/uStRNIemP16bVMZtbMmhk6pr0U= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= +github.com/transparency-dev/formats v0.1.1 h1:4bVHJc+KdBgpA1OJD1yjI+g0i5Z1graCppTMH8lWKJI= +github.com/transparency-dev/formats v0.1.1/go.mod h1:qtZ8goRuJ8FTBG9c9+Bj0rn2rUG7eG/AUTkr+Aw3jFw= +github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= +github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 h1:Au6te5hbKUV8pIYWHqOUZ1pva5qK/rwbIhoXEUB9Lu8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= +google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= diff --git a/cli/release-automation/internal/architecture/architecture_test.go b/cli/release-automation/internal/architecture/architecture_test.go index 728bcbfff4..1c7de0269b 100644 --- a/cli/release-automation/internal/architecture/architecture_test.go +++ b/cli/release-automation/internal/architecture/architecture_test.go @@ -253,7 +253,7 @@ func TestInternalBoundariesPerModule(t *testing.T) { { moduleDir: filepath.Join(repositoryRoot, contract.Layout.Modules.Dispatcher), modulePath: dispatcherModulePath, - allowed: []string{"dispatcher", "install", "nativepath", "uninstall", "update"}, + allowed: []string{"attestation", "dispatcher", "install", "nativepath", "uninstall", "update"}, }, { moduleDir: filepath.Join(repositoryRoot, contract.Layout.Modules.ReleaseAutomation),