Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 47 additions & 8 deletions cmd/expiry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -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
}
41 changes: 41 additions & 0 deletions cmd/expiry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
16 changes: 15 additions & 1 deletion cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"io/ioutil"
"os"
"strings"
"time"

"github.com/codegangsta/cli"
"github.com/square/certstrap/depot"
Expand All @@ -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", ""},
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion cmd/revoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
16 changes: 15 additions & 1 deletion cmd/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"os"
"strings"
"time"

"github.com/codegangsta/cli"
"github.com/square/certstrap/depot"
Expand All @@ -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", ""},
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down
16 changes: 9 additions & 7 deletions pkix/cert_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand All @@ -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,

Expand All @@ -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}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkix/cert_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion pkix/cert_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkix/crl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down