open-standards/smart-byte/smart-byte-rs/crates/smart-byte-mdl/src/mdoc.rs
Transaction Science 6575ff1eb7 Ingest batch 3: 9 more crates across openpay / smart-byte / eoc
openpay/ (+3 crates)
  op-bnpl/        Affirm + Klarna + Afterpay/Clearpay rails. Full lifecycle
                  typestate (Initiated→Approved→Captured→Settled→Refunded).
                  Per-provider sandbox+production base URLs, regions (NA/EU/OC
                  for Klarna; US/AuNz/Gb/Ca/Eu for Afterpay). HMAC-SHA256
                  webhook verification with constant-time compare. 61 tests.
  op-batch/       NACHA ACH (94-char records, PPD/CCD/WEB/TEL/CTX, R01-R99
                  return codes, Same-Day ACH cutoffs); SEPA pain.001.001.09
                  (CT) + pain.008.001.08 (DD) with SCT-Inst flagging; Fedwire
                  MT103/MT202/CHIPS + pacs.008.001.12 / pacs.009.001.10 +
                  UETR; Bacs Standard 18. Per-rail cutoff scheduler. Exception
                  taxonomy unified across rails. 51 tests.
  op-screening/   OFAC SDN + EU consolidated + UN consolidated + HMT UK
                  consolidated + Australia DFAT + Canada SEMA + Japan MOF.
                  Levenshtein + Jaro-Winkler + Soundex + Metaphone + token
                  Jaccard scoring. Unicode NFKD normalisation + Cyrillic /
                  Arabic transliteration. Ed25519-signed BLAKE3-chained
                  audit log. BSA helpers: CTR + SAR pattern detection
                  (structuring, round-dollar, rapid-in-out). 43 tests.

smart-byte/smart-byte-rs/ (+3 crates)
  smart-byte-bbs/      BBS+ signatures (IETF draft-irtf-cfrg-bbs-signatures-08).
                       BLS12-381 keys, signatures, disclosure proofs with
                       cryptographic unlinkability. bbs-2023 Data Integrity
                       cryptosuite. AnonCreds-shaped link secrets +
                       predicate proofs. SecretKey zeroize-on-drop. 49 tests.
  smart-byte-didcomm/  DIDComm v2 messaging per DIF spec. Pack-encrypted
                       (ECDH-1PU + ECDH-ES via X25519), pack-signed (JWS),
                       pack-plaintext. Aries protocols: issue-credential v3,
                       present-proof v3, basic-message, trust-ping, discover-
                       features, out-of-band, coordinate-mediation, message-
                       pickup. Mediator role with pluggable storage. 23 tests.
  smart-byte-mdl/      ISO/IEC 18013-5 mobile driver's licence + 18013-7
                       online presentment. mDoc + MSO with salted-hash
                       selective disclosure (per item; SHA-256 digest tree).
                       org.iso.18013.5.1 namespace + AAMVA add-on. ES256
                       device-auth + issuer-auth. QR-code device-engagement
                       scaffolding. 28 tests.

eoc/eoc-rs/ (+3 crates)
  eoc-local/      Local-inference backends behind one NeuralBackend:
                  llama.cpp (GGUF, Q4/Q5/Q6/Q8/F16/F32 quantisation),
                  MLX (Apple Silicon, feature-gated), MLC-LLM (TVM, feature-
                  gated marker), ONNX Runtime. GGUF file-format reader.
                  HuggingFace tokenizers. Greedy / top-k / top-p / temperature /
                  Mirostat samplers. Model registry scanning ~/.eoc/models/.
                  Joule attribution via eoc-meter. 39 tests.
  eoc-rerank/     BM25 sparse + HNSW dense + RRF + min-max hybrid fusion.
                  Cohere v3.5 + Voyage v2 + bge-reranker v2-m3 + ms-marco
                  MiniLM + ColBERT v2 rerankers. Two-stage pipeline:
                  retrieve top-K hybrid → cross-encoder rerank → top-N.
                  KV-stage recovery shim into the cascade. 31 tests.
  eoc-eval/       15 canonical eval harnesses: MMLU, MMLU-Pro, GPQA,
                  HumanEval (pass@1 with python3 sandbox), BBH, IFEval (25
                  verifiable constraints), AlpacaEval, AGIEval, HellaSwag,
                  ARC (Easy + Challenge), TruthfulQA (MC1+MC2), BoolQ,
                  GSM8K, MATH (boxed-answer equivalence), Winogrande. Real
                  questions in BuiltinSamples. EvalRunner emits joules-per-
                  correct alongside accuracy. 49 tests.

