Skip to content

Commit

Permalink
Add support for setting pathlen on CA certificates and intermediate…
Browse files Browse the repository at this point in the history
… CA (#135)
  • Loading branch information
socheatsok78 authored and mbyczkowski committed May 12, 2023
1 parent b0b3dbb commit 63ada5f
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 4 deletions.
21 changes: 20 additions & 1 deletion cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ func NewInitCommand() cli.Command {
Name: "permit-domain",
Usage: "Create a CA restricted to subdomains of this domain (can be specified multiple times)",
},
cli.IntFlag{
Name: "path-length",
Value: 0,
Usage: "Maximum number of non-self-issued intermediate certificates that may follow this CA certificate in a valid certification path",
},
cli.BoolFlag{
Name: "exclude-path-length",
Usage: "Exclude 'Path Length Constraint' from this CA certificate",
},
},
Action: initAction,
}
Expand All @@ -111,6 +120,11 @@ func initAction(c *cli.Context) {
os.Exit(1)
}

if c.IsSet("path-length") && c.IsSet("exclude-path-length") {
fmt.Fprintf(os.Stderr, "The \"path-length\" and \"exclude-path-length\" flags cannot be used together!\n")
os.Exit(1)
}

var err error
expires := c.String("expires")
if years := c.Int("years"); years != 0 {
Expand Down Expand Up @@ -176,7 +190,12 @@ func initAction(c *cli.Context) {
}
}

crt, err := pkix.CreateCertificateAuthority(key, c.String("organizational-unit"), expiresTime, c.String("organization"), c.String("country"), c.String("province"), c.String("locality"), c.String("common-name"), c.StringSlice("permit-domain"))
opts := []pkix.Option{
pkix.WithPathlenOption(c.Int("path-length"), c.Bool("exclude-path-length")),
}

crt, err := pkix.CreateCertificateAuthorityWithOptions(key, c.String("organizational-unit"), expiresTime, c.String("organization"), c.String("country"), c.String("province"), c.String("locality"), c.String("common-name"), c.StringSlice("permit-domain"), opts...)

