Enrollment and renewal¶
est.Client runs one EST operation per method call. Each call opens its own connection, runs the
TLS handshake, sends one request and closes the connection. The context of the call bounds all of
it.
Creating a client¶
Only Host is required. It is the host:port of the server, such as est.example.com:8443,
without a scheme or path. The listing in the next section creates a client.
AdditionalPathSegmentselects one of several CAs that the server offers (RFC 7030 section 3.2.2). The client then callshttps://<host>/.well-known/est/<segment>/<operation>.- Trust anchors authenticate the server.
ExplicitAnchoris used when it is set. OtherwiseImplicitAnchoris used. When neither is set, the system roots apply. InsecureSkipVerifyaccepts any server certificate. Use it only to fetch the CA certificates once and check them out of band, as Getting started does. Every later call needs a trust anchor.
Signing in adds credentials and Connections and hooks covers proxies, limits and tracing. Client fields lists every field.
Getting the CA certificates¶
CACerts returns every certificate of the /cacerts response in the order the server sent them.
In this listing and the ones below, ctx is a context with a deadline, such as one from
context.WithTimeout:
client := &est.Client{
Host: host,
ExplicitAnchor: anchor,
}
cacerts, err := client.CACerts(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("CA:", cacerts[0].Subject.CommonName)
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatal(err)
}
der, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
Subject: pkix.Name{CommonName: "device-1"},
DNSNames: []string{"device-1.example.com"},
}, key)
if err != nil {
log.Fatal(err)
}
csr, err := x509.ParseCertificateRequest(der)
if err != nil {
log.Fatal(err)
}
cert, err := client.Enroll(ctx, csr)
if err != nil {
log.Fatal(err)
}
fmt.Println("enrolled:", cert.Subject.CommonName, cert.DNSNames)
The same listing shows Enroll. It sends a PKCS #10 request, usually created with
x509.CreateCertificateRequest and returns the issued certificate.
RFC 7030 section 4.2.3 allows only the issued certificate in the response. Some servers add the CA
chain. The client refuses such a response unless AllowCertificateChain is set, see
Connections and hooks.
Binding the request to the TLS session¶
RFC 7030 section 3.5 lets the client put the tls-unique value of the TLS session into the
challenge password of the CSR, which proves that the requester holds the key on this connection.
Set SigningKey to the key that signs the CSR. The client then signs the request again with the
challenge password added, in Enroll and Reenroll.
SigningKeymust be an RSA or ECDSA key. Over TLS 1.2 another key, such as Ed25519 or ML-DSA, fails the call withx509: only RSA and ECDSA keys supported.tls-uniqueexists only in TLS 1.2. Over TLS 1.3 the client sends the request unchanged. TLS-SRP always runs over TLS 1.2, so the binding works there.- The client has no setting to cap the TLS version. Against a server that offers TLS 1.3 the binding is never added.
- The router does not check the challenge password. A CA that requires the binding must compare it with the session itself.
Renewing a certificate¶
Reenroll renews a certificate. The certificate being renewed authenticates the TLS connection,
so set Certificates and PrivateKey:
// The certificate being renewed authenticates the TLS connection.
client.Certificates = []*x509.Certificate{cert}
client.PrivateKey = key
// The request repeats the Subject and SubjectAltName of the certificate.
der, err = x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
RawSubject: cert.RawSubject,
DNSNames: cert.DNSNames,
}, key)
if err != nil {
log.Fatal(err)
}
csr, err = x509.ParseCertificateRequest(der)
if err != nil {
log.Fatal(err)
}
renewed, err := client.Reenroll(ctx, csr)
if err != nil {
log.Fatal(err)
}
fmt.Println("renewed:", renewed.Subject.CommonName, renewed.SerialNumber.Cmp(cert.SerialNumber) != 0)
- The request must repeat the Subject and the SubjectAltName extension of the certificate,
byte for byte, as RFC 7030 section 4.2.2 requires. A go-pki-est server refuses any difference
with 403. Building the request from
RawSubjectand the names of the certificate keeps both equal. A new key is allowed, so the same call rekeys. - Send the intermediates.
Certificatesholds the client certificate first and the CA certificates the server needs to build the chain after it. When the server names the CAs it accepts, the Go TLS client presents the certificate only if one of the certificates in the list was issued by one of them. Otherwise it sends no certificate.estservernames only its root, so without the intermediate the client presents nothing and the server answers 403client certificate must be provided for /simplereenroll. - Servers may also accept a renewal signed in with a password, a bearer token or TLS-SRP instead of the certificate. See Signing in.
Server-generated keys¶
ServerKeyGen asks the server to generate the key (RFC 7030 section 4.4). The server ignores the
key and the signature of the request and uses only its names, so a throwaway key may sign it:
// The request carries the names. The server ignores its key, so a throwaway key signs it.
throwaway, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatal(err)
}
csr, err := newCSR(throwaway, "device-2")
if err != nil {
log.Fatal(err)
}
cert, keyDER, err := client.ServerKeyGen(ctx, csr)
if err != nil {
log.Fatal(err)
}
// Without additional encryption the key is PKCS #8.
key, err := x509.ParsePKCS8PrivateKey(keyDER)
if err != nil {
log.Fatal(err)
}
fmt.Printf("certificate: %s\nkey: %T\n", cert.Subject.CommonName, key)
The key comes back as DER bytes in one of two forms:
- PKCS #8 in an
application/pkcs8part, when the server adds no encryption. Parse it withx509.ParsePKCS8PrivateKey. Keys thatcrypto/x509does not know, such as ML-KEM, are returned as they are. - CMS in an
application/pkcs7-mime; smime-type=server-generated-keypart, when the server encrypts the key. The client returns the CMS bytes without decrypting them. Decrypt them with the key or password your CA agreed with the client.
The response must hold exactly two parts, a key and a certificate. Otherwise ServerKeyGen fails
with no certificate returned, no private key returned or more than 2 parts in HTTP response.
CSR attributes¶
CSRAttrs returns the attributes the CA wants in a request (RFC 7030 section 4.5):
attrs, err := client.CSRAttrs(ctx)
if err != nil {
log.Fatal(err)
}
for _, oid := range attrs.OIDs {
fmt.Println("OID:", oid)
}
for _, attr := range attrs.Attributes {
fmt.Println("attribute:", attr.Type, attr.Values)
}
OIDs holds the bare object identifiers and Attributes the attributes with values. Values of
type OBJECT IDENTIFIER, BOOLEAN, INTEGER and most string types become asn1.ObjectIdentifier,
bool, *big.Int and string. Other values stay asn1.RawValue. A 204 or 404 answer means the
server has no attributes to ask for, so CSRAttrs returns an empty value and no error.
Deferred and refused requests¶
Every status other than 200 returns an error that implements est.Error, including 202 Accepted,
because a deferred enrollment has no certificate yet:
_, err = client.Enroll(ctx, csr)
if estErr, ok := errors.AsType[est.Error](err); ok {
fmt.Println("status:", estErr.StatusCode())
fmt.Println("retry after:", estErr.RetryAfter())
fmt.Println("message:", strings.TrimSpace(estErr.Error()))
}
RetryAfter comes from the Retry-After header, given in seconds or as an HTTP date and is 0
without one. The client never retries or polls. Repeat the same call after the delay to collect
the certificate. Error() returns the response body, which a go-pki-est server writes as the
status and the reason followed by a newline. Errors lists them.
Errors without an HTTP status, such as a failed handshake, a refused proxy tunnel, an expired
context or a response that does not decode, do not implement est.Error.