Skip to content

Commit 787b9ce

Browse files
committed
Implement legacy name constraints verification
These changes reproduce the Go 1.14 name constraint verification in the MSP. Without these changes, certificate chains that would fail verification in Go 1.14 would successfully validate in Go 1.15. Signed-off-by: Matthew Sykes <[email protected]>
1 parent e209720 commit 787b9ce

File tree

2 files changed

+200
-28
lines changed

2 files changed

+200
-28
lines changed

msp/msp_test.go

Lines changed: 100 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,33 @@ func computeSKI(key *ecdsa.PublicKey) []byte {
294294
return hash[:]
295295
}
296296

297+
func TestValidHostname(t *testing.T) {
298+
tests := []struct {
299+
name string
300+
valid bool
301+
}{
302+
{"", false},
303+
{".", false},
304+
{"example.com", true},
305+
{"example.com.", true},
306+
{"*.example.com", true},
307+
{".example.com", false},
308+
{"host.*.example.com", false},
309+
{"localhost", true},
310+
{"-localhost", false},
311+
{"Not_Quite.example.com", true},
312+
{"weird:colon.example.com", true},
313+
{"1-2-3.example.com", true},
314+
}
315+
for _, tt := range tests {
316+
if tt.valid {
317+
require.True(t, validHostname(tt.name), "expected %s to be a valid hostname", tt.name)
318+
} else {
319+
require.False(t, validHostname(tt.name), "expected %s to be an invalid hostname", tt.name)
320+
}
321+
}
322+
}
323+
297324
func TestValidateCANameConstraintsMitigation(t *testing.T) {
298325
// Prior to Go 1.15, if a signing certificate contains a name constraint, the
299326
// leaf certificate does not include a SAN, and the leaf common name looks
@@ -320,9 +347,9 @@ func TestValidateCANameConstraintsMitigation(t *testing.T) {
320347
KeyUsage: caKeyUsage,
321348
SubjectKeyId: computeSKI(caKey.Public().(*ecdsa.PublicKey)),
322349
}
323-
certBytes, err := x509.CreateCertificate(rand.Reader, &caTemplate, &caTemplate, caKey.Public(), caKey)
350+
caCertBytes, err := x509.CreateCertificate(rand.Reader, &caTemplate, &caTemplate, caKey.Public(), caKey)
324351
require.NoError(t, err)
325-
ca, err := x509.ParseCertificate(certBytes)
352+
ca, err := x509.ParseCertificate(caCertBytes)
326353
require.NoError(t, err)
327354

328355
leafTemplate := x509.Certificate{
@@ -339,37 +366,82 @@ func TestValidateCANameConstraintsMitigation(t *testing.T) {
339366
keyBytes, err := x509.MarshalPKCS8PrivateKey(leafKey)
340367
require.NoError(t, err)
341368

342-
caCertPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
369+
caCertPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCertBytes})
343370
leafCertPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCertBytes})
344371
leafKeyPem := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
345-
fabricMSPConfig := &msp.FabricMSPConfig{
346-
Name: "ConstraintsMSP",
347-
RootCerts: [][]byte{caCertPem},
348-
SigningIdentity: &msp.SigningIdentityInfo{
349-
PublicSigner: leafCertPem,
350-
PrivateSigner: &msp.KeyInfo{
351-
KeyIdentifier: "Certificate Without SAN",
352-
KeyMaterial: leafKeyPem,
353-
},
354-
},
355-
}
356-
mspConfig := &msp.MSPConfig{
357-
Config: protoutil.MarshalOrPanic(fabricMSPConfig),
358-
}
359372

360-
ks, err := sw.NewFileBasedKeyStore(nil, filepath.Join(configtest.GetDevMspDir(), "keystore"), true)
361-
require.NoError(t, err)
362-
cryptoProvider, err := sw.NewDefaultSecurityLevelWithKeystore(ks)
363-
require.NoError(t, err)
373+
t.Run("VerifyNameConstraintsSingleCert", func(t *testing.T) {
374+
for _, der := range [][]byte{caCertBytes, leafCertBytes} {
375+
cert, err := x509.ParseCertificate(der)
376+
require.NoError(t, err, "failed to parse certificate")
364377

365-
testMSP, err := NewBccspMspWithKeyStore(MSPv1_0, ks, cryptoProvider)
366-
require.NoError(t, err)
378+
err = verifyLegacyNameConstraints([]*x509.Certificate{cert})
379+
require.NoError(t, err, "single certificate should not trigger legacy constraints")
380+
}
381+
})
367382

