Getting started¶
This page runs the example CA and client from the repository and explains what they exchange. Every piece of the example maps to a page of the client or server guide, so once it runs you know what to replace in your own code.
The example CA keeps everything in memory and signs with a throwaway self-signed root. The client enrolls a key with a shared secret, updates it with a signed request and revokes the result.
What you need¶
- Go 1.27.3 or newer, the minimum the module declares
- A clone of the repository
- Port 8080 free on localhost
git clone https://github.com/misiektoja/go-pki-cmp.git
cd go-pki-cmp
Step 1: start the CA¶
go run ./examples/mockserver/cmd
INFO CA ready subject="CN=Test CA" serial=240565888259335087214779031597002404039
INFO mockserver listening addr=localhost:8080 path=/cmp
The program creates the CA, registers one shared secret under the reference my-device and serves
CMP on http://localhost:8080/cmp:
// Command mockserver is a minimal CMP server for testing.
package main
import (
"crypto/x509"
"log/slog"
"net/http"
"os"
"time"
"github.com/misiektoja/go-pki-cmp/examples/mockserver"
"github.com/misiektoja/go-pki-cmp/server"
)
func main() {
// Create a logger for the server.
log := slog.Default()
// Create a CA with shared secrets for MAC-based enrollment.
// Each entry maps a senderKID (reference number) to an Initial Authentication
// Key (IAK). In production these would be loaded from a database or a
// secrets manager.
ca, err := mockserver.New(map[string][]byte{
"my-device": []byte("test-shared-secret"),
}, log)
if err != nil {
log.Error("creating CA", "error", err)
return
}
log.Info("CA ready", "subject", ca.Cert().Subject.String(), "serial", ca.Cert().SerialNumber)
// Connect the CA into a CMP server.
//
// - LightweightPolicy: enforces the mandatory checks from RFC 9483.
// - WithSigner: signs every outgoing response with the CA key; a crypto.Signer
// so an HSM-backed signer can be used in production.
// - WithExtraCerts: appends the CA cert to outgoing response extraCerts.
// - WithSecretLookup: enables MAC (shared-secret) protection for IR/CR.
// - WithCertificateLookup: enables signature protection for KUR/certConf/RR.
// - CentralKeyGeneration: lets a client ask the CA to generate its key pair.
// - WithRAAuthorizer: accepts requests that a registration authority with
// an id-kp-cmcRA certificate from this CA forwards in nested messages.
srv := server.NewCAServer(ca,
server.LightweightPolicy(),
server.WithSigner(ca.Key(), ca.Cert()),
server.WithExtraCerts([]*x509.Certificate{ca.Cert()}),
server.WithSecretLookup(ca),
server.WithCertificateLookup(ca),
ca.CentralKeyGeneration(),
server.WithRAAuthorizer(ca),
)
if err := srv.Err(); err != nil {
log.Error("server misconfigured", "err", err)
os.Exit(1)
}
// Periodically remove stale transactions from clients that never send certConf.
// A transaction is stale once the confirm wait time has passed, 10 seconds
// unless the server is built with WithConfirmWaitTime.
go func() {
for range time.Tick(30 * time.Second) {
srv.CleanupExpired()
}
}()
mux := http.NewServeMux()
mux.Handle("/cmp", srv)
log.Info("mockserver listening", "addr", "localhost:8080", "path", "/cmp")
httpSrv := &http.Server{
Addr: "localhost:8080",
Handler: mux,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
if err := httpSrv.ListenAndServe(); err != nil {
log.Error("server error", "error", err)
}
}
mockserver.MockCA implements the CA side: issuance, revocation, the shared secret and certificate
lookups and the support messages. Its source is in
examples/mockserver.
Step 2: run the client¶
In a second terminal:
go run ./examples/mockclient/cmd
INFO sending IR with MAC protection
INFO enrolled subject="CN=my-device" issuer="CN=Test CA" serial=322200269625980906720798033765771843406
INFO received CA subject="CN=Test CA" issuer="CN=Test CA" serial=240565888259335087214779031597002404039
INFO certificate verified
INFO sending KUR with signature protection
INFO renewed subject="CN=my-device" issuer="CN=Test CA" serial=296525914232565556297304034835094423049
INFO certificate verified
INFO sending RR with signature protection
INFO certificate revoked serial=296525914232565556297304034835094423049
The client runs three operations in turn:
// Command mockclient is a minimal CMP client for testing.
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"log/slog"
"github.com/misiektoja/go-pki-cmp/client"
"github.com/misiektoja/go-pki-cmp/pkicmp"
)
func printCert(log *slog.Logger, label string, cert *x509.Certificate) {
log.Info(label,
"subject", cert.Subject.String(),
"issuer", cert.Issuer.String(),
"serial", cert.SerialNumber.String(),
)
}
func verifyCert(log *slog.Logger, cert *x509.Certificate, trustedCAs *x509.CertPool) {
if _, err := cert.Verify(x509.VerifyOptions{Roots: trustedCAs}); err != nil {
log.Error("certificate verification failed", "error", err)
return
}
log.Info("certificate verified")
}
func main() {
log := slog.Default()
endpoint := "http://localhost:8080/cmp"
subject := pkix.Name{CommonName: "my-device"}
// --- Step 1: Initial enrollment (IR) with MAC protection ---
//
// A new device has no certificate yet. It authenticates using a pre-shared
// secret (Initial Authentication Key) and a reference number (senderKID)
// that tells the server which secret to look up.
// Generate a key pair for the certificate we're requesting.
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Error("generating key", "error", err)
return
}
// Create MAC credentials from the shared secret.
creds, err := pkicmp.NewMACCredentials([]byte("test-shared-secret"))
if err != nil {
log.Error("creating credentials", "error", err)
return
}
// Send the IR request.
c := client.NewClient(endpoint)
log.Info("sending IR with MAC protection")
result, err := c.SendIR(context.Background(), key, creds,
client.WithSenderKID([]byte("my-device")), // reference number for the shared secret
client.WithSender(subject), // sender DN in PKIHeader
client.WithTemplateSubject(subject), // requested certificate subject
)
if err != nil {
log.Error("IR failed", "error", err)
return
}
printCert(log, "enrolled", result.Certificate)
// The IR response includes CA certificates in caPubs for trust bootstrap.
// We use them to verify the enrolled certificate and as trust anchors for
// the server's signature-protected KUR response later.
trustedCAs := x509.NewCertPool()
for _, ca := range result.CAPubs {
printCert(log, "received CA", ca)
trustedCAs.AddCert(ca)
}
verifyCert(log, result.Certificate, trustedCAs)
// --- Step 2: Key update (KUR) with signature protection ---
//
// The device now has a certificate. It proves its identity by signing the
// request with the existing key, and sends the CA chain in extraCerts so
// the server can verify the sender's certificate.
// Generate a new key for the renewed certificate.
newKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Error("generating new key", "error", err)
return
}
// Create signature credentials: the old key signs the request, the old
// certificate identifies the sender, and the CA chain enables the server
// to verify the sender's certificate.
sigCreds, err := pkicmp.NewSignatureCredentials(key, result.Certificate, result.CAPubs...)
if err != nil {
log.Error("creating signature credentials", "error", err)
return
}
// For KUR, configure trust anchors so the client can verify the server's
// signature-protected response.
c = client.NewClient(endpoint, client.WithTrustedCAs(trustedCAs))
log.Info("sending KUR with signature protection")
renewal, err := c.SendKUR(context.Background(), newKey, sigCreds,
client.WithSender(subject),
client.WithTemplateSubject(subject),
)
if err != nil {
log.Error("KUR failed", "error", err)
return
}
printCert(log, "renewed", renewal.Certificate)
verifyCert(log, renewal.Certificate, trustedCAs)
// --- Step 3: Revocation (RR) with signature protection ---
//
// The device retires the renewed certificate. RFC 9483 ยง4.2 expects the
// request to be signed with the certificate being revoked, which proves
// the device is entitled to revoke it.
revCreds, err := pkicmp.NewSignatureCredentials(newKey, renewal.Certificate, result.CAPubs...)
if err != nil {
log.Error("creating revocation credentials", "error", err)
return
}
log.Info("sending RR with signature protection")
err = c.SendRR(context.Background(), renewal.Certificate, pkicmp.CRLReasonCessationOfOperation, revCreds)
if err != nil {
log.Error("RR failed", "error", err)
return
}
log.Info("certificate revoked", "serial", renewal.Certificate.SerialNumber.String())
}
What happens behind the scenes¶
- Initial enrollment. The client sends an
irprotected with a MAC derived from the shared secret.senderKIDcarriesmy-device, so the server finds the secret throughLookupSecret. The server verifies the MAC and the proof of possession, builds a certificate template from the request and callsIssueCertificate. Theipcarries the certificate and the CA certificate incaPubs. The client confirms the certificate withcertConfand the server answerspkiConf. - Key update. The client signs a
kurwith the certificate from step 1. The server finds that certificate throughLookupCertificate, verifies the signature and issues a certificate for the new key. The client verifies the signedkupagainst the CA certificate it received in step 1. - Revocation. The client sends an
rrsigned with the certificate it revokes. The server checks that the signer is that certificate and callsRevokeCertificate. Therpreports acceptance.
The server log shows each lookup, issuance, confirmation and revocation.
Use the library in your own module¶
go get github.com/misiektoja/go-pki-cmp
Each piece of the example has a production replacement:
| Example piece | What replaces it |
|---|---|
The secrets map of MockCA |
A SecretLookup over your device registry with unique references, see Authentication |
| The in-memory self-signed CA | Your CA behind CA.IssueCertificate and a crypto.Signer such as an HSM key in WithSigner, see Implementing a CA |
LightweightPolicy alone |
A MiddlewareChain with your authorization policy in front of it, see Policy and client compatibility |
The CleanupExpired ticker |
Keep it. Add snapshots when the server must survive a restart, see Transactions and recovery |
| A client without a trust anchor | client.WithTrustedCAs wherever an anchor is known, see Response verification |
Next steps¶
For a client:
- Enrollment - the request types, credentials and request options
- Response verification - trust anchors and the CAs that need extra options
- Revocation and Support messages
For a server:
- Implementing a CA - the
CAinterface and what the template carries - Authentication - shared secret and certificate lookups
- Policy and client compatibility - authorization and the options deployed clients need
- Transactions and recovery - limits, cleanup and snapshots