mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-10-30 17:32:04 +00:00
parity: PGP key derivation, byte-for-byte with PGPy
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]>
This commit is contained in:
1 parent
5dcd779a6f
commit
c2d7306bb7
12 files changed
+457
-13
No files matched your search
@@ -52,7 +52,9 @@ import {
|
||||
addManagedAccountEntry,
|
||||
addNostrKeyEntry,
|
||||
addSshKeyEntry,
|
||||
addPgpKeyEntry,
|
||||
deriveSshKeyPair,
|
||||
derivePgpKey,
|
||||
sshPublicKeyOpenSsh,
|
||||
} from "@seedpass/core";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
@@ -407,6 +409,40 @@ export function buildProgram(io: ProgramIo = defaultIo): Command {
|
||||
await finishAdd(vault, id);
|
||||
});
|
||||
|
||||
commonAddOptions(
|
||||
add
|
||||
.command("pgp <label>")
|
||||
.description("derived Ed25519 PGP key entry")
|
||||
.option("--user-id <uid>", "PGP user id", ""),
|
||||
).action(
|
||||
async (
|
||||
label: string,
|
||||
o: { userId: string; notes?: string; tags?: string[]; archived?: boolean },
|
||||
) => {
|
||||
const vault = await openFromOptions(program.opts());
|
||||
const id = addPgpKeyEntry(vault.index, label, {
|
||||
...commonOpts(o),
|
||||
userId: o.userId,
|
||||
});
|
||||
await finishAdd(vault, id);
|
||||
},
|
||||
);
|
||||
|
||||
entry
|
||||
.command("pgp-public <refOrQuery>")
|
||||
.description("print a PGP entry's public key block (not secret material)")
|
||||
.action(async (refOrQuery: string) => {
|
||||
const vault = await openFromOptions(program.opts());
|
||||
const hit = resolveEntry(vault.index, refOrQuery);
|
||||
if (hit.entry.kind !== "pgp") throw new Error(`${hit.ref} is not a pgp entry`);
|
||||
const key = derivePgpKey(vault.mnemonic, hit.entry.index, {
|
||||
userId: hit.entry.user_id,
|
||||
keyType: hit.entry.key_type,
|
||||
});
|
||||
io.out(JSON.stringify({ fingerprint: key.fingerprint }));
|
||||
io.out(key.publicKeyArmored.trimEnd());
|
||||
});
|
||||
|
||||
entry
|
||||
.command("ssh-public <refOrQuery>")
|
||||
.description("print an SSH entry's public key (not secret material)")
|
||||
|
||||
Reference in new issue
Block a user