diff --git a/pkg/bundle/bundle.go b/pkg/bundle/bundle.go index ef4afaab..b6094679 100644 --- a/pkg/bundle/bundle.go +++ b/pkg/bundle/bundle.go @@ -98,15 +98,6 @@ func (b *ProtobufBundle) validate() error { } } - // if bundle version >= v0.3, require verification material to not be X.509 certificate chain (only single certificate is allowed) - if semver.Compare(bundleVersion, "v0.3") >= 0 { - certs := b.Bundle.VerificationMaterial.GetX509CertificateChain() - - if certs != nil { - return errors.New("verification material cannot be X.509 certificate chain (for bundle v0.3)") - } - } - // if bundle version is >= v0.4, return error as this version is not supported if semver.Compare(bundleVersion, "v0.4") >= 0 { return fmt.Errorf("%w: bundle version %s is not yet supported", ErrUnsupportedMediaType, bundleVersion) @@ -205,14 +196,18 @@ func (b *ProtobufBundle) VerificationContent() (verify.VerificationContent, erro if len(certs) == 0 { return nil, ErrMissingVerificationMaterial } - parsedCert, err := x509.ParseCertificate(certs[0].RawBytes) - if err != nil { - return nil, ErrValidationError(err) + parsedCerts := make([]*x509.Certificate, len(certs)) + var err error + for i, c := range certs { + parsedCerts[i], err = x509.ParseCertificate(c.RawBytes) + if err != nil { + return nil, ErrValidationError(err) + } } - cert := &Certificate{ - Certificate: parsedCert, + certChain := &CertificateChain{ + Certificates: parsedCerts, } - return cert, nil + return certChain, nil case *protobundle.VerificationMaterial_Certificate: parsedCert, err := x509.ParseCertificate(content.Certificate.RawBytes) if err != nil { diff --git a/pkg/bundle/bundle_test.go b/pkg/bundle/bundle_test.go index 162c3ff6..9bd464ef 100644 --- a/pkg/bundle/bundle_test.go +++ b/pkg/bundle/bundle_test.go @@ -366,7 +366,7 @@ func Test_validate(t *testing.T) { }, }, }, - wantErr: true, + wantErr: false, }, { name: "v0.3 without x.509 certificate chain", diff --git a/pkg/bundle/verification_content.go b/pkg/bundle/verification_content.go index b775295d..3f038fc8 100644 --- a/pkg/bundle/verification_content.go +++ b/pkg/bundle/verification_content.go @@ -35,6 +35,10 @@ func (pk PublicKey) Hint() string { return pk.hint } +type CertificateChain struct { + Certificates []*x509.Certificate +} + func (c *Certificate) CompareKey(key any, _ root.TrustedMaterial) bool { x509Key, ok := key.(*x509.Certificate) if !ok { @@ -56,6 +60,10 @@ func (c *Certificate) HasPublicKey() (verify.PublicKeyProvider, bool) { return PublicKey{}, false } +func (c *Certificate) GetCertificateChain() []*x509.Certificate { + return nil +} + func (pk *PublicKey) CompareKey(key any, tm root.TrustedMaterial) bool { verifier, err := tm.PublicKeyVerifier(pk.hint) if err != nil { @@ -86,3 +94,39 @@ func (pk *PublicKey) GetCertificate() *x509.Certificate { func (pk *PublicKey) HasPublicKey() (verify.PublicKeyProvider, bool) { return *pk, true } + +func (pk *PublicKey) GetCertificateChain() []*x509.Certificate { + return nil +} + +func (cc *CertificateChain) CompareKey(key any, tm root.TrustedMaterial) bool { + if len(cc.Certificates) < 1 { + return false + } + return (&Certificate{cc.Certificates[0]}).CompareKey(key, tm) +} + +func (cc *CertificateChain) ValidAtTime(t time.Time, tm root.TrustedMaterial) bool { + if len(cc.Certificates) < 1 { + return false + } + return (&Certificate{cc.Certificates[0]}).ValidAtTime(t, tm) +} + +func (cc *CertificateChain) GetCertificate() *x509.Certificate { + if len(cc.Certificates) < 1 { + return nil + } + return cc.Certificates[0] +} + +func (cc *CertificateChain) HasPublicKey() (verify.PublicKeyProvider, bool) { + return PublicKey{}, false +} + +func (cc *CertificateChain) GetCertificateChain() []*x509.Certificate { + if len(cc.Certificates) < 2 { + return nil + } + return cc.Certificates[1:] +} diff --git a/pkg/testing/ca/ca.go b/pkg/testing/ca/ca.go index 3a3e6ad4..cb78a8da 100644 --- a/pkg/testing/ca/ca.go +++ b/pkg/testing/ca/ca.go @@ -332,6 +332,34 @@ func (ca *VirtualSigstore) PublicKeyVerifier(keyID string) (root.TimeConstrained return v, nil } +func (ca *VirtualSigstore) GenerateNewFulcioIntermediate(name string) (*x509.Certificate, *ecdsa.PrivateKey, error) { + subTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: name, + Organization: []string{"sigstore.dev"}, + }, + NotBefore: time.Now().Add(-2 * time.Minute), + NotAfter: time.Now().Add(2 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + BasicConstraintsValid: true, + IsCA: true, + } + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + cert, err := createCertificate(subTemplate, ca.fulcioCA.Intermediates[0], &priv.PublicKey, ca.fulcioIntermediateKey) + if err != nil { + return nil, nil, err + } + + return cert, priv, nil +} + func generateRekorEntry(kind, version string, artifact []byte, cert []byte, sig []byte) (string, error) { // Generate the Rekor Entry entryImpl, err := createEntry(context.Background(), kind, version, artifact, cert, sig) diff --git a/pkg/verify/certificate.go b/pkg/verify/certificate.go index 4ce2dff2..074638b8 100644 --- a/pkg/verify/certificate.go +++ b/pkg/verify/certificate.go @@ -22,7 +22,7 @@ import ( "github.com/sigstore/sigstore-go/pkg/root" ) -func VerifyLeafCertificate(observerTimestamp time.Time, leafCert *x509.Certificate, trustedMaterial root.TrustedMaterial) error { // nolint: revive +func VerifyLeafCertificate(observerTimestamp time.Time, verificationContent VerificationContent, trustedMaterial root.TrustedMaterial) error { // nolint: revive for _, ca := range trustedMaterial.FulcioCertificateAuthorities() { if !ca.ValidityPeriodStart.IsZero() && observerTimestamp.Before(ca.ValidityPeriodStart) { continue @@ -38,6 +38,10 @@ func VerifyLeafCertificate(observerTimestamp time.Time, leafCert *x509.Certifica intermediateCertPool.AddCert(cert) } + for _, cert := range verificationContent.GetCertificateChain() { + intermediateCertPool.AddCert(cert) + } + // From spec: // > ## Certificate // > For a signature with a given certificate to be considered valid, it must have a timestamp while every certificate in the chain up to the root is valid (the so-called “hybrid model” of certificate verification per Braun et al. (2013)). @@ -51,6 +55,8 @@ func VerifyLeafCertificate(observerTimestamp time.Time, leafCert *x509.Certifica }, } + leafCert := verificationContent.GetCertificate() + _, err := leafCert.Verify(opts) if err == nil { return nil diff --git a/pkg/verify/certificate_test.go b/pkg/verify/certificate_test.go index 8c7df7d7..7dd22222 100644 --- a/pkg/verify/certificate_test.go +++ b/pkg/verify/certificate_test.go @@ -15,9 +15,14 @@ package verify_test import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" "testing" "time" + "github.com/sigstore/sigstore-go/pkg/bundle" "github.com/sigstore/sigstore-go/pkg/testing/ca" "github.com/sigstore/sigstore-go/pkg/verify" "github.com/stretchr/testify/assert" @@ -30,30 +35,64 @@ func TestVerifyValidityPeriod(t *testing.T) { leaf, _, err := virtualSigstore.GenerateLeafCert("example@example.com", "issuer") assert.NoError(t, err) + altIntermediate, intermediateKey, err := virtualSigstore.GenerateNewFulcioIntermediate("sigstore-subintermediate") + assert.NoError(t, err) + + altPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.NoError(t, err) + altLeaf, err := ca.GenerateLeafCert("example2@example.com", "issuer", time.Now().Add(time.Hour*24), altPrivKey, altIntermediate, intermediateKey) + assert.NoError(t, err) + tests := []struct { - name string - observerTimestamp time.Time - wantErr bool + name string + observerTimestamp time.Time + verificationContent verify.VerificationContent + wantErr bool }{ { - name: "before validity period", - observerTimestamp: time.Now().Add(time.Hour * -24), - wantErr: true, + name: "before validity period", + observerTimestamp: time.Now().Add(time.Hour * -24), + verificationContent: &bundle.Certificate{leaf}, + wantErr: true, }, { - name: "inside validity period", + name: "inside validity period", + observerTimestamp: time.Now(), + verificationContent: &bundle.Certificate{leaf}, + wantErr: false, + }, + { + name: "after validity period", + observerTimestamp: time.Now().Add(time.Hour * 24), + verificationContent: &bundle.Certificate{leaf}, + wantErr: true, + }, + { + name: "with intermediates", observerTimestamp: time.Now(), - wantErr: false, + verificationContent: &bundle.CertificateChain{ + Certificates: []*x509.Certificate{ + altIntermediate, + altLeaf, + }, + }, + wantErr: false, }, { - name: "after validity period", - observerTimestamp: time.Now().Add(time.Hour * 24), - wantErr: true, + name: "with invalid intermediates", + observerTimestamp: time.Now(), + verificationContent: &bundle.CertificateChain{ + Certificates: []*x509.Certificate{ + altLeaf, + leaf, + }, + }, + wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := verify.VerifyLeafCertificate(tt.observerTimestamp, leaf, virtualSigstore); (err != nil) != tt.wantErr { + if err := verify.VerifyLeafCertificate(tt.observerTimestamp, tt.verificationContent, virtualSigstore); (err != nil) != tt.wantErr { t.Errorf("VerifyLeafCertificate() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/pkg/verify/interface.go b/pkg/verify/interface.go index 464b0d5f..10d95285 100644 --- a/pkg/verify/interface.go +++ b/pkg/verify/interface.go @@ -65,6 +65,7 @@ type VerificationContent interface { CompareKey(any, root.TrustedMaterial) bool ValidAtTime(time.Time, root.TrustedMaterial) bool GetCertificate() *x509.Certificate + GetCertificateChain() []*x509.Certificate HasPublicKey() (PublicKeyProvider, bool) } diff --git a/pkg/verify/sct.go b/pkg/verify/sct.go index 7b6edf67..8467fccc 100644 --- a/pkg/verify/sct.go +++ b/pkg/verify/sct.go @@ -15,7 +15,6 @@ package verify import ( - "crypto/x509" "encoding/hex" "fmt" @@ -29,10 +28,12 @@ import ( // leaf certificate, will extract SCTs from the leaf certificate and verify the // timestamps using the TrustedMaterial's FulcioCertificateAuthorities() and // CTLogs() -func VerifySignedCertificateTimestamp(leafCert *x509.Certificate, threshold int, trustedMaterial root.TrustedMaterial) error { // nolint: revive +func VerifySignedCertificateTimestamp(verificationContent VerificationContent, threshold int, trustedMaterial root.TrustedMaterial) error { // nolint: revive ctlogs := trustedMaterial.CTLogs() fulcioCerts := trustedMaterial.FulcioCertificateAuthorities() + leafCert := verificationContent.GetCertificate() + scts, err := x509util.ParseSCTsFromCertificate(leafCert.Raw) if err != nil { return err @@ -56,6 +57,15 @@ func VerifySignedCertificateTimestamp(leafCert *x509.Certificate, threshold int, fulcioChain := make([]*ctx509.Certificate, len(leafCTCert)) copy(fulcioChain, leafCTCert) + bundleIntermediates := verificationContent.GetCertificateChain() + for _, cert := range bundleIntermediates { + convertedCert, err := ctx509.ParseCertificate(cert.Raw) + if err != nil { + continue + } + fulcioChain = append(fulcioChain, convertedCert) + } + var parentCert []byte if len(fulcioCa.Intermediates) == 0 { diff --git a/pkg/verify/sct_test.go b/pkg/verify/sct_test.go index c26bead5..dfb1802b 100644 --- a/pkg/verify/sct_test.go +++ b/pkg/verify/sct_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package verify +package verify_test import ( "crypto" @@ -31,7 +31,9 @@ import ( "github.com/google/certificate-transparency-go/trillian/ctfe" ctx509 "github.com/google/certificate-transparency-go/x509" ctx509util "github.com/google/certificate-transparency-go/x509util" + "github.com/sigstore/sigstore-go/pkg/bundle" "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/verify" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/stretchr/testify/assert" ) @@ -61,21 +63,23 @@ func TestVerifySignedCertificateTimestamp(t *testing.T) { } tests := []struct { name string - getCertFn func() *x509.Certificate + getCertFn func() verify.VerificationContent threshold int trustedMaterial root.TrustedMaterial wantErr bool }{ { - name: "missing sct in cert", - getCertFn: func() *x509.Certificate { return createBaseCert(t, privateKey, skid, big.NewInt(1)) }, + name: "missing sct in cert", + getCertFn: func() verify.VerificationContent { + return createVerificationContent(t, privateKey, skid, big.NewInt(1)) + }, threshold: 1, trustedMaterial: &fakeTrustedMaterial{}, wantErr: true, }, { name: "sct missing from ct logs", - getCertFn: func() *x509.Certificate { + getCertFn: func() verify.VerificationContent { return embedSCTs(t, privateKey, skid, createBaseCert(t, privateKey, skid, big.NewInt(1)), []ct.SignedCertificateTimestamp{{ SCTVersion: ct.V1, Timestamp: 12345, @@ -90,7 +94,7 @@ func TestVerifySignedCertificateTimestamp(t *testing.T) { }, { name: "missing fulcio CAs", - getCertFn: func() *x509.Certificate { + getCertFn: func() verify.VerificationContent { return embedSCTs(t, privateKey, skid, createBaseCert(t, privateKey, skid, big.NewInt(1)), []ct.SignedCertificateTimestamp{{ SCTVersion: ct.V1, Timestamp: 12345, @@ -107,7 +111,7 @@ func TestVerifySignedCertificateTimestamp(t *testing.T) { }, { name: "one valid sct", - getCertFn: func() *x509.Certificate { + getCertFn: func() verify.VerificationContent { return embedSCTs(t, privateKey, skid, createBaseCert(t, privateKey, skid, big.NewInt(1)), []ct.SignedCertificateTimestamp{{ SCTVersion: ct.V1, Timestamp: 12345, @@ -130,7 +134,7 @@ func TestVerifySignedCertificateTimestamp(t *testing.T) { }, { name: "one invalid sct", - getCertFn: func() *x509.Certificate { + getCertFn: func() verify.VerificationContent { return embedSCTs(t, privateKey, skid, createBaseCert(t, privateKey, skid, big.NewInt(1)), []ct.SignedCertificateTimestamp{ { SCTVersion: ct.V1, @@ -156,7 +160,7 @@ func TestVerifySignedCertificateTimestamp(t *testing.T) { }, { name: "one valid sct out of multiple invalid scts", - getCertFn: func() *x509.Certificate { + getCertFn: func() verify.VerificationContent { return embedSCTs(t, privateKey, skid, createBaseCert(t, privateKey, skid, big.NewInt(1)), []ct.SignedCertificateTimestamp{ { SCTVersion: ct.V1, @@ -186,7 +190,7 @@ func TestVerifySignedCertificateTimestamp(t *testing.T) { }, { name: "threshold of 2 with only 1 valid sct", - getCertFn: func() *x509.Certificate { + getCertFn: func() verify.VerificationContent { return embedSCTs(t, privateKey, skid, createBaseCert(t, privateKey, skid, big.NewInt(1)), []ct.SignedCertificateTimestamp{ { SCTVersion: ct.V1, @@ -217,7 +221,7 @@ func TestVerifySignedCertificateTimestamp(t *testing.T) { }, { name: "no valid scts out of multiple", - getCertFn: func() *x509.Certificate { + getCertFn: func() verify.VerificationContent { return embedSCTs(t, privateKey, skid, createBaseCert(t, privateKey, skid, big.NewInt(1)), []ct.SignedCertificateTimestamp{ { SCTVersion: ct.V1, @@ -248,7 +252,7 @@ func TestVerifySignedCertificateTimestamp(t *testing.T) { }, { name: "fulcio CA has intermediates", - getCertFn: func() *x509.Certificate { + getCertFn: func() verify.VerificationContent { return embedSCTs(t, privateKey, skid, createBaseCert(t, privateKey, skid, big.NewInt(1)), []ct.SignedCertificateTimestamp{{ SCTVersion: ct.V1, Timestamp: 12345, @@ -274,7 +278,7 @@ func TestVerifySignedCertificateTimestamp(t *testing.T) { }, { name: "no valid fulcio CAs", - getCertFn: func() *x509.Certificate { + getCertFn: func() verify.VerificationContent { return embedSCTs(t, privateKey, skid, createBaseCert(t, privateKey, skid, big.NewInt(1)), []ct.SignedCertificateTimestamp{{ SCTVersion: ct.V1, Timestamp: 12345, @@ -297,14 +301,16 @@ func TestVerifySignedCertificateTimestamp(t *testing.T) { wantErr: true, }, { - name: "threshold of 0", - getCertFn: func() *x509.Certificate { return createBaseCert(t, privateKey, skid, big.NewInt(1)) }, + name: "threshold of 0", + getCertFn: func() verify.VerificationContent { + return createVerificationContent(t, privateKey, skid, big.NewInt(1)) + }, threshold: 0, trustedMaterial: &fakeTrustedMaterial{}, }, { name: "threshold of 2 with 2 valid scts", - getCertFn: func() *x509.Certificate { + getCertFn: func() verify.VerificationContent { return embedSCTs(t, privateKey, skid, createBaseCert(t, privateKey, skid, big.NewInt(1)), []ct.SignedCertificateTimestamp{ { SCTVersion: ct.V1, @@ -335,7 +341,7 @@ func TestVerifySignedCertificateTimestamp(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - err = VerifySignedCertificateTimestamp(test.getCertFn(), test.threshold, test.trustedMaterial) + err = verify.VerifySignedCertificateTimestamp(test.getCertFn(), test.threshold, test.trustedMaterial) if test.wantErr { assert.Error(t, err) } else { @@ -361,7 +367,11 @@ func createBaseCert(t *testing.T, privateKey *rsa.PrivateKey, skid []byte, seria return parsedCert } -func embedSCTs(t *testing.T, privateKey *rsa.PrivateKey, skid []byte, preCert *x509.Certificate, sctInput []ct.SignedCertificateTimestamp) *x509.Certificate { +func createVerificationContent(t *testing.T, privateKey *rsa.PrivateKey, skid []byte, serialNumber *big.Int) verify.VerificationContent { + return &bundle.Certificate{createBaseCert(t, privateKey, skid, serialNumber)} +} + +func embedSCTs(t *testing.T, privateKey *rsa.PrivateKey, skid []byte, preCert *x509.Certificate, sctInput []ct.SignedCertificateTimestamp) verify.VerificationContent { scts := make([]*ct.SignedCertificateTimestamp, 0) for _, s := range sctInput { logEntry := ct.LogEntry{ @@ -431,7 +441,7 @@ func embedSCTs(t *testing.T, privateKey *rsa.PrivateKey, skid []byte, preCert *x if err != nil { t.Fatal(err) } - return parsedCert + return &bundle.Certificate{parsedCert} } type fakeTrustedMaterial struct { diff --git a/pkg/verify/signed_entity.go b/pkg/verify/signed_entity.go index 08a3b513..67ca507a 100644 --- a/pkg/verify/signed_entity.go +++ b/pkg/verify/signed_entity.go @@ -554,7 +554,7 @@ func (v *SignedEntityVerifier) Verify(entity SignedEntity, pb PolicyBuilder) (*V for _, verifiedTs := range verifiedTimestamps { // verify the leaf certificate against the root - err = VerifyLeafCertificate(verifiedTs.Timestamp, leafCert, v.trustedMaterial) + err = VerifyLeafCertificate(verifiedTs.Timestamp, verificationContent, v.trustedMaterial) if err != nil { return nil, fmt.Errorf("failed to verify leaf certificate: %w", err) } @@ -564,7 +564,7 @@ func (v *SignedEntityVerifier) Verify(entity SignedEntity, pb PolicyBuilder) (*V // > Unless performing online verification (see §Alternative Workflows), the Verifier MUST extract the SignedCertificateTimestamp embedded in the leaf certificate, and verify it as in RFC 9162 §8.1.3, using the verification key from the Certificate Transparency Log. if v.config.weExpectSCTs { - err = VerifySignedCertificateTimestamp(leafCert, v.config.ctlogEntriesThreshold, v.trustedMaterial) + err = VerifySignedCertificateTimestamp(verificationContent, v.config.ctlogEntriesThreshold, v.trustedMaterial) if err != nil { return nil, fmt.Errorf("failed to verify signed certificate timestamp: %w", err) }