Implementing a CA¶
The server package handles the protocol. Your code signs certificates by implementing server.CA:
type CA interface {
IssueCertificate(ctx context.Context, reqType RequestType,
template *x509.Certificate, sender *SenderIdentity) (*Response, error)
}
The server calls IssueCertificate for every ir, cr, kur and p10cr that passed protection,
transaction, policy and proof of possession checks. reqType names the request and sender the
authenticated requester.
The template¶
The template arrives with the subject, public key, extensions and subject key identifier from the
request. SerialNumber is nil. The CA must set the serial number and validity and may enforce
policy, change the subject, add or remove extensions or refuse the request. A validity the CRMF
template asks for is not passed on.
func (c *myCA) IssueCertificate(ctx context.Context, reqType server.RequestType,
tmpl *x509.Certificate, sender *server.SenderIdentity) (*server.Response, error) {
serial, err := server.GenerateSerial()
if err != nil {
return nil, err
}
tmpl.SerialNumber = serial
tmpl.NotBefore = time.Now()
tmpl.NotAfter = time.Now().Add(365 * 24 * time.Hour)
// Drop a requested keyUsage, otherwise it would override the one set below.
oidKeyUsage := asn1.ObjectIdentifier{2, 5, 29, 15}
tmpl.ExtraExtensions = slices.DeleteFunc(tmpl.ExtraExtensions,
func(e pkix.Extension) bool { return e.Id.Equal(oidKeyUsage) })
tmpl.KeyUsage = x509.KeyUsageDigitalSignature
der, err := x509.CreateCertificate(rand.Reader, tmpl, c.cert, tmpl.PublicKey, c.key)
if err != nil {
return nil, err
}
cert, err := x509.ParseCertificate(der)
if err != nil {
return nil, err
}
return &server.Response{Certificate: cert}, nil
}
Requested extensions win over typed fields
template.ExtraExtensions holds the extensions the requester asked for and nothing has vetted
them. x509.CreateCertificate gives an extension in ExtraExtensions precedence over the
matching typed field, so assigning template.KeyUsage does not override a requested keyUsage.
The same applies to ExtKeyUsage, BasicConstraints and the other typed fields. Remove an
extension from ExtraExtensions before you decide it yourself, as the example does.
Subject names¶
The subject is rebuilt from pkix.Name, which drops attribute types it does not model, such as
domainComponent and emailAddress. It may also reorder the rest and change their string types.
server.WithRawSubject also sets template.RawSubject to the requested name, which
x509.CreateCertificate issues unchanged. Clear RawSubject before you change the subject.
A request that names the entity only in a subjectAltName has an empty subject. The template marks that subjectAltName critical, as RFC 5280 section 4.2.1.6 requires. A CA that supplies a subject should clear the flag.
Post-quantum keys¶
- A composite ML-DSA subject key arrives as
*compositemldsa.PublicKey. Onlycompositex509.CreateCertificatefrom go-composite-mldsa can certify it. A CA with a composite key signs with the same function. - Clients can check a composite CA signature only when the CA certificate travels in the response, so
pass it to
WithExtraCertsor return it inResponse.CACerts. - An ML-KEM key arrives as
*mlkem.EncapsulationKey768or*mlkem.EncapsulationKey1024, which crypto/x509 cannot certify.pkicmp.MarshalPublicKeyencodes its SubjectPublicKeyInfo, whose only allowed key usage is keyEncipherment. See Keys that cannot sign. - An SLH-DSA key arrives as an
slhdsa.PublicKeyvalue from circl, which crypto/x509 cannot certify either.pkicmp.MarshalPublicKeyencodes its SubjectPublicKeyInfo (RFC 9909). Its key usage may include digitalSignature, nonRepudiation, keyCertSign and cRLSign but no encipherment. - A CA with an SLH-DSA key signs its certificates outside crypto/x509 too, with the RFC 9909
identifier and absent parameters. It can also be the
WithSignerkey. Clients and the server verify paths through it and confirm its certificates with an explicit SHA-512 hash. Each signature is slow, see Known limitations.
The example CA uses
compositex509.CreateCertificate, so it certifies composite keys as well as the key types
crypto/x509 supports. It certifies ML-KEM keys with keyEncipherment as their only key usage.
The response¶
| Field | Effect |
|---|---|
Certificate |
The issued certificate |
CACerts |
CA certificates sent in caPubs |
Waiting |
Delays the answer, see Delayed issuance and confirmation |
GrantedWithMods |
Answers grantedWithMods instead of accepted for a certificate that differs from the request |
StatusText |
The statusString of the issued certificate, for example to name what the CA changed |
IssueRef |
Your own reference, passed back when the certificate is confirmed or expires |
Return a *server.Error to refuse a request with a chosen status and failure bits:
return nil, &server.Error{
Status: pkicmp.StatusRejection,
FailureInfo: pkicmp.FailBadCertTemplate,
StatusText: "subject not allowed",
}
Any other error becomes a bare systemFailure. Its text is not sent to the peer, so internal detail is never disclosed.
Serving it¶
srv := server.NewCAServer(ca,
server.LightweightPolicy(),
server.WithSigner(caKey, caCert),
server.WithExtraCerts([]*x509.Certificate{caCert}),
server.WithSecretLookup(server.SecretLookupFunc(lookupSecret)),
server.WithCertificateLookup(server.CertificateLookupFunc(lookupCertificate)),
// A messageTime outside this tolerance is refused as badTime.
server.WithMessageTimeTolerance(5*time.Minute),
)
if err := srv.Err(); err != nil {
log.Fatal(err)
}
http.Handle("/cmp", srv)
WithSigner takes a crypto.Signer, so the key can live in an HSM. Err reports a
misconfiguration such as a signer key that does not match its certificate. Such a server answers
every request with systemFailure rather than issue certificates it cannot protect, so check it at
startup.
The second argument is the policy middleware. Policy and client compatibility explains
LightweightPolicy and how to add your own authorization.
Several CAs¶
Each Server serves one CA. Create one per CA and route by path. RFC 9483 section 6.1 defines the
well-known path /.well-known/cmp/p/<name>:
mux := http.NewServeMux()
mux.Handle("/.well-known/cmp/p/devices", devicesServer)
mux.Handle("/.well-known/cmp/p/servers", serversServer)
Optional interfaces¶
A CA can implement more interfaces. NewCAServer detects them:
| Interface | Page |
|---|---|
CertificateConfirmer, PendingChecker, WaitingExpirer |
Delayed issuance and confirmation |
Revoker, RevocationAuthorizer |
Revocation |
CACertificatesProvider, RootCAUpdateProvider, CertReqTemplateProvider, CRLProvider |
Support messages |
Full control¶
server.New takes any server.Handler, which receives the raw *pkicmp.PKIMessage and the
authenticated SenderIdentity. Use it for custom routing or a proxy. Most applications should use
NewCAServer, because the handler it builds also runs the proof of possession and BasicConstraints
checks.