wolfcose 0.1.0

Safe Rust API for wolfSSL wolfCOSE.
#![allow(missing_docs)]

use wolfcose::{
    from_slice, from_slice_detailed, to_vec, Algorithm, AlgorithmClass, ByteBuf, ByteStr,
    CborDeserialize, CborItemReader, CborSerialize, CborSerializer, CborValue, CoseKeyBuilder,
    CoseKeyView, CoseSign1Message, Error, HeaderLabel, HeaderMap, HeaderValue, PayloadMode,
    ProtectedHeader, SymmetricKey,
};

#[derive(Debug, Eq, PartialEq, CborSerialize, CborDeserialize)]
#[cbor(rename_all = "snake_case")]
struct DerivedOptions {
    device_id: u32,
    #[cbor(skip_serializing_if = "Option::is_none", default)]
    display_name: Option<String>,
}

#[derive(Debug, Eq, PartialEq, CborSerialize, CborDeserialize)]
#[cbor(transparent)]
struct TransparentId(u32);

#[derive(Debug, Eq, PartialEq, CborSerialize, CborDeserialize)]
enum Event {
    Started,
    Reading(u32, String),
    #[cbor(rename = "device_state")]
    State {
        online: bool,
    },
}

mod plus_one {
    pub fn serialize(
        value: &u8,
        serializer: &mut wolfcose::CborSerializer<'_>,
    ) -> wolfcose::Result<()> {
        (value + 1).serialize(serializer)
    }

    pub fn deserialize(deserializer: &mut wolfcose::CborDeserializer<'_>) -> wolfcose::Result<u8> {
        Ok(u8::deserialize(deserializer)? - 1)
    }

    use wolfcose::{CborDeserialize, CborSerialize};
}

#[derive(Debug, Eq, PartialEq, CborSerialize, CborDeserialize)]
struct WithModule {
    #[cbor(with = "plus_one")]
    value: u8,
}

#[test]
fn algorithm_metadata_and_key_builder_work() {
    assert_eq!(Algorithm::A128GCM.class(), AlgorithmClass::Encryption);
    assert!(Algorithm::ES256.is_signing());
    assert!(Algorithm::HMAC256.is_mac());
    assert!(Algorithm::DIRECT.is_key_management());
    assert_eq!(Algorithm::A128GCM.key_bits_hint(), Some(128));
    assert_eq!(Algorithm::A128GCM.iv_len_hint(), Some(12));

    let key = CoseKeyBuilder::symmetric([0u8; 16])
        .algorithm(Algorithm::A128GCM)
        .kid(b"k1")
        .has_private(true)
        .build()
        .unwrap();
    let view = CoseKeyView::from_key(&key);
    assert_eq!(view.algorithm, Algorithm::A128GCM);
    assert_eq!(view.kid, b"k1");
    assert_eq!(view.symmetric_len, Some(16));
    assert!(view.has_private);

    assert_eq!(
        CoseKeyBuilder::symmetric([0u8; 15])
            .algorithm(Algorithm::A128GCM)
            .build()
            .err(),
        Some(Error::CoseKeyType)
    );

    let key = SymmetricKey::new([0u8; 32])
        .with_algorithm(Algorithm::A256GCM)
        .with_kid(b"k2")
        .into_cose_key()
        .unwrap();
    assert_eq!(CoseKeyView::from_key(&key).symmetric_len, Some(32));
}

#[test]
fn key_debug_output_redacts_symmetric_material() {
    let key = SymmetricKey::new([1u8, 2, 3, 4])
        .with_algorithm(Algorithm::A128GCM)
        .with_kid(b"kid");
    let debug = format!("{key:?}");
    assert!(debug.contains("material_len"));
    assert!(!debug.contains("[1, 2, 3, 4]"));

    let builder = CoseKeyBuilder::symmetric([5u8, 6, 7, 8]).algorithm(Algorithm::A128GCM);
    let debug = format!("{builder:?}");
    assert!(debug.contains("symmetric_len"));
    assert!(!debug.contains("[5, 6, 7, 8]"));
}

