diff --git a/pkg/cmd/attestation/inspect/inspect.go b/pkg/cmd/attestation/inspect/inspect.go index 6fbddd6da..b571eee01 100644 --- a/pkg/cmd/attestation/inspect/inspect.go +++ b/pkg/cmd/attestation/inspect/inspect.go @@ -105,7 +105,11 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command config.TrustDomain = td } - opts.SigstoreVerifier = verification.NewLiveSigstoreVerifier(config) + sgVerifier, err := verification.NewLiveSigstoreVerifier(config) + if err != nil { + return fmt.Errorf("failed to create Sigstore verifier: %w", err) + } + opts.SigstoreVerifier = sgVerifier if runF != nil { return runF(opts) diff --git a/pkg/cmd/attestation/verification/sigstore.go b/pkg/cmd/attestation/verification/sigstore.go index 6dd31dac0..190ea5c0f 100644 --- a/pkg/cmd/attestation/verification/sigstore.go +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -44,12 +44,11 @@ type SigstoreVerifier interface { } type LiveSigstoreVerifier struct { - TrustedRoot string Logger *io.Handler NoPublicGood bool - // If tenancy mode is not used, trust domain is empty - TrustDomain string - TUFMetadataDir o.Option[string] + PublicGood *verify.SignedEntityVerifier + GitHub *verify.SignedEntityVerifier + Custom map[string]*verify.SignedEntityVerifier } var ErrNoAttestationsVerified = errors.New("no attestations were verified") @@ -57,56 +56,43 @@ var ErrNoAttestationsVerified = errors.New("no attestations were verified") // NewLiveSigstoreVerifier creates a new LiveSigstoreVerifier struct // that is used to verify artifacts and attestations against the // Public Good, GitHub, or a custom trusted root. -func NewLiveSigstoreVerifier(config SigstoreConfig) *LiveSigstoreVerifier { - return &LiveSigstoreVerifier{ - TrustedRoot: config.TrustedRoot, - Logger: config.Logger, - NoPublicGood: config.NoPublicGood, - TrustDomain: config.TrustDomain, - TUFMetadataDir: config.TUFMetadataDir, +func NewLiveSigstoreVerifier(config SigstoreConfig) (*LiveSigstoreVerifier, error) { + liveVerifier := &LiveSigstoreVerifier{ + Logger: config.Logger, + NoPublicGood: config.NoPublicGood, } -} - -func getBundleIssuer(b *bundle.Bundle) (string, error) { - if !b.MinVersion("0.2") { - return "", fmt.Errorf("unsupported bundle version: %s", b.MediaType) - } - verifyContent, err := b.VerificationContent() - if err != nil { - return "", fmt.Errorf("failed to get bundle verification content: %v", err) - } - leafCert := verifyContent.Certificate() - if leafCert == nil { - return "", fmt.Errorf("leaf cert not found") - } - if len(leafCert.Issuer.Organization) != 1 { - return "", fmt.Errorf("expected the leaf certificate issuer to only have one organization") - } - return leafCert.Issuer.Organization[0], nil -} - -func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.SignedEntityVerifier, error) { - // if no custom trusted root is set, attempt to create a Public Good or - // GitHub Sigstore verifier - if v.TrustedRoot == "" { - switch issuer { - case PublicGoodIssuerOrg: - if v.NoPublicGood { - return nil, fmt.Errorf("detected public good instance but requested verification without public good instance") - } - return newPublicGoodVerifier(v.TUFMetadataDir) - case GitHubIssuerOrg: - return newGitHubVerifier(v.TrustDomain, v.TUFMetadataDir) - default: - return nil, fmt.Errorf("leaf certificate issuer is not recognized") + // if a custom trusted root is set, configure custom verifiers + if config.TrustedRoot != "" { + customVerifiers, err := createCustomVerifiers(config.TrustedRoot, config.NoPublicGood) + if err != nil { + return nil, err } + liveVerifier.Custom = customVerifiers + return liveVerifier, nil } - - customTrustRoots, err := os.ReadFile(v.TrustedRoot) + if !config.NoPublicGood { + publicGoodVerifier, err := newPublicGoodVerifier(config.TUFMetadataDir) + if err != nil { + return nil, err + } + liveVerifier.PublicGood = publicGoodVerifier + } + github, err := newGitHubVerifier(config.TrustDomain, config.TUFMetadataDir) if err != nil { - return nil, fmt.Errorf("unable to read file %s: %v", v.TrustedRoot, err) + return nil, err + } + liveVerifier.GitHub = github + + return liveVerifier, nil +} + +func createCustomVerifiers(trustedRoot string, noPublicGood bool) (map[string]*verify.SignedEntityVerifier, error) { + customTrustRoots, err := os.ReadFile(trustedRoot) + if err != nil { + return nil, fmt.Errorf("unable to read file %s: %v", trustedRoot, err) } + verifiers := make(map[string]*verify.SignedEntityVerifier) reader := bufio.NewReader(bytes.NewReader(customTrustRoots)) var line []byte var readError error @@ -130,10 +116,11 @@ func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.SignedEnti return nil, err } - // if the custom trusted root issuer is not set or doesn't match the given issuer, skip it - if len(lowestCert.Issuer.Organization) == 0 || lowestCert.Issuer.Organization[0] != issuer { + // if the custom trusted root issuer is not set, skip it + if len(lowestCert.Issuer.Organization) == 0 { continue } + issuer := lowestCert.Issuer.Organization[0] // Determine what policy to use with this trusted root. // @@ -141,21 +128,88 @@ func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.SignedEnti // issuer. We *must* use the trusted root provided. switch issuer { case PublicGoodIssuerOrg: - if v.NoPublicGood { + if noPublicGood { return nil, fmt.Errorf("detected public good instance but requested verification without public good instance") } - return newPublicGoodVerifierWithTrustedRoot(trustedRoot) + if _, ok := verifiers[PublicGoodIssuerOrg]; ok { + // we have already created a public good verifier with this custom trusted root + // so we skip it + continue + } + publicGood, err := newPublicGoodVerifierWithTrustedRoot(trustedRoot) + if err != nil { + return nil, err + } + verifiers[PublicGoodIssuerOrg] = publicGood case GitHubIssuerOrg: - return newGitHubVerifierWithTrustedRoot(trustedRoot) + if _, ok := verifiers[GitHubIssuerOrg]; ok { + // we have already created a github verifier with this custom trusted root + // so we skip it + continue + } + github, err := newGitHubVerifierWithTrustedRoot(trustedRoot) + if err != nil { + return nil, err + } + verifiers[GitHubIssuerOrg] = github default: + if _, ok := verifiers[issuer]; ok { + // we have already created a custom verifier with this custom trusted root + // so we skip it + continue + } // Make best guess at reasonable policy - return newCustomVerifier(trustedRoot) + custom, err := newCustomVerifier(trustedRoot) + if err != nil { + return nil, err + } + verifiers[issuer] = custom } } line, readError = reader.ReadBytes('\n') } + return verifiers, nil +} - return nil, fmt.Errorf("unable to use provided trusted roots") +func getBundleIssuer(b *bundle.Bundle) (string, error) { + if !b.MinVersion("0.2") { + return "", fmt.Errorf("unsupported bundle version: %s", b.MediaType) + } + verifyContent, err := b.VerificationContent() + if err != nil { + return "", fmt.Errorf("failed to get bundle verification content: %v", err) + } + leafCert := verifyContent.Certificate() + if leafCert == nil { + return "", fmt.Errorf("leaf cert not found") + } + if len(leafCert.Issuer.Organization) != 1 { + return "", fmt.Errorf("expected the leaf certificate issuer to only have one organization") + } + return leafCert.Issuer.Organization[0], nil +} + +func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.SignedEntityVerifier, error) { + // if no custom trusted root is set, return either the Public Good or GitHub verifier + // If the chosen verifier has not yet been created, create it as a LiveSigstoreVerifier field for use in future calls + if v.Custom != nil { + custom, ok := v.Custom[issuer] + if !ok { + return nil, fmt.Errorf("no custom verifier found for issuer \"%s\"", issuer) + } + return custom, nil + } + switch issuer { + case PublicGoodIssuerOrg: + if v.NoPublicGood { + return nil, fmt.Errorf("detected public good instance but requested verification without public good instance") + } + return v.PublicGood, nil + case GitHubIssuerOrg: + return v.GitHub, nil + default: + return nil, fmt.Errorf("leaf certificate issuer is not recognized") + } } func getLowestCertInChain(ca *root.FulcioCertificateAuthority) (*x509.Certificate, error) { @@ -177,7 +231,7 @@ func (v *LiveSigstoreVerifier) verify(attestation *api.Attestation, policy verif // determine which verifier should attempt verification against the bundle verifier, err := v.chooseVerifier(issuer) if err != nil { - return nil, fmt.Errorf("failed to find recognized issuer from bundle content: %v", err) + return nil, fmt.Errorf("failed to choose verifier based on provided bundle issuer: %v", err) } v.Logger.VerbosePrintf("Attempting verification against issuer \"%s\"\n", issuer) diff --git a/pkg/cmd/attestation/verification/sigstore_integration_test.go b/pkg/cmd/attestation/verification/sigstore_integration_test.go index 987fb9caa..2a2d3beea 100644 --- a/pkg/cmd/attestation/verification/sigstore_integration_test.go +++ b/pkg/cmd/attestation/verification/sigstore_integration_test.go @@ -50,10 +50,11 @@ func TestLiveSigstoreVerifier(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - verifier := NewLiveSigstoreVerifier(SigstoreConfig{ + verifier, err := NewLiveSigstoreVerifier(SigstoreConfig{ Logger: io.NewTestHandler(), TUFMetadataDir: o.Some(t.TempDir()), }) + require.NoError(t, err) results, err := verifier.Verify(tc.attestations, publicGoodPolicy(t)) @@ -69,10 +70,11 @@ func TestLiveSigstoreVerifier(t *testing.T) { } t.Run("with 2/3 verified attestations", func(t *testing.T) { - verifier := NewLiveSigstoreVerifier(SigstoreConfig{ + verifier, err := NewLiveSigstoreVerifier(SigstoreConfig{ Logger: io.NewTestHandler(), TUFMetadataDir: o.Some(t.TempDir()), }) + require.NoError(t, err) invalidBundle := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0-bundle-v0.1.json") attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") @@ -86,10 +88,11 @@ func TestLiveSigstoreVerifier(t *testing.T) { }) t.Run("fail with 0/2 verified attestations", func(t *testing.T) { - verifier := NewLiveSigstoreVerifier(SigstoreConfig{ + verifier, err := NewLiveSigstoreVerifier(SigstoreConfig{ Logger: io.NewTestHandler(), TUFMetadataDir: o.Some(t.TempDir()), }) + require.NoError(t, err) invalidBundle := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0-bundle-v0.1.json") attestations := getAttestationsFor(t, "../test/data/sigstoreBundle-invalid-signature.json") @@ -110,10 +113,11 @@ func TestLiveSigstoreVerifier(t *testing.T) { attestations := getAttestationsFor(t, "../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle.jsonl") - verifier := NewLiveSigstoreVerifier(SigstoreConfig{ + verifier, err := NewLiveSigstoreVerifier(SigstoreConfig{ Logger: io.NewTestHandler(), TUFMetadataDir: o.Some(t.TempDir()), }) + require.NoError(t, err) results, err := verifier.Verify(attestations, githubPolicy) require.Len(t, results, 1) @@ -123,11 +127,12 @@ func TestLiveSigstoreVerifier(t *testing.T) { t.Run("with custom trusted root", func(t *testing.T) { attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") - verifier := NewLiveSigstoreVerifier(SigstoreConfig{ + verifier, err := NewLiveSigstoreVerifier(SigstoreConfig{ Logger: io.NewTestHandler(), TrustedRoot: test.NormalizeRelativePath("../test/data/trusted_root.json"), TUFMetadataDir: o.Some(t.TempDir()), }) + require.NoError(t, err) results, err := verifier.Verify(attestations, publicGoodPolicy(t)) require.Len(t, results, 2) diff --git a/pkg/cmd/attestation/verify/attestation_integration_test.go b/pkg/cmd/attestation/verify/attestation_integration_test.go index 9ff174141..73452c425 100644 --- a/pkg/cmd/attestation/verify/attestation_integration_test.go +++ b/pkg/cmd/attestation/verify/attestation_integration_test.go @@ -25,10 +25,11 @@ func getAttestationsFor(t *testing.T, bundlePath string) []*api.Attestation { } func TestVerifyAttestations(t *testing.T) { - sgVerifier := verification.NewLiveSigstoreVerifier(verification.SigstoreConfig{ + sgVerifier, err := verification.NewLiveSigstoreVerifier(verification.SigstoreConfig{ Logger: io.NewTestHandler(), TUFMetadataDir: o.Some(t.TempDir()), }) + require.NoError(t, err) certSummary := certificate.Summary{} certSummary.SourceRepositoryOwnerURI = "https://github.com/sigstore" diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 3affdfabb..b3bad519a 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -211,7 +211,11 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command return runF(opts) } - opts.SigstoreVerifier = verification.NewLiveSigstoreVerifier(config) + sigstoreVerifier, err := verification.NewLiveSigstoreVerifier(config) + if err != nil { + return fmt.Errorf("error creating Sigstore verifier: %w", err) + } + opts.SigstoreVerifier = sigstoreVerifier opts.Config = f.Config if err := runVerify(opts); err != nil { diff --git a/pkg/cmd/attestation/verify/verify_integration_test.go b/pkg/cmd/attestation/verify/verify_integration_test.go index 09479995c..92864f78e 100644 --- a/pkg/cmd/attestation/verify/verify_integration_test.go +++ b/pkg/cmd/attestation/verify/verify_integration_test.go @@ -33,6 +33,8 @@ func TestVerifyIntegration(t *testing.T) { host, _ := auth.DefaultHost() + sigstoreVerifier, err := verification.NewLiveSigstoreVerifier(sigstoreConfig) + require.NoError(t, err) publicGoodOpts := Options{ APIClient: api.NewLiveClient(hc, host, logger), ArtifactPath: artifactPath, @@ -44,7 +46,7 @@ func TestVerifyIntegration(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "^https://github.com/sigstore/", - SigstoreVerifier: verification.NewLiveSigstoreVerifier(sigstoreConfig), + SigstoreVerifier: sigstoreVerifier, } t.Run("with valid owner", func(t *testing.T) { @@ -106,6 +108,8 @@ func TestVerifyIntegration(t *testing.T) { }) t.Run("with bundle from OCI registry", func(t *testing.T) { + sigstoreVerifier, err := verification.NewLiveSigstoreVerifier(sigstoreConfig) + require.NoError(t, err) opts := Options{ APIClient: api.NewLiveClient(hc, host, logger), ArtifactPath: "oci://ghcr.io/github/artifact-attestations-helm-charts/policy-controller:v0.10.0-github9", @@ -117,10 +121,10 @@ func TestVerifyIntegration(t *testing.T) { Owner: "github", PredicateType: verification.SLSAPredicateV1, SANRegex: "^https://github.com/github/", - SigstoreVerifier: verification.NewLiveSigstoreVerifier(sigstoreConfig), + SigstoreVerifier: sigstoreVerifier, } - err := runVerify(&opts) + err = runVerify(&opts) require.NoError(t, err) }) } @@ -145,6 +149,8 @@ func TestVerifyIntegrationCustomIssuer(t *testing.T) { host, _ := auth.DefaultHost() + sigstoreVerifier, err := verification.NewLiveSigstoreVerifier(sigstoreConfig) + require.NoError(t, err) baseOpts := Options{ APIClient: api.NewLiveClient(hc, host, logger), ArtifactPath: artifactPath, @@ -154,7 +160,7 @@ func TestVerifyIntegrationCustomIssuer(t *testing.T) { OCIClient: oci.NewLiveClient(), OIDCIssuer: "https://token.actions.githubusercontent.com/hammer-time", PredicateType: verification.SLSAPredicateV1, - SigstoreVerifier: verification.NewLiveSigstoreVerifier(sigstoreConfig), + SigstoreVerifier: sigstoreVerifier, } t.Run("with owner and valid workflow SAN", func(t *testing.T) { @@ -216,6 +222,8 @@ func TestVerifyIntegrationReusableWorkflow(t *testing.T) { host, _ := auth.DefaultHost() + sigstoreVerifier, err := verification.NewLiveSigstoreVerifier(sigstoreConfig) + require.NoError(t, err) baseOpts := Options{ APIClient: api.NewLiveClient(hc, host, logger), ArtifactPath: artifactPath, @@ -225,7 +233,7 @@ func TestVerifyIntegrationReusableWorkflow(t *testing.T) { OCIClient: oci.NewLiveClient(), OIDCIssuer: verification.GitHubOIDCIssuer, PredicateType: verification.SLSAPredicateV1, - SigstoreVerifier: verification.NewLiveSigstoreVerifier(sigstoreConfig), + SigstoreVerifier: sigstoreVerifier, } t.Run("with owner and valid reusable workflow SAN", func(t *testing.T) { @@ -306,6 +314,8 @@ func TestVerifyIntegrationReusableWorkflowSignerWorkflow(t *testing.T) { host, _ := auth.DefaultHost() + sigstoreVerifier, err := verification.NewLiveSigstoreVerifier(sigstoreConfig) + require.NoError(t, err) baseOpts := Options{ APIClient: api.NewLiveClient(hc, host, logger), ArtifactPath: artifactPath, @@ -318,7 +328,7 @@ func TestVerifyIntegrationReusableWorkflowSignerWorkflow(t *testing.T) { Owner: "malancas", PredicateType: verification.SLSAPredicateV1, Repo: "malancas/attest-demo", - SigstoreVerifier: verification.NewLiveSigstoreVerifier(sigstoreConfig), + SigstoreVerifier: sigstoreVerifier, } type testcase struct {