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.

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.

With AGLEDGER_FIPS=true set, the API and worker log FIPS provider active at boot, and refuse to start when the provider is not active rather than run on the default provider under a FIPS configuration. To ask a running container directly:

$ docker compose exec -e NODE_OPTIONS= 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 signs with Ed25519 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. The acknowledgment is required here for the same reason it is on a fresh --fips install: the Server refuses to boot on a non-default algorithm without it.

  3. Restart. This stages the key: on boot the Server reconciles the registry to the configured VAULT_SIGNING_KEY, activating it beside the Ed25519 key that is already active. Nothing is retired yet, and the install signs with both until you close the old key's window.

    $ docker compose up -d --force-recreate
    
  4. Turn the FIPS provider on. Staging an ES256 key does not by itself run the Server under the FIPS provider; that needs AGLEDGER_FIPS=true and the docker-compose.fips.yml overlay, and --fips refuses to add them over an existing Ed25519 key. Re-run the installer now that the active key is ES256, then confirm the provider exactly as on a fresh install:

    $ ./scripts/install.sh --fips
    $ docker compose exec -e NODE_OPTIONS= agledger-api /nodejs/bin/node \
        -e "console.log(require('crypto').getFips())"
    
  5. Retire the Ed25519 key, once every api and worker process has rolled onto the new one. Read GET /health on each process directly, not through a load balancer, and check signingKey.keyId reports the ES256 key everywhere; lastSignedAt is not the check, because it reads null on an idle process or one that has only signed webhooks or certificates. Then:

    $ curl -X POST -H "Authorization: Bearer $AGLEDGER_PLATFORM_KEY" \
        "$AGLEDGER_API_URL/v1/admin/vault/signing-keys/<old-ed25519-key-id>/retire"
    

    You end up with an active P-256 key beside the retired Ed25519 one, each carrying the exact instants it was active, which is what lets a verifier check every entry against the key that actually signed it. Nothing is re-signed and no history is abandoned. The routine version of these two steps is in day-2 operations; nothing about them changes for an algorithm change.

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.

The result of a full scan (POST /v1/admin/vault/scan, then GET /v1/admin/vault/scan/{jobId}) across a mixed history counts those records under unsupportedAlgorithm, apart from broken and signatureErrors, and they do not make healthy false (result abridged):

{"recordsScanned": 4, "verified": 0, "broken": 0, "signatureErrors": 0, "healthy": true,
 "unsupportedAlgorithm": 4,
 "brokenRecords": [{"recordId": "019fe8a5-...", "brokenAt": 1, "reason": "unsupported_algorithm"}],
 ...}

healthy: true there means nothing on the host was found wrong, not that those entries were checked: verified does not count them. The same database dumped and checked off the FIPS host resolves each entry against whichever key signed it and reports it clean; the audit runbook has that handoff. Decide up front where your pre-switch history gets verified, and tell whoever watches chain health to read unsupportedAlgorithm alongside healthy.

If on-host verification of 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 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 FIPS boot gate that runs this exact configuration on linux/amd64, asserting both that the ES256 serve path works and that a federation-configured Server refuses to start under it. The general boot gate (migrations, /health, /llms.txt, /v1/verification-keys) runs on both linux/amd64 and linux/arm64; the FIPS-provider assertions run on linux/amd64 only. The arm64 image is built from the same sources and is not exercised under the FIPS provider by that gate.

Sources