From 42175c9dca63dbc6986b82c3ad6b055386c7a0c7 Mon Sep 17 00:00:00 2001 From: Neal Date: Wed, 28 Aug 2024 13:36:06 -0700 Subject: [PATCH 01/10] cred status list impl --- crates/web5/Cargo.toml | 3 +- crates/web5/src/credentials/mod.rs | 1 + .../src/credentials/status_list_credential.rs | 482 ++++++++++++++++++ .../credentials/verifiable_credential_1_1.rs | 21 + 4 files changed, 506 insertions(+), 1 deletion(-) create mode 100644 crates/web5/src/credentials/status_list_credential.rs diff --git a/crates/web5/Cargo.toml b/crates/web5/Cargo.toml index 9ea9d4c6..7e20d091 100644 --- a/crates/web5/Cargo.toml +++ b/crates/web5/Cargo.toml @@ -8,7 +8,7 @@ license-file.workspace = true rust-version = "1.74.0" [dependencies] -base64 = { workspace = true } +base64.workspace = true byteorder = "1.5.0" chrono = { workspace = true } ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } @@ -30,6 +30,7 @@ uuid = { workspace = true } x25519-dalek = { version = "2.0.1", features = ["getrandom", "static_secrets"] } zbase32 = "0.1.2" lazy_static = "1.5.0" +flate2 = "1.0.33" [dev-dependencies] mockito = "1.5.0" diff --git a/crates/web5/src/credentials/mod.rs b/crates/web5/src/credentials/mod.rs index 6a24f0da..1f639ca0 100644 --- a/crates/web5/src/credentials/mod.rs +++ b/crates/web5/src/credentials/mod.rs @@ -9,6 +9,7 @@ use super::dids::resolution::resolution_metadata::ResolutionMetadataError; pub mod presentation_definition; pub mod verifiable_credential_1_1; +mod status_list_credential; #[derive(thiserror::Error, Debug)] pub enum CredentialError { diff --git a/crates/web5/src/credentials/status_list_credential.rs b/crates/web5/src/credentials/status_list_credential.rs new file mode 100644 index 00000000..3f7460f0 --- /dev/null +++ b/crates/web5/src/credentials/status_list_credential.rs @@ -0,0 +1,482 @@ +use std::collections::HashMap; +use std::io::{Read, Write}; + +use base64::Engine; +use flate2::{read::GzDecoder, write::GzEncoder, Compression}; + +use crate::errors::{Result, Web5Error}; +use crate::json::{JsonObject, JsonValue}; +use super::verifiable_credential_1_1::{CredentialSubject, Issuer, VerifiableCredential, VerifiableCredentialCreateOptions}; + +pub const STATUS_LIST_CREDENTIAL_CONTEXT: &str = "https://w3id.org/vc/status-list/2021/v1"; +pub const STATUS_LIST_CREDENTIAL_TYPE: &str = "StatusList2021Credential"; +pub const STATUS_LIST_2021: &str = "StatusList2021"; + +pub struct StatusListCredential { + base: VerifiableCredential, + encoded_list: String +} + +impl StatusListCredential { + pub fn create( + issuer: Issuer, + status_purpose: String, + credentials_to_disable: Option>, + ) -> Result { + // Determine the status list indexes based on the provided credentials to disable. + let status_list_indexes = match credentials_to_disable { + Some(credentials) => Self::get_status_list_indexes(&status_purpose, credentials)?, + None => Vec::new(), + }; + + // Generate the base64 bitstring from the status list indexes. + let base64_bitstring = Self::bitstring_generation(status_list_indexes)?; + + // Construct the properties for the credential subject. + let additional_properties = { + let mut properties = HashMap::new(); + properties.insert( + "statusPurpose".to_string(), + JsonValue::String(status_purpose.clone()), + ); + properties.insert( + "type".to_string(), + JsonValue::String(STATUS_LIST_2021.to_string()), + ); + properties.insert( + "encodedList".to_string(), + JsonValue::String(base64_bitstring.clone()), + ); + JsonObject { properties } + }; + + let credential_subject = CredentialSubject { + id: format!("urn:uuid:{}", uuid::Uuid::new_v4()), + additional_properties: Some(additional_properties), + }; + + let vc_options = VerifiableCredentialCreateOptions { + id: Some(format!("urn:uuid:{}", uuid::Uuid::new_v4())), + context: Some(vec![STATUS_LIST_CREDENTIAL_CONTEXT.to_string()]), + r#type: Some(vec![STATUS_LIST_CREDENTIAL_TYPE.to_string()]), + issuance_date: None, + expiration_date: None, + credential_status: None, + }; + + let verifiable_credential = VerifiableCredential::create( + issuer, + credential_subject, + Some(vc_options), + )?; + + Ok(Self { + base: verifiable_credential, + encoded_list: base64_bitstring, + }) + } + + /// Updates the `encoded_list` by adding the provided credentials to the disable list + /// + /// # Arguments + /// * `credentials_to_disable` - A vector of `VerifiableCredential` objects to be disabled. + /// + /// # Returns + /// A `Result` containing the updated `StatusListCredential`, or an error if there was an issue. + pub fn update_credentials_to_disable( + &mut self, + credentials_to_disable: Vec, + ) -> Result<()> { + // Retrieve the statusPurpose from the existing credential subject + let status_purpose = self.base.credential_subject.additional_properties + .as_ref() + .and_then(|props| props.properties.get("statusPurpose")) + .and_then(|value| match value { + JsonValue::String(s) => Some(s.clone()), + _ => None, + }) + .ok_or_else(|| Web5Error::Parameter("no statusPurpose found in status list credential".to_string()))?; + + // Determine the status list indexes based on the provided credentials to disable. + let status_list_indexes = Self::get_status_list_indexes(&status_purpose, credentials_to_disable)?; + + // Generate the base64 bitstring from the status list indexes. + let base64_bitstring = Self::bitstring_generation(status_list_indexes)?; + + // Update the encodedList property in the additional_properties + if let Some(additional_properties) = &mut self.base.credential_subject.additional_properties { + additional_properties.properties.insert( + "encodedList".to_string(), + JsonValue::String(base64_bitstring.clone()), + ); + } + + // Update the encoded_list in the StatusListCredential struct + self.encoded_list = base64_bitstring; + + Ok(()) + } + + /// Checks if a given credential is disabled according to this Status List Credential. + /// + /// # Arguments + /// + /// * `credential` - The `VerifiableCredential` to check. + /// + /// # Returns + /// + /// * `Ok(true)` if the credential is disabled, `Ok(false)` otherwise. + /// * `Err` if the credential status is invalid or incompatible. + /// + /// # Example + /// + /// let is_disabled = status_list_credential.is_disabled(&credential_to_check)?; + /// println!("Credential is disabled: {}", is_disabled); + /// + pub fn is_disabled(&self, credential: &VerifiableCredential) -> Result { + let status = credential.credential_status.as_ref().ok_or_else(|| { + Web5Error::Parameter("no credential status found in credential".to_string()) + })?; + + // Check if the status type matches + if status.r#type != STATUS_LIST_2021 { + return Err(Web5Error::Parameter(format!("unsupported status type: {}", status.r#type))); + } + + // Check if the status purpose matches + let credential_subject = self.base.credential_subject.additional_properties.as_ref().ok_or_else(|| { + Web5Error::Parameter("no additional properties found in status list credential".to_string()) + })?; + + let status_purpose = credential_subject.properties.get("statusPurpose").ok_or_else(|| { + Web5Error::Parameter("no statusPurpose found in status list credential".to_string()) + })?; + + if let JsonValue::String(purpose) = status_purpose { + if *purpose != status.status_purpose { + return Err(Web5Error::Parameter("status purpose mismatch".to_string())); + } + } else { + return Err(Web5Error::Parameter("invalid statusPurpose format".to_string())); + } + + // Get the bit index + let index = status.status_list_index.parse::().map_err(|_| { + Web5Error::Parameter(format!("invalid status list index: {}", status.status_list_index)) + })?; + + // Check the bit in the encoded list + Self::get_bit(&self.encoded_list, index) + } + + /// Extracts status list indexes from a vector of verifiable credentials that match the specified status purpose. + /// + /// # Arguments + /// * `status_purpose` - The status purpose to match. + /// * `credentials` - A vector of `VerifiableCredential` objects. + /// + /// # Returns + /// A `Result` containing a vector of `usize` indexes, or an error if a credential is missing + fn get_status_list_indexes( + status_purpose: &str, + credentials: Vec, + ) -> Result> { + let mut status_list_indexes = Vec::new(); + + for vc in credentials { + let status_list_entry = vc.credential_status.as_ref().ok_or_else(|| { + Web5Error::Parameter("no credential status found in credential".to_string()) + })?; + + if status_list_entry.status_purpose != *status_purpose { + return Err(Web5Error::Parameter("status purpose mismatch".to_string())); + } + + let index = status_list_entry.status_list_index.parse::().map_err(|_| { + Web5Error::Parameter(format!("invalid status list index: {}", status_list_entry.status_list_index)) + })?; + + status_list_indexes.push(index); + } + + Ok(status_list_indexes) + } + + /// Generates a compressed, base64-encoded bitstring from a list of status list indexes. + /// + /// # Arguments + /// * `status_list_indexes` - A vector of indexes to set in the bitstring. + /// + /// # Returns + /// A `Result` containing the compressed, base64-encoded bitstring, or an error if an index is out of range. + fn bitstring_generation(status_list_indexes: Vec) -> Result { + const BITSET_SIZE: usize = 16 * 1024 * 8; + let mut bit_vec = vec![0u8; BITSET_SIZE / 8]; + + for index in status_list_indexes { + if index >= BITSET_SIZE { + return Err(Web5Error::Parameter(format!( + "invalid status list index: {}, index is larger than the bitset size", + index + ))); + } + let byte_index = index / 8; + let bit_index = 7 - (index % 8); + bit_vec[byte_index] |= 1 << bit_index; + } + + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(&bit_vec) + .map_err(|e| Web5Error::Parameter(format!("encoder write_all issue while creating bitstring: {}", e)))?; + let compressed = encoder.finish() + .map_err(|e| Web5Error::Parameter(format!("encoder finish issue while creating bitstring: {}", e)))?; + + Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&compressed)) + } + + /// Retrieves the value of a specific bit from a compressed base64 URL-encoded bitstring + /// by decoding and decompressing a bitstring, then extracting a bit's value by its index. + /// + /// # Arguments + /// * `compressed_bitstring` - A base64 URL-encoded string representing the compressed bitstring. + /// * `bit_index` - The zero-based index of the bit to retrieve from the decompressed bitstream. + /// + /// # Returns + /// `true` if the bit at the specified index is 1, `false` if it is 0. + fn get_bit(compressed_bitstring: &str, bit_index: usize) -> Result { + // Base64-decode the compressed bitstring + let compressed_data = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(compressed_bitstring) + .map_err(|e| Web5Error::Parameter(format!("failed to decode base64: {}", e)))?; + + // Decompress the data using GZIP + let mut decoder = GzDecoder::new(&compressed_data[..]); + let mut decompressed_data = Vec::new(); + decoder + .read_to_end(&mut decompressed_data) + .map_err(|e| Web5Error::Parameter(format!("failed to decompress data: {}", e)))?; + + // Find the byte index, and bit index within the byte + let byte_index = bit_index / 8; + let bit_index_within_byte = 7 - (bit_index % 8); + let byte = decompressed_data + .get(byte_index) + .ok_or_else(|| Web5Error::Parameter("bit index out of range in decompressed data".into()))?; + + // Extract the targeted bit + let bit_integer = (byte >> bit_index_within_byte) & 1; + + Ok(bit_integer == 1) + } +} + +#[cfg(test)] +mod tests { + use crate::credentials::verifiable_credential_1_1::{CredentialStatus, BASE_CONTEXT, BASE_TYPE}; + use super::*; + + const ISSUER_DID_URI: &str = "did:web:tbd.website"; + const SUBJECT_DID_URI: &str = "did:dht:qgmmpyjw5hwnqfgzn7wmrm33ady8gb8z9ideib6m9gj4ys6wny8y"; + + fn issuer() -> Issuer { + Issuer::from(ISSUER_DID_URI) + } + + fn credential_subject() -> CredentialSubject { + CredentialSubject::from(SUBJECT_DID_URI) + } + + fn create_test_credential(index: &str, purpose: &str) -> VerifiableCredential { + let credential_status = CredentialStatus { + id: format!("https://example.com/status/{}", index), + r#type: STATUS_LIST_2021.to_string(), + status_purpose: purpose.to_string(), + status_list_index: index.to_string(), + status_list_credential: "https://example.com/status/1".to_string() + }; + + VerifiableCredential::create( + Issuer::from("did:example:issuer"), + CredentialSubject::from("did:example:subject"), + Some(VerifiableCredentialCreateOptions { + credential_status: Some(credential_status), + ..Default::default() + }) + ).unwrap() + } + + fn create_test_credential_with_type(index: &str, status_type: &str, purpose: &str) -> VerifiableCredential { + let mut credential = create_test_credential(index, purpose); + if let Some(status) = &mut credential.credential_status { + status.r#type = status_type.to_string(); + } + credential + } + + #[test] + fn test_create_status_list_credential2() { + let issuer = Issuer::from("did:example:123".to_string()); + let status_purpose = "revocation".to_string(); + let credentials_to_disable = None; + + let result = StatusListCredential::create(issuer, status_purpose, credentials_to_disable); + + assert!(result.is_ok()); + let status_list_credential = result.unwrap(); + + assert_eq!(status_list_credential.base.r#type, vec![BASE_TYPE.to_string(), STATUS_LIST_CREDENTIAL_TYPE.to_string()]); + assert_eq!(status_list_credential.base.context, vec![BASE_CONTEXT.to_string(), STATUS_LIST_CREDENTIAL_CONTEXT.to_string()]); + + let additional_properties = status_list_credential.base.credential_subject.additional_properties.unwrap(); + assert_eq!(additional_properties.properties.get("statusPurpose").unwrap(), &JsonValue::String("revocation".to_string())); + assert_eq!(additional_properties.properties.get("type").unwrap(), &JsonValue::String(STATUS_LIST_2021.to_string())); + assert!(additional_properties.properties.get("encodedList").is_some()); + } + + #[test] + fn test_get_bit() { + let bit_indices = vec![3, 1023]; + + let bitstring = StatusListCredential::bitstring_generation(bit_indices).unwrap(); + + assert_eq!(StatusListCredential::get_bit(&bitstring, 3).unwrap(), true); + assert_eq!(StatusListCredential::get_bit(&bitstring, 1023).unwrap(), true); + assert_eq!(StatusListCredential::get_bit(&bitstring, 0).unwrap(), false); + assert_eq!(StatusListCredential::get_bit(&bitstring, 1024).unwrap(), false); + + let result = StatusListCredential::get_bit(&bitstring, 16 * 1024 * 8 + 1); + assert!(result.is_err()); + } + + #[test] + fn test_is_disabled() -> Result<()> { + // Create a StatusListCredential with some disabled credentials + let issuer = Issuer::from("did:example:issuer"); + let status_purpose = "revocation".to_string(); + let credentials_to_disable = Some(vec![ + create_test_credential("3", &status_purpose), + create_test_credential("1023", &status_purpose), + ]); + let status_list_credential = StatusListCredential::create(issuer, status_purpose.clone(), credentials_to_disable)?; + + // Test 1: Check a disabled credential (index 3) + let disabled_credential = create_test_credential("3", &status_purpose); + assert!(status_list_credential.is_disabled(&disabled_credential)?); + + // Test 2: Check another disabled credential (index 1023) + let another_disabled_credential = create_test_credential("1023", &status_purpose); + assert!(status_list_credential.is_disabled(&another_disabled_credential)?); + + // Test 3: Check an enabled credential (index 5) + let enabled_credential = create_test_credential("5", &status_purpose); + assert!(!status_list_credential.is_disabled(&enabled_credential)?); + + // Test 4: Check a credential with mismatched status type + let mismatched_type_credential = create_test_credential_with_type("7", "InvalidType", &status_purpose); + assert!(status_list_credential.is_disabled(&mismatched_type_credential).is_err()); + + // Test 5: Check a credential with mismatched status purpose + let mismatched_purpose_credential = create_test_credential("9", "suspension"); + assert!(status_list_credential.is_disabled(&mismatched_purpose_credential).is_err()); + + // Test 6: Check a credential without a status + let no_status_credential = VerifiableCredential::create( + Issuer::from("did:example:issuer"), + CredentialSubject::from("did:example:subject"), + None, + )?; + assert!(status_list_credential.is_disabled(&no_status_credential).is_err()); + + Ok(()) + } + + #[test] + fn test_update_credentials_to_disable() { + let status_purpose = "revocation".to_string(); + + // Step 1: Create an initial StatusListCredential without any disabled credentials + let mut status_list_credential = StatusListCredential::create(issuer(), status_purpose.clone(), None).unwrap(); + + // Verify initial state: No bits should be set in the encoded list + assert_eq!(StatusListCredential::get_bit(&status_list_credential.encoded_list, 3).unwrap(), false); + assert_eq!(StatusListCredential::get_bit(&status_list_credential.encoded_list, 1023).unwrap(), false); + + // Step 2: Create test credentials to disable + let credentials_to_disable = vec![ + create_test_credential("3", &status_purpose), + create_test_credential("1023", &status_purpose), + ]; + + // Step 3: Update the StatusListCredential with these credentials + status_list_credential.update_credentials_to_disable(credentials_to_disable).unwrap(); + + // Step 4: Verify the encoded list is updated correctly + assert_eq!(StatusListCredential::get_bit(&status_list_credential.encoded_list, 3).unwrap(), true); + assert_eq!(StatusListCredential::get_bit(&status_list_credential.encoded_list, 1023).unwrap(), true); + + // Step 5: Verify other bits remain unset + assert_eq!(StatusListCredential::get_bit(&status_list_credential.encoded_list, 0).unwrap(), false); + assert_eq!(StatusListCredential::get_bit(&status_list_credential.encoded_list, 1000).unwrap(), false); + + // Step 6: Verify that the base additional properties got updated + let additional_properties = status_list_credential.base.credential_subject.additional_properties.as_ref().unwrap(); + assert_eq!( + additional_properties.properties.get("encodedList").unwrap(), + &JsonValue::String(status_list_credential.encoded_list.clone()) + ); + } + + #[test] + fn test_full_flow() { + let status_purpose = "revocation".to_string(); + let credentials_to_disable = None; + let status_list_credential = StatusListCredential::create(issuer(), status_purpose, credentials_to_disable).unwrap(); + + let encoded_list = &status_list_credential.encoded_list; + + // Test various bit positions + assert_eq!(StatusListCredential::get_bit(encoded_list, 0).unwrap(), false); + assert_eq!(StatusListCredential::get_bit(encoded_list, 100).unwrap(), false); + assert_eq!(StatusListCredential::get_bit(encoded_list, 1000).unwrap(), false); + + let vc1_options = Some(VerifiableCredentialCreateOptions { + credential_status: Some(CredentialStatus { + id: "https://example.com/status/1".to_string(), + r#type: STATUS_LIST_2021.to_string(), + status_purpose: "revocation".to_string(), + status_list_index: "3".to_string(), + status_list_credential: "https://example.com/status/1".to_string() + }), + ..Default::default() + }); + + let vc1 = VerifiableCredential::create(issuer(), credential_subject(), vc1_options).unwrap(); + + let vc2_options = Some(VerifiableCredentialCreateOptions { + credential_status: Some(CredentialStatus { + id: "https://example.com/status/2".to_string(), + r#type: STATUS_LIST_2021.to_string(), + status_purpose: "revocation".to_string(), + status_list_index: "1023".to_string(), + status_list_credential: "https://example.com/status/1".to_string() + }), + ..Default::default() + }); + + let vc2 = VerifiableCredential::create(issuer(), credential_subject(), vc2_options).unwrap(); + + let credentials_to_disable = Some(vec![vc1, vc2]); + + let updated_status_list_credential = StatusListCredential::create(Issuer::from("did:example:123".to_string()), "revocation".to_string(), credentials_to_disable).unwrap(); + let updated_encoded_list = &updated_status_list_credential.encoded_list; + + // Test the bits corresponding to the disabled credentials + assert_eq!(StatusListCredential::get_bit(updated_encoded_list, 3).unwrap(), true); + assert_eq!(StatusListCredential::get_bit(updated_encoded_list, 1023).unwrap(), true); + + // Test other bits are still false + assert_eq!(StatusListCredential::get_bit(updated_encoded_list, 0).unwrap(), false); + assert_eq!(StatusListCredential::get_bit(updated_encoded_list, 100).unwrap(), false); + assert_eq!(StatusListCredential::get_bit(updated_encoded_list, 1000).unwrap(), false); + } +} diff --git a/crates/web5/src/credentials/verifiable_credential_1_1.rs b/crates/web5/src/credentials/verifiable_credential_1_1.rs index 26ac28d2..8978f684 100644 --- a/crates/web5/src/credentials/verifiable_credential_1_1.rs +++ b/crates/web5/src/credentials/verifiable_credential_1_1.rs @@ -124,6 +124,21 @@ pub struct VerifiableCredential { deserialize_with = "deserialize_optional_system_time" )] pub expiration_date: Option, + #[serde(rename = "credentialStatus")] + pub credential_status: Option, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +pub struct CredentialStatus { + pub id: String, + #[serde(rename = "type")] + pub r#type: String, + #[serde(rename = "statusPurpose")] + pub status_purpose: String, + #[serde(rename = "statusListIndex")] + pub status_list_index: String, + #[serde(rename = "statusListCredential")] + pub status_list_credential: String, } impl FromJson for VerifiableCredential {} @@ -136,6 +151,7 @@ pub struct VerifiableCredentialCreateOptions { pub r#type: Option>, pub issuance_date: Option, pub expiration_date: Option, + pub credential_status: Option } impl VerifiableCredential { @@ -202,6 +218,7 @@ impl VerifiableCredential { issuance_date: options.issuance_date.unwrap_or_else(SystemTime::now), expiration_date: options.expiration_date, credential_subject, + credential_status: options.credential_status }) } @@ -225,6 +242,7 @@ impl VerifiableCredential { issuance_date: Some(self.issuance_date), expiration_date: self.expiration_date, credential_subject: Some(self.credential_subject.clone()), + credential_status: self.credential_status.clone() }; payload.set_claim("vc", Some(serde_json::to_value(vc_claim)?))?; payload.set_issuer(self.issuer.to_string()); @@ -381,6 +399,7 @@ impl VerifiableCredential { issuance_date: nbf, expiration_date: exp, credential_subject: vc_credential_subject, + credential_status: vc_payload.credential_status }; validate_vc_data_model(&vc)?; @@ -467,6 +486,8 @@ struct JwtPayloadVerifiableCredential { expiration_date: Option, #[serde(skip_serializing_if = "Option::is_none", rename = "credentialSubject")] credential_subject: Option, + #[serde(rename = "credentialSubject")] + pub credential_status: Option, } #[derive(Clone)] From f86157bdaa90cda455c92a29890a80ee15e6915a Mon Sep 17 00:00:00 2001 From: Neal Date: Wed, 28 Aug 2024 13:37:39 -0700 Subject: [PATCH 02/10] update --- crates/web5/src/credentials/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/web5/src/credentials/mod.rs b/crates/web5/src/credentials/mod.rs index 1f639ca0..cf667ab8 100644 --- a/crates/web5/src/credentials/mod.rs +++ b/crates/web5/src/credentials/mod.rs @@ -9,7 +9,7 @@ use super::dids::resolution::resolution_metadata::ResolutionMetadataError; pub mod presentation_definition; pub mod verifiable_credential_1_1; -mod status_list_credential; +pub mod status_list_credential; #[derive(thiserror::Error, Debug)] pub enum CredentialError { From fb926797d09a3fa0ddf396f455e7ff895f548d4b Mon Sep 17 00:00:00 2001 From: Neal Date: Wed, 28 Aug 2024 15:36:11 -0700 Subject: [PATCH 03/10] lint and bind --- bindings/web5_uniffi/src/lib.rs | 5 +- bindings/web5_uniffi/src/web5.udl | 10 +- .../credentials/verifiable_credential_1_1.rs | 6 +- .../src/main/kotlin/web5/sdk/rust/UniFFI.kt | 86 ++++- crates/web5/src/credentials/mod.rs | 2 +- .../src/credentials/status_list_credential.rs | 301 +++++++++++++----- .../credentials/verifiable_credential_1_1.rs | 8 +- 7 files changed, 327 insertions(+), 91 deletions(-) diff --git a/bindings/web5_uniffi/src/lib.rs b/bindings/web5_uniffi/src/lib.rs index eaee1a6b..208218f8 100644 --- a/bindings/web5_uniffi/src/lib.rs +++ b/bindings/web5_uniffi/src/lib.rs @@ -29,7 +29,10 @@ use web5_uniffi_wrapper::{ }; use web5::{ - credentials::verifiable_credential_1_1::VerifiableCredentialCreateOptions as VerifiableCredentialCreateOptionsData, + credentials::{ + verifiable_credential_1_1::CredentialStatus as CredentialStatusData, + verifiable_credential_1_1::VerifiableCredentialCreateOptions as VerifiableCredentialCreateOptionsData, + }, crypto::{dsa::Dsa, jwk::Jwk as JwkData}, dids::{ bearer_did::BearerDidGetSignerOptions as BearerDidGetSignerOptionsData, diff --git a/bindings/web5_uniffi/src/web5.udl b/bindings/web5_uniffi/src/web5.udl index 1ec02386..3b011cde 100644 --- a/bindings/web5_uniffi/src/web5.udl +++ b/bindings/web5_uniffi/src/web5.udl @@ -253,7 +253,13 @@ interface PresentationDefinition { - +dictionary CredentialStatusData { + string id; + string type; + string status_purpose; + string status_list_index; + string status_list_credential; +}; dictionary VerifiableCredentialCreateOptionsData { string? id; @@ -261,6 +267,7 @@ dictionary VerifiableCredentialCreateOptionsData { sequence? type; timestamp? issuance_date; timestamp? expiration_date; + CredentialStatusData? credential_status; }; interface VerifiableCredential { @@ -281,4 +288,5 @@ dictionary VerifiableCredentialData { string json_serialized_credential_subject; timestamp issuance_date; timestamp? expiration_date; + CredentialStatusData? credential_status; }; \ No newline at end of file diff --git a/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs b/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs index 0d38cb7e..acaf3f18 100644 --- a/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs +++ b/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs @@ -2,8 +2,8 @@ use crate::errors::Result; use std::time::SystemTime; use web5::{ credentials::verifiable_credential_1_1::{ - CredentialSubject, Issuer, VerifiableCredential as InnerVerifiableCredential, - VerifiableCredentialCreateOptions, + CredentialStatus, CredentialSubject, Issuer, + VerifiableCredential as InnerVerifiableCredential, VerifiableCredentialCreateOptions, }, json::FromJson, }; @@ -42,6 +42,7 @@ impl VerifiableCredential { json_serialized_credential_subject: self.json_serialized_credential_subject.clone(), issuance_date: self.inner_vc.issuance_date, expiration_date: self.inner_vc.expiration_date, + credential_status: self.inner_vc.credential_status.clone(), } } } @@ -55,4 +56,5 @@ pub struct VerifiableCredentialData { pub json_serialized_credential_subject: String, pub issuance_date: SystemTime, pub expiration_date: Option, + pub credential_status: Option, } diff --git a/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt b/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt index f34fdf0d..f5a34ef4 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt @@ -5711,6 +5711,47 @@ public object FfiConverterTypeBearerDidGetSignerOptionsData: FfiConverterRustBuf +data class CredentialStatusData ( + var `id`: kotlin.String, + var `type`: kotlin.String, + var `statusPurpose`: kotlin.String, + var `statusListIndex`: kotlin.String, + var `statusListCredential`: kotlin.String +) { + + companion object +} + +public object FfiConverterTypeCredentialStatusData: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): CredentialStatusData { + return CredentialStatusData( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + FfiConverterString.read(buf), + FfiConverterString.read(buf), + FfiConverterString.read(buf), + ) + } + + override fun allocationSize(value: CredentialStatusData) = ( + FfiConverterString.allocationSize(value.`id`) + + FfiConverterString.allocationSize(value.`type`) + + FfiConverterString.allocationSize(value.`statusPurpose`) + + FfiConverterString.allocationSize(value.`statusListIndex`) + + FfiConverterString.allocationSize(value.`statusListCredential`) + ) + + override fun write(value: CredentialStatusData, buf: ByteBuffer) { + FfiConverterString.write(value.`id`, buf) + FfiConverterString.write(value.`type`, buf) + FfiConverterString.write(value.`statusPurpose`, buf) + FfiConverterString.write(value.`statusListIndex`, buf) + FfiConverterString.write(value.`statusListCredential`, buf) + } +} + + + data class DidData ( var `uri`: kotlin.String, var `url`: kotlin.String, @@ -6212,7 +6253,8 @@ data class VerifiableCredentialCreateOptionsData ( var `context`: List?, var `type`: List?, var `issuanceDate`: java.time.Instant?, - var `expirationDate`: java.time.Instant? + var `expirationDate`: java.time.Instant?, + var `credentialStatus`: CredentialStatusData? ) { companion object @@ -6226,6 +6268,7 @@ public object FfiConverterTypeVerifiableCredentialCreateOptionsData: FfiConverte FfiConverterOptionalSequenceString.read(buf), FfiConverterOptionalTimestamp.read(buf), FfiConverterOptionalTimestamp.read(buf), + FfiConverterOptionalTypeCredentialStatusData.read(buf), ) } @@ -6234,7 +6277,8 @@ public object FfiConverterTypeVerifiableCredentialCreateOptionsData: FfiConverte FfiConverterOptionalSequenceString.allocationSize(value.`context`) + FfiConverterOptionalSequenceString.allocationSize(value.`type`) + FfiConverterOptionalTimestamp.allocationSize(value.`issuanceDate`) + - FfiConverterOptionalTimestamp.allocationSize(value.`expirationDate`) + FfiConverterOptionalTimestamp.allocationSize(value.`expirationDate`) + + FfiConverterOptionalTypeCredentialStatusData.allocationSize(value.`credentialStatus`) ) override fun write(value: VerifiableCredentialCreateOptionsData, buf: ByteBuffer) { @@ -6243,6 +6287,7 @@ public object FfiConverterTypeVerifiableCredentialCreateOptionsData: FfiConverte FfiConverterOptionalSequenceString.write(value.`type`, buf) FfiConverterOptionalTimestamp.write(value.`issuanceDate`, buf) FfiConverterOptionalTimestamp.write(value.`expirationDate`, buf) + FfiConverterOptionalTypeCredentialStatusData.write(value.`credentialStatus`, buf) } } @@ -6255,7 +6300,8 @@ data class VerifiableCredentialData ( var `jsonSerializedIssuer`: kotlin.String, var `jsonSerializedCredentialSubject`: kotlin.String, var `issuanceDate`: java.time.Instant, - var `expirationDate`: java.time.Instant? + var `expirationDate`: java.time.Instant?, + var `credentialStatus`: CredentialStatusData? ) { companion object @@ -6271,6 +6317,7 @@ public object FfiConverterTypeVerifiableCredentialData: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): CredentialStatusData? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterTypeCredentialStatusData.read(buf) + } + + override fun allocationSize(value: CredentialStatusData?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterTypeCredentialStatusData.allocationSize(value) + } + } + + override fun write(value: CredentialStatusData?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterTypeCredentialStatusData.write(value, buf) + } + } +} + + + + public object FfiConverterOptionalTypeDidDhtCreateOptions: FfiConverterRustBuffer { override fun read(buf: ByteBuffer): DidDhtCreateOptions? { if (buf.get().toInt() == 0) { diff --git a/crates/web5/src/credentials/mod.rs b/crates/web5/src/credentials/mod.rs index cf667ab8..50b39bbd 100644 --- a/crates/web5/src/credentials/mod.rs +++ b/crates/web5/src/credentials/mod.rs @@ -8,8 +8,8 @@ use crate::errors::Web5Error; use super::dids::resolution::resolution_metadata::ResolutionMetadataError; pub mod presentation_definition; -pub mod verifiable_credential_1_1; pub mod status_list_credential; +pub mod verifiable_credential_1_1; #[derive(thiserror::Error, Debug)] pub enum CredentialError { diff --git a/crates/web5/src/credentials/status_list_credential.rs b/crates/web5/src/credentials/status_list_credential.rs index 3f7460f0..4db099d2 100644 --- a/crates/web5/src/credentials/status_list_credential.rs +++ b/crates/web5/src/credentials/status_list_credential.rs @@ -4,9 +4,11 @@ use std::io::{Read, Write}; use base64::Engine; use flate2::{read::GzDecoder, write::GzEncoder, Compression}; +use super::verifiable_credential_1_1::{ + CredentialSubject, Issuer, VerifiableCredential, VerifiableCredentialCreateOptions, +}; use crate::errors::{Result, Web5Error}; use crate::json::{JsonObject, JsonValue}; -use super::verifiable_credential_1_1::{CredentialSubject, Issuer, VerifiableCredential, VerifiableCredentialCreateOptions}; pub const STATUS_LIST_CREDENTIAL_CONTEXT: &str = "https://w3id.org/vc/status-list/2021/v1"; pub const STATUS_LIST_CREDENTIAL_TYPE: &str = "StatusList2021Credential"; @@ -14,7 +16,7 @@ pub const STATUS_LIST_2021: &str = "StatusList2021"; pub struct StatusListCredential { base: VerifiableCredential, - encoded_list: String + encoded_list: String, } impl StatusListCredential { @@ -64,11 +66,8 @@ impl StatusListCredential { credential_status: None, }; - let verifiable_credential = VerifiableCredential::create( - issuer, - credential_subject, - Some(vc_options), - )?; + let verifiable_credential = + VerifiableCredential::create(issuer, credential_subject, Some(vc_options))?; Ok(Self { base: verifiable_credential, @@ -88,23 +87,30 @@ impl StatusListCredential { credentials_to_disable: Vec, ) -> Result<()> { // Retrieve the statusPurpose from the existing credential subject - let status_purpose = self.base.credential_subject.additional_properties + let status_purpose = self + .base + .credential_subject + .additional_properties .as_ref() .and_then(|props| props.properties.get("statusPurpose")) .and_then(|value| match value { JsonValue::String(s) => Some(s.clone()), _ => None, }) - .ok_or_else(|| Web5Error::Parameter("no statusPurpose found in status list credential".to_string()))?; + .ok_or_else(|| { + Web5Error::Parameter("no statusPurpose found in status list credential".to_string()) + })?; // Determine the status list indexes based on the provided credentials to disable. - let status_list_indexes = Self::get_status_list_indexes(&status_purpose, credentials_to_disable)?; + let status_list_indexes = + Self::get_status_list_indexes(&status_purpose, credentials_to_disable)?; // Generate the base64 bitstring from the status list indexes. let base64_bitstring = Self::bitstring_generation(status_list_indexes)?; // Update the encodedList property in the additional_properties - if let Some(additional_properties) = &mut self.base.credential_subject.additional_properties { + if let Some(additional_properties) = &mut self.base.credential_subject.additional_properties + { additional_properties.properties.insert( "encodedList".to_string(), JsonValue::String(base64_bitstring.clone()), @@ -140,29 +146,47 @@ impl StatusListCredential { // Check if the status type matches if status.r#type != STATUS_LIST_2021 { - return Err(Web5Error::Parameter(format!("unsupported status type: {}", status.r#type))); + return Err(Web5Error::Parameter(format!( + "unsupported status type: {}", + status.r#type + ))); } // Check if the status purpose matches - let credential_subject = self.base.credential_subject.additional_properties.as_ref().ok_or_else(|| { - Web5Error::Parameter("no additional properties found in status list credential".to_string()) - })?; + let credential_subject = self + .base + .credential_subject + .additional_properties + .as_ref() + .ok_or_else(|| { + Web5Error::Parameter( + "no additional properties found in status list credential".to_string(), + ) + })?; - let status_purpose = credential_subject.properties.get("statusPurpose").ok_or_else(|| { - Web5Error::Parameter("no statusPurpose found in status list credential".to_string()) - })?; + let status_purpose = credential_subject + .properties + .get("statusPurpose") + .ok_or_else(|| { + Web5Error::Parameter("no statusPurpose found in status list credential".to_string()) + })?; if let JsonValue::String(purpose) = status_purpose { if *purpose != status.status_purpose { return Err(Web5Error::Parameter("status purpose mismatch".to_string())); } } else { - return Err(Web5Error::Parameter("invalid statusPurpose format".to_string())); + return Err(Web5Error::Parameter( + "invalid statusPurpose format".to_string(), + )); } // Get the bit index let index = status.status_list_index.parse::().map_err(|_| { - Web5Error::Parameter(format!("invalid status list index: {}", status.status_list_index)) + Web5Error::Parameter(format!( + "invalid status list index: {}", + status.status_list_index + )) })?; // Check the bit in the encoded list @@ -192,9 +216,15 @@ impl StatusListCredential { return Err(Web5Error::Parameter("status purpose mismatch".to_string())); } - let index = status_list_entry.status_list_index.parse::().map_err(|_| { - Web5Error::Parameter(format!("invalid status list index: {}", status_list_entry.status_list_index)) - })?; + let index = status_list_entry + .status_list_index + .parse::() + .map_err(|_| { + Web5Error::Parameter(format!( + "invalid status list index: {}", + status_list_entry.status_list_index + )) + })?; status_list_indexes.push(index); } @@ -226,10 +256,18 @@ impl StatusListCredential { } let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); - encoder.write_all(&bit_vec) - .map_err(|e| Web5Error::Parameter(format!("encoder write_all issue while creating bitstring: {}", e)))?; - let compressed = encoder.finish() - .map_err(|e| Web5Error::Parameter(format!("encoder finish issue while creating bitstring: {}", e)))?; + encoder.write_all(&bit_vec).map_err(|e| { + Web5Error::Parameter(format!( + "encoder write_all issue while creating bitstring: {}", + e + )) + })?; + let compressed = encoder.finish().map_err(|e| { + Web5Error::Parameter(format!( + "encoder finish issue while creating bitstring: {}", + e + )) + })?; Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&compressed)) } @@ -248,32 +286,34 @@ impl StatusListCredential { let compressed_data = base64::engine::general_purpose::URL_SAFE_NO_PAD .decode(compressed_bitstring) .map_err(|e| Web5Error::Parameter(format!("failed to decode base64: {}", e)))?; - + // Decompress the data using GZIP let mut decoder = GzDecoder::new(&compressed_data[..]); let mut decompressed_data = Vec::new(); decoder .read_to_end(&mut decompressed_data) .map_err(|e| Web5Error::Parameter(format!("failed to decompress data: {}", e)))?; - + // Find the byte index, and bit index within the byte let byte_index = bit_index / 8; let bit_index_within_byte = 7 - (bit_index % 8); - let byte = decompressed_data - .get(byte_index) - .ok_or_else(|| Web5Error::Parameter("bit index out of range in decompressed data".into()))?; - + let byte = decompressed_data.get(byte_index).ok_or_else(|| { + Web5Error::Parameter("bit index out of range in decompressed data".into()) + })?; + // Extract the targeted bit let bit_integer = (byte >> bit_index_within_byte) & 1; - + Ok(bit_integer == 1) } } #[cfg(test)] mod tests { - use crate::credentials::verifiable_credential_1_1::{CredentialStatus, BASE_CONTEXT, BASE_TYPE}; use super::*; + use crate::credentials::verifiable_credential_1_1::{ + CredentialStatus, BASE_CONTEXT, BASE_TYPE, + }; const ISSUER_DID_URI: &str = "did:web:tbd.website"; const SUBJECT_DID_URI: &str = "did:dht:qgmmpyjw5hwnqfgzn7wmrm33ady8gb8z9ideib6m9gj4ys6wny8y"; @@ -292,7 +332,7 @@ mod tests { r#type: STATUS_LIST_2021.to_string(), status_purpose: purpose.to_string(), status_list_index: index.to_string(), - status_list_credential: "https://example.com/status/1".to_string() + status_list_credential: "https://example.com/status/1".to_string(), }; VerifiableCredential::create( @@ -301,11 +341,16 @@ mod tests { Some(VerifiableCredentialCreateOptions { credential_status: Some(credential_status), ..Default::default() - }) - ).unwrap() + }), + ) + .unwrap() } - fn create_test_credential_with_type(index: &str, status_type: &str, purpose: &str) -> VerifiableCredential { + fn create_test_credential_with_type( + index: &str, + status_type: &str, + purpose: &str, + ) -> VerifiableCredential { let mut credential = create_test_credential(index, purpose); if let Some(status) = &mut credential.credential_status { status.r#type = status_type.to_string(); @@ -323,14 +368,42 @@ mod tests { assert!(result.is_ok()); let status_list_credential = result.unwrap(); - - assert_eq!(status_list_credential.base.r#type, vec![BASE_TYPE.to_string(), STATUS_LIST_CREDENTIAL_TYPE.to_string()]); - assert_eq!(status_list_credential.base.context, vec![BASE_CONTEXT.to_string(), STATUS_LIST_CREDENTIAL_CONTEXT.to_string()]); - - let additional_properties = status_list_credential.base.credential_subject.additional_properties.unwrap(); - assert_eq!(additional_properties.properties.get("statusPurpose").unwrap(), &JsonValue::String("revocation".to_string())); - assert_eq!(additional_properties.properties.get("type").unwrap(), &JsonValue::String(STATUS_LIST_2021.to_string())); - assert!(additional_properties.properties.get("encodedList").is_some()); + + assert_eq!( + status_list_credential.base.r#type, + vec![ + BASE_TYPE.to_string(), + STATUS_LIST_CREDENTIAL_TYPE.to_string() + ] + ); + assert_eq!( + status_list_credential.base.context, + vec![ + BASE_CONTEXT.to_string(), + STATUS_LIST_CREDENTIAL_CONTEXT.to_string() + ] + ); + + let additional_properties = status_list_credential + .base + .credential_subject + .additional_properties + .unwrap(); + assert_eq!( + additional_properties + .properties + .get("statusPurpose") + .unwrap(), + &JsonValue::String("revocation".to_string()) + ); + assert_eq!( + additional_properties.properties.get("type").unwrap(), + &JsonValue::String(STATUS_LIST_2021.to_string()) + ); + assert!(additional_properties + .properties + .get("encodedList") + .is_some()); } #[test] @@ -340,10 +413,16 @@ mod tests { let bitstring = StatusListCredential::bitstring_generation(bit_indices).unwrap(); assert_eq!(StatusListCredential::get_bit(&bitstring, 3).unwrap(), true); - assert_eq!(StatusListCredential::get_bit(&bitstring, 1023).unwrap(), true); + assert_eq!( + StatusListCredential::get_bit(&bitstring, 1023).unwrap(), + true + ); assert_eq!(StatusListCredential::get_bit(&bitstring, 0).unwrap(), false); - assert_eq!(StatusListCredential::get_bit(&bitstring, 1024).unwrap(), false); - + assert_eq!( + StatusListCredential::get_bit(&bitstring, 1024).unwrap(), + false + ); + let result = StatusListCredential::get_bit(&bitstring, 16 * 1024 * 8 + 1); assert!(result.is_err()); } @@ -357,7 +436,8 @@ mod tests { create_test_credential("3", &status_purpose), create_test_credential("1023", &status_purpose), ]); - let status_list_credential = StatusListCredential::create(issuer, status_purpose.clone(), credentials_to_disable)?; + let status_list_credential = + StatusListCredential::create(issuer, status_purpose.clone(), credentials_to_disable)?; // Test 1: Check a disabled credential (index 3) let disabled_credential = create_test_credential("3", &status_purpose); @@ -372,12 +452,17 @@ mod tests { assert!(!status_list_credential.is_disabled(&enabled_credential)?); // Test 4: Check a credential with mismatched status type - let mismatched_type_credential = create_test_credential_with_type("7", "InvalidType", &status_purpose); - assert!(status_list_credential.is_disabled(&mismatched_type_credential).is_err()); + let mismatched_type_credential = + create_test_credential_with_type("7", "InvalidType", &status_purpose); + assert!(status_list_credential + .is_disabled(&mismatched_type_credential) + .is_err()); // Test 5: Check a credential with mismatched status purpose let mismatched_purpose_credential = create_test_credential("9", "suspension"); - assert!(status_list_credential.is_disabled(&mismatched_purpose_credential).is_err()); + assert!(status_list_credential + .is_disabled(&mismatched_purpose_credential) + .is_err()); // Test 6: Check a credential without a status let no_status_credential = VerifiableCredential::create( @@ -385,7 +470,9 @@ mod tests { CredentialSubject::from("did:example:subject"), None, )?; - assert!(status_list_credential.is_disabled(&no_status_credential).is_err()); + assert!(status_list_credential + .is_disabled(&no_status_credential) + .is_err()); Ok(()) } @@ -395,11 +482,18 @@ mod tests { let status_purpose = "revocation".to_string(); // Step 1: Create an initial StatusListCredential without any disabled credentials - let mut status_list_credential = StatusListCredential::create(issuer(), status_purpose.clone(), None).unwrap(); + let mut status_list_credential = + StatusListCredential::create(issuer(), status_purpose.clone(), None).unwrap(); // Verify initial state: No bits should be set in the encoded list - assert_eq!(StatusListCredential::get_bit(&status_list_credential.encoded_list, 3).unwrap(), false); - assert_eq!(StatusListCredential::get_bit(&status_list_credential.encoded_list, 1023).unwrap(), false); + assert_eq!( + StatusListCredential::get_bit(&status_list_credential.encoded_list, 3).unwrap(), + false + ); + assert_eq!( + StatusListCredential::get_bit(&status_list_credential.encoded_list, 1023).unwrap(), + false + ); // Step 2: Create test credentials to disable let credentials_to_disable = vec![ @@ -408,18 +502,37 @@ mod tests { ]; // Step 3: Update the StatusListCredential with these credentials - status_list_credential.update_credentials_to_disable(credentials_to_disable).unwrap(); + status_list_credential + .update_credentials_to_disable(credentials_to_disable) + .unwrap(); // Step 4: Verify the encoded list is updated correctly - assert_eq!(StatusListCredential::get_bit(&status_list_credential.encoded_list, 3).unwrap(), true); - assert_eq!(StatusListCredential::get_bit(&status_list_credential.encoded_list, 1023).unwrap(), true); + assert_eq!( + StatusListCredential::get_bit(&status_list_credential.encoded_list, 3).unwrap(), + true + ); + assert_eq!( + StatusListCredential::get_bit(&status_list_credential.encoded_list, 1023).unwrap(), + true + ); // Step 5: Verify other bits remain unset - assert_eq!(StatusListCredential::get_bit(&status_list_credential.encoded_list, 0).unwrap(), false); - assert_eq!(StatusListCredential::get_bit(&status_list_credential.encoded_list, 1000).unwrap(), false); + assert_eq!( + StatusListCredential::get_bit(&status_list_credential.encoded_list, 0).unwrap(), + false + ); + assert_eq!( + StatusListCredential::get_bit(&status_list_credential.encoded_list, 1000).unwrap(), + false + ); // Step 6: Verify that the base additional properties got updated - let additional_properties = status_list_credential.base.credential_subject.additional_properties.as_ref().unwrap(); + let additional_properties = status_list_credential + .base + .credential_subject + .additional_properties + .as_ref() + .unwrap(); assert_eq!( additional_properties.properties.get("encodedList").unwrap(), &JsonValue::String(status_list_credential.encoded_list.clone()) @@ -430,14 +543,24 @@ mod tests { fn test_full_flow() { let status_purpose = "revocation".to_string(); let credentials_to_disable = None; - let status_list_credential = StatusListCredential::create(issuer(), status_purpose, credentials_to_disable).unwrap(); + let status_list_credential = + StatusListCredential::create(issuer(), status_purpose, credentials_to_disable).unwrap(); let encoded_list = &status_list_credential.encoded_list; // Test various bit positions - assert_eq!(StatusListCredential::get_bit(encoded_list, 0).unwrap(), false); - assert_eq!(StatusListCredential::get_bit(encoded_list, 100).unwrap(), false); - assert_eq!(StatusListCredential::get_bit(encoded_list, 1000).unwrap(), false); + assert_eq!( + StatusListCredential::get_bit(encoded_list, 0).unwrap(), + false + ); + assert_eq!( + StatusListCredential::get_bit(encoded_list, 100).unwrap(), + false + ); + assert_eq!( + StatusListCredential::get_bit(encoded_list, 1000).unwrap(), + false + ); let vc1_options = Some(VerifiableCredentialCreateOptions { credential_status: Some(CredentialStatus { @@ -445,12 +568,13 @@ mod tests { r#type: STATUS_LIST_2021.to_string(), status_purpose: "revocation".to_string(), status_list_index: "3".to_string(), - status_list_credential: "https://example.com/status/1".to_string() + status_list_credential: "https://example.com/status/1".to_string(), }), ..Default::default() }); - let vc1 = VerifiableCredential::create(issuer(), credential_subject(), vc1_options).unwrap(); + let vc1 = + VerifiableCredential::create(issuer(), credential_subject(), vc1_options).unwrap(); let vc2_options = Some(VerifiableCredentialCreateOptions { credential_status: Some(CredentialStatus { @@ -458,25 +582,46 @@ mod tests { r#type: STATUS_LIST_2021.to_string(), status_purpose: "revocation".to_string(), status_list_index: "1023".to_string(), - status_list_credential: "https://example.com/status/1".to_string() + status_list_credential: "https://example.com/status/1".to_string(), }), ..Default::default() }); - let vc2 = VerifiableCredential::create(issuer(), credential_subject(), vc2_options).unwrap(); + let vc2 = + VerifiableCredential::create(issuer(), credential_subject(), vc2_options).unwrap(); let credentials_to_disable = Some(vec![vc1, vc2]); - let updated_status_list_credential = StatusListCredential::create(Issuer::from("did:example:123".to_string()), "revocation".to_string(), credentials_to_disable).unwrap(); + let updated_status_list_credential = StatusListCredential::create( + Issuer::from("did:example:123".to_string()), + "revocation".to_string(), + credentials_to_disable, + ) + .unwrap(); let updated_encoded_list = &updated_status_list_credential.encoded_list; // Test the bits corresponding to the disabled credentials - assert_eq!(StatusListCredential::get_bit(updated_encoded_list, 3).unwrap(), true); - assert_eq!(StatusListCredential::get_bit(updated_encoded_list, 1023).unwrap(), true); + assert_eq!( + StatusListCredential::get_bit(updated_encoded_list, 3).unwrap(), + true + ); + assert_eq!( + StatusListCredential::get_bit(updated_encoded_list, 1023).unwrap(), + true + ); // Test other bits are still false - assert_eq!(StatusListCredential::get_bit(updated_encoded_list, 0).unwrap(), false); - assert_eq!(StatusListCredential::get_bit(updated_encoded_list, 100).unwrap(), false); - assert_eq!(StatusListCredential::get_bit(updated_encoded_list, 1000).unwrap(), false); + assert_eq!( + StatusListCredential::get_bit(updated_encoded_list, 0).unwrap(), + false + ); + assert_eq!( + StatusListCredential::get_bit(updated_encoded_list, 100).unwrap(), + false + ); + assert_eq!( + StatusListCredential::get_bit(updated_encoded_list, 1000).unwrap(), + false + ); } } diff --git a/crates/web5/src/credentials/verifiable_credential_1_1.rs b/crates/web5/src/credentials/verifiable_credential_1_1.rs index fa83ce0b..a68caa48 100644 --- a/crates/web5/src/credentials/verifiable_credential_1_1.rs +++ b/crates/web5/src/credentials/verifiable_credential_1_1.rs @@ -151,7 +151,7 @@ pub struct VerifiableCredentialCreateOptions { pub r#type: Option>, pub issuance_date: Option, pub expiration_date: Option, - pub credential_status: Option + pub credential_status: Option, } impl VerifiableCredential { @@ -218,7 +218,7 @@ impl VerifiableCredential { issuance_date: options.issuance_date.unwrap_or_else(SystemTime::now), expiration_date: options.expiration_date, credential_subject, - credential_status: options.credential_status + credential_status: options.credential_status, }) } @@ -242,7 +242,7 @@ impl VerifiableCredential { issuance_date: Some(self.issuance_date), expiration_date: self.expiration_date, credential_subject: Some(self.credential_subject.clone()), - credential_status: self.credential_status.clone() + credential_status: self.credential_status.clone(), }; payload.set_claim("vc", Some(serde_json::to_value(vc_claim)?))?; payload.set_issuer(self.issuer.to_string()); @@ -399,7 +399,7 @@ impl VerifiableCredential { issuance_date: nbf, expiration_date: exp, credential_subject: vc_credential_subject, - credential_status: vc_payload.credential_status + credential_status: vc_payload.credential_status, }; validate_vc_data_model(&vc)?; From 39f17dafcaf426ce86ff781f38d70d25ca8759f9 Mon Sep 17 00:00:00 2001 From: Neal Date: Tue, 3 Sep 2024 11:23:34 -0700 Subject: [PATCH 04/10] update to bindings and kt code --- bindings/web5_uniffi/src/lib.rs | 1 + bindings/web5_uniffi/src/web5.udl | 14 +- .../src/credentials/mod.rs | 1 + .../src/credentials/status_list_credential.rs | 36 ++ .../credentials/verifiable_credential_1_1.rs | 2 +- .../src/main/kotlin/web5/sdk/rust/UniFFI.kt | 323 ++++++++++++++++++ .../web5/sdk/vc/StatusListCredential.kt | 27 ++ .../web5/sdk/vc/VerifiableCredential.kt | 27 +- .../web5/sdk/vc/StatusListCredentialTest.kt | 40 +++ .../src/credentials/status_list_credential.rs | 223 ++++-------- docs/API_DESIGN.md | 8 +- 11 files changed, 534 insertions(+), 168 deletions(-) create mode 100644 bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs create mode 100644 bound/kt/src/main/kotlin/web5/sdk/vc/StatusListCredential.kt create mode 100644 bound/kt/src/test/kotlin/web5/sdk/vc/StatusListCredentialTest.kt diff --git a/bindings/web5_uniffi/src/lib.rs b/bindings/web5_uniffi/src/lib.rs index 208218f8..23706759 100644 --- a/bindings/web5_uniffi/src/lib.rs +++ b/bindings/web5_uniffi/src/lib.rs @@ -2,6 +2,7 @@ use web5_uniffi_wrapper::{ credentials::{ presentation_definition::PresentationDefinition, verifiable_credential_1_1::{VerifiableCredential, VerifiableCredentialData}, + status_list_credential::{StatusListCredential} }, crypto::{ dsa::{ diff --git a/bindings/web5_uniffi/src/web5.udl b/bindings/web5_uniffi/src/web5.udl index 3b011cde..2957b4de 100644 --- a/bindings/web5_uniffi/src/web5.udl +++ b/bindings/web5_uniffi/src/web5.udl @@ -251,8 +251,6 @@ interface PresentationDefinition { sequence select_credentials([ByRef] sequence vc_jwts); }; - - dictionary CredentialStatusData { string id; string type; @@ -289,4 +287,16 @@ dictionary VerifiableCredentialData { timestamp issuance_date; timestamp? expiration_date; CredentialStatusData? credential_status; +}; + +interface StatusListCredential { + [Throws=Web5Error, Name=create] + constructor( + string json_serialized_issuer, + string status_purpose, + sequence? disabled_credentials + ); + + [Throws=Web5Error] + boolean is_disabled(VerifiableCredential credential); }; \ No newline at end of file diff --git a/bindings/web5_uniffi_wrapper/src/credentials/mod.rs b/bindings/web5_uniffi_wrapper/src/credentials/mod.rs index e2356aca..01f8e3ab 100644 --- a/bindings/web5_uniffi_wrapper/src/credentials/mod.rs +++ b/bindings/web5_uniffi_wrapper/src/credentials/mod.rs @@ -1,2 +1,3 @@ pub mod presentation_definition; pub mod verifiable_credential_1_1; +pub mod status_list_credential; diff --git a/bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs b/bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs new file mode 100644 index 00000000..941f82f2 --- /dev/null +++ b/bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs @@ -0,0 +1,36 @@ +use std::sync::Arc; +use crate::errors::Result; +use web5::{ + credentials::verifiable_credential_1_1::{ + Issuer, VerifiableCredential as InnerVerifiableCredential, + }, + credentials::status_list_credential::StatusListCredential as InnerStatusListCredential, + json::FromJson, +}; +use crate::credentials::verifiable_credential_1_1::{VerifiableCredential}; + +pub struct StatusListCredential(pub InnerStatusListCredential); + +impl StatusListCredential { + pub fn create( + json_serialized_issuer: String, + status_purpose: String, + credentials_to_disable: Option>>, + ) -> Result { + let issuer = Issuer::from_json_string(&json_serialized_issuer)?; + + let inner_vcs: Option> = credentials_to_disable + .map(|credentials| { + credentials + .into_iter() + .map(|vc| vc.inner_vc.clone()) + .collect() + }); + + Ok(Self(InnerStatusListCredential::create(issuer, status_purpose, inner_vcs)?)) + } + + pub fn is_disabled(&self, credential: Arc) -> Result { + Ok(self.0.is_disabled(&credential.inner_vc.clone())?) + } +} \ No newline at end of file diff --git a/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs b/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs index acaf3f18..5e5814d7 100644 --- a/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs +++ b/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs @@ -9,7 +9,7 @@ use web5::{ }; pub struct VerifiableCredential { - inner_vc: InnerVerifiableCredential, + pub inner_vc: InnerVerifiableCredential, json_serialized_issuer: String, json_serialized_credential_subject: String, } diff --git a/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt b/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt index f5a34ef4..0f57c464 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt @@ -894,6 +894,12 @@ internal open class UniffiVTableCallbackInterfaceVerifier( + + + + + + @@ -1054,6 +1060,14 @@ internal interface UniffiLib : Library { ): Unit fun uniffi_web5_uniffi_fn_method_signer_sign(`ptr`: Pointer,`payload`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue + fun uniffi_web5_uniffi_fn_clone_statuslistcredential(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_web5_uniffi_fn_free_statuslistcredential(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_web5_uniffi_fn_constructor_statuslistcredential_create(`jsonSerializedIssuer`: RustBuffer.ByValue,`statusPurpose`: RustBuffer.ByValue,`disabledCredentials`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_web5_uniffi_fn_method_statuslistcredential_is_disabled(`ptr`: Pointer,`credential`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Byte fun uniffi_web5_uniffi_fn_clone_verifiablecredential(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, ): Pointer fun uniffi_web5_uniffi_fn_free_verifiablecredential(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, @@ -1260,6 +1274,8 @@ internal interface UniffiLib : Library { ): Short fun uniffi_web5_uniffi_checksum_method_signer_sign( ): Short + fun uniffi_web5_uniffi_checksum_method_statuslistcredential_is_disabled( + ): Short fun uniffi_web5_uniffi_checksum_method_verifiablecredential_get_data( ): Short fun uniffi_web5_uniffi_checksum_method_verifier_verify( @@ -1290,6 +1306,8 @@ internal interface UniffiLib : Library { ): Short fun uniffi_web5_uniffi_checksum_constructor_resolutionresult_resolve( ): Short + fun uniffi_web5_uniffi_checksum_constructor_statuslistcredential_create( + ): Short fun uniffi_web5_uniffi_checksum_constructor_verifiablecredential_create( ): Short fun ffi_web5_uniffi_uniffi_contract_version( @@ -1402,6 +1420,9 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_web5_uniffi_checksum_method_signer_sign() != 5738.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_web5_uniffi_checksum_method_statuslistcredential_is_disabled() != 23900.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_web5_uniffi_checksum_method_verifiablecredential_get_data() != 34047.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -1447,6 +1468,9 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_web5_uniffi_checksum_constructor_resolutionresult_resolve() != 11404.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_web5_uniffi_checksum_constructor_statuslistcredential_create() != 49374.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_web5_uniffi_checksum_constructor_verifiablecredential_create() != 31236.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -5224,6 +5248,251 @@ public object FfiConverterTypeSigner: FfiConverter { // +public interface StatusListCredentialInterface { + + fun `isDisabled`(`credential`: VerifiableCredential): kotlin.Boolean + + companion object +} + +open class StatusListCredential: Disposable, AutoCloseable, StatusListCredentialInterface { + + constructor(pointer: Pointer) { + this.pointer = pointer + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + + /** + * This constructor can be used to instantiate a fake object. Only used for tests. Any + * attempt to actually use an object constructed this way will fail as there is no + * connected Rust object. + */ + @Suppress("UNUSED_PARAMETER") + constructor(noPointer: NoPointer) { + this.pointer = null + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + + protected val pointer: Pointer? + protected val cleanable: UniffiCleaner.Cleanable + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun callWithPointer(block: (ptr: Pointer) -> R): R { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + do { + val c = this.callCounter.get() + if (c == 0L) { + throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") + } + if (c == Long.MAX_VALUE) { + throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") + } + } while (! this.callCounter.compareAndSet(c, c + 1L)) + // Now we can safely do the method call without the pointer being freed concurrently. + try { + return block(this.uniffiClonePointer()) + } finally { + // This decrement always matches the increment we performed above. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + // Use a static inner class instead of a closure so as not to accidentally + // capture `this` as part of the cleanable's action. + private class UniffiCleanAction(private val pointer: Pointer?) : Runnable { + override fun run() { + pointer?.let { ptr -> + uniffiRustCall { status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_free_statuslistcredential(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_clone_statuslistcredential(pointer!!, status) + } + } + + + @Throws(Web5Exception::class)override fun `isDisabled`(`credential`: VerifiableCredential): kotlin.Boolean { + return FfiConverterBoolean.lift( + callWithPointer { + uniffiRustCallWithError(Web5Exception) { _status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_method_statuslistcredential_is_disabled( + it, FfiConverterTypeVerifiableCredential.lower(`credential`),_status) +} + } + ) + } + + + + + + companion object { + + @Throws(Web5Exception::class) fun `create`(`jsonSerializedIssuer`: kotlin.String, `statusPurpose`: kotlin.String, `disabledCredentials`: List?): StatusListCredential { + return FfiConverterTypeStatusListCredential.lift( + uniffiRustCallWithError(Web5Exception) { _status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_constructor_statuslistcredential_create( + FfiConverterString.lower(`jsonSerializedIssuer`),FfiConverterString.lower(`statusPurpose`),FfiConverterOptionalSequenceTypeVerifiableCredential.lower(`disabledCredentials`),_status) +} + ) + } + + + + } + +} + +public object FfiConverterTypeStatusListCredential: FfiConverter { + + override fun lower(value: StatusListCredential): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): StatusListCredential { + return StatusListCredential(value) + } + + override fun read(buf: ByteBuffer): StatusListCredential { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return lift(Pointer(buf.getLong())) + } + + override fun allocationSize(value: StatusListCredential) = 8UL + + override fun write(value: StatusListCredential, buf: ByteBuffer) { + // The Rust code always expects pointers written as 8 bytes, + // and will fail to compile if they don't fit. + buf.putLong(Pointer.nativeValue(lower(value))) + } +} + + +// This template implements a class for working with a Rust struct via a Pointer/Arc +// to the live Rust struct on the other side of the FFI. +// +// Each instance implements core operations for working with the Rust `Arc` and the +// Kotlin Pointer to work with the live Rust struct on the other side of the FFI. +// +// There's some subtlety here, because we have to be careful not to operate on a Rust +// struct after it has been dropped, and because we must expose a public API for freeing +// theq Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: +// +// * Each instance holds an opaque pointer to the underlying Rust struct. +// Method calls need to read this pointer from the object's state and pass it in to +// the Rust FFI. +// +// * When an instance is no longer needed, its pointer should be passed to a +// special destructor function provided by the Rust FFI, which will drop the +// underlying Rust struct. +// +// * Given an instance, calling code is expected to call the special +// `destroy` method in order to free it after use, either by calling it explicitly +// or by using a higher-level helper like the `use` method. Failing to do so risks +// leaking the underlying Rust struct. +// +// * We can't assume that calling code will do the right thing, and must be prepared +// to handle Kotlin method calls executing concurrently with or even after a call to +// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. +// +// * We must never allow Rust code to operate on the underlying Rust struct after +// the destructor has been called, and must never call the destructor more than once. +// Doing so may trigger memory unsafety. +// +// * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner` +// is implemented to call the destructor when the Kotlin object becomes unreachable. +// This is done in a background thread. This is not a panacea, and client code should be aware that +// 1. the thread may starve if some there are objects that have poorly performing +// `drop` methods or do significant work in their `drop` methods. +// 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`, +// or `android = true` in the [`kotlin` section of the `uniffi.toml` file](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html). +// +// If we try to implement this with mutual exclusion on access to the pointer, there is the +// possibility of a race between a method call and a concurrent call to `destroy`: +// +// * Thread A starts a method call, reads the value of the pointer, but is interrupted +// before it can pass the pointer over the FFI to Rust. +// * Thread B calls `destroy` and frees the underlying Rust struct. +// * Thread A resumes, passing the already-read pointer value to Rust and triggering +// a use-after-free. +// +// One possible solution would be to use a `ReadWriteLock`, with each method call taking +// a read lock (and thus allowed to run concurrently) and the special `destroy` method +// taking a write lock (and thus blocking on live method calls). However, we aim not to +// generate methods with any hidden blocking semantics, and a `destroy` method that might +// block if called incorrectly seems to meet that bar. +// +// So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track +// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` +// has been called. These are updated according to the following rules: +// +// * The initial value of the counter is 1, indicating a live object with no in-flight calls. +// The initial value for the flag is false. +// +// * At the start of each method call, we atomically check the counter. +// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. +// If it is nonzero them we atomically increment it by 1 and proceed with the method call. +// +// * At the end of each method call, we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// * When `destroy` is called, we atomically flip the flag from false to true. +// If the flag was already true we silently fail. +// Otherwise we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works, +// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. +// +// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been +// called *and* all in-flight method calls have completed, avoiding violating any of the expectations +// of the underlying Rust code. +// +// This makes a cleaner a better alternative to _not_ calling `destroy()` as +// and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop` +// method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner +// thread may be starved, and the app will leak memory. +// +// In this case, `destroy`ing manually may be a better solution. +// +// The cleaner can live side by side with the manual calling of `destroy`. In the order of responsiveness, uniffi objects +// with Rust peers are reclaimed: +// +// 1. By calling the `destroy` method of the object, which calls `rustObject.free()`. If that doesn't happen: +// 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then: +// 3. The memory is reclaimed when the process terminates. +// +// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 +// + + public interface VerifiableCredentialInterface { fun `getData`(): VerifiableCredentialData @@ -6916,6 +7185,35 @@ public object FfiConverterOptionalSequenceString: FfiConverterRustBuffer?> { + override fun read(buf: ByteBuffer): List? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterSequenceTypeVerifiableCredential.read(buf) + } + + override fun allocationSize(value: List?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterSequenceTypeVerifiableCredential.allocationSize(value) + } + } + + override fun write(value: List?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterSequenceTypeVerifiableCredential.write(value, buf) + } + } +} + + + + public object FfiConverterOptionalSequenceTypeServiceData: FfiConverterRustBuffer?> { override fun read(buf: ByteBuffer): List? { if (buf.get().toInt() == 0) { @@ -7028,6 +7326,31 @@ public object FfiConverterSequenceString: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeVerifiableCredential.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterTypeVerifiableCredential.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeVerifiableCredential.write(it, buf) + } + } +} + + + + public object FfiConverterSequenceTypeJwkData: FfiConverterRustBuffer> { override fun read(buf: ByteBuffer): List { val len = buf.getInt() diff --git a/bound/kt/src/main/kotlin/web5/sdk/vc/StatusListCredential.kt b/bound/kt/src/main/kotlin/web5/sdk/vc/StatusListCredential.kt new file mode 100644 index 00000000..66b513e0 --- /dev/null +++ b/bound/kt/src/main/kotlin/web5/sdk/vc/StatusListCredential.kt @@ -0,0 +1,27 @@ +package web5.sdk.vc + +import web5.sdk.Json +import web5.sdk.rust.StatusListCredential as RustCoreStatusListCredential + +data class StatusListCredential( + internal val rustCoreStatusListCredential: RustCoreStatusListCredential +) { + companion object { + fun create( + issuer: Issuer, + statusPurpose: String, + credentialsToDisable: List? = null + ): StatusListCredential { + val jsonSerializedIssuer = Json.stringify(issuer) + val rustCoreCredentials = credentialsToDisable?.map { it.rustCoreVerifiableCredential } + + val rustCoreStatusListCredential = RustCoreStatusListCredential.create(jsonSerializedIssuer, statusPurpose, rustCoreCredentials) + + return StatusListCredential(rustCoreStatusListCredential) + } + } + + fun isDisabled(credential: VerifiableCredential): Boolean { + return rustCoreStatusListCredential.isDisabled(credential.rustCoreVerifiableCredential) + } +} \ No newline at end of file diff --git a/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt b/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt index d4a8a266..f1ef8a6b 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt @@ -2,17 +2,29 @@ package web5.sdk.vc import com.fasterxml.jackson.annotation.JsonValue import web5.sdk.Json + import java.time.Instant -import java.util.Date +import java.util.* + +import web5.sdk.rust.CredentialStatusData as RustCoreCredentialStatus import web5.sdk.rust.VerifiableCredential as RustCoreVerifiableCredential import web5.sdk.rust.VerifiableCredentialCreateOptionsData as RustCoreVerifiableCredentialCreateOptions +data class CredentialStatus( + var id: String, + var type: String, + var statusPurpose: String, + var statusListIndex: String, + var statusListCredential: String +) + data class VerifiableCredentialCreateOptions( val id: String? = null, var context: List? = null, var type: List? = null, var issuanceDate: Date? = null, - var expirationDate: Date? = null + var expirationDate: Date? = null, + var credentialStatus: CredentialStatus? = null ) class VerifiableCredential private constructor( @@ -34,6 +46,16 @@ class VerifiableCredential private constructor( val jsonSerializedIssuer = Json.stringify(issuer) val jsonSerializedCredentialSubject = Json.stringify(credentialSubject) + val rustCoreCredentialStatus = options?.credentialStatus?.let { + RustCoreCredentialStatus( + id = it.id, + type = it.type, + statusPurpose = it.statusPurpose, + statusListIndex = it.statusListIndex, + statusListCredential = it.statusListCredential + ) + } + val rustCoreVerifiableCredential = RustCoreVerifiableCredential.create( jsonSerializedIssuer, jsonSerializedCredentialSubject, @@ -43,6 +65,7 @@ class VerifiableCredential private constructor( options?.type, options?.issuanceDate?.toInstant(), options?.expirationDate?.toInstant(), + rustCoreCredentialStatus ) ) diff --git a/bound/kt/src/test/kotlin/web5/sdk/vc/StatusListCredentialTest.kt b/bound/kt/src/test/kotlin/web5/sdk/vc/StatusListCredentialTest.kt new file mode 100644 index 00000000..0f8e51d0 --- /dev/null +++ b/bound/kt/src/test/kotlin/web5/sdk/vc/StatusListCredentialTest.kt @@ -0,0 +1,40 @@ +package web5.sdk.vc + +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.* + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class StatusListCredentialTest { + companion object { + const val ISSUER_DID_URI = "did:web:example.com" + const val SUBJECT_DID_URI = "did:example:123456789abcdefghi" + val ISSUER = Issuer.StringIssuer(ISSUER_DID_URI) + val CREDENTIAL_SUBJECT = CredentialSubject(SUBJECT_DID_URI) + } + + @Test + fun test_create_status_list_credential() { + val statusPurpose = "revocation" + + val optionsVc1 = VerifiableCredentialCreateOptions( + credentialStatus = CredentialStatus("vc-cred-status-id-1", "StatusList2021Entry", statusPurpose, "123", "status-list-credential-id"), + ) + val vc1 = VerifiableCredential.create(ISSUER, CREDENTIAL_SUBJECT, optionsVc1) + + val optionsVc2 = VerifiableCredentialCreateOptions( + credentialStatus = CredentialStatus("vc-cred-status-id-2", "StatusList2021Entry", statusPurpose, "9999", "status-list-credential-id"), + ) + val vc2 = VerifiableCredential.create(ISSUER, CREDENTIAL_SUBJECT, optionsVc2) + + val optionsVc3 = VerifiableCredentialCreateOptions( + credentialStatus = CredentialStatus("vc-cred-status-id-3", "StatusList2021Entry", statusPurpose, "876", "status-list-credential-id"), + ) + val vc3 = VerifiableCredential.create(ISSUER, CREDENTIAL_SUBJECT, optionsVc3) + + val statusListCredential = StatusListCredential.create(ISSUER, statusPurpose, listOf(vc1, vc2)) + + assertTrue(statusListCredential.isDisabled(vc1)) + assertTrue(statusListCredential.isDisabled(vc2)) + assertFalse(statusListCredential.isDisabled(vc3)) + } +} \ No newline at end of file diff --git a/crates/web5/src/credentials/status_list_credential.rs b/crates/web5/src/credentials/status_list_credential.rs index 4db099d2..20f60fe0 100644 --- a/crates/web5/src/credentials/status_list_credential.rs +++ b/crates/web5/src/credentials/status_list_credential.rs @@ -14,19 +14,20 @@ pub const STATUS_LIST_CREDENTIAL_CONTEXT: &str = "https://w3id.org/vc/status-lis pub const STATUS_LIST_CREDENTIAL_TYPE: &str = "StatusList2021Credential"; pub const STATUS_LIST_2021: &str = "StatusList2021"; +pub const STATUS_LIST_2021_ENTRY: &str = "StatusList2021Entry"; + pub struct StatusListCredential { - base: VerifiableCredential, - encoded_list: String, + pub base: VerifiableCredential, } impl StatusListCredential { pub fn create( issuer: Issuer, status_purpose: String, - credentials_to_disable: Option>, + disabled_credentials: Option>, ) -> Result { // Determine the status list indexes based on the provided credentials to disable. - let status_list_indexes = match credentials_to_disable { + let status_list_indexes = match disabled_credentials { Some(credentials) => Self::get_status_list_indexes(&status_purpose, credentials)?, None => Vec::new(), }; @@ -71,58 +72,9 @@ impl StatusListCredential { Ok(Self { base: verifiable_credential, - encoded_list: base64_bitstring, }) } - /// Updates the `encoded_list` by adding the provided credentials to the disable list - /// - /// # Arguments - /// * `credentials_to_disable` - A vector of `VerifiableCredential` objects to be disabled. - /// - /// # Returns - /// A `Result` containing the updated `StatusListCredential`, or an error if there was an issue. - pub fn update_credentials_to_disable( - &mut self, - credentials_to_disable: Vec, - ) -> Result<()> { - // Retrieve the statusPurpose from the existing credential subject - let status_purpose = self - .base - .credential_subject - .additional_properties - .as_ref() - .and_then(|props| props.properties.get("statusPurpose")) - .and_then(|value| match value { - JsonValue::String(s) => Some(s.clone()), - _ => None, - }) - .ok_or_else(|| { - Web5Error::Parameter("no statusPurpose found in status list credential".to_string()) - })?; - - // Determine the status list indexes based on the provided credentials to disable. - let status_list_indexes = - Self::get_status_list_indexes(&status_purpose, credentials_to_disable)?; - - // Generate the base64 bitstring from the status list indexes. - let base64_bitstring = Self::bitstring_generation(status_list_indexes)?; - - // Update the encodedList property in the additional_properties - if let Some(additional_properties) = &mut self.base.credential_subject.additional_properties - { - additional_properties.properties.insert( - "encodedList".to_string(), - JsonValue::String(base64_bitstring.clone()), - ); - } - - // Update the encoded_list in the StatusListCredential struct - self.encoded_list = base64_bitstring; - - Ok(()) - } - /// Checks if a given credential is disabled according to this Status List Credential. /// /// # Arguments @@ -140,57 +92,53 @@ impl StatusListCredential { /// println!("Credential is disabled: {}", is_disabled); /// pub fn is_disabled(&self, credential: &VerifiableCredential) -> Result { - let status = credential.credential_status.as_ref().ok_or_else(|| { - Web5Error::Parameter("no credential status found in credential".to_string()) - })?; + let status = credential.credential_status.as_ref() + .ok_or_else(|| Web5Error::Parameter("no credential status found in credential".to_string()))?; // Check if the status type matches - if status.r#type != STATUS_LIST_2021 { - return Err(Web5Error::Parameter(format!( - "unsupported status type: {}", - status.r#type - ))); + if status.r#type != STATUS_LIST_2021_ENTRY { + return Err(Web5Error::Parameter(format!("unsupported status type: {}", status.r#type))); } // Check if the status purpose matches - let credential_subject = self - .base - .credential_subject + let status_purpose = self.base.credential_subject .additional_properties .as_ref() - .ok_or_else(|| { - Web5Error::Parameter( - "no additional properties found in status list credential".to_string(), - ) - })?; - - let status_purpose = credential_subject - .properties - .get("statusPurpose") - .ok_or_else(|| { - Web5Error::Parameter("no statusPurpose found in status list credential".to_string()) - })?; + .and_then(|props| props.properties.get("statusPurpose")) + .and_then(|value| { + if let JsonValue::String(s) = value { + Some(s.as_str()) + } else { + None + } + }) + .ok_or_else(|| Web5Error::Parameter("no valid statusPurpose found in status list credential".to_string()))?; - if let JsonValue::String(purpose) = status_purpose { - if *purpose != status.status_purpose { - return Err(Web5Error::Parameter("status purpose mismatch".to_string())); - } - } else { - return Err(Web5Error::Parameter( - "invalid statusPurpose format".to_string(), - )); + if status_purpose != status.status_purpose { + return Err(Web5Error::Parameter("status purpose mismatch".to_string())); } // Get the bit index - let index = status.status_list_index.parse::().map_err(|_| { - Web5Error::Parameter(format!( - "invalid status list index: {}", - status.status_list_index - )) - })?; + let index = status.status_list_index.parse::() + .map_err(|_| Web5Error::Parameter(format!("invalid status list index: {}", status.status_list_index)))?; + + let encoded_list = match self.base.credential_subject + .additional_properties + .as_ref() + .and_then(|props| props.properties.get("encodedList")) + .and_then(|value| { + if let JsonValue::String(s) = value { + Some(s.as_str()) + } else { + None + } + }) { + Some(el) => el, + None => return Err(Web5Error::Parameter("invalid or missing encodedList".to_string())), + }; // Check the bit in the encoded list - Self::get_bit(&self.encoded_list, index) + Self::get_bit(&encoded_list, index) } /// Extracts status list indexes from a vector of verifiable credentials that match the specified status purpose. @@ -329,7 +277,7 @@ mod tests { fn create_test_credential(index: &str, purpose: &str) -> VerifiableCredential { let credential_status = CredentialStatus { id: format!("https://example.com/status/{}", index), - r#type: STATUS_LIST_2021.to_string(), + r#type: STATUS_LIST_2021_ENTRY.to_string(), status_purpose: purpose.to_string(), status_list_index: index.to_string(), status_list_credential: "https://example.com/status/1".to_string(), @@ -477,68 +425,6 @@ mod tests { Ok(()) } - #[test] - fn test_update_credentials_to_disable() { - let status_purpose = "revocation".to_string(); - - // Step 1: Create an initial StatusListCredential without any disabled credentials - let mut status_list_credential = - StatusListCredential::create(issuer(), status_purpose.clone(), None).unwrap(); - - // Verify initial state: No bits should be set in the encoded list - assert_eq!( - StatusListCredential::get_bit(&status_list_credential.encoded_list, 3).unwrap(), - false - ); - assert_eq!( - StatusListCredential::get_bit(&status_list_credential.encoded_list, 1023).unwrap(), - false - ); - - // Step 2: Create test credentials to disable - let credentials_to_disable = vec![ - create_test_credential("3", &status_purpose), - create_test_credential("1023", &status_purpose), - ]; - - // Step 3: Update the StatusListCredential with these credentials - status_list_credential - .update_credentials_to_disable(credentials_to_disable) - .unwrap(); - - // Step 4: Verify the encoded list is updated correctly - assert_eq!( - StatusListCredential::get_bit(&status_list_credential.encoded_list, 3).unwrap(), - true - ); - assert_eq!( - StatusListCredential::get_bit(&status_list_credential.encoded_list, 1023).unwrap(), - true - ); - - // Step 5: Verify other bits remain unset - assert_eq!( - StatusListCredential::get_bit(&status_list_credential.encoded_list, 0).unwrap(), - false - ); - assert_eq!( - StatusListCredential::get_bit(&status_list_credential.encoded_list, 1000).unwrap(), - false - ); - - // Step 6: Verify that the base additional properties got updated - let additional_properties = status_list_credential - .base - .credential_subject - .additional_properties - .as_ref() - .unwrap(); - assert_eq!( - additional_properties.properties.get("encodedList").unwrap(), - &JsonValue::String(status_list_credential.encoded_list.clone()) - ); - } - #[test] fn test_full_flow() { let status_purpose = "revocation".to_string(); @@ -546,7 +432,17 @@ mod tests { let status_list_credential = StatusListCredential::create(issuer(), status_purpose, credentials_to_disable).unwrap(); - let encoded_list = &status_list_credential.encoded_list; + let encoded_list = status_list_credential.base.credential_subject + .additional_properties + .as_ref() + .and_then(|props| props.properties.get("encodedList")) + .and_then(|value| { + if let JsonValue::String(s) = value { + Some(s.as_str()) + } else { + None + } + }).unwrap(); // Test various bit positions assert_eq!( @@ -565,7 +461,7 @@ mod tests { let vc1_options = Some(VerifiableCredentialCreateOptions { credential_status: Some(CredentialStatus { id: "https://example.com/status/1".to_string(), - r#type: STATUS_LIST_2021.to_string(), + r#type: STATUS_LIST_2021_ENTRY.to_string(), status_purpose: "revocation".to_string(), status_list_index: "3".to_string(), status_list_credential: "https://example.com/status/1".to_string(), @@ -579,7 +475,7 @@ mod tests { let vc2_options = Some(VerifiableCredentialCreateOptions { credential_status: Some(CredentialStatus { id: "https://example.com/status/2".to_string(), - r#type: STATUS_LIST_2021.to_string(), + r#type: STATUS_LIST_2021_ENTRY.to_string(), status_purpose: "revocation".to_string(), status_list_index: "1023".to_string(), status_list_credential: "https://example.com/status/1".to_string(), @@ -598,7 +494,18 @@ mod tests { credentials_to_disable, ) .unwrap(); - let updated_encoded_list = &updated_status_list_credential.encoded_list; + + let updated_encoded_list = updated_status_list_credential.base.credential_subject + .additional_properties + .as_ref() + .and_then(|props| props.properties.get("encodedList")) + .and_then(|value| { + if let JsonValue::String(s) = value { + Some(s.as_str()) + } else { + None + } + }).unwrap(); // Test the bits corresponding to the disabled credentials assert_eq!( diff --git a/docs/API_DESIGN.md b/docs/API_DESIGN.md index cc7d3cc4..570eb942 100644 --- a/docs/API_DESIGN.md +++ b/docs/API_DESIGN.md @@ -136,13 +136,11 @@ CLASS CreateOptions #### `StatusListCredential` ```pseudocode! -CLASS StatusListCredential IMPLEMENTS VerifiableCredential - PUBLIC DATA status_purpose: string - PUBLIC DATA credentials_to_disable: []VerifiableCredential +CLASS StatusListCredential + PUBLIC DATA base: VerifiableCredential - CONSTRUCTOR create(issuer: Issuer, status_purpose: string, credentials_to_disable: []VerifiableCredential, options: CreateOptions?) + CONSTRUCTOR create(issuer: Issuer, status_purpose: string, disabled_credentials: []VerifiableCredential) - METHOD update_credentials_to_disable(credentials_to_disable: []VerifiableCredential): StatusListCredential METHOD is_disabled(credential VerifiableCredential): bool ``` From 5dbe68f202700461684a4a7e07ba7da265617d38 Mon Sep 17 00:00:00 2001 From: Neal Date: Tue, 3 Sep 2024 11:27:05 -0700 Subject: [PATCH 05/10] lint --- bindings/web5_uniffi/src/lib.rs | 2 +- .../src/credentials/mod.rs | 2 +- .../src/credentials/status_list_credential.rs | 18 ++++--- .../src/credentials/status_list_credential.rs | 54 ++++++++++++++----- 4 files changed, 53 insertions(+), 23 deletions(-) diff --git a/bindings/web5_uniffi/src/lib.rs b/bindings/web5_uniffi/src/lib.rs index 23706759..8789f8df 100644 --- a/bindings/web5_uniffi/src/lib.rs +++ b/bindings/web5_uniffi/src/lib.rs @@ -1,8 +1,8 @@ use web5_uniffi_wrapper::{ credentials::{ presentation_definition::PresentationDefinition, + status_list_credential::StatusListCredential, verifiable_credential_1_1::{VerifiableCredential, VerifiableCredentialData}, - status_list_credential::{StatusListCredential} }, crypto::{ dsa::{ diff --git a/bindings/web5_uniffi_wrapper/src/credentials/mod.rs b/bindings/web5_uniffi_wrapper/src/credentials/mod.rs index 01f8e3ab..00344386 100644 --- a/bindings/web5_uniffi_wrapper/src/credentials/mod.rs +++ b/bindings/web5_uniffi_wrapper/src/credentials/mod.rs @@ -1,3 +1,3 @@ pub mod presentation_definition; -pub mod verifiable_credential_1_1; pub mod status_list_credential; +pub mod verifiable_credential_1_1; diff --git a/bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs b/bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs index 941f82f2..ea756359 100644 --- a/bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs +++ b/bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs @@ -1,13 +1,13 @@ -use std::sync::Arc; +use crate::credentials::verifiable_credential_1_1::VerifiableCredential; use crate::errors::Result; +use std::sync::Arc; use web5::{ + credentials::status_list_credential::StatusListCredential as InnerStatusListCredential, credentials::verifiable_credential_1_1::{ Issuer, VerifiableCredential as InnerVerifiableCredential, }, - credentials::status_list_credential::StatusListCredential as InnerStatusListCredential, json::FromJson, }; -use crate::credentials::verifiable_credential_1_1::{VerifiableCredential}; pub struct StatusListCredential(pub InnerStatusListCredential); @@ -19,18 +19,22 @@ impl StatusListCredential { ) -> Result { let issuer = Issuer::from_json_string(&json_serialized_issuer)?; - let inner_vcs: Option> = credentials_to_disable - .map(|credentials| { + let inner_vcs: Option> = + credentials_to_disable.map(|credentials| { credentials .into_iter() .map(|vc| vc.inner_vc.clone()) .collect() }); - Ok(Self(InnerStatusListCredential::create(issuer, status_purpose, inner_vcs)?)) + Ok(Self(InnerStatusListCredential::create( + issuer, + status_purpose, + inner_vcs, + )?)) } pub fn is_disabled(&self, credential: Arc) -> Result { Ok(self.0.is_disabled(&credential.inner_vc.clone())?) } -} \ No newline at end of file +} diff --git a/crates/web5/src/credentials/status_list_credential.rs b/crates/web5/src/credentials/status_list_credential.rs index 20f60fe0..ea1fe74c 100644 --- a/crates/web5/src/credentials/status_list_credential.rs +++ b/crates/web5/src/credentials/status_list_credential.rs @@ -92,16 +92,22 @@ impl StatusListCredential { /// println!("Credential is disabled: {}", is_disabled); /// pub fn is_disabled(&self, credential: &VerifiableCredential) -> Result { - let status = credential.credential_status.as_ref() - .ok_or_else(|| Web5Error::Parameter("no credential status found in credential".to_string()))?; + let status = credential.credential_status.as_ref().ok_or_else(|| { + Web5Error::Parameter("no credential status found in credential".to_string()) + })?; // Check if the status type matches if status.r#type != STATUS_LIST_2021_ENTRY { - return Err(Web5Error::Parameter(format!("unsupported status type: {}", status.r#type))); + return Err(Web5Error::Parameter(format!( + "unsupported status type: {}", + status.r#type + ))); } // Check if the status purpose matches - let status_purpose = self.base.credential_subject + let status_purpose = self + .base + .credential_subject .additional_properties .as_ref() .and_then(|props| props.properties.get("statusPurpose")) @@ -112,17 +118,27 @@ impl StatusListCredential { None } }) - .ok_or_else(|| Web5Error::Parameter("no valid statusPurpose found in status list credential".to_string()))?; + .ok_or_else(|| { + Web5Error::Parameter( + "no valid statusPurpose found in status list credential".to_string(), + ) + })?; if status_purpose != status.status_purpose { return Err(Web5Error::Parameter("status purpose mismatch".to_string())); } // Get the bit index - let index = status.status_list_index.parse::() - .map_err(|_| Web5Error::Parameter(format!("invalid status list index: {}", status.status_list_index)))?; + let index = status.status_list_index.parse::().map_err(|_| { + Web5Error::Parameter(format!( + "invalid status list index: {}", + status.status_list_index + )) + })?; - let encoded_list = match self.base.credential_subject + let encoded_list = match self + .base + .credential_subject .additional_properties .as_ref() .and_then(|props| props.properties.get("encodedList")) @@ -134,11 +150,15 @@ impl StatusListCredential { } }) { Some(el) => el, - None => return Err(Web5Error::Parameter("invalid or missing encodedList".to_string())), + None => { + return Err(Web5Error::Parameter( + "invalid or missing encodedList".to_string(), + )) + } }; // Check the bit in the encoded list - Self::get_bit(&encoded_list, index) + Self::get_bit(encoded_list, index) } /// Extracts status list indexes from a vector of verifiable credentials that match the specified status purpose. @@ -432,7 +452,9 @@ mod tests { let status_list_credential = StatusListCredential::create(issuer(), status_purpose, credentials_to_disable).unwrap(); - let encoded_list = status_list_credential.base.credential_subject + let encoded_list = status_list_credential + .base + .credential_subject .additional_properties .as_ref() .and_then(|props| props.properties.get("encodedList")) @@ -442,7 +464,8 @@ mod tests { } else { None } - }).unwrap(); + }) + .unwrap(); // Test various bit positions assert_eq!( @@ -495,7 +518,9 @@ mod tests { ) .unwrap(); - let updated_encoded_list = updated_status_list_credential.base.credential_subject + let updated_encoded_list = updated_status_list_credential + .base + .credential_subject .additional_properties .as_ref() .and_then(|props| props.properties.get("encodedList")) @@ -505,7 +530,8 @@ mod tests { } else { None } - }).unwrap(); + }) + .unwrap(); // Test the bits corresponding to the disabled credentials assert_eq!( From 44e0b9eda9beffe75d55c20a8dc8a071e405c952 Mon Sep 17 00:00:00 2001 From: Neal Date: Tue, 3 Sep 2024 12:28:39 -0700 Subject: [PATCH 06/10] remove comments --- .../main/kotlin/web5/sdk/vc/VerifiableCredential.kt | 10 ---------- crates/web5/Cargo.toml | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt b/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt index 1be34ffd..19974217 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt @@ -60,16 +60,6 @@ data class VerifiableCredential private constructor( val jsonSerializedCredentialSubject = Json.stringify(credentialSubject) val jsonSerializedEvidence = options?.evidence?.let { Json.stringify(it) } -// val rustCoreCredentialStatus = options?.credentialStatus?.let { -// RustCoreCredentialStatus( -// id = it.id, -// type = it.type, -// statusPurpose = it.statusPurpose, -// statusListIndex = it.statusListIndex, -// statusListCredential = it.statusListCredential -// ) -// } - val rustCoreVerifiableCredential = RustCoreVerifiableCredential.create( jsonSerializedIssuer, jsonSerializedCredentialSubject, diff --git a/crates/web5/Cargo.toml b/crates/web5/Cargo.toml index 08a4faf2..ba9594bf 100644 --- a/crates/web5/Cargo.toml +++ b/crates/web5/Cargo.toml @@ -8,7 +8,7 @@ license-file.workspace = true rust-version = "1.74.0" [dependencies] -base64.workspace = true +base64 = { workspace = true } byteorder = "1.5.0" chrono = { workspace = true } ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } From d9ca4f03d45a8d89c2a540cb146da0bf74dbb14c Mon Sep 17 00:00:00 2001 From: Neal Date: Tue, 3 Sep 2024 12:36:43 -0700 Subject: [PATCH 07/10] lint --- crates/web5/src/credentials/status_list_credential.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/web5/src/credentials/status_list_credential.rs b/crates/web5/src/credentials/status_list_credential.rs index b6834b32..bc300c5b 100644 --- a/crates/web5/src/credentials/status_list_credential.rs +++ b/crates/web5/src/credentials/status_list_credential.rs @@ -237,7 +237,7 @@ impl StatusListCredential { )) })?; - Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&compressed)) + Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(compressed)) } /// Retrieves the value of a specific bit from a compressed base64 URL-encoded bitstring From 97887444a57cf6ff946fa2b80a2c1ea92901367f Mon Sep 17 00:00:00 2001 From: nitro-neal <5314059+nitro-neal@users.noreply.github.com> Date: Tue, 3 Sep 2024 13:25:41 -0700 Subject: [PATCH 08/10] Update crates/web5/src/credentials/mod.rs Co-authored-by: Diane Huxley --- crates/web5/src/credentials/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/web5/src/credentials/mod.rs b/crates/web5/src/credentials/mod.rs index 1737f236..c043bb3c 100644 --- a/crates/web5/src/credentials/mod.rs +++ b/crates/web5/src/credentials/mod.rs @@ -8,7 +8,8 @@ mod josekit; mod jwt_payload_vc; pub mod presentation_definition; mod sign; -pub mod status_list_credential; +mod status_list_credential; +pub use status_list_credential::{StatusListCredential, STATUS_LIST_CREDENTIAL_CONTEXT, STATUS_LIST_CREDENTIAL_TYPE, STATUS_LIST_2021, STATUS_LIST_2021_ENTRY}; pub mod verifiable_credential_1_1; pub use credential_schema::CredentialSchema; From 3c793c2634add92b59fa4f3a2fc61bcbc17fc347 Mon Sep 17 00:00:00 2001 From: Neal Date: Tue, 3 Sep 2024 13:40:06 -0700 Subject: [PATCH 09/10] nits and updates --- .../src/credentials/status_list_credential.rs | 3 +- crates/web5/src/credentials/mod.rs | 5 +- .../src/credentials/status_list_credential.rs | 151 +++++++----------- 3 files changed, 67 insertions(+), 92 deletions(-) diff --git a/bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs b/bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs index c7f62cb6..54093d04 100644 --- a/bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs +++ b/bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs @@ -3,9 +3,8 @@ use crate::errors::Result; use std::sync::Arc; use web5::credentials::Issuer; use web5::{ - credentials::status_list_credential::StatusListCredential as InnerStatusListCredential, credentials::verifiable_credential_1_1::VerifiableCredential as InnerVerifiableCredential, - json::FromJson, + credentials::StatusListCredential as InnerStatusListCredential, json::FromJson, }; pub struct StatusListCredential(pub InnerStatusListCredential); diff --git a/crates/web5/src/credentials/mod.rs b/crates/web5/src/credentials/mod.rs index c043bb3c..e0fbaa87 100644 --- a/crates/web5/src/credentials/mod.rs +++ b/crates/web5/src/credentials/mod.rs @@ -9,7 +9,10 @@ mod jwt_payload_vc; pub mod presentation_definition; mod sign; mod status_list_credential; -pub use status_list_credential::{StatusListCredential, STATUS_LIST_CREDENTIAL_CONTEXT, STATUS_LIST_CREDENTIAL_TYPE, STATUS_LIST_2021, STATUS_LIST_2021_ENTRY}; +pub use status_list_credential::{ + StatusListCredential, STATUS_LIST_2021, STATUS_LIST_2021_ENTRY, STATUS_LIST_CREDENTIAL_CONTEXT, + STATUS_LIST_CREDENTIAL_TYPE, +}; pub mod verifiable_credential_1_1; pub use credential_schema::CredentialSchema; diff --git a/crates/web5/src/credentials/status_list_credential.rs b/crates/web5/src/credentials/status_list_credential.rs index bc300c5b..9a4908e6 100644 --- a/crates/web5/src/credentials/status_list_credential.rs +++ b/crates/web5/src/credentials/status_list_credential.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::io::{Read, Write}; use super::verifiable_credential_1_1::{VerifiableCredential, VerifiableCredentialCreateOptions}; @@ -34,21 +33,23 @@ impl StatusListCredential { let base64_bitstring = Self::bitstring_generation(status_list_indexes)?; // Construct the properties for the credential subject. - let additional_properties = { - let mut properties = HashMap::new(); - properties.insert( - "statusPurpose".to_string(), - JsonValue::String(status_purpose.clone()), - ); - properties.insert( - "type".to_string(), - JsonValue::String(STATUS_LIST_2021.to_string()), - ); - properties.insert( - "encodedList".to_string(), - JsonValue::String(base64_bitstring.clone()), - ); - JsonObject { properties } + let additional_properties = JsonObject { + properties: [ + ( + "statusPurpose".to_string(), + JsonValue::String(status_purpose), + ), + ( + "type".to_string(), + JsonValue::String(STATUS_LIST_2021.to_string()), + ), + ( + "encodedList".to_string(), + JsonValue::String(base64_bitstring), + ), + ] + .into_iter() + .collect(), }; let credential_subject = CredentialSubject { @@ -60,11 +61,7 @@ impl StatusListCredential { id: Some(format!("urn:uuid:{}", uuid::Uuid::new_v4())), context: Some(vec![STATUS_LIST_CREDENTIAL_CONTEXT.to_string()]), r#type: Some(vec![STATUS_LIST_CREDENTIAL_TYPE.to_string()]), - issuance_date: None, - expiration_date: None, - credential_status: None, - credential_schema: None, - evidence: None, + ..Default::default() }; let verifiable_credential = @@ -90,7 +87,6 @@ impl StatusListCredential { /// /// let is_disabled = status_list_credential.is_disabled(&credential_to_check)?; /// println!("Credential is disabled: {}", is_disabled); - /// pub fn is_disabled(&self, credential: &VerifiableCredential) -> Result { let status = credential.credential_status.as_ref().ok_or_else(|| { Web5Error::Parameter("no credential status found in credential".to_string()) @@ -105,24 +101,10 @@ impl StatusListCredential { } // Check if the status purpose matches - let status_purpose = self - .base - .credential_subject - .additional_properties - .as_ref() - .and_then(|props| props.properties.get("statusPurpose")) - .and_then(|value| { - if let JsonValue::String(s) = value { - Some(s.as_str()) - } else { - None - } - }) - .ok_or_else(|| { - Web5Error::Parameter( - "no valid statusPurpose found in status list credential".to_string(), - ) - })?; + let status_purpose = Self::get_additional_property( + &self.base.credential_subject.additional_properties, + "statusPurpose", + )?; if status_purpose != status.status_purpose { return Err(Web5Error::Parameter("status purpose mismatch".to_string())); @@ -136,26 +118,10 @@ impl StatusListCredential { )) })?; - let encoded_list = match self - .base - .credential_subject - .additional_properties - .as_ref() - .and_then(|props| props.properties.get("encodedList")) - .and_then(|value| { - if let JsonValue::String(s) = value { - Some(s.as_str()) - } else { - None - } - }) { - Some(el) => el, - None => { - return Err(Web5Error::Parameter( - "invalid or missing encodedList".to_string(), - )) - } - }; + let encoded_list = Self::get_additional_property( + &self.base.credential_subject.additional_properties, + "encodedList", + )?; // Check the bit in the encoded list Self::get_bit(encoded_list, index) @@ -181,7 +147,10 @@ impl StatusListCredential { })?; if status_list_entry.status_purpose != *status_purpose { - return Err(Web5Error::Parameter("status purpose mismatch".to_string())); + return Err(Web5Error::Parameter(format!( + "status purpose mismatch: expected '{}', found '{}'", + status_purpose, status_list_entry.status_purpose + ))); } let index = status_list_entry @@ -274,6 +243,21 @@ impl StatusListCredential { Ok(bit_integer == 1) } + + /// Helper function to extract a string property from the additional_properties + fn get_additional_property<'a>(props: &'a Option, key: &str) -> Result<&'a str> { + props + .as_ref() + .and_then(|p| p.properties.get(key)) + .and_then(|value| { + if let JsonValue::String(s) = value { + Some(s.as_str()) + } else { + None + } + }) + .ok_or_else(|| Web5Error::Parameter(format!("no valid {} found", key))) + } } #[cfg(test)] @@ -357,6 +341,7 @@ mod tests { .credential_subject .additional_properties .unwrap(); + assert_eq!( additional_properties .properties @@ -452,20 +437,14 @@ mod tests { let status_list_credential = StatusListCredential::create(issuer(), status_purpose, credentials_to_disable).unwrap(); - let encoded_list = status_list_credential - .base - .credential_subject - .additional_properties - .as_ref() - .and_then(|props| props.properties.get("encodedList")) - .and_then(|value| { - if let JsonValue::String(s) = value { - Some(s.as_str()) - } else { - None - } - }) - .unwrap(); + let encoded_list = StatusListCredential::get_additional_property( + &status_list_credential + .base + .credential_subject + .additional_properties, + "encodedList", + ) + .unwrap(); // Test various bit positions assert_eq!( @@ -518,20 +497,14 @@ mod tests { ) .unwrap(); - let updated_encoded_list = updated_status_list_credential - .base - .credential_subject - .additional_properties - .as_ref() - .and_then(|props| props.properties.get("encodedList")) - .and_then(|value| { - if let JsonValue::String(s) = value { - Some(s.as_str()) - } else { - None - } - }) - .unwrap(); + let updated_encoded_list = StatusListCredential::get_additional_property( + &updated_status_list_credential + .base + .credential_subject + .additional_properties, + "encodedList", + ) + .unwrap(); // Test the bits corresponding to the disabled credentials assert_eq!( From eebc84eecfd99395f21de33b62838bf0733705cf Mon Sep 17 00:00:00 2001 From: Kendall Weihe Date: Tue, 3 Sep 2024 18:16:45 -0400 Subject: [PATCH 10/10] Add base vc to status list credential --- bindings/web5_uniffi/src/web5.udl | 2 ++ .../src/credentials/status_list_credential.rs | 5 ++++ .../credentials/verifiable_credential_1_1.rs | 11 +++++++++ .../src/main/kotlin/web5/sdk/rust/UniFFI.kt | 24 +++++++++++++++++++ .../web5/sdk/vc/StatusListCredential.kt | 5 +++- .../web5/sdk/vc/VerifiableCredential.kt | 22 +++++++++++++++++ 6 files changed, 68 insertions(+), 1 deletion(-) diff --git a/bindings/web5_uniffi/src/web5.udl b/bindings/web5_uniffi/src/web5.udl index 0e4ba7f0..40533579 100644 --- a/bindings/web5_uniffi/src/web5.udl +++ b/bindings/web5_uniffi/src/web5.udl @@ -311,6 +311,8 @@ interface StatusListCredential { sequence? disabled_credentials ); + [Throws=Web5Error] + VerifiableCredential get_base(); [Throws=Web5Error] boolean is_disabled(VerifiableCredential credential); }; \ No newline at end of file diff --git a/bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs b/bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs index 54093d04..ad71a5ae 100644 --- a/bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs +++ b/bindings/web5_uniffi_wrapper/src/credentials/status_list_credential.rs @@ -32,6 +32,11 @@ impl StatusListCredential { )?)) } + pub fn get_base(&self) -> Result> { + let vc = VerifiableCredential::from_inner(&self.0.base)?; + Ok(Arc::new(vc)) + } + pub fn is_disabled(&self, credential: Arc) -> Result { Ok(self.0.is_disabled(&credential.inner_vc.clone())?) } diff --git a/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs b/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs index cba3194b..f254ea20 100644 --- a/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs +++ b/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs @@ -2,6 +2,7 @@ use crate::{dids::bearer_did::BearerDid, errors::Result}; use std::{sync::Arc, time::SystemTime}; use web5::credentials::verifiable_credential_1_1::CredentialStatus; use web5::credentials::Issuer; +use web5::json::ToJson; use web5::{ credentials::{ verifiable_credential_1_1::{ @@ -110,6 +111,16 @@ impl VerifiableCredential { let vc_jwt = self.inner_vc.sign(&bearer_did.0, verification_method_id)?; Ok(vc_jwt) } + + pub(crate) fn from_inner(inner_vc: &InnerVerifiableCredential) -> Result { + let json_serialized_issuer = inner_vc.issuer.to_json_string()?; + let json_serialized_credential_subject = inner_vc.credential_subject.to_json_string()?; + Ok(Self { + inner_vc: inner_vc.clone(), + json_serialized_issuer, + json_serialized_credential_subject, + }) + } } #[derive(Clone)] diff --git a/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt b/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt index 263198a8..4573c97a 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt @@ -908,6 +908,8 @@ internal open class UniffiVTableCallbackInterfaceVerifier( + + @@ -1070,6 +1072,8 @@ internal interface UniffiLib : Library { ): Unit fun uniffi_web5_uniffi_fn_constructor_statuslistcredential_create(`jsonSerializedIssuer`: RustBuffer.ByValue,`statusPurpose`: RustBuffer.ByValue,`disabledCredentials`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): Pointer + fun uniffi_web5_uniffi_fn_method_statuslistcredential_get_base(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer fun uniffi_web5_uniffi_fn_method_statuslistcredential_is_disabled(`ptr`: Pointer,`credential`: Pointer,uniffi_out_err: UniffiRustCallStatus, ): Byte fun uniffi_web5_uniffi_fn_clone_verifiablecredential(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, @@ -1282,6 +1286,8 @@ internal interface UniffiLib : Library { ): Short fun uniffi_web5_uniffi_checksum_method_signer_sign( ): Short + fun uniffi_web5_uniffi_checksum_method_statuslistcredential_get_base( + ): Short fun uniffi_web5_uniffi_checksum_method_statuslistcredential_is_disabled( ): Short fun uniffi_web5_uniffi_checksum_method_verifiablecredential_get_data( @@ -1432,6 +1438,9 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_web5_uniffi_checksum_method_signer_sign() != 5738.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_web5_uniffi_checksum_method_statuslistcredential_get_base() != 15197.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_web5_uniffi_checksum_method_statuslistcredential_is_disabled() != 23900.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -5268,6 +5277,8 @@ public object FfiConverterTypeSigner: FfiConverter { public interface StatusListCredentialInterface { + fun `getBase`(): VerifiableCredential + fun `isDisabled`(`credential`: VerifiableCredential): kotlin.Boolean companion object @@ -5355,6 +5366,19 @@ open class StatusListCredential: Disposable, AutoCloseable, StatusListCredential } + @Throws(Web5Exception::class)override fun `getBase`(): VerifiableCredential { + return FfiConverterTypeVerifiableCredential.lift( + callWithPointer { + uniffiRustCallWithError(Web5Exception) { _status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_method_statuslistcredential_get_base( + it, _status) +} + } + ) + } + + + @Throws(Web5Exception::class)override fun `isDisabled`(`credential`: VerifiableCredential): kotlin.Boolean { return FfiConverterBoolean.lift( callWithPointer { diff --git a/bound/kt/src/main/kotlin/web5/sdk/vc/StatusListCredential.kt b/bound/kt/src/main/kotlin/web5/sdk/vc/StatusListCredential.kt index 66b513e0..f5792aca 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/vc/StatusListCredential.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/vc/StatusListCredential.kt @@ -4,6 +4,7 @@ import web5.sdk.Json import web5.sdk.rust.StatusListCredential as RustCoreStatusListCredential data class StatusListCredential( + val base: VerifiableCredential, internal val rustCoreStatusListCredential: RustCoreStatusListCredential ) { companion object { @@ -17,7 +18,9 @@ data class StatusListCredential( val rustCoreStatusListCredential = RustCoreStatusListCredential.create(jsonSerializedIssuer, statusPurpose, rustCoreCredentials) - return StatusListCredential(rustCoreStatusListCredential) + val baseVerifiableCredential = VerifiableCredential.fromRustCore(rustCoreStatusListCredential.getBase()) + + return StatusListCredential(baseVerifiableCredential, rustCoreStatusListCredential) } } diff --git a/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt b/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt index 19974217..db41afd7 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt @@ -51,6 +51,28 @@ data class VerifiableCredential private constructor( internal val rustCoreVerifiableCredential: RustCoreVerifiableCredential, ) { companion object { + internal fun fromRustCore(rustCoreVerifiableCredential: RustCoreVerifiableCredential): VerifiableCredential { + val data = rustCoreVerifiableCredential.getData() + + val issuer = Json.jsonMapper.readValue(data.jsonSerializedIssuer, Issuer::class.java) + val credentialSubject = Json.jsonMapper.readValue(data.jsonSerializedCredentialSubject, CredentialSubject::class.java) + val evidence = data.jsonSerializedEvidence?.let { Json.jsonMapper.readValue>>(it) } + + return VerifiableCredential( + data.context, + data.type, + data.id, + issuer, + credentialSubject, + Date.from(data.issuanceDate), + data.expirationDate?.let { Date.from(it) }, + data.credentialStatus?.let { CredentialStatus(it.id, it.type, it.statusPurpose, it.statusListIndex, it.statusListCredential) }, + data.credentialSchema?.let { CredentialSchema(it.id, it.type) }, + evidence, + rustCoreVerifiableCredential + ) + } + fun create( issuer: Issuer, credentialSubject: CredentialSubject,