Skip to content

The CA interface

The router hands every EST operation that passed its checks to a value that implements est.CA. That value is your CA: it may sign in process, call a CA over the network or queue a request for an operator.

A minimal CA

This CA signs every request with an in-memory root. It is the CA of the examples in example_test.go. Its issue helper, which signs with x509.CreateCertificate, is in the same file:

// caError is an error with an HTTP status. The server sends its status, message and Retry-After
// value to the client.
type caError struct {
    status     int
    msg        string
    retryAfter int
}

// Error returns the message sent to the client.
func (e caError) Error() string { return e.msg }

// StatusCode returns the HTTP status of the response.
func (e caError) StatusCode() int { return e.status }

// RetryAfter returns the Retry-After value in seconds, or 0 for none.
func (e caError) RetryAfter() int { return e.retryAfter }

// CACerts returns the certificates clients use as trust anchors.
func (ca *exampleCA) CACerts(ctx context.Context, aps string, r *http.Request) ([]*x509.Certificate, error) {
    return []*x509.Certificate{ca.root}, nil
}

// CSRAttrs asks clients for a P-384 key, using the example of RFC 7030 section 4.5.2.
func (ca *exampleCA) CSRAttrs(ctx context.Context, aps string, r *http.Request) (est.CSRAttrs, error) {
    return est.CSRAttrs{
        Attributes: []est.Attribute{{
            Type:   asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1},
            Values: est.AttributeValueSET{asn1.ObjectIdentifier{1, 3, 132, 0, 34}},
        }},
    }, nil
}

// Enroll issues a certificate for the key in the request.
func (ca *exampleCA) Enroll(ctx context.Context, csr *x509.CertificateRequest, aps string, r *http.Request) (*x509.Certificate, error) {
    return ca.issue(csr, csr.PublicKey)
}

// Reenroll renews a certificate. cert is nil when the client signed in with TLS-SRP or, with
// PasswordRenewal, with a password or bearer token instead of the certificate being renewed.
func (ca *exampleCA) Reenroll(ctx context.Context, cert *x509.Certificate, csr *x509.CertificateRequest, aps string, r *http.Request) (*x509.Certificate, error) {
    if cert == nil {
        // Any client can name any subject in a CSR, so the renewal is tied to the signed-in user.
        user := est.SRPUsernameFromContext(ctx)
        if user == "" {
            user, _, _ = r.BasicAuth()
        }
        if user == "" || csr.Subject.CommonName != user {
            return nil, caError{status: http.StatusForbidden, msg: "renewal is limited to the signed-in user"}
        }
    }
    return ca.issue(csr, csr.PublicKey)
}

// ServerKeyGen generates a key for the client and returns it unencrypted as PKCS #8.
func (ca *exampleCA) ServerKeyGen(ctx context.Context, csr *x509.CertificateRequest, aps string, r *http.Request) (*x509.Certificate, []byte, error) {
    key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    if err != nil {
        return nil, nil, err
    }
    // The server ignores the key and the signature of the request, so only its names are used.
    cert, err := ca.issue(csr, key.Public())
    if err != nil {
        return nil, nil, err
    }
    der, err := x509.MarshalPKCS8PrivateKey(key)
    if err != nil {
        return nil, nil, err
    }
    return cert, der, nil
}

// TPMEnroll refuses TPM enrollment, which this CA does not offer.
func (ca *exampleCA) TPMEnroll(ctx context.Context, csr *x509.CertificateRequest, ekcerts []*x509.Certificate, ekPub, akPub []byte, aps string, r *http.Request) ([]byte, []byte, []byte, error) {
    return nil, nil, nil, caError{status: http.StatusNotImplemented, msg: "TPM enrollment is not offered"}
}

What every method receives

  • ctx is the request context. LoggerFromContext(ctx) returns the server logger and SRPUsernameFromContext(ctx) the TLS-SRP username, if any. It ends when the client disconnects or ServerConfig.Timeout passes.
  • aps is the additional path segment of the request or empty. Use it to select one of several CAs or profiles.
  • r is the HTTP request. It carries the headers, the Host, the Basic credentials and the TLS state. The router has already read the body of a POST request.

Methods

CACerts

Returns the CA certificates for /cacerts. The router sends them in the order returned and also uses them to check client certificates on renewal: self-signed certificates become the trusted roots and all others intermediates. Include the root. Without a self-signed certificate no client certificate verifies and every renewal over TLS answers 403 invalid client certificate. The system roots are never used for this check.

For the renewal check the router caches the result per additional path segment for 5 minutes and for at most 1024 segments. A /cacerts request also replaces a cached entry older than that.

CSRAttrs

Returns the attributes for /csrattrs. An empty value is sent as an empty sequence with status 200.

Enroll

Issues a certificate for /simpleenroll. The request is parsed and its signature is checked before the call. Everything else is policy for the CA: which names it allows, which key types and whether a tls-unique challenge password must match the TLS session. Return exactly one certificate.

Reenroll

Renews a certificate for /simplereenroll. cert is the TLS client certificate being renewed. The router has checked that the request repeats its Subject and SubjectAltName and that the certificate chains to the CACerts roots, is within its validity period and allows client authentication. With VerifyClientCertificate set, that hook decides instead of the chain check.

  • The router does not check revocation. A CA that revokes certificates must refuse a revoked cert here.
  • cert is nil when the client signed in with TLS-SRP or, with PasswordRenewal, with a password or bearer token. The CA must then choose the certificate to renew by the signed-in identity, never by the Subject of the request, which any client can name. The example above shows one way.
  • A ChangeSubjectName attribute (RFC 7030 section 4.2.2) stays in the request the CA receives. The router does not act on it and the Subject and SubjectAltName of the request must still match the certificate.

ServerKeyGen

Generates a key and issues a certificate for /serverkeygen. The router does not check the request signature, as RFC 7030 section 4.4.1 requires, so use only the names of the request. Return the key in one of two forms:

  • PKCS #8 DER to send it unencrypted. The router recognizes PKCS #8 by its structure, so keys that crypto/x509 cannot parse, such as ML-KEM, are sent as they are.
  • A CMS SignedData inside an EnvelopedData to send it encrypted. The router sends anything that parses as PKCS #7 with smime-type=server-generated-key.

Any other bytes answer 500.

TPMEnroll

Issues a certificate for /tpmenroll and returns the encrypted credential, the encrypted seed and the certificate inside a CMS EnvelopedData encrypted with the credential. See TPM enrollment. A CA that does not offer it returns an error such as 501, as the example does.

Errors

An error that implements est.Error sets the response:

Method Effect
StatusCode() The HTTP status, such as 400, 403, 202 or 503
Error() The reason, sent as the text body <status> <reason> followed by a newline
RetryAfter() The Retry-After header in seconds, sent when it is above 0

Any other error answers 500 internal server error and is logged with its text, so the client never sees internal details. A 500 from an est.Error is logged too. A panic in the CA is recovered, logged with its stack and answered with 500.

To defer an enrollment, for example until an operator approves it, return status 202 with a Retry-After value. The client returns both to its caller, see Deferred and refused requests. The router keeps no state, so the CA must recognize the repeated request itself, for example by its public key.