use std::collections::BTreeMap;
use std::fs;
use anp::authentication::{
create_did_wba_document, extract_signature_metadata, generate_auth_header, generate_http_signature_headers,
verify_auth_header_signature, verify_http_message_signature, AuthMode,
DIDWbaAuthHeader, DidDocumentOptions, DidProfile, DidWbaVerifier,
DidWbaVerifierConfig,
};
use serde_json::json;
use tempfile::tempdir;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[test]
fn test_create_did_document_profiles() {
let e1 = create_did_wba_document(
"example.com",
DidDocumentOptions {
path_segments: vec!["user".to_string(), "alice".to_string()],
..DidDocumentOptions::default()
},
)
.expect("e1 DID creation should succeed");
assert!(e1.did_document["id"].as_str().unwrap().contains(":e1_"));
assert_eq!(e1.did_document["proof"]["type"], json!("DataIntegrityProof"));
let k1 = create_did_wba_document(
"example.com",
DidDocumentOptions {
path_segments: vec!["user".to_string(), "alice".to_string()],
did_profile: DidProfile::K1,
..DidDocumentOptions::default()
},
)
.expect("k1 DID creation should succeed");
assert!(k1.did_document["id"].as_str().unwrap().contains(":k1_"));
let legacy = create_did_wba_document(
"example.com",
DidDocumentOptions {
path_segments: vec!["user".to_string(), "alice".to_string()],
did_profile: DidProfile::PlainLegacy,
..DidDocumentOptions::default()
},
)
.expect("legacy DID creation should succeed");
assert_eq!(legacy.did_document["proof"]["type"], json!("EcdsaSecp256k1Signature2019"));
}
#[test]
fn test_legacy_auth_header_generation_and_verification() {
let bundle = create_did_wba_document(
"example.com",
DidDocumentOptions {
path_segments: vec!["user".to_string(), "alice".to_string()],
did_profile: DidProfile::K1,
..DidDocumentOptions::default()
},
)
.expect("DID creation should succeed");
let private_key = anp::PrivateKeyMaterial::from_pem(&bundle.keys["key-1"].private_key_pem)
.expect("private key should load");
let header = generate_auth_header(
&bundle.did_document,
"api.example.com",
&private_key,
"1.1",
)
.expect("auth header generation should succeed");
verify_auth_header_signature(&header, &bundle.did_document, "api.example.com")
.expect("verification should succeed");
}
#[test]
fn test_http_signature_verification_rejects_tampered_body() {
let bundle = create_did_wba_document("example.com", DidDocumentOptions::default())
.expect("DID creation should succeed");
let private_key = anp::PrivateKeyMaterial::from_pem(&bundle.keys["key-1"].private_key_pem)
.expect("private key should load");
let headers = generate_http_signature_headers(
&bundle.did_document,
"https://api.example.com/orders",
"POST",
&private_key,
None,
Some(br#"{"item":"book"}"#),
Default::default(),
)
.expect("HTTP signature generation should succeed");
assert!(verify_http_message_signature(
&bundle.did_document,
"POST",
"https://api.example.com/orders",
&headers,
Some(br#"{"item":"book"}"#),
)
.is_ok());
assert!(verify_http_message_signature(
&bundle.did_document,
"POST",
"https://api.example.com/orders",
&headers,
Some(br#"{"item":"music"}"#),
)
.is_err());
}
#[test]
fn test_did_wba_auth_header_reads_files_and_generates_headers() {
let bundle = create_did_wba_document("example.com", DidDocumentOptions::default())
.expect("DID creation should succeed");
let temp = tempdir().expect("temp dir should exist");
let did_path = temp.path().join("did.json");
let key_path = temp.path().join("key.pem");
fs::write(&did_path, serde_json::to_vec(&bundle.did_document).unwrap()).unwrap();
fs::write(&key_path, &bundle.keys["key-1"].private_key_pem).unwrap();
let mut helper = DIDWbaAuthHeader::new(&did_path, &key_path, AuthMode::HttpSignatures);
let headers = helper
.get_auth_header("https://api.example.com/orders", false, "GET", None, None)
.expect("header generation should succeed");
assert!(headers.contains_key("Signature-Input"));
assert!(headers.contains_key("Signature"));
}
#[test]
fn test_did_wba_auth_header_reuses_server_nonce_for_challenge() {
let bundle = create_did_wba_document("example.com", DidDocumentOptions::default())
.expect("DID creation should succeed");
let temp = tempdir().expect("temp dir should exist");
let did_path = temp.path().join("did.json");
let key_path = temp.path().join("key.pem");
fs::write(&did_path, serde_json::to_vec(&bundle.did_document).unwrap()).unwrap();
fs::write(&key_path, &bundle.keys["key-1"].private_key_pem).unwrap();
let mut helper = DIDWbaAuthHeader::new(&did_path, &key_path, AuthMode::HttpSignatures);
let mut response_headers = BTreeMap::new();
response_headers.insert(
"WWW-Authenticate".to_string(),
"DIDWba realm=\"api.example.com\", error=\"invalid_nonce\", error_description=\"Retry\", nonce=\"server-nonce-123\"".to_string(),
);
response_headers.insert(
"Accept-Signature".to_string(),
"sig1=(\"@method\" \"@target-uri\" \"@authority\" \"content-digest\" \"content-type\");created;expires;nonce;keyid".to_string(),
);
let mut request_headers = BTreeMap::new();
request_headers.insert("Content-Type".to_string(), "application/json".to_string());
let headers = helper
.get_challenge_auth_header(
"https://api.example.com/orders",
&response_headers,
"POST",
Some(&request_headers),
Some(br#"{"item":"book"}"#),
)
.expect("challenge auth headers should be generated");
let metadata = extract_signature_metadata(&headers).expect("metadata should parse");
assert_eq!(metadata.nonce.as_deref(), Some("server-nonce-123"));
assert!(metadata.components.iter().any(|value| value == "content-type"));
assert!(headers.contains_key("Content-Digest"));
}
#[test]
fn test_did_wba_auth_header_should_not_retry_invalid_did() {
let bundle = create_did_wba_document("example.com", DidDocumentOptions::default())
.expect("DID creation should succeed");
let temp = tempdir().expect("temp dir should exist");
let did_path = temp.path().join("did.json");
let key_path = temp.path().join("key.pem");
fs::write(&did_path, serde_json::to_vec(&bundle.did_document).unwrap()).unwrap();
fs::write(&key_path, &bundle.keys["key-1"].private_key_pem).unwrap();
let helper = DIDWbaAuthHeader::new(&did_path, &key_path, AuthMode::HttpSignatures);
let mut response_headers = BTreeMap::new();
response_headers.insert(
"WWW-Authenticate".to_string(),
"DIDWba realm=\"api.example.com\", error=\"invalid_did\", error_description=\"Unknown DID\"".to_string(),
);
assert!(!helper.should_retry_after_401(&response_headers));
}
#[tokio::test]
async fn test_did_wba_verifier_accepts_http_signatures() {
let bundle = create_did_wba_document("example.com", DidDocumentOptions::default())
.expect("DID creation should succeed");
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/.well-known/did.json"))
.respond_with(ResponseTemplate::new(200).set_body_json(bundle.did_document.clone()))
.mount(&server)
.await;
let private_key = anp::PrivateKeyMaterial::from_pem(&bundle.keys["key-1"].private_key_pem)
.expect("private key should load");
let request_url = format!("{}/orders", server.uri());
let headers = generate_http_signature_headers(
&bundle.did_document,
&request_url,
"GET",
&private_key,
None,
None,
Default::default(),
)
.expect("HTTP signature generation should succeed");
let mut verifier = DidWbaVerifier::new(DidWbaVerifierConfig {
jwt_private_key: Some("test-secret".to_string()),
jwt_public_key: Some("test-secret".to_string()),
jwt_algorithm: "HS256".to_string(),
did_resolution_options: anp::authentication::DidResolutionOptions {
base_url_override: Some(server.uri()),
verify_ssl: false,
timeout_seconds: 5.0,
},
..DidWbaVerifierConfig::default()
});
let result = verifier
.verify_request("GET", &request_url, &headers, None, Some("api.example.com"))
.await
.expect("verification should succeed");
assert_eq!(result.auth_scheme, "http_signatures");
assert!(result.access_token.is_some());
}