1use crate::error::LaserError;
2use ed25519_dalek::{Signer, SigningKey as DalekSigningKey, Verifier, VerifyingKey};
3use laser_wire::agent::{
4 AgentEnvelope, SIGNATURE_DOMAIN, SIGNATURE_SCHEME_ED25519, Signature, SignatureContext,
5};
6use laser_wire::framing::encode_named;
7pub use laser_wire::keys::KeyKind;
8use sha2::{Digest, Sha256};
9use std::collections::HashMap;
10
11const KEY_ID_BYTES: usize = 8;
13const SIGNATURE_BYTES: usize = 64;
15#[cfg(feature = "kv")]
17pub const DEFAULT_KEY_NAMESPACE: &str = "agent.keys";
18
19fn signing_input(
24 envelope: &AgentEnvelope,
25 context: Option<&SignatureContext>,
26) -> Result<Vec<u8>, LaserError> {
27 let mut bare = envelope.clone();
28 bare.signature = None;
29 let body =
30 encode_named(&bare).map_err(|error| LaserError::Codec(format!("sign encode: {error}")))?;
31 let context_bytes = match context {
36 Some(context) => encode_named(context)
37 .map_err(|error| LaserError::Codec(format!("sign context encode: {error}")))?,
38 None => Vec::new(),
39 };
40 let mut input = Vec::with_capacity(SIGNATURE_DOMAIN.len() + context_bytes.len() + body.len());
41 input.extend_from_slice(SIGNATURE_DOMAIN);
42 input.extend_from_slice(&context_bytes);
43 input.extend_from_slice(&body);
44 Ok(input)
45}
46
47fn key_id_of(verifying: &VerifyingKey) -> Vec<u8> {
49 Sha256::digest(verifying.as_bytes())[..KEY_ID_BYTES].to_vec()
50}
51
52#[derive(Clone)]
55pub struct SigningKey {
56 inner: DalekSigningKey,
57 key_id: Vec<u8>,
58}
59
60impl SigningKey {
61 pub fn from_bytes(secret: &[u8; 32]) -> Self {
63 let inner = DalekSigningKey::from_bytes(secret);
64 let key_id = key_id_of(&inner.verifying_key());
65 Self { inner, key_id }
66 }
67
68 pub fn key_id(&self) -> &[u8] {
70 &self.key_id
71 }
72
73 pub fn verifying_key(&self) -> VerifyingKey {
75 self.inner.verifying_key()
76 }
77
78 pub fn sign(&self, envelope: &AgentEnvelope) -> Result<Signature, LaserError> {
81 self.sign_inner(envelope, None)
82 }
83
84 pub fn sign_with_context(
89 &self,
90 envelope: &AgentEnvelope,
91 context: SignatureContext,
92 ) -> Result<Signature, LaserError> {
93 self.sign_inner(envelope, Some(context))
94 }
95
96 fn sign_inner(
97 &self,
98 envelope: &AgentEnvelope,
99 context: Option<SignatureContext>,
100 ) -> Result<Signature, LaserError> {
101 let input = signing_input(envelope, context.as_ref())?;
102 let signature = self.inner.sign(&input);
103 Ok(Signature {
104 scheme: SIGNATURE_SCHEME_ED25519,
105 key_id: self.key_id.clone(),
106 bytes: signature.to_bytes().to_vec(),
107 context,
108 })
109 }
110}
111
112#[cfg(feature = "a2a-bridge")]
116pub fn sign_card_value(
117 key: &SigningKey,
118 card: &serde_json::Value,
119) -> Result<crate::a2a::AgentCardSignature, LaserError> {
120 let payload = jcs_bytes(&without_signatures(card))?;
121 let protected = base64url(br#"{"alg":"EdDSA"}"#);
122 let input = jws_signing_input(&protected, &payload);
123 let signature = key.inner.sign(input.as_bytes());
124 Ok(crate::a2a::AgentCardSignature {
125 protected,
126 signature: base64url(&signature.to_bytes()),
127 })
128}
129
130#[cfg(feature = "a2a-bridge")]
135pub fn verify_card(
136 card: &serde_json::Value,
137 signature: &crate::a2a::AgentCardSignature,
138 verifying: &VerifyingKey,
139) -> Result<(), LaserError> {
140 let payload = jcs_bytes(&without_signatures(card))?;
141 let input = jws_signing_input(&signature.protected, &payload);
142 let payload = base64url_decode(&signature.signature)?;
143 let payload: [u8; SIGNATURE_BYTES] = payload
144 .try_into()
145 .map_err(|_| LaserError::Signature("card signature is not 64 bytes".to_owned()))?;
146 verifying
147 .verify(
148 input.as_bytes(),
149 &ed25519_dalek::Signature::from_bytes(&payload),
150 )
151 .map_err(|_| LaserError::Signature("card signature does not verify".to_owned()))
152}
153
154#[cfg(feature = "kv")]
161#[derive(Clone)]
162pub struct KvKeyRegistry {
163 laser: crate::laser::Laser,
164 namespace: String,
165}
166
167#[cfg(feature = "kv")]
168impl KvKeyRegistry {
169 pub fn new(laser: crate::laser::Laser) -> Self {
171 Self::in_namespace(laser, DEFAULT_KEY_NAMESPACE)
172 }
173
174 pub fn in_namespace(laser: crate::laser::Laser, namespace: impl Into<String>) -> Self {
176 Self {
177 laser,
178 namespace: namespace.into(),
179 }
180 }
181
182 pub async fn enroll(
184 &self,
185 principal: impl Into<String>,
186 verifying: &VerifyingKey,
187 ) -> Result<(), LaserError> {
188 self.enroll_record(&KeyRecord::agent(principal, *verifying))
189 .await
190 .map(|_| ())
191 }
192
193 pub async fn enroll_record(&self, record: &KeyRecord) -> Result<u64, LaserError> {
195 let stored = stored_key_record(record);
196 stored
197 .validate()
198 .map_err(|error| LaserError::Invalid(error.to_owned()))?;
199 let payload = encode_named(&stored)?;
200 let kv = self.laser.kv(&self.namespace);
201 let key = key_id_hex(&stored.key_id);
202 let current = kv.get_entry(&key).await?;
203 let write = kv.set(&key).bytes(payload);
204 match current {
205 Some(entry) => write.expect_version(entry.version).commit().await,
206 None => write.expect_absent().commit().await,
207 }
208 }
209
210 pub async fn revoke(&self, key_id: impl AsRef<[u8]>) -> Result<u64, LaserError> {
212 let key_id = key_id.as_ref();
213 if key_id.len() != KEY_ID_BYTES {
214 return Err(LaserError::Invalid(
215 "Ed25519 key id must be 8 bytes".to_owned(),
216 ));
217 }
218 let kv = self.laser.kv(&self.namespace);
219 let key = key_id_hex(key_id);
220 let entry = kv
221 .get_entry(&key)
222 .await?
223 .ok_or_else(|| LaserError::Invalid("signing key is not enrolled".to_owned()))?;
224 let mut stored: laser_wire::keys::KeyRecord =
225 laser_wire::framing::decode_named(&entry.value)?;
226 stored
227 .validate()
228 .map_err(|error| LaserError::Invalid(error.to_owned()))?;
229 if stored.key_id != key_id {
230 return Err(LaserError::Invalid(
231 "key record identifier differs from its storage key".to_owned(),
232 ));
233 }
234 if stored.revoked {
235 return Ok(entry.version);
236 }
237 stored.revoked = true;
238 kv.set(&key)
239 .bytes(encode_named(&stored)?)
240 .expect_version(entry.version)
241 .commit()
242 .await
243 }
244
245 pub async fn registry(&self) -> Result<KeyRegistry, LaserError> {
249 let entries = self.laser.kv(&self.namespace).scan().entries().await?;
250 let mut registry = KeyRegistry::default();
251 for entry in entries {
252 if let Ok(stored) =
253 laser_wire::framing::decode_named::<laser_wire::keys::KeyRecord>(&entry.value)
254 && stored.validate().is_ok()
255 && entry.key == key_id_hex(&stored.key_id).into_bytes()
256 && let Ok(payload) = <[u8; 32]>::try_from(stored.verifying_key.as_slice())
257 && let Ok(verifying) = VerifyingKey::from_bytes(&payload)
258 && stored.key_id == key_id_of(&verifying)
259 {
260 registry.enroll_record(KeyRecord {
261 principal: stored.principal,
262 verifying,
263 kind: stored.kind,
264 valid_from_micros: stored.valid_from_micros,
265 valid_to_micros: stored.valid_to_micros,
266 revoked: stored.revoked,
267 });
268 }
269 }
270 Ok(registry)
271 }
272}
273
274#[cfg(feature = "kv")]
275fn stored_key_record(record: &KeyRecord) -> laser_wire::keys::KeyRecord {
276 laser_wire::keys::KeyRecord {
277 v: laser_wire::keys::KEY_RECORD_VERSION,
278 principal: record.principal.clone(),
279 key_id: record.key_id(),
280 verifying_key: record.verifying.as_bytes().to_vec(),
281 kind: record.kind,
282 valid_from_micros: record.valid_from_micros,
283 valid_to_micros: record.valid_to_micros,
284 revoked: record.revoked,
285 }
286}
287
288#[cfg(feature = "kv")]
289fn key_id_hex(key_id: &[u8]) -> String {
290 key_id
291 .iter()
292 .copied()
293 .map(|byte| format!("{byte:02x}"))
294 .collect::<Vec<_>>()
295 .join("")
296}
297
298#[cfg(feature = "a2a-bridge")]
305fn jcs_bytes(value: &serde_json::Value) -> Result<Vec<u8>, LaserError> {
306 fn assert_no_floats(value: &serde_json::Value) -> Result<(), LaserError> {
307 match value {
308 serde_json::Value::Number(number) if !number.is_i64() && !number.is_u64() => {
309 Err(LaserError::Signature(
310 "card canonicalization does not cover non-integer numbers".to_owned(),
311 ))
312 }
313 serde_json::Value::Array(items) => items.iter().try_for_each(assert_no_floats),
314 serde_json::Value::Object(fields) => fields.values().try_for_each(assert_no_floats),
315 _ => Ok(()),
316 }
317 }
318 assert_no_floats(value)?;
319 serde_json::to_vec(value).map_err(|error| LaserError::Codec(format!("canonicalize: {error}")))
320}
321
322#[cfg(feature = "a2a-bridge")]
325fn without_signatures(card: &serde_json::Value) -> serde_json::Value {
326 let mut bare = card.clone();
327 if let Some(fields) = bare.as_object_mut() {
328 fields.remove("signatures");
329 }
330 bare
331}
332
333#[cfg(feature = "a2a-bridge")]
335fn jws_signing_input(protected_b64: &str, payload: &[u8]) -> String {
336 format!("{protected_b64}.{}", base64url(payload))
337}
338
339#[cfg(feature = "a2a-bridge")]
341fn base64url(payload: &[u8]) -> String {
342 const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
343 let mut out = String::with_capacity(payload.len().div_ceil(3) * 4);
344 for chunk in payload.chunks(3) {
345 let buffer = [
346 chunk[0],
347 chunk.get(1).copied().unwrap_or(0),
348 chunk.get(2).copied().unwrap_or(0),
349 ];
350 let index = u32::from_be_bytes([0, buffer[0], buffer[1], buffer[2]]);
351 out.push(ALPHABET[(index >> 18) as usize & 63] as char);
352 out.push(ALPHABET[(index >> 12) as usize & 63] as char);
353 if chunk.len() > 1 {
354 out.push(ALPHABET[(index >> 6) as usize & 63] as char);
355 }
356 if chunk.len() > 2 {
357 out.push(ALPHABET[index as usize & 63] as char);
358 }
359 }
360 out
361}
362
363#[cfg(feature = "a2a-bridge")]
365fn base64url_decode(text: &str) -> Result<Vec<u8>, LaserError> {
366 fn value_of(byte: u8) -> Result<u32, LaserError> {
367 match byte {
368 b'A'..=b'Z' => Ok(u32::from(byte - b'A')),
369 b'a'..=b'z' => Ok(u32::from(byte - b'a') + 26),
370 b'0'..=b'9' => Ok(u32::from(byte - b'0') + 52),
371 b'-' => Ok(62),
372 b'_' => Ok(63),
373 _ => Err(LaserError::Signature("invalid base64url".to_owned())),
374 }
375 }
376 let payload = text.as_bytes();
377 let mut out = Vec::with_capacity(payload.len() * 3 / 4);
378 for chunk in payload.chunks(4) {
379 if chunk.len() == 1 {
380 return Err(LaserError::Signature("truncated base64url".to_owned()));
381 }
382 let mut index = 0u32;
383 for &byte in chunk {
384 index = (index << 6) | value_of(byte)?;
385 }
386 index <<= 6 * (4 - chunk.len()) as u32;
387 out.push((index >> 16) as u8);
388 if chunk.len() > 2 {
389 out.push((index >> 8) as u8);
390 }
391 if chunk.len() > 3 {
392 out.push(index as u8);
393 }
394 }
395 Ok(out)
396}
397
398#[derive(Debug, Clone, PartialEq, Eq)]
403pub struct VerifiedPrincipal {
404 pub principal: String,
406 pub kind: KeyKind,
408}
409
410#[derive(Debug, Clone)]
415pub struct KeyRecord {
416 pub principal: String,
418 pub verifying: VerifyingKey,
420 pub kind: KeyKind,
422 pub valid_from_micros: u64,
424 pub valid_to_micros: Option<u64>,
426 pub revoked: bool,
428}
429
430impl KeyRecord {
431 pub fn key_id(&self) -> Vec<u8> {
433 key_id_of(&self.verifying)
434 }
435
436 pub fn agent(principal: impl Into<String>, verifying: VerifyingKey) -> Self {
438 Self::new(principal, verifying, KeyKind::Agent)
439 }
440
441 pub fn operator(principal: impl Into<String>, verifying: VerifyingKey) -> Self {
443 Self::new(principal, verifying, KeyKind::Operator)
444 }
445
446 pub fn from_verifying_bytes(
448 principal: impl Into<String>,
449 verifying: &[u8],
450 kind: KeyKind,
451 ) -> Result<Self, LaserError> {
452 let bytes: [u8; 32] = verifying.try_into().map_err(|_| {
453 LaserError::Invalid("an Ed25519 public key requires exactly 32 bytes".to_owned())
454 })?;
455 let verifying = VerifyingKey::from_bytes(&bytes)
456 .map_err(|_| LaserError::Invalid("invalid Ed25519 public key".to_owned()))?;
457 Ok(Self::new(principal, verifying, kind))
458 }
459
460 fn new(principal: impl Into<String>, verifying: VerifyingKey, kind: KeyKind) -> Self {
461 Self {
462 principal: principal.into(),
463 verifying,
464 kind,
465 valid_from_micros: 0,
466 valid_to_micros: None,
467 revoked: false,
468 }
469 }
470
471 #[must_use]
473 pub fn valid_window(mut self, from_micros: u64, to_micros: Option<u64>) -> Self {
474 self.valid_from_micros = from_micros;
475 self.valid_to_micros = to_micros;
476 self
477 }
478
479 #[must_use]
481 pub fn revoked(mut self) -> Self {
482 self.revoked = true;
483 self
484 }
485}
486
487#[derive(Clone, Default)]
495pub struct KeyRegistry {
496 keys: HashMap<Vec<u8>, KeyRecord>,
497}
498
499impl KeyRegistry {
500 pub fn new() -> Self {
502 Self::default()
503 }
504
505 pub fn enroll(&mut self, principal: impl Into<String>, verifying: VerifyingKey) {
508 self.enroll_record(KeyRecord::agent(principal, verifying));
509 }
510
511 pub fn enroll_operator(&mut self, principal: impl Into<String>, verifying: VerifyingKey) {
514 self.enroll_record(KeyRecord::operator(principal, verifying));
515 }
516
517 pub fn enroll_record(&mut self, record: KeyRecord) {
519 self.keys.insert(key_id_of(&record.verifying), record);
520 }
521
522 pub fn verify(&self, envelope: &AgentEnvelope) -> Result<String, LaserError> {
527 self.check(envelope, None)
528 .map(|verified| verified.principal)
529 }
530
531 pub fn verify_at(
536 &self,
537 envelope: &AgentEnvelope,
538 at_micros: u64,
539 ) -> Result<VerifiedPrincipal, LaserError> {
540 self.check(envelope, Some(at_micros))
541 }
542
543 pub fn verify_observed_at(
548 &self,
549 envelope: &AgentEnvelope,
550 observed: &SignatureContext,
551 at_micros: u64,
552 ) -> Result<VerifiedPrincipal, LaserError> {
553 let signed = envelope
554 .signature
555 .as_ref()
556 .and_then(|signature| signature.context.as_ref())
557 .ok_or_else(|| {
558 LaserError::Signature("signature does not bind record headers".to_owned())
559 })?;
560 if signed != observed {
561 return Err(LaserError::Signature(
562 "signature context differs from record headers".to_owned(),
563 ));
564 }
565 self.check(envelope, Some(at_micros))
566 }
567
568 fn check(
569 &self,
570 envelope: &AgentEnvelope,
571 at_micros: Option<u64>,
572 ) -> Result<VerifiedPrincipal, LaserError> {
573 let signature = envelope
574 .signature
575 .as_ref()
576 .ok_or_else(|| LaserError::Signature("envelope is not signed".to_owned()))?;
577 if signature.scheme != SIGNATURE_SCHEME_ED25519 {
578 return Err(LaserError::Signature(format!(
579 "unsupported signature scheme {}",
580 signature.scheme
581 )));
582 }
583 let record = self
584 .keys
585 .get(signature.key_id.as_slice())
586 .ok_or_else(|| LaserError::Signature("signing key is not enrolled".to_owned()))?;
587 if record.revoked {
588 return Err(LaserError::Signature("signing key is revoked".to_owned()));
589 }
590 if let Some(now) = at_micros {
591 if now < record.valid_from_micros {
592 return Err(LaserError::Signature(
593 "signing key is not yet valid".to_owned(),
594 ));
595 }
596 if record.valid_to_micros.is_some_and(|to| now >= to) {
597 return Err(LaserError::Signature("signing key has expired".to_owned()));
598 }
599 }
600 let payload: [u8; SIGNATURE_BYTES] = signature
601 .bytes
602 .as_slice()
603 .try_into()
604 .map_err(|_| LaserError::Signature("malformed Ed25519 signature".to_owned()))?;
605 let signature_bytes = ed25519_dalek::Signature::from_bytes(&payload);
606 let input = signing_input(envelope, signature.context.as_ref())?;
607 record
608 .verifying
609 .verify(&input, &signature_bytes)
610 .map_err(|_| LaserError::Signature("signature verification failed".to_owned()))?;
611 Ok(VerifiedPrincipal {
612 principal: record.principal.clone(),
613 kind: record.kind,
614 })
615 }
616}
617
618pub fn verify_delegation(
625 registry: &KeyRegistry,
626 envelope: &AgentEnvelope,
627) -> Result<Option<(String, String)>, LaserError> {
628 let signer = registry.verify(envelope)?;
629 let delegated = envelope
630 .metadata
631 .as_ref()
632 .and_then(|meta| meta.get(laser_wire::agent::METADATA_DELEGATED_BY))
633 .and_then(|value| match value {
634 laser_wire::query::Value::Str(user) => Some(user.clone()),
635 _ => None,
636 });
637 Ok(delegated.map(|user| (signer, user)))
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643 use laser_wire::agent::{AgentEnvelope, ConversationId, CorrelationId, RecordId};
644
645 fn envelope() -> AgentEnvelope {
646 AgentEnvelope::command(
647 RecordId::from_u128(1),
648 ConversationId::from_u128(2),
649 "planner".parse().expect("valid agent id"),
650 CorrelationId::from_u128(3),
651 b"{}".to_vec(),
652 )
653 }
654
655 #[test]
656 fn given_a_signed_envelope_when_verified_then_should_return_the_enrolled_principal() {
657 let key = SigningKey::from_bytes(&[7u8; 32]);
658 let mut registry = KeyRegistry::new();
659 registry.enroll("user-42", key.verifying_key());
660
661 let signed = envelope().with_signature(key.sign(&envelope()).expect("signs"));
662 assert_eq!(registry.verify(&signed).expect("verifies"), "user-42");
663 }
664
665 #[test]
666 fn given_a_tampered_body_when_verified_then_should_fail() {
667 let key = SigningKey::from_bytes(&[7u8; 32]);
668 let mut registry = KeyRegistry::new();
669 registry.enroll("user-42", key.verifying_key());
670
671 let signature = key.sign(&envelope()).expect("signs");
672 let mut tampered = envelope();
673 tampered.body = b"{\"evil\":true}".to_vec();
674 let tampered = tampered.with_signature(signature);
675 assert!(matches!(
676 registry.verify(&tampered),
677 Err(LaserError::Signature(_))
678 ));
679 }
680
681 #[test]
682 fn given_an_unenrolled_key_when_verified_then_should_fail() {
683 let key = SigningKey::from_bytes(&[7u8; 32]);
684 let signed = envelope().with_signature(key.sign(&envelope()).expect("signs"));
685 assert!(matches!(
686 KeyRegistry::new().verify(&signed),
687 Err(LaserError::Signature(_))
688 ));
689 }
690
691 #[test]
692 fn given_an_unsigned_envelope_when_verified_then_should_fail() {
693 assert!(matches!(
694 KeyRegistry::new().verify(&envelope()),
695 Err(LaserError::Signature(_))
696 ));
697 }
698
699 #[test]
700 fn given_a_signed_delegation_when_verified_then_should_return_the_signer_and_user() {
701 let key = SigningKey::from_bytes(&[9u8; 32]);
702 let mut registry = KeyRegistry::new();
703 registry.enroll("agent-a", key.verifying_key());
704
705 let env = envelope().with_metadata(laser_wire::agent::METADATA_DELEGATED_BY, "user-7");
706 let signed = env.clone().with_signature(key.sign(&env).expect("signs"));
707 assert_eq!(
708 verify_delegation(®istry, &signed).expect("verifies"),
709 Some(("agent-a".to_owned(), "user-7".to_owned()))
710 );
711
712 let plain = envelope().with_signature(key.sign(&envelope()).expect("signs"));
714 assert_eq!(
715 verify_delegation(®istry, &plain).expect("verifies"),
716 None
717 );
718
719 let mut forged = envelope().with_signature(key.sign(&envelope()).expect("signs"));
721 forged = forged.with_metadata(laser_wire::agent::METADATA_DELEGATED_BY, "user-999");
722 assert!(matches!(
723 verify_delegation(®istry, &forged),
724 Err(LaserError::Signature(_))
725 ));
726 }
727 #[cfg(feature = "a2a-bridge")]
728 #[test]
729 fn given_a_signed_card_when_verified_then_should_pass_and_fail_on_tamper() {
730 let key = SigningKey::from_bytes(&[7u8; 32]);
731 let card = serde_json::json!({
732 "name": "a2a-bridge",
733 "version": "0.0.1",
734 "supportedInterfaces": [
735 {"url": "/", "protocolBinding": "JSONRPC", "protocolVersion": "1.0"}
736 ],
737 "skills": []
738 });
739 let signature = sign_card_value(&key, &card).expect("signs");
740 verify_card(&card, &signature, &key.verifying_key()).expect("verifies");
741
742 let mut tampered = card.clone();
744 tampered["name"] = serde_json::json!("impostor");
745 assert!(verify_card(&tampered, &signature, &key.verifying_key()).is_err());
746
747 let mut signed = card.clone();
750 signed["signatures"] = serde_json::json!([{
751 "protected": signature.protected,
752 "signature": signature.signature,
753 }]);
754 verify_card(&signed, &signature, &key.verifying_key()).expect("self-carrying verifies");
755 }
756
757 #[test]
758 fn given_a_context_signature_when_the_interpretation_flips_then_should_fail() {
759 let key = SigningKey::from_bytes(&[21u8; 32]);
760 let mut registry = KeyRegistry::new();
761 registry.enroll("user-1", key.verifying_key());
762 let context = SignatureContext {
763 content_type: Some(1),
764 agent_version: Some(1),
765 };
766 let signed =
767 envelope().with_signature(key.sign_with_context(&envelope(), context).expect("signs"));
768 assert_eq!(registry.verify(&signed).expect("verifies"), "user-1");
769 let mut flipped = signed.clone();
772 if let Some(signature) = flipped.signature.as_mut() {
773 signature.context = Some(SignatureContext {
774 content_type: Some(3),
775 agent_version: Some(1),
776 });
777 }
778 assert!(registry.verify(&flipped).is_err());
779 }
780
781 #[test]
782 fn given_an_operator_key_when_verified_at_then_should_report_the_operator_kind() {
783 let key = SigningKey::from_bytes(&[11u8; 32]);
784 let mut registry = KeyRegistry::new();
785 registry.enroll_operator("op-1", key.verifying_key());
786 let signed = envelope().with_signature(key.sign(&envelope()).expect("signs"));
787 let verified = registry.verify_at(&signed, 1_000).expect("verifies");
788 assert_eq!(verified.principal, "op-1");
789 assert_eq!(verified.kind, KeyKind::Operator);
790 let agent_key = SigningKey::from_bytes(&[12u8; 32]);
792 registry.enroll("agent-x", agent_key.verifying_key());
793 let agent_signed = envelope().with_signature(agent_key.sign(&envelope()).expect("signs"));
794 assert_eq!(
795 registry
796 .verify_at(&agent_signed, 1_000)
797 .expect("verifies")
798 .kind,
799 KeyKind::Agent
800 );
801 }
802
803 #[test]
804 fn given_a_key_outside_its_window_when_verified_at_then_should_fail() {
805 let key = SigningKey::from_bytes(&[13u8; 32]);
806 let mut registry = KeyRegistry::new();
807 registry.enroll_record(
808 KeyRecord::operator("op-2", key.verifying_key()).valid_window(100, Some(200)),
809 );
810 let signed = envelope().with_signature(key.sign(&envelope()).expect("signs"));
811 assert!(
812 registry.verify_at(&signed, 150).is_ok(),
813 "inside the window"
814 );
815 assert!(
816 registry.verify_at(&signed, 50).is_err(),
817 "before valid_from"
818 );
819 assert!(registry.verify_at(&signed, 200).is_err(), "at valid_to");
820 assert!(registry.verify(&signed).is_ok());
822 }
823
824 #[test]
825 fn given_a_revoked_key_when_verified_then_should_fail_regardless_of_time() {
826 let key = SigningKey::from_bytes(&[14u8; 32]);
827 let mut registry = KeyRegistry::new();
828 registry.enroll_record(KeyRecord::operator("op-3", key.verifying_key()).revoked());
829 let signed = envelope().with_signature(key.sign(&envelope()).expect("signs"));
830 assert!(registry.verify(&signed).is_err());
831 assert!(registry.verify_at(&signed, 1_000).is_err());
832 }
833
834 #[test]
835 fn given_base64url_when_round_tripped_then_should_be_lossless() {
836 for input in [&b""[..], b"f", b"fo", b"foo", b"foob", &[0xff, 0x00, 0x7f]] {
837 #[cfg(feature = "a2a-bridge")]
838 assert_eq!(
839 base64url_decode(&base64url(input)).expect("decodes"),
840 input.to_vec()
841 );
842 }
843 }
844}