FIPS 140 hosts

AGLedger signs its chain with Ed25519 by default. A host running OpenSSL in FIPS mode cannot compute Ed25519: the provider carries no EdDSA, and signing fails with error:0308010C:digital envelope routines::unsupported.

For those hosts AGLedger supports a single-Server ES256 configuration. Every signature the Server makes - chain entries, signed statements, and webhook signatures - uses ECDSA P-256 with SHA-256 instead. The FIPS provider ships in the runtime image; no FIPS kernel and no openssl fipsinstall step are required.

Validated against API v1.4.0 on 2026-08-08 (Developer Edition, Docker Compose).

Install

$ ./scripts/install.sh --fips

--fips does two things. It implies ES256, so the installer generates a P-256 vault key and writes AGLEDGER_ALLOW_NON_DEFAULT_SIGNING_ALG=true into compose/.env. That acknowledgment is required: the Server refuses to boot on a non-default algorithm without it, so nobody reaches this configuration by accident. It also records AGLEDGER_FIPS=true, which adds docker-compose.fips.yml to every compose command the scripts run from then on, including upgrade.sh.

That last part is why the flag exists rather than a documented -f list. The overlay mounts compose/openssl-fips.cnf and points OPENSSL_CONF at it. If a later upgrade recreated the containers without it, the host would quietly stop being FIPS and nothing would say so, because an ES256 key signs perfectly well with or without the provider active.

Confirm the provider is active:

$ docker compose exec agledger-api /nodejs/bin/node \
    -e "console.log(require('crypto').getFips())"

1 means active. With an ES256 key the install then runs normally: records notarize, gates evaluate, and a chain scan reports the chain healthy.

What this configuration gives up

The Server enforces each of these rather than degrading quietly.

Federation is not available. The federation transport is Ed25519 and X25519 by design. A Server configured with both ES256 and federation keys refuses to boot and names the conflict. Run single-Server.

Ed25519-pinned surfaces refuse explicitly. A webhook subscription requesting signingAlg: "ed25519" returns 422 naming what this Server can use (ecdsa-p256-sha256, hmac). Nothing silently downgrades to an algorithm you did not ask for.

Your license needs an ES256 reissue. Licenses are Ed25519-signed by default, and this host cannot verify one, so a valid license reads as unverifiable and the Server raises its license banner. The banner carries the remedy rather than calling the file corrupt: request an ES256-signed reissue from AGLedger (no charge, same terms) and replace the configured key.

Agent-signature surfaces are unavailable. Ephemeral certificate issuance and the X-Agent-Signature hook bind Ed25519 agent keys, so both refuse on a FIPS host with the real reason instead of a false "proof-of-possession invalid". API-key authentication is unaffected and is the path here; the record spine is unaffected.

Your chain consumers need a current verifier. Anyone verifying offline needs @agledger/verify 1.4.0 or later. The algorithm support lives in its @agledger/verify-core dependency rather than in verify itself; 1.4.0 is the verify release that resolves a core carrying ES256 under every install shape, including a consumer with an older core pinned at the top level. Quote the verify version, because that is the package people install. Your Server publishes the exact floor per key:

$ curl -s http://localhost:3001/v1/verification-keys \
    | jq '.data[] | {keyId, algorithm, minVerifierVersion}'

The people who have to upgrade are usually not the people who ran the installer. Tell them before they run one, because a verifier that cannot compute the algorithm does not reliably say so: builds from verify-core 1.1.0 on report CHAIN_UNSUPPORTED_ALGORITHM and name the fix, but older builds have no such code path and report a signature failure instead, which reads as tampering on an intact chain.

Switching an install that already has a chain

Do not reinstall. Entries already written must keep verifying, and reinstalling would abandon them. Rotate instead.

  1. Generate a P-256 key on the Server host:

    $ docker compose run --rm agledger-api \
        dist/scripts/generate-signing-key.js --algorithm es256
    
  2. Put the new VAULT_SIGNING_KEY and AGLEDGER_ALLOW_NON_DEFAULT_SIGNING_ALG=true in compose/.env.

  3. Restart. This is what rotates: on boot the Server reconciles the key registry to the configured VAULT_SIGNING_KEY, retiring the previous active key and activating the new one.

    $ docker compose up -d --force-recreate
    
  4. Confirm the registry agrees:

    $ curl -s http://localhost:3001/v1/verification-keys \
        | jq '.data[] | {keyId, algorithm, status, activatedAt, retiredAt}'
    

    The new key reads active and the old one retired, with the retirement instant matching the restart.

POST /v1/admin/vault/signing-keys/rotate exists for the case where the registry has not caught up with the configured key. It runs the same reconciliation on demand, so after a restart it returns status: "already_active". That is the expected answer there, not a failure.

Rotation retires the old key, it does not delete it. GET /v1/verification-keys keeps serving every historical public key with the exact instants it was active (activatedAt / retiredAt), which is what lets a verifier check each entry against the key that actually signed it. Nothing is re-signed and no history is abandoned.

Read the pre-rotation entries carefully, though. Their signatures are Ed25519, and this host cannot compute Ed25519 - that is why you rotated. An on-host chain scan therefore reports those entries unsupported_algorithm rather than verifying them. That is a host capability gap, not a tamper signal: the class is deliberately separate from signature_invalid, and the same entries verify normally with the offline verifiers on any non-FIPS machine. Entries written after the rotation verify on-host as usual.

If an on-host clean scan over the whole history matters to you more than the existing entries do, a fresh install signing ES256 from its first entry is the configuration that gives you that. Rotate when the history is worth keeping; install fresh when it is not.

Re-running install.sh with AGLEDGER_SIGNING_ALGORITHM set does not change the algorithm of an existing install: the signing key is generated only alongside the other secrets, so a run that finds an existing .env keeps the key it already has. The installer says so rather than reporting a success that did not happen.

What is and is not claimed

AGLedger runs correctly with a FIPS provider active and signs with an approved algorithm. That is a statement about how the software is configured and what it calls, not a FIPS 140-2 or 140-3 validation certificate for AGLedger itself, and not a FedRAMP or GovCloud authorization. The validated module is the one your platform supplies.

Ed25519 is itself FIPS-approved under FIPS 186-5, and validated modules implementing it exist. The reason it is unavailable here is the vintage of the provider shipped with the runtime base image, which predates that standard, not any property of the algorithm.

Every release blocks on a boot gate that runs this exact configuration on both architectures, asserting both that the ES256 serve path works and that a federation-configured Server refuses to start under it.

Sources