Skip to content

feat(webauthn): passkeys in the vault — dcrypt as a software authenticator - #26

Merged
pyramation merged 1 commit into
mainfrom
feat/passkeys
Aug 9, 2026
Merged

feat(webauthn): passkeys in the vault — dcrypt as a software authenticator#26
pyramation merged 1 commit into
mainfrom
feat/passkeys

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

Summary

A passkey is not a stored password — it is a P-256 keypair, and holding keys in an encrypted store is precisely what a platform authenticator is. So dcrypt becomes one: @decryption/webauthn mints the key, keeps the private half as a concealed vault field, and signs the challenge a site issues.

The signed bytes are the whole point:

signature = ECDSA-P256( authenticatorData || sha256(clientDataJSON) )
clientDataJSON = { type, challenge, origin, crossOrigin: false }
authenticatorData = sha256(rpId) || flags || signCount [|| aaguid || credId || coseKey]

The origin and the RP id hash are inside what is signed, so a site cannot obtain a signature naming a different origin — that is the unphishable property, and it is why nothing here can be replayed elsewhere. verifyAssertion checks the signature the way a relying party would, so the round trip is actually proved rather than asserted.

Attestation is fmt: "none" with an all-zero AAGUID. This is software; claiming hardware attestation is the exact lie attestation exists to catch, and a relying party demanding it should reject us.

PasskeyStore puts the key in the vault as a new passkey item kind, concealing only the private key — a site name and a sign count are not secrets, and leaving them readable is what lets the list render without decrypting anything:

Field
rp_id, credential_id, user_handle, user_name, sign_count plain
private_key concealed

assert() persists the advanced sign count before returning, because a site that later sees the count fail to advance is entitled to conclude the key was cloned — losing that write is worse than losing the assertion.

From the CLI:

dcrypt passkey register auth.example.com --user dev@example.com --challenge "$CHALLENGE" --json
dcrypt passkey assert auth.example.com --challenge "$CHALLENGE" --json   # pipes into a relying party
dcrypt passkey list | forget

The challenge is required rather than generated — signing a challenge we invented ourselves proves nothing. It is allowed in argv, unlike a password: it is a public nonce.

Not in this PR: the relying-party half against constructive-db's auth:passkey procedures (webauthn_begin_registration / finish_registration / begin_sign_in / finish_sign_in), and the desktop UI. Those procedures are deliberately crypto-free and are currently tested with opaque-but-plausible bytes, so this is the piece that will let them be exercised with real signatures for the first time.

One infrastructure note: adding a workspace package forces pnpm to re-resolve, which trips minimumReleaseAge on a transitive ws@8.21.3 that the lockfile already pins (published 41h ago, gate is 48h). Rather than weaken the policy, the new importers were added to pnpm-lock.yaml directly — the resolution step is then skipped and the install is a no-op, as on main.

Link to Devin session: https://app.devin.ai/sessions/04636534e07048089ffb6b78142e12cd
Requested by: @pyramation

…cator

A passkey is a P-256 keypair, not a stored password: @decryption/webauthn
mints one, keeps the private half as a concealed vault field, and signs the
challenge a site issues over authenticatorData || sha256(clientDataJSON).

`dcrypt passkey register|list|assert|forget` drives it from the CLI, printing
the JSON a WebAuthn relying party expects, so it can be piped into one.

Attestation is fmt "none" with an all-zero AAGUID — this is software and says
so; a relying party demanding hardware attestation should reject it.
@pyramation pyramation self-assigned this Aug 9, 2026
@devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@pyramation
pyramation merged commit 0f420a6 into main Aug 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant