foundation-urtypes 0.5.0

Uniform Resource (UR) types. This crate provides a collection of Uniform Resources types that can serialized and deserialized.
Documentation
// SPDX-FileCopyrightText: © 2023 Foundation Devices, Inc. <hello@foundationdevices.com>
// SPDX-License-Identifier: GPL-3.0-or-later

//! # Passport specific UR types.
//!
//! ## CDDL for Request.
//!
//! ```cddl
//! ; At least one of the fields MUST be specified.
//!
//! passport-request = {
//!     transaction-id: uuid
//!     ? scv-challenge-request: #6.710(scv-challenge),
//!     ? passport-model-request: #6.720(bool) .default false,
//!     ? passport-firmware-version-request: #6.770(bool) .default false,
//! }
//!
//! ; TODO: use fixed numbers.
//! transaction-id = uint
//! scv-challenge-request = uint
//! passport-model-request = uint
//! passport-firmware-version-request = uint
//! ```
//!
//! # CDDL for Response.
//!
//! ```cddl
//! passport-response = {
//!     transaction-id: uuid,
//!     ? scv-solution-response: #6.711(scv-solution),
//!     ? passport-model-response: #6.721(passport-model),
//!     ? passport-firmware-version-response: #6.771(text)
//! }
//!
//! ; TODO: use fixed numbers.
//! transaction-id = uint
//! scv-solution-response = uint
//! passport-model-response = uint
//! passport-firmware-version-response = uint
//!
//! ```

use crate::{
    passport::Model,
    supply_chain_validation::{Challenge, Solution},
};

use minicbor::{
    data::{Tag, Type},
    decode::Error,
    encode::Write,
    Decode, Decoder, Encode, Encoder,
};
use uuid::Uuid;

/// Passport model request tag.
const PASSPORT_MODEL_REQUEST_TAG: Tag = Tag::new(720);
/// Passport firmware version request tag.
const PASSPORT_FIRMWARE_VERSION_REQUEST_TAG: Tag = Tag::new(770);
/// Passport firmware version response tag.
const PASSPORT_FIRMWARE_VERSION_RESPONSE_TAG: Tag = Tag::new(771);

/// Passport custom `crypto-request`.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct PassportRequest {
    /// Transaction identifier.
    pub transaction_id: Uuid,
    /// Supply chain validation challenge.
    pub scv_challenge: Option<Challenge>,
    /// Request Passport model.
    pub passport_model: bool,
    /// Request Passport firmware version.
    pub passport_firmware_version: bool,
}

impl<'b, C> Decode<'b, C> for PassportRequest {
    fn decode(d: &mut Decoder<'b>, ctx: &mut C) -> Result<Self, Error> {
        let mut transaction_id = None;
        let mut scv_challenge = None;
        let mut passport_model = false;
        let mut passport_firmware_version = false;

        macro_rules! decode_inner {
            () => {
                // TODO: ignore index for now.
                //  Decoding is based on the tags. Use indexes once Envoy
                //  sends "stable" indexes.
                d.u32()?;
                match d.probe().tag()? {
                    crate::cbor::uuid::TAG => {
                        transaction_id = Some(crate::cbor::uuid::decode(d, ctx)?);
                    }
                    Challenge::TAG => {
                        d.tag()?;
                        scv_challenge = Some(Challenge::decode(d, ctx)?);
                    }
                    PASSPORT_MODEL_REQUEST_TAG => {
                        d.tag()?;
                        passport_model = d.bool()?;
                    }
                    PASSPORT_FIRMWARE_VERSION_REQUEST_TAG => {
                        d.tag()?;
                        passport_firmware_version = d.bool()?;
                    }
                    _ => return Err(Error::message("unknown tag")),
                }
            };
        }

        if let Some(len) = d.map()? {
            for _ in 0..len {
                decode_inner!();
            }
        } else {
            while d.datatype()? != Type::Break {
                decode_inner!();
            }
        }

        Ok(Self {
            transaction_id: transaction_id
                .ok_or_else(|| Error::message("transaction-id missing"))?,
            scv_challenge,
            passport_model,
            passport_firmware_version,
        })
    }
}

impl<C> Encode<C> for PassportRequest {
    fn encode<W: Write>(
        &self,
        e: &mut Encoder<W>,
        ctx: &mut C,
    ) -> Result<(), minicbor::encode::Error<W::Error>> {
        let len = 1
            + self.scv_challenge.is_some() as u64
            + self.passport_model as u64
            + self.passport_firmware_version as u64;
        e.map(len)?;

        e.u8(1)?;
        crate::cbor::uuid::encode(&self.transaction_id, e, ctx)?;

        if let Some(ref scv_challenge) = self.scv_challenge {
            e.u8(2)?.tag(Challenge::TAG)?;
            scv_challenge.encode(e, ctx)?;
        }

        if self.passport_model {
            e.u8(3)?
                .tag(PASSPORT_MODEL_REQUEST_TAG)?
                .bool(self.passport_model)?;
        }

        if self.passport_firmware_version {
            e.u8(4)?
                .tag(PASSPORT_FIRMWARE_VERSION_REQUEST_TAG)?
                .bool(self.passport_firmware_version)?;
        }

        Ok(())
    }
}

/// Passport custom `crypto-request`.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct PassportResponse<'a> {
    /// Transaction identifier.
    pub transaction_id: Uuid,
    /// Solution to the supply validation challenge.
    pub scv_solution: Option<Solution<'a>>,
    /// Passport model
    pub passport_model: Option<Model>,
    /// Passport firmware version.
    pub passport_firmware_version: Option<&'a str>,
}

