2021-02-12 11:03:08 +00:00
package scep
import (
2021-02-26 11:32:43 +00:00
"context"
2021-03-21 15:42:41 +00:00
"crypto/subtle"
2021-02-12 11:03:08 +00:00
"crypto/x509"
2022-03-24 15:08:23 +00:00
"errors"
"fmt"
2021-03-05 11:40:42 +00:00
"net/url"
2021-02-12 11:03:08 +00:00
2021-03-26 21:04:18 +00:00
microx509util "github.com/micromdm/scep/v2/cryptoutil/x509util"
microscep "github.com/micromdm/scep/v2/scep"
2021-02-25 23:55:37 +00:00
"go.mozilla.org/pkcs7"
2021-02-25 23:32:21 +00:00
"go.step.sm/crypto/x509util"
2021-02-12 11:03:08 +00:00
2022-03-24 15:08:23 +00:00
"github.com/smallstep/certificates/authority/provisioner"
)
2021-02-12 11:03:08 +00:00
// Authority is the layer that handles all SCEP interactions.
type Authority struct {
2021-03-21 15:42:41 +00:00
prefix string
dns string
2021-02-12 16:02:39 +00:00
intermediateCertificate * x509 . Certificate
2022-01-14 09:48:23 +00:00
caCerts [ ] * x509 . Certificate // TODO(hs): change to use these instead of root and intermediate
2021-03-21 15:42:41 +00:00
service * Service
signAuth SignAuthority
2021-02-12 11:03:08 +00:00
}
2022-04-28 01:02:37 +00:00
type authorityKey struct { }
// NewContext adds the given authority to the context.
func NewContext ( ctx context . Context , a * Authority ) context . Context {
return context . WithValue ( ctx , authorityKey { } , a )
}
// FromContext returns the current authority from the given context.
func FromContext ( ctx context . Context ) ( a * Authority , ok bool ) {
a , ok = ctx . Value ( authorityKey { } ) . ( * Authority )
return
}
// MustFromContext returns the current authority from the given context. It will
// panic if the authority is not in the context.
func MustFromContext ( ctx context . Context ) * Authority {
if a , ok := FromContext ( ctx ) ; ! ok {
panic ( "scep authority is not in the context" )
} else {
return a
}
}
2021-02-12 11:03:08 +00:00
// AuthorityOptions required to create a new SCEP Authority.
type AuthorityOptions struct {
2021-03-21 15:42:41 +00:00
// Service provides the certificate chain, the signer and the decrypter to the Authority
2021-03-12 15:58:52 +00:00
Service * Service
2021-03-21 15:42:41 +00:00
// DNS is the host used to generate accurate SCEP links. By default the authority
2021-02-12 11:03:08 +00:00
// 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
}
2021-02-25 23:32:21 +00:00
// SignAuthority is the interface for a signing authority
2021-02-12 11:03:08 +00:00
type SignAuthority interface {
Sign ( cr * x509 . CertificateRequest , opts provisioner . SignOptions , signOpts ... provisioner . SignOption ) ( [ ] * x509 . Certificate , error )
2022-01-21 15:07:31 +00:00
LoadProvisionerByName ( string ) ( provisioner . Interface , error )
2021-02-12 11:03:08 +00:00
}
2021-02-12 16:02:39 +00:00
// New returns a new Authority that implements the SCEP interface.
func New ( signAuth SignAuthority , ops AuthorityOptions ) ( * Authority , error ) {
2021-03-12 15:58:52 +00:00
authority := & Authority {
prefix : ops . Prefix ,
dns : ops . DNS ,
signAuth : signAuth ,
}
// 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
2021-03-21 15:42:41 +00:00
// step-ca, I think.
2021-03-12 15:58:52 +00:00
if ops . Service != nil {
2022-01-14 09:48:23 +00:00
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).
2021-03-12 15:58:52 +00:00
authority . intermediateCertificate = ops . Service . certificateChain [ 0 ]
authority . service = ops . Service
}
return authority , nil
2021-02-12 16:02:39 +00:00
}
2021-03-06 23:50:00 +00:00
var (
// TODO: check the default capabilities; https://tools.ietf.org/html/rfc8894#section-3.5.2
defaultCapabilities = [ ] string {
2022-01-14 09:48:23 +00:00
"Renewal" , // NOTE: removing this will result in macOS SCEP client stating the server doesn't support renewal, but it uses PKCSreq to do so.
2021-03-06 23:50:00 +00:00
"SHA-1" ,
"SHA-256" ,
"AES" ,
"DES3" ,
"SCEPStandard" ,
"POSTPKIOperation" ,
}
)
2022-01-21 15:07:31 +00:00
// LoadProvisionerByName calls out to the SignAuthority interface to load a
// provisioner by name.
func ( a * Authority ) LoadProvisionerByName ( name string ) ( provisioner . Interface , error ) {
return a . signAuth . LoadProvisionerByName ( name )
}
2021-03-05 11:40:42 +00:00
// 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 , inputs ... string ) string {
2021-03-26 15:11:35 +00:00
link := "/" + provisionerName
2021-03-05 11:40:42 +00:00
if abs {
// Copy the baseURL value from the pointer. https://github.com/golang/go/issues/38351
u := url . URL { }
if baseURL != nil {
u = * baseURL
}
2021-03-06 23:50:00 +00:00
// If no Scheme is set, then default to http (in case of SCEP)
2021-03-05 11:40:42 +00:00
if u . Scheme == "" {
2021-03-06 23:50:00 +00:00
u . Scheme = "http"
2021-03-05 11:40:42 +00:00
}
// 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
}
2021-02-25 23:32:21 +00:00
// GetCACertificates returns the certificate (chain) for the CA
2022-01-14 09:48:23 +00:00
func ( a * Authority ) GetCACertificates ( ctx context . Context ) ( [ ] * x509 . Certificate , error ) {
2021-02-25 23:32:21 +00:00
// 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.
2021-03-06 23:50:00 +00:00
//
2022-01-14 09:48:23 +00:00
// The certificate to use should probably depend on the (configured) provisioner and may
2021-03-06 23:50:00 +00:00
// use a distinct certificate, apart from the intermediate.
2021-02-25 23:32:21 +00:00
2023-04-28 13:47:22 +00:00
p , err := ProvisionerFromContext ( ctx )
2022-01-14 09:48:23 +00:00
if err != nil {
return nil , err
}
if len ( a . caCerts ) == 0 {
2021-02-25 23:32:21 +00:00
return nil , errors . New ( "no intermediate certificate available in SCEP authority" )
}
2022-01-14 09:48:23 +00:00
certs := [ ] * x509 . Certificate { }
certs = append ( certs , a . caCerts [ 0 ] )
2022-01-18 14:54:18 +00:00
// 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.
2022-01-19 10:31:33 +00:00
if p . ShouldIncludeRootInChain ( ) && len ( a . caCerts ) > 1 {
certs = append ( certs , a . caCerts [ 1 ] )
2022-01-14 09:48:23 +00:00
}
return certs , nil
2021-02-25 23:32:21 +00:00
}
// DecryptPKIEnvelope decrypts an enveloped message
2021-02-26 11:32:43 +00:00
func ( a * Authority ) DecryptPKIEnvelope ( ctx context . Context , msg * PKIMessage ) error {
2021-03-06 22:24:49 +00:00
p7c , err := pkcs7 . Parse ( msg . P7 . Content )
2021-02-25 23:32:21 +00:00
if err != nil {
2022-03-24 15:08:23 +00:00
return fmt . Errorf ( "error parsing pkcs7 content: %w" , err )
2021-02-25 23:32:21 +00:00
}
2021-03-12 14:49:39 +00:00
envelope , err := p7c . Decrypt ( a . intermediateCertificate , a . service . decrypter )
2021-02-25 23:32:21 +00:00
if err != nil {
2022-03-24 15:08:23 +00:00
return fmt . Errorf ( "error decrypting encrypted pkcs7 content: %w" , err )
2021-02-25 23:32:21 +00:00
}
msg . pkiEnvelope = envelope
switch msg . MessageType {
case microscep . CertRep :
certs , err := microscep . CACerts ( msg . pkiEnvelope )
if err != nil {
2022-03-24 15:08:23 +00:00
return fmt . Errorf ( "error extracting CA certs from pkcs7 degenerate data: %w" , err )
2021-02-25 23:32:21 +00:00
}
2021-03-21 15:42:41 +00:00
msg . CertRepMessage . Certificate = certs [ 0 ]
2021-02-25 23:32:21 +00:00
return nil
case microscep . PKCSReq , microscep . UpdateReq , microscep . RenewalReq :
csr , err := x509 . ParseCertificateRequest ( msg . pkiEnvelope )
if err != nil {
2022-03-24 15:08:23 +00:00
return fmt . Errorf ( "parse CSR from pkiEnvelope: %w" , err )
2021-02-25 23:32:21 +00:00
}
// check for challengePassword
cp , err := microx509util . ParseChallengePassword ( msg . pkiEnvelope )
if err != nil {
2022-03-24 15:08:23 +00:00
return fmt . Errorf ( "parse challenge password in pkiEnvelope: %w" , err )
2021-02-25 23:32:21 +00:00
}
msg . CSRReqMessage = & microscep . CSRReqMessage {
RawDecrypted : msg . pkiEnvelope ,
CSR : csr ,
ChallengePassword : cp ,
}
return nil
case microscep . GetCRL , microscep . GetCert , microscep . CertPoll :
2022-03-24 15:08:23 +00:00
return errors . New ( "not implemented" )
2021-02-25 23:32:21 +00:00
}
return nil
}
2021-02-26 11:32:43 +00:00
// SignCSR creates an x509.Certificate based on a CSR template and Cert Authority credentials
2021-02-25 23:32:21 +00:00
// returns a new PKIMessage with CertRep data
2021-02-26 17:07:50 +00:00
func ( a * Authority ) SignCSR ( ctx context . Context , csr * x509 . CertificateRequest , msg * PKIMessage ) ( * PKIMessage , error ) {
2021-02-26 13:00:47 +00:00
// TODO: intermediate storage of the request? In SCEP it's possible to request a csr/certificate
// to be signed, which can be performed asynchronously / out-of-band. In that case a client can
// 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.
2023-04-28 13:47:22 +00:00
p , err := ProvisionerFromContext ( ctx )
2021-02-26 11:32:43 +00:00
if err != nil {
return nil , err
}
2021-02-25 23:32:21 +00:00
// check if CSRReqMessage has already been decrypted
if msg . CSRReqMessage . CSR == nil {
2021-02-26 11:32:43 +00:00
if err := a . DecryptPKIEnvelope ( ctx , msg ) ; err != nil {
2021-02-25 23:32:21 +00:00
return nil , err
}
2021-02-26 17:07:50 +00:00
csr = msg . CSRReqMessage . CSR
2021-02-25 23:32:21 +00:00
}
2021-02-12 16:02:39 +00:00
2021-02-25 23:32:21 +00:00
// Template data
2021-03-26 15:11:35 +00:00
sans := [ ] string { }
sans = append ( sans , csr . DNSNames ... )
sans = append ( sans , csr . EmailAddresses ... )
for _ , v := range csr . IPAddresses {
sans = append ( sans , v . String ( ) )
}
for _ , v := range csr . URIs {
sans = append ( sans , v . String ( ) )
}
if len ( sans ) == 0 {
sans = append ( sans , csr . Subject . CommonName )
}
data := x509util . CreateTemplateData ( csr . Subject . CommonName , sans )
data . SetCertificateRequest ( csr )
data . SetSubject ( x509util . Subject {
Country : csr . Subject . Country ,
Organization : csr . Subject . Organization ,
OrganizationalUnit : csr . Subject . OrganizationalUnit ,
Locality : csr . Subject . Locality ,
Province : csr . Subject . Province ,
StreetAddress : csr . Subject . StreetAddress ,
PostalCode : csr . Subject . PostalCode ,
SerialNumber : csr . Subject . SerialNumber ,
CommonName : csr . Subject . CommonName ,
} )
2021-02-25 23:32:21 +00:00
2021-02-26 13:00:47 +00:00
// Get authorizations from the SCEP provisioner.
ctx = provisioner . NewContextWithMethod ( ctx , provisioner . SignMethod )
signOps , err := p . AuthorizeSign ( ctx , "" )
if err != nil {
2022-03-24 15:08:23 +00:00
return nil , fmt . Errorf ( "error retrieving authorization options from SCEP provisioner: %w" , err )
2021-02-26 13:00:47 +00:00
}
2022-09-30 00:16:26 +00:00
// Unlike most of the provisioners, scep's AuthorizeSign method doesn't
// define the templates, and the template data used in WebHooks is not
// available.
for _ , signOp := range signOps {
if wc , ok := signOp . ( * provisioner . WebhookController ) ; ok {
wc . TemplateData = data
}
}
2021-02-26 13:00:47 +00:00
2022-01-27 20:06:55 +00:00
opts := provisioner . SignOptions { }
2021-02-26 11:32:43 +00:00
templateOptions , err := provisioner . TemplateOptions ( p . GetOptions ( ) , data )
if err != nil {
2022-03-24 15:08:23 +00:00
return nil , fmt . Errorf ( "error creating template options from SCEP provisioner: %w" , err )
2021-02-26 11:32:43 +00:00
}
signOps = append ( signOps , templateOptions )
2021-02-25 23:32:21 +00:00
2021-02-26 13:00:47 +00:00
certChain , err := a . signAuth . Sign ( csr , opts , signOps ... )
2021-02-12 16:02:39 +00:00
if err != nil {
2022-03-24 15:08:23 +00:00
return nil , fmt . Errorf ( "error generating certificate for order: %w" , err )
2021-02-12 16:02:39 +00:00
}
2021-03-21 15:42:41 +00:00
// take the issued certificate (only); https://tools.ietf.org/html/rfc8894#section-3.3.2
2021-02-26 13:00:47 +00:00
cert := certChain [ 0 ]
2021-02-25 23:32:21 +00:00
2021-03-21 15:42:41 +00:00
// and create a degenerate cert structure
deg , err := microscep . DegenerateCertificates ( [ ] * x509 . Certificate { cert } )
2021-02-25 23:32:21 +00:00
if err != nil {
return nil , err
2021-02-12 16:02:39 +00:00
}
2022-01-14 09:48:23 +00:00
// 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
2022-01-18 14:54:18 +00:00
// this call to Encrypt(). We are not required to use the same algorithm the SCEP client uses.
2022-01-14 09:48:23 +00:00
encryptionAlgorithmToRestore := pkcs7 . ContentEncryptionAlgorithm
pkcs7 . ContentEncryptionAlgorithm = p . GetContentEncryptionAlgorithm ( )
2021-03-06 22:24:49 +00:00
e7 , err := pkcs7 . Encrypt ( deg , msg . P7 . Certificates )
2021-02-12 16:02:39 +00:00
if err != nil {
return nil , err
}
2022-01-14 09:48:23 +00:00
pkcs7 . ContentEncryptionAlgorithm = encryptionAlgorithmToRestore
2021-02-12 16:02:39 +00:00
2021-02-25 23:32:21 +00:00
// PKIMessageAttributes to be signed
config := pkcs7 . SignerInfoConfig {
ExtraSignedAttributes : [ ] pkcs7 . Attribute {
{
Type : oidSCEPtransactionID ,
Value : msg . TransactionID ,
} ,
{
Type : oidSCEPpkiStatus ,
Value : microscep . SUCCESS ,
} ,
{
Type : oidSCEPmessageType ,
Value : microscep . CertRep ,
} ,
{
Type : oidSCEPrecipientNonce ,
Value : msg . SenderNonce ,
} ,
2021-03-10 21:20:02 +00:00
{
2021-03-10 20:13:05 +00:00
Type : oidSCEPsenderNonce ,
Value : msg . SenderNonce ,
} ,
2021-02-25 23:32:21 +00:00
} ,
}
2021-02-12 16:02:39 +00:00
2021-02-25 23:32:21 +00:00
signedData , err := pkcs7 . NewSignedData ( e7 )
if err != nil {
return nil , err
}
2021-02-12 11:03:08 +00:00
2021-02-25 23:32:21 +00:00
// add the certificate into the signed data type
// this cert must be added before the signedData because the recipient will expect it
// as the first certificate in the array
signedData . AddCertificate ( cert )
2021-02-12 16:02:39 +00:00
2021-02-25 23:32:21 +00:00
authCert := a . intermediateCertificate
// sign the attributes
2021-03-12 14:49:39 +00:00
if err := signedData . AddSigner ( authCert , a . service . signer , config ) ; err != nil {
2021-02-25 23:32:21 +00:00
return nil , err
2021-02-12 16:02:39 +00:00
}
2021-02-25 23:32:21 +00:00
certRepBytes , err := signedData . Finish ( )
if err != nil {
return nil , err
}
2021-02-12 16:02:39 +00:00
2021-02-25 23:32:21 +00:00
cr := & CertRepMessage {
PKIStatus : microscep . SUCCESS ,
RecipientNonce : microscep . RecipientNonce ( msg . SenderNonce ) ,
Certificate : cert ,
degenerate : deg ,
}
2021-02-12 11:03:08 +00:00
2021-03-10 20:13:05 +00:00
// create a CertRep message from the original
crepMsg := & PKIMessage {
Raw : certRepBytes ,
TransactionID : msg . TransactionID ,
MessageType : microscep . CertRep ,
CertRepMessage : cr ,
}
return crepMsg , nil
}
// CreateFailureResponse creates an appropriately signed reply for PKI operations
func ( a * Authority ) CreateFailureResponse ( ctx context . Context , csr * x509 . CertificateRequest , msg * PKIMessage , info FailInfoName , infoText string ) ( * PKIMessage , error ) {
config := pkcs7 . SignerInfoConfig {
ExtraSignedAttributes : [ ] pkcs7 . Attribute {
2021-03-10 21:20:02 +00:00
{
2021-03-10 20:13:05 +00:00
Type : oidSCEPtransactionID ,
Value : msg . TransactionID ,
} ,
2021-03-10 21:20:02 +00:00
{
2021-03-10 20:13:05 +00:00
Type : oidSCEPpkiStatus ,
Value : microscep . FAILURE ,
} ,
2021-03-10 21:20:02 +00:00
{
2021-03-10 20:13:05 +00:00
Type : oidSCEPfailInfo ,
Value : info ,
} ,
2021-03-10 21:20:02 +00:00
{
2021-03-10 20:13:05 +00:00
Type : oidSCEPfailInfoText ,
Value : infoText ,
} ,
2021-03-10 21:20:02 +00:00
{
2021-03-10 20:13:05 +00:00
Type : oidSCEPmessageType ,
Value : microscep . CertRep ,
} ,
2021-03-10 21:20:02 +00:00
{
2021-03-10 20:13:05 +00:00
Type : oidSCEPsenderNonce ,
Value : msg . SenderNonce ,
} ,
2021-03-10 21:20:02 +00:00
{
2021-03-10 20:13:05 +00:00
Type : oidSCEPrecipientNonce ,
Value : msg . SenderNonce ,
} ,
} ,
}
signedData , err := pkcs7 . NewSignedData ( nil )
if err != nil {
return nil , err
}
// sign the attributes
2021-03-12 14:49:39 +00:00
if err := signedData . AddSigner ( a . intermediateCertificate , a . service . signer , config ) ; err != nil {
2021-03-10 20:13:05 +00:00
return nil , err
}
certRepBytes , err := signedData . Finish ( )
if err != nil {
return nil , err
}
cr := & CertRepMessage {
PKIStatus : microscep . FAILURE ,
2021-05-06 22:23:09 +00:00
FailInfo : microscep . FailInfo ( info ) ,
2021-03-10 20:13:05 +00:00
RecipientNonce : microscep . RecipientNonce ( msg . SenderNonce ) ,
}
2021-02-25 23:32:21 +00:00
// create a CertRep message from the original
crepMsg := & PKIMessage {
Raw : certRepBytes ,
TransactionID : msg . TransactionID ,
MessageType : microscep . CertRep ,
CertRepMessage : cr ,
2021-02-12 16:02:39 +00:00
}
2021-02-12 11:03:08 +00:00
2021-02-25 23:32:21 +00:00
return crepMsg , nil
}
2021-03-06 23:30:37 +00:00
// MatchChallengePassword verifies a SCEP challenge password
func ( a * Authority ) MatchChallengePassword ( ctx context . Context , password string ) ( bool , error ) {
2023-04-28 13:47:22 +00:00
p , err := ProvisionerFromContext ( ctx )
2021-03-06 23:30:37 +00:00
if err != nil {
return false , err
}
2021-03-21 15:42:41 +00:00
if subtle . ConstantTimeCompare ( [ ] byte ( p . GetChallengePassword ( ) ) , [ ] byte ( password ) ) == 1 {
2021-03-06 23:30:37 +00:00
return true , nil
}
// TODO: support dynamic challenges, i.e. a list of challenges instead of one?
// That's probably a bit harder to configure, though; likely requires some data store
// that can be interacted with more easily, via some internal API, for example.
return false , nil
}
2021-03-06 23:50:00 +00:00
// GetCACaps returns the CA capabilities
func ( a * Authority ) GetCACaps ( ctx context . Context ) [ ] string {
2023-04-28 13:47:22 +00:00
p , err := ProvisionerFromContext ( ctx )
2021-03-06 23:50:00 +00:00
if err != nil {
return defaultCapabilities
}
caps := p . GetCapabilities ( )
if len ( caps ) == 0 {
return defaultCapabilities
}
// TODO: validate the caps? Ensure they are the right format according to RFC?
// TODO: ensure that the capabilities are actually "enforced"/"verified" in code too:
// check that only parts of the spec are used in the implementation belonging to the capabilities.
// For example for renewals, which we could disable in the provisioner, should then also
// not be reported in cacaps operation.
return caps
}