#[test]
fn header_map_and_sign1_message_parse() {
    let mut protected = HeaderMap::new();
    protected.insert(
        HeaderLabel::Algorithm,
        HeaderValue::Algorithm(Algorithm::HMAC256),
    );
    let protected_bytes = to_vec(&protected).unwrap();

    let mut unprotected = HeaderMap::new();
    unprotected.insert(HeaderLabel::Kid, HeaderValue::Bytes(b"kid".to_vec()));
    unprotected.insert(
        HeaderLabel::Text("vendor".to_owned()),
        HeaderValue::Text("wolf".to_owned()),
    );

    let mut out = [0u8; 256];
    let mut serializer = CborSerializer::new(&mut out);
    serializer
        .tag(wolfcose::raw::WOLFCOSE_TAG_SIGN1 as u64)
        .unwrap();
    serializer.array(4).unwrap();
    ByteStr(&protected_bytes)
        .serialize(&mut serializer)
        .unwrap();
    unprotected.serialize(&mut serializer).unwrap();
    Option::<u8>::None.serialize(&mut serializer).unwrap();
    ByteStr(b"signature").serialize(&mut serializer).unwrap();

    let message = CoseSign1Message::parse(serializer.as_written()).unwrap();
    assert_eq!(message.protected().algorithm(), Some(Algorithm::HMAC256));
    assert_eq!(message.unprotected().kid(), Some(&b"kid"[..]));
    assert!(!message.payload_attached());
    assert_eq!(message.raw(), serializer.as_written());

    assert_eq!(
        ProtectedHeader::from_bstr(&protected_bytes)
            .unwrap()
            .0
            .algorithm(),
        Some(Algorithm::HMAC256)
    );
}

#[test]
fn serializer_helpers_stream_and_detailed_errors_work() {
    let mut out = [0u8; 128];
    let mut serializer = CborSerializer::new(&mut out);
    serializer.sequence([1u8, 2, 3]).unwrap();
    let decoded: Vec<u8> = from_slice(serializer.as_written()).unwrap();
    assert_eq!(decoded, vec![1, 2, 3]);

    let mut map_out = [0u8; 128];
    let mut serializer = CborSerializer::new(&mut map_out);
    serializer.map_entries([("a", 1u8), ("b", 2u8)]).unwrap();
    let value: CborValue = from_slice(serializer.as_written()).unwrap();
    assert!(matches!(value, CborValue::Map(_)));

    let encoded = to_vec(&(1u8, 2u8)).unwrap();
    let mut reader = CborItemReader::new(&encoded);
    assert!(reader.skip_next().unwrap());
    assert!(reader.is_finished());

    let mut trailing = to_vec(&1u8).unwrap();
    trailing.push(0);
    let err = from_slice_detailed::<u8>(&trailing).unwrap_err();
    assert_eq!(err.error, Error::CborMalformed);
}

#[test]
fn derive_macro_enhancements_round_trip() {
    let options = DerivedOptions {
        device_id: 7,
        display_name: None,
    };
    let value: CborValue = from_slice(&to_vec(&options).unwrap()).unwrap();
    let CborValue::Map(entries) = value else {
        panic!("expected map");
    };
    assert_eq!(entries.len(), 1);
    assert_eq!(entries[0].0, CborValue::Text("device_id".to_owned()));
    assert_eq!(
        from_slice::<DerivedOptions>(&to_vec(&options).unwrap()).unwrap(),
        options
    );

    let transparent = TransparentId(9);
    assert_eq!(
        from_slice::<TransparentId>(&to_vec(&transparent).unwrap()).unwrap(),
        transparent
    );

    for event in [
        Event::Started,
        Event::Reading(3, "temp".to_owned()),
        Event::State { online: true },
    ] {
        assert_eq!(
            from_slice::<Event>(&to_vec(&event).unwrap()).unwrap(),
            event
        );
    }

    let with_module = WithModule { value: 4 };
    assert_eq!(
        from_slice::<WithModule>(&to_vec(&with_module).unwrap()).unwrap(),
        with_module
    );
    let encoded: CborValue = from_slice(&to_vec(&with_module).unwrap()).unwrap();
    assert!(format!("{encoded:?}").contains('5'));
}

#[test]
fn payload_mode_remains_explicit() {
    assert_eq!(PayloadMode::Attached(b"a"), PayloadMode::Attached(b"a"));
    assert_ne!(PayloadMode::Attached(b"a"), PayloadMode::Detached(b"a"));
    assert_eq!(ByteBuf(b"x".to_vec()).into_vec(), b"x");
}