|
| 1 | +# passcay |
| 2 | + |
| 3 | +[](https://github.com/uzyn/passcay/actions/workflows/test.yml) |
| 4 | + |
| 5 | + |
| 6 | +**Minimal**, **fast** and **secure** Passkey (WebAuthn) relying party (RP) library for Zig. |
| 7 | + |
| 8 | +## Features |
| 9 | + |
| 10 | +- Passkey WebAuthn registration |
| 11 | +- Passkey WebAuthn authentication/verification (login) |
| 12 | +- Attestation-less passkey usage (privacy-preserving, does not affect security) |
| 13 | +- Cryptographic signature verification. Supports both ES256 & RS256, covering 100% of all Passkey authenticators today. |
| 14 | +- Secure challenge generation |
| 15 | + |
| 16 | +## Dependencies |
| 17 | + |
| 18 | +Compiles and tested on both Zig stable 0.14+ and nightly (0.15+). |
| 19 | + |
| 20 | +Dynamically linked to system's OpenSSL for crypto verification. |
| 21 | + |
| 22 | +Works on Linux and macOS. Not yet on Windows. |
| 23 | + |
| 24 | +### Installation |
| 25 | + |
| 26 | +Add `passcay` to your `build.zig.zon` dependencies: |
| 27 | + |
| 28 | +```zig |
| 29 | +.dependencies = .{ |
| 30 | + .passcay = .{ |
| 31 | + .url = "https://github.com/uzyn/passcay/archive/main.tar.gz", |
| 32 | + // Optionally pin to a specific commit hash |
| 33 | + }, |
| 34 | +}, |
| 35 | +``` |
| 36 | + |
| 37 | +And update your `build.zig` to load `passcay`: |
| 38 | + |
| 39 | +```zig |
| 40 | +const passcay = b.dependency("passcay", .{ |
| 41 | + .optimize = optimize, |
| 42 | + .target = target, |
| 43 | +}); |
| 44 | +exe.root_module.addImport("passcay", passcay.module("passcay")); |
| 45 | +``` |
| 46 | + |
| 47 | +## Build & Test |
| 48 | + |
| 49 | +```sh |
| 50 | +zig build |
| 51 | +zig build test --summary all |
| 52 | +``` |
| 53 | + |
| 54 | + |
| 55 | +## Usage |
| 56 | + |
| 57 | +### Registration |
| 58 | + |
| 59 | +```zig |
| 60 | +const passcay = @import("passcay"); |
| 61 | +
|
| 62 | +const input = passcay.register.RegVerifyInput{ |
| 63 | + .attestation_object = attestation_object, |
| 64 | + .client_data_json = client_data_json, |
| 65 | +}; |
| 66 | +
|
| 67 | +const expectations = passcay.register.RegVerifyExpectations{ |
| 68 | + .challenge = challenge, |
| 69 | + .origin = "https://example.com", |
| 70 | + .rp_id = "example.com", |
| 71 | + .require_user_verification = true, |
| 72 | +}; |
| 73 | +
|
| 74 | +const reg = try passcay.register.verify(allocator, input, expectations); |
| 75 | +
|
| 76 | +// Save reg.credential_id, reg.public_key, and reg.sign_count |
| 77 | +// to database for authentication |
| 78 | +``` |
| 79 | + |
| 80 | +Store the following in database for authentication: |
| 81 | +- `reg.credential_id` |
| 82 | +- `reg.public_key` |
| 83 | +- `reg.sign_count` (usually starts at 0) |
| 84 | + |
| 85 | +### Authentication |
| 86 | + |
| 87 | +```zig |
| 88 | +const challenge = try passcay.challenge.generate(allocator); |
| 89 | +// Pass challenge to client-side for authentication |
| 90 | +
|
| 91 | +const input = passcay.auth.AuthVerifyInput{ |
| 92 | + .authenticator_data = authenticator_data, |
| 93 | + .client_data_json = client_data_json, |
| 94 | + .signature = signature, |
| 95 | +}; |
| 96 | +
|
| 97 | +const expectations = passcay.auth.AuthVerifyExpectations{ |
| 98 | + .public_key = user_public_key, // Retrieve public_key from database, given credential_id from navigator.credentials.get |
| 99 | + .challenge = challenge, |
| 100 | + .origin = "https://example.com", |
| 101 | + .rp_id = "example.com", |
| 102 | + .require_user_verification = true, |
| 103 | + .enable_sign_count_check = true, |
| 104 | + .known_sign_count = stored_sign_count, |
| 105 | +}; |
| 106 | +
|
| 107 | +const auth = try passcay.auth.verify(allocator, input, expectations); |
| 108 | +``` |
| 109 | + |
| 110 | +Update the stored sign count with `auth.recommended_sign_count`: |
| 111 | + |
| 112 | +### Client-Side (JavaScript) |
| 113 | + |
| 114 | +```javascript |
| 115 | +// Registration |
| 116 | +const regOptions = { |
| 117 | + challenge: base64UrlDecode(challenge), |
| 118 | + rp: { |
| 119 | + name: "Example", |
| 120 | + id: "example.com", // Must match your domain without protocol/port |
| 121 | + }, |
| 122 | + user: { name: username }, |
| 123 | + pubKeyCredParams: [ |
| 124 | + { type: "public-key", alg: -7 }, // ES256 (Most widely supported) |
| 125 | + { type: "public-key", alg: -257 }, // RS256 |
| 126 | + ], |
| 127 | + authenticatorSelection: { |
| 128 | + authenticatorAttachment: "platform", |
| 129 | + userVerification: "required", // or "preferred" |
| 130 | + }, |
| 131 | + attestation: "none", // Fast & privacy-preserving auth without security compromise |
| 132 | +}; |
| 133 | + |
| 134 | +const credential = await navigator.credentials.create({ publicKey: regOptions }); |
| 135 | +console.log('Credential details:', credential); |
| 136 | +// Pass credential to server for verification: passcay.register.verify |
| 137 | + |
| 138 | +// Authentication |
| 139 | +const authOptions = { |
| 140 | + challenge: base64UrlDecode(challenge), |
| 141 | + rpId: 'example.com', |
| 142 | + userVerification: 'preferred', |
| 143 | +}; |
| 144 | +const assertion = await navigator.credentials.get({ publicKey: authOptions }); |
| 145 | +console.log('Assertion details:', assertion); |
| 146 | +// Retrieve public_key from assertion_id that's returned |
| 147 | +// Pass assertion to server for verification: passcay.auth.verify |
| 148 | +``` |
| 149 | + |
| 150 | + |
| 151 | +<details> |
| 152 | + |
| 153 | +<summary>JavaScript utils for base64url <-> ArrayBuffer</summary> |
| 154 | + |
| 155 | +```javascript |
| 156 | +// Convert base64url <-> ArrayBuffer |
| 157 | +function base64UrlToBuffer(b64url) { |
| 158 | + const pad = '='.repeat((4 - (b64url.length % 4)) % 4); |
| 159 | + const b64 = (b64url + pad).replace(/-/g, '+').replace(/_/g, '/'); |
| 160 | + const bin = atob(b64); |
| 161 | + const arr = new Uint8Array(bin.length); |
| 162 | + for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); |
| 163 | + return arr.buffer; |
| 164 | +} |
| 165 | +function bufferToBase64Url(buf) { |
| 166 | + const bytes = new Uint8Array(buf); |
| 167 | + let bin = ''; |
| 168 | + for (const b of bytes) bin += String.fromCharCode(b); |
| 169 | + return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +</details> |
| 174 | + |
| 175 | +## Demo |
| 176 | + |
| 177 | +Reference implementations for integrating passcay into your application: |
| 178 | + |
| 179 | +- `demo/register.md` - Registration flow with challenge generation |
| 180 | +- `demo/login.md` - Authentication flow with verification |
| 181 | + |
| 182 | +## See also |
| 183 | + |
| 184 | +For passkey authenticator implementations and library for Zig, check out [Zig-Sec/keylib](https://github.com/Zig-Sec/keylib). |
| 185 | + |
| 186 | + |
| 187 | + ## Spec references |
| 188 | + |
| 189 | +- [W3C WebAuthn](https://www.w3.org/TR/webauthn/) |
| 190 | +- [FIDO2 Client to Authenticator Protocol (CTAP)](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html) |
| 191 | + |
| 192 | +## License |
| 193 | +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. |
| 194 | + |
| 195 | +Copyright (c) 2025 [U-Zyn Chua](https://uzyn.com). |
0 commit comments