use base64::Engine as _;
use serde_json::json;
use crate::credential::{
CredentialBuilder, CredentialRole, DppCredentialSubject, VerificationResult,
authenticate_access_credential, sign_access_credential,
};
use crate::did_builder::build_did_document;
use crate::test_support::temp_store;
const KEY_ID: &str = "issuer-key";
const BASE_URL: &str = "https://authority.example";
const ISSUER_DID: &str = "did:web:authority.example";
fn credential_from(issuer: &str) -> crate::credential::DppAccessCredential {
CredentialBuilder::new(
issuer.to_owned(),
DppCredentialSubject {
id: "did:web:repairer.example".into(),
name: "Repairs GmbH".into(),
role: CredentialRole::AuthorisedRepairer,
country: "DE".into(),
product_groups: vec!["battery".into()],
product_categories: Vec::new(),
},
)
.expires_in_days(30)
.build()
}
fn issued(issuer: &str) -> (String, serde_json::Value) {
let store = temp_store("jwt", KEY_ID);
let doc = build_did_document(&store, BASE_URL, KEY_ID).expect("did document");
let jws = sign_access_credential(&credential_from(issuer), &store, KEY_ID).expect("sign");
(jws, doc)
}
#[test]
fn a_credential_signed_by_its_issuer_authenticates() {
let (jws, doc) = issued(ISSUER_DID);
let credential = authenticate_access_credential(&jws, &doc).expect("must authenticate");
assert_eq!(credential.issuer, ISSUER_DID);
assert_eq!(
credential.credential_subject.role,
CredentialRole::AuthorisedRepairer
);
assert_eq!(credential.credential_subject.id, "did:web:repairer.example");
}
#[test]
fn authentication_does_not_check_the_validity_window() {
let store = temp_store("jwt-expired", KEY_ID);
let doc = build_did_document(&store, BASE_URL, KEY_ID).expect("did document");
let expired = CredentialBuilder::new(
ISSUER_DID.to_owned(),
DppCredentialSubject {
id: "did:web:repairer.example".into(),
name: "Repairs GmbH".into(),
role: CredentialRole::AuthorisedRepairer,
country: "DE".into(),
product_groups: vec!["battery".into()],
product_categories: Vec::new(),
},
)
.expires_at(chrono::Utc::now() - chrono::Duration::days(1))
.build();
let jws = sign_access_credential(&expired, &store, KEY_ID).expect("sign");
assert!(
authenticate_access_credential(&jws, &doc).is_ok(),
"an expired credential is still authentic; expiry is the claims check's job"
);
}
#[test]
fn a_tampered_payload_is_refused() {
let (jws, doc) = issued(ISSUER_DID);
let mut parts: Vec<String> = jws.split('.').map(String::from).collect();
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
let raw = b64.decode(&parts[1]).expect("payload decodes");
let mut payload: serde_json::Value = serde_json::from_slice(&raw).expect("payload is json");
payload["credentialSubject"]["role"] = json!("recycler");
parts[1] = b64.encode(serde_json::to_vec(&payload).expect("re-encode"));
let tampered = parts.join(".");
let err = authenticate_access_credential(&tampered, &doc).expect_err("must refuse");
assert!(
matches!(err, VerificationResult::InvalidSignature(_)),
"expected InvalidSignature, got {err:?}"
);
}
#[test]
fn a_credential_checked_against_another_issuers_document_is_refused() {
let (jws, _own_doc) = issued(ISSUER_DID);
let other_store = temp_store("jwt-other", KEY_ID);
let other_doc =
build_did_document(&other_store, "https://other.example", KEY_ID).expect("did document");
let err = authenticate_access_credential(&jws, &other_doc).expect_err("must refuse");
assert!(
matches!(err, VerificationResult::InvalidSignature(_)),
"expected InvalidSignature, got {err:?}"
);
}
#[test]
fn a_document_for_a_different_did_is_refused_even_with_the_right_key() {
let store = temp_store("jwt-relabel", KEY_ID);
let mut doc = build_did_document(&store, BASE_URL, KEY_ID).expect("did document");
let jws = sign_access_credential(&credential_from(ISSUER_DID), &store, KEY_ID).expect("sign");
doc["id"] = json!("did:web:impostor.example");
let err = authenticate_access_credential(&jws, &doc).expect_err("must refuse");
match err {
VerificationResult::InvalidSignature(reason) => {
assert!(
reason.contains("impostor.example"),
"the refusal should name the mismatched document: {reason}"
);
}
other => panic!("expected InvalidSignature, got {other:?}"),
}
}
#[test]
fn an_unsigned_alg_none_token_is_refused() {
let (_jws, doc) = issued(ISSUER_DID);
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
let header = b64.encode(br#"{"alg":"none","kid":"whatever"}"#);
let payload = b64.encode(serde_json::to_vec(&credential_from(ISSUER_DID)).expect("serialise"));
let forged = format!("{header}.{payload}.");
assert!(
authenticate_access_credential(&forged, &doc).is_err(),
"alg:none must never authenticate"
);
}
#[test]
fn a_document_without_the_signing_key_is_refused() {
let (jws, _doc) = issued(ISSUER_DID);
let empty = json!({ "id": ISSUER_DID, "verificationMethod": [] });
let err = authenticate_access_credential(&jws, &empty).expect_err("must refuse");
assert!(
matches!(err, VerificationResult::InvalidSignature(_)),
"expected InvalidSignature, got {err:?}"
);
}
#[test]
fn a_payload_that_is_not_a_credential_is_refused() {
let store = temp_store("jwt-garbage", KEY_ID);
let doc = build_did_document(&store, BASE_URL, KEY_ID).expect("did document");
let jws =
dpp_crypto::jws::signer::sign(&store, KEY_ID, &json!({ "hello": "world" })).expect("sign");
let err = authenticate_access_credential(&jws, &doc).expect_err("must refuse");
assert!(
matches!(err, VerificationResult::MalformedCredential(_)),
"expected MalformedCredential, got {err:?}"
);
}
#[test]
fn malformed_input_is_refused_not_panicked_on() {
let (_jws, doc) = issued(ISSUER_DID);
for bad in ["", ".", "..", "not-a-jws", "a.b", "a.b.c.d", "!!!.???.***"] {
assert!(
authenticate_access_credential(bad, &doc).is_err(),
"{bad:?} must be refused"
);
}
}
#[test]
fn signing_is_stable_across_serialisations_of_the_same_credential() {
let store = temp_store("jwt-canon", KEY_ID);
let credential = credential_from(ISSUER_DID);
let first = sign_access_credential(&credential, &store, KEY_ID).expect("sign");
let second = sign_access_credential(&credential, &store, KEY_ID).expect("sign again");
assert_eq!(
first, second,
"the same credential must sign to the same bytes"
);
}