if err != nil {
fmt.Fprintln(os.Stderr, "Create certificate error:", err)
os.Exit(1)
Expand Down
17 changes: 16 additions & 1 deletion cmd/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ func NewSignCommand() cli.Command {
Name: "intermediate",
Usage: "Whether generated certificate should be a intermediate",
},
cli.IntFlag{
Name: "path-length",
Value: 0,
Usage: "Maximum number of non-self-issued intermediate certificates that may follow this CA certificate in a valid certification path",
},
},
Action: newSignAction,
}
Expand Down Expand Up @@ -140,8 +145,18 @@ func newSignAction(c *cli.Context) {
var crtOut *pkix.Certificate
if c.Bool("intermediate") {
fmt.Fprintln(os.Stderr, "Building intermediate")
crtOut, err = pkix.CreateIntermediateCertificateAuthority(crt, key, csr, expiresTime)

opts := []pkix.Option{
pkix.WithPathlenOption(c.Int("path-length"), false),
}

crtOut, err = pkix.CreateIntermediateCertificateAuthorityWithOptions(crt, key, csr, expiresTime, opts...)
} else {
if c.IsSet("path-length") {
fmt.Fprintln(os.Stderr, "The 'path-length' can only be used with 'intermediate' flag.")
os.Exit(1)
}

crtOut, err = pkix.CreateCertificateHost(crt, key, csr, expiresTime)
}

Expand Down
40 changes: 38 additions & 2 deletions pkix/cert_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,18 @@ import (
"time"
)

type Option func(*x509.Certificate)

// CreateCertificateAuthority creates Certificate Authority using existing key.
// CertificateAuthorityInfo returned is the extra infomation required by Certificate Authority.
func CreateCertificateAuthority(key *Key, organizationalUnit string, expiry time.Time, organization string, country string, province string, locality string, commonName string, permitDomains []string) (*Certificate, error) {
// Passing all arguments to CreateCertificateAuthorityWithOptions
return CreateCertificateAuthorityWithOptions(key, organizationalUnit, expiry, organization, country, province, locality, commonName, permitDomains)
}

// CreateCertificateAuthorityWithOptions creates Certificate Authority using existing key with options.
// CertificateAuthorityInfo returned is the extra infomation required by Certificate Authority.
func CreateCertificateAuthorityWithOptions(key *Key, organizationalUnit string, expiry time.Time, organization string, country string, province string, locality string, commonName string, permitDomains []string, opts ...Option) (*Certificate, error) {
authTemplate := newAuthTemplate()

subjectKeyID, err := GenerateSubjectKeyID(key.Public)
Expand Down Expand Up @@ -59,6 +68,8 @@ func CreateCertificateAuthority(key *Key, organizationalUnit string, expiry time
authTemplate.PermittedDNSDomains = permitDomains
}

applyOptions(&authTemplate, opts)

crtBytes, err := x509.CreateCertificate(rand.Reader, &authTemplate, &authTemplate, key.Public, key.Private)
if err != nil {
return nil, err
Expand All @@ -70,6 +81,13 @@ func CreateCertificateAuthority(key *Key, organizationalUnit string, expiry time
// CreateIntermediateCertificateAuthority creates an intermediate
// CA certificate signed by the given authority.
func CreateIntermediateCertificateAuthority(crtAuth *Certificate, keyAuth *Key, csr *CertificateSigningRequest, proposedExpiry time.Time) (*Certificate, error) {
// Passing all arguments to CreateIntermediateCertificateAuthorityWithOptions
return CreateIntermediateCertificateAuthorityWithOptions(crtAuth, keyAuth, csr, proposedExpiry)
}

// CreateIntermediateCertificateAuthorityWithOptions creates an intermediate with options.
// CA certificate signed by the given authority.
func CreateIntermediateCertificateAuthorityWithOptions(crtAuth *Certificate, keyAuth *Key, csr *CertificateSigningRequest, proposedExpiry time.Time, opts ...Option) (*Certificate, error) {
authTemplate := newAuthTemplate()

serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
Expand All @@ -78,7 +96,6 @@ func CreateIntermediateCertificateAuthority(crtAuth *Certificate, keyAuth *Key,
return nil, err
}
authTemplate.SerialNumber.Set(serialNumber)
authTemplate.MaxPathLenZero = false

rawCsr, err := csr.GetRawCertificateSigningRequest()
if err != nil {
Expand Down Expand Up @@ -109,6 +126,8 @@ func CreateIntermediateCertificateAuthority(crtAuth *Certificate, keyAuth *Key,
return nil, err
}

applyOptions(&authTemplate, opts)

crtOutBytes, err := x509.CreateCertificate(rand.Reader, &authTemplate, rawCrtAuth, rawCsr.PublicKey, keyAuth.Private)
if err != nil {
return nil, err
Expand All @@ -117,12 +136,29 @@ func CreateIntermediateCertificateAuthority(crtAuth *Certificate, keyAuth *Key,
return NewCertificateFromDER(crtOutBytes), nil
}

// WithPathlenOption will check if the certificate should have `pathlen` or not.
func WithPathlenOption(pathlen int, excludePathlen bool) func(template *x509.Certificate) {
return func(template *x509.Certificate) {
template.MaxPathLen = pathlen

if excludePathlen {
template.MaxPathLen = -1
}
}
}

func applyOptions(template *x509.Certificate, opts []Option) {
for _, opt := range opts {
opt(template)
}
}

func newAuthTemplate() x509.Certificate {
// Build CA based on RFC5280
return x509.Certificate{
SerialNumber: big.NewInt(1),
// NotBefore is set to be 10min earlier to fix gap on time difference in cluster
NotBefore: time.Now().Add(-10*time.Minute).UTC(),
NotBefore: time.Now().Add(-10 * time.Minute).UTC(),
NotAfter: time.Time{},
// Used for certificate signing only
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
Expand Down
80 changes: 80 additions & 0 deletions pkix/cert_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,83 @@ func TestCreateCertificateAuthority(t *testing.T) {
t.Fatalf("Wrong permitted DNS domain, want %q, got %q", ".example.com", crt.crt.PermittedDNSDomains[0])
}
}

func TestCreateCertificateAuthorityWithOptions(t *testing.T) {
tests := []struct {
name string
pathlen int
excludePathlen bool
}{{
name: "pathlen: 0, excludePathlen: false",
pathlen: 0,
excludePathlen: false,
}, {
name: "pathlen: 1, excludePathlen: false",
pathlen: 1,
excludePathlen: false,
}, {
name: "pathlen: 0, excludePathlen: true",
pathlen: 0,
excludePathlen: true,
}, {
name: "pathlen: 1, excludePathlen: true",
pathlen: 1,
excludePathlen: true,
}}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
key, err := CreateRSAKey(rsaBits)
if err != nil {
t.Fatal("Failed creating rsa key:", err)
}

crt, err := CreateCertificateAuthorityWithOptions(key, "OU", time.Now().AddDate(5, 0, 0), "test", "US", "California", "San Francisco", "CA Name", []string{".example.com"}, WithPathlenOption(tc.pathlen, tc.excludePathlen))
if err != nil {
t.Fatal("Failed creating certificate authority:", err)
}
rawCrt, err := crt.GetRawCertificate()
if err != nil {
t.Fatal("Failed to get x509.Certificate:", err)
}

if err = rawCrt.CheckSignatureFrom(rawCrt); err != nil {
t.Fatal("Failed to check signature:", err)
}

if rawCrt.Subject.OrganizationalUnit[0] != "OU" {
t.Fatal("Failed to verify hostname:", err)
}

if !time.Now().After(rawCrt.NotBefore) {
t.Fatal("Failed to be after NotBefore")
}

if !time.Now().Before(rawCrt.NotAfter) {
t.Fatal("Failed to be before NotAfter")
}

if crt.crt.PermittedDNSDomainsCritical != true {
t.Fatal("Permitted DNS Domains is not set to critical")
}

if len(crt.crt.PermittedDNSDomains) != 1 {
t.Fatal("More than one entry found in list of permitted DNS domains")
}

if crt.crt.PermittedDNSDomains[0] != ".example.com" {
t.Fatalf("Wrong permitted DNS domain, want %q, got %q", ".example.com", crt.crt.PermittedDNSDomains[0])
}

if tc.excludePathlen {
if crt.crt.MaxPathLen != -1 {
t.Fatalf("Wrong MaxPathLen value, want %v, got %v", -1, crt.crt.MaxPathLen)
}
} else {
if crt.crt.MaxPathLen != tc.pathlen {
t.Fatalf("Wrong MaxPathLen value, want %v, got %v", tc.pathlen, crt.crt.MaxPathLen)
}
}
})
}
}

0 comments on commit 63ada5f

Please # to comment.