diff --git a/cmd/init.go b/cmd/init.go index e0473b6..3343884 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -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, } @@ -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 { @@ -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) diff --git a/cmd/sign.go b/cmd/sign.go index a1d19e7..05d4256 100644 --- a/cmd/sign.go +++ b/cmd/sign.go @@ -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, } @@ -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) } diff --git a/pkix/cert_auth.go b/pkix/cert_auth.go index ecaa2ee..e410c62 100644 --- a/pkix/cert_auth.go +++ b/pkix/cert_auth.go @@ -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) @@ -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 @@ -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) @@ -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 { @@ -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 @@ -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, diff --git a/pkix/cert_auth_test.go b/pkix/cert_auth_test.go index 3553cc5..ed27b34 100644 --- a/pkix/cert_auth_test.go +++ b/pkix/cert_auth_test.go @@ -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) + } + } + }) + } +}