diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2192cd47..ab4adcd3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,6 +9,7 @@ on: env: CARGO_TERM_COLOR: always RUSTFLAGS: "-Dwarnings" + RUSTDOCFLAGS: "-Dwarnings" jobs: build: @@ -30,4 +31,12 @@ jobs: run: cargo clippy --all-targets - name: Fmt - run: cargo fmt --all -- --check \ No newline at end of file + run: cargo fmt --all -- --check + - name: Fmt + run: cargo fmt --all -- --check + + - name: Doc + run: | + cargo doc --all-features --no-deps + cd macros + cargo doc --all-features --no-deps diff --git a/docs/on_simulated_device.txt b/docs/on_simulated_device.txt new file mode 100644 index 00000000..167381e0 --- /dev/null +++ b/docs/on_simulated_device.txt @@ -0,0 +1,86 @@ + +---------+ + | | + | | + | User | + | | + | | + +---+-----+ + | + | ++----------------------------------------------------v--------------------------------------------------------------+ +| Device | +| | +| +-------------------+ | +| | | | +| |SessionManagerInit | | +| | | | +| +---------------------------------+ +-------------+ | +| | | | | | +| | | | | | +| | +-------------------+ | | +| | | | +| | qr_engagement | +| | | | +| | | | +| qr_engagement | | +| | | | +| | +----------v------------+ | +| | | | | +| | | SessionManagerEngaged <-------------+ | +| | +----------+ | | | +| | | | | | | +| | | +-----------------------+ | | +| | | | | +| | | | | +| | | | | +| | | | | +| | | | | +| | | | | +| | | | | +| | process_session_establishment | | +| | | | | +| | +--------------------------------------v----------------------------------+ | | +| | | SessionManager | | | +| | | | | | +| | | +--------------------+ | | | +| | | | | | | | +| | | +-----------------------+ AwaitingRequest <----------------+ | | | +| | | | | | | | | | +| | | | +-----------+ | | | | | +| | |prepare_response | +--------------------+ | | | | +| | | | | | | | | +| | | | | | | establish_session | +| | | | | | | | | +| | | | handle_request | | | | +| | | | | | | | | +| | | | | | | | | +| | | +--v-----------v--------+ retrieve_response | | | +| | | | +--------- | | | | +| | | | Signing | get_next_signature_payload | | | | +| | | | <--------- | | | | +| | | +---------+-------------+ | | | | +| | | | | | | | +| | | | | | | | +| | | | | | | | +| | | submit_next_signature | | | | +| | | | | | | | +| | | | +----------------------+ | | | | +| | | | | | | | | | +| | | | | ReadyToRespond | | | | | +| | | +-----------------> +-------------+ | | | +| | | | | | | | +| | | +----------+-----------+ | | | +| | | | | | | +| | +-----------------------------------------+-------------------------------+ | | +| | handle_response | | ++-------+------------------------------------------------+---------------------------------------------+------------+ + | | | + | +-------v-------+ | + | | Reader | | + | | | | + | | | | + +----------------------------------------> +-------------------------------------+ + | | + | | + | | + +---------------+ diff --git a/docs/on_simulated_reader.txt b/docs/on_simulated_reader.txt new file mode 100644 index 00000000..8120708b --- /dev/null +++ b/docs/on_simulated_reader.txt @@ -0,0 +1,29 @@ + +--------+ + | | + | User | + | | + +---+----+ + | + | + +---v-----+ + | | + | Device | + +---------+ |<----------------+ + | | | | + | | | | + | +--+--^---+ | + | | | | + | | | establish_session | + |qr_engagement | | | + | | | | new_request + | | | | ++-v--------------v--+-------------------+---+ +| Reader | +| | +| +--------------------+ handle_response | +| | +---------------+ | +| | SessionManager | | | +| | |<--------------+ | +| +--------------------+ | +| | ++-------------------------------------------+ diff --git a/docs/simulated_device_and_reader.txt b/docs/simulated_device_and_reader.txt new file mode 100644 index 00000000..5b0f3369 --- /dev/null +++ b/docs/simulated_device_and_reader.txt @@ -0,0 +1,45 @@ + +---------------------+ +----------------------+ + | | | | + | | | | + | Device | | Reader | + | | | | + | | | | + +---------+-----------+ +----------+-----------+ + | | + Initialize session | + | | + | | +Create QR code engagement | + | | + +-------------+ | + | | | + | | | + <-------------+ | + | | + | Send QR code | + +-----------------------------------------------------------------> + | | + | | Establish session + | +-----------+ + | | | + | | | + | +-----------+ + | Request age_over_21 | + <-----------------------------------------------------------------+ + | | + | | + | | + | Send age_over_21 | + +-----------------------------------------------------------------> + | | + | | + | | Process age_over_21 + | +-----------+ + | | | + | | | + | +-----------+ + | | + | | + | | + | Session finished | + | | diff --git a/src/definitions/device_engagement.rs b/src/definitions/device_engagement.rs index 5f7bf7c8..c8a713d1 100644 --- a/src/definitions/device_engagement.rs +++ b/src/definitions/device_engagement.rs @@ -1,3 +1,9 @@ +//! This module contains the definitions for the [DeviceEngagement] struct and related types. +//! +//! The [DeviceEngagement] struct represents a device engagement object, which contains information about a device's engagement with a server. +//! It includes fields such as the `version`, `security details, `device retrieval methods, `server retrieval methods, and `protocol information. +//! +//! The module also provides implementations for conversions between [DeviceEngagement] and [CborValue], as well as other utility functions. use crate::definitions::helpers::Tag24; use crate::definitions::helpers::{ByteStr, NonEmptyVec}; use crate::definitions::CoseKey; @@ -21,15 +27,25 @@ pub type ProtocolInfo = CborValue; pub type Oidc = (u64, String, String); pub type WebApi = (u64, String, String); +/// Represents a device engagement. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(try_from = "CborValue", into = "CborValue", rename_all = "camelCase")] pub struct DeviceEngagement { + /// The version of the device engagement. pub version: String, + + /// The security settings for the device engagement. pub security: Security, + + /// The optional device retrieval methods for the device engagement. #[serde(skip_serializing_if = "Option::is_none")] pub device_retrieval_methods: Option, + + /// The optional server retrieval methods for the device engagement. #[serde(skip_serializing_if = "Option::is_none")] pub server_retrieval_methods: Option, + + /// The optional protocol information for the device engagement. #[serde(skip_serializing_if = "Option::is_none")] pub protocol_info: Option, } @@ -37,52 +53,82 @@ pub struct DeviceEngagement { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(try_from = "CborValue", into = "CborValue")] pub enum DeviceRetrievalMethod { + /// Represents the options for a WiFi connection. WIFI(WifiOptions), + + /// Represents the BLE options for device engagement. + /// + /// This struct is used to configure the BLE options for device engagement. + /// It contains the necessary parameters and settings for BLE communication. BLE(BleOptions), + + /// Represents the options for NFC engagement. NFC(NfcOptions), } +/// Represents the bytes of an EDevice key. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Security(pub u64, pub EDeviceKeyBytes); +/// Represents the server retrieval methods for device engagement. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ServerRetrievalMethods { + /// The `web API retrieval method. This field is optional and will be skipped during serialization if it is [None]. #[serde(skip_serializing_if = "Option::is_none")] web_api: Option, + + /// The `OIDC`` retrieval method. This field is optional and will be skipped during serialization if it is [None]. #[serde(skip_serializing_if = "Option::is_none")] oidc: Option, } +/// Represents the options for `Bluetooth Low Energy` (BLE) device engagement. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(try_from = "CborValue", into = "CborValue")] pub struct BleOptions { + /// The peripheral server mode for `BLE` device engagement. #[serde(skip_serializing_if = "Option::is_none")] pub peripheral_server_mode: Option, + + /// The central client mode for `BLE` device engagement. #[serde(skip_serializing_if = "Option::is_none")] pub central_client_mode: Option, } +/// Represents a peripheral server mode. #[derive(Clone, Debug, PartialEq, Eq)] pub struct PeripheralServerMode { + /// The 'UUID' of the peripheral server. pub uuid: Uuid, + + /// The 'BLE' device address of the peripheral server, if available. pub ble_device_address: Option, } +/// Represents the central client mode for device engagement. #[derive(Clone, Debug, PartialEq, Eq)] pub struct CentralClientMode { pub uuid: Uuid, } +/// Represents the options for a `WiFi` device engagement. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)] #[serde(try_from = "CborValue", into = "CborValue")] pub struct WifiOptions { + /// The passphrase for the `WiFi connection. If [None], no passphrase is required. #[serde(skip_serializing_if = "Option::is_none")] pass_phrase: Option, + + /// The operating class of the `WiFi` channel. If [None], the operating class is not specified. #[serde(skip_serializing_if = "Option::is_none")] channel_info_operating_class: Option, + + /// The channel number of the `WiFi` channel. If [None], the channel number is not specified. #[serde(skip_serializing_if = "Option::is_none")] channel_info_channel_number: Option, + + /// The band information of the `WiFi channel. If [None], the band information is not specified. #[serde(skip_serializing_if = "Option::is_none")] band_info: Option, } diff --git a/src/definitions/device_key/cose_key.rs b/src/definitions/device_key/cose_key.rs index 4fe1a62e..1e796ace 100644 --- a/src/definitions/device_key/cose_key.rs +++ b/src/definitions/device_key/cose_key.rs @@ -1,3 +1,27 @@ +//! An implementation of `RFC-8152` `COSE_Key` restricted to the requirements of `ISO/IEC 18013-5:2021`. +//! +//! This module provides the [CoseKey] enum, which represents a `COSE_Key` object as defined in `RFC-8152`. +//! It supports two key types: `EC2 (Elliptic Curve)` and `OKP (Octet Key Pair). +//! +//! # Examples +//! +//! ```ignore +//! use ssi_jwk::JWK; +//! use std::convert::TryInto; +//! use crate::CoseKey; +//! +//! let jwk: JWK = /* ... */; +//! let cose_key: Result = jwk.try_into(); +//! +//! match cose_key { +//! Ok(key) => { +//! // Perform operations with the COSE_Key +//! } +//! Err(err) => { +//! // Handle the error +//! } +//! } +//! ``` use aes::cipher::generic_array::{typenum::U8, GenericArray}; use cose_rs::algorithm::Algorithm; use p256::EncodedPoint; @@ -64,6 +88,7 @@ pub enum Error { } impl CoseKey { + /// Returns the signature algorithm associated with the key. pub fn signature_algorithm(&self) -> Option { match self { CoseKey::EC2 { diff --git a/src/definitions/device_key/mod.rs b/src/definitions/device_key/mod.rs index 750615c0..af08bca0 100644 --- a/src/definitions/device_key/mod.rs +++ b/src/definitions/device_key/mod.rs @@ -1,3 +1,46 @@ +//! This module contains definitions related to device keys. +//! +//! The [DeviceKeyInfo] struct represents information about a device key, including the key itself, +//! key authorizations, and additional key info. +//! +//! The [KeyAuthorizations] struct represents the authorizations for a device key, including +//! namespaces and data elements. +//! +//! # Examples +//! +//! ```ignore +//! use crate::definitions::device_key::{DeviceKeyInfo, KeyAuthorizations}; +//! +//! let key_info = DeviceKeyInfo { +//! device_key: /* initialize device key */, +//! key_authorizations: Some(KeyAuthorizations { +//! namespaces: Some(vec!["namespace1".to_string(), "namespace2".to_string()]), +//! data_elements: None, +//! }), +//! key_info: None, +//! }; +//! ``` +//! +//! # Errors +//! +//! The [Error] enum represents the possible errors that can occur when validating key authorizations. +//! +//! - [Error::DoubleAuthorized] indicates that a namespace is present in both `authorized_namespaces` and `authorized_data_elements`. +//! +//! # Examples +//! +//! ```ignore +//! use crate::definitions::device_key::{KeyAuthorizations, Error}; +//! +//! let key_auth = KeyAuthorizations { +//! namespaces: Some(vec!["namespace1".to_string(), "namespace2".to_string()]), +//! data_elements: Some(Default::default()), +//! }; +//! +//! let result = key_auth.validate(); +//! assert!(result.is_err()); +//! assert_eq!(result.unwrap_err(), Error::DoubleAuthorized("namespace1".to_string())); +//! ``` use crate::definitions::helpers::{NonEmptyMap, NonEmptyVec}; use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; @@ -7,12 +50,18 @@ pub mod cose_key; pub use cose_key::CoseKey; pub use cose_key::EC2Curve; +/// Represents information about a device key. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DeviceKeyInfo { + /// The device key. pub device_key: CoseKey, + + /// Optional key authorizations. #[serde(skip_serializing_if = "Option::is_none")] pub key_authorizations: Option, + + /// Optional key information. #[serde(skip_serializing_if = "Option::is_none")] pub key_info: Option>, } @@ -20,14 +69,19 @@ pub struct DeviceKeyInfo { #[derive(Clone, Serialize, Deserialize, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct KeyAuthorizations { + /// The namespaces associated with the key. This field is optional and will + /// be skipped during serialization if it is [None]. #[serde(skip_serializing_if = "Option::is_none", rename = "nameSpaces")] pub namespaces: Option>, + + /// The data elements associated with the key. This field is optional and will + /// be skipped during serialization if it is [None]. #[serde(skip_serializing_if = "Option::is_none")] pub data_elements: Option>>, } impl KeyAuthorizations { - /// If a namespace is present in authorized namespaces then it cannot be present in + /// If a namespace is present in authorized namespaces, then it cannot be present in /// authorized data elements. pub fn validate(&self) -> Result<(), Error> { let authorized_data_elements: &NonEmptyMap>; diff --git a/src/definitions/device_request.rs b/src/definitions/device_request.rs index ced1cbe7..c3e4a97f 100644 --- a/src/definitions/device_request.rs +++ b/src/definitions/device_request.rs @@ -1,3 +1,4 @@ +//! This module contains the definitions for the device request functionality. use crate::definitions::helpers::{NonEmptyMap, NonEmptyVec, Tag24}; use cose_rs::CoseSign1; use serde::{Deserialize, Serialize}; @@ -12,27 +13,42 @@ pub type DataElements = NonEmptyMap; pub type Namespaces = NonEmptyMap; pub type ReaderAuth = CoseSign1; +/// Represents a device request. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] + pub struct DeviceRequest { + /// The version of the device request. pub version: String, + + /// A non-empty vector of document requests. pub doc_requests: NonEmptyVec, } +/// Represents a document request. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DocRequest { + /// The items request for the document. pub items_request: ItemsRequestBytes, + + /// The reader authentication, if provided. #[serde(skip_serializing_if = "Option::is_none")] pub reader_auth: Option, } +/// Represents a request for items. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ItemsRequest { + /// The type of document. pub doc_type: DocType, + + /// The namespaces associated with the request. #[serde(rename = "nameSpaces")] pub namespaces: Namespaces, + + /// Additional information for the request. #[serde(skip_serializing_if = "Option::is_none")] pub request_info: Option>, } diff --git a/src/definitions/device_response.rs b/src/definitions/device_response.rs index 036059d0..9c1091d2 100644 --- a/src/definitions/device_response.rs +++ b/src/definitions/device_response.rs @@ -1,3 +1,4 @@ +//! This module contains the definition of the `DeviceResponse` struct and related types. use crate::definitions::{ helpers::{NonEmptyMap, NonEmptyVec}, DeviceSigned, IssuerSigned, @@ -5,33 +6,55 @@ use crate::definitions::{ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; +/// Represents a device response. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct DeviceResponse { + /// The version of the response. pub version: String, + + /// The documents associated with the response, if any. #[serde(skip_serializing_if = "Option::is_none")] pub documents: Option, + + /// The errors associated with the documents, if any. #[serde(skip_serializing_if = "Option::is_none")] pub document_errors: Option, + + /// The status of the response. pub status: Status, } pub type Documents = NonEmptyVec; +/// Represents a document. +/// +/// This struct is used to store information about a document. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Document { + /// A string representing the type of the document. pub doc_type: String, + + /// An instance of the [IssuerSigned] struct representing the issuer-signed data. pub issuer_signed: IssuerSigned, + + /// An instance of the [DeviceSigned] struct representing the device-signed data. pub device_signed: DeviceSigned, + + /// An optional instance of the [Errors] struct representing any errors associated with the document. #[serde(skip_serializing_if = "Option::is_none")] pub errors: Option, } +/// Errors mapped by namespace and element identifier. pub type Errors = NonEmptyMap>; +/// A list of document errors. pub type DocumentErrors = NonEmptyVec; +/// A map of document type to document error for them. pub type DocumentError = BTreeMap; +/// Document specific errors. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(try_from = "i128", into = "i128")] pub enum DocumentErrorCode { diff --git a/src/definitions/device_signed.rs b/src/definitions/device_signed.rs index 05a4716b..438ae6c1 100644 --- a/src/definitions/device_signed.rs +++ b/src/definitions/device_signed.rs @@ -1,3 +1,9 @@ +//! This module contains the definitions related to device signing. +//! +//! The [DeviceSigned] struct represents a device signed object, which includes namespaces and device authentication information. +//! +//! The [Error] enum represents the possible errors that can occur in this module. +//! - [Error::UnableToEncode]: Indicates an error when encoding a value as CBOR. use crate::definitions::{ helpers::{NonEmptyMap, Tag24}, session::SessionTranscript, @@ -7,11 +13,15 @@ use serde::{Deserialize, Serialize}; use serde_cbor::{Error as CborError, Value as CborValue}; use std::collections::BTreeMap; +/// Represents a device-signed structure. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct DeviceSigned { #[serde(rename = "nameSpaces")] + /// A [DeviceNamespacesBytes] struct representing the namespaces. pub namespaces: DeviceNamespacesBytes, + + /// A [DeviceAuth] struct representing the device authentication. pub device_auth: DeviceAuth, } @@ -19,6 +29,10 @@ pub type DeviceNamespacesBytes = Tag24; pub type DeviceNamespaces = BTreeMap; pub type DeviceSignedItems = NonEmptyMap; +/// Represents a device signature. +/// +/// This struct contains the device signature in the form of a [CoseSign1] object. +/// The [CoseSign1] object represents a `COSE (CBOR Object Signing and Encryption) signature. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum DeviceAuth { diff --git a/src/definitions/issuer_signed.rs b/src/definitions/issuer_signed.rs index d9848bf7..eae4a8f9 100644 --- a/src/definitions/issuer_signed.rs +++ b/src/definitions/issuer_signed.rs @@ -1,3 +1,14 @@ +//! This module contains the definition of the [IssuerSigned] struct and related types. +//! +//! The [IssuerSigned] struct represents a signed issuer object, which includes information about `namespaces`, `authentication`, and `signed items`. +//! +//! # Notes +//! +//! - [IssuerSigned] struct is serialized and deserialized using the [Serialize] and [Deserialize] traits from the [serde] crate. +//! - [IssuerNamespaces] type is an alias for [`NonEmptyMap>`]. +//! - [IssuerSignedItemBytes] type is an alias for [`Tag24`]. +//! - [IssuerSignedItem] struct represents a signed item within the [IssuerSigned] object, including information such as digest ID, random bytes, element identifier, and element value. +//! - [IssuerSigned] struct also includes a test module with a unit test for serialization and deserialization. use crate::definitions::{ helpers::{ByteStr, NonEmptyMap, NonEmptyVec, Tag24}, DigestId, @@ -6,6 +17,11 @@ use cose_rs::sign1::CoseSign1; use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; +/// Represents an issuer-signed object. +/// +/// This struct is used to store information about an issuer-signed object, which includes namespaces and issuer authentication. +/// [IssuerSigned::namespaces] field is an optional [IssuerNamespaces] object that contains namespaces associated with the issuer. +/// [IssuerSigned::issuer_auth] field is a [CoseSign1] object that represents the issuer authentication. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct IssuerSigned { @@ -17,13 +33,21 @@ pub struct IssuerSigned { pub type IssuerNamespaces = NonEmptyMap>; pub type IssuerSignedItemBytes = Tag24; +/// Represents an item signed by the issuer. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct IssuerSignedItem { + /// The ID of the digest used for signing. #[serde(rename = "digestID")] pub digest_id: DigestId, + + /// Random bytes associated with the signed item. pub random: ByteStr, + + /// The identifier of the element. pub element_identifier: String, + + /// The value of the element. pub element_value: CborValue, } diff --git a/src/definitions/mod.rs b/src/definitions/mod.rs index f146dccf..387ee320 100644 --- a/src/definitions/mod.rs +++ b/src/definitions/mod.rs @@ -1,3 +1,6 @@ +//! This module contains the definitions for various components related to device engagement, +//! device keys, device requests, device responses, device signing, helpers, issuer signing, +//! MSO (Mobile Security Object), namespaces, session management, traits, and validity information. pub mod device_engagement; pub mod device_key; pub mod device_request; diff --git a/src/definitions/mso.rs b/src/definitions/mso.rs index d3a91a36..01d0b765 100644 --- a/src/definitions/mso.rs +++ b/src/definitions/mso.rs @@ -1,22 +1,67 @@ +//! This module contains the definitions for the `MSO` (Mobile Security Object) structure. +//! +//! The `MSO structure represents a mobile security object, which is used in cryptographic operations +//! within the system. It contains information such as the version, digest algorithm, value digests, +//! device key info, document type, and validity info. +//! +//! # Examples +//! +//! ```ignore +//! use spruceid::definitions::mso::{Mso, DigestAlgorithm}; +//! use std::collections::BTreeMap; +//! +//! // Create a new MSO object +//! let mso = Mso { +//! version: String::from("1.0"), +//! digest_algorithm: DigestAlgorithm::SHA256, +//! value_digests: BTreeMap::new(), +//! device_key_info: Default::default(), +//! doc_type: String::from("document"), +//! validity_info: Default::default(), +//! }; +//! +//! // Print the MSO object +//! println!("{:?}", mso); +//! ``` +//! +//! # Notes +//! +//! - [DigestId] struct represents an unsigned integer between `0` and `(2^31 - 1)` inclusive. +//! It is enforced to be positive. +//! - [DigestIds] type is a [BTreeMap] that maps [DigestId] to [ByteStr]. +//! - [DigestAlgorithm] enum represents different digest algorithms, such as `SHA-256, `SHA-384, +//! and `SHA-512`. use crate::definitions::{helpers::ByteStr, DeviceKeyInfo, ValidityInfo}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; -/// DigestId is a unsigned integer between 0 and (2^31 - 1) inclusive. +/// DigestId is a unsigned integer between `0` and `(2^31 - 1)` inclusive. /// Therefore the most straightforward way to represent it is as a i32 that is enforced to be /// positive. #[derive(Clone, Debug, Serialize, Deserialize, Eq, Ord, PartialEq, PartialOrd, Copy, Hash)] pub struct DigestId(i32); pub type DigestIds = BTreeMap; +/// Represents an [Mso] object. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Mso { + /// The version of the Mso object. pub version: String, + + /// The digest algorithm used by the Mso object. pub digest_algorithm: DigestAlgorithm, + + /// A map of value digests associated with their respective digest IDs. pub value_digests: BTreeMap, + + /// Information about the device key used by the Mso object. pub device_key_info: DeviceKeyInfo, + + /// The document type associated with the Mso object. pub doc_type: String, + + /// Information about the validity of the Mso object. pub validity_info: ValidityInfo, } diff --git a/src/definitions/session.rs b/src/definitions/session.rs index 7502826f..a8b51711 100644 --- a/src/definitions/session.rs +++ b/src/definitions/session.rs @@ -1,3 +1,7 @@ +//! This module contains the definitions and functions related to session establishment and management. +//! +//! The [get_initialization_vector] function generates an initialization vector for encryption/decryption +//! based on a message count and a flag indicating whether the vector is for the reader or the device. use super::helpers::Tag24; use super::DeviceEngagement; use crate::definitions::device_engagement::EReaderKeyBytes; @@ -32,17 +36,27 @@ pub type DeviceEngagementBytes = Tag24; pub type SessionTranscriptBytes = Tag24; pub type NfcHandover = (ByteStr, Option); +/// Represents the establishment of a session. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionEstablishment { + /// The EReader key used for session establishment. pub e_reader_key: EReaderKeyBytes, + + /// The data associated with the session establishment. pub data: ByteStr, } +/// Represents session data. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionData { + /// An optional [ByteStr] that represents the data associated with the session. + /// The field is skipped during serialization if it is [None]. #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, + + /// An optional [Status] that represents the status of the session. + /// Similarly, the field is skipped during serialization if it is [None]. #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, } @@ -80,6 +94,7 @@ impl TryFrom for Status { pub trait SessionTranscript: Serialize + for<'a> Deserialize<'a> {} +/// Represents the device engagement bytes. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionTranscript180135( pub DeviceEngagementBytes, @@ -110,17 +125,30 @@ pub enum Handover { } pub enum EphemeralSecrets { + /// Represents an Eph256 session. + /// This enum variant holds an `EphemeralSecret` of type `NistP256`. Eph256(EphemeralSecret), + + /// Represents an ephemeral secret using the NIST P-384 elliptic curve. Eph384(EphemeralSecret), } pub enum EncodedPoints { + /// Represents a session with an Ep256 encoded point. Ep256(EncodedPoint), + + /// Represents an Ep384 session. + /// This struct holds an encoded point of type `EncodedPoint`. Ep384(EncodedPoint), } pub enum SharedSecrets { + /// Represents a session with a shared secret using the `SS256` algorithm. + /// The shared secret is generated using the `NistP256` elliptic curve. Ss256(SharedSecret), + + /// Represents a session with a shared secret using the `Ss384` algorithm. + /// The shared secret is of type [`SharedSecret`]. Ss384(SharedSecret), } diff --git a/src/definitions/validity_info.rs b/src/definitions/validity_info.rs index 1eae51c7..e13f8c09 100644 --- a/src/definitions/validity_info.rs +++ b/src/definitions/validity_info.rs @@ -1,3 +1,32 @@ +//! This module contains the definition of the [ValidityInfo] struct and related error types. +//! +//! The [ValidityInfo] struct represents information about the validity of a certain entity. +//! It contains fields such as `signed`, `valid_from`, `valid_until`, and `expected_update`. +//! +//! # Errors +//! +//! The [Error] enum represents various errors that can occur when working with [ValidityInfo] objects. +//! +//! # Serialization and Deserialization +//! +//! The [ValidityInfo] struct implements the [Serialize] and [Deserialize] traits from the [serde] crate, +//! allowing it to be easily serialized and deserialized to and from CBOR format. +//! +//! # Conversion to and from CBOR +//! +//! The [ValidityInfo] struct also provides implementations of the [TryFrom] trait for converting +//! to and from [CborValue], which is a type provided by the [serde_cbor] crate for representing CBOR values. +//! These implementations allow you to convert [ValidityInfo] objects to `CBOR` format and vice versa. +//! +//! # Dependencies +//! +//! This module depends on the following external crates: +//! +//! - [serde]: Provides the serialization and deserialization traits and macros. +//! - [serde_cbor]: Provides the `CBOR` serialization and deserialization functionality. +//! - [std::collections::BTreeMap]: Provides the [BTreeMap] type for storing key-value pairs in a sorted order. +//! - [time]: Provides date and time manipulation functionality. +//! - [thiserror]: Provides the [thiserror::Error] trait for defining custom error types. use serde::{ ser::{Error as SerError, Serializer}, Deserialize, Serialize, diff --git a/src/issuance/mod.rs b/src/issuance/mod.rs index 27129fdc..263b8012 100644 --- a/src/issuance/mod.rs +++ b/src/issuance/mod.rs @@ -1,3 +1,6 @@ +//! This module contains the implementation of the `issuance` module. +//! +//! The `issuance` module provides functionality for handling issuance related operations. pub mod mdoc; pub mod x5chain; diff --git a/src/issuance/x5chain.rs b/src/issuance/x5chain.rs index 898ba765..655aeb73 100644 --- a/src/issuance/x5chain.rs +++ b/src/issuance/x5chain.rs @@ -1,3 +1,62 @@ +//! This module provides functionality for working with `X.509`` certificate chains. +//! +//! The [X5Chain] struct represents a chain of `X.509`` certificates. It can be built using +//! the [Builder] struct, which allows adding certificates in either `PEM`` or `DER`` format. +//! The resulting [X5Chain] can be converted to `CBOR`` format using the [X5Chain::into_cbor] method. +//! +//! # Examples +//! +//! ```ignore +//! use crate::isomdl::issuance::x5chain::{X5Chain, Builder}; +//! +//! // Create an X5Chain using the Builder +//! let pem_data = include_bytes!("../../test/issuance/256-cert.pem"); +//! let x5chain = X5Chain::builder() +//! .with_pem(&pem_data) +//! .expect("Failed to add certificate") +//! .build() +//! .expect("Failed to build X5Chain"); +//! +//! // Convert the X5Chain to CBOR format +//! let cbor_value = x5chain.into_cbor(); +//! ``` +//! +//! The [Builder] struct provides methods for adding certificates to the chain. Certificates can be added +//! either from PEM or DER data, or from files containing PEM or DER data. +//! +//! # Examples +//! +//! ```ignore +//! use std::fs::File; +//! use crate::isomdl::issuance::x5chain::Builder; +//! +//! // Create a Builder and add a certificate from PEM data +//! let pem_data = include_bytes!("../../test/issuance/256-cert.pem"); +//! let builder = Builder::default() +//! .with_pem(pem_data) +//! .expect("Failed to add certificate"); +//! +//! // Add a certificate from DER data +//! let der_data = include_bytes!("../../test/issuance/256-cert.der"); +//! let builder = builder.with_der(der_data) +//! .expect("Failed to add certificate"); +//! +//! // Add a certificate from a PEM file +//! let pem_file = File::open("256-cert.pem").unwrap(); +//! let builder = builder.with_pem_from_file(pem_file) +//! .expect("Failed to add certificate"); +//! +//! // Add a certificate from a DER file +//! let der_file = File::open("256-cert.der").unwrap(); +//! let builder = builder.with_der_from_file(der_file) +//! .expect("Failed to add certificate"); +//! +//! // Build the X5Chain +//! let x5chain = builder.build() +//! .expect("Failed to build X5Chain"); +//! ``` +//! +//! The [X5Chain] struct also provides a [X5Chain::builder] method for creating a new [Builder] instance. use crate::definitions::helpers::NonEmptyVec; use anyhow::{anyhow, Result}; use serde_cbor::Value as CborValue; @@ -9,11 +68,13 @@ use x509_cert::{ pub const X5CHAIN_HEADER_LABEL: i128 = 33; +/// Represents an X509 certificate. #[derive(Debug, Clone)] pub struct X509 { bytes: Vec, } +/// Represents a chain of [X509] certificates. #[derive(Debug, Clone)] pub struct X5Chain(NonEmptyVec); @@ -23,11 +84,16 @@ impl From> for X5Chain { } } +/// Implements the [X5Chain] struct. +/// +/// This struct provides methods for building and converting the X5Chain object. impl X5Chain { + /// Creates a new [Builder] instance for [X5Chain]. pub fn builder() -> Builder { Builder::default() } + /// Converts the [X5Chain] object into a [CborValue]. pub fn into_cbor(&self) -> CborValue { match &self.0.as_ref() { &[cert] => CborValue::Bytes(cert.bytes.clone()), @@ -43,12 +109,30 @@ impl X5Chain { } } +/// Builder for creating an [X5Chain]. +/// +/// This struct is used to build an [X5Chain] by providing a vector of [X509] certificates. +/// The [X5Chain] represents a chain of `X.509`` certificates used for issuance. +/// +/// # Note +/// +/// The `Builder` struct is typically used in the context of the `issuance` module. #[derive(Default, Debug, Clone)] pub struct Builder { certs: Vec, } impl Builder { + /// Adds a `PEM-encoded`` certificate to the builder. + /// + /// # Errors + /// + /// Returns an error if the `PEM`` cannot be parsed or the certificate + /// cannot be converted to bytes. + /// + /// # Returns + /// + /// Returns a [Result] containing the updated [Builder] if successful. pub fn with_pem(mut self, data: &[u8]) -> Result { let bytes = pem_rfc7468::decode_vec(data) .map_err(|e| anyhow!("unable to parse pem: {}", e))? @@ -63,6 +147,17 @@ impl Builder { self.certs.push(x509); Ok(self) } + + /// Adds a `DER`-encoded certificate to the builder. + /// + /// # Errors + /// + /// Returns an error if the certificate cannot be parsed from `DER` encoding + /// or cannot be converted to bytes. + /// + /// # Returns + /// + /// Returns a [Result] containing the updated [Builder] if successful. pub fn with_der(mut self, data: &[u8]) -> Result { let cert: Certificate = Certificate::from_der(data) .map_err(|e| anyhow!("unable to parse certificate from der encoding: {}", e))?; @@ -74,16 +169,46 @@ impl Builder { self.certs.push(x509); Ok(self) } + + /// Adds a `PEM`-encoded certificate from a file to the builder. + /// + /// # Errors + /// + /// Returns an error if the file cannot be read or the certificate cannot be parsed or converted to bytes. + /// + /// # Returns + /// + /// Returns a [Result] containing the updated [Builder] if successful. pub fn with_pem_from_file(self, mut f: File) -> Result { let mut data: Vec = vec![]; f.read_to_end(&mut data)?; self.with_pem(&data) } + + /// Adds a `DER`-encoded certificate from a file to the builder. + /// + /// # Errors + /// + /// Returns an error if the file cannot be read or the certificate cannot be parsed or converted to bytes. + /// + /// # Returns + /// + /// Returns a [Result] containing the updated [Builder] if successful. pub fn with_der_from_file(self, mut f: File) -> Result { let mut data: Vec = vec![]; f.read_to_end(&mut data)?; self.with_der(&data) } + + /// Builds the [X5Chain] from the added certificates. + /// + /// # Errors + /// + /// Returns an error if at least one certificate is not added to the builder. + /// + /// # Returns + /// + /// Returns a [Result] containing the built [X5Chain] if successful. pub fn build(self) -> Result { Ok(X5Chain(self.certs.try_into().map_err(|_| { anyhow!("at least one certificate must be given to the builder") diff --git a/src/presentation/device.rs b/src/presentation/device.rs index c900fdfd..1dce6063 100644 --- a/src/presentation/device.rs +++ b/src/presentation/device.rs @@ -1,3 +1,21 @@ +//! This module is responsible for establishing the device's session with the reader. +//! +//! The device's [SessionManager] state machine is responsible +//! for handling the session with the reader. +//! +//! The session is managed through a set of session management states: initialization, engaged, +//! and established. +//! +//! To initialize a session management state, see the [SessionManagerInit] struct. +//! +//! ```ignore +#![doc = include_str!("../../docs/on_simulated_device.txt")] +//! ``` +//! +//! ### Example +//! +//! You can view examples in `tests` directory in `simulated_device_and_reader.rs`, for a basic example and +//! `simulated_device_and_reader_state.rs` which uses `State` pattern, `Arc` and `Mutex`. use crate::definitions::IssuerSignedItem; use crate::{ definitions::{ @@ -26,6 +44,18 @@ use std::collections::BTreeMap; use std::num::ParseIntError; use uuid::Uuid; +/// Initialisation state. +/// +/// You enter this state using [SessionManagerInit::initialise] method, providing +/// the documents and optional non-empty list of device [DeviceRetrievalMethod] and +/// server [ServerRetrievalMethods] retrieval methods. +/// +/// The [SessionManagerInit] state is restricted to creating a QR-code engagement, +/// using the [SessionManagerInit::qr_engagement] method, which will return the +/// [SessionManagerEngaged] Session Manager state. +/// +/// For convenience, the [SessionManagerInit] state surfaces the [SessionManagerInit::ble_ident] method +/// to provide the BLE identification string for the device. #[derive(Serialize, Deserialize)] pub struct SessionManagerInit { documents: Documents, @@ -33,6 +63,10 @@ pub struct SessionManagerInit { device_engagement: Tag24, } +/// Engaged state. +/// +/// Transition to this state is made with [SessionManagerInit::qr_engagement]. +/// That creates the `QR code` that the reader will use to establish the session. #[derive(Clone, Serialize, Deserialize)] pub struct SessionManagerEngaged { documents: Documents, @@ -41,6 +75,20 @@ pub struct SessionManagerEngaged { handover: Handover, } +/// The initial state of the Session Manager. +/// +/// The Session Manager contains the documents, ephemeral device key, and device engagement. +/// +/// Create a new Session Manager using the [SessionManagerInit::initialise] method, providing +/// the documents and optional non-empty list of device [DeviceRetrievalMethod] and +/// server [ServerRetrievalMethods] retrieval methods. +/// +/// The [SessionManagerInit] state is restricted to creating a QR-code engagement, +/// using the [SessionManagerInit::qr_engagement] method, which will return the +/// [SessionManagerEngaged] Session Manager state. +/// +/// For convience, the [SessionManagerInit] state surfaces the [SessionManagerInit::ble_ident] method +/// to provide the BLE identification string for the device. #[derive(Clone, Serialize, Deserialize)] pub struct SessionManager { documents: Documents, @@ -52,32 +100,45 @@ pub struct SessionManager { state: State, } +/// The internal states of the [SessionManager]. #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub enum State { + /// This is the default one where the device is waiting for a request from the reader. #[default] AwaitingRequest, + /// The device is signing the response. The response could be a document or an error. Signing(PreparedDeviceResponse), + /// The device is ready to respond to the reader with a signed response. ReadyToRespond(Vec), } +/// Various errors that can occur during the interaction with the reader. #[derive(Debug, thiserror::Error)] pub enum Error { + /// Unable to generate ephemeral key. #[error("unable to generate ephemeral key: {0}")] EKeyGeneration(session::Error), + /// Error encoding value to CBOR. #[error("error encoding value to CBOR: {0}")] Tag24CborEncoding(tag24::Error), + /// Unable to generate shared secret. #[error("unable to generate shared secret: {0}")] SharedSecretGeneration(anyhow::Error), + /// Error encoding value to CBOR. #[error("error encoding value to CBOR: {0}")] CborEncoding(serde_cbor::Error), + /// Session manager was used incorrectly. #[error("session manager was used incorrectly")] ApiMisuse, + /// Could not parse age attestation claim. #[error("could not parse age attestation claim")] ParsingError(#[from] ParseIntError), + /// `age_over` element identifier is malformed. #[error("age_over element identifier is malformed")] PrefixError, } +/// The documents the device owns. pub type Documents = NonEmptyMap; type DocType = String; @@ -90,6 +151,15 @@ pub struct Document { pub namespaces: Namespaces, } +/// Stores the prepared response. +/// +/// After the device parses the request from the reader, +/// If there were errors, +/// it will prepare a list of [DocumentErrors]. +/// If there are documents to be signed, +/// it will keep a list of prepared documents +/// which needs to be signed with [SessionManager::get_next_signature_payload] and [SessionManager::submit_next_signature]. +/// After those are signed, they are kept in a list of [DeviceResponseDoc] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PreparedDeviceResponse { prepared_documents: Vec, @@ -108,15 +178,23 @@ struct PreparedDocument { errors: Option, } +/// Elements in a namespace. type Namespaces = NonEmptyMap>; type Namespace = String; type ElementIdentifier = String; +/// A list of the requested items by the reader. pub type RequestedItems = Vec; +/// The lis of items that are permitted to be shared grouped by document type and namespace. pub type PermittedItems = BTreeMap>>; impl SessionManagerInit { /// Initialise the SessionManager. + /// + /// This is the first transition in the flow interaction. + /// Internally, it generates the ephemeral key and creates the device engagement. + /// + /// It transition to [SessionManagerInit] state. pub fn initialise( documents: Documents, device_retrieval_methods: Option>, @@ -150,7 +228,9 @@ impl SessionManagerInit { super::calculate_ble_ident(&self.device_engagement.as_ref().security.1) } - /// Begin device engagement using QR code. + /// Begins the device engagement using **QR code**. + /// + /// The response contains the device's public key and engagement data. pub fn qr_engagement(self) -> anyhow::Result<(SessionManagerEngaged, String)> { let qr_code_uri = self.device_engagement.to_qr_code_uri()?; let sm = SessionManagerEngaged { @@ -164,6 +244,14 @@ impl SessionManagerInit { } impl SessionManagerEngaged { + /// It transitions to [SessionManager] state + /// by processing the [SessionEstablishment] received from the reader. + /// + /// Internally, it generates the session keys based on the calculated shared secret + /// (using **Diffie–Hellman key exchange**). + /// + /// Along with transitioning to [SessionManagerEngaged] state, + /// it returns the requested items by the reader. pub fn process_session_establishment( self, session_establishment: SessionEstablishment, @@ -235,6 +323,21 @@ impl SessionManager { .collect()) } + /// When the device is ready to respond, it prepares the response specifying the permitted items. + /// + /// It changes the internal state to [State::Signing], + /// and you need + /// to call [SessionManager::get_next_signature_payload] and then [SessionManager::submit_next_signature] + /// to sign the documents. + /// + /// # Example + /// + /// ```text + /// session_manager.prepare_response(&requested_items, permitted_items); + /// let (_, payload)) = session_manager.get_next_signature_payload()?; + /// let signature = sign(&payload); + /// session_manager.submit_next_signature(signature); + /// ``` pub fn prepare_response(&mut self, requests: &RequestedItems, permitted: PermittedItems) { let prepared_response = DeviceSession::prepare_response(self, requests, permitted); self.state = State::Signing(prepared_response); @@ -267,13 +370,30 @@ impl SessionManager { Ok(request) } - /// Handle a request from the reader. + /// Handle a new request from the reader. + /// + /// The request is expected to be a [CBOR](https://cbor.io) + /// encoded [SessionData] and encrypted. + /// It will parse and validate it. + /// + /// It returns the requested items by the reader. pub fn handle_request(&mut self, request: &[u8]) -> anyhow::Result { let session_data: SessionData = serde_cbor::from_slice(request)?; self.handle_decoded_request(session_data) } - /// Get the next payload for signing. + /// When there are documents to be signed, it will return then next one for signing. + /// + /// After signed, you need to call [SessionManager::submit_next_signature]. + /// + /// # Example + /// + /// ```ignore + /// while let Some((_, payload)) = session_manager.get_next_signature_payload()? { + /// let signature = sign(&payload); + /// session_manager.submit_next_signature(signature); + /// } + /// ``` pub fn get_next_signature_payload(&self) -> Option<(Uuid, &[u8])> { match &self.state { State::Signing(p) => p.get_next_signature_payload(), @@ -281,7 +401,20 @@ impl SessionManager { } } - /// Submit the externally signed signature. + /// Submit the externally signed signature for object + /// returned by [SessionManager::get_next_signature_payload]. + /// + /// After all documents are signed, you can call [SessionManager::retrieve_response] + /// to get the response that can then be sent to the reader. + /// + /// # Example + /// + /// ```ignore + /// while let Some((_, payload)) = session_manager.get_next_signature_payload()? { + /// let signature = sign(&payload); + /// session_manager.submit_next_signature(signature); + /// } + /// ``` pub fn submit_next_signature(&mut self, signature: Vec) -> anyhow::Result<()> { if matches!(self.state, State::Signing(_)) { match std::mem::take(&mut self.state) { @@ -319,12 +452,19 @@ impl SessionManager { Ok(()) } - /// Identifies that the response is ready. + /// Identifies if the response is ready. + /// + /// The internal state is [State::ReadyToRespond] in this returns `true`. pub fn response_ready(&self) -> bool { matches!(self.state, State::ReadyToRespond(_)) } - /// Retrieve the completed response. + /// Retrieves the prepared response. + /// + /// Will return [Some] after all documents have been signed. + /// In that case, it will return the response + /// and change the internal state to [State::AwaitingRequest] + /// where it can accept new a request from the reader. pub fn retrieve_response(&mut self) -> Option> { if self.response_ready() { // Replace state with AwaitingRequest. @@ -352,17 +492,20 @@ impl PreparedDeviceResponse { /// Identifies that the response ready to be finalized. /// - /// If false, then there are still items that need to be authorized. + /// If `false`, then there are still items that need to be authorized. pub fn is_complete(&self) -> bool { self.prepared_documents.is_empty() } + /// When there are documents to be signed, it will return then next one for signing. pub fn get_next_signature_payload(&self) -> Option<(Uuid, &[u8])> { self.prepared_documents .last() .map(|doc| (doc.id, doc.prepared_cose_sign1.signature_payload())) } + /// Submit the externally signed signature for object + /// returned by [PreparedDeviceResponse::get_next_signature_payload]. pub fn submit_next_signature(&mut self, signature: Vec) { let signed_doc = match self.prepared_documents.pop() { Some(doc) => doc.finalize(signature), @@ -376,6 +519,7 @@ impl PreparedDeviceResponse { self.signed_documents.push(signed_doc); } + /// Will finalize and prepare the device response. pub fn finalize_response(self) -> DeviceResponse { if !self.is_complete() { //tracing::warn!("attempt to finalize PreparedDeviceResponse before all prepared documents had been authorized"); @@ -417,11 +561,17 @@ impl PreparedDocument { } } +/// Keeps the device session data. +/// +/// One implementation is [SessionManager]. pub trait DeviceSession { type ST: SessionTranscript; + /// Get the device documents. fn documents(&self) -> &Documents; fn session_transcript(&self) -> Self::ST; + + /// Prepare the response based on the requested items and permitted ones. fn prepare_response( &self, requests: &RequestedItems, @@ -711,10 +861,22 @@ pub fn nearest_age_attestation( Ok(None) } +/// Will parse the corresponding age as a number from the `age_over_*` element identifier. +/// +/// # Example +/// +/// ``` +/// use isomdl::presentation::device::parse_age_from_element_identifier; +/// +/// let element = "age_over_21".to_string(); +/// let age = parse_age_from_element_identifier(element).unwrap(); +/// assert_eq!(age, 21); +/// ``` pub fn parse_age_from_element_identifier(element_identifier: String) -> Result { Ok(AgeOver::try_from(element_identifier)?.0) } +/// Holds the age part from the `age_over_*` element identifier. pub struct AgeOver(u8); impl TryFrom for AgeOver { diff --git a/src/presentation/mod.rs b/src/presentation/mod.rs index e38a2dd9..9d65b430 100644 --- a/src/presentation/mod.rs +++ b/src/presentation/mod.rs @@ -1,3 +1,44 @@ +//! This module responsible on handling the interaction between the device and reader. +//! +//! You can see examples on how to use this module in `examples` +//! directory and read about in the dedicated `README.md`. +//! +//! # **Device** and **Reader** interaction +//! +//! This flow demonstrates a simulated device and reader interaction. +//! The reader requests the `age_over_21` element, and the device responds with that value. +//! The flow is something like this: +//! +//! ```ignore +#![doc = include_str!("../../docs/simulated_device_and_reader.txt")] +//! ``` +//! +//! ## The flow of the interaction +//! +//! 1. **Device initialization and engagement:** +//! - The device creates a `QR code` containing `DeviceEngagement` data, which includes its public key. +//! - Internally: +//! - The device initializes with the `mDL` data, private key, and public key. +//! 2. **Reader processing `QR code` and requesting needed fields:** +//! - The reader processes the QR code and creates a request for the `age_over_21` element. +//! - Internally: +//! - Generates its private and public keys. +//! - Initiates a key exchange, and generates the session keys. +//! - The request is encrypted with the reader's session key. +//! 3. **Device accepting request and responding:** +//! - The device receives the request and creates a response with the `age_over_21` element. +//! - Internally: +//! - Initiates the key exchange, and generates the session keys. +//! - Decrypts the request with the reader's session key. +//! - Parse and validate it creating error response if needed. +//! - The response is encrypted with the device's session key. +//! 4. **Reader Processing mDL data:** +//! - The reader processes the response and prints the value of the `age_over_21` element. +//! +//! ### Examples +//! +//! You can see the example in `simulated_device_and_reader.rs` from `examples` directory or a version that +//! uses **State pattern**, `Arc` and `Mutex` `simulated_device_and_reader_state.rs`. pub mod device; pub mod reader; @@ -5,13 +46,50 @@ use anyhow::Result; use base64::{decode, encode}; use serde::{Deserialize, Serialize}; +/// Trait that handles serialization of [CBOR](https://cbor.io) objects to/from [String]. +/// It is an auto trait. pub trait Stringify: Serialize + for<'a> Deserialize<'a> { + /// Serialize to [CBOR](https://cbor.io) representation. + /// + /// Operation may fail, so it returns a [Result]. + /// + /// # Example + /// + /// ``` + /// use base64::decode; + /// use serde::Serialize; + /// use isomdl::presentation::{device, Stringify}; + /// use isomdl::presentation::device::Document; + /// + /// let doc_str = include_str!("../../test/stringified-mdl.txt").to_string(); + /// let doc : Document = serde_cbor::from_slice(&decode(doc_str).unwrap()).unwrap(); + /// let serialized = doc.stringify().unwrap(); + /// assert_eq!(serialized, Document::parse(serialized.clone()).unwrap().stringify().unwrap()); + /// ``` fn stringify(&self) -> Result { let data = serde_cbor::to_vec(self)?; let encoded = encode(data); Ok(encoded) } + /// Deserialize the object from the [CBOR](https://cbor.io) representation. + /// + /// You can call this on something returned by [Stringify::stringify]. + /// Operation may fail, so it returns a [Result]. + /// + /// # Example + /// + /// ``` + /// use base64::decode; + /// use serde::Serialize; + /// use isomdl::presentation::{device, Stringify}; + /// use isomdl::presentation::device::Document; + /// + /// let doc_str = include_str!("../../test/stringified-mdl.txt").to_string(); + /// let doc : Document = serde_cbor::from_slice(&decode(doc_str).unwrap()).unwrap(); + /// let serialized = doc.stringify().unwrap(); + /// assert_eq!(serialized, Document::parse(serialized.clone()).unwrap().stringify().unwrap()); + /// ``` fn parse(encoded: String) -> Result { let data = decode(encoded)?; let this = serde_cbor::from_slice(&data)?; diff --git a/src/presentation/reader.rs b/src/presentation/reader.rs index e9fb2aa0..2f1d5e76 100644 --- a/src/presentation/reader.rs +++ b/src/presentation/reader.rs @@ -1,3 +1,18 @@ +//! This module is responsible for the reader's interaction with the device. +//! +//! It handles this through [SessionManager] state +//! which is responsible for handling the session with the device. +//! +//! From the reader's perspective, the flow is as follows: +//! +//! ```ignore +#![doc = include_str!("../../docs/on_simulated_reader.txt")] +//! ``` +//! +//! ### Example +//! +//! You can view examples in `tests` directory in `simulated_device_and_reader.rs`, for a basic example and +//! `simulated_device_and_reader_state.rs` which uses `State` pattern, `Arc` and `Mutex`. use crate::definitions::{ device_engagement::DeviceRetrievalMethod, device_request::{self, DeviceRequest, DocRequest, ItemsRequest}, @@ -16,6 +31,12 @@ use serde_json::Value; use std::collections::BTreeMap; use uuid::Uuid; +/// The main state of the reader. +/// +/// The reader's [SessionManager] state machine is responsible +/// for handling the session with the device. +/// +/// The transition to this state is made by [SessionManager::establish_session]. #[derive(Serialize, Deserialize)] pub struct SessionManager { session_transcript: SessionTranscript180135, @@ -25,28 +46,40 @@ pub struct SessionManager { reader_message_counter: u32, } +/// Various errors that can occur during the interaction with the device. #[derive(Debug, thiserror::Error)] pub enum Error { + /// The QR code had the wrong prefix or the contained data could not be decoded. #[error("the qr code had the wrong prefix or the contained data could not be decoded: {0}")] InvalidQrCode(anyhow::Error), + /// Device did not transmit any data. #[error("Device did not transmit any data.")] DeviceTransmissionError, + /// Device did not transmit an mDL. #[error("Device did not transmit an mDL.")] DocumentTypeError, + /// The device did not transmit any mDL data. #[error("the device did not transmit any mDL data.")] NoMdlDataTransmission, + /// Device did not transmit any data in the `org.iso.18013.5.1` namespace. #[error("device did not transmit any data in the org.iso.18013.5.1 namespace.")] IncorrectNamespace, + /// The Device responded with an error. #[error("device responded with an error.")] HolderError, + /// Could not decrypt the response. #[error("could not decrypt the response.")] DecryptionError, + /// Unexpected CBOR type for offered value. #[error("Unexpected CBOR type for offered value")] CborDecodingError, + /// Not a valid JSON input. #[error("not a valid JSON input.")] JsonError, + /// Unexpected date type for data_element. #[error("Unexpected date type for data_element.")] ParsingError, + /// Request for data is invalid. #[error("Request for data is invalid.")] InvalidRequest, } @@ -64,6 +97,11 @@ impl From for Error { } impl SessionManager { + /// Establish a session with the device. + /// + /// Internally it generates the ephemeral keys, + /// derives the shared secret, and derives the session keys + /// (using **Diffie–Hellman key exchange**). pub fn establish_session( qr_code: String, namespaces: device_request::Namespaces, @@ -139,6 +177,7 @@ impl SessionManager { }) } + /// Creates a new request with specified elements to request. pub fn new_request(&mut self, namespaces: device_request::Namespaces) -> Result> { let request = self.build_request(namespaces)?; let session = SessionData { @@ -176,6 +215,12 @@ impl SessionManager { .map_err(|e| anyhow!("unable to encrypt request: {}", e)) } + /// Handles a response from the device. + /// + /// The response is expected to be a [CBOR](https://cbor.io) + /// encoded [SessionData] and encrypted. + /// + /// Will return the elements and values grouped by namespace. pub fn handle_response( &mut self, response: &[u8],