Merge pull request #6542 from tanujd11/fix/name-constraints-csr-structure

fix: structure of nameconstraint in CSR
This commit is contained in:
jetstack-bot 2023-12-12 16:07:16 +00:00 committed by GitHub
commit 8da699a735
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 467 additions and 170 deletions

View File

@ -263,7 +263,7 @@ comma = ,
# Helm's "--set" interprets commas, which means we want to escape commas
# for "--set featureGates". That's why we have "\$(comma)".
feature_gates_controller := $(subst $(space),\$(comma),$(filter AllAlpha=% AllBeta=% AdditionalCertificateOutputFormats=% ValidateCAA=% ExperimentalCertificateSigningRequestControllers=% ExperimentalGatewayAPISupport=% ServerSideApply=% LiteralCertificateSubject=% UseCertificateRequestBasicConstraints=% UseCertificateRequestNameConstraints=% SecretsFilteredCaching=%, $(subst $(comma),$(space),$(FEATURE_GATES))))
feature_gates_webhook := $(subst $(space),\$(comma),$(filter AllAlpha=% AllBeta=% AdditionalCertificateOutputFormats=% LiteralCertificateSubject=%, UseCertificateRequestNameConstraints=% $(subst $(comma),$(space),$(FEATURE_GATES))))
feature_gates_webhook := $(subst $(space),\$(comma),$(filter AllAlpha=% AllBeta=% AdditionalCertificateOutputFormats=% LiteralCertificateSubject=% UseCertificateRequestNameConstraints=%, $(subst $(comma),$(space),$(FEATURE_GATES))))
feature_gates_cainjector := $(subst $(space),\$(comma),$(filter AllAlpha=% AllBeta=% ServerSideApply=%, $(subst $(comma),$(space),$(FEATURE_GATES))))
# Install cert-manager with E2E specific images and deployment settings.

View File

@ -198,16 +198,15 @@ func CertificateTemplateFromCSR(csr *x509.CertificateRequest, validatorMutators
if err != nil {
return err
}
template.PermittedDNSDomainsCritical = nameConstraints.PermittedDNSDomainsCritical
template.PermittedDNSDomainsCritical = val.Critical
template.PermittedDNSDomains = nameConstraints.PermittedDNSDomains
template.ExcludedDNSDomains = nameConstraints.ExcludedDNSDomains
template.PermittedIPRanges = convertIPNetSliceToIPNetPointerSlice(nameConstraints.PermittedIPRanges)
template.ExcludedIPRanges = convertIPNetSliceToIPNetPointerSlice(nameConstraints.ExcludedIPRanges)
template.PermittedIPRanges = nameConstraints.PermittedIPRanges
template.PermittedEmailAddresses = nameConstraints.PermittedEmailAddresses
template.ExcludedEmailAddresses = nameConstraints.ExcludedEmailAddresses
template.PermittedURIDomains = nameConstraints.PermittedURIDomains
template.ExcludedURIDomains = nameConstraints.ExcludedEmailAddresses
template.ExcludedDNSDomains = nameConstraints.ExcludedDNSDomains
template.ExcludedIPRanges = nameConstraints.ExcludedIPRanges
template.ExcludedEmailAddresses = nameConstraints.ExcludedEmailAddresses
template.ExcludedURIDomains = nameConstraints.ExcludedURIDomains
}
// RFC 5280, 4.2.1.3

View File

