Merge pull request #10750 from malancas/refactor-sigstore-verifier-logic

Refactor Sigstore verifier logic
This commit is contained in:
Meredith Lancaster 2025-04-15 09:05:47 -06:00 committed by GitHub
commit 6dbb3fe541
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 148 additions and 70 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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"

View file

@ -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 {

View file

@ -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 {