Skip to content

Commit 0377fe5

Browse files
committed
Add basic version of provisioner specific SCEP decrypter
1 parent 2ef45a2 commit 0377fe5

File tree

9 files changed

+184
-57
lines changed

9 files changed

+184
-57
lines changed

api/api.go

+18-4
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,25 @@ func (p ProvisionersResponse) MarshalJSON() ([]byte, error) {
244244
continue
245245
}
246246

247-
old := scepProv.ChallengePassword
247+
type old struct {
248+
challengePassword string
249+
decrypterCertificate string
250+
decrypterKey string
251+
decrypterKeyPassword string
252+
}
253+
o := old{scepProv.ChallengePassword, scepProv.DecrypterCert, scepProv.DecrypterKey, scepProv.DecrypterKeyPassword}
248254
scepProv.ChallengePassword = "*** REDACTED ***"
249-
defer func(p string) { //nolint:gocritic // defer in loop required to restore initial state of provisioners
250-
scepProv.ChallengePassword = p
251-
}(old)
255+
// TODO: remove the details in the API response
256+
// scepProv.DecrypterCert = ""
257+
// scepProv.DecrypterKey = ""
258+
// scepProv.DecrtyperKeyPassword = ""
259+
260+
defer func(o old) { //nolint:gocritic // defer in loop required to restore initial state of provisioners
261+
scepProv.ChallengePassword = o.challengePassword
262+
scepProv.DecrypterCert = o.decrypterCertificate
263+
scepProv.DecrypterKey = o.decrypterKey
264+
scepProv.DecrypterKeyPassword = o.decrypterKeyPassword
265+
}(o)
252266
}
253267

254268
var list = struct {

authority/authority.go

+20-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"crypto"
7+
"crypto/rsa"
78
"crypto/sha256"
89
"crypto/x509"
910
"encoding/hex"
@@ -666,13 +667,30 @@ func (a *Authority) init() error {
666667
return err
667668
}
668669

670+
options.SignerCert = options.CertificateChain[0]
671+
options.DecrypterCert = options.CertificateChain[0]
672+
673+
// TODO: instead of creating the decrypter here, pass the
674+
// intermediate key + chain down to the SCEP service / authority,
675+
// and only instantiate it when required there.
676+
// TODO: if moving the logic, try improving the logic for the
677+
// decrypter password too?
669678
if km, ok := a.keyManager.(kmsapi.Decrypter); ok {
670679
options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
671680
DecryptionKey: a.config.IntermediateKey,
672681
Password: a.password,
673682
})
674-
if err != nil {
675-
return err
683+
if err == nil {
684+
// when creating the decrypter fails, ignore the error
685+
// TODO(hs): decide if this is OK. It could fail at startup, but it
686+
// could be up later. Right now decryption would always fail.
687+
key, ok := options.Decrypter.Public().(*rsa.PublicKey)
688+
if !ok {
689+
return errors.New("only RSA keys are currently supported as decrypters")
690+
}
691+
if !key.Equal(options.DecrypterCert.PublicKey) {
692+
return errors.New("mismatch between decryption certificate and decrypter public keys")
693+
}
676694
}
677695
}
678696

authority/provisioner/scep.go

+47
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@ package provisioner
22

33
import (
44
"context"
5+
"crypto"
6+
"crypto/rsa"
57
"crypto/subtle"
8+
"crypto/x509"
69
"fmt"
710
"net/http"
811
"time"
912

1013
"github.com/pkg/errors"
1114

15+
"go.step.sm/crypto/kms"
16+
kmsapi "go.step.sm/crypto/kms/apiv1"
17+
"go.step.sm/crypto/pemutil"
1218
"go.step.sm/linkedca"
1319

1420
"github.com/smallstep/certificates/webhook"
@@ -32,6 +38,12 @@ type SCEP struct {
3238
// MinimumPublicKeyLength is the minimum length for public keys in CSRs
3339
MinimumPublicKeyLength int `json:"minimumPublicKeyLength,omitempty"`
3440

41+
// TODO
42+
KMS *kms.Options `json:"kms,omitempty"`
43+
DecrypterCert string `json:"decrypterCert"`
44+
DecrypterKey string `json:"decrypterKey"`
45+
DecrypterKeyPassword string `json:"decrypterKeyPassword"`
46+
3547
// Numerical identifier for the ContentEncryptionAlgorithm as defined in github.com/mozilla-services/pkcs7
3648
// at https://github.com/mozilla-services/pkcs7/blob/33d05740a3526e382af6395d3513e73d4e66d1cb/encrypt.go#L63
3749
// Defaults to 0, being DES-CBC
@@ -41,6 +53,9 @@ type SCEP struct {
4153
ctl *Controller
4254
encryptionAlgorithm int
4355
challengeValidationController *challengeValidationController
56+
keyManager kmsapi.KeyManager
57+
decrypter crypto.Decrypter
58+
decrypterCertificate *x509.Certificate
4459
}
4560

4661
// GetID returns the provisioner unique identifier.
@@ -177,6 +192,34 @@ func (s *SCEP) Init(config Config) (err error) {
177192
s.GetOptions().GetWebhooks(),
178193
)
179194

195+
if s.KMS != nil {
196+
if s.keyManager, err = kms.New(context.Background(), *s.KMS); err != nil {
197+
return fmt.Errorf("failed initializing kms: %w", err)
198+
}
199+
km, ok := s.keyManager.(kmsapi.Decrypter)
200+
if !ok {
201+
return fmt.Errorf(`%q is not a kmsapi.Decrypter`, s.KMS.Type)
202+
}
203+
if s.DecrypterKey != "" || s.DecrypterCert != "" {
204+
if s.decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
205+
DecryptionKey: s.DecrypterKey,
206+
Password: []byte(s.DecrypterKeyPassword),
207+
}); err != nil {
208+
return fmt.Errorf("failed creating decrypter: %w", err)
209+
}
210+
if s.decrypterCertificate, err = pemutil.ReadCertificate(s.DecrypterCert); err != nil {
211+
return fmt.Errorf("failed reading certificate: %w", err)
212+
}
213+
decrypterPublicKey, ok := s.decrypter.Public().(*rsa.PublicKey)
214+
if !ok {
215+
return fmt.Errorf("only RSA keys are supported")
216+
}
217+
if !decrypterPublicKey.Equal(s.decrypterCertificate.PublicKey) {
218+
return errors.New("mismatch between decryption certificate and decrypter public keys")
219+
}
220+
}
221+
}
222+
180223
// TODO: add other, SCEP specific, options?
181224

