Skip to content

Commit 4d4e947

Browse files
Implement hybrid JWT signing with passkey attestation
- Remove Ethereum dependencies. - Use SQLite for credential storage. - Add comprehensive documentation, including guides for signing and verification flows. - Add integrate tests. - Update project structure and configuration files accordingly.
1 parent fce3169 commit 4d4e947

36 files changed

+10165
-3169
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,6 @@ yarn-error.log*
4141
*.tsbuildinfo
4242
next-env.d.ts
4343

44-
/database.json
44+
# database files
45+
/database.json
46+
/passkeys.db

FLOW.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Signing & Verification Flow
2+
3+
## Signing Flow
4+
5+
### Step 1: Generate Ephemeral Key Pair
6+
7+
```typescript
8+
const ephemeralKeys = await generateEphemeralKeyPair();
9+
// Creates: { publicKey, privateKey, publicKeyJWK, publicKeyFingerprint }
10+
```
11+
12+
**What happens:**
13+
14+
- Ed25519 key pair generated using Web Crypto API
15+
- Public key exported as JWK (JSON Web Key)
16+
- Fingerprint = SHA-256(canonical JWK)
17+
18+
### Step 2: Passkey Signs Ephemeral Public Key
19+
20+
```typescript
21+
const passkeyAttestation = await startAuthentication({
22+
challenge: ephemeralKeys.publicKeyFingerprint,
23+
});
24+
```
25+
26+
**What happens:**
27+
28+
- Challenge = ephemeral public key fingerprint
29+
- User authenticates (biometric, PIN, etc.)
30+
- Passkey (in secure hardware) signs the challenge
31+
- Returns WebAuthn `AuthenticationResponseJSON`
32+
33+
### Step 3: Build JWT Payload
34+
35+
```typescript
36+
const payload = {
37+
message: "your data",
38+
nonce: "unique-value",
39+
timestamp: Date.now(),
40+
epk: ephemeralKeys.publicKeyJWK, // Ephemeral public key
41+
passkey_attestation: {
42+
credential_id: credentialId,
43+
fingerprint: ephemeralKeys.publicKeyFingerprint,
44+
signature: passkeyAttestation, // WebAuthn response
45+
},
46+
};
47+
```
48+
49+
### Step 4: Sign JWT with Ephemeral Private Key
50+
51+
```typescript
52+
const jwt = await new SignJWT(payload)
53+
.setProtectedHeader({ alg: "EdDSA", typ: "JWT" })
54+
.sign(ephemeralKeys.privateKey);
55+
```
56+
57+
**Result:** Standard JWT with structure `header.payload.signature`
58+
59+
---
60+
61+
## Verification Flow
62+
63+
### Stage 1: Standard JWT Verification
64+
65+
```typescript
66+
// Extract ephemeral public key from payload
67+
const parts = jwt.split(".");
68+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
69+
const publicKey = await importJWK(payload.epk, "EdDSA");
70+
71+
// Verify JWT signature using jose
72+
const result = await jwtVerify(jwt, publicKey, { algorithms: ["EdDSA"] });
73+
// ✅ JWT signature valid
74+
```
75+
76+
**Works with ANY JWT library** - This is standard JWT verification.
77+
78+
### Stage 2: Passkey Attestation Verification
79+
80+
```typescript
81+
const attestation = payload.passkey_attestation;
82+
83+
// 1. Verify fingerprint matches ephemeral public key
84+
const fingerprintValid = await verifyPublicKeyFingerprint(
85+
payload.epk,
86+
attestation.fingerprint
87+
);
88+
89+
// 2. Verify passkey signed the fingerprint
90+
const passkeyValid = await verifyAuthenticationResponse({
91+
response: attestation.signature,
92+
expectedChallenge: attestation.fingerprint,
93+
expectedOrigin: origin,
94+
expectedRPID: "localhost",
95+
credential: { id, publicKey, counter, transports },
96+
});
97+
// ✅ Passkey attestation valid
98+
```
99+
100+
**WebAuthn verification** - Ensures hardware-backed trust.
101+
102+
---
103+
104+
## JWT Structure
105+
106+
```
107+
eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdl...
108+
│ │
109+
│ Header (base64url) │ Payload (base64url)
110+
│ { alg: "EdDSA", typ: "JWT" } │ {
111+
│ │ message: "...",
112+
│ │ epk: { kty, crv, x },
113+
│ │ passkey_attestation: { ... }
114+
│ │ }
115+
116+
│ Signature (base64url)
117+
│ EdDSA signature by ephemeral private key
118+
```
119+
120+
---
121+
122+
## Security Properties
123+
124+
### From Stage 1 (JWT)
125+
126+
- Signature integrity (payload can't be modified)
127+
- Standard verification (any JWT library)
128+
129+
### From Stage 2 (Passkey)
130+
131+
- Hardware-backed trust (ephemeral key is attested)
132+
- Origin verification (prevents phishing)
133+
- User presence (proves user interaction)
134+
- Replay protection (counter mechanism)
135+
- Non-repudiation (only passkey holder can attest)
136+
137+
---
138+
139+
## API Endpoints
140+
141+
### Sign JWT
142+
143+
```typescript
144+
POST /api/sign
145+
{ jwt: "eyJ...", credentialId: "..." }
146+
```
147+
148+
### Verify JWT
149+
150+
```typescript
151+
POST / api / validate;
152+
{
153+
jwt: "eyJ...";
154+
}
155+
// Returns: { valid, jwt_verified, passkey_verified, ... }
156+
```
157+
158+
### Verify JWT Only (Stage 1)
159+
160+
```typescript
161+
POST /api/validate
162+
{ jwt: "eyJ...", mode: "jwt_only" }
163+
// Demonstrates standard JWT verification works
164+
```
165+
166+
### Inspect JWT
167+
168+
```typescript
169+
POST /api/validate
170+
{ jwt: "eyJ...", mode: "inspect" }
171+
// Decodes without verification
172+
```

JWT-VERIFICATION-GUIDE.md

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# JWT Verification Guide
2+
3+
## For External Applications
4+
5+
This guide shows how to verify these JWTs in any application.
6+
7+
## Option 1: Use the Verification Library
8+
9+
### Installation
10+
11+
```bash
12+
npm install jose @simplewebauthn/server
13+
```
14+
15+
### Copy the Verifier
16+
17+
Copy `src/lib/jwt-hybrid-verifier.ts` to your project.
18+
19+
### Verify
20+
21+
```typescript
22+
import { verifyHybridJWT } from "./jwt-hybrid-verifier";
23+
24+
const result = await verifyHybridJWT(jwt, origin, async (credentialId) => {
25+
// Your database lookup
26+
const cred = await db.getCredential(credentialId);
27+
return {
28+
publicKey: cred.publicKeyBytes,
29+
counter: cred.counter,
30+
transports: cred.transports,
31+
algorithm: cred.algorithm,
32+
};
33+
});
34+
35+
if (result.valid) {
36+
console.log("✅ JWT verified!");
37+
console.log("Payload:", result.payload);
38+
}
39+
```
40+
41+
## Option 2: Use the REST API
42+
43+
```bash
44+
curl -X POST http://localhost:3000/api/validate \
45+
-H "Content-Type: application/json" \
46+
-d '{"jwt": "eyJhbGci..."}'
47+
```
48+
49+
**Response:**
50+
51+
```json
52+
{
53+
"valid": true,
54+
"jwt_verified": true,
55+
"passkey_verified": true,
56+
"payload": { "message": "...", "nonce": "..." },
57+
"details": {
58+
"jwt_verification": "JWT signature verified",
59+
"passkey_verification": "Passkey attested ephemeral key"
60+
}
61+
}
62+
```
63+
64+
## Option 3: Manual Implementation
65+
66+
### Stage 1: Standard JWT Verification
67+
68+
```typescript
69+
import { jwtVerify, importJWK } from "jose";
70+
71+
// Decode to get ephemeral public key
72+
const parts = jwt.split(".");
73+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
74+
75+
// Import and verify
76+
const publicKey = await importJWK(payload.epk, "EdDSA");
77+
const result = await jwtVerify(jwt, publicKey, { algorithms: ["EdDSA"] });
78+
// ✅ Stage 1 complete
79+
```
80+
81+
### Stage 2: Passkey Attestation
82+
83+
```typescript
84+
import { verifyAuthenticationResponse } from "@simplewebauthn/server";
85+
86+
const attestation = payload.passkey_attestation;
87+
88+
// Verify fingerprint matches
89+
const fingerprintMatches = await verifyPublicKeyFingerprint(
90+
payload.epk,
91+
attestation.fingerprint
92+
);
93+
94+
// Verify WebAuthn signature
95+
const webauthnResult = await verifyAuthenticationResponse({
96+
response: attestation.signature,
97+
expectedChallenge: attestation.fingerprint,
98+
expectedOrigin: origin,
99+
expectedRPID: "localhost",
100+
credential: {
101+
/* from your database */
102+
},
103+
});
104+
// ✅ Stage 2 complete
105+
```
106+
107+
## Verification Modes
108+
109+
### Full (Both Stages)
110+
111+
```json
112+
{ "jwt": "..." }
113+
```
114+
115+
Returns: `{ valid, jwt_verified, passkey_verified }`
116+
117+
### JWT Only (Stage 1)
118+
119+
```json
120+
{ "jwt": "...", "mode": "jwt_only" }
121+
```
122+
123+
Demonstrates standard JWT verification works.
124+
125+
### Inspect (No Verification)
126+
127+
```json
128+
{ "jwt": "...", "mode": "inspect" }
129+
```
130+
131+
Decodes header and payload using jose.
132+
133+
## Other Languages
134+
135+
### Python
136+
137+
```python
138+
import requests
139+
140+
response = requests.post(
141+
'http://localhost:3000/api/validate',
142+
json={'jwt': jwt_token}
143+
)
144+
result = response.json()
145+
```
146+
147+
### Go
148+
149+
```go
150+
type ValidationRequest struct {
151+
JWT string `json:"jwt"`
152+
}
153+
154+
resp, _ := http.Post(
155+
"http://localhost:3000/api/validate",
156+
"application/json",
157+
bytes.NewBuffer(jsonData),
158+
)
159+
```
160+
161+
### Any Language
162+
163+
Use HTTP client → POST to `/api/validate` → Parse JSON response
164+
165+
## FAQ
166+
167+
**Q: Can I use standard JWT libraries?**
168+
A: Yes for Stage 1! The JWT signature is standard EdDSA. Stage 2 requires WebAuthn verification.
169+
170+
**Q: Why two stages?**
171+
A: Stage 1 proves JWT integrity (standard). Stage 2 proves the signing key was attested by hardware (WebAuthn security).
172+
173+
**Q: What if I only do Stage 1?**
174+
A: You get standard JWT security. Stage 2 adds hardware-backed trust.
175+
176+
**Q: Is this production-ready?**
177+
A: Yes. Uses `jose` (20M+ weekly downloads) and `@simplewebauthn/server` (battle-tested).

0 commit comments

Comments
 (0)