@ -319,11 +319,36 @@ func GenerateCSR(crt *v1.Certificate, optFuncs ...GenerateCSROption) (*x509.Cert
}
if opts.EncodeNameConstraintsInRequest && crt.Spec.NameConstraints != nil {
extension, err := MarshalNameConstraints(crt.Spec.NameConstraints)
if err != nil {
return nil, err
nameConstraints := &NameConstraints{}
if crt.Spec.NameConstraints.Permitted != nil {
nameConstraints.PermittedDNSDomains = crt.Spec.NameConstraints.Permitted.DNSDomains
nameConstraints.PermittedIPRanges, err = parseCIDRs(crt.Spec.NameConstraints.Permitted.IPRanges)
if err != nil {
return nil, err
}
nameConstraints.PermittedEmailAddresses = crt.Spec.NameConstraints.Permitted.EmailAddresses
nameConstraints.ExcludedURIDomains = crt.Spec.NameConstraints.Permitted.URIDomains
}
if crt.Spec.NameConstraints.Excluded != nil {
nameConstraints.ExcludedDNSDomains = crt.Spec.NameConstraints.Excluded.DNSDomains
nameConstraints.ExcludedIPRanges, err = parseCIDRs(crt.Spec.NameConstraints.Excluded.IPRanges)
if err != nil {
return nil, err
}
nameConstraints.ExcludedEmailAddresses = crt.Spec.NameConstraints.Excluded.EmailAddresses
nameConstraints.ExcludedURIDomains = crt.Spec.NameConstraints.Excluded.URIDomains
}
if !nameConstraints.IsEmpty() {
extension, err := MarshalNameConstraints(nameConstraints, crt.Spec.NameConstraints.Critical)
if err != nil {
return nil, err
}
extraExtensions = append(extraExtensions, extension)
}
extraExtensions = append(extraExtensions, extension)
}
cr := &x509.CertificateRequest{

View File

@ -346,28 +346,6 @@ func TestGenerateCSR(t *testing.T) {
t.Fatal(err)
}
_, permittedIPNet, err := net.ParseCIDR("10.10.0.0/16")
if err != nil {
t.Fatal(err)
}
_, excludedIPNet, err := net.ParseCIDR("10.10.0.0/24")
if err != nil {
t.Fatal(err)
}
nameConstraints := NameConstraints{
PermittedDNSDomainsCritical: true,
PermittedDNSDomains: []string{"example.org"},
PermittedIPRanges: []net.IPNet{*permittedIPNet},
PermittedEmailAddresses: []string{"email@email.org"},
ExcludedIPRanges: []net.IPNet{*excludedIPNet},
}
asn1NameConstraints, err := asn1.Marshal(nameConstraints)
if err != nil {
t.Fatal(err)
}
// 0xa0 = DigitalSignature, Encipherment and KeyCertSign usage
asn1KeyUsageWithCa, err := asn1.Marshal(asn1.BitString{Bytes: []byte{0xa4}, BitLength: asn1BitLength([]byte{0xa4})})
if err != nil {
@ -652,7 +630,7 @@ func TestGenerateCSR(t *testing.T) {
},
{
Id: OIDExtensionNameConstraints,
Value: asn1NameConstraints,
Value: []byte{0x30, 0x3e, 0xa0, 0x2e, 0x30, 0xd, 0x82, 0xb, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x6f, 0x72, 0x67, 0x30, 0xa, 0x87, 0x8, 0xa, 0xa, 0x0, 0x0, 0xff, 0xff, 0x0, 0x0, 0x30, 0x11, 0x81, 0xf, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x40, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x6f, 0x72, 0x67, 0xa1, 0xc, 0x30, 0xa, 0x87, 0x8, 0xa, 0xa, 0x0, 0x0, 0xff, 0xff, 0xff, 0x0},
Critical: true,
},
},
@ -690,7 +668,7 @@ func TestSignCSRTemplate(t *testing.T) {
require.NoError(t, err)
var permittedIPRanges []*net.IPNet
if nameConstraints != nil {
permittedIPRanges = convertIPNetSliceToIPNetPointerSlice(nameConstraints.PermittedIPRanges)
permittedIPRanges = nameConstraints.PermittedIPRanges
}
tmpl := &x509.Certificate{
Version: 3,
@ -731,7 +709,7 @@ func TestSignCSRTemplate(t *testing.T) {
// vars for testing name constraints
_, permittedIPNet, _ := net.ParseCIDR("10.10.0.0/16")
_, ncRootCert, _, ncRootPK := mustCreatePair(nil, nil, "ncroot", true, &NameConstraints{PermittedIPRanges: []net.IPNet{*permittedIPNet}})
_, ncRootCert, _, ncRootPK := mustCreatePair(nil, nil, "ncroot", true, &NameConstraints{PermittedIPRanges: []*net.IPNet{permittedIPNet}})
_, _, ncLeafTmpl, _ := mustCreatePair(ncRootCert, ncRootPK, "ncleaf", false, nil)
ncLeafTmpl.IPAddresses = []net.IP{net.ParseIP("10.20.0.5")}

View File

@ -18,11 +18,13 @@ package pki
import (
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
"net"
"unicode"
v1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
"golang.org/x/crypto/cryptobyte"
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
)
// Copied from x509.go
@ -32,58 +34,146 @@ var (
// NameConstraints represents the NameConstraints extension.
type NameConstraints struct {
PermittedDNSDomainsCritical bool `asn1:"optional,explicit,tag:0"`
PermittedDNSDomains []string `asn1:"optional,explicit,tag:1"`
ExcludedDNSDomains []string `asn1:"optional,explicit,tag:2"`
PermittedIPRanges []net.IPNet `asn1:"optional,explicit,tag:3"`
ExcludedIPRanges []net.IPNet `asn1:"optional,explicit,tag:4"`
PermittedEmailAddresses []string `asn1:"optional,explicit,tag:5"`
ExcludedEmailAddresses []string `asn1:"optional,explicit,tag:6"`
PermittedURIDomains []string `asn1:"optional,explicit,tag:7"`
ExcludedURIDomains []string `asn1:"optional,explicit,tag:8"`
PermittedDNSDomains []string
ExcludedDNSDomains []string
PermittedIPRanges []*net.IPNet
ExcludedIPRanges []*net.IPNet
PermittedEmailAddresses []string
ExcludedEmailAddresses []string
PermittedURIDomains []string
ExcludedURIDomains []string
}
func (nc NameConstraints) IsEmpty() bool {
return len(nc.PermittedDNSDomains) == 0 &&
len(nc.PermittedIPRanges) == 0 &&
len(nc.PermittedEmailAddresses) == 0 &&
len(nc.PermittedURIDomains) == 0 &&
len(nc.ExcludedDNSDomains) == 0 &&
len(nc.ExcludedIPRanges) == 0 &&
len(nc.ExcludedEmailAddresses) == 0 &&
len(nc.ExcludedURIDomains) == 0
}
// Adapted from x509.go
func MarshalNameConstraints(nameConstraints *v1.NameConstraints) (pkix.Extension, error) {
ext := pkix.Extension{Id: OIDExtensionNameConstraints, Critical: true}
var nameConstraintsForMarshalling NameConstraints
if nameConstraints.Permitted != nil {
permittedIPRanges, err := parseCIDRs(nameConstraints.Permitted.IPRanges)
if err != nil {
return pkix.Extension{}, err
}
nameConstraintsForMarshalling = NameConstraints{
PermittedDNSDomainsCritical: nameConstraints.Critical,
PermittedDNSDomains: nameConstraints.Permitted.DNSDomains,
PermittedIPRanges: permittedIPRanges,
PermittedEmailAddresses: nameConstraints.Permitted.EmailAddresses,
PermittedURIDomains: nameConstraints.Permitted.URIDomains,
}
func MarshalNameConstraints(nameConstraints *NameConstraints, critical bool) (pkix.Extension, error) {
ipAndMask := func(ipNet *net.IPNet) []byte {
maskedIP := ipNet.IP.Mask(ipNet.Mask)
ipAndMask := make([]byte, 0, len(maskedIP)+len(ipNet.Mask))
ipAndMask = append(ipAndMask, maskedIP...)
ipAndMask = append(ipAndMask, ipNet.Mask...)
return ipAndMask
}
if nameConstraints.Excluded != nil {
excludedIPRanges, err := parseCIDRs(nameConstraints.Excluded.IPRanges)
if err != nil {
return pkix.Extension{}, err
serialiseConstraints := func(dns []string, ips []*net.IPNet, emails []string, uriDomains []string) (der []byte, err error) {
var b cryptobyte.Builder
for _, name := range dns {
if err = isIA5String(name); err != nil {
return nil, err
}
b.AddASN1(cryptobyte_asn1.SEQUENCE, func(b *cryptobyte.Builder) {
b.AddASN1(cryptobyte_asn1.Tag(2).ContextSpecific(), func(b *cryptobyte.Builder) {
b.AddBytes([]byte(name))
})
})
}
nameConstraintsForMarshalling.ExcludedDNSDomains = nameConstraints.Excluded.DNSDomains
nameConstraintsForMarshalling.ExcludedIPRanges = excludedIPRanges
nameConstraintsForMarshalling.ExcludedEmailAddresses = nameConstraints.Excluded.EmailAddresses
nameConstraintsForMarshalling.ExcludedURIDomains = nameConstraints.Excluded.URIDomains
for _, ipNet := range ips {
b.AddASN1(cryptobyte_asn1.SEQUENCE, func(b *cryptobyte.Builder) {
b.AddASN1(cryptobyte_asn1.Tag(7).ContextSpecific(), func(b *cryptobyte.Builder) {
b.AddBytes(ipAndMask(ipNet))
})
})
}
for _, email := range emails {
if err = isIA5String(email); err != nil {
return nil, err
}
b.AddASN1(cryptobyte_asn1.SEQUENCE, func(b *cryptobyte.Builder) {
b.AddASN1(cryptobyte_asn1.Tag(1).ContextSpecific(), func(b *cryptobyte.Builder) {
b.AddBytes([]byte(email))
})
})
}
for _, uriDomain := range uriDomains {
if err = isIA5String(uriDomain); err != nil {
return nil, err
}
b.AddASN1(cryptobyte_asn1.SEQUENCE, func(b *cryptobyte.Builder) {
b.AddASN1(cryptobyte_asn1.Tag(6).ContextSpecific(), func(b *cryptobyte.Builder) {
b.AddBytes([]byte(uriDomain))
})
})
}
return b.Bytes()
}
var permitted []byte
var err error
ext.Value, err = asn1.Marshal(nameConstraintsForMarshalling)
return ext, err
permitted, err = serialiseConstraints(nameConstraints.PermittedDNSDomains, nameConstraints.PermittedIPRanges, nameConstraints.PermittedEmailAddresses, nameConstraints.PermittedURIDomains)
if err != nil {
return pkix.Extension{}, err
}
var excluded []byte
excluded, err = serialiseConstraints(nameConstraints.ExcludedDNSDomains, nameConstraints.ExcludedIPRanges, nameConstraints.ExcludedEmailAddresses, nameConstraints.ExcludedURIDomains)
if err != nil {
return pkix.Extension{}, err
}
var b cryptobyte.Builder
b.AddASN1(cryptobyte_asn1.SEQUENCE, func(b *cryptobyte.Builder) {
if len(permitted) > 0 {
b.AddASN1(cryptobyte_asn1.Tag(0).ContextSpecific().Constructed(), func(b *cryptobyte.Builder) {
b.AddBytes(permitted)
})
}
if len(excluded) > 0 {
b.AddASN1(cryptobyte_asn1.Tag(1).ContextSpecific().Constructed(), func(b *cryptobyte.Builder) {
b.AddBytes(excluded)
})
}
})
bytes, err := b.Bytes()
if err != nil {
return pkix.Extension{}, err
}
return pkix.Extension{
Id: OIDExtensionNameConstraints,
Critical: critical,
Value: bytes,
}, nil
}
func parseCIDRs(cidrs []string) ([]net.IPNet, error) {
ipRanges := []net.IPNet{}
func isIA5String(s string) error {
for _, r := range s {
// Per RFC5280 "IA5String is limited to the set of ASCII characters"
if r > unicode.MaxASCII {
return fmt.Errorf("x509: %q cannot be encoded as an IA5String", s)
}
}
return nil
}
func parseCIDRs(cidrs []string) ([]*net.IPNet, error) {
ipRanges := []*net.IPNet{}
for _, cidr := range cidrs {
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
return nil, err
}
ipRanges = append(ipRanges, net.IPNet{
ipRanges = append(ipRanges, &net.IPNet{
IP: ipNet.IP,
Mask: ipNet.Mask,
})
@ -91,27 +181,145 @@ func parseCIDRs(cidrs []string) ([]net.IPNet, error) {
return ipRanges, nil
}
func UnmarshalNameConstraints(value []byte) (NameConstraints, error) {
var constraints NameConstraints
var rest []byte
// Adapted from crypto/x509/parser.go
func UnmarshalNameConstraints(value []byte) (*NameConstraints, error) {
// RFC 5280, 4.2.1.10
// NameConstraints ::= SEQUENCE {
// permittedSubtrees [0] GeneralSubtrees OPTIONAL,
// excludedSubtrees [1] GeneralSubtrees OPTIONAL }
//
// GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree
//
// GeneralSubtree ::= SEQUENCE {
// base GeneralName,
// minimum [0] BaseDistance DEFAULT 0,
// maximum [1] BaseDistance OPTIONAL }
//
// BaseDistance ::= INTEGER (0..MAX)
outer := cryptobyte.String(value)
var toplevel, permitted, excluded cryptobyte.String
var havePermitted, haveExcluded bool
if !outer.ReadASN1(&toplevel, cryptobyte_asn1.SEQUENCE) ||
!outer.Empty() ||
!toplevel.ReadOptionalASN1(&permitted, &havePermitted, cryptobyte_asn1.Tag(0).ContextSpecific().Constructed()) ||
!toplevel.ReadOptionalASN1(&excluded, &haveExcluded, cryptobyte_asn1.Tag(1).ContextSpecific().Constructed()) ||
!toplevel.Empty() {
return nil, errors.New("x509: invalid NameConstraints extension")
}
if !havePermitted && !haveExcluded || len(permitted) == 0 && len(excluded) == 0 {
// From RFC 5280, Section 4.2.1.10:
// “either the permittedSubtrees field
// or the excludedSubtrees MUST be
// present”
return nil, errors.New("x509: empty name constraints extension")
}
getValues := func(subtrees cryptobyte.String) (dnsNames []string, ips []*net.IPNet, emails, uriDomains []string, err error) {
for !subtrees.Empty() {
var seq, value cryptobyte.String
var tag cryptobyte_asn1.Tag
if !subtrees.ReadASN1(&seq, cryptobyte_asn1.SEQUENCE) ||
!seq.ReadAnyASN1(&value, &tag) {
return nil, nil, nil, nil, fmt.Errorf("x509: invalid NameConstraints extension")
}
var (
dnsTag = cryptobyte_asn1.Tag(2).ContextSpecific()
emailTag = cryptobyte_asn1.Tag(1).ContextSpecific()
ipTag = cryptobyte_asn1.Tag(7).ContextSpecific()
uriTag = cryptobyte_asn1.Tag(6).ContextSpecific()
)
switch tag {
case dnsTag:
domain := string(value)
if err := isIA5String(domain); err != nil {
return nil, nil, nil, nil, errors.New("x509: invalid constraint value: " + err.Error())
}
dnsNames = append(dnsNames, domain)
case ipTag:
l := len(value)
var ip, mask []byte
switch l {
case 2 * net.IPv4len:
ip = value[:net.IPv4len]
mask = value[net.IPv4len:]
case 2 * net.IPv6len:
ip = value[:net.IPv6len]
mask = value[net.IPv6len:]
default:
return nil, nil, nil, nil, fmt.Errorf("x509: IP constraint contained value of length %d", l)
}
if !isValidIPMask(mask) {
return nil, nil, nil, nil, fmt.Errorf("x509: IP constraint contained invalid mask %x", mask)
}
ips = append(ips, &net.IPNet{IP: net.IP(ip), Mask: net.IPMask(mask)})
case emailTag:
constraint := string(value)
if err := isIA5String(constraint); err != nil {
return nil, nil, nil, nil, errors.New("x509: invalid constraint value: " + err.Error())
}
emails = append(emails, constraint)
case uriTag:
domain := string(value)
if err := isIA5String(domain); err != nil {
return nil, nil, nil, nil, errors.New("x509: invalid constraint value: " + err.Error())
}
uriDomains = append(uriDomains, domain)
}
}
return dnsNames, ips, emails, uriDomains, nil
}
out := &NameConstraints{}
var err error
if rest, err = asn1.Unmarshal(value, &constraints); err != nil {
return constraints, err
} else if len(rest) != 0 {
return constraints, errors.New("x509: trailing data after X.509 NameConstraints")
if out.PermittedDNSDomains, out.PermittedIPRanges, out.PermittedEmailAddresses, out.PermittedURIDomains, err = getValues(permitted); err != nil {
return nil, err
}
if out.ExcludedDNSDomains, out.ExcludedIPRanges, out.ExcludedEmailAddresses, out.ExcludedURIDomains, err = getValues(excluded); err != nil {
return nil, err
}
return constraints, nil
return out, nil
}
// convertIPNetSliceToIPNetPointerSlice converts []net.IPNet to []*net.IPNet.
func convertIPNetSliceToIPNetPointerSlice(ipNetPointerSlice []net.IPNet) []*net.IPNet {
if ipNetPointerSlice == nil {
return nil
// isValidIPMask reports whether mask consists of zero or more 1 bits, followed by zero bits.
func isValidIPMask(mask []byte) bool {
seenZero := false
for _, b := range mask {
if seenZero {
if b != 0 {
return false
}
continue
}
switch b {
case 0x00, 0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe:
seenZero = true
case 0xff:
default:
return false
}
}
var ipNets []*net.IPNet
for _, ipNet := range ipNetPointerSlice {
ipNets = append(ipNets, &ipNet)
}
return ipNets
return true
}

View File

@ -17,119 +17,206 @@ limitations under the License.
package pki
import (
"bytes"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"net"
"strings"
"testing"
v1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
"github.com/stretchr/testify/assert"
)
func TestMarshalNameConstraints(t *testing.T) {
// TestMarshalNameConstraints tests the MarshalNameConstraints function
// To generate the expectedPEM, do something like this:
// openssl req -new -key private_key.pem -out csr1.pem -subj "/CN=example.org" -config config.cnf
//
// where config.cnf is(replace nameConstraints with the values mentioned in the testcase):
// [req]
// default_bits = 2048
// prompt = no
// default_md = sha256
// req_extensions = req_ext
// [req_ext]
// nameConstraints = critical,permitted;DNS:example.com,permitted;IP:192.168.1.0/255.255.255.0,permitted;email:user@example.com,permitted;URI:https://example.com,excluded;DNS:excluded.com,excluded;IP:192.168.0.0/255.255.255.0,excluded;email:user@excluded.com,excluded;URI:https://excluded.com
func TestMarshalUnmarshalNameConstraints(t *testing.T) {
// Test data
testCases := []struct {
name string
input *v1.NameConstraints
expectedErr error
expectedResult pkix.Extension
name string
input *NameConstraints
expectedErr error
expectedPEM string
}{
{
name: "Permitted constraints",
input: &v1.NameConstraints{
Critical: true,
Permitted: &v1.NameConstraintItem{
DNSDomains: []string{"example.com"},
IPRanges: []string{"192.168.0.1/24"},
EmailAddresses: []string{"user@example.com"},
URIDomains: []string{"https://example.com"},
},
input: &NameConstraints{
PermittedDNSDomains: []string{"example.com"},
PermittedIPRanges: []*net.IPNet{{IP: net.IPv4(192, 168, 1, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}},
PermittedEmailAddresses: []string{"user@example.com"},
PermittedURIDomains: []string{"https://example.com"},
},
expectedErr: nil,
expectedResult: pkix.Extension{
Id: OIDExtensionNameConstraints,
Critical: true,
Value: []byte{0x30, 0x57, 0xa0, 0x3, 0x1, 0x1, 0xff, 0xa1, 0xf, 0x30, 0xd, 0x13, 0xb, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0xa3, 0x10, 0x30, 0xe, 0x30, 0xc, 0x4, 0x4, 0xc0, 0xa8, 0x0, 0x0, 0x4, 0x4, 0xff, 0xff, 0xff, 0x0, 0xa5, 0x14, 0x30, 0x12, 0xc, 0x10, 0x75, 0x73, 0x65, 0x72, 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0xa7, 0x17, 0x30, 0x15, 0x13, 0x13, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d},
},
// nameConstraints = critical,permitted;DNS:example.com,permitted;IP:192.168.1.0/255.255.255.0,permitted;email:user@example.com,permitted;URI:https://example.com
expectedPEM: `-----BEGIN CERTIFICATE REQUEST-----
MIICwjCCAaoCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5vcmcwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQCXy2XEkqESyr8/Y2x1A7AQaQlu3wry8QSmVwcb
QYQ12xpA9derxd6f2qV+UZq/7tSwvaFfcdzbY4MTG+dq3QmlyXNEpVmzg/CbQJpQ
ae/aacnb7MEvPGQpD8eHBt14QdoH0B5qreARa/IND4I+BazEAn9yAWc9o5BQMqPb
5OGa5PMWR8apRyJrMfupMS0R3Nnmi+BP0fWepbOZHzRA6d2rbwkPBNBHQUyinxXS
oIMg/WbrG0tbps8H6PTZg3Ki+XutPm5rFJ3CKVCzIfWLFIa3jHDNbeRc359EgBI9
r1H7ecuPKxhxewugl0NirKIaEgzc609FIP++pmm3J5P10HF7AgMBAAGgZzBlBgkq
hkiG9w0BCQ4xWDBWMFQGA1UdHgEB/wRKMEigRjANggtleGFtcGxlLmNvbTAKhwjA
qAEA////ADASgRB1c2VyQGV4YW1wbGUuY29tMBWGE2h0dHBzOi8vZXhhbXBsZS5j
b20wDQYJKoZIhvcNAQELBQADggEBAG4mhMt9iOGu1LInHW7oZyD8/FILhhafO7NF
OLPLNK37yZmPWn3idIei/oooFspKspLSMqyCGgibr6jo613+6ENCHgzM/MUDrbfP
i0VmriogMVB6qF73Qozylk1HPMcNe32aKsZygFAzKT586aO/F/exMx3NlKWa36m2
rXKPgtD+T4R+hBxmsYAGVWFlvish+L1UIXtxddna4dYHSbLBz+uZXzrxyuJgSQV3
2wF++GJ1zOi47CEUukqQOAZKPCE59erY+vUas8hwMTHMT22D5ZGbdjg6qVBCQdqW
Nu6OGP4KFgW0HWyeGeNBzioGUeyIHFKILLvj2n94WJMqXNyT5eE=
-----END CERTIFICATE REQUEST-----`,
},
{
name: "Mixed constraints",
input: &v1.NameConstraints{
Critical: true,
Permitted: &v1.NameConstraintItem{
DNSDomains: []string{"example.com"},
IPRanges: []string{"192.168.0.1/24"},
EmailAddresses: []string{"user@example.com"},
URIDomains: []string{"https://example.com"},
},
Excluded: &v1.NameConstraintItem{
DNSDomains: []string{"excluded.com"},
IPRanges: []string{"192.168.0.0/24"},
EmailAddresses: []string{"user@excluded.com"},
URIDomains: []string{"https://excluded.com"},
},
input: &NameConstraints{
PermittedDNSDomains: []string{"example.com"},
PermittedIPRanges: []*net.IPNet{{IP: net.IPv4(192, 168, 1, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}},
PermittedEmailAddresses: []string{"user@example.com"},
PermittedURIDomains: []string{"https://example.com"},
ExcludedDNSDomains: []string{"excluded.com"},
ExcludedIPRanges: []*net.IPNet{{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}},
ExcludedEmailAddresses: []string{"user@excluded.com"},
ExcludedURIDomains: []string{"https://excluded.com"},
},
expectedErr: nil,
expectedResult: pkix.Extension{
Id: OIDExtensionNameConstraints,
Critical: true,
Value: []byte{0x30, 0x81, 0xac, 0xa0, 0x3, 0x1, 0x1, 0xff, 0xa1, 0xf, 0x30, 0xd, 0x13, 0xb, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0xa2, 0x10, 0x30, 0xe, 0x13, 0xc, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x2e, 0x63, 0x6f, 0x6d, 0xa3, 0x10, 0x30, 0xe, 0x30, 0xc, 0x4, 0x4, 0xc0, 0xa8, 0x0, 0x0, 0x4, 0x4, 0xff, 0xff, 0xff, 0x0, 0xa4, 0x10, 0x30, 0xe, 0x30, 0xc, 0x4, 0x4, 0xc0, 0xa8, 0x0, 0x0, 0x4, 0x4, 0xff, 0xff, 0xff, 0x0, 0xa5, 0x14, 0x30, 0x12, 0xc, 0x10, 0x75, 0x73, 0x65, 0x72, 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0xa6, 0x15, 0x30, 0x13, 0xc, 0x11, 0x75, 0x73, 0x65, 0x72, 0x40, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x2e, 0x63, 0x6f, 0x6d, 0xa7, 0x17, 0x30, 0x15, 0x13, 0x13, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0xa8, 0x18, 0x30, 0x16, 0x13, 0x14, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x2e, 0x63, 0x6f, 0x6d},
},
},
{
name: "Empty constraints",
input: &v1.NameConstraints{},
expectedErr: nil,
expectedResult: pkix.Extension{
Id: OIDExtensionNameConstraints,
Critical: true,
Value: []byte{0x30, 0x0},
},
// nameConstraints = critical,permitted;DNS:example.com,permitted;IP:192.168.1.0/255.255.255.0,permitted;email:user@example.com,permitted;URI:https://example.com,excluded;DNS:excluded.com,excluded;IP:192.168.0.0/255.255.255.0,excluded;email:user@excluded.com,excluded;URI:https://excluded.com
expectedPEM: `-----BEGIN CERTIFICATE REQUEST-----
MIIDFDCCAfwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5vcmcwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQCXy2XEkqESyr8/Y2x1A7AQaQlu3wry8QSmVwcb
QYQ12xpA9derxd6f2qV+UZq/7tSwvaFfcdzbY4MTG+dq3QmlyXNEpVmzg/CbQJpQ
ae/aacnb7MEvPGQpD8eHBt14QdoH0B5qreARa/IND4I+BazEAn9yAWc9o5BQMqPb
5OGa5PMWR8apRyJrMfupMS0R3Nnmi+BP0fWepbOZHzRA6d2rbwkPBNBHQUyinxXS
oIMg/WbrG0tbps8H6PTZg3Ki+XutPm5rFJ3CKVCzIfWLFIa3jHDNbeRc359EgBI9
r1H7ecuPKxhxewugl0NirKIaEgzc609FIP++pmm3J5P10HF7AgMBAAGggbgwgbUG
CSqGSIb3DQEJDjGBpzCBpDCBoQYDVR0eAQH/BIGWMIGToEYwDYILZXhhbXBsZS5j
b20wCocIwKgBAP///wAwEoEQdXNlckBleGFtcGxlLmNvbTAVhhNodHRwczovL2V4
YW1wbGUuY29toUkwDoIMZXhjbHVkZWQuY29tMAqHCMCoAAD///8AMBOBEXVzZXJA
ZXhjbHVkZWQuY29tMBaGFGh0dHBzOi8vZXhjbHVkZWQuY29tMA0GCSqGSIb3DQEB
CwUAA4IBAQCEBMhHw4wbP+aBDViKtvpaMar3ZWYVuV7j2qck5yDlXYGhpTQlwg5C
XEIP7zKM1yGgCITEpA5KML4PV55rEU6TCa2E9oQfy51QQcmSTGYLjolOahpALwzn
38n9e4WBiHwDVMVsSR5Zhw2dy9tqSslAHjp3TFFCcx7gaKoTs6OOJzv784PzX7xp
Vbm68hvWwkdD0lwGJlNkykPmNGxpC1kVn6L1p7LUubWOkkqBHwgny+DW3fPtKpvO
AHpUq+yDI0oaIz6BIfn2Vs7jUSXCZIoQBwajALg9kGqh3O6+ds617+AzxGXk0LBQ
0GsHVWCimOgcqgU5Qg4K6iMUtlDU2WAW
-----END CERTIFICATE REQUEST-----`,
},
{
name: "Excluded constraints",
input: &v1.NameConstraints{
Excluded: &v1.NameConstraintItem{
DNSDomains: []string{"excluded.com"},
IPRanges: []string{"192.168.0.0/24"},
EmailAddresses: []string{"user@excluded.com"},
URIDomains: []string{"https://excluded.com"},
},
input: &NameConstraints{
ExcludedDNSDomains: []string{"excluded.com"},
ExcludedIPRanges: []*net.IPNet{{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}},
ExcludedEmailAddresses: []string{"user@excluded.com"},
ExcludedURIDomains: []string{"https://excluded.com"},
},
expectedErr: nil,
expectedResult: pkix.Extension{
Id: OIDExtensionNameConstraints,
Critical: true,
Value: []byte{0x30, 0x55, 0xa2, 0x10, 0x30, 0xe, 0x13, 0xc, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x2e, 0x63, 0x6f, 0x6d, 0xa4, 0x10, 0x30, 0xe, 0x30, 0xc, 0x4, 0x4, 0xc0, 0xa8, 0x0, 0x0, 0x4, 0x4, 0xff, 0xff, 0xff, 0x0, 0xa6, 0x15, 0x30, 0x13, 0xc, 0x11, 0x75, 0x73, 0x65, 0x72, 0x40, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x2e, 0x63, 0x6f, 0x6d, 0xa8, 0x18, 0x30, 0x16, 0x13, 0x14, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x2e, 0x63, 0x6f, 0x6d},
},
},
{
name: "Invalid NameConstraints",
input: &v1.NameConstraints{
Excluded: &v1.NameConstraintItem{
IPRanges: []string{"invalidCIDR"},
},
},
expectedErr: fmt.Errorf("invalid CIDR address: invalidCIDR"),
expectedResult: pkix.Extension{},
// nameConstraints = critical,excluded;DNS:excluded.com,excluded;IP:192.168.0.0/255.255.255.0,excluded;email:user@excluded.com,excluded;URI:https://excluded.com
expectedPEM: `-----BEGIN CERTIFICATE REQUEST-----
MIICxTCCAa0CAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5vcmcwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQCXy2XEkqESyr8/Y2x1A7AQaQlu3wry8QSmVwcb
QYQ12xpA9derxd6f2qV+UZq/7tSwvaFfcdzbY4MTG+dq3QmlyXNEpVmzg/CbQJpQ
ae/aacnb7MEvPGQpD8eHBt14QdoH0B5qreARa/IND4I+BazEAn9yAWc9o5BQMqPb
5OGa5PMWR8apRyJrMfupMS0R3Nnmi+BP0fWepbOZHzRA6d2rbwkPBNBHQUyinxXS
oIMg/WbrG0tbps8H6PTZg3Ki+XutPm5rFJ3CKVCzIfWLFIa3jHDNbeRc359EgBI9
r1H7ecuPKxhxewugl0NirKIaEgzc609FIP++pmm3J5P10HF7AgMBAAGgajBoBgkq
hkiG9w0BCQ4xWzBZMFcGA1UdHgEB/wRNMEuhSTAOggxleGNsdWRlZC5jb20wCocI
wKgAAP///wAwE4ERdXNlckBleGNsdWRlZC5jb20wFoYUaHR0cHM6Ly9leGNsdWRl
ZC5jb20wDQYJKoZIhvcNAQELBQADggEBABQGXpovgvk8Ag+FSv0fVcHAalNrNHkL
8kJmLjJKMjYhrI4KwkrVDwRvm96ueSfDYLMu56Vd/cLzVbqgFNEeGY+7/fwty/PK
PwjPjMC3i09D1JZjrpc2gpIxmrwP/vf1DpxPUVF5wzE9xRiYvKu3/ZHy1d3FYYgT
cpf+w2cqzt2J8imToJUtjbVTACqBwhwRrn7xyP0trvAo1tfHS4qK7urJxbuT+OAf
mYfy24EOPhpvyIyYS+lbkc9wdYT4BSIjQCFNAjcBD+/04SkHgtbFLy0i8xsKcfOy
3haWYno4zTZ0v6LAdn3CgtbvUtFBfIMjmEfsldVZpIbpuSEqjMFDGls=
-----END CERTIFICATE REQUEST-----`,
},
}
compareIPArrays := func(a, b []*net.IPNet) bool {
if len(a) != len(b) {
return false
}
for i, ipNet := range a {
if !ipNet.IP.Equal(b[i].IP) || !bytes.Equal(ipNet.Mask, b[i].Mask) {
return false
}
}
return true
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := MarshalNameConstraints(tc.input)
t.Run(tc.name+"_marshal", func(t *testing.T) {
expectedResult, err := getExtensionFromPem(tc.expectedPEM)
assert.NoError(t, err)
result, err := MarshalNameConstraints(tc.input, expectedResult.Critical)
if tc.expectedErr != nil {
assert.Error(t, err)
assert.EqualError(t, err, tc.expectedErr.Error())
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedResult.Id, result.Id)
assert.Equal(t, tc.expectedResult.Critical, result.Critical)
assert.Equal(t, expectedResult.Id, result.Id)
assert.Equal(t, expectedResult.Critical, result.Critical)
assert.Equal(t, expectedResult.Value, result.Value)
}
})
expectedPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE EXTENSION", Bytes: tc.expectedResult.Value})
actualPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE EXTENSION", Bytes: result.Value})
assert.Equal(t, expectedPEM, actualPEM)
t.Run(tc.name+"_unmarshal", func(t *testing.T) {
expectedResult, err := getExtensionFromPem(tc.expectedPEM)
assert.NoError(t, err)
constraints, err := UnmarshalNameConstraints(expectedResult.Value)
if tc.expectedErr != nil {
assert.Error(t, err)
assert.EqualError(t, err, tc.expectedErr.Error())
} else {
assert.NoError(t, err)
assert.Equal(t, constraints.ExcludedDNSDomains, tc.input.ExcludedDNSDomains)
assert.Equal(t, constraints.ExcludedEmailAddresses, tc.input.ExcludedEmailAddresses)
assert.True(t, compareIPArrays(constraints.ExcludedIPRanges, tc.input.ExcludedIPRanges))
assert.Equal(t, constraints.ExcludedURIDomains, tc.input.ExcludedURIDomains)
assert.Equal(t, constraints.PermittedDNSDomains, tc.input.PermittedDNSDomains)
assert.Equal(t, constraints.PermittedEmailAddresses, tc.input.PermittedEmailAddresses)
assert.True(t, compareIPArrays(constraints.PermittedIPRanges, tc.input.PermittedIPRanges))
assert.Equal(t, constraints.PermittedURIDomains, tc.input.PermittedURIDomains)
}
})
}
}
func getExtensionFromPem(pemData string) (pkix.Extension, error) {
if pemData == "" {
return pkix.Extension{}, nil
}
pemData = strings.TrimSpace(pemData)
fmt.Println(pemData)
csrPEM := []byte(pemData)
block, _ := pem.Decode(csrPEM)
if block == nil || block.Type != "CERTIFICATE REQUEST" {
return pkix.Extension{}, fmt.Errorf("Failed to decode PEM block or the type is not 'CERTIFICATE REQUEST'")
}
csr, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil {
return pkix.Extension{}, fmt.Errorf("Error parsing CSR: %v", err)
}
for _, ext := range csr.Extensions {
if ext.Id.Equal(OIDExtensionNameConstraints) {
return ext, nil
}
}
return pkix.Extension{}, nil
}