Skip to content

Commit 04f339a

Browse files
committed
Init for public release
0 parents  commit 04f339a

File tree

18 files changed

+3818
-0
lines changed

18 files changed

+3818
-0
lines changed

.github/workflows/test.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
6+
jobs:
7+
test:
8+
strategy:
9+
matrix:
10+
zig-version: [0.14.0, master]
11+
12+
name: Ubuntu / Zig ${{ matrix.zig-version }}
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Setup Zig
20+
uses: mlugg/setup-zig@main
21+
with:
22+
version: ${{ matrix.zig-version }}
23+
24+
- name: Build library
25+
run: zig build
26+
27+
- name: Run tests
28+
run: zig build test --summary all
29+
30+
- name: Check formatting
31+
run: zig fmt --check .

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
zig-out/
3+
.zig-cache/
4+
/ref-*

README.md

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# passcay
2+
3+
[![Tests](https://github.com/uzyn/passcay/actions/workflows/test.yml/badge.svg)](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).

build.zig

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
const std = @import("std");
2+
3+
// Although this function looks imperative, note that its job is to
4+
// declaratively construct a build graph that will be executed by an external
5+
// runner.
6+
7+
/// Helper function to configure OpenSSL
8+
/// This function just does basic system library linking since we'll use the native OpenSSL
9+
fn linkWithOpenSSL(_: *std.Build, step: *std.Build.Step.Compile, target: std.Build.ResolvedTarget) void {
10+
// Always link with libc
11+
step.linkLibC();
12+
13+
// Link with OpenSSL libraries
14+
step.linkSystemLibrary("crypto");
15+
step.linkSystemLibrary("ssl");
16+
17+
// Add special frameworks for macOS
18+
if (target.result.os.tag == .macos) {
19+
// step.linkFramework("Security");
20+
// step.linkFramework("CoreFoundation");
21+
}
22+
}
23+
24+
pub fn build(b: *std.Build) void {
25+
// Standard target options allows the person running `zig build` to choose
26+
// what target to build for. Here we do not override the defaults, which
27+
// means any target is allowed, and the default is native. Other options
28+
// for restricting supported target set are available.
29+
const target = b.standardTargetOptions(.{});
30+
31+
// Standard optimization options allow the person running `zig build` to select
32+
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
33+
// set a preferred release mode, allowing the user to decide how to optimize.
34+
const optimize = b.standardOptimizeOption(.{});
35+
36+
// Add dependencies
37+
const zbor_dep = b.dependency("zbor", .{
38+
.target = target,
39+
.optimize = optimize,
40+
});
41+
const zbor_mod = zbor_dep.module("zbor");
42+
43+
// Create and add the passcay module to the build so it can be referenced as a dependency
44+
const passcay_mod = b.addModule("passcay", .{
45+
.root_source_file = b.path("src/public.zig"),
46+
.target = target,
47+
.optimize = optimize,
48+
});
49+
passcay_mod.addImport("zbor", zbor_mod);
50+
51+
// Now, we will create a static library based on the module we created above.
52+
// This creates a `std.Build.Step.Compile`, which is the build step responsible
53+
// for actually invoking the compiler.
54+
const lib = b.addLibrary(.{
55+
.linkage = .static,
56+
.name = "passcay",
57+
.root_module = passcay_mod,
58+
});
59+
60+
// Link with OpenSSL dynamically
61+
linkWithOpenSSL(b, lib, target);
62+
63+
// This declares intent for the library to be installed into the standard
64+
// location when the user invokes the "install" step (the default step when
65+
// running `zig build`).
66+
b.installArtifact(lib);
67+
68+
// Creates a step for unit testing. This only builds the test executable
69+
// but does not run it.
70+
const lib_unit_tests = b.addTest(.{
71+
.root_source_file = b.path("src/test_entry.zig"),
72+
.target = target,
73+
.optimize = optimize,
74+
});
75+
lib_unit_tests.root_module.addImport("zbor", zbor_mod);
76+
77+
// Link with OpenSSL for tests
78+
linkWithOpenSSL(b, lib_unit_tests, target);
79+
80+
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
81+
82+
// Similar to creating the run step earlier, this exposes a `test` step to
83+
// the `zig build --help` menu, providing a way for the user to request
84+
// running the unit tests.
85+
const test_step = b.step("test", "Run unit tests");
86+
test_step.dependOn(&run_lib_unit_tests.step);
87+
}

build.zig.zon

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
.{
2+
// This is the default name used by packages depending on this one. For
3+
// example, when a user runs `zig fetch --save <url>`, this field is used
4+
// as the key in the `dependencies` table. Although the user can choose a
5+
// different name, most users will stick with this provided value.
6+
//
7+
// It is redundant to include "zig" in this name because it is already
8+
// within the Zig package namespace.
9+
.name = .passcay,
10+
11+
// This is a [Semantic Version](https://semver.org/).
12+
// In a future version of Zig it will be used for package deduplication.
13+
.version = "0.0.0",
14+
15+
// Together with name, this represents a globally unique package
16+
// identifier. This field is generated by the Zig toolchain when the
17+
// package is first created, and then *never changes*. This allows
18+
// unambiguous detection of one package being an updated version of
19+
// another.
20+
//
21+
// When forking a Zig project, this id should be regenerated (delete the
22+
// field and run `zig build`) if the upstream project is still maintained.
23+
// Otherwise, the fork is *hostile*, attempting to take control over the
24+
// original project's identity. Thus it is recommended to leave the comment
25+
// on the following line intact, so that it shows up in code reviews that
26+
// modify the field.
27+
.fingerprint = 0x6afaa0c072c64272, // Changing this has security and trust implications.
28+
29+
// Tracks the earliest Zig version that the package considers to be a
30+
// supported use case.
31+
.minimum_zig_version = "0.15.0-dev.345+ec2888858",
32+
33+
// This field is optional.
34+
// Each dependency must either provide a `url` and `hash`, or a `path`.
35+
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
36+
// Once all dependencies are fetched, `zig build` no longer requires
37+
// internet connectivity.
38+
.dependencies = .{
39+
.zbor = .{
40+
.url = "https://github.com/r4gus/zbor/archive/refs/tags/0.17.2.tar.gz",
41+
.hash = "zbor-0.17.0-kr-CoHIkAwCy2WhoS6MgwSvyQsLWzzNy6a7UTHqMPMmO",
42+
},
43+
// Note: OpenSSL is a system dependency and needs to be installed separately
44+
},
45+
46+
// Specifies the set of files and directories that are included in this package.
47+
// Only files and directories listed here are included in the `hash` that
48+
// is computed for this package. Only files listed here will remain on disk
49+
// when using the zig package manager. As a rule of thumb, one should list
50+
// files required for compilation plus any license(s).
51+
// Paths are relative to the build root. Use the empty string (`""`) to refer to
52+
// the build root itself.
53+
// A directory listed here means that all files within, recursively, are included.
54+
.paths = .{
55+
"build.zig",
56+
"build.zig.zon",
57+
"src",
58+
"demo",
59+
"README.md",
60+
},
61+
}

0 commit comments

Comments
 (0)