Merge pull request #1414 from smallstep/herman/scep-provisioner-decrypter

Add SCEP provisioner decrypter
pull/1555/head
Mariano Cano 8 months ago committed by GitHub
commit b66a92ca41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -25,6 +25,7 @@ import (
"golang.org/x/crypto/ssh"
"github.com/smallstep/certificates/api/log"
"github.com/smallstep/certificates/api/models"
"github.com/smallstep/certificates/api/render"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/config"
@ -231,6 +232,29 @@ type ProvisionersResponse struct {
NextCursor string
}
const redacted = "*** REDACTED ***"
func scepFromProvisioner(p *provisioner.SCEP) *models.SCEP {
return &models.SCEP{
ID: p.ID,
Type: p.Type,
Name: p.Name,
ForceCN: p.ForceCN,
ChallengePassword: redacted,
Capabilities: p.Capabilities,
IncludeRoot: p.IncludeRoot,
ExcludeIntermediate: p.ExcludeIntermediate,
MinimumPublicKeyLength: p.MinimumPublicKeyLength,
DecrypterCertificate: []byte(redacted),
DecrypterKeyPEM: []byte(redacted),
DecrypterKeyURI: redacted,
DecrypterKeyPassword: []byte(redacted),
EncryptionAlgorithmIdentifier: p.EncryptionAlgorithmIdentifier,
Options: p.Options,
Claims: p.Claims,
}
}
// MarshalJSON implements json.Marshaler. It marshals the ProvisionersResponse
// into a byte slice.
//
@ -238,24 +262,22 @@ type ProvisionersResponse struct {
// challenge secret that MUST NOT be leaked in (public) HTTP responses. The
// challenge value is thus redacted in HTTP responses.
func (p ProvisionersResponse) MarshalJSON() ([]byte, error) {
var responseProvisioners provisioner.List
for _, item := range p.Provisioners {
scepProv, ok := item.(*provisioner.SCEP)
if !ok {
responseProvisioners = append(responseProvisioners, item)
continue
}
old := scepProv.ChallengePassword
scepProv.ChallengePassword = "*** REDACTED ***"
defer func(p string) { //nolint:gocritic // defer in loop required to restore initial state of provisioners
scepProv.ChallengePassword = p
}(old)
responseProvisioners = append(responseProvisioners, scepFromProvisioner(scepProv))
}
var list = struct {
Provisioners []provisioner.Interface `json:"provisioners"`
NextCursor string `json:"nextCursor"`
}{
Provisioners: []provisioner.Interface(p.Provisioners),
Provisioners: []provisioner.Interface(responseProvisioners),
NextCursor: p.NextCursor,
}

@ -1569,7 +1569,6 @@ func mustCertificate(t *testing.T, pub, priv interface{}) *x509.Certificate {
}
func TestProvisionersResponse_MarshalJSON(t *testing.T) {
k := map[string]any{
"use": "sig",
"kty": "EC",
@ -1581,9 +1580,14 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) {
}
key := squarejose.JSONWebKey{}
b, err := json.Marshal(k)
assert.FatalError(t, err)
require.NoError(t, err)
err = json.Unmarshal(b, &key)
assert.FatalError(t, err)
require.NoError(t, err)
var encodedPassword bytes.Buffer
enc := base64.NewEncoder(base64.StdEncoding, &encodedPassword)
_, err = enc.Write([]byte("super-secret-password"))
require.NoError(t, err)
r := ProvisionersResponse{
Provisioners: provisioner.List{
@ -1593,6 +1597,12 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) {
ChallengePassword: "not-so-secret",
MinimumPublicKeyLength: 2048,
EncryptionAlgorithmIdentifier: 2,
IncludeRoot: true,
ExcludeIntermediate: true,
DecrypterCertificate: []byte{1, 2, 3, 4},
DecrypterKeyPEM: []byte{5, 6, 7, 8},
DecrypterKeyURI: "softkms:path=/path/to/private.key",
DecrypterKeyPassword: encodedPassword.Bytes(),
},
&provisioner.JWK{
EncryptedKey: "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg",
@ -1609,7 +1619,14 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) {
{
"type": "scep",
"name": "scep",
"forceCN": false,
"includeRoot": true,
"excludeIntermediate": true,
"challenge": "*** REDACTED ***",
"decrypterCertificate": []byte("*** REDACTED ***"),
"decrypterKey": "*** REDACTED ***",
"decrypterKeyPEM": []byte("*** REDACTED ***"),
"decrypterKeyPassword": []byte("*** REDACTED ***"),
"minimumPublicKeyLength": 2048,
"encryptionAlgorithmIdentifier": 2,
},
@ -1646,6 +1663,12 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) {
ChallengePassword: "not-so-secret",
MinimumPublicKeyLength: 2048,
EncryptionAlgorithmIdentifier: 2,
IncludeRoot: true,
ExcludeIntermediate: true,
DecrypterCertificate: []byte{1, 2, 3, 4},
DecrypterKeyPEM: []byte{5, 6, 7, 8},
DecrypterKeyURI: "softkms:path=/path/to/private.key",
DecrypterKeyPassword: encodedPassword.Bytes(),
},
&provisioner.JWK{
EncryptedKey: "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg",

@ -0,0 +1,118 @@
package models
import (
"context"
"crypto/x509"
"errors"
"github.com/smallstep/certificates/authority/provisioner"
"golang.org/x/crypto/ssh"
)
var errDummyImplementation = errors.New("dummy implementation")
// SCEP is the SCEP provisioner model used solely in CA API
// responses. All methods for the [provisioner.Interface] interface
// are implemented, but return a dummy error.
// TODO(hs): remove reliance on the interface for the API responses
type SCEP struct {
ID string `json:"-"`
Type string `json:"type"`
Name string `json:"name"`
ForceCN bool `json:"forceCN"`
ChallengePassword string `json:"challenge"`
Capabilities []string `json:"capabilities,omitempty"`
IncludeRoot bool `json:"includeRoot"`
ExcludeIntermediate bool `json:"excludeIntermediate"`
MinimumPublicKeyLength int `json:"minimumPublicKeyLength"`
DecrypterCertificate []byte `json:"decrypterCertificate"`
DecrypterKeyPEM []byte `json:"decrypterKeyPEM"`
DecrypterKeyURI string `json:"decrypterKey"`
DecrypterKeyPassword []byte `json:"decrypterKeyPassword"`
EncryptionAlgorithmIdentifier int `json:"encryptionAlgorithmIdentifier"`
Options *provisioner.Options `json:"options,omitempty"`
Claims *provisioner.Claims `json:"claims,omitempty"`
}
// GetID returns the provisioner unique identifier.
func (s *SCEP) GetID() string {
if s.ID != "" {
return s.ID
}
return s.GetIDForToken()
}
// GetIDForToken returns an identifier that will be used to load the provisioner
// from a token.
func (s *SCEP) GetIDForToken() string {
return "scep/" + s.Name
}
// GetName returns the name of the provisioner.
func (s *SCEP) GetName() string {
return s.Name
}
// GetType returns the type of provisioner.
func (s *SCEP) GetType() provisioner.Type {
return provisioner.TypeSCEP
}
// GetEncryptedKey returns the base provisioner encrypted key if it's defined.
func (s *SCEP) GetEncryptedKey() (string, string, bool) {
return "", "", false
}
// GetTokenID returns the identifier of the token.
func (s *SCEP) GetTokenID(string) (string, error) {
return "", errDummyImplementation
}
// Init initializes and validates the fields of a SCEP type.
func (s *SCEP) Init(_ provisioner.Config) (err error) {
return errDummyImplementation
}
// AuthorizeSign returns an unimplemented error. Provisioners should overwrite
// this method if they will support authorizing tokens for signing x509 Certificates.
func (s *SCEP) AuthorizeSign(context.Context, string) ([]provisioner.SignOption, error) {
return nil, errDummyImplementation
}
// AuthorizeRevoke returns an unimplemented error. Provisioners should overwrite
// this method if they will support authorizing tokens for revoking x509 Certificates.
func (s *SCEP) AuthorizeRevoke(context.Context, string) error {
return errDummyImplementation
}
// AuthorizeRenew returns an unimplemented error. Provisioners should overwrite
// this method if they will support authorizing tokens for renewing x509 Certificates.
func (s *SCEP) AuthorizeRenew(context.Context, *x509.Certificate) error {
return errDummyImplementation
}
// AuthorizeSSHSign returns an unimplemented error. Provisioners should overwrite
// this method if they will support authorizing tokens for signing SSH Certificates.
func (s *SCEP) AuthorizeSSHSign(context.Context, string) ([]provisioner.SignOption, error) {
return nil, errDummyImplementation
}
// AuthorizeRevoke returns an unimplemented error. Provisioners should overwrite
// this method if they will support authorizing tokens for revoking SSH Certificates.
func (s *SCEP) AuthorizeSSHRevoke(context.Context, string) error {
return errDummyImplementation
}
// AuthorizeSSHRenew returns an unimplemented error. Provisioners should overwrite
// this method if they will support authorizing tokens for renewing SSH Certificates.
func (s *SCEP) AuthorizeSSHRenew(context.Context, string) (*ssh.Certificate, error) {
return nil, errDummyImplementation
}
// AuthorizeSSHRekey returns an unimplemented error. Provisioners should overwrite
// this method if they will support authorizing tokens for rekeying SSH Certificates.
func (s *SCEP) AuthorizeSSHRekey(context.Context, string) (*ssh.Certificate, []provisioner.SignOption, error) {
return nil, nil, errDummyImplementation
}
var _ provisioner.Interface = (*SCEP)(nil)

@ -56,9 +56,7 @@ func validateWebhook(webhook *linkedca.Webhook) error {
}
// kind
switch webhook.Kind {
case linkedca.Webhook_ENRICHING, linkedca.Webhook_AUTHORIZING, linkedca.Webhook_SCEPCHALLENGE:
default:
if _, ok := linkedca.Webhook_Kind_name[int32(webhook.Kind)]; !ok || webhook.Kind == linkedca.Webhook_NO_KIND {
return admin.NewError(admin.ErrorBadRequestType, "webhook kind %q is invalid", webhook.Kind)
}

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
@ -61,7 +62,9 @@ type Authority struct {
x509Enforcers []provisioner.CertificateEnforcer
// SCEP CA
scepService *scep.Service
scepOptions *scep.Options
validateSCEP bool
scepAuthority *scep.Authority
// SSH CA
sshHostPassword []byte
@ -122,6 +125,7 @@ func New(cfg *config.Config, opts ...Option) (*Authority, error) {
var a = &Authority{
config: cfg,
certificates: new(sync.Map),
validateSCEP: true,
}
// Apply options.
@ -261,6 +265,24 @@ func (a *Authority) ReloadAdminResources(ctx context.Context) error {
a.config.AuthorityConfig.Admins = adminList
a.admins = adminClxn
switch {
case a.requiresSCEP() && a.GetSCEP() == nil:
// TODO(hs): try to initialize SCEP here too? It's a bit
// problematic if this method is called as part of an update
// via Admin API and a password needs to be provided.
case a.requiresSCEP() && a.GetSCEP() != nil:
// update the SCEP Authority with the currently active SCEP
// provisioner names and revalidate the configuration.
a.scepAuthority.UpdateProvisioners(a.getSCEPProvisionerNames())
if err := a.scepAuthority.Validate(); err != nil {
log.Printf("failed validating SCEP authority: %v\n", err)
}
case !a.requiresSCEP() && a.GetSCEP() != nil:
// TODO(hs): don't remove the authority if we can't also
// reload it.
//a.scepAuthority = nil
}
return nil
}
@ -640,48 +662,83 @@ func (a *Authority) init() error {
return err
}
// Check if a KMS with decryption capability is required and available
if a.requiresDecrypter() {
if _, ok := a.keyManager.(kmsapi.Decrypter); !ok {
return errors.New("keymanager doesn't provide crypto.Decrypter")
// The SCEP functionality is provided through an instance of
// scep.Authority. It is initialized when the CA is started and
// if it doesn't exist yet. It gets refreshed if it already
// exists. If the SCEP authority is no longer required on reload,
// it gets removed.
// TODO(hs): reloading through SIGHUP doesn't hit these cases. This
// is because an entirely new authority.Authority is created, including
// a new scep.Authority. Look into this to see if we want this to
// keep working like that, or want to reuse a single instance and
// update that.
switch {
case a.requiresSCEP() && a.GetSCEP() == nil:
if a.scepOptions == nil {
options := &scep.Options{
Roots: a.rootX509Certs,
Intermediates: a.intermediateX509Certs,
SignerCert: a.intermediateX509Certs[0],
}
if options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.IntermediateKey,
Password: a.password,
}); err != nil {
return err
}
// TODO(hs): instead of creating the decrypter here, pass the
// intermediate key + chain down to the SCEP authority,
// and only instantiate it when required there. Is that possible?
// Also with entering passwords?
// TODO(hs): if moving the logic, try improving the logic for the
// decrypter password too? Right now it needs to be entered multiple
// times; I've observed it to be three times maximum, every time
// the intermediate key is read.
_, isRSA := options.Signer.Public().(*rsa.PublicKey)
if km, ok := a.keyManager.(kmsapi.Decrypter); ok && isRSA {
if decrypter, err := km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKey: a.config.IntermediateKey,
Password: a.password,
}); err == nil {
// only pass the decrypter down when it was successfully created,
// meaning it's an RSA key, and `CreateDecrypter` did not fail.
options.Decrypter = decrypter
options.DecrypterCert = options.Intermediates[0]
}
}
a.scepOptions = options
}
}
// TODO: decide if this is a good approach for providing the SCEP functionality
// It currently mirrors the logic for the x509CAService
if a.requiresSCEPService() && a.scepService == nil {
var options scep.Options
// provide the current SCEP provisioner names, so that the provisioners
// can be validated when the CA is started.
a.scepOptions.SCEPProvisionerNames = a.getSCEPProvisionerNames()
// Read intermediate and create X509 signer and decrypter for default CAS.
options.CertificateChain, err = pemutil.ReadCertificateBundle(a.config.IntermediateCert)
if err != nil {
return err
}
options.CertificateChain = append(options.CertificateChain, a.rootX509Certs...)
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.IntermediateKey,
Password: a.password,
})
// create a new SCEP authority
scepAuthority, err := scep.New(a, *a.scepOptions)
if err != nil {
return err
}
if km, ok := a.keyManager.(kmsapi.Decrypter); ok {
options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKey: a.config.IntermediateKey,
Password: a.password,
})
if err != nil {
return err
if a.validateSCEP {
// validate the SCEP authority
if err := scepAuthority.Validate(); err != nil {
a.initLogf("failed validating SCEP authority: %v", err)
}
}
a.scepService, err = scep.NewService(ctx, options)
if err != nil {
return err
// set the SCEP authority
a.scepAuthority = scepAuthority
case !a.requiresSCEP() && a.GetSCEP() != nil:
// clear the SCEP authority if it's no longer required
a.scepAuthority = nil
case a.requiresSCEP() && a.GetSCEP() != nil:
// update the SCEP Authority with the currently active SCEP
// provisioner names and revalidate the configuration.
a.scepAuthority.UpdateProvisioners(a.getSCEPProvisionerNames())
if err := a.scepAuthority.Validate(); err != nil {
log.Printf("failed validating SCEP authority: %v\n", err)
}
// TODO: mimick the x509CAService GetCertificateAuthority here too?
}
// Load X509 constraints engine.
@ -833,17 +890,9 @@ func (a *Authority) IsRevoked(sn string) (bool, error) {
return a.db.IsRevoked(sn)
}
// requiresDecrypter returns whether the Authority
// requires a KMS that provides a crypto.Decrypter
// Currently this is only required when SCEP is
// enabled.
func (a *Authority) requiresDecrypter() bool {
return a.requiresSCEPService()
}
// requiresSCEPService iterates over the configured provisioners
// and determines if one of them is a SCEP provisioner.
func (a *Authority) requiresSCEPService() bool {
// requiresSCEP iterates over the configured provisioners
// and determines if at least one of them is a SCEP provisioner.
func (a *Authority) requiresSCEP() bool {
for _, p := range a.config.AuthorityConfig.Provisioners {
if p.GetType() == provisioner.TypeSCEP {
return true
@ -852,13 +901,21 @@ 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.
func (a *Authority) GetSCEPService() *scep.Service {
return a.scepService
// getSCEPProvisionerNames returns the names of the SCEP provisioners
// that are currently available in the CA.
func (a *Authority) getSCEPProvisionerNames() (names []string) {
for _, p := range a.config.AuthorityConfig.Provisioners {
if p.GetType() == provisioner.TypeSCEP {
names = append(names, p.GetName())
}
}
return
}
// GetSCEP returns the configured SCEP Authority
func (a *Authority) GetSCEP() *scep.Authority {
return a.scepAuthority
}
func (a *Authority) startCRLGenerator() error {

@ -478,7 +478,7 @@ func testScepAuthority(t *testing.T, opts ...Option) *Authority {
return a
}
func TestAuthority_GetSCEPService(t *testing.T) {
func TestAuthority_GetSCEP(t *testing.T) {
_ = testScepAuthority(t)
p := provisioner.List{
&provisioner.SCEP{
@ -542,7 +542,7 @@ func TestAuthority_GetSCEPService(t *testing.T) {
return
}
if tt.wantService {
if got := a.GetSCEPService(); (got != nil) != tt.wantService {
if got := a.GetSCEP(); (got != nil) != tt.wantService {
t.Errorf("Authority.GetSCEPService() = %v, wantService %v", got, tt.wantService)
}
}

@ -18,6 +18,7 @@ import (
"github.com/smallstep/certificates/cas"
casapi "github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/scep"
)
// Option sets options to the Authority.
@ -205,6 +206,17 @@ func WithX509SignerFunc(fn func() ([]*x509.Certificate, crypto.Signer, error)) O
}
}
// WithFullSCEPOptions defines the options used for SCEP support.
//
// This feature is EXPERIMENTAL and might change at any time.
func WithFullSCEPOptions(options *scep.Options) Option {
return func(a *Authority) error {
a.scepOptions = options
a.validateSCEP = false
return nil
}
}
// WithSSHUserSigner defines the signer used to sign SSH user certificates.
func WithSSHUserSigner(s crypto.Signer) Option {
return func(a *Authority) error {

@ -2,13 +2,20 @@ package provisioner
import (
"context"
"crypto"
"crypto/rsa"
"crypto/subtle"
"crypto/x509"
"encoding/pem"
"fmt"
"net/http"
"time"
"github.com/pkg/errors"
"go.step.sm/crypto/kms"
kmsapi "go.step.sm/crypto/kms/apiv1"
"go.step.sm/crypto/kms/uri"
"go.step.sm/linkedca"
"github.com/smallstep/certificates/webhook"
@ -29,9 +36,19 @@ type SCEP struct {
// intermediate in the GetCACerts response
IncludeRoot bool `json:"includeRoot,omitempty"`
// ExcludeIntermediate makes the provisioner skip the intermediate CA in the
// GetCACerts response
ExcludeIntermediate bool `json:"excludeIntermediate,omitempty"`
// MinimumPublicKeyLength is the minimum length for public keys in CSRs
MinimumPublicKeyLength int `json:"minimumPublicKeyLength,omitempty"`
// TODO(hs): also support a separate signer configuration?
DecrypterCertificate []byte `json:"decrypterCertificate,omitempty"`
DecrypterKeyPEM []byte `json:"decrypterKeyPEM,omitempty"`
DecrypterKeyURI string `json:"decrypterKey,omitempty"`
DecrypterKeyPassword []byte `json:"decrypterKeyPassword,omitempty"`
// Numerical identifier for the ContentEncryptionAlgorithm as defined in github.com/mozilla-services/pkcs7
// at https://github.com/mozilla-services/pkcs7/blob/33d05740a3526e382af6395d3513e73d4e66d1cb/encrypt.go#L63
// Defaults to 0, being DES-CBC
@ -41,6 +58,12 @@ type SCEP struct {
ctl *Controller
encryptionAlgorithm int
challengeValidationController *challengeValidationController
notificationController *notificationController
keyManager kmsapi.KeyManager
decrypter crypto.Decrypter
decrypterCertificate *x509.Certificate
signer crypto.Signer
signerCertificate *x509.Certificate
}
// GetID returns the provisioner unique identifier.
@ -113,7 +136,8 @@ func newChallengeValidationController(client *http.Client, webhooks []*Webhook)
}
var (
ErrSCEPChallengeInvalid = errors.New("webhook server did not allow request")
ErrSCEPChallengeInvalid = errors.New("webhook server did not allow request")
ErrSCEPNotificationFailed = errors.New("scep notification failed")
)
// Validate executes zero or more configured webhooks to
@ -122,12 +146,14 @@ var (
// that case, the other webhooks will be skipped. If none of
// the webhooks indicates the value of the challenge was accepted,
// an error is returned.
func (c *challengeValidationController) Validate(ctx context.Context, challenge, transactionID string) error {
func (c *challengeValidationController) Validate(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) error {
for _, wh := range c.webhooks {
req := &webhook.RequestBody{
SCEPChallenge: challenge,
SCEPTransactionID: transactionID,
req, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr))
if err != nil {
return fmt.Errorf("failed creating new webhook request: %w", err)
}
req.SCEPChallenge = challenge
req.SCEPTransactionID = transactionID
resp, err := wh.DoWithContext(ctx, c.client, req, nil) // TODO(hs): support templated URL? Requires some refactoring
if err != nil {
return fmt.Errorf("failed executing webhook request: %w", err)
@ -140,6 +166,63 @@ func (c *challengeValidationController) Validate(ctx context.Context, challenge,
return ErrSCEPChallengeInvalid
}
type notificationController struct {
client *http.Client
webhooks []*Webhook
}
// newNotificationController creates a new notificationController
// that performs SCEP notifications through webhooks.
func newNotificationController(client *http.Client, webhooks []*Webhook) *notificationController {
scepHooks := []*Webhook{}
for _, wh := range webhooks {
if wh.Kind != linkedca.Webhook_NOTIFYING.String() {
continue
}
if !isCertTypeOK(wh) {
continue
}
scepHooks = append(scepHooks, wh)
}
return &notificationController{
client: client,
webhooks: scepHooks,
}
}
func (c *notificationController) Success(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error {
for _, wh := range c.webhooks {
req, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr), webhook.WithX509Certificate(nil, cert)) // TODO(hs): pass in the x509util.Certifiate too?
if err != nil {
return fmt.Errorf("failed creating new webhook request: %w", err)
}
req.X509Certificate.Raw = cert.Raw // adding the full certificate DER bytes
req.SCEPTransactionID = transactionID
if _, err = wh.DoWithContext(ctx, c.client, req, nil); err != nil {
return fmt.Errorf("failed executing webhook request: %w: %w", ErrSCEPNotificationFailed, err)
}
}
return nil
}
func (c *notificationController) Failure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error {
for _, wh := range c.webhooks {
req, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr))
if err != nil {
return fmt.Errorf("failed creating new webhook request: %w", err)
}
req.SCEPTransactionID = transactionID
req.SCEPErrorCode = errorCode
req.SCEPErrorDescription = errorDescription
if _, err = wh.DoWithContext(ctx, c.client, req, nil); err != nil {
return fmt.Errorf("failed executing webhook request: %w: %w", ErrSCEPNotificationFailed, err)
}
}
return nil
}
// isCertTypeOK returns whether or not the webhook can be used
// with the SCEP challenge validation webhook controller.
func isCertTypeOK(wh *Webhook) bool {
@ -162,21 +245,139 @@ func (s *SCEP) Init(config Config) (err error) {
if s.MinimumPublicKeyLength == 0 {
s.MinimumPublicKeyLength = 2048
}
if s.MinimumPublicKeyLength%8 != 0 {
return errors.Errorf("%d bits is not exactly divisible by 8", s.MinimumPublicKeyLength)
}
// Set the encryption algorithm to use
s.encryptionAlgorithm = s.EncryptionAlgorithmIdentifier // TODO(hs): we might want to upgrade the default security to AES-CBC?
if s.encryptionAlgorithm < 0 || s.encryptionAlgorithm > 4 {
return errors.New("only encryption algorithm identifiers from 0 to 4 are valid")
}
// Prepare the SCEP challenge validator
s.challengeValidationController = newChallengeValidationController(
config.WebhookClient,
s.GetOptions().GetWebhooks(),
)
// Prepare the SCEP notification controller
s.notificationController = newNotificationController(
config.WebhookClient,
s.GetOptions().GetWebhooks(),
)
// parse the decrypter key PEM contents if available
if decryptionKeyPEM := s.DecrypterKeyPEM; len(decryptionKeyPEM) > 0 {
// try reading the PEM for validation
block, rest := pem.Decode(decryptionKeyPEM)
if len(rest) > 0 {
return errors.New("failed parsing decrypter key: trailing data")
}
if block == nil {
return errors.New("failed parsing decrypter key: no PEM block found")
}
opts := kms.Options{
Type: kmsapi.SoftKMS,
}
if s.keyManager, err = kms.New(context.Background(), opts); err != nil {
return fmt.Errorf("failed initializing kms: %w", err)
}
kmsDecrypter, ok := s.keyManager.(kmsapi.Decrypter)
if !ok {
return fmt.Errorf("%q is not a kmsapi.Decrypter", opts.Type)
}
if s.decrypter, err = kmsDecrypter.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKeyPEM: decryptionKeyPEM,
Password: s.DecrypterKeyPassword,
PasswordPrompter: kmsapi.NonInteractivePasswordPrompter,
}); err != nil {
return fmt.Errorf("failed creating decrypter: %w", err)
}
if s.signer, err = s.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKeyPEM: decryptionKeyPEM, // TODO(hs): support distinct signer key in the future?
Password: s.DecrypterKeyPassword,
PasswordPrompter: kmsapi.NonInteractivePasswordPrompter,
}); err != nil {
return fmt.Errorf("failed creating signer: %w", err)
}
}
if decryptionKeyURI := s.DecrypterKeyURI; len(decryptionKeyURI) > 0 {
u, err := uri.Parse(s.DecrypterKeyURI)
if err != nil {
return fmt.Errorf("failed parsing decrypter key: %w", err)
}
var kmsType kmsapi.Type
switch {
case u.Scheme != "":
kmsType = kms.Type(u.Scheme)
default:
kmsType = kmsapi.SoftKMS
}
opts := kms.Options{
Type: kmsType,
URI: s.DecrypterKeyURI,
}
if s.keyManager, err = kms.New(context.Background(), opts); err != nil {
return fmt.Errorf("failed initializing kms: %w", err)
}
kmsDecrypter, ok := s.keyManager.(kmsapi.Decrypter)
if !ok {
return fmt.Errorf("%q is not a kmsapi.Decrypter", opts.Type)
}
if kmsType != "softkms" { // TODO(hs): this should likely become more transparent?
decryptionKeyURI = u.Opaque
}
if s.decrypter, err = kmsDecrypter.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKey: decryptionKeyURI,
Password: s.DecrypterKeyPassword,
PasswordPrompter: kmsapi.NonInteractivePasswordPrompter,
}); err != nil {
return fmt.Errorf("failed creating decrypter: %w", err)
}
if s.signer, err = s.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: decryptionKeyURI, // TODO(hs): support distinct signer key in the future?
Password: s.DecrypterKeyPassword,
PasswordPrompter: kmsapi.NonInteractivePasswordPrompter,
}); err != nil {
return fmt.Errorf("failed creating signer: %w", err)
}
}
// parse the decrypter certificate contents if available
if len(s.DecrypterCertificate) > 0 {
block, rest := pem.Decode(s.DecrypterCertificate)
if len(rest) > 0 {
return errors.New("failed parsing decrypter certificate: trailing data")
}
if block == nil {
return errors.New("failed parsing decrypter certificate: no PEM block found")
}
if s.decrypterCertificate, err = x509.ParseCertificate(block.Bytes); err != nil {
return fmt.Errorf("failed parsing decrypter certificate: %w", err)
}
// the decrypter certificate is also the signer certificate
s.signerCertificate = s.decrypterCertificate
}
// TODO(hs): alternatively, check if the KMS keyManager is a CertificateManager
// and load the certificate corresponding to the decryption key?
// Final validation for the decrypter.
if s.decrypter != nil {
decrypterPublicKey, ok := s.decrypter.Public().(*rsa.PublicKey)
if !ok {
return fmt.Errorf("only RSA keys are supported")
}
if s.decrypterCertificate == nil {
return fmt.Errorf("provisioner %q does not have a decrypter certificate set", s.Name)
}
if !decrypterPublicKey.Equal(s.decrypterCertificate.PublicKey) {
return errors.New("mismatch between decrypter certificate and decrypter public keys")
}
}
// TODO: add other, SCEP specific, options?
s.ctl, err = NewController(s, s.Claims, config, s.Options)
@ -214,6 +415,15 @@ func (s *SCEP) ShouldIncludeRootInChain() bool {
return s.IncludeRoot
}
// ShouldIncludeIntermediateInChain indicates if the
// CA should include the intermediate CA certificate in the
// GetCACerts response. This is true by default, but can be
// overridden through configuration in case SCEP clients
// don't pick the right recipient.
func (s *SCEP) ShouldIncludeIntermediateInChain() bool {
return !s.ExcludeIntermediate
}
// GetContentEncryptionAlgorithm returns the numeric identifier
// for the pkcs7 package encryption algorithm to use.
func (s *SCEP) GetContentEncryptionAlgorithm() int {
@ -223,13 +433,13 @@ func (s *SCEP) GetContentEncryptionAlgorithm() int {
// ValidateChallenge validates the provided challenge. It starts by
// selecting the validation method to use, then performs validation
// according to that method.
func (s *SCEP) ValidateChallenge(ctx context.Context, challenge, transactionID string) error {
func (s *SCEP) ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) error {
if s.challengeValidationController == nil {
return fmt.Errorf("provisioner %q wasn't initialized", s.Name)
}
switch s.selectValidationMethod() {
case validationMethodWebhook:
return s.challengeValidationController.Validate(ctx, challenge, transactionID)
return s.challengeValidationController.Validate(ctx, csr, challenge, transactionID)
default:
if subtle.ConstantTimeCompare([]byte(s.ChallengePassword), []byte(challenge)) == 0 {
return errors.New("invalid challenge password provided")
@ -238,6 +448,20 @@ func (s *SCEP) ValidateChallenge(ctx context.Context, challenge, transactionID s
}
}
func (s *SCEP) NotifySuccess(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error {
if s.notificationController == nil {
return fmt.Errorf("provisioner %q wasn't initialized", s.Name)
}
return s.notificationController.Success(ctx, csr, cert, transactionID)
}
func (s *SCEP) NotifyFailure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error {
if s.notificationController == nil {
return fmt.Errorf("provisioner %q wasn't initialized", s.Name)
}
return s.notificationController.Failure(ctx, csr, transactionID, errorCode, errorDescription)
}
type validationMethod string
const (
@ -259,3 +483,20 @@ func (s *SCEP) selectValidationMethod() validationMethod {
}
return validationMethodNone
}
// GetDecrypter returns the provisioner specific decrypter,
// used to decrypt SCEP request messages sent by a SCEP client.
// The decrypter consists of a crypto.Decrypter (a private key)
// and a certificate for the public key corresponding to the
// private key.
func (s *SCEP) GetDecrypter() (*x509.Certificate, crypto.Decrypter) {
return s.decrypterCertificate, s.decrypter
}
// GetSigner returns the provisioner specific signer, used to
// sign SCEP response messages for the client. The signer consists
// of a crypto.Signer and a certificate for the public key
// corresponding to the private key.
func (s *SCEP) GetSigner() (*x509.Certificate, crypto.Signer) {
return s.signerCertificate, s.signer
}

@ -2,6 +2,7 @@ package provisioner
import (
"context"
"crypto/x509"
"encoding/json"
"errors"
"net/http"
@ -12,12 +13,18 @@ import (
"github.com/stretchr/testify/require"
"go.step.sm/linkedca"
"github.com/smallstep/certificates/webhook"
)
func Test_challengeValidationController_Validate(t *testing.T) {
dummyCSR := &x509.CertificateRequest{
Raw: []byte{1},
}
type request struct {
Challenge string `json:"scepChallenge"`
TransactionID string `json:"scepTransactionID"`
Request *webhook.X509CertificateRequest `json:"x509CertificateRequest,omitempty"`
Challenge string `json:"scepChallenge"`
TransactionID string `json:"scepTransactionID"`
}
type response struct {
Allow bool `json:"allow"`
@ -39,6 +46,9 @@ func Test_challengeValidationController_Validate(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "challenge", req.Challenge)
assert.Equal(t, "transaction-1", req.TransactionID)
if assert.NotNil(t, req.Request) {
assert.Equal(t, []byte{1}, req.Request.Raw)
}
b, err := json.Marshal(response{Allow: true})
require.NoError(t, err)
w.WriteHeader(200)
@ -141,7 +151,7 @@ func Test_challengeValidationController_Validate(t *testing.T) {
}
ctx := context.Background()
err := c.Validate(ctx, tt.args.challenge, tt.args.transactionID)
err := c.Validate(ctx, dummyCSR, tt.args.challenge, tt.args.transactionID)
if tt.expErr != nil {
assert.EqualError(t, err, tt.expErr.Error())
@ -221,9 +231,13 @@ func Test_selectValidationMethod(t *testing.T) {
}
func TestSCEP_ValidateChallenge(t *testing.T) {
dummyCSR := &x509.CertificateRequest{
Raw: []byte{1},
}
type request struct {
Challenge string `json:"scepChallenge"`
TransactionID string `json:"scepTransactionID"`
Request *webhook.X509CertificateRequest `json:"x509CertificateRequest,omitempty"`
Challenge string `json:"scepChallenge"`
TransactionID string `json:"scepTransactionID"`
}
type response struct {
Allow bool `json:"allow"`
@ -234,6 +248,9 @@ func TestSCEP_ValidateChallenge(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "webhook-challenge", req.Challenge)
assert.Equal(t, "webhook-transaction-1", req.TransactionID)
if assert.NotNil(t, req.Request) {
assert.Equal(t, []byte{1}, req.Request.Raw)
}
b, err := json.Marshal(response{Allow: true})
require.NoError(t, err)
w.WriteHeader(200)
@ -330,7 +347,7 @@ func TestSCEP_ValidateChallenge(t *testing.T) {
require.NoError(t, err)
ctx := context.Background()
err = tt.p.ValidateChallenge(ctx, tt.args.challenge, tt.args.transactionID)
err = tt.p.ValidateChallenge(ctx, dummyCSR, tt.args.challenge, tt.args.transactionID)
if tt.expErr != nil {
assert.EqualError(t, err, tt.expErr.Error())
return

@ -235,7 +235,7 @@ func (a *Authority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisi
}
if err := certProv.Init(provisionerConfig); err != nil {
return admin.WrapError(admin.ErrorBadRequestType, err, "error validating configuration for provisioner %s", prov.Name)
return admin.WrapError(admin.ErrorBadRequestType, err, "error validating configuration for provisioner %q", prov.Name)
}
// Store to database -- this will set the ID.
@ -974,7 +974,7 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface,
}, nil
case *linkedca.ProvisionerDetails_SCEP:
cfg := d.SCEP
return &provisioner.SCEP{
s := &provisioner.SCEP{
ID: p.Id,
Type: p.Type.String(),
Name: p.Name,
@ -982,11 +982,19 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface,
ChallengePassword: cfg.Challenge,
Capabilities: cfg.Capabilities,
IncludeRoot: cfg.IncludeRoot,
ExcludeIntermediate: cfg.ExcludeIntermediate,
MinimumPublicKeyLength: int(cfg.MinimumPublicKeyLength),
EncryptionAlgorithmIdentifier: int(cfg.EncryptionAlgorithmIdentifier),
Claims: claims,
Options: options,
}, nil
}
if decrypter := cfg.GetDecrypter(); decrypter != nil {
s.DecrypterCertificate = decrypter.Certificate
s.DecrypterKeyPEM = decrypter.Key
s.DecrypterKeyURI = decrypter.KeyUri
s.DecrypterKeyPassword = decrypter.KeyPassword
}
return s, nil
case *linkedca.ProvisionerDetails_Nebula:
var roots []byte
for i, root := range d.Nebula.GetRoots() {
@ -1241,7 +1249,14 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
Capabilities: p.Capabilities,
MinimumPublicKeyLength: int32(p.MinimumPublicKeyLength),
IncludeRoot: p.IncludeRoot,
ExcludeIntermediate: p.ExcludeIntermediate,
EncryptionAlgorithmIdentifier: int32(p.EncryptionAlgorithmIdentifier),
Decrypter: &linkedca.SCEPDecrypter{
Certificate: p.DecrypterCertificate,
Key: p.DecrypterKeyPEM,
KeyUri: p.DecrypterKeyURI,
KeyPassword: p.DecrypterKeyPassword,
},
},
},
},

@ -250,19 +250,14 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
var scepAuthority *scep.Authority
if ca.shouldServeSCEPEndpoints() {
scepPrefix := "scep"
scepAuthority, err = scep.New(auth, scep.AuthorityOptions{
Service: auth.GetSCEPService(),
DNS: dns,
Prefix: scepPrefix,
})
if err != nil {
return nil, errors.Wrap(err, "error creating SCEP authority")
}
// get the SCEP authority configuration. Validation is
// performed within the authority instantiation process.
scepAuthority = auth.GetSCEP()
// According to the RFC (https://tools.ietf.org/html/rfc8894#section-7.10),
// SCEP operations are performed using HTTP, so that's why the API is mounted
// to the insecure mux.
scepPrefix := "scep"
insecureMux.Route("/"+scepPrefix, func(r chi.Router) {
scepAPI.Route(r)
})
@ -584,10 +579,10 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, *tls.Config,
// shouldServeSCEPEndpoints returns if the CA should be
// configured with endpoints for SCEP. This is assumed to be
// true if a SCEPService exists, which is true in case a
// SCEP provisioner was configured.
// true if a SCEPService exists, which is true in case at
// least one SCEP provisioner was configured.
func (ca *CA) shouldServeSCEPEndpoints() bool {
return ca.auth.GetSCEPService() != nil
return ca.auth.GetSCEP() != nil
}
//nolint:unused // useful for debugging

@ -32,7 +32,7 @@ require (
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
go.step.sm/cli-utils v0.8.0
go.step.sm/crypto v0.35.1
go.step.sm/linkedca v0.20.0
go.step.sm/linkedca v0.20.1
golang.org/x/crypto v0.13.0
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0
golang.org/x/net v0.15.0
@ -43,7 +43,7 @@ require (
)
require (
cloud.google.com/go v0.110.6 // indirect
cloud.google.com/go v0.110.7 // indirect
cloud.google.com/go/compute v1.23.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.1 // indirect
@ -69,19 +69,19 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/go-kit/kit v0.10.0 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-piv/piv-go v1.11.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
github.com/golang/glog v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/certificate-transparency-go v1.1.4 // indirect
github.com/google/certificate-transparency-go v1.1.6 // indirect
github.com/google/go-tpm-tools v0.4.1 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect
@ -136,13 +136,13 @@ require (
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.1.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
// use github.com/smallstep/pkcs7 fork with patches applied
replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20230302202335-4c094085c948
replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20230615175518-7ce6486b74eb

@ -1,7 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.110.6 h1:8uYAkj3YHTP/1iwReuHPxLSbdcyc+dSBbzFMrVwDR6Q=
cloud.google.com/go v0.110.6/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI=
cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o=
cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI=
cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
@ -126,8 +126,9 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WA
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
@ -163,8 +164,8 @@ github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KE
github.com/go-piv/piv-go v1.11.0 h1:5vAaCdRTFSIW4PeqMbnsDlUZ7odMYWnHBDGdmtU/Zhg=
github.com/go-piv/piv-go v1.11.0/go.mod h1:NZ2zmjVkfFaL/CF8cVQ/pXdXtuj110zEKGdJM6fJZZM=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@ -211,8 +212,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/certificate-transparency-go v1.1.4 h1:hCyXHDbtqlr/lMXU0D4WgbalXL0Zk4dSWWMbPV8VrqY=
github.com/google/certificate-transparency-go v1.1.4/go.mod h1:D6lvbfwckhNrbM9WVl1EVeMOyzC19mpIjMOI4nxBHtQ=
github.com/google/certificate-transparency-go v1.1.6 h1:SW5K3sr7ptST/pIvNkSVWMiJqemRmkjJPPT0jzXdOOY=
github.com/google/certificate-transparency-go v1.1.6/go.mod h1:0OJjOsOk+wj6aYQgP7FU0ioQ0AJUmnWPFMqTjQeazPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@ -382,6 +383,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@ -391,8 +393,8 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
@ -457,7 +459,6 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/newrelic/go-agent/v3 v3.25.1 h1:Fa+4apO08bcGJk9aOB0TlnacAOrXS4FzMYJzoG0ihA8=
github.com/newrelic/go-agent/v3 v3.25.1/go.mod h1:MANAXqchXM8ko+EXPZ+6mzX243/lehYwJWq8HOV2ytc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
@ -512,6 +513,7 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
@ -548,8 +550,8 @@ github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2 h1:UIAS
github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
github.com/smallstep/nosql v0.6.0 h1:ur7ysI8s9st0cMXnTvB8tA3+x5Eifmkb6hl4uqNV5jc=
github.com/smallstep/nosql v0.6.0/go.mod h1:jOXwLtockXORUPPZ2MCUcIkGR6w0cN1QGZniY9DITQA=
github.com/smallstep/pkcs7 v0.0.0-20230302202335-4c094085c948 h1:/80FqDt6pzL9clNW8G2IsRAzKGNAuzsEs7g1Y5oaM/Y=
github.com/smallstep/pkcs7 v0.0.0-20230302202335-4c094085c948/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
github.com/smallstep/pkcs7 v0.0.0-20230615175518-7ce6486b74eb h1:wWc8z37baPz2oyusY9BVuM+uPtq6XAOb7qSegevnRs0=
github.com/smallstep/pkcs7 v0.0.0-20230615175518-7ce6486b74eb/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=
@ -619,8 +621,8 @@ go.step.sm/cli-utils v0.8.0 h1:b/Tc1/m3YuQq+u3ghTFP7Dz5zUekZj6GUmd5pCvkEXQ=
go.step.sm/cli-utils v0.8.0/go.mod h1:S77aISrC0pKuflqiDfxxJlUbiXcAanyJ4POOnzFSxD4=
go.step.sm/crypto v0.35.1 h1:QAZZ7Q8xaM4TdungGSAYw/zxpyH4fMYTkfaXVV9H7pY=
go.step.sm/crypto v0.35.1/go.mod h1:vn8Vkx/Mbqgoe7AG8btC0qZ995Udm3e+JySuDS1LCJA=
go.step.sm/linkedca v0.20.0 h1:bH41rvyDm3nSSJ5xgGsKUZOpzJcq5x2zacMIeqtq9oI=
go.step.sm/linkedca v0.20.0/go.mod h1:eybHw6ZTpuFmkUQnTBRWM2SPIGaP0VbYeo1bupfPT70=
go.step.sm/linkedca v0.20.1 h1:bHDn1+UG1NgRrERkWbbCiAIvv4lD5NOFaswPDTyO5vU=
go.step.sm/linkedca v0.20.1/go.mod h1:Vaq4+Umtjh7DLFI1KuIxeo598vfBzgSYZUjgVJ7Syxw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
@ -765,8 +767,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -805,12 +807,12 @@ google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRn
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g=
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8=
google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb h1:XFBgcDwm7irdHTbz4Zk2h7Mh+eis4nfJEFQFYzJzuIA=
google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 h1:nIgk/EEq3/YlnmVVXVnm14rC2oxgs1o0ong4sD/rd44=
google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb h1:Isk1sSH7bovx8Rti2wZK0UZF6oraBDK74uoyLEEVFN0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 h1:N3bU/SQDCDyD6R528GJ/PwW9KjYcJA3dgyH+MovAkIM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:KSqppvjFjtoCI+KGd4PELB0qLNxdJHRGqRI09mB6pQA=
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=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
@ -841,8 +843,8 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=

@ -18,6 +18,7 @@ import (
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/api/log"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/scep"
)
@ -208,7 +209,7 @@ func lookupProvisioner(next http.HandlerFunc) http.HandlerFunc {
}
ctx := r.Context()
auth := scep.MustFromContext(ctx)
auth := authority.MustFromContext(ctx)
p, err := auth.LoadProvisionerByName(provisionerName)
if err != nil {
fail(w, err)
@ -221,7 +222,7 @@ func lookupProvisioner(next http.HandlerFunc) http.HandlerFunc {
return
}
ctx = context.WithValue(ctx, scep.ProvisionerContextKey, scep.Provisioner(prov))
ctx = scep.NewProvisionerContext(ctx, scep.Provisioner(prov))
next(w, r.WithContext(ctx))
}
}
@ -314,7 +315,7 @@ func PKIOperation(ctx context.Context, req request) (Response, error) {
// 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 err := auth.ValidateChallenge(ctx, csr, challengePassword, transactionID); err != nil {
if errors.Is(err, provisioner.ErrSCEPChallengeInvalid) {
return createFailureResponse(ctx, csr, msg, microscep.BadRequest, err)
}
@ -332,9 +333,18 @@ func PKIOperation(ctx context.Context, req request) (Response, error) {
certRep, err := auth.SignCSR(ctx, csr, msg)
if err != nil {
if notifyErr := auth.NotifyFailure(ctx, csr, transactionID, 0, err.Error()); notifyErr != nil {
// TODO(hs): ignore this error case? It's not critical if the notification fails; but logging it might be good
_ = notifyErr
}
return createFailureResponse(ctx, csr, msg, microscep.BadRequest, fmt.Errorf("error when signing new certificate: %w", err))
}
if notifyErr := auth.NotifySuccess(ctx, csr, certRep.Certificate, transactionID); notifyErr != nil {
// TODO(hs): ignore this error case? It's not critical if the notification fails; but logging it might be good
_ = notifyErr
}
res := Response{
Operation: opnPKIOperation,
Data: certRep.Raw,

@ -2,10 +2,11 @@ package scep
import (
"context"
"crypto"
"crypto/x509"
"errors"
"fmt"
"net/url"
"sync"
microx509util "github.com/micromdm/scep/v2/cryptoutil/x509util"
microscep "github.com/micromdm/scep/v2/scep"
@ -18,12 +19,17 @@ import (
// Authority is the layer that handles all SCEP interactions.
type Authority struct {
prefix string
dns string
intermediateCertificate *x509.Certificate
caCerts []*x509.Certificate // TODO(hs): change to use these instead of root and intermediate
service *Service
signAuth SignAuthority
signAuth SignAuthority
roots []*x509.Certificate
intermediates []*x509.Certificate
defaultSigner crypto.Signer
signerCertificate *x509.Certificate
defaultDecrypter crypto.Decrypter
decrypterCertificate *x509.Certificate
scepProvisionerNames []string
provisionersMutex sync.RWMutex
encryptionAlgorithmMutex sync.Mutex
}
type authorityKey struct{}
@ -49,19 +55,6 @@ func MustFromContext(ctx context.Context) *Authority {
}
}
// AuthorityOptions required to create a new SCEP Authority.
type AuthorityOptions struct {
// Service provides the certificate chain, the signer and the decrypter to the Authority
Service *Service
// DNS is the host used to generate accurate SCEP links. By default the authority
// will use the Host from the request, so this value will only be used if
// request.Host is empty.
DNS string
// Prefix is a URL path prefix under which the SCEP api is served. This
// prefix is required to generate accurate SCEP links.
Prefix string
}
// SignAuthority is the interface for a signing authority
type SignAuthority interface {
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
@ -69,24 +62,67 @@ type SignAuthority interface {
}
// New returns a new Authority that implements the SCEP interface.
func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) {
authority := &Authority{
prefix: ops.Prefix,
dns: ops.DNS,
signAuth: signAuth,
func New(signAuth SignAuthority, opts Options) (*Authority, error) {
if err := opts.Validate(); err != nil {
return nil, err
}
return &Authority{
signAuth: signAuth, // TODO: provide signAuth through context instead?
roots: opts.Roots,
intermediates: opts.Intermediates,
defaultSigner: opts.Signer,
signerCertificate: opts.SignerCert,
defaultDecrypter: opts.Decrypter,
decrypterCertificate: opts.SignerCert, // the intermediate signer cert is also the decrypter cert (if RSA)
scepProvisionerNames: opts.SCEPProvisionerNames,
}, nil
}
// Validate validates if the SCEP Authority has a valid configuration.
// The validation includes a check if a decrypter is available, either
// an authority wide decrypter, or a provisioner specific decrypter.
func (a *Authority) Validate() error {
if a == nil {
return nil
}
// TODO: this is not really nice to do; the Service should be removed
// in its entirety to make this more interoperable with the rest of
// step-ca, I think.
if ops.Service != nil {
authority.caCerts = ops.Service.certificateChain
// TODO(hs): look into refactoring SCEP into using just caCerts everywhere, if it makes sense for more elaborate SCEP configuration. Keeping it like this for clarity (for now).
authority.intermediateCertificate = ops.Service.certificateChain[0]
authority.service = ops.Service
a.provisionersMutex.RLock()
defer a.provisionersMutex.RUnlock()
noDefaultDecrypterAvailable := a.defaultDecrypter == nil
for _, name := range a.scepProvisionerNames {
p, err := a.LoadProvisionerByName(name)
if err != nil {
return fmt.Errorf("failed loading provisioner %q: %w", name, err)
}
if scepProv, ok := p.(*provisioner.SCEP); ok {
cert, decrypter := scepProv.GetDecrypter()
// TODO(hs): return sentinel/typed error, to be able to ignore/log these cases during init?
if cert == nil && noDefaultDecrypterAvailable {
return fmt.Errorf("SCEP provisioner %q does not have a decrypter certificate", name)
}
if decrypter == nil && noDefaultDecrypterAvailable {
return fmt.Errorf("SCEP provisioner %q does not have decrypter", name)
}
}
}
return authority, nil
return nil
}
// UpdateProvisioners updates the SCEP Authority with the new, and hopefully
// current SCEP provisioners configured. This allows the Authority to be
// validated with the latest data.
func (a *Authority) UpdateProvisioners(scepProvisionerNames []string) {
if a == nil {
return
}
a.provisionersMutex.Lock()
defer a.provisionersMutex.Unlock()
a.scepProvisionerNames = scepProvisionerNames
}
var (
@ -108,87 +144,58 @@ func (a *Authority) LoadProvisionerByName(name string) (provisioner.Interface, e
return a.signAuth.LoadProvisionerByName(name)
}
// GetLinkExplicit returns the requested link from the directory.
func (a *Authority) GetLinkExplicit(provName string, abs bool, baseURL *url.URL, inputs ...string) string {
return a.getLinkExplicit(provName, abs, baseURL, inputs...)
}
// getLinkExplicit returns an absolute or partial path to the given resource and a base
// URL dynamically obtained from the request for which the link is being calculated.
func (a *Authority) getLinkExplicit(provisionerName string, abs bool, baseURL *url.URL, _ ...string) string {
link := "/" + provisionerName
if abs {
// Copy the baseURL value from the pointer. https://github.com/golang/go/issues/38351
u := url.URL{}
if baseURL != nil {
u = *baseURL
}
// If no Scheme is set, then default to http (in case of SCEP)
if u.Scheme == "" {
u.Scheme = "http"
}
// If no Host is set, then use the default (first DNS attr in the ca.json).
if u.Host == "" {
u.Host = a.dns
}
u.Path = a.prefix + link
return u.String()
}
return link
}
// GetCACertificates returns the certificate (chain) for the CA
func (a *Authority) GetCACertificates(ctx context.Context) ([]*x509.Certificate, error) {
// TODO: this should return: the "SCEP Server (RA)" certificate, the issuing CA up to and excl. the root
// Some clients do need the root certificate however; also see: https://github.com/openxpki/openxpki/issues/73
//
// This means we might need to think about if we should use the current intermediate CA
// certificate as the "SCEP Server (RA)" certificate. It might be better to have a distinct
// RA certificate, with a corresponding rsa.PrivateKey, just for SCEP usage, which is signed by
// the intermediate CA. Will need to look how we can provide this nicely within step-ca.
//
// This might also mean that we might want to use a distinct instance of KMS for doing the key operations,
// so that we can use RSA just for SCEP.
//
// Using an RA does not seem to exist in https://tools.ietf.org/html/rfc8894, but is mentioned in
// https://tools.ietf.org/id/draft-nourse-scep-21.html. Will continue using the CA directly for now.
//
// The certificate to use should probably depend on the (configured) provisioner and may
// use a distinct certificate, apart from the intermediate.
p, err := provisionerFromContext(ctx)
if err != nil {
return nil, err
}
if len(a.caCerts) == 0 {
return nil, errors.New("no intermediate certificate available in SCEP authority")
}
certs := []*x509.Certificate{}
certs = append(certs, a.caCerts[0])
// NOTE: we're adding the CA roots here, but they are (highly likely) different than what the RFC means.
// Clients are responsible to select the right cert(s) to use, though.
if p.ShouldIncludeRootInChain() && len(a.caCerts) > 1 {
certs = append(certs, a.caCerts[1])
// GetCACertificates returns the certificate (chain) for the CA.
//
// This methods returns the "SCEP Server (RA)" certificate, the issuing CA up to and excl. the root.
// Some clients do need the root certificate however; also see: https://github.com/openxpki/openxpki/issues/73
//
// In case a provisioner specific decrypter is available, this is used as the "SCEP Server (RA)" certificate
// instead of the CA intermediate directly. This uses a distinct instance of a KMS for doing the SCEP key
// operations, so that RSA can be used for just SCEP.
//
// Using an RA does not seem to exist in https://tools.ietf.org/html/rfc8894, but is mentioned in
// https://tools.ietf.org/id/draft-nourse-scep-21.html.
func (a *Authority) GetCACertificates(ctx context.Context) (certs []*x509.Certificate, err error) {
p := provisionerFromContext(ctx)
// if a provisioner specific RSA decrypter is available, it is returned as
// the first certificate.
if decrypterCertificate, _ := p.GetDecrypter(); decrypterCertificate != nil {
certs = append(certs, decrypterCertificate)
}
// the CA intermediate is added to the chain by default. It's possible to
// exclude it from being added through configuration. This can be useful in
// environments where the SCEP client doesn't select the right RSA decrypter
// certificate, resulting in the wrong recipient in the PKCS7 message.
if p.ShouldIncludeIntermediateInChain() || len(certs) == 0 {
// TODO(hs): ensure logic is in place that checks the signer is the first
// intermediate and that there are no double certificates.
certs = append(certs, a.intermediates...)
}
// the CA roots are added for completeness when configured to do so. Clients
// are responsible to select the right cert(s) to store and use.
if p.ShouldIncludeRootInChain() {
certs = append(certs, a.roots...)
}
return certs, nil
}
// DecryptPKIEnvelope decrypts an enveloped message
func (a *Authority) DecryptPKIEnvelope(_ context.Context, msg *PKIMessage) error {
func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) error {
p7c, err := pkcs7.Parse(msg.P7.Content)
if err != nil {
return fmt.Errorf("error parsing pkcs7 content: %w", err)
}
envelope, err := p7c.Decrypt(a.intermediateCertificate, a.service.decrypter)
cert, decrypter, err := a.selectDecrypter(ctx)
if err != nil {
return fmt.Errorf("failed selecting decrypter: %w", err)
}
envelope, err := p7c.Decrypt(cert, decrypter)
if err != nil {
return fmt.Errorf("error decrypting encrypted pkcs7 content: %w", err)
}
@ -208,7 +215,10 @@ func (a *Authority) DecryptPKIEnvelope(_ context.Context, msg *PKIMessage) error
if err != nil {
return fmt.Errorf("parse CSR from pkiEnvelope: %w", err)
}
// check for challengePassword
if err := csr.CheckSignature(); err != nil {
return fmt.Errorf("invalid CSR signature; %w", err)
}
// extract the challenge password
cp, err := microx509util.ParseChallengePassword(msg.pkiEnvelope)
if err != nil {
return fmt.Errorf("parse challenge password in pkiEnvelope: %w", err)
@ -234,10 +244,7 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
// poll for the status. It seems to be similar as what can happen in ACME, so might want to model
// the implementation after the one in the ACME authority. Requires storage, etc.
p, err := provisionerFromContext(ctx)
if err != nil {
return nil, err
}
p := provisionerFromContext(ctx)
// check if CSRReqMessage has already been decrypted
if msg.CSRReqMessage.CSR == nil {
@ -307,20 +314,13 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
// and create a degenerate cert structure
deg, err := microscep.DegenerateCertificates([]*x509.Certificate{cert})
if err != nil {
return nil, err
return nil, fmt.Errorf("failed generating degenerate certificate: %w", err)
}
// apparently the pkcs7 library uses a global default setting for the content encryption
// algorithm to use when en- or decrypting data. We need to restore the current setting after
// the cryptographic operation, so that other usages of the library are not influenced by
// this call to Encrypt(). We are not required to use the same algorithm the SCEP client uses.
encryptionAlgorithmToRestore := pkcs7.ContentEncryptionAlgorithm
pkcs7.ContentEncryptionAlgorithm = p.GetContentEncryptionAlgorithm()
e7, err := pkcs7.Encrypt(deg, msg.P7.Certificates)
e7, err := a.encrypt(deg, msg.P7.Certificates, p.GetContentEncryptionAlgorithm())
if err != nil {
return nil, err
return nil, fmt.Errorf("failed encrypting degenerate certificate: %w", err)
}
pkcs7.ContentEncryptionAlgorithm = encryptionAlgorithmToRestore
// PKIMessageAttributes to be signed
config := pkcs7.SignerInfoConfig{
@ -358,10 +358,13 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
// as the first certificate in the array
signedData.AddCertificate(cert)
authCert := a.intermediateCertificate
signerCert, signer, err := a.selectSigner(ctx)
if err != nil {
return nil, fmt.Errorf("failed selecting signer: %w", err)
}
// sign the attributes
if err := signedData.AddSigner(authCert, a.service.signer, config); err != nil {
if err := signedData.AddSigner(signerCert, signer, config); err != nil {
return nil, err
}
@ -388,8 +391,30 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
return crepMsg, nil
}
func (a *Authority) encrypt(content []byte, recipients []*x509.Certificate, algorithm int) ([]byte, error) {
// apparently the pkcs7 library uses a global default setting for the content encryption
// algorithm to use when en- or decrypting data. We need to restore the current setting after
// the cryptographic operation, so that other usages of the library are not influenced by
// this call to Encrypt(). We are not required to use the same algorithm the SCEP client uses.
a.encryptionAlgorithmMutex.Lock()
defer a.encryptionAlgorithmMutex.Unlock()
encryptionAlgorithmToRestore := pkcs7.ContentEncryptionAlgorithm
defer func() {
pkcs7.ContentEncryptionAlgorithm = encryptionAlgorithmToRestore
}()
pkcs7.ContentEncryptionAlgorithm = algorithm
e7, err := pkcs7.Encrypt(content, recipients)
if err != nil {
return nil, err
}
return e7, nil
}
// CreateFailureResponse creates an appropriately signed reply for PKI operations
func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.CertificateRequest, msg *PKIMessage, info FailInfoName, infoText string) (*PKIMessage, error) {
func (a *Authority) CreateFailureResponse(ctx context.Context, _ *x509.CertificateRequest, msg *PKIMessage, info FailInfoName, infoText string) (*PKIMessage, error) {
config := pkcs7.SignerInfoConfig{
ExtraSignedAttributes: []pkcs7.Attribute{
{
@ -428,8 +453,13 @@ func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.Certificate
return nil, err
}
signerCert, signer, err := a.selectSigner(ctx)
if err != nil {
return nil, fmt.Errorf("failed selecting signer: %w", err)
}
// sign the attributes
if err := signedData.AddSigner(a.intermediateCertificate, a.service.signer, config); err != nil {
if err := signedData.AddSigner(signerCert, signer, config); err != nil {
return nil, err
}
@ -457,10 +487,7 @@ func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.Certificate
// GetCACaps returns the CA capabilities
func (a *Authority) GetCACaps(ctx context.Context) []string {
p, err := provisionerFromContext(ctx)
if err != nil {
return defaultCapabilities
}
p := provisionerFromContext(ctx)
caps := p.GetCapabilities()
if len(caps) == 0 {
@ -476,10 +503,63 @@ func (a *Authority) GetCACaps(ctx context.Context) []string {
return caps
}
func (a *Authority) ValidateChallenge(ctx context.Context, challenge, transactionID string) error {
p, err := provisionerFromContext(ctx)
if err != nil {
return err
func (a *Authority) ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) error {
p := provisionerFromContext(ctx)
return p.ValidateChallenge(ctx, csr, challenge, transactionID)
}
func (a *Authority) NotifySuccess(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error {
p := provisionerFromContext(ctx)
return p.NotifySuccess(ctx, csr, cert, transactionID)
}
func (a *Authority) NotifyFailure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error {
p := provisionerFromContext(ctx)
return p.NotifyFailure(ctx, csr, transactionID, errorCode, errorDescription)
}
func (a *Authority) selectDecrypter(ctx context.Context) (cert *x509.Certificate, decrypter crypto.Decrypter, err error) {
p := provisionerFromContext(ctx)
cert, decrypter = p.GetDecrypter()
switch {
case cert != nil && decrypter != nil:
return
case cert == nil && decrypter != nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a decrypter certificate available", p.GetName())
case cert != nil && decrypter == nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a decrypter available", p.GetName())
}
return p.ValidateChallenge(ctx, challenge, transactionID)
cert, decrypter = a.decrypterCertificate, a.defaultDecrypter
switch {
case cert == nil && decrypter != nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a default decrypter certificate available", p.GetName())
case cert != nil && decrypter == nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a default decrypter available", p.GetName())
}
return
}
func (a *Authority) selectSigner(ctx context.Context) (cert *x509.Certificate, signer crypto.Signer, err error) {
p := provisionerFromContext(ctx)
cert, signer = p.GetSigner()
switch {
case cert != nil && signer != nil:
return
case cert == nil && signer != nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a signer certificate available", p.GetName())
case cert != nil && signer == nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a signer available", p.GetName())
}
cert, signer = a.signerCertificate, a.defaultSigner
switch {
case cert == nil && signer != nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a default signer certificate available", p.GetName())
case cert != nil && signer == nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a default signer available", p.GetName())
}
return
}

@ -0,0 +1,73 @@
package scep
import (
"crypto/x509"
"crypto/x509/pkix"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mozilla.org/pkcs7"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/minica"
"go.step.sm/crypto/randutil"
)
func generateContent(t *testing.T, size int) []byte {
t.Helper()
b, err := randutil.Bytes(size)
require.NoError(t, err)
return b
}
func generateRecipients(t *testing.T) []*x509.Certificate {
ca, err := minica.New()
require.NoError(t, err)
s, err := keyutil.GenerateSigner("RSA", "", 2048)
require.NoError(t, err)
tmpl := &x509.Certificate{
PublicKey: s.Public(),
Subject: pkix.Name{CommonName: "Test PKCS#7 Encryption"},
}
cert, err := ca.Sign(tmpl)
require.NoError(t, err)
return []*x509.Certificate{cert}
}
func TestAuthority_encrypt(t *testing.T) {
t.Parallel()
a := &Authority{}
recipients := generateRecipients(t)
type args struct {
content []byte
recipients []*x509.Certificate
algorithm int
}
tests := []struct {
name string
args args
wantErr bool
}{
{"alg-0", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmDESCBC}, false},
{"alg-1", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES128CBC}, false},
{"alg-2", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES256CBC}, false},
{"alg-3", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES128GCM}, false},
{"alg-4", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES256GCM}, false},
{"alg-unknown", args{generateContent(t, 32), recipients, 42}, true},
}
for _, tt := range tests {
tc := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := a.encrypt(tc.args.content, tc.args.recipients, tc.args.algorithm)
if tc.wantErr {
assert.Error(t, err)
assert.Nil(t, got)
return
}
assert.NoError(t, err)
assert.NotEmpty(t, got)
})
}
}

@ -1,29 +0,0 @@
package scep
import (
"context"
"errors"
)
// ContextKey is the key type for storing and searching for SCEP request
// essentials in the context of a request.
type ContextKey string
const (
// ProvisionerContextKey provisioner key
ProvisionerContextKey = ContextKey("provisioner")
)
// provisionerFromContext searches the context for a SCEP provisioner.
// Returns the provisioner or an error.
func provisionerFromContext(ctx context.Context) (Provisioner, error) {
val := ctx.Value(ProvisionerContextKey)
if val == nil {
return nil, errors.New("provisioner expected in request context")
}
p, ok := val.(Provisioner)
if !ok || p == nil {
return nil, errors.New("provisioner in context is not a SCEP provisioner")
}
return p, nil
}

@ -1,7 +0,0 @@
package scep
import "crypto/x509"
type DB interface {
StoreCertificate(crt *x509.Certificate) error
}

@ -4,65 +4,78 @@ import (
"crypto"
"crypto/rsa"
"crypto/x509"
"github.com/pkg/errors"
"errors"
)
type Options struct {
// CertificateChain is the issuer certificate, along with any other bundled certificates
// to be returned in the chain for consumers. Configured in the ca.json crt property.
CertificateChain []*x509.Certificate
// Roots contains the (federated) CA roots certificate(s)
Roots []*x509.Certificate `json:"-"`
// Intermediates points issuer certificate, along with any other bundled certificates
// to be returned in the chain for consumers.
Intermediates []*x509.Certificate `json:"-"`
// SignerCert points to the certificate of the CA signer. It usually is the same as the
// first certificate in the CertificateChain.
SignerCert *x509.Certificate `json:"-"`
// Signer signs CSRs in SCEP. Configured in the ca.json key property.
Signer crypto.Signer `json:"-"`
// Decrypter decrypts encrypted SCEP messages. Configured in the ca.json key property.
Decrypter crypto.Decrypter `json:"-"`
// DecrypterCert points to the certificate of the CA decrypter.
DecrypterCert *x509.Certificate `json:"-"`
// SCEPProvisionerNames contains the currently configured SCEP provioner names. These
// are used to be able to load the provisioners when the SCEP authority is being
// validated.
SCEPProvisionerNames []string
}
type comparablePublicKey interface {
Equal(crypto.PublicKey) bool
}
// Validate checks the fields in Options.
func (o *Options) Validate() error {
if o.CertificateChain == nil {
return errors.New("certificate chain not configured correctly")
switch {
case len(o.Intermediates) == 0:
return errors.New("no intermediate certificate available for SCEP authority")
case o.Signer == nil:
return errors.New("no signer available for SCEP authority")
case o.SignerCert == nil:
return errors.New("no signer certificate available for SCEP authority")
}
if len(o.CertificateChain) < 1 {
return errors.New("certificate chain should at least have one certificate")
// check if the signer (intermediate CA) certificate has the same public key as
// the signer. According to the RFC it seems valid to have different keys for
// the intermediate and the CA signing new certificates, so this might change
// in the future.
signerPublicKey := o.Signer.Public().(comparablePublicKey)
if !signerPublicKey.Equal(o.SignerCert.PublicKey) {
return errors.New("mismatch between signer certificate and public key")
}
// According to the RFC: https://tools.ietf.org/html/rfc8894#section-3.1, SCEP
// can be used with something different than RSA, but requires the encryption
// to be performed using the challenge password. An older version of specification
// states that only RSA is supported: https://tools.ietf.org/html/draft-nourse-scep-23#section-2.1.1
// Other algorithms than RSA do not seem to be supported in certnanny/sscep, but it might work
// in micromdm/scep. Currently only RSA is allowed, but it might be an option
// to try other algorithms in the future.
intermediate := o.CertificateChain[0]
if intermediate.PublicKeyAlgorithm != x509.RSA {
return errors.New("only the RSA algorithm is (currently) supported")
}
// TODO: add checks for key usage?
signerPublicKey, ok := o.Signer.Public().(*rsa.PublicKey)
if !ok {
return errors.New("only RSA public keys are (currently) supported as signers")
}
// check if the intermediate ca certificate has the same public key as the signer.
// According to the RFC it seems valid to have different keys for the intermediate
// and the CA signing new certificates, so this might change in the future.
if !signerPublicKey.Equal(intermediate.PublicKey) {
return errors.New("mismatch between certificate chain and signer public keys")
// decrypter can be nil in case a signing only key is used; validation complete.
if o.Decrypter == nil {
return nil
}
// If a decrypter is available, check that it's backed by an RSA key. According to the
// RFC: https://tools.ietf.org/html/rfc8894#section-3.1, SCEP can be used with something
// different than RSA, but requires the encryption to be performed using the challenge
// password in that case. An older version of specification states that only RSA is
// supported: https://tools.ietf.org/html/draft-nourse-scep-23#section-2.1.1. Other
// algorithms do not seem to be supported in certnanny/sscep, but it might work
// in micromdm/scep. Currently only RSA is allowed, but it might be an option
// to try other algorithms in the future.
decrypterPublicKey, ok := o.Decrypter.Public().(*rsa.PublicKey)
if !ok {
return errors.New("only RSA public keys are (currently) supported as decrypters")
return errors.New("only RSA keys are (currently) supported as decrypters")
}
// check if intermediate public key is the same as the decrypter public key.
// In certnanny/sscep it's mentioned that the signing key can be different
// from the decrypting (and encrypting) key. Currently that's not supported.
if !decrypterPublicKey.Equal(intermediate.PublicKey) {
// from the decrypting (and encrypting) key. These options are only used and
// validated when the intermediate CA is also used as the decrypter, though,
// so they should match.
if !decrypterPublicKey.Equal(o.SignerCert.PublicKey) {
return errors.New("mismatch between certificate chain and decrypter public keys")
}

@ -2,20 +2,43 @@ package scep
import (
"context"
"time"
"crypto"
"crypto/x509"
"github.com/smallstep/certificates/authority/provisioner"
)
// Provisioner is an interface that implements a subset of the provisioner.Interface --
// only those methods required by the SCEP api/authority.
// Provisioner is an interface that embeds the
// provisioner.Interface and adds some SCEP specific
// functions.
type Provisioner interface {
AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error)
GetName() string
DefaultTLSCertDuration() time.Duration
provisioner.Interface
GetOptions() *provisioner.Options
GetCapabilities() []string
ShouldIncludeRootInChain() bool
ShouldIncludeIntermediateInChain() bool
GetDecrypter() (*x509.Certificate, crypto.Decrypter)
GetSigner() (*x509.Certificate, crypto.Signer)
GetContentEncryptionAlgorithm() int
ValidateChallenge(ctx context.Context, challenge, transactionID string) error
ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) error
NotifySuccess(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error
NotifyFailure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error
}
// provisionerKey is the key type for storing and searching a
// SCEP provisioner in the context.
type provisionerKey struct{}
// provisionerFromContext searches the context for a SCEP provisioner.
// Returns the provisioner or panics if no SCEP provisioner is found.
func provisionerFromContext(ctx context.Context) Provisioner {
p, ok := ctx.Value(provisionerKey{}).(Provisioner)
if !ok {
panic("SCEP provisioner expected in request context")
}
return p
}
func NewProvisionerContext(ctx context.Context, p Provisioner) context.Context {
return context.WithValue(ctx, provisionerKey{}, p)
}

@ -1,28 +0,0 @@
package scep
import (
"context"
"crypto"
"crypto/x509"
)
// Service is a wrapper for crypto.Signer and crypto.Decrypter
type Service struct {
certificateChain []*x509.Certificate
signer crypto.Signer
decrypter crypto.Decrypter
}
// NewService returns a new Service type.
func NewService(_ context.Context, opts Options) (*Service, error) {
if err := opts.Validate(); err != nil {
return nil, err
}
// TODO: should this become similar to the New CertificateAuthorityService as in x509CAService?
return &Service{
certificateChain: opts.CertificateChain,
signer: opts.Signer,
decrypter: opts.Decrypter,
}, nil
}

@ -30,6 +30,7 @@ type X509Certificate struct {
PublicKeyAlgorithm string `json:"publicKeyAlgorithm"`
NotBefore time.Time `json:"notBefore"`
NotAfter time.Time `json:"notAfter"`
Raw []byte `json:"raw"`
}
// SSHCertificateRequest is the certificate request sent to webhook servers for
@ -79,9 +80,11 @@ type RequestBody struct {
X509Certificate *X509Certificate `json:"x509Certificate,omitempty"`
SSHCertificateRequest *SSHCertificateRequest `json:"sshCertificateRequest,omitempty"`
SSHCertificate *SSHCertificate `json:"sshCertificate,omitempty"`
// Only set for SCEP challenge validation requests
SCEPChallenge string `json:"scepChallenge,omitempty"`
SCEPTransactionID string `json:"scepTransactionID,omitempty"`
// Only set for SCEP webhook requests
SCEPChallenge string `json:"scepChallenge,omitempty"`
SCEPTransactionID string `json:"scepTransactionID,omitempty"`
SCEPErrorCode int `json:"scepErrorCode,omitempty"`
SCEPErrorDescription string `json:"scepErrorDescription,omitempty"`
// Only set for X5C provisioners
X5CCertificate *X5CCertificate `json:"x5cCertificate,omitempty"`
// Set for X5C, AWS, GCP, and Azure provisioners

Loading…
Cancel
Save