impl<'b, C> Decode<'b, C> for PassportResponse<'b> {
    fn decode(d: &mut Decoder<'b>, ctx: &mut C) -> Result<Self, Error> {
        let mut transaction_id = None;
        let mut scv_solution = None;
        let mut passport_model = None;
        let mut passport_firmware_version = None;

        macro_rules! decode_inner {
            () => {
                // TODO: ignore index for now.
                //  Decoding is based on the tags. Use indexes once Envoy
                //  sends "stable" indexes.
                d.u32()?;
                match d.probe().tag()? {
                    crate::cbor::uuid::TAG => {
                        transaction_id = Some(crate::cbor::uuid::decode(d, ctx)?);
                    }
                    Solution::TAG => {
                        d.tag()?;
                        scv_solution = Some(Solution::decode(d, ctx)?);
                    }
                    Model::TAG => {
                        d.tag()?;
                        passport_model = Some(Model::decode(d, ctx)?);
                    }
                    PASSPORT_FIRMWARE_VERSION_RESPONSE_TAG => {
                        d.tag()?;
                        passport_firmware_version = Some(d.str()?);
                    }
                    _ => return Err(Error::message("unknown map entry")),
                }
            };
        }

        if let Some(len) = d.map()? {
            for _ in 0..len {
                decode_inner!();
            }
        } else {
            while d.datatype()? != Type::Break {
                decode_inner!();
            }
        }

        Ok(Self {
            transaction_id: transaction_id
                .ok_or_else(|| Error::message("transaction-id is not present"))?,
            scv_solution,
            passport_model,
            passport_firmware_version,
        })
    }
}

impl<'a, C> Encode<C> for PassportResponse<'a> {
    fn encode<W: Write>(
        &self,
        e: &mut Encoder<W>,
        ctx: &mut C,
    ) -> Result<(), minicbor::encode::Error<W::Error>> {
        let len = 1
            + self.scv_solution.is_some() as u64
            + self.passport_model.is_some() as u64
            + self.passport_firmware_version.is_some() as u64;

        e.map(len)?;

        e.u8(1)?;
        crate::cbor::uuid::encode(&self.transaction_id, e, ctx)?;

        if let Some(ref scv_solution) = self.scv_solution {
            e.u8(2)?.tag(Solution::TAG)?;
            scv_solution.encode(e, ctx)?;
        }

        if let Some(ref passport_model) = self.passport_model {
            e.u8(3)?.tag(Model::TAG)?;
            passport_model.encode(e, ctx)?;
        }

        if let Some(passport_firmware_version) = self.passport_firmware_version {
            e.u8(4)?
                .tag(PASSPORT_FIRMWARE_VERSION_RESPONSE_TAG)?
                .str(passport_firmware_version)?;
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_roundtrip_passport_request() {
        let mut id = [0; 32];
        let mut signature = [0; 64];

        hex::decode_to_slice(
            "2e99758548972a8e8822ad47fa1017ff72f06f3ff6a016851f45c398732bc50c",
            &mut id,
        )
        .unwrap();
        hex::decode_to_slice("7d0a8468ed220400c0b8e6f335baa7e070ce880a37e2ac5995b9a97b809026de626da636ac7365249bb974c719edf543b52ed286646f437dc7f810cc2068375c", &mut signature).unwrap();

        let request = PassportRequest {
            transaction_id: Default::default(),
            scv_challenge: Some(Challenge { id, signature }),
            passport_model: true,
            passport_firmware_version: true,
        };

        let encoded = &minicbor::to_vec(&request).unwrap();
        let decoded: PassportRequest = minicbor::decode(&encoded).unwrap();

        assert_eq!(decoded, request);
    }

    #[test]
    fn test_roundtrip_passport_response() {
        let mut id = [0; 32];
        let mut signature = [0; 64];

        hex::decode_to_slice(
            "2e99758548972a8e8822ad47fa1017ff72f06f3ff6a016851f45c398732bc50c",
            &mut id,
        )
        .unwrap();
        hex::decode_to_slice("7d0a8468ed220400c0b8e6f335baa7e070ce880a37e2ac5995b9a97b809026de626da636ac7365249bb974c719edf543b52ed286646f437dc7f810cc2068375c", &mut signature).unwrap();

        let response = PassportResponse {
            transaction_id: Default::default(),
            scv_solution: Some(Solution {
                word1: "abandon",
                word2: "ability",
                word3: "able",
                word4: "about",
            }),
            passport_model: Some(Model::Batch2),
            passport_firmware_version: Some("2.0.5"),
        };

        let encoded = &minicbor::to_vec(&response).unwrap();
        let decoded: PassportResponse = minicbor::decode(&encoded).unwrap();

        assert_eq!(decoded, response);
    }

    #[test]
    fn test_request_decode() {
        const TEST_VECTOR: &str = "a201d8255083816f6064ff4046b93a85687f8f608202d902c6a30178403338633663303561633639613166623737626366333736333330383835326364336530383066653165343630346361613831623534383236653264613062643202788037393035306564663562386636343937663936626661633031333363396365663561303764613363343835613432373466646666396433616637346632393833636566386432303337663164626636613435356431356530666236346162313665333664643336353062363533323265333239303138313639633631356636610378603045022079050ec39f5bc28f64c297c3b96bc3bac380133cc29cc3af5a07c39a3c485a4274c3bdc3bfc29d3ac3b74f29c283022100c38ec3b8c392037f1dc2bf6a455d15c3a0c3bb64c2ab16c3a36dc393650b65322e32c2901816c29c615f6a";
        let cbor = hex::decode(TEST_VECTOR).unwrap();
        minicbor::decode::<'_, PassportRequest>(&cbor).unwrap();
    }
}