Skip to content

Commit

Permalink
Add support for the must-staple extension
Browse files Browse the repository at this point in the history
  • Loading branch information
bifurcation committed Dec 4, 2015
1 parent 6b5101c commit 02a98e8
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 1 deletion.
37 changes: 36 additions & 1 deletion ca/certificate-authority.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"crypto"
"crypto/rand"
"crypto/x509"
"encoding/asn1"
"encoding/hex"
"encoding/json"
"encoding/pem"
Expand Down Expand Up @@ -47,6 +48,19 @@ var badSignatureAlgorithms = map[x509.SignatureAlgorithm]bool{
x509.ECDSAWithSHA1: true,
}

// OID and fixed value for the "must staple" variant of the TLS Feature
// extension:
//
// Features ::= SEQUENCE OF INTEGER [RFC7633]
// enum { ... status_request(5) ...} ExtensionType; [RFC6066]
//
// DER Encoding:
// 30 03 - SEQUENCE (3 octets)
// |-- 02 01 - INTEGER (1 octet)
// | |-- 05 - 5
var oidTLSFeature = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
var mustStapleFeatureValue = "0303020105"

// Metrics for CA statistics
const (
// Increments when CA observes an HSM fault
Expand Down Expand Up @@ -298,6 +312,26 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
}
}

// Process requested extensions. For now, the only extension we support is
// TLS Feature [RFC7633], i.e., "Must Staple", and for that we ignore the
// value provided by the client and just overwrite it with the specific value
// that only requires stapling.
//
// Other requested extensions are silently ignored.
//
// XXX(rlb): It might be good to add a generic mechanism for extensions, but
// on the other hand, we probably *don't* want to copy arbitrary client-
// provided data into extensions.
extensions := core.ExtensionsFromCSR(&csr)
requestedExtensions := []signer.Extension{}
if _, present := extensions[oidTLSFeature.String()]; present {
requestedExtensions = append(requestedExtensions, signer.Extension{
ID: cfsslConfig.OID(oidTLSFeature),
Critical: false,
Value: mustStapleFeatureValue,
})
}

notAfter := ca.clk.Now().Add(ca.validityPeriod)

if ca.notAfter.Before(notAfter) {
Expand Down Expand Up @@ -336,7 +370,8 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
Subject: &signer.Subject{
CN: commonName,
},
Serial: serialBigInt,
Serial: serialBigInt,
Extensions: requestedExtensions,
}

certPEM, err := ca.signer.Sign(req)
Expand Down
79 changes: 79 additions & 0 deletions ca/certificate-authority_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"crypto"
"crypto/x509"
"encoding/asn1"
"encoding/hex"
"fmt"
"io/ioutil"
"sort"
Expand Down Expand Up @@ -92,6 +93,24 @@ var (
// * DNSNames = moreCAPs.com, morecaps.com, evenMOREcaps.com, Capitalizedletters.COM
CapitalizedCSR = mustRead("./testdata/capitalized_cn_and_san.der.csr")

// CSR generated by Go:
// * Random public key
// * CN = not-example.com
// * Includes an extensionRequest attribute for a well-formed TLS Feature extension
MustStapleCSR = mustRead("./testdata/must_staple.der.csr")

// CSR generated by Go:
// * Random public key
// * CN = not-example.com
// * Includes an extensionRequest attribute for an empty TLS Feature extension
TLSFeatureUnknownCSR = mustRead("./testdata/tls_feature_unknown.der.csr")

// CSR generated by Go:
// * Random public key
// * CN = not-example.com
// * Includes an extensionRequest attribute for the CT Poison extension (not supported)
UnsupportedExtensionCSR = mustRead("./testdata/unsupported_extension.der.csr")

log = mocks.UseMockLog()
)

