From 7958f6ebb50b263dabbd1b68724d907560caea1e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 19 Mar 2021 13:19:49 -0700 Subject: [PATCH] Add support for lifetime. --- cas/stepcas/stepcas.go | 15 +++++++-- cas/stepcas/x5c_issuer.go | 20 +++++++++++- cas/stepcas/x5c_issuer_test.go | 59 ++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/cas/stepcas/stepcas.go b/cas/stepcas/stepcas.go index 86a995ef..8b468e25 100644 --- a/cas/stepcas/stepcas.go +++ b/cas/stepcas/stepcas.go @@ -172,8 +172,9 @@ func (s *StepCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.D } resp, err := s.client.Sign(&api.SignRequest{ - CsrPEM: api.CertificateRequest{CertificateRequest: cr}, - OTT: token, + CsrPEM: api.CertificateRequest{CertificateRequest: cr}, + OTT: token, + NotAfter: s.lifetime(lifetime), }) if err != nil { return nil, nil, err @@ -203,3 +204,13 @@ func (s *StepCAS) revokeToken(subject string) (string, error) { return "", errors.New("stepCAS does not have any provisioner configured") } + +func (s *StepCAS) lifetime(d time.Duration) api.TimeDuration { + if s.x5c != nil { + d = s.x5c.Lifetime(d) + } + var td api.TimeDuration + td.SetDuration(d) + println(td.String(), d.String()) + return td +} diff --git a/cas/stepcas/x5c_issuer.go b/cas/stepcas/x5c_issuer.go index 8046b8ae..d5d9a0f0 100644 --- a/cas/stepcas/x5c_issuer.go +++ b/cas/stepcas/x5c_issuer.go @@ -17,6 +17,12 @@ import ( const defaultValidity = 5 * time.Minute +// timeNow returns the current time. +// This method is used for unit testing purposes. +var timeNow = func() time.Time { + return time.Now() +} + type x5cIssuer struct { caURL *url.URL certFile string @@ -58,6 +64,18 @@ func (i *x5cIssuer) RevokeToken(subject string) (string, error) { return i.createToken(aud, subject, nil) } +func (i *x5cIssuer) Lifetime(d time.Duration) time.Duration { + cert, err := pemutil.ReadCertificate(i.certFile, pemutil.WithFirstBlock()) + if err != nil { + return d + } + now := timeNow() + if now.Add(d + time.Minute).After(cert.NotAfter) { + return cert.NotAfter.Sub(now) - time.Minute + } + return d +} + func (i *x5cIssuer) createToken(aud, sub string, sans []string) (string, error) { signer, err := newX5CSigner(i.certFile, i.keyFile) if err != nil { @@ -86,7 +104,7 @@ func (i *x5cIssuer) createToken(aud, sub string, sans []string) (string, error) } func defaultClaims(iss, sub, aud, id string) jose.Claims { - now := time.Now() + now := timeNow() return jose.Claims{ ID: id, Issuer: iss, diff --git a/cas/stepcas/x5c_issuer_test.go b/cas/stepcas/x5c_issuer_test.go index f8972741..a3190255 100644 --- a/cas/stepcas/x5c_issuer_test.go +++ b/cas/stepcas/x5c_issuer_test.go @@ -11,6 +11,7 @@ import ( "net/url" "reflect" "testing" + "time" "go.step.sm/crypto/jose" ) @@ -25,6 +26,17 @@ func (b noneSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) return digest, nil } +func fakeTime(t *testing.T) { + t.Helper() + tmp := timeNow + t.Cleanup(func() { + timeNow = tmp + }) + timeNow = func() time.Time { + return testX5CCrt.NotBefore + } +} + func Test_x5cIssuer_SignToken(t *testing.T) { caURL, err := url.Parse("https://ca.smallstep.com") if err != nil { @@ -154,6 +166,53 @@ func Test_x5cIssuer_RevokeToken(t *testing.T) { } } +func Test_x5cIssuer_Lifetime(t *testing.T) { + fakeTime(t) + caURL, err := url.Parse("https://ca.smallstep.com") + if err != nil { + t.Fatal(err) + } + + // With a leeway of 1m the max duration will be 59m. + maxDuration := testX5CCrt.NotAfter.Sub(timeNow()) - time.Minute + + type fields struct { + caURL *url.URL + certFile string + keyFile string + issuer string + } + type args struct { + d time.Duration + } + tests := []struct { + name string + fields fields + args args + want time.Duration + }{ + {"ok 0s", fields{caURL, testX5CPath, testX5CKeyPath, "X5C"}, args{0}, 0}, + {"ok 1m", fields{caURL, testX5CPath, testX5CKeyPath, "X5C"}, args{time.Minute}, time.Minute}, + {"ok max-1m", fields{caURL, testX5CPath, testX5CKeyPath, "X5C"}, args{maxDuration - time.Minute}, maxDuration - time.Minute}, + {"ok max", fields{caURL, testX5CPath, testX5CKeyPath, "X5C"}, args{maxDuration}, maxDuration}, + {"ok max+1m", fields{caURL, testX5CPath, testX5CKeyPath, "X5C"}, args{maxDuration + time.Minute}, maxDuration}, + {"ok fail", fields{caURL, testX5CPath + ".missing", testX5CKeyPath, "X5C"}, args{maxDuration + time.Minute}, maxDuration + time.Minute}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := &x5cIssuer{ + caURL: tt.fields.caURL, + certFile: tt.fields.certFile, + keyFile: tt.fields.keyFile, + issuer: tt.fields.issuer, + } + if got := i.Lifetime(tt.args.d); got != tt.want { + t.Errorf("x5cIssuer.Lifetime() = %v, want %v", got, tt.want) + } + }) + } +} + func Test_newJoseSigner(t *testing.T) { mustSigner := func(args ...interface{}) crypto.Signer { if err := args[len(args)-1]; err != nil {