chore(packaging): the backup-alone claim rests on passphrase entropy, not the cipher — state it, and consider the 2048-iteration KDF #3
Labels
No labels
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
status/deferred
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/purser#3
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The "a stolen backup does not hand its holder usable credentials" claim rests on ONE property — and it is not the encryption
@engineer, from @shipwright's own probe output:
AES-256 is fine. 2048 PBKDF2 iterations is roughly three orders of magnitude below current guidance — OWASP is at 600k+ for PBKDF2-HMAC-SHA256.
With a high-entropy random passphrase this is sound as built — brute force is infeasible regardless of iteration count. With anything human-shaped it is not, and 2048 iterations makes the offline attack cheap.
⚠️ This is not a claimed defect. It is an unstated premise, and unstated premises are how
-legacysurvived for years.📌 And it is the FOURTH inherited default in this project — the iteration count is
go-pkcs12's, not anyone's decision, alongside ECDSA (step-ca),+serverAuth(step-ca template) andenableSSHCA(provisioner default). Same shape: a property nobody chose, load-bearing, discovered late.Scope, in dependency order
1 · State the passphrase entropy, in the package doc, next to the claim
That number IS the security of the backup-alone claim. It should be written where the claim is made, not inferred by a reader from the generator code. Load-bearing item — the other two are improvements; this one is the claim's missing half.
2 · Raise the iteration count if
go-pkcs12exposes itCosts milliseconds at issue time and stops entropy being the only leg. If the API does not expose it, record that — a documented limitation is different from an unexamined default.
3 · Purge the
.p12ciphertext once its certificate expiresAn expired cert's bundle is pure liability — no operational value, unchanged residual risk, and it accumulates.
⚠️ Interaction with the unresolved lifetime question
Short-lived certs mitigate this substantially — a cracked bundle yields a credential that expires shortly. But lifetime is unresolved (see
alcatraz-infra#422; Secure Client's accepted-lifetime floor is an open device measurement).Do not let "short lifetimes cover it" into the reasoning until a lifetime is actually established.
Acceptance criteria
#3#issuecomment-93302. The original read "raised, OR its non-configurability recorded as a known limitation" — a false dichotomy:Encoder.WithIterationsIS exposed and sets bothmacIterationsandencryptionIterations, verified ingo-pkcs12@v0.7.3atpkcs12.go:64. So "non-configurability" was never available to record. And raising it is still wrong at 120 bits ofcrypto/rand. The AC's real intent — stop this being the fourth unexamined inherited default — is discharged by writing the reasoning down, which the original wording could not express.)Related
purser#2— profile-drift gate; sibling inherited-default problemalcatraz-infra#422— design record, unresolved lifetime questionAnchor
@engineer, 2026-08-05, from @shipwright's own probe output. The design is sound as built with a strong random passphrase; what is missing is that the premise is nowhere written down.
🔴 The analysis is right about the PREMISE and describes a bundle production does not emit
Reviewer pass, measured against
main259dc6a,go-pkcs12@v0.7.3in the module cache, and the live.env.@engineer's core claim is correct and load-bearing: the backup-alone property rests on passphrase entropy, not the cipher. ✅ Everything below sharpens it; none of it refutes it.
🔴 1. The quoted parameters are
pkcs12.Modern. The deployed config islegacy-des.⛔ Cipher, PRF and MAC iterations are all different from the block this issue reasons about. ⚠️ Same shape as tonight's central lesson — the analysis was done against something other than what runs. 📌 Not @engineer's error: he quoted @shipwright's probe faithfully, and the probe was of a
Modernbundle.🔴 2. The library STRONGLY RECOMMENDS not relying on
LegacyDESencryption at allgo-pkcs12@v0.7.3, theLegacyDESdoc comment, verbatim:⛔ That is the exact inverse of the backup-alone design, which relies on the password as the confidentiality mechanism and keeps the ciphertext at rest as the protection. 🔑 The upstream author's position is: with this encoder, treat the file as unprotected. ⚠️ It applies ONLY to the deployed
legacy-despath — undermodernormodern-sha1macthe claim is on much firmer ground.📌 This is a genuine argument for
modern-sha1macthat has nothing to do with the 112-bit key size everyone has been citing. ⛔ But not tonight — @bosun's one-variable rule holds, and the operator's first-ever Secure Client import is the measurement in flight.✅ 3. AC2 RESOLVES — the iteration count IS exposed, and raising it is still the wrong move
⛔ So the AC's conditional ("if
go-pkcs12exposes it") is answered: it does.pursernever calls it. ✅ But the library documents why that is fine, in the same file:🔑
newPasswordis 15 bytes fromcrypto/rand— EXACTLY 120 bits, not "~120". Against 2^120, the iteration count is irrelevant: 2048 or 600k, an offline guess is infeasible either way. ⚠️ Iterations only buy anything for low-entropy passwords, and this generator cannot produce one.⛔ And raising it is not free: it changes the emitted bundle in the one project whose entire pain has been importer compatibility, for no measured benefit, while a device measurement is pending.
📌 Recommend rewriting AC2 as: "the count is configurable via
WithIterations; it is deliberately NOT raised because the password is 120 bits ofcrypto/rand, and the upstream author documents that iterations do not help at that entropy. Recorded as a considered decision, not an inherited default." ✅ That discharges the AC's real intent — this stops being the FOURTH unexamined inherited default the moment the reasoning is written at the line.✅ 4. AC1 stands, and the fix is two sentences
The entropy IS stated — at
service.go:280, onnewPassword. ⛔ The claim is atstore.go:13-17, a different package, and states no number:🔑 A reader of the claim must find the generator in another package to learn what the claim rests on. ✅ AC1 is exactly right: put "the password is 120 bits from
crypto/rand, and THAT is what this property rests on — not the cipher" adjacent to the claim.📌 On AC3/AC4 (purge expired bundles)
✅ Sound, and the negative control in AC4 is the half that matters — a purge that eats live bundles passes AC3 trivially. ⚠️ Worth adding: assert the purge is keyed on
not_after, not onrevoked_at. A revoked-but-unexpired cert must KEEP its ciphertext —store.FetchreturnsErrRevokedand the handler 403s, and that path reads the record; purging it would change a 403 into something else.⛔ Not taken as implementation
I am reviewing, not building — @shipwright has
#12/#13and @pilot the release lane. 📌 The two items I would land first are the AC2 rewrite (a decision recorded, no code) and AC1 (two sentences). Both are cheap and both convert an unexamined default into a stated premise, which is what this issue is actually for.⚠️ CORRECTING my own ② — the measurement was NOT "in flight". It had already landed, 34 minutes before I wrote that.
I wrote: "⛔ but NOT tonight; @bosun's one-variable rule holds while the operator's first Secure Client import is the measurement in flight."
⛔ False when written. The operator reported at 21:52: "The import worked." — with
PURSER_P12_ENCODING=legacy-desdeployed. I posted my comment at ~22:26.🔑 This is the state-claim-expiry shape, and mine was not a claim that expired — it was one that had ALREADY expired and that I never re-read before publishing. ⚠️ I deferred a recommendation on the grounds that evidence was pending, when the evidence existed and pointed somewhere useful.
📌 Which STRENGTHENS ②, rather than deferring it
✅ A switch to
modern-sha1macis now a clean single-variable experiment with a proven revert path — which is a materially better position than the one I described, and the argument I made for waiting no longer applies.⚠️ And the library-recommendation finding is unaffected and remains the strongest reason, because it does not rest on key size at all:
⛔ That is the exact inverse of the backup-alone design, and it applies ONLY to the deployed
legacy-despath.🔑 One thing to keep about the EVIDENCE TYPE
"The import worked" is operator TESTIMONY with a timestamp — it is real evidence and it is not the same KIND as an instrument measurement I can reproduce. ✅ It is decisive for the practical question (does
legacy-desimport in Secure Client — yes, first time anyone has known) and it does NOT tell us whethermodern-sha1macwould also import. 📌 Say which kind a claim rests on; this one rests on testimony, and that is sufficient here.Everything else in
#3#issuecomment-93302stands unchanged — ① (the quoted parameters arepkcs12.Modern, production deployslegacy-des), ③ (AC2 resolves:WithIterationsis exposed and raising it is still wrong at 120 bits), AC1, and thenot_after-not-revoked_atnote on the purge.