Motivation
age is a widely-deployed file-encryption format. Both its classic (X25519, since 2021)
and post-quantum (X-Wing / HPKE-MLKEM768-X25519, age v1.3.0+ Dec 2024) identity types take a 32-byte uniform seed as the secret-key material — which is precisely what BIP-85's HEX application at num_bytes=32 already returns.
This PR adds a brief example-use subsection at the end of the HEX application section, documenting how to interpret the existing 32-byte HEX output as an age identity. No new application number, no normative MUSTs added, no changes required in existing implementations — the HEX application already produces exactly what age consumes; this PR just records the connection in the spec.
Documenting this gives users a single backup story: the same BIP-39 mnemonic that already secures their Bitcoin wallets can deterministically produce age identities, with no additional key-rotation infrastructure on top of what they already do.
It also unblocks downstream hardware-wallet firmware integration maintainers of firmware projects accept "implement this BIP-85 use case" much more readily than "implement a convention from a third-party repo." A spec-blessed convention turns the conversation from a bespoke contribution into a standards-conformance issue.
What changes
A new ====Example use: age key derivation==== subsection at
the end of the ===HEX=== section in bip-0085.mediawiki.
The subsection:
- Describes the bech32 encoding for both classic and PQ age identities + their corresponding recipients.
- Adds a worked test vector using the same master xprv as the existing HEX test vector, so a reader can derive both side-by-side from one master.
- Links to a reference implementation (age-keygen-det).
Test vectors
Derived from the master xprv already used by the HEX test vector in the same section. Reproducible end-to-end with stock tools:
INPUT:
* MASTER BIP32 ROOT KEY: xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb
* PATH: m/83696968'/128169'/32'/0'
OUTPUT
* DERIVED ENTROPY=ea3ceb0b02ee8e587779c63f4b7b3a21e950a213f1ec53cab608d13e8796e6dc
# Classic (X25519):
* DERIVED AGE IDENTITY=AGE-SECRET-KEY-1AG7WKZCZA689SAMECCL5K7E6Y854PGSN78K98J4KPRGNAPUKUMWQWNNT4U
* DERIVED AGE RECIPIENT=age1m0hhzxelxsxnxm4ennvdpk75j8s7mn5w4tt3e4ntug5qx256wslqmdz8e9
# Post-quantum (X-Wing):
* DERIVED AGE PQ IDENTITY=AGE-SECRET-KEY-PQ-1AG7WKZCZA689SAMECCL5K7E6Y854PGSN78K98J4KPRGNAPUKUMWQ5AN2M5
* DERIVED AGE PQ RECIPIENT (SHA-256)=feecdb11f82478ea4b9dd934965f974e37701d963ac2ec5d4fb120357032641a
The PQ recipient is a 1959-character bech32 string (it encodes
the 1216-byte X-Wing public key); only its SHA-256 is shown
inline. The full string is regenerable from the seed via
X-Wing.NewKeyFromSeed(seed).EncapsulationKey() and lives in
the reference implementation's test data.
End-to-end verification path:
bipsea derive -a hex -n 64 -i 0 -x <MASTER>reproduces the existing HEX64 test vector (sanity check).bipsea derive -a hex -n 32 -i 0 -x <MASTER>produces the 32-byte entropy above.- Bech32-encoding with the respective HRPs produces the identity strings.
- Stock
age-keygen -y(age v1.3.0+) on each identity returns the matching recipient byte-for-byte (classic full string; PQ full string hashes to the stated SHA-256).
Scope
This amendment is intentionally minimal:
- No new application number — reuses the existing HEX application (128169').
- No normative MUSTs added beyond what BIP-85 already says about the HEX application.
- No changes required in existing implementations (bipsea, bip85-js, ethankosakovsky/bip85).
Possible follow-ups (not in this PR):
- A dedicated
AGEsubsection paralleling the existing RSA-GPG subsection, with full worked examples for each flavour and an optional index-partition recommendation (e.g.[0, 1000)for PQ identities,[1000, 2000)for classic identities, to avoid producing the same 32 bytes under two different HRPs when the same master is used for both flavours). - A dedicated application number for age (e.g.
0x616765'= hex-pack of ASCII "age"), with independent paths per flavour.
These are noted only to clarify that this PR deliberately picks the minimal form; they can be raised separately if there is appetite.
Reference implementation
age-keygen-det
— single-binary Go tool that converts a 32-byte hex seed into
an age identity file (classic or PQ). The output format mirrors
stock age-keygen so existing age tooling (age-keygen -y,
age -d, age -r) works unmodified.
- BSD-3-Clause licensed.
- Cross-validates byte-for-byte against stock
age-keygen -yin CI for both flavours. - Reproducible static build (
CGO_ENABLED=0,-trimpath,-buildid=); same source → byte-identical binary.
A worked end-to-end example using bipsea + age-keygen-det + the spec's own master xprv is documented in the project's README under "Worked example: BIP-85 → age".
Open question
Should the subsection also include the index-range partition guidance ([0, 1000) for PQ identities, [1000, 2000) for
classic identities) to avoid producing the same 32 bytes under two different HRPs when both flavours are derived from one master? The reference implementation does NOT enforce this partition — it is a documentation convention, not a crypto contract. Including it would be useful guidance; excluding it keeps the amendment maximally minimal. Open to reviewer preference.
cc @ethankosakovsky @akarve — original BIP-85 authors per the BIP header. Happy to revise based on review feedback.