Created
February 11, 2026 23:19
-
-
Save philips/c00ac10f4ecafda2b8e4f267f639254c to your computer and use it in GitHub Desktop.
Passkey test
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Passkey Signer & Verifier</title> | |
| <style> | |
| body { font-family: system-ui, -apple-system, sans-serif; line-height: 1.5; max-width: 600px; margin: 40px auto; padding: 20px; color: #333; } | |
| .card { border: 1px solid #ddd; padding: 20px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); margin-bottom: 20px; } | |
| button { background: #007bff; color: white; border: none; padding: 10px 18px; border-radius: 6px; cursor: pointer; font-weight: 600; margin-top: 10px; } | |
| button:disabled { background: #ccc; cursor: not-allowed; } | |
| #payload-display { font-family: monospace; background: #eee; padding: 10px; border-radius: 4px; display: block; margin: 10px 0; } | |
| .status { font-weight: bold; margin-top: 15px; } | |
| .success { color: #28a745; } | |
| .error { color: #dc3545; } | |
| </style> | |
| </head> | |
| <body> | |
| <h2>Passkey Tool</h2> | |
| <div class="card"> | |
| <h3>1. Setup</h3> | |
| <p>First, register this device to create a local public key.</p> | |
| <button onclick="register()">Register Passkey</button> | |
| <div id="reg-status" class="status"></div> | |
| </div> | |
| <div class="card"> | |
| <h3>2. Sign Payload</h3> | |
| <p>Payload from URL:</p> | |
| <span id="payload-display">None detected in #hash</span> | |
| <button id="sign-btn" onclick="sign()" disabled>Sign with Passkey</button> | |
| <div id="sign-status" class="status"></div> | |
| </div> | |
| <script> | |
| // --- UTILS --- | |
| const bufferEncode = (str) => new TextEncoder().encode(str); | |
| const base64ToBuffer = (b64) => Uint8Array.from(atob(b64), c => c.charCodeAt(0)); | |
| const bufferToBase64 = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf))); | |
| // --- 1. REGISTRATION --- | |
| async function register() { | |
| const status = document.getElementById('reg-status'); | |
| try { | |
| const options = { | |
| publicKey: { | |
| challenge: crypto.getRandomValues(new Uint8Array(32)), | |
| rp: { name: "Passkey Demo", id: window.location.hostname }, | |
| user: { id: crypto.getRandomValues(new Uint8Array(16)), name: "user@demo", displayName: "Demo User" }, | |
| pubKeyCredParams: [{ alg: -7, type: "public-key" }], // ES256 | |
| authenticatorSelection: { residentKey: "required" }, | |
| timeout: 60000 | |
| } | |
| }; | |
| const cred = await navigator.credentials.create(options); | |
| // Save Credential ID and Public Key for later local use | |
| localStorage.setItem("psk_id", bufferToBase64(cred.rawId)); | |
| localStorage.setItem("psk_pubkey", bufferToBase64(cred.response.getPublicKey())); | |
| status.className = "status success"; | |
| status.innerText = "✅ Passkey Registered!"; | |
| document.getElementById('sign-btn').disabled = false; | |
| } catch (err) { | |
| status.className = "status error"; | |
| status.innerText = "❌ Error: " + err.message; | |
| } | |
| } | |
| // --- 2. SIGNING --- | |
| async function sign() { | |
| const status = document.getElementById('sign-status'); | |
| const payload = window.location.hash.substring(1) || "default-payload"; | |
| const savedId = localStorage.getItem("psk_id"); | |
| try { | |
| const options = { | |
| publicKey: { | |
| challenge: bufferEncode(payload), | |
| allowCredentials: [{ id: base64ToBuffer(savedId), type: 'public-key' }], | |
| userVerification: "required", | |
| rpId: window.location.hostname | |
| } | |
| }; | |
| const assertion = await navigator.credentials.get(options); | |
| // --- 3. IMMEDIATE VERIFICATION --- | |
| const isValid = await verifyLocally(assertion, payload); | |
| if (isValid) { | |
| status.className = "status success"; | |
| status.innerText = "✅ Signature Verified Locally!"; | |
| } else { | |
| throw new Error("Signature failed verification."); | |
| } | |
| } catch (err) { | |
| status.className = "status error"; | |
| status.innerText = "❌ Error: " + err.message; | |
| } | |
| } | |
| // --- 4. VERIFICATION (WEB CRYPTO API) --- | |
| async function verifyLocally(assertion, originalPayload) { | |
| const pubKeyB64 = localStorage.getItem("psk_pubkey"); | |
| if (!pubKeyB64) return false; | |
| // Import public key | |
| const publicKey = await crypto.subtle.importKey( | |
| "spki", | |
| base64ToBuffer(pubKeyB64), | |
| { name: "ECDSA", namedCurve: "P-256" }, | |
| false, | |
| ["verify"] | |
| ); | |
| // Reconstruct the signed data | |
| const clientDataHash = await crypto.subtle.digest("SHA-256", assertion.response.clientDataJSON); | |
| const authData = new Uint8Array(assertion.response.authenticatorData); | |
| const signedData = new Uint8Array(authData.length + clientDataHash.byteLength); | |
| signedData.set(authData, 0); | |
| signedData.set(new Uint8Array(clientDataHash), authData.length); | |
| // Verify | |
| return await crypto.subtle.verify( | |
| { name: "ECDSA", hash: { name: "SHA-256" } }, | |
| publicKey, | |
| assertion.response.signature, | |
| signedData | |
| ); | |
| } | |
| // Initialize UI | |
| window.onload = () => { | |
| const hash = window.location.hash.substring(1); | |
| if (hash) document.getElementById('payload-display').innerText = decodeURIComponent(hash); | |
| if (localStorage.getItem("psk_id")) document.getElementById('sign-btn').disabled = false; | |
| }; | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment