caden 0.7.0

Server-side WebAuthn/Passkey library implementing the W3C WebAuthn Level 2 specification
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
# CLAUDE.md — Caden

> This project was renamed from "WebAuthn" to "Caden" (repo: caden-rs) on 2026-06-24.
> All references to "WebAuthn" in this codebase refer to the W3C standard, not the project name.

Developer guide for this codebase. Start here every session.

---

## Project overview

This is **Caden**, a WebAuthn relying-party library written in Rust. It implements
the server-side verification logic for the two core WebAuthn ceremonies:

- **Registration** (`navigator.credentials.create`) — the authenticator generates
  a keypair (P-256 or RSA), and the relying party verifies the attestation and
  stores the public key and credential ID.
- **Authentication** (`navigator.credentials.get`) — the authenticator signs a
  challenge with the stored private key, and the relying party verifies the
  signature and sign count.

Supported algorithms: **ES256** (ECDSA P-256, COSE `-7`), **ES384** (ECDSA P-384,
COSE `-35`), **EdDSA** (Ed25519, COSE `-8`), and **RS256** (RSA PKCS#1 v1.5
SHA-256, COSE `-257`).

The library follows the [W3C WebAuthn Level 3 specification](https://www.w3.org/TR/webauthn-3/).

### Project goals

- Correct implementation of a real cryptographic protocol (no "close enough")
- Idiomatic, readable Rust — every line explainable in an interview
- Layered module design: each file owns exactly one concept
- Precise, debuggable error messages that never leak sensitive material
- Every error path exercised by a dedicated test
- Zero custom crypto — all cryptographic operations delegated to `ring`

---

## Crate structure

| File | Purpose |
|------|---------|
| `src/lib.rs` | Public API surface: `RelyingParty`, wire types, re-exports |
| `src/error.rs` | `WebAuthnError` enum + `Result<T>` alias |
| `src/credential.rs` | `Credential`, `PublicKey { ES256, EdDSA, RS256 }`, `Challenge`, result types |
| `src/algorithm.rs` | COSE algorithm constants: `COSE_ES256=-7`, `COSE_EDDSA=-8`, `COSE_RS256=-257`, kty values |
| `src/der.rs` | DER builder: `rsa_components_to_der(n,e)` → RSAPublicKey for ring |
| `src/crypto.rs` | `sha256`, `verify_es256`, `verify_eddsa`, `verify_rs256`, `generate_challenge` |
| `src/challenge.rs` | Challenge expiry helpers: `is_expired`, `CHALLENGE_MAX_AGE_SECS` |
| `src/client_data.rs` | `clientDataJSON` base64url → JSON → `ClientData` |
| `src/authenticator_data.rs` | Binary authenticator data → `AuthenticatorData`; `CoseKey` enum |
| `src/attestation.rs` | Attestation verification: "none", "packed", "fido-u2f", "android-key" |
| `src/registration.rs` | §7.1 registration ceremony; dispatches `CoseKey``PublicKey` |
| `src/authentication.rs` | §7.2 authentication ceremony; dispatches `PublicKey` → verifier |
| `examples/demo.rs` | End-to-end demo: ES256 and RS256 registration/auth/replay |
| `examples/server.rs` | Axum HTTP server: all 5 WebAuthn endpoints with in-memory state |
| `tests/integration.rs` | Integration tests for ES256, EdDSA, and RS256 full ceremony flows |

---

## Key design decisions

### `ring` over RustCrypto

`ring` is used for all cryptographic operations (ECDSA P-256 verification,
RSA PKCS#1 v1.5 verification, SHA-256, CSPRNG). Reasons:

- **Audit lineage**: ring descends from BoringSSL, which has a longer and
  broader audit history than the RustCrypto family.
- **Constrained API**: ring's API is intentionally narrow — you cannot
  accidentally use an insecure mode that a more flexible library might expose.
- **Production pedigree**: rustls and many production TLS stacks use ring.
- **No custom crypto**: the library does not implement any cryptographic
  primitives itself. All security-critical operations are inside ring's
  audited boundary.

### `ciborium` for CBOR

WebAuthn uses CBOR for the attestation object and the COSE public key inside
authenticator data. `ciborium` is chosen over serde-based CBOR crates because:

- It decodes into a `Value` enum (similar to `serde_json::Value`), which we
  navigate explicitly. This keeps parsing code readable and maps one-to-one
  with the CBOR structures described in the spec.
- Serde-derive-based CBOR deserialization would hide the CBOR structure behind
  opaque attributes, making it harder to audit against the spec.

### `thiserror` for structured errors

`thiserror` generates `Display` and `Error` trait implementations from the
`#[error("...")]` attribute. This keeps `WebAuthnError` definitions DRY —
the display string lives next to the variant declaration — while still
producing the `std::error::Error` impl that callers expect from a library.
Errors intentionally never include raw key bytes or challenge values.

### Spec step comments in ceremony code

`registration.rs` and `authentication.rs` cite spec step numbers inline
(`// §7.1 step 8`). This is a security-library convention: it makes the
implementation auditable against the spec without reading both documents
side-by-side. **Always add a step comment when implementing or modifying a
ceremony step.** See `/spec-audit` to check compliance.

### Stateless `RelyingParty`

`RelyingParty` holds no state. The caller passes in the stored `Credential`
and receives result types back. This design is storage-agnostic — the library
does not prescribe a database, an ORM, or a session store.

### `PublicKey::ES256(Vec<u8>)` stores the uncompressed point

The inner `Vec<u8>` is `0x04 || x (32 bytes) || y (32 bytes)` — the
uncompressed EC point format that `ring` expects for signature verification.
The COSE key encodes `x` and `y` as separate byte strings; `authenticator_data.rs`
joins them into the `0x04 || x || y` form during parsing.

### Optional `serde` feature for public-type serialization

`serde` is already an unconditional dependency (used by `client_data.rs` for
`clientDataJSON` parsing). The `serde` feature gates `Serialize + Deserialize`
derives on all public-facing types — `Credential`, `PublicKey`, `Challenge`,
`RegistrationResult`, `AuthenticationResult`, `AttestationType`, and
`WebAuthnError`. `Vec<u8>` fields use `#[serde(with = "serde_bytes")]` so they
encode as compact byte sequences (base64 in JSON) rather than arrays of integers.
`serde_bytes` is an optional dependency pulled in only when the feature is enabled.
Enable with `features = ["serde"]` in `Cargo.toml`.

---

## Registration ceremony — W3C §7.1 step mapping

`src/registration.rs::verify` implements these steps:

| Step | What it checks | Code location |
|------|---------------|---------------|
| §7.1 step 5 | Parse `clientDataJSON` (base64url → JSON) | `client_data::parse` |
| §7.1 step 7 | `type` field must equal `"webauthn.create"` | inline check |
| §7.1 step 8 | Challenge in client data must match issued challenge | inline check |
| §7.1 step 9 | Origin must be in `allowed_origins` (exact match, any entry) | inline check |
| §7.1 step 11 | Hash `clientDataJSON` with SHA-256 | `crypto::sha256` |
| §7.1 step 12 | Decode `attestationObject` (base64url → CBOR bytes) | inline |
| §7.1 step 13 | Parse CBOR attestation object → `(fmt, authData)` | `parse_attestation_object` |
| §7.1 step 14 | Parse raw authenticator data bytes | `authenticator_data::parse` |
| §7.1 step 15 | `rpIdHash` must equal SHA-256(`rp_id`) | inline check |
| §7.1 step 16 | User Present (UP) flag must be set | inline check |
| §7.1 step 21 | Extract attested credential data (credential ID + public key) | inline check |
| §7.1 step 22 | Verify attestation statement | `attestation::verify` |
| §7.1 step 25 | Assemble `Credential` struct for caller to persist | return value |

Steps not listed (e.g. steps 1–4, tokenBinding, client extension processing)
are either delegated to the caller or intentionally out of scope for a library.

---

## Authentication ceremony — W3C §7.2 step mapping

`src/authentication.rs::verify` implements these steps:

| Step | What it checks | Code location |
|------|---------------|---------------|
| §7.2 step 11 | Parse `clientDataJSON` (base64url → JSON) | `client_data::parse` |
| §7.2 step 13 | `type` field must equal `"webauthn.get"` | inline check |
| §7.2 step 14 | Challenge must match issued challenge | inline check |
| §7.2 step 15 | Origin must be in `allowed_origins` (exact match, any entry) | inline check |
| §7.2 step 17 | Hash `clientDataJSON` with SHA-256 | `crypto::sha256` |
| §7.2 step 18 | Decode + parse `authenticatorData` bytes | `authenticator_data::parse` |
| §7.2 step 19 | `rpIdHash` must equal SHA-256(credential's `rp_id`) | inline check |
| §7.2 step 20 | User Present (UP) flag must be set | inline check |
| §7.2 step 21 | UV flag enforced when `rp.require_user_verification == true`; skipped by default | inline check |
| §7.2 step 23 | Decode DER signature bytes (base64url → bytes) | inline |
| §7.2 step 24 | Verify signature over `authData \|\| SHA-256(clientDataJSON)` | `crypto::verify_es256` or `crypto::verify_rs256` |
| §7.2 step 25 | Sign count must be strictly greater than stored value | inline check |

---

## Common commands

```bash
# Build
cargo build

# Run all tests (unit + integration + doc tests)
cargo test

# Run tests with the serde feature enabled (+5 round-trip tests)
cargo test --features serde

# Run only unit tests inside each module
cargo test --lib

# Run only integration tests
cargo test --test integration

# Run the end-to-end demo (ES256 + RS256 registration / auth / replay)
cargo run --example demo

# Run the Axum HTTP server on port 3000
cargo run --example server

# Clippy (zero-warning policy)
cargo clippy -- -D warnings

# Auto-format
cargo fmt

# Check formatting without modifying files
cargo fmt --check

# Generate API documentation (zero warnings expected)
cargo doc --no-deps

# Check crates.io packaging readiness
cargo package --no-verify --allow-dirty
```

---

## Running the demo

```
cargo run --example demo
```

`examples/demo.rs` simulates a full browser/authenticator interaction entirely
in software: it generates a real P-256 keypair with `ring`, constructs valid
`clientDataJSON`, `authenticatorData`, and an attestation object, then calls
the library. The demo demonstrates:

1. A successful registration
2. A valid authentication
3. Replay attack rejection (`WebAuthnError::ChallengeMismatch`)

Expected: the final line of output is `All checks passed.`

---

## Running tests

```bash
cargo test               # everything
cargo test --lib         # unit tests only
cargo test --test integration  # integration tests only
```

Unit tests live inside each module under `#[cfg(test)]`. Follow the pattern:

```rust
#[test]
fn rejects_my_new_error_case() {
    // Arrange: minimal valid state
    // Act: mutate one thing to trigger the error
    // Assert: match on the specific WebAuthnError variant
}
```

Integration tests in `tests/integration.rs` use the `Fixture` struct which
generates a real P-256 keypair. Follow the pattern:

1. Create a `Fixture`
2. Call `fixture.make_registration_response(...)` with desired parameters
3. Call `rp.verify_registration(...)` and assert `Ok` or `Err`
4. For authentication: call `fixture.make_assertion_response(...)`

---

## W3C spec references (with links)

| Section | Topic | URL |
|---------|-------|-----|
| §6.1 | Authenticator Data binary format | https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data |
| §6.5 | Attestation object structure | https://www.w3.org/TR/webauthn-3/#sctn-attestation |
| §7.1 | Registration ceremony algorithm | https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential |
| §7.2 | Authentication ceremony algorithm | https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion |
| §8.7 | None attestation format | https://www.w3.org/TR/webauthn-3/#sctn-none-attestation |
| RFC 8152 §13 | COSE Key Parameters | https://www.rfc-editor.org/rfc/rfc8152#section-13 |
| RFC 9052 | COSE (updated) | https://www.rfc-editor.org/rfc/rfc9052 |

Canonical spec: https://www.w3.org/TR/webauthn-3/

---

## Known limitations and future work

| Limitation | Notes |
|------------|-------|
| Cert chain trust anchors (all formats) | `x5c` chain order is verified; root checked against `RelyingParty::trust_anchors` when configured, otherwise accepted as `Basic`. No FIDO MDS integration — authenticator model/provenance cannot be verified. |
| No FIDO Metadata Service | Authenticator model/provenance cannot be verified |
| UV flag optional | Off by default; enable with `RelyingParty::new(...).require_user_verification(true)` |

Challenge single-use enforcement is now opt-in via `RelyingParty::enforce_single_use_challenges(true)`. When enabled, the library maintains an `Arc<Mutex<HashSet<Vec<u8>>>>` of consumed challenge bytes shared across all clones of the instance. Without this opt-in, the caller is responsible for deleting each challenge from their session store after it is used.

---

## Contribution guidelines

### Branch naming

```
feat/<short-description>       # new capability
fix/<short-description>        # bug fix
test/<short-description>       # tests only
docs/<short-description>       # docs only
refactor/<short-description>   # no behaviour change
chore/<short-description>      # tooling, config, deps
```

### Commit format (Conventional Commits)

```
<type>(<scope>): <short description>

<optional body>
```

Types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`

Examples:
```
feat(crypto): add EdDSA signature verification via ring
fix(auth): reject sign count equal to stored value (was: only less-than)
test(registration): add test for missing attested credential data
docs(claude): add hooks & automation section
```

### Before opening a PR

Run `/check` or manually:

```bash
cargo build
cargo clippy -- -D warnings
cargo test
cargo fmt --check
```

All four must pass. See `.github/pull_request_template.md` for the full checklist.

### Style rules

- No comments that restate what the code does — only WHY (hidden constraints,
  spec citations, non-obvious invariants).
- All public items must have `///` doc comments.
- Ceremony verification steps must cite the spec section and step number.
- Errors must be specific enough to debug without leaking key bytes or
  challenge values.
- No `unwrap()` in library code (`src/`). Tests and `examples/` may use it.

---

## Hooks & Automation

This project uses Claude Code hooks (`.claude/settings.json`) for intelligent
automation during AI-assisted development. All hooks are in `.claude/scripts/`.

### Hook summary

| Hook | Event | Trigger | What it does |
|------|-------|---------|-------------|
| build-clippy | PostToolUse | Any `.rs` file edited | Runs `cargo build` + `cargo clippy -- -D warnings`; prints ✅ or ❌ |
| docs-sync | PostToolUse | Specific `src/` files edited | Prints a targeted warning reminding Claude to check related docs |
| test-runner | PostToolUse | Any file in `src/` or `tests/` edited | Runs `cargo test`; prints ✅ or ❌ |
| log-command | PreToolUse | Every Bash command | Appends `[timestamp] command` to `.claude/logs/commands.log` |
| auto-commit | Stop | *(disabled)* | Script kept at `.claude/scripts/auto-commit.sh` but hook removed — commits are made manually per logical change |

### Why commits are manual

The auto-commit Stop hook has been disabled. Commits are made explicitly during
a turn, one per logical change, with specific conventional commit messages. This
gives a meaningful git history instead of one catch-all "update Caden (N
files changed)" commit per Claude turn.

To push when you're ready: `git push`.

### Reading the command log

```bash
cat .claude/logs/commands.log
```

Each line is `[ISO-8601 UTC timestamp] <first line of bash command>`. The log
is never committed (it is in `.gitignore`) and resets each working directory
session.

### Custom slash commands

| Command | When to use |
|---------|-------------|
| `/add-algorithm` | Adding a new COSE algorithm (RS256, EdDSA, etc.) — scaffolds enum variant, crypto function, and tests |
| `/run-demo` | Quickly verify the end-to-end demo works and print its output |
| `/check` | Run the full quality suite (build + clippy + test + fmt) before a PR |
| `/spec-audit` | Audit ceremony code for missing W3C spec step comments — important for security review |
| `/status` | Project health snapshot: git state, build, test count, clippy warnings, public API list |

### Docs sync warnings

The `docs-sync` hook prints warnings when files with broad impact are edited:

| File edited | Warning |
|-------------|---------|
| `src/registration.rs` | Check `docs/architecture.md` and §7.1 step comments |
| `src/authentication.rs` | Check `docs/architecture.md` and §7.2 step comments |
| `src/crypto.rs` | Update `docs/security-considerations.md` if behaviour changed |
| `src/error.rs` | Ensure new variants are documented and tested |
| `src/lib.rs` | Update `README.md` quick-start if public API changed |

### Why spec compliance comments matter

This is a security library. The WebAuthn spec defines exact algorithms
for registration and authentication — implementing a step incorrectly or
skipping it can introduce authentication bypasses. Step comments (`// §7.1 step 8`)
serve as an audit trail: a reviewer can open the spec and the source file
side-by-side and confirm each step is handled. Use `/spec-audit` to check
that all implemented steps are annotated.

---

## Phase 3 — Hardening

Phase 3 (implemented June 2026) turned the library from a working demo into a
correct and robust implementation. Summary of what changed:

### Panic audit

All `src/` files were audited for `unwrap()` calls. None were found; the
library already used `expect()` with safety comments for provably-infallible
conversions (e.g., `try_into()` on slices whose length was just verified).

`#![deny(clippy::unwrap_used)]` was added to `src/lib.rs` to enforce this
as a compile-time guarantee going forward. `.expect()` in library code is
still permitted where the preceding bounds check makes the panic impossible.

### Test vectors

Fixed test vectors are stored in `tests/vectors/registration.json` and
`tests/vectors/authentication.json`. They were generated once by
`examples/generate_vectors.rs` using a simulated P-256 authenticator and
committed. The vectors contain pre-encoded `clientDataJSON`,
`attestationObject`, `authenticatorData`, and `signature` blobs; integration
tests verify the library parses and accepts them unchanged between runs.

### Edge cases now hardened

| Module | New checks |
|--------|-----------|
| `authenticator_data.rs` | credentialIdLength=0, >1023, CBOR key duplicate detection, crv≠1, missing kty/alg/crv/x/y fields, x/y coordinate length != 32, empty COSE input |
| `client_data.rs` | empty bytes, non-JSON UTF-8, missing type/challenge/origin, empty type field, origin trailing slash, crossOrigin flag |
| `challenge.rs` | TTL=0 (immediately expired), TTL=u64::MAX (never expired), future created_at, two-call non-equality |
| `crypto.rs` | 64-byte key (missing 0x04 prefix), empty key, empty signature, wrong-message signature, garbage DER |
| `registration.rs` | missing attStmt, authData not bytes |
| `authentication.rs` | sign-count wrap-around (stored=u32::MAX, received=0) now rejected |

### No-panic property

Two fuzz-style tests (`no_panic_on_random_registration_input`,
`no_panic_on_random_authentication_input`) pass 100 randomly-constructed
inputs through the full ceremony verification paths and assert no panic occurs.
Both tests use a deterministic LCG so they are fully reproducible.

### Sign-count boundary fix

The expiry check in `Challenge` was changed from `age > ttl` to `age >= ttl`
so that a TTL of 0 seconds means the challenge expires at the moment of
creation, and a challenge exactly at the boundary counts as expired (safer
default). The change propagates to `challenge::is_expired`,
`challenge::is_expired_with_max_age`, and `Challenge::is_expired`.

The sign-count check in `authentication.rs` was updated: the old check
(`received != 0 && received <= stored`) accepted any received=0 value
regardless of the stored count. The new check (`(stored > 0 || received > 0)
&& received <= stored`) rejects received=0 when stored>0, catching the
wrap-around (u32::MAX → 0) case as a `SignCountInvalid` error.

---

## Phase 5 — Stretch Goals (Project complete)

Phase 5 (implemented June 2026) elevated the library from a solid implementation
to a portfolio-ready, publishable crate.

### `#![forbid(unsafe_code)]`

Added to `src/lib.rs` as the first crate-level attribute. This is a strong signal
for a security library: it eliminates entire classes of memory safety vulnerabilities
at compile time and signals to reviewers that no unsafe code exists anywhere in this
crate. All security-critical operations remain inside `ring`'s audited boundary.

### Attestation verification

`src/attestation.rs` implements multiple attestation formats:

- **Self-attestation** (`x5c` absent): `alg` verified to match the credential key
  algorithm, `authData || clientDataHash` verified using `verify_es256` or
  `verify_rs256`. Returns `AttestationType::SelfAttestation`.
- **Basic attestation** (`x5c` present): the leaf cert's EC P-256 public key is
  used to verify `sig` over `authData || clientDataHash`. The `x5c` chain order is
  verified (each cert signed by the next). Returns `AttestationType::Basic` when no
  trust anchors are configured, or `AttestationType::BasicVerified` when the chain
  root matches a configured trust anchor.
- **FIDO U2F** (`"fido-u2f"`): signature verified against the attestation cert's
  EC P-256 public key. `x5c` chain order verified. Returns `AttestationType::Basic`
  or `BasicVerified` depending on trust anchors.
- **Android Key** (`"android-key"`): `alg`, `sig`, and `x5c` are required. The
  attestation cert's EC P-256 public key must equal the credential public key
  (the key security property proving the key lives in a hardware-backed Keystore).
  Signature verified over `authData || clientDataHash`. `x5c` chain order verified.
  Returns `AttestationType::Basic` or `BasicVerified`.
- **Apple** (`"apple"`): `x5c` is required. The credential certificate must contain
  the Apple nonce extension (OID 1.2.840.113635.100.8.2) whose value equals
  SHA-256(`authData || clientDataHash`). The cert's EC P-256 public key must equal
  the credential public key. `x5c` chain order verified. Returns
  `AttestationType::Basic` or `BasicVerified`.
- **TPM** (`"tpm"`): `ver` must be `"2.0"`, `alg` must match the credential key,
  `x5c` is required. `sig` is verified over the raw `certInfo` bytes using the
  AIK cert's public key. `certInfo` (TPM2B_ATTEST) is parsed to verify `magic`,
  `type`, `extraData` (= SHA-256(authData || clientDataHash)), and
  `attested.name` (= nameAlg_bytes || H_nameAlg(pubArea)). `pubArea` key
  coordinates are compared against the credential key. `x5c` chain order verified.
  Returns `AttestationType::Basic` or `BasicVerified`.
- Other formats: accepted with `AttestationType::None`
  (provenance unverifiable but credential usable).

`parse_attestation_object` in `registration.rs` was updated to return the `attStmt`
CBOR value alongside `fmt` and `authData`. The `attestation::verify` signature was
extended to accept `att_stmt`, `auth_data_bytes`, `client_data_hash`, and
`credential_public_key`.

`AttestationType::Basic` and `AttestationType::BasicVerified` variants were added to `credential.rs`.
`WebAuthnError::AttestationChainInvalid` and `WebAuthnError::AttestationRootUntrusted` were added to `error.rs`.
`RelyingParty::trust_anchors(roots)` builder method added to `registration.rs` for configuring DER-encoded root CA certificates.

### Axum HTTP server example (`examples/server.rs`)

A real Axum 0.7 HTTP server that exercises the full Caden library API:

| Endpoint | Description |
|----------|-------------|
| `GET  /health` | Version check |
| `POST /register/begin` | Issue registration challenge |
| `POST /register/complete` | Verify attestation, store credential |
| `POST /authenticate/begin` | Issue authentication challenge |
| `POST /authenticate/complete` | Verify assertion, update sign count |

State is in-memory (`tokio::sync::Mutex<HashMap<…>>`). `axum`, `tokio`, and `tower`
added as dev-dependencies.

### crates.io preparation

- Package name changed to `webauthn-rs-demo` (the `webauthn` name is taken);
  `[lib] name = "webauthn"` keeps the Rust import path unchanged for all existing code.
- `Cargo.toml` updated with `description`, `keywords`, `categories`, `repository`,
  `documentation`, `license = "MIT OR Apache-2.0"`, `rust-version = "1.88"`.
- `LICENSE-MIT` and `LICENSE-APACHE` added.
- `CHANGELOG.md` added.
- `cargo package --no-verify --allow-dirty` produces zero warnings (46 files, ~89 KiB).

### cargo doc polish

- `src/lib.rs` crate-level doc comment updated: complete quick-start example
  (registration + authentication), algorithm table, security properties summary,
  learning-project disclaimer, spec references.
- `src/attestation.rs` doc comment updated to reflect `"packed"` support.
- Two bare URLs in module doc comments wrapped as `<URL>` to eliminate the two
  `rustdoc::bare_urls` warnings. `cargo doc --no-deps` now produces zero warnings.

### How to prepare a release

1. Update `version` in `Cargo.toml` and `CHANGELOG.md`.
2. Run the quality suite: `cargo build && cargo clippy -- -D warnings && cargo test && cargo fmt --check && cargo doc --no-deps`.
3. `cargo package --no-verify` to verify the .crate file.
4. `git tag v<version> && git push --tags`.
5. `cargo publish` (requires `cargo login` with crates.io token first).

---

## Multiple allowed origins

`RelyingParty` now holds `allowed_origins: Vec<String>` instead of a single
`origin: String`. The origin check in `validate_client_data` accepts the
client-supplied origin if it equals **any** entry in the list (exact match).

### Constructors

| Constructor | When to use |
|-------------|-------------|
| `RelyingParty::new(id, origin, name)` | Single origin — existing callers compile unchanged |
| `RelyingParty::with_origins(id, origins, name)` | Multiple origins — accepts any `IntoIterator<Item = impl Into<String>>` |

```rust
// Single origin (backward-compatible with all existing code)
let rp = RelyingParty::new("example.com", "https://example.com", "My Service");

// Multiple origins — prod + localhost in one instance
let rp = RelyingParty::with_origins(
    "example.com",
    ["https://example.com", "http://localhost:8080"],
    "My Service",
);
```

### Error behaviour

`WebAuthnError::OriginMismatch.expected` now contains the allowed list formatted
as a comma-separated string (e.g. `"https://example.com, http://localhost:8080"`).
For a single-origin RP the string is identical to the old single-origin display.

### What changed

| File | Change |
|------|--------|
| `src/registration.rs` | `RelyingParty.origin``allowed_origins: Vec<String>`; added `with_origins()` |
| `src/authentication.rs` | Passes `&rp.allowed_origins` to `validate_client_data` |
| `src/client_data.rs` | `validate_client_data` last param `&str``&[String]`; origin check uses `.any()` |
| `tests/integration.rs` | Added `multi_origin_relying_party_accepts_registered_origin` |

---

## CI & Quality

### Pipeline overview

GitHub Actions runs `.github/workflows/ci.yml` on every push and pull request to `main`.
Three jobs run in parallel:

| Job | What it runs |
|-----|-------------|
| **Build & Test** | `cargo build`, `cargo fmt --check`, `cargo clippy -- -D warnings`, `cargo test`, `cargo doc --no-deps`, `cargo run --example demo` |
| **MSRV (1.88)** | `cargo build` + `cargo test` on Rust 1.88 |
| **Security Audit** | `cargo audit` via `cargo-audit` |

### Always run `cargo fmt` before committing

The CI `fmt --check` step fails on any unformatted file. Run `cargo fmt` locally before
every commit to avoid a red CI build. The `/check` command does this for you.

### Running the full CI suite locally

Use `/check` or run manually in this order (same as CI):

```bash
cargo build
cargo fmt --check
cargo clippy -- -D warnings
cargo test
cargo doc --no-deps 2>&1 | grep "^error" && exit 1 || true
cargo run --example demo
```

All six must pass before pushing.

### MSRV — Minimum Supported Rust Version (1.88)

`rust-version = "1.88"` in `Cargo.toml` declares that the crate compiles on Rust 1.88.
The MSRV job on CI enforces this by building and testing on that exact toolchain.

1.88 is set by the dependency tree, not by any language feature this crate uses directly.
`x509-parser` pulls in `time@0.3.52` (and its sub-crates `time-core`, `time-macros`),
which declare `rust-version = "1.88"`. This is the pattern to watch for: a transitive
dependency silently raises the effective MSRV.

**When the MSRV CI job fails** with a message like:

```
time@0.3.52 requires rustc 1.88.0
```

The fix is to bump both `rust-version` in `Cargo.toml` and the toolchain in
`.github/workflows/ci.yml` to the version the error names — no guessing needed.
Also update the cache key suffix (`cargo-msrv-1.88-…`) to bust the stale cache,
and update every `1.XX` reference in this file.

**When adding a new dependency**, check its `rust-version` (and its deps') on
crates.io before committing. The MSRV CI job will catch it if you miss it, but
reading the error message gives you the exact version to use immediately.

### cargo-audit — dependency vulnerability scanning

`cargo audit` checks every dependency in `Cargo.lock` against the
[RustSec Advisory Database](https://rustsec.org/). It fails the build if any dependency
has a known CVE or security advisory. The security-audit CI job installs `cargo-audit`
with `--locked` to get a reproducible version.

To run locally (requires `cargo install cargo-audit`):
```bash
cargo audit
```

If audit fails on CI, check the advisory at `rustsec.org/advisories/<ID>` and either:
- Upgrade the affected dependency to a patched version, or
- Add a `[patch]` or `[dependencies]` version bump in `Cargo.toml`.

### Fixing a failing CI build

1. Check the failing job in the GitHub Actions tab.
2. Reproduce locally with the command from the table above.
3. Fix the issue, run `/check` to confirm all six steps pass, then push.
4. For MSRV failures: read the error — it names the exact version required (e.g. "requires rustc 1.88.0"). Update `rust-version` in `Cargo.toml`, the toolchain in `ci.yml`, the cache key suffix, and all `1.XX` references in `CLAUDE.md` to match.
5. For audit failures: upgrade the flagged dependency or open an issue to track it.