diff --git a/.github/workflows/vc_model_spec_test.yaml b/.github/workflows/vc_model_spec_test.yaml new file mode 100644 index 0000000..c40dce2 --- /dev/null +++ b/.github/workflows/vc_model_spec_test.yaml @@ -0,0 +1,50 @@ +name: VC Model Spec Test +# Tests the VC Model Spec against the vc-test-suite - https://github.com/w3c/vc-test-suite +on: + pull_request: + branches: + - main +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + + - name: Install Protoc and cargo-make + run: ./container/deps.sh protoc cargo-make + + - uses: knox-networks/github-actions-public/.github/actions/setup-protofetch@main + with: + cross-repo-username: "developersKnox" + cross-repo-token: ${{ secrets.PROTOFETCH_GITHUB_TOKEN }} + + - name: Test protos + run: (cd registry_resolver; protofetch fetch) && git diff --exit-code + + - name: Build CLI + run: cargo build --package cli --release + + - name: Add CLI to bin + run: sudo cp $PWD/target/release/ssi_cli /bin/ssi_cli + + - name: Install Node & NPM + uses: actions/setup-node@v2 + with: + node-version: "20" + + - name: Clone & Install vc-test-suite + run: | + git clone https://github.com/knox-networks/vc-test-suite + cd vc-test-suite + npm install + + - name: Copy vc-test-suite config + run: cp tests/vc_model_spec_test/config.json vc-test-suite/config.json + + - name: Run vc-test-suite + run: | + cd vc-test-suite + npm run test diff --git a/Cargo.lock b/Cargo.lock index 5c7f5a2..9239c55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,54 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.75" @@ -284,6 +332,60 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "clap" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "cli" +version = "0.4.0" +dependencies = [ + "clap", + "ssi", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "contextual" version = "0.1.6" @@ -918,6 +1020,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1531,6 +1634,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb175ec8981211357b7b379869c2f8d555881c55ea62311428ec0de46d89bd5c" +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pbjson" version = "0.5.1" @@ -2306,6 +2415,50 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_valid" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0adc7a19d45e581abc6d169c865a0b14b84bb43a9e966d1cca4d733e70f7f35a" +dependencies = [ + "indexmap 1.9.3", + "itertools", + "num-traits", + "once_cell", + "paste", + "regex", + "serde", + "serde_json", + "serde_valid_derive", + "serde_valid_literal", + "thiserror", + "unicode-segmentation", +] + +[[package]] +name = "serde_valid_derive" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071237362e267e2a76ffe4434094e089dcd8b5e9d8423ada499e5550dcb0181d" +dependencies = [ + "paste", + "proc-macro-error", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "serde_valid_literal" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f57df292b1d64449f90794fc7a67efca0b21acca91493e64a46418a29bbe36b4" +dependencies = [ + "paste", + "regex", +] + [[package]] name = "sha2" version = "0.9.9" @@ -2579,6 +2732,7 @@ dependencies = [ "rstest", "serde", "serde_json", + "serde_valid", "sha2 0.10.7", "signature", "sophia", @@ -2602,6 +2756,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.5.0" @@ -3032,6 +3192,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "uninit" version = "0.3.0" @@ -3061,6 +3227,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca61eb27fa339aa08826a29f03e87b99b4d8f0fc2255306fd266bb1b6a9de498" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index dcca643..2b1e6f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,8 @@ members = [ "signature", "ssi", "ephemeral_resolver", - "ffi" + "ffi", + "cli", ] diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..fa8ed53 --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "cli" +version.workspace = true +edition.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = {version = "4.4.8", features = ["derive"]} +ssi = {path = "../ssi"} + +# binary definition +[[bin]] +name = "ssi_cli" +path = "src/main.rs" diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..f82cb88 --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,45 @@ +use std::str::FromStr; + +use clap::{command, Parser, Subcommand}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +struct CliArguments { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand, Debug)] +enum Command { + /// Read the JSON string file in input_file and parse it into a VerifiableCredential. Then prints the VerifiableCredential + TestIssueCredential { + #[arg(short, long)] + input_file: String, + }, + /// Read the JSON string file in input_file and parse it into a VerifiablePresentation. Then prints the VerifiablePresentation + TestIssuePresentation { + #[arg(short, long)] + input_file: String, + }, +} + +fn main() { + let args = CliArguments::parse(); + match args.command { + Command::TestIssueCredential { + input_file: input_file_path, + } => { + let input = std::fs::read_to_string(input_file_path).unwrap(); + let vc = ssi::credential::VerifiableCredential::from_str(&input).unwrap(); + println!("{}", vc); + } + Command::TestIssuePresentation { + input_file: input_file_path, + } => { + let input = std::fs::read_to_string(input_file_path).unwrap(); + let vc = ssi::credential::VerifiablePresentation::from_str(&input).unwrap(); + println!("{}", vc); + } + } +} diff --git a/core/Cargo.toml b/core/Cargo.toml index c9851e7..4338cb0 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -16,6 +16,7 @@ mockall = {workspace = true} thiserror = {workspace = true} json-ld = "0.15.0" sophia = { git = "https://github.com/pchampin/sophia_rs.git", rev = "572512bd4a13dce4ca52f9310ac907b06dbea556", features = ["jsonld","http_client"] } +serde_valid = "0.16.3" [dev-dependencies] rstest = "0.15.0" diff --git a/core/src/credential.rs b/core/src/credential.rs index e2d60b3..c89534c 100644 --- a/core/src/credential.rs +++ b/core/src/credential.rs @@ -7,64 +7,65 @@ // Users can also user their own types that implement trait X if they need a different structure // --- // Default context and Cred types are defaulted but can be redefined +mod validation; -pub type VerificationContext = Vec; +use serde_valid::json::{FromJsonStr, ToJsonString}; +use serde_valid::Validate; +use std::str::FromStr; -pub const BASE_CREDENDIAL_CONTEXT: &str = "https://www.w3.org/2018/credentials/v1"; +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(untagged)] +pub enum ContextValue { + String(String), + Object(std::collections::HashMap), +} + +pub type DocumentContext = Vec; + +pub const BASE_CREDENTIAL_CONTEXT: &str = "https://www.w3.org/2018/credentials/v1"; pub const BANK_ACCOUNT_CREDENTIAL_CONTEXT: &str = "https://w3id.org/traceability/v1"; #[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)] pub enum CredentialType { - #[serde(rename = "VerifiableCredential")] - Common, + VerifiableCredential, // credential type common to all credentials PermanentResidentCard, BankCard, BankAccount, + UniversityDegreeCredential, + AlumniCredential, } -impl CredentialType { - pub fn as_str(&self) -> &str { - match self { - CredentialType::Common => "VerifiableCredential", - CredentialType::PermanentResidentCard => "PermanentResidentCard", - CredentialType::BankCard => "BankCard", - CredentialType::BankAccount => "BankAccount", - } - } - - pub fn from_string(cred_type: &str) -> Option { - match cred_type { - "BankCard" => Some(CredentialType::BankCard), - "BankAccount" => Some(CredentialType::BankAccount), - "PermanentResidentCard" => Some(CredentialType::PermanentResidentCard), - "VerifiableCredential" => Some(CredentialType::Common), - _ => None, - } - } +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)] +pub enum PresentationType { + VerifiablePresentation, // presentation type common to all presentations + CredentialManagerPresentation, } -pub type CredentialSubject = std::collections::HashMap; - #[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] -pub struct VerifiableCredential { - #[serde(flatten)] - pub credential: Credential, - pub proof: crate::proof::DataIntegrityProof, +#[serde(untagged)] +pub enum CredentialSubject { + Single(std::collections::HashMap), + Set(Vec>), } -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Validate)] pub struct Credential { + #[validate(custom(validation::credential_context_validation))] #[serde(rename = "@context")] - pub context: Vec, + pub context: DocumentContext, - #[serde(rename = "id")] - pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, #[serde(rename = "type")] pub cred_type: Vec, #[serde(rename = "issuanceDate")] - pub issuance_date: String, + pub issuance_date: chrono::DateTime, //chrono by default serializes to RFC3339 + + #[serde(rename = "expirationDate")] + #[serde(skip_serializing_if = "Option::is_none")] + pub expiration_date: Option>, //chrono by default serializes to RFC3339 pub issuer: String, @@ -75,6 +76,96 @@ pub struct Credential { pub property_set: std::collections::HashMap, } +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Validate)] +pub struct VerifiableCredential { + #[serde(flatten)] + #[validate] + pub credential: Credential, + pub proof: crate::proof::CredentialProof, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Validate)] +pub struct Presentation { + #[serde(rename = "@context")] + pub context: DocumentContext, + + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + + #[serde(rename = "type")] + pub presentation_type: Vec, + + #[serde(rename = "verifiableCredential")] + #[serde(skip_serializing_if = "Option::is_none")] + #[validate] + pub verifiable_credential: Option>, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Validate)] +pub struct VerifiablePresentation { + #[serde(flatten)] + #[validate] + pub presentation: Presentation, + + pub proof: crate::proof::CredentialProof, +} + +impl FromStr for CredentialType { + type Err = super::error::Error; + + fn from_str(s: &str) -> Result { + match s { + "VerifiableCredential" => Ok(CredentialType::VerifiableCredential), + "PermanentResidentCard" => Ok(CredentialType::PermanentResidentCard), + "BankCard" => Ok(CredentialType::BankCard), + "BankAccount" => Ok(CredentialType::BankAccount), + "UniversityDegreeCredential" => Ok(CredentialType::UniversityDegreeCredential), + "AlumniCredential" => Ok(CredentialType::AlumniCredential), + _ => Err(super::error::Error::Unknown( + "Unknown CredentialType".to_string(), + )), + } + } +} + +impl CredentialType { + pub fn as_str(&self) -> &str { + match self { + CredentialType::VerifiableCredential => "VerifiableCredential", + CredentialType::PermanentResidentCard => "PermanentResidentCard", + CredentialType::BankCard => "BankCard", + CredentialType::BankAccount => "BankAccount", + CredentialType::UniversityDegreeCredential => "UniversityDegreeCredential", + CredentialType::AlumniCredential => "AlumniCredential", + } + } +} + +impl std::fmt::Display for VerifiableCredential { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.to_json_string() { + Ok(vc) => write!(f, "{}", vc), + Err(e) => write!(f, "Error: {}", e), + } + } +} + +impl FromStr for Credential { + type Err = super::error::Error; + + fn from_str(s: &str) -> Result { + Ok(Credential::from_json_str(s)?) + } +} + +impl FromStr for VerifiableCredential { + type Err = super::error::Error; + + fn from_str(s: &str) -> Result { + Ok(VerifiableCredential::from_json_str(s)?) + } +} + impl Credential { pub fn try_into_verifiable_credential( self, @@ -96,7 +187,7 @@ impl Credential { pub fn into_verifiable_credential( self, - integrity_proof: crate::proof::DataIntegrityProof, + integrity_proof: crate::proof::CredentialProof, ) -> VerifiableCredential { VerifiableCredential { credential: self, @@ -105,19 +196,29 @@ impl Credential { } } -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] -pub struct VerifiablePresentation { - #[serde(flatten)] - pub presentation: Presentation, - pub proof: crate::proof::DataIntegrityProof, +impl FromStr for Presentation { + type Err = super::error::Error; + + fn from_str(s: &str) -> Result { + Ok(Presentation::from_json_str(s)?) + } } -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] -pub struct Presentation { - #[serde(rename = "@context")] - pub context: VerificationContext, - #[serde(rename = "verifiableCredential")] - pub verifiable_credential: Vec, +impl FromStr for VerifiablePresentation { + type Err = super::error::Error; + + fn from_str(s: &str) -> Result { + Ok(VerifiablePresentation::from_json_str(s)?) + } +} + +impl std::fmt::Display for VerifiablePresentation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.to_json_string() { + Ok(vp) => write!(f, "{}", vp), + Err(e) => write!(f, "Error: {}", e), + } + } } #[cfg(test)] @@ -146,10 +247,11 @@ mod tests { } }); - let res = serde_json::from_str::(&expect.to_string()); + let res = VerifiableCredential::from_str(&expect.to_string()); assert!(res.is_ok()); if let Ok(vc) = res { let vc = serde_json::to_value(vc).unwrap(); + println!("{}", vc); assert_json_eq!(expect, vc); } Ok(()) @@ -183,7 +285,7 @@ mod tests { }, }); - let res = serde_json::from_str::(&expect.to_string()); + let res = Credential::from_str(&expect.to_string()); assert!(res.is_ok()); if let Ok(vc) = res { let vc = serde_json::to_value(vc).unwrap(); diff --git a/core/src/credential/validation.rs b/core/src/credential/validation.rs new file mode 100644 index 0000000..c579219 --- /dev/null +++ b/core/src/credential/validation.rs @@ -0,0 +1,66 @@ +use super::ContextValue; + +// Context must contain at least one URI +// The first URI must be https://www.w3.org/2018/credentials/v1 (use BASE_CREDENDIAL_CONTEXT) +pub fn credential_context_validation( + val: &[ContextValue], +) -> Result<(), serde_valid::validation::Error> { + match val.get(0) { + None => { + // Context must contain at least one URI + Err(serde_valid::validation::Error::Custom( + "Context must contain at least one URI".to_string(), + )) + } + Some(ContextValue::String(ref s)) if s != super::BASE_CREDENTIAL_CONTEXT => { + Err(serde_valid::validation::Error::Custom(format!( + "The first URI must be {}, instead found {}", + super::BASE_CREDENTIAL_CONTEXT, + s + ))) + } + Some(ContextValue::Object(_)) => Err(serde_valid::validation::Error::Custom( + "The first URI must be a string".to_string(), + )), + _ => Ok(()), + } +} + +#[cfg(test)] +mod tests { + use super::super::BASE_CREDENTIAL_CONTEXT; + use super::ContextValue; + + #[rstest::rstest] + #[case::empty_context( + vec![], + Err(serde_valid::validation::Error::Custom( + "Context must contain at least one URI".to_string() + )) + )] + #[case::first_uri_not_base( + vec![super::ContextValue::String("https://www.w3.org/2018/credentials/v2".to_string())], + Err(serde_valid::validation::Error::Custom( + "The first URI must be https://www.w3.org/2018/credentials/v1, instead found https://www.w3.org/2018/credentials/v2".to_string() + )) + )] + #[case::first_uri_not_string( + vec![super::ContextValue::Object(std::collections::HashMap::new())], + Err(serde_valid::validation::Error::Custom( + "The first URI must be a string".to_string() + )) + )] + #[case::valid_context( + vec![super::ContextValue::String(BASE_CREDENTIAL_CONTEXT.to_string())], + Ok(()) + )] + fn test_validate_credential_context( + #[case] context: Vec, + #[case] expected: Result<(), serde_valid::validation::Error>, + ) { + match super::credential_context_validation(&context) { + Ok(_) => assert!(expected.is_ok()), + Err(e) => assert_eq!(e.to_string(), expected.unwrap_err().to_string()), + } + } +} diff --git a/core/src/error.rs b/core/src/error.rs index ed0a2cd..15031d3 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -6,6 +6,9 @@ pub enum Error { #[error("Serde Error: {0}")] Serde(#[from] serde_json::Error), + #[error("Serde Valid Error: {0}")] + SerdeValid(#[from] serde_valid::Error), + #[error("Signature Error: {0}")] Signature(#[from] signature::suite::error::Error), } diff --git a/core/src/lib.rs b/core/src/lib.rs index 90c1cd5..14bfda7 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -90,16 +90,22 @@ impl Clone for MockDIDResolver { } pub trait DocumentBuilder { - fn get_contexts(cred_type: &credential::CredentialType) -> credential::VerificationContext { + fn get_contexts(cred_type: &credential::CredentialType) -> credential::DocumentContext { match cred_type { credential::CredentialType::BankAccount => { vec![ - credential::BASE_CREDENDIAL_CONTEXT.to_string(), - credential::BANK_ACCOUNT_CREDENTIAL_CONTEXT.to_string(), + credential::ContextValue::String( + credential::BASE_CREDENTIAL_CONTEXT.to_string(), + ), + credential::ContextValue::String( + credential::BANK_ACCOUNT_CREDENTIAL_CONTEXT.to_string(), + ), ] } _ => { - vec![credential::BASE_CREDENDIAL_CONTEXT.to_string()] + vec![credential::ContextValue::String( + credential::BASE_CREDENTIAL_CONTEXT.to_string(), + )] } } } @@ -119,11 +125,12 @@ pub trait DocumentBuilder { Ok(credential::Credential { context, - id: id.to_string(), - cred_type: vec![credential::CredentialType::Common, cred_type], - issuance_date: chrono::Utc::now().to_rfc3339(), + id: Some(id.to_string()), + cred_type: vec![credential::CredentialType::VerifiableCredential, cred_type], + issuance_date: chrono::Utc::now(), + expiration_date: None, issuer, - subject: cred_subject, + subject: credential::CredentialSubject::Single(cred_subject), property_set, }) } @@ -135,10 +142,12 @@ pub trait DocumentBuilder { &self, credentials: Vec, ) -> Result { - let context = Self::get_contexts(&credential::CredentialType::Common); + let context = Self::get_contexts(&credential::CredentialType::VerifiableCredential); Ok(credential::Presentation { context, - verifiable_credential: credentials, + id: None, + presentation_type: vec![credential::PresentationType::VerifiablePresentation], + verifiable_credential: Some(credentials), }) } } @@ -323,6 +332,7 @@ mod tests { let builder = DefaultDocumentBuilder {}; let mut expect_presentation = json!({ "@context" : ["https://www.w3.org/2018/credentials/v1"], + "type" : ["VerifiablePresentation"], "verifiableCredential":[ { "@context":["https://www.w3.org/2018/credentials/v1"], @@ -387,8 +397,8 @@ mod tests { let interim_presentation = builder .create_presentation(credentials) .expect("unable to create presentation from credentials"); - - let interim_proof = &interim_presentation.verifiable_credential[0].proof; + let verifiable_credential = interim_presentation.verifiable_credential.clone().unwrap(); + let interim_proof = &verifiable_credential[0].proof; let interim_proof = serde_json::to_value(interim_proof).unwrap(); expect_presentation["verifiableCredential"][0]["proof"] = interim_proof; diff --git a/core/src/proof.rs b/core/src/proof.rs index a8ad99c..cb02b48 100644 --- a/core/src/proof.rs +++ b/core/src/proof.rs @@ -6,8 +6,8 @@ mod normalization; pub struct DataIntegrityProof { #[serde(rename = "type")] pub proof_type: String, - #[serde(rename = "created")] - pub created: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub created: Option, #[serde(rename = "verificationMethod")] pub verification_method: String, #[serde(rename = "proofPurpose")] @@ -16,11 +16,38 @@ pub struct DataIntegrityProof { pub proof_value: String, } +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] +pub struct RsaSignature2018 { + #[serde(rename = "type")] + pub proof_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub created: Option, + #[serde(rename = "verificationMethod")] + pub verification_method: String, + #[serde(rename = "proofPurpose")] + pub proof_purpose: String, + pub jws: String, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] +#[serde(untagged)] +pub enum CredentialProof { + Single(ProofType), + Set(Vec), +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] +#[serde(untagged)] +pub enum ProofType { + Ed25519Signature2020(DataIntegrityProof), + RsaSignature2018(RsaSignature2018), +} + impl std::fmt::Display for DataIntegrityProof { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, - "{{\"type\": \"{}\", \"created\": \"{}\", \"verificationMethod\": \"{}\", \"proofPurpose\": \"{}\", \"proofValue\": \"{}\"}}", + "{{\"type\": \"{}\", \"created\": \"{:?}\", \"verificationMethod\": \"{}\", \"proofPurpose\": \"{}\", \"proofValue\": \"{}\"}}", self.proof_type, self.created, self.verification_method, self.proof_purpose, self.proof_value ) } @@ -35,20 +62,22 @@ pub fn create_data_integrity_proof( signer: &impl signature::suite::DIDSigner, unsecured_doc: serde_json::Value, relation: signature::suite::VerificationRelation, -) -> Result { +) -> Result { let transformed_data = normalization::create_hashed_normalized_doc(unsecured_doc)?; let mut hasher = sophia::c14n::hash::Sha256::initialize(); hasher.update(&transformed_data); let hash_data = hasher.finalize(); let proof = signer.encoded_relational_sign(&hash_data, relation)?; - Ok(DataIntegrityProof { - proof_type: signer.get_proof_type(), - created: chrono::Utc::now().to_rfc3339(), - verification_method: signer.get_verification_method(relation), - proof_purpose: relation.to_string(), - proof_value: proof, - }) + Ok(CredentialProof::Single(ProofType::Ed25519Signature2020( + DataIntegrityProof { + proof_type: signer.get_proof_type(), + created: Some(chrono::Utc::now().to_rfc3339()), + verification_method: signer.get_verification_method(relation), + proof_purpose: relation.to_string(), + proof_value: proof, + }, + ))) } #[cfg(test)] @@ -87,21 +116,28 @@ mod tests { assert!(res.is_ok()); match res { Ok(proof) => { - assert_eq!(proof.proof_type, signer.get_proof_type()); - assert_eq!( - proof.verification_method, - signer.get_verification_method(relation) - ); - assert_eq!(proof.proof_purpose, relation.to_string()); - let transformed_data = - crate::proof::normalization::create_hashed_normalized_doc(doc).unwrap(); - let mut hasher = sophia::c14n::hash::Sha256::initialize(); - hasher.update(&transformed_data); - let hash_data = hasher.finalize(); + if let super::CredentialProof::Single(super::ProofType::Ed25519Signature2020( + proof, + )) = proof + { + assert_eq!(proof.proof_type, signer.get_proof_type()); + assert_eq!( + proof.verification_method, + signer.get_verification_method(relation) + ); + assert_eq!(proof.proof_purpose, relation.to_string()); + let transformed_data = + crate::proof::normalization::create_hashed_normalized_doc(doc).unwrap(); + let mut hasher = sophia::c14n::hash::Sha256::initialize(); + hasher.update(&transformed_data); + let hash_data = hasher.finalize(); - assert!(verifier - .decoded_relational_verify(&hash_data, proof.proof_value, relation) - .is_ok()); + assert!(verifier + .decoded_relational_verify(&hash_data, proof.proof_value, relation) + .is_ok()); + } else { + panic!("Expected single proof but got set of proofs: {:?}", proof); + } } Err(e) => panic!("{e:?}"), } diff --git a/tests/vc_model_spec_test/config.json b/tests/vc_model_spec_test/config.json new file mode 100644 index 0000000..e2233a6 --- /dev/null +++ b/tests/vc_model_spec_test/config.json @@ -0,0 +1,41 @@ +{ + "generator": "ssi_cli test-issue-credential --input-file", + "presentationGenerator": "ssi_cli test-issue-presentation --input-file", + "generatorOptions": "", + + "sectionsNotSupported": [ + "jwt", + "zkp", + "evidence", + "tou", + "refresh", + "status", + "schema" + ], + + "jwt": { + "es256kPrivateKeyJwk": { + "kty": "EC", + "kid": "did:example:0xab#verikey-1", + "crv": "P-256K", + "x": "7KEKZa5xJPh7WVqHJyUpb2MgEe3nA8Rk7eUlXsmBl-M", + "y": "3zIgl_ml4RhapyEm5J7lvU-4f5jiBvZr4KgxUjEhl9o", + "key_ops": ["sign", "verify"], + "d": "IQkxsrZICFvhYEe6ft3wk-LISUUkcFj5uScVQAUGizo" + }, + "rs256PrivateKeyJwk": { + "kty": "RSA", + "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", + "e": "AQAB", + "d": "X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q", + "p": "83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs", + "q": "3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk", + "dp": "G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0", + "dq": "s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk", + "qi": "GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU", + "alg": "RS256", + "kid": "did:example:0xab#verikey-1" + }, + "aud": "did:example:0xcafe" + } +}