From 0b26eac1d77dad27eb0e9c69546b33ed75144b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Mon, 26 Aug 2024 11:46:06 +0200 Subject: [PATCH] Fix W3C Bitstring status list `statusMessage.status` property de/serialization. (#605) Fixes #594 --- crates/claims/crates/jws/src/compact/bytes.rs | 8 +- crates/claims/crates/jws/src/lib.rs | 11 +- crates/claims/crates/jws/src/linked_data.rs | 118 ------------------ crates/claims/crates/vc/examples/sign.rs | 4 +- .../src/impl/bitstring_status_list/mod.rs | 31 ++++- .../impl/bitstring_status_list/syntax/mod.rs | 4 + .../syntax/status_list/mod.rs | 50 ++++++++ 7 files changed, 94 insertions(+), 132 deletions(-) delete mode 100644 crates/claims/crates/jws/src/linked_data.rs diff --git a/crates/claims/crates/jws/src/compact/bytes.rs b/crates/claims/crates/jws/src/compact/bytes.rs index 0f70fca63..90e3e0cb4 100644 --- a/crates/claims/crates/jws/src/compact/bytes.rs +++ b/crates/claims/crates/jws/src/compact/bytes.rs @@ -176,11 +176,11 @@ impl CompactJWS { /// Any type that providing a `JWKResolver` through the `ResolverProvider` /// trait will be fine. Notable implementors are: /// - [`VerificationParameters`](ssi_claims_core::VerificationParameters): - /// A good default providing many other common verification parameters that - /// are not necessary here. + /// A good default providing many other common verification parameters that + /// are not necessary here. /// - [`JWK`](ssi_jwk::JWK): allows you to put a JWK as `params`, which - /// will resolve into itself. Can be useful if you don't need key resolution - /// because you know in advance what key was used to sign the JWS. + /// will resolve into itself. Can be useful if you don't need key resolution + /// because you know in advance what key was used to sign the JWS. /// /// # Passing the parameters by reference /// diff --git a/crates/claims/crates/jws/src/lib.rs b/crates/claims/crates/jws/src/lib.rs index 8f4b64d09..7420dc3b0 100644 --- a/crates/claims/crates/jws/src/lib.rs +++ b/crates/claims/crates/jws/src/lib.rs @@ -94,9 +94,6 @@ pub use signature::*; mod verification; pub use verification::*; -#[cfg(feature = "linked-data")] -mod linked_data; - /// Decoded JWS. #[derive(Clone, PartialEq, Eq)] pub struct JWS> { @@ -202,11 +199,11 @@ impl DecodedJWS { /// a `JWKResolver` through the `ResolverProvider` trait. /// Notable implementors are: /// - [`VerificationParameters`](ssi_claims_core::VerificationParameters): - /// A good default providing many other common verification parameters that - /// are not necessary here. + /// A good default providing many other common verification parameters that + /// are not necessary here. /// - [`JWK`]: allows you to put a JWK as `params`, which - /// will resolve into itself. Can be useful if you don't need key resolution - /// because you know in advance what key was used to sign the JWS. + /// will resolve into itself. Can be useful if you don't need key resolution + /// because you know in advance what key was used to sign the JWS. /// /// # Passing the parameters by reference /// diff --git a/crates/claims/crates/jws/src/linked_data.rs b/crates/claims/crates/jws/src/linked_data.rs deleted file mode 100644 index 958804756..000000000 --- a/crates/claims/crates/jws/src/linked_data.rs +++ /dev/null @@ -1,118 +0,0 @@ -impl LinkedDataResource for CompactJWSString { - fn interpretation( - &self, - _vocabulary: &mut V, - _interpretation: &mut I, - ) -> linked_data::ResourceInterpretation { - use linked_data::{xsd_types::ValueRef, CowRdfTerm, RdfLiteralRef, ResourceInterpretation}; - ResourceInterpretation::Uninterpreted(Some(CowRdfTerm::Borrowed(RdfTermRef::Literal( - RdfLiteralRef::Xsd(ValueRef::String(&self.0)), - )))) - } -} - -impl LinkedDataSubject for CompactJWSString { - fn visit_subject(&self, serializer: S) -> Result - where - S: linked_data::SubjectVisitor, - { - serializer.end() - } -} - -impl LinkedDataDeserializeSubject for CompactJWSString -where - V: Vocabulary, - I: ReverseIriInterpretation + ReverseLiteralInterpretation, -{ - fn deserialize_subject_in( - vocabulary: &V, - interpretation: &I, - _dataset: &D, - _graph: Option<&I::Resource>, - resource: &I::Resource, - context: linked_data::Context, - ) -> Result - where - D: PatternMatchingDataset, - { - let mut literal_ty = None; - for l in interpretation.literals_of(resource) { - let literal = vocabulary.literal(l).unwrap(); - - match literal.type_ { - LiteralTypeRef::Any(ty) => { - let ty_iri = vocabulary.iri(ty).unwrap(); - - if ty_iri == linked_data::xsd_types::XSD_STRING { - return literal.value.parse().map_err(|_| { - linked_data::FromLinkedDataError::InvalidLiteral( - context.into_iris(vocabulary, interpretation), - ) - }); - } - - literal_ty = Some(ty_iri) - } - LiteralTypeRef::LangString(_) => literal_ty = Some(RDF_LANG_STRING), - } - } - - match literal_ty { - Some(ty) => Err(linked_data::FromLinkedDataError::LiteralTypeMismatch { - context: context.into_iris(vocabulary, interpretation), - expected: Some(linked_data::xsd_types::XSD_STRING.to_owned()), - found: ty.to_owned(), - }), - None => Err(linked_data::FromLinkedDataError::ExpectedLiteral( - context.into_iris(vocabulary, interpretation), - )), - } - } -} - -impl LinkedDataPredicateObjects for CompactJWSString { - fn visit_objects(&self, mut visitor: S) -> Result - where - S: linked_data::PredicateObjectsVisitor, - { - visitor.object(self)?; - visitor.end() - } -} - -impl LinkedDataDeserializePredicateObjects - for CompactJWSString -where - V: Vocabulary, - I: ReverseIriInterpretation + ReverseLiteralInterpretation, -{ - fn deserialize_objects_in<'a, D>( - vocabulary: &V, - interpretation: &I, - dataset: &D, - graph: Option<&I::Resource>, - objects: impl IntoIterator, - context: linked_data::Context, - ) -> Result - where - I::Resource: 'a, - D: PatternMatchingDataset, - { - let mut objects = objects.into_iter(); - match objects.next() { - Some(object) => { - if objects.next().is_none() { - Self::deserialize_subject(vocabulary, interpretation, dataset, graph, object) - } else { - Err(linked_data::FromLinkedDataError::TooManyValues( - context.into_iris(vocabulary, interpretation), - )) - } - } - None => Err(linked_data::FromLinkedDataError::MissingRequiredValue( - context.into_iris(vocabulary, interpretation), - )), - } - } -} diff --git a/crates/claims/crates/vc/examples/sign.rs b/crates/claims/crates/vc/examples/sign.rs index f7c5eb58e..915958c2d 100644 --- a/crates/claims/crates/vc/examples/sign.rs +++ b/crates/claims/crates/vc/examples/sign.rs @@ -14,7 +14,7 @@ use ssi_verification_methods::{ }; use static_iref::{iri, uri}; use std::{borrow::Cow, collections::HashMap, sync::Arc}; -use xsd_types::DateTime; +use xsd_types::{DateTime, DateTimeStamp}; #[derive(Clone, linked_data::Serialize, serde::Serialize)] #[serde(rename_all = "camelCase")] @@ -159,7 +159,7 @@ async fn main() { // Signature options, defining the crypto suite, signature date, // signing key and proof purpose. let proof_options = ProofOptions::new( - DateTime::now(), + DateTimeStamp::now(), iri!("https://example.com/controller#key").to_owned().into(), ProofPurpose::Assertion, (), diff --git a/crates/status/src/impl/bitstring_status_list/mod.rs b/crates/status/src/impl/bitstring_status_list/mod.rs index a7f777c19..5cca39a4b 100644 --- a/crates/status/src/impl/bitstring_status_list/mod.rs +++ b/crates/status/src/impl/bitstring_status_list/mod.rs @@ -1,4 +1,4 @@ -//! W3C Bitstring Status List v1.0 +//! W3C Bitstring Status List v1.0 (Working Draft 06 April 2024) //! //! A privacy-preserving, space-efficient, and high-performance mechanism for //! publishing status information such as suspension or revocation of Verifiable @@ -17,10 +17,17 @@ pub use syntax::*; #[derive(Debug, Serialize, Deserialize)] pub struct StatusMessage { + #[serde(with = "prefixed_hexadecimal")] pub status: u8, pub message: String, } +impl StatusMessage { + pub fn new(status: u8, message: String) -> Self { + Self { status, message } + } +} + #[derive(Debug, thiserror::Error)] #[error("invalid status size `{0}`")] pub struct InvalidStatusSize(u8); @@ -545,6 +552,28 @@ impl StatusMap for StatusList { } } +mod prefixed_hexadecimal { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize(value: &u8, serializer: S) -> Result + where + S: Serializer, + { + format!("{value:#x}").serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let string = String::deserialize(deserializer)?; + let number = string + .strip_prefix("0x") + .ok_or_else(|| serde::de::Error::custom("missing `0x` prefix"))?; + u8::from_str_radix(number, 16).map_err(serde::de::Error::custom) + } +} + #[cfg(test)] mod tests { use rand::{rngs::StdRng, RngCore, SeedableRng}; diff --git a/crates/status/src/impl/bitstring_status_list/syntax/mod.rs b/crates/status/src/impl/bitstring_status_list/syntax/mod.rs index d3bf318a5..e7f0b6475 100644 --- a/crates/status/src/impl/bitstring_status_list/syntax/mod.rs +++ b/crates/status/src/impl/bitstring_status_list/syntax/mod.rs @@ -26,6 +26,10 @@ impl EncodedList { /// 16MB. pub const DEFAULT_LIMIT: u64 = 16 * 1024 * 1024; + pub fn new(value: String) -> Self { + Self(value) + } + pub fn encode(bytes: &[u8]) -> Self { let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); encoder.write_all(bytes).unwrap(); diff --git a/crates/status/src/impl/bitstring_status_list/syntax/status_list/mod.rs b/crates/status/src/impl/bitstring_status_list/syntax/status_list/mod.rs index 3bddbacd9..36c99d7d7 100644 --- a/crates/status/src/impl/bitstring_status_list/syntax/status_list/mod.rs +++ b/crates/status/src/impl/bitstring_status_list/syntax/status_list/mod.rs @@ -62,3 +62,53 @@ impl BitstringStatusList { Ok(StatusList::from_bytes(self.status_size, bytes, self.ttl)) } } + +#[cfg(test)] +mod tests { + use super::BitstringStatusList; + use crate::bitstring_status_list::{EncodedList, StatusMessage, StatusPurpose, TimeToLive}; + + const STATUS_LIST: &str = r#"{ + "id": "https://example.com/status/3#list", + "type": "BitstringStatusList", + "ttl": 500, + "statusPurpose": "message", + "statusReference": "https://example.org/status-dictionary/", + "statusSize": 2, + "statusMessage": [ + {"status":"0x0", "message":"valid"}, + {"status":"0x1", "message":"invalid"}, + {"status":"0x2", "message":"pending_review"} + ], + "encodedList": "uH4sIAAAAAAAAA-3BMQEAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA" + }"#; + + #[test] + fn deserialize() { + serde_json::from_str::(STATUS_LIST).unwrap(); + } + + #[test] + fn serialize() { + let expected: serde_json::Value = serde_json::from_str(STATUS_LIST).unwrap(); + + let status_list = BitstringStatusList { + id: Some("https://example.com/status/3#list".parse().unwrap()), + ttl: TimeToLive(500), + status_purpose: StatusPurpose::Message, + status_reference: Some("https://example.org/status-dictionary/".parse().unwrap()), + status_size: 2.try_into().unwrap(), + status_message: vec![ + StatusMessage::new(0, "valid".to_owned()), + StatusMessage::new(1, "invalid".to_owned()), + StatusMessage::new(2, "pending_review".to_owned()), + ], + encoded_list: EncodedList::new( + "uH4sIAAAAAAAAA-3BMQEAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA".to_owned(), + ), + }; + + let value = serde_json::to_value(status_list).unwrap(); + assert_eq!(value, expected); + } +}