Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 85 additions & 43 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,67 @@
# AGENTS.md -- zig-ctap2

## Capabilities
## Persona

- CBOR encoding/decoding (CTAP2 subset)
- CTAP2 command encoding and response parsing
- CTAPHID USB HID transport framing
- Platform USB HID device enumeration and I/O
- CTAP2 Client PIN protocol v2 (ECDH + AES + HMAC)
You are working on zig-ctap2, a portable CTAP2/FIDO2 library written in Zig with a C FFI surface. It communicates directly with USB security keys (YubiKey, SoloKeys, etc.) over HID -- IOKit on macOS, hidraw on Linux. No Apple entitlements or platform authentication frameworks needed. Part of the [Tinyland Zig Libraries](https://libs.tinyland.dev).

## Stack

- **Language:** Zig 0.14.1+
- **Output:** Static C library (`libctap2.a`) + Zig module
- **Dependencies:** None (pure Zig `std.crypto` for ECDH/AES/SHA/HMAC)
- **Header:** `include/ctap2.h` (17 C FFI functions)
- **Platform I/O:** IOKit + CoreFoundation (macOS), hidraw (Linux)
- **Tests:** Unit tests per module + property-based tests (1000 iterations) + hardware integration tests (YubiKey)
- **Docs:** Zig autodoc (`zig build docs`)

## Structure

```
src/ffi.zig C FFI exports (17 functions)
src/cbor.zig Minimal CBOR encoder/decoder (CTAP2 subset)
src/ctap2.zig CTAP2 command encoding and response parsing
src/ctaphid.zig CTAPHID transport framing (64-byte packets)
src/pin.zig Client PIN protocol v2 (ECDH P-256 + AES-256-CBC + HMAC-SHA-256)
src/hid.zig Platform-selected HID transport
src/hid_macos.zig macOS USB HID via IOKit
src/hid_linux.zig Linux USB HID via hidraw
include/ctap2.h C header
tests/pbt_*.zig Property-based tests
tests/hardware_test.zig Hardware integration tests (YubiKey)
examples/ C usage examples
```

## Commands

```bash
zig build # static library -> zig-out/lib/
zig build -Doptimize=ReleaseFast # optimized build
zig build test # unit tests (no hardware)
zig build test-pbt # property-based tests (1000 iterations)
zig build test-hardware # hardware tests (needs YubiKey)
zig build docs # generate API documentation
```

## Style

- Format with `zig fmt`
- All `pub` and `export` functions require `///` doc comments
- C FFI exports live exclusively in `src/ffi.zig`
- Protocol implementations in `src/<module>.zig` (cbor, ctap2, ctaphid, pin)
- Platform HID transports in `src/hid_macos.zig` and `src/hid_linux.zig`
- Property-based tests in `tests/pbt_<module>.zig`
- Error convention: negative = library error, 0 = success, positive = CTAP2 device status byte

## Boundaries

- **Do not** add browser or WebKit dependencies -- this is a USB HID library
- **Do not** bypass USB HID framing (CTAPHID packet structure is required by spec)
- **Do not** introduce OpenSSL, BoringSSL, CommonCrypto, or any C crypto dependency
- **Do not** add allocator-dependent APIs to the FFI surface (all buffers are caller-provided)
- **Do not** link IOKit/CoreFoundation in the static library (resolved at final link by the consumer)
- **Do** keep the library stateless and thread-safe
- **Do** ensure all new commands have both unit tests and property-based tests where applicable
- **Do** test against real hardware (YubiKey) for any transport-layer changes

## C FFI Exports (ctap2.h)

Expand All @@ -21,55 +76,42 @@
| `ctap2_parse_make_credential_response` | `int` | Parse a raw MakeCredential response (status byte + CBOR attestation object). |
| `ctap2_parse_get_assertion_response` | `int` | Parse a raw GetAssertion response (status byte + CBOR). fallback_cred_id: credential ID to use when the response omits key 1 (CTAP2 spec: single-entry allowList). Pass NULL/0 if no fallback. |
| `ctap2_get_pin_retries` | `int` | Get PIN retry count from the authenticator. out_retries: receives the number of remaining PIN retries. Returns CTAP2_OK on success, or negative error code. |
| `ctap2_get_pin_token` | `int` | Get a PIN token for authentication. Performs the full PIN protocol v2 handshake (key agreement + ECDH + PIN encryption) and returns a decrypted 32-byte PIN token. pin: null-terminated UTF-8 PIN string. out_pin_token: receives the 32-byte decrypted PIN token. out_pin_token_len: must be >= 32. Returns CTAP2_OK on success, positive CTAP2 status byte on device error (e.g. 0x31 = wrong PIN), or negative error code. |
| `ctap2_make_credential_with_pin` | `int` | Same as the parsed functions above, but with optional PIN auth. Pass pin_token=NULL, pin_protocol=0 for no PIN authentication. Pass pin_token=<32-byte token from ctap2_get_pin_token>, pin_protocol=2 to include pinAuth in the CTAP2 command. |
| `ctap2_get_assertion_with_pin` | `int` | |
| `ctap2_make_credential_with_keepalive` | `int` | |
| `ctap2_get_assertion_with_keepalive` | `int` | |
| `ctap2_debug_last_ioreturn` | `int` | Debug: get the last IOReturn error code from HID operations. |
| `ctap2_get_pin_token` | `int` | Get a PIN token for authentication. Performs the full PIN protocol v2 handshake (key agreement + ECDH + PIN encryption) and returns a decrypted 32-byte PIN token. |
| `ctap2_make_credential_with_pin` | `int` | MakeCredential with optional PIN auth. Pass pin_token=NULL, pin_protocol=0 for no PIN. Pass pin_token=<32-byte token>, pin_protocol=2 for PIN-authenticated. |
| `ctap2_get_assertion_with_pin` | `int` | GetAssertion with optional PIN auth. Same token/protocol convention. |
| `ctap2_make_credential_with_keepalive` | `int` | MakeCredential with keepalive callback (status 1=processing, 2=user presence needed). Returns raw response bytes. |
| `ctap2_get_assertion_with_keepalive` | `int` | GetAssertion with keepalive callback. Returns raw response bytes. |
| `ctap2_status_message` | `const char *` | Map a CTAP2 status byte to a human-readable message string. |
| `ctap2_debug_last_ioreturn` | `int` | Debug: get the last IOReturn error code from HID operations (macOS only). |

## Error Conventions

Defined in `ctap2.h`:

| Code | Value | Meaning |
|------|-------|---------|
| `CTAP2_OK` | 0 | |
| `CTAP2_ERR_NO_DEVICE` | -1 | |
| `CTAP2_ERR_TIMEOUT` | -2 | |
| `CTAP2_ERR_PROTOCOL` | -3 | |
| `CTAP2_ERR_BUFFER_TOO_SMALL` | -4 | |
| `CTAP2_ERR_OPEN_FAILED` | -5 | |
| `CTAP2_ERR_WRITE_FAILED` | -6 | |
| `CTAP2_ERR_READ_FAILED` | -7 | |
| `CTAP2_ERR_CBOR` | -8 | |
| `CTAP2_ERR_DEVICE` | -9 | |
| `CTAP2_ERR_PIN` | -10 | |
| `CTAP2_OK` | 0 | Success |
| `CTAP2_ERR_NO_DEVICE` | -1 | No FIDO2 device connected |
| `CTAP2_ERR_TIMEOUT` | -2 | Device communication timeout |
| `CTAP2_ERR_PROTOCOL` | -3 | CTAPHID protocol error |
| `CTAP2_ERR_BUFFER_TOO_SMALL` | -4 | Output buffer too small |
| `CTAP2_ERR_OPEN_FAILED` | -5 | Failed to open HID device |
| `CTAP2_ERR_WRITE_FAILED` | -6 | USB write failed |
| `CTAP2_ERR_READ_FAILED` | -7 | USB read failed |
| `CTAP2_ERR_CBOR` | -8 | CBOR encoding/decoding error |
| `CTAP2_ERR_DEVICE` | -9 | CTAP2 device error |
| `CTAP2_ERR_PIN` | -10 | PIN protocol error |
| `CTAP2_ERR_NOT_ACCESSIBLE` | -11 | Devices found but not openable (permissions) |

## Platform Requirements

**macOS:**
- Frameworks: CoreFoundation, IOKit
- Frameworks: CoreFoundation, IOKit (linked at final build, not in the static lib)
- Entitlement: `com.apple.security.device.usb` (hardened runtime)
- Permission: Input Monitoring (System Settings > Privacy & Security)
- Targets: arm64, x86_64

**Linux:**
- Libraries: hidraw (kernel)
- hidraw kernel support (default on most distributions)
- Read/write access to `/dev/hidraw*` devices (udev rule or group membership)
- Targets: arm64, x86_64

## Build

```bash
zig build # static library -> zig-out/lib/
zig build -Doptimize=ReleaseFast # optimized build
zig build test # unit tests
zig build test-pbt # property-based tests
```

## Linking

The library builds as a static archive. Include the header
from `include/` and link `zig-out/lib/libctap2.a`.

At final link time, the consuming application must link platform frameworks/libraries.
The static library intentionally does not link them to support cross-compilation.

83 changes: 83 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Contributing to zig-ctap2

## Installation

### Zig Package Manager (recommended)

```bash
zig fetch --save git+https://github.com/Jesssullivan/zig-ctap2.git
```

Then in your `build.zig`:

```zig
const dep = b.dependency("zig-ctap2", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("zig-ctap2", dep.module("zig-ctap2"));
```

### Git Submodule (C FFI consumers)

```bash
git submodule add https://github.com/Jesssullivan/zig-ctap2.git vendor/ctap2
cd vendor/ctap2 && zig build -Doptimize=ReleaseFast
```

Link `-lctap2` and include `ctap2.h`. At final link time, add platform frameworks:
- **macOS:** `-framework IOKit -framework CoreFoundation`
- **Linux:** no extra libraries needed (uses hidraw via kernel)

## Development

### Prerequisites

- Zig 0.14.1+
- **macOS:** IOKit and CoreFoundation (available by default)
- **Linux:** hidraw kernel support (enabled by default on most distributions)
- **Hardware tests:** USB security key (YubiKey 5C NFC recommended)

### Build & Test

```bash
zig build # static library (libctap2.a)
zig build test # unit tests (no hardware needed)
zig build test-pbt # property-based tests (1000 iterations)
zig build test-hardware # hardware tests (requires YubiKey + YUBIKEY_TESTS=1)
zig build docs # generate API documentation
```

### Code Style

- `zig fmt` for formatting
- All `pub` and `export` functions need `///` doc comments
- C FFI exports go in `src/ffi.zig`
- Protocol implementations in `src/<module>.zig` (cbor, ctap2, ctaphid, pin)
- Platform HID transports in `src/hid_macos.zig` and `src/hid_linux.zig`
- Property-based tests in `tests/pbt_<module>.zig`

### Adding a new CTAP2 command

1. Add the encoder/parser in `src/ctap2.zig` (or `src/pin.zig` for PIN commands)
2. Add `export fn ctap2_<command>` wrapper in `src/ffi.zig`
3. Add the C declaration to `include/ctap2.h`
4. Add unit tests in the module
5. Wire the test file into `build.zig`
6. Update `AGENTS.md` FFI table

### Hardware Testing

Hardware tests require a physical FIDO2 security key:

```bash
# Connect a YubiKey, then:
YUBIKEY_TESTS=1 zig build test-hardware
```

Tests run getInfo, enumerate devices, and verify CTAPHID framing against real hardware. They do not create or consume credentials.

## Filing Issues

Open an issue at [github.com/Jesssullivan/zig-ctap2/issues](https://github.com/Jesssullivan/zig-ctap2/issues).

## License

Dual-licensed under [Zlib](https://opensource.org/licenses/Zlib) and [MIT](https://opensource.org/licenses/MIT).
Loading
Loading