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
//! Kernel-side passkey-capability verification.
//!
//! The kernel sits behind the issuer surface ([`chio_custody_hw::IssuerService`]).
//! When a caller presents a [`PasskeyCapability`] to the kernel, the kernel
//! delegates to [`PasskeyCapabilityVerifier`] which:
//!
//! 1. Refuses any capability with an empty signature (custody contract:
//! empty signatures NEVER verify).
//! 2. Checks the audience pin matches the kernel's configured identity.
//! 3. Checks the capability is live (`now < exp`).
//! 4. Reconstructs the signing message via
//! [`chio_custody_hw::mint::signing_message`] and verifies the
//! detached signature against the configured issuer
//! [`chio_core_types::crypto::PublicKey`].
//!
//! All four gates are fail-closed: any path that does not strictly
//! satisfy its precondition returns a typed [`CustodyError`] with a
//! stable `urn:chio:error:custody:*` code.
//!
//! # Async discipline
//!
//! The verifier surface takes no `&mut self`.
//! `PasskeyCapabilityVerifier::verify` takes `&self` so it can be shared
//! behind an `Arc<>` and consumed concurrently from the async kernel.
use chio_core_types::crypto::{PublicKey, Signature};
use chio_custody_hw::capability::PasskeyCapability;
use chio_custody_hw::error::CustodyError;
use chio_custody_hw::mint::signing_message;
use chrono::{DateTime, Utc};
/// Kernel-side verifier for [`PasskeyCapability`] envelopes.
///
/// The verifier is configured with the audience the kernel pins itself
/// to, and the issuer's public key (the public half of the
/// [`chio_core_types::crypto::SigningBackend`] the issuer used to sign
/// the capability). The verifier is `Send + Sync` and stateless;
/// production deployments hold one in an `Arc<>` and re-use it across
/// every kernel call.
pub struct PasskeyCapabilityVerifier {
audience: String,
issuer_public_key: PublicKey,
}
impl PasskeyCapabilityVerifier {
/// Build a verifier pinned to a kernel audience and the issuer
/// public key.
#[must_use]
pub fn new(audience: impl Into<String>, issuer_public_key: PublicKey) -> Self {
Self {
audience: audience.into(),
issuer_public_key,
}
}
/// Configured kernel audience pin.
#[must_use]
pub fn audience(&self) -> &str {
&self.audience
}
/// Configured issuer public key.
#[must_use]
pub fn issuer_public_key(&self) -> &PublicKey {
&self.issuer_public_key
}
/// Verify a [`PasskeyCapability`] presented to this kernel.
///
/// Returns `Ok(())` only when ALL four gates pass:
///
/// 1. `capability.signature` is non-empty.
/// 2. `capability.audience == self.audience`.
/// 3. `now < capability.exp`.
/// 4. The detached hex-encoded `capability.signature` verifies
/// under [`Self::issuer_public_key`] over the canonical-JSON
/// bytes of the envelope WITH `signature = ""` (the same
/// message the issuer signed).
///
/// Fail-closed: any failing gate returns a typed [`CustodyError`].
/// The first failing gate short-circuits; deployments that need
/// distinct telemetry per gate read the [`CustodyError::urn`].
pub fn verify(
&self,
capability: &PasskeyCapability,
now: DateTime<Utc>,
) -> Result<(), CustodyError> {
// Gate 1: empty signatures NEVER verify. The kernel
// refuses to admit an unsigned envelope even if the audience and clock
// would otherwise let it through.
if capability.signature.is_empty() {
return Err(CustodyError::AssertionRejected(
"PasskeyCapability presented with empty signature".into(),
));
}
// Gate 2: audience pin.
capability.require_audience(&self.audience)?;
// Gate 3: liveness.
capability.require_live(now)?;
// Gate 4: cryptographic verification.
let message = signing_message(capability)?;
let signature = Signature::from_hex(&capability.signature).map_err(|err| {
CustodyError::AssertionRejected(format!("signature hex decode failed: {err}"))
})?;
if !self.issuer_public_key.verify(&message, &signature) {
return Err(CustodyError::AssertionRejected(
"PasskeyCapability signature did not verify against issuer public key".into(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use chio_core_types::crypto::{Ed25519Backend, Keypair, SigningBackend};
use chio_custody_hw::capability::ScopeSet;
use chio_custody_hw::issuer::{IssuerService, MintRequest};
use chio_custody_hw::verifier::VerifiedAssertion;
use chrono::TimeZone;
const AUDIENCE: &str = "urn:chio:audience:kernel";
fn fixed_now() -> chrono::DateTime<Utc> {
match Utc.with_ymd_and_hms(2026, 4, 29, 12, 0, 0) {
chrono::LocalResult::Single(t) => t,
_ => panic!("fixed_now fixture must construct"),
}
}
fn signed_cap_for_audience(audience: &str) -> (PasskeyCapability, PublicKey) {
let backend = Ed25519Backend::new(Keypair::from_seed(&[41u8; 32]));
let public = backend.public_key();
let backend_arc: Arc<dyn SigningBackend> = Arc::new(backend);
let svc = IssuerService::with_signer(audience, backend_arc);
let req = MintRequest {
audience: audience.into(),
scope_set: ScopeSet::new(["tool:read"]),
challenge_nonce: "n".into(),
};
let assertion = VerifiedAssertion {
credential_id_b64: "AAAA".into(),
user_verified: true,
};
let resp = match svc.mint_capability(&assertion, &req, fixed_now()) {
Ok(r) => r,
Err(e) => panic!("mint must succeed: {e}"),
};
(resp.capability, public)
}
#[test]
fn verifies_signed_capability_for_pinned_audience() {
let (cap, public) = signed_cap_for_audience(AUDIENCE);
let verifier = PasskeyCapabilityVerifier::new(AUDIENCE, public);
if let Err(e) = verifier.verify(&cap, fixed_now()) {
panic!("matching capability must verify: {e}");
}
}
#[test]
fn rejects_audience_mismatch() {
let (cap, public) = signed_cap_for_audience("urn:chio:audience:other");
let verifier = PasskeyCapabilityVerifier::new(AUDIENCE, public);
let res = verifier.verify(&cap, fixed_now());
assert!(matches!(res, Err(CustodyError::AudienceMismatch { .. })));
}
#[test]
fn rejects_empty_signature() {
let (mut cap, public) = signed_cap_for_audience(AUDIENCE);
cap.signature.clear();
let verifier = PasskeyCapabilityVerifier::new(AUDIENCE, public);
let res = verifier.verify(&cap, fixed_now());
assert!(matches!(res, Err(CustodyError::AssertionRejected(_))));
}
#[test]
fn rejects_expired_capability() {
let (cap, public) = signed_cap_for_audience(AUDIENCE);
let verifier = PasskeyCapabilityVerifier::new(AUDIENCE, public);
let later = cap.exp + chrono::Duration::seconds(1);
let res = verifier.verify(&cap, later);
assert!(matches!(res, Err(CustodyError::CapabilityExpired)));
}
#[test]
fn rejects_signature_from_wrong_issuer_key() {
let (cap, _real_public) = signed_cap_for_audience(AUDIENCE);
// Same audience, different issuer key. The kernel pins the
// public key, so a foreign issuer never produces a verifying
// capability.
let other_backend = Ed25519Backend::new(Keypair::from_seed(&[99u8; 32]));
let other_public = other_backend.public_key();
let verifier = PasskeyCapabilityVerifier::new(AUDIENCE, other_public);
let res = verifier.verify(&cap, fixed_now());
assert!(matches!(res, Err(CustodyError::AssertionRejected(_))));
}
}