use std::collections::HashMap;
use bh_jws_utils::{JwkPublic, SignatureVerifier, SigningAlgorithm};
use bh_status_list::StatusClaim;
use bherror::traits::ForeignBoxed as _;
use crate::{
models::{
data_retrieval::{
common::DocType,
device_retrieval::{
issuer_auth::ValidityInfo,
request::DeviceRequest,
response::{DeviceSigned, Document, IssuerSigned},
},
BorrowedClaims, Claims,
},
DeviceResponse,
},
DeviceKey, MdocError, Result,
};
#[derive(Debug)]
pub struct Device {
doc_type: DocType,
issuer_signed: IssuerSigned,
}
impl Device {
pub fn doc_type(&self) -> &DocType {
&self.doc_type
}
pub fn verify_issued<'a>(
issuer_signed: &str,
doc_type: DocType,
current_time: u64,
get_signature_verifier: impl Fn(SigningAlgorithm) -> Option<&'a dyn SignatureVerifier>,
) -> Result<Self> {
let issuer_signed = IssuerSigned::from_base64_url(issuer_signed)?;
issuer_signed.verify_signature(None, get_signature_verifier)?;
issuer_signed.validate_device(current_time, &doc_type)?;
Ok(Self {
doc_type,
issuer_signed,
})
}
#[allow(clippy::too_many_arguments)]
pub fn present(
&self,
current_time: u64,
request: &DeviceRequest,
client_id: &str,
response_uri: &str,
nonce: &str,
jwk_public: Option<&JwkPublic>,
signer: &impl bh_jws_utils::Signer,
) -> Result<DeviceResponse> {
self.check_device_key(signer)?;
let Some(doc_request) = request.find_by_doc_type(&self.doc_type) else {
return Ok(DeviceResponse::new(Vec::new()));
};
let issuer_signed = self
.issuer_signed
.filtered_claims(doc_request.name_spaces());
let device_name_spaces = HashMap::new().into();
let device_signed = DeviceSigned::new(
device_name_spaces,
client_id,
response_uri,
nonce,
jwk_public,
&self.doc_type,
signer,
)?;
let document = Document::new(self.doc_type.clone(), issuer_signed, device_signed);
document.validate(current_time)?;
Ok(DeviceResponse::new(vec![document]))
}
pub fn into_claims(self) -> (DocType, Claims) {
(self.doc_type, self.issuer_signed.into_claims())
}
pub fn claims(&self) -> (&DocType, BorrowedClaims<'_>) {
(&self.doc_type, self.issuer_signed.claims())
}
pub fn validity_info(&self) -> Result<ValidityInfo> {
self.issuer_signed.issuer_auth.validity_info()
}
pub fn status(&self) -> Result<Option<StatusClaim>> {
self.issuer_signed.status()
}
fn check_device_key(&self, signer: &impl bh_jws_utils::Signer) -> Result<()> {
let mut signed_device_key = self.issuer_signed.device_key()?;
signed_device_key.canonicalize();
let mut signer_device_key =
DeviceKey::from_jwk(&signer.public_jwk().foreign_boxed_err(|| {
MdocError::InvalidDeviceSigner("unable to fetch public JWK".to_owned())
})?)?;
signer_device_key.canonicalize();
if signed_device_key != signer_device_key {
return Err(bherror::Error::root(MdocError::InvalidDeviceSigner(
"public key does not match the signed one".to_owned(),
)));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use assert_matches::assert_matches;
use bh_jws_utils::Es256Verifier;
use super::*;
use crate::{
models::{
data_retrieval::device_retrieval::request::DocRequest,
mdl::{MDL_DOCUMENT_TYPE, MDL_NAMESPACE},
},
utils::test::{device_signer, issue_dummy_mdoc, issue_dummy_mdoc_to_device, issuer_signer},
MdocError, Verifier,
};
#[test]
fn test_verify_issued_success() {
let issued = issue_dummy_mdoc(100, None);
let device = Device::verify_issued(
&issued.serialize_issuer_signed().unwrap(),
MDL_DOCUMENT_TYPE.into(),
105,
|_| Some(&Es256Verifier),
)
.unwrap();
let (doc_type, claims) = device.into_claims();
let expected_claims = Claims(HashMap::from([(
MDL_NAMESPACE.into(),
HashMap::from([
("firstName".into(), "John".into()),
("lastName".into(), "Doe".into()),
]),
)]));
assert_eq!(DocType::from(MDL_DOCUMENT_TYPE), doc_type);
assert_eq!(expected_claims.0, claims.0);
}
#[test]
fn test_verify_issued_parse_fail() {
let err = Device::verify_issued("<INVALID-MDOC>", MDL_DOCUMENT_TYPE.into(), 100, |_| None)
.unwrap_err();
assert_matches!(err.error, MdocError::IssuerSignedParse);
}
#[test]
fn test_verify_issued_expired_fails() {
let issued = issue_dummy_mdoc(100, None);
let err = Device::verify_issued(
&issued.serialize_issuer_signed().unwrap(),
MDL_DOCUMENT_TYPE.into(),
100 + 400 * 24 * 60 * 60, |_| Some(&Es256Verifier),
)
.unwrap_err();
assert_matches!(err.error, MdocError::DocumentExpired(_));
}
#[test]
fn test_verify_issued_not_yet_valid_success() {
let issued = issue_dummy_mdoc(100, None);
let _device = Device::verify_issued(
&issued.serialize_issuer_signed().unwrap(),
MDL_DOCUMENT_TYPE.into(),
40, |_| Some(&Es256Verifier),
)
.unwrap();
}
#[test]
fn test_verify_issued_invalid_doc_type_fails() {
let issued = issue_dummy_mdoc(100, None);
let err = Device::verify_issued(
&issued.serialize_issuer_signed().unwrap(),
"<INVALID-DOC-TYPE>".into(),
100,
|_| Some(&Es256Verifier),
)
.unwrap_err();
assert_matches!(
err.error,
MdocError::InvalidDocType(expected, actual)
if expected == "<INVALID-DOC-TYPE>".into() && actual == MDL_DOCUMENT_TYPE.into()
);
}
#[test]
fn test_present_successful() {
let device = issue_dummy_mdoc_to_device(100, None);
let doc_request = DocRequest::builder(MDL_DOCUMENT_TYPE.into())
.add_name_space(
MDL_NAMESPACE.into(),
HashMap::from([("lastName".into(), false.into())]),
)
.build();
let request = DeviceRequest::new(vec![doc_request]);
let device_response = device
.present(
101,
&request,
"client_id",
"response_uri",
"nonce",
None,
&device_signer(),
)
.unwrap();
let documents = device_response.into_documents().unwrap();
assert_eq!(1, documents.len());
let document = documents.into_iter().next().unwrap();
let issuer_signed_claims = document.issuer_signed.into_claims().0;
let device_signed_claims = document.device_signed.into_claims().0;
let expected_claims = HashMap::from([(
MDL_NAMESPACE.into(),
HashMap::from([("lastName".into(), "Doe".into())]),
)]);
assert_eq!(expected_claims, issuer_signed_claims);
assert!(device_signed_claims.is_empty());
}
#[test]
fn test_present_with_jwk_public_successful() {
let device = issue_dummy_mdoc_to_device(100, None);
let doc_request = DocRequest::builder(MDL_DOCUMENT_TYPE.into())
.add_name_space(
MDL_NAMESPACE.into(),
HashMap::from([("lastName".into(), false.into())]),
)
.build();
let request = DeviceRequest::new(vec![doc_request]);
let jwk_public = serde_json::json!({
"kty": "EC",
"crv": "P-256",
"x": "rT9kLm3Qw7Zx2Vc8Bn5Hs1Jd4Fg0Yp6UaNe2Kv8RcXt",
"y": "Ck8Wp2Ls9Xd4Rf6Hy1Nm3Tq7Vb0Zj5UgPe4Ka9Mn2Qo",
});
let device_response = device
.present(
101,
&request,
"client_id",
"response_uri",
"nonce",
Some(jwk_public.as_object().unwrap()),
&device_signer(),
)
.unwrap();
let documents = device_response.into_documents().unwrap();
assert_eq!(1, documents.len());
let document = documents.into_iter().next().unwrap();
let issuer_signed_claims = document.issuer_signed.into_claims().0;
let device_signed_claims = document.device_signed.into_claims().0;
let expected_claims = HashMap::from([(
MDL_NAMESPACE.into(),
HashMap::from([("lastName".into(), "Doe".into())]),
)]);
assert_eq!(expected_claims, issuer_signed_claims);
assert!(device_signed_claims.is_empty());
}
#[test]
fn test_present_no_target_documents_successful() {
let device = issue_dummy_mdoc_to_device(100, None);
let request = DeviceRequest::new(Vec::new());
let device_response = device
.present(
101,
&request,
"client_id",
"response_uri",
"nonce",
None,
&device_signer(),
)
.unwrap();
let documents = device_response.into_documents();
assert!(documents.is_none());
}
#[test]
fn test_present_no_matching_documents_successful() {
let device = issue_dummy_mdoc_to_device(100, None);
let doc_request = DocRequest::builder("NON-EXISTENT DOCUMENT".into())
.add_name_space(
MDL_NAMESPACE.into(),
HashMap::from([("lastName".into(), false.into())]),
)
.build();
let request = DeviceRequest::new(vec![doc_request]);
let device_response = device
.present(
101,
&request,
"client_id",
"response_uri",
"nonce",
None,
&device_signer(),
)
.unwrap();
let documents = device_response.into_documents();
assert!(documents.is_none());
}
#[test]
fn test_present_non_existent_claims_successful() {
let device = issue_dummy_mdoc_to_device(100, None);
let doc_request = DocRequest::builder(MDL_DOCUMENT_TYPE.into())
.add_name_space(
MDL_NAMESPACE.into(),
HashMap::from([
("lastName".into(), true.into()),
("nonExistentClaim".into(), false.into()),
]),
)
.add_name_space(
"NON-EXISTENT-NAMESPACE".into(),
HashMap::from([("anotherNonExistentClaim".into(), false.into())]),
)
.build();
let request = DeviceRequest::new(vec![doc_request]);
let device_response = device
.present(
101,
&request,
"client_id",
"response_uri",
"nonce",
None,
&device_signer(),
)
.unwrap();
let documents = device_response.into_documents().unwrap();
assert_eq!(1, documents.len());
let document = documents.into_iter().next().unwrap();
let issuer_signed_claims = document.issuer_signed.into_claims().0;
let device_signed_claims = document.device_signed.into_claims().0;
let expected_claims = HashMap::from([(
MDL_NAMESPACE.into(),
HashMap::from([("lastName".into(), "Doe".into())]),
)]);
assert_eq!(expected_claims, issuer_signed_claims);
assert!(device_signed_claims.is_empty());
}
#[test]
fn test_present_no_matching_claims_successful() {
let device = issue_dummy_mdoc_to_device(100, None);
let doc_request = DocRequest::builder(MDL_DOCUMENT_TYPE.into()).build();
let request = DeviceRequest::new(vec![doc_request]);
let device_response = device
.present(
101,
&request,
"client_id",
"response_uri",
"nonce",
None,
&device_signer(),
)
.unwrap();
let documents = device_response.into_documents().unwrap();
assert_eq!(1, documents.len());
let document = documents.into_iter().next().unwrap();
let issuer_signed_claims = document.issuer_signed.into_claims();
assert!(issuer_signed_claims.0.is_empty());
let device_signed_claims = document.device_signed.into_claims().0;
assert!(device_signed_claims.is_empty());
}
#[test]
fn test_present_expired_credential_fails() {
let device = issue_dummy_mdoc_to_device(100, None);
let doc_request = DocRequest::builder(MDL_DOCUMENT_TYPE.into()).build();
let request = DeviceRequest::new(vec![doc_request]);
let err = device
.present(
100 + 400 * 24 * 60 * 60, &request,
"client_id",
"response_uri",
"nonce",
None,
&device_signer(),
)
.unwrap_err();
assert_matches!(err.error, MdocError::DocumentExpired(_));
}
#[test]
fn test_present_not_yet_valid_credential_fails() {
let device = issue_dummy_mdoc_to_device(100, None);
let doc_request = DocRequest::builder(MDL_DOCUMENT_TYPE.into()).build();
let request = DeviceRequest::new(vec![doc_request]);
let err = device
.present(
90, &request,
"client_id",
"response_uri",
"nonce",
None,
&device_signer(),
)
.unwrap_err();
assert_matches!(err.error, MdocError::DocumentNotYetValid(100));
}
#[test]
fn test_presentation_verifies_successfully() {
let device = issue_dummy_mdoc_to_device(100, None);
let verifier = Verifier::from_parts(
"client_id".to_owned(),
"response_uri".to_owned(),
"nonce".to_owned(),
);
let doc_request = DocRequest::builder(MDL_DOCUMENT_TYPE.into())
.add_name_space(
MDL_NAMESPACE.into(),
HashMap::from([("lastName".into(), false.into())]),
)
.build();
let request = DeviceRequest::new(vec![doc_request]);
let device_response = device
.present(
101,
&request,
"client_id",
"response_uri",
"nonce",
None,
&device_signer(),
)
.unwrap();
let claims = verifier
.verify(device_response, 101, None, None, |_| Some(&Es256Verifier))
.unwrap();
assert_eq!(1, claims.len());
let claims = claims.into_iter().next().unwrap().claims.0;
let expected_claims = HashMap::from([(
MDL_NAMESPACE.into(),
HashMap::from([("lastName".into(), "Doe".into())]),
)]);
assert_eq!(expected_claims, claims);
}
#[test]
fn test_present_check_device_key() {
let device = issue_dummy_mdoc_to_device(100, None);
let _device_response = device
.present(
105,
&DeviceRequest::new(vec![]),
"client_id",
"response_uri",
"nonce",
None,
&device_signer(), )
.unwrap();
let err = device
.present(
110,
&DeviceRequest::new(vec![]),
"client_id",
"response_uri",
"nonce",
None,
&issuer_signer(), )
.unwrap_err();
assert_matches!(
err.error,
MdocError::InvalidDeviceSigner(s) if s == "public key does not match the signed one"
);
}
}