open-passkey
An open-source library for adding passkey authentication to any app. Built on WebAuthn with hybrid post-quantum signature verification (ML-DSA-65-ES256) that works today in Go and TypeScript.
Server-side and client-side. Core protocol libraries, Go HTTP handlers, and Angular components.
Hybrid Post-Quantum Support
open-passkey implements ML-DSA-65-ES256 hybrid composite signatures (draft-ietf-jose-pq-composite-sigs), combining a NIST-standardized post-quantum algorithm with classical ECDSA in a single credential. Both signature components must verify independently, if either is broken, the other still protects you.
| Algorithm | COSE alg | Status | Go | TS |
|---|---|---|---|---|
| ML-DSA-65-ES256 (composite) | -52 |
IETF Draft | ||
| ML-DSA-65 (PQ only) | -49 |
NIST FIPS 204 | ||
| ES256 (ECDSA P-256) | -7 |
Generally Available |
How it works: During registration, the server advertises preferred algorithms in pubKeyCredParams. During authentication, the core libraries read the COSE alg field from the stored credential and dispatch to the correct verifier automatically. ES256, ML-DSA-65, or the composite ML-DSA-65-ES256 path. No application code changes needed.
Architecture
open-passkey separates concerns into two layers. The core protocol is pure WebAuthn/FIDO2 verification logic with no framework dependencies. Framework bindings are thin adapters that wire the core into specific frameworks. Adding passkey support to a new framework only requires writing an adapter, not reimplementing cryptography.
open-passkey/ ├── spec/vectors/ # Shared JSON test vectors ├── packages/ │ ├── core-go/ # Go core protocol │ ├── server-go/ # Go HTTP bindings │ ├── core-ts/ # TypeScript core protocol │ └── angular/ # Angular components └── tools/vecgen/ # Test vector generation
Code Examples
Server Setup (server-go)
HTTP handlers for the full WebAuthn ceremony. Works with any Go router. Defaults to hybrid ML-DSA-65-ES256 preferred.
import "github.com/locke-inc/open-passkey/packages/server-go" p, _ := passkey.New(passkey.Config{ RPID: "example.com", RPDisplayName: "My App", Origin: "https://example.com", ChallengeStore: passkey.NewMemoryChallengeStore(), CredentialStore: myDBCredentialStore, }) mux := http.NewServeMux() mux.HandleFunc("POST /passkey/register/begin", p.BeginRegistration) mux.HandleFunc("POST /passkey/register/finish", p.FinishRegistration) mux.HandleFunc("POST /passkey/login/begin", p.BeginAuthentication) mux.HandleFunc("POST /passkey/login/finish", p.FinishAuthentication)
Pluggable interfaces: ChallengeStore (single-use challenge storage; in-memory default provided) and CredentialStore (credential persistence; you implement for your DB).
Core Verification (core-go)
Lower-level API. Algorithm dispatch is automatic based on the stored COSE key.
import "github.com/locke-inc/open-passkey/packages/core-go/webauthn" // Registration result, err := webauthn.VerifyRegistration(webauthn.RegistrationInput{ RPID: "example.com", ExpectedChallenge: challengeB64URL, ExpectedOrigin: "https://example.com", ClientDataJSON: credential.Response.ClientDataJSON, AttestationObject: credential.Response.AttestationObject, }) // result.CredentialID, result.PublicKeyCOSE -- store for future auth // Authentication: dispatches to ES256, ML-DSA-65, or ML-DSA-65-ES256 result, err := webauthn.VerifyAuthentication(webauthn.AuthenticationInput{ RPID: "example.com", ExpectedChallenge: challengeB64URL, ExpectedOrigin: "https://example.com", StoredPublicKeyCOSE: storedKeyBytes, StoredSignCount: storedCount, ClientDataJSON: credential.Response.ClientDataJSON, AuthenticatorData: credential.Response.AuthenticatorData, Signature: credential.Response.Signature, })
Algorithm Constants
webauthn.AlgES256 // -7 (ECDSA P-256) webauthn.AlgMLDSA65 // -49 (ML-DSA-65 / Dilithium3) webauthn.AlgCompositeMLDSA65ES256 // -52 (hybrid composite)
Core Verification (core-ts)
Full parity with Go core, ie. same 16 spec vectors passing. Uses Node crypto, @noble/post-quantum for ML-DSA-65, cbor-x for CBOR.
import { verifyRegistration, verifyAuthentication, COSE_ALG_ES256, // -7 COSE_ALG_MLDSA65, // -49 COSE_ALG_COMPOSITE_MLDSA65_ES256, // -52 } from "@open-passkey/core";
The API mirrors the Go core. Same verifyRegistration / verifyAuthentication functions, same automatic algorithm dispatch based on the stored COSE key.
Provider Setup
Headless components with content projection. You provide the UI and the library handles the WebAuthn ceremony.
import { providePasskey } from "@open-passkey/angular"; export const appConfig = { providers: [ provideHttpClient(), providePasskey({ baseUrl: "/passkey" }), ], };
Components
<!-- Registration --> <passkey-register [userId]="userId" [username]="username" (registered)="onRegistered($event)" (error)="onError($event)" #reg> <button (click)="reg.register()" [disabled]="reg.loading()"> Register Passkey </button> </passkey-register> <!-- Authentication --> <passkey-login [userId]="userId" (authenticated)="onAuthenticated($event)" (error)="onError($event)" #login> <button (click)="login.login()" [disabled]="login.loading()"> Sign in with Passkey </button> </passkey-login>
Service API
import { PasskeyService } from "@open-passkey/angular"; @Component({ ... }) class MyComponent { private passkey = inject(PasskeyService); register() { this.passkey.register(userId, username).subscribe(result => { ... }); } login() { this.passkey.authenticate(userId).subscribe(result => { ... }); } }
The client passes through whatever algorithm the server and authenticator negotiate. No client-side changes needed for PQ support.
React component bindings are on the roadmap.
The core TypeScript library (@open-passkey/core) can be used directly in React projects today.
Packages
core-go
16 testsGo core protocol. Registration + authentication ceremony verification. ES256, ML-DSA-65, ML-DSA-65-ES256 composite.
Go stdlib crypto, fxamacker/cbor, cloudflare/circl
server-go
16 testsGo HTTP bindings. Challenge management, 4 ceremony handlers, pluggable store interfaces. Works with any Go router.
Hybrid-preferred: ML-DSA-65-ES256 first in pubKeyCredParams
core-ts
16 testsTypeScript core. Full parity with Go utilizing the same 16 spec vectors. ES256, ML-DSA-65, and composite verification.
Node crypto, @noble/post-quantum, cbor-x
angular
28 testsHeadless Angular components + injectable service. Content projection for custom UI. Handles WebAuthn + server comms.
Passes through server/authenticator algorithm negotiation
Cross-Language Testing
Every implementation runs against the same JSON test vectors in spec/vectors/. These contain real WebAuthn payloads generated by a software authenticator, covering both happy paths and failure modes. When a bug is found in any language, a new vector is added and all implementations gain the test case automatically.
Registration
5 vectors- ES256 + none attestation
- RP ID mismatch
- Challenge mismatch
- Origin mismatch
- Wrong ceremony type
Authentication
5 vectors- ES256 signature
- RP ID mismatch
- Challenge mismatch
- Tampered signature
- Wrong ceremony type
Hybrid PQ
6 vectors- ML-DSA-65-ES256 composite
- RP ID mismatch
- Challenge mismatch
- ML-DSA component tampered
- ECDSA component tampered
- Wrong ceremony type
# Go core cd packages/core-go && go test ./... -v # Go server cd packages/server-go && go test ./... -v # TypeScript core cd packages/core-ts && npm test # Angular cd packages/angular && npm test
Roadmap
Contributing
Strict TDD. To add a new test case:
- Add a vector to
spec/vectors/(or updatetools/vecgen/main.goand regenerate) - Run tests in all languages, the new vector should fail
- Implement the fix in each language
- All vectors pass = done
New language implementation: Create packages/core-{lang}/, write a test runner that loads spec/vectors/*.json, implement until all 16 vectors pass.