This document describes the process to allow step-ca to renew a certificate after it has expired.
The classic way to renew a certificate on step-ca
is to do an empty POST
to
the /renew
endpoint using a valid client certificate. The request will go
through an mTLS connection where the CA will validate the validity of the
certificate, it will also check that the provisioner that created it exists and
allows renewals, and finally the CA will sign a new certificate with almost the
same information available in the old one. The serial number and the signature
will change, and if the intermediate has changed the authority key identifier
will too.
Using the cli you can do:
step ca renew --force internal.crt internal.key
With curl you will do:
curl --cacert $(step path)/certs/root_ca.crt --cert internal.crt --key internal.key -X POST https://ca.smallstep.com:9000/renew
And then just combine the response "certChain"
property to create your renewed certificate.
step-ca
also supports a rekey mechanism sending a Certificate Signing Request
to /rekey
using mTLS, this process will also sign a new certificate like
before, but it will also replace the public key with the one in the CSR.
The first question that probably comes to mind is, can we use the same protocol with an expired certificate?
The short answer is yes, but it will mean to skip the certificate validation
that Go is doing and do it manually, and this can be dangerous. We cannot skip
the validation for just one or two endpoints, we'll have to do it at the server
level. And to do it we will need to set tls.Config.ClientAuth = RequestClientCert
and implement an empty tls.Config.VerifyPeerCertificate
, at this point we can
accept expired certificates, but then we'll have to add a middleware to validate
client connections to all the endpoints, in case the client is using identity
certificates, and a special middleware for the renewal endpoints that validates
only the certificates for those provisioners where renew-after-expiry is not
enabled, but accepting expired ones when the provisioner supports
renew-after-expiry. To be clear, this is something that we're not doing.
The second option that comes to mind is if can we create a token with an x5c
claim with the expired certificate and signed by the certificate key. Basically
use a flow like the one used on X5C provisioners, but accepting expired
certificates. Ok this can work, but right now the X5C provisioner does not signs
a certificate based on the embedded one, a token for an X5C provisioners has the
sans
claim that contains the SANs that we want to grant.
We can continue to explore the X5C route but it will be probably better to
create a new type of provisioner rather than having if
conditions on the
provisioner method, to validate a provisioner without taking into account the
lifetime of the certificate as well as generate a "clone" of the embedded
certificate instead of a new one from the SANs and a given CSR.
The configuration of a renew-after-expiry will be very similar to the X5C, we will need a string with the all the root certificates that we can validate, but if this is empty we can safely use the roots configured in the ca.json. So this two configurations will be totally valid:
{
"type": "TBD",
"name": "RenewAfterExpiry"
}
{
"type": "TBD",
"name": "RenewAfterExpiry",
"roots": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JS..."
}
In the generated token we will validate:
- The audience, a link to the ca with the provisioner in a fragment.
- The issuer, the provisioner name.
- The expiration and not before of the token with the current time.
- The token subject. Should it match the embedded certificate common name?
- The validity of the certificate in the x5c claim, taking into account:
- The time when the certificate was issued (NotBefore).
- The roots to validate the certificate with.
- The key usage
x5c.KeyUsageDigitalSignature
as we will use this certificate to sign a token. - The extended key usage of the given certificate, X5C provisioner requires
x5c.ExtKeyUsageClientAuth
. Should we really require this? What if the certificate was generated with a provisioner that only uses thex509.ExtKeyUsageServerAuth
extended key usage? Should we allow any?
This provisioner should also support the renewal of SSH certificates. In this
case we cannot use roots
as the public keys in those certs won't match the
private key used to sign the SSH certificate. So we'll need to extend the
provisioner with the properties sshHostCAs
and sshUserCAs
that contains a
collection of public keys that we will accept. For this property we can use a
list of ssh public keys that we want to support in SSH encoding.
With the default keys:
{
"type": "TBD",
"name": "RenewAfterExpiry",
"claims": {
"enableSSH": true
}
}
With specific keys:
```json
{
"type": "TBD",
"name": "RenewAfterExpiry",
"roots": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JS...",
"sshHostCAs": [
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAXhns07OVxxNxOVXoMuPcwJ87J45EBd8iweuRkLjKNzynn+scrVGr6+P3K5eDOJ2pafJa7EqORG3bn/BVAp+qQ=",
"..."
],
"sshUserCAs": [
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBClQDrRrVZh1Ani1Bj8I6GquFpN8QoivhSUbaPLMVEOYL8oZNrKqtmfJrG0/GDEIIyNbRZIsCe8l/FrTZs+6d4Q=",
"..."
]
}
As we don't currently support the renewal of user certificates, the only key
included by default will be the one used to sign host certificates in
sshHostCas
. The user will always have the option to add the user one and we
can accept user certificates too.
The token for renewing ssh certificates will be similar to the one for X.509,
but instead of having a x5c
claim it will have the sshpop
claim with the SSH
certificate that we want to renew, and it will be signed by its private key.
The token validation will be similar to the X.509 one, with the following differences:
- The token subject should match the certificate key id.
- The
sshpop
claim should should be validate against the user or hosts CAs depending of the type of the certificate.
Finally the provisioner should probably implement AuthorizeRenew
and
AuthorizeSSHRenew
and optionally AuthorizeRekey
and AuthorizeSSHRekey
if
we want to support rekeys too.
Renew after expiry endpoints should re-use the /renew
, /rekey
and the SSH
ones /ssh/renew
and /ssh/rekey
endpoints.
Right now the X.509 endpoints /renew
and /rekey
require a client
certificate, but we can change them to either require an Authorization
header
like:
Authentication: Mutual <token>
If the token is given the renew and rekey endpoints will request the authority
to authorize it using the new AuthorizeRenew
and AuthorizeSSHRenew
methods:
AuthorizeRenew(ctx context.Context, token string) (*x509.Certificate, error)
AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error)
These methods will retrieve the appropriate provisioner for the token and call
the new AuthorizeRenewByToken
and the already existing AuthorizeSSHRenew
implemented by the new renew-after-expiry provisioner:
type AuthorizeRenewerByToken interface {
AuthorizeRenewByToken(ctx context.Context, token string) (*x509.Certificate, error)
}
type AuthorizeSSHRenewer interface {
AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error)
}
The AuthorizeRenewByToken
can optionally be implemented by the X5C provisioner
to be able to renew the certificate in the token. The SSHPOP provisioner already
supports the AuthorizeSSHRenew
, and we should change /ssh/renew
and
/ssh/rekey
to use authority.AuthoritheSSHRenew
instead of just
authority.Authorize
and then getting the certificate again from the token.
Alternatively we can change the current implementation of
provisioner.AuthorizeRenew
from:
AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error
to one of these options:
// Here the token is passed by context.
AuthorizeRenew(ctx context.Context, cert *x509.Certificate) (*x509.Certificate, error)
// Here the token is a parameter.
AuthorizeRenew(ctx context.Context, cert *x509.Certificate, token string) (*x509.Certificate, error)
- What would be the type of a renew-after-expiry provisioner? Or should we just extend the capabilities of X5C?
- Should we use the
/sign
(and/ssh/sign
) endpoint, the/renew
and/rekey
endpoints or new ones? Currently/sign
requires a CRL, but renew-after-expiry doesn't really need one, although we can easily generate it as we will have access to the private key.- Let's use the old methods
- Should we allow to renew any kind of certificate? Perhaps only the ones coming from a provisioner that has a special claim that allow after expiry, or perhaps a custom extension that is added if a provisioner has that claim.
- If we use that special claim, do we have to set it in the renew after expiry provisioner, or is it true by default in this case?
- Should we allow the use of templates in the renew after expiry provisioner? Renewal/Rekey endpoints do not support any kind of templates, so perhaps we don't want to do it.
- Should we convert the
roots
property to a list instead of a string will all the certificates? Right now this is how it is implemented in the X5C provisioner but a list will match the list of ssh keys. - Should we use a JSON body instead of the Authorization header?