Quality:
- cargo check across all three workspaces: clean
- cargo clippy --all-targets -- -D warnings on every new crate: clean
- 374 new tests, all passing
- #![forbid(unsafe_code)] across every new crate
- No unwrap/expect outside #[cfg(test)]

Rust 1.95.0 edition 2024 (inherited via root rust-toolchain.toml).
2026-05-24 01:09:35 -04:00

689 lines
25 KiB
Rust

//! ISO/IEC 18013-5 `mdoc` data structures and CBOR codec.
//!
//! Wire layout (ISO/IEC 18013-5 §8.3.2.1.2.2):
//!
//! ```text
//! mdoc = {
//! "docType": tstr,
//! "issuerSigned": IssuerSigned,
//! ? "deviceSigned": DeviceSigned,
//! }
//! IssuerSigned = {
//! "nameSpaces": { tstr => [+ #6.24(bstr .cbor IssuerSignedItem)] },
//! "issuerAuth": COSE_Sign1, ; detached payload = bstr .cbor MSO
//! }
//! IssuerSignedItem = {
//! "digestID": uint,
//! "random": bstr (>= 16 bytes),
//! "elementIdentifier": tstr,
//! "elementValue": any,
//! }
//! DeviceSigned = {
//! "nameSpaces": #6.24(bstr .cbor { tstr => { tstr => any } }),
//! "deviceAuth": { "deviceSignature": COSE_Sign1 / "deviceMac": COSE_Mac0 },
//! }
//! ```
//!
//! Tag 24 (`encoded-cbor`) wraps an inner CBOR encoding so the bytes
//! delivered to the digest function are stable: implementations must
//! hash the *exact* tag-24 bytes, not re-encode the inner item.
use std::collections::BTreeMap;
use ciborium::value::{Integer, Value as CborValue};
use serde::{Deserialize, Serialize};
use crate::error::MdlError;
/// CBOR tag 24 — "encoded-cbor data item" (RFC 8949 §3.4.5.1).
pub const TAG_ENCODED_CBOR: u64 = 24;
/// A single issuer-signed claim, plus its random salt and digest id.
///
/// The digest committed to in the MSO is `H(encoded-cbor(IssuerSignedItem))`
/// where the outer bytes are the *exact* tag-24 encoding of the item's
/// CBOR map.
#[derive(Clone, Debug, PartialEq)]
pub struct IssuerSignedItem {
/// Index assigned by the issuer for this item within its namespace.
/// The MSO indexes digests under this id.
pub digest_id: u64,
/// Random salt (>= 16 bytes per ISO 18013-5 §9.1.2.4).
pub random: Vec<u8>,
/// Element name (e.g. `family_name`).
pub element_identifier: String,
/// Element value (any CBOR type).
pub element_value: CborValue,
}
impl IssuerSignedItem {
/// Encode the item as a CBOR map.
pub fn to_map(&self) -> CborValue {
CborValue::Map(vec![
(
CborValue::Text("digestID".into()),
CborValue::Integer(Integer::from(self.digest_id)),
),
(
CborValue::Text("random".into()),
CborValue::Bytes(self.random.clone()),
),
(
CborValue::Text("elementIdentifier".into()),
CborValue::Text(self.element_identifier.clone()),
),
(
CborValue::Text("elementValue".into()),
self.element_value.clone(),
),
])
}
/// Parse from a CBOR map.
pub fn from_map(value: &CborValue) -> Result<Self, MdlError> {
let entries = match value {
CborValue::Map(m) => m,
_ => return Err(MdlError::Type("IssuerSignedItem not a map".into())),
};
let mut digest_id: Option<u64> = None;
let mut random: Option<Vec<u8>> = None;
let mut element_identifier: Option<String> = None;
let mut element_value: Option<CborValue> = None;
for (k, v) in entries {
let CborValue::Text(name) = k else { continue };
match name.as_str() {
"digestID" => {
if let CborValue::Integer(i) = v {
let n: i128 = (*i).into();
if n < 0 {
return Err(MdlError::Type(
"digestID negative".into(),
));
}
digest_id = Some(n as u64);
}
}
"random" => {
if let CborValue::Bytes(b) = v {
random = Some(b.clone());
}
}
"elementIdentifier" => {
if let CborValue::Text(s) = v {
element_identifier = Some(s.clone());
}
}
"elementValue" => element_value = Some(v.clone()),
_ => {}
}
}
Ok(Self {
digest_id: digest_id
.ok_or_else(|| MdlError::Missing("digestID".into()))?,
random: random
.ok_or_else(|| MdlError::Missing("random".into()))?,
element_identifier: element_identifier
.ok_or_else(|| MdlError::Missing("elementIdentifier".into()))?,
element_value: element_value
.ok_or_else(|| MdlError::Missing("elementValue".into()))?,
})
}
/// Encode as the exact tag-24 bytes that the MSO digests over.
///
/// Returns the *outer* tagged CBOR encoding `#6.24(bstr .cbor item)`.
/// Verifiers MUST hash these exact bytes, not re-encode the inner.
pub fn to_tag24_bytes(&self) -> Result<Vec<u8>, MdlError> {
let inner = encode_cbor(&self.to_map())?;
let tagged = CborValue::Tag(TAG_ENCODED_CBOR, Box::new(CborValue::Bytes(inner)));
encode_cbor(&tagged)
}
/// Parse the inner item out of a tag-24 encoding.
pub fn from_tag24_bytes(bytes: &[u8]) -> Result<Self, MdlError> {
let v: CborValue = decode_cbor(bytes)?;
let (tag, inner) = match v {
CborValue::Tag(t, inner) => (t, *inner),
_ => {
return Err(MdlError::Type(
"expected tag-24 tagged value".into(),
));
}
};
if tag != TAG_ENCODED_CBOR {
return Err(MdlError::Type(format!(
"expected tag 24, got {tag}"
)));
}
let inner_bytes = match inner {
CborValue::Bytes(b) => b,
_ => {
return Err(MdlError::Type(
"tag-24 content not bytes".into(),
));
}
};
let inner_value: CborValue = decode_cbor(&inner_bytes)?;
Self::from_map(&inner_value)
}
}
/// Issuer-signed half of an mdoc.
#[derive(Clone, Debug, PartialEq)]
pub struct IssuerSigned {
/// Map of namespace -> list of issuer-signed items.
pub name_spaces: BTreeMap<String, Vec<IssuerSignedItem>>,
/// Detached COSE_Sign1 — payload is the bstr-encoded MSO.
pub issuer_auth: CoseSign1,
}
impl IssuerSigned {
/// Encode as the canonical CBOR map.
pub fn to_value(&self) -> Result<CborValue, MdlError> {
let mut ns_entries: Vec<(CborValue, CborValue)> = Vec::new();
for (ns, items) in &self.name_spaces {
let mut arr: Vec<CborValue> = Vec::with_capacity(items.len());
for item in items {
let inner = encode_cbor(&item.to_map())?;
arr.push(CborValue::Tag(
TAG_ENCODED_CBOR,
Box::new(CborValue::Bytes(inner)),
));
}
ns_entries.push((CborValue::Text(ns.clone()), CborValue::Array(arr)));
}
let map = vec![
(CborValue::Text("nameSpaces".into()), CborValue::Map(ns_entries)),
(CborValue::Text("issuerAuth".into()), self.issuer_auth.to_value()),
];
Ok(CborValue::Map(map))
}
/// Decode from a CBOR map.
pub fn from_value(v: &CborValue) -> Result<Self, MdlError> {
let entries = match v {
CborValue::Map(m) => m,
_ => return Err(MdlError::Type("IssuerSigned not a map".into())),
};
let mut name_spaces: BTreeMap<String, Vec<IssuerSignedItem>> = BTreeMap::new();
let mut issuer_auth: Option<CoseSign1> = None;
for (k, val) in entries {
let CborValue::Text(name) = k else { continue };
match name.as_str() {
"nameSpaces" => {
let CborValue::Map(ns_map) = val else {
return Err(MdlError::Type(
"nameSpaces not a map".into(),
));
};
for (ns_key, items_val) in ns_map {
let CborValue::Text(ns_name) = ns_key else {
continue;
};
let CborValue::Array(items) = items_val else {
return Err(MdlError::Type(
"namespace items not an array".into(),
));
};
let mut parsed = Vec::with_capacity(items.len());
for it in items {
let bytes = encode_cbor(it)?;
parsed.push(IssuerSignedItem::from_tag24_bytes(&bytes)?);
}
name_spaces.insert(ns_name.clone(), parsed);
}
}
"issuerAuth" => {
issuer_auth = Some(CoseSign1::from_value(val)?);
}
_ => {}
}
}
Ok(Self {
name_spaces,
issuer_auth: issuer_auth
.ok_or_else(|| MdlError::Missing("issuerAuth".into()))?,
})
}
}
/// Device-signed half of an mdoc.
#[derive(Clone, Debug, PartialEq)]
pub struct DeviceSigned {
/// Device-signed namespaces (typically empty — most claims are issuer-signed).
pub name_spaces: BTreeMap<String, BTreeMap<String, CborValue>>,
/// Device authentication: either a signature (COSE_Sign1) or a MAC
/// (COSE_Mac0). This crate ships the signature path.
pub device_auth: DeviceAuth,
}
impl DeviceSigned {
/// Encode as a CBOR map. The `nameSpaces` field is the standard
/// tag-24-wrapped CBOR encoding for canonical hashing.
pub fn to_value(&self) -> Result<CborValue, MdlError> {
let mut ns_entries: Vec<(CborValue, CborValue)> = Vec::new();
for (ns, items) in &self.name_spaces {
let mut inner: Vec<(CborValue, CborValue)> = Vec::new();
for (k, v) in items {
inner.push((CborValue::Text(k.clone()), v.clone()));
}
ns_entries.push((CborValue::Text(ns.clone()), CborValue::Map(inner)));
}
let ns_bytes = encode_cbor(&CborValue::Map(ns_entries))?;
let tagged = CborValue::Tag(
TAG_ENCODED_CBOR,
Box::new(CborValue::Bytes(ns_bytes)),
);
let map = vec![
(CborValue::Text("nameSpaces".into()), tagged),
(CborValue::Text("deviceAuth".into()), self.device_auth.to_value()),
];
Ok(CborValue::Map(map))
}
/// Decode from a CBOR map.
pub fn from_value(v: &CborValue) -> Result<Self, MdlError> {
let entries = match v {
CborValue::Map(m) => m,
_ => return Err(MdlError::Type("DeviceSigned not a map".into())),
};
let mut name_spaces: BTreeMap<String, BTreeMap<String, CborValue>> =
BTreeMap::new();
let mut device_auth: Option<DeviceAuth> = None;
for (k, val) in entries {
let CborValue::Text(name) = k else { continue };
match name.as_str() {
"nameSpaces" => {
let (tag, inner) = match val {
CborValue::Tag(t, b) => (*t, b.as_ref().clone()),
_ => {
return Err(MdlError::Type(
"nameSpaces not tagged".into(),
));
}
};
if tag != TAG_ENCODED_CBOR {
return Err(MdlError::Type(format!(
"deviceSigned.nameSpaces wrong tag {tag}"
)));
}
let bytes = match inner {
CborValue::Bytes(b) => b,
_ => {
return Err(MdlError::Type(
"tag-24 content not bytes".into(),
));
}
};
let inner_value: CborValue = decode_cbor(&bytes)?;
let CborValue::Map(ns_map) = inner_value else {
return Err(MdlError::Type(
"deviceSigned namespaces inner not map".into(),
));
};
for (ns_key, ns_val) in ns_map {
let CborValue::Text(ns_name) = ns_key else {
continue;
};
let CborValue::Map(items) = ns_val else {
continue;
};
let mut m: BTreeMap<String, CborValue> = BTreeMap::new();
for (k2, v2) in items {
if let CborValue::Text(s) = k2 {
m.insert(s, v2);
}
}
name_spaces.insert(ns_name, m);
}
}
"deviceAuth" => device_auth = Some(DeviceAuth::from_value(val)?),
_ => {}
}
}
Ok(Self {
name_spaces,
device_auth: device_auth
.ok_or_else(|| MdlError::Missing("deviceAuth".into()))?,
})
}
}
/// Device-authentication payload. ISO 18013-5 §9.1.3 allows either a
/// COSE_Sign1 signature or a COSE_Mac0 MAC. We expose both shapes;
/// only the signature path is signed/verified by [`crate::verifier`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DeviceAuth {
/// COSE_Sign1 signature over the DeviceAuthentication structure.
Signature(CoseSign1),
/// COSE_Mac0 MAC over the DeviceAuthentication structure.
Mac(CoseMac0),
}
impl DeviceAuth {
/// Encode as a CBOR map.
pub fn to_value(&self) -> CborValue {
match self {
DeviceAuth::Signature(s) => CborValue::Map(vec![(
CborValue::Text("deviceSignature".into()),
s.to_value(),
)]),
DeviceAuth::Mac(m) => CborValue::Map(vec![(
CborValue::Text("deviceMac".into()),
m.to_value(),
)]),
}
}
/// Parse from a CBOR map.
pub fn from_value(v: &CborValue) -> Result<Self, MdlError> {
let entries = match v {
CborValue::Map(m) => m,
_ => return Err(MdlError::Type("DeviceAuth not a map".into())),
};
for (k, val) in entries {
if let CborValue::Text(name) = k {
match name.as_str() {
"deviceSignature" => {
return Ok(DeviceAuth::Signature(CoseSign1::from_value(val)?));
}
"deviceMac" => {
return Ok(DeviceAuth::Mac(CoseMac0::from_value(val)?));
}
_ => {}
}
}
}
Err(MdlError::Missing("deviceAuth body".into()))
}
}
/// Minimal COSE_Sign1 representation used by mdoc. The full COSE wire
/// form is a 4-tuple `[protected, unprotected, payload, signature]`.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CoseSign1 {
/// Protected header parameters, serialized as CBOR bytes (per RFC 9052).
pub protected: Vec<u8>,
/// Unprotected header parameters, encoded as a CBOR map's bytes.
pub unprotected: Vec<u8>,
/// Payload bytes (the bstr-encoded MSO for issuerAuth, or empty for
/// detached signatures).
pub payload: Vec<u8>,
/// Raw signature bytes (algorithm-specific).
pub signature: Vec<u8>,
}
impl CoseSign1 {
/// Build the array CBOR value `[protected, unprotected, payload, signature]`.
pub fn to_value(&self) -> CborValue {
let unprotected_val = decode_cbor(&self.unprotected)
.unwrap_or(CborValue::Map(Vec::new()));
CborValue::Array(vec![
CborValue::Bytes(self.protected.clone()),
unprotected_val,
CborValue::Bytes(self.payload.clone()),
CborValue::Bytes(self.signature.clone()),
])
}
/// Parse from a CBOR array.
pub fn from_value(v: &CborValue) -> Result<Self, MdlError> {
let arr = match v {
CborValue::Array(a) => a,
_ => return Err(MdlError::Type("COSE_Sign1 not array".into())),
};
if arr.len() != 4 {
return Err(MdlError::Type(format!(
"COSE_Sign1 len {} (want 4)",
arr.len()
)));
}
let protected = match &arr[0] {
CborValue::Bytes(b) => b.clone(),
_ => return Err(MdlError::Type("protected not bstr".into())),
};
let unprotected = encode_cbor(&arr[1])?;
let payload = match &arr[2] {
CborValue::Bytes(b) => b.clone(),
CborValue::Null => Vec::new(),
_ => return Err(MdlError::Type("payload not bstr".into())),
};
let signature = match &arr[3] {
CborValue::Bytes(b) => b.clone(),
_ => return Err(MdlError::Type("signature not bstr".into())),
};
Ok(Self {
protected,
unprotected,
payload,
signature,
})
}
/// Build the Sig_structure as defined in RFC 9052 §4.4. This is the
/// exact byte string a signer must hash and sign.
pub fn sig_structure(&self, external_aad: &[u8]) -> Result<Vec<u8>, MdlError> {
let arr = CborValue::Array(vec![
CborValue::Text("Signature1".into()),
CborValue::Bytes(self.protected.clone()),
CborValue::Bytes(external_aad.to_vec()),
CborValue::Bytes(self.payload.clone()),
]);
encode_cbor(&arr)
}
}
/// Minimal COSE_Mac0 representation. Carried by mdocs that use MAC
/// device authentication; verification is out of scope for this crate.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CoseMac0 {
/// Protected header bytes.
pub protected: Vec<u8>,
/// Unprotected header bytes.
pub unprotected: Vec<u8>,
/// MACed payload bytes (or empty if detached).
pub payload: Vec<u8>,
/// Tag bytes.
pub tag: Vec<u8>,
}
impl CoseMac0 {
/// Build the array CBOR value.
pub fn to_value(&self) -> CborValue {
let unprotected_val = decode_cbor(&self.unprotected)
.unwrap_or(CborValue::Map(Vec::new()));
CborValue::Array(vec![
CborValue::Bytes(self.protected.clone()),
unprotected_val,
CborValue::Bytes(self.payload.clone()),
CborValue::Bytes(self.tag.clone()),
])
}
/// Parse from a CBOR array.
pub fn from_value(v: &CborValue) -> Result<Self, MdlError> {
let arr = match v {
CborValue::Array(a) => a,
_ => return Err(MdlError::Type("COSE_Mac0 not array".into())),
};
if arr.len() != 4 {
return Err(MdlError::Type(format!(
"COSE_Mac0 len {} (want 4)",
arr.len()
)));
}
let protected = match &arr[0] {
CborValue::Bytes(b) => b.clone(),
_ => return Err(MdlError::Type("protected not bstr".into())),
};
let unprotected = encode_cbor(&arr[1])?;
let payload = match &arr[2] {
CborValue::Bytes(b) => b.clone(),
CborValue::Null => Vec::new(),
_ => return Err(MdlError::Type("payload not bstr".into())),
};
let tag = match &arr[3] {
CborValue::Bytes(b) => b.clone(),
_ => return Err(MdlError::Type("tag not bstr".into())),
};
Ok(Self {
protected,
unprotected,
payload,
tag,
})
}
}
/// A complete `mdoc` (ISO 18013-5 §8.3.2.1.2.2).
#[derive(Clone, Debug, PartialEq)]
pub struct MobileDoc {
/// `docType`, e.g. `org.iso.18013.5.1.mDL`.
pub doc_type: String,
/// Issuer-signed component.
pub issuer_signed: IssuerSigned,
/// Optional device-signed component (omitted in an offline issuance
/// bundle; present in a presentment).
pub device_signed: Option<DeviceSigned>,
}
impl MobileDoc {
/// Encode to canonical CBOR.
pub fn to_cbor(&self) -> Result<Vec<u8>, MdlError> {
let issuer_signed = self.issuer_signed.to_value()?;
let mut entries = vec![
(
CborValue::Text("docType".into()),
CborValue::Text(self.doc_type.clone()),
),
(CborValue::Text("issuerSigned".into()), issuer_signed),
];
if let Some(d) = &self.device_signed {
entries.push((
CborValue::Text("deviceSigned".into()),
d.to_value()?,
));
}
encode_cbor(&CborValue::Map(entries))
}
/// Decode from canonical CBOR.
pub fn from_cbor(bytes: &[u8]) -> Result<Self, MdlError> {
let v: CborValue = decode_cbor(bytes)?;
let entries = match v {
CborValue::Map(m) => m,
_ => return Err(MdlError::Type("mdoc not a map".into())),
};
let mut doc_type: Option<String> = None;
let mut issuer_signed: Option<IssuerSigned> = None;
let mut device_signed: Option<DeviceSigned> = None;
for (k, val) in entries {
let CborValue::Text(name) = k else { continue };
match name.as_str() {
"docType" => {
if let CborValue::Text(s) = val {
doc_type = Some(s);
}
}
"issuerSigned" => {
issuer_signed = Some(IssuerSigned::from_value(&val)?);
}
"deviceSigned" => {
device_signed = Some(DeviceSigned::from_value(&val)?);
}
_ => {}
}
}
Ok(Self {
doc_type: doc_type
.ok_or_else(|| MdlError::Missing("docType".into()))?,
issuer_signed: issuer_signed
.ok_or_else(|| MdlError::Missing("issuerSigned".into()))?,
device_signed,
})
}
}
/// Encode a CBOR value with ciborium.
pub fn encode_cbor(value: &CborValue) -> Result<Vec<u8>, MdlError> {
let mut buf = Vec::new();
ciborium::ser::into_writer(value, &mut buf)
.map_err(|e| MdlError::Cbor(e.to_string()))?;
Ok(buf)
}
/// Decode a CBOR value with ciborium.
pub fn decode_cbor(bytes: &[u8]) -> Result<CborValue, MdlError> {
ciborium::de::from_reader(bytes).map_err(|e| MdlError::Cbor(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture_item(id: u64, name: &str, value: &str) -> IssuerSignedItem {
IssuerSignedItem {
digest_id: id,
random: vec![0xAB; 16],
element_identifier: name.into(),
element_value: CborValue::Text(value.into()),
}
}
#[test]
fn tag24_round_trip() {
let item = fixture_item(1, "family_name", "Doe");
let bytes = item.to_tag24_bytes().unwrap();
let back = IssuerSignedItem::from_tag24_bytes(&bytes).unwrap();
assert_eq!(item, back);
}
#[test]
fn issuer_signed_round_trip() {
let mut ns: BTreeMap<String, Vec<IssuerSignedItem>> = BTreeMap::new();
ns.insert(
crate::namespace::NS_MDL.into(),
vec![
fixture_item(0, "family_name", "Doe"),
fixture_item(1, "given_name", "Jane"),
],
);
let issuer_signed = IssuerSigned {
name_spaces: ns,
issuer_auth: CoseSign1 {
protected: vec![0xA1, 0x01, 0x26],
unprotected: encode_cbor(&CborValue::Map(Vec::new())).unwrap(),
payload: vec![1, 2, 3, 4],
signature: vec![9; 64],
},
};
let v = issuer_signed.to_value().unwrap();
let back = IssuerSigned::from_value(&v).unwrap();
assert_eq!(back, issuer_signed);
}
#[test]
fn mdoc_round_trip() {
let mut ns: BTreeMap<String, Vec<IssuerSignedItem>> = BTreeMap::new();
ns.insert(
crate::namespace::NS_MDL.into(),
vec![fixture_item(0, "family_name", "Doe")],
);
let doc = MobileDoc {
doc_type: "org.iso.18013.5.1.mDL".into(),
issuer_signed: IssuerSigned {
name_spaces: ns,
issuer_auth: CoseSign1 {
protected: vec![],
unprotected: encode_cbor(&CborValue::Map(Vec::new())).unwrap(),
payload: vec![],
signature: vec![],
},
},
device_signed: None,
};
let bytes = doc.to_cbor().unwrap();
let back = MobileDoc::from_cbor(&bytes).unwrap();
assert_eq!(back, doc);
}
}