Skip to content

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

  1. Initial enrollment. The client sends an ir protected with a MAC derived from the shared secret. senderKID carries my-device, so the server finds the secret through LookupSecret. The server verifies the MAC and the proof of possession, builds a certificate template from the request and calls IssueCertificate. The ip carries the certificate and the CA certificate in caPubs. The client confirms the certificate with certConf and the server answers pkiConf.
  2. Key update. The client signs a kur with the certificate from step 1. The server finds that certificate through LookupCertificate, verifies the signature and issues a certificate for the new key. The client verifies the signed kup against the CA certificate it received in step 1.
  3. Revocation. The client sends an rr signed with the certificate it revokes. The server checks that the signer is that certificate and calls RevokeCertificate. The rp reports 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:

  1. Enrollment - the request types, credentials and request options
  2. Response verification - trust anchors and the CAs that need extra options
  3. Revocation and Support messages

For a server:

  1. Implementing a CA - the CA interface and what the template carries
  2. Authentication - shared secret and certificate lookups
  3. Policy and client compatibility - authorization and the options deployed clients need
  4. Transactions and recovery - limits, cleanup and snapshots