use auths_core::signing::StorageSigner;
use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyRole, KeyStorage};
use auths_core::testing::{IsolatedKeychainHandle, TestPassphraseProvider};
use auths_id::attestation::create::create_signed_attestation;
use auths_id::identity::initialize::initialize_keri_identity;
use auths_id::identity::rotate::rotate_keri_identity;
use auths_id::keri::{Event, GitKel, resolve_did_keri, resolve_did_keri_at_sequence, validate_kel};
use auths_id::storage::git_refs::AttestationMetadata;
use auths_id::storage::layout::StorageLayoutConfig;
use auths_id::testing::fakes::FakeIdentityStorage;
use auths_verifier::verify::{verify_at_time, verify_with_keys};
use auths_verifier::{
CanonicalDid, DevicePublicKey, VerificationStatus, verify_chain, verify_device_authorization,
};
fn ed(pk: &[u8]) -> DevicePublicKey {
DevicePublicKey::try_new(auths_crypto::CurveType::Ed25519, pk).unwrap()
}
use chrono::Utc;
use git2::Repository;
use ring::rand::SystemRandom;
use ring::signature::{Ed25519KeyPair, KeyPair};
use std::path::Path;
fn init_identity(
repo_path: &Path,
alias: &str,
passphrase: &str,
keychain: &IsolatedKeychainHandle,
) -> (String, String) {
let provider = TestPassphraseProvider::new(passphrase);
let identity_storage = FakeIdentityStorage::new();
let alias = KeyAlias::new_unchecked(alias);
let (did, alias) = initialize_keri_identity(
repo_path,
&alias,
None,
&provider,
&identity_storage,
keychain,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.expect("Failed to initialize identity");
(did.to_string(), alias.into_inner())
}
fn generate_device_keypair(
identity_did: &str,
device_alias: &str,
passphrase: &str,
keychain: &IsolatedKeychainHandle,
) -> (CanonicalDid, [u8; 32]) {
let rng = SystemRandom::new();
let device_pkcs8 =
Ed25519KeyPair::generate_pkcs8(&rng).expect("Failed to generate device keypair");
let device_keypair =
Ed25519KeyPair::from_pkcs8(device_pkcs8.as_ref()).expect("Failed to parse device keypair");
let device_pk: [u8; 32] = device_keypair
.public_key()
.as_ref()
.try_into()
.expect("Public key should be 32 bytes");
let device_did =
CanonicalDid::from_public_key_did_key(&device_pk, auths_crypto::CurveType::Ed25519);
let encrypted = auths_core::crypto::signer::encrypt_keypair(device_pkcs8.as_ref(), passphrase)
.expect("Failed to encrypt device key");
let identity_did_typed = IdentityDID::new_unchecked(identity_did);
keychain
.store_key(
&KeyAlias::new_unchecked(device_alias),
&identity_did_typed,
KeyRole::Primary,
&encrypted,
)
.expect("Failed to store device key");
(device_did, device_pk)
}
#[allow(clippy::too_many_arguments)]
fn create_test_attestation(
rid: &str,
identity_did: &str,
identity_alias: &str,
subject: &CanonicalDid,
device_pk: &[u8],
device_alias: Option<&str>,
passphrase: &str,
keychain: &IsolatedKeychainHandle,
) -> auths_verifier::core::Attestation {
let signer = StorageSigner::new(keychain.clone());
let provider = TestPassphraseProvider::new(passphrase);
let now = Utc::now();
let meta = AttestationMetadata {
note: Some("integration test".to_string()),
timestamp: Some(now),
expires_at: None,
};
let identity_did = IdentityDID::new_unchecked(identity_did);
let identity_alias = KeyAlias::new_unchecked(identity_alias);
let device_alias = device_alias.map(KeyAlias::new_unchecked);
create_signed_attestation(
now,
auths_id::attestation::create::AttestationInput {
rid,
identity_did: &identity_did,
subject,
device_public_key: device_pk,
device_curve: auths_crypto::CurveType::Ed25519,
payload: None,
meta: &meta,
identity_alias: Some(&identity_alias),
device_alias: device_alias.as_ref(),
delegated_by: None,
commit_sha: None,
signer_type: None,
},
&signer,
&provider,
)
.expect("Failed to create signed attestation")
}
fn resolve_identity_public_key(repo_path: &Path, did: &str) -> Vec<u8> {
let repo = Repository::open(repo_path).expect("Failed to open repo");
let resolution = resolve_did_keri(&repo, did).expect("Failed to resolve did:keri");
resolution.public_key
}
fn resolve_identity_public_key_at_sequence(repo_path: &Path, did: &str, sequence: u64) -> Vec<u8> {
let repo = Repository::open(repo_path).expect("Failed to open repo");
let resolution = resolve_did_keri_at_sequence(&repo, did, sequence as u128)
.expect("Failed to resolve at sequence");
resolution.public_key
}
fn rotate_identity(
repo_path: &Path,
current_alias: &str,
next_alias: &str,
passphrase: &str,
keychain: &IsolatedKeychainHandle,
) {
let provider = TestPassphraseProvider::new(passphrase);
let config = StorageLayoutConfig::default();
let current_alias = KeyAlias::new_unchecked(current_alias);
let next_alias = KeyAlias::new_unchecked(next_alias);
rotate_keri_identity(
repo_path,
¤t_alias,
&next_alias,
&provider,
&config,
keychain,
None,
chrono::Utc::now(),
)
.expect("Failed to rotate identity");
}
#[tokio::test(flavor = "multi_thread")]
async fn test_full_identity_lifecycle() {
let kc = IsolatedKeychainHandle::new();
let (_dir, _repo) = auths_test_utils::git::init_test_repo();
let repo_path = _dir.path().to_path_buf();
let passphrase = "Test-P@ss12345";
let (identity_did, identity_alias) = init_identity(&repo_path, "main", passphrase, &kc);
assert!(
identity_did.starts_with("did:keri:"),
"DID should be a KERI DID"
);
let identity_pk = resolve_identity_public_key(&repo_path, &identity_did);
assert_eq!(
identity_pk.len(),
32,
"Ed25519 public key should be 32 bytes"
);
let (device_did, device_pk) =
generate_device_keypair(&identity_did, "device-laptop", passphrase, &kc);
let attestation = create_test_attestation(
"test-repo",
&identity_did,
&identity_alias,
&device_did,
&device_pk,
Some("device-laptop"),
passphrase,
&kc,
);
verify_with_keys(&attestation, &ed(&identity_pk))
.await
.expect("Attestation should verify");
rotate_identity(&repo_path, "main", "main-rot1", passphrase, &kc);
let old_pk = resolve_identity_public_key_at_sequence(&repo_path, &identity_did, 0);
assert_eq!(old_pk, identity_pk, "Historical key should match original");
verify_at_time(&attestation, &ed(&old_pk), attestation.timestamp.unwrap())
.await
.expect("Old attestation should verify with historical key");
let new_identity_pk = resolve_identity_public_key(&repo_path, &identity_did);
assert_ne!(
new_identity_pk, identity_pk,
"Rotated key should differ from original"
);
let (device_did2, device_pk2) =
generate_device_keypair(&identity_did, "device-phone", passphrase, &kc);
let new_attestation = create_test_attestation(
"test-repo",
&identity_did,
"main-rot1",
&device_did2,
&device_pk2,
Some("device-phone"),
passphrase,
&kc,
);
verify_with_keys(&new_attestation, &ed(&new_identity_pk))
.await
.expect("New attestation should verify with rotated key");
}
#[tokio::test(flavor = "multi_thread")]
async fn test_attestation_chain_after_rotation() {
let kc = IsolatedKeychainHandle::new();
let (_dir, _repo) = auths_test_utils::git::init_test_repo();
let repo_path = _dir.path().to_path_buf();
let passphrase = "Test-P@ss12345";
let (identity_did, identity_alias) = init_identity(&repo_path, "chain-id", passphrase, &kc);
let identity_pk = resolve_identity_public_key(&repo_path, &identity_did);
let (device1_did, device1_pk) =
generate_device_keypair(&identity_did, "chain-device1", passphrase, &kc);
let att1 = create_test_attestation(
"test-repo",
&identity_did,
&identity_alias,
&device1_did,
&device1_pk,
Some("chain-device1"),
passphrase,
&kc,
);
let (device2_did, device2_pk) =
generate_device_keypair(&identity_did, "chain-device2", passphrase, &kc);
let device1_did_str = device1_did.to_string();
let att2 = create_test_attestation(
"test-repo",
&device1_did_str,
"chain-device1",
&device2_did,
&device2_pk,
Some("chain-device2"),
passphrase,
&kc,
);
let report = verify_chain(&[att1.clone(), att2], &ed(&identity_pk))
.await
.expect("Chain verify failed");
assert!(report.is_valid(), "Chain should be valid");
assert_eq!(report.chain.len(), 2);
rotate_identity(&repo_path, "chain-id", "chain-id-rot1", passphrase, &kc);
let old_pk = resolve_identity_public_key_at_sequence(&repo_path, &identity_did, 0);
verify_at_time(&att1, &ed(&old_pk), att1.timestamp.unwrap())
.await
.expect("First chain link should still verify with historical key");
}
#[tokio::test(flavor = "multi_thread")]
async fn test_verify_device_authorization_lifecycle() {
let kc = IsolatedKeychainHandle::new();
let (_dir, _repo) = auths_test_utils::git::init_test_repo();
let repo_path = _dir.path().to_path_buf();
let passphrase = "Test-P@ss12345";
let (identity_did, identity_alias) = init_identity(&repo_path, "authz-id", passphrase, &kc);
let identity_pk = resolve_identity_public_key(&repo_path, &identity_did);
let (device_did, device_pk) =
generate_device_keypair(&identity_did, "authz-device", passphrase, &kc);
let attestation = create_test_attestation(
"test-repo",
&identity_did,
&identity_alias,
&device_did,
&device_pk,
Some("authz-device"),
passphrase,
&kc,
);
let report = verify_device_authorization(
&identity_did,
&device_did,
std::slice::from_ref(&attestation),
&ed(&identity_pk),
)
.await
.expect("verify_device_authorization failed");
assert!(report.is_valid(), "Device should be authorized");
let mut revoked_att = attestation;
revoked_att.revoked_at = Some(Utc::now());
let report = verify_device_authorization(
&identity_did,
&device_did,
&[revoked_att],
&ed(&identity_pk),
)
.await
.expect("verify_device_authorization failed");
assert!(
!report.is_valid(),
"Revoked device should not be authorized"
);
match report.status {
VerificationStatus::Revoked { .. } => {}
_ => panic!("Expected Revoked status, got {:?}", report.status),
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_multiple_rotations_maintain_verification() {
let kc = IsolatedKeychainHandle::new();
let (_dir, _repo) = auths_test_utils::git::init_test_repo();
let repo_path = _dir.path().to_path_buf();
let passphrase = "Test-P@ss12345";
let (identity_did, _identity_alias) = init_identity(&repo_path, "multi-rot", passphrase, &kc);
let original_pk = resolve_identity_public_key(&repo_path, &identity_did);
let (device_did, device_pk) =
generate_device_keypair(&identity_did, "multi-rot-device", passphrase, &kc);
let original_attestation = create_test_attestation(
"test-repo",
&identity_did,
"multi-rot",
&device_did,
&device_pk,
Some("multi-rot-device"),
passphrase,
&kc,
);
verify_with_keys(&original_attestation, &ed(&original_pk))
.await
.expect("Original attestation should verify");
rotate_identity(&repo_path, "multi-rot", "multi-rot2", passphrase, &kc);
rotate_identity(&repo_path, "multi-rot2", "multi-rot3", passphrase, &kc);
rotate_identity(&repo_path, "multi-rot3", "multi-rot4", passphrase, &kc);
let historical_pk = resolve_identity_public_key_at_sequence(&repo_path, &identity_did, 0);
assert_eq!(historical_pk, original_pk);
verify_at_time(
&original_attestation,
&ed(&historical_pk),
original_attestation.timestamp.unwrap(),
)
.await
.expect("Original attestation should verify with historical key after 3 rotations");
let current_pk = resolve_identity_public_key(&repo_path, &identity_did);
assert_ne!(
current_pk, original_pk,
"Key should have changed after rotations"
);
let (device_did2, device_pk2) =
generate_device_keypair(&identity_did, "multi-rot-device2", passphrase, &kc);
let new_attestation = create_test_attestation(
"test-repo",
&identity_did,
"multi-rot4",
&device_did2,
&device_pk2,
Some("multi-rot-device2"),
passphrase,
&kc,
);
verify_with_keys(&new_attestation, &ed(¤t_pk))
.await
.expect("New attestation should verify with current key");
}
#[test]
fn test_init_creates_keri_kel() {
let kc = IsolatedKeychainHandle::new();
let (_dir, _repo) = auths_test_utils::git::init_test_repo();
let repo_path = _dir.path().to_path_buf();
let passphrase = "Test-P@ss12345";
let (identity_did, _alias) = init_identity(&repo_path, "kel-test", passphrase, &kc);
let prefix = identity_did
.strip_prefix("did:keri:")
.expect("Should be a did:keri");
let repo = Repository::open(&repo_path).expect("Failed to open repo");
let kel = GitKel::new(&repo, prefix);
let events = kel.get_events().expect("Failed to read KEL events");
assert_eq!(events.len(), 1, "KEL should have exactly 1 inception event");
assert!(
matches!(events[0], Event::Icp(_)),
"First event should be inception"
);
let state = validate_kel(&events).expect("KEL validation failed");
assert_eq!(state.sequence, 0, "Inception should be sequence 0");
}
#[test]
fn test_rotation_appends_to_kel() {
let kc = IsolatedKeychainHandle::new();
let (_dir, _repo) = auths_test_utils::git::init_test_repo();
let repo_path = _dir.path().to_path_buf();
let passphrase = "Test-P@ss12345";
let (identity_did, _alias) = init_identity(&repo_path, "kel-rot", passphrase, &kc);
let prefix = identity_did
.strip_prefix("did:keri:")
.expect("Should be a did:keri");
let repo = Repository::open(&repo_path).expect("Failed to open repo");
let kel = GitKel::new(&repo, prefix);
let events = kel.get_events().expect("Failed to read KEL");
assert_eq!(events.len(), 1);
rotate_identity(&repo_path, "kel-rot", "kel-rot2", passphrase, &kc);
let repo = Repository::open(&repo_path).expect("Failed to reopen repo");
let kel = GitKel::new(&repo, prefix);
let events = kel.get_events().expect("Failed to read KEL after rotation");
assert_eq!(events.len(), 2, "KEL should have 2 events after 1 rotation");
assert!(matches!(events[0], Event::Icp(_)));
assert!(matches!(events[1], Event::Rot(_)));
let state = validate_kel(&events).expect("KEL validation failed");
assert_eq!(state.sequence, 1);
rotate_identity(&repo_path, "kel-rot2", "kel-rot3", passphrase, &kc);
let repo = Repository::open(&repo_path).expect("Failed to reopen repo");
let kel = GitKel::new(&repo, prefix);
let events = kel
.get_events()
.expect("Failed to read KEL after 2nd rotation");
assert_eq!(
events.len(),
3,
"KEL should have 3 events after 2 rotations"
);
let state = validate_kel(&events).expect("KEL validation failed");
assert_eq!(state.sequence, 2);
for (i, event) in events.iter().enumerate() {
assert_eq!(
event.sequence().value(),
i as u128,
"Event {} should have sequence {}",
i,
i
);
}
}