Enrollment and renewal¶
A device enrolls with a PKCSReq, waits while the CA keeps the request pending and later renews with a RenewalReq. Each request needs the sorted CA certificates from CA certificates and trust.
Creating the CSR¶
A PKCSReq usually carries a challenge password in the CSR, which crypto/x509 cannot add.
x509util.CreateCertificateRequest creates the CSR with it:
der, err := x509util.CreateCertificateRequest(rand.Reader, &x509util.CertificateRequest{
Subject: pkix.Name{CommonName: "device-42"},
DNSNames: []string{"device-42.example.com"},
ChallengePassword: password,
}, key)
if err != nil {
return err
}
csr, err := x509.ParseCertificateRequest(der)
if err != nil {
return err
}
CertificateRequest embeds x509.CertificateRequest, so the usual template fields apply. With a
challenge password the key must be RSA or ECDSA on P-224, P-256, P-384 or P-521. The template
SignatureAlgorithm is honored, including the RSA-PSS algorithms. Without a password the function
returns the CSR crypto/x509 creates.
Enrolling with a PKCSReq¶
The quickstart enrolls a new RSA key:
// enroll creates an RSA key and a CSR with the challenge password and enrolls it with a PKCSReq.
func enroll(ctx context.Context, client *scepclient.Client, ca *scepclient.CACertificates, password string) (*x509.Certificate, *rsa.PrivateKey, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}
der, err := x509util.CreateCertificateRequest(rand.Reader, &x509util.CertificateRequest{
Subject: pkix.Name{CommonName: "quickstart-device"},
ChallengePassword: password,
}, key)
if err != nil {
return nil, nil, err
}
csr, err := x509.ParseCertificateRequest(der)
if err != nil {
return nil, nil, err
}
// A PKCSReq is signed with a self-signed certificate for the CSR key. The answer is encrypted
// for that certificate.
signer, err := selfSigned(key, csr.Subject)
if err != nil {
return nil, nil, err
}
res, err := client.EnrollAndWait(ctx, ca, scepclient.EnrollRequest{
CSR: csr,
SignerCertificate: signer,
SignerKey: key,
}, 5*time.Second)
if err != nil {
return nil, nil, err
}
if res.Status != scep.SUCCESS {
return nil, nil, fmt.Errorf("enrollment answered %s %s", res.Status, res.FailInfo)
}
return res.Certificate, key, nil
}
// selfSigned returns a short-lived self-signed certificate for key.
func selfSigned(key *rsa.PrivateKey, subject pkix.Name) (*x509.Certificate, error) {
template := &x509.Certificate{
SerialNumber: randomSerial(),
Subject: subject,
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
return nil, err
}
return x509.ParseCertificate(der)
}
EnrollRequest describes the request:
| Field | Meaning |
|---|---|
MessageType |
scep.PKCSReq (the default) or scep.RenewalReq |
CSR |
The certificate request. Its public key identifies the issued certificate in the answer |
SignerCertificate, SignerKey |
Sign the request. The key also decrypts the answer, so it must be an RSA crypto.Decrypter. A PKCSReq uses a self-signed certificate for the CSR key |
TransactionID |
Identifies the enrollment. The zero value derives it from the CSR key with scep.CSRTransactionID |
Digest, Encryption |
The signature digest and the content cipher and key transport. Zero values follow the server capabilities, see Capabilities, answers and retries |
Enroll and EnrollAndWait return a *scepclient.Result:
| Field | Content |
|---|---|
Status |
scep.SUCCESS, scep.PENDING or scep.FAILURE |
FailInfo |
The reason of a FAILURE |
Certificate |
The issued certificate, the one whose public key is the CSR key |
Certificates |
Every certificate of a SUCCESS answer in server order, such as the chain |
TransactionID |
The transaction ID, which Poll needs |
Response |
The verified CertRep |
A FAILURE answer is a result, not an error. The error is set when the request could not be sent, the server answered without a CertRep or the answer did not verify. A SUCCESS answer without a certificate for the CSR key is an error too.
Devices with ECDSA keys¶
The answer is encrypted for the certificate that signs the request, with RSA key transport. A device whose CSR key is ECDSA signs the PKCSReq with a separate RSA key and a self-signed certificate for it. The CA issues the certificate for the ECDSA key in the CSR:
res, err := client.Enroll(ctx, ca, scepclient.EnrollRequest{
CSR: ecdsaCSR,
SignerCertificate: rsaSignerCert,
SignerKey: rsaSignerKey,
})
res.Certificate then holds the ECDSA key. With a SignerKey that is not RSA, Enroll sends
nothing and returns scepclient: the signer key must be an RSA crypto.Decrypter. The handler of
this module answers FAILURE with badAlg when another client signs a request with such a key.
Waiting for approval¶
EnrollAndWait sends the request and, while the CA answers PENDING, polls with CertPoll every
interval:
ctx, cancel := context.WithTimeout(ctx, 30*time.Minute)
defer cancel()
res, err := client.EnrollAndWait(ctx, ca, req, time.Minute)
It returns the first SUCCESS or FAILURE result. When the context ends first, it returns the last PENDING result together with the context error, so set a deadline that matches how long an approval may take. The interval must be positive.
A device that stops and polls later keeps the CSR, the signer certificate and key and the
transaction ID, then sends one CertPoll at a time with Poll:
res, err := client.Poll(ctx, ca, scepclient.EnrollRequest{
CSR: csr,
SignerCertificate: signerCert,
SignerKey: signerKey,
TransactionID: savedTransactionID,
})
Poll derives the transaction ID from the CSR when TransactionID is empty, which matches an
enrollment that used the default.
Renewing with a RenewalReq¶
A RenewalReq is signed with the certificate being renewed and its key, which the CA uses to authorize the request. The quickstart renews for a new key:
// renew requests a certificate for a new key with a RenewalReq signed by the current certificate.
func renew(ctx context.Context, client *scepclient.Client, ca *scepclient.CACertificates, cert *x509.Certificate, key *rsa.PrivateKey) (*x509.Certificate, error) {
newKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
der, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{Subject: cert.Subject}, newKey)
if err != nil {
return nil, err
}
csr, err := x509.ParseCertificateRequest(der)
if err != nil {
return nil, err
}
res, err := client.Enroll(ctx, ca, scepclient.EnrollRequest{
MessageType: scep.RenewalReq,
CSR: csr,
SignerCertificate: cert,
SignerKey: key,
})
if err != nil {
return nil, err
}
if res.Status != scep.SUCCESS {
return nil, fmt.Errorf("renewal answered %s %s", res.Status, res.FailInfo)
}
return res.Certificate, nil
}
The current key decrypts the answer, so it must be RSA. The CSR may carry a new key or the current
one. The client sends a RenewalReq whether or not the server lists the Renewal capability. Check
GetCACaps first when you need to fall back to a PKCSReq with a new challenge password.
Enroll sends PKCSReq and RenewalReq only. Build an UpdateReq from earlier SCEP drafts with
scep.NewCSRRequest from the message layer.
Choosing the algorithms¶
The client picks the digest and the cipher from the server capabilities. Set them on the request to override that choice, for example AES-256-CBC with RSAES-OAEP key transport for a server that accepts both:
req.Encryption = pkcs7.EncryptOptions{
ContentEncryption: pkcs7.AES256CBC,
KeyEncryption: pkcs7.RSAOAEP,
}
The answer comes back with RSA PKCS #1 v1.5 key transport from the handler of this module, which always uses it for answers.