diff --git a/wolfx509/certgen.go b/wolfx509/certgen.go index bd37eae..6e525ac 100644 --- a/wolfx509/certgen.go +++ b/wolfx509/certgen.go @@ -83,6 +83,7 @@ func CreateCertificate(template, parent *Certificate, pubKey, signer KeyHandle) DNSNames: template.DNSNames, IPAddresses: template.IPAddresses, AcmeKeyAuth: template.AcmeKeyAuth, + BasicConstraintsValid: template.BasicConstraintsValid, } if template == parent && pubKey == signer { diff --git a/wolfx509/certgen_test.go b/wolfx509/certgen_test.go index 16cf2a7..d4b0a05 100644 --- a/wolfx509/certgen_test.go +++ b/wolfx509/certgen_test.go @@ -22,6 +22,8 @@ package wolfx509 import ( "bytes" + "encoding/asn1" + "encoding/hex" "math/big" "net" "testing" @@ -448,3 +450,74 @@ func TestCreateCertificateValidDays(t *testing.T) { t.Errorf("NotAfter = %s, want within [%s, %s]", parsed.NotAfter, naLo, naHi) } } + +// oidBasicConstraints is id-ce-basicConstraints (RFC 5280 4.2.1.9). +var oidBasicConstraints = asn1.ObjectIdentifier{2, 5, 29, 19} + +// TestBasicConstraintsMatrix pins the BasicConstraints extension that is +// emitted for a given template: +// - emitted only when BasicConstraintsValid is set, +// - always critical, +// - present but with the cA boolean omitted (an empty SEQUENCE) when IsCA +// is false. +func TestBasicConstraintsMatrix(t *testing.T) { + for _, tc := range []struct { + name string + bcValid bool + isCA bool + wantPresent bool + wantValue string // hex of the extension value + }{ + {"valid+CA", true, true, true, "30030101ff"}, + {"valid+notCA", true, false, true, "3000"}, + {"notValid+CA", false, true, false, ""}, + {"notValid+notCA", false, false, false, ""}, + } { + t.Run(tc.name, func(t *testing.T) { + k, err := GenerateP256Key() + if err != nil { + t.Fatalf("GenerateP256Key: %v", err) + } + defer k.Free() + + tmpl := &Certificate{ + SerialNumber: big.NewInt(1), + Subject: Name{CommonName: "bc test"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + BasicConstraintsValid: tc.bcValid, + IsCA: tc.isCA, + } + der, err := CreateCertificate(tmpl, tmpl, k, k) + if err != nil { + t.Fatalf("CreateCertificate: %v", err) + } + parsed, err := stdlibParseCert(der) + if err != nil { + t.Fatalf("stdlibParseCert: %v", err) + } + + for _, ext := range parsed.Extensions { + if !ext.Id.Equal(oidBasicConstraints) { + continue + } + if !tc.wantPresent { + t.Fatalf("BasicConstraints emitted for BasicConstraintsValid=false") + } + if !ext.Critical { + t.Error("BasicConstraints is not critical; RFC 5280 4.2.1.9 requires it of CA certs and crypto/x509 always marks it") + } + if got := hex.EncodeToString(ext.Value); got != tc.wantValue { + t.Errorf("BasicConstraints value = %s, want %s", got, tc.wantValue) + } + if parsed.IsCA != tc.isCA { + t.Errorf("parsed IsCA = %v, want %v", parsed.IsCA, tc.isCA) + } + return + } + if tc.wantPresent { + t.Fatal("BasicConstraints extension missing") + } + }) + } +} diff --git a/wolfx509/certgen_wolfcrypt.go b/wolfx509/certgen_wolfcrypt.go index b7389fe..a12302b 100644 --- a/wolfx509/certgen_wolfcrypt.go +++ b/wolfx509/certgen_wolfcrypt.go @@ -51,6 +51,12 @@ package wolfx509 // return -174; // } // #endif +// +// /* Cert.basicConstSet is a C bitfield, which cgo cannot address +// * without a setter. */ +// static void wolfx509_set_basic_const(Cert* cert, int toggle) { +// cert->basicConstSet = (toggle != 0); +// } import "C" import ( "encoding/asn1" @@ -120,6 +126,9 @@ type certBuildOpts struct { DNSNames []string // SubjectAltName dNSName entries IPAddresses []net.IP // SubjectAltName iPAddress entries + // BasicConstraintsValid requests the BasicConstraints extension. + BasicConstraintsValid bool + // AcmeKeyAuth, if non-empty, sets the RFC 8737 id-pe-acmeIdentifier // (1.3.6.1.5.5.7.1.31) extension on the cert. Pass the raw keyAuth // bytes (token "." JWK_thumbprint per RFC 8555 ยง8.1); wolfCrypt @@ -182,8 +191,16 @@ func buildAndSignCert(opts certBuildOpts, parentDER []byte, pubKey, signerKey Ke } cert.version = 2 cert.sigType = C.int(sigType) - if opts.IsCA { - cert.isCA = 1 + // BasicConstraints: emitted only when BasicConstraintsValid is set, has to + // be marked CRITICAL if set. + // When IsCA is TRUE, wolfSSL will automatically encode the extension. + if opts.BasicConstraintsValid { + cert.basicConstCrit = 1 + if opts.IsCA { + cert.isCA = 1 + } else { + C.wolfx509_set_basic_const(&cert, 1) + } } cert.keyUsage = C.word16(opts.KeyUsage) cert.extKeyUsage = C.byte(opts.ExtKeyUsage)