From 56926b9012b4e89377f923f0391db489a01280d4 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Sat, 30 Oct 2021 15:52:50 +0800 Subject: [PATCH 01/66] initial support for CRL --- api/api.go | 2 + api/crl.go | 16 +++++++ authority/tls.go | 95 ++++++++++++++++++++++++++++++++++++++++++ cas/apiv1/services.go | 6 +++ cas/softcas/softcas.go | 12 ++++++ db/db.go | 69 +++++++++++++++++++++++++++++- db/simple.go | 15 +++++++ 7 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 api/crl.go diff --git a/api/api.go b/api/api.go index 30ba03f9..d8cfa15f 100644 --- a/api/api.go +++ b/api/api.go @@ -46,6 +46,7 @@ type Authority interface { GetRoots() (federation []*x509.Certificate, err error) GetFederation() ([]*x509.Certificate, error) Version() authority.Version + GenerateCertificateRevocationList(force bool) (string, error) } // TimeDuration is an alias of provisioner.TimeDuration @@ -254,6 +255,7 @@ func (h *caHandler) Route(r Router) { r.MethodFunc("POST", "/renew", h.Renew) r.MethodFunc("POST", "/rekey", h.Rekey) r.MethodFunc("POST", "/revoke", h.Revoke) + r.MethodFunc("GET", "/crl", h.CRL) r.MethodFunc("GET", "/provisioners", h.Provisioners) r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", h.ProvisionerKey) r.MethodFunc("GET", "/roots", h.Roots) diff --git a/api/crl.go b/api/crl.go new file mode 100644 index 00000000..50b29d6c --- /dev/null +++ b/api/crl.go @@ -0,0 +1,16 @@ +package api + +import "net/http" + +// CRL is an HTTP handler that returns the current CRL +func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { + crl, err := h.Authority.GenerateCertificateRevocationList(false) + + if err != nil { + w.WriteHeader(500) + return + } + + w.WriteHeader(200) + _, err = w.Write([]byte(crl)) +} diff --git a/authority/tls.go b/authority/tls.go index 839866a2..0bd4a7e7 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -5,9 +5,12 @@ import ( "crypto" "crypto/tls" "crypto/x509" + "crypto/x509/pkix" "encoding/asn1" "encoding/base64" "encoding/pem" + "fmt" + "math/big" "net/http" "time" @@ -426,6 +429,9 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error // Save as revoked in the Db. err = a.revoke(revokedCert, rci) + + // Generate a new CRL so CRL requesters will always get an up-to-date CRL whenever they request it + _, _ = a.GenerateCertificateRevocationList(true) } switch err { case nil: @@ -458,6 +464,95 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn return a.db.Revoke(rci) } +// GenerateCertificateRevocationList returns a PEM representation of a signed CRL. +// It will look for a valid generated CRL in the database, check if it has expired, and generate +// a new CRL on demand if it has expired (or a CRL does not already exist). +// +// force set to true will force regeneration of the CRL regardless of whether it has actually expired +func (a *Authority) GenerateCertificateRevocationList(force bool) (string, error) { + + // check for an existing CRL in the database, and return that if its valid + crlInfo, err := a.db.GetCRL() + + if err != nil { + return "", err + } + + if !force && crlInfo != nil && crlInfo.ExpiresAt.After(time.Now().UTC()) { + return crlInfo.PEM, nil + } + + // some CAS may not implement the CRLGenerator interface, so check before we proceed + caCRLGenerator, ok := a.x509CAService.(casapi.CertificateAuthorityCRLGenerator) + + if !ok { + return "", errors.Errorf("CRL Generator not implemented") + } + + revokedList, err := a.db.GetRevokedCertificates() + + // Number is a monotonically increasing integer (essentially the CRL version number) that we need to + // keep track of and increase every time we generate a new CRL + var n int64 = 0 + var bn big.Int + + if crlInfo != nil { + n = crlInfo.Number + 1 + } + bn.SetInt64(n) + + // Convert our database db.RevokedCertificateInfo types into the pkix representation ready for the + // CAS to sign it + var revokedCertificates []pkix.RevokedCertificate + + for _, revokedCert := range *revokedList { + var sn big.Int + sn.SetString(revokedCert.Serial, 10) + revokedCertificates = append(revokedCertificates, pkix.RevokedCertificate{ + SerialNumber: &sn, + RevocationTime: revokedCert.RevokedAt, + Extensions: nil, + }) + } + + // Create a RevocationList representation ready for the CAS to sign + // TODO: use a config value for the NextUpdate time duration + // TODO: allow SignatureAlgorithm to be specified? + revocationList := x509.RevocationList{ + SignatureAlgorithm: 0, + RevokedCertificates: revokedCertificates, + Number: &bn, + ThisUpdate: time.Now().UTC(), + NextUpdate: time.Now().UTC().Add(time.Minute * 10), + ExtraExtensions: nil, + } + + certificateRevocationList, err := caCRLGenerator.CreateCertificateRevocationList(&revocationList) + if err != nil { + return "", err + } + + // Quick and dirty PEM encoding + // TODO: clean this up + pemCRL := fmt.Sprintf("-----BEGIN X509 CRL-----\n%s\n-----END X509 CRL-----\n", base64.StdEncoding.EncodeToString(certificateRevocationList)) + + // Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the + // expiry time, and the byte-encoded CRL - then store it in the DB + newCRLInfo := db.CertificateRevocationListInfo{ + Number: n, + ExpiresAt: revocationList.NextUpdate, + PEM: pemCRL, + } + + err = a.db.StoreCRL(&newCRLInfo) + if err != nil { + return "", err + } + + // Finally, return our CRL PEM + return pemCRL, nil +} + // GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server. func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { fatal := func(err error) (*tls.Certificate, error) { diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index cf9a5470..143c1f17 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -14,6 +14,12 @@ type CertificateAuthorityService interface { RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) } +// CertificateAuthorityCRLGenerator is an optional interface implemented by CertificateAuthorityService +// that has a method to create a CRL +type CertificateAuthorityCRLGenerator interface { + CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error) +} + // CertificateAuthorityGetter is an interface implemented by a // CertificateAuthorityService that has a method to get the root certificate. type CertificateAuthorityGetter interface { diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index 8e67d016..24e7dd54 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -3,6 +3,7 @@ package softcas import ( "context" "crypto" + "crypto/rand" "crypto/x509" "time" @@ -112,6 +113,17 @@ func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1 }, nil } +// CreateCertificateRevocationList will create a new CRL based on the RevocationList passed to it +func (c *SoftCAS) CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error) { + + revocationList, err := x509.CreateRevocationList(rand.Reader, crl, c.CertificateChain[0], c.Signer) + if err != nil { + return nil, err + } + + return revocationList, nil +} + // CreateCertificateAuthority creates a root or an intermediate certificate. func (c *SoftCAS) CreateCertificateAuthority(req *apiv1.CreateCertificateAuthorityRequest) (*apiv1.CreateCertificateAuthorityResponse, error) { switch { diff --git a/db/db.go b/db/db.go index 2643e577..5826d7b9 100644 --- a/db/db.go +++ b/db/db.go @@ -16,6 +16,7 @@ import ( var ( certsTable = []byte("x509_certs") revokedCertsTable = []byte("revoked_x509_certs") + crlTable = []byte("x509_crl") revokedSSHCertsTable = []byte("revoked_ssh_certs") usedOTTTable = []byte("used_ott") sshCertsTable = []byte("ssh_certs") @@ -24,6 +25,9 @@ var ( sshHostPrincipalsTable = []byte("ssh_host_principals") ) +var crlKey = []byte("crl") //TODO: at the moment we store a single CRL in the database, in a dedicated table. +// is this acceptable? probably not.... + // ErrAlreadyExists can be returned if the DB attempts to set a key that has // been previously set. var ErrAlreadyExists = errors.New("already exists") @@ -47,6 +51,9 @@ type AuthDB interface { IsSSHRevoked(sn string) (bool, error) Revoke(rci *RevokedCertificateInfo) error RevokeSSH(rci *RevokedCertificateInfo) error + GetRevokedCertificates() (*[]RevokedCertificateInfo, error) + GetCRL() (*CertificateRevocationListInfo, error) + StoreCRL(*CertificateRevocationListInfo) error GetCertificate(serialNumber string) (*x509.Certificate, error) StoreCertificate(crt *x509.Certificate) error UseToken(id, tok string) (bool, error) @@ -82,7 +89,7 @@ func New(c *Config) (AuthDB, error) { tables := [][]byte{ revokedCertsTable, certsTable, usedOTTTable, sshCertsTable, sshHostsTable, sshHostPrincipalsTable, sshUsersTable, - revokedSSHCertsTable, + revokedSSHCertsTable, crlTable, } for _, b := range tables { if err := db.CreateTable(b); err != nil { @@ -106,6 +113,14 @@ type RevokedCertificateInfo struct { MTLS bool } +// CertificateRevocationListInfo contains a CRL in PEM and associated metadata to allow a decision on whether +// to regenerate the CRL or not easier +type CertificateRevocationListInfo struct { + Number int64 + ExpiresAt time.Time + PEM string +} + // IsRevoked returns whether or not a certificate with the given identifier // has been revoked. // In the case of an X509 Certificate the `id` should be the Serial Number of @@ -188,6 +203,58 @@ func (db *DB) RevokeSSH(rci *RevokedCertificateInfo) error { } } +// GetRevokedCertificates gets a list of all revoked certificates. +func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { + entries, err := db.List(revokedCertsTable) + if err != nil { + return nil, err + } + var revokedCerts []RevokedCertificateInfo + for _, e := range entries { + var data RevokedCertificateInfo + if err := json.Unmarshal(e.Value, &data); err != nil { + return nil, err + } + revokedCerts = append(revokedCerts, data) + + } + return &revokedCerts, nil +} + +// StoreCRL stores a CRL in the DB +func (db *DB) StoreCRL(crlInfo *CertificateRevocationListInfo) error { + + crlInfoBytes, err := json.Marshal(crlInfo) + if err != nil { + return errors.Wrap(err, "json Marshal error") + } + + if err := db.Set(crlTable, crlKey, crlInfoBytes); err != nil { + return errors.Wrap(err, "database Set error") + } + return nil +} + +// GetCRL gets the existing CRL from the database +func (db *DB) GetCRL() (*CertificateRevocationListInfo, error) { + crlInfoBytes, err := db.Get(crlTable, crlKey) + + if database.IsErrNotFound(err) { + return nil, nil + } + + if err != nil { + return nil, errors.Wrap(err, "database Get error") + } + + var crlInfo CertificateRevocationListInfo + err = json.Unmarshal(crlInfoBytes, &crlInfo) + if err != nil { + return nil, errors.Wrap(err, "json Unmarshal error") + } + return &crlInfo, err +} + // GetCertificate retrieves a certificate by the serial number. func (db *DB) GetCertificate(serialNumber string) (*x509.Certificate, error) { asn1Data, err := db.Get(certsTable, []byte(serialNumber)) diff --git a/db/simple.go b/db/simple.go index 0e5426ec..c34cdb5d 100644 --- a/db/simple.go +++ b/db/simple.go @@ -41,6 +41,21 @@ func (s *SimpleDB) Revoke(rci *RevokedCertificateInfo) error { return ErrNotImplemented } +// GetRevokedCertificates returns a "NotImplemented" error. +func (s *SimpleDB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { + return nil, ErrNotImplemented +} + +// GetCRL returns a "NotImplemented" error. +func (s *SimpleDB) GetCRL() (*CertificateRevocationListInfo, error) { + return nil, ErrNotImplemented +} + +// StoreCRL returns a "NotImplemented" error. +func (s *SimpleDB) StoreCRL(crlInfo *CertificateRevocationListInfo) error { + return ErrNotImplemented +} + // RevokeSSH returns a "NotImplemented" error. func (s *SimpleDB) RevokeSSH(rci *RevokedCertificateInfo) error { return ErrNotImplemented From 8545adea92c039de2aa4d202661febfaec5f498f Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Tue, 2 Nov 2021 13:26:07 +0800 Subject: [PATCH 02/66] change GenerateCertificateRevocationList to return DER, store DER in db instead of PEM, nicer PEM encoding of CRL, add Mock stubs --- api/api.go | 2 +- api/api_test.go | 4 ++++ api/crl.go | 16 ++++++++++++---- authority/tls.go | 21 ++++++++------------- db/db.go | 18 +++++++++++++++--- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/api/api.go b/api/api.go index d8cfa15f..75a0397b 100644 --- a/api/api.go +++ b/api/api.go @@ -46,7 +46,7 @@ type Authority interface { GetRoots() (federation []*x509.Certificate, err error) GetFederation() ([]*x509.Certificate, error) Version() authority.Version - GenerateCertificateRevocationList(force bool) (string, error) + GenerateCertificateRevocationList(force bool) ([]byte, error) } // TimeDuration is an alias of provisioner.TimeDuration diff --git a/api/api_test.go b/api/api_test.go index 89596165..aef6db77 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -580,6 +580,10 @@ type mockAuthority struct { version func() authority.Version } +func (m *mockAuthority) GenerateCertificateRevocationList(force bool) ([]byte, error) { + panic("implement me") +} + // TODO: remove once Authorize is deprecated. func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) { return m.AuthorizeSign(ott) diff --git a/api/crl.go b/api/crl.go index 50b29d6c..46758777 100644 --- a/api/crl.go +++ b/api/crl.go @@ -1,16 +1,24 @@ package api -import "net/http" +import ( + "encoding/pem" + "net/http" +) -// CRL is an HTTP handler that returns the current CRL +// CRL is an HTTP handler that returns the current CRL in PEM format func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { - crl, err := h.Authority.GenerateCertificateRevocationList(false) + crlBytes, err := h.Authority.GenerateCertificateRevocationList(false) if err != nil { w.WriteHeader(500) return } + pemBytes := pem.EncodeToMemory(&pem.Block{ + Type: "X509 CRL", + Bytes: crlBytes, + }) + w.WriteHeader(200) - _, err = w.Write([]byte(crl)) + _, err = w.Write(pemBytes) } diff --git a/authority/tls.go b/authority/tls.go index 0bd4a7e7..b46cb40d 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -9,7 +9,6 @@ import ( "encoding/asn1" "encoding/base64" "encoding/pem" - "fmt" "math/big" "net/http" "time" @@ -469,24 +468,24 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn // a new CRL on demand if it has expired (or a CRL does not already exist). // // force set to true will force regeneration of the CRL regardless of whether it has actually expired -func (a *Authority) GenerateCertificateRevocationList(force bool) (string, error) { +func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error) { // check for an existing CRL in the database, and return that if its valid crlInfo, err := a.db.GetCRL() if err != nil { - return "", err + return nil, err } if !force && crlInfo != nil && crlInfo.ExpiresAt.After(time.Now().UTC()) { - return crlInfo.PEM, nil + return crlInfo.DER, nil } // some CAS may not implement the CRLGenerator interface, so check before we proceed caCRLGenerator, ok := a.x509CAService.(casapi.CertificateAuthorityCRLGenerator) if !ok { - return "", errors.Errorf("CRL Generator not implemented") + return nil, errors.Errorf("CRL Generator not implemented") } revokedList, err := a.db.GetRevokedCertificates() @@ -529,28 +528,24 @@ func (a *Authority) GenerateCertificateRevocationList(force bool) (string, error certificateRevocationList, err := caCRLGenerator.CreateCertificateRevocationList(&revocationList) if err != nil { - return "", err + return nil, err } - // Quick and dirty PEM encoding - // TODO: clean this up - pemCRL := fmt.Sprintf("-----BEGIN X509 CRL-----\n%s\n-----END X509 CRL-----\n", base64.StdEncoding.EncodeToString(certificateRevocationList)) - // Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the // expiry time, and the byte-encoded CRL - then store it in the DB newCRLInfo := db.CertificateRevocationListInfo{ Number: n, ExpiresAt: revocationList.NextUpdate, - PEM: pemCRL, + DER: certificateRevocationList, } err = a.db.StoreCRL(&newCRLInfo) if err != nil { - return "", err + return nil, err } // Finally, return our CRL PEM - return pemCRL, nil + return certificateRevocationList, nil } // GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server. diff --git a/db/db.go b/db/db.go index 5826d7b9..72c4ec71 100644 --- a/db/db.go +++ b/db/db.go @@ -113,12 +113,12 @@ type RevokedCertificateInfo struct { MTLS bool } -// CertificateRevocationListInfo contains a CRL in PEM and associated metadata to allow a decision on whether -// to regenerate the CRL or not easier +// CertificateRevocationListInfo contains a CRL in DER format and associated +// metadata to allow a decision on whether to regenerate the CRL or not easier type CertificateRevocationListInfo struct { Number int64 ExpiresAt time.Time - PEM string + DER []byte } // IsRevoked returns whether or not a certificate with the given identifier @@ -378,6 +378,18 @@ type MockAuthDB struct { MShutdown func() error } +func (m *MockAuthDB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { + panic("implement me") +} + +func (m *MockAuthDB) GetCRL() (*CertificateRevocationListInfo, error) { + panic("implement me") +} + +func (m *MockAuthDB) StoreCRL(info *CertificateRevocationListInfo) error { + panic("implement me") +} + // IsRevoked mock. func (m *MockAuthDB) IsRevoked(sn string) (bool, error) { if m.MIsRevoked != nil { From 26cb52a573bef0cc1d3c509d355ba1b2c1afb0cc Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Tue, 2 Nov 2021 16:39:29 +0800 Subject: [PATCH 03/66] missed some mentions of PEM when changing the returned format to DER regarding CRL generation --- authority/tls.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/authority/tls.go b/authority/tls.go index b46cb40d..82179f13 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -463,7 +463,7 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn return a.db.Revoke(rci) } -// GenerateCertificateRevocationList returns a PEM representation of a signed CRL. +// GenerateCertificateRevocationList returns a DER representation of a signed CRL. // It will look for a valid generated CRL in the database, check if it has expired, and generate // a new CRL on demand if it has expired (or a CRL does not already exist). // @@ -532,7 +532,7 @@ func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error } // Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the - // expiry time, and the byte-encoded CRL - then store it in the DB + // expiry time, and the DER-encoded CRL - then store it in the DB newCRLInfo := db.CertificateRevocationListInfo{ Number: n, ExpiresAt: revocationList.NextUpdate, @@ -544,7 +544,7 @@ func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error return nil, err } - // Finally, return our CRL PEM + // Finally, return our CRL in DER return certificateRevocationList, nil } From 222b52db130543bc6b6497fecddc8c7e7f084a69 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Thu, 4 Nov 2021 14:05:07 +0800 Subject: [PATCH 04/66] implement changes from review --- api/api.go | 3 +- api/crl.go | 44 ++++++++++++++--- authority/authority.go | 70 +++++++++++++++++++++++++++ authority/config/config.go | 7 +++ authority/tls.go | 98 +++++++++++++++++++++++++++----------- cas/apiv1/requests.go | 10 ++++ cas/apiv1/services.go | 2 +- cas/softcas/softcas.go | 8 ++-- db/db.go | 28 +++++++++-- 9 files changed, 224 insertions(+), 46 deletions(-) diff --git a/api/api.go b/api/api.go index 75a0397b..fa7602e1 100644 --- a/api/api.go +++ b/api/api.go @@ -46,7 +46,8 @@ type Authority interface { GetRoots() (federation []*x509.Certificate, err error) GetFederation() ([]*x509.Certificate, error) Version() authority.Version - GenerateCertificateRevocationList(force bool) ([]byte, error) + GenerateCertificateRevocationList() error + GetCertificateRevocationList() ([]byte, error) } // TimeDuration is an alias of provisioner.TimeDuration diff --git a/api/crl.go b/api/crl.go index 46758777..4d20cfeb 100644 --- a/api/crl.go +++ b/api/crl.go @@ -2,23 +2,53 @@ package api import ( "encoding/pem" + "fmt" + "github.com/pkg/errors" "net/http" ) -// CRL is an HTTP handler that returns the current CRL in PEM format +// CRL is an HTTP handler that returns the current CRL in DER or PEM format func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { - crlBytes, err := h.Authority.GenerateCertificateRevocationList(false) + crlBytes, err := h.Authority.GetCertificateRevocationList() + + _, formatAsPEM := r.URL.Query()["pem"] if err != nil { w.WriteHeader(500) + _, err = fmt.Fprintf(w, "%v\n", err) + if err != nil { + panic(errors.Wrap(err, "error writing http response")) + } return } - pemBytes := pem.EncodeToMemory(&pem.Block{ - Type: "X509 CRL", - Bytes: crlBytes, - }) + if crlBytes == nil { + w.WriteHeader(404) + _, err = fmt.Fprintln(w, "No CRL available") + if err != nil { + panic(errors.Wrap(err, "error writing http response")) + } + return + } + + if formatAsPEM { + pemBytes := pem.EncodeToMemory(&pem.Block{ + Type: "X509 CRL", + Bytes: crlBytes, + }) + w.Header().Add("Content-Type", "application/x-pem-file") + w.Header().Add("Content-Disposition", "attachment; filename=\"crl.pem\"") + _, err = w.Write(pemBytes) + } else { + w.Header().Add("Content-Type", "application/pkix-crl") + w.Header().Add("Content-Disposition", "attachment; filename=\"crl.der\"") + _, err = w.Write(crlBytes) + } w.WriteHeader(200) - _, err = w.Write(pemBytes) + + if err != nil { + panic(errors.Wrap(err, "error writing http response")) + } + } diff --git a/authority/authority.go b/authority/authority.go index aa8698d7..0b885170 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -64,6 +64,9 @@ type Authority struct { sshCAUserFederatedCerts []ssh.PublicKey sshCAHostFederatedCerts []ssh.PublicKey + // CRL vars + crlChannel chan int + // Do not re-initialize initOnce bool startTime time.Time @@ -550,6 +553,16 @@ func (a *Authority) init() error { // not be repeated. a.initOnce = true + // Start the CRL generator + if a.config.CRL != nil { + if a.config.CRL.Generate && a.config.CRL.CacheDuration.Duration > time.Duration(0) { + err := a.startCRLGenerator() + if err != nil { + return err + } + } + } + return nil } @@ -615,3 +628,60 @@ func (a *Authority) requiresSCEPService() bool { func (a *Authority) GetSCEPService() *scep.Service { return a.scepService } + +func (a *Authority) startCRLGenerator() error { + + if a.config.CRL.CacheDuration.Duration > time.Duration(0) { + // Check that there is a valid CRL in the DB right now. If it doesnt exist + // or is expired, generated one now + crlDB, ok := a.db.(db.CertificateRevocationListDB) + if !ok { + return errors.Errorf("CRL Generation requested, but database does not support CRL generation") + } + + crlInfo, err := crlDB.GetCRL() + if err != nil { + return errors.Wrap(err, "could not retrieve CRL from database") + } + + if crlInfo == nil { + log.Println("No CRL exists in the DB, generating one now") + err = a.GenerateCertificateRevocationList() + if err != nil { + return errors.Wrap(err, "could not generate a CRL") + } + } + + if crlInfo.ExpiresAt.Before(time.Now().UTC()) { + log.Printf("Existing CRL has expired (at %v), generating a new one", crlInfo.ExpiresAt) + err = a.GenerateCertificateRevocationList() + if err != nil { + return errors.Wrap(err, "could not generate a CRL") + } + } + + log.Printf("CRL will be auto-generated every %v", a.config.CRL.CacheDuration) + tickerDuration := a.config.CRL.CacheDuration.Duration - time.Minute // generate the new CRL 1 minute before it expires + if tickerDuration <= 0 { + log.Printf("WARNING: Addition of jitter to CRL generation time %v creates a negative duration (%v). Using 1 minute cacheDuration", a.config.CRL.CacheDuration, tickerDuration) + tickerDuration = time.Minute + } + crlTicker := time.NewTicker(tickerDuration) + + go func() { + for { + select { + case <-crlTicker.C: + log.Println("Regenerating CRL") + err := a.GenerateCertificateRevocationList() + if err != nil { + // TODO: log or panic here? + panic(errors.Wrap(err, "authority.crlGenerator encountered an error")) + } + } + } + }() + } + + return nil +} diff --git a/authority/config/config.go b/authority/config/config.go index 75c32994..9177dc22 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -64,6 +64,7 @@ type Config struct { TLS *TLSOptions `json:"tls,omitempty"` Password string `json:"password,omitempty"` Templates *templates.Templates `json:"templates,omitempty"` + CRL *CRLConfig `json:"crl,omitempty"` } // ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer @@ -95,6 +96,12 @@ type AuthConfig struct { EnableAdmin bool `json:"enableAdmin,omitempty"` } +// CRLConfig represents config options for CRL generation +type CRLConfig struct { + Generate bool `json:"generate,omitempty"` + CacheDuration *provisioner.Duration `json:"cacheDuration,omitempty"` +} + // init initializes the required fields in the AuthConfig if they are not // provided. func (c *AuthConfig) init() { diff --git a/authority/tls.go b/authority/tls.go index 82179f13..8f113342 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -364,6 +364,16 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error p provisioner.Interface err error ) + + // Attempt to get the certificate expiry using the serial number. + cert, err := a.db.GetCertificate(revokeOpts.Serial) + + // Revocation of a certificate not in the database may be requested, so fill in the expiry only + // if we can + if err == nil { + rci.ExpiresAt = cert.NotAfter + } + // If not mTLS then get the TokenID of the token. if !revokeOpts.MTLS { token, err := jose.ParseSigned(revokeOpts.OTT) @@ -430,7 +440,10 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error err = a.revoke(revokedCert, rci) // Generate a new CRL so CRL requesters will always get an up-to-date CRL whenever they request it - _, _ = a.GenerateCertificateRevocationList(true) + err = a.GenerateCertificateRevocationList() + if err != nil { + return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...) + } } switch err { case nil: @@ -463,36 +476,64 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn return a.db.Revoke(rci) } -// GenerateCertificateRevocationList returns a DER representation of a signed CRL. -// It will look for a valid generated CRL in the database, check if it has expired, and generate -// a new CRL on demand if it has expired (or a CRL does not already exist). -// -// force set to true will force regeneration of the CRL regardless of whether it has actually expired -func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error) { - - // check for an existing CRL in the database, and return that if its valid - crlInfo, err := a.db.GetCRL() - - if err != nil { - return nil, err +// GetCertificateRevocationList will return the currently generated CRL from the DB, or a not implemented +// error if the underlying AuthDB does not support CRLs +func (a *Authority) GetCertificateRevocationList() ([]byte, error) { + if a.config.CRL == nil { + return nil, errs.Wrap(http.StatusInternalServerError, errors.Errorf("Certificate Revocation Lists are not enabled"), "authority.GetCertificateRevocationList") } - if !force && crlInfo != nil && crlInfo.ExpiresAt.After(time.Now().UTC()) { - return crlInfo.DER, nil + crlDB, ok := a.db.(db.CertificateRevocationListDB) + if !ok { + return nil, errs.Wrap(http.StatusInternalServerError, errors.Errorf("Database does not support Certificate Revocation Lists"), "authority.GetCertificateRevocationList") + } + + crlInfo, err := crlDB.GetCRL() + if err != nil { + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetCertificateRevocationList") + + } + + if crlInfo == nil { + return nil, nil + } + + return crlInfo.DER, nil +} + +// GenerateCertificateRevocationList generates a DER representation of a signed CRL and stores it in the +// database. Returns nil if CRL generation has been disabled in the config +func (a *Authority) GenerateCertificateRevocationList() error { + + if a.config.CRL == nil { + // CRL is disabled + return nil + } + + crlDB, ok := a.db.(db.CertificateRevocationListDB) + if !ok { + return errors.Errorf("Database does not support CRL generation") } // some CAS may not implement the CRLGenerator interface, so check before we proceed caCRLGenerator, ok := a.x509CAService.(casapi.CertificateAuthorityCRLGenerator) - if !ok { - return nil, errors.Errorf("CRL Generator not implemented") + return errors.Errorf("CA does not support CRL Generation") } - revokedList, err := a.db.GetRevokedCertificates() + crlInfo, err := crlDB.GetCRL() + if err != nil { + return errors.Wrap(err, "could not retrieve CRL from database") + } + + revokedList, err := crlDB.GetRevokedCertificates() + if err != nil { + return errors.Wrap(err, "could not retrieve revoked certificates list from database") + } // Number is a monotonically increasing integer (essentially the CRL version number) that we need to // keep track of and increase every time we generate a new CRL - var n int64 = 0 + var n int64 var bn big.Int if crlInfo != nil { @@ -515,37 +556,36 @@ func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error } // Create a RevocationList representation ready for the CAS to sign - // TODO: use a config value for the NextUpdate time duration // TODO: allow SignatureAlgorithm to be specified? revocationList := x509.RevocationList{ SignatureAlgorithm: 0, RevokedCertificates: revokedCertificates, Number: &bn, ThisUpdate: time.Now().UTC(), - NextUpdate: time.Now().UTC().Add(time.Minute * 10), + NextUpdate: time.Now().UTC().Add(a.config.CRL.CacheDuration.Duration), ExtraExtensions: nil, } - certificateRevocationList, err := caCRLGenerator.CreateCertificateRevocationList(&revocationList) + certificateRevocationList, err := caCRLGenerator.CreateCRL(&casapi.CreateCRLRequest{RevocationList: &revocationList}) if err != nil { - return nil, err + return errors.Wrap(err, "could not create CRL") } // Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the - // expiry time, and the DER-encoded CRL - then store it in the DB + // expiry time, and the DER-encoded CRL newCRLInfo := db.CertificateRevocationListInfo{ Number: n, ExpiresAt: revocationList.NextUpdate, - DER: certificateRevocationList, + DER: certificateRevocationList.CRL, } - err = a.db.StoreCRL(&newCRLInfo) + // Store the CRL in the database ready for retrieval by api endpoints + err = crlDB.StoreCRL(&newCRLInfo) if err != nil { - return nil, err + return errors.Wrap(err, "could not store CRL in database") } - // Finally, return our CRL in DER - return certificateRevocationList, nil + return nil } // GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server. diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go index bf745c17..630cecce 100644 --- a/cas/apiv1/requests.go +++ b/cas/apiv1/requests.go @@ -144,3 +144,13 @@ type CreateCertificateAuthorityResponse struct { PrivateKey crypto.PrivateKey Signer crypto.Signer } + +// CreateCRLRequest is the request to create a Certificate Revocation List. +type CreateCRLRequest struct { + RevocationList *x509.RevocationList +} + +// CreateCRLResponse is the response to a Certificate Revocation List request. +type CreateCRLResponse struct { + CRL []byte //the CRL in DER format +} diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index 143c1f17..ce7d0364 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -17,7 +17,7 @@ type CertificateAuthorityService interface { // CertificateAuthorityCRLGenerator is an optional interface implemented by CertificateAuthorityService // that has a method to create a CRL type CertificateAuthorityCRLGenerator interface { - CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error) + CreateCRL(req *CreateCRLRequest) (*CreateCRLResponse, error) } // CertificateAuthorityGetter is an interface implemented by a diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index 24e7dd54..23e48d45 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -113,15 +113,15 @@ func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1 }, nil } -// CreateCertificateRevocationList will create a new CRL based on the RevocationList passed to it -func (c *SoftCAS) CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error) { +// CreateCRL will create a new CRL based on the RevocationList passed to it +func (c *SoftCAS) CreateCRL(req *apiv1.CreateCRLRequest) (*apiv1.CreateCRLResponse, error) { - revocationList, err := x509.CreateRevocationList(rand.Reader, crl, c.CertificateChain[0], c.Signer) + revocationListBytes, err := x509.CreateRevocationList(rand.Reader, req.RevocationList, c.CertificateChain[0], c.Signer) if err != nil { return nil, err } - return revocationList, nil + return &apiv1.CreateCRLResponse{CRL: revocationListBytes}, nil } // CreateCertificateAuthority creates a root or an intermediate certificate. diff --git a/db/db.go b/db/db.go index 72c4ec71..21cea901 100644 --- a/db/db.go +++ b/db/db.go @@ -51,9 +51,6 @@ type AuthDB interface { IsSSHRevoked(sn string) (bool, error) Revoke(rci *RevokedCertificateInfo) error RevokeSSH(rci *RevokedCertificateInfo) error - GetRevokedCertificates() (*[]RevokedCertificateInfo, error) - GetCRL() (*CertificateRevocationListInfo, error) - StoreCRL(*CertificateRevocationListInfo) error GetCertificate(serialNumber string) (*x509.Certificate, error) StoreCertificate(crt *x509.Certificate) error UseToken(id, tok string) (bool, error) @@ -63,6 +60,13 @@ type AuthDB interface { Shutdown() error } +// CertificateRevocationListDB is an interface to indicate whether the DB supports CRL generation +type CertificateRevocationListDB interface { + GetRevokedCertificates() (*[]RevokedCertificateInfo, error) + GetCRL() (*CertificateRevocationListInfo, error) + StoreCRL(*CertificateRevocationListInfo) error +} + // DB is a wrapper over the nosql.DB interface. type DB struct { nosql.DB @@ -109,6 +113,7 @@ type RevokedCertificateInfo struct { ReasonCode int Reason string RevokedAt time.Time + ExpiresAt time.Time TokenID string MTLS bool } @@ -215,7 +220,22 @@ func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { if err := json.Unmarshal(e.Value, &data); err != nil { return nil, err } - revokedCerts = append(revokedCerts, data) + + if !data.ExpiresAt.IsZero() && data.ExpiresAt.After(time.Now().UTC()) { + revokedCerts = append(revokedCerts, data) + } else if data.ExpiresAt.IsZero() { + cert, err := db.GetCertificate(data.Serial) + if err != nil { + revokedCerts = append(revokedCerts, data) // a revoked certificate may not be in the database, + // so its expiry date is undiscoverable and will need + // to be added to the crl always + continue + } + + if cert.NotAfter.After(time.Now().UTC()) { + revokedCerts = append(revokedCerts, data) + } + } } return &revokedCerts, nil From 45975b061c36b8cd3525d651635fa39c7a96a110 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Tue, 29 Mar 2022 08:51:39 +0800 Subject: [PATCH 05/66] requested changes --- api/crl.go | 2 -- authority/authority.go | 77 ++++++++++++++++++------------------------ authority/tls.go | 4 +-- 3 files changed, 34 insertions(+), 49 deletions(-) diff --git a/api/crl.go b/api/crl.go index 4d20cfeb..c90a77f9 100644 --- a/api/crl.go +++ b/api/crl.go @@ -45,8 +45,6 @@ func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { _, err = w.Write(crlBytes) } - w.WriteHeader(200) - if err != nil { panic(errors.Wrap(err, "error writing http response")) } diff --git a/authority/authority.go b/authority/authority.go index 0b885170..5f52d04e 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "crypto/x509" "encoding/hex" + "fmt" "log" "strings" "sync" @@ -631,57 +632,43 @@ func (a *Authority) GetSCEPService() *scep.Service { func (a *Authority) startCRLGenerator() error { - if a.config.CRL.CacheDuration.Duration > time.Duration(0) { - // Check that there is a valid CRL in the DB right now. If it doesnt exist - // or is expired, generated one now - crlDB, ok := a.db.(db.CertificateRevocationListDB) - if !ok { - return errors.Errorf("CRL Generation requested, but database does not support CRL generation") - } + if a.config.CRL.CacheDuration.Duration <= 0 { + return nil + } - crlInfo, err := crlDB.GetCRL() - if err != nil { - return errors.Wrap(err, "could not retrieve CRL from database") - } + // Check that there is a valid CRL in the DB right now. If it doesn't exist + // or is expired, generate one now + _, ok := a.db.(db.CertificateRevocationListDB) + if !ok { + return errors.Errorf("CRL Generation requested, but database does not support CRL generation") + } - if crlInfo == nil { - log.Println("No CRL exists in the DB, generating one now") - err = a.GenerateCertificateRevocationList() - if err != nil { - return errors.Wrap(err, "could not generate a CRL") - } - } + // Always create a new CRL on startup in case the CA has been down and the time to next expected CRL + // update is less than the cache duration. + err := a.GenerateCertificateRevocationList() + if err != nil { + return errors.Wrap(err, "could not generate a CRL") + } - if crlInfo.ExpiresAt.Before(time.Now().UTC()) { - log.Printf("Existing CRL has expired (at %v), generating a new one", crlInfo.ExpiresAt) - err = a.GenerateCertificateRevocationList() - if err != nil { - return errors.Wrap(err, "could not generate a CRL") - } - } + log.Printf("CRL will be auto-generated every %v", a.config.CRL.CacheDuration) + tickerDuration := a.config.CRL.CacheDuration.Duration - time.Minute // generate the new CRL 1 minute before it expires + if tickerDuration <= 0 { + panic(fmt.Sprintf("ERROR: Addition of jitter to CRL generation time %v creates a negative duration (%v). Use a CRL generation time of longer than 1 minute.", a.config.CRL.CacheDuration, tickerDuration)) + } + crlTicker := time.NewTicker(tickerDuration) - log.Printf("CRL will be auto-generated every %v", a.config.CRL.CacheDuration) - tickerDuration := a.config.CRL.CacheDuration.Duration - time.Minute // generate the new CRL 1 minute before it expires - if tickerDuration <= 0 { - log.Printf("WARNING: Addition of jitter to CRL generation time %v creates a negative duration (%v). Using 1 minute cacheDuration", a.config.CRL.CacheDuration, tickerDuration) - tickerDuration = time.Minute - } - crlTicker := time.NewTicker(tickerDuration) - - go func() { - for { - select { - case <-crlTicker.C: - log.Println("Regenerating CRL") - err := a.GenerateCertificateRevocationList() - if err != nil { - // TODO: log or panic here? - panic(errors.Wrap(err, "authority.crlGenerator encountered an error")) - } + go func() { + for { + select { + case <-crlTicker.C: + log.Println("Regenerating CRL") + err := a.GenerateCertificateRevocationList() + if err != nil { + log.Printf("ERROR: authority.crlGenerator encountered an error when regenerating the CRL: %v", err) } } - }() - } + } + }() return nil } diff --git a/authority/tls.go b/authority/tls.go index 8f113342..ba1c4939 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -480,12 +480,12 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn // error if the underlying AuthDB does not support CRLs func (a *Authority) GetCertificateRevocationList() ([]byte, error) { if a.config.CRL == nil { - return nil, errs.Wrap(http.StatusInternalServerError, errors.Errorf("Certificate Revocation Lists are not enabled"), "authority.GetCertificateRevocationList") + return nil, errs.Wrap(http.StatusNotFound, errors.Errorf("Certificate Revocation Lists are not enabled"), "authority.GetCertificateRevocationList") } crlDB, ok := a.db.(db.CertificateRevocationListDB) if !ok { - return nil, errs.Wrap(http.StatusInternalServerError, errors.Errorf("Database does not support Certificate Revocation Lists"), "authority.GetCertificateRevocationList") + return nil, errs.Wrap(http.StatusNotImplemented, errors.Errorf("Database does not support Certificate Revocation Lists"), "authority.GetCertificateRevocationList") } crlInfo, err := crlDB.GetCRL() From 8520c861d554b31eb21e92603b690c06790ff2d4 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Tue, 5 Apr 2022 11:19:13 +0800 Subject: [PATCH 06/66] implemented some requested changes --- api/api.go | 1 - api/api_test.go | 2 +- api/crl.go | 18 +++++++++--------- authority/authority.go | 15 ++++++++++++--- authority/tls.go | 14 ++++++++------ db/db.go | 6 ++++-- 6 files changed, 34 insertions(+), 22 deletions(-) diff --git a/api/api.go b/api/api.go index fa7602e1..f20df474 100644 --- a/api/api.go +++ b/api/api.go @@ -46,7 +46,6 @@ type Authority interface { GetRoots() (federation []*x509.Certificate, err error) GetFederation() ([]*x509.Certificate, error) Version() authority.Version - GenerateCertificateRevocationList() error GetCertificateRevocationList() ([]byte, error) } diff --git a/api/api_test.go b/api/api_test.go index aef6db77..7ce54d73 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -580,7 +580,7 @@ type mockAuthority struct { version func() authority.Version } -func (m *mockAuthority) GenerateCertificateRevocationList(force bool) ([]byte, error) { +func (m *mockAuthority) GetCertificateRevocationList() ([]byte, error) { panic("implement me") } diff --git a/api/crl.go b/api/crl.go index c90a77f9..45024470 100644 --- a/api/crl.go +++ b/api/crl.go @@ -4,6 +4,7 @@ import ( "encoding/pem" "fmt" "github.com/pkg/errors" + "github.com/smallstep/certificates/errs" "net/http" ) @@ -14,6 +15,14 @@ func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { _, formatAsPEM := r.URL.Query()["pem"] if err != nil { + + caErr, isCaErr := err.(*errs.Error) + + if isCaErr { + http.Error(w, caErr.Msg, caErr.Status) + return + } + w.WriteHeader(500) _, err = fmt.Fprintf(w, "%v\n", err) if err != nil { @@ -22,15 +31,6 @@ func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { return } - if crlBytes == nil { - w.WriteHeader(404) - _, err = fmt.Fprintln(w, "No CRL available") - if err != nil { - panic(errors.Wrap(err, "error writing http response")) - } - return - } - if formatAsPEM { pemBytes := pem.EncodeToMemory(&pem.Block{ Type: "X509 CRL", diff --git a/authority/authority.go b/authority/authority.go index 5f52d04e..7414a0d4 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -66,7 +66,7 @@ type Authority struct { sshCAHostFederatedCerts []ssh.PublicKey // CRL vars - crlChannel chan int + crlTicker *time.Ticker // Do not re-initialize initOnce bool @@ -586,6 +586,10 @@ func (a *Authority) IsAdminAPIEnabled() bool { // Shutdown safely shuts down any clients, databases, etc. held by the Authority. func (a *Authority) Shutdown() error { + if a.crlTicker != nil { + a.crlTicker.Stop() + } + if err := a.keyManager.Close(); err != nil { log.Printf("error closing the key manager: %v", err) } @@ -594,6 +598,11 @@ func (a *Authority) Shutdown() error { // CloseForReload closes internal services, to allow a safe reload. func (a *Authority) CloseForReload() { + + if a.crlTicker != nil { + a.crlTicker.Stop() + } + if err := a.keyManager.Close(); err != nil { log.Printf("error closing the key manager: %v", err) } @@ -655,12 +664,12 @@ func (a *Authority) startCRLGenerator() error { if tickerDuration <= 0 { panic(fmt.Sprintf("ERROR: Addition of jitter to CRL generation time %v creates a negative duration (%v). Use a CRL generation time of longer than 1 minute.", a.config.CRL.CacheDuration, tickerDuration)) } - crlTicker := time.NewTicker(tickerDuration) + a.crlTicker = time.NewTicker(tickerDuration) go func() { for { select { - case <-crlTicker.C: + case <-a.crlTicker.C: log.Println("Regenerating CRL") err := a.GenerateCertificateRevocationList() if err != nil { diff --git a/authority/tls.go b/authority/tls.go index ba1c4939..44da6b3a 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -365,13 +365,15 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error err error ) - // Attempt to get the certificate expiry using the serial number. - cert, err := a.db.GetCertificate(revokeOpts.Serial) + if revokeOpts.Crt == nil { + // Attempt to get the certificate expiry using the serial number. + cert, err := a.db.GetCertificate(revokeOpts.Serial) - // Revocation of a certificate not in the database may be requested, so fill in the expiry only - // if we can - if err == nil { - rci.ExpiresAt = cert.NotAfter + // Revocation of a certificate not in the database may be requested, so fill in the expiry only + // if we can + if err == nil { + rci.ExpiresAt = cert.NotAfter + } } // If not mTLS then get the TokenID of the token. diff --git a/db/db.go b/db/db.go index 21cea901..883f54ed 100644 --- a/db/db.go +++ b/db/db.go @@ -215,13 +215,15 @@ func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { return nil, err } var revokedCerts []RevokedCertificateInfo + now := time.Now().UTC() + for _, e := range entries { var data RevokedCertificateInfo if err := json.Unmarshal(e.Value, &data); err != nil { return nil, err } - if !data.ExpiresAt.IsZero() && data.ExpiresAt.After(time.Now().UTC()) { + if !data.ExpiresAt.IsZero() && data.ExpiresAt.After(now) { revokedCerts = append(revokedCerts, data) } else if data.ExpiresAt.IsZero() { cert, err := db.GetCertificate(data.Serial) @@ -232,7 +234,7 @@ func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { continue } - if cert.NotAfter.After(time.Now().UTC()) { + if cert.NotAfter.After(now) { revokedCerts = append(revokedCerts, data) } } From e8fdb703c9a6a825d75e9dc88df0dc2e2e8ba4f2 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Sat, 30 Oct 2021 15:52:50 +0800 Subject: [PATCH 07/66] initial support for CRL --- api/api.go | 2 + api/crl.go | 16 +++++++ authority/tls.go | 94 ++++++++++++++++++++++++++++++++++++++++++ cas/apiv1/services.go | 6 +++ cas/softcas/softcas.go | 12 ++++++ db/db.go | 69 ++++++++++++++++++++++++++++++- db/simple.go | 15 +++++++ 7 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 api/crl.go diff --git a/api/api.go b/api/api.go index da6309fd..7c935f3f 100644 --- a/api/api.go +++ b/api/api.go @@ -50,6 +50,7 @@ type Authority interface { GetRoots() ([]*x509.Certificate, error) GetFederation() ([]*x509.Certificate, error) Version() authority.Version + GenerateCertificateRevocationList(force bool) (string, error) } // TimeDuration is an alias of provisioner.TimeDuration @@ -258,6 +259,7 @@ func (h *caHandler) Route(r Router) { r.MethodFunc("POST", "/renew", h.Renew) r.MethodFunc("POST", "/rekey", h.Rekey) r.MethodFunc("POST", "/revoke", h.Revoke) + r.MethodFunc("GET", "/crl", h.CRL) r.MethodFunc("GET", "/provisioners", h.Provisioners) r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", h.ProvisionerKey) r.MethodFunc("GET", "/roots", h.Roots) diff --git a/api/crl.go b/api/crl.go new file mode 100644 index 00000000..50b29d6c --- /dev/null +++ b/api/crl.go @@ -0,0 +1,16 @@ +package api + +import "net/http" + +// CRL is an HTTP handler that returns the current CRL +func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { + crl, err := h.Authority.GenerateCertificateRevocationList(false) + + if err != nil { + w.WriteHeader(500) + return + } + + w.WriteHeader(200) + _, err = w.Write([]byte(crl)) +} diff --git a/authority/tls.go b/authority/tls.go index 58a1247c..d6d6d6d1 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -5,12 +5,14 @@ import ( "crypto" "crypto/tls" "crypto/x509" + "crypto/x509/pkix" "encoding/asn1" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "net" + "math/big" "net/http" "strings" "time" @@ -470,6 +472,9 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error // Save as revoked in the Db. err = a.revoke(revokedCert, rci) + + // Generate a new CRL so CRL requesters will always get an up-to-date CRL whenever they request it + _, _ = a.GenerateCertificateRevocationList(true) } switch err { case nil: @@ -504,6 +509,95 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn return a.db.Revoke(rci) } +// GenerateCertificateRevocationList returns a PEM representation of a signed CRL. +// It will look for a valid generated CRL in the database, check if it has expired, and generate +// a new CRL on demand if it has expired (or a CRL does not already exist). +// +// force set to true will force regeneration of the CRL regardless of whether it has actually expired +func (a *Authority) GenerateCertificateRevocationList(force bool) (string, error) { + + // check for an existing CRL in the database, and return that if its valid + crlInfo, err := a.db.GetCRL() + + if err != nil { + return "", err + } + + if !force && crlInfo != nil && crlInfo.ExpiresAt.After(time.Now().UTC()) { + return crlInfo.PEM, nil + } + + // some CAS may not implement the CRLGenerator interface, so check before we proceed + caCRLGenerator, ok := a.x509CAService.(casapi.CertificateAuthorityCRLGenerator) + + if !ok { + return "", errors.Errorf("CRL Generator not implemented") + } + + revokedList, err := a.db.GetRevokedCertificates() + + // Number is a monotonically increasing integer (essentially the CRL version number) that we need to + // keep track of and increase every time we generate a new CRL + var n int64 = 0 + var bn big.Int + + if crlInfo != nil { + n = crlInfo.Number + 1 + } + bn.SetInt64(n) + + // Convert our database db.RevokedCertificateInfo types into the pkix representation ready for the + // CAS to sign it + var revokedCertificates []pkix.RevokedCertificate + + for _, revokedCert := range *revokedList { + var sn big.Int + sn.SetString(revokedCert.Serial, 10) + revokedCertificates = append(revokedCertificates, pkix.RevokedCertificate{ + SerialNumber: &sn, + RevocationTime: revokedCert.RevokedAt, + Extensions: nil, + }) + } + + // Create a RevocationList representation ready for the CAS to sign + // TODO: use a config value for the NextUpdate time duration + // TODO: allow SignatureAlgorithm to be specified? + revocationList := x509.RevocationList{ + SignatureAlgorithm: 0, + RevokedCertificates: revokedCertificates, + Number: &bn, + ThisUpdate: time.Now().UTC(), + NextUpdate: time.Now().UTC().Add(time.Minute * 10), + ExtraExtensions: nil, + } + + certificateRevocationList, err := caCRLGenerator.CreateCertificateRevocationList(&revocationList) + if err != nil { + return "", err + } + + // Quick and dirty PEM encoding + // TODO: clean this up + pemCRL := fmt.Sprintf("-----BEGIN X509 CRL-----\n%s\n-----END X509 CRL-----\n", base64.StdEncoding.EncodeToString(certificateRevocationList)) + + // Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the + // expiry time, and the byte-encoded CRL - then store it in the DB + newCRLInfo := db.CertificateRevocationListInfo{ + Number: n, + ExpiresAt: revocationList.NextUpdate, + PEM: pemCRL, + } + + err = a.db.StoreCRL(&newCRLInfo) + if err != nil { + return "", err + } + + // Finally, return our CRL PEM + return pemCRL, nil +} + // GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server. func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { fatal := func(err error) (*tls.Certificate, error) { diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index cf9a5470..143c1f17 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -14,6 +14,12 @@ type CertificateAuthorityService interface { RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) } +// CertificateAuthorityCRLGenerator is an optional interface implemented by CertificateAuthorityService +// that has a method to create a CRL +type CertificateAuthorityCRLGenerator interface { + CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error) +} + // CertificateAuthorityGetter is an interface implemented by a // CertificateAuthorityService that has a method to get the root certificate. type CertificateAuthorityGetter interface { diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index 2a97145b..3c400080 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -3,6 +3,7 @@ package softcas import ( "context" "crypto" + "crypto/rand" "crypto/x509" "time" @@ -129,6 +130,17 @@ func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1 }, nil } +// CreateCertificateRevocationList will create a new CRL based on the RevocationList passed to it +func (c *SoftCAS) CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error) { + + revocationList, err := x509.CreateRevocationList(rand.Reader, crl, c.CertificateChain[0], c.Signer) + if err != nil { + return nil, err + } + + return revocationList, nil +} + // CreateCertificateAuthority creates a root or an intermediate certificate. func (c *SoftCAS) CreateCertificateAuthority(req *apiv1.CreateCertificateAuthorityRequest) (*apiv1.CreateCertificateAuthorityResponse, error) { switch { diff --git a/db/db.go b/db/db.go index 6d48723f..a4c1fa01 100644 --- a/db/db.go +++ b/db/db.go @@ -16,6 +16,7 @@ import ( var ( certsTable = []byte("x509_certs") revokedCertsTable = []byte("revoked_x509_certs") + crlTable = []byte("x509_crl") revokedSSHCertsTable = []byte("revoked_ssh_certs") usedOTTTable = []byte("used_ott") sshCertsTable = []byte("ssh_certs") @@ -24,6 +25,9 @@ var ( sshHostPrincipalsTable = []byte("ssh_host_principals") ) +var crlKey = []byte("crl") //TODO: at the moment we store a single CRL in the database, in a dedicated table. +// is this acceptable? probably not.... + // ErrAlreadyExists can be returned if the DB attempts to set a key that has // been previously set. var ErrAlreadyExists = errors.New("already exists") @@ -47,6 +51,9 @@ type AuthDB interface { IsSSHRevoked(sn string) (bool, error) Revoke(rci *RevokedCertificateInfo) error RevokeSSH(rci *RevokedCertificateInfo) error + GetRevokedCertificates() (*[]RevokedCertificateInfo, error) + GetCRL() (*CertificateRevocationListInfo, error) + StoreCRL(*CertificateRevocationListInfo) error GetCertificate(serialNumber string) (*x509.Certificate, error) StoreCertificate(crt *x509.Certificate) error UseToken(id, tok string) (bool, error) @@ -82,7 +89,7 @@ func New(c *Config) (AuthDB, error) { tables := [][]byte{ revokedCertsTable, certsTable, usedOTTTable, sshCertsTable, sshHostsTable, sshHostPrincipalsTable, sshUsersTable, - revokedSSHCertsTable, + revokedSSHCertsTable, crlTable, } for _, b := range tables { if err := db.CreateTable(b); err != nil { @@ -107,6 +114,14 @@ type RevokedCertificateInfo struct { ACME bool } +// CertificateRevocationListInfo contains a CRL in PEM and associated metadata to allow a decision on whether +// to regenerate the CRL or not easier +type CertificateRevocationListInfo struct { + Number int64 + ExpiresAt time.Time + PEM string +} + // IsRevoked returns whether or not a certificate with the given identifier // has been revoked. // In the case of an X509 Certificate the `id` should be the Serial Number of @@ -189,6 +204,58 @@ func (db *DB) RevokeSSH(rci *RevokedCertificateInfo) error { } } +// GetRevokedCertificates gets a list of all revoked certificates. +func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { + entries, err := db.List(revokedCertsTable) + if err != nil { + return nil, err + } + var revokedCerts []RevokedCertificateInfo + for _, e := range entries { + var data RevokedCertificateInfo + if err := json.Unmarshal(e.Value, &data); err != nil { + return nil, err + } + revokedCerts = append(revokedCerts, data) + + } + return &revokedCerts, nil +} + +// StoreCRL stores a CRL in the DB +func (db *DB) StoreCRL(crlInfo *CertificateRevocationListInfo) error { + + crlInfoBytes, err := json.Marshal(crlInfo) + if err != nil { + return errors.Wrap(err, "json Marshal error") + } + + if err := db.Set(crlTable, crlKey, crlInfoBytes); err != nil { + return errors.Wrap(err, "database Set error") + } + return nil +} + +// GetCRL gets the existing CRL from the database +func (db *DB) GetCRL() (*CertificateRevocationListInfo, error) { + crlInfoBytes, err := db.Get(crlTable, crlKey) + + if database.IsErrNotFound(err) { + return nil, nil + } + + if err != nil { + return nil, errors.Wrap(err, "database Get error") + } + + var crlInfo CertificateRevocationListInfo + err = json.Unmarshal(crlInfoBytes, &crlInfo) + if err != nil { + return nil, errors.Wrap(err, "json Unmarshal error") + } + return &crlInfo, err +} + // GetCertificate retrieves a certificate by the serial number. func (db *DB) GetCertificate(serialNumber string) (*x509.Certificate, error) { asn1Data, err := db.Get(certsTable, []byte(serialNumber)) diff --git a/db/simple.go b/db/simple.go index 0e5426ec..c34cdb5d 100644 --- a/db/simple.go +++ b/db/simple.go @@ -41,6 +41,21 @@ func (s *SimpleDB) Revoke(rci *RevokedCertificateInfo) error { return ErrNotImplemented } +// GetRevokedCertificates returns a "NotImplemented" error. +func (s *SimpleDB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { + return nil, ErrNotImplemented +} + +// GetCRL returns a "NotImplemented" error. +func (s *SimpleDB) GetCRL() (*CertificateRevocationListInfo, error) { + return nil, ErrNotImplemented +} + +// StoreCRL returns a "NotImplemented" error. +func (s *SimpleDB) StoreCRL(crlInfo *CertificateRevocationListInfo) error { + return ErrNotImplemented +} + // RevokeSSH returns a "NotImplemented" error. func (s *SimpleDB) RevokeSSH(rci *RevokedCertificateInfo) error { return ErrNotImplemented From 7d024cc4cb3f50e64fdbc62e812de6e96f55b34b Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Tue, 2 Nov 2021 13:26:07 +0800 Subject: [PATCH 08/66] change GenerateCertificateRevocationList to return DER, store DER in db instead of PEM, nicer PEM encoding of CRL, add Mock stubs --- api/api.go | 2 +- api/crl.go | 16 ++++++++++++---- authority/tls.go | 20 ++++++++------------ db/db.go | 18 +++++++++++++++--- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/api/api.go b/api/api.go index 7c935f3f..ba7d3d17 100644 --- a/api/api.go +++ b/api/api.go @@ -50,7 +50,7 @@ type Authority interface { GetRoots() ([]*x509.Certificate, error) GetFederation() ([]*x509.Certificate, error) Version() authority.Version - GenerateCertificateRevocationList(force bool) (string, error) + GenerateCertificateRevocationList(force bool) ([]byte, error) } // TimeDuration is an alias of provisioner.TimeDuration diff --git a/api/crl.go b/api/crl.go index 50b29d6c..46758777 100644 --- a/api/crl.go +++ b/api/crl.go @@ -1,16 +1,24 @@ package api -import "net/http" +import ( + "encoding/pem" + "net/http" +) -// CRL is an HTTP handler that returns the current CRL +// CRL is an HTTP handler that returns the current CRL in PEM format func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { - crl, err := h.Authority.GenerateCertificateRevocationList(false) + crlBytes, err := h.Authority.GenerateCertificateRevocationList(false) if err != nil { w.WriteHeader(500) return } + pemBytes := pem.EncodeToMemory(&pem.Block{ + Type: "X509 CRL", + Bytes: crlBytes, + }) + w.WriteHeader(200) - _, err = w.Write([]byte(crl)) + _, err = w.Write(pemBytes) } diff --git a/authority/tls.go b/authority/tls.go index d6d6d6d1..d6d80643 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -514,24 +514,24 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn // a new CRL on demand if it has expired (or a CRL does not already exist). // // force set to true will force regeneration of the CRL regardless of whether it has actually expired -func (a *Authority) GenerateCertificateRevocationList(force bool) (string, error) { +func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error) { // check for an existing CRL in the database, and return that if its valid crlInfo, err := a.db.GetCRL() if err != nil { - return "", err + return nil, err } if !force && crlInfo != nil && crlInfo.ExpiresAt.After(time.Now().UTC()) { - return crlInfo.PEM, nil + return crlInfo.DER, nil } // some CAS may not implement the CRLGenerator interface, so check before we proceed caCRLGenerator, ok := a.x509CAService.(casapi.CertificateAuthorityCRLGenerator) if !ok { - return "", errors.Errorf("CRL Generator not implemented") + return nil, errors.Errorf("CRL Generator not implemented") } revokedList, err := a.db.GetRevokedCertificates() @@ -574,28 +574,24 @@ func (a *Authority) GenerateCertificateRevocationList(force bool) (string, error certificateRevocationList, err := caCRLGenerator.CreateCertificateRevocationList(&revocationList) if err != nil { - return "", err + return nil, err } - // Quick and dirty PEM encoding - // TODO: clean this up - pemCRL := fmt.Sprintf("-----BEGIN X509 CRL-----\n%s\n-----END X509 CRL-----\n", base64.StdEncoding.EncodeToString(certificateRevocationList)) - // Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the // expiry time, and the byte-encoded CRL - then store it in the DB newCRLInfo := db.CertificateRevocationListInfo{ Number: n, ExpiresAt: revocationList.NextUpdate, - PEM: pemCRL, + DER: certificateRevocationList, } err = a.db.StoreCRL(&newCRLInfo) if err != nil { - return "", err + return nil, err } // Finally, return our CRL PEM - return pemCRL, nil + return certificateRevocationList, nil } // GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server. diff --git a/db/db.go b/db/db.go index a4c1fa01..534c4000 100644 --- a/db/db.go +++ b/db/db.go @@ -114,12 +114,12 @@ type RevokedCertificateInfo struct { ACME bool } -// CertificateRevocationListInfo contains a CRL in PEM and associated metadata to allow a decision on whether -// to regenerate the CRL or not easier +// CertificateRevocationListInfo contains a CRL in DER format and associated +// metadata to allow a decision on whether to regenerate the CRL or not easier type CertificateRevocationListInfo struct { Number int64 ExpiresAt time.Time - PEM string + DER []byte } // IsRevoked returns whether or not a certificate with the given identifier @@ -379,6 +379,18 @@ type MockAuthDB struct { MShutdown func() error } +func (m *MockAuthDB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { + panic("implement me") +} + +func (m *MockAuthDB) GetCRL() (*CertificateRevocationListInfo, error) { + panic("implement me") +} + +func (m *MockAuthDB) StoreCRL(info *CertificateRevocationListInfo) error { + panic("implement me") +} + // IsRevoked mock. func (m *MockAuthDB) IsRevoked(sn string) (bool, error) { if m.MIsRevoked != nil { From 668cb6f39c756bc17adf2814b5377ef524d6dbc4 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Tue, 2 Nov 2021 16:39:29 +0800 Subject: [PATCH 09/66] missed some mentions of PEM when changing the returned format to DER regarding CRL generation --- authority/tls.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/authority/tls.go b/authority/tls.go index d6d80643..c6dc7d27 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -509,7 +509,7 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn return a.db.Revoke(rci) } -// GenerateCertificateRevocationList returns a PEM representation of a signed CRL. +// GenerateCertificateRevocationList returns a DER representation of a signed CRL. // It will look for a valid generated CRL in the database, check if it has expired, and generate // a new CRL on demand if it has expired (or a CRL does not already exist). // @@ -578,7 +578,7 @@ func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error } // Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the - // expiry time, and the byte-encoded CRL - then store it in the DB + // expiry time, and the DER-encoded CRL - then store it in the DB newCRLInfo := db.CertificateRevocationListInfo{ Number: n, ExpiresAt: revocationList.NextUpdate, @@ -590,7 +590,7 @@ func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error return nil, err } - // Finally, return our CRL PEM + // Finally, return our CRL in DER return certificateRevocationList, nil } From d417ce32326067ff249f34eeff20ee82ac243d9d Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Thu, 4 Nov 2021 14:05:07 +0800 Subject: [PATCH 10/66] implement changes from review --- api/api.go | 3 +- api/crl.go | 44 ++++++++++++++--- authority/authority.go | 70 +++++++++++++++++++++++++++ authority/config/config.go | 7 +++ authority/tls.go | 98 +++++++++++++++++++++++++++----------- cas/apiv1/requests.go | 10 ++++ cas/apiv1/services.go | 2 +- cas/softcas/softcas.go | 8 ++-- db/db.go | 28 +++++++++-- 9 files changed, 224 insertions(+), 46 deletions(-) diff --git a/api/api.go b/api/api.go index ba7d3d17..25b6efa7 100644 --- a/api/api.go +++ b/api/api.go @@ -50,7 +50,8 @@ type Authority interface { GetRoots() ([]*x509.Certificate, error) GetFederation() ([]*x509.Certificate, error) Version() authority.Version - GenerateCertificateRevocationList(force bool) ([]byte, error) + GenerateCertificateRevocationList() error + GetCertificateRevocationList() ([]byte, error) } // TimeDuration is an alias of provisioner.TimeDuration diff --git a/api/crl.go b/api/crl.go index 46758777..4d20cfeb 100644 --- a/api/crl.go +++ b/api/crl.go @@ -2,23 +2,53 @@ package api import ( "encoding/pem" + "fmt" + "github.com/pkg/errors" "net/http" ) -// CRL is an HTTP handler that returns the current CRL in PEM format +// CRL is an HTTP handler that returns the current CRL in DER or PEM format func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { - crlBytes, err := h.Authority.GenerateCertificateRevocationList(false) + crlBytes, err := h.Authority.GetCertificateRevocationList() + + _, formatAsPEM := r.URL.Query()["pem"] if err != nil { w.WriteHeader(500) + _, err = fmt.Fprintf(w, "%v\n", err) + if err != nil { + panic(errors.Wrap(err, "error writing http response")) + } return } - pemBytes := pem.EncodeToMemory(&pem.Block{ - Type: "X509 CRL", - Bytes: crlBytes, - }) + if crlBytes == nil { + w.WriteHeader(404) + _, err = fmt.Fprintln(w, "No CRL available") + if err != nil { + panic(errors.Wrap(err, "error writing http response")) + } + return + } + + if formatAsPEM { + pemBytes := pem.EncodeToMemory(&pem.Block{ + Type: "X509 CRL", + Bytes: crlBytes, + }) + w.Header().Add("Content-Type", "application/x-pem-file") + w.Header().Add("Content-Disposition", "attachment; filename=\"crl.pem\"") + _, err = w.Write(pemBytes) + } else { + w.Header().Add("Content-Type", "application/pkix-crl") + w.Header().Add("Content-Disposition", "attachment; filename=\"crl.der\"") + _, err = w.Write(crlBytes) + } w.WriteHeader(200) - _, err = w.Write(pemBytes) + + if err != nil { + panic(errors.Wrap(err, "error writing http response")) + } + } diff --git a/authority/authority.go b/authority/authority.go index b6071060..2454c272 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -65,6 +65,9 @@ type Authority struct { sshCAUserFederatedCerts []ssh.PublicKey sshCAHostFederatedCerts []ssh.PublicKey + // CRL vars + crlChannel chan int + // Do not re-initialize initOnce bool startTime time.Time @@ -553,6 +556,16 @@ func (a *Authority) init() error { // not be repeated. a.initOnce = true + // Start the CRL generator + if a.config.CRL != nil { + if a.config.CRL.Generate && a.config.CRL.CacheDuration.Duration > time.Duration(0) { + err := a.startCRLGenerator() + if err != nil { + return err + } + } + } + return nil } @@ -646,3 +659,60 @@ func (a *Authority) requiresSCEPService() bool { func (a *Authority) GetSCEPService() *scep.Service { return a.scepService } + +func (a *Authority) startCRLGenerator() error { + + if a.config.CRL.CacheDuration.Duration > time.Duration(0) { + // Check that there is a valid CRL in the DB right now. If it doesnt exist + // or is expired, generated one now + crlDB, ok := a.db.(db.CertificateRevocationListDB) + if !ok { + return errors.Errorf("CRL Generation requested, but database does not support CRL generation") + } + + crlInfo, err := crlDB.GetCRL() + if err != nil { + return errors.Wrap(err, "could not retrieve CRL from database") + } + + if crlInfo == nil { + log.Println("No CRL exists in the DB, generating one now") + err = a.GenerateCertificateRevocationList() + if err != nil { + return errors.Wrap(err, "could not generate a CRL") + } + } + + if crlInfo.ExpiresAt.Before(time.Now().UTC()) { + log.Printf("Existing CRL has expired (at %v), generating a new one", crlInfo.ExpiresAt) + err = a.GenerateCertificateRevocationList() + if err != nil { + return errors.Wrap(err, "could not generate a CRL") + } + } + + log.Printf("CRL will be auto-generated every %v", a.config.CRL.CacheDuration) + tickerDuration := a.config.CRL.CacheDuration.Duration - time.Minute // generate the new CRL 1 minute before it expires + if tickerDuration <= 0 { + log.Printf("WARNING: Addition of jitter to CRL generation time %v creates a negative duration (%v). Using 1 minute cacheDuration", a.config.CRL.CacheDuration, tickerDuration) + tickerDuration = time.Minute + } + crlTicker := time.NewTicker(tickerDuration) + + go func() { + for { + select { + case <-crlTicker.C: + log.Println("Regenerating CRL") + err := a.GenerateCertificateRevocationList() + if err != nil { + // TODO: log or panic here? + panic(errors.Wrap(err, "authority.crlGenerator encountered an error")) + } + } + } + }() + } + + return nil +} diff --git a/authority/config/config.go b/authority/config/config.go index 2c437725..4bf51cfe 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -68,6 +68,7 @@ type Config struct { TLS *TLSOptions `json:"tls,omitempty"` Password string `json:"password,omitempty"` Templates *templates.Templates `json:"templates,omitempty"` + CRL *CRLConfig `json:"crl,omitempty"` } // ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer @@ -99,6 +100,12 @@ type AuthConfig struct { EnableAdmin bool `json:"enableAdmin,omitempty"` } +// CRLConfig represents config options for CRL generation +type CRLConfig struct { + Generate bool `json:"generate,omitempty"` + CacheDuration *provisioner.Duration `json:"cacheDuration,omitempty"` +} + // init initializes the required fields in the AuthConfig if they are not // provided. func (c *AuthConfig) init() { diff --git a/authority/tls.go b/authority/tls.go index c6dc7d27..19e06659 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -408,6 +408,16 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error p provisioner.Interface err error ) + + // Attempt to get the certificate expiry using the serial number. + cert, err := a.db.GetCertificate(revokeOpts.Serial) + + // Revocation of a certificate not in the database may be requested, so fill in the expiry only + // if we can + if err == nil { + rci.ExpiresAt = cert.NotAfter + } + // If not mTLS nor ACME, then get the TokenID of the token. if !(revokeOpts.MTLS || revokeOpts.ACME) { token, err := jose.ParseSigned(revokeOpts.OTT) @@ -474,7 +484,10 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error err = a.revoke(revokedCert, rci) // Generate a new CRL so CRL requesters will always get an up-to-date CRL whenever they request it - _, _ = a.GenerateCertificateRevocationList(true) + err = a.GenerateCertificateRevocationList() + if err != nil { + return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...) + } } switch err { case nil: @@ -509,36 +522,64 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn return a.db.Revoke(rci) } -// GenerateCertificateRevocationList returns a DER representation of a signed CRL. -// It will look for a valid generated CRL in the database, check if it has expired, and generate -// a new CRL on demand if it has expired (or a CRL does not already exist). -// -// force set to true will force regeneration of the CRL regardless of whether it has actually expired -func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error) { - - // check for an existing CRL in the database, and return that if its valid - crlInfo, err := a.db.GetCRL() - - if err != nil { - return nil, err +// GetCertificateRevocationList will return the currently generated CRL from the DB, or a not implemented +// error if the underlying AuthDB does not support CRLs +func (a *Authority) GetCertificateRevocationList() ([]byte, error) { + if a.config.CRL == nil { + return nil, errs.Wrap(http.StatusInternalServerError, errors.Errorf("Certificate Revocation Lists are not enabled"), "authority.GetCertificateRevocationList") } - if !force && crlInfo != nil && crlInfo.ExpiresAt.After(time.Now().UTC()) { - return crlInfo.DER, nil + crlDB, ok := a.db.(db.CertificateRevocationListDB) + if !ok { + return nil, errs.Wrap(http.StatusInternalServerError, errors.Errorf("Database does not support Certificate Revocation Lists"), "authority.GetCertificateRevocationList") + } + + crlInfo, err := crlDB.GetCRL() + if err != nil { + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetCertificateRevocationList") + + } + + if crlInfo == nil { + return nil, nil + } + + return crlInfo.DER, nil +} + +// GenerateCertificateRevocationList generates a DER representation of a signed CRL and stores it in the +// database. Returns nil if CRL generation has been disabled in the config +func (a *Authority) GenerateCertificateRevocationList() error { + + if a.config.CRL == nil { + // CRL is disabled + return nil + } + + crlDB, ok := a.db.(db.CertificateRevocationListDB) + if !ok { + return errors.Errorf("Database does not support CRL generation") } // some CAS may not implement the CRLGenerator interface, so check before we proceed caCRLGenerator, ok := a.x509CAService.(casapi.CertificateAuthorityCRLGenerator) - if !ok { - return nil, errors.Errorf("CRL Generator not implemented") + return errors.Errorf("CA does not support CRL Generation") } - revokedList, err := a.db.GetRevokedCertificates() + crlInfo, err := crlDB.GetCRL() + if err != nil { + return errors.Wrap(err, "could not retrieve CRL from database") + } + + revokedList, err := crlDB.GetRevokedCertificates() + if err != nil { + return errors.Wrap(err, "could not retrieve revoked certificates list from database") + } // Number is a monotonically increasing integer (essentially the CRL version number) that we need to // keep track of and increase every time we generate a new CRL - var n int64 = 0 + var n int64 var bn big.Int if crlInfo != nil { @@ -561,37 +602,36 @@ func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error } // Create a RevocationList representation ready for the CAS to sign - // TODO: use a config value for the NextUpdate time duration // TODO: allow SignatureAlgorithm to be specified? revocationList := x509.RevocationList{ SignatureAlgorithm: 0, RevokedCertificates: revokedCertificates, Number: &bn, ThisUpdate: time.Now().UTC(), - NextUpdate: time.Now().UTC().Add(time.Minute * 10), + NextUpdate: time.Now().UTC().Add(a.config.CRL.CacheDuration.Duration), ExtraExtensions: nil, } - certificateRevocationList, err := caCRLGenerator.CreateCertificateRevocationList(&revocationList) + certificateRevocationList, err := caCRLGenerator.CreateCRL(&casapi.CreateCRLRequest{RevocationList: &revocationList}) if err != nil { - return nil, err + return errors.Wrap(err, "could not create CRL") } // Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the - // expiry time, and the DER-encoded CRL - then store it in the DB + // expiry time, and the DER-encoded CRL newCRLInfo := db.CertificateRevocationListInfo{ Number: n, ExpiresAt: revocationList.NextUpdate, - DER: certificateRevocationList, + DER: certificateRevocationList.CRL, } - err = a.db.StoreCRL(&newCRLInfo) + // Store the CRL in the database ready for retrieval by api endpoints + err = crlDB.StoreCRL(&newCRLInfo) if err != nil { - return nil, err + return errors.Wrap(err, "could not store CRL in database") } - // Finally, return our CRL in DER - return certificateRevocationList, nil + return nil } // GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server. diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go index bf745c17..630cecce 100644 --- a/cas/apiv1/requests.go +++ b/cas/apiv1/requests.go @@ -144,3 +144,13 @@ type CreateCertificateAuthorityResponse struct { PrivateKey crypto.PrivateKey Signer crypto.Signer } + +// CreateCRLRequest is the request to create a Certificate Revocation List. +type CreateCRLRequest struct { + RevocationList *x509.RevocationList +} + +// CreateCRLResponse is the response to a Certificate Revocation List request. +type CreateCRLResponse struct { + CRL []byte //the CRL in DER format +} diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index 143c1f17..ce7d0364 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -17,7 +17,7 @@ type CertificateAuthorityService interface { // CertificateAuthorityCRLGenerator is an optional interface implemented by CertificateAuthorityService // that has a method to create a CRL type CertificateAuthorityCRLGenerator interface { - CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error) + CreateCRL(req *CreateCRLRequest) (*CreateCRLResponse, error) } // CertificateAuthorityGetter is an interface implemented by a diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index 3c400080..bec1d81e 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -130,15 +130,15 @@ func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1 }, nil } -// CreateCertificateRevocationList will create a new CRL based on the RevocationList passed to it -func (c *SoftCAS) CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error) { +// CreateCRL will create a new CRL based on the RevocationList passed to it +func (c *SoftCAS) CreateCRL(req *apiv1.CreateCRLRequest) (*apiv1.CreateCRLResponse, error) { - revocationList, err := x509.CreateRevocationList(rand.Reader, crl, c.CertificateChain[0], c.Signer) + revocationListBytes, err := x509.CreateRevocationList(rand.Reader, req.RevocationList, c.CertificateChain[0], c.Signer) if err != nil { return nil, err } - return revocationList, nil + return &apiv1.CreateCRLResponse{CRL: revocationListBytes}, nil } // CreateCertificateAuthority creates a root or an intermediate certificate. diff --git a/db/db.go b/db/db.go index 534c4000..bf3b97ec 100644 --- a/db/db.go +++ b/db/db.go @@ -51,9 +51,6 @@ type AuthDB interface { IsSSHRevoked(sn string) (bool, error) Revoke(rci *RevokedCertificateInfo) error RevokeSSH(rci *RevokedCertificateInfo) error - GetRevokedCertificates() (*[]RevokedCertificateInfo, error) - GetCRL() (*CertificateRevocationListInfo, error) - StoreCRL(*CertificateRevocationListInfo) error GetCertificate(serialNumber string) (*x509.Certificate, error) StoreCertificate(crt *x509.Certificate) error UseToken(id, tok string) (bool, error) @@ -63,6 +60,13 @@ type AuthDB interface { Shutdown() error } +// CertificateRevocationListDB is an interface to indicate whether the DB supports CRL generation +type CertificateRevocationListDB interface { + GetRevokedCertificates() (*[]RevokedCertificateInfo, error) + GetCRL() (*CertificateRevocationListInfo, error) + StoreCRL(*CertificateRevocationListInfo) error +} + // DB is a wrapper over the nosql.DB interface. type DB struct { nosql.DB @@ -109,6 +113,7 @@ type RevokedCertificateInfo struct { ReasonCode int Reason string RevokedAt time.Time + ExpiresAt time.Time TokenID string MTLS bool ACME bool @@ -216,7 +221,22 @@ func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { if err := json.Unmarshal(e.Value, &data); err != nil { return nil, err } - revokedCerts = append(revokedCerts, data) + + if !data.ExpiresAt.IsZero() && data.ExpiresAt.After(time.Now().UTC()) { + revokedCerts = append(revokedCerts, data) + } else if data.ExpiresAt.IsZero() { + cert, err := db.GetCertificate(data.Serial) + if err != nil { + revokedCerts = append(revokedCerts, data) // a revoked certificate may not be in the database, + // so its expiry date is undiscoverable and will need + // to be added to the crl always + continue + } + + if cert.NotAfter.After(time.Now().UTC()) { + revokedCerts = append(revokedCerts, data) + } + } } return &revokedCerts, nil From a607ab189a8de305205313f25497e526add217a6 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Tue, 29 Mar 2022 08:51:39 +0800 Subject: [PATCH 11/66] requested changes --- api/crl.go | 2 -- authority/authority.go | 77 ++++++++++++++++++------------------------ authority/tls.go | 4 +-- 3 files changed, 34 insertions(+), 49 deletions(-) diff --git a/api/crl.go b/api/crl.go index 4d20cfeb..c90a77f9 100644 --- a/api/crl.go +++ b/api/crl.go @@ -45,8 +45,6 @@ func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { _, err = w.Write(crlBytes) } - w.WriteHeader(200) - if err != nil { panic(errors.Wrap(err, "error writing http response")) } diff --git a/authority/authority.go b/authority/authority.go index 2454c272..025e3d39 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "crypto/x509" "encoding/hex" + "fmt" "log" "strings" "sync" @@ -662,57 +663,43 @@ func (a *Authority) GetSCEPService() *scep.Service { func (a *Authority) startCRLGenerator() error { - if a.config.CRL.CacheDuration.Duration > time.Duration(0) { - // Check that there is a valid CRL in the DB right now. If it doesnt exist - // or is expired, generated one now - crlDB, ok := a.db.(db.CertificateRevocationListDB) - if !ok { - return errors.Errorf("CRL Generation requested, but database does not support CRL generation") - } + if a.config.CRL.CacheDuration.Duration <= 0 { + return nil + } - crlInfo, err := crlDB.GetCRL() - if err != nil { - return errors.Wrap(err, "could not retrieve CRL from database") - } + // Check that there is a valid CRL in the DB right now. If it doesn't exist + // or is expired, generate one now + _, ok := a.db.(db.CertificateRevocationListDB) + if !ok { + return errors.Errorf("CRL Generation requested, but database does not support CRL generation") + } - if crlInfo == nil { - log.Println("No CRL exists in the DB, generating one now") - err = a.GenerateCertificateRevocationList() - if err != nil { - return errors.Wrap(err, "could not generate a CRL") - } - } + // Always create a new CRL on startup in case the CA has been down and the time to next expected CRL + // update is less than the cache duration. + err := a.GenerateCertificateRevocationList() + if err != nil { + return errors.Wrap(err, "could not generate a CRL") + } - if crlInfo.ExpiresAt.Before(time.Now().UTC()) { - log.Printf("Existing CRL has expired (at %v), generating a new one", crlInfo.ExpiresAt) - err = a.GenerateCertificateRevocationList() - if err != nil { - return errors.Wrap(err, "could not generate a CRL") - } - } + log.Printf("CRL will be auto-generated every %v", a.config.CRL.CacheDuration) + tickerDuration := a.config.CRL.CacheDuration.Duration - time.Minute // generate the new CRL 1 minute before it expires + if tickerDuration <= 0 { + panic(fmt.Sprintf("ERROR: Addition of jitter to CRL generation time %v creates a negative duration (%v). Use a CRL generation time of longer than 1 minute.", a.config.CRL.CacheDuration, tickerDuration)) + } + crlTicker := time.NewTicker(tickerDuration) - log.Printf("CRL will be auto-generated every %v", a.config.CRL.CacheDuration) - tickerDuration := a.config.CRL.CacheDuration.Duration - time.Minute // generate the new CRL 1 minute before it expires - if tickerDuration <= 0 { - log.Printf("WARNING: Addition of jitter to CRL generation time %v creates a negative duration (%v). Using 1 minute cacheDuration", a.config.CRL.CacheDuration, tickerDuration) - tickerDuration = time.Minute - } - crlTicker := time.NewTicker(tickerDuration) - - go func() { - for { - select { - case <-crlTicker.C: - log.Println("Regenerating CRL") - err := a.GenerateCertificateRevocationList() - if err != nil { - // TODO: log or panic here? - panic(errors.Wrap(err, "authority.crlGenerator encountered an error")) - } + go func() { + for { + select { + case <-crlTicker.C: + log.Println("Regenerating CRL") + err := a.GenerateCertificateRevocationList() + if err != nil { + log.Printf("ERROR: authority.crlGenerator encountered an error when regenerating the CRL: %v", err) } } - }() - } + } + }() return nil } diff --git a/authority/tls.go b/authority/tls.go index 19e06659..1d016f71 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -526,12 +526,12 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn // error if the underlying AuthDB does not support CRLs func (a *Authority) GetCertificateRevocationList() ([]byte, error) { if a.config.CRL == nil { - return nil, errs.Wrap(http.StatusInternalServerError, errors.Errorf("Certificate Revocation Lists are not enabled"), "authority.GetCertificateRevocationList") + return nil, errs.Wrap(http.StatusNotFound, errors.Errorf("Certificate Revocation Lists are not enabled"), "authority.GetCertificateRevocationList") } crlDB, ok := a.db.(db.CertificateRevocationListDB) if !ok { - return nil, errs.Wrap(http.StatusInternalServerError, errors.Errorf("Database does not support Certificate Revocation Lists"), "authority.GetCertificateRevocationList") + return nil, errs.Wrap(http.StatusNotImplemented, errors.Errorf("Database does not support Certificate Revocation Lists"), "authority.GetCertificateRevocationList") } crlInfo, err := crlDB.GetCRL() From 53dbe2309ba383c51048a6d5147e465b44f29ab0 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Tue, 5 Apr 2022 11:19:13 +0800 Subject: [PATCH 12/66] implemented some requested changes --- api/api.go | 1 - api/crl.go | 18 +++++++++--------- authority/authority.go | 15 ++++++++++++--- authority/tls.go | 14 ++++++++------ db/db.go | 6 ++++-- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/api/api.go b/api/api.go index 25b6efa7..014a89e9 100644 --- a/api/api.go +++ b/api/api.go @@ -50,7 +50,6 @@ type Authority interface { GetRoots() ([]*x509.Certificate, error) GetFederation() ([]*x509.Certificate, error) Version() authority.Version - GenerateCertificateRevocationList() error GetCertificateRevocationList() ([]byte, error) } diff --git a/api/crl.go b/api/crl.go index c90a77f9..45024470 100644 --- a/api/crl.go +++ b/api/crl.go @@ -4,6 +4,7 @@ import ( "encoding/pem" "fmt" "github.com/pkg/errors" + "github.com/smallstep/certificates/errs" "net/http" ) @@ -14,6 +15,14 @@ func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { _, formatAsPEM := r.URL.Query()["pem"] if err != nil { + + caErr, isCaErr := err.(*errs.Error) + + if isCaErr { + http.Error(w, caErr.Msg, caErr.Status) + return + } + w.WriteHeader(500) _, err = fmt.Fprintf(w, "%v\n", err) if err != nil { @@ -22,15 +31,6 @@ func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { return } - if crlBytes == nil { - w.WriteHeader(404) - _, err = fmt.Fprintln(w, "No CRL available") - if err != nil { - panic(errors.Wrap(err, "error writing http response")) - } - return - } - if formatAsPEM { pemBytes := pem.EncodeToMemory(&pem.Block{ Type: "X509 CRL", diff --git a/authority/authority.go b/authority/authority.go index 025e3d39..3adfc14b 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -67,7 +67,7 @@ type Authority struct { sshCAHostFederatedCerts []ssh.PublicKey // CRL vars - crlChannel chan int + crlTicker *time.Ticker // Do not re-initialize initOnce bool @@ -604,6 +604,10 @@ func (a *Authority) IsAdminAPIEnabled() bool { // Shutdown safely shuts down any clients, databases, etc. held by the Authority. func (a *Authority) Shutdown() error { + if a.crlTicker != nil { + a.crlTicker.Stop() + } + if err := a.keyManager.Close(); err != nil { log.Printf("error closing the key manager: %v", err) } @@ -612,6 +616,11 @@ func (a *Authority) Shutdown() error { // CloseForReload closes internal services, to allow a safe reload. func (a *Authority) CloseForReload() { + + if a.crlTicker != nil { + a.crlTicker.Stop() + } + if err := a.keyManager.Close(); err != nil { log.Printf("error closing the key manager: %v", err) } @@ -686,12 +695,12 @@ func (a *Authority) startCRLGenerator() error { if tickerDuration <= 0 { panic(fmt.Sprintf("ERROR: Addition of jitter to CRL generation time %v creates a negative duration (%v). Use a CRL generation time of longer than 1 minute.", a.config.CRL.CacheDuration, tickerDuration)) } - crlTicker := time.NewTicker(tickerDuration) + a.crlTicker = time.NewTicker(tickerDuration) go func() { for { select { - case <-crlTicker.C: + case <-a.crlTicker.C: log.Println("Regenerating CRL") err := a.GenerateCertificateRevocationList() if err != nil { diff --git a/authority/tls.go b/authority/tls.go index 1d016f71..549f0c07 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -409,13 +409,15 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error err error ) - // Attempt to get the certificate expiry using the serial number. - cert, err := a.db.GetCertificate(revokeOpts.Serial) + if revokeOpts.Crt == nil { + // Attempt to get the certificate expiry using the serial number. + cert, err := a.db.GetCertificate(revokeOpts.Serial) - // Revocation of a certificate not in the database may be requested, so fill in the expiry only - // if we can - if err == nil { - rci.ExpiresAt = cert.NotAfter + // Revocation of a certificate not in the database may be requested, so fill in the expiry only + // if we can + if err == nil { + rci.ExpiresAt = cert.NotAfter + } } // If not mTLS nor ACME, then get the TokenID of the token. diff --git a/db/db.go b/db/db.go index bf3b97ec..070f3596 100644 --- a/db/db.go +++ b/db/db.go @@ -216,13 +216,15 @@ func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { return nil, err } var revokedCerts []RevokedCertificateInfo + now := time.Now().UTC() + for _, e := range entries { var data RevokedCertificateInfo if err := json.Unmarshal(e.Value, &data); err != nil { return nil, err } - if !data.ExpiresAt.IsZero() && data.ExpiresAt.After(time.Now().UTC()) { + if !data.ExpiresAt.IsZero() && data.ExpiresAt.After(now) { revokedCerts = append(revokedCerts, data) } else if data.ExpiresAt.IsZero() { cert, err := db.GetCertificate(data.Serial) @@ -233,7 +235,7 @@ func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { continue } - if cert.NotAfter.After(time.Now().UTC()) { + if cert.NotAfter.After(now) { revokedCerts = append(revokedCerts, data) } } From 49c41636cc4bb1970d68e6c5275fa438ca24dea5 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Wed, 6 Apr 2022 08:31:40 +0800 Subject: [PATCH 13/66] implemented some requested changes --- api/api_test.go | 4 ++++ authority/tls.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/api/api_test.go b/api/api_test.go index 39c77de7..06b165bf 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -201,6 +201,10 @@ type mockAuthority struct { version func() authority.Version } +func (m *mockAuthority) GetCertificateRevocationList() ([]byte, error) { + panic("implement me") +} + // TODO: remove once Authorize is deprecated. func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) { return m.AuthorizeSign(ott) diff --git a/authority/tls.go b/authority/tls.go index 549f0c07..50dc5642 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -11,8 +11,8 @@ import ( "encoding/json" "encoding/pem" "fmt" - "net" "math/big" + "net" "net/http" "strings" "time" From c8b38c0e13245b400d5f2a4c4d79726d90679ee0 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Wed, 6 Apr 2022 10:50:09 +0800 Subject: [PATCH 14/66] implemented requested changes --- cas/softcas/softcas.go | 6 +++++- db/db.go | 10 +--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index bec1d81e..0b2270bb 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -133,7 +133,11 @@ func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1 // CreateCRL will create a new CRL based on the RevocationList passed to it func (c *SoftCAS) CreateCRL(req *apiv1.CreateCRLRequest) (*apiv1.CreateCRLResponse, error) { - revocationListBytes, err := x509.CreateRevocationList(rand.Reader, req.RevocationList, c.CertificateChain[0], c.Signer) + certChain, signer, err := c.getCertSigner() + if err != nil { + return nil, err + } + revocationListBytes, err := x509.CreateRevocationList(rand.Reader, req.RevocationList, certChain[0], signer) if err != nil { return nil, err } diff --git a/db/db.go b/db/db.go index 5b7bfc21..ccaf4056 100644 --- a/db/db.go +++ b/db/db.go @@ -127,14 +127,6 @@ type CertificateRevocationListInfo struct { DER []byte } -// CertificateRevocationListInfo contains a CRL in DER format and associated -// metadata to allow a decision on whether to regenerate the CRL or not easier -type CertificateRevocationListInfo struct { - Number int64 - ExpiresAt time.Time - DER []byte -} - // IsRevoked returns whether or not a certificate with the given identifier // has been revoked. // In the case of an X509 Certificate the `id` should be the Serial Number of @@ -224,7 +216,7 @@ func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { return nil, err } var revokedCerts []RevokedCertificateInfo - now := time.Now().UTC() + now := time.Now().Truncate(time.Second) for _, e := range entries { var data RevokedCertificateInfo From 9fa5f462131e04d6086d3d11be494c0c88262f8c Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Wed, 13 Jul 2022 08:56:47 +0800 Subject: [PATCH 15/66] add minor doco, Test_CRLGeneration(), fix some issues from merge --- api/api_test.go | 46 ++++++++++++++++++++++++++++++++++++++++- api/crl.go | 4 ++-- db/db.go | 4 ++-- docs/GETTING_STARTED.md | 5 +++++ 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/api/api_test.go b/api/api_test.go index 485244b9..de573097 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -199,6 +199,7 @@ type mockAuthority struct { getEncryptedKey func(kid string) (string, error) getRoots func() ([]*x509.Certificate, error) getFederation func() ([]*x509.Certificate, error) + getCRL func() ([]byte, error) signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) @@ -213,7 +214,11 @@ type mockAuthority struct { } func (m *mockAuthority) GetCertificateRevocationList() ([]byte, error) { - panic("implement me") + if m.getCRL != nil { + return m.getCRL() + } + + return m.ret1.([]byte), m.err } // TODO: remove once Authorize is deprecated. @@ -776,6 +781,45 @@ func (m *mockProvisioner) AuthorizeSSHRekey(ctx context.Context, token string) ( return m.ret1.(*ssh.Certificate), m.ret2.([]provisioner.SignOption), m.err } +func Test_CRLGeneration(t *testing.T) { + tests := []struct { + name string + err error + statusCode int + expected []byte + }{ + {"empty", nil, http.StatusOK, nil}, + } + + chiCtx := chi.NewRouteContext() + req := httptest.NewRequest("GET", "http://example.com/crl", nil) + req = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockMustAuthority(t, &mockAuthority{ret1: tt.expected, err: tt.err}) + w := httptest.NewRecorder() + CRL(w, req) + res := w.Result() + + if res.StatusCode != tt.statusCode { + t.Errorf("caHandler.CRL StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) + } + + body, err := io.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Errorf("caHandler.Root unexpected error = %v", err) + } + if tt.statusCode == 200 { + if !bytes.Equal(bytes.TrimSpace(body), tt.expected) { + t.Errorf("caHandler.Root CRL = %s, wants %s", body, tt.expected) + } + } + }) + } +} + func Test_caHandler_Route(t *testing.T) { type fields struct { Authority Authority diff --git a/api/crl.go b/api/crl.go index 45024470..ce0d8f73 100644 --- a/api/crl.go +++ b/api/crl.go @@ -9,8 +9,8 @@ import ( ) // CRL is an HTTP handler that returns the current CRL in DER or PEM format -func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { - crlBytes, err := h.Authority.GetCertificateRevocationList() +func CRL(w http.ResponseWriter, r *http.Request) { + crlBytes, err := mustAuthority(r.Context()).GetCertificateRevocationList() _, formatAsPEM := r.URL.Query()["pem"] diff --git a/db/db.go b/db/db.go index b93b23ca..ee8007c1 100644 --- a/db/db.go +++ b/db/db.go @@ -255,9 +255,9 @@ func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { return nil, err } - if !data.ExpiresAt.IsZero() && data.ExpiresAt.After(now) { + if !data.RevokedAt.IsZero() && data.RevokedAt.After(now) { revokedCerts = append(revokedCerts, data) - } else if data.ExpiresAt.IsZero() { + } else if data.RevokedAt.IsZero() { cert, err := db.GetCertificate(data.Serial) if err != nil { revokedCerts = append(revokedCerts, data) // a revoked certificate may not be in the database, diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 67c5673d..328fba69 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -119,6 +119,11 @@ starting the CA. * `address`: e.g. `127.0.0.1:8080` - address and port on which the CA will bind and respond to requests. +* `crl`: Certificate Revocation List settings: + - generate: Enable/Disable CRL generation (`true` to generate, `false` to disable) + + - cacheDuration: Time between CRL regeneration task. E.g if set to `5m`, step-ca will regenerate the CRL every 5 minutes. + * `dnsNames`: comma separated list of DNS Name(s) for the CA. * `logger`: the default logging format for the CA is `text`. The other option From 924082bb492e2e13e4a26f327717fe6d26c9aaf1 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Thu, 8 Sep 2022 10:09:37 +0800 Subject: [PATCH 16/66] fix linter errors --- authority/authority.go | 13 ++++++------- authority/tls.go | 3 +++ cas/softcas/softcas.go | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 31c2890e..efd7ab66 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -794,14 +794,13 @@ func (a *Authority) startCRLGenerator() error { go func() { for { - select { - case <-a.crlTicker.C: - log.Println("Regenerating CRL") - err := a.GenerateCertificateRevocationList() - if err != nil { - log.Printf("ERROR: authority.crlGenerator encountered an error when regenerating the CRL: %v", err) - } + <-a.crlTicker.C + log.Println("Regenerating CRL") + err := a.GenerateCertificateRevocationList() + if err != nil { + log.Printf("ERROR: authority.crlGenerator encountered an error when regenerating the CRL: %v", err) } + } }() diff --git a/authority/tls.go b/authority/tls.go index 8d3bd73d..5cde341c 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -549,6 +549,9 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error // Save as revoked in the Db. err = a.revoke(revokedCert, rci) + if err != nil { + return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...) + } // Generate a new CRL so CRL requesters will always get an up-to-date CRL whenever they request it err = a.GenerateCertificateRevocationList() diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index 03adc667..ed909d6d 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -3,8 +3,8 @@ package softcas import ( "context" "crypto" - "crypto/rsa" "crypto/rand" + "crypto/rsa" "crypto/x509" "time" From 4a4f7ca9ba127e2ffc9abf18036d2265194b7958 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 14 Sep 2022 11:16:47 -0700 Subject: [PATCH 17/66] Fix panic if cacheDuration is not set --- authority/authority.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index efd7ab66..619482bd 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -638,8 +638,8 @@ func (a *Authority) init() error { a.initOnce = true // Start the CRL generator - if a.config.CRL != nil { - if a.config.CRL.Generate && a.config.CRL.CacheDuration.Duration > time.Duration(0) { + if a.config.CRL != nil && a.config.CRL.Generate { + if v := a.config.CRL.CacheDuration; v != nil && v.Duration > 0 { err := a.startCRLGenerator() if err != nil { return err From 0829f37fe83adfdc36f0f8b7e205846c65eaef3d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 14 Sep 2022 11:43:58 -0700 Subject: [PATCH 18/66] Define a default crl cache duration --- authority/config/config.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/authority/config/config.go b/authority/config/config.go index 721c691c..86e950cb 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -35,6 +35,8 @@ var ( // DefaultEnableSSHCA enable SSH CA features per provisioner or globally // for all provisioners. DefaultEnableSSHCA = false + // DefaultCRLCacheDuration is the default cache duration for the CRL. + DefaultCRLCacheDuration = &provisioner.Duration{Duration: 24 * time.Hour} // GlobalProvisionerClaims default claims for the Authority. Can be overridden // by provisioner specific claims. GlobalProvisionerClaims = provisioner.Claims{ @@ -190,6 +192,9 @@ func (c *Config) Init() { if c.CommonName == "" { c.CommonName = "Step Online CA" } + if c.CRL != nil && c.CRL.Generate && c.CRL.CacheDuration == nil { + c.CRL.CacheDuration = DefaultCRLCacheDuration + } c.AuthorityConfig.init() } From 221e756f4098032b7f899497710602e862d13314 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 14 Sep 2022 11:50:11 -0700 Subject: [PATCH 19/66] Use render.Error on crl endpoint --- api/crl.go | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/api/crl.go b/api/crl.go index ce0d8f73..1a4d309a 100644 --- a/api/crl.go +++ b/api/crl.go @@ -2,35 +2,20 @@ package api import ( "encoding/pem" - "fmt" - "github.com/pkg/errors" - "github.com/smallstep/certificates/errs" "net/http" + + "github.com/smallstep/certificates/api/render" ) // CRL is an HTTP handler that returns the current CRL in DER or PEM format func CRL(w http.ResponseWriter, r *http.Request) { crlBytes, err := mustAuthority(r.Context()).GetCertificateRevocationList() - - _, formatAsPEM := r.URL.Query()["pem"] - if err != nil { - - caErr, isCaErr := err.(*errs.Error) - - if isCaErr { - http.Error(w, caErr.Msg, caErr.Status) - return - } - - w.WriteHeader(500) - _, err = fmt.Fprintf(w, "%v\n", err) - if err != nil { - panic(errors.Wrap(err, "error writing http response")) - } + render.Error(w, err) return } + _, formatAsPEM := r.URL.Query()["pem"] if formatAsPEM { pemBytes := pem.EncodeToMemory(&pem.Block{ Type: "X509 CRL", @@ -38,15 +23,10 @@ func CRL(w http.ResponseWriter, r *http.Request) { }) w.Header().Add("Content-Type", "application/x-pem-file") w.Header().Add("Content-Disposition", "attachment; filename=\"crl.pem\"") - _, err = w.Write(pemBytes) + w.Write(pemBytes) } else { w.Header().Add("Content-Type", "application/pkix-crl") w.Header().Add("Content-Disposition", "attachment; filename=\"crl.der\"") - _, err = w.Write(crlBytes) + w.Write(crlBytes) } - - if err != nil { - panic(errors.Wrap(err, "error writing http response")) - } - } From 4e19aa4c52e1ee1db9f981cd3dd0024a3c36102b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 14 Sep 2022 12:21:52 -0700 Subject: [PATCH 20/66] Add cache duration if crl is set --- authority/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authority/config/config.go b/authority/config/config.go index 86e950cb..7e6922ed 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -192,7 +192,7 @@ func (c *Config) Init() { if c.CommonName == "" { c.CommonName = "Step Online CA" } - if c.CRL != nil && c.CRL.Generate && c.CRL.CacheDuration == nil { + if c.CRL != nil && c.CRL.CacheDuration == nil { c.CRL.CacheDuration = DefaultCRLCacheDuration } c.AuthorityConfig.init() From 40baf73dffb6c0392113ee6bf3f5579e8628de49 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Thu, 15 Sep 2022 15:03:42 +0800 Subject: [PATCH 21/66] remove incorrect check on revoked certificate dates, add mutex lock for generating CRLs, --- authority/authority.go | 1 + authority/tls.go | 3 +++ db/db.go | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/authority/authority.go b/authority/authority.go index 619482bd..1b2faf4b 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -71,6 +71,7 @@ type Authority struct { // CRL vars crlTicker *time.Ticker + crlMutex sync.Mutex // Do not re-initialize initOnce bool diff --git a/authority/tls.go b/authority/tls.go index 5cde341c..4652735a 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -637,6 +637,9 @@ func (a *Authority) GenerateCertificateRevocationList() error { return errors.Errorf("CA does not support CRL Generation") } + a.crlMutex.Lock() // use a mutex to ensure only one CRL is generated at a time to avoid concurrency issues + defer a.crlMutex.Unlock() + crlInfo, err := crlDB.GetCRL() if err != nil { return errors.Wrap(err, "could not retrieve CRL from database") diff --git a/db/db.go b/db/db.go index ee8007c1..a8ae23ee 100644 --- a/db/db.go +++ b/db/db.go @@ -255,7 +255,7 @@ func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { return nil, err } - if !data.RevokedAt.IsZero() && data.RevokedAt.After(now) { + if !data.RevokedAt.IsZero() { revokedCerts = append(revokedCerts, data) } else if data.RevokedAt.IsZero() { cert, err := db.GetCertificate(data.Serial) From acdf080308f82d0d2f9990b3fd6fa39e624a40b6 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 29 Sep 2022 15:08:32 +0200 Subject: [PATCH 22/66] Add `enableAdmin` and `enableACME` to Helm values.yml generation --- pki/helm.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pki/helm.go b/pki/helm.go index e13bb97c..7651d8ef 100644 --- a/pki/helm.go +++ b/pki/helm.go @@ -17,6 +17,7 @@ type helmVariables struct { Defaults *linkedca.Defaults Password string EnableSSH bool + EnableAdmin bool TLS authconfig.TLSOptions Provisioners []provisioner.Interface } @@ -35,7 +36,11 @@ func (p *PKI) WriteHelmTemplate(w io.Writer) error { } // Convert provisioner to ca.json - provisioners := make([]provisioner.Interface, len(p.Authority.Provisioners)) + numberOfProvisioners := len(p.Authority.Provisioners) + if p.options.enableACME { + numberOfProvisioners++ + } + provisioners := make([]provisioner.Interface, numberOfProvisioners) for i, p := range p.Authority.Provisioners { pp, err := authority.ProvisionerToCertificates(p) if err != nil { @@ -44,11 +49,25 @@ func (p *PKI) WriteHelmTemplate(w io.Writer) error { provisioners[i] = pp } + // Add default ACME provisioner if enabled. Note that this logic is similar + // to what's in p.GenerateConfig(), but that codepath isn't taken when + // writing the Helm template. The default JWK provisioner is added earlier in + // the process and that's part of the provisioners above. + // TODO(hs): consider refactoring the initialization, so that this becomes + // easier to reason about and maintain. + if p.options.enableACME { + provisioners[len(provisioners)-1] = &provisioner.ACME{ + Type: "ACME", + Name: "acme", + } + } + if err := tmpl.Execute(w, helmVariables{ Configuration: &p.Configuration, Defaults: &p.Defaults, Password: "", EnableSSH: p.options.enableSSH, + EnableAdmin: p.options.enableAdmin, TLS: authconfig.DefaultTLSOptions, Provisioners: provisioners, }); err != nil { @@ -88,6 +107,7 @@ inject: type: badgerv2 dataSource: /home/step/db authority: + enableAdmin: {{ .EnableAdmin }} provisioners: {{- range .Provisioners }} - {{ . | toJson }} From cebb7d7ef0c2962654adf958482b026e17532370 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 6 Oct 2022 17:14:02 +0200 Subject: [PATCH 23/66] Add automatic migration of provisioners Provisioners stored in the CA configuration file are automatically migrated to the database. Currently no cleanup of the provisioners in the configuration file yet. In certain situations this may not work as expected, for example if the CA can't write to the file. But it's probalby good to try it, so that we can keep the configuration state of the CA consistent. --- authority/authority.go | 53 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 5271842d..a667cfa8 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -600,20 +600,59 @@ func (a *Authority) init() error { return admin.WrapErrorISE(err, "error loading provisioners to initialize authority") } if len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, "linked") { - // Create First Provisioner - prov, err := CreateFirstProvisioner(ctx, a.adminDB, string(a.password)) - if err != nil { - return admin.WrapErrorISE(err, "error creating first provisioner") + + var firstJWKProvisioner *linkedca.Provisioner + if len(a.config.AuthorityConfig.Provisioners) > 0 { + log.Printf("Starting migration of provisioners") + // Existing provisioners detected; try migrating them to DB storage + for _, p := range a.config.AuthorityConfig.Provisioners { + lp, err := ProvisionerToLinkedca(p) + if err != nil { + return admin.WrapErrorISE(err, "error transforming provisioner %q while migrating", p.GetName()) + } + + // Store the provisioner to be migrated + if err := a.adminDB.CreateProvisioner(ctx, lp); err != nil { + return admin.WrapErrorISE(err, "error creating provisioner %q while migrating", p.GetName()) + } + + // Mark the first JWK provisioner, so that it can be used for administration purposes + if firstJWKProvisioner == nil && lp.Type == linkedca.Provisioner_JWK { + firstJWKProvisioner = lp + log.Printf("Migrated JWK provisioner %q with admin permissions", p.GetName()) // TODO(hs): change the wording? + } else { + log.Printf("Migrated %s provisioner %q", p.GetType(), p.GetName()) + } + } + + // TODO(hs): try to update ca.json to remove migrated provisioners from the + // file? This may not always be possible though, so we shouldn't fail hard on + // every error. The next time the CA runs, it won't have perform the migration, + // because there'll be at least a JWK provisioner. + + log.Printf("Finished migrating provisioners") } - // Create first admin + // Create first JWK provisioner for remote administration purposes if none exists yet + if firstJWKProvisioner == nil { + firstJWKProvisioner, err = CreateFirstProvisioner(ctx, a.adminDB, string(a.password)) + if err != nil { + return admin.WrapErrorISE(err, "error creating first provisioner") + } + log.Printf("Created JWK provisioner %q with admin permissions", firstJWKProvisioner.GetName()) // TODO(hs): change the wording? + } + + // Create first super admin, belonging to the first JWK provisioner + firstSuperAdminSubject := "step" if err := a.adminDB.CreateAdmin(ctx, &linkedca.Admin{ - ProvisionerId: prov.Id, - Subject: "step", + ProvisionerId: firstJWKProvisioner.Id, + Subject: firstSuperAdminSubject, Type: linkedca.Admin_SUPER_ADMIN, }); err != nil { return admin.WrapErrorISE(err, "error creating first admin") } + + log.Printf("Created super admin %q for JWK provisioner %q", firstSuperAdminSubject, firstJWKProvisioner.GetName()) } } From f7df865687ff469204edaf7e6c95fe7d54e14b7a Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Fri, 7 Oct 2022 10:30:00 +0800 Subject: [PATCH 24/66] refactor crl config, add some tests --- authority/authority.go | 45 ++++++++---- authority/authority_test.go | 6 ++ authority/config/config.go | 6 +- authority/tls.go | 27 +++++-- authority/tls_test.go | 138 ++++++++++++++++++++++++++++++++++++ db/db.go | 47 +++++++----- 6 files changed, 232 insertions(+), 37 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index ede13269..b72fe61e 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -7,7 +7,6 @@ import ( "crypto/sha256" "crypto/x509" "encoding/hex" - "fmt" "log" "strings" "sync" @@ -664,12 +663,22 @@ func (a *Authority) init() error { a.initOnce = true // Start the CRL generator - if a.config.CRL != nil && a.config.CRL.Generate { - if v := a.config.CRL.CacheDuration; v != nil && v.Duration > 0 { - err := a.startCRLGenerator() - if err != nil { - return err - } + if a.config.CRL != nil && a.config.CRL.Enabled { + if v := a.config.CRL.CacheDuration; v != nil && v.Duration < 0 { + return errors.New("crl cacheDuration must be >= 0") + } + + if v := a.config.CRL.CacheDuration; v != nil && v.Duration == 0 { + a.config.CRL.CacheDuration.Duration, _ = time.ParseDuration("24h") + } + + if a.config.CRL.CacheDuration == nil { + a.config.CRL.CacheDuration, _ = provisioner.NewDuration("24h") + } + + err = a.startCRLGenerator() + if err != nil { + return err } } @@ -797,6 +806,12 @@ func (a *Authority) startCRLGenerator() error { return nil } + // Make the renewal ticker run ~2/3 of cacheDuration by default, or use renewPeriod if available + tickerDuration := (a.config.CRL.CacheDuration.Duration / 3) * 2 + if v := a.config.CRL.RenewPeriod; v != nil && v.Duration > 0 { + tickerDuration = v.Duration + } + // Check that there is a valid CRL in the DB right now. If it doesn't exist // or is expired, generate one now _, ok := a.db.(db.CertificateRevocationListDB) @@ -811,11 +826,6 @@ func (a *Authority) startCRLGenerator() error { return errors.Wrap(err, "could not generate a CRL") } - log.Printf("CRL will be auto-generated every %v", a.config.CRL.CacheDuration) - tickerDuration := a.config.CRL.CacheDuration.Duration - time.Minute // generate the new CRL 1 minute before it expires - if tickerDuration <= 0 { - panic(fmt.Sprintf("ERROR: Addition of jitter to CRL generation time %v creates a negative duration (%v). Use a CRL generation time of longer than 1 minute.", a.config.CRL.CacheDuration, tickerDuration)) - } a.crlTicker = time.NewTicker(tickerDuration) go func() { @@ -832,3 +842,14 @@ func (a *Authority) startCRLGenerator() error { return nil } + +func (a *Authority) resetCRLGeneratorTimer() { + if a.crlTicker != nil { + tickerDuration := (a.config.CRL.CacheDuration.Duration / 3) * 2 + if v := a.config.CRL.RenewPeriod; v != nil && v.Duration > 0 { + tickerDuration = v.Duration + } + + a.crlTicker.Reset(tickerDuration) + } +} diff --git a/authority/authority_test.go b/authority/authority_test.go index 9f35f23e..48479d5b 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -80,6 +80,12 @@ func testAuthority(t *testing.T, opts ...Option) *Authority { AuthorityConfig: &AuthConfig{ Provisioners: p, }, + CRL: &config.CRLConfig{ + Enabled: true, + CacheDuration: nil, + GenerateOnRevoke: true, + RenewPeriod: nil, + }, } a, err := New(c, opts...) assert.FatalError(t, err) diff --git a/authority/config/config.go b/authority/config/config.go index 7e6922ed..ad261cdb 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -111,8 +111,10 @@ type AuthConfig struct { // CRLConfig represents config options for CRL generation type CRLConfig struct { - Generate bool `json:"generate,omitempty"` - CacheDuration *provisioner.Duration `json:"cacheDuration,omitempty"` + Enabled bool `json:"enabled"` + CacheDuration *provisioner.Duration `json:"cacheDuration,omitempty"` + GenerateOnRevoke bool `json:"generateOnRevoke,omitempty"` + RenewPeriod *provisioner.Duration `json:"renewPeriod,omitempty"` } // init initializes the required fields in the AuthConfig if they are not diff --git a/authority/tls.go b/authority/tls.go index d70c1558..051e09c9 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -572,10 +572,17 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...) } - // Generate a new CRL so CRL requesters will always get an up-to-date CRL whenever they request it - err = a.GenerateCertificateRevocationList() - if err != nil { - return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...) + if a.config.CRL != nil && a.config.CRL.GenerateOnRevoke { + // Generate a new CRL so CRL requesters will always get an up-to-date CRL whenever they request it + err = a.GenerateCertificateRevocationList() + if err != nil { + return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...) + } + + // the timer only gets reset if CRL is enabled + if a.config.CRL.Enabled { + a.resetCRLGeneratorTimer() + } } } switch { @@ -693,6 +700,13 @@ func (a *Authority) GenerateCertificateRevocationList() error { }) } + var updateDuration time.Duration + if a.config.CRL.CacheDuration != nil { + updateDuration = a.config.CRL.CacheDuration.Duration + } else if crlInfo != nil { + updateDuration = crlInfo.Duration + } + // Create a RevocationList representation ready for the CAS to sign // TODO: allow SignatureAlgorithm to be specified? revocationList := x509.RevocationList{ @@ -700,7 +714,7 @@ func (a *Authority) GenerateCertificateRevocationList() error { RevokedCertificates: revokedCertificates, Number: &bn, ThisUpdate: time.Now().UTC(), - NextUpdate: time.Now().UTC().Add(a.config.CRL.CacheDuration.Duration), + NextUpdate: time.Now().UTC().Add(updateDuration), ExtraExtensions: nil, } @@ -710,11 +724,12 @@ func (a *Authority) GenerateCertificateRevocationList() error { } // Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the - // expiry time, and the DER-encoded CRL + // expiry time, duration, and the DER-encoded CRL newCRLInfo := db.CertificateRevocationListInfo{ Number: n, ExpiresAt: revocationList.NextUpdate, DER: certificateRevocationList.CRL, + Duration: updateDuration, } // Store the CRL in the database ready for retrieval by api endpoints diff --git a/authority/tls_test.go b/authority/tls_test.go index bc9e3e3a..6223bb3d 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -1673,3 +1673,141 @@ func TestAuthority_constraints(t *testing.T) { }) } } + +func TestAuthority_CRL(t *testing.T) { + reasonCode := 2 + reason := "bob was let go" + validIssuer := "step-cli" + validAudience := testAudiences.Revoke + now := time.Now().UTC() + // + jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass"))) + assert.FatalError(t, err) + // + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, + (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID)) + assert.FatalError(t, err) + + crlCtx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod) + + var crlStore db.CertificateRevocationListInfo + var revokedList []db.RevokedCertificateInfo + + type test struct { + auth *Authority + ctx context.Context + expected []string + err error + code int + } + tests := map[string]func() test{ + "ok/empty-crl": func() test { + _a := testAuthority(t, WithDatabase(&db.MockAuthDB{ + MUseToken: func(id, tok string) (bool, error) { + return true, nil + }, + MGetCertificate: func(sn string) (*x509.Certificate, error) { + return nil, errors.New("not found") + }, + MStoreCRL: func(i *db.CertificateRevocationListInfo) error { + crlStore = *i + return nil + }, + MGetCRL: func() (*db.CertificateRevocationListInfo, error) { + return &crlStore, nil + }, + MGetRevokedCertificates: func() (*[]db.RevokedCertificateInfo, error) { + return &revokedList, nil + }, + MRevoke: func(rci *db.RevokedCertificateInfo) error { + revokedList = append(revokedList, *rci) + return nil + }, + })) + + return test{ + auth: _a, + ctx: crlCtx, + expected: nil, + } + }, + "ok/crl-full": func() test { + _a := testAuthority(t, WithDatabase(&db.MockAuthDB{ + MUseToken: func(id, tok string) (bool, error) { + return true, nil + }, + MGetCertificate: func(sn string) (*x509.Certificate, error) { + return nil, errors.New("not found") + }, + MStoreCRL: func(i *db.CertificateRevocationListInfo) error { + crlStore = *i + return nil + }, + MGetCRL: func() (*db.CertificateRevocationListInfo, error) { + return &crlStore, nil + }, + MGetRevokedCertificates: func() (*[]db.RevokedCertificateInfo, error) { + return &revokedList, nil + }, + MRevoke: func(rci *db.RevokedCertificateInfo) error { + revokedList = append(revokedList, *rci) + return nil + }, + })) + + var ex []string + + for i := 0; i < 100; i++ { + sn := fmt.Sprintf("%v", i) + + cl := jwt.Claims{ + Subject: fmt.Sprintf("sn-%v", i), + Issuer: validIssuer, + NotBefore: jwt.NewNumericDate(now), + Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + Audience: validAudience, + ID: sn, + } + raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + assert.FatalError(t, err) + err = _a.Revoke(crlCtx, &RevokeOptions{ + Serial: sn, + ReasonCode: reasonCode, + Reason: reason, + OTT: raw, + }) + + assert.FatalError(t, err) + + ex = append(ex, sn) + } + + return test{ + auth: _a, + ctx: crlCtx, + expected: ex, + } + }, + } + for name, f := range tests { + tc := f() + t.Run(name, func(t *testing.T) { + if crlBytes, _err := tc.auth.GetCertificateRevocationList(); _err == nil { + crl, parseErr := x509.ParseCRL(crlBytes) + if parseErr != nil { + t.Errorf("x509.ParseCertificateRequest() error = %v, wantErr %v", parseErr, nil) + } + + var cmpList []string + for _, c := range crl.TBSCertList.RevokedCertificates { + cmpList = append(cmpList, c.SerialNumber.String()) + } + + assert.Equals(t, cmpList, tc.expected) + + } else { + assert.NotNil(t, tc.err) + } + }) + } +} diff --git a/db/db.go b/db/db.go index a8ae23ee..4db437fe 100644 --- a/db/db.go +++ b/db/db.go @@ -155,6 +155,7 @@ type RevokedCertificateInfo struct { type CertificateRevocationListInfo struct { Number int64 ExpiresAt time.Time + Duration time.Duration DER []byte } @@ -471,32 +472,44 @@ func (db *DB) Shutdown() error { // MockAuthDB mocks the AuthDB interface. // type MockAuthDB struct { - Err error - Ret1 interface{} - MIsRevoked func(string) (bool, error) - MIsSSHRevoked func(string) (bool, error) - MRevoke func(rci *RevokedCertificateInfo) error - MRevokeSSH func(rci *RevokedCertificateInfo) error - MGetCertificate func(serialNumber string) (*x509.Certificate, error) - MGetCertificateData func(serialNumber string) (*CertificateData, error) - MStoreCertificate func(crt *x509.Certificate) error - MUseToken func(id, tok string) (bool, error) - MIsSSHHost func(principal string) (bool, error) - MStoreSSHCertificate func(crt *ssh.Certificate) error - MGetSSHHostPrincipals func() ([]string, error) - MShutdown func() error + Err error + Ret1 interface{} + MIsRevoked func(string) (bool, error) + MIsSSHRevoked func(string) (bool, error) + MRevoke func(rci *RevokedCertificateInfo) error + MRevokeSSH func(rci *RevokedCertificateInfo) error + MGetCertificate func(serialNumber string) (*x509.Certificate, error) + MGetCertificateData func(serialNumber string) (*CertificateData, error) + MStoreCertificate func(crt *x509.Certificate) error + MUseToken func(id, tok string) (bool, error) + MIsSSHHost func(principal string) (bool, error) + MStoreSSHCertificate func(crt *ssh.Certificate) error + MGetSSHHostPrincipals func() ([]string, error) + MShutdown func() error + MGetRevokedCertificates func() (*[]RevokedCertificateInfo, error) + MGetCRL func() (*CertificateRevocationListInfo, error) + MStoreCRL func(*CertificateRevocationListInfo) error } func (m *MockAuthDB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { - panic("implement me") + if m.MGetRevokedCertificates != nil { + return m.MGetRevokedCertificates() + } + return m.Ret1.(*[]RevokedCertificateInfo), m.Err } func (m *MockAuthDB) GetCRL() (*CertificateRevocationListInfo, error) { - panic("implement me") + if m.MGetCRL != nil { + return m.MGetCRL() + } + return m.Ret1.(*CertificateRevocationListInfo), m.Err } func (m *MockAuthDB) StoreCRL(info *CertificateRevocationListInfo) error { - panic("implement me") + if m.MStoreCRL != nil { + return m.MStoreCRL(info) + } + return m.Err } // IsRevoked mock. From c9ee4a9f9d309236f839551725a8584e23d4fb60 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 11 Oct 2022 12:19:29 +0200 Subject: [PATCH 25/66] Disable initialization log output if started with `--quiet` --- authority/authority.go | 33 ++++++++++++++++++++++++--------- authority/options.go | 8 ++++++++ ca/ca.go | 4 ++++ 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index a667cfa8..188633c3 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -73,7 +73,7 @@ type Authority struct { sshCAUserFederatedCerts []ssh.PublicKey sshCAHostFederatedCerts []ssh.PublicKey - // Do not re-initialize + // If true, do not re-initialize initOnce bool startTime time.Time @@ -91,8 +91,11 @@ type Authority struct { adminMutex sync.RWMutex - // Do Not initialize the authority + // If true, do not initialize the authority skipInit bool + + // If true, does not output initialization logs + quietInit bool } // Info contains information about the authority. @@ -600,10 +603,9 @@ func (a *Authority) init() error { return admin.WrapErrorISE(err, "error loading provisioners to initialize authority") } if len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, "linked") { - var firstJWKProvisioner *linkedca.Provisioner if len(a.config.AuthorityConfig.Provisioners) > 0 { - log.Printf("Starting migration of provisioners") + a.initLogf("Starting migration of provisioners") // Existing provisioners detected; try migrating them to DB storage for _, p := range a.config.AuthorityConfig.Provisioners { lp, err := ProvisionerToLinkedca(p) @@ -619,9 +621,9 @@ func (a *Authority) init() error { // Mark the first JWK provisioner, so that it can be used for administration purposes if firstJWKProvisioner == nil && lp.Type == linkedca.Provisioner_JWK { firstJWKProvisioner = lp - log.Printf("Migrated JWK provisioner %q with admin permissions", p.GetName()) // TODO(hs): change the wording? + a.initLogf("Migrated JWK provisioner %q with admin permissions", p.GetName()) // TODO(hs): change the wording? } else { - log.Printf("Migrated %s provisioner %q", p.GetType(), p.GetName()) + a.initLogf("Migrated %s provisioner %q", p.GetType(), p.GetName()) } } @@ -630,7 +632,12 @@ func (a *Authority) init() error { // every error. The next time the CA runs, it won't have perform the migration, // because there'll be at least a JWK provisioner. - log.Printf("Finished migrating provisioners") + // 1. check if prerequisites for writing files look OK (user/group, permission bits, etc) + // 2. update the configuration to write (internal representation; do a deep copy first?) + // 3. try writing the new ca.json + // 4. on failure, perform rollback of the write (restore original in internal representation) + + a.initLogf("Finished migrating provisioners") } // Create first JWK provisioner for remote administration purposes if none exists yet @@ -639,7 +646,7 @@ func (a *Authority) init() error { if err != nil { return admin.WrapErrorISE(err, "error creating first provisioner") } - log.Printf("Created JWK provisioner %q with admin permissions", firstJWKProvisioner.GetName()) // TODO(hs): change the wording? + a.initLogf("Created JWK provisioner %q with admin permissions", firstJWKProvisioner.GetName()) // TODO(hs): change the wording? } // Create first super admin, belonging to the first JWK provisioner @@ -652,7 +659,7 @@ func (a *Authority) init() error { return admin.WrapErrorISE(err, "error creating first admin") } - log.Printf("Created super admin %q for JWK provisioner %q", firstSuperAdminSubject, firstJWKProvisioner.GetName()) + a.initLogf("Created super admin %q for JWK provisioner %q", firstSuperAdminSubject, firstJWKProvisioner.GetName()) } } @@ -702,6 +709,14 @@ func (a *Authority) init() error { return nil } +// initLogf is used to log initialization information. The output +// can be disabled by starting the CA with the `--quiet` flag. +func (a *Authority) initLogf(format string, v ...any) { + if !a.quietInit { + log.Printf(format, v...) + } +} + // GetID returns the define authority id or a zero uuid. func (a *Authority) GetID() string { const zeroUUID = "00000000-0000-0000-0000-000000000000" diff --git a/authority/options.go b/authority/options.go index f332d4a9..bf443ed6 100644 --- a/authority/options.go +++ b/authority/options.go @@ -86,6 +86,14 @@ func WithDatabase(d db.AuthDB) Option { } } +// WithQuietInit disables log output when the authority is initialized. +func WithQuietInit() Option { + return func(a *Authority) error { + a.quietInit = true + return nil + } +} + // WithWebhookClient sets the http.Client to be used for outbound requests. func WithWebhookClient(c *http.Client) Option { return func(a *Authority) error { diff --git a/ca/ca.go b/ca/ca.go index ab2aa8ac..98f845b0 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -156,6 +156,10 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { opts = append(opts, authority.WithDatabase(ca.opts.database)) } + if ca.opts.quiet { + opts = append(opts, authority.WithQuietInit()) + } + webhookTransport := http.DefaultTransport.(*http.Transport).Clone() opts = append(opts, authority.WithWebhookClient(&http.Client{Transport: webhookTransport})) From 674206320c4280f8a11b76872138285a2c179902 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 11 Oct 2022 14:12:06 +0200 Subject: [PATCH 26/66] Write updated CA configuration after migrating provisioners --- authority/authority.go | 31 ++++++++++++++++++++++--------- authority/config/config.go | 25 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 188633c3..e3bc3473 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -605,8 +605,8 @@ func (a *Authority) init() error { if len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, "linked") { var firstJWKProvisioner *linkedca.Provisioner if len(a.config.AuthorityConfig.Provisioners) > 0 { - a.initLogf("Starting migration of provisioners") // Existing provisioners detected; try migrating them to DB storage + a.initLogf("Starting migration of provisioners") for _, p := range a.config.AuthorityConfig.Provisioners { lp, err := ProvisionerToLinkedca(p) if err != nil { @@ -627,15 +627,28 @@ func (a *Authority) init() error { } } - // TODO(hs): try to update ca.json to remove migrated provisioners from the - // file? This may not always be possible though, so we shouldn't fail hard on - // every error. The next time the CA runs, it won't have perform the migration, - // because there'll be at least a JWK provisioner. + // TODO(hs): test if this works with LinkedCA too. Also could be useful + // for printing out where the configuration is read from in case of LinkedCA. + c := a.config + if c.WasLoadedFromFile() { + // TODO(hs): check if prerequisites for writing files look OK (user/group, permission bits, etc) as + // extra safety check before trying to write at all? - // 1. check if prerequisites for writing files look OK (user/group, permission bits, etc) - // 2. update the configuration to write (internal representation; do a deep copy first?) - // 3. try writing the new ca.json - // 4. on failure, perform rollback of the write (restore original in internal representation) + // Remove the existing provisioners from the authority configuration + // and commit it to the existing configuration file. NOTE: committing + // the configuration at this point also writes other properties that + // have been initialized with default values, such as the `backdate` and + // `template` settings in the `authority`. + oldProvisioners := c.AuthorityConfig.Provisioners + c.AuthorityConfig.Provisioners = []provisioner.Interface{} + if err := c.Commit(); err != nil { + // Restore the provisioners in in-memory representation for consistency + // when writing the updated configuration fails. This is considered a soft + // error, so execution can continue. + c.AuthorityConfig.Provisioners = oldProvisioners + a.initLogf("Failed removing provisioners from configuration: %v", err) + } + } a.initLogf("Finished migrating provisioners") } diff --git a/authority/config/config.go b/authority/config/config.go index c5e74b39..79228e98 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -73,6 +73,9 @@ type Config struct { Templates *templates.Templates `json:"templates,omitempty"` CommonName string `json:"commonName,omitempty"` SkipValidation bool `json:"-"` + + // Keeps record of the filename the Config is read from + loadedFromFilename string } // ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer @@ -163,6 +166,10 @@ func LoadConfiguration(filename string) (*Config, error) { return nil, errors.Wrapf(err, "error parsing %s", filename) } + // store filename that was read to populate Config + c.loadedFromFilename = filename + + // initialize the Config c.Init() return &c, nil @@ -199,6 +206,24 @@ func (c *Config) Save(filename string) error { return errors.Wrapf(enc.Encode(c), "error writing %s", filename) } +// Commit saves the current configuration to the same +// file it was initially loaded from. +// +// TODO(hs): rename Save() to WriteTo() and replace this +// with Save()? Or is Commit clear enough. +func (c *Config) Commit() error { + if !c.WasLoadedFromFile() { + return errors.New("cannot commit configuration if not loaded from file") + } + return c.Save(c.loadedFromFilename) +} + +// WasLoadedFromFile returns whether or not the Config was +// read from a file. +func (c *Config) WasLoadedFromFile() bool { + return c.loadedFromFilename != "" +} + // Validate validates the configuration. func (c *Config) Validate() error { switch { From 8616d3160fa38d87810758628ab67a9205ad7289 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 11 Oct 2022 17:18:19 +0200 Subject: [PATCH 27/66] Add tests for writing the Helm template --- pki/helm_test.go | 104 +++++++++++++++++++++++++++++++ pki/testdata/helm/simple.yml | 66 ++++++++++++++++++++ pki/testdata/helm/with-acme.yml | 67 ++++++++++++++++++++ pki/testdata/helm/with-admin.yml | 66 ++++++++++++++++++++ pki/testdata/helm/with-ssh.yml | 82 ++++++++++++++++++++++++ 5 files changed, 385 insertions(+) create mode 100644 pki/helm_test.go create mode 100644 pki/testdata/helm/simple.yml create mode 100644 pki/testdata/helm/with-acme.yml create mode 100644 pki/testdata/helm/with-admin.yml create mode 100644 pki/testdata/helm/with-ssh.yml diff --git a/pki/helm_test.go b/pki/helm_test.go new file mode 100644 index 00000000..703e6cb8 --- /dev/null +++ b/pki/helm_test.go @@ -0,0 +1,104 @@ +package pki + +import ( + "bytes" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + + "github.com/smallstep/certificates/cas/apiv1" +) + +func TestPKI_WriteHelmTemplate(t *testing.T) { + type fields struct { + casOptions apiv1.Options + pkiOptions []Option + } + tests := []struct { + name string + fields fields + testFile string + wantErr bool + }{ + { + name: "ok/simple", + fields: fields{ + pkiOptions: []Option{ + WithHelm(), + }, + casOptions: apiv1.Options{ + Type: "softcas", + IsCreator: true, + }, + }, + testFile: "testdata/helm/simple.yml", + wantErr: false, + }, + { + name: "ok/with-acme", + fields: fields{ + pkiOptions: []Option{ + WithHelm(), + WithACME(), + }, + casOptions: apiv1.Options{ + Type: "softcas", + IsCreator: true, + }, + }, + testFile: "testdata/helm/with-acme.yml", + wantErr: false, + }, + { + name: "ok/with-admin", + fields: fields{ + pkiOptions: []Option{ + WithHelm(), + WithAdmin(), + }, + casOptions: apiv1.Options{ + Type: "softcas", + IsCreator: true, + }, + }, + testFile: "testdata/helm/with-admin.yml", + wantErr: false, + }, + { + name: "ok/with-ssh", + fields: fields{ + pkiOptions: []Option{ + WithHelm(), + WithSSH(), + }, + casOptions: apiv1.Options{ + Type: "softcas", + IsCreator: true, + }, + }, + testFile: "testdata/helm/with-ssh.yml", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := tt.fields.casOptions + opts := tt.fields.pkiOptions + p, err := New(o, opts...) + assert.NoError(t, err) + w := &bytes.Buffer{} + if err := p.WriteHelmTemplate(w); (err != nil) != tt.wantErr { + t.Errorf("PKI.WriteHelmTemplate() error = %v, wantErr %v", err, tt.wantErr) + return + } + wantBytes, err := os.ReadFile(tt.testFile) + assert.NoError(t, err) + if diff := cmp.Diff(wantBytes, w.Bytes()); diff != "" { + t.Logf("Generated Helm template did not match reference %q\n", tt.testFile) + t.Errorf("Diff follows:\n%s\n", diff) + } + }) + } +} diff --git a/pki/testdata/helm/simple.yml b/pki/testdata/helm/simple.yml new file mode 100644 index 00000000..1c3049c3 --- /dev/null +++ b/pki/testdata/helm/simple.yml @@ -0,0 +1,66 @@ +# Helm template +inject: + enabled: true + # Config contains the configuration files ca.json and defaults.json + config: + files: + ca.json: + root: /home/step/certs/root_ca.crt + federateRoots: [] + crt: /home/step/certs/intermediate_ca.crt + key: /home/step/secrets/intermediate_ca_key + address: 127.0.0.1:9000 + dnsNames: + - 127.0.0.1 + logger: + format: json + db: + type: badgerv2 + dataSource: /home/step/db + authority: + enableAdmin: false + provisioners: + tls: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + minVersion: 1.2 + maxVersion: 1.3 + renegotiation: false + + defaults.json: + ca-url: https://127.0.0.1 + ca-config: /home/step/config/ca.json + fingerprint: + root: /home/step/certs/root_ca.crt + + # Certificates contains the root and intermediate certificate and + # optionally the SSH host and user public keys + certificates: + # intermediate_ca contains the text of the intermediate CA Certificate + intermediate_ca: | + + + # root_ca contains the text of the root CA Certificate + root_ca: | + + + # Secrets contains the root and intermediate keys and optionally the SSH + # private keys + secrets: + # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key + # This value must be base64 encoded. + ca_password: + provisioner_password: + + x509: + # intermediate_ca_key contains the contents of your encrypted intermediate CA key + intermediate_ca_key: | + + + # root_ca_key contains the contents of your encrypted root CA key + # Note that this value can be omitted without impacting the functionality of step-certificates + # If supplied, this should be encrypted using a unique password that is not used for encrypting + # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. + root_ca_key: | + diff --git a/pki/testdata/helm/with-acme.yml b/pki/testdata/helm/with-acme.yml new file mode 100644 index 00000000..17ff6f81 --- /dev/null +++ b/pki/testdata/helm/with-acme.yml @@ -0,0 +1,67 @@ +# Helm template +inject: + enabled: true + # Config contains the configuration files ca.json and defaults.json + config: + files: + ca.json: + root: /home/step/certs/root_ca.crt + federateRoots: [] + crt: /home/step/certs/intermediate_ca.crt + key: /home/step/secrets/intermediate_ca_key + address: 127.0.0.1:9000 + dnsNames: + - 127.0.0.1 + logger: + format: json + db: + type: badgerv2 + dataSource: /home/step/db + authority: + enableAdmin: false + provisioners: + - {"type":"ACME","name":"acme"} + tls: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + minVersion: 1.2 + maxVersion: 1.3 + renegotiation: false + + defaults.json: + ca-url: https://127.0.0.1 + ca-config: /home/step/config/ca.json + fingerprint: + root: /home/step/certs/root_ca.crt + + # Certificates contains the root and intermediate certificate and + # optionally the SSH host and user public keys + certificates: + # intermediate_ca contains the text of the intermediate CA Certificate + intermediate_ca: | + + + # root_ca contains the text of the root CA Certificate + root_ca: | + + + # Secrets contains the root and intermediate keys and optionally the SSH + # private keys + secrets: + # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key + # This value must be base64 encoded. + ca_password: + provisioner_password: + + x509: + # intermediate_ca_key contains the contents of your encrypted intermediate CA key + intermediate_ca_key: | + + + # root_ca_key contains the contents of your encrypted root CA key + # Note that this value can be omitted without impacting the functionality of step-certificates + # If supplied, this should be encrypted using a unique password that is not used for encrypting + # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. + root_ca_key: | + diff --git a/pki/testdata/helm/with-admin.yml b/pki/testdata/helm/with-admin.yml new file mode 100644 index 00000000..75fd1999 --- /dev/null +++ b/pki/testdata/helm/with-admin.yml @@ -0,0 +1,66 @@ +# Helm template +inject: + enabled: true + # Config contains the configuration files ca.json and defaults.json + config: + files: + ca.json: + root: /home/step/certs/root_ca.crt + federateRoots: [] + crt: /home/step/certs/intermediate_ca.crt + key: /home/step/secrets/intermediate_ca_key + address: 127.0.0.1:9000 + dnsNames: + - 127.0.0.1 + logger: + format: json + db: + type: badgerv2 + dataSource: /home/step/db + authority: + enableAdmin: true + provisioners: + tls: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + minVersion: 1.2 + maxVersion: 1.3 + renegotiation: false + + defaults.json: + ca-url: https://127.0.0.1 + ca-config: /home/step/config/ca.json + fingerprint: + root: /home/step/certs/root_ca.crt + + # Certificates contains the root and intermediate certificate and + # optionally the SSH host and user public keys + certificates: + # intermediate_ca contains the text of the intermediate CA Certificate + intermediate_ca: | + + + # root_ca contains the text of the root CA Certificate + root_ca: | + + + # Secrets contains the root and intermediate keys and optionally the SSH + # private keys + secrets: + # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key + # This value must be base64 encoded. + ca_password: + provisioner_password: + + x509: + # intermediate_ca_key contains the contents of your encrypted intermediate CA key + intermediate_ca_key: | + + + # root_ca_key contains the contents of your encrypted root CA key + # Note that this value can be omitted without impacting the functionality of step-certificates + # If supplied, this should be encrypted using a unique password that is not used for encrypting + # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. + root_ca_key: | + diff --git a/pki/testdata/helm/with-ssh.yml b/pki/testdata/helm/with-ssh.yml new file mode 100644 index 00000000..b2ba96f6 --- /dev/null +++ b/pki/testdata/helm/with-ssh.yml @@ -0,0 +1,82 @@ +# Helm template +inject: + enabled: true + # Config contains the configuration files ca.json and defaults.json + config: + files: + ca.json: + root: /home/step/certs/root_ca.crt + federateRoots: [] + crt: /home/step/certs/intermediate_ca.crt + key: /home/step/secrets/intermediate_ca_key + ssh: + hostKey: /home/step/secrets/ssh_host_ca_key + userKey: /home/step/secrets/ssh_user_ca_key + address: 127.0.0.1:9000 + dnsNames: + - 127.0.0.1 + logger: + format: json + db: + type: badgerv2 + dataSource: /home/step/db + authority: + enableAdmin: false + provisioners: + tls: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + minVersion: 1.2 + maxVersion: 1.3 + renegotiation: false + + defaults.json: + ca-url: https://127.0.0.1 + ca-config: /home/step/config/ca.json + fingerprint: + root: /home/step/certs/root_ca.crt + + # Certificates contains the root and intermediate certificate and + # optionally the SSH host and user public keys + certificates: + # intermediate_ca contains the text of the intermediate CA Certificate + intermediate_ca: | + + + # root_ca contains the text of the root CA Certificate + root_ca: | + + # ssh_host_ca contains the text of the public ssh key for the SSH root CA + ssh_host_ca: + + # ssh_user_ca contains the text of the public ssh key for the SSH root CA + ssh_user_ca: + + # Secrets contains the root and intermediate keys and optionally the SSH + # private keys + secrets: + # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key + # This value must be base64 encoded. + ca_password: + provisioner_password: + + x509: + # intermediate_ca_key contains the contents of your encrypted intermediate CA key + intermediate_ca_key: | + + + # root_ca_key contains the contents of your encrypted root CA key + # Note that this value can be omitted without impacting the functionality of step-certificates + # If supplied, this should be encrypted using a unique password that is not used for encrypting + # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. + root_ca_key: | + + ssh: + # ssh_host_ca_key contains the contents of your encrypted SSH Host CA key + host_ca_key: | + + + # ssh_user_ca_key contains the contents of your encrypted SSH User CA key + user_ca_key: | + From 317efa456860d8265fa3730c513343db768249a0 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 11 Oct 2022 17:39:35 +0200 Subject: [PATCH 28/66] Add some TODOs for improvingin PKI initialization maintainability --- pki/helm_test.go | 6 ++++++ pki/pki.go | 3 +++ 2 files changed, 9 insertions(+) diff --git a/pki/helm_test.go b/pki/helm_test.go index 703e6cb8..f83a8370 100644 --- a/pki/helm_test.go +++ b/pki/helm_test.go @@ -86,6 +86,12 @@ func TestPKI_WriteHelmTemplate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { o := tt.fields.casOptions opts := tt.fields.pkiOptions + // TODO(hs): invoking `New` doesn't perform all operations that are executed + // when `ca init --helm` is executed. The list of provisioners on the authority + // is not populated, for example, resulting in this test not being entirely + // realistic. Ideally this logic should be handled in one place and probably + // inside of the PKI initialization, but if that becomes messy, some more + // logic needs to be performed here to get the PKI instance in good shape. p, err := New(o, opts...) assert.NoError(t, err) w := &bytes.Buffer{} diff --git a/pki/pki.go b/pki/pki.go index c05eadbd..5bbd42a1 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -307,6 +307,9 @@ type PKI struct { // New creates a new PKI configuration. func New(o apiv1.Options, opts ...Option) (*PKI, error) { + // TODO(hs): invoking `New` with a context active will use values from + // that CA context while generating the context. Thay may or may not + // be fully expected and/or what we want. Check that. currentCtx := step.Contexts().GetCurrent() caService, err := cas.New(context.Background(), o) if err != nil { From 6516384160b5ddf23a154a1c465ea841f8025f44 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Wed, 12 Oct 2022 15:54:32 +0200 Subject: [PATCH 29/66] Trigger CI From 1a5523f5c023af64e7ff2289256d1b92cfd30ece Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 14 Oct 2022 00:09:32 +0200 Subject: [PATCH 30/66] Add default JWK to the Helm tests --- pki/helm_test.go | 77 ++++++++++++++++++++++++++ pki/testdata/helm/simple.yml | 1 + pki/testdata/helm/with-acme.yml | 1 + pki/testdata/helm/with-admin.yml | 1 + pki/testdata/helm/with-provisioner.yml | 67 ++++++++++++++++++++++ pki/testdata/helm/with-ssh.yml | 1 + 6 files changed, 148 insertions(+) create mode 100644 pki/testdata/helm/with-provisioner.yml diff --git a/pki/helm_test.go b/pki/helm_test.go index f83a8370..0a383614 100644 --- a/pki/helm_test.go +++ b/pki/helm_test.go @@ -2,12 +2,16 @@ package pki import ( "bytes" + "encoding/json" "os" "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" + "go.step.sm/crypto/jose" + "go.step.sm/linkedca" + "github.com/smallstep/certificates/cas/apiv1" ) @@ -36,6 +40,21 @@ func TestPKI_WriteHelmTemplate(t *testing.T) { testFile: "testdata/helm/simple.yml", wantErr: false, }, + { + name: "ok/with-provisioner", + fields: fields{ + pkiOptions: []Option{ + WithHelm(), + WithProvisioner("a-provisioner"), + }, + casOptions: apiv1.Options{ + Type: "softcas", + IsCreator: true, + }, + }, + testFile: "testdata/helm/with-provisioner.yml", + wantErr: false, + }, { name: "ok/with-acme", fields: fields{ @@ -94,11 +113,21 @@ func TestPKI_WriteHelmTemplate(t *testing.T) { // logic needs to be performed here to get the PKI instance in good shape. p, err := New(o, opts...) assert.NoError(t, err) + + // setKeyPairs sets a predefined JWK and a default JWK provisioner. This is one + // of the things performed in the `ca init` code that's not part of `New`, but + // performed after that in p.GenerateKeyPairs`. We're currently using the same + // JWK for every test to keep test variance small: we're not testing JWK generation + // here after all. It's a bit dangerous to redefine the function here, but it's + // the simplest way to make this fully testable without refactoring the init now. + setKeyPairs(t, p) + w := &bytes.Buffer{} if err := p.WriteHelmTemplate(w); (err != nil) != tt.wantErr { t.Errorf("PKI.WriteHelmTemplate() error = %v, wantErr %v", err, tt.wantErr) return } + wantBytes, err := os.ReadFile(tt.testFile) assert.NoError(t, err) if diff := cmp.Diff(wantBytes, w.Bytes()); diff != "" { @@ -108,3 +137,51 @@ func TestPKI_WriteHelmTemplate(t *testing.T) { }) } } + +func setKeyPairs(t *testing.T, p *PKI) { + t.Helper() + + var err error + + p.ottPublicKey, err = jose.ParseKey([]byte(`{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"}`)) + if err != nil { + t.Fatal(err) + } + + p.ottPrivateKey, err = jose.ParseEncrypted("eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA") + if err != nil { + t.Fatal(err) + } + + var claims *linkedca.Claims + if p.options.enableSSH { + claims = &linkedca.Claims{ + Ssh: &linkedca.SSHClaims{ + Enabled: true, + }, + } + } + + // Add JWK provisioner to the configuration. + publicKey, err := json.Marshal(p.ottPublicKey) + if err != nil { + t.Fatal(err) + } + encryptedKey, err := p.ottPrivateKey.CompactSerialize() + if err != nil { + t.Fatal(err) + } + p.Authority.Provisioners = append(p.Authority.Provisioners, &linkedca.Provisioner{ + Type: linkedca.Provisioner_JWK, + Name: p.options.provisioner, + Claims: claims, + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_JWK{ + JWK: &linkedca.JWKProvisioner{ + PublicKey: publicKey, + EncryptedPrivateKey: []byte(encryptedKey), + }, + }, + }, + }) +} diff --git a/pki/testdata/helm/simple.yml b/pki/testdata/helm/simple.yml index 1c3049c3..8b1f053e 100644 --- a/pki/testdata/helm/simple.yml +++ b/pki/testdata/helm/simple.yml @@ -20,6 +20,7 @@ inject: authority: enableAdmin: false provisioners: + - {"type":"JWK","name":"step-cli","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","options":{"x509":{},"ssh":{}}} tls: cipherSuites: - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 diff --git a/pki/testdata/helm/with-acme.yml b/pki/testdata/helm/with-acme.yml index 17ff6f81..cf135946 100644 --- a/pki/testdata/helm/with-acme.yml +++ b/pki/testdata/helm/with-acme.yml @@ -20,6 +20,7 @@ inject: authority: enableAdmin: false provisioners: + - {"type":"JWK","name":"step-cli","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","options":{"x509":{},"ssh":{}}} - {"type":"ACME","name":"acme"} tls: cipherSuites: diff --git a/pki/testdata/helm/with-admin.yml b/pki/testdata/helm/with-admin.yml index 75fd1999..5a88e071 100644 --- a/pki/testdata/helm/with-admin.yml +++ b/pki/testdata/helm/with-admin.yml @@ -20,6 +20,7 @@ inject: authority: enableAdmin: true provisioners: + - {"type":"JWK","name":"step-cli","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","options":{"x509":{},"ssh":{}}} tls: cipherSuites: - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 diff --git a/pki/testdata/helm/with-provisioner.yml b/pki/testdata/helm/with-provisioner.yml new file mode 100644 index 00000000..257a4623 --- /dev/null +++ b/pki/testdata/helm/with-provisioner.yml @@ -0,0 +1,67 @@ +# Helm template +inject: + enabled: true + # Config contains the configuration files ca.json and defaults.json + config: + files: + ca.json: + root: /home/step/certs/root_ca.crt + federateRoots: [] + crt: /home/step/certs/intermediate_ca.crt + key: /home/step/secrets/intermediate_ca_key + address: 127.0.0.1:9000 + dnsNames: + - 127.0.0.1 + logger: + format: json + db: + type: badgerv2 + dataSource: /home/step/db + authority: + enableAdmin: false + provisioners: + - {"type":"JWK","name":"a-provisioner","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","options":{"x509":{},"ssh":{}}} + tls: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + minVersion: 1.2 + maxVersion: 1.3 + renegotiation: false + + defaults.json: + ca-url: https://127.0.0.1 + ca-config: /home/step/config/ca.json + fingerprint: + root: /home/step/certs/root_ca.crt + + # Certificates contains the root and intermediate certificate and + # optionally the SSH host and user public keys + certificates: + # intermediate_ca contains the text of the intermediate CA Certificate + intermediate_ca: | + + + # root_ca contains the text of the root CA Certificate + root_ca: | + + + # Secrets contains the root and intermediate keys and optionally the SSH + # private keys + secrets: + # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key + # This value must be base64 encoded. + ca_password: + provisioner_password: + + x509: + # intermediate_ca_key contains the contents of your encrypted intermediate CA key + intermediate_ca_key: | + + + # root_ca_key contains the contents of your encrypted root CA key + # Note that this value can be omitted without impacting the functionality of step-certificates + # If supplied, this should be encrypted using a unique password that is not used for encrypting + # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. + root_ca_key: | + diff --git a/pki/testdata/helm/with-ssh.yml b/pki/testdata/helm/with-ssh.yml index b2ba96f6..a44192cd 100644 --- a/pki/testdata/helm/with-ssh.yml +++ b/pki/testdata/helm/with-ssh.yml @@ -23,6 +23,7 @@ inject: authority: enableAdmin: false provisioners: + - {"type":"JWK","name":"step-cli","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","claims":{"enableSSHCA":true,"disableRenewal":false,"allowRenewalAfterExpiry":false},"options":{"x509":{},"ssh":{}}} tls: cipherSuites: - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 From 3262ffd43bf381d80dea56991bd5390ece9d8153 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 14 Oct 2022 01:06:43 +0200 Subject: [PATCH 31/66] Add X.509 intermedaite and root certificates to Helm tests --- pki/helm_test.go | 21 ++++++++++++++++++--- pki/testdata/helm/simple.yml | 7 +++++++ pki/testdata/helm/with-acme.yml | 7 +++++++ pki/testdata/helm/with-admin.yml | 7 +++++++ pki/testdata/helm/with-provisioner.yml | 7 +++++++ pki/testdata/helm/with-ssh.yml | 7 +++++++ 6 files changed, 53 insertions(+), 3 deletions(-) diff --git a/pki/helm_test.go b/pki/helm_test.go index 0a383614..6d684c29 100644 --- a/pki/helm_test.go +++ b/pki/helm_test.go @@ -2,6 +2,7 @@ package pki import ( "bytes" + "crypto/x509" "encoding/json" "os" "testing" @@ -114,13 +115,19 @@ func TestPKI_WriteHelmTemplate(t *testing.T) { p, err := New(o, opts...) assert.NoError(t, err) - // setKeyPairs sets a predefined JWK and a default JWK provisioner. This is one + // setKeyPair sets a predefined JWK and a default JWK provisioner. This is one // of the things performed in the `ca init` code that's not part of `New`, but // performed after that in p.GenerateKeyPairs`. We're currently using the same // JWK for every test to keep test variance small: we're not testing JWK generation // here after all. It's a bit dangerous to redefine the function here, but it's // the simplest way to make this fully testable without refactoring the init now. - setKeyPairs(t, p) + // The password for the predefined encrypted key is \x01\x03\x03\x07. + setKeyPair(t, p) + + // setFiles sets some static intermediate and root CA certificate bytes. It + // replaces the logic executed in `p.GenerateRootCertificate`, `p.WriteRootCertificate`, + // and `p.GenerateIntermediateCertificate`. + setFiles(t, p) w := &bytes.Buffer{} if err := p.WriteHelmTemplate(w); (err != nil) != tt.wantErr { @@ -133,12 +140,14 @@ func TestPKI_WriteHelmTemplate(t *testing.T) { if diff := cmp.Diff(wantBytes, w.Bytes()); diff != "" { t.Logf("Generated Helm template did not match reference %q\n", tt.testFile) t.Errorf("Diff follows:\n%s\n", diff) + t.Errorf("Full output:\n%s\n", w.Bytes()) } }) } } -func setKeyPairs(t *testing.T, p *PKI) { +// setKeyPair sets a predefined JWK and a default JWK provisioner. +func setKeyPair(t *testing.T, p *PKI) { t.Helper() var err error @@ -185,3 +194,9 @@ func setKeyPairs(t *testing.T, p *PKI) { }, }) } + +// setFiles sets some static, gibberish intermediate and root CA certificate bytes. +func setFiles(t *testing.T, p *PKI) { + p.Files["/home/step/certs/root_ca.crt"] = encodeCertificate(&x509.Certificate{Raw: []byte("these are just some fake root CA cert bytes")}) + p.Files["/home/step/certs/intermediate_ca.crt"] = encodeCertificate(&x509.Certificate{Raw: []byte("these are just some fake intermediate CA cert bytes")}) +} diff --git a/pki/testdata/helm/simple.yml b/pki/testdata/helm/simple.yml index 8b1f053e..c0f5f993 100644 --- a/pki/testdata/helm/simple.yml +++ b/pki/testdata/helm/simple.yml @@ -40,10 +40,17 @@ inject: certificates: # intermediate_ca contains the text of the intermediate CA Certificate intermediate_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5 + dGVz + -----END CERTIFICATE----- # root_ca contains the text of the root CA Certificate root_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw== + -----END CERTIFICATE----- # Secrets contains the root and intermediate keys and optionally the SSH diff --git a/pki/testdata/helm/with-acme.yml b/pki/testdata/helm/with-acme.yml index cf135946..393a7a01 100644 --- a/pki/testdata/helm/with-acme.yml +++ b/pki/testdata/helm/with-acme.yml @@ -41,10 +41,17 @@ inject: certificates: # intermediate_ca contains the text of the intermediate CA Certificate intermediate_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5 + dGVz + -----END CERTIFICATE----- # root_ca contains the text of the root CA Certificate root_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw== + -----END CERTIFICATE----- # Secrets contains the root and intermediate keys and optionally the SSH diff --git a/pki/testdata/helm/with-admin.yml b/pki/testdata/helm/with-admin.yml index 5a88e071..28896e73 100644 --- a/pki/testdata/helm/with-admin.yml +++ b/pki/testdata/helm/with-admin.yml @@ -40,10 +40,17 @@ inject: certificates: # intermediate_ca contains the text of the intermediate CA Certificate intermediate_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5 + dGVz + -----END CERTIFICATE----- # root_ca contains the text of the root CA Certificate root_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw== + -----END CERTIFICATE----- # Secrets contains the root and intermediate keys and optionally the SSH diff --git a/pki/testdata/helm/with-provisioner.yml b/pki/testdata/helm/with-provisioner.yml index 257a4623..9095aa27 100644 --- a/pki/testdata/helm/with-provisioner.yml +++ b/pki/testdata/helm/with-provisioner.yml @@ -40,10 +40,17 @@ inject: certificates: # intermediate_ca contains the text of the intermediate CA Certificate intermediate_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5 + dGVz + -----END CERTIFICATE----- # root_ca contains the text of the root CA Certificate root_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw== + -----END CERTIFICATE----- # Secrets contains the root and intermediate keys and optionally the SSH diff --git a/pki/testdata/helm/with-ssh.yml b/pki/testdata/helm/with-ssh.yml index a44192cd..770da794 100644 --- a/pki/testdata/helm/with-ssh.yml +++ b/pki/testdata/helm/with-ssh.yml @@ -43,10 +43,17 @@ inject: certificates: # intermediate_ca contains the text of the intermediate CA Certificate intermediate_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5 + dGVz + -----END CERTIFICATE----- # root_ca contains the text of the root CA Certificate root_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw== + -----END CERTIFICATE----- # ssh_host_ca contains the text of the public ssh key for the SSH root CA ssh_host_ca: From 459bfc4c4fa71fd413d747689b7dfbf5d9b7b59e Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 14 Oct 2022 01:45:07 +0200 Subject: [PATCH 32/66] Add gibberish test key bytes to Helm tests --- pki/helm_test.go | 23 +++++++++++++++++++++-- pki/testdata/helm/simple.yml | 4 ++-- pki/testdata/helm/with-acme.yml | 4 ++-- pki/testdata/helm/with-admin.yml | 4 ++-- pki/testdata/helm/with-provisioner.yml | 4 ++-- pki/testdata/helm/with-ssh.yml | 12 ++++++------ 6 files changed, 35 insertions(+), 16 deletions(-) diff --git a/pki/helm_test.go b/pki/helm_test.go index 6d684c29..aeffb5ca 100644 --- a/pki/helm_test.go +++ b/pki/helm_test.go @@ -129,6 +129,10 @@ func TestPKI_WriteHelmTemplate(t *testing.T) { // and `p.GenerateIntermediateCertificate`. setFiles(t, p) + // setSSHSigningKeys sets predefined SSH user and host certificate and key bytes. + // This replaces the logic in `p.GenerateSSHSigningKeys` + setSSHSigningKeys(t, p) + w := &bytes.Buffer{} if err := p.WriteHelmTemplate(w); (err != nil) != tt.wantErr { t.Errorf("PKI.WriteHelmTemplate() error = %v, wantErr %v", err, tt.wantErr) @@ -197,6 +201,21 @@ func setKeyPair(t *testing.T, p *PKI) { // setFiles sets some static, gibberish intermediate and root CA certificate bytes. func setFiles(t *testing.T, p *PKI) { - p.Files["/home/step/certs/root_ca.crt"] = encodeCertificate(&x509.Certificate{Raw: []byte("these are just some fake root CA cert bytes")}) - p.Files["/home/step/certs/intermediate_ca.crt"] = encodeCertificate(&x509.Certificate{Raw: []byte("these are just some fake intermediate CA cert bytes")}) + p.Files[p.Root[0]] = encodeCertificate(&x509.Certificate{Raw: []byte("these are just some fake root CA cert bytes")}) + p.Files[p.RootKey[0]] = []byte("these are just some fake root CA key bytes") + p.Files[p.Intermediate] = encodeCertificate(&x509.Certificate{Raw: []byte("these are just some fake intermediate CA cert bytes")}) + p.Files[p.IntermediateKey] = []byte("these are just some fake intermediate CA key bytes") +} + +// setSSHSigningKeys sets some static, gibberish ssh user and host CA certificate and key bytes. +func setSSHSigningKeys(t *testing.T, p *PKI) { + + if !p.options.enableSSH { + return + } + + p.Files[p.Ssh.HostKey] = []byte("fake ssh host key bytes") + p.Files[p.Ssh.HostPublicKey] = []byte("fake ssh host cert bytes") + p.Files[p.Ssh.UserKey] = []byte("fake ssh user key bytes") + p.Files[p.Ssh.UserPublicKey] = []byte("fake ssh user cert bytes") } diff --git a/pki/testdata/helm/simple.yml b/pki/testdata/helm/simple.yml index c0f5f993..9cc82806 100644 --- a/pki/testdata/helm/simple.yml +++ b/pki/testdata/helm/simple.yml @@ -64,11 +64,11 @@ inject: x509: # intermediate_ca_key contains the contents of your encrypted intermediate CA key intermediate_ca_key: | - + these are just some fake intermediate CA key bytes # root_ca_key contains the contents of your encrypted root CA key # Note that this value can be omitted without impacting the functionality of step-certificates # If supplied, this should be encrypted using a unique password that is not used for encrypting # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. root_ca_key: | - + these are just some fake root CA key bytes diff --git a/pki/testdata/helm/with-acme.yml b/pki/testdata/helm/with-acme.yml index 393a7a01..4f9d5761 100644 --- a/pki/testdata/helm/with-acme.yml +++ b/pki/testdata/helm/with-acme.yml @@ -65,11 +65,11 @@ inject: x509: # intermediate_ca_key contains the contents of your encrypted intermediate CA key intermediate_ca_key: | - + these are just some fake intermediate CA key bytes # root_ca_key contains the contents of your encrypted root CA key # Note that this value can be omitted without impacting the functionality of step-certificates # If supplied, this should be encrypted using a unique password that is not used for encrypting # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. root_ca_key: | - + these are just some fake root CA key bytes diff --git a/pki/testdata/helm/with-admin.yml b/pki/testdata/helm/with-admin.yml index 28896e73..b90647ea 100644 --- a/pki/testdata/helm/with-admin.yml +++ b/pki/testdata/helm/with-admin.yml @@ -64,11 +64,11 @@ inject: x509: # intermediate_ca_key contains the contents of your encrypted intermediate CA key intermediate_ca_key: | - + these are just some fake intermediate CA key bytes # root_ca_key contains the contents of your encrypted root CA key # Note that this value can be omitted without impacting the functionality of step-certificates # If supplied, this should be encrypted using a unique password that is not used for encrypting # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. root_ca_key: | - + these are just some fake root CA key bytes diff --git a/pki/testdata/helm/with-provisioner.yml b/pki/testdata/helm/with-provisioner.yml index 9095aa27..75788acc 100644 --- a/pki/testdata/helm/with-provisioner.yml +++ b/pki/testdata/helm/with-provisioner.yml @@ -64,11 +64,11 @@ inject: x509: # intermediate_ca_key contains the contents of your encrypted intermediate CA key intermediate_ca_key: | - + these are just some fake intermediate CA key bytes # root_ca_key contains the contents of your encrypted root CA key # Note that this value can be omitted without impacting the functionality of step-certificates # If supplied, this should be encrypted using a unique password that is not used for encrypting # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. root_ca_key: | - + these are just some fake root CA key bytes diff --git a/pki/testdata/helm/with-ssh.yml b/pki/testdata/helm/with-ssh.yml index 770da794..1f922994 100644 --- a/pki/testdata/helm/with-ssh.yml +++ b/pki/testdata/helm/with-ssh.yml @@ -56,10 +56,10 @@ inject: -----END CERTIFICATE----- # ssh_host_ca contains the text of the public ssh key for the SSH root CA - ssh_host_ca: + ssh_host_ca: fake ssh host cert bytes # ssh_user_ca contains the text of the public ssh key for the SSH root CA - ssh_user_ca: + ssh_user_ca: fake ssh user cert bytes # Secrets contains the root and intermediate keys and optionally the SSH # private keys @@ -72,19 +72,19 @@ inject: x509: # intermediate_ca_key contains the contents of your encrypted intermediate CA key intermediate_ca_key: | - + these are just some fake intermediate CA key bytes # root_ca_key contains the contents of your encrypted root CA key # Note that this value can be omitted without impacting the functionality of step-certificates # If supplied, this should be encrypted using a unique password that is not used for encrypting # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. root_ca_key: | - + these are just some fake root CA key bytes ssh: # ssh_host_ca_key contains the contents of your encrypted SSH Host CA key host_ca_key: | - + fake ssh host key bytes # ssh_user_ca_key contains the contents of your encrypted SSH User CA key user_ca_key: | - + fake ssh user key bytes From c423e2f664543561c8212ab190fede588c6c97b4 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 14 Oct 2022 13:52:27 +0200 Subject: [PATCH 33/66] Improve Helm test data to be more realistic --- pki/helm.go | 3 ++ pki/helm_test.go | 52 +++++++++++++++++--------- pki/pki.go | 2 +- pki/testdata/helm/simple.yml | 13 +++++-- pki/testdata/helm/with-acme.yml | 13 +++++-- pki/testdata/helm/with-admin.yml | 13 +++++-- pki/testdata/helm/with-provisioner.yml | 13 +++++-- pki/testdata/helm/with-ssh.yml | 27 +++++++++---- 8 files changed, 99 insertions(+), 37 deletions(-) diff --git a/pki/helm.go b/pki/helm.go index 7651d8ef..3fbadb40 100644 --- a/pki/helm.go +++ b/pki/helm.go @@ -62,6 +62,9 @@ func (p *PKI) WriteHelmTemplate(w io.Writer) error { } } + // TODO(hs): add default SSHPOP provisioner if SSH is configured, similar + // as the ACME one above. + if err := tmpl.Execute(w, helmVariables{ Configuration: &p.Configuration, Defaults: &p.Defaults, diff --git a/pki/helm_test.go b/pki/helm_test.go index aeffb5ca..1eb621a8 100644 --- a/pki/helm_test.go +++ b/pki/helm_test.go @@ -2,9 +2,13 @@ package pki import ( "bytes" + "crypto/sha256" "crypto/x509" + "encoding/hex" "encoding/json" + "encoding/pem" "os" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -106,12 +110,12 @@ func TestPKI_WriteHelmTemplate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { o := tt.fields.casOptions opts := tt.fields.pkiOptions + // TODO(hs): invoking `New` doesn't perform all operations that are executed - // when `ca init --helm` is executed. The list of provisioners on the authority - // is not populated, for example, resulting in this test not being entirely - // realistic. Ideally this logic should be handled in one place and probably - // inside of the PKI initialization, but if that becomes messy, some more - // logic needs to be performed here to get the PKI instance in good shape. + // when `ca init --helm` is executed. Ideally this logic should be handled + // in one place and probably inside of the PKI initialization. For testing + // purposes the missing operations to fill a Helm template fully are faked + // by `setKeyPair`, `setCertificates` and `setSSHSigningKeys` p, err := New(o, opts...) assert.NoError(t, err) @@ -124,10 +128,10 @@ func TestPKI_WriteHelmTemplate(t *testing.T) { // The password for the predefined encrypted key is \x01\x03\x03\x07. setKeyPair(t, p) - // setFiles sets some static intermediate and root CA certificate bytes. It + // setCertificates sets some static intermediate and root CA certificate bytes. It // replaces the logic executed in `p.GenerateRootCertificate`, `p.WriteRootCertificate`, // and `p.GenerateIntermediateCertificate`. - setFiles(t, p) + setCertificates(t, p) // setSSHSigningKeys sets predefined SSH user and host certificate and key bytes. // This replaces the logic in `p.GenerateSSHSigningKeys` @@ -175,7 +179,6 @@ func setKeyPair(t *testing.T, p *PKI) { } } - // Add JWK provisioner to the configuration. publicKey, err := json.Marshal(p.ottPublicKey) if err != nil { t.Fatal(err) @@ -199,12 +202,21 @@ func setKeyPair(t *testing.T, p *PKI) { }) } -// setFiles sets some static, gibberish intermediate and root CA certificate bytes. -func setFiles(t *testing.T, p *PKI) { - p.Files[p.Root[0]] = encodeCertificate(&x509.Certificate{Raw: []byte("these are just some fake root CA cert bytes")}) - p.Files[p.RootKey[0]] = []byte("these are just some fake root CA key bytes") +// setCertificates sets some static, gibberish intermediate and root CA certificate and key bytes. +func setCertificates(t *testing.T, p *PKI) { + raw := []byte("these are just some fake root CA cert bytes") + p.Files[p.Root[0]] = encodeCertificate(&x509.Certificate{Raw: raw}) + p.Files[p.RootKey[0]] = pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: []byte("these are just some fake root CA key bytes"), + }) p.Files[p.Intermediate] = encodeCertificate(&x509.Certificate{Raw: []byte("these are just some fake intermediate CA cert bytes")}) - p.Files[p.IntermediateKey] = []byte("these are just some fake intermediate CA key bytes") + p.Files[p.IntermediateKey] = pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: []byte("these are just some fake intermediate CA key bytes"), + }) + sum := sha256.Sum256(raw) + p.Defaults.Fingerprint = strings.ToLower(hex.EncodeToString(sum[:])) } // setSSHSigningKeys sets some static, gibberish ssh user and host CA certificate and key bytes. @@ -214,8 +226,14 @@ func setSSHSigningKeys(t *testing.T, p *PKI) { return } - p.Files[p.Ssh.HostKey] = []byte("fake ssh host key bytes") - p.Files[p.Ssh.HostPublicKey] = []byte("fake ssh host cert bytes") - p.Files[p.Ssh.UserKey] = []byte("fake ssh user key bytes") - p.Files[p.Ssh.UserPublicKey] = []byte("fake ssh user cert bytes") + p.Files[p.Ssh.HostKey] = pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: []byte("fake ssh host key bytes"), + }) + p.Files[p.Ssh.HostPublicKey] = []byte("ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ0IdS5sZm6KITBMZLEJD6b5ROVraYHcAOr3feFel8r1Wp4DRPR1oU0W00J/zjNBRBbANlJoYN4x/8WNNVZ49Ms=") + p.Files[p.Ssh.UserKey] = pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: []byte("fake ssh user key bytes"), + }) + p.Files[p.Ssh.UserPublicKey] = []byte("ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEWA1qUxaGwVNErsvEOGe2d6TvLMF+aiVpuOiIEvpMJ3JeJmecLQctjWqeIbpSvy6/gRa7c82Ge5rLlapYmOChs=") } diff --git a/pki/pki.go b/pki/pki.go index 5bbd42a1..a4a64344 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -648,7 +648,7 @@ func (p *PKI) GetCertificateAuthority() error { // SSH user certificates and a private key used for signing host certificates. func (p *PKI) GenerateSSHSigningKeys(password []byte) error { // Enable SSH - p.options.enableSSH = true + p.options.enableSSH = true // TODO(hs): change this function to not mutate configuration state // Create SSH key used to sign host certificates. Using // kmsapi.UnspecifiedSignAlgorithm will default to the default algorithm. diff --git a/pki/testdata/helm/simple.yml b/pki/testdata/helm/simple.yml index 9cc82806..8a7e369f 100644 --- a/pki/testdata/helm/simple.yml +++ b/pki/testdata/helm/simple.yml @@ -32,7 +32,7 @@ inject: defaults.json: ca-url: https://127.0.0.1 ca-config: /home/step/config/ca.json - fingerprint: + fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3 root: /home/step/certs/root_ca.crt # Certificates contains the root and intermediate certificate and @@ -64,11 +64,18 @@ inject: x509: # intermediate_ca_key contains the contents of your encrypted intermediate CA key intermediate_ca_key: | - these are just some fake intermediate CA key bytes + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0 + ZXM= + -----END EC PRIVATE KEY----- + # root_ca_key contains the contents of your encrypted root CA key # Note that this value can be omitted without impacting the functionality of step-certificates # If supplied, this should be encrypted using a unique password that is not used for encrypting # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. root_ca_key: | - these are just some fake root CA key bytes + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz + -----END EC PRIVATE KEY----- + diff --git a/pki/testdata/helm/with-acme.yml b/pki/testdata/helm/with-acme.yml index 4f9d5761..488bc32f 100644 --- a/pki/testdata/helm/with-acme.yml +++ b/pki/testdata/helm/with-acme.yml @@ -33,7 +33,7 @@ inject: defaults.json: ca-url: https://127.0.0.1 ca-config: /home/step/config/ca.json - fingerprint: + fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3 root: /home/step/certs/root_ca.crt # Certificates contains the root and intermediate certificate and @@ -65,11 +65,18 @@ inject: x509: # intermediate_ca_key contains the contents of your encrypted intermediate CA key intermediate_ca_key: | - these are just some fake intermediate CA key bytes + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0 + ZXM= + -----END EC PRIVATE KEY----- + # root_ca_key contains the contents of your encrypted root CA key # Note that this value can be omitted without impacting the functionality of step-certificates # If supplied, this should be encrypted using a unique password that is not used for encrypting # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. root_ca_key: | - these are just some fake root CA key bytes + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz + -----END EC PRIVATE KEY----- + diff --git a/pki/testdata/helm/with-admin.yml b/pki/testdata/helm/with-admin.yml index b90647ea..790fbdd4 100644 --- a/pki/testdata/helm/with-admin.yml +++ b/pki/testdata/helm/with-admin.yml @@ -32,7 +32,7 @@ inject: defaults.json: ca-url: https://127.0.0.1 ca-config: /home/step/config/ca.json - fingerprint: + fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3 root: /home/step/certs/root_ca.crt # Certificates contains the root and intermediate certificate and @@ -64,11 +64,18 @@ inject: x509: # intermediate_ca_key contains the contents of your encrypted intermediate CA key intermediate_ca_key: | - these are just some fake intermediate CA key bytes + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0 + ZXM= + -----END EC PRIVATE KEY----- + # root_ca_key contains the contents of your encrypted root CA key # Note that this value can be omitted without impacting the functionality of step-certificates # If supplied, this should be encrypted using a unique password that is not used for encrypting # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. root_ca_key: | - these are just some fake root CA key bytes + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz + -----END EC PRIVATE KEY----- + diff --git a/pki/testdata/helm/with-provisioner.yml b/pki/testdata/helm/with-provisioner.yml index 75788acc..de17ef0a 100644 --- a/pki/testdata/helm/with-provisioner.yml +++ b/pki/testdata/helm/with-provisioner.yml @@ -32,7 +32,7 @@ inject: defaults.json: ca-url: https://127.0.0.1 ca-config: /home/step/config/ca.json - fingerprint: + fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3 root: /home/step/certs/root_ca.crt # Certificates contains the root and intermediate certificate and @@ -64,11 +64,18 @@ inject: x509: # intermediate_ca_key contains the contents of your encrypted intermediate CA key intermediate_ca_key: | - these are just some fake intermediate CA key bytes + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0 + ZXM= + -----END EC PRIVATE KEY----- + # root_ca_key contains the contents of your encrypted root CA key # Note that this value can be omitted without impacting the functionality of step-certificates # If supplied, this should be encrypted using a unique password that is not used for encrypting # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. root_ca_key: | - these are just some fake root CA key bytes + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz + -----END EC PRIVATE KEY----- + diff --git a/pki/testdata/helm/with-ssh.yml b/pki/testdata/helm/with-ssh.yml index 1f922994..e1ce4143 100644 --- a/pki/testdata/helm/with-ssh.yml +++ b/pki/testdata/helm/with-ssh.yml @@ -35,7 +35,7 @@ inject: defaults.json: ca-url: https://127.0.0.1 ca-config: /home/step/config/ca.json - fingerprint: + fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3 root: /home/step/certs/root_ca.crt # Certificates contains the root and intermediate certificate and @@ -56,10 +56,10 @@ inject: -----END CERTIFICATE----- # ssh_host_ca contains the text of the public ssh key for the SSH root CA - ssh_host_ca: fake ssh host cert bytes + ssh_host_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ0IdS5sZm6KITBMZLEJD6b5ROVraYHcAOr3feFel8r1Wp4DRPR1oU0W00J/zjNBRBbANlJoYN4x/8WNNVZ49Ms= # ssh_user_ca contains the text of the public ssh key for the SSH root CA - ssh_user_ca: fake ssh user cert bytes + ssh_user_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEWA1qUxaGwVNErsvEOGe2d6TvLMF+aiVpuOiIEvpMJ3JeJmecLQctjWqeIbpSvy6/gRa7c82Ge5rLlapYmOChs= # Secrets contains the root and intermediate keys and optionally the SSH # private keys @@ -72,19 +72,32 @@ inject: x509: # intermediate_ca_key contains the contents of your encrypted intermediate CA key intermediate_ca_key: | - these are just some fake intermediate CA key bytes + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0 + ZXM= + -----END EC PRIVATE KEY----- + # root_ca_key contains the contents of your encrypted root CA key # Note that this value can be omitted without impacting the functionality of step-certificates # If supplied, this should be encrypted using a unique password that is not used for encrypting # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. root_ca_key: | - these are just some fake root CA key bytes + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz + -----END EC PRIVATE KEY----- + ssh: # ssh_host_ca_key contains the contents of your encrypted SSH Host CA key host_ca_key: | - fake ssh host key bytes + -----BEGIN EC PRIVATE KEY----- + ZmFrZSBzc2ggaG9zdCBrZXkgYnl0ZXM= + -----END EC PRIVATE KEY----- + # ssh_user_ca_key contains the contents of your encrypted SSH User CA key user_ca_key: | - fake ssh user key bytes + -----BEGIN EC PRIVATE KEY----- + ZmFrZSBzc2ggdXNlciBrZXkgYnl0ZXM= + -----END EC PRIVATE KEY----- + From 57001168a5e6757f0472eea24a20a58460fea04d Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 14 Oct 2022 14:05:39 +0200 Subject: [PATCH 34/66] Add default `SSHPOP` provisioner to Helm template output --- pki/helm.go | 29 ++++--- pki/helm_test.go | 16 ++++ pki/testdata/helm/with-ssh-and-acme.yml | 105 ++++++++++++++++++++++++ pki/testdata/helm/with-ssh.yml | 1 + 4 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 pki/testdata/helm/with-ssh-and-acme.yml diff --git a/pki/helm.go b/pki/helm.go index 3fbadb40..72d95971 100644 --- a/pki/helm.go +++ b/pki/helm.go @@ -35,18 +35,14 @@ func (p *PKI) WriteHelmTemplate(w io.Writer) error { p.Ssh = nil } - // Convert provisioner to ca.json - numberOfProvisioners := len(p.Authority.Provisioners) - if p.options.enableACME { - numberOfProvisioners++ - } - provisioners := make([]provisioner.Interface, numberOfProvisioners) - for i, p := range p.Authority.Provisioners { + // Convert provisioners to ca.json representation + provisioners := []provisioner.Interface{} + for _, p := range p.Authority.Provisioners { pp, err := authority.ProvisionerToCertificates(p) if err != nil { return err } - provisioners[i] = pp + provisioners = append(provisioners, pp) } // Add default ACME provisioner if enabled. Note that this logic is similar @@ -56,14 +52,23 @@ func (p *PKI) WriteHelmTemplate(w io.Writer) error { // TODO(hs): consider refactoring the initialization, so that this becomes // easier to reason about and maintain. if p.options.enableACME { - provisioners[len(provisioners)-1] = &provisioner.ACME{ + provisioners = append(provisioners, &provisioner.ACME{ Type: "ACME", Name: "acme", - } + }) } - // TODO(hs): add default SSHPOP provisioner if SSH is configured, similar - // as the ACME one above. + // Add default SSHPOP provisioner if enabled. Similar to the above, this is + // the same as what happens in p.GenerateConfig(). + if p.options.enableSSH { + provisioners = append(provisioners, &provisioner.SSHPOP{ + Type: "SSHPOP", + Name: "sshpop", + Claims: &provisioner.Claims{ + EnableSSHCA: &p.options.enableSSH, + }, + }) + } if err := tmpl.Execute(w, helmVariables{ Configuration: &p.Configuration, diff --git a/pki/helm_test.go b/pki/helm_test.go index 1eb621a8..21d6d4db 100644 --- a/pki/helm_test.go +++ b/pki/helm_test.go @@ -105,6 +105,22 @@ func TestPKI_WriteHelmTemplate(t *testing.T) { testFile: "testdata/helm/with-ssh.yml", wantErr: false, }, + { + name: "ok/with-ssh-and-acme", + fields: fields{ + pkiOptions: []Option{ + WithHelm(), + WithACME(), + WithSSH(), + }, + casOptions: apiv1.Options{ + Type: "softcas", + IsCreator: true, + }, + }, + testFile: "testdata/helm/with-ssh-and-acme.yml", + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pki/testdata/helm/with-ssh-and-acme.yml b/pki/testdata/helm/with-ssh-and-acme.yml new file mode 100644 index 00000000..639aca6a --- /dev/null +++ b/pki/testdata/helm/with-ssh-and-acme.yml @@ -0,0 +1,105 @@ +# Helm template +inject: + enabled: true + # Config contains the configuration files ca.json and defaults.json + config: + files: + ca.json: + root: /home/step/certs/root_ca.crt + federateRoots: [] + crt: /home/step/certs/intermediate_ca.crt + key: /home/step/secrets/intermediate_ca_key + ssh: + hostKey: /home/step/secrets/ssh_host_ca_key + userKey: /home/step/secrets/ssh_user_ca_key + address: 127.0.0.1:9000 + dnsNames: + - 127.0.0.1 + logger: + format: json + db: + type: badgerv2 + dataSource: /home/step/db + authority: + enableAdmin: false + provisioners: + - {"type":"JWK","name":"step-cli","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","claims":{"enableSSHCA":true,"disableRenewal":false,"allowRenewalAfterExpiry":false},"options":{"x509":{},"ssh":{}}} + - {"type":"ACME","name":"acme"} + - {"type":"SSHPOP","name":"sshpop","claims":{"enableSSHCA":true}} + tls: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + minVersion: 1.2 + maxVersion: 1.3 + renegotiation: false + + defaults.json: + ca-url: https://127.0.0.1 + ca-config: /home/step/config/ca.json + fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3 + root: /home/step/certs/root_ca.crt + + # Certificates contains the root and intermediate certificate and + # optionally the SSH host and user public keys + certificates: + # intermediate_ca contains the text of the intermediate CA Certificate + intermediate_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5 + dGVz + -----END CERTIFICATE----- + + + # root_ca contains the text of the root CA Certificate + root_ca: | + -----BEGIN CERTIFICATE----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw== + -----END CERTIFICATE----- + + # ssh_host_ca contains the text of the public ssh key for the SSH root CA + ssh_host_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ0IdS5sZm6KITBMZLEJD6b5ROVraYHcAOr3feFel8r1Wp4DRPR1oU0W00J/zjNBRBbANlJoYN4x/8WNNVZ49Ms= + + # ssh_user_ca contains the text of the public ssh key for the SSH root CA + ssh_user_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEWA1qUxaGwVNErsvEOGe2d6TvLMF+aiVpuOiIEvpMJ3JeJmecLQctjWqeIbpSvy6/gRa7c82Ge5rLlapYmOChs= + + # Secrets contains the root and intermediate keys and optionally the SSH + # private keys + secrets: + # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key + # This value must be base64 encoded. + ca_password: + provisioner_password: + + x509: + # intermediate_ca_key contains the contents of your encrypted intermediate CA key + intermediate_ca_key: | + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0 + ZXM= + -----END EC PRIVATE KEY----- + + + # root_ca_key contains the contents of your encrypted root CA key + # Note that this value can be omitted without impacting the functionality of step-certificates + # If supplied, this should be encrypted using a unique password that is not used for encrypting + # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. + root_ca_key: | + -----BEGIN EC PRIVATE KEY----- + dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz + -----END EC PRIVATE KEY----- + + ssh: + # ssh_host_ca_key contains the contents of your encrypted SSH Host CA key + host_ca_key: | + -----BEGIN EC PRIVATE KEY----- + ZmFrZSBzc2ggaG9zdCBrZXkgYnl0ZXM= + -----END EC PRIVATE KEY----- + + + # ssh_user_ca_key contains the contents of your encrypted SSH User CA key + user_ca_key: | + -----BEGIN EC PRIVATE KEY----- + ZmFrZSBzc2ggdXNlciBrZXkgYnl0ZXM= + -----END EC PRIVATE KEY----- + diff --git a/pki/testdata/helm/with-ssh.yml b/pki/testdata/helm/with-ssh.yml index e1ce4143..2e4845f0 100644 --- a/pki/testdata/helm/with-ssh.yml +++ b/pki/testdata/helm/with-ssh.yml @@ -24,6 +24,7 @@ inject: enableAdmin: false provisioners: - {"type":"JWK","name":"step-cli","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","claims":{"enableSSHCA":true,"disableRenewal":false,"allowRenewalAfterExpiry":false},"options":{"x509":{},"ssh":{}}} + - {"type":"SSHPOP","name":"sshpop","claims":{"enableSSHCA":true}} tls: cipherSuites: - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 From d981b9e0dc5cf1d430a4c5db3f1c8a8e0d05c265 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 14 Oct 2022 16:01:18 +0200 Subject: [PATCH 35/66] Add `--admin-subject` flag to `ca init` The first super admin subject can now be provided through the `--admin-subject` flag when initializing a CA. It's not yet possible to configure the subject of the first super admin when provisioners are migrated from `ca.json` to the database. This effectively limits usage of the flag to scenarios in which the provisioners are written to the database immediately, so when `--remote-management` is enabled. It currently also doesn't work with Helm deployments, because there's no mechanism yet to pass this type of option to the Helm chart. This commit partially addresses https://github.com/smallstep/cli/issues/697 --- authority/authority.go | 8 ++ pki/helm_test.go | 225 ++++++++++++++++++----------------------- pki/pki.go | 45 ++++++--- 3 files changed, 141 insertions(+), 137 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index e3bc3473..ae8b9a56 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -663,6 +663,14 @@ func (a *Authority) init() error { } // Create first super admin, belonging to the first JWK provisioner + // TODO(hs): pass a user-provided first super admin subject to here. With `ca init` it's + // added to the DB immediately if using remote management. But when migrating from + // ca.json to the DB, this option doesn't exist. Adding a flag just to do it during + // migration isn't nice. We could opt for a user to change it afterwards. There exist + // cases in which creation of `step` could lock out a user from API access. This is the + // case if `step` isn't allowed to be signed by Name Constraints or the X.509 policy. + // We have protection for that when creating and updating a policy, but if a policy or + // Name Constraints are in use at the time of migration, that could lock the user out. firstSuperAdminSubject := "step" if err := a.adminDB.CreateAdmin(ctx, &linkedca.Admin{ ProvisionerId: firstJWKProvisioner.Id, diff --git a/pki/helm_test.go b/pki/helm_test.go index 21d6d4db..ea1c4acd 100644 --- a/pki/helm_test.go +++ b/pki/helm_test.go @@ -21,148 +21,125 @@ import ( ) func TestPKI_WriteHelmTemplate(t *testing.T) { - type fields struct { - casOptions apiv1.Options - pkiOptions []Option + var preparePKI = func(t *testing.T, opts ...Option) *PKI { + o := apiv1.Options{ + Type: "softcas", + IsCreator: true, + } + + // Add default WithHelm option + opts = append(opts, WithHelm()) + + // TODO(hs): invoking `New` doesn't perform all operations that are executed + // when `ca init --helm` is executed. Ideally this logic should be handled + // in one place and probably inside of the PKI initialization. For testing + // purposes the missing operations to fill a Helm template fully are faked + // by `setKeyPair`, `setCertificates` and `setSSHSigningKeys` + p, err := New(o, opts...) + assert.NoError(t, err) + + // setKeyPair sets a predefined JWK and a default JWK provisioner. This is one + // of the things performed in the `ca init` code that's not part of `New`, but + // performed after that in p.GenerateKeyPairs`. We're currently using the same + // JWK for every test to keep test variance small: we're not testing JWK generation + // here after all. It's a bit dangerous to redefine the function here, but it's + // the simplest way to make this fully testable without refactoring the init now. + // The password for the predefined encrypted key is \x01\x03\x03\x07. + setKeyPair(t, p) + + // setCertificates sets some static intermediate and root CA certificate bytes. It + // replaces the logic executed in `p.GenerateRootCertificate`, `p.WriteRootCertificate`, + // and `p.GenerateIntermediateCertificate`. + setCertificates(t, p) + + // setSSHSigningKeys sets predefined SSH user and host certificate and key bytes. + // This replaces the logic in `p.GenerateSSHSigningKeys` + setSSHSigningKeys(t, p) + + return p } - tests := []struct { - name string - fields fields + type test struct { + pki *PKI testFile string wantErr bool - }{ - { - name: "ok/simple", - fields: fields{ - pkiOptions: []Option{ - WithHelm(), - }, - casOptions: apiv1.Options{ - Type: "softcas", - IsCreator: true, - }, - }, - testFile: "testdata/helm/simple.yml", - wantErr: false, + } + var tests = map[string]func(t *testing.T) test{ + "ok/simple": func(t *testing.T) test { + return test{ + pki: preparePKI(t), + testFile: "testdata/helm/simple.yml", + wantErr: false, + } }, - { - name: "ok/with-provisioner", - fields: fields{ - pkiOptions: []Option{ - WithHelm(), - WithProvisioner("a-provisioner"), - }, - casOptions: apiv1.Options{ - Type: "softcas", - IsCreator: true, - }, - }, - testFile: "testdata/helm/with-provisioner.yml", - wantErr: false, + "ok/with-provisioner": func(t *testing.T) test { + return test{ + pki: preparePKI(t, WithProvisioner("a-provisioner")), + testFile: "testdata/helm/with-provisioner.yml", + wantErr: false, + } }, - { - name: "ok/with-acme", - fields: fields{ - pkiOptions: []Option{ - WithHelm(), - WithACME(), - }, - casOptions: apiv1.Options{ - Type: "softcas", - IsCreator: true, - }, - }, - testFile: "testdata/helm/with-acme.yml", - wantErr: false, + "ok/with-acme": func(t *testing.T) test { + return test{ + pki: preparePKI(t, WithACME()), + testFile: "testdata/helm/with-acme.yml", + wantErr: false, + } }, - { - name: "ok/with-admin", - fields: fields{ - pkiOptions: []Option{ - WithHelm(), - WithAdmin(), - }, - casOptions: apiv1.Options{ - Type: "softcas", - IsCreator: true, - }, - }, - testFile: "testdata/helm/with-admin.yml", - wantErr: false, + "ok/with-admin": func(t *testing.T) test { + return test{ + pki: preparePKI(t, WithAdmin()), + testFile: "testdata/helm/with-admin.yml", + wantErr: false, + } }, - { - name: "ok/with-ssh", - fields: fields{ - pkiOptions: []Option{ - WithHelm(), - WithSSH(), - }, - casOptions: apiv1.Options{ - Type: "softcas", - IsCreator: true, - }, - }, - testFile: "testdata/helm/with-ssh.yml", - wantErr: false, + "ok/with-ssh": func(t *testing.T) test { + return test{ + pki: preparePKI(t, WithSSH()), + testFile: "testdata/helm/with-ssh.yml", + wantErr: false, + } }, - { - name: "ok/with-ssh-and-acme", - fields: fields{ - pkiOptions: []Option{ - WithHelm(), - WithACME(), - WithSSH(), + "ok/with-ssh-and-acme": func(t *testing.T) test { + return test{ + pki: preparePKI(t, WithSSH(), WithACME()), + testFile: "testdata/helm/with-ssh-and-acme.yml", + wantErr: false, + } + }, + "fail/authority.ProvisionerToCertificates": func(t *testing.T) test { + pki := preparePKI(t) + pki.Authority.Provisioners = append(pki.Authority.Provisioners, + &linkedca.Provisioner{ + Type: linkedca.Provisioner_JWK, + Name: "Broken JWK", + Details: nil, }, - casOptions: apiv1.Options{ - Type: "softcas", - IsCreator: true, - }, - }, - testFile: "testdata/helm/with-ssh-and-acme.yml", - wantErr: false, + ) + return test{ + pki: pki, + wantErr: true, + } }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - o := tt.fields.casOptions - opts := tt.fields.pkiOptions - - // TODO(hs): invoking `New` doesn't perform all operations that are executed - // when `ca init --helm` is executed. Ideally this logic should be handled - // in one place and probably inside of the PKI initialization. For testing - // purposes the missing operations to fill a Helm template fully are faked - // by `setKeyPair`, `setCertificates` and `setSSHSigningKeys` - p, err := New(o, opts...) - assert.NoError(t, err) - - // setKeyPair sets a predefined JWK and a default JWK provisioner. This is one - // of the things performed in the `ca init` code that's not part of `New`, but - // performed after that in p.GenerateKeyPairs`. We're currently using the same - // JWK for every test to keep test variance small: we're not testing JWK generation - // here after all. It's a bit dangerous to redefine the function here, but it's - // the simplest way to make this fully testable without refactoring the init now. - // The password for the predefined encrypted key is \x01\x03\x03\x07. - setKeyPair(t, p) - - // setCertificates sets some static intermediate and root CA certificate bytes. It - // replaces the logic executed in `p.GenerateRootCertificate`, `p.WriteRootCertificate`, - // and `p.GenerateIntermediateCertificate`. - setCertificates(t, p) - - // setSSHSigningKeys sets predefined SSH user and host certificate and key bytes. - // This replaces the logic in `p.GenerateSSHSigningKeys` - setSSHSigningKeys(t, p) + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { w := &bytes.Buffer{} - if err := p.WriteHelmTemplate(w); (err != nil) != tt.wantErr { - t.Errorf("PKI.WriteHelmTemplate() error = %v, wantErr %v", err, tt.wantErr) + if err := tc.pki.WriteHelmTemplate(w); (err != nil) != tc.wantErr { + t.Errorf("PKI.WriteHelmTemplate() error = %v, wantErr %v", err, tc.wantErr) return } - wantBytes, err := os.ReadFile(tt.testFile) + if tc.wantErr { + // don't compare output if an error was expected on output + return + } + + wantBytes, err := os.ReadFile(tc.testFile) assert.NoError(t, err) if diff := cmp.Diff(wantBytes, w.Bytes()); diff != "" { - t.Logf("Generated Helm template did not match reference %q\n", tt.testFile) + t.Logf("Generated Helm template did not match reference %q\n", tc.testFile) t.Errorf("Diff follows:\n%s\n", diff) t.Errorf("Full output:\n%s\n", w.Bytes()) } diff --git a/pki/pki.go b/pki/pki.go index a4a64344..cf7c7d09 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -175,18 +175,19 @@ func GetProvisionerKey(caURL, rootFile, kid string) (string, error) { } type options struct { - provisioner string - pkiOnly bool - enableACME bool - enableSSH bool - enableAdmin bool - noDB bool - isHelm bool - deploymentType DeploymentType - rootKeyURI string - intermediateKeyURI string - hostKeyURI string - userKeyURI string + provisioner string + firstSuperAdminSubject string + pkiOnly bool + enableACME bool + enableSSH bool + enableAdmin bool + noDB bool + isHelm bool + deploymentType DeploymentType + rootKeyURI string + intermediateKeyURI string + hostKeyURI string + userKeyURI string } // Option is the type of a configuration option on the pki constructor. @@ -220,6 +221,15 @@ func WithProvisioner(s string) Option { } } +// WithFirstSuperAdminSubject defines the subject of the first +// super admin for use with the Admin API. The admin will belong +// to the first JWK provisioner. +func WithFirstSuperAdminSubject(s string) Option { + return func(p *PKI) { + p.options.firstSuperAdminSubject = s + } +} + // WithPKIOnly will only generate the PKI without the step-ca config files. func WithPKIOnly() Option { return func(p *PKI) { @@ -886,6 +896,11 @@ func (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) { // // Note that we might want to be able to define the database as a // flag in `step ca init` so we can write to the proper place. + // + // TODO(hs): the logic for creating the provisioners and the super admin + // is similar to what's done when automatically migrating the provisioners. + // This is related to the existing comment above. Refactor this to exist in + // a single place and ensure it happensonly once. _db, err := db.New(cfg.DB) if err != nil { return nil, err @@ -909,9 +924,13 @@ func (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) { } } // Add the first provisioner as an admin. + firstSuperAdminSubject := "step" + if p.options.firstSuperAdminSubject != "" { + firstSuperAdminSubject = p.options.firstSuperAdminSubject + } if err := adminDB.CreateAdmin(context.Background(), &linkedca.Admin{ AuthorityId: admin.DefaultAuthorityID, - Subject: "step", + Subject: firstSuperAdminSubject, Type: linkedca.Admin_SUPER_ADMIN, ProvisionerId: adminID, }); err != nil { From d07c9accea530c60ba10f95af2ce06520565fc10 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 19 Oct 2022 16:28:31 -0700 Subject: [PATCH 36/66] Use sh instead of bash in .version.sh script Fixes #1115 --- .version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.version.sh b/.version.sh index 14adccbf..e7f823cd 100755 --- a/.version.sh +++ b/.version.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh read -r firstline < .VERSION last_half="${firstline##*tag: }" if [[ ${last_half::1} == "v" ]]; then From 18555a3cb25c1c8b0cd291d8115df75435a30f98 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 19 Oct 2022 17:55:18 -0700 Subject: [PATCH 37/66] Split build and download in Dockerfiles On systems with low resources the command `go mod download` can fail. This causes long builds of the docker images. This change adds a new layer in the docker build splitting the build and download in two steps. Fixes #1114 --- docker/Dockerfile.step-ca | 1 + docker/Dockerfile.step-ca.hsm | 1 + 2 files changed, 2 insertions(+) diff --git a/docker/Dockerfile.step-ca b/docker/Dockerfile.step-ca index 46677a91..ed6b5f56 100644 --- a/docker/Dockerfile.step-ca +++ b/docker/Dockerfile.step-ca @@ -4,6 +4,7 @@ WORKDIR /src COPY . . RUN apk add --no-cache curl git make +RUN make V=1 download RUN make V=1 bin/step-ca bin/step-awskms-init bin/step-cloudkms-init diff --git a/docker/Dockerfile.step-ca.hsm b/docker/Dockerfile.step-ca.hsm index ac59c909..8f413cd7 100644 --- a/docker/Dockerfile.step-ca.hsm +++ b/docker/Dockerfile.step-ca.hsm @@ -5,6 +5,7 @@ COPY . . RUN apk add --no-cache curl git make RUN apk add --no-cache gcc musl-dev pkgconf pcsc-lite-dev +RUN make V=1 download RUN make V=1 GOFLAGS="" build From aefdfc7be7a7c4d5b038de0b3142b47c7dce1b00 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 19 Oct 2022 19:10:50 -0700 Subject: [PATCH 38/66] Use RawSubject on renew and rekey Renew was not replicating exactly the subject because extra names gets decoded into pkix.Name.Names, the non-default ones should be added to pkix.Name.ExtraNames. Instead of doing that, this commit sets the RawSubject that will also keep the order. Fixes #1106 --- authority/tls.go | 2 +- authority/tls_test.go | 42 +++++++++++++++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/authority/tls.go b/authority/tls.go index 90316cc3..6c176f06 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -320,7 +320,7 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5 // Create new certificate from previous values. // Issuer, NotBefore, NotAfter and SubjectKeyId will be set by the CAS. newCert := &x509.Certificate{ - Subject: oldCert.Subject, + RawSubject: oldCert.RawSubject, KeyUsage: oldCert.KeyUsage, UnhandledCriticalExtensions: oldCert.UnhandledCriticalExtensions, ExtKeyUsage: oldCert.ExtKeyUsage, diff --git a/authority/tls_test.go b/authority/tls_test.go index dd52ea74..53d0401a 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -139,6 +139,13 @@ func generateIntermidiateCertificate(t *testing.T, issuer *x509.Certificate, sig return cert, priv } +func withSubject(sub pkix.Name) provisioner.CertificateModifierFunc { + return func(crt *x509.Certificate, _ provisioner.SignOptions) error { + crt.Subject = sub + return nil + } +} + func withProvisionerOID(name, kid string) provisioner.CertificateModifierFunc { return func(crt *x509.Certificate, _ provisioner.SignOptions) error { b, err := asn1.Marshal(stepProvisionerASN1{ @@ -952,6 +959,18 @@ func TestAuthority_Renew(t *testing.T) { withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID), withSigner(issuer, signer)) + certExtraNames := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"}, + withSubject(pkix.Name{ + CommonName: "renew", + ExtraNames: []pkix.AttributeTypeAndValue{ + {Type: asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 25}, Value: "dc"}, + }, + }), + withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()), + withDefaultASN1DN(a.config.AuthorityConfig.Template), + withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID), + withSigner(issuer, signer)) + certNoRenew := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"}, withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()), withDefaultASN1DN(a.config.AuthorityConfig.Template), @@ -1001,6 +1020,12 @@ func TestAuthority_Renew(t *testing.T) { cert: cert, }, nil }, + "ok/WithExtraNames": func() (*renewTest, error) { + return &renewTest{ + auth: a, + cert: certExtraNames, + }, nil + }, "ok/success-new-intermediate": func() (*renewTest, error) { rootCert, rootSigner := generateRootCertificate(t) intCert, intSigner := generateIntermidiateCertificate(t, rootCert, rootSigner) @@ -1063,15 +1088,14 @@ func TestAuthority_Renew(t *testing.T) { assert.True(t, leaf.NotAfter.Before(expiry.Add(time.Hour))) tmplt := a.config.AuthorityConfig.Template - assert.Equals(t, leaf.Subject.String(), - pkix.Name{ - Country: []string{tmplt.Country}, - Organization: []string{tmplt.Organization}, - Locality: []string{tmplt.Locality}, - StreetAddress: []string{tmplt.StreetAddress}, - Province: []string{tmplt.Province}, - CommonName: tmplt.CommonName, - }.String()) + assert.Equals(t, leaf.RawSubject, tc.cert.RawSubject) + assert.Equals(t, leaf.Subject.Country, []string{tmplt.Country}) + assert.Equals(t, leaf.Subject.Organization, []string{tmplt.Organization}) + assert.Equals(t, leaf.Subject.Locality, []string{tmplt.Locality}) + assert.Equals(t, leaf.Subject.StreetAddress, []string{tmplt.StreetAddress}) + assert.Equals(t, leaf.Subject.Province, []string{tmplt.Province}) + assert.Equals(t, leaf.Subject.CommonName, tmplt.CommonName) + assert.Equals(t, leaf.Issuer, intermediate.Subject) assert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256) From 49718f1bbb96fc5e889a249408dfb32e265b2eca Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Fri, 21 Oct 2022 11:11:42 +0200 Subject: [PATCH 39/66] Fix some comments --- authority/authority.go | 14 ++++++++------ pki/pki.go | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index ae8b9a56..c3155d96 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -94,7 +94,7 @@ type Authority struct { // If true, do not initialize the authority skipInit bool - // If true, does not output initialization logs + // If true, do not output initialization logs quietInit bool } @@ -603,9 +603,13 @@ func (a *Authority) init() error { return admin.WrapErrorISE(err, "error loading provisioners to initialize authority") } if len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, "linked") { + // Migration will currently only be kicked off once, because either one or more provisioners + // are migrated or a default JWK provisioner will be created in the DB. It won't run for + // linked or hosted deployments. Not for linked, because that case is explicitly checked + // for above. Not for hosted, because there'll be at least an existing OIDC provisioner. var firstJWKProvisioner *linkedca.Provisioner if len(a.config.AuthorityConfig.Provisioners) > 0 { - // Existing provisioners detected; try migrating them to DB storage + // Existing provisioners detected; try migrating them to DB storage. a.initLogf("Starting migration of provisioners") for _, p := range a.config.AuthorityConfig.Provisioners { lp, err := ProvisionerToLinkedca(p) @@ -621,14 +625,12 @@ func (a *Authority) init() error { // Mark the first JWK provisioner, so that it can be used for administration purposes if firstJWKProvisioner == nil && lp.Type == linkedca.Provisioner_JWK { firstJWKProvisioner = lp - a.initLogf("Migrated JWK provisioner %q with admin permissions", p.GetName()) // TODO(hs): change the wording? + a.initLogf("Migrated JWK provisioner %q with admin permissions", p.GetName()) } else { a.initLogf("Migrated %s provisioner %q", p.GetType(), p.GetName()) } } - // TODO(hs): test if this works with LinkedCA too. Also could be useful - // for printing out where the configuration is read from in case of LinkedCA. c := a.config if c.WasLoadedFromFile() { // TODO(hs): check if prerequisites for writing files look OK (user/group, permission bits, etc) as @@ -659,7 +661,7 @@ func (a *Authority) init() error { if err != nil { return admin.WrapErrorISE(err, "error creating first provisioner") } - a.initLogf("Created JWK provisioner %q with admin permissions", firstJWKProvisioner.GetName()) // TODO(hs): change the wording? + a.initLogf("Created JWK provisioner %q with admin permissions", firstJWKProvisioner.GetName()) } // Create first super admin, belonging to the first JWK provisioner diff --git a/pki/pki.go b/pki/pki.go index cf7c7d09..df65a721 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -900,7 +900,7 @@ func (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) { // TODO(hs): the logic for creating the provisioners and the super admin // is similar to what's done when automatically migrating the provisioners. // This is related to the existing comment above. Refactor this to exist in - // a single place and ensure it happensonly once. + // a single place and ensure it happens only once. _db, err := db.New(cfg.DB) if err != nil { return nil, err From fd38dd34f945bedb9728139a38573806d73c6b59 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 24 Oct 2022 14:51:27 +0200 Subject: [PATCH 40/66] Fix PR comments --- authority/authority.go | 28 ++++++++-------------------- pki/pki.go | 40 ++++++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index c3155d96..47a1de1c 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -633,23 +633,11 @@ func (a *Authority) init() error { c := a.config if c.WasLoadedFromFile() { - // TODO(hs): check if prerequisites for writing files look OK (user/group, permission bits, etc) as - // extra safety check before trying to write at all? - - // Remove the existing provisioners from the authority configuration - // and commit it to the existing configuration file. NOTE: committing - // the configuration at this point also writes other properties that - // have been initialized with default values, such as the `backdate` and - // `template` settings in the `authority`. - oldProvisioners := c.AuthorityConfig.Provisioners - c.AuthorityConfig.Provisioners = []provisioner.Interface{} - if err := c.Commit(); err != nil { - // Restore the provisioners in in-memory representation for consistency - // when writing the updated configuration fails. This is considered a soft - // error, so execution can continue. - c.AuthorityConfig.Provisioners = oldProvisioners - a.initLogf("Failed removing provisioners from configuration: %v", err) - } + // The provisioners in the configuration file can be deleted from + // the file by editing it. Automatic rewriting of the file was considered + // to be too surprising for users and not the right solution for all + // use cases, so we leave it up to users to this themselves. + a.initLogf("Provisioners that were migrated can now be removed from `ca.json` by editing it.") } a.initLogf("Finished migrating provisioners") @@ -673,16 +661,16 @@ func (a *Authority) init() error { // case if `step` isn't allowed to be signed by Name Constraints or the X.509 policy. // We have protection for that when creating and updating a policy, but if a policy or // Name Constraints are in use at the time of migration, that could lock the user out. - firstSuperAdminSubject := "step" + superAdminSubject := "step" if err := a.adminDB.CreateAdmin(ctx, &linkedca.Admin{ ProvisionerId: firstJWKProvisioner.Id, - Subject: firstSuperAdminSubject, + Subject: superAdminSubject, Type: linkedca.Admin_SUPER_ADMIN, }); err != nil { return admin.WrapErrorISE(err, "error creating first admin") } - a.initLogf("Created super admin %q for JWK provisioner %q", firstSuperAdminSubject, firstJWKProvisioner.GetName()) + a.initLogf("Created super admin %q for JWK provisioner %q", superAdminSubject, firstJWKProvisioner.GetName()) } } diff --git a/pki/pki.go b/pki/pki.go index df65a721..cee3f06a 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -175,19 +175,19 @@ func GetProvisionerKey(caURL, rootFile, kid string) (string, error) { } type options struct { - provisioner string - firstSuperAdminSubject string - pkiOnly bool - enableACME bool - enableSSH bool - enableAdmin bool - noDB bool - isHelm bool - deploymentType DeploymentType - rootKeyURI string - intermediateKeyURI string - hostKeyURI string - userKeyURI string + provisioner string + superAdminSubject string + pkiOnly bool + enableACME bool + enableSSH bool + enableAdmin bool + noDB bool + isHelm bool + deploymentType DeploymentType + rootKeyURI string + intermediateKeyURI string + hostKeyURI string + userKeyURI string } // Option is the type of a configuration option on the pki constructor. @@ -221,12 +221,12 @@ func WithProvisioner(s string) Option { } } -// WithFirstSuperAdminSubject defines the subject of the first +// WithSuperAdminSubject defines the subject of the first // super admin for use with the Admin API. The admin will belong // to the first JWK provisioner. -func WithFirstSuperAdminSubject(s string) Option { +func WithSuperAdminSubject(s string) Option { return func(p *PKI) { - p.options.firstSuperAdminSubject = s + p.options.superAdminSubject = s } } @@ -924,13 +924,13 @@ func (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) { } } // Add the first provisioner as an admin. - firstSuperAdminSubject := "step" - if p.options.firstSuperAdminSubject != "" { - firstSuperAdminSubject = p.options.firstSuperAdminSubject + superAdminSubject := "step" + if p.options.superAdminSubject != "" { + superAdminSubject = p.options.superAdminSubject } if err := adminDB.CreateAdmin(context.Background(), &linkedca.Admin{ AuthorityId: admin.DefaultAuthorityID, - Subject: firstSuperAdminSubject, + Subject: superAdminSubject, Type: linkedca.Admin_SUPER_ADMIN, ProvisionerId: adminID, }); err != nil { From 54c560f62027b12f917244d654e8e33e5a82a069 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 24 Oct 2022 15:22:37 +0200 Subject: [PATCH 41/66] Improve configuration file initialization log output --- authority/config/config.go | 16 +++++++++++----- ca/ca.go | 9 ++++++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/authority/config/config.go b/authority/config/config.go index 79228e98..7ec1d12e 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -75,7 +75,7 @@ type Config struct { SkipValidation bool `json:"-"` // Keeps record of the filename the Config is read from - loadedFromFilename string + loadedFromFilepath string } // ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer @@ -167,7 +167,7 @@ func LoadConfiguration(filename string) (*Config, error) { } // store filename that was read to populate Config - c.loadedFromFilename = filename + c.loadedFromFilepath = filename // initialize the Config c.Init() @@ -215,13 +215,19 @@ func (c *Config) Commit() error { if !c.WasLoadedFromFile() { return errors.New("cannot commit configuration if not loaded from file") } - return c.Save(c.loadedFromFilename) + return c.Save(c.loadedFromFilepath) } // WasLoadedFromFile returns whether or not the Config was -// read from a file. +// loaded from a file. func (c *Config) WasLoadedFromFile() bool { - return c.loadedFromFilename != "" + return c.loadedFromFilepath != "" +} + +// Filepath returns the path to the file the Config was +// loaded from. +func (c *Config) Filepath() string { + return c.loadedFromFilepath } // Validate validates the configuration. diff --git a/ca/ca.go b/ca/ca.go index 98f845b0..880f7e46 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -349,7 +349,7 @@ func (ca *CA) Run() error { if step.Contexts().GetCurrent() != nil { log.Printf("Current context: %s", step.Contexts().GetCurrent().Name) } - log.Printf("Config file: %s", ca.opts.configFile) + log.Printf("Config file: %s", ca.getConfigFileOutput()) baseURL := fmt.Sprintf("https://%s%s", authorityInfo.DNSNames[0], ca.config.Address[strings.LastIndex(ca.config.Address, ":"):]) @@ -569,3 +569,10 @@ func dumpRoutes(mux chi.Routes) { fmt.Printf("Logging err: %s\n", err.Error()) } } + +func (ca *CA) getConfigFileOutput() string { + if ca.config.WasLoadedFromFile() { + return ca.config.Filepath() + } + return "loaded from token" +} From 9d04e7d1dcf089d981001e5c6ac7c5b423fd95f0 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 24 Oct 2022 15:33:48 +0200 Subject: [PATCH 42/66] Remove period in log output --- authority/authority.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authority/authority.go b/authority/authority.go index 47a1de1c..1efe1a7c 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -637,7 +637,7 @@ func (a *Authority) init() error { // the file by editing it. Automatic rewriting of the file was considered // to be too surprising for users and not the right solution for all // use cases, so we leave it up to users to this themselves. - a.initLogf("Provisioners that were migrated can now be removed from `ca.json` by editing it.") + a.initLogf("Provisioners that were migrated can now be removed from `ca.json` by editing it") } a.initLogf("Finished migrating provisioners") From e90fe4bfa05b39080597b34059c8745b96fad273 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Mon, 24 Oct 2022 16:34:34 +0200 Subject: [PATCH 43/66] Update CHANGELOG.md with provisioner migration --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7172eda..59829021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Added name constraints evaluation and enforcement when issuing or renewing X.509 certificates. - Added provisioner webhooks for augmenting template data and authorizing certificate requests before signing. +- Added automatic migration of provisioners when enabling remote managment. ### Fixed - MySQL DSN parsing issues fixed with upgrade to [smallstep/nosql@v0.5.0](https://github.com/smallstep/nosql/releases/tag/v0.5.0). From 016973fd2b0d59696c70e5bfff2ea3156d102f1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 15:44:56 +0000 Subject: [PATCH 44/66] Bump google.golang.org/api from 0.99.0 to 0.100.0 Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.99.0 to 0.100.0. - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.99.0...v0.100.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 836f3007..a351e9cf 100644 --- a/go.mod +++ b/go.mod @@ -45,11 +45,11 @@ require ( go.step.sm/crypto v0.21.0 go.step.sm/linkedca v0.19.0-rc.3 golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b - golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458 + golang.org/x/net v0.0.0-20221014081412-f15817d10f9b golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect - google.golang.org/api v0.99.0 - google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e + google.golang.org/api v0.100.0 + google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a google.golang.org/grpc v1.50.1 google.golang.org/protobuf v1.28.1 gopkg.in/square/go-jose.v2 v2.6.0 @@ -141,7 +141,7 @@ require ( go.etcd.io/bbolt v1.3.6 // indirect go.opencensus.io v0.23.0 // indirect go.uber.org/atomic v1.9.0 // indirect - golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1 // indirect + golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect golang.org/x/text v0.3.8 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index 9e7182b6..95d3808a 100644 --- a/go.sum +++ b/go.sum @@ -906,8 +906,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458 h1:MgJ6t2zo8v0tbmLCueaCbF1RM+TtB0rs3Lv8DGtOIpY= -golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -924,8 +924,8 @@ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1 h1:3VPzK7eqH25j7GYw5w6g/GzNRc0/fYtrxz27z1gD4W0= -golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1135,8 +1135,8 @@ google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3h google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.99.0 h1:tsBtOIklCE2OFxhmcYSVqGwSAN/Y897srxmcvAQnwK8= -google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= +google.golang.org/api v0.100.0 h1:LGUYIrbW9pzYQQ8NWXlaIVkgnfubVBZbMFb9P8TK374= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1212,8 +1212,8 @@ google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e h1:halCgTFuLWDRD61piiNSxPsARANGD3Xl16hPrLgLiIg= -google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a h1:GH6UPn3ixhWcKDhpnEC55S75cerLPdpp3hrhfKYjZgw= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= From 3e961131621b9e4aea4241c5a21e2c00950ba8a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 15:45:07 +0000 Subject: [PATCH 45/66] Bump github.com/stretchr/testify from 1.8.0 to 1.8.1 Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.0 to 1.8.1. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.8.0...v1.8.1) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 836f3007..2f6360cd 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/slackhq/nebula v1.6.1 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 github.com/smallstep/nosql v0.5.0 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.1 github.com/urfave/cli v1.22.10 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.7.5 diff --git a/go.sum b/go.sum index 9e7182b6..709263bd 100644 --- a/go.sum +++ b/go.sum @@ -736,8 +736,9 @@ github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5J github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -745,8 +746,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= From aed1738ad0f6ded6ff3725de94cd96f6345b60ef Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 24 Oct 2022 11:07:28 -0700 Subject: [PATCH 46/66] Upgrade pkcs7 to the latest patches branch smallstep/pkcs7@patches includes now support for generic Decrypter methods, so KMS can be used instead of a key in disk with SCIM --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6d98804d..21d1f557 100644 --- a/go.mod +++ b/go.mod @@ -155,4 +155,4 @@ require ( // replace go.step.sm/linkedca => ../linkedca // use github.com/smallstep/pkcs7 fork with patches applied -replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20211016004704-52592125d6f6 +replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20221024180420-e1aab68dda05 diff --git a/go.sum b/go.sum index cad12021..e50444bb 100644 --- a/go.sum +++ b/go.sum @@ -710,8 +710,8 @@ github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/smallstep/nosql v0.5.0 h1:1BPyHy8bha8qSaxgULGEdqhXpNFXimAfudnauFVqmxw= github.com/smallstep/nosql v0.5.0/go.mod h1:yKZT5h7cdIVm6wEKM9+jN5dgK80Hljpuy8HNsnI7Gzo= -github.com/smallstep/pkcs7 v0.0.0-20211016004704-52592125d6f6 h1:8Rjy6IZbSM/jcYgBWCoLIGjug7QcoLtF9sUuhDrHD2U= -github.com/smallstep/pkcs7 v0.0.0-20211016004704-52592125d6f6/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +github.com/smallstep/pkcs7 v0.0.0-20221024180420-e1aab68dda05 h1:nVZXaJTwrUcfPUSZknkOidfITqOXSO0wE8pkOUTOdSM= +github.com/smallstep/pkcs7 v0.0.0-20221024180420-e1aab68dda05/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= From a9359522e60904277773e7fbe5d76a09fc85c1c7 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Tue, 25 Oct 2022 11:47:54 +0200 Subject: [PATCH 47/66] Add provisioner and super admin subject output to `ca init` When initializing a CA with `--remote-management`, it wasn't made clear that the default JWK provisioner is used when authenticating for administration purposes and that a default `step` user is created to login with. This commit adds some additional information to the CLI output on completion of `ca init`. --- pki/pki.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pki/pki.go b/pki/pki.go index cee3f06a..d6c15c9e 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -1013,6 +1013,18 @@ func (p *PKI) Save(opt ...ConfigOption) error { ui.PrintSelected("Default profile configuration", p.profileDefaults) } ui.PrintSelected("Certificate Authority configuration", p.config) + if cfg.AuthorityConfig.EnableAdmin && p.options.deploymentType != LinkedDeployment { + // TODO(hs): we may want to get this information from the DB, because that's + // where the admin and provisioner are stored in this case. Requires some + // refactoring. + superAdminSubject := "step" + if p.options.superAdminSubject != "" { + superAdminSubject = p.options.superAdminSubject + } + ui.PrintSelected("Admin provisioner", fmt.Sprintf("%s (JWK)", p.options.provisioner)) + ui.PrintSelected("Super admin subject", superAdminSubject) + } + if p.options.deploymentType != LinkedDeployment { ui.Println() if p.casOptions.Is(apiv1.SoftCAS) { From c43d59a69a220f82142677fe9fc624d9203f416e Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 25 Oct 2022 21:26:50 -0700 Subject: [PATCH 48/66] [action] keyless cosign for all release artifacts --- .github/workflows/release.yml | 38 +++++++++++++++++------------------ .goreleaser.yml | 9 +++++---- make/docker.mk | 2 +- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4c00ad04..48bbf730 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,8 +13,8 @@ jobs: create_release: name: Create Release - #needs: ci - runs-on: ubuntu-20.04 + needs: ci + runs-on: ubuntu-latest outputs: is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }} steps: @@ -25,7 +25,7 @@ jobs: echo ${{ github.ref }} | grep "\-rc.*" OUT=$? if [ $OUT -eq 0 ]; then IS_PRERELEASE=true; else IS_PRERELEASE=false; fi - echo "::set-output name=IS_PRERELEASE::${IS_PRERELEASE}" + echo "IS_PRERELEASE=${IS_PRERELEASE}" >> ${GITHUB_OUTPUT} - name: Create Release id: create_release uses: actions/create-release@v1 @@ -39,8 +39,11 @@ jobs: goreleaser: name: Upload Assets To Github w/ goreleaser - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest needs: create_release + permissions: + id-token: write + contents: write steps: - name: Checkout uses: actions/checkout@v3 @@ -50,17 +53,14 @@ jobs: go-version: 1.19 check-latest: true - name: Install cosign - uses: sigstore/cosign-installer@v2.7.0 + uses: sigstore/cosign-installer@v2 with: - cosign-release: 'v1.12.1' - - name: Write cosign key to disk - id: write_key - run: echo "${{ secrets.COSIGN_KEY }}" > "/tmp/cosign.key" + cosign-release: 'v1.13.1' - name: Get Release Date id: release_date run: | RELEASE_DATE=$(date +"%y-%m-%d") - echo "::set-output name=RELEASE_DATE::${RELEASE_DATE}" + echo "RELEASE_DATE=${RELEASE_DATE}" >> ${GITHUB_ENV} - name: Run GoReleaser uses: goreleaser/goreleaser-action@v3 with: @@ -68,13 +68,16 @@ jobs: args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GORELEASER_PAT }} - COSIGN_PWD: ${{ secrets.COSIGN_PWD }} - RELEASE_DATE: ${{ steps.release_date.outputs.RELEASE_DATE }} + RELEASE_DATE: ${RELEASE_DATE} + COSIGN_EXPERIMENTAL: 1 build_upload_docker: name: Build & Upload Docker Images - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest needs: ci + permissions: + id-token: write + contents: write steps: - name: Checkout uses: actions/checkout@v3 @@ -84,12 +87,9 @@ jobs: go-version: '1.19' check-latest: true - name: Install cosign - uses: sigstore/cosign-installer@v1.1.0 + uses: sigstore/cosign-installer@v2 with: - cosign-release: 'v1.1.0' - - name: Write cosign key to disk - id: write_key - run: echo "${{ secrets.COSIGN_KEY }}" > "/tmp/cosign.key" + cosign-release: 'v1.13.1' - name: Build id: build run: | @@ -98,4 +98,4 @@ jobs: env: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - COSIGN_PWD: ${{ secrets.COSIGN_PWD }} + COSIGN_EXPERIMENTAL: 1 diff --git a/.goreleaser.yml b/.goreleaser.yml index c8650d5b..43ffadb3 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -87,8 +87,9 @@ checksum: signs: - cmd: cosign - stdin: '{{ .Env.COSIGN_PWD }}' - args: ["sign-blob", "-key=/tmp/cosign.key", "-output-signature=${signature}", "${artifact}"] + signature: "${artifact}.sig" + certificate: "${artifact}.pem" + args: ["sign-blob", "--oidc-issuer=https://token.actions.githubusercontent.com", "--output-certificate=${certificate}", "--output-signature=${signature}", "${artifact}"] artifacts: all snapshot: @@ -154,8 +155,8 @@ release: ``` cosign verify-blob \ - -key https://raw.githubusercontent.com/smallstep/certificates/master/cosign.pub \ - -signature ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig + --certificate ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig.pem \ + --signature ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig \ ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz ``` diff --git a/make/docker.mk b/make/docker.mk index edb82423..0d56e663 100644 --- a/make/docker.mk +++ b/make/docker.mk @@ -54,7 +54,7 @@ define DOCKER_BUILDX # $(1) -- Image Tag # $(2) -- Push (empty is no push | --push will push to dockerhub) docker buildx build . --progress plain -t $(DOCKER_IMAGE_NAME):$(1) -f docker/Dockerfile.step-ca --platform="$(DOCKER_PLATFORMS)" $(2) - echo -n "$(COSIGN_PWD)" | cosign sign -key /tmp/cosign.key -r $(DOCKER_IMAGE_NAME):$(1) + cosign sign -r $(DOCKER_IMAGE_NAME):$(1) endef From 8200d198941b370cc197e8c06075085c1c97c08b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 26 Oct 2022 18:55:24 -0700 Subject: [PATCH 49/66] Improve CRL implementation This commit adds some changes to PR #731, some of them are: - Add distribution point to the CRL - Properly stop the goroutine that generates the CRLs - CRL config validation - Remove expired certificates from the CRL - Require enable set to true to generate a CRL This last point is the principal change in behaviour from the previous implementation. The CRL will not be generated if it's not enabled, and if it is enabled it will always be regenerated at some point, not only if there is a revocation. --- authority/authority.go | 84 +++++++---------- authority/authority_test.go | 6 -- authority/config/config.go | 71 ++++++++++++--- authority/tls.go | 176 +++++++++++++++++++++--------------- authority/tls_test.go | 35 ++++--- cas/softcas/softcas.go | 1 - db/db.go | 7 -- docs/GETTING_STARTED.md | 28 +++--- 8 files changed, 230 insertions(+), 178 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index b72fe61e..43216f1e 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -72,8 +72,9 @@ type Authority struct { sshCAHostFederatedCerts []ssh.PublicKey // CRL vars - crlTicker *time.Ticker - crlMutex sync.Mutex + crlTicker *time.Ticker + crlStopper chan struct{} + crlMutex sync.Mutex // Do not re-initialize initOnce bool @@ -662,22 +663,14 @@ func (a *Authority) init() error { // not be repeated. a.initOnce = true - // Start the CRL generator - if a.config.CRL != nil && a.config.CRL.Enabled { - if v := a.config.CRL.CacheDuration; v != nil && v.Duration < 0 { - return errors.New("crl cacheDuration must be >= 0") + // Start the CRL generator, we can assume the configuration is validated. + if a.config.CRL.IsEnabled() { + // Default cache duration to the default one + if v := a.config.CRL.CacheDuration; v == nil || v.Duration <= 0 { + a.config.CRL.CacheDuration = config.DefaultCRLCacheDuration } - - if v := a.config.CRL.CacheDuration; v != nil && v.Duration == 0 { - a.config.CRL.CacheDuration.Duration, _ = time.ParseDuration("24h") - } - - if a.config.CRL.CacheDuration == nil { - a.config.CRL.CacheDuration, _ = provisioner.NewDuration("24h") - } - - err = a.startCRLGenerator() - if err != nil { + // Start CRL generator + if err := a.startCRLGenerator(); err != nil { return err } } @@ -736,6 +729,7 @@ func (a *Authority) IsAdminAPIEnabled() bool { func (a *Authority) Shutdown() error { if a.crlTicker != nil { a.crlTicker.Stop() + close(a.crlStopper) } if err := a.keyManager.Close(); err != nil { @@ -746,9 +740,9 @@ func (a *Authority) Shutdown() error { // CloseForReload closes internal services, to allow a safe reload. func (a *Authority) CloseForReload() { - if a.crlTicker != nil { a.crlTicker.Stop() + close(a.crlStopper) } if err := a.keyManager.Close(); err != nil { @@ -791,27 +785,20 @@ func (a *Authority) requiresSCEPService() bool { return false } -// GetSCEPService returns the configured SCEP Service -// TODO: this function is intended to exist temporarily -// in order to make SCEP work more easily. It can be -// made more correct by using the right interfaces/abstractions -// after it works as expected. +// GetSCEPService returns the configured SCEP Service. +// +// TODO: this function is intended to exist temporarily in order to make SCEP +// work more easily. It can be made more correct by using the right +// interfaces/abstractions after it works as expected. func (a *Authority) GetSCEPService() *scep.Service { return a.scepService } func (a *Authority) startCRLGenerator() error { - - if a.config.CRL.CacheDuration.Duration <= 0 { + if !a.config.CRL.IsEnabled() { return nil } - // Make the renewal ticker run ~2/3 of cacheDuration by default, or use renewPeriod if available - tickerDuration := (a.config.CRL.CacheDuration.Duration / 3) * 2 - if v := a.config.CRL.RenewPeriod; v != nil && v.Duration > 0 { - tickerDuration = v.Duration - } - // Check that there is a valid CRL in the DB right now. If it doesn't exist // or is expired, generate one now _, ok := a.db.(db.CertificateRevocationListDB) @@ -819,37 +806,28 @@ func (a *Authority) startCRLGenerator() error { return errors.Errorf("CRL Generation requested, but database does not support CRL generation") } - // Always create a new CRL on startup in case the CA has been down and the time to next expected CRL - // update is less than the cache duration. - err := a.GenerateCertificateRevocationList() - if err != nil { + // Always create a new CRL on startup in case the CA has been down and the + // time to next expected CRL update is less than the cache duration. + if err := a.GenerateCertificateRevocationList(); err != nil { return errors.Wrap(err, "could not generate a CRL") } - a.crlTicker = time.NewTicker(tickerDuration) + a.crlStopper = make(chan struct{}, 1) + a.crlTicker = time.NewTicker(a.config.CRL.TickerDuration()) go func() { for { - <-a.crlTicker.C - log.Println("Regenerating CRL") - err := a.GenerateCertificateRevocationList() - if err != nil { - log.Printf("ERROR: authority.crlGenerator encountered an error when regenerating the CRL: %v", err) + select { + case <-a.crlTicker.C: + log.Println("Regenerating CRL") + if err := a.GenerateCertificateRevocationList(); err != nil { + log.Printf("error regenerating the CRL: %v", err) + } + case <-a.crlStopper: + return } - } }() return nil } - -func (a *Authority) resetCRLGeneratorTimer() { - if a.crlTicker != nil { - tickerDuration := (a.config.CRL.CacheDuration.Duration / 3) * 2 - if v := a.config.CRL.RenewPeriod; v != nil && v.Duration > 0 { - tickerDuration = v.Duration - } - - a.crlTicker.Reset(tickerDuration) - } -} diff --git a/authority/authority_test.go b/authority/authority_test.go index 48479d5b..9f35f23e 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -80,12 +80,6 @@ func testAuthority(t *testing.T, opts ...Option) *Authority { AuthorityConfig: &AuthConfig{ Provisioners: p, }, - CRL: &config.CRLConfig{ - Enabled: true, - CacheDuration: nil, - GenerateOnRevoke: true, - RenewPeriod: nil, - }, } a, err := New(c, opts...) assert.FatalError(t, err) diff --git a/authority/config/config.go b/authority/config/config.go index ad261cdb..79ac3e88 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -37,8 +37,11 @@ var ( DefaultEnableSSHCA = false // DefaultCRLCacheDuration is the default cache duration for the CRL. DefaultCRLCacheDuration = &provisioner.Duration{Duration: 24 * time.Hour} - // GlobalProvisionerClaims default claims for the Authority. Can be overridden - // by provisioner specific claims. + // DefaultCRLExpiredDuration is the default duration in which expired + // certificates will remain in the CRL after expiration. + DefaultCRLExpiredDuration = time.Hour + // GlobalProvisionerClaims is the default duration that expired certificates + // remain in the CRL after expiration. GlobalProvisionerClaims = provisioner.Claims{ MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, @@ -78,6 +81,55 @@ type Config struct { SkipValidation bool `json:"-"` } +// CRLConfig represents config options for CRL generation +type CRLConfig struct { + Enabled bool `json:"enabled"` + GenerateOnRevoke bool `json:"generateOnRevoke,omitempty"` + CacheDuration *provisioner.Duration `json:"cacheDuration,omitempty"` + RenewPeriod *provisioner.Duration `json:"renewPeriod,omitempty"` +} + +// IsEnabled returns if the CRL is enabled. +func (c *CRLConfig) IsEnabled() bool { + return c != nil && c.Enabled +} + +// Validate validates the CRL configuration. +func (c *CRLConfig) Validate() error { + if c == nil { + return nil + } + + if c.CacheDuration != nil && c.CacheDuration.Duration < 0 { + return errors.New("crl.cacheDuration must be greater than or equal to 0") + } + + if c.RenewPeriod != nil && c.RenewPeriod.Duration < 0 { + return errors.New("crl.renewPeriod must be greater than or equal to 0") + } + + if c.RenewPeriod != nil && c.CacheDuration != nil && + c.RenewPeriod.Duration > c.CacheDuration.Duration { + return errors.New("crl.cacheDuration must be greater than or equal to crl.renewPeriod") + } + + return nil +} + +// TickerDuration the renewal ticker duration. This is set by renewPeriod, of it +// is not set is ~2/3 of cacheDuration. +func (c *CRLConfig) TickerDuration() time.Duration { + if !c.IsEnabled() { + return 0 + } + + if c.RenewPeriod != nil && c.RenewPeriod.Duration > 0 { + return c.RenewPeriod.Duration + } + + return (c.CacheDuration.Duration / 3) * 2 +} + // ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer // x509 Certificate blocks. type ASN1DN struct { @@ -109,14 +161,6 @@ type AuthConfig struct { DisableGetSSHHosts bool `json:"disableGetSSHHosts,omitempty"` } -// CRLConfig represents config options for CRL generation -type CRLConfig struct { - Enabled bool `json:"enabled"` - CacheDuration *provisioner.Duration `json:"cacheDuration,omitempty"` - GenerateOnRevoke bool `json:"generateOnRevoke,omitempty"` - RenewPeriod *provisioner.Duration `json:"renewPeriod,omitempty"` -} - // init initializes the required fields in the AuthConfig if they are not // provided. func (c *AuthConfig) init() { @@ -194,7 +238,7 @@ func (c *Config) Init() { if c.CommonName == "" { c.CommonName = "Step Online CA" } - if c.CRL != nil && c.CRL.CacheDuration == nil { + if c.CRL != nil && c.CRL.Enabled && c.CRL.CacheDuration == nil { c.CRL.CacheDuration = DefaultCRLCacheDuration } c.AuthorityConfig.init() @@ -283,6 +327,11 @@ func (c *Config) Validate() error { return err } + // Validate crl config: nil is ok + if err := c.CRL.Validate(); err != nil { + return err + } + return c.AuthorityConfig.Validate(c.GetAudiences()) } diff --git a/authority/tls.go b/authority/tls.go index 051e09c9..84106002 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -30,6 +30,7 @@ import ( casapi "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" + "github.com/smallstep/nosql/database" ) // GetTLSOptions returns the tls options configured. @@ -37,8 +38,11 @@ func (a *Authority) GetTLSOptions() *config.TLSOptions { return a.config.TLS } -var oidAuthorityKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 35} -var oidSubjectKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 14} +var ( + oidAuthorityKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 35} + oidSubjectKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 14} + oidExtensionIssuingDistributionPoint = asn1.ObjectIdentifier{2, 5, 29, 28} +) func withDefaultASN1DN(def *config.ASN1DN) provisioner.CertificateModifierFunc { return func(crt *x509.Certificate, opts provisioner.SignOptions) error { @@ -488,19 +492,15 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error RevokedAt: time.Now().UTC(), } - var ( - p provisioner.Interface - err error - ) - - if revokeOpts.Crt == nil { - // Attempt to get the certificate expiry using the serial number. - cert, err := a.db.GetCertificate(revokeOpts.Serial) - - // Revocation of a certificate not in the database may be requested, so fill in the expiry only - // if we can - if err == nil { - rci.ExpiresAt = cert.NotAfter + // For X509 CRLs attempt to get the expiration date of the certificate. + if provisioner.MethodFromContext(ctx) == provisioner.RevokeMethod { + if revokeOpts.Crt == nil { + cert, err := a.db.GetCertificate(revokeOpts.Serial) + if err == nil { + rci.ExpiresAt = cert.NotAfter + } + } else { + rci.ExpiresAt = revokeOpts.Crt.NotAfter } } @@ -508,8 +508,7 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error if !(revokeOpts.MTLS || revokeOpts.ACME) { token, err := jose.ParseSigned(revokeOpts.OTT) if err != nil { - return errs.Wrap(http.StatusUnauthorized, err, - "authority.Revoke; error parsing token", opts...) + return errs.Wrap(http.StatusUnauthorized, err, "authority.Revoke; error parsing token", opts...) } // Get claims w/out verification. @@ -519,28 +518,43 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error } // This method will also validate the audiences for JWK provisioners. - p, err = a.LoadProvisionerByToken(token, &claims.Claims) + p, err := a.LoadProvisionerByToken(token, &claims.Claims) if err != nil { return err } rci.ProvisionerID = p.GetID() rci.TokenID, err = p.GetTokenID(revokeOpts.OTT) if err != nil && !errors.Is(err, provisioner.ErrAllowTokenReuse) { - return errs.Wrap(http.StatusInternalServerError, err, - "authority.Revoke; could not get ID for token") + return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke; could not get ID for token") } opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID), errs.WithKeyVal("tokenID", rci.TokenID), ) - } else if p, err = a.LoadProvisionerByCertificate(revokeOpts.Crt); err == nil { + } else if p, err := a.LoadProvisionerByCertificate(revokeOpts.Crt); err == nil { // Load the Certificate provisioner if one exists. rci.ProvisionerID = p.GetID() opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID)) } + failRevoke := func(err error) error { + switch { + case errors.Is(err, db.ErrNotImplemented): + return errs.NotImplemented("authority.Revoke; no persistence layer configured", opts...) + case errors.Is(err, db.ErrAlreadyExists): + return errs.ApplyOptions( + errs.BadRequest("certificate with serial number '%s' is already revoked", rci.Serial), + opts..., + ) + default: + return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...) + } + } + if provisioner.MethodFromContext(ctx) == provisioner.SSHRevokeMethod { - err = a.revokeSSH(nil, rci) + if err := a.revokeSSH(nil, rci); err != nil { + return failRevoke(err) + } } else { // Revoke an X.509 certificate using CAS. If the certificate is not // provided we will try to read it from the db. If the read fails we @@ -555,7 +569,7 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error // CAS operation, note that SoftCAS (default) is a noop. // The revoke happens when this is stored in the db. - _, err = a.x509CAService.RevokeCertificate(&casapi.RevokeCertificateRequest{ + _, err := a.x509CAService.RevokeCertificate(&casapi.RevokeCertificateRequest{ Certificate: revokedCert, SerialNumber: rci.Serial, Reason: rci.Reason, @@ -567,37 +581,20 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error } // Save as revoked in the Db. - err = a.revoke(revokedCert, rci) - if err != nil { - return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...) + if err := a.revoke(revokedCert, rci); err != nil { + return failRevoke(err) } - if a.config.CRL != nil && a.config.CRL.GenerateOnRevoke { - // Generate a new CRL so CRL requesters will always get an up-to-date CRL whenever they request it - err = a.GenerateCertificateRevocationList() - if err != nil { + // Generate a new CRL so CRL requesters will always get an up-to-date + // CRL whenever they request it. + if a.config.CRL.IsEnabled() && a.config.CRL.GenerateOnRevoke { + if err := a.GenerateCertificateRevocationList(); err != nil { return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...) } - - // the timer only gets reset if CRL is enabled - if a.config.CRL.Enabled { - a.resetCRLGeneratorTimer() - } } } - switch { - case err == nil: - return nil - case errors.Is(err, db.ErrNotImplemented): - return errs.NotImplemented("authority.Revoke; no persistence layer configured", opts...) - case errors.Is(err, db.ErrAlreadyExists): - return errs.ApplyOptions( - errs.BadRequest("certificate with serial number '%s' is already revoked", rci.Serial), - opts..., - ) - default: - return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...) - } + + return nil } func (a *Authority) revoke(crt *x509.Certificate, rci *db.RevokedCertificateInfo) error { @@ -621,7 +618,7 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn // GetCertificateRevocationList will return the currently generated CRL from the DB, or a not implemented // error if the underlying AuthDB does not support CRLs func (a *Authority) GetCertificateRevocationList() ([]byte, error) { - if a.config.CRL == nil { + if !a.config.CRL.IsEnabled() { return nil, errs.Wrap(http.StatusNotFound, errors.Errorf("Certificate Revocation Lists are not enabled"), "authority.GetCertificateRevocationList") } @@ -633,11 +630,6 @@ func (a *Authority) GetCertificateRevocationList() ([]byte, error) { crlInfo, err := crlDB.GetCRL() if err != nil { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetCertificateRevocationList") - - } - - if crlInfo == nil { - return nil, nil } return crlInfo.DER, nil @@ -646,9 +638,7 @@ func (a *Authority) GetCertificateRevocationList() ([]byte, error) { // GenerateCertificateRevocationList generates a DER representation of a signed CRL and stores it in the // database. Returns nil if CRL generation has been disabled in the config func (a *Authority) GenerateCertificateRevocationList() error { - - if a.config.CRL == nil { - // CRL is disabled + if !a.config.CRL.IsEnabled() { return nil } @@ -663,34 +653,40 @@ func (a *Authority) GenerateCertificateRevocationList() error { return errors.Errorf("CA does not support CRL Generation") } - a.crlMutex.Lock() // use a mutex to ensure only one CRL is generated at a time to avoid concurrency issues + // use a mutex to ensure only one CRL is generated at a time to avoid + // concurrency issues + a.crlMutex.Lock() defer a.crlMutex.Unlock() crlInfo, err := crlDB.GetCRL() - if err != nil { + if err != nil && !database.IsErrNotFound(err) { return errors.Wrap(err, "could not retrieve CRL from database") } + now := time.Now().UTC() revokedList, err := crlDB.GetRevokedCertificates() if err != nil { return errors.Wrap(err, "could not retrieve revoked certificates list from database") } - // Number is a monotonically increasing integer (essentially the CRL version number) that we need to - // keep track of and increase every time we generate a new CRL - var n int64 + // Number is a monotonically increasing integer (essentially the CRL version + // number) that we need to keep track of and increase every time we generate + // a new CRL var bn big.Int - if crlInfo != nil { - n = crlInfo.Number + 1 + bn.SetInt64(crlInfo.Number + 1) } - bn.SetInt64(n) - // Convert our database db.RevokedCertificateInfo types into the pkix representation ready for the - // CAS to sign it + // Convert our database db.RevokedCertificateInfo types into the pkix + // representation ready for the CAS to sign it var revokedCertificates []pkix.RevokedCertificate - + skipExpiredTime := now.Add(-config.DefaultCRLExpiredDuration.Abs()) for _, revokedCert := range *revokedList { + // skip expired certificates + if !revokedCert.ExpiresAt.IsZero() && revokedCert.ExpiresAt.Before(skipExpiredTime) { + continue + } + var sn big.Int sn.SetString(revokedCert.Serial, 10) revokedCertificates = append(revokedCertificates, pkix.RevokedCertificate{ @@ -713,9 +709,18 @@ func (a *Authority) GenerateCertificateRevocationList() error { SignatureAlgorithm: 0, RevokedCertificates: revokedCertificates, Number: &bn, - ThisUpdate: time.Now().UTC(), - NextUpdate: time.Now().UTC().Add(updateDuration), - ExtraExtensions: nil, + ThisUpdate: now, + NextUpdate: now.Add(updateDuration), + } + + // Add distribution point. + // + // Note that this is currently using the port 443 by default. + fullName := a.config.Audience("/1.0/crl")[0] + if b, err := marshalDistributionPoint(fullName, false); err == nil { + revocationList.ExtraExtensions = []pkix.Extension{ + {Id: oidExtensionIssuingDistributionPoint, Value: b}, + } } certificateRevocationList, err := caCRLGenerator.CreateCRL(&casapi.CreateCRLRequest{RevocationList: &revocationList}) @@ -726,7 +731,7 @@ func (a *Authority) GenerateCertificateRevocationList() error { // Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the // expiry time, duration, and the DER-encoded CRL newCRLInfo := db.CertificateRevocationListInfo{ - Number: n, + Number: bn.Int64(), ExpiresAt: revocationList.NextUpdate, DER: certificateRevocationList.CRL, Duration: updateDuration, @@ -834,6 +839,33 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { return &tlsCrt, nil } +// RFC 5280, 5.2.5 +type distributionPoint struct { + DistributionPoint distributionPointName `asn1:"optional,tag:0"` + OnlyContainsUserCerts bool `asn1:"optional,tag:1"` + OnlyContainsCACerts bool `asn1:"optional,tag:2"` + OnlySomeReasons asn1.BitString `asn1:"optional,tag:3"` + IndirectCRL bool `asn1:"optional,tag:4"` + OnlyContainsAttributeCerts bool `asn1:"optional,tag:5"` +} + +type distributionPointName struct { + FullName []asn1.RawValue `asn1:"optional,tag:0"` + RelativeName pkix.RDNSequence `asn1:"optional,tag:1"` +} + +func marshalDistributionPoint(fullName string, isCA bool) ([]byte, error) { + return asn1.Marshal(distributionPoint{ + DistributionPoint: distributionPointName{ + FullName: []asn1.RawValue{ + {Class: 2, Tag: 6, Bytes: []byte(fullName)}, + }, + }, + OnlyContainsUserCerts: !isCA, + OnlyContainsCACerts: isCA, + }) +} + // templatingError tries to extract more information about the cause of // an error related to (most probably) malformed template data and adds // this to the error message. diff --git a/authority/tls_test.go b/authority/tls_test.go index 6223bb3d..1d542cb9 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -28,11 +28,13 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/certificates/api/render" + "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/cas/softcas" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" + "github.com/smallstep/nosql/database" ) var ( @@ -1364,7 +1366,7 @@ func TestAuthority_Revoke(t *testing.T) { return true, nil }, MGetCertificate: func(sn string) (*x509.Certificate, error) { - return nil, nil + return nil, errors.New("not found") }, Err: errors.New("force"), })) @@ -1404,7 +1406,7 @@ func TestAuthority_Revoke(t *testing.T) { return true, nil }, MGetCertificate: func(sn string) (*x509.Certificate, error) { - return nil, nil + return nil, errors.New("not found") }, Err: db.ErrAlreadyExists, })) @@ -1698,11 +1700,10 @@ func TestAuthority_CRL(t *testing.T) { ctx context.Context expected []string err error - code int } tests := map[string]func() test{ - "ok/empty-crl": func() test { - _a := testAuthority(t, WithDatabase(&db.MockAuthDB{ + "fail/empty-crl": func() test { + a := testAuthority(t, WithDatabase(&db.MockAuthDB{ MUseToken: func(id, tok string) (bool, error) { return true, nil }, @@ -1714,7 +1715,7 @@ func TestAuthority_CRL(t *testing.T) { return nil }, MGetCRL: func() (*db.CertificateRevocationListInfo, error) { - return &crlStore, nil + return nil, database.ErrNotFound }, MGetRevokedCertificates: func() (*[]db.RevokedCertificateInfo, error) { return &revokedList, nil @@ -1724,15 +1725,19 @@ func TestAuthority_CRL(t *testing.T) { return nil }, })) + a.config.CRL = &config.CRLConfig{ + Enabled: true, + } return test{ - auth: _a, + auth: a, ctx: crlCtx, expected: nil, + err: database.ErrNotFound, } }, "ok/crl-full": func() test { - _a := testAuthority(t, WithDatabase(&db.MockAuthDB{ + a := testAuthority(t, WithDatabase(&db.MockAuthDB{ MUseToken: func(id, tok string) (bool, error) { return true, nil }, @@ -1754,6 +1759,10 @@ func TestAuthority_CRL(t *testing.T) { return nil }, })) + a.config.CRL = &config.CRLConfig{ + Enabled: true, + GenerateOnRevoke: true, + } var ex []string @@ -1770,7 +1779,7 @@ func TestAuthority_CRL(t *testing.T) { } raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) - err = _a.Revoke(crlCtx, &RevokeOptions{ + err = a.Revoke(crlCtx, &RevokeOptions{ Serial: sn, ReasonCode: reasonCode, Reason: reason, @@ -1783,7 +1792,7 @@ func TestAuthority_CRL(t *testing.T) { } return test{ - auth: _a, + auth: a, ctx: crlCtx, expected: ex, } @@ -1792,10 +1801,11 @@ func TestAuthority_CRL(t *testing.T) { for name, f := range tests { tc := f() t.Run(name, func(t *testing.T) { - if crlBytes, _err := tc.auth.GetCertificateRevocationList(); _err == nil { + if crlBytes, err := tc.auth.GetCertificateRevocationList(); err == nil { crl, parseErr := x509.ParseCRL(crlBytes) if parseErr != nil { t.Errorf("x509.ParseCertificateRequest() error = %v, wantErr %v", parseErr, nil) + return } var cmpList []string @@ -1804,9 +1814,8 @@ func TestAuthority_CRL(t *testing.T) { } assert.Equals(t, cmpList, tc.expected) - } else { - assert.NotNil(t, tc.err) + assert.NotNil(t, tc.err, err.Error()) } }) } diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index af93420d..6eae9e9e 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -135,7 +135,6 @@ func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1 // CreateCRL will create a new CRL based on the RevocationList passed to it func (c *SoftCAS) CreateCRL(req *apiv1.CreateCRLRequest) (*apiv1.CreateCRLResponse, error) { - certChain, signer, err := c.getCertSigner() if err != nil { return nil, err diff --git a/db/db.go b/db/db.go index 4db437fe..bf1b2f48 100644 --- a/db/db.go +++ b/db/db.go @@ -271,14 +271,12 @@ func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { revokedCerts = append(revokedCerts, data) } } - } return &revokedCerts, nil } // StoreCRL stores a CRL in the DB func (db *DB) StoreCRL(crlInfo *CertificateRevocationListInfo) error { - crlInfoBytes, err := json.Marshal(crlInfo) if err != nil { return errors.Wrap(err, "json Marshal error") @@ -293,11 +291,6 @@ func (db *DB) StoreCRL(crlInfo *CertificateRevocationListInfo) error { // GetCRL gets the existing CRL from the database func (db *DB) GetCRL() (*CertificateRevocationListInfo, error) { crlInfoBytes, err := db.Get(crlTable, crlKey) - - if database.IsErrNotFound(err) { - return nil, nil - } - if err != nil { return nil, errors.Wrap(err, "database Get error") } diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 328fba69..0465f0b7 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -119,11 +119,6 @@ starting the CA. * `address`: e.g. `127.0.0.1:8080` - address and port on which the CA will bind and respond to requests. -* `crl`: Certificate Revocation List settings: - - generate: Enable/Disable CRL generation (`true` to generate, `false` to disable) - - - cacheDuration: Time between CRL regeneration task. E.g if set to `5m`, step-ca will regenerate the CRL every 5 minutes. - * `dnsNames`: comma separated list of DNS Name(s) for the CA. * `logger`: the default logging format for the CA is `text`. The other option @@ -131,17 +126,20 @@ is `json`. * `db`: data persistence layer. See [database documentation](./database.md) for more info. + * `type`: `badger`, `bbolt`, `mysql`, etc. + * `dataSource`: string that can be interpreted differently depending on the + type of the database. Usually a path to where the data is stored. See the + [database configuration docs](./database.md#configuration) for more info. + * `database`: name of the database. Used for backends that may have multiple + databases. e.g. MySQL + * `valueDir`: directory to store the value log in (Badger specific). - - type: `badger`, `bbolt`, `mysql`, etc. - - - dataSource: `string` that can be interpreted differently depending on the - type of the database. Usually a path to where the data is stored. See - the [database configuration docs](./database.md#configuration) for more info. - - - database: name of the database. Used for backends that may have - multiple databases. e.g. MySQL - - - valueDir: directory to store the value log in (Badger specific). +* `crl`: Certificate Revocation List settings: + * `enable`: enables CRL generation (`true` to generate, `false` to disable) + * `generateOnRevoke`: a revoke will generate a new CRL if the crl is enabled. + * `cacheDuration`: the duration until next update of the CRL, defaults to 24h. + * `renewPeriod`: the time between CRL regeneration. If not set ~2/3 of the + cacheDuration will be used. * `tls`: settings for negotiating communication with the CA; includes acceptable ciphersuites, min/max TLS version, etc. From 812fee76308c716da57e00d12534fa17d8a9aef2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 27 Oct 2022 11:38:30 -0700 Subject: [PATCH 50/66] Start crl generator before setting initOnce --- authority/authority.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index deebdaa9..3fb6f51f 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -716,12 +716,6 @@ func (a *Authority) init() error { a.templates.Data["Step"] = tmplVars } - // JWT numeric dates are seconds. - a.startTime = time.Now().Truncate(time.Second) - // Set flag indicating that initialization has been completed, and should - // not be repeated. - a.initOnce = true - // Start the CRL generator, we can assume the configuration is validated. if a.config.CRL.IsEnabled() { // Default cache duration to the default one @@ -734,6 +728,12 @@ func (a *Authority) init() error { } } + // JWT numeric dates are seconds. + a.startTime = time.Now().Truncate(time.Second) + // Set flag indicating that initialization has been completed, and should + // not be repeated. + a.initOnce = true + return nil } From 6d4fd7d016e5e8afaa17c36d961d22d06bf6c805 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 27 Oct 2022 11:39:44 -0700 Subject: [PATCH 51/66] Update changelog with CRL support --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59829021..cb552de6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. X.509 certificates. - Added provisioner webhooks for augmenting template data and authorizing certificate requests before signing. - Added automatic migration of provisioners when enabling remote managment. +- Added experimental support for CRLs. ### Fixed - MySQL DSN parsing issues fixed with upgrade to [smallstep/nosql@v0.5.0](https://github.com/smallstep/nosql/releases/tag/v0.5.0). From 51c7f560307e332bbe5e5fc87178d61d4ec0fbf2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 27 Oct 2022 11:57:48 -0700 Subject: [PATCH 52/66] Truncate time to the second --- authority/tls.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authority/tls.go b/authority/tls.go index 184944a0..e80c9091 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -30,8 +30,8 @@ import ( casapi "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" - "github.com/smallstep/nosql/database" "github.com/smallstep/certificates/webhook" + "github.com/smallstep/nosql/database" ) // GetTLSOptions returns the tls options configured. @@ -689,7 +689,7 @@ func (a *Authority) GenerateCertificateRevocationList() error { return errors.Wrap(err, "could not retrieve CRL from database") } - now := time.Now().UTC() + now := time.Now().Truncate(time.Second).UTC() revokedList, err := crlDB.GetRevokedCertificates() if err != nil { return errors.Wrap(err, "could not retrieve revoked certificates list from database") From f066ac3d401339399871084721ea7b3ca6b1d068 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 27 Oct 2022 11:58:01 -0700 Subject: [PATCH 53/66] Remove buggy logic on GetRevokedCertificates() --- db/db.go | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/db/db.go b/db/db.go index bf1b2f48..784c75f4 100644 --- a/db/db.go +++ b/db/db.go @@ -248,29 +248,12 @@ func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { return nil, err } var revokedCerts []RevokedCertificateInfo - now := time.Now().Truncate(time.Second) - for _, e := range entries { var data RevokedCertificateInfo if err := json.Unmarshal(e.Value, &data); err != nil { return nil, err } - - if !data.RevokedAt.IsZero() { - revokedCerts = append(revokedCerts, data) - } else if data.RevokedAt.IsZero() { - cert, err := db.GetCertificate(data.Serial) - if err != nil { - revokedCerts = append(revokedCerts, data) // a revoked certificate may not be in the database, - // so its expiry date is undiscoverable and will need - // to be added to the crl always - continue - } - - if cert.NotAfter.After(now) { - revokedCerts = append(revokedCerts, data) - } - } + revokedCerts = append(revokedCerts, data) } return &revokedCerts, nil } From 89c8c6d0a0d8a8d062d6b0078ba34151c8d1cb15 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 27 Oct 2022 12:06:38 -0700 Subject: [PATCH 54/66] Fix package name in tls test --- authority/tls_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/authority/tls_test.go b/authority/tls_test.go index 37066843..918adbdc 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -1903,15 +1903,15 @@ func TestAuthority_CRL(t *testing.T) { for i := 0; i < 100; i++ { sn := fmt.Sprintf("%v", i) - cl := jwt.Claims{ + cl := jose.Claims{ Subject: fmt.Sprintf("sn-%v", i), Issuer: validIssuer, - NotBefore: jwt.NewNumericDate(now), - Expiry: jwt.NewNumericDate(now.Add(time.Minute)), + NotBefore: jose.NewNumericDate(now), + Expiry: jose.NewNumericDate(now.Add(time.Minute)), Audience: validAudience, ID: sn, } - raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize() + raw, err := jose.Signed(sig).Claims(cl).CompactSerialize() assert.FatalError(t, err) err = a.Revoke(crlCtx, &RevokeOptions{ Serial: sn, From 2d582e5694030434b08d6ce519f7f2a5ec322d5c Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 27 Oct 2022 12:20:13 -0700 Subject: [PATCH 55/66] Remove use of time.Duration.Abs time.Duration.Abs() was added in Go 1.19 --- authority/tls.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authority/tls.go b/authority/tls.go index e80c9091..b5d85074 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -706,7 +706,7 @@ func (a *Authority) GenerateCertificateRevocationList() error { // Convert our database db.RevokedCertificateInfo types into the pkix // representation ready for the CAS to sign it var revokedCertificates []pkix.RevokedCertificate - skipExpiredTime := now.Add(-config.DefaultCRLExpiredDuration.Abs()) + skipExpiredTime := now.Add(-config.DefaultCRLExpiredDuration) for _, revokedCert := range *revokedList { // skip expired certificates if !revokedCert.ExpiresAt.IsZero() && revokedCert.ExpiresAt.Before(skipExpiredTime) { From c36b36f0708740ae6c2cbb46b70b051d558f9029 Mon Sep 17 00:00:00 2001 From: max furman Date: Wed, 26 Oct 2022 23:31:02 -0700 Subject: [PATCH 56/66] [action] cosign over docker image digest --- .github/workflows/release.yml | 47 +++++++++--------- Makefile | 10 ---- make/docker.mk | 91 ----------------------------------- 3 files changed, 23 insertions(+), 125 deletions(-) delete mode 100644 make/docker.mk diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 48bbf730..f66ad67b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,8 +15,12 @@ jobs: name: Create Release needs: ci runs-on: ubuntu-latest + env: + DOCKER_IMAGE: smallstep/step-ca outputs: + version: ${{ steps.extract-tag.outputs.VERSION }} is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }} + docker_tags: ${{ env.DOCKER_TAGS }} steps: - name: Is Pre-release id: is_prerelease @@ -26,6 +30,16 @@ jobs: OUT=$? if [ $OUT -eq 0 ]; then IS_PRERELEASE=true; else IS_PRERELEASE=false; fi echo "IS_PRERELEASE=${IS_PRERELEASE}" >> ${GITHUB_OUTPUT} + - name: Extract Tag Names + id: extract-tag + run: | + VERSION=${GITHUB_REF#refs/tags/v} + echo "VERSION=${VERSION}" >> ${GITHUB_OUTPUT} + echo "DOCKER_TAGS=${{ env.DOCKER_IMAGE }}:${VERSION}" >> ${GITHUB_ENV} + - name: Add Latest Tag + if: steps.is_prerelease.outputs.IS_PRERELEASE == 'false' + run: | + echo "DOCKER_TAGS=${{ env.DOCKER_TAGS }},${{ env.DOCKER_IMAGE }}:latest" >> ${GITHUB_ENV} - name: Create Release id: create_release uses: actions/create-release@v1 @@ -68,34 +82,19 @@ jobs: args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GORELEASER_PAT }} - RELEASE_DATE: ${RELEASE_DATE} + RELEASE_DATE: ${{ env.RELEASE_DATE }} COSIGN_EXPERIMENTAL: 1 build_upload_docker: name: Build & Upload Docker Images - runs-on: ubuntu-latest - needs: ci + needs: create_release permissions: id-token: write contents: write - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Go - uses: actions/setup-go@v3 - with: - go-version: '1.19' - check-latest: true - - name: Install cosign - uses: sigstore/cosign-installer@v2 - with: - cosign-release: 'v1.13.1' - - name: Build - id: build - run: | - PATH=$PATH:/usr/local/go/bin:/home/admin/go/bin - make docker-artifacts - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - COSIGN_EXPERIMENTAL: 1 + uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main + with: + platforms: linux/amd64,linux/386,linux/arm,linux/arm64 + tags: ${{ needs.create_release.outputs.docker_tags }} + docker_image: smallstep/step-ca + docker_file: docker/Dockerfile.step-ca + secrets: inherit diff --git a/Makefile b/Makefile index 55b97c62..90e96993 100644 --- a/Makefile +++ b/Makefile @@ -79,8 +79,6 @@ $(info DEB_VERSION is $(DEB_VERSION)) $(info PUSHTYPE is $(PUSHTYPE)) endif -include make/docker.mk - ######################################### # Build ######################################### @@ -232,11 +230,3 @@ debian: changelog distclean: clean .PHONY: changelog debian distclean - -################################################# -# Targets for creating step artifacts -################################################# - -docker-artifacts: docker-$(PUSHTYPE) - -.PHONY: docker-artifacts diff --git a/make/docker.mk b/make/docker.mk deleted file mode 100644 index 0d56e663..00000000 --- a/make/docker.mk +++ /dev/null @@ -1,91 +0,0 @@ -######################################### -# Building Docker Image -# -# This uses a multi-stage build file. The first stage is a builder (that might -# be large in size). After the build has succeeded, the statically linked -# binary is copied to a new image that is optimized for size. -######################################### - -ifeq (, $(shell which docker)) - DOCKER_CLIENT_OS := linux -else - DOCKER_CLIENT_OS := $(strip $(shell docker version -f '{{.Client.Os}}' 2>/dev/null)) -endif - -DOCKER_PLATFORMS = linux/amd64,linux/386,linux/arm,linux/arm64 -DOCKER_IMAGE_NAME = smallstep/step-ca - -docker-prepare: - # Ensure, we can build for ARM architecture -ifeq (linux,$(DOCKER_CLIENT_OS)) - [ -f /proc/sys/fs/binfmt_misc/qemu-arm ] || docker run --rm --privileged linuxkit/binfmt:v0.8-amd64 -endif - - # Register buildx builder - mkdir -p $$HOME/.docker/cli-plugins - - test -f $$HOME/.docker/cli-plugins/docker-buildx || \ - (wget -q -O $$HOME/.docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/v0.4.1/buildx-v0.4.1.$(DOCKER_CLIENT_OS)-amd64 && \ - chmod +x $$HOME/.docker/cli-plugins/docker-buildx) - - docker buildx create --use --name mybuilder --platform="$(DOCKER_PLATFORMS)" || true - -.PHONY: docker-prepare - -################################################# -# Releasing Docker Images -# -# Using the docker build infrastructure, this section is responsible for -# logging into docker hub. -################################################# - -# Rely on DOCKER_USERNAME and DOCKER_PASSWORD being set inside the CI or -# equivalent environment -docker-login: - $Q docker login -u="$(DOCKER_USERNAME)" -p="$(DOCKER_PASSWORD)" - -.PHONY: docker-login - -################################################# -# Targets for different type of builds -################################################# - -define DOCKER_BUILDX - # $(1) -- Image Tag - # $(2) -- Push (empty is no push | --push will push to dockerhub) - docker buildx build . --progress plain -t $(DOCKER_IMAGE_NAME):$(1) -f docker/Dockerfile.step-ca --platform="$(DOCKER_PLATFORMS)" $(2) - cosign sign -r $(DOCKER_IMAGE_NAME):$(1) - -endef - -# For non-master builds don't build the docker containers. -docker-branch: - -# For master builds don't build the docker containers. -docker-master: - -# For all builds with a release candidate tag build and push the containers. -docker-release-candidate: docker-prepare docker-login - $(call DOCKER_BUILDX,$(VERSION),--push) - -# For all builds with a release tag build and push the containers. -docker-release: docker-prepare docker-login - $(call DOCKER_BUILDX,latest,--push) - $(call DOCKER_BUILDX,$(VERSION),--push) - -.PHONY: docker-branch docker-master docker-release-candidate docker-release - -# XXX We put the output for the build in 'output' so we don't mess with how we -# do rule overriding from the base Makefile (if you name it 'build' it messes up -# the wildcarding). -DOCKER_OUTPUT=$(OUTPUT_ROOT)docker/ - -DOCKER_MAKE=V=$V GOOS_OVERRIDE='GOOS=linux GOARCH=amd64' PREFIX=$(1) make $(1)bin/$(BINNAME) -DOCKER_BUILD=$Q docker build -t $(DOCKER_IMAGE_NAME):latest -f docker/Dockerfile.step-ca --build-arg BINPATH=$(DOCKER_OUTPUT)bin/$(BINNAME) . - -docker-dev: docker/Dockerfile.step-ca - mkdir -p $(DOCKER_OUTPUT) - $(call DOCKER_MAKE,$(DOCKER_OUTPUT),step-ca) - $(call DOCKER_BUILD) - -.PHONY: docker-dev From 4e077f997e47b0adc8199cbe6f6cb05d50dbeda2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 15:28:32 +0000 Subject: [PATCH 57/66] Bump cloud.google.com/go/security from 1.8.0 to 1.9.0 Bumps [cloud.google.com/go/security](https://github.com/googleapis/google-cloud-go) from 1.8.0 to 1.9.0. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/documentai/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/asset/v1.8.0...asset/v1.9.0) --- updated-dependencies: - dependency-name: cloud.google.com/go/security dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 21d1f557..a5315666 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( cloud.google.com/go v0.104.0 - cloud.google.com/go/security v1.8.0 + cloud.google.com/go/security v1.9.0 github.com/Azure/azure-sdk-for-go v65.0.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.28 // indirect github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect diff --git a/go.sum b/go.sum index e50444bb..416fd18f 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,8 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/security v1.8.0 h1:linnRc3/gJYDfKbAtNixVQ52+66DpOx5MmCz0NNxal8= -cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/security v1.9.0 h1:o9frPOtXW2f4zMlw4SYPE42LRz/hhrYVWtAEUkPvyA4= +cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= From 22d2c1c31f32ece0605f3b7fe69fec1f62a6b746 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 15:28:56 +0000 Subject: [PATCH 58/66] Bump google.golang.org/api from 0.100.0 to 0.101.0 Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.100.0 to 0.101.0. - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.100.0...v0.101.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 21d1f557..62bf801f 100644 --- a/go.mod +++ b/go.mod @@ -48,8 +48,8 @@ require ( golang.org/x/net v0.0.0-20221014081412-f15817d10f9b golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect - google.golang.org/api v0.100.0 - google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a + google.golang.org/api v0.101.0 + google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 google.golang.org/grpc v1.50.1 google.golang.org/protobuf v1.28.1 gopkg.in/square/go-jose.v2 v2.6.0 @@ -142,7 +142,7 @@ require ( go.opencensus.io v0.23.0 // indirect go.uber.org/atomic v1.9.0 // indirect golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect - golang.org/x/text v0.3.8 // indirect + golang.org/x/text v0.4.0 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index e50444bb..d1237185 100644 --- a/go.sum +++ b/go.sum @@ -1027,8 +1027,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1137,8 +1137,8 @@ google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3h google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.100.0 h1:LGUYIrbW9pzYQQ8NWXlaIVkgnfubVBZbMFb9P8TK374= -google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.101.0 h1:lJPPeEBIRxGpGLwnBTam1NPEM8Z2BmmXEd3z812pjwM= +google.golang.org/api v0.101.0/go.mod h1:CjxAAWWt3A3VrUE2IGDY2bgK5qhoG/OkyWVlYcP05MY= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1214,8 +1214,8 @@ google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a h1:GH6UPn3ixhWcKDhpnEC55S75cerLPdpp3hrhfKYjZgw= -google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 h1:U1u4KB2kx6KR/aJDjQ97hZ15wQs8ZPvDcGcRynBhkvg= +google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55/go.mod h1:45EK0dUbEZ2NHjCeAd2LXmyjAgGUGrpGROgjhC3ADck= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= From d26414a8647a8704c55ebfbab915870e862f13fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 15:29:30 +0000 Subject: [PATCH 59/66] Bump github.com/hashicorp/vault/api from 1.8.1 to 1.8.2 Bumps [github.com/hashicorp/vault/api](https://github.com/hashicorp/vault) from 1.8.1 to 1.8.2. - [Release notes](https://github.com/hashicorp/vault/releases) - [Changelog](https://github.com/hashicorp/vault/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/vault/compare/v1.8.1...v1.8.2) --- updated-dependencies: - dependency-name: github.com/hashicorp/vault/api dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 21d1f557..675dbcd8 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/google/go-cmp v0.5.9 github.com/google/uuid v1.3.0 github.com/googleapis/gax-go/v2 v2.6.0 - github.com/hashicorp/vault/api v1.8.1 + github.com/hashicorp/vault/api v1.8.2 github.com/hashicorp/vault/api/auth/approle v0.3.0 github.com/hashicorp/vault/api/auth/kubernetes v0.3.0 github.com/jhump/protoreflect v1.9.0 // indirect @@ -95,7 +95,7 @@ require ( github.com/hashicorp/go-hclog v0.16.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.4.3 // indirect + github.com/hashicorp/go-plugin v1.4.5 // indirect github.com/hashicorp/go-retryablehttp v0.6.6 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect diff --git a/go.sum b/go.sum index e50444bb..36ae837f 100644 --- a/go.sum +++ b/go.sum @@ -393,8 +393,9 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= +github.com/hashicorp/go-plugin v1.4.5 h1:oTE/oQR4eghggRg8VY7PAz3dr++VwDNBGCcOfIvHpBo= +github.com/hashicorp/go-plugin v1.4.5/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= @@ -434,8 +435,8 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/vault/api v1.8.0/go.mod h1:uJrw6D3y9Rv7hhmS17JQC50jbPDAZdjZoTtrCCxxs7E= -github.com/hashicorp/vault/api v1.8.1 h1:bMieWIe6dAlqAAPReZO/8zYtXaWUg/21umwqGZpEjCI= -github.com/hashicorp/vault/api v1.8.1/go.mod h1:uJrw6D3y9Rv7hhmS17JQC50jbPDAZdjZoTtrCCxxs7E= +github.com/hashicorp/vault/api v1.8.2 h1:C7OL9YtOtwQbTKI9ogB0A1wffRbCN+rH/LLCHO3d8HM= +github.com/hashicorp/vault/api v1.8.2/go.mod h1:ML8aYzBIhY5m1MD1B2Q0JV89cC85YVH4t5kBaZiyVaE= github.com/hashicorp/vault/api/auth/approle v0.3.0 h1:Ib0oCNXsCq/QZhPYtXPzJEbGS5WR/KoZf8c84QoFdkU= github.com/hashicorp/vault/api/auth/approle v0.3.0/go.mod h1:hm51TbjzUkPO0Y17wkrpwOpvyyMRpXJNueTHiG04t3k= github.com/hashicorp/vault/api/auth/kubernetes v0.3.0 h1:HkaCmTKzcgLa2tjdiAid1rbmyQNmQGHfnmvIIM2WorY= From 917d8dc103e930711f69a16bd84a84551b884496 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 17:29:34 +0000 Subject: [PATCH 60/66] Bump go.step.sm/crypto from 0.21.0 to 0.22.0 Bumps [go.step.sm/crypto](https://github.com/smallstep/crypto) from 0.21.0 to 0.22.0. - [Release notes](https://github.com/smallstep/crypto/releases) - [Commits](https://github.com/smallstep/crypto/compare/v0.21.0...v0.22.0) --- updated-dependencies: - dependency-name: go.step.sm/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index a5315666..13c9c8cd 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,13 @@ go 1.18 require ( cloud.google.com/go v0.104.0 cloud.google.com/go/security v1.9.0 - github.com/Azure/azure-sdk-for-go v65.0.0+incompatible // indirect + github.com/Azure/azure-sdk-for-go v67.0.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.28 // indirect github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Masterminds/sprig/v3 v3.2.2 github.com/ThalesIgnite/crypto11 v1.2.5 // indirect - github.com/aws/aws-sdk-go v1.44.111 // indirect + github.com/aws/aws-sdk-go v1.44.117 // indirect github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/fatih/color v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 @@ -42,7 +42,7 @@ require ( github.com/urfave/cli v1.22.10 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.7.5 - go.step.sm/crypto v0.21.0 + go.step.sm/crypto v0.22.0 go.step.sm/linkedca v0.19.0-rc.3 golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b golang.org/x/net v0.0.0-20221014081412-f15817d10f9b diff --git a/go.sum b/go.sum index 416fd18f..ececd80f 100644 --- a/go.sum +++ b/go.sum @@ -63,8 +63,8 @@ filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= -github.com/Azure/azure-sdk-for-go v65.0.0+incompatible h1:HzKLt3kIwMm4KeJYTdx9EbjRYTySD/t8i1Ee/W5EGXw= -github.com/Azure/azure-sdk-for-go v65.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v67.0.0+incompatible h1:SVBwznSETB0Sipd0uyGJr7khLhJOFRUEUb+0JgkCvDo= +github.com/Azure/azure-sdk-for-go v67.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= @@ -128,8 +128,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.44.111 h1:AcWfOgeedSQ4gQVwcIe6aLxpQNJMloZQyqnr7Dzki+s= -github.com/aws/aws-sdk-go v1.44.111/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.117 h1:mZuODB3Y4soG9QWAXyGb2po+6Easa/enifpj4MnZ91s= +github.com/aws/aws-sdk-go v1.44.117/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -788,8 +788,8 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe go.step.sm/cli-utils v0.7.5 h1:jyp6X8k8mN1B0uWJydTid0C++8tQhm2kaaAdXKQQzdk= go.step.sm/cli-utils v0.7.5/go.mod h1:taSsY8haLmXoXM3ZkywIyRmVij/4Aj0fQbNTlJvv71I= go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= -go.step.sm/crypto v0.21.0 h1:Qwxk5JrqG0Q1t8tOfDM3zKTNECG6J5J24qgWZCRM0Ic= -go.step.sm/crypto v0.21.0/go.mod h1:diT2XWIHQy0397UI3i78qCKeLLLp2wu0/DIJI66u/MU= +go.step.sm/crypto v0.22.0 h1:aloAQsNYEX87e4xL+jqsd6zCA4mGThPBmz2akMKss8Q= +go.step.sm/crypto v0.22.0/go.mod h1:7PTDUApWUTmQbDt4x82cbk3nx5k7JwBo2dFG9CGEp9A= go.step.sm/linkedca v0.19.0-rc.3 h1:3Uu8j187wm7mby+/pz/aQ0wHKRm7w/2AsVPpvcAn4v8= go.step.sm/linkedca v0.19.0-rc.3/go.mod h1:MCZmPIdzElEofZbiw4eyUHayXgFTwa94cNAV34aJ5ew= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= From bd577e75318a41c0b7faef28fd965dc6dc3d9f6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 18:54:46 +0000 Subject: [PATCH 61/66] Bump cloud.google.com/go from 0.104.0 to 0.105.0 Bumps [cloud.google.com/go](https://github.com/googleapis/google-cloud-go) from 0.104.0 to 0.105.0. - [Release notes](https://github.com/googleapis/google-cloud-go/releases) - [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-cloud-go/compare/v0.104.0...v0.105.0) --- updated-dependencies: - dependency-name: cloud.google.com/go dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 6 ++++-- go.sum | 10 ++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 1c313d60..f12f8447 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/smallstep/certificates go 1.18 require ( - cloud.google.com/go v0.104.0 + cloud.google.com/go v0.105.0 // indirect cloud.google.com/go/security v1.9.0 github.com/Azure/azure-sdk-for-go v67.0.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.28 // indirect @@ -49,12 +49,14 @@ require ( golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect google.golang.org/api v0.101.0 - google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 + google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e google.golang.org/grpc v1.50.1 google.golang.org/protobuf v1.28.1 gopkg.in/square/go-jose.v2 v2.6.0 ) +require cloud.google.com/go/longrunning v0.2.1 + require ( cloud.google.com/go/compute v1.10.0 // indirect cloud.google.com/go/iam v0.3.0 // indirect diff --git a/go.sum b/go.sum index 3d075345..8fdb5fae 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Ud cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.104.0 h1:gSmWO7DY1vOm0MVU6DNXM11BWHHsTUmsC5cv1fuW5X8= -cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -47,6 +47,8 @@ cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/kms v1.4.0 h1:iElbfoE61VeLhnZcGOltqL8HIly8Nhbe5t6JlH9GXjo= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= +cloud.google.com/go/longrunning v0.2.1 h1:x3E/YapFCMe2G1D9qCv9COrBldOwK/n0OC7w9PLzeX0= +cloud.google.com/go/longrunning v0.2.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -1215,8 +1217,8 @@ google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 h1:U1u4KB2kx6KR/aJDjQ97hZ15wQs8ZPvDcGcRynBhkvg= -google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55/go.mod h1:45EK0dUbEZ2NHjCeAd2LXmyjAgGUGrpGROgjhC3ADck= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e h1:S9GbmC1iCgvbLyAokVCwiO6tVIrU9Y7c5oMx1V/ki/Y= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= From 4ccc9a0c3216b4422fd86d24d50041319ffa65ae Mon Sep 17 00:00:00 2001 From: max furman Date: Mon, 31 Oct 2022 12:01:18 -0700 Subject: [PATCH 62/66] go.mod syntax --- go.mod | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/go.mod b/go.mod index f12f8447..0b8adae9 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( cloud.google.com/go v0.105.0 // indirect + cloud.google.com/go/longrunning v0.2.1 cloud.google.com/go/security v1.9.0 github.com/Azure/azure-sdk-for-go v67.0.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.28 // indirect @@ -55,8 +56,6 @@ require ( gopkg.in/square/go-jose.v2 v2.6.0 ) -require cloud.google.com/go/longrunning v0.2.1 - require ( cloud.google.com/go/compute v1.10.0 // indirect cloud.google.com/go/iam v0.3.0 // indirect From 3728cee02a5b2954ab279150f3b9f4a19942b796 Mon Sep 17 00:00:00 2001 From: max furman Date: Tue, 1 Nov 2022 18:50:12 -0700 Subject: [PATCH 63/66] [action] Add COSIGN_EXPERIMENTAL env var to cosign release docs --- .goreleaser.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 43ffadb3..9b5398a9 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -154,7 +154,7 @@ release: Below is an example using `cosign` to verify a release artifact: ``` - cosign verify-blob \ + COSIGN_EXPERIMENTAL=1 cosign verify-blob \ --certificate ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig.pem \ --signature ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig \ ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz From e27c6c529b5f72c43841295befec16b0e01b420d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 3 Nov 2022 16:58:25 -0700 Subject: [PATCH 64/66] Add support for custom acme ports This change adds the flags --acme-http-port, --acme-tls-port, that combined with --insecure can be used to set custom ports for ACME http-01 and tls-alpn-01 challenges. These flags should only be used for testing purposes. Fixes #1015 --- acme/challenge.go | 27 +++++++++- acme/challenge_test.go | 115 ++++++++++++++++++++++++++++++++++++++++- commands/app.go | 31 +++++++++++ 3 files changed, 171 insertions(+), 2 deletions(-) diff --git a/acme/challenge.go b/acme/challenge.go index 84b3f83a..1a45a252 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -44,6 +44,18 @@ const ( DEVICEATTEST01 ChallengeType = "device-attest-01" ) +var ( + // InsecurePortHTTP01 is the port used to verify http-01 challenges. If not set it + // defaults to 80. + InsecurePortHTTP01 int + + // InsecurePortTLSALPN01 is the port used to verify tls-alpn-01 challenges. If not + // set it defaults to 443. + // + // This variable can be used for testing purposes. + InsecurePortTLSALPN01 int +) + // Challenge represents an ACME response Challenge type. type Challenge struct { ID string `json:"-"` @@ -93,6 +105,12 @@ func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey) error { u := &url.URL{Scheme: "http", Host: http01ChallengeHost(ch.Value), Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)} + // Append insecure port if set. + // Only used for testing purposes. + if InsecurePortHTTP01 != 0 { + u.Host += ":" + strconv.Itoa(InsecurePortHTTP01) + } + vc := MustClientFromContext(ctx) resp, err := vc.Get(u.String()) if err != nil { @@ -165,7 +183,14 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON InsecureSkipVerify: true, //nolint:gosec // we expect a self-signed challenge certificate } - hostPort := net.JoinHostPort(ch.Value, "443") + var hostPort string + + // Allow to change TLS port for testing purposes. + if port := InsecurePortTLSALPN01; port == 0 { + hostPort = net.JoinHostPort(ch.Value, "443") + } else { + hostPort = net.JoinHostPort(ch.Value, strconv.Itoa(port)) + } vc := MustClientFromContext(ctx) conn, err := vc.TLSDial("tcp", hostPort, config) diff --git a/acme/challenge_test.go b/acme/challenge_test.go index 0027f5b1..1aa9f6ab 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -24,6 +24,7 @@ import ( "net/http" "net/http/httptest" "reflect" + "strconv" "strings" "testing" "time" @@ -370,6 +371,47 @@ func TestChallenge_Validate(t *testing.T) { }, } }, + "ok/http-01-insecure": func(t *testing.T) test { + t.Cleanup(func() { + InsecurePortHTTP01 = 0 + }) + + ch := &Challenge{ + ID: "chID", + Status: StatusPending, + Type: "http-01", + Token: "token", + Value: "zap.internal", + } + + InsecurePortHTTP01 = 8080 + + return test{ + ch: ch, + vc: &mockClient{ + get: func(url string) (*http.Response, error) { + return nil, errors.New("force") + }, + }, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equals(t, updch.ID, ch.ID) + assert.Equals(t, updch.Token, ch.Token) + assert.Equals(t, updch.Type, ch.Type) + assert.Equals(t, updch.Status, ch.Status) + assert.Equals(t, updch.Value, ch.Value) + + err := NewError(ErrorConnectionType, "error doing http GET for url http://zap.internal:8080/.well-known/acme-challenge/%s: force", ch.Token) + assert.HasPrefix(t, updch.Error.Err.Error(), err.Err.Error()) + assert.Equals(t, updch.Error.Type, err.Type) + assert.Equals(t, updch.Error.Detail, err.Detail) + assert.Equals(t, updch.Error.Status, err.Status) + assert.Equals(t, updch.Error.Detail, err.Detail) + return nil + }, + }, + } + }, "fail/dns-01": func(t *testing.T) test { ch := &Challenge{ ID: "chID", @@ -501,6 +543,72 @@ func TestChallenge_Validate(t *testing.T) { srv, tlsDial := newTestTLSALPNServer(cert) srv.Start() + return test{ + ch: ch, + vc: &mockClient{ + tlsDial: tlsDial, + }, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + assert.Equals(t, updch.ID, ch.ID) + assert.Equals(t, updch.Token, ch.Token) + assert.Equals(t, updch.Status, ch.Status) + assert.Equals(t, updch.Type, ch.Type) + assert.Equals(t, updch.Value, ch.Value) + assert.Equals(t, updch.Error, nil) + return nil + }, + }, + srv: srv, + jwk: jwk, + } + }, + "ok/tls-alpn-01-insecure": func(t *testing.T) test { + t.Cleanup(func() { + InsecurePortTLSALPN01 = 0 + }) + + ch := &Challenge{ + ID: "chID", + Token: "token", + Type: "tls-alpn-01", + Status: StatusPending, + Value: "zap.internal", + } + + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + + expKeyAuth, err := KeyAuthorization(ch.Token, jwk) + assert.FatalError(t, err) + expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth)) + + cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.Value) + assert.FatalError(t, err) + + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + if l, err = net.Listen("tcp6", "[::1]:0"); err != nil { + t.Fatalf("failed to listen on a port: %v", err) + } + } + _, port, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + t.Fatalf("failed to split host port: %v", err) + } + + // Use an insecure port + InsecurePortTLSALPN01, err = strconv.Atoi(port) + if err != nil { + t.Fatalf("failed to convert port to int: %v", err) + } + + srv, tlsDial := newTestTLSALPNServer(cert, func(srv *httptest.Server) { + srv.Listener.Close() + srv.Listener = l + }) + srv.Start() + return test{ ch: ch, vc: &mockClient{ @@ -1248,7 +1356,7 @@ func TestDNS01Validate(t *testing.T) { type tlsDialer func(network, addr string, config *tls.Config) (conn *tls.Conn, err error) -func newTestTLSALPNServer(validationCert *tls.Certificate) (*httptest.Server, tlsDialer) { +func newTestTLSALPNServer(validationCert *tls.Certificate, opts ...func(*httptest.Server)) (*httptest.Server, tlsDialer) { srv := httptest.NewUnstartedServer(http.NewServeMux()) srv.Config.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){ @@ -1273,6 +1381,11 @@ func newTestTLSALPNServer(validationCert *tls.Certificate) (*httptest.Server, tl }, } + // Apply options + for _, fn := range opts { + fn(srv) + } + srv.Listener = tls.NewListener(srv.Listener, srv.TLS) //srv.Config.ErrorLog = log.New(ioutil.Discard, "", 0) // hush diff --git a/commands/app.go b/commands/app.go index 7a0b2637..f624843a 100644 --- a/commands/app.go +++ b/commands/app.go @@ -12,6 +12,7 @@ import ( "unicode" "github.com/pkg/errors" + "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/ca" @@ -71,6 +72,19 @@ certificate issuer private key used in the RA mode.`, Usage: "The name of the authority's context.", EnvVar: "STEP_CA_CONTEXT", }, + cli.IntFlag{ + Name: "acme-http-port", + Usage: `The port used on http-01 challenges. It can be changed for testing purposes. +Requires **--insecure** flag.`, + }, + cli.IntFlag{ + Name: "acme-tls-port", + Usage: `The port used on tls-alpn-01 challenges. It can be changed for testing purposes. +Requires **--insecure** flag.`, + }, + cli.BoolFlag{ + Name: "insecure", + }, }, } @@ -88,6 +102,23 @@ func appAction(ctx *cli.Context) error { return errs.TooManyArguments(ctx) } + // Allow custom ACME ports with insecure + if acmePort := ctx.Int("acme-http-port"); acmePort != 0 { + if ctx.Bool("insecure") { + acme.InsecurePortHTTP01 = acmePort + } else { + return fmt.Errorf("flag '--acme-http-port' requires the '--insecure' flag") + } + } + if acmePort := ctx.Int("acme-tls-port"); acmePort != 0 { + if ctx.Bool("insecure") { + acme.InsecurePortTLSALPN01 = acmePort + } else { + return fmt.Errorf("flag '--acme-tls-port' requires the '--insecure' flag") + } + } + + // Allow custom contexts. if caCtx := ctx.String("context"); caCtx != "" { if err := step.Contexts().SetCurrent(caCtx); err != nil { return err From bae9a0c1524fb6a9595189f9aac3df924965a7c7 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 4 Nov 2022 10:31:11 -0700 Subject: [PATCH 65/66] Use the same style of flags It changes the new step-ca flags to use a standard style. --- commands/app.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/commands/app.go b/commands/app.go index f624843a..125c42db 100644 --- a/commands/app.go +++ b/commands/app.go @@ -69,21 +69,22 @@ certificate issuer private key used in the RA mode.`, }, cli.StringFlag{ Name: "context", - Usage: "The name of the authority's context.", + Usage: "the name of the authority's context.", EnvVar: "STEP_CA_CONTEXT", }, cli.IntFlag{ Name: "acme-http-port", - Usage: `The port used on http-01 challenges. It can be changed for testing purposes. + Usage: `the used on http-01 challenges. It can be changed for testing purposes. Requires **--insecure** flag.`, }, cli.IntFlag{ Name: "acme-tls-port", - Usage: `The port used on tls-alpn-01 challenges. It can be changed for testing purposes. + Usage: `the used on tls-alpn-01 challenges. It can be changed for testing purposes. Requires **--insecure** flag.`, }, cli.BoolFlag{ - Name: "insecure", + Name: "insecure", + Usage: "enable insecure flags.", }, }, } From e00781873e07bdd835482f90a8b55bcbb2d11078 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 4 Nov 2022 10:41:06 -0700 Subject: [PATCH 66/66] Update commands/app.go Co-authored-by: Max --- commands/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/app.go b/commands/app.go index 125c42db..66030be2 100644 --- a/commands/app.go +++ b/commands/app.go @@ -69,7 +69,7 @@ certificate issuer private key used in the RA mode.`, }, cli.StringFlag{ Name: "context", - Usage: "the name of the authority's context.", + Usage: "the of the authority's context.", EnvVar: "STEP_CA_CONTEXT", }, cli.IntFlag{