diff --git a/Cargo.toml b/Cargo.toml index 2c315c1f5..97e2e08a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ sha2 = { version = "0.9", optional = true } hyper = { version = "0.14", optional = true, features = ["server", "client", "http1", "stream"] } hyper-tls = { version = "0.5", optional = true } http = { version = "0.2", optional = true } +hex = "0.4" serde_urlencoded = "0.7" percent-encoding = { version = "2.1", optional = true } tokio = { version = "1.0", optional = true, features = ["macros"] } diff --git a/did-ethr/src/lib.rs b/did-ethr/src/lib.rs index 1a3731e5b..bf72f512e 100644 --- a/did-ethr/src/lib.rs +++ b/did-ethr/src/lib.rs @@ -210,10 +210,8 @@ mod tests { async fn credential_prove_verify_did_ethr() { eprintln!("with EcdsaSecp256k1RecoveryMethod2020..."); credential_prove_verify_did_ethr2(false).await; - /* eprintln!("with Eip712Method2021..."); credential_prove_verify_did_ethr2(true).await; - */ } async fn credential_prove_verify_did_ethr2(eip712: bool) { @@ -326,4 +324,14 @@ mod tests { vp2.holder = Some(URI::String("did:example:bad".to_string())); assert!(vp2.verify(None, &DIDEthr).await.errors.len() > 0); } + + #[tokio::test] + async fn credential_verify_eip712vm() { + use ssi::vc::Credential; + let vc: Credential = serde_json::from_str(include_str!("../tests/vc.jsonld")).unwrap(); + eprintln!("vc {:?}", vc); + let verification_result = vc.verify(None, &DIDEthr).await; + println!("{:#?}", verification_result); + assert!(verification_result.errors.is_empty()); + } } diff --git a/did-ethr/tests/vc.jsonld b/did-ethr/tests/vc.jsonld new file mode 100644 index 000000000..988a0539d --- /dev/null +++ b/did-ethr/tests/vc.jsonld @@ -0,0 +1,79 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "sameAs": "https://www.w3.org/TR/owl-ref/#sameAs-def" + } + ], + "id": "urn:uuid:dc093ee6-e039-40e7-8599-da2075666e2c", + "type": [ + "VerifiableCredential" + ], + "credentialSubject": { + "id": "did:example:foo", + "sameAs": "did:ethr:0xf7398bacf610bb4e3b567811279fcb3c41919f89" + }, + "issuer": "did:ethr:0xf7398bacf610bb4e3b567811279fcb3c41919f89", + "issuanceDate": "2021-02-27T04:03:23.188Z", + "proof": { + "@context": [ + { + "Eip712Method2021": "https://w3id.org/security#Eip712Method2021", + "Eip712Signature2021": { + "@context": { + "@protected": true, + "@version": 1.1, + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "id": "@id", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@context": { + "@protected": true, + "@version": 1.1, + "assertionMethod": { + "@container": "@set", + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id" + }, + "authentication": { + "@container": "@set", + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id" + }, + "id": "@id", + "type": "@type" + }, + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab" + }, + "proofValue": "https://w3id.org/security#proofValue", + "publicKeyJwk": { + "@id": "https://w3id.org/security#publicKeyJwk", + "@type": "@json" + }, + "type": "@type", + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + }, + "@id": "https://w3id.org/security#Eip712Signature2021" + } + } + ], + "type": "Eip712Signature2021", + "proofPurpose": "assertionMethod", + "proofValue": "0x258e647f27ac39de2fe43b629b6189a20bfa1454bf76eb486f1461565c6c119c58b0676b2003c75ad51e88bbbca0a5ca944a2f292dc5f76f5d2db2be705a50ca1b", + "verificationMethod": "did:ethr:0xf7398bacf610bb4e3b567811279fcb3c41919f89#Eip712Method2021", + "created": "2021-02-27T04:03:23.189Z" + } +} \ No newline at end of file diff --git a/src/eip712.rs b/src/eip712.rs index ce74efb91..7108a4346 100644 --- a/src/eip712.rs +++ b/src/eip712.rs @@ -1,124 +1,88 @@ use std::collections::HashMap; use std::convert::TryFrom; +use std::fmt; use std::num::ParseIntError; use std::str::FromStr; use keccak_hash::keccak; use serde::{Deserialize, Serialize}; +use serde_json::{Number, Value}; use thiserror::Error; +use crate::keccak_hash::bytes_to_lowerhex; use crate::ldp::LinkedDataDocument; use crate::vc::Proof; -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -#[serde(try_from = "String", into = "String")] -pub enum Type { - Bytes, - String, - BytesN(u16), - UintN(u16), - IntN(u16), - Bool, - Address, - Array(Box), - ArrayN(Box, u16), - Reference(String), -} +static EMPTY_32: [u8; 32] = [0; 32]; #[derive(Error, Debug)] pub enum TypedDataParseError { - // #[error("Unknown data type: {0}")] - // UnknownType(String), + #[error("Unexpected null value")] + UnexpectedNull, + #[error("Unexpected number: {0:?}")] + Number(Number), #[error("Unable to parse data type size: {0}")] SizeParse(#[from] ParseIntError), } -impl TryFrom for Type { - type Error = TypedDataParseError; - fn try_from(string: String) -> Result { - match &string[..] { - "bytes" => return Ok(Type::Bytes), - "string" => return Ok(Type::String), - "address" => return Ok(Type::Address), - "bool" => return Ok(Type::Bool), - _ => {} - } - if string.starts_with("uint") { - return Ok(Type::UintN(u16::from_str(&string[4..])?)); - } else if string.starts_with("int") { - return Ok(Type::IntN(u16::from_str(&string[3..])?)); - } else if string.starts_with("bytes") { - return Ok(Type::BytesN(u16::from_str(&string[5..])?)); - } else if string.ends_with("]") { - let mut parts = string.rsplitn(2, "["); - let amount_str = parts.next().unwrap().split("]").next().unwrap(); - let base = Type::try_from(parts.next().unwrap().to_string())?; - if amount_str.len() == 0 { - return Ok(Type::Array(Box::new(base))); - } else { - return Ok(Type::ArrayN(Box::new(base), u16::from_str(amount_str)?)); - } - } - Ok(Type::Reference(string)) - } -} +pub type StructName = String; -impl From for String { - fn from(type_: Type) -> String { - match type_ { - Type::Bytes => String::from("bytes"), - Type::String => String::from("string"), - Type::BytesN(n) => format!("bytes{}", n), - Type::UintN(n) => format!("uint{}", n), - Type::IntN(n) => format!("int{}", n), - Type::Bool => String::from("address"), - Type::Address => String::from("address"), - Type::Array(type_) => format!("{}[]", String::from(*type_)), - Type::ArrayN(type_, n) => format!("{}[{}]", String::from(*type_), n), - Type::Reference(string) => string, - } - } -} +/// Structured typed data as described in +/// [Definition of typed structured data 𝕊](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-typed-structured-data-%F0%9D%95%8A) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StructType(Vec); -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Member { - pub name: String, +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MemberVariable { #[serde(rename = "type")] - pub type_: Type, + pub type_: EIP712Type, + pub name: String, } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Struct(Vec); - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Types { - // TODO: collapse eip712_domain into hashmap - #[serde(rename = "EIP712Domain")] - eip712_domain: Struct, - #[serde(flatten)] - types: HashMap, +/// EIP-712 types +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +#[serde(try_from = "String", into = "String")] +pub enum EIP712Type { + BytesN(usize), + UintN(usize), + IntN(usize), + Bool, + Address, + Bytes, + String, + Array(Box), + ArrayN(Box, usize), + Struct(StructName), } +/// EIP-712 values, JSON-compatible #[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(untagged)] -pub enum Data { - Struct(HashMap), - Array(Vec), +#[serde(try_from = "Value", into = "Value")] +pub enum EIP712Value { String(String), Bytes(Vec), + Array(Vec), + Struct(HashMap), Bool(bool), - NegativeNumber(i32), - Number(u32), + Integer(i64), +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Types { + #[serde(rename = "EIP712Domain")] + pub eip712_domain: StructType, + #[serde(flatten)] + pub types: HashMap, } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct TypedData { pub types: Types, - pub primary_type: String, - pub domain: Data, - pub message: Data, + pub primary_type: StructName, + pub domain: EIP712Value, + pub message: EIP712Value, } #[derive(Error, Debug)] @@ -135,175 +99,504 @@ pub enum TypedDataConstructionError { #[derive(Error, Debug)] pub enum TypedDataHashError { - #[error("Error parsing types: {0}")] - Parse(TypedDataParseError), - #[error("Missing primary type struct")] - MissingPrimaryTypeStruct, #[error("Missing referenced type: {0}")] MissingReferencedType(String), - #[error("Missing defined type: {0}")] - MissingDefinedType(String), - #[error("Expected reference type for '{0}'")] - ExpectedReference(String), - #[error("Not implemented")] - NotImplemented, + #[error("Missing struct member: {0}")] + MissingStructMember(String), + #[error("Expected string")] + ExpectedString, + #[error("Expected bytes")] + ExpectedBytes, + #[error("Expected boolean")] + ExpectedBoolean, + #[error("Expected array with type '{0}'")] + ExpectedArray(String), + #[error("Expected object with type '{0}'")] + ExpectedObject(String), + #[error("Expected integer")] + ExpectedInteger, + #[error("Expected address length 20 but found {0}")] + ExpectedAddressLength(usize), + #[error("Expected bytes length {0} but found {1}")] + ExpectedBytesLength(usize, usize), + #[error("Expected array length {0} but found {1}")] + ExpectedArrayLength(usize, usize), + #[error("Expected integer max length 32 bytes but found {0}")] + IntegerTooLong(usize), + #[error("Type not byte-aligned: {0} {1}")] + TypeNotByteAligned(&'static str, usize), + #[error("Expected bytes length between 1 and 32: {0}")] + BytesLength(usize), + #[error("Expected integer length between 8 and 256: {0}")] + IntegerLength(usize), + #[error("Expected string to be hex bytes")] + ExpectedHex, } -lazy_static! { - /// - pub static ref EIP712_DOMAIN: Struct = { - Struct(vec![ - Member { - name: "name".to_string(), - type_: Type::String - }, - Member { - name: "version".to_string(), - type_: Type::String - }, - Member { - name: "chainId".to_string(), - type_: Type::UintN(256) - }, - Member { - name: "verifyingContract".to_string(), - type_: Type::Address +impl EIP712Value { + fn as_bytes(&self) -> Result>, TypedDataHashError> { + let bytes = match self { + EIP712Value::Bytes(bytes) => bytes.to_vec(), + EIP712Value::Integer(int) => int.to_be_bytes().to_vec(), + EIP712Value::String(string) => { + bytes_from_hex(&string).ok_or(TypedDataHashError::ExpectedHex)? + } + _ => { + return Err(TypedDataHashError::ExpectedBytes); + } + }; + Ok(Some(bytes)) + } + + fn as_bool(&self) -> Option { + match self { + EIP712Value::Bool(b) => Some(*b), + EIP712Value::String(string) => { + // JS treats non-empty strings as boolean true. + // To catch possible mistakes, let's only allow that for + // a few special cases. + match &string[..] { + "" => Some(false), + "true" => Some(true), + "1" => Some(true), + _ => None, + } + } + EIP712Value::Integer(int) => match int { + 0 => Some(false), + 1 => Some(true), + _ => None, }, - Member { - name: "salt".to_string(), - type_: Type::BytesN(32) + _ => None, + } + } +} + +impl fmt::Display for EIP712Type { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + EIP712Type::Bytes => write!(f, "bytes"), + EIP712Type::String => write!(f, "string"), + EIP712Type::BytesN(n) => write!(f, "bytes{}", n), + EIP712Type::UintN(n) => write!(f, "uint{}", n), + EIP712Type::IntN(n) => write!(f, "int{}", n), + EIP712Type::Bool => write!(f, "bool"), + EIP712Type::Address => write!(f, "address"), + EIP712Type::Array(type_) => { + write!(f, "{}[]", *type_) } - ]) - }; + EIP712Type::ArrayN(type_, n) => { + write!(f, "{}[{}]", *type_, n) + } + EIP712Type::Struct(name) => { + write!(f, "{}", name) + } + } + } } -impl Struct { - /// [`encodeType`](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-encodetype) - pub fn encode(&self, name: &str, more_types: &Types) -> Result, TypedDataHashError> { - let mut string = String::new(); - self.encode_single(name, &mut string); - let mut referenced_types = HashMap::new(); - self.gather_referenced_types(more_types, &mut referenced_types)?; - let mut types: Vec<(&String, &Struct)> = referenced_types.into_iter().collect(); - types.sort_by(|(name1, _), (name2, _)| name1.cmp(name2)); - for (name, type_) in types { - type_.encode_single(name, &mut string); +impl From for String { + fn from(type_: EIP712Type) -> String { + match type_ { + EIP712Type::Struct(name) => name, + _ => { + format!("{}", &type_) + } } - Ok(string.into_bytes()) } +} - fn encode_single(&self, name: &str, string: &mut String) { - string.push_str(&name); - string.push('('); - let mut first = true; - for member in &self.0 { - if first { - first = false; +impl TryFrom for EIP712Type { + type Error = TypedDataParseError; + fn try_from(string: String) -> Result { + match &string[..] { + "bytes" => return Ok(EIP712Type::Bytes), + "string" => return Ok(EIP712Type::String), + "address" => return Ok(EIP712Type::Address), + "bool" => return Ok(EIP712Type::Bool), + _ => {} + } + if string.ends_with("]") { + let mut parts = string.rsplitn(2, "["); + let amount_str = parts.next().unwrap().split("]").next().unwrap(); + let base = EIP712Type::try_from(parts.next().unwrap().to_string())?; + if amount_str.len() == 0 { + return Ok(EIP712Type::Array(Box::new(base))); } else { - string.push(','); + return Ok(EIP712Type::ArrayN( + Box::new(base), + usize::from_str(amount_str)?, + )); } - string.push_str(&String::from(member.type_.clone())); - string.push(' '); - string.push_str(&member.name); + } else if string.starts_with("uint") { + return Ok(EIP712Type::UintN(usize::from_str(&string[4..])?)); + } else if string.starts_with("int") { + return Ok(EIP712Type::IntN(usize::from_str(&string[3..])?)); + } else if string.starts_with("bytes") { + return Ok(EIP712Type::BytesN(usize::from_str(&string[5..])?)); } - string.push(')'); + Ok(EIP712Type::Struct(string)) } +} - fn gather_referenced_types<'a>( - &'a self, - types: &'a Types, - memo: &mut HashMap<&'a String, &'a Struct>, - ) -> Result<(), TypedDataHashError> { - for member in &self.0 { - if let Type::Reference(ref reference_name) = member.type_ { - use std::collections::hash_map::Entry; - let entry = memo.entry(reference_name); - if let Entry::Vacant(o) = entry { - let referenced_struct = types.types.get(reference_name).ok_or( - TypedDataHashError::MissingReferencedType(reference_name.to_string()), - )?; - o.insert(referenced_struct); - referenced_struct.gather_referenced_types(types, memo)?; +impl From for Value { + fn from(value: EIP712Value) -> Value { + match value { + EIP712Value::Bool(true) => Value::Bool(true), + EIP712Value::Bool(false) => Value::Bool(false), + EIP712Value::Integer(int) => Value::Number(Number::from(int)), + EIP712Value::Bytes(bytes) => { + Value::String("0x".to_string() + &bytes_to_lowerhex(&bytes)) + } + EIP712Value::String(string) => Value::String(string), + EIP712Value::Array(array) => Value::Array(array.into_iter().map(Value::from).collect()), + EIP712Value::Struct(hash_map) => Value::Object( + hash_map + .into_iter() + .map(|(name, value)| (name, Value::from(value))) + .collect(), + ), + } + } +} + +impl TryFrom for EIP712Value { + type Error = TypedDataParseError; + fn try_from(value: Value) -> Result { + let eip712_value = match value { + Value::Null => return Err(Self::Error::UnexpectedNull), + Value::Bool(true) => EIP712Value::Bool(true), + Value::Bool(false) => EIP712Value::Bool(false), + Value::String(string) => EIP712Value::String(string), + Value::Number(number) => { + if let Some(int) = number.as_i64() { + EIP712Value::Integer(int) + } else { + return Err(Self::Error::Number(number)); } } + Value::Array(array) => EIP712Value::Array( + array + .into_iter() + .map(EIP712Value::try_from) + .collect::, Self::Error>>()?, + ), + Value::Object(object) => EIP712Value::Struct( + object + .into_iter() + .map(|(name, value)| EIP712Value::try_from(value).map(|v| (name, v))) + .collect::, Self::Error>>()?, + ), + }; + Ok(eip712_value) + } +} + +impl Types { + pub fn get(&self, struct_name: &str) -> Option<&StructType> { + if struct_name == "EIP712Domain" { + Some(&self.eip712_domain) + } else { + self.types.get(struct_name) + } + } +} + +/// Hash the result of [`encodeType`] +pub fn hash_type( + struct_name: &StructName, + struct_type: &StructType, + types: &Types, +) -> Result<[u8; 32], TypedDataHashError> { + let encoded_type = encode_type(struct_name, struct_type, types)?.to_vec(); + let type_hash = keccak(encoded_type).to_fixed_bytes(); + Ok(type_hash) +} + +/// [`hashStruct`](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-hashstruct) +pub fn hash_struct( + data: &EIP712Value, + struct_name: &StructName, + types: &Types, +) -> Result<[u8; 32], TypedDataHashError> { + let encoded_data = encode_data(data, &EIP712Type::Struct(struct_name.clone()), types)?.to_vec(); + Ok(keccak(encoded_data).to_fixed_bytes()) +} + +/// [`encodeType`](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-encodetype) +pub fn encode_type( + struct_name: &StructName, + struct_type: &StructType, + types: &Types, +) -> Result, TypedDataHashError> { + let mut string = String::new(); + encode_type_single(struct_name, struct_type, &mut string); + let mut referenced_types = HashMap::new(); + gather_referenced_struct_types(struct_type, types, &mut referenced_types)?; + let mut types: Vec<(&String, &StructType)> = referenced_types.into_iter().collect(); + types.sort_by(|(name1, _), (name2, _)| name1.cmp(name2)); + for (name, type_) in types { + encode_type_single(name, type_, &mut string); + } + Ok(string.into_bytes()) +} + +fn encode_type_single(type_name: &StructName, type_: &StructType, string: &mut String) { + string.push_str(type_name); + string.push('('); + let mut first = true; + for member in &type_.0 { + if first { + first = false; + } else { + string.push(','); } - Ok(()) + string.push_str(&String::from(member.type_.clone())); + string.push(' '); + string.push_str(&member.name); } + string.push(')'); } -impl Data { - /// [`hashStruct`](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-hashstruct) - pub fn hash( - &self, - type_: &Struct, - name: &str, - more_types: &Types, - ) -> Result<[u8; 32], TypedDataHashError> { - let encoded_data = self.encode(type_, more_types)?.to_vec(); - let encoded_type = type_.encode(name, more_types)?; - let type_hash = keccak(encoded_type).to_fixed_bytes().to_vec(); - Ok(keccak(vec![type_hash, encoded_data].concat()).to_fixed_bytes()) +impl EIP712Type { + /// Return name of struct if this type is a reference to a struct or array of structs + fn as_struct_name(&self) -> Option<&StructName> { + match self { + Self::Struct(name) => Some(name), + Self::Array(type_box) | Self::ArrayN(type_box, _) => type_box.as_struct_name(), + _ => None, + } + } +} + +pub(crate) fn bytes_from_hex(s: &str) -> Option> { + if s.starts_with("0x") { + hex::decode(&s[2..]).ok() + } else { + None + } +} + +fn gather_referenced_struct_types<'a>( + type_: &'a StructType, + types: &'a Types, + memo: &mut HashMap<&'a String, &'a StructType>, +) -> Result<(), TypedDataHashError> { + for member in &type_.0 { + if let Some(struct_name) = member.type_.as_struct_name() { + use std::collections::hash_map::Entry; + let entry = memo.entry(struct_name); + if let Entry::Vacant(o) = entry { + let referenced_struct = + types + .get(struct_name) + .ok_or(TypedDataHashError::MissingReferencedType( + struct_name.to_string(), + ))?; + o.insert(referenced_struct); + gather_referenced_struct_types(referenced_struct, types, memo)?; + } + } } + Ok(()) +} - /// [`encodeData`](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-encodedata) - pub fn encode(&self, _type: &Struct, _types: &Types) -> Result, TypedDataHashError> { - Err(TypedDataHashError::NotImplemented) - /* - let enc = match self { - Data::Struct(map) => { - let mut enc = Vec::with_capacity(32 * type_.0.len()); - for member in &type_.0 { - / * - let member_encoded = value.encode(member_type, types)?; - enc.extend_from_slice(&mut member_encoded); - * / - eprintln!("member {:?}", member); - let value = - map.get(&member.name) - .ok_or(TypedDataHashError::MissingDefinedType( - member.name.to_string(), - ))?; - eprintln!("value {:?}", value); - let member_struct = if let Type::Reference(ref reference_name) = member.type_ { - types.types.get(reference_name).ok_or( - TypedDataHashError::MissingReferencedType(reference_name.to_string()), - )? - } else { - / * - return Err(TypedDataHashError::ExpectedReference( - member.name.to_string(), - )) - } - * / - }; - eprintln!("memstruct {:?}", member_struct); - let mut member_hash = value.hash(member_struct, &member.name, types)?; - enc.extend_from_slice(&mut member_hash); +fn encode_field( + data: &EIP712Value, + type_: &EIP712Type, + types: &Types, +) -> Result, TypedDataHashError> { + let is_struct_or_array = match type_ { + EIP712Type::Struct(_) | EIP712Type::Array(_) | EIP712Type::ArrayN(_, _) => true, + _ => false, + }; + let encoded = encode_data(&data, type_, types)?; + if is_struct_or_array { + let hash = keccak(&encoded).to_fixed_bytes().to_vec(); + return Ok(hash); + } else { + return Ok(encoded); + } +} + +/// [`encodeData`](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-encodedata) +pub fn encode_data( + data: &EIP712Value, + type_: &EIP712Type, + types: &Types, +) -> Result, TypedDataHashError> { + let bytes = match type_ { + EIP712Type::Bytes => { + let bytes_opt; + let bytes = match data { + EIP712Value::Bytes(bytes) => Some(bytes), + EIP712Value::String(string) => { + bytes_opt = bytes_from_hex(&string); + bytes_opt.as_ref() } - // TODO: check that no extra types in map - enc + _ => None, } - Data::Array(array) => { - let mut enc = Vec::with_capacity(32 * array.len()); - for data in array { - // enc.push(data.encode()) + .ok_or(TypedDataHashError::ExpectedBytes)?; + keccak(bytes).to_fixed_bytes().to_vec() + } + EIP712Type::String => { + let string = match data { + EIP712Value::String(string) => string, + _ => { + return Err(TypedDataHashError::ExpectedString); } - enc + }; + keccak(string.as_bytes()).to_fixed_bytes().to_vec() + } + EIP712Type::BytesN(n) => { + let n = *n; + if n < 1 || n > 32 { + return Err(TypedDataHashError::BytesLength(n)); } - Data::String(string) => keccak(string.as_bytes()).to_fixed_bytes().to_vec(), - Data::Bytes(bytes) => keccak(bytes).to_fixed_bytes().to_vec(), - Data::Bool(true) => { - vec![0, 0, 0, 0, 0, 0, 0, 1] + let mut bytes = match data { + EIP712Value::Bytes(bytes) => Some(bytes.to_vec()), + EIP712Value::String(string) => bytes_from_hex(&string), + _ => None, } - Data::Bool(false) => { - vec![0, 0, 0, 0, 0, 0, 0, 0] + .ok_or(TypedDataHashError::ExpectedBytes)?; + let len = bytes.len(); + if len != n { + return Err(TypedDataHashError::ExpectedBytesLength(n, len)); } - Data::Number(num) => num.to_be_bytes().to_vec(), - Data::NegativeNumber(num) => num.to_be_bytes().to_vec(), - }; - Ok(enc) - */ - } + if len < 32 { + bytes.resize(32, 0); + } + bytes + } + EIP712Type::UintN(n) => { + let n = *n; + if n % 8 != 0 { + return Err(TypedDataHashError::TypeNotByteAligned("uint", n)); + } + if n < 8 || n > 256 { + return Err(TypedDataHashError::IntegerLength(n)); + } + let int = data + .as_bytes()? + .ok_or(TypedDataHashError::ExpectedInteger)?; + let len = int.len(); + if len > 32 { + return Err(TypedDataHashError::IntegerTooLong(len)); + } + if len == 32 { + return Ok(int); + } + // Left-pad to 256 bits + let padded = vec![EMPTY_32[0..(32 - len)].to_vec(), int].concat(); + padded + } + EIP712Type::IntN(n) => { + let n = *n; + if n % 8 != 0 { + return Err(TypedDataHashError::TypeNotByteAligned("int", n)); + } + if n < 8 || n > 256 { + return Err(TypedDataHashError::IntegerLength(n)); + } + let int = data + .as_bytes()? + .ok_or(TypedDataHashError::ExpectedInteger)?; + let len = int.len(); + if len > 32 { + return Err(TypedDataHashError::IntegerTooLong(len)); + } + if len == 32 { + return Ok(int); + } + // Left-pad to 256 bits, with sign extension. + let negative = int[0] & 0x80 == 0x80; + static PADDING_POS: [u8; 32] = [0; 32]; + static PADDING_NEG: [u8; 32] = [0xff; 32]; + let padding = if negative { PADDING_NEG } else { PADDING_POS }; + let padded = vec![padding[0..(32 - len)].to_vec(), int].concat(); + padded + } + EIP712Type::Bool => { + let b = data.as_bool().ok_or(TypedDataHashError::ExpectedBoolean)?; + let mut bytes: [u8; 32] = [0; 32]; + if b { + bytes[31] = 1; + } + bytes.to_vec() + } + EIP712Type::Address => { + let bytes = data.as_bytes()?.ok_or(TypedDataHashError::ExpectedBytes)?; + if bytes.len() != 20 { + return Err(TypedDataHashError::ExpectedAddressLength(bytes.len())); + } + static PADDING: [u8; 12] = [0; 12]; + let padded = vec![PADDING.to_vec(), bytes].concat(); + padded + } + EIP712Type::Array(member_type) => { + // Note: this implementation follows eth-sig-util + // which diverges from EIP-712 when encoding arrays. + // Ref: https://github.com/MetaMask/eth-sig-util/issues/106 + let array = match data { + EIP712Value::Array(array) => array, + _ => { + return Err(TypedDataHashError::ExpectedArray(member_type.to_string())); + } + }; + let mut enc = Vec::with_capacity(32 * array.len()); + for member in array { + let mut member_enc = encode_field(&member, member_type, types)?; + enc.append(&mut member_enc); + } + enc + } + EIP712Type::ArrayN(member_type, n) => { + let array = match data { + EIP712Value::Array(array) => array, + _ => { + return Err(TypedDataHashError::ExpectedArray(member_type.to_string())); + } + }; + let n = *n; + let len = array.len(); + if len != n { + return Err(TypedDataHashError::ExpectedArrayLength(n, len)); + } + let mut enc = Vec::with_capacity(32 * n); + for member in array { + let mut member_enc = encode_field(&member, member_type, types)?; + enc.append(&mut member_enc); + } + enc + } + EIP712Type::Struct(struct_name) => { + let struct_type = + types + .get(struct_name) + .ok_or(TypedDataHashError::MissingReferencedType( + struct_name.to_string(), + ))?; + let hash_map = match data { + EIP712Value::Struct(hash_map) => hash_map, + _ => { + return Err(TypedDataHashError::ExpectedObject(struct_name.to_string())); + } + }; + let mut enc = Vec::with_capacity(32 * (struct_type.0.len() + 1)); + let type_hash = hash_type(&struct_name, &struct_type, types)?; + enc.append(&mut type_hash.to_vec()); + for member in &struct_type.0 { + let mut member_enc = match hash_map.get(&member.name) { + Some(value) => encode_field(value, &member.type_, types)?, + // Allow missing member structs + None => EMPTY_32.to_vec(), + }; + enc.append(&mut member_enc); + } + enc + } + }; + Ok(bytes) } impl TypedData { @@ -325,20 +618,24 @@ impl TypedData { .map_err(|e| TypedDataConstructionError::NormalizeProof(e.to_string()))?; let types = Types { - eip712_domain: Struct(vec![Member { + eip712_domain: StructType(vec![MemberVariable { name: "name".to_string(), - type_: Type::String, + type_: EIP712Type::String, }]), types: vec![( "LDPSigningRequest".to_string(), - Struct(vec![ - Member { + StructType(vec![ + MemberVariable { name: "document".to_string(), - type_: Type::Array(Box::new(Type::Array(Box::new(Type::String)))), + type_: EIP712Type::Array(Box::new(EIP712Type::Array(Box::new( + EIP712Type::String, + )))), }, - Member { + MemberVariable { name: "proof".to_string(), - type_: Type::Array(Box::new(Type::Array(Box::new(Type::String)))), + type_: EIP712Type::Array(Box::new(EIP712Type::Array(Box::new( + EIP712Type::String, + )))), }, ]), )] @@ -346,34 +643,34 @@ impl TypedData { .collect(), }; use crate::rdf::Statement; - fn encode_statement(statement: Statement) -> Data { + fn encode_statement(statement: Statement) -> EIP712Value { let mut terms = vec![ - Data::String(String::from(&statement.subject)), - Data::String(String::from(&statement.predicate)), - Data::String(String::from(&statement.object)), + EIP712Value::String(String::from(&statement.subject)), + EIP712Value::String(String::from(&statement.predicate)), + EIP712Value::String(String::from(&statement.object)), ]; if let Some(graph_label) = statement.graph_label.as_ref() { - terms.push(Data::String(String::from(graph_label))); + terms.push(EIP712Value::String(String::from(graph_label))); } - Data::Array(terms) + EIP712Value::Array(terms) } - Ok(Self { + let typed_data = Self { types, primary_type: "LDPSigningRequest".to_string(), - domain: Data::Struct( + domain: EIP712Value::Struct( vec![( "name".to_string(), - Data::String("Eip712Method2021".to_string()), + EIP712Value::String("Eip712Method2021".to_string()), )] .into_iter() .collect(), ), - message: Data::Struct( + message: EIP712Value::Struct( vec![ ( "document".to_string(), - Data::Array( + EIP712Value::Array( doc_dataset_normalized .statements() .into_iter() @@ -383,7 +680,7 @@ impl TypedData { ), ( "proof".to_string(), - Data::Array( + EIP712Value::Array( sigopts_dataset_normalized .statements() .into_iter() @@ -393,32 +690,27 @@ impl TypedData { ), ] .into_iter() - .collect(), + .collect::>(), ), - }) + }; + Ok(typed_data) } /// Encode a typed data message for hashing and signing. - /// [Reference[(https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#specification) + /// [Reference](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#specification) pub fn hash(&self) -> Result, TypedDataHashError> { - let message_struct = self - .types - .types - .get(&self.primary_type) - .ok_or(TypedDataHashError::MissingPrimaryTypeStruct)?; - let message_hash = self - .message - .hash(&message_struct, &self.primary_type, &self.types)?; + let message_hash = hash_struct(&self.message, &self.primary_type, &self.types)?; let domain_separator = - self.domain - .hash(&self.types.eip712_domain, "EIP712Domain", &self.types)?; + hash_struct(&self.domain, &StructName::from("EIP712Domain"), &self.types)?; + let bytes = vec![ vec![0x19, 0x01], domain_separator.to_vec(), message_hash.to_vec(), ] .concat(); - Ok(keccak(bytes).to_fixed_bytes().to_vec()) + let hash = keccak(bytes).to_fixed_bytes().to_vec(); + Ok(hash) } } @@ -428,50 +720,50 @@ mod tests { use serde_json::json; #[test] - fn encode_type() { + fn test_encode_type() { let types = Types { - eip712_domain: Struct(Vec::new()), + eip712_domain: StructType(Vec::new()), types: vec![ ( "Transaction".to_string(), - Struct(vec![ - Member { + StructType(vec![ + MemberVariable { name: "from".to_string(), - type_: Type::Reference("Person".to_string()), + type_: EIP712Type::Struct("Person".to_string()), }, - Member { + MemberVariable { name: "to".to_string(), - type_: Type::Reference("Person".to_string()), + type_: EIP712Type::Struct("Person".to_string()), }, - Member { + MemberVariable { name: "tx".to_string(), - type_: Type::Reference("Asset".to_string()), + type_: EIP712Type::Struct("Asset".to_string()), }, ]), ), ( "Person".to_string(), - Struct(vec![ - Member { + StructType(vec![ + MemberVariable { name: "wallet".to_string(), - type_: Type::Address, + type_: EIP712Type::Address, }, - Member { + MemberVariable { name: "name".to_string(), - type_: Type::String, + type_: EIP712Type::String, }, ]), ), ( "Asset".to_string(), - Struct(vec![ - Member { + StructType(vec![ + MemberVariable { name: "token".to_string(), - type_: Type::Address, + type_: EIP712Type::Address, }, - Member { + MemberVariable { name: "amount".to_string(), - type_: Type::UintN(256), + type_: EIP712Type::UintN(256), }, ]), ), @@ -479,8 +771,12 @@ mod tests { .into_iter() .collect(), }; - let transaction_struct = types.types.get("Transaction").unwrap(); - let type_encoded = transaction_struct.encode("Transaction", &types).unwrap(); + let type_encoded = encode_type( + &StructName::from("Transaction"), + types.get("Transaction").unwrap(), + &types, + ) + .unwrap(); let type_encoded_string = String::from_utf8(type_encoded).unwrap(); assert_eq!(type_encoded_string, "Transaction(Person from,Person to,Asset tx)Asset(address token,uint256 amount)Person(address wallet,string name)"); } @@ -488,53 +784,24 @@ mod tests { #[test] // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#example // https://github.com/ethereum/EIPs/blob/master/assets/eip-712/Example.js - #[ignore] - // TODO fn hash_typed_data() { let _addr = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"; let typed_data: TypedData = serde_json::from_value(json!({ "types": { "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } ], "Person": [ - { - "name": "name", - "type": "string" - }, - { - "name": "wallet", - "type": "address" - } + { "name": "name", "type": "string" }, + { "name": "wallet", "type": "address" } ], "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Person" - }, - { - "name": "contents", - "type": "string" - } + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person" }, + { "name": "contents", "type": "string" } ] }, "primaryType": "Mail", @@ -557,15 +824,120 @@ mod tests { } })) .unwrap(); + + // Hash Type + let struct_type = typed_data.types.get("Mail").unwrap(); + let type_encoded = encode_type(&"Mail".to_string(), struct_type, &typed_data.types) + .unwrap() + .to_vec(); + let type_hash = keccak(&type_encoded).to_fixed_bytes().to_vec(); + let type_encoded_string = String::from_utf8(type_encoded).unwrap(); + assert_eq!( + type_encoded_string, + "Mail(Person from,Person to,string contents)Person(string name,address wallet)" + ); + assert_eq!( + bytes_to_lowerhex(&type_hash), + "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2" + ); + + // Hash struct + let data: EIP712Value = serde_json::from_value(json!({ + "name": "Cow", + "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + })) + .unwrap(); + let data_encoded = encode_data( + &data, + &EIP712Type::Struct("Person".to_string()), + &typed_data.types, + ) + .unwrap(); + assert_eq!( + bytes_to_lowerhex(&data_encoded), + "0xb9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c795008c1d2bd5348394761719da11ec67eedae9502d137e8940fee8ecd6f641ee1648000000000000000000000000cd2a3d9f938e13cd947ec05abc7fe734df8dd826" + ); + + // Encode message + let data_encoded = encode_data( + &typed_data.message, + &EIP712Type::Struct(typed_data.primary_type.clone()), + &typed_data.types, + ) + .unwrap(); + assert_eq!( + bytes_to_lowerhex(&data_encoded), + "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8" + ); + + // Hash message + let data_hashed = hash_struct( + &typed_data.message, + &typed_data.primary_type, + &typed_data.types, + ) + .unwrap(); + assert_eq!( + bytes_to_lowerhex(&data_hashed), + "0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e" + ); + let hash = typed_data.hash().unwrap(); - let hash_hex = crate::keccak_hash::bytes_to_lowerhex(&hash); + let hash_hex = bytes_to_lowerhex(&hash); assert_eq!( hash_hex, "0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2" ); - /* - let sig_hex = crate::keccak_hash::bytes_to_lowerhex(&sig); - assert_eq!(sig_hex, "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c"); - */ + + // Test more types + let typed_data: TypedData = serde_json::from_value(json!({ + "types": { + "EIP712Domain": [ + { "type": "string", "name": "name" } + ], + "Message": [ + { "name": "bytes8", "type": "bytes8" }, + { "name": "bytes32", "type": "bytes32" }, + { "name": "uint8", "type": "uint8" }, + { "name": "uint32", "type": "uint32" }, + { "name": "uint256", "type": "uint256" }, + { "name": "int8", "type": "int8" }, + { "name": "int16", "type": "int16" }, + { "name": "true", "type": "bool" }, + { "name": "empty", "type": "Empty[1]" }, + { "name": "missing", "type": "Empty" }, + { "name": "bitmatrix", "type": "bool[2][2]" } + ], + "Empty": [ + ] + }, + "primaryType": "Message", + "domain": { + "name": "Test" + }, + "message": { + "bytes8": "0x0102030405060708", + "bytes32": "0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f10", + "uint8": "0x03", + "uint32": 0x01020304, + "uint256": "0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f10", + "int8": -5, + "int16": 5, + "true": true, + "empty": [{ + }], + "bitmatrix": [ + [true, false], + [false, true] + ] + } + } + )) + .unwrap(); + let hash = typed_data.hash().unwrap(); + assert_eq!( + bytes_to_lowerhex(&hash), + "0x3128ae562d7141585a21f9c04e87520857ae9025d5c57293255f25d72f869b2e" + ); } } diff --git a/src/error.rs b/src/error.rs index 28f946872..8d182f353 100644 --- a/src/error.rs +++ b/src/error.rs @@ -157,6 +157,8 @@ pub enum Error { TypedDataConstruction(TypedDataConstructionError), #[cfg(feature = "keccak-hash")] TypedDataHash(TypedDataHashError), + FromHex(hex::FromHexError), + HexString, } impl fmt::Display for Error { @@ -264,6 +266,7 @@ impl fmt::Display for Error { Error::ExpectedOutput(expected, found) => write!(f, "Expected output '{}', but found '{}'", expected, found), Error::UnknownProcessingMode(mode) => write!(f, "Unknown processing mode '{}'", mode), Error::UnknownRdfDirection(direction) => write!(f, "Unknown RDF direction '{}'", direction), + Error::HexString => write!(f, "Expected string beginning with '0x'"), Error::FromUtf8(e) => e.fmt(f), Error::TryFromSlice(e) => e.fmt(f), #[cfg(feature = "ring")] @@ -290,6 +293,7 @@ impl fmt::Display for Error { Error::TypedDataConstruction(e) => e.fmt(f), #[cfg(feature = "keccak-hash")] Error::TypedDataHash(e) => e.fmt(f), + Error::FromHex(e) => e.fmt(f), } } } @@ -432,3 +436,9 @@ impl From for Error { Error::TypedDataHash(err) } } + +impl From for Error { + fn from(err: hex::FromHexError) -> Error { + Error::FromHex(err) + } +} diff --git a/src/keccak_hash.rs b/src/keccak_hash.rs index f4fe6d49e..4c22489be 100644 --- a/src/keccak_hash.rs +++ b/src/keccak_hash.rs @@ -26,11 +26,6 @@ pub fn hash_public_key(jwk: &JWK) -> Result { let hash = keccak(&pk_bytes[1..65]).to_fixed_bytes(); let hash_last20 = &hash[12..32]; let hash_last20_hex = bytes_to_lowerhex(hash_last20); - eprintln!( - "jwk {}. hex: {}", - serde_json::to_string_pretty(jwk)?, - hash_last20_hex - ); Ok(hash_last20_hex) } diff --git a/src/ldp.rs b/src/ldp.rs index a87bae2b7..cac01b787 100644 --- a/src/ldp.rs +++ b/src/ldp.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "keccak-hash")] +use std::convert::TryFrom; use std::str::FromStr; use async_trait::async_trait; @@ -708,8 +710,18 @@ impl ProofSuite for Eip712Signature2021 { }; let typed_data = TypedData::from_document_and_options(document, &proof).await?; let bytes = typed_data.hash()?; - let sig = crate::jws::sign_bytes_b64(Algorithm::ES256KR, &bytes, &key)?; - proof.proof_value = Some(sig); + let ec_params = match &key.params { + JWKParams::EC(ec) => ec, + _ => return Err(Error::KeyTypeNotImplemented), + }; + let secret_key = secp256k1::SecretKey::try_from(ec_params)?; + let msg = secp256k1::Message::parse_slice(&bytes)?; + let (sig, rec_id) = secp256k1::sign(&msg, &secret_key); + let mut sig = sig.serialize().to_vec(); + // Use ethereum-style recovery byte + sig.push(rec_id.serialize() + 27); + let sig_hex = crate::keccak_hash::bytes_to_lowerhex(&sig); + proof.proof_value = Some(sig_hex); Ok(proof) } @@ -718,7 +730,6 @@ impl ProofSuite for Eip712Signature2021 { options: &LinkedDataProofOptions, _public_key: &JWK, ) -> Result { - // TODO let proof = Proof { context: serde_json::json!([EIP712VM_CONTEXT.clone()]), proof_purpose: options.proof_purpose.clone(), @@ -747,7 +758,7 @@ impl ProofSuite for Eip712Signature2021 { document: &(dyn LinkedDataDocument + Sync), resolver: &dyn DIDResolver, ) -> Result<(), Error> { - let sig_b64 = proof + let sig_hex = proof .proof_value .as_ref() .ok_or(Error::MissingProofSignature)?; @@ -761,8 +772,27 @@ impl ProofSuite for Eip712Signature2021 { } let typed_data = TypedData::from_document_and_options(document, &proof).await?; let bytes = typed_data.hash()?; - let sig = base64::decode_config(sig_b64, base64::URL_SAFE_NO_PAD)?; - let jwk = crate::jws::recover(Algorithm::ES256KR, &bytes, &sig)?; + if !sig_hex.starts_with("0x") { + return Err(Error::HexString); + } + let sig = hex::decode(&sig_hex[2..])?; + let msg = secp256k1::Message::parse_slice(&bytes)?; + let (rec_byte, sig_bytes) = sig.split_last().ok_or(Error::InvalidSignature)?; + let rec_id = secp256k1::RecoveryId::parse_rpc(*rec_byte)?; + let sig = secp256k1::Signature::parse_slice(sig_bytes)?; + let public_key = secp256k1::recover(&msg, &sig, &rec_id)?; + use crate::jwk::ECParams; + let jwk = JWK { + params: JWKParams::EC(ECParams::try_from(&public_key)?), + public_key_use: None, + key_operations: None, + algorithm: None, + key_id: None, + x509_url: None, + x509_certificate_chain: None, + x509_thumbprint_sha1: None, + x509_thumbprint_sha256: None, + }; let account_id_str = vm.blockchain_account_id.ok_or(Error::MissingAccountId)?; let account_id = BlockchainAccountId::from_str(&account_id_str)?; account_id.verify(&jwk)?; @@ -773,14 +803,14 @@ impl ProofSuite for Eip712Signature2021 { #[cfg(test)] mod tests { use super::*; + use crate::jsonld::CREDENTIALS_V1_CONTEXT; struct ExampleDocument; #[async_trait] impl LinkedDataDocument for ExampleDocument { fn get_contexts(&self) -> Result, Error> { - // Ok(Some(serde_json::to_string(&*EIP712VM_CONTEXT)?)) - Ok(None) + Ok(Some(serde_json::to_string(&*CREDENTIALS_V1_CONTEXT)?)) } async fn to_dataset_for_signing( &self, @@ -805,7 +835,6 @@ mod tests { #[cfg(feature = "secp256k1")] #[async_std::test] - #[ignore] async fn eip712vm() { let mut key = JWK::generate_secp256k1().unwrap(); key.algorithm = Some(Algorithm::ES256KR); diff --git a/src/vc.rs b/src/vc.rs index d967053d7..7bd8b4d23 100644 --- a/src/vc.rs +++ b/src/vc.rs @@ -987,6 +987,7 @@ impl LinkedDataDocument for Proof { ) -> Result { let mut copy = self.clone(); copy.jws = None; + copy.proof_value = None; let json = serde_json::to_string(©)?; let more_contexts = match parent { Some(parent) => parent.get_contexts()?,