Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for SCEP Polling #1502

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions authority/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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
Expand Down
32 changes: 32 additions & 0 deletions authority/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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.
//
Expand Down
13 changes: 13 additions & 0 deletions ca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
74 changes: 73 additions & 1 deletion db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
123 changes: 89 additions & 34 deletions scep/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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 := &microscep.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 {
Expand Down Expand Up @@ -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:
Expand Down
Loading