From 14d17adb6c0a257cdf01699d5252aa2a1897b295 Mon Sep 17 00:00:00 2001 From: jm Date: Thu, 25 Oct 2018 16:16:52 -0700 Subject: [PATCH] Add support for setting NotBefore --- README.md | 2 +- cmd/expiry.go | 55 ++++++++++++++++++++++++++++++++++++------ cmd/expiry_test.go | 41 +++++++++++++++++++++++++++++++ cmd/init.go | 16 +++++++++++- cmd/revoke_test.go | 2 +- cmd/sign.go | 16 +++++++++++- pkix/cert_auth.go | 16 ++++++------ pkix/cert_auth_test.go | 2 +- pkix/cert_host.go | 2 +- pkix/crl_test.go | 2 +- 10 files changed, 132 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index f8d402c..e2d385b 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ certstrap can init multiple certificate authorities to sign certificates with. ### Building -certstrap must be built with Go 1.4+. You can build certstrap from source: +certstrap must be built with Go 1.11+. You can build certstrap from source: ``` $ git clone https://github.com/square/certstrap diff --git a/cmd/expiry.go b/cmd/expiry.go index 10d116c..d6724bf 100644 --- a/cmd/expiry.go +++ b/cmd/expiry.go @@ -9,11 +9,10 @@ import ( var nowFunc = time.Now -func parseExpiry(fromNow string) (time.Time, error) { - now := nowFunc().UTC() +func parseTime(timeString string) (map[string]int, error) { re := regexp.MustCompile("\\s*(\\d+)\\s*(day|month|year|hour)s?") - matches := re.FindAllStringSubmatch(fromNow, -1) - addDate := map[string]int{ + matches := re.FindAllStringSubmatch(timeString, -1) + date := map[string]int{ "day": 0, "month": 0, "year": 0, @@ -22,15 +21,25 @@ func parseExpiry(fromNow string) (time.Time, error) { for _, r := range matches { number, err := strconv.ParseInt(r[1], 10, 32) if err != nil { - return now, err + return nil, err } - addDate[r[2]] = int(number) + date[r[2]] = int(number) } // Ensure that we do not overflow time.Duration. // Doing so is silent and causes signed integer overflow like issues. - if _, err := time.ParseDuration(fmt.Sprintf("%dh", addDate["hour"])); err != nil { - return now, fmt.Errorf("hour unit too large to process") + if _, err := time.ParseDuration(fmt.Sprintf("%dh", date["hour"])); err != nil { + return nil, fmt.Errorf("hour unit too large to process") + } + + return date, nil +} + +func parseExpiry(fromNow string) (time.Time, error) { + now := nowFunc().UTC() + addDate, err := parseTime(fromNow) + if err != nil { + return now, err } result := now. @@ -49,3 +58,33 @@ func parseExpiry(fromNow string) (time.Time, error) { return result, nil } + +func parseNotBefore(notBefore string) (time.Time, error) { + now := nowFunc().UTC() + tenMinutesAgo := nowFunc().Add(-time.Minute * 10).UTC() + + subDate, err := parseTime(notBefore) + if err != nil { + return tenMinutesAgo, err + } + + for unitOfTime, value := range subDate { + subDate[unitOfTime] = -value + } + + result := now. + AddDate(subDate["year"], subDate["month"], subDate["day"]). + Add(time.Duration(subDate["hour"]) * time.Hour) + + if now == result { + return tenMinutesAgo, fmt.Errorf("invalid or empty format") + } + + // ASN.1 (encoding format used by SSL) can support down to year 0 + // https://www.openssl.org/docs/man1.1.0/crypto/ASN1_TIME_check.html + if result.Year() < 0 { + return tenMinutesAgo, fmt.Errorf("proposed date too far in to the past: %s. Expiry year must be greater than or equal to 0", result) + } + + return result, nil +} diff --git a/cmd/expiry_test.go b/cmd/expiry_test.go index 6ded2f1..9462ae4 100644 --- a/cmd/expiry_test.go +++ b/cmd/expiry_test.go @@ -99,6 +99,47 @@ func TestParseInvalidExpiry(t *testing.T) { } } +func TestParseNotBeforeWithMixed(t *testing.T) { + t1, _ := parseNotBefore("2 days 3 months 1 year") + t2, _ := parseNotBefore("5 years 5 days 6 months") + expectedt1, _ := time.Parse(dateFormat, "2015-09-29") + expectedt2, _ := time.Parse(dateFormat, "2011-06-26") + + if t1 != expectedt1 { + t.Fatalf("Parsing notbefore for mixed format t1 did not return expected value (wanted: %s, got: %s)", expectedt1, t1) + } + + if t2 != expectedt2 { + t.Fatalf("Parsing notbefore for mixed format t2 did not return expected value (wanted: %s, got: %s)", expectedt2, t2) + } +} + +func TestParseInvalidNotBefore(t *testing.T) { + errorTime := onlyTime(time.Parse("2006-01-02 15:04:05", "2016-12-31 23:50:00")) + cases := []struct { + Input string + Expected time.Time + ExpectedErr string + }{ + {"53257284647843897", errorTime, "invalid or empty format"}, + {"5y", errorTime, "invalid or empty format"}, + {"53257284647843897 days", errorTime, ".*value out of range"}, + {"2147483647 hours", errorTime, ".*hour unit too large.*"}, + {"2147483647 days", errorTime, ".*proposed date too far in to the past.*"}, + } + + for _, c := range cases { + result, err := parseNotBefore(c.Input) + if result != c.Expected { + t.Fatalf("Invalid notbefore '%s' did not have expected value (wanted: %s, got: %s)", c.Input, c.Expected, result) + } + + if match, _ := regexp.MatchString(c.ExpectedErr, fmt.Sprintf("%s", err)); !match { + t.Fatalf("Invalid notbefore '%s' did not have expected error (wanted: %s, got: %s)", c.Input, c.ExpectedErr, err) + } + } +} + func onlyTime(a time.Time, b error) time.Time { return a } diff --git a/cmd/init.go b/cmd/init.go index 74c4106..ef57ee4 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -22,6 +22,7 @@ import ( "io/ioutil" "os" "strings" + "time" "github.com/codegangsta/cli" "github.com/square/certstrap/depot" @@ -39,6 +40,7 @@ func NewInitCommand() cli.Command { cli.IntFlag{"key-bits", 4096, "Bit size of RSA keypair to generate", ""}, cli.IntFlag{"years", 0, "DEPRECATED; Use --expires instead", ""}, cli.StringFlag{"expires", "18 months", "How long until the certificate expires. Example: 1 year 2 days 3 months 4 hours", ""}, + cli.StringFlag{"notbefore", "18 months", "Sets NotBefore. If blank, will use current time", ""}, cli.StringFlag{"organization, o", "", "CA Certificate organization", ""}, cli.StringFlag{"organizational-unit, ou", "", "CA Certificate organizational unit", ""}, cli.StringFlag{"country, c", "", "CA Certificate country", ""}, @@ -80,6 +82,18 @@ func initAction(c *cli.Context) { os.Exit(1) } + var notBeforeTime time.Time + if c.IsSet("notbefore") { + notBefore := c.String("notbefore") + notBeforeTime, err = parseNotBefore(notBefore) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid notbefore: %s\n", err) + os.Exit(1) + } + } else { + notBeforeTime = time.Now().Add(-time.Minute * 10).UTC() + } + var passphrase []byte if c.IsSet("passphrase") { passphrase = []byte(c.String("passphrase")) @@ -113,7 +127,7 @@ 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")) + crt, err := pkix.CreateCertificateAuthority(key, c.String("organizational-unit"), expiresTime, notBeforeTime, c.String("organization"), c.String("country"), c.String("province"), c.String("locality"), c.String("common-name")) if err != nil { fmt.Fprintln(os.Stderr, "Create certificate error:", err) os.Exit(1) diff --git a/cmd/revoke_test.go b/cmd/revoke_test.go index 65bc10b..3ea385e 100644 --- a/cmd/revoke_test.go +++ b/cmd/revoke_test.go @@ -73,7 +73,7 @@ func setupCA(t *testing.T, dt depot.Depot) { } // create certificate authority - caCert, err := pkix.CreateCertificateAuthority(key, caName, time.Now().Add(1*time.Minute), "", "", "", "", caName) + caCert, err := pkix.CreateCertificateAuthority(key, caName, time.Now().Add(1*time.Minute), time.Now().Add(-time.Minute*10).UTC(), "", "", "", "", caName) if err != nil { t.Fatalf("could not create authority cert: %v", err) } diff --git a/cmd/sign.go b/cmd/sign.go index cc7a772..7c5b890 100644 --- a/cmd/sign.go +++ b/cmd/sign.go @@ -21,6 +21,7 @@ import ( "fmt" "os" "strings" + "time" "github.com/codegangsta/cli" "github.com/square/certstrap/depot" @@ -37,6 +38,7 @@ func NewSignCommand() cli.Command { cli.StringFlag{"passphrase", "", "Passphrase to decrypt private-key PEM block of CA", ""}, cli.IntFlag{"years", 0, "DEPRECATED; Use --expires instead", ""}, cli.StringFlag{"expires", "2 years", "How long until the certificate expires. Example: 1 year 2 days 3 months 4 hours", ""}, + cli.StringFlag{"notbefore", "18 months", "NotBefore. If blank, will use current time", ""}, cli.StringFlag{"CA", "", "CA to sign cert", ""}, cli.BoolFlag{"stdout", "Print certificate to stdout in addition to saving file", ""}, cli.BoolFlag{"intermediate", "Generated certificate should be a intermediate", ""}, @@ -72,6 +74,18 @@ func newSignAction(c *cli.Context) { os.Exit(1) } + var notBeforeTime time.Time + if c.IsSet("notbefore") { + notBefore := c.String("notbefore") + notBeforeTime, err = parseNotBefore(notBefore) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid notbefore: %s\n", err) + os.Exit(1) + } + } else { + notBeforeTime = time.Now().Add(-time.Minute * 10).UTC() + } + csr, err := depot.GetCertificateSigningRequest(d, formattedReqName) if err != nil { fmt.Fprintln(os.Stderr, "Get certificate request error:", err) @@ -108,7 +122,7 @@ func newSignAction(c *cli.Context) { var crtOut *pkix.Certificate if c.Bool("intermediate") { fmt.Fprintf(os.Stderr, "Building intermediate") - crtOut, err = pkix.CreateIntermediateCertificateAuthority(crt, key, csr, expiresTime) + crtOut, err = pkix.CreateIntermediateCertificateAuthority(crt, key, csr, expiresTime, notBeforeTime) } else { crtOut, err = pkix.CreateCertificateHost(crt, key, csr, expiresTime) } diff --git a/pkix/cert_auth.go b/pkix/cert_auth.go index 68c8219..665542d 100644 --- a/pkix/cert_auth.go +++ b/pkix/cert_auth.go @@ -47,9 +47,8 @@ var ( authTemplate = x509.Certificate{ SerialNumber: big.NewInt(1), Subject: authPkixName, - // NotBefore is set to be 10min earlier to fix gap on time difference in cluster - NotBefore: time.Now().Add(-600).UTC(), - NotAfter: time.Time{}, + NotBefore: time.Time{}, + NotAfter: time.Time{}, // Used for certificate signing only KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, @@ -58,7 +57,7 @@ var ( // activate CA BasicConstraintsValid: true, - IsCA: true, + IsCA: true, // Not allow any non-self-issued intermediate CA, sets MaxPathLen=0 MaxPathLenZero: true, @@ -77,13 +76,15 @@ var ( // 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) (*Certificate, error) { +func CreateCertificateAuthority(key *Key, organizationalUnit string, notAfter time.Time, notBefore time.Time, organization string, country string, province string, locality string, commonName string) (*Certificate, error) { subjectKeyID, err := GenerateSubjectKeyID(key.Public) if err != nil { return nil, err } authTemplate.SubjectKeyId = subjectKeyID - authTemplate.NotAfter = expiry + authTemplate.NotAfter = notAfter + authTemplate.NotBefore = notBefore + if len(country) > 0 { authTemplate.Subject.Country = []string{country} } @@ -113,7 +114,7 @@ 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) { +func CreateIntermediateCertificateAuthority(crtAuth *Certificate, keyAuth *Key, csr *CertificateSigningRequest, proposedExpiry time.Time, notBeforeTime time.Time) (*Certificate, error) { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { @@ -136,6 +137,7 @@ func CreateIntermediateCertificateAuthority(crtAuth *Certificate, keyAuth *Key, } else { authTemplate.NotAfter = proposedExpiry } + authTemplate.NotBefore = notBeforeTime authTemplate.SubjectKeyId, err = GenerateSubjectKeyID(rawCsr.PublicKey) if err != nil { diff --git a/pkix/cert_auth_test.go b/pkix/cert_auth_test.go index 00424a4..e623085 100644 --- a/pkix/cert_auth_test.go +++ b/pkix/cert_auth_test.go @@ -28,7 +28,7 @@ func TestCreateCertificateAuthority(t *testing.T) { t.Fatal("Failed creating rsa key:", err) } - crt, err := CreateCertificateAuthority(key, "OU", time.Now().AddDate(5, 0, 0), "test", "US", "California", "San Francisco", "CA Name") + crt, err := CreateCertificateAuthority(key, "OU", time.Now().AddDate(5, 0, 0), time.Now().Add(-time.Minute*10).UTC(), "test", "US", "California", "San Francisco", "CA Name") if err != nil { t.Fatal("Failed creating certificate authority:", err) } diff --git a/pkix/cert_host.go b/pkix/cert_host.go index dcbd71c..1073c8e 100644 --- a/pkix/cert_host.go +++ b/pkix/cert_host.go @@ -33,7 +33,7 @@ var ( // **SHOULD** be filled in host info Subject: pkix.Name{}, // NotBefore is set to be 10min earlier to fix gap on time difference in cluster - NotBefore: time.Now().Add(-600).UTC(), + NotBefore: time.Now().Add(-time.Minute * 10).UTC(), // 10-year lease NotAfter: time.Time{}, // Used for certificate signing only diff --git a/pkix/crl_test.go b/pkix/crl_test.go index 2f48b01..33ae1a3 100644 --- a/pkix/crl_test.go +++ b/pkix/crl_test.go @@ -49,7 +49,7 @@ func TestCreateCertificateRevocationList(t *testing.T) { t.Fatal("Failed creating rsa key:", err) } - crt, err := CreateCertificateAuthority(key, "OU", time.Now().AddDate(5, 0, 0), "test", "US", "California", "San Francisco", "CA Name") + crt, err := CreateCertificateAuthority(key, "OU", time.Now().AddDate(5, 0, 0), time.Now().Add(-time.Minute*10).UTC(), "test", "US", "California", "San Francisco", "CA Name") if err != nil { t.Fatal("Failed creating certificate authority:", err) }