Skip to content

Commit 410da84

Browse files
committed
Support intermediate certificates in bundle
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]>
1 parent fbc922a commit 410da84

12 files changed

+193
-82
lines changed

pkg/bundle/bundle.go

+12-17
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,6 @@ func (b *Bundle) validate() error {
108108
}
109109
}
110110

111-
// if bundle version >= v0.3, require verification material to not be X.509 certificate chain (only single certificate is allowed)
112-
if semver.Compare(bundleVersion, "v0.3") >= 0 {
113-
certs := b.Bundle.VerificationMaterial.GetX509CertificateChain()
114-
115-
if certs != nil {
116-
return errors.New("verification material cannot be X.509 certificate chain (for bundle v0.3)")
117-
}
118-
}
119-
120111
// if bundle version is >= v0.4, return error as this version is not supported
121112
if semver.Compare(bundleVersion, "v0.4") >= 0 {
122113
return fmt.Errorf("%w: bundle version %s is not yet supported", ErrUnsupportedMediaType, bundleVersion)
@@ -250,14 +241,18 @@ func (b *Bundle) VerificationContent() (verify.VerificationContent, error) {
250241
if len(certs) == 0 || certs[0].RawBytes == nil {
251242
return nil, ErrMissingVerificationMaterial
252243
}
253-
parsedCert, err := x509.ParseCertificate(certs[0].RawBytes)
254-
if err != nil {
255-
return nil, ErrValidationError(err)
244+
parsedCerts := make([]*x509.Certificate, len(certs))
245+
var err error
246+
for i, c := range certs {
247+
parsedCerts[i], err = x509.ParseCertificate(c.RawBytes)
248+
if err != nil {
249+
return nil, ErrValidationError(err)
250+
}
256251
}
257-
cert := &Certificate{
258-
Certificate: parsedCert,
252+
certChain := &CertificateChain{
253+
Certificates: parsedCerts,
259254
}
260-
return cert, nil
255+
return certChain, nil
261256
case *protobundle.VerificationMaterial_Certificate:
262257
if content.Certificate == nil || content.Certificate.RawBytes == nil {
263258
return nil, ErrMissingVerificationMaterial
@@ -266,8 +261,8 @@ func (b *Bundle) VerificationContent() (verify.VerificationContent, error) {
266261
if err != nil {
267262
return nil, ErrValidationError(err)
268263
}
269-
cert := &Certificate{
270-
Certificate: parsedCert,
264+
cert := &CertificateChain{
265+
Certificates: []*x509.Certificate{parsedCert},
271266
}
272267
return cert, nil
273268
case *protobundle.VerificationMaterial_PublicKey:

pkg/bundle/bundle_test.go

+13-10
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ func Test_validate(t *testing.T) {
520520
Content: &protobundle.Bundle_MessageSignature{},
521521
},
522522
},
523-
wantErr: true,
523+
wantErr: false,
524524
},
525525
{
526526
name: "v0.3 without x.509 certificate chain",
@@ -584,11 +584,12 @@ func TestVerificationContent(t *testing.T) {
584584
leafDer, err := x509.CreateCertificate(rand.Reader, leafCert, caCert, &leafKey.PublicKey, caKey)
585585
require.NoError(t, err)
586586
tests := []struct {
587-
name string
588-
pb Bundle
589-
wantCertificate bool
590-
wantPublicKey bool
591-
wantErr bool
587+
name string
588+
pb Bundle
589+
wantCertificateChain bool
590+
wantCertificate bool
591+
wantPublicKey bool
592+
wantErr bool
592593
}{
593594
{
594595
name: "no verification material",
@@ -649,7 +650,8 @@ func TestVerificationContent(t *testing.T) {
649650
},
650651
},
651652
},
652-
wantCertificate: true,
653+
wantCertificateChain: true,
654+
wantCertificate: true,
653655
},
654656
{
655657
name: "certificate chain with invalid cert",
@@ -813,14 +815,15 @@ func TestVerificationContent(t *testing.T) {
813815
return
814816
}
815817
require.NoError(t, gotErr)
818+
if tt.wantCertificateChain {
819+
require.NotNil(t, got.GetIntermediates())
820+
}
816821
if tt.wantCertificate {
817-
require.NotNil(t, got.GetCertificate())
818-
return
822+
require.NotNil(t, got.GetLeafCertificate())
819823
}
820824
if tt.wantPublicKey {
821825
_, hasPubKey := got.HasPublicKey()
822826
require.True(t, hasPubKey)
823-
return
824827
}
825828
})
826829
}

pkg/bundle/verification_content.go

+30-11
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import (
2323
"github.com/sigstore/sigstore-go/pkg/verify"
2424
)
2525

26-
type Certificate struct {
27-
*x509.Certificate
26+
type CertificateChain struct {
27+
Certificates []*x509.Certificate
2828
}
2929

3030
type PublicKey struct {
@@ -35,27 +35,42 @@ func (pk PublicKey) Hint() string {
3535
return pk.hint
3636
}
3737

38-
func (c *Certificate) CompareKey(key any, _ root.TrustedMaterial) bool {
38+
func (c *CertificateChain) CompareKey(key any, tm root.TrustedMaterial) bool {
39+
if len(c.Certificates) < 1 {
40+
return false
41+
}
3942
x509Key, ok := key.(*x509.Certificate)
4043
if !ok {
4144
return false
4245
}
43-
44-
return c.Certificate.Equal(x509Key)
46+
return c.Certificates[0].Equal(x509Key)
4547
}
4648

47-
func (c *Certificate) ValidAtTime(t time.Time, _ root.TrustedMaterial) bool {
48-
return !(c.Certificate.NotAfter.Before(t) || c.Certificate.NotBefore.After(t))
49+
func (c *CertificateChain) ValidAtTime(t time.Time, tm root.TrustedMaterial) bool {
50+
if len(c.Certificates) < 1 {
51+
return false
52+
}
53+
return !(c.Certificates[0].NotAfter.Before(t) || c.Certificates[0].NotBefore.After(t))
4954
}
5055

51-
func (c *Certificate) GetCertificate() *x509.Certificate {
52-
return c.Certificate
56+
func (c *CertificateChain) GetLeafCertificate() *x509.Certificate {
57+
if len(c.Certificates) < 1 {
58+
return nil
59+
}
60+
return c.Certificates[0]
5361
}
5462

55-
func (c *Certificate) HasPublicKey() (verify.PublicKeyProvider, bool) {
63+
func (c *CertificateChain) HasPublicKey() (verify.PublicKeyProvider, bool) {
5664
return PublicKey{}, false
5765
}
5866

67+
func (c *CertificateChain) GetIntermediates() []*x509.Certificate {
68+
if len(c.Certificates) < 2 {
69+
return nil
70+
}
71+
return c.Certificates[1:]
72+
}
73+
5974
func (pk *PublicKey) CompareKey(key any, tm root.TrustedMaterial) bool {
6075
verifier, err := tm.PublicKeyVerifier(pk.hint)
6176
if err != nil {
@@ -79,10 +94,14 @@ func (pk *PublicKey) ValidAtTime(t time.Time, tm root.TrustedMaterial) bool {
7994
return verifier.ValidAtTime(t)
8095
}
8196

82-
func (pk *PublicKey) GetCertificate() *x509.Certificate {
97+
func (pk *PublicKey) GetLeafCertificate() *x509.Certificate {
8398
return nil
8499
}
85100

86101
func (pk *PublicKey) HasPublicKey() (verify.PublicKeyProvider, bool) {
87102
return *pk, true
88103
}
104+
105+
func (pk *PublicKey) GetIntermediates() []*x509.Certificate {
106+
return nil
107+
}

pkg/fulcio/certificate/summarize_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func TestSummarizeCertificateWithActionsBundle(t *testing.T) {
3030
t.Fatalf("failed to get verification content: %v", err)
3131
}
3232

33-
leaf := vc.GetCertificate()
33+
leaf := vc.GetLeafCertificate()
3434

3535
if leaf == nil {
3636
t.Fatalf("expected verification content to be a certificate chain")
@@ -79,7 +79,7 @@ func TestSummarizeCertificateWithOauthBundle(t *testing.T) {
7979
t.Fatalf("failed to get verification content: %v", err)
8080
}
8181

82-
leaf := vc.GetCertificate()
82+
leaf := vc.GetLeafCertificate()
8383

8484
if leaf == nil {
8585
t.Fatalf("expected verification content to be a certificate chain")
@@ -108,7 +108,7 @@ func TestSummarizeCertificateWithOtherNameSAN(t *testing.T) {
108108
t.Fatalf("failed to get verification content: %v", err)
109109
}
110110

111-
leaf := vc.GetCertificate()
111+
leaf := vc.GetLeafCertificate()
112112

113113
if leaf == nil {
114114
t.Fatalf("expected verification content to be a certificate chain")

pkg/testing/ca/ca.go

+29-1
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,34 @@ func (ca *VirtualSigstore) PublicKeyVerifier(keyID string) (root.TimeConstrained
332332
return v, nil
333333
}
334334

335+
func (ca *VirtualSigstore) GenerateNewFulcioIntermediate(name string) (*x509.Certificate, *ecdsa.PrivateKey, error) {
336+
subTemplate := &x509.Certificate{
337+
SerialNumber: big.NewInt(1),
338+
Subject: pkix.Name{
339+
CommonName: name,
340+
Organization: []string{"sigstore.dev"},
341+
},
342+
NotBefore: time.Now().Add(-2 * time.Minute),
343+
NotAfter: time.Now().Add(2 * time.Hour),
344+
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
345+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
346+
BasicConstraintsValid: true,
347+
IsCA: true,
348+
}
349+
350+
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
351+
if err != nil {
352+
return nil, nil, err
353+
}
354+
355+
cert, err := createCertificate(subTemplate, ca.fulcioCA.Intermediates[0], &priv.PublicKey, ca.fulcioIntermediateKey)
356+
if err != nil {
357+
return nil, nil, err
358+
}
359+
360+
return cert, priv, nil
361+
}
362+
335363
func generateRekorEntry(kind, version string, artifact []byte, cert []byte, sig []byte) (string, error) {
336364
// Generate the Rekor Entry
337365
entryImpl, err := createEntry(context.Background(), kind, version, artifact, cert, sig)
@@ -481,7 +509,7 @@ type TestEntity struct {
481509
}
482510

483511
func (e *TestEntity) VerificationContent() (verify.VerificationContent, error) {
484-
return &bundle.Certificate{Certificate: e.certChain[0]}, nil
512+
return &bundle.CertificateChain{[]*x509.Certificate{e.certChain[0]}}, nil
485513
}
486514

487515
func (e *TestEntity) HasInclusionPromise() bool {

pkg/verify/certificate.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222
"github.com/sigstore/sigstore-go/pkg/root"
2323
)
2424

25-
func VerifyLeafCertificate(observerTimestamp time.Time, leafCert *x509.Certificate, trustedMaterial root.TrustedMaterial) error { // nolint: revive
25+
func VerifyLeafCertificate(observerTimestamp time.Time, verificationContent VerificationContent, trustedMaterial root.TrustedMaterial) error { // nolint: revive
2626
for _, ca := range trustedMaterial.FulcioCertificateAuthorities() {
2727
if !ca.ValidityPeriodStart.IsZero() && observerTimestamp.Before(ca.ValidityPeriodStart) {
2828
continue
@@ -38,6 +38,10 @@ func VerifyLeafCertificate(observerTimestamp time.Time, leafCert *x509.Certifica
3838
intermediateCertPool.AddCert(cert)
3939
}
4040

41+
for _, cert := range verificationContent.GetIntermediates() {
42+
intermediateCertPool.AddCert(cert)
43+
}
44+
4145
// From spec:
4246
// > ## Certificate
4347
// > 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
5155
},
5256
}
5357

58+
leafCert := verificationContent.GetLeafCertificate()
59+
5460
_, err := leafCert.Verify(opts)
5561
if err == nil {
5662
return nil

pkg/verify/certificate_test.go

+51-12
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@
1515
package verify_test
1616

1717
import (
18+
"crypto/ecdsa"
19+
"crypto/elliptic"
20+
"crypto/rand"
21+
"crypto/x509"
1822
"testing"
1923
"time"
2024

25+
"github.com/sigstore/sigstore-go/pkg/bundle"
2126
"github.com/sigstore/sigstore-go/pkg/testing/ca"
2227
"github.com/sigstore/sigstore-go/pkg/verify"
2328
"github.com/stretchr/testify/assert"
@@ -30,30 +35,64 @@ func TestVerifyValidityPeriod(t *testing.T) {
3035
leaf, _, err := virtualSigstore.GenerateLeafCert("[email protected]", "issuer")
3136
assert.NoError(t, err)
3237

38+
altIntermediate, intermediateKey, err := virtualSigstore.GenerateNewFulcioIntermediate("sigstore-subintermediate")
39+
assert.NoError(t, err)
40+
41+
altPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
42+
assert.NoError(t, err)
43+
altLeaf, err := ca.GenerateLeafCert("[email protected]", "issuer", time.Now().Add(time.Hour*24), altPrivKey, altIntermediate, intermediateKey)
44+
assert.NoError(t, err)
45+
3346
tests := []struct {
34-
name string
35-
observerTimestamp time.Time
36-
wantErr bool
47+
name string
48+
observerTimestamp time.Time
49+
verificationContent verify.VerificationContent
50+
wantErr bool
3751
}{
3852
{
39-
name: "before validity period",
40-
observerTimestamp: time.Now().Add(time.Hour * -24),
41-
wantErr: true,
53+
name: "before validity period",
54+
observerTimestamp: time.Now().Add(time.Hour * -24),
55+
verificationContent: &bundle.CertificateChain{[]*x509.Certificate{leaf}},
56+
wantErr: true,
4257
},
4358
{
44-
name: "inside validity period",
59+
name: "inside validity period",
60+
observerTimestamp: time.Now(),
61+
verificationContent: &bundle.CertificateChain{[]*x509.Certificate{leaf}},
62+
wantErr: false,
63+
},
64+
{
65+
name: "after validity period",
66+
observerTimestamp: time.Now().Add(time.Hour * 24),
67+
verificationContent: &bundle.CertificateChain{[]*x509.Certificate{leaf}},
68+
wantErr: true,
69+
},
70+
{
71+
name: "with intermediates",
4572
observerTimestamp: time.Now(),
46-
wantErr: false,
73+
verificationContent: &bundle.CertificateChain{
74+
[]*x509.Certificate{
75+
altIntermediate,
76+
altLeaf,
77+
},
78+
},
79+
wantErr: false,
4780
},
4881
{
49-
name: "after validity period",
50-
observerTimestamp: time.Now().Add(time.Hour * 24),
51-
wantErr: true,
82+
name: "with invalid intermediates",
83+
observerTimestamp: time.Now(),
84+
verificationContent: &bundle.CertificateChain{
85+
[]*x509.Certificate{
86+
altLeaf,
87+
leaf,
88+
},
89+
},
90+
wantErr: true,
5291
},
5392
}
5493
for _, tt := range tests {
5594
t.Run(tt.name, func(t *testing.T) {
56-
if err := verify.VerifyLeafCertificate(tt.observerTimestamp, leaf, virtualSigstore); (err != nil) != tt.wantErr {
95+
if err := verify.VerifyLeafCertificate(tt.observerTimestamp, tt.verificationContent, virtualSigstore); (err != nil) != tt.wantErr {
5796
t.Errorf("VerifyLeafCertificate() error = %v, wantErr %v", err, tt.wantErr)
5897
}
5998
})

pkg/verify/interface.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ type SignedEntity interface {
6464
type VerificationContent interface {
6565
CompareKey(any, root.TrustedMaterial) bool
6666
ValidAtTime(time.Time, root.TrustedMaterial) bool
67-
GetCertificate() *x509.Certificate
67+
GetLeafCertificate() *x509.Certificate
68+
GetIntermediates() []*x509.Certificate
6869
HasPublicKey() (PublicKeyProvider, bool)
6970
}
7071

0 commit comments

Comments
 (0)