diff --git a/sdk/helper/certutil/certutil_test.go b/sdk/helper/certutil/certutil_test.go index 013af92461..79936b2134 100644 --- a/sdk/helper/certutil/certutil_test.go +++ b/sdk/helper/certutil/certutil_test.go @@ -20,6 +20,7 @@ import ( "fmt" "math/big" mathrand "math/rand" + "net" "reflect" "strings" "sync" @@ -27,6 +28,7 @@ import ( "time" "github.com/fatih/structs" + "github.com/go-test/deep" "github.com/hashicorp/vault/sdk/helper/cryptoutil" ) @@ -1107,6 +1109,114 @@ func TestIgnoreCSRSigning(t *testing.T) { }) } +// TestSignIntermediat_name_constraints verifies that all the name constraints extension fields are +// used when signing a certificate. +func TestSignCertificate_name_constraints(t *testing.T) { + t.Parallel() + + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("failed generating ca key: %v", err) + } + subjKeyID, err := GetSubjKeyID(caKey) + if err != nil { + t.Fatalf("failed generating ca subject key id: %v", err) + } + caCertTemplate := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: "root.localhost", + }, + SubjectKeyId: subjKeyID, + DNSNames: []string{"root.localhost"}, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + SerialNumber: big.NewInt(mathrand.Int63()), + NotBefore: time.Now().Add(-30 * time.Second), + NotAfter: time.Now().Add(262980 * time.Hour), + BasicConstraintsValid: true, + IsCA: true, + } + caBytes, err := x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, caKey.Public(), caKey) + if err != nil { + t.Fatalf("failed creating ca certificate: %v", err) + } + caCert, err := x509.ParseCertificate(caBytes) + if err != nil { + t.Fatalf("failed parsing ca certificate: %v", err) + } + + signingBundle := &CAInfoBundle{ + ParsedCertBundle: ParsedCertBundle{ + PrivateKeyType: ECPrivateKey, + PrivateKey: caKey, + CertificateBytes: caBytes, + Certificate: caCert, + CAChain: nil, + }, + URLs: &URLEntries{}, + } + + key := genEdDSA(t) + csr := &x509.CertificateRequest{ + PublicKeyAlgorithm: x509.ECDSA, + PublicKey: key.Public(), + Subject: pkix.Name{ + CommonName: "test.dadgarcorp.com", + }, + } + _, ipnet1, err := net.ParseCIDR("1.2.3.4/32") + if err != nil { + t.Fatal(err) + } + _, ipnet2, err := net.ParseCIDR("1.2.3.4/16") + if err != nil { + t.Fatal(err) + } + params := &CreationParameters{ + IgnoreCSRSignature: true, + URLs: &URLEntries{}, + NotAfter: time.Now().Add(10000 * time.Hour), + PermittedDNSDomains: []string{"example.com", ".example.com"}, + ExcludedDNSDomains: []string{"bad.example.com"}, + PermittedIPRanges: []*net.IPNet{ipnet1}, + ExcludedIPRanges: []*net.IPNet{ipnet2}, + PermittedEmailAddresses: []string{"one@example.com", "two@example.com"}, + ExcludedEmailAddresses: []string{"un@example.com", "deux@example.com"}, + PermittedURIDomains: []string{"domain1", "domain2"}, + ExcludedURIDomains: []string{"domain3", "domain4"}, + } + data := &CreationBundle{ + Params: params, + SigningBundle: signingBundle, + CSR: csr, + } + + parsedBundle, err := SignCertificate(data) + if err != nil { + t.Fatal("should have failed signing csr with ignore csr signature disabled") + } + + var failedChecks []error + check := func(fieldName string, expected any, actual any) { + diff := deep.Equal(expected, actual) + if len(diff) > 0 { + failedChecks = append(failedChecks, fmt.Errorf("error in field %q: %v", fieldName, diff)) + } + } + cert := parsedBundle.Certificate + check("PermittedDNSDomains", params.PermittedDNSDomains, cert.PermittedDNSDomains) + check("ExcludedDNSDomains", params.ExcludedDNSDomains, cert.ExcludedDNSDomains) + check("PermittedIPRanges", params.PermittedIPRanges, cert.PermittedIPRanges) + check("ExcludedIPRanges", params.ExcludedIPRanges, cert.ExcludedIPRanges) + check("PermittedEmailAddresses", params.PermittedEmailAddresses, cert.PermittedEmailAddresses) + check("ExcludedEmailAddresses", params.ExcludedEmailAddresses, cert.ExcludedEmailAddresses) + check("PermittedURIDomains", params.PermittedURIDomains, cert.PermittedURIDomains) + check("ExcludedURIDomains", params.ExcludedURIDomains, cert.ExcludedURIDomains) + + if err := errors.Join(failedChecks...); err != nil { + t.Error(err) + } +} + func genRsaKey(t *testing.T) *rsa.PrivateKey { key, err := cryptoutil.GenerateRSAKey(rand.Reader, 2048) if err != nil { diff --git a/sdk/helper/certutil/helpers.go b/sdk/helper/certutil/helpers.go index 918dbe81df..c7abd72692 100644 --- a/sdk/helper/certutil/helpers.go +++ b/sdk/helper/certutil/helpers.go @@ -1360,6 +1360,7 @@ func signCertificate(data *CreationBundle, randReader io.Reader) (*ParsedCertBun certTemplate.IsCA = false } + certTemplate.PermittedDNSDomains = append(certTemplate.PermittedDNSDomains, data.Params.PermittedDNSDomains...) certTemplate.ExcludedDNSDomains = append(certTemplate.ExcludedDNSDomains, data.Params.ExcludedDNSDomains...) certTemplate.PermittedIPRanges = append(certTemplate.PermittedIPRanges, data.Params.PermittedIPRanges...) certTemplate.ExcludedIPRanges = append(certTemplate.ExcludedIPRanges, data.Params.ExcludedIPRanges...) diff --git a/website/content/api-docs/secret/pki/index.mdx b/website/content/api-docs/secret/pki/index.mdx index 889438116e..2f5df4bcda 100644 --- a/website/content/api-docs/secret/pki/index.mdx +++ b/website/content/api-docs/secret/pki/index.mdx @@ -2180,6 +2180,44 @@ use the values set via `config/urls`. [RFC 5280 Section 4.2.1.10 - Name Constraints](https://tools.ietf.org/html/rfc5280#section-4.2.1.10). +- `excluded_dns_domains` `(string: "")` - A comma separated string (or, string + array) containing DNS domains for which certificates are not allowed to be issued + or signed by this CA certificate. Supports subdomains via a `.` in front of + the domain, as per [RFC 5280 Section 4.2.1.10 - Name + Constraints](https://tools.ietf.org/html/rfc5280#section-4.2.1.10) + +- `permitted_ip_ranges` `(string: "")` - A comma separated string (or, string + array) containing IP ranges for which certificates are allowed to be issued or + signed by this CA certificate. IP ranges must be in the CIDR notation of IP + address and prefix length like "192.0.2.0/24" or "2001:db8::/32", as defined + in RFC 4632 and RFC 4291. + +- `excluded_ip_ranges` `(string: "")` - A comma separated string (or, string + array) containing IP ranges for which certificates are not allowed to be + issued or signed by this CA certificate. IP ranges must be in the CIDR + notation of IP address and prefix length like "192.0.2.0/24" or + "2001:db8::/32", as defined in RFC 4632 and RFC 4291. + +- `permitted_email_addresses` `(string: "")` - A comma separated string (or, string + array) containing email addresses for which certificates are allowed to be issued or + signed by this CA certificate. + +- `excluded_email_addresses` `(string: "")` - A comma separated string (or, + string array) containing email addresses for which certificates are not + allowed to be issued or signed by this CA certificate. + +- `permitted_uri_domains` `(string: "")` - A comma separated string (or, string + array) containing fully qualified domain names for which certificates are + allowed to be issued or signed by this CA certificate. Supports subdomains via + a `.` in front of the domain, as per [RFC 5280 Section 4.2.1.10 - Name + Constraints](https://tools.ietf.org/html/rfc5280#section-4.2.1.10) + +- `excluded_uri_domains` `(string: "")` - A comma separated string (or, string + array) containing fully qualified domain names for which certificates are not + allowed to be issued or signed by this CA certificate. Supports subdomains via + a `.` in front of the domain, as per [RFC 5280 Section 4.2.1.10 - Name + Constraints](https://tools.ietf.org/html/rfc5280#section-4.2.1.10) + - `ou` `(string: "")` - Specifies the OU (OrganizationalUnit) values in the subject field of the resulting certificate. This is a comma-separated string or JSON array.