Expand Down Expand Up @@ -191,6 +210,9 @@ func setup(t *testing.T) *testCtx {
PublicKey: true,
SignatureAlgorithm: true,
},
AllowedExtensions: []cfsslConfig.OID{
cfsslConfig.OID(oidTLSFeature),
},
},
},
Default: &cfsslConfig.SigningProfile{
Expand Down Expand Up @@ -499,3 +521,60 @@ func TestHSMFaultTimeout(t *testing.T) {
test.AssertEquals(t, ctx.stats.Counters[metricHSMFaultObserved], int64(2))
test.AssertEquals(t, ctx.stats.Counters[metricHSMFaultRejected], int64(4))
}

func TestExtensions(t *testing.T) {
ctx := setup(t)
defer ctx.cleanUp()
ctx.caConfig.MaxNames = 3
ca, err := NewCertificateAuthorityImpl(ctx.caConfig, ctx.fc, ctx.stats, caCertFile)
ca.Publisher = &mocks.Publisher{}
ca.PA = ctx.pa
ca.SA = ctx.sa

// A TLS feature extension should put a must-staple extension into the cert
csr, _ := x509.ParseCertificateRequest(MustStapleCSR)
cert, err := ca.IssueCertificate(*csr, ctx.reg.ID)
test.AssertNotError(t, err, "Failed to gracefully handle a CSR with must_staple")
parsedCert, err := x509.ParseCertificate(cert.DER)
test.AssertNotError(t, err, "Error parsing certificate produced by CA")

// We already expect 8 extensions: extKeyUsage, basicConstraints,
// subjectKeyIdentifier, authorityKeyIdentifier, subjectAlternativeName,
// crlDistributionPoints, authorityInfoAccess, certificatePolicies
test.AssertEquals(t, len(parsedCert.Extensions), 9)
foundMustStaple := false
for _, ext := range parsedCert.Extensions {
if ext.Id.Equal(oidTLSFeature) {
foundMustStaple = true
test.Assert(t, !ext.Critical, "Extension was marked critical")
test.AssertEquals(t, hex.EncodeToString(ext.Value), mustStapleFeatureValue)
}
}
test.Assert(t, foundMustStaple, "TLS Feature extension not found")

// ... even if it doesn't ask for stapling
csr, _ = x509.ParseCertificateRequest(TLSFeatureUnknownCSR)
cert, err = ca.IssueCertificate(*csr, ctx.reg.ID)
test.AssertNotError(t, err, "Failed to gracefully handle a CSR with an empty TLS feature extension")
parsedCert, err = x509.ParseCertificate(cert.DER)
test.AssertNotError(t, err, "Error parsing certificate produced by CA")

test.AssertEquals(t, len(parsedCert.Extensions), 9)
foundMustStaple = false
for _, ext := range parsedCert.Extensions {
if ext.Id.Equal(oidTLSFeature) {
foundMustStaple = true
test.Assert(t, !ext.Critical, "Extension was marked critical")
test.AssertEquals(t, hex.EncodeToString(ext.Value), mustStapleFeatureValue)
}
}
test.Assert(t, foundMustStaple, "TLS Feature extension not found")

// Unsupported extensions should be ignored
csr, _ = x509.ParseCertificateRequest(UnsupportedExtensionCSR)
cert, err = ca.IssueCertificate(*csr, ctx.reg.ID)
test.AssertNotError(t, err, "Failed to gracefully handle a CSR with an unsupported extension")
parsedCert, err = x509.ParseCertificate(cert.DER)
test.AssertNotError(t, err, "Error parsing certificate produced by CA")
test.AssertEquals(t, len(parsedCert.Extensions), 8)
}
30 changes: 30 additions & 0 deletions core/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,36 @@ func VerifyCSR(csr *x509.CertificateRequest) error {
return errors.New("Unsupported CSR signing algorithm")
}

var (
oidExtensionRequest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 14}
oidSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17}
)

// ExtensionsFromCSR extracts the set of requested extensions from a CSR
func ExtensionsFromCSR(csr *x509.CertificateRequest) map[string][]byte {
extensionMap := map[string][]byte{}
for _, attr := range csr.Attributes {
if !attr.Type.Equal(oidExtensionRequest) || len(attr.Value) != 1 {
continue
}

for _, ext := range attr.Value[0] {
// SubjectAltName is already handled by ParseCertificateRequest
if ext.Type.Equal(oidSubjectAltName) {
continue
}

extValue, ok := ext.Value.([]byte)
if !ok {
continue
}

extensionMap[ext.Type.String()] = extValue
}
}
return extensionMap
}

// SerialToString converts a certificate serial number (big.Int) to a String
// consistently.
func SerialToString(serial *big.Int) string {
Expand Down

0 comments on commit 02a98e8

Please sign in to comment.