diff --git a/authority/config/config.go b/authority/config/config.go index 0494183bb..44dc7fcf0 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -82,12 +82,18 @@ type Config struct { Templates *templates.Templates `json:"templates,omitempty"` CommonName string `json:"commonName,omitempty"` CRL *CRLConfig `json:"crl,omitempty"` + Polling *PollingConfig `json:"polling,omitempty"` SkipValidation bool `json:"-"` // Keeps record of the filename the Config is read from loadedFromFilepath string } +// PollingConfig represents config options for SCEP polling +type PollingConfig struct { + Enabled bool `json:"enabled"` +} + // CRLConfig represents config options for CRL generation type CRLConfig struct { Enabled bool `json:"enabled"` @@ -97,6 +103,11 @@ type CRLConfig struct { IDPurl string `json:"idpURL,omitempty"` } +// IsEnabled returns if polling is enabled. +func (c *PollingConfig) IsEnabled() bool { + return c != nil && c.Enabled +} + // IsEnabled returns if the CRL is enabled. func (c *CRLConfig) IsEnabled() bool { return c != nil && c.Enabled diff --git a/authority/tls.go b/authority/tls.go index 6e9679209..ed2194f55 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -289,6 +289,14 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign } } + // Store certificate and certificate request in the db. + if err = a.storeCertificateAndRequest(csr, fullchain); err != nil { + if !errors.Is(err, db.ErrNotImplemented) { + return nil, errs.Wrap(http.StatusInternalServerError, err, + "authority.Sign; error storing certificate and request in db", opts...) + } + } + return fullchain, nil } @@ -449,6 +457,30 @@ func (a *Authority) RenewContext(ctx context.Context, oldCert *x509.Certificate, return fullchain, nil } +// storeCertificateAndRequest logs the full chain of certificates and its +// certificate signing request. +func (a *Authority) storeCertificateAndRequest(csr *x509.CertificateRequest, fullchain []*x509.Certificate) error { + type certificateChainAndRequestStorer interface { + StoreCertificateChainAndRequest(*x509.CertificateRequest, ...*x509.Certificate) error + } + + // Store certificate and request in linkedca + if s, ok := a.adminDB.(certificateChainAndRequestStorer); ok { + return s.StoreCertificateChainAndRequest(csr, fullchain...) + } + + // Store certificate in local db + switch s := a.db.(type) { + case certificateChainAndRequestStorer: + return s.StoreCertificateChainAndRequest(csr, fullchain...) + case db.CertificateAndRequestStorer: + return s.StoreCertificateAndRequest(csr, fullchain[0]) + default: + return nil + } + +} + // storeCertificate allows to use an extension of the db.AuthDB interface that // can log the full chain of certificates. // diff --git a/ca/ca.go b/ca/ca.go index b8f653322..cf7e26546 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -251,11 +251,24 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { var scepAuthority *scep.Authority if ca.shouldServeSCEPEndpoints() { scepPrefix := "scep" + var pollingDB db.PollingDB = nil + var polling bool = false + if cfg.Polling.IsEnabled() { + authDB := auth.GetDatabase() + if authDB == nil { + return nil, errors.Wrap(err, "error initializing AuthDB") + } + pollingDB = authDB.(db.PollingDB) + polling = true + } scepAuthority, err = scep.New(auth, scep.AuthorityOptions{ Service: auth.GetSCEPService(), DNS: dns, Prefix: scepPrefix, + DB: pollingDB, + Polling: polling, }) + if err != nil { return nil, errors.Wrap(err, "error creating SCEP authority") } diff --git a/db/db.go b/db/db.go index b3137a50e..4d4bebd64 100644 --- a/db/db.go +++ b/db/db.go @@ -20,6 +20,8 @@ var ( certsDataTable = []byte("x509_certs_data") revokedCertsTable = []byte("revoked_x509_certs") crlTable = []byte("x509_crl") + csrTable = []byte("x509_csr") + certByCSRTable = []byte("x509_certs_csr") revokedSSHCertsTable = []byte("revoked_ssh_certs") usedOTTTable = []byte("used_ott") sshCertsTable = []byte("ssh_certs") @@ -85,6 +87,12 @@ func MustFromContext(ctx context.Context) AuthDB { } } +// CertificateAndRequestStorer is an interface that allows to store +// certificates and certificate requests. +type CertificateAndRequestStorer interface { + StoreCertificateAndRequest(csr *x509.CertificateRequest, cert *x509.Certificate) error +} + // CertificateStorer is an extension of AuthDB that allows to store // certificates. type CertificateStorer interface { @@ -99,6 +107,13 @@ type CertificateRevocationListDB interface { StoreCRL(*CertificateRevocationListInfo) error } +// PollingDB is an interface that implements SCEP polling functionality +type PollingDB interface { + GetCSR(transactionID string) (*x509.CertificateRequest, error) + StoreCSR(transactionID string, csr *x509.CertificateRequest) error + GetCertificateByCSR(csr *x509.CertificateRequest) (*x509.Certificate, error) +} + // DB is a wrapper over the nosql.DB interface. type DB struct { nosql.DB @@ -125,7 +140,7 @@ func New(c *Config) (AuthDB, error) { tables := [][]byte{ revokedCertsTable, certsTable, usedOTTTable, sshCertsTable, sshHostsTable, sshHostPrincipalsTable, sshUsersTable, - revokedSSHCertsTable, certsDataTable, crlTable, + revokedSSHCertsTable, certsDataTable, crlTable, csrTable, certByCSRTable, } for _, b := range tables { if err := db.CreateTable(b); err != nil { @@ -321,6 +336,40 @@ func (db *DB) StoreCertificate(crt *x509.Certificate) error { return nil } +// GetCSR returns a certificate request based off a transaction ID. +func (db *DB) GetCSR(transactionID string) (*x509.CertificateRequest, error) { + asn1Data, err := db.Get(csrTable, []byte(transactionID)) + if err != nil { + return nil, errors.Wrap(err, "database Get error") + } + csr, err := x509.ParseCertificateRequest(asn1Data) + if err != nil { + return nil, errors.Wrapf(err, "error parsing certificate request with transaction ID %s", transactionID) + } + return csr, nil +} + +// StoreCSR stores a certificate signing request. +func (db *DB) StoreCSR(transactionID string, csr *x509.CertificateRequest) error { + if err := db.Set(csrTable, []byte(transactionID), csr.Raw); err != nil { + return errors.Wrap(err, "database Set error") + } + return nil +} + +// GetCertificateByCSR returns a certificate using its certificate signing request. +func (db *DB) GetCertificateByCSR(csr *x509.CertificateRequest) (*x509.Certificate, error) { + asn1Data, err := db.Get(certByCSRTable, csr.Raw) + if err != nil { + return nil, errors.Wrap(err, "database Get error") + } + cert, err := x509.ParseCertificate(asn1Data) + if err != nil { + return nil, errors.Wrapf(err, "error parsing certificate with csr %s", csr.Subject.SerialNumber) + } + return cert, nil +} + // CertificateData is the JSON representation of the data stored in // x509_certs_data table. type CertificateData struct { @@ -370,6 +419,29 @@ func (db *DB) StoreCertificateChain(p provisioner.Interface, chain ...*x509.Cert return nil } +// StoreCertificateChainAndRequest stores the leaf certificate and the +// matching certificate request +func (db *DB) StoreCertificateChainAndRequest(csr *x509.CertificateRequest, chain ...*x509.Certificate) error { + leaf := chain[0] + tx := new(database.Tx) + tx.Set(certByCSRTable, csr.Raw, leaf.Raw) + if err := db.Update(tx); err != nil { + return errors.Wrap(err, "database Update error") + } + return nil +} + +// StoreCertificateAndRequest stores the leaf certificate and the +// matching certificate request +func (db *DB) StoreCertificateAndRequest(csr *x509.CertificateRequest, cert *x509.Certificate) error { + tx := new(database.Tx) + tx.Set(certByCSRTable, csr.Raw, cert.Raw) + if err := db.Update(tx); err != nil { + return errors.Wrap(err, "database Update error") + } + return nil +} + // StoreRenewedCertificate stores the leaf certificate and the provisioner that // authorized the old certificate if available. func (db *DB) StoreRenewedCertificate(oldCert *x509.Certificate, chain ...*x509.Certificate) error { diff --git a/scep/api/api.go b/scep/api/api.go index 98da818be..6439523cb 100644 --- a/scep/api/api.go +++ b/scep/api/api.go @@ -19,6 +19,7 @@ import ( "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/api/log" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/scep" ) @@ -304,44 +305,70 @@ func PKIOperation(ctx context.Context, req request) (Response, error) { } // NOTE: at this point we have sufficient information for returning nicely signed CertReps - csr := msg.CSRReqMessage.CSR - transactionID := string(msg.TransactionID) - challengePassword := msg.CSRReqMessage.ChallengePassword - - // NOTE: we're blocking the RenewalReq if the challenge does not match, because otherwise we don't have any authentication. - // The macOS SCEP client performs renewals using PKCSreq. The CertNanny SCEP client will use PKCSreq with challenge too, it seems, - // even if using the renewal flow as described in the README.md. MicroMDM SCEP client also only does PKCSreq by default, unless - // a certificate exists; then it will use RenewalReq. Adding the challenge check here may be a small breaking change for clients. - // We'll have to see how it works out. - if msg.MessageType == microscep.PKCSReq || msg.MessageType == microscep.RenewalReq { - if err := auth.ValidateChallenge(ctx, challengePassword, transactionID); err != nil { - if errors.Is(err, provisioner.ErrSCEPChallengeInvalid) { + + switch msg.MessageType { + case microscep.CertPoll: + transactionID := string(msg.TransactionID) + var csr *x509.CertificateRequest + if csr, err = auth.GetCertificateRequest(transactionID); err != nil { + return createFailureResponse(ctx, csr, msg, microscep.BadRequest, err) + } + if isSigned, _ := auth.CertificateIsSigned(csr); isSigned { + // Reconstruct CSRReqMessage before sending a success response. + certReq := µscep.CSRReqMessage{ + CSR: csr, + } + msg := &scep.PKIMessage{ + TransactionID: msg.TransactionID, + MessageType: msg.MessageType, + SenderNonce: msg.SenderNonce, + CSRReqMessage: certReq, + CertRepMessage: msg.CertRepMessage, + Raw: msg.Raw, + P7: msg.P7, + Recipients: msg.Recipients, + } + return createSuccessResponse(ctx, csr, msg) + } + return createPendingResponse(ctx, msg) + default: + csr := msg.CSRReqMessage.CSR + transactionID := string(msg.TransactionID) + challengePassword := msg.CSRReqMessage.ChallengePassword + if auth.IsEnabled() { + var isInDB bool + if isInDB, err = auth.CertificateRequestInDB(transactionID); err != nil { return createFailureResponse(ctx, csr, msg, microscep.BadRequest, err) } - return createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("failed validating challenge password")) + if !isInDB { + err := auth.StoreCertificateRequest(transactionID, csr) + if err != nil { + return createFailureResponse(ctx, csr, msg, microscep.BadRequest, err) + } + } + return createPendingResponse(ctx, msg) + // NOTE: we're blocking the RenewalReq if the challenge does not match, because otherwise we don't have any authentication. + // The macOS SCEP client performs renewals using PKCSreq. The CertNanny SCEP client will use PKCSreq with challenge too, it seems, + // even if using the renewal flow as described in the README.md. MicroMDM SCEP client also only does PKCSreq by default, unless + // a certificate exists; then it will use RenewalReq. Adding the challenge check here may be a small breaking change for clients. + // We'll have to see how it works out. + } else if msg.MessageType == microscep.PKCSReq || msg.MessageType == microscep.RenewalReq { + if err := auth.ValidateChallenge(ctx, challengePassword, transactionID); err != nil { + if errors.Is(err, provisioner.ErrSCEPChallengeInvalid) { + return createFailureResponse(ctx, csr, msg, microscep.BadRequest, err) + } + return createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("failed validating challenge password")) + } } + // TODO: authorize renewal: we can authorize renewals with the challenge password (if reusable secrets are used). + // Renewals OPTIONALLY include the challenge if the existing cert is used as authentication, but client SHOULD omit the challenge. + // This means that for renewal requests we should check the certificate provided to be signed before by the CA. We could + // enforce use of the challenge if we want too. That way we could be more flexible in terms of authentication scheme (i.e. reusing + // tokens from other provisioners, calling a webhook, storing multiple secrets, allowing them to be multi-use, etc). + // Authentication by the (self-signed) certificate with an optional challenge is required; supporting renewals incl. verification + // of the client cert is not. + return createSuccessResponse(ctx, csr, msg) } - - // TODO: authorize renewal: we can authorize renewals with the challenge password (if reusable secrets are used). - // Renewals OPTIONALLY include the challenge if the existing cert is used as authentication, but client SHOULD omit the challenge. - // This means that for renewal requests we should check the certificate provided to be signed before by the CA. We could - // enforce use of the challenge if we want too. That way we could be more flexible in terms of authentication scheme (i.e. reusing - // tokens from other provisioners, calling a webhook, storing multiple secrets, allowing them to be multi-use, etc). - // Authentication by the (self-signed) certificate with an optional challenge is required; supporting renewals incl. verification - // of the client cert is not. - - certRep, err := auth.SignCSR(ctx, csr, msg) - if err != nil { - return createFailureResponse(ctx, csr, msg, microscep.BadRequest, fmt.Errorf("error when signing new certificate: %w", err)) - } - - res := Response{ - Operation: opnPKIOperation, - Data: certRep.Raw, - Certificate: certRep.Certificate, - } - - return res, nil } func formatCapabilities(caps []string) []byte { @@ -381,6 +408,34 @@ func createFailureResponse(ctx context.Context, csr *x509.CertificateRequest, ms }, nil } +func createPendingResponse(ctx context.Context, msg *scep.PKIMessage) (Response, error) { + auth := scep.MustFromContext(ctx) + certRep, err := auth.CreatePendingResponse(msg) + if err != nil { + return Response{}, err + } + return Response{ + Operation: opnPKIOperation, + Data: certRep.Raw, + }, nil +} + +func createSuccessResponse(ctx context.Context, csr *x509.CertificateRequest, msg *scep.PKIMessage) (Response, error) { + auth := scep.MustFromContext(ctx) + certRep, err := auth.SignCSR(ctx, csr, msg) + if err != nil { + return createFailureResponse(ctx, csr, msg, microscep.BadRequest, fmt.Errorf("error when signing new certificate: %w", err)) + } + + res := Response{ + Operation: opnPKIOperation, + Data: certRep.Raw, + Certificate: certRep.Certificate, + } + + return res, nil +} + func contentHeader(r Response) string { switch r.Operation { default: diff --git a/scep/authority.go b/scep/authority.go index 23c288133..de59da6ff 100644 --- a/scep/authority.go +++ b/scep/authority.go @@ -14,6 +14,7 @@ import ( "go.step.sm/crypto/x509util" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/db" ) // Authority is the layer that handles all SCEP interactions. @@ -24,6 +25,8 @@ type Authority struct { caCerts []*x509.Certificate // TODO(hs): change to use these instead of root and intermediate service *Service signAuth SignAuthority + db db.PollingDB + polling bool } type authorityKey struct{} @@ -60,6 +63,9 @@ type AuthorityOptions struct { // Prefix is a URL path prefix under which the SCEP api is served. This // prefix is required to generate accurate SCEP links. Prefix string + + DB db.PollingDB + Polling bool } // SignAuthority is the interface for a signing authority @@ -74,6 +80,8 @@ func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) { prefix: ops.Prefix, dns: ops.DNS, signAuth: signAuth, + db: ops.DB, + polling: ops.Polling, } // TODO: this is not really nice to do; the Service should be removed @@ -220,7 +228,7 @@ func (a *Authority) DecryptPKIEnvelope(_ context.Context, msg *PKIMessage) error } return nil case microscep.GetCRL, microscep.GetCert, microscep.CertPoll: - return errors.New("not implemented") + return nil } return nil @@ -455,6 +463,64 @@ func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.Certificate return crepMsg, nil } +// CreatePendingResponse creates a pending reply when the CA is in polling mode +func (a *Authority) CreatePendingResponse(msg *PKIMessage) (*PKIMessage, error) { + config := pkcs7.SignerInfoConfig{ + ExtraSignedAttributes: []pkcs7.Attribute{ + { + Type: oidSCEPtransactionID, + Value: msg.TransactionID, + }, + { + Type: oidSCEPpkiStatus, + Value: microscep.PENDING, + }, + { + Type: oidSCEPmessageType, + Value: microscep.CertRep, + }, + { + Type: oidSCEPsenderNonce, + Value: msg.SenderNonce, + }, + { + Type: oidSCEPrecipientNonce, + Value: msg.SenderNonce, + }, + }, + } + + signedData, err := pkcs7.NewSignedData(nil) + if err != nil { + return nil, err + } + + // sign the attributes + if err := signedData.AddSigner(a.intermediateCertificate, a.service.signer, config); err != nil { + return nil, err + } + + certRepBytes, err := signedData.Finish() + if err != nil { + return nil, err + } + + cr := &CertRepMessage{ + PKIStatus: microscep.PENDING, + RecipientNonce: microscep.RecipientNonce(msg.SenderNonce), + } + + // create a CertRep message from the original + crepMsg := &PKIMessage{ + Raw: certRepBytes, + TransactionID: msg.TransactionID, + MessageType: microscep.CertRep, + CertRepMessage: cr, + } + + return crepMsg, nil +} + // GetCACaps returns the CA capabilities func (a *Authority) GetCACaps(ctx context.Context) []string { p, err := provisionerFromContext(ctx) diff --git a/scep/polling.go b/scep/polling.go new file mode 100644 index 000000000..61f001e99 --- /dev/null +++ b/scep/polling.go @@ -0,0 +1,52 @@ +package scep + +import ( + "crypto/x509" + + "github.com/pkg/errors" +) + +// CertificateIsSigned looks if the certificate matching the csr is signed, and returns a bool if it's been signed or not. +func (a *Authority) CertificateIsSigned(csr *x509.CertificateRequest) (bool, error) { + _, err := a.db.GetCertificateByCSR(csr) + if err != nil { + return false, errors.Errorf("Error finding certificate in database") + } + // If there's no errors, then it exists and is signed. + return true, nil +} + +// CertificateRequestInDB looks for the CSR matching the transaction ID, and returns a bool if it exists or not. +func (a *Authority) CertificateRequestInDB(transactionID string) (bool, error) { + csr, err := a.db.GetCSR(transactionID) + if err != nil { + return false, nil + } + if csr == nil { + return false, nil + } + return true, nil +} + +// StoreCertificateRequest stores the incoming certificate signing request. +func (a *Authority) StoreCertificateRequest(transactionID string, csr *x509.CertificateRequest) error { + err := a.db.StoreCSR(transactionID, csr) + if err != nil { + return err + } + return nil +} + +// GetCertificateRequest fetches and returns the CSR matching the transaction ID. +func (a *Authority) GetCertificateRequest(transactionID string) (*x509.CertificateRequest, error) { + csr, err := a.db.GetCSR(transactionID) + if err != nil { + return nil, errors.Errorf("Error finding serial number in database") + } + return csr, nil +} + +// IsEnabled returns a bool if SCEP polling is enabled or not. +func (a *Authority) IsEnabled() bool { + return a.polling +}