Skip to content

Commit

Permalink
Support intermediate certificates in bundle
Browse files Browse the repository at this point in the history
Add support for processing and verifying a v0.3 bundle that contains a
`X509CertificateChain` rather than a single X.509 certificate or public
key.

Signed-off-by: Colleen Murphy <[email protected]>
  • Loading branch information
cmurphy committed Aug 1, 2024
1 parent 8554eb6 commit 5c8e670
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 52 deletions.
25 changes: 10 additions & 15 deletions pkg/bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/bundle/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ func Test_validate(t *testing.T) {
},
},
},
wantErr: true,
wantErr: false,
},
{
name: "v0.3 without x.509 certificate chain",
Expand Down
44 changes: 44 additions & 0 deletions pkg/bundle/verification_content.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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:]
}
28 changes: 28 additions & 0 deletions pkg/testing/ca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion pkg/verify/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)).
Expand All @@ -51,6 +55,8 @@ func VerifyLeafCertificate(observerTimestamp time.Time, leafCert *x509.Certifica
},
}

leafCert := verificationContent.GetCertificate()

_, err := leafCert.Verify(opts)
if err == nil {
return nil
Expand Down
63 changes: 51 additions & 12 deletions pkg/verify/certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -30,30 +35,64 @@ func TestVerifyValidityPeriod(t *testing.T) {
leaf, _, err := virtualSigstore.GenerateLeafCert("[email protected]", "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("[email protected]", "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)
}
})
Expand Down
1 change: 1 addition & 0 deletions pkg/verify/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
14 changes: 12 additions & 2 deletions pkg/verify/sct.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package verify

import (
"crypto/x509"
"encoding/hex"
"fmt"

Expand All @@ -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
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit 5c8e670

Please sign in to comment.