mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-10-29 00:42:08 +00:00
The last unported derivation, and the one the plan flagged as highest risk. It turned out to be fully reproducible: the key material is deterministic, EdDSA signatures are deterministic (RFC 8032), and the packet layout is fixed -- so the armored blocks match exactly, with no OpenPGP dependency. core/derive/pgp.ts emits the packets directly: new-format headers, the secret-key packet (unencrypted MPI plus 16-bit checksum), the user-id packet, and the self-certification signature with PGPy's exact subpacket set and order, wrapped in ASCII armor with a CRC-24 footer. Verified against PGPy across two seeds, three indices and three user-id shapes -- armored private key, armored public key, and fingerprint. RSA is deliberately unsupported: PyCryptodome's seeded prime search is not reproducible, so the TS port refuses the key type rather than silently deriving a different key. Two things worth recording from the build: the v4 fingerprint reproduces from first principles (SHA-1 over the public-key packet), which is what made byte-parity look feasible before committing to it; and the only mismatch in the first run was packet framing -- PGPy uses new-format headers where I had emitted old-format. CLI: `entry add pgp`, reveal (armored private key), `entry pgp-public`. Cross-impl phases A/B now cover a pgp entry in both directions, completing coverage of every creatable entry kind. 193 core + 47 CLI tests green; 30/30 cross-implementation checks green. Co-Authored-By: Claude Fable 5 <[email protected]>
39 lines
1.5 KiB
TypeScript
39 lines
1.5 KiB
TypeScript
/**
|
|
* PGP key derivation parity against PGPy's output.
|
|
*
|
|
* The key material is deterministic and EdDSA signatures are deterministic,
|
|
* so the armored blocks are expected to match byte-for-byte.
|
|
*/
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
import { pgpKeyCases, pgpCreatedAt, mnemonics } from "@seedpass/test-vectors";
|
|
import { derivePgpKey, pgpFingerprint, PGP_CREATED_AT } from "@seedpass/core";
|
|
|
|
describe("PGP key derivation", () => {
|
|
it("agrees with Python on the pinned creation time", () => {
|
|
expect(PGP_CREATED_AT).toBe(pgpCreatedAt);
|
|
});
|
|
|
|
it.each(pgpKeyCases)("$mnemonic_id index $index fingerprint", (c) => {
|
|
const pair = derivePgpKey(mnemonics[c.mnemonic_id]!, c.index, { userId: c.user_id });
|
|
expect(pair.fingerprint).toBe(c.fingerprint);
|
|
expect(pgpFingerprint(pair.publicKey)).toBe(c.fingerprint);
|
|
});
|
|
|
|
it.each(pgpKeyCases)("$mnemonic_id index $index armored private key", (c) => {
|
|
const pair = derivePgpKey(mnemonics[c.mnemonic_id]!, c.index, { userId: c.user_id });
|
|
expect(pair.privateKeyArmored).toBe(c.private_key_armored);
|
|
});
|
|
|
|
it.each(pgpKeyCases)("$mnemonic_id index $index armored public key", (c) => {
|
|
const pair = derivePgpKey(mnemonics[c.mnemonic_id]!, c.index, { userId: c.user_id });
|
|
expect(pair.publicKeyArmored).toBe(c.public_key_armored);
|
|
});
|
|
|
|
it("refuses unsupported key types rather than deriving a different key", () => {
|
|
expect(() =>
|
|
derivePgpKey(mnemonics["abandon12"]!, 0, { keyType: "rsa" }),
|
|
).toThrow(/not supported/);
|
|
});
|
|
});
|