bhmdoc 0.6.3

TBTL's library for handling mDL/mdoc specification.
Documentation
// Copyright (C) 2020-2026  The Blockhouse Technology Limited (TBTL).
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or (at your
// option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
// License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

use std::{collections::HashMap, str::FromStr};

use bh_jws_utils::{Es256Signer, Es256Verifier, SignerWithChain};
use bh_status_list::{StatusClaim, UriBuf};
use bhmdoc::{
    models::{
        data_retrieval::device_retrieval::issuer_auth::ValidityInfo,
        mdl::{MDLMandatory, MDL},
        DeviceRequest, DocRequest, FullDate,
    },
    Device, DeviceKey, Issuer, Verifier,
};

// Issuer certs and key generated by `generate_certs_and_keys.sh` script.
const INTERMEDIARY_KEY: &str = include_str!("certs/intermediary.key");
const INTERMEDIARY_CERT: &str = include_str!("certs/intermediary.crt");
const ROOT_CERT: &str = include_str!("certs/root.crt");

/// The document type for an _mDL_ document.
///
/// The value is currently specified in the section `7.1` of the
/// [ISO/IEC 18013-5:2021][1].
///
/// [1]: <https://www.iso.org/standard/69084.html>
const MDL_DOCUMENT_TYPE: &str = "org.iso.18013.5.1.mDL";

/// The namespace for _mDL_ data.
///
/// The value is currently specified in the section `7.1` of the
/// [ISO/IEC 18013-5:2021][1].
///
/// [1]: <https://www.iso.org/standard/69084.html>
const MDL_NAMESPACE: &str = "org.iso.18013.5.1";

/// This example shows the full flow of issuing, presenting, and verifying an
/// mDL document using the [`bhmdoc`] crate. In a real world example, the
/// [`Issuer`], [`Device`] and [`Verifier`] would be separate entities, but
/// for the sake of this example, we will use them in the same function.
fn main() {
    // We create an x5chain builder based on the generated keys and certificates.
    let x5chain_builder =
        bhx5chain::Builder::new(INTERMEDIARY_KEY, INTERMEDIARY_CERT, ROOT_CERT).unwrap();

    // We create the issuer signer from a fresh key, using the x5chain builder
    // to give it a certificate chain.
    let issuer_signer = Es256Signer::generate("issuer".to_owned()).unwrap();
    let cert_chain = x5chain_builder
        .generate_x5chain(&issuer_signer.public_key_pem().unwrap(), None)
        .unwrap();
    let issuer_signer = SignerWithChain::new(issuer_signer, cert_chain).unwrap();

    // We create a signer for the device.
    let device_signer = Es256Signer::generate("example_kid".to_string()).unwrap();

    let mdl_mandatory = MDLMandatory {
        family_name: "Doe".to_owned(),
        given_name: "John".to_owned(),
        birth_date: "1980-01-02".parse().unwrap(),
        issue_date: FullDate::from_str("2024-01-01").unwrap().into(),
        expiry_date: FullDate::from_str("2029-01-01").unwrap().into(),
        issuing_authority: "MUP".to_owned(),
        issuing_country: "RH".to_owned(),
        document_number: "1234".to_owned(),
        portrait: vec![1u8, 2, 3].into(),
        driving_privileges: 7,
        un_distinguishing_sign: "sign".to_owned(),
    };

    let mdl = MDL::new(mdl_mandatory);

    // Get the device's public key in JWK format.
    let device_jwk = device_signer.public_jwk().unwrap();

    // We create a device key from the JWK.
    let device_key = DeviceKey::from_jwk(&device_jwk).unwrap();

    // We create a hardcoded current time for the document. In a real-world
    // scenario, this should be the current time in seconds since the Unix
    // epoch.
    let current_time = 100;

    // Set the pointer to the credential's status.
    let status_list_uri: UriBuf = "https://issuer.com/status-list".parse().unwrap();
    let status_list_idx = 532;

    // We issue the document using the Issuer.
    let issued_document = Issuer
        .issue_mdl(
            mdl,
            device_key,
            &issuer_signer,
            &mut rand::thread_rng(),
            ValidityInfo::new(
                current_time.try_into().unwrap(),
                current_time.try_into().unwrap(),
                // set expiration to 1 year
                (current_time + 365 * 24 * 60 * 60).try_into().unwrap(),
                None,
            )
            .unwrap(),
            // set to None if status list should not be used
            Some(StatusClaim::new(status_list_uri.clone(), status_list_idx)),
        )
        .unwrap();

    // Serialize the issued document into base64url encoded CBOR.
    let serialized_issued_document = issued_document.serialize_issuer_signed().unwrap();

    // Simulate the passage of time by adding 10 seconds to the current time.
    // In a real-world scenario, this would be the actual current time.
    let current_time = current_time + 10;

    // We create a device based on the serialized issued document.
    let device = Device::verify_issued(
        &serialized_issued_document,
        MDL_DOCUMENT_TYPE.into(),
        current_time,
        |_alg| Some(&Es256Verifier),
    )
    .unwrap();

    // Check that Validity info exists.
    let _ = device.validity_info().expect("Validity info should exist");

    // Build the `DocRequest` that contains a collection of claims that the
    // device will present to the verifier.
    // We do not need to request all claims, only the ones we are interested in.
    let doc_request = DocRequest::builder(MDL_DOCUMENT_TYPE.into())
        .add_name_space(
            MDL_NAMESPACE.into(),
            HashMap::from([
                ("family_name".into(), false.into()),
                ("given_name".into(), false.into()),
                ("expiry_date".into(), false.into()),
            ]),
        )
        .build();

    let device_request = DeviceRequest::new(vec![doc_request]);

    let client_id = "example_client_id".to_owned();
    let response_uri = "http://example_response_uri".to_owned();

    let verifier = Verifier::new(
        client_id.clone(),
        response_uri.clone(),
        &mut rand::thread_rng(),
    );

    // Again we simulate the passage of time by adding 10 seconds to the current
    // time. In a real-world scenario, this would be the actual current time.
    let current_time = current_time + 10;

    let device_response = device
        .present(
            current_time,
            &device_request,
            &client_id,
            &response_uri,
            verifier.nonce(),
            None,
            &device_signer,
        )
        .unwrap();

    // Again we simulate the passage of time by adding 10 seconds to the current
    // time. In a real-world scenario, this would be the actual current time.
    let current_time = current_time + 10;

    // Verify the device response and extract the claims.
    let verified_claims = verifier
        .verify(
            device_response,
            current_time,
            None,
            None,
            // Currently we only support ES256.
            |_alg| Some(&Es256Verifier),
        )
        .unwrap();

    // We are verifying only one document so we expect the claims vector to
    // contain only one element.
    assert_eq!(verified_claims.len(), 1);

    // Get the claims of that one document.
    let claims = &verified_claims[0].claims.0;

    // Get the status pointer of that one document.
    let credential_status_pointer = &verified_claims[0].status;

    // We get the claims for the mDL namespace.
    let mdl_namespace_claims = claims.get(&MDL_NAMESPACE.into()).unwrap();

    // Assert that there are exactly 3 claims in the mDL namespace.
    assert_eq!(mdl_namespace_claims.len(), 3);

    // Assert that all requested claims are present
    assert!(mdl_namespace_claims.contains_key(&"family_name".into()));
    assert!(mdl_namespace_claims.contains_key(&"given_name".into()));
    assert!(mdl_namespace_claims.contains_key(&"expiry_date".into()));

    // Assert that the status pointer is as the issuer set it.
    assert_eq!(
        credential_status_pointer,
        &Some(StatusClaim::new(status_list_uri, status_list_idx))
    );

    // Print the claims.
    println!("Claim: {:?}", claims);
}