From f569b40b2f2c69be1e01b864c2778e7714433fed Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Wed, 26 Aug 2020 17:13:04 -0400 Subject: [PATCH 1/4] Add JWT VC support for RSA keys Re: #3 - Pass jwt tests in vc-test-suite - Added RSA key for testing - Convert RSA key to DER for use with jsonwebtoken/ring - Consolidate OneOrMany enums - Add Error type --- Cargo.toml | 2 + src/bin/ssi-vc-test/config.json | 19 +- src/bin/ssi-vc-test/main.rs | 144 +++++++--- src/der.rs | 144 ++++++++++ src/did.rs | 2 +- src/error.rs | 68 +++++ src/jwk.rs | 193 +++++++++++++ src/lib.rs | 12 +- src/vc.rs | 483 +++++++++++++++++++++++++------- tests/rsa2048-2020-08-25.der | Bin 0 -> 1191 bytes tests/rsa2048-2020-08-25.json | 14 + 11 files changed, 932 insertions(+), 149 deletions(-) create mode 100644 src/der.rs create mode 100644 src/error.rs create mode 100644 src/jwk.rs create mode 100644 tests/rsa2048-2020-08-25.der create mode 100644 tests/rsa2048-2020-08-25.json diff --git a/Cargo.toml b/Cargo.toml index bd4ed6619..e03e0c0c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,5 @@ serde_json = "1.0" pest = "2.1" pest_derive = "2.1" derive_builder = "0.9" +base64 = "0.12" +jsonwebtoken = "7" diff --git a/src/bin/ssi-vc-test/config.json b/src/bin/ssi-vc-test/config.json index 112de065e..dbdf0b214 100644 --- a/src/bin/ssi-vc-test/config.json +++ b/src/bin/ssi-vc-test/config.json @@ -3,7 +3,22 @@ "presentationGenerator": "../ssi/target/debug/ssi-vc-test generate-presentation", "generatorOptions": "", "sectionsNotSupported": [ - "jwt", "zkp" - ] + ], + "jwt": { + "rs256PrivateKeyJwk": { + "kty": "RSA", + "n": "ALG1_NjU1eiMpcQoezH1eIZcr2p4gmo7j_exsjqkr05qBHQTn5Rb2dOpEf_Q-nLbeORZOobJH52oNhhuvTQC1R9qOEQkerUIi0aSnDEbQ2BP7YRJnwbw85v1VZDiGH8bviKbqPSf0uVwzQ43M8dr_t6A4efgiB5FOOobxXgr6VEvi_EyUm4F9JI1ZP5uHiG_cIet67Zg-lM_ygbQqOZpkeExLXC2SmHajKd8Aozfk5YB4s_-KFV1mx1HnwhHvKxZ19YkJ6Qx1OPf0ml0KLndYwmnsRZ9uZ_gGvH_G3OVZvuUnkcInR2uhK7xIU739Xc-hqDT6dokb5vsCdYMcFiRGD0=", + "e": "AQAB", + "d": "Eym3sT4KLwBzo5pl5nY83-hAti92iLQRizkrKe22RbNi9Y1kKOBatdtGaJqFVztZZu5ERGKNuTd5VdsjJeekSbXviVGRtdHNCvgmRZlWA5261AgIUPxMmKW062GmGJbKQvscFfziBgHK6tyDBd8cZavqMFHi-7ilMYF7IsFBcJKM85x_30pnfd4YwhGQIc9hzv238aOwYKg8c-MzYhEVUnL273jaiLVlfZWQ5ca-GXJHmdOb_Y4fE5gpXfPFBseqleXsMp0VuXxCEsN30LIJHYscdPtbzLD3LFbuMJglFbQqYqssqymILGqJ7Tc2mB2LmXevfqRWz5D7A_K1WzvuoQ==", + "p": "ANwlk-eVXPQplCmr7VddX8MAlN5YWvfXkbJe2KOhyS7naSlfMyeW6I0z6q6MAI4h8cs9yEzwmN1oEl_6tZ_-NPd1Oda2Hq5jHx0Jq2P5exIMMbzTTHbB-LjMB4c-b1DZLOrL7ZpCS-CcEHvBz4phzHa7gqz2SrNIGozufbjS_tK5", + "q": "AM6nKRFqRgHiUtGc0xJawpXJeokGhJQFfinDlakjkptuRQNv0BOz8fRUxk6zwwYrx-T_Yk-0oAFsD8qWIgiXg8Wf0bdRW0L0dIH4c6ff3mSREXeAT2h3XDaF0F1YKns08WyYWtOuIiYWChyO9sweK7AUuaOJ-6lr6lElzTGHVf-l", + "dp": "AIHFBPK2cRzchaIq3rVpLVHdveNzYexG_nOOxVVvwRANCUiB_b2Qj3Ts7aIGlS0zhTyxJql0Cig5eNtrBjVRvBdC2t1ebaeOdoC_enBsV8fDuG3-gExg-ySz4JwwiZ2252tg2qbb_a5hULYjARwpmkVDMzyR0mbsUfpRe3q_pcbB", + "dq": "Id2bCVOVLXHdiKReor9k7A8cmaAL0gYkasu2lwVRXU9w1-NXAiOXHydVaEhlSXmbRJflkJJVNmZzIAwCf830tko-oAAhKJPPFA2XRoeVdn2fkynf2YrV_cloICP2skI23kkJeW8sAXnTJmL3ZvP6zNxYn8hZCaa5u5qqSdeX7FE=", + "qi": "WKIToXXnjl7GDbz7jCNbX9nWYOE5BDNzVmwiVOnyGoTZfwJ_qtgizj7pOapxi6dT9S9mMavmeAi6LAsEe1WUWtaKSNhbNh0PUGGXlXHGlhkS8jI1ot0e-scrHAuACE567YQ4VurpNorPKtZ5UENXIn74DEmt4l5m6902VF3X5Wo=", + "alg": "RS256", + "kid": "rsa2048-2020-08-25" + }, + "aud": "did:example:0xcafe" + } } diff --git a/src/bin/ssi-vc-test/main.rs b/src/bin/ssi-vc-test/main.rs index 8d1f0bfa4..3dac3384d 100644 --- a/src/bin/ssi-vc-test/main.rs +++ b/src/bin/ssi-vc-test/main.rs @@ -1,3 +1,4 @@ +use ssi::jwk::JWTKeys; use ssi::vc::Contexts; use ssi::vc::Credential; use ssi::vc::Presentation; @@ -11,6 +12,12 @@ fn generate(data: String) -> String { if !doc.type_.contains(&"VerifiableCredential".to_string()) { panic!("Missing type VerifiableCredential"); } + if doc.issuer.is_none() { + panic!("Missing issuer"); + } + if doc.issuance_date.is_none() { + panic!("Missing issuance date"); + } // work around https://github.com/w3c/vc-test-suite/issues/96 if doc.type_.len() > 1 { @@ -21,12 +28,25 @@ fn generate(data: String) -> String { } } - // @TODO: sign/verify - return serde_json::to_string_pretty(&doc).unwrap(); + serde_json::to_string_pretty(&doc).unwrap() +} + +fn generate_jwt(data: &String, keys: &JWTKeys, aud: &String, sign: bool) -> String { + let vc: Credential = serde_json::from_str(data).unwrap(); + if sign { + vc.encode_sign_jwt(keys, aud).unwrap() + } else { + vc.encode_jwt_unsigned(aud).unwrap() + } +} + +fn decode_jwt_unsigned(data: &String) -> String { + let vc = Credential::from_jwt_unsigned(data).unwrap(); + serde_json::to_string_pretty(&vc).unwrap() } -fn generate_presentation(data: String) -> String { - let doc: Presentation = serde_json::from_str(&data).unwrap(); +fn generate_presentation(data: &String) -> String { + let doc: Presentation = serde_json::from_str(data).unwrap(); if !doc.type_.contains(&"VerifiablePresentation".to_string()) { panic!("Missing type VerifiablePresentation"); } @@ -36,57 +56,109 @@ fn generate_presentation(data: String) -> String { panic!("Missing proof"); } - // @TODO: sign/verify - let response = serde_json::to_string_pretty(&doc).unwrap(); - return response; + serde_json::to_string_pretty(&doc).unwrap() } -fn read_json(filename: &String) -> String { - let mut file = match std::fs::File::open(filename) { - Err(err) => panic!("Unable to open {}: {}", filename, err), - Ok(file) => file, - }; - let mut data = String::new(); +fn generate_jwt_presentation(data: &String, keys: &JWTKeys, aud: &String) -> String { + let vp: Presentation = serde_json::from_str(data).unwrap(); + vp.encode_sign_jwt(keys, aud).unwrap() +} +fn read_file(filename: &String) -> String { + let mut file = std::fs::File::open(filename).unwrap(); + let mut data = String::new(); use std::io::Read; - let data = match file.read_to_string(&mut data) { - Err(err) => panic!("Unable to read {}: {}", filename, err), - Ok(_) => data, - }; - // TODO: parse JSON + file.read_to_string(&mut data).unwrap(); data } -fn write_json(data: String) { +fn write_out(data: String) { use std::io::Write; let stdout = std::io::stdout(); - let mut handle = stdout.lock(); - match handle.write_all(data.as_bytes()) { - Err(err) => panic!("Unable to write output: {}", err), - Ok(_) => {} - } + stdout.lock().write_all(data.as_bytes()).unwrap(); } fn main() { - let args: Vec = std::env::args().collect(); - if args.len() != 3 { + let args = std::env::args(); + let mut cmd: Option = None; + let mut filename: Option = None; + let mut jwt_keys: Option = None; + let mut jwt_aud: Option = None; + let mut jwt_no_jws = false; + let mut jwt_presentation = false; + let mut jwt_decode = false; + let mut args_iter = args.into_iter(); + let _bin = args_iter.next().unwrap(); + loop { + match args_iter.next() { + Some(arg) => match (arg.starts_with("--"), arg.as_ref()) { + (true, "--jwt") => match args_iter.next() { + Some(jwt_b64) => { + let jwt_json = base64::decode(jwt_b64).unwrap(); + jwt_keys = Option::Some(serde_json::from_slice(&jwt_json).unwrap()); + } + None => {} + }, + (true, "--jwt-aud") => jwt_aud = args_iter.next(), + (true, "--jwt-no-jws") => jwt_no_jws = true, + (true, "--jwt-presentation") => jwt_presentation = true, + (true, "--jwt-decode") => jwt_decode = true, + (true, _) => panic!("Unexpected option '{}'", arg), + (false, _) => { + if cmd == None { + cmd = Option::Some(arg); + } else if filename == None { + filename = Option::Some(arg); + } else { + panic!("Unexpected argument '{}'", arg); + } + } + }, + None => break, + } + } + if cmd == None || filename == None { return usage(); } - let cmd = &args[1]; - let filename = &args[2]; - match &cmd[..] { + let cmd_str = cmd.unwrap(); + match cmd_str.as_ref() { "generate" => { - let data: String = read_json(&filename); - let output: String = generate(data); - write_json(output); + let data: String = read_file(&filename.unwrap()); + let output: String; + if jwt_decode { + output = decode_jwt_unsigned(&data); + } else if let Some(keys) = jwt_keys { + if let Some(aud) = jwt_aud { + output = generate_jwt(&data, &keys, &aud, !jwt_no_jws); + } else { + panic!("Expected --jwt-aud with --jwt"); + } + } else { + output = generate(data); + } + write_out(output); } "generate-presentation" => { - let data: String = read_json(&filename); - let output: String = generate_presentation(data); - write_json(output); + let data: String = read_file(&filename.unwrap()); + let output: String; + if let Some(keys) = jwt_keys { + if let Some(aud) = jwt_aud { + if !jwt_presentation { + // vc-test-suite says this is optional, but it seems + // to be always used. + panic!("Expected --jwt-presentation with --jwt"); + } + output = generate_jwt_presentation(&data, &keys, &aud); + } else { + panic!("Expected --jwt-aud with --jwt"); + } + } else { + output = generate_presentation(&data); + } + write_out(output); } _ => { - eprintln!("Unexpected command '{}'", cmd); + eprintln!("Unexpected command '{}'", cmd_str); std::process::exit(1); } } diff --git a/src/der.rs b/src/der.rs new file mode 100644 index 000000000..961f6fbb7 --- /dev/null +++ b/src/der.rs @@ -0,0 +1,144 @@ +// http://luca.ntop.org/Teaching/Appunti/asn1.html +// https://tls.mbed.org/kb/cryptography/asn1-key-structures-in-der-and-pem +// https://en.wikipedia.org/wiki/Distinguished_Encoding_Rules#BER_encoding +// https://serde.rs/impl-serializer.html +// ISO/IEC 8825-1:2015 (E) +// https://tools.ietf.org/html/rfc8017#page-55 + +pub trait ASN1: Clone { + fn as_bytes(self) -> Vec; +} + +#[derive(Debug, Clone)] +pub struct RSAPrivateKey { + pub modulus: Integer, + pub public_exponent: Integer, + pub private_exponent: Integer, + pub prime1: Integer, + pub prime2: Integer, + pub exponent1: Integer, + pub exponent2: Integer, + pub coefficient: Integer, + pub other_prime_infos: Option, +} + +#[derive(Debug, Clone)] +pub struct OtherPrimeInfos(pub Vec); + +#[derive(Debug, Clone)] +pub struct OtherPrimeInfo { + pub prime: Integer, + pub exponent: Integer, + pub coefficient: Integer, +} + +#[derive(Debug, Clone)] +pub struct Integer(pub Vec); + +fn trim_bytes(bytes: &[u8]) -> Vec { + // Remove leading zeros from an array. + match bytes.into_iter().position(|&x| x != 0) { + Some(n) => bytes[n..].to_vec(), + None => vec![0], + } +} + +fn encode(tag: u8, constructed: bool, contents: Vec) -> Vec { + // prepare an ASN1 tag-length-value + let id = tag + | match constructed { + true => 0x20, + false => 0, + }; + let len = contents.len(); + let len_bytes = trim_bytes(&len.to_be_bytes()); + if len <= 127 { + return [vec![id, len_bytes[0]], contents].concat(); + } + let len_len = len_bytes.len(); + if len_len >= 127 { + // This can't really happen, since to_be_bytes returns an array of length 2, 4, or 8. + panic!("Key data too large"); + } + let len_len_bytes = trim_bytes(&len_len.to_be_bytes()); + [vec![id, 0x80 | len_len_bytes[0]], len_bytes, contents].concat() +} + +impl ASN1 for RSAPrivateKey { + fn as_bytes(self) -> Vec { + let multiprime = self.other_prime_infos.is_some(); + let version = Integer(vec![if multiprime { 1 } else { 0 }]); + encode( + 0x10, + true, + [ + version.as_bytes().to_vec(), + self.modulus.as_bytes().to_vec(), + self.public_exponent.as_bytes().to_vec(), + self.private_exponent.as_bytes().to_vec(), + self.prime1.as_bytes().to_vec(), + self.prime2.as_bytes().to_vec(), + self.exponent1.as_bytes().to_vec(), + self.exponent2.as_bytes().to_vec(), + self.coefficient.as_bytes().to_vec(), + self.other_prime_infos.as_bytes().to_vec(), + ] + .concat(), + ) + } +} + +impl ASN1 for Integer { + fn as_bytes(self) -> Vec { + encode(0x02, false, self.0) + } +} + +impl ASN1 for Option { + fn as_bytes(self) -> Vec { + match self { + Some(t) => t.as_bytes(), + None => vec![], + } + } +} + +impl ASN1 for OtherPrimeInfos { + fn as_bytes(self) -> Vec { + encode( + 0x10, + true, + self.0 + .into_iter() + .flat_map(|info| info.as_bytes()) + .collect(), + ) + } +} + +impl ASN1 for OtherPrimeInfo { + fn as_bytes(self) -> Vec { + encode( + 0x10, + true, + [ + self.prime.as_bytes().to_vec(), + self.exponent.as_bytes().to_vec(), + self.coefficient.as_bytes().to_vec(), + ] + .concat(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encode_integer() { + let integer = Integer(vec![5]); + let expected = vec![0x02, 0x01, 0x05]; + assert_eq!(integer.as_bytes(), expected); + } +} diff --git a/src/did.rs b/src/did.rs index 9e5e0c2f1..29ae3ef07 100644 --- a/src/did.rs +++ b/src/did.rs @@ -3,7 +3,7 @@ use std::collections::HashMap as Map; use chrono::prelude::*; use serde::{Deserialize, Serialize}; use serde_json; -use serde_json::{json, Value}; +use serde_json::Value; // *********************************************** // * Data Structures for Decentralized Identifiers diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 000000000..bb7788d17 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,68 @@ +// use std::error::Error as StdError; +use base64::DecodeError as Base64Error; +use jsonwebtoken::errors::Error as JWTError; +use serde_json::Error as JSONError; +use std::fmt; + +#[derive(Debug)] +pub enum Error { + InvalidSubject, + InvalidIssuer, + AlgorithmNotImplemented, + KeyTypeNotImplemented, + MissingKey, + MissingCredential, + MissingKeyParameters, + Key, + TimeError, + URI, + InvalidContext, + MissingContext, + JWT(JWTError), + Base64(Base64Error), + JSON(JSONError), + + #[doc(hidden)] + __Nonexhaustive, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::InvalidSubject => write!(f, "Invalid subject for JWT"), + Error::InvalidIssuer => write!(f, "Invalid issuer for JWT"), + Error::MissingKey => write!(f, "JWT key not found"), + Error::MissingKeyParameters => write!(f, "JWT key parameters not found"), + Error::MissingCredential => write!(f, "Verifiable credential not found in JWT"), + Error::Key => write!(f, "problem with JWT key"), + Error::AlgorithmNotImplemented => write!(f, "JWA algorithm not implemented"), + Error::KeyTypeNotImplemented => write!(f, "key type not implemented"), + Error::TimeError => write!(f, "Unable to convert date/time"), + Error::InvalidContext => write!(f, "Invalid context"), + Error::MissingContext => write!(f, "Missing context"), + Error::URI => write!(f, "Invalid URI"), + Error::Base64(e) => e.fmt(f), + Error::JWT(e) => e.fmt(f), + Error::JSON(e) => e.fmt(f), + _ => unreachable!(), + } + } +} + +impl From for Error { + fn from(err: JWTError) -> Error { + Error::JWT(err) + } +} + +impl From for Error { + fn from(err: Base64Error) -> Error { + Error::Base64(err) + } +} + +impl From for Error { + fn from(err: JSONError) -> Error { + Error::JSON(err) + } +} diff --git a/src/jwk.rs b/src/jwk.rs new file mode 100644 index 000000000..7e30d4126 --- /dev/null +++ b/src/jwk.rs @@ -0,0 +1,193 @@ +use std::convert::TryFrom; +use std::result::Result; + +use crate::der::{Integer, RSAPrivateKey, ASN1}; +use crate::error::Error; + +use serde::{Deserialize, Serialize}; + +// RFC 7515 - JSON Web Signature (JWS) +// RFC 7516 - JSON Web Encryption (JWE) +// RFC 7517 - JSON Web Key (JWK) +// RFC 7518 - JSON Web Algorithms (JWA) +// RFC 7519 - JSON Web Token (JWT) + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct JWTKeys { + #[serde(rename = "es256kPrivateKeyJwk")] + #[serde(skip_serializing_if = "Option::is_none")] + pub es256k_private_key: Option, + #[serde(rename = "rs256PrivateKeyJwk")] + #[serde(skip_serializing_if = "Option::is_none")] + pub rs256_private_key: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct JWK { + #[serde(rename = "crv")] + pub public_key_use: Option, + #[serde(rename = "key_ops")] + pub key_operations: Option>, + #[serde(rename = "alg")] + pub algorithm: Option, + #[serde(rename = "kid")] + pub key_id: Option, + #[serde(rename = "x5u")] + pub x509_url: Option, + #[serde(rename = "x5c")] + pub x509_certificate_chain: Option, + #[serde(rename = "x5t")] + pub x509_certificate_sha1: Option, + #[serde(rename = "x5t#S256")] + pub x509_certificate_sha256: Option, + #[serde(flatten)] + pub params: Params, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "kty")] +pub enum Params { + EC(ECParams), + RSA(RSAParams), + Symmetric(SymmetricParams), + // @TODO: OKP (RFC 8037) +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ECParams { + // Parameters for Elliptic Curve Public Keys + #[serde(rename = "crv")] + pub curve: Option, + #[serde(rename = "x")] + pub x_coordinate: Option, + #[serde(rename = "y")] + pub y_coordinate: Option, + + // Parameters for Elliptic Curve Private Keys + #[serde(rename = "d")] + pub ecc_private_key: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RSAParams { + // Parameters for RSA Public Keys + #[serde(rename = "n")] + pub modulus: Option, + #[serde(rename = "e")] + pub exponent: Option, + + // Parameters for RSA Private Keys + #[serde(rename = "d")] + pub private_exponent: Option, + #[serde(rename = "p")] + pub first_prime_factor: Option, + #[serde(rename = "q")] + pub second_prime_factor: Option, + #[serde(rename = "dp")] + pub first_prime_factor_crt_exponent: Option, + #[serde(rename = "dq")] + pub second_prime_factor_crt_exponent: Option, + #[serde(rename = "qi")] + pub first_crt_coefficient: Option, + #[serde(rename = "oth")] + pub other_primes_info: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename = "oct")] +pub struct SymmetricParams { + // Parameters for Symmetric Keys + #[serde(rename = "k")] + pub key_value: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Prime { + #[serde(rename = "r")] + pub prime_factor: String, // Base64urlUInt + #[serde(rename = "d")] + pub factor_crt_exponent: String, // Base64urlUInt + #[serde(rename = "t")] + pub factor_crt_coefficient: String, // Base64urlUInt +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(try_from = "String")] +pub struct Base64urlUInt(pub Vec); + +impl JWK { + pub fn to_der(&self) -> Result, Error> { + match &self.params { + // EC(params) => params.to_der(), + Params::RSA(params) => params.to_der(), + // Symmetric(params) => params.to_der(), + _ => Err(Error::KeyTypeNotImplemented), + } + } +} + +impl RSAParams { + pub fn to_der(&self) -> Result, Error> { + let key = RSAPrivateKey { + modulus: match &self.modulus { + Some(integer) => Integer(integer.0.clone()), + None => Integer(vec![]), + }, + public_exponent: match &self.exponent { + Some(integer) => Integer(integer.0.clone()), + None => Integer(vec![]), + }, + private_exponent: match &self.private_exponent { + Some(integer) => Integer(integer.0.clone()), + None => Integer(vec![]), + }, + prime1: match &self.first_prime_factor { + Some(integer) => Integer(integer.0.clone()), + None => Integer(vec![]), + }, + prime2: match &self.second_prime_factor { + Some(integer) => Integer(integer.0.clone()), + None => Integer(vec![]), + }, + exponent1: match &self.first_prime_factor_crt_exponent { + Some(integer) => Integer(integer.0.clone()), + None => Integer(vec![]), + }, + exponent2: match &self.second_prime_factor_crt_exponent { + Some(integer) => Integer(integer.0.clone()), + None => Integer(vec![]), + }, + coefficient: match &self.first_crt_coefficient { + Some(integer) => Integer(integer.0.clone()), + None => Integer(vec![0]), + }, + other_prime_infos: None, + }; + Ok(key.as_bytes()) + } +} + +impl TryFrom for Base64urlUInt { + type Error = Error; + fn try_from(data: String) -> Result { + match base64::decode_config(data, base64::URL_SAFE) { + Ok(bytes) => Ok(Base64urlUInt(bytes)), + Err(err) => Err(Error::Base64(err)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn jwk_to_der_rsa() { + const JSON: &'static [u8] = include_bytes!("../tests/rsa2048-2020-08-25.json"); + const DER: &'static [u8] = include_bytes!("../tests/rsa2048-2020-08-25.der"); + + let key: JWK = serde_json::from_slice(JSON).unwrap(); + let der = key.to_der().unwrap(); + assert_eq!(der, DER); + } +} diff --git a/src/lib.rs b/src/lib.rs index 045e4bd57..f7fd982f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,22 +1,26 @@ +pub mod der; pub mod did; +pub mod error; +pub mod jwk; pub mod vc; + extern crate pest; #[macro_use] extern crate pest_derive; #[macro_use] extern crate derive_builder; -use pest::error::Error; -use pest::Parser; - #[derive(Parser)] #[grammar = "did.pest"] -struct DidParser; +pub struct DidParser; #[cfg(test)] mod tests { use super::*; + // use pest::error::Error; + use pest::Parser; + #[test] fn parse_did_components() { let input = "did:deadbeef:cafe/sub/path/?p1=v1&p2=v2#frag1"; diff --git a/src/vc.rs b/src/vc.rs index f8feb9a21..536e55962 100644 --- a/src/vc.rs +++ b/src/vc.rs @@ -1,10 +1,13 @@ use std::collections::HashMap as Map; use std::convert::TryFrom; +use crate::error::Error; +use crate::jwk::{JWTKeys, Params}; + use chrono::prelude::*; +use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; -use serde_json; -use serde_json::{json, Value}; +use serde_json::Value; // ******************************************** // * Data Structures for Verifiable Credentials @@ -12,26 +15,36 @@ use serde_json::{json, Value}; // * https://w3c.github.io/vc-data-model/ // ******************************************** // @TODO items: -// - `id` fields must all be URIs +// - implement HS256 and ES256 (RFC 7518) for JWT +// - ensure Credential in Presentation has credential_schema +// - more complete URI checking +// - decode Presentation from JWT +// - ensure refreshService id and credentialStatus id are URLs pub const DEFAULT_CONTEXT: &str = "https://www.w3.org/2018/credentials/v1"; -#[derive(Debug, Serialize, Deserialize)] +// work around https://github.com/w3c/vc-test-suite/issues/103 +pub const ALT_DEFAULT_CONTEXT: &str = "https://w3.org/2018/credentials/v1"; + +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Credential { #[serde(rename = "@context")] pub context: Contexts, - pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, #[serde(rename = "type")] - pub type_: Vec, - pub credential_subject: Subjects, - pub issuer: Issuer, - pub issuance_date: DateTime, // must be RFC3339 + pub type_: OneOrMany, + pub credential_subject: OneOrMany, + #[serde(skip_serializing_if = "Option::is_none")] + pub issuer: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub issuance_date: Option>, // must be RFC3339 // This field is populated only when using // embedded proofs such as LD-PROOF // https://w3c-ccg.github.io/ld-proofs/ #[serde(skip_serializing_if = "Option::is_none")] - pub proof: Option, + pub proof: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub expiration_date: Option>, // must be RFC3339 #[serde(skip_serializing_if = "Option::is_none")] @@ -39,76 +52,62 @@ pub struct Credential { #[serde(skip_serializing_if = "Option::is_none")] pub terms_of_use: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub evidence: Option, + pub evidence: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub credential_schema: Option, + pub credential_schema: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub refresh_service: Option, + pub refresh_service: Option>, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(untagged)] -#[serde(try_from = "ContextsUnchecked")] -pub enum Contexts { - One(Context), - Many(Vec), +pub enum OneOrMany { + One(T), + Many(Vec), } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(untagged)] -pub enum ContextsUnchecked { +#[serde(try_from = "OneOrMany")] +pub enum Contexts { One(Context), Many(Vec), } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(untagged)] pub enum Context { URI(URI), Object(Map), } -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum Subjects { - One(Subject), - Many(Vec), -} - -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Subject { #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, + pub id: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(flatten)] pub property_set: Option>, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(untagged)] pub enum Issuer { URI(URI), Object(ObjectWithId), } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ObjectWithId { - pub id: String, + pub id: URI, #[serde(skip_serializing_if = "Option::is_none")] #[serde(flatten)] pub property_set: Option>, } -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum Proofs { - One(Proof), - Many(Vec), -} - -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Proof { #[serde(rename = "type")] @@ -118,7 +117,7 @@ pub struct Proof { pub property_set: Option>, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct TermsOfUse { #[serde(skip_serializing_if = "Option::is_none")] @@ -128,14 +127,7 @@ pub struct TermsOfUse { pub type_: String, } -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum Evidences { - One(Evidence), - Many(Vec), -} - -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Evidence { #[serde(skip_serializing_if = "Option::is_none")] @@ -146,29 +138,22 @@ pub struct Evidence { pub property_set: Option>, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Status { - pub id: String, + pub id: URI, #[serde(rename = "type")] pub type_: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(try_from = "String")] #[serde(untagged)] pub enum URI { String(String), } -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum Schemas { - One(Schema), - Many(Vec), -} - -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Schema { pub id: URI, @@ -178,14 +163,7 @@ pub struct Schema { pub property_set: Option>, } -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum RefreshServices { - One(RefreshService), - Many(Vec), -} - -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct RefreshService { pub id: URI, @@ -195,75 +173,311 @@ pub struct RefreshService { pub property_set: Option>, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Presentation { #[serde(rename = "@context")] pub context: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, #[serde(rename = "type")] pub type_: Vec, - // @TODO: credential must have credential_schema - pub verifiable_credential: Vec, + pub verifiable_credential: Vec, // This field is populated only when using // embedded proofs such as LD-PROOF // https://w3c-ccg.github.io/ld-proofs/ #[serde(skip_serializing_if = "Option::is_none")] - pub proof: Option, + pub proof: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub holder: Option, + pub holder: Option, } -impl TryFrom for Contexts { - type Error = &'static str; - fn try_from(context: ContextsUnchecked) -> Result { - // first context must be the default - match context { - // @TODO: make more DRY - ContextsUnchecked::One(context) => match context { - Context::URI(URI::String(uri)) => { - if uri != DEFAULT_CONTEXT { - Err("Invalid context") - } else { - Ok(Contexts::One(Context::URI(URI::String(uri)))) - } - } - _ => Err("Base context must be URI"), - }, - ContextsUnchecked::Many(contexts) => { - if contexts.len() == 0 { - return Err("Missing context"); +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum CredentialOrJWT { + Credential(Credential), + JWT(String), +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct JWTClaims { + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "exp")] + pub expiration_time: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "iss")] + pub issuer: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "nbf")] + pub not_before: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "jti")] + pub jwt_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "sub")] + pub subject: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "aud")] + pub audience: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "vc")] + pub verifiable_credential: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "vp")] + pub verifiable_presentation: Option, +} + +impl OneOrMany { + pub fn len(&self) -> usize { + match self { + Self::One(_) => 1, + Self::Many(values) => values.len(), + } + } + + pub fn contains(&self, x: &T) -> bool + where + T: PartialEq, + { + match self { + Self::One(value) => x == value, + Self::Many(values) => values.contains(x), + } + } + + pub fn first(&self) -> Option<&T> { + match self { + Self::One(value) => Some(&value), + Self::Many(values) => { + if values.len() > 0 { + Some(&values[0]) + } else { + None } - let first_context = &contexts[0]; - match first_context { - Context::URI(URI::String(uri)) => { - if uri != DEFAULT_CONTEXT { - return Err("Invalid context"); - } - Ok(Contexts::Many(contexts)) - } - _ => Err("Base context must be URI"), + } + } + } + + pub fn to_single(&self) -> Option<&T> { + match self { + Self::One(value) => Some(&value), + Self::Many(values) => { + if values.len() == 1 { + Some(&values[0]) + } else { + None } } } } } +impl TryFrom> for Contexts { + type Error = Error; + fn try_from(context: OneOrMany) -> Result { + let first_uri = match context.first() { + None => return Err(Error::MissingContext), + Some(Context::URI(URI::String(uri))) => uri, + Some(Context::Object(_)) => return Err(Error::InvalidContext), + }; + if first_uri != DEFAULT_CONTEXT && first_uri != ALT_DEFAULT_CONTEXT { + return Err(Error::InvalidContext); + } + Ok(match context { + OneOrMany::One(context) => Contexts::One(context), + OneOrMany::Many(contexts) => Contexts::Many(contexts), + }) + } +} + impl TryFrom for URI { - type Error = &'static str; + type Error = Error; fn try_from(uri: String) -> Result { - // @TODO: more complete checking if uri.contains(":") { Ok(URI::String(uri)) } else { - Err("String is not a URI") + Err(Error::URI) } } } +impl From for String { + fn from(uri: URI) -> String { + let URI::String(string) = uri; + string + } +} + +fn base64_encode_json(object: &T) -> Result { + let json = serde_json::to_string(&object)?; + Ok(base64::encode_config(json, base64::URL_SAFE_NO_PAD)) +} + +fn jwt_encode(claims: &JWTClaims, keys: &JWTKeys) -> Result { + let mut header = Header::default(); + let key: EncodingKey; + if let Some(rs256_key) = &keys.rs256_private_key { + header.alg = Algorithm::RS256; + if let Some(ref key_id) = rs256_key.key_id { + header.kid = Some(key_id.to_owned()); + } + let der = rs256_key.to_der()?; + key = EncodingKey::from_rsa_der(&der); + } else if keys.es256k_private_key.is_some() { + return Err(Error::AlgorithmNotImplemented); + } else { + return Err(Error::MissingKey); + } + Ok(jsonwebtoken::encode(&header, claims, &key)?) +} + +impl Credential { + pub fn from_jwt_keys(jwt: &String, keys: &JWTKeys) -> Result { + if let Some(rs256_key) = &keys.rs256_private_key { + let validation = Validation::new(Algorithm::RS256); + let rsa_params = match &rs256_key.params { + Params::RSA(params) => params, + _ => return Err(Error::MissingKeyParameters), + }; + let modulus = match &rsa_params.modulus { + Some(n) => n.0.clone(), + None => return Err(Error::MissingKeyParameters), + }; + let exponent = match &rsa_params.exponent { + Some(n) => n.0.clone(), + None => return Err(Error::MissingKeyParameters), + }; + let modulus_b64 = base64::encode_config(modulus, base64::URL_SAFE_NO_PAD); + let exponent_b64 = base64::encode_config(exponent, base64::URL_SAFE_NO_PAD); + let key = DecodingKey::from_rsa_components(&modulus_b64, &exponent_b64); + Credential::from_jwt(jwt, &key, &validation) + } else if keys.es256k_private_key.is_some() { + Err(Error::AlgorithmNotImplemented) + } else { + Err(Error::MissingKey) + } + } + + pub fn from_jwt( + jwt: &String, + key: &DecodingKey, + validation: &Validation, + ) -> Result { + let token_data = jsonwebtoken::decode::(jwt, &key, validation)?; + Self::from_token_data(token_data) + } + + pub fn from_jwt_unsigned(jwt: &String) -> Result { + let token_data = jsonwebtoken::dangerous_insecure_decode::(jwt)?; + Self::from_token_data(token_data) + } + + pub fn from_token_data(token_data: jsonwebtoken::TokenData) -> Result { + let mut vc = match token_data.claims.verifiable_credential { + Some(vc) => vc, + None => return Err(Error::MissingCredential), + }; + if let Some(exp) = token_data.claims.expiration_time { + vc.expiration_date = Utc.timestamp_opt(exp, 0).latest(); + } + if let Some(iss) = token_data.claims.issuer { + vc.issuer = Some(Issuer::URI(URI::String(iss))); + } + if let Some(nbf) = token_data.claims.not_before { + if let Some(time) = Utc.timestamp_opt(nbf, 0).latest() { + vc.issuance_date = Some(time); + } else { + return Err(Error::TimeError); + } + } + if let Some(sub) = token_data.claims.subject { + if let OneOrMany::One(ref mut subject) = vc.credential_subject { + subject.id = Some(URI::String(sub)); + } else { + return Err(Error::InvalidSubject); + } + } + if let Some(id) = token_data.claims.jwt_id { + let uri = URI::try_from(id)?; + vc.id = Some(uri); + } + Ok(vc) + } + + fn to_jwt_claims(&self, aud: &String) -> Result { + let subject = match self.credential_subject.to_single() { + Some(subject) => subject, + None => return Err(Error::InvalidSubject), + }; + let subject_id: String = match subject.id.clone() { + Some(id) => id.into(), + // Credential subject must have id for JWT + None => return Err(Error::InvalidSubject), + }; + + let mut vc = self.clone(); + // Remove fields from vc that are duplicated into the claims, + // except for timestamps (in case of conversion discrepencies). + Ok(JWTClaims { + expiration_time: vc.expiration_date.map(|date| date.timestamp()), + issuer: match vc.issuer.take() { + Some(Issuer::URI(URI::String(uri))) => Some(uri), + Some(Issuer::Object(_)) => return Err(Error::InvalidIssuer), + None => None, + }, + not_before: vc.issuance_date.map(|date| date.timestamp()), + jwt_id: vc.id.take().map(|id| id.into()), + subject: Some(subject_id), + audience: Some(aud.clone()), + verifiable_credential: Some(vc), + verifiable_presentation: None, + }) + } + + pub fn encode_jwt_unsigned(&self, aud: &String) -> Result { + let claims = self.to_jwt_claims(aud)?; + Ok([ + base64_encode_json(&Header::default())?.as_ref(), + base64_encode_json(&claims)?.as_ref(), + "", + ] + .join(".")) + } + + pub fn encode_sign_jwt(&self, keys: &JWTKeys, aud: &String) -> Result { + let claims = self.to_jwt_claims(aud)?; + jwt_encode(&claims, &keys) + } +} + +impl Presentation { + pub fn encode_sign_jwt(&self, keys: &JWTKeys, aud: &String) -> Result { + let claims = JWTClaims { + expiration_time: None, + not_before: None, + subject: None, + issuer: self.holder.clone().map(|id| id.into()), + jwt_id: self.id.clone().map(|id| id.into()), + audience: Some(aud.clone()), + verifiable_credential: None, + verifiable_presentation: Some(self.clone()), + }; + jwt_encode(&claims, &keys) + } +} + #[cfg(test)] mod tests { use super::*; + #[derive(Debug, Serialize, Deserialize, Clone)] + struct Config { + #[serde(rename = "jwt")] + pub keys: JWTKeys, + #[serde(flatten)] + pub property_set: Option>, + } + #[test] fn credential_from_json() { let doc_str = r###"{ @@ -279,7 +493,8 @@ mod tests { let id = "http://example.org/credentials/3731"; let doc: Credential = serde_json::from_str(doc_str).unwrap(); println!("{}", serde_json::to_string_pretty(&doc).unwrap()); - assert_eq!(doc.id, id); + let id1: String = doc.id.unwrap().into(); + assert_eq!(id1, id); } #[test] @@ -322,4 +537,60 @@ mod tests { let doc: Credential = serde_json::from_str(doc_str).unwrap(); println!("{}", serde_json::to_string_pretty(&doc).unwrap()); } + + #[test] + fn encode_sign_jwt() { + let vc_str = r###"{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "http://example.org/credentials/192783", + "type": "VerifiableCredential", + "issuer": "https://example.org/issuers/1345", + "issuanceDate": "2020-08-25T11:26:53Z", + "expirationDate": "2021-08-25T00:00:00Z", + "credentialSubject": { + "id": "did:example:a6c78986cc36418b95a22d7f736", + "spouse": "Example Person" + } + }"###; + + const CONFIG: &'static [u8] = include_bytes!("bin/ssi-vc-test/config.json"); + let conf: Config = serde_json::from_slice(CONFIG).unwrap(); + + let vc: Credential = serde_json::from_str(vc_str).unwrap(); + let aud = "did:example:90336644520443d28ba78beb949".to_string(); + let signed_jwt = vc.encode_sign_jwt(&conf.keys, &aud).unwrap(); + println!("{:?}", signed_jwt); + } + + #[test] + fn decode_verify_jwt() { + const CONFIG: &'static [u8] = include_bytes!("bin/ssi-vc-test/config.json"); + let conf: Config = serde_json::from_slice(CONFIG).unwrap(); + + let vc_str = r###"{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "http://example.org/credentials/192783", + "type": "VerifiableCredential", + "issuer": "https://example.org/issuers/1345", + "issuanceDate": "2020-08-25T11:26:53Z", + "expirationDate": "2021-08-25T00:00:00Z", + "credentialSubject": { + "id": "did:example:a6c78986cc36418b95a22d7f736", + "spouse": "Example Person" + } + }"###; + + let vc: Credential = serde_json::from_str(vc_str).unwrap(); + let aud = "did:example:90336644520443d28ba78beb949".to_string(); + let signed_jwt = vc.encode_sign_jwt(&conf.keys, &aud).unwrap(); + + let vc1 = Credential::from_jwt_keys(&signed_jwt, &conf.keys).unwrap(); + assert_eq!(vc.id, vc1.id); + } } diff --git a/tests/rsa2048-2020-08-25.der b/tests/rsa2048-2020-08-25.der new file mode 100644 index 0000000000000000000000000000000000000000..9c2923d8076188e9639d6b3d1db7b12e4d30a990 GIT binary patch literal 1191 zcmV;Y1X%kpf&`-i0RRGm0RaH9wfxxB)#!|+#3*|)^>~I{uWERLYCDhjv9da(uTE+N zbQ7PHTiMg85&zKoa@%<1SvrQvADyT+7;e2Z0@WXCI7B3RwFrwwlAJLcLtszsgh`(U z@bjDXRgmHse;d9co2c}k(&cc?4mUH$YyRGV;pgCp9z{6n8^w4l=}|9>@iJ0w1@w|N zWd3d*A-`~kt?Ra6`cps32GFSHX_4VEEpWCME&;BS?b(=&L-D3%-u@-%~pWqtt{~L3aX8V+$M+luAu7s}fAx`)8cRq%o z)9KnIZ=381)(mi1kr+J!0|5X50)hbm6DhZ`J_;`YbEBGN=5{>a=s>nFc8IhQi#aPP z?Y2d;V)cz=DBxPP+eT=bg;zUSX6{5pVvV^sc~#pZCFi6`weN{hk+spy3iu{PnN|ax zy3_~=Q2b1orL^l|rWlsWLi-#Q{Ne@y%Ie&M1>YQHtLiXO;`_LzF@bv`!9j46jPsm- z-%4kF-Wb9WkRi`u&i%LXqp)D8JagkSVi6Tma`x|d+K9DfeU*^q#=aSHN14-`{f-|K zm?>TJ#RkW!mF4U*ofWx!LK4Gw(6R{~iyUTXTKCtHvR>Gu zp~)`iX(?YbCzj}qGwQC40FEK?%RR_U@R;3b5?}hYpZ+xWbvf3y9dWn#LQCMB5PQMTieb!lyMnCtO0!5BjP8B7(*Dx90)c@5 z&Zj96YDNL#Qqi2#5?aEQ$$E(fgp>t-DZ`biBa)kLMFVfp6SML3RK`xT!v-tIAW{Q6RW@dGAPfS3&GfcPKA-?0D3i|=4VOlTm3DoflPTZXiq-weXdol@vO+fANeOvx zECG4bCSvzy^ZLx(Sf9vQ38uNbnyN|Hm+Vmjfq+<|6QOnIj$XzMy!(tJTVL7MVBt9g zGjmpKB2?+}8id(@0)ML5BF;YPIjV7sr&IMWW-+VgcnG>I3j}*rlv>t`NZ4C89S=}p zmz8nGmKhTAGBu*z9{R^C91DO5PI~QxI9BTEHj2+G)_G7vS0aA+3`woxUS{jvHdI~L F Date: Thu, 27 Aug 2020 11:49:40 -0400 Subject: [PATCH 2/4] vc-test-suite: test zkp --- src/bin/ssi-vc-test/config.json | 4 +--- src/bin/ssi-vc-test/main.rs | 20 ++++++++++++++++++++ src/vc.rs | 18 ++++++++++++++++-- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/bin/ssi-vc-test/config.json b/src/bin/ssi-vc-test/config.json index dbdf0b214..635c2d7fe 100644 --- a/src/bin/ssi-vc-test/config.json +++ b/src/bin/ssi-vc-test/config.json @@ -2,9 +2,7 @@ "generator": "../ssi/target/debug/ssi-vc-test generate", "presentationGenerator": "../ssi/target/debug/ssi-vc-test generate-presentation", "generatorOptions": "", - "sectionsNotSupported": [ - "zkp" - ], + "sectionsNotSupported": [], "jwt": { "rs256PrivateKeyJwk": { "kty": "RSA", diff --git a/src/bin/ssi-vc-test/main.rs b/src/bin/ssi-vc-test/main.rs index 3dac3384d..56818fa66 100644 --- a/src/bin/ssi-vc-test/main.rs +++ b/src/bin/ssi-vc-test/main.rs @@ -18,6 +18,19 @@ fn generate(data: String) -> String { if doc.issuance_date.is_none() { panic!("Missing issuance date"); } + if doc.proof.is_none() { + panic!("Missing proof"); + } + + let is_zkp = match &doc.proof { + Some(proofs) => proofs.any(|proof| proof.type_.contains(&"CLSignature2019".to_string())), + _ => false, + }; + if is_zkp { + if doc.credential_schema.is_none() { + panic!("Missing credential schema for ZKP"); + } + } // work around https://github.com/w3c/vc-test-suite/issues/96 if doc.type_.len() > 1 { @@ -120,6 +133,13 @@ fn main() { if cmd == None || filename == None { return usage(); } + // work around https://github.com/w3c/vc-test-suite/issues/98 + if filename.as_ref().unwrap().contains("example-015-zkp") { + jwt_keys = None; + jwt_aud = None; + jwt_decode = false; + } + let cmd_str = cmd.unwrap(); match cmd_str.as_ref() { "generate" => { diff --git a/src/vc.rs b/src/vc.rs index 536e55962..b66c7c606 100644 --- a/src/vc.rs +++ b/src/vc.rs @@ -17,9 +17,13 @@ use serde_json::Value; // @TODO items: // - implement HS256 and ES256 (RFC 7518) for JWT // - ensure Credential in Presentation has credential_schema +// - ensure Credential has credential_schema if using ZKP +// - ensure vc/vp proof and vc issuance_date are set // - more complete URI checking // - decode Presentation from JWT // - ensure refreshService id and credentialStatus id are URLs +// - implement IntoIterator for OneOrMany, instead of using own +// functions for any, len, contains, etc. pub const DEFAULT_CONTEXT: &str = "https://www.w3.org/2018/credentials/v1"; @@ -181,8 +185,8 @@ pub struct Presentation { #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, #[serde(rename = "type")] - pub type_: Vec, - pub verifiable_credential: Vec, + pub type_: OneOrMany, + pub verifiable_credential: OneOrMany, // This field is populated only when using // embedded proofs such as LD-PROOF // https://w3c-ccg.github.io/ld-proofs/ @@ -228,6 +232,16 @@ pub struct JWTClaims { } impl OneOrMany { + pub fn any(&self, f: F) -> bool + where + F: Fn(&T) -> bool, + { + match self { + Self::One(value) => f(value), + Self::Many(values) => values.iter().any(f), + } + } + pub fn len(&self) -> usize { match self { Self::One(_) => 1, From 33cb0e5f6b6887746bd9831ea9f6f5047cc32634 Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Thu, 27 Aug 2020 13:56:08 -0400 Subject: [PATCH 3/4] Work around cardinality issues https://github.com/w3c/vc-test-suite/pull/106 --- src/bin/ssi-vc-test/main.rs | 17 ++++++++++------- src/vc.rs | 9 +++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/bin/ssi-vc-test/main.rs b/src/bin/ssi-vc-test/main.rs index 56818fa66..95ae07f9b 100644 --- a/src/bin/ssi-vc-test/main.rs +++ b/src/bin/ssi-vc-test/main.rs @@ -1,6 +1,7 @@ use ssi::jwk::JWTKeys; -use ssi::vc::Contexts; +use ssi::vc::Context; use ssi::vc::Credential; +use ssi::vc::OneOrMany; use ssi::vc::Presentation; fn usage() { @@ -33,12 +34,14 @@ fn generate(data: String) -> String { } // work around https://github.com/w3c/vc-test-suite/issues/96 - if doc.type_.len() > 1 { - if let Contexts::Many(ref context) = doc.context { - if context.len() == 1 { - panic!("If there are multiple types, there should be multiple contexts."); - } - } + let contexts: &OneOrMany = &doc.context.clone().into(); + if doc.type_.len() > 1 && contexts.len() <= 1 { + panic!("If there are multiple types, there should be multiple contexts."); + } + + // work around https://github.com/w3c/vc-test-suite/issues/97 + if contexts.len() > 1 && doc.type_.len() <= 1 { + panic!("If there are multiple contexts, there should be multiple types."); } serde_json::to_string_pretty(&doc).unwrap() diff --git a/src/vc.rs b/src/vc.rs index b66c7c606..7a6f81516 100644 --- a/src/vc.rs +++ b/src/vc.rs @@ -304,6 +304,15 @@ impl TryFrom> for Contexts { } } +impl From for OneOrMany { + fn from(contexts: Contexts) -> OneOrMany { + match contexts { + Contexts::One(context) => OneOrMany::One(context), + Contexts::Many(contexts) => OneOrMany::Many(contexts), + } + } +} + impl TryFrom for URI { type Error = Error; fn try_from(uri: String) -> Result { From 948b7a785af21fd09a973e52624196efe301a354 Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Thu, 27 Aug 2020 16:51:09 -0400 Subject: [PATCH 4/4] Explain DER tag bytes --- src/der.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/der.rs b/src/der.rs index 961f6fbb7..61c30a796 100644 --- a/src/der.rs +++ b/src/der.rs @@ -5,6 +5,9 @@ // ISO/IEC 8825-1:2015 (E) // https://tools.ietf.org/html/rfc8017#page-55 +const TAG_INTEGER: u8 = 0x02; +const TAG_SEQUENCE: u8 = 0x10; + pub trait ASN1: Clone { fn as_bytes(self) -> Vec; } @@ -46,6 +49,7 @@ fn trim_bytes(bytes: &[u8]) -> Vec { fn encode(tag: u8, constructed: bool, contents: Vec) -> Vec { // prepare an ASN1 tag-length-value let id = tag + // set bit for constructed (vs primitive) | match constructed { true => 0x20, false => 0, @@ -69,7 +73,7 @@ impl ASN1 for RSAPrivateKey { let multiprime = self.other_prime_infos.is_some(); let version = Integer(vec![if multiprime { 1 } else { 0 }]); encode( - 0x10, + TAG_SEQUENCE, true, [ version.as_bytes().to_vec(), @@ -90,7 +94,7 @@ impl ASN1 for RSAPrivateKey { impl ASN1 for Integer { fn as_bytes(self) -> Vec { - encode(0x02, false, self.0) + encode(TAG_INTEGER, false, self.0) } } @@ -106,7 +110,7 @@ impl ASN1 for Option { impl ASN1 for OtherPrimeInfos { fn as_bytes(self) -> Vec { encode( - 0x10, + TAG_SEQUENCE, true, self.0 .into_iter() @@ -119,7 +123,7 @@ impl ASN1 for OtherPrimeInfos { impl ASN1 for OtherPrimeInfo { fn as_bytes(self) -> Vec { encode( - 0x10, + TAG_SEQUENCE, true, [ self.prime.as_bytes().to_vec(), @@ -138,6 +142,9 @@ mod tests { #[test] fn encode_integer() { let integer = Integer(vec![5]); + // 0x02: Integer type + // 0x01: Content length of one byte + // 0x05: The integer 5 let expected = vec![0x02, 0x01, 0x05]; assert_eq!(integer.as_bytes(), expected); }