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:
thePR0M3TH3ANandClaude Fable 5 committed 2026-08-14 16:46:24 -04:00
1 parent 5dcd779a6f
commit c2d7306bb7
12 files changed
+457 -13

No files matched your search

+6 -5
View File
@@ -17,7 +17,7 @@ P0 gate is green, Python remains the normative reference
| Cross-implementation profile interop | green |
| Sync protocol parity | green |
| Agent security model | green (TS ahead of Python) |
| Feature parity for daily use | partial |
| Feature parity for daily use | partial (all derivations green) |
| Packaging + release | not started |
| Migration + rollback story | not started |
@@ -47,14 +47,15 @@ P0 gate is green, Python remains the normative reference
and the TS CLI restores it with secrets intact. **Green.**
5. **Feature parity for real daily use.** **Partial.** Remaining:
- PGP key material (entries round-trip; derivation not ported — highest
parity risk, PGPy serialization)
- document import/export commands
- `semantic` and `api` command groups (deliberately deferred; decide
whether they block cutover or ship post-cutover)
- PGP RSA keys: unsupported by design (not byte-reproducible). Decide
whether existing RSA entries block cutover; ed25519 is at parity.
Done since this list was written: SSH key derivation (PEM byte-for-byte)
and schema migrations 0→4, so legacy Python profiles now open in TS.
Done since this list was written: SSH and PGP key derivation (both
byte-for-byte) and schema migrations 0→4, so legacy Python profiles now
open in TS.
6. **Packaging and distribution exist.** Not started. Needs: an installable
artifact (npm bin, single-file build, or both), checksums and signatures
+3 -3
View File
@@ -31,7 +31,7 @@ until a later milestone.
| Password KDF (Argon2id) | P0 | green | `password_kdf.json` | `@noble/hashes` argon2id (pure JS, audited family); default (t=2, m=64MiB, p=8) and light params verified. |
| KDF metadata parsing | P0 | green | `kdf_metadata.json`, `legacy_payloads.json` | KdfConfig schema with Python-default fallbacks. |
| SSH key derivation | P1 | green | `ssh_keys.json` | Ed25519 from BIP-85 app 32; PKCS#8 and SubjectPublicKeyInfo PEM match Python `cryptography` byte-for-byte (fixed DER prefixes, no ASN.1 dependency). CLI: `entry add ssh`, reveal (private PEM), `entry ssh-public` (PEM or OpenSSH line — the OpenSSH format is a TS-only convenience Python does not emit). Cross-impl phases A/B include an ssh entry. |
| PGP key derivation | P1/P2 | todo | — | Highest parity risk (PGPy serialization); import/roundtrip first. |
| PGP key derivation | P1/P2 | green | `pgp_keys.json` | Ed25519/EdDSA from BIP-85 app 32, creation time pinned to 2000-01-01Z. OpenPGP packets are emitted directly (no OpenPGP dependency): new-format headers, secret-key/user-id/self-signature packets, ASCII armor with CRC-24. Armored private and public blocks match PGPy byte-for-byte across two seeds, three indices and three user-id shapes. **RSA is deliberately unsupported** — PyCryptodome's seeded prime search is not reproducible, so TS refuses rather than emitting a different key. CLI: `entry add pgp`, reveal, `entry pgp-public`. |
| Nostr snapshot chunk/manifest model | P0 | green | `nostr_snapshot.json` | Kinds 30070/30071/30072; gzip chunking, chunk-hash verify, reassembly of Python-produced chunks, manifest JSON parse, manifest id HMAC (key_index chain). Compressed bytes are deliberately not byte-pinned (deflate encoders differ); cross-decompression verified both ways. |
| Relay transport + sync flows | P0 | green | — (live mock relay) | RelayPool over platform WebSocket (multi-relay publish/fetch, dedupe, signature verification on ingest); publish/fetch snapshot with chunk-hash verify, delta publish/replay. End-to-end tested against an in-process NIP-01 relay incl. corrupted-chunk rejection, and at the CLI level: sync -> destroy local vault -> restore. jsdom skips transport tests (real-browser run: Milestone 6 CI). |
| Deterministic conflict merge | P0 | green | `sync_merge.json` | Full `merge_index_payloads` port: ts precedence, canonical-hash tie-breaks (Python-compatible ensure_ascii canonical JSON), equal-ts field union incl. the `custom_fields: null` quirk, tombstone lifecycle, retention cap, `_sync_meta`. |
@@ -51,7 +51,7 @@ until a later milestone.
| Environment | Status |
|---|---|
| Node 22 (vitest) | green — 173/173 core + 44 CLI |
| Node 22 (vitest) | green — 193/193 core + 44 CLI |
| jsdom browser-like env | green — 148/148 core (3 transport tests Node-only) |
| Real Chromium/Firefox (vitest browser mode) | todo — lands with Milestone 6 web app CI |
@@ -83,7 +83,7 @@ the same values:
Entry coverage in phases A/B: password (plain and policy-constrained), TOTP
(deterministic and imported), key_value, document, seed, managed_account,
nostr, ssh.
nostr, ssh, pgp — every creatable kind.
## Known intentional divergences