182225
s.ctl, err = NewController(s, s.Claims, config, s.Options)
@@ -259,3 +302,7 @@ func (s *SCEP) selectValidationMethod() validationMethod {
259302
}
260303
return validationMethodNone
261304
}
305+
306+
func (s *SCEP) GetDecrypter() (*x509.Certificate, crypto.Decrypter) {
307+
return s.decrypterCertificate, s.decrypter
308+
}

authority/provisioner/webhook.go

+3
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ retry:
152152
return nil, err
153153
}
154154

155+
fmt.Println(req)
156+
155157
secret, err := base64.StdEncoding.DecodeString(w.Secret)
156158
if err != nil {
157159
return nil, err
@@ -201,6 +203,7 @@ retry:
201203
time.Sleep(time.Second)
202204
goto retry
203205
}
206+
fmt.Println(fmt.Sprintf("%#+v", resp))
204207
if resp.StatusCode >= 400 {
205208
return nil, fmt.Errorf("Webhook server responded with %d", resp.StatusCode)
206209
}

scep/api/api.go

+3
Original file line numberDiff line numberDiff line change
@@ -308,13 +308,16 @@ func PKIOperation(ctx context.Context, req request) (Response, error) {
308308
transactionID := string(msg.TransactionID)
309309
challengePassword := msg.CSRReqMessage.ChallengePassword
310310

311+
fmt.Println("challenge password: ", challengePassword)
312+
311313
// NOTE: we're blocking the RenewalReq if the challenge does not match, because otherwise we don't have any authentication.
312314
// The macOS SCEP client performs renewals using PKCSreq. The CertNanny SCEP client will use PKCSreq with challenge too, it seems,
313315
// even if using the renewal flow as described in the README.md. MicroMDM SCEP client also only does PKCSreq by default, unless
314316
// a certificate exists; then it will use RenewalReq. Adding the challenge check here may be a small breaking change for clients.
315317
// We'll have to see how it works out.
316318
if msg.MessageType == microscep.PKCSReq || msg.MessageType == microscep.RenewalReq {
317319
if err := auth.ValidateChallenge(ctx, challengePassword, transactionID); err != nil {
320+
fmt.Println(err)
318321
if errors.Is(err, provisioner.ErrSCEPChallengeInvalid) {
319322
return createFailureResponse(ctx, csr, msg, microscep.BadRequest, err)
320323
}

scep/authority.go

+53-26
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package scep
22

33
import (
44
"context"
5+
"crypto"
56
"crypto/x509"
67
"errors"
78
"fmt"
@@ -18,12 +19,10 @@ import (
1819

1920
// Authority is the layer that handles all SCEP interactions.
2021
type Authority struct {
21-
prefix string
22-
dns string
23-
intermediateCertificate *x509.Certificate
24-
caCerts []*x509.Certificate // TODO(hs): change to use these instead of root and intermediate
25-
service *Service
26-
signAuth SignAuthority
22+
prefix string
23+
dns string
24+
service *Service
25+
signAuth SignAuthority
2726
}
2827

2928
type authorityKey struct{}
@@ -74,18 +73,8 @@ func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) {
7473
prefix: ops.Prefix,
7574
dns: ops.DNS,
7675
signAuth: signAuth,
76+
service: ops.Service,
7777
}
78-
79-
// TODO: this is not really nice to do; the Service should be removed
80-
// in its entirety to make this more interoperable with the rest of
81-
// step-ca, I think.
82-
if ops.Service != nil {
83-
authority.caCerts = ops.Service.certificateChain
84-
// 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).
85-
authority.intermediateCertificate = ops.Service.certificateChain[0]
86-
authority.service = ops.Service
87-
}
88-
8978
return authority, nil
9079
}
9180

@@ -165,30 +154,46 @@ func (a *Authority) GetCACertificates(ctx context.Context) ([]*x509.Certificate,
165154
return nil, err
166155
}
167156

168-
if len(a.caCerts) == 0 {
157+
if len(a.service.certificateChain) == 0 {
169158
return nil, errors.New("no intermediate certificate available in SCEP authority")
170159
}
171160

172161
certs := []*x509.Certificate{}
173-
certs = append(certs, a.caCerts[0])
162+
if decrypterCertificate, _ := p.GetDecrypter(); decrypterCertificate != nil {
163+
certs = append(certs, decrypterCertificate)
164+
certs = append(certs, a.service.signerCertificate)
165+
} else {
166+
certs = append(certs, a.service.defaultDecrypterCertificate)
167+
}
174168

175169
// NOTE: we're adding the CA roots here, but they are (highly likely) different than what the RFC means.
176170
// Clients are responsible to select the right cert(s) to use, though.
177-
if p.ShouldIncludeRootInChain() && len(a.caCerts) > 1 {
178-
certs = append(certs, a.caCerts[1])
171+
if p.ShouldIncludeRootInChain() && len(a.service.certificateChain) > 1 {
172+
certs = append(certs, a.service.certificateChain[1])
179173
}
180174

181175
return certs, nil
182176
}
183177

184178
// DecryptPKIEnvelope decrypts an enveloped message
185-
func (a *Authority) DecryptPKIEnvelope(_ context.Context, msg *PKIMessage) error {
179+
func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) error {
186180
p7c, err := pkcs7.Parse(msg.P7.Content)
187181
if err != nil {
188182
return fmt.Errorf("error parsing pkcs7 content: %w", err)
189183
}
190184

191-
envelope, err := p7c.Decrypt(a.intermediateCertificate, a.service.decrypter)
185+
fmt.Println(fmt.Sprintf("%#+v", a.service.defaultDecrypterCertificate))
186+
fmt.Println(fmt.Sprintf("%#+v", a.service.defaultDecrypter))
187+
188+
cert, pkey, err := a.selectDecrypter(ctx)
189+
if err != nil {
190+
return fmt.Errorf("failed selecting decrypter: %w", err)
191+
}
192+
193+
fmt.Println(fmt.Sprintf("%#+v", cert))
194+
fmt.Println(fmt.Sprintf("%#+v", pkey))
195+
196+
envelope, err := p7c.Decrypt(cert, pkey)
192197
if err != nil {
193198
return fmt.Errorf("error decrypting encrypted pkcs7 content: %w", err)
194199
}
@@ -208,6 +213,9 @@ func (a *Authority) DecryptPKIEnvelope(_ context.Context, msg *PKIMessage) error
208213
if err != nil {
209214
return fmt.Errorf("parse CSR from pkiEnvelope: %w", err)
210215
}
216+
if err := csr.CheckSignature(); err != nil {
217+
return fmt.Errorf("invalid CSR signature; %w", err)
218+
}
211219
// check for challengePassword
212220
cp, err := microx509util.ParseChallengePassword(msg.pkiEnvelope)
213221
if err != nil {
@@ -226,6 +234,24 @@ func (a *Authority) DecryptPKIEnvelope(_ context.Context, msg *PKIMessage) error
226234
return nil
227235
}
228236

237+
func (a *Authority) selectDecrypter(ctx context.Context) (cert *x509.Certificate, pkey crypto.PrivateKey, err error) {
238+
p, err := provisionerFromContext(ctx)
239+
if err != nil {
240+
return nil, nil, err
241+
}
242+
243+
// return provisioner specific decrypter, if available
244+
if cert, pkey = p.GetDecrypter(); cert != nil && pkey != nil {
245+
return
246+
}
247+
248+
// fallback to the CA wide decrypter
249+
cert = a.service.defaultDecrypterCertificate
250+
pkey = a.service.defaultDecrypter
251+
252+
return
253+
}
254+
229255
// SignCSR creates an x509.Certificate based on a CSR template and Cert Authority credentials
230256
// returns a new PKIMessage with CertRep data
231257
func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, msg *PKIMessage) (*PKIMessage, error) {
@@ -358,10 +384,11 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
358384
// as the first certificate in the array
359385
signedData.AddCertificate(cert)
360386

361-
authCert := a.intermediateCertificate
387+
authCert := a.service.signerCertificate
388+
signer := a.service.signer
362389

363390
// sign the attributes
364-
if err := signedData.AddSigner(authCert, a.service.signer, config); err != nil {
391+
if err := signedData.AddSigner(authCert, signer, config); err != nil {
365392
return nil, err
366393
}
367394

@@ -429,7 +456,7 @@ func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.Certificate
429456
}
430457

431458
// sign the attributes
432-
if err := signedData.AddSigner(a.intermediateCertificate, a.service.signer, config); err != nil {
459+
if err := signedData.AddSigner(a.service.signerCertificate, a.service.signer, config); err != nil {
433460
return nil, err
434461
}
435462

0 commit comments

Comments
 (0)