Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to specify certificate identity via a regular expression #236

Merged
merged 2 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/conformance/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ func main() {

identityPolicies := []verify.PolicyOption{}
if *certOIDC != "" || *certSAN != "" {
certID, err := verify.NewShortCertificateIdentity(*certOIDC, *certSAN, "")
certID, err := verify.NewShortCertificateIdentity(*certOIDC, "", *certSAN, "")
if err != nil {
fmt.Println(err)
os.Exit(1)
Expand Down Expand Up @@ -333,7 +333,7 @@ func main() {
// Configure verification options
identityPolicies := []verify.PolicyOption{}
if *certOIDC != "" || *certSAN != "" {
certID, err := verify.NewShortCertificateIdentity(*certOIDC, *certSAN, "")
certID, err := verify.NewShortCertificateIdentity(*certOIDC, "", *certSAN, "")
if err != nil {
fmt.Println(err)
os.Exit(1)
Expand Down
2 changes: 1 addition & 1 deletion cmd/sigstore-go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func run() error {
verifierConfig = append(verifierConfig, verify.WithOnlineVerification())
}

certID, err := verify.NewShortCertificateIdentity(*expectedOIDIssuer, *expectedSAN, *expectedSANRegex)
certID, err := verify.NewShortCertificateIdentity(*expectedOIDIssuer, "", *expectedSAN, *expectedSANRegex)
steiza marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions docs/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Then, we need to prepare the expected artifact digest. Note that this option has
In this case, we also need to prepare the expected certificate identity. Note that this option has an alternative option `WithoutIdentitiesUnsafe`. This is a failsafe to ensure that the caller is aware that simply verifying the bundle is not enough, you must also verify the contents of the bundle against a specific identity. If your bundle was signed with a key, and thus does not have a certificate identity, a better choice is to use the `WithKey` option.

```go
certID, err := verify.NewShortCertificateIdentity("https://token.actions.githubusercontent.com", "", "^https://github.com/sigstore/sigstore-js/")
certID, err := verify.NewShortCertificateIdentity("https://token.actions.githubusercontent.com", "", "", "^https://github.com/sigstore/sigstore-js/")
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -221,7 +221,7 @@ func main() {
panic(err)
}

certID, err := verify.NewShortCertificateIdentity("https://token.actions.githubusercontent.com", "", "^https://github.com/sigstore/sigstore-js/")
certID, err := verify.NewShortCertificateIdentity("https://token.actions.githubusercontent.com", "", "", "^https://github.com/sigstore/sigstore-js/")
if err != nil {
panic(err)
}
Expand Down
2 changes: 1 addition & 1 deletion examples/oci-image-verification/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func run() error {
}

if *expectedOIDIssuer != "" || *expectedSAN != "" || *expectedSANRegex != "" {
steiza marked this conversation as resolved.
Show resolved Hide resolved
certID, err := verify.NewShortCertificateIdentity(*expectedOIDIssuer, *expectedSAN, *expectedSANRegex)
certID, err := verify.NewShortCertificateIdentity(*expectedOIDIssuer, "", *expectedSAN, *expectedSANRegex)
if err != nil {
return err
}
Expand Down
32 changes: 26 additions & 6 deletions pkg/verify/certificate_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type SubjectAlternativeNameMatcher struct {

type CertificateIdentity struct {
SubjectAlternativeName SubjectAlternativeNameMatcher `json:"subjectAlternativeName"`
IssuerRegexp *regexp.Regexp
certificate.Extensions
}

Expand Down Expand Up @@ -116,15 +117,19 @@ func (s SubjectAlternativeNameMatcher) Verify(actualCert certificate.Summary) er
return nil
}

func NewCertificateIdentity(sanMatcher SubjectAlternativeNameMatcher, extensions certificate.Extensions) (CertificateIdentity, error) {
func NewCertificateIdentity(sanMatcher SubjectAlternativeNameMatcher, issuerRegexp *regexp.Regexp, extensions certificate.Extensions) (CertificateIdentity, error) {
steiza marked this conversation as resolved.
Show resolved Hide resolved
if sanMatcher.SubjectAlternativeName == "" && sanMatcher.Regexp.String() == "" {
return CertificateIdentity{}, errors.New("when verifying a certificate identity, there must be subject alternative name criteria")
}

certID := CertificateIdentity{SubjectAlternativeName: sanMatcher, Extensions: extensions}
certID := CertificateIdentity{
SubjectAlternativeName: sanMatcher,
IssuerRegexp: issuerRegexp,
Extensions: extensions,
}

if certID.Issuer == "" {
return CertificateIdentity{}, errors.New("when verifying a certificate identity, the Issuer field can't be empty")
if certID.Issuer == "" && issuerRegexp == nil {
return CertificateIdentity{}, errors.New("when verifying a certificate identity, must specify Issuer criteria")
}

return certID, nil
Expand All @@ -133,13 +138,23 @@ func NewCertificateIdentity(sanMatcher SubjectAlternativeNameMatcher, extensions
// NewShortCertificateIdentity provides a more convenient way of initializing
// a CertificiateIdentity with a SAN and the Issuer OID extension. If you need
// to check more OID extensions, use NewCertificateIdentity instead.
func NewShortCertificateIdentity(issuer, sanValue, sanRegex string) (CertificateIdentity, error) {
func NewShortCertificateIdentity(issuer, issuerRegex, sanValue, sanRegex string) (CertificateIdentity, error) {
var r *regexp.Regexp
var err error

if issuerRegex != "" {
r, err = regexp.Compile(issuerRegex)
if err != nil {
return CertificateIdentity{}, err
}
}

sanMatcher, err := NewSANMatcher(sanValue, sanRegex)
if err != nil {
return CertificateIdentity{}, err
}

return NewCertificateIdentity(sanMatcher, certificate.Extensions{Issuer: issuer})
return NewCertificateIdentity(sanMatcher, r, certificate.Extensions{Issuer: issuer})
steiza marked this conversation as resolved.
Show resolved Hide resolved
steiza marked this conversation as resolved.
Show resolved Hide resolved
}

// Verify verifies the CertificateIdentities, and if ANY of them match the cert,
Expand All @@ -164,5 +179,10 @@ func (c CertificateIdentity) Verify(actualCert certificate.Summary) error {
if err = c.SubjectAlternativeName.Verify(actualCert); err != nil {
return err
}
if c.IssuerRegexp != nil {
if !c.IssuerRegexp.MatchString(actualCert.Extensions.Issuer) {
return fmt.Errorf("expected issuer value to match regex \"%s\", got \"%s\"", c.IssuerRegexp.String(), actualCert.Extensions.Issuer)
}
}
return certificate.CompareExtensions(c.Extensions, actualCert.Extensions)
}
47 changes: 35 additions & 12 deletions pkg/verify/certificate_identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package verify

import (
"regexp"
"testing"

"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
Expand All @@ -23,6 +24,7 @@ import (

const (
ActionsIssuerValue = "https://token.actions.githubusercontent.com"
ActionsIssuerRegex = "githubusercontent.com$"
SigstoreSanValue = "https://github.com/sigstore/sigstore-js/.github/workflows/release.yml@refs/heads/main"
SigstoreSanRegex = "^https://github.com/sigstore/sigstore-js/"
)
Expand Down Expand Up @@ -57,32 +59,39 @@ func TestCertificateIdentityVerify(t *testing.T) {
}

// First, let's test happy paths:
issuerOnlyID, _ := certIDForTesting("", "", ActionsIssuerValue, "")
issuerOnlyID, _ := certIDForTesting("", "", ActionsIssuerValue, "", "")
assert.NoError(t, issuerOnlyID.Verify(actualCert))

sanValueOnly, _ := certIDForTesting(SigstoreSanValue, "", "", "")
issuerOnlyRegex, _ := certIDForTesting("", "", "", ActionsIssuerRegex, "")
assert.NoError(t, issuerOnlyRegex.Verify(actualCert))

sanValueOnly, _ := certIDForTesting(SigstoreSanValue, "", "", "", "")
assert.NoError(t, sanValueOnly.Verify(actualCert))

sanRegexOnly, _ := certIDForTesting("", SigstoreSanRegex, "", "")
sanRegexOnly, _ := certIDForTesting("", SigstoreSanRegex, "", "", "")
assert.NoError(t, sanRegexOnly.Verify(actualCert))

// multiple values can be specified
sanRegexAndIssuer, _ := certIDForTesting("", SigstoreSanRegex, ActionsIssuerValue, "github-hosted")
sanRegexAndIssuer, _ := certIDForTesting("", SigstoreSanRegex, ActionsIssuerValue, "", "github-hosted")
assert.NoError(t, sanRegexAndIssuer.Verify(actualCert))

// unhappy paths:
// wrong issuer
sanRegexAndWrongIssuer, _ := certIDForTesting("", SigstoreSanRegex, "https://token.actions.example.com", "")
sanRegexAndWrongIssuer, _ := certIDForTesting("", SigstoreSanRegex, "https://token.actions.example.com", "", "")
errCompareExtensions := &certificate.ErrCompareExtensions{}
assert.ErrorAs(t, sanRegexAndWrongIssuer.Verify(actualCert), &errCompareExtensions)
assert.Equal(t, "expected Issuer to be \"https://token.actions.example.com\", got \"https://token.actions.githubusercontent.com\"", errCompareExtensions.Error())

// bad san regex
badRegex, _ := certIDForTesting("", "^badregex.*", "", "")
badRegex, _ := certIDForTesting("", "^badregex.*", "", "", "")
errSANValueRegexMismatch := &ErrSANValueRegexMismatch{}
assert.ErrorAs(t, badRegex.Verify(actualCert), &errSANValueRegexMismatch)
assert.Equal(t, "expected SAN value to match regex \"^badregex.*\", got \"https://github.com/sigstore/sigstore-js/.github/workflows/release.yml@refs/heads/main\"", errSANValueRegexMismatch.Error())

// bad issuer regex
badIssuerRegex, _ := certIDForTesting("", "", "", "^badregex$", "")
assert.Error(t, badIssuerRegex.Verify(actualCert))

// if we have an array of certIDs, only one needs to match
ci, err := CertificateIdentities{sanRegexAndWrongIssuer, sanRegexAndIssuer}.Verify(actualCert)
assert.NoError(t, err)
Expand All @@ -105,24 +114,38 @@ func TestCertificateIdentityVerify(t *testing.T) {
}

func TestThatCertIDsAreFullySpecified(t *testing.T) {
_, err := NewShortCertificateIdentity("", "", "")
_, err := NewShortCertificateIdentity("", "", "", "")
assert.Error(t, err)

_, err = NewShortCertificateIdentity("foobar", "", "", "")
assert.Error(t, err)

_, err = NewShortCertificateIdentity("foobar", "", "")
_, err = NewShortCertificateIdentity("", ActionsIssuerRegex, "", "")
assert.Error(t, err)

_, err = NewShortCertificateIdentity("", "", SigstoreSanRegex)
_, err = NewShortCertificateIdentity("", "", "", SigstoreSanRegex)
assert.Error(t, err)

_, err = NewShortCertificateIdentity("foobar", "", SigstoreSanRegex)
_, err = NewShortCertificateIdentity("foobar", "", "", SigstoreSanRegex)
assert.Nil(t, err)

_, err = NewShortCertificateIdentity("", ActionsIssuerRegex, "", SigstoreSanRegex)
assert.Nil(t, err)
}

func certIDForTesting(sanValue, sanRegex, issuer, runnerEnv string) (CertificateIdentity, error) {
func certIDForTesting(sanValue, sanRegex, issuer, issuerRegex, runnerEnv string) (CertificateIdentity, error) {
san, err := NewSANMatcher(sanValue, sanRegex)
if err != nil {
return CertificateIdentity{}, err
}

return CertificateIdentity{SubjectAlternativeName: san, Extensions: certificate.Extensions{Issuer: issuer, RunnerEnvironment: runnerEnv}}, nil
var r *regexp.Regexp
if issuerRegex != "" {
r, err = regexp.Compile(issuerRegex)
if err != nil {
return CertificateIdentity{}, err
}
}

return CertificateIdentity{SubjectAlternativeName: san, IssuerRegexp: r, Extensions: certificate.Extensions{Issuer: issuer, RunnerEnvironment: runnerEnv}}, nil
}
10 changes: 5 additions & 5 deletions pkg/verify/signed_entity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ func TestEntityWithOthernameSan(t *testing.T) {
digest, err := hex.DecodeString("bc103b4a84971ef6459b294a2b98568a2bfb72cded09d4acd1e16366a401f95b")
assert.NoError(t, err)

certID, err := verify.NewShortCertificateIdentity("http://oidc.local:8080", "foo!oidc.local", "")
certID, err := verify.NewShortCertificateIdentity("http://oidc.local:8080", "", "foo!oidc.local", "")
assert.NoError(t, err)
res, err := v.Verify(entity, verify.NewPolicy(verify.WithArtifactDigest("sha256", digest), verify.WithCertificateIdentity(certID)))
assert.NoError(t, err)
Expand All @@ -220,7 +220,7 @@ func TestEntityWithOthernameSan(t *testing.T) {
assert.Equal(t, res.VerifiedIdentity.SubjectAlternativeName.SubjectAlternativeName, "foo!oidc.local")

// an email address doesn't verify
certID, err = verify.NewShortCertificateIdentity("http://oidc.local:8080", "foo@oidc.local", "")
certID, err = verify.NewShortCertificateIdentity("http://oidc.local:8080", "", "foo@oidc.local", "")
assert.NoError(t, err)
_, err = v.Verify(entity, verify.NewPolicy(verify.WithArtifactDigest("sha256", digest), verify.WithCertificateIdentity(certID)))
assert.Error(t, err)
Expand All @@ -235,7 +235,7 @@ func TestVerifyPolicyOptionErors(t *testing.T) {
verifier, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1))
assert.Nil(t, err)

goodCertID, err := verify.NewShortCertificateIdentity(verify.ActionsIssuerValue, "", verify.SigstoreSanRegex)
goodCertID, err := verify.NewShortCertificateIdentity(verify.ActionsIssuerValue, "", "", verify.SigstoreSanRegex)
assert.Nil(t, err)

digest, _ := hex.DecodeString("46d4e2f74c4877316640000a6fdf8a8b59f1e0847667973e9859f774dd31b8f1e0937813b777fb66a2ac67d50540fe34640966eee9fc2ccca387082b4c85cd3c")
Expand Down Expand Up @@ -326,8 +326,8 @@ func TestEntitySignedByPublicGoodWithCertificateIdentityVerifiesSuccessfully(t *
tr := data.PublicGoodTrustedMaterialRoot(t)
entity := data.SigstoreJS200ProvenanceBundle(t)

goodCI, _ := verify.NewShortCertificateIdentity(verify.ActionsIssuerValue, "", verify.SigstoreSanRegex)
badCI, _ := verify.NewShortCertificateIdentity(verify.ActionsIssuerValue, "BadSANValue", "")
goodCI, _ := verify.NewShortCertificateIdentity(verify.ActionsIssuerValue, "", "", verify.SigstoreSanRegex)
badCI, _ := verify.NewShortCertificateIdentity(verify.ActionsIssuerValue, "", "BadSANValue", "")

verifier, err := verify.NewSignedEntityVerifier(tr, verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1))

Expand Down
Loading