Repository navigation
v2.2.0 — seeds and recovery phrases
Seeds and recovery phrases
This SDK gains the most from seeds, because its ML-DSA-65 private key already is a 32-byte seed. paragonie/pqcrypto_compat implements FIPS 204 key generation from a seed but not skEncode/skDecode, so until now a PHP identity could not leave PHP and no other SDK's identity could enter it.
The seed closes that in both directions, because every Activeledger SDK can now import one. The README's opening section said private keys are not portable; that is no longer true and it now says what is.
$pq = KeyPair::fromSeed($seed); // 32 bytes
$pq = KeyPair::fromPhrase($phrase); // BIP-39
$ec = Secp256k1KeyPair::fromSeed($seed);
$ec = Secp256k1KeyPair::fromPhrase($phrase);Measured — the same 32 bytes in PHP and in JavaScript:
php_pub_sha256 = 0afbc415fe4d35d9fd19e53b15eb26865a40030bd6e4c396db2791357d7c9e49 (1952 bytes)
js_pub_sha256 = 0afbc415fe4d35d9fd19e53b15eb26865a40030bd6e4c396db2791357d7c9e49 (1952 bytes)
Secp256k1KeyPair::fromSeed derives the public point by handing OpenSSL a SEC1 private key with the optional public field omitted, which it fills in. This build has neither gmp nor bcmath, so there is no other scalar-to-point route — the result was checked against @noble/curves.
ext-intl is optional and only affects non-ASCII passphrases, which BIP-39 requires to be NFKD-normalised.
The derivation
| Type | Seed from the BIP-39 seed S |
|---|---|
secp256k1 |
HMAC-SHA512("Bitcoin seed", S)[0..32] |
ml-dsa-65 |
HKDF-SHA512(S, salt="", info="activeledger-seed-v1:ml-dsa-65", 32) |
falcon-512 |
HKDF-SHA512(S, salt="", info="activeledger-seed-v1:falcon-512", 48) |
secp256k1 deliberately keeps the older construction: the JavaScript SDK shipped it before the post-quantum types existed, so phrases are already in use, and changing it would hand those users a different key for a phrase that used to work — not an error, just an identity that is no longer theirs.
Refusals that matter
A seed of the wrong length is refused, not padded. For secp256k1 the seed is the private scalar, so an out-of-range one is refused, not reduced mod n — reducing returns a perfectly functional key belonging to a different identity, with nothing downstream reporting a problem.
The phrase is validated, wordlist and checksum. An unchecked phrase does not fail loudly: a typo lands on a different valid key for an identity nobody owns.
Full changelog: v2.1.0...v2.2.0