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.
-
Generate a P-256 key on the Server host:
$ docker compose run --rm agledger-api \ dist/scripts/generate-signing-key.js --algorithm es256 -
Put the new
VAULT_SIGNING_KEYandAGLEDGER_ALLOW_NON_DEFAULT_SIGNING_ALG=trueincompose/.env. -
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 -
Confirm the registry agrees:
$ curl -s http://localhost:3001/v1/verification-keys \ | jq '.data[] | {keyId, algorithm, status, activatedAt, retiredAt}'The new key reads
activeand the old oneretired, 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
- FIPS 186-5: Digital Signature Standard - NIST, the standard that approves EdDSA
- FIPS 140-3 validated modules - NIST CMVP search
- RFC 6979: Deterministic ECDSA - IETF