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 4dcd3c82c..74251cffb 100644 --- a/pkg/cmd/attestation/verification/sigstore.go +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -49,6 +49,7 @@ type LiveSigstoreVerifier struct { NoPublicGood bool PublicGoodVerifier *verify.SignedEntityVerifier GitHubVerifier *verify.SignedEntityVerifier + CustomVerifiers map[string]*verify.SignedEntityVerifier // If tenancy mode is not used, trust domain is empty TrustDomain string TUFMetadataDir o.Option[string] @@ -59,14 +60,111 @@ 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{ +func NewLiveSigstoreVerifier(config SigstoreConfig) (*LiveSigstoreVerifier, error) { + liveVerifier := &LiveSigstoreVerifier{ TrustedRoot: config.TrustedRoot, Logger: config.Logger, NoPublicGood: config.NoPublicGood, TrustDomain: config.TrustDomain, TUFMetadataDir: config.TUFMetadataDir, } + // 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.CustomVerifiers = customVerifiers + return liveVerifier, nil + } + + return liveVerifier, nil +} + +func createCustomVerifiers(trustedRoot string, noPublicGood bool) (map[string]*verify.SignedEntityVerifier, error) { + verifiers := make(map[string]*verify.SignedEntityVerifier) + + customTrustRoots, err := os.ReadFile(trustedRoot) + if err != nil { + return nil, fmt.Errorf("unable to read file %s: %v", trustedRoot, err) + } + + reader := bufio.NewReader(bytes.NewReader(customTrustRoots)) + var line []byte + var readError error + line, readError = reader.ReadBytes('\n') + for readError == nil { + // Load each trusted root + trustedRoot, err := root.NewTrustedRootFromJSON(line) + if err != nil { + return nil, fmt.Errorf("failed to create custom verifier: %v", err) + } + + // Compare bundle leafCert issuer with trusted root cert authority + certAuthorities := trustedRoot.FulcioCertificateAuthorities() + for _, certAuthority := range certAuthorities { + fulcioCertAuthority, ok := certAuthority.(*root.FulcioCertificateAuthority) + if !ok { + return nil, fmt.Errorf("trusted root cert authority is not a FulcioCertificateAuthority") + } + lowestCert, err := getLowestCertInChain(fulcioCertAuthority) + if err != nil { + return nil, err + } + + // 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. + // + // Note that we are *only* inferring the policy with the + // issuer. We *must* use the trusted root provided. + switch issuer { + case PublicGoodIssuerOrg: + if noPublicGood { + return nil, fmt.Errorf("detected public good instance but requested verification without public good instance") + } + 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: + 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 + custom, err := newCustomVerifier(trustedRoot) + if err != nil { + return nil, err + } + verifiers[issuer] = custom + } + } + line, readError = reader.ReadBytes('\n') + } + return verifiers, nil } func getBundleIssuer(b *bundle.Bundle) (string, error) { @@ -90,7 +188,7 @@ func getBundleIssuer(b *bundle.Bundle) (string, error) { 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.TrustedRoot == "" { + if v.CustomVerifiers == nil { switch issuer { case PublicGoodIssuerOrg: if v.NoPublicGood { @@ -118,60 +216,12 @@ func (v *LiveSigstoreVerifier) chooseVerifier(issuer string) (*verify.SignedEnti } } - customTrustRoots, err := os.ReadFile(v.TrustedRoot) - if err != nil { - return nil, fmt.Errorf("unable to read file %s: %v", v.TrustedRoot, err) + custom, ok := v.CustomVerifiers[issuer] + if !ok { + return nil, fmt.Errorf("no custom verifier found for issuer \"%s\"", issuer) + //return nil, fmt.Errorf("unable to use provided trusted roots") } - - reader := bufio.NewReader(bytes.NewReader(customTrustRoots)) - var line []byte - var readError error - line, readError = reader.ReadBytes('\n') - for readError == nil { - // Load each trusted root - trustedRoot, err := root.NewTrustedRootFromJSON(line) - if err != nil { - return nil, fmt.Errorf("failed to create custom verifier: %v", err) - } - - // Compare bundle leafCert issuer with trusted root cert authority - certAuthorities := trustedRoot.FulcioCertificateAuthorities() - for _, certAuthority := range certAuthorities { - fulcioCertAuthority, ok := certAuthority.(*root.FulcioCertificateAuthority) - if !ok { - return nil, fmt.Errorf("trusted root cert authority is not a FulcioCertificateAuthority") - } - lowestCert, err := getLowestCertInChain(fulcioCertAuthority) - if err != nil { - 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 { - continue - } - - // Determine what policy to use with this trusted root. - // - // Note that we are *only* inferring the policy with the - // issuer. We *must* use the trusted root provided. - switch issuer { - case PublicGoodIssuerOrg: - if v.NoPublicGood { - return nil, fmt.Errorf("detected public good instance but requested verification without public good instance") - } - return newPublicGoodVerifierWithTrustedRoot(trustedRoot) - case GitHubIssuerOrg: - return newGitHubVerifierWithTrustedRoot(trustedRoot) - default: - // Make best guess at reasonable policy - return newCustomVerifier(trustedRoot) - } - } - line, readError = reader.ReadBytes('\n') - } - - return nil, fmt.Errorf("unable to use provided trusted roots") + return custom, nil } func getLowestCertInChain(ca *root.FulcioCertificateAuthority) (*x509.Certificate, error) { 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 {