1use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
7use git2::Repository;
8use ring::signature::Ed25519KeyPair;
9
10use auths_core::crypto::said::compute_said;
11
12use super::event::KeriSequence;
13use super::seal::SealType;
14use super::types::{Prefix, Said};
15use super::{
16 Event, GitKel, IxnEvent, KERI_VERSION, KelError, Seal, ValidationError, parse_did_keri,
17 validate_kel,
18};
19
20#[derive(Debug, thiserror::Error)]
22#[non_exhaustive]
23pub enum AnchorError {
24 #[error("KEL error: {0}")]
25 Kel(#[from] KelError),
26
27 #[error("Validation error: {0}")]
28 Validation(#[from] ValidationError),
29
30 #[error("Serialization error: {0}")]
31 Serialization(String),
32
33 #[error("Invalid DID format: {0}")]
34 InvalidDid(String),
35
36 #[error("KEL not found for prefix: {0}")]
37 NotFound(String),
38}
39
40impl auths_core::error::AuthsErrorInfo for AnchorError {
41 fn error_code(&self) -> &'static str {
42 match self {
43 Self::Kel(_) => "AUTHS-E4961",
44 Self::Validation(_) => "AUTHS-E4962",
45 Self::Serialization(_) => "AUTHS-E4963",
46 Self::InvalidDid(_) => "AUTHS-E4964",
47 Self::NotFound(_) => "AUTHS-E4965",
48 }
49 }
50
51 fn suggestion(&self) -> Option<&'static str> {
52 match self {
53 Self::Kel(_) => None,
54 Self::Validation(_) => None,
55 Self::Serialization(_) => None,
56 Self::InvalidDid(_) => Some("Use the format 'did:keri:E<prefix>'"),
57 Self::NotFound(_) => Some("Initialize the identity first with 'auths init'"),
58 }
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct AnchorVerification {
65 pub anchored: bool,
67
68 pub anchor_said: Option<Said>,
70
71 pub anchor_sequence: Option<u64>,
73
74 pub signing_key: Option<String>,
76}
77
78pub fn anchor_data<T: serde::Serialize>(
92 repo: &Repository,
93 prefix: &Prefix,
94 data: &T,
95 seal_type: SealType,
96 current_keypair: &Ed25519KeyPair,
97 now: chrono::DateTime<chrono::Utc>,
98) -> Result<Said, AnchorError> {
99 let kel = GitKel::new(repo, prefix.as_str());
100 if !kel.exists() {
101 return Err(AnchorError::NotFound(prefix.as_str().to_string()));
102 }
103
104 let events = kel.get_events()?;
105 let state = validate_kel(&events)?;
106
107 let data_json =
109 serde_json::to_vec(data).map_err(|e| AnchorError::Serialization(e.to_string()))?;
110 let data_digest = compute_said(&data_json);
111
112 let seal = Seal::new(data_digest, seal_type);
114
115 let new_sequence = state.sequence + 1;
117 let mut ixn = IxnEvent {
118 v: KERI_VERSION.to_string(),
119 d: Said::default(),
120 i: prefix.clone(),
121 s: KeriSequence::new(new_sequence),
122 p: state.last_event_said.clone(),
123 a: vec![seal],
124 x: String::new(), };
126
127 let ixn_json = serde_json::to_vec(&Event::Ixn(ixn.clone()))
129 .map_err(|e| AnchorError::Serialization(e.to_string()))?;
130 ixn.d = compute_said(&ixn_json);
131
132 let canonical = super::serialize_for_signing(&Event::Ixn(ixn.clone()))?;
134 let sig = current_keypair.sign(&canonical);
135 ixn.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
136
137 kel.append(&Event::Ixn(ixn.clone()), now)?;
139
140 Ok(ixn.d)
141}
142
143pub fn anchor_attestation<T: serde::Serialize>(
147 repo: &Repository,
148 prefix: &Prefix,
149 attestation: &T,
150 current_keypair: &Ed25519KeyPair,
151 now: chrono::DateTime<chrono::Utc>,
152) -> Result<Said, AnchorError> {
153 anchor_data(
154 repo,
155 prefix,
156 attestation,
157 SealType::DeviceAttestation,
158 current_keypair,
159 now,
160 )
161}
162
163pub fn anchor_idp_binding<T: serde::Serialize>(
167 repo: &Repository,
168 prefix: &Prefix,
169 binding: &T,
170 current_keypair: &Ed25519KeyPair,
171 now: chrono::DateTime<chrono::Utc>,
172) -> Result<Said, AnchorError> {
173 anchor_data(
174 repo,
175 prefix,
176 binding,
177 SealType::IdpBinding,
178 current_keypair,
179 now,
180 )
181}
182
183pub fn find_anchor_event(
193 repo: &Repository,
194 prefix: &Prefix,
195 data_digest: &str,
196) -> Result<Option<IxnEvent>, AnchorError> {
197 let kel = GitKel::new(repo, prefix.as_str());
198 if !kel.exists() {
199 return Err(AnchorError::NotFound(prefix.as_str().to_string()));
200 }
201
202 let events = kel.get_events()?;
203
204 for event in events {
205 if let Event::Ixn(ixn) = event {
206 for seal in &ixn.a {
207 if seal.d == data_digest {
208 return Ok(Some(ixn));
209 }
210 }
211 }
212 }
213
214 Ok(None)
215}
216
217pub fn verify_anchor<T: serde::Serialize>(
229 repo: &Repository,
230 prefix: &Prefix,
231 data: &T,
232) -> Result<AnchorVerification, AnchorError> {
233 let data_json =
235 serde_json::to_vec(data).map_err(|e| AnchorError::Serialization(e.to_string()))?;
236 let data_digest = compute_said(&data_json);
237
238 verify_anchor_by_digest(repo, prefix, data_digest.as_str())
239}
240
241pub fn verify_anchor_by_digest(
243 repo: &Repository,
244 prefix: &Prefix,
245 data_digest: &str,
246) -> Result<AnchorVerification, AnchorError> {
247 let kel = GitKel::new(repo, prefix.as_str());
248 if !kel.exists() {
249 return Err(AnchorError::NotFound(prefix.as_str().to_string()));
250 }
251
252 let anchor = find_anchor_event(repo, prefix, data_digest)?;
254
255 match anchor {
256 Some(ixn) => {
257 let events = kel.get_events()?;
258
259 let anchor_seq = ixn.s.value();
260 let events_subset: Vec<_> = events
261 .into_iter()
262 .take_while(|e| e.sequence().value() <= anchor_seq)
263 .collect();
264
265 let state = validate_kel(&events_subset)?;
266
267 Ok(AnchorVerification {
268 anchored: true,
269 anchor_said: Some(ixn.d),
270 anchor_sequence: Some(anchor_seq),
271 signing_key: state.current_key().map(|s| s.to_string()),
272 })
273 }
274 None => Ok(AnchorVerification {
275 anchored: false,
276 anchor_said: None,
277 anchor_sequence: None,
278 signing_key: None,
279 }),
280 }
281}
282
283pub fn verify_attestation_anchor_by_issuer<T: serde::Serialize>(
288 repo: &Repository,
289 issuer_did: &str,
290 attestation: &T,
291) -> Result<AnchorVerification, AnchorError> {
292 let prefix: Prefix =
293 parse_did_keri(issuer_did).map_err(|e| AnchorError::InvalidDid(e.to_string()))?;
294 verify_anchor(repo, &prefix, attestation)
295}
296
297#[cfg(test)]
298#[allow(clippy::disallowed_methods)]
299mod tests {
300 use super::*;
301 use crate::keri::{Prefix, create_keri_identity};
302 use ring::signature::Ed25519KeyPair as TestKeyPair;
303 use serde::{Deserialize, Serialize};
304 use tempfile::TempDir;
305
306 fn setup_repo() -> (TempDir, Repository) {
307 let dir = TempDir::new().unwrap();
308 let repo = Repository::init(dir.path()).unwrap();
309
310 let mut config = repo.config().unwrap();
311 config.set_str("user.name", "Test User").unwrap();
312 config.set_str("user.email", "test@example.com").unwrap();
313
314 (dir, repo)
315 }
316
317 #[derive(Debug, Serialize, Deserialize)]
318 struct TestAttestation {
319 issuer: String,
320 subject: String,
321 capabilities: Vec<String>,
322 }
323
324 fn make_test_attestation(issuer: &str, subject: &str) -> TestAttestation {
325 TestAttestation {
326 issuer: issuer.to_string(),
327 subject: subject.to_string(),
328 capabilities: vec!["sign-commit".to_string()],
329 }
330 }
331
332 #[test]
333 fn anchor_creates_ixn_event() {
334 let (_dir, repo) = setup_repo();
335
336 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
337 let issuer_did = format!("did:keri:{}", init.prefix);
338 let current_keypair = TestKeyPair::from_pkcs8(init.current_keypair_pkcs8.as_ref()).unwrap();
339
340 let attestation = make_test_attestation(&issuer_did, "did:key:device123");
341 let anchor_said = anchor_attestation(
342 &repo,
343 &init.prefix,
344 &attestation,
345 ¤t_keypair,
346 chrono::Utc::now(),
347 )
348 .unwrap();
349
350 let kel = GitKel::new(&repo, init.prefix.as_str());
352 let events = kel.get_events().unwrap();
353 assert_eq!(events.len(), 2); assert!(events[0].is_inception());
356 assert!(events[1].is_interaction());
357
358 if let Event::Ixn(ixn) = &events[1] {
359 assert_eq!(ixn.d, anchor_said);
360 assert_eq!(ixn.a.len(), 1);
361 assert_eq!(ixn.a[0].seal_type, SealType::DeviceAttestation);
362 } else {
363 panic!("Expected IXN event");
364 }
365 }
366
367 #[test]
368 fn anchor_with_delegation_seal_type() {
369 let (_dir, repo) = setup_repo();
370
371 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
372 let current_keypair = TestKeyPair::from_pkcs8(init.current_keypair_pkcs8.as_ref()).unwrap();
373
374 let data = serde_json::json!({"delegation": "data"});
375 let anchor_said = anchor_data(
376 &repo,
377 &init.prefix,
378 &data,
379 SealType::Delegation,
380 ¤t_keypair,
381 chrono::Utc::now(),
382 )
383 .unwrap();
384
385 let kel = GitKel::new(&repo, init.prefix.as_str());
386 let events = kel.get_events().unwrap();
387
388 if let Event::Ixn(ixn) = &events[1] {
389 assert_eq!(ixn.d, anchor_said);
390 assert_eq!(ixn.a[0].seal_type, SealType::Delegation);
391 } else {
392 panic!("Expected IXN event");
393 }
394 }
395
396 #[test]
397 fn find_anchor_locates_attestation() {
398 let (_dir, repo) = setup_repo();
399
400 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
401 let issuer_did = format!("did:keri:{}", init.prefix);
402 let current_keypair = TestKeyPair::from_pkcs8(init.current_keypair_pkcs8.as_ref()).unwrap();
403
404 let attestation = make_test_attestation(&issuer_did, "did:key:device123");
405 anchor_attestation(
406 &repo,
407 &init.prefix,
408 &attestation,
409 ¤t_keypair,
410 chrono::Utc::now(),
411 )
412 .unwrap();
413
414 let att_json = serde_json::to_vec(&attestation).unwrap();
416 let att_digest = compute_said(&att_json);
417
418 let found = find_anchor_event(&repo, &init.prefix, att_digest.as_str()).unwrap();
419 assert!(found.is_some());
420 assert_eq!(found.unwrap().a[0].d, att_digest);
421 }
422
423 #[test]
424 fn verify_anchor_works() {
425 let (_dir, repo) = setup_repo();
426
427 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
428 let issuer_did = format!("did:keri:{}", init.prefix);
429 let current_keypair = TestKeyPair::from_pkcs8(init.current_keypair_pkcs8.as_ref()).unwrap();
430
431 let attestation = make_test_attestation(&issuer_did, "did:key:device123");
432 anchor_attestation(
433 &repo,
434 &init.prefix,
435 &attestation,
436 ¤t_keypair,
437 chrono::Utc::now(),
438 )
439 .unwrap();
440
441 let verification = verify_anchor(&repo, &init.prefix, &attestation).unwrap();
442 assert!(verification.anchored);
443 assert!(verification.anchor_said.is_some());
444 assert_eq!(verification.anchor_sequence, Some(1));
445 assert!(verification.signing_key.is_some());
446 }
447
448 #[test]
449 fn unanchored_attestation_not_found() {
450 let (_dir, repo) = setup_repo();
451
452 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
453 let issuer_did = format!("did:keri:{}", init.prefix);
454
455 let attestation = make_test_attestation(&issuer_did, "did:key:device123");
456 let verification = verify_anchor(&repo, &init.prefix, &attestation).unwrap();
459 assert!(!verification.anchored);
460 assert!(verification.anchor_said.is_none());
461 }
462
463 #[test]
464 fn multiple_anchors_work() {
465 let (_dir, repo) = setup_repo();
466
467 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
468 let issuer_did = format!("did:keri:{}", init.prefix);
469 let current_keypair = TestKeyPair::from_pkcs8(init.current_keypair_pkcs8.as_ref()).unwrap();
470
471 let att1 = make_test_attestation(&issuer_did, "did:key:device1");
472 let att2 = make_test_attestation(&issuer_did, "did:key:device2");
473
474 let said1 = anchor_attestation(
475 &repo,
476 &init.prefix,
477 &att1,
478 ¤t_keypair,
479 chrono::Utc::now(),
480 )
481 .unwrap();
482 let said2 = anchor_attestation(
483 &repo,
484 &init.prefix,
485 &att2,
486 ¤t_keypair,
487 chrono::Utc::now(),
488 )
489 .unwrap();
490
491 assert_ne!(said1, said2);
492
493 let v1 = verify_anchor(&repo, &init.prefix, &att1).unwrap();
495 let v2 = verify_anchor(&repo, &init.prefix, &att2).unwrap();
496
497 assert!(v1.anchored);
498 assert!(v2.anchored);
499 assert_eq!(v1.anchor_sequence, Some(1));
500 assert_eq!(v2.anchor_sequence, Some(2));
501 }
502
503 #[test]
504 fn verify_by_issuer_did() {
505 let (_dir, repo) = setup_repo();
506
507 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
508 let issuer_did = format!("did:keri:{}", init.prefix);
509 let current_keypair = TestKeyPair::from_pkcs8(init.current_keypair_pkcs8.as_ref()).unwrap();
510
511 let attestation = make_test_attestation(&issuer_did, "did:key:device123");
512 anchor_attestation(
513 &repo,
514 &init.prefix,
515 &attestation,
516 ¤t_keypair,
517 chrono::Utc::now(),
518 )
519 .unwrap();
520
521 let verification =
522 verify_attestation_anchor_by_issuer(&repo, &issuer_did, &attestation).unwrap();
523 assert!(verification.anchored);
524 }
525
526 #[test]
527 fn anchor_not_found_for_missing_kel() {
528 let (_dir, repo) = setup_repo();
529
530 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
532 let current_keypair = TestKeyPair::from_pkcs8(init.current_keypair_pkcs8.as_ref()).unwrap();
533
534 let attestation = make_test_attestation("did:keri:ENotExist", "did:key:device");
536 let fake_prefix = Prefix::new_unchecked("ENotExist".to_string());
537 let result = anchor_attestation(
538 &repo,
539 &fake_prefix,
540 &attestation,
541 ¤t_keypair,
542 chrono::Utc::now(),
543 );
544 assert!(matches!(result, Err(AnchorError::NotFound(_))));
545 }
546}