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::{LinkedDataDocument, Proof};
use ssi_crypto::hashes::keccak::bytes_to_lowerhex;
use ssi_json_ld::ContextLoader;
static EMPTY_32: [u8; 32] = [0; 32];
#[derive(Error, Debug)]
pub enum TypedDataParseError {
#[error("Unexpected null value")]
UnexpectedNull,
#[error("Unmatched bracket")]
UnmatchedBracket,
#[error("Unexpected number: {0:?}")]
Number(Number),
#[error("Unable to parse data type size: {0}")]
SizeParse(#[from] ParseIntError),
}
pub type StructName = String;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct StructType(Vec<MemberVariable>);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemberVariable {
#[serde(rename = "type")]
pub type_: EIP712Type,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
#[serde(try_from = "String", into = "String")]
pub enum EIP712Type {
BytesN(usize),
UintN(usize),
IntN(usize),
Bool,
Address,
Bytes,
String,
Array(Box<EIP712Type>),
ArrayN(Box<EIP712Type>, usize),
Struct(StructName),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(try_from = "Value", into = "Value")]
pub enum EIP712Value {
String(String),
Bytes(Vec<u8>),
Array(Vec<EIP712Value>),
Struct(HashMap<StructName, EIP712Value>),
Bool(bool),
Integer(i64),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Types {
#[serde(rename = "EIP712Domain")]
#[serde(default = "eip712sig_default_domain")]
pub eip712_domain: StructType,
#[serde(flatten)]
pub types: HashMap<StructName, StructType>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TypedData {
pub types: Types,
pub primary_type: StructName,
pub domain: EIP712Value,
pub message: EIP712Value,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum TypesOrURI {
URI(String),
Object(Types),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct ProofInfo {
#[serde(rename = "types", alias = "messageSchema")]
pub types_or_uri: TypesOrURI,
pub primary_type: StructName,
pub domain: EIP712Value,
}
#[derive(Error, Debug)]
pub enum TypedDataConstructionError {
#[error("Unable to convert document to data set: {0}")]
DocumentToDataset(String),
#[error("Unable to convert proof to data set: {0}")]
ProofToDataset(String),
#[error("Unable to normalize document: {0}")]
NormalizeDocument(String),
#[error("Unable to normalize proof: {0}")]
NormalizeProof(String),
}
#[derive(Error, Debug)]
pub enum TypedDataConstructionJSONError {
#[error("Not Implemented")]
NotImplemented,
#[error("Unable to convert document to JSON: {0}")]
DocumentToJSON(String),
#[error("Unable to convert proof object to JSON: {0}")]
ProofToJSON(String),
#[error("Expected document to be a JSON object")]
ExpectedDocumentObject,
#[error("Expected proof to be a JSON object")]
ExpectedProofObject,
#[error("Expected types in proof.eip712")]
ExpectedTypes,
#[error("Unable to parse eip712: {0}")]
ParseInfo(serde_json::Error),
#[error("Unable to convert document to EIP-712 message: {0}")]
ConvertMessage(TypedDataParseError),
#[error("Unable to dereference EIP-712 types: {0}")]
DereferenceTypes(DereferenceTypesError),
#[error("Unable to generate EIP-712 types and proof info: {0}")]
GenerateProofInfo(#[from] ProofGenerationError),
}
#[derive(Error, Debug)]
pub enum TypedDataHashError {
#[error("Missing referenced type: {0}")]
MissingReferencedType(String),
#[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,
#[error("Untyped properties: {0:?}")]
UntypedProperties(Vec<String>),
}
#[derive(Error, Debug)]
pub enum DereferenceTypesError {
#[error("Remote types loading not implemented")]
RemoteLoadingNotImplemented,
#[error("Unable to convert types from JSON: {0}")]
JSON(serde_json::Error),
}
impl EIP712Value {
fn as_bytes(&self) -> Result<Option<Vec<u8>>, 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<bool> {
match self {
EIP712Value::Bool(b) => Some(*b),
EIP712Value::String(string) => {
match &string[..] {
"" => Some(false),
"true" => Some(true),
"1" => Some(true),
_ => None,
}
}
EIP712Value::Integer(int) => match int {
0 => Some(false),
1 => Some(true),
_ => None,
},
_ => None,
}
}
fn as_struct(&self) -> Option<&HashMap<StructName, EIP712Value>> {
match self {
EIP712Value::Struct(map) => Some(map),
_ => 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 From<EIP712Type> for String {
fn from(type_: EIP712Type) -> String {
match type_ {
EIP712Type::Struct(name) => name,
_ => {
format!("{}", &type_)
}
}
}
}
impl TryFrom<String> for EIP712Type {
type Error = TypedDataParseError;
fn try_from(string: String) -> Result<Self, Self::Error> {
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 inner = parts.next().ok_or(TypedDataParseError::UnmatchedBracket)?;
let base = EIP712Type::try_from(inner.to_string())?;
if amount_str.is_empty() {
return Ok(EIP712Type::Array(Box::new(base)));
} else {
return Ok(EIP712Type::ArrayN(
Box::new(base),
usize::from_str(amount_str)?,
));
}
} else if let Some(suffix) = string.strip_prefix("uint") {
return Ok(EIP712Type::UintN(usize::from_str(suffix)?));
} else if let Some(suffix) = string.strip_prefix("int") {
return Ok(EIP712Type::IntN(usize::from_str(suffix)?));
} else if let Some(suffix) = string.strip_prefix("bytes") {
return Ok(EIP712Type::BytesN(usize::from_str(suffix)?));
}
Ok(EIP712Type::Struct(string))
}
}
impl From<EIP712Value> 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<Value> for EIP712Value {
type Error = TypedDataParseError;
fn try_from(value: Value) -> Result<Self, Self::Error> {
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::<Result<Vec<Self>, Self::Error>>()?,
),
Value::Object(object) => EIP712Value::Struct(
object
.into_iter()
.map(|(name, value)| EIP712Value::try_from(value).map(|v| (name, v)))
.collect::<Result<HashMap<StructName, Self>, 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)
}
}
}
impl TypesOrURI {
#[allow(clippy::match_single_binding)]
#[allow(unreachable_code)]
#[allow(unused_variables)]
async fn dereference(self) -> Result<Types, DereferenceTypesError> {
let uri = match self {
Self::URI(string) => string,
Self::Object(types) => return Ok(types),
};
let value = match &uri[..] {
#[cfg(test)]
"https://example.org/types.json" => tests::EXAMPLE_TYPES.clone(),
_ => return Err(DereferenceTypesError::RemoteLoadingNotImplemented),
};
let types: Types = serde_json::from_value(value).map_err(DereferenceTypesError::JSON)?;
Ok(types)
}
}
fn property_to_struct_name(property_name: &str) -> StructName {
let mut chars = property_name.chars();
let first_char = chars.next().unwrap_or_default();
first_char.to_uppercase().chain(chars).collect()
}
#[allow(clippy::ptr_arg)]
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)
}
#[allow(clippy::ptr_arg)]
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())
}
#[allow(clippy::ptr_arg)]
pub fn encode_type(
struct_name: &StructName,
struct_type: &StructType,
types: &Types,
) -> Result<Vec<u8>, 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())
}
#[allow(clippy::ptr_arg)]
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(',');
}
string.push_str(&String::from(member.type_.clone()));
string.push(' ');
string.push_str(&member.name);
}
string.push(')');
}
impl EIP712Type {
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<Vec<u8>> {
s.strip_prefix("0x")
.and_then(|hex_str| hex::decode(hex_str).ok())
}
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_else(|| {
TypedDataHashError::MissingReferencedType(struct_name.to_string())
})?;
o.insert(referenced_struct);
gather_referenced_struct_types(referenced_struct, types, memo)?;
}
}
}
Ok(())
}
fn encode_field(
data: &EIP712Value,
type_: &EIP712Type,
types: &Types,
) -> Result<Vec<u8>, TypedDataHashError> {
let is_struct_or_array = matches!(
type_,
EIP712Type::Struct(_) | EIP712Type::Array(_) | EIP712Type::ArrayN(_, _)
);
let encoded = encode_data(data, type_, types)?;
if is_struct_or_array {
let hash = keccak(&encoded).to_fixed_bytes().to_vec();
Ok(hash)
} else {
Ok(encoded)
}
}
pub fn encode_data(
data: &EIP712Value,
type_: &EIP712Type,
types: &Types,
) -> Result<Vec<u8>, 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()
}
_ => None,
}
.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);
}
};
keccak(string.as_bytes()).to_fixed_bytes().to_vec()
}
EIP712Type::BytesN(n) => {
let n = *n;
if !(1..=32).contains(&n) {
return Err(TypedDataHashError::BytesLength(n));
}
let mut bytes = match data {
EIP712Value::Bytes(bytes) => Some(bytes.to_vec()),
EIP712Value::String(string) => bytes_from_hex(string),
_ => None,
}
.ok_or(TypedDataHashError::ExpectedBytes)?;
let len = bytes.len();
if len != n {
return Err(TypedDataHashError::ExpectedBytesLength(n, len));
}
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 !(8..=256).contains(&n) {
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);
}
vec![EMPTY_32[0..(32 - len)].to_vec(), int].concat()
}
EIP712Type::IntN(n) => {
let n = *n;
if n % 8 != 0 {
return Err(TypedDataHashError::TypeNotByteAligned("int", n));
}
if !(8..=256).contains(&n) {
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);
}
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 };
vec![padding[0..(32 - len)].to_vec(), int].concat()
}
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];
vec![PADDING.to_vec(), bytes].concat()
}
EIP712Type::Array(member_type) => {
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_else(|| {
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());
let mut keys: std::collections::HashSet<String> =
hash_map.keys().map(|k| k.to_owned()).collect();
for member in &struct_type.0 {
let mut member_enc = match hash_map.get(&member.name) {
Some(value) => encode_field(value, &member.type_, types)?,
None => EMPTY_32.to_vec(),
};
keys.remove(&member.name);
enc.append(&mut member_enc);
}
if !keys.is_empty() {
let names: Vec<String> = keys.into_iter().collect();
return Err(TypedDataHashError::UntypedProperties(names));
}
enc
}
};
Ok(bytes)
}
fn eip712sig_default_domain() -> StructType {
StructType(vec![MemberVariable {
name: String::from("name"),
type_: EIP712Type::String,
}])
}
impl TypedData {
pub async fn from_document_and_options(
document: &(dyn LinkedDataDocument + Sync),
proof: &Proof,
context_loader: &mut ContextLoader,
) -> Result<Self, TypedDataConstructionError> {
let doc_dataset = document
.to_dataset_for_signing(None, context_loader)
.await
.map_err(|e| TypedDataConstructionError::DocumentToDataset(e.to_string()))?;
let doc_dataset_normalized = crate::urdna2015::normalize(&doc_dataset)
.map_err(|e| TypedDataConstructionError::NormalizeDocument(e.to_string()))?;
let mut doc_statements_normalized = doc_dataset_normalized.statements();
#[allow(clippy::redundant_closure)]
doc_statements_normalized.sort_by_cached_key(|x| String::from(x));
let sigopts_dataset = proof
.to_dataset_for_signing(Some(document), context_loader)
.await
.map_err(|e| TypedDataConstructionError::ProofToDataset(e.to_string()))?;
let sigopts_dataset_normalized = crate::urdna2015::normalize(&sigopts_dataset)
.map_err(|e| TypedDataConstructionError::NormalizeProof(e.to_string()))?;
let mut sigopts_statements_normalized = sigopts_dataset_normalized.statements();
#[allow(clippy::redundant_closure)]
sigopts_statements_normalized.sort_by_cached_key(|x| String::from(x));
let types = Types {
eip712_domain: StructType(vec![MemberVariable {
name: "name".to_string(),
type_: EIP712Type::String,
}]),
types: vec![(
"LDPSigningRequest".to_string(),
StructType(vec![
MemberVariable {
name: "document".to_string(),
type_: EIP712Type::Array(Box::new(EIP712Type::Array(Box::new(
EIP712Type::String,
)))),
},
MemberVariable {
name: "proof".to_string(),
type_: EIP712Type::Array(Box::new(EIP712Type::Array(Box::new(
EIP712Type::String,
)))),
},
]),
)]
.into_iter()
.collect(),
};
use ssi_json_ld::rdf::Statement;
fn encode_statement(statement: Statement) -> EIP712Value {
let mut terms = vec![
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(EIP712Value::String(String::from(graph_label)));
}
EIP712Value::Array(terms)
}
let typed_data = Self {
types,
primary_type: "LDPSigningRequest".to_string(),
domain: EIP712Value::Struct(
vec![(
"name".to_string(),
EIP712Value::String("Eip712Method2021".to_string()),
)]
.into_iter()
.collect(),
),
message: EIP712Value::Struct(
vec![
(
"document".to_string(),
EIP712Value::Array(
doc_statements_normalized
.into_iter()
.map(encode_statement)
.collect(),
),
),
(
"proof".to_string(),
EIP712Value::Array(
sigopts_statements_normalized
.into_iter()
.map(encode_statement)
.collect(),
),
),
]
.into_iter()
.collect::<HashMap<StructName, EIP712Value>>(),
),
};
Ok(typed_data)
}
pub async fn from_document_and_options_json(
document: &(dyn LinkedDataDocument + Sync),
proof: &Proof,
) -> Result<Self, TypedDataConstructionJSONError> {
let mut doc_value = document
.to_value()
.map_err(|e| TypedDataConstructionJSONError::DocumentToJSON(e.to_string()))?;
let doc_obj = doc_value
.as_object_mut()
.ok_or(TypedDataConstructionJSONError::ExpectedDocumentObject)?;
let mut proof_value = serde_json::to_value(proof)
.map_err(|e| TypedDataConstructionJSONError::ProofToJSON(e.to_string()))?;
let proof_obj = proof_value
.as_object_mut()
.ok_or(TypedDataConstructionJSONError::ExpectedProofObject)?;
proof_obj.remove("proofValue");
let info = proof_obj
.remove("eip712")
.or_else(|| proof_obj.remove("eip712Domain"));
doc_obj.insert("proof".to_string(), proof_value);
let message = EIP712Value::try_from(doc_value)
.map_err(TypedDataConstructionJSONError::ConvertMessage)?;
let proof_info: ProofInfo = match info {
Some(info) => {
serde_json::from_value(info).map_err(TypedDataConstructionJSONError::ParseInfo)?
}
None => generate_proof_info(&message)?,
};
let ProofInfo {
types_or_uri,
primary_type,
domain,
} = proof_info;
let types = types_or_uri
.dereference()
.await
.map_err(TypedDataConstructionJSONError::DereferenceTypes)?;
let typed_data = Self {
types,
primary_type,
domain,
message,
};
Ok(typed_data)
}
pub fn hash(&self) -> Result<Vec<u8>, TypedDataHashError> {
let bytes = self.bytes()?;
let hash = keccak(bytes).to_fixed_bytes().to_vec();
Ok(hash)
}
pub fn bytes(&self) -> Result<Vec<u8>, TypedDataHashError> {
let message_hash = hash_struct(&self.message, &self.primary_type, &self.types)?;
let domain_separator =
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(bytes)
}
}
#[derive(Error, Debug)]
pub enum TypesGenerationError {
#[error("Expected object")]
ExpectedObject,
#[error("Found empty array under property: {0}")]
EmptyArray(String),
#[error("Array inconsistency: expected type {0} under property: {1}")]
ArrayInconsistency(&'static str, String),
#[error("Array value must be boolean, number or string. Property: {0}")]
ComplexArrayValue(String),
#[error("Value must be boolean, number, string, array or struct. Property: {0}")]
ComplexValue(String),
#[error("Missing primaryType in recursive output. primaryType: {0}")]
MissingPrimaryTypeInRecursiveOutput(String),
#[error("JCS: {0}")]
JCS(serde_json::Error),
#[error("Proof type already exists")]
ProofAlreadyExists,
}
pub fn generate_types(
doc: &EIP712Value,
primary_type: Option<StructName>,
) -> Result<HashMap<StructName, StructType>, TypesGenerationError> {
let mut output = HashMap::default();
let mut types = StructType::default();
let doc_jcs = serde_jcs::to_string(doc).map_err(TypesGenerationError::JCS)?;
let doc: EIP712Value = serde_json::from_str(&doc_jcs).map_err(TypesGenerationError::JCS)?;
let primary_type = primary_type.unwrap_or_else(|| StructName::from("Document"));
let object = doc
.as_struct()
.ok_or(TypesGenerationError::ExpectedObject)?;
let mut props: Vec<(&String, &EIP712Value)> = object.iter().collect();
props.sort_by_cached_key(|(name, _value)| name.encode_utf16().collect::<Vec<u16>>());
for (property_name, value) in props {
match value {
EIP712Value::Bool(_) => {
types.0.push(MemberVariable {
type_: EIP712Type::Bool,
name: String::from(property_name),
});
}
EIP712Value::Integer(_) => {
types.0.push(MemberVariable {
type_: EIP712Type::UintN(256),
name: String::from(property_name),
});
}
EIP712Value::String(_) => {
types.0.push(MemberVariable {
type_: EIP712Type::String,
name: String::from(property_name),
});
}
EIP712Value::Array(array) => {
let mut values = array.iter();
let first_value = values
.next()
.ok_or_else(|| TypesGenerationError::EmptyArray(property_name.clone()))?;
match first_value {
EIP712Value::Bool(_) => {
for value in values {
if !matches!(value, EIP712Value::Bool(_)) {
return Err(TypesGenerationError::ArrayInconsistency(
"boolean",
property_name.clone(),
));
}
}
types.0.push(MemberVariable {
type_: EIP712Type::Array(Box::new(EIP712Type::Bool)),
name: String::from(property_name),
});
}
EIP712Value::Integer(_) => {
for value in values {
if !matches!(value, EIP712Value::Integer(_)) {
return Err(TypesGenerationError::ArrayInconsistency(
"number",
property_name.clone(),
));
}
}
types.0.push(MemberVariable {
type_: EIP712Type::Array(Box::new(EIP712Type::UintN(256))),
name: String::from(property_name),
});
}
EIP712Value::String(_) => {
for value in values {
if !matches!(value, EIP712Value::String(_)) {
return Err(TypesGenerationError::ArrayInconsistency(
"string",
property_name.clone(),
));
}
}
types.0.push(MemberVariable {
type_: EIP712Type::Array(Box::new(EIP712Type::String)),
name: String::from(property_name),
});
}
_ => {
return Err(TypesGenerationError::ComplexArrayValue(
property_name.clone(),
));
}
}
}
EIP712Value::Struct(object) => {
let mut recursive_output = generate_types(
&EIP712Value::Struct(object.clone()),
Some(primary_type.clone()),
)?;
let recursive_types = recursive_output.remove(&primary_type).ok_or_else(|| {
TypesGenerationError::MissingPrimaryTypeInRecursiveOutput(primary_type.clone())
})?;
let property_type = property_to_struct_name(property_name);
types.0.push(MemberVariable {
name: String::from(property_name),
type_: EIP712Type::Struct(property_type.clone()),
});
output.insert(property_type, recursive_types);
for (prop, type_) in recursive_output.into_iter() {
output.insert(prop, type_);
}
}
_ => {
return Err(TypesGenerationError::ComplexValue(property_name.clone()));
}
}
}
output.insert(primary_type, types);
Ok(output)
}
pub fn generate_types_with_proof(
doc: &EIP712Value,
primary_type: Option<StructName>,
) -> Result<HashMap<StructName, StructType>, TypesGenerationError> {
let mut map = if let EIP712Value::Struct(ref map) = doc {
map.clone()
} else {
return Err(TypesGenerationError::ExpectedObject);
};
if map.get("proof").is_some() {
return Err(TypesGenerationError::ProofAlreadyExists);
}
map.insert(
"proof".to_string(),
EIP712Value::Struct(
vec![
(
"type".to_string(),
EIP712Value::String("ExampleSignatureType".to_string()),
),
(
"created".to_string(),
EIP712Value::String("2022-02-03T19:18:58Z".to_string()),
),
(
"proofPurpose".to_string(),
EIP712Value::String("assertionMethod".to_string()),
),
(
"verificationMethod".to_string(),
EIP712Value::String("did:example:eip712sig".to_string()),
),
]
.into_iter()
.collect(),
),
);
generate_types(&EIP712Value::Struct(map), primary_type)
}
#[derive(Error, Debug)]
pub enum ProofGenerationError {
#[error("Unable to generate types: {0}")]
TypesGeneration(#[from] TypesGenerationError),
}
pub fn generate_proof_info(doc: &EIP712Value) -> Result<ProofInfo, ProofGenerationError> {
let primary_type = StructName::from("Document");
let types = generate_types(doc, Some(primary_type.clone()))?;
let domain = EIP712Value::Struct(HashMap::default());
let eip712_domain = eip712sig_default_domain();
Ok(ProofInfo {
types_or_uri: TypesOrURI::Object(Types {
eip712_domain,
types,
}),
primary_type,
domain,
})
}
#[cfg(test)]
mod tests {
use super::*;
use lazy_static::lazy_static;
use serde_json::json;
#[test]
fn test_parse_type() {
let string_type = EIP712Type::try_from(String::from("string")).unwrap();
assert_eq!(string_type, EIP712Type::String);
let string_array_type = EIP712Type::try_from(String::from("string[]")).unwrap();
let string_array_type_expected = EIP712Type::Array(Box::new(EIP712Type::String));
assert_eq!(string_array_type, string_array_type_expected);
EIP712Type::try_from(String::from("string]")).unwrap_err();
}
#[test]
fn test_encode_type() {
let types = Types {
eip712_domain: StructType(Vec::new()),
types: vec![
(
"Transaction".to_string(),
StructType(vec![
MemberVariable {
name: "from".to_string(),
type_: EIP712Type::Struct("Person".to_string()),
},
MemberVariable {
name: "to".to_string(),
type_: EIP712Type::Struct("Person".to_string()),
},
MemberVariable {
name: "tx".to_string(),
type_: EIP712Type::Struct("Asset".to_string()),
},
]),
),
(
"Person".to_string(),
StructType(vec![
MemberVariable {
name: "wallet".to_string(),
type_: EIP712Type::Address,
},
MemberVariable {
name: "name".to_string(),
type_: EIP712Type::String,
},
]),
),
(
"Asset".to_string(),
StructType(vec![
MemberVariable {
name: "token".to_string(),
type_: EIP712Type::Address,
},
MemberVariable {
name: "amount".to_string(),
type_: EIP712Type::UintN(256),
},
]),
),
]
.into_iter()
.collect(),
};
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)");
}
#[test]
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" }
],
"Person": [
{ "name": "name", "type": "string" },
{ "name": "wallet", "type": "address" }
],
"Mail": [
{ "name": "from", "type": "Person" },
{ "name": "to", "type": "Person" },
{ "name": "contents", "type": "string" }
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": 1,
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"from": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!"
}
}))
.unwrap();
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"
);
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"
);
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"
);
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 = bytes_to_lowerhex(&hash);
assert_eq!(
hash_hex,
"0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2"
);
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"
);
}
lazy_static! {
static ref TEST_BASIC_DOCUMENT: Value = {
json!({
"@context": ["https://schema.org", "https://w3id.org/security/v2"],
"@type": "Person",
"firstName": "Jane",
"lastName": "Does",
"jobTitle": "Professor",
"telephone": "(425) 123-4567",
"email": "jane.doe@example.com"
})
};
static ref TEST_NESTED_DOCUMENT: Value = {
json!({
"@context": ["https://schema.org", "https://w3id.org/security/v2"],
"@type": "Person",
"data": {
"name": {
"firstName": "John",
"lastName": "Doe"
},
"job": {
"jobTitle": "Professor",
"employer": "University of Waterloo"
}
},
"telephone": "(425) 123-4567"
})
};
static ref MOCK_ETHR_DID_RESOLVER: MockEthrDIDResolver =
MockEthrDIDResolver {
doc: serde_json::from_value(json!({
"@context": [
"https://www.w3.org/ns/did/v1",
{
"EcdsaSecp256k1RecoveryMethod2020": "https://identity.foundation/EcdsaSecp256k1RecoverySignature2020#EcdsaSecp256k1RecoveryMethod2020",
"blockchainAccountId": "https://w3id.org/security#blockchainAccountId"
}
],
"id": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443",
"verificationMethod": [{
"id": "#blockchainAccountId",
"type": "EcdsaSecp256k1RecoveryMethod2020",
"controller": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443",
"blockchainAccountId": "eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443"
}],
"assertionMethod": [
"#blockchainAccountId"
]
})).unwrap()
};
pub static ref EXAMPLE_TYPES: Value = {
serde_json::json!({
"Data": [
{
"name": "job",
"type": "Job"
},
{
"name": "name",
"type": "Name"
}
],
"Job": [
{
"name": "employer",
"type": "string"
},
{
"name": "jobTitle",
"type": "string"
}
],
"Name": [
{
"name": "firstName",
"type": "string"
},
{
"name": "lastName",
"type": "string"
}
],
"Document": [
{
"name": "@context",
"type": "string[]"
},
{
"name": "@type",
"type": "string"
},
{
"name": "data",
"type": "Data"
},
{
"name": "proof",
"type": "Proof"
},
{
"name": "telephone",
"type": "string"
}
],
"Proof": [
{
"name": "created",
"type": "string"
},
{
"name": "proofPurpose",
"type": "string"
},
{
"name": "type",
"type": "string"
},
{
"name": "verificationMethod",
"type": "string"
}
]
})
};
}
#[test]
fn test_property_sorting() {
let object: EIP712Value = serde_json::from_str(
r#"{
"\u20ac": "Euro Sign",
"\r": "Carriage Return",
"\ufb33": "Hebrew Letter Dalet With Dagesh",
"1": "One",
"\ud83d\ude00": "Emoji: Grinning Face",
"\u0080": "Control",
"\u00f6": "Latin Small Letter O With Diaeresis"
}"#,
)
.unwrap();
let mut props: Vec<(&String, &EIP712Value)> = object.as_struct().unwrap().iter().collect();
props.sort_by_cached_key(|(name, _value)| name.encode_utf16().collect::<Vec<u16>>());
let expected_values = vec![
"Carriage Return",
"One",
"Control",
"Latin Small Letter O With Diaeresis",
"Euro Sign",
"Emoji: Grinning Face",
"Hebrew Letter Dalet With Dagesh",
];
let values: Vec<String> = props
.iter()
.map(|(_name_, value)| Value::from((*value).clone()).as_str().unwrap().to_string())
.collect();
assert_eq!(values, expected_values);
}
#[test]
fn test_types_generation() {
let doc: EIP712Value = serde_json::from_value(json!({
"@context": ["https://schema.org", "https://w3id.org/security/v2"],
"@type": "Person",
"name": {
"first": "Jane",
"last": "Doe",
},
"otherData": {
"jobTitle": "Professor",
"school": "University of ExampleLand",
},
"telephone": "(425) 123-4567",
"email": "jane.doe@example.com",
}))
.unwrap();
let expected_types: HashMap<StructName, StructType> = serde_json::from_value(json!({
"Name": [
{ "name": "first", "type": "string" },
{ "name": "last", "type": "string" },
],
"OtherData": [
{ "name": "jobTitle", "type": "string" },
{ "name": "school", "type": "string" },
],
"Document": [
{ "name": "@context", "type": "string[]" },
{ "name": "@type", "type": "string" },
{ "name": "email", "type": "string" },
{ "name": "name", "type": "Name" },
{ "name": "otherData", "type": "OtherData" },
{ "name": "telephone", "type": "string" },
]
}))
.unwrap();
let types = generate_types(&doc, None).unwrap();
eprintln!("types: {}", serde_json::to_string_pretty(&types).unwrap());
let types_value = serde_json::to_value(types).unwrap();
let expected_types_value = serde_json::to_value(expected_types).unwrap();
assert_eq!(types_value, expected_types_value);
let test_basic_document: EIP712Value =
serde_json::from_value(TEST_BASIC_DOCUMENT.clone()).unwrap();
let types = generate_types(&test_basic_document, None).unwrap();
eprintln!("types: {}", serde_json::to_string_pretty(&types).unwrap());
let types_value = serde_json::to_value(types).unwrap();
let expected_types_value: Value = json!({
"Document": [
{
"name": "@context",
"type": "string[]"
},
{
"name": "@type",
"type": "string"
},
{
"name": "email",
"type": "string"
},
{
"name": "firstName",
"type": "string"
},
{
"name": "jobTitle",
"type": "string"
},
{
"name": "lastName",
"type": "string"
},
{
"name": "telephone",
"type": "string"
}
]
});
assert_eq!(types_value, expected_types_value);
}
use async_trait::async_trait;
use ssi_dids::did_resolve::{
DIDResolver, DocumentMetadata, ResolutionInputMetadata, ResolutionMetadata, ERROR_NOT_FOUND,
};
use ssi_dids::Document;
use crate::LinkedDataProofOptions;
use ssi_core::uri::URI;
use ssi_dids::VerificationRelationship as ProofPurpose;
#[async_std::test]
async fn eip712sig_keypair() {
let addr = "0xaed7ea8035eec47e657b34ef5d020c7005487443";
let sk_hex = "0x149195a4059ac8cafe2d56fc612f613b6b18b9265a73143c9f6d7cfbbed76b7e";
let sk_bytes = bytes_from_hex(sk_hex).unwrap();
use ssi_jwk::{Base64urlUInt, ECParams, Params, JWK};
let sk = k256::SecretKey::from_be_bytes(&sk_bytes).unwrap();
let pk = sk.public_key();
let mut ec_params = ECParams::try_from(&pk).unwrap();
ec_params.ecc_private_key = Some(Base64urlUInt(sk_bytes.to_vec()));
let jwk = JWK::from(Params::EC(ec_params));
let hash = ssi_jwk::eip155::hash_public_key(&jwk).unwrap();
assert_eq!(hash, addr);
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct InputOptions {
#[serde(skip_serializing_if = "Option::is_none")]
types: Option<HashMap<StructName, StructType>>,
#[serde(skip_serializing_if = "Option::is_none")]
domain: Option<EIP712Value>,
#[serde(skip_serializing_if = "Option::is_none")]
date: Option<chrono::DateTime<chrono::Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "embed")]
embed: Option<bool>,
#[serde(rename = "embedAsURI")]
#[serde(skip_serializing_if = "Option::is_none")]
embed_as_uri: Option<bool>,
}
impl From<InputOptions> for LinkedDataProofOptions {
fn from(input_options: InputOptions) -> LinkedDataProofOptions {
LinkedDataProofOptions {
created: input_options.date,
verification_method: Some(URI::String(
"did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443".to_string(),
)),
proof_purpose: Some(ProofPurpose::AssertionMethod),
..Default::default()
}
}
}
struct ExampleDocument(Value);
#[async_trait]
impl LinkedDataDocument for ExampleDocument {
fn get_contexts(&self) -> Result<Option<String>, crate::error::Error> {
Ok(None)
}
async fn to_dataset_for_signing(
&self,
_parent: Option<&(dyn LinkedDataDocument + Sync)>,
_context_loader: &mut ContextLoader,
) -> Result<ssi_json_ld::rdf::DataSet, crate::error::Error> {
todo!();
}
fn to_value(&self) -> Result<Value, crate::error::Error> {
Ok(self.0.clone())
}
}
#[derive(Debug, Clone)]
pub struct MockEthrDIDResolver {
doc: Document,
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl DIDResolver for MockEthrDIDResolver {
async fn resolve(
&self,
did: &str,
_input_metadata: &ResolutionInputMetadata,
) -> (
ResolutionMetadata,
Option<Document>,
Option<DocumentMetadata>,
) {
let doc: Document = match did {
"did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443" => self.doc.clone(),
_ => return (ResolutionMetadata::from_error(ERROR_NOT_FOUND), None, None),
};
(
ResolutionMetadata::default(),
Some(doc),
Some(DocumentMetadata::default()),
)
}
}
#[async_std::test]
#[ignore] async fn eip712sig_types_generation_no_embedding() {
let input_options: InputOptions = serde_json::from_value(json!({
"date": "2021-08-30T13:28:02Z",
"verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId",
"domain": {
"name": "Test"
}
}))
.unwrap();
let _ldp_options = LinkedDataProofOptions::from(input_options);
let proof: Proof = serde_json::from_value(json!({
"created": "2021-08-30T13:28:02Z",
"proofPurpose": "assertionMethod",
"proofValue": "0xbbdf2914c7572185bbc263e066dfb43f3136e4441fddb3fe3ea4541bbf7fd1f00d8e5af3ce4fbb1f2ebd5256f39b22cef7f285189df2976ea0c385c77f0a42791b",
"type": "EthereumEip712Signature2021",
"verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId",
}))
.unwrap();
let basic_doc = ExampleDocument(TEST_BASIC_DOCUMENT.clone());
let resolver = MOCK_ETHR_DID_RESOLVER.clone();
let mut context_loader = ssi_json_ld::ContextLoader::default();
let verification_result = proof
.verify(&basic_doc, &resolver, &mut context_loader)
.await;
println!("{:#?}", verification_result);
assert!(verification_result.errors.is_empty());
}
#[async_std::test]
async fn eip712sig_typeddata_provided_embedded_eip712_properties() {
let input_options: InputOptions = serde_json::from_value(json!({
"verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId",
"types": {
"Data": [
{
"name": "job",
"type": "Job"
},
{
"name": "name",
"type": "Name"
}
],
"Document": [
{
"name": "@context",
"type": "string[]"
},
{
"name": "@type",
"type": "string"
},
{
"name": "data",
"type": "Data"
},
{
"name": "telephone",
"type": "string"
},
{
"name": "proof",
"type": "Proof"
}
],
"Job": [
{
"name": "employer",
"type": "string"
},
{
"name": "jobTitle",
"type": "string"
}
],
"Proof": [
{
"name": "created",
"type": "string"
},
{
"name": "proofPurpose",
"type": "string"
},
{
"name": "type",
"type": "string"
},
{
"name": "verificationMethod",
"type": "string"
}
],
"Name": [
{
"name": "firstName",
"type": "string"
},
{
"name": "lastName",
"type": "string"
}
]
},
"domain": {
"name": "Test"
},
"date": "2021-08-30T13:28:02Z",
"embed": true
}))
.unwrap();
let _ldp_options = LinkedDataProofOptions::from(input_options);
let proof: Proof = serde_json::from_value(json!({
"created": "2021-08-30T13:28:02Z",
"eip712": {
"domain": {
"name": "Test",
},
"primaryType": "Document",
"types": {
"Data": [
{
"name": "job",
"type": "Job",
},
{
"name": "name",
"type": "Name",
},
],
"Document": [
{
"name": "@context",
"type": "string[]",
},
{
"name": "@type",
"type": "string",
},
{
"name": "data",
"type": "Data",
},
{
"name": "telephone",
"type": "string",
},
{
"name": "proof",
"type": "Proof",
},
],
"Job": [
{
"name": "employer",
"type": "string",
},
{
"name": "jobTitle",
"type": "string",
},
],
"Name": [
{
"name": "firstName",
"type": "string",
},
{
"name": "lastName",
"type": "string",
},
],
"Proof": [
{
"name": "created",
"type": "string",
},
{
"name": "proofPurpose",
"type": "string",
},
{
"name": "type",
"type": "string",
},
{
"name": "verificationMethod",
"type": "string",
},
],
},
},
"proofPurpose": "assertionMethod",
"proofValue": "0xcf5844be1f1a5c1a083565d492ab4bee93bd0e24a4573bd8ff47331ad225b9d11c4831aade8d071f4abb8c9e266aaaf30612c582c2bc8f082b8788448895fa4a1b",
"type": "EthereumEip712Signature2021",
"verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId",
})).unwrap();
let nested_doc = ExampleDocument(TEST_NESTED_DOCUMENT.clone());
let resolver = MOCK_ETHR_DID_RESOLVER.clone();
let mut context_loader = ssi_json_ld::ContextLoader::default();
let verification_result = proof
.verify(&nested_doc, &resolver, &mut context_loader)
.await;
println!("{:#?}", verification_result);
assert!(verification_result.errors.is_empty());
}
#[async_std::test]
async fn eip712sig_typeddata_types_generation_typeddata_schema_as_uri() {
let input_options: InputOptions = serde_json::from_value(json!({
"embedAsURI": true,
"date": "2021-08-30T13:28:02Z",
"verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId",
"domain": {
"name": "Test"
}
}))
.unwrap();
let _ldp_options = LinkedDataProofOptions::from(input_options);
let proof: Proof = serde_json::from_value(json!({
"created": "2021-08-30T13:28:02Z",
"proofPurpose": "assertionMethod",
"type": "EthereumEip712Signature2021",
"verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId",
"proofValue": "0x8327ad5e4b2426eac7626400c75f000c3e04caf2a863b888988e4e85533880183d4b9cc6870183e55dabfa96b9486624f45ef849bb146257d123f297a2dbf3a11c",
"eip712": {
"domain": {
"name": "Test"
},
"types": "https://example.org/types.json",
"primaryType": "Document"
}
})).unwrap();
let nested_doc = ExampleDocument(TEST_NESTED_DOCUMENT.clone());
let resolver = MOCK_ETHR_DID_RESOLVER.clone();
let mut context_loader = ssi_json_ld::ContextLoader::default();
let verification_result = proof
.verify(&nested_doc, &resolver, &mut context_loader)
.await;
println!("{:#?}", verification_result);
assert!(verification_result.errors.is_empty());
let test_nested_document: EIP712Value =
serde_json::from_value(TEST_NESTED_DOCUMENT.clone()).unwrap();
let types = generate_types_with_proof(&test_nested_document, None).unwrap();
eprintln!("types: {}", serde_json::to_string_pretty(&types).unwrap());
let types_value = serde_json::to_value(types).unwrap();
assert_eq!(types_value, *EXAMPLE_TYPES);
}
#[async_std::test]
async fn eip712sig_typeddata_provided_embedded_types() {
let input_options: InputOptions = serde_json::from_value(json!({
"date": "2021-08-30T13:28:02Z",
"verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId",
"domain": {
"name": "Test"
},
"embed": true
}))
.unwrap();
let _ldp_options = LinkedDataProofOptions::from(input_options);
let proof: Proof = serde_json::from_value(json!({
"created": "2021-08-30T13:28:02Z",
"eip712": {
"domain": {
"name": "EthereumEip712Signature2021",
},
"primaryType": "Document",
"types": {
"Data": [
{
"name": "job",
"type": "Job",
},
{
"name": "name",
"type": "Name",
},
],
"Document": [
{
"name": "@context",
"type": "string[]",
},
{
"name": "@type",
"type": "string",
},
{
"name": "data",
"type": "Data",
},
{
"name": "proof",
"type": "Proof",
},
{
"name": "telephone",
"type": "string",
},
],
"Job": [
{
"name": "employer",
"type": "string",
},
{
"name": "jobTitle",
"type": "string",
},
],
"Name": [
{
"name": "firstName",
"type": "string",
},
{
"name": "lastName",
"type": "string",
},
],
"Proof": [
{
"name": "created",
"type": "string",
},
{
"name": "proofPurpose",
"type": "string",
},
{
"name": "type",
"type": "string",
},
{
"name": "verificationMethod",
"type": "string",
},
],
},
},
"proofPurpose": "assertionMethod",
"proofValue": "0x7d57ace2be9cc3944aac023f66130935e489bbb1c9b469a4a5b4f16e5c298b57291bc80d52c6f873b11f4bf45c97c6e2506419af7506eaac5374e9ed381fcc5b1b",
"type": "EthereumEip712Signature2021",
"verificationMethod": "did:pkh:eip155:1:0xAED7EA8035eEc47E657B34eF5D020c7005487443#blockchainAccountId",
})).unwrap();
let nested_doc = ExampleDocument(TEST_NESTED_DOCUMENT.clone());
let resolver = MOCK_ETHR_DID_RESOLVER.clone();
let mut context_loader = ssi_json_ld::ContextLoader::default();
let verification_result = proof
.verify(&nested_doc, &resolver, &mut context_loader)
.await;
println!("{:#?}", verification_result);
assert!(verification_result.errors.is_empty());
}
}