Skip to content

Commit 5c8e670

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 8554eb6 commit 5c8e670

10 files changed

+185
-52
lines changed

pkg/bundle/bundle.go

+10-15
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,6 @@ func (b *ProtobufBundle) validate() error {
9898
}
9999
}
100100

101-
// if bundle version >= v0.3, require verification material to not be X.509 certificate chain (only single certificate is allowed)
102-
if semver.Compare(bundleVersion, "v0.3") >= 0 {
103-
certs := b.Bundle.VerificationMaterial.GetX509CertificateChain()
104-
105-
if certs != nil {
106-
return errors.New("verification material cannot be X.509 certificate chain (for bundle v0.3)")
107-
}
108-
}
109-
110101
// if bundle version is >= v0.4, return error as this version is not supported
111102
if semver.Compare(bundleVersion, "v0.4") >= 0 {
112103
return fmt.Errorf("%w: bundle version %s is not yet supported", ErrUnsupportedMediaType, bundleVersion)
@@ -205,14 +196,18 @@ func (b *ProtobufBundle) VerificationContent() (verify.VerificationContent, erro
205196
if len(certs) == 0 {
206197
return nil, ErrMissingVerificationMaterial
207198
}
208-
parsedCert, err := x509.ParseCertificate(certs[0].RawBytes)
209-
if err != nil {
210-
return nil, ErrValidationError(err)
199+
parsedCerts := make([]*x509.Certificate, len(certs))
200+
var err error
201+
for i, c := range certs {
202+
parsedCerts[i], err = x509.ParseCertificate(c.RawBytes)
203+
if err != nil {
204+
return nil, ErrValidationError(err)
205+
}
211206
}
212-
cert := &Certificate{
213-
Certificate: parsedCert,
207+
certChain := &CertificateChain{
208+
Certificates: parsedCerts,
214209
}
215-
return cert, nil
210+
return certChain, nil
216211
case *protobundle.VerificationMaterial_Certificate:
217212
parsedCert, err := x509.ParseCertificate(content.Certificate.RawBytes)
218213
if err != nil {

pkg/bundle/bundle_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ func Test_validate(t *testing.T) {
366366
},
367367
},
368368
},
369-
wantErr: true,
369+
wantErr: false,
370370
},
371371
{
372372
name: "v0.3 without x.509 certificate chain",

pkg/bundle/verification_content.go

+44
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ func (pk PublicKey) Hint() string {
3535
return pk.hint
3636
}
3737

38+
type CertificateChain struct {
39+
Certificates []*x509.Certificate
40+
}
41+
3842
func (c *Certificate) CompareKey(key any, _ root.TrustedMaterial) bool {
3943
x509Key, ok := key.(*x509.Certificate)
4044
if !ok {
@@ -56,6 +60,10 @@ func (c *Certificate) HasPublicKey() (verify.PublicKeyProvider, bool) {
5660
return PublicKey{}, false
5761
}
5862

63+
func (c *Certificate) GetCertificateChain() []*x509.Certificate {
64+
return nil
65+
}
66+
5967
func (pk *PublicKey) CompareKey(key any, tm root.TrustedMaterial) bool {
6068
verifier, err := tm.PublicKeyVerifier(pk.hint)
6169
if err != nil {
@@ -86,3 +94,39 @@ func (pk *PublicKey) GetCertificate() *x509.Certificate {
8694
func (pk *PublicKey) HasPublicKey() (verify.PublicKeyProvider, bool) {
8795
return *pk, true
8896
}
97+
98+
func (pk *PublicKey) GetCertificateChain() []*x509.Certificate {
99+
return nil
100+
}
101+
102+
func (cc *CertificateChain) CompareKey(key any, tm root.TrustedMaterial) bool {
103+
if len(cc.Certificates) < 1 {
104+
return false
105+
}
106+
return (&Certificate{cc.Certificates[0]}).CompareKey(key, tm)
107+
}
108+
109+
func (cc *CertificateChain) ValidAtTime(t time.Time, tm root.TrustedMaterial) bool {
110+
if len(cc.Certificates) < 1 {
111+
return false
112+
}
113+
return (&Certificate{cc.Certificates[0]}).ValidAtTime(t, tm)
114+
}
115+
116+
func (cc *CertificateChain) GetCertificate() *x509.Certificate {
117+
if len(cc.Certificates) < 1 {
118+
return nil
119+
}
120+
return cc.Certificates[0]
121+
}
122+
123+
func (cc *CertificateChain) HasPublicKey() (verify.PublicKeyProvider, bool) {
124+
return PublicKey{}, false
125+
}
126+
127+
func (cc *CertificateChain) GetCertificateChain() []*x509.Certificate {
128+
if len(cc.Certificates) < 2 {
129+
return nil
130+
}
131+
return cc.Certificates[1:]
132+
}

pkg/testing/ca/ca.go

+28
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)

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.GetCertificateChain() {
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.GetCertificate()
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.Certificate{leaf},
56+
wantErr: true,
4257
},
4358
{
44-
name: "inside validity period",
59+
name: "inside validity period",
60+
observerTimestamp: time.Now(),
61+
verificationContent: &bundle.Certificate{leaf},
62+
wantErr: false,
63+
},
64+
{
65+
name: "after validity period",
66+
observerTimestamp: time.Now().Add(time.Hour * 24),
67+
verificationContent: &bundle.Certificate{leaf},
68+
wantErr: true,
69+
},
70+
{
71+
name: "with intermediates",
4572
observerTimestamp: time.Now(),
46-
wantErr: false,
73+
verificationContent: &bundle.CertificateChain{
74+
Certificates: []*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+
Certificates: []*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

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ type VerificationContent interface {
6565
CompareKey(any, root.TrustedMaterial) bool
6666
ValidAtTime(time.Time, root.TrustedMaterial) bool
6767
GetCertificate() *x509.Certificate
68+
GetCertificateChain() []*x509.Certificate
6869
HasPublicKey() (PublicKeyProvider, bool)
6970
}
7071

pkg/verify/sct.go

+12-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
package verify
1616

1717
import (
18-
"crypto/x509"
1918
"encoding/hex"
2019
"fmt"
2120

@@ -29,10 +28,12 @@ import (
2928
// leaf certificate, will extract SCTs from the leaf certificate and verify the
3029
// timestamps using the TrustedMaterial's FulcioCertificateAuthorities() and
3130
// CTLogs()
32-
func VerifySignedCertificateTimestamp(leafCert *x509.Certificate, threshold int, trustedMaterial root.TrustedMaterial) error { // nolint: revive
31+
func VerifySignedCertificateTimestamp(verificationContent VerificationContent, threshold int, trustedMaterial root.TrustedMaterial) error { // nolint: revive
3332
ctlogs := trustedMaterial.CTLogs()
3433
fulcioCerts := trustedMaterial.FulcioCertificateAuthorities()
3534

35+
leafCert := verificationContent.GetCertificate()
36+
3637
scts, err := x509util.ParseSCTsFromCertificate(leafCert.Raw)
3738
if err != nil {
3839
return err
@@ -56,6 +57,15 @@ func VerifySignedCertificateTimestamp(leafCert *x509.Certificate, threshold int,
5657
fulcioChain := make([]*ctx509.Certificate, len(leafCTCert))
5758
copy(fulcioChain, leafCTCert)
5859

60+
bundleIntermediates := verificationContent.GetCertificateChain()
61+
for _, cert := range bundleIntermediates {
62+
convertedCert, err := ctx509.ParseCertificate(cert.Raw)
63+
if err != nil {
64+
continue
65+
}
66+
fulcioChain = append(fulcioChain, convertedCert)
67+
}
68+
5969
var parentCert []byte
6070

6171
if len(fulcioCa.Intermediates) == 0 {

0 commit comments

Comments
 (0)