368-
err = testMSP.Setup(mspConfig)
369-
require.Error(t, err)
370-
var cie x509.CertificateInvalidError
371-
require.True(t, errors.As(err, &cie))
372-
require.Equal(t, x509.NameConstraintsWithoutSANs, cie.Reason)
383+
t.Run("VerifyNameConstraints", func(t *testing.T) {
384+
var certs []*x509.Certificate
385+
for _, der := range [][]byte{leafCertBytes, caCertBytes} {
386+
cert, err := x509.ParseCertificate(der)
387+
require.NoError(t, err, "failed to parse certificate")
388+
certs = append(certs, cert)
389+
}
390+
391+
err = verifyLegacyNameConstraints(certs)
392+
require.Error(t, err, "certificate chain should trigger legacy constraints")
393+
var cie x509.CertificateInvalidError
394+
require.True(t, errors.As(err, &cie))
395+
require.Equal(t, x509.NameConstraintsWithoutSANs, cie.Reason)
396+
})
397+
398+
t.Run("VerifyNameConstraintsWithSAN", func(t *testing.T) {
399+
caCert, err := x509.ParseCertificate(caCertBytes)
400+
require.NoError(t, err)
401+
402+
leafTemplate := leafTemplate
403+
leafTemplate.DNSNames = []string{"localhost"}
404+
405+
leafCertBytes, err := x509.CreateCertificate(rand.Reader, &leafTemplate, caCert, leafKey.Public(), caKey)
406+
require.NoError(t, err)
407+
408+
leafCert, err := x509.ParseCertificate(leafCertBytes)
409+
require.NoError(t, err)
410+
411+
err = verifyLegacyNameConstraints([]*x509.Certificate{leafCert, caCert})
412+
require.NoError(t, err, "signer with name constraints and leaf with SANs should be valid")
413+
})
414+
415+
t.Run("ValidationAtSetup", func(t *testing.T) {
416+
fabricMSPConfig := &msp.FabricMSPConfig{
417+
Name: "ConstraintsMSP",
418+
RootCerts: [][]byte{caCertPem},
419+
SigningIdentity: &msp.SigningIdentityInfo{
420+
PublicSigner: leafCertPem,
421+
PrivateSigner: &msp.KeyInfo{
422+
KeyIdentifier: "Certificate Without SAN",
423+
KeyMaterial: leafKeyPem,
424+
},
425+
},
426+
}
427+
mspConfig := &msp.MSPConfig{
428+
Config: protoutil.MarshalOrPanic(fabricMSPConfig),
429+
}
430+
431+
ks, err := sw.NewFileBasedKeyStore(nil, filepath.Join(configtest.GetDevMspDir(), "keystore"), true)
432+
require.NoError(t, err)
433+
cryptoProvider, err := sw.NewDefaultSecurityLevelWithKeystore(ks)
434+
require.NoError(t, err)
435+
436+
testMSP, err := NewBccspMspWithKeyStore(MSPv1_0, ks, cryptoProvider)
437+
require.NoError(t, err)
438+
439+
err = testMSP.Setup(mspConfig)
440+
require.Error(t, err)
441+
var cie x509.CertificateInvalidError
442+
require.True(t, errors.As(err, &cie))
443+
require.Equal(t, x509.NameConstraintsWithoutSANs, cie.Reason)
444+
})
373445
}
374446

