New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for Must-Staple / TLS-Feature #1224
Changes from 35 commits
6b5101c
02a98e8
b4f202f
580c79c
b48d3a3
dfc7021
54e336e
a439df4
f95c5bb
b552d24
0103dbb
d05e908
eb5a00f
02595e0
55d1795
e3daa6c
de8f1f5
2e74211
3411e9d
65ea6f6
5020352
e588c52
d2f3a89
22bc89f
c1f1649
14fbf3b
e243442
746fc88
4c3f14b
06ac584
51c1a1c
0afa843
d9fdfac
4157112
1ec567e
5d35967
4e31d80
42782be
f1ccf2f
edd2471
4f81e8c
1d62ca8
8a79e48
901f651
6fb913a
File filter...
Jump to…
@@ -6,11 +6,13 @@ | ||
package ca | ||
|
||
import ( | ||
"bytes" | ||
"crypto" | ||
"crypto/ecdsa" | ||
"crypto/rand" | ||
"crypto/rsa" | ||
"crypto/x509" | ||
"encoding/asn1" | ||
"encoding/base64" | ||
"encoding/hex" | ||
"encoding/json" | ||
@@ -50,13 +52,67 @@ var badSignatureAlgorithms = map[x509.SignatureAlgorithm]bool{ | ||
x509.ECDSAWithSHA1: true, | ||
} | ||
|
||
// Miscellaneous PKIX OIDs that we need to refer to | ||
var ( | ||
// X.509 Extensions | ||
oidAuthorityInfoAccess = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 1} | ||
oidAuthorityKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 35} | ||
oidBasicConstraints = asn1.ObjectIdentifier{2, 5, 29, 19} | ||
oidCertificatePolicies = asn1.ObjectIdentifier{2, 5, 29, 32} | ||
oidCrlDistributionPoints = asn1.ObjectIdentifier{2, 5, 29, 31} | ||
oidExtKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37} | ||
oidKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 15} | ||
oidSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17} | ||
oidSubjectKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 14} | ||
oidTLSFeature = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24} | ||
|
||
// CSR attribute requesting extensions | ||
oidExtensionRequest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 14} | ||
) | ||
|
||
// 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 ( | ||
mustStapleFeatureValue = []byte{0x30, 0x03, 0x02, 0x01, 0x05} | ||
mustStapleExtension = signer.Extension{ | ||
ID: cfsslConfig.OID(oidTLSFeature), | ||
Critical: false, | ||
Value: hex.EncodeToString(mustStapleFeatureValue), | ||
} | ||
) | ||
|
||
// Metrics for CA statistics | ||
const ( | ||
// Increments when CA observes an HSM fault | ||
metricHSMFaultObserved = "CA.OCSP.HSMFault.Observed" | ||
|
||
// Increments when CA rejects a request due to an HSM fault | ||
metricHSMFaultRejected = "CA.OCSP.HSMFault.Rejected" | ||
|
||
// Increments when CA handles a CSR requesting a "basic" extension: | ||
// authorityInfoAccess, authorityKeyIdentifier, extKeyUsage, keyUsage, | ||
// basicConstraints, certificatePolicies, crlDistributionPoints, | ||
// subjectAlternativeName, subjectKeyIdentifier, | ||
metricCSRExtensionBasic = "CA.OCSP.CSRExtensions.Basic" | ||
|
||
// Increments when CA handles a CSR requesting a TLS Feature extension | ||
metricCSRExtensionTLSFeature = "CA.OCSP.CSRExtensions.TLSFeature" | ||
|
||
// Increments when CA handles a CSR requesting a TLS Feature extension with | ||
// an invalid value | ||
metricCSRExtensionTLSFeatureInvalid = "CA.OCSP.CSRExtensions.TLSFeatureInvalid" | ||
|
||
// Increments when CA handles a CSR requesting an extension other than those | ||
// listed above | ||
metricCSRExtensionOther = "CA.OCSP.CSRExtensions.Other" | ||
) | ||
|
||
// CertificateAuthorityImpl represents a CA that signs certificates, CRLs, and | ||
@@ -216,6 +272,57 @@ func (ca *CertificateAuthorityImpl) noteHSMFault(err error) { | ||
return | ||
} | ||
|
||
// Extract supported extensions from a CSR. The following extensions are | ||
// currently supported: | ||
// | ||
// * 1.3.6.1.5.5.7.1.24 - TLS Feature [RFC7633], with the "must staple" value. | ||
// Any other value will result in an error. | ||
// | ||
// Other requested extensions are silently ignored. | ||
func (ca *CertificateAuthorityImpl) extensionsFromCSR(csr *x509.CertificateRequest) (extensions []signer.Extension, err error) { | ||
extensions = []signer.Extension{} | ||
for _, attr := range csr.Attributes { | ||
if !attr.Type.Equal(oidExtensionRequest) { | ||
continue | ||
} | ||
|
||
for _, extList := range attr.Value { | ||
for _, ext := range extList { | ||
switch { | ||
case ext.Type.Equal(oidTLSFeature): | ||
ca.stats.Inc(metricCSRExtensionTLSFeature, 1, 1.0) | ||
value, ok := ext.Value.([]byte) | ||
if !ok { | ||
msg := fmt.Sprintf("Mal-formed extension with OID %v", ext.Type) | ||
err = core.CertificateIssuanceError(msg) | ||
return | ||
|
||
} else if !bytes.Equal(value, mustStapleFeatureValue) { | ||
msg := fmt.Sprintf("Unsupported value for extension with OID %v", ext.Type) | ||
ca.stats.Inc(metricCSRExtensionTLSFeatureInvalid, 1, 1.0) | ||
err = core.CertificateIssuanceError(msg) | ||
return | ||
} | ||
|
||
extensions = append(extensions, mustStapleExtension) | ||
rolandshoemaker
Member
|
||
case ext.Type.Equal(oidAuthorityInfoAccess), | ||
ext.Type.Equal(oidAuthorityKeyIdentifier), | ||
ext.Type.Equal(oidBasicConstraints), | ||
ext.Type.Equal(oidCertificatePolicies), | ||
ext.Type.Equal(oidCrlDistributionPoints), | ||
ext.Type.Equal(oidExtKeyUsage), | ||
ext.Type.Equal(oidKeyUsage), | ||
ext.Type.Equal(oidSubjectAltName), | ||
ext.Type.Equal(oidSubjectKeyIdentifier): | ||
ca.stats.Inc(metricCSRExtensionBasic, 1, 1.0) | ||
jsha
Contributor
|
||
default: | ||
ca.stats.Inc(metricCSRExtensionOther, 1, 1.0) | ||
} | ||
} | ||
} | ||
} | ||
return | ||
} | ||
|
||
// GenerateOCSP produces a new OCSP response and returns it | ||
func (ca *CertificateAuthorityImpl) GenerateOCSP(xferObj core.OCSPSigningRequest) ([]byte, error) { | ||
if err := ca.checkHSMFault(); err != nil { | ||
@@ -315,6 +422,11 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest | ||
} | ||
} | ||
|
||
requestedExtensions, err := ca.extensionsFromCSR(&csr) | ||
if err != nil { | ||
return emptyCert, err | ||
} | ||
|
||
notAfter := ca.clk.Now().Add(ca.validityPeriod) | ||
|
||
if ca.notAfter.Before(notAfter) { | ||
@@ -366,7 +478,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) | ||
@@ -96,6 +96,24 @@ var ( | ||
// Edited signature to become invalid. | ||
WrongSignatureCSR = mustRead("./testdata/invalid_signature.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") | ||
jsha
Contributor
|
||
|
||
// 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") | ||
|
||
// CSR generated by Go: | ||
// * Random ECDSA public key. | ||
// * CN = [none] | ||
@@ -204,6 +222,9 @@ func setup(t *testing.T) *testCtx { | ||
PublicKey: true, | ||
SignatureAlgorithm: true, | ||
}, | ||
AllowedExtensions: []cfsslConfig.OID{ | ||
cfsslConfig.OID(oidTLSFeature), | ||
}, | ||
}, | ||
ecdsaProfileName: &cfsslConfig.SigningProfile{ | ||
Usage: []string{"digital signature", "server auth"}, | ||
@@ -591,3 +612,51 @@ 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, caCert, caKey, ctx.keyPolicy) | ||
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") | ||
parsedCert1, err := x509.ParseCertificate(cert.DER) | ||
test.AssertNotError(t, err, "Error parsing certificate produced by CA") | ||
|
||
foundMustStaple := false | ||
for _, ext := range parsedCert1.Extensions { | ||
if ext.Id.Equal(oidTLSFeature) { | ||
foundMustStaple = true | ||
test.Assert(t, !ext.Critical, "Extension was marked critical") | ||
test.AssertByteEquals(t, ext.Value, mustStapleFeatureValue) | ||
} | ||
} | ||
test.Assert(t, foundMustStaple, "TLS Feature extension not found") | ||
test.AssertEquals(t, ctx.stats.Counters[metricCSRExtensionTLSFeature], int64(1)) | ||
|
||
// ... but if it doesn't ask for stapling, there should be an error | ||
csr, _ = x509.ParseCertificateRequest(TLSFeatureUnknownCSR) | ||
cert, err = ca.IssueCertificate(*csr, ctx.reg.ID) | ||
test.AssertError(t, err, "Allowed a CSR with an empty TLS feature extension") | ||
test.AssertEquals(t, ctx.stats.Counters[metricCSRExtensionTLSFeature], int64(2)) | ||
test.AssertEquals(t, ctx.stats.Counters[metricCSRExtensionTLSFeatureInvalid], int64(1)) | ||
|
||
// Unsupported extensions should be silently ignored, having the same | ||
// extensions as the TLS Feature cert above, minus the TLS Feature Extension | ||
csr, _ = x509.ParseCertificateRequest(UnsupportedExtensionCSR) | ||
cert, err = ca.IssueCertificate(*csr, ctx.reg.ID) | ||
test.AssertNotError(t, err, "Failed to gracefully handle a CSR with an unknown extension") | ||
parsedCert2, err := x509.ParseCertificate(cert.DER) | ||
test.AssertNotError(t, err, "Error parsing certificate produced by CA") | ||
test.AssertEquals(t, len(parsedCert2.Extensions), len(parsedCert1.Extensions)-1) | ||
test.AssertEquals(t, ctx.stats.Counters[metricCSRExtensionOther], int64(1)) | ||
|
||
// None of the above CSRs have basic extensions | ||
test.AssertEquals(t, ctx.stats.Counters[metricCSRExtensionBasic], int64(0)) | ||
} |
This method is too long for naked returns. Please use explicit
return nil, core.CertificateIssuanceError(msg)
, and remove the named return types.