375447
func TestIsWellFormed(t *testing.T) {

msp/mspimpl.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import (
1010
"bytes"
1111
"crypto/x509"
1212
"crypto/x509/pkix"
13+
"encoding/asn1"
1314
"encoding/hex"
1415
"encoding/pem"
16+
"strings"
1517

1618
"github.com/golang/protobuf/proto"
1719
m "github.com/hyperledger/fabric-protos-go/msp"
@@ -735,9 +737,107 @@ func (msp *bccspmsp) getUniqueValidationChain(cert *x509.Certificate, opts x509.
735737
return nil, errors.Errorf("this MSP only supports a single validation chain, got %d", len(validationChains))
736738
}
737739

740+
// Make the additional verification checks that were done in Go 1.14.
741+
err = verifyLegacyNameConstraints(validationChains[0])
742+
if err != nil {
743+
return nil, errors.WithMessage(err, "the supplied identity is not valid")
744+
}
745+
738746
return validationChains[0], nil
739747
}
740748

749+
var (
750+
oidExtensionSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17}
751+
oidExtensionNameConstraints = asn1.ObjectIdentifier{2, 5, 29, 30}
752+
)
753+
754+
// verifyLegacyNameConstraints exercises the name constraint validation rules
755+
// that were part of the certificate verification process in Go 1.14.
756+
//
757+
// If a signing certificate contains a name constratint, the leaf certificate
758+
// does not include SAN extensions, and the leaf's common name looks like a
759+
// host name, the validation would fail with an x509.CertificateInvalidError
760+
// and a rason of x509.NameConstraintsWithoutSANs.
761+
func verifyLegacyNameConstraints(chain []*x509.Certificate) error {
762+
if len(chain) < 2 {
763+
return nil
764+
}
765+
766+
// Leaf certificates with SANs are fine.
767+
if oidInExtensions(oidExtensionSubjectAltName, chain[0].Extensions) {
768+
return nil
769+
}
770+
// Leaf certificates without a hostname in CN are fine.
771+
if !validHostname(chain[0].Subject.CommonName) {
772+
return nil
773+
}
774+
// If an intermediate or root have a name constraint, validation
775+
// would fail in Go 1.14.
776+
for _, c := range chain[1:] {
777+
if oidInExtensions(oidExtensionNameConstraints, c.Extensions) {
778+
return x509.CertificateInvalidError{Cert: chain[0], Reason: x509.NameConstraintsWithoutSANs}
779+
}
780+
}
781+
return nil
782+
}
783+
784+
func oidInExtensions(oid asn1.ObjectIdentifier, exts []pkix.Extension) bool {
785+
for _, ext := range exts {
786+
if ext.Id.Equal(oid) {
787+
return true
788+
}
789+
}
790+
return false
791+
}
792+
793+
// validHostname reports whether host is a valid hostname that can be matched or
794+
// matched against according to RFC 6125 2.2, with some leniency to accommodate
795+
// legacy values.
796+
//
797+
// This implementation is sourced from the standaard library.
798+
func validHostname(host string) bool {
799+
host = strings.TrimSuffix(host, ".")
800+
801+
if len(host) == 0 {
802+
return false
803+
}
804+
805+
for i, part := range strings.Split(host, ".") {
806+
if part == "" {
807+
// Empty label.
808+
return false
809+
}
810+
if i == 0 && part == "*" {
811+
// Only allow full left-most wildcards, as those are the only ones
812+
// we match, and matching literal '*' characters is probably never
813+
// the expected behavior.
814+
continue
815+
}
816+
for j, c := range part {
817+
if 'a' <= c && c <= 'z' {
818+
continue
819+
}
820+
if '0' <= c && c <= '9' {
821+
continue
822+
}
823+
if 'A' <= c && c <= 'Z' {
824+
continue
825+
}
826+
if c == '-' && j != 0 {
827+
continue
828+
}
829+
if c == '_' || c == ':' {
830+
// Not valid characters in hostnames, but commonly
831+
// found in deployments outside the WebPKI.
832+
continue
833+
}
834+
return false
835+
}
836+
}
837+
838+
return true
839+
}
840+
741841
func (msp *bccspmsp) getValidationChain(cert *x509.Certificate, isIntermediateChain bool) ([]*x509.Certificate, error) {
742842
validationChain, err := msp.getUniqueValidationChain(cert, msp.getValidityOptsForCert(cert))
743843
if err != nil {

0 commit comments

Comments
 (0)