-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7f08502
commit 1fb0f78
Showing
8 changed files
with
396 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
use { | ||
super::CacaoError, | ||
serde::{Deserialize, Serialize}, | ||
}; | ||
|
||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] | ||
pub struct Header { | ||
pub t: String, | ||
} | ||
|
||
impl Header { | ||
pub fn is_valid(&self) -> Result<(), CacaoError> { | ||
match self.t.as_str() { | ||
"eip4361" => Ok(()), | ||
_ => Err(CacaoError::Header), | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
use { | ||
self::{header::Header, payload::Payload, signature::Signature}, | ||
core::fmt::Debug, | ||
serde::{Deserialize, Serialize}, | ||
std::fmt::{Display, Write as _}, | ||
thiserror::Error as ThisError, | ||
}; | ||
|
||
pub mod header; | ||
pub mod payload; | ||
pub mod signature; | ||
|
||
/// Errors that can occur during JWT verification | ||
#[derive(Debug, ThisError)] | ||
pub enum CacaoError { | ||
#[error("Invalid header")] | ||
Header, | ||
|
||
#[error("Invalid or missing identity key in payload resources")] | ||
PayloadIdentityKey, | ||
|
||
#[error("Invalid payload resources")] | ||
PayloadResources, | ||
|
||
#[error("Unsupported signature type")] | ||
UnsupportedSignature, | ||
|
||
#[error("Unable to verify")] | ||
Verification, | ||
} | ||
|
||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] | ||
pub enum Version { | ||
V1 = 1, | ||
} | ||
|
||
impl<'de> Deserialize<'de> for Version { | ||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | ||
where | ||
D: serde::Deserializer<'de>, | ||
{ | ||
let version = String::deserialize(deserializer)?; | ||
match version.as_str() { | ||
"1" => Ok(Version::V1), | ||
_ => Err(serde::de::Error::custom("Invalid version")), | ||
} | ||
} | ||
} | ||
|
||
impl Serialize for Version { | ||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | ||
where | ||
S: serde::Serializer, | ||
{ | ||
serializer.serialize_str(&format!("{}", *self as u8)) | ||
} | ||
} | ||
|
||
impl Display for Version { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
write!(f, "{}", *self as u8) | ||
} | ||
} | ||
|
||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] | ||
pub struct Cacao { | ||
pub h: Header, | ||
pub p: Payload, | ||
pub s: Signature, | ||
} | ||
|
||
impl Cacao { | ||
const ETHEREUM: &'static str = "Ethereum"; | ||
|
||
pub fn verify(&self) -> Result<bool, CacaoError> { | ||
self.p.is_valid()?; | ||
self.h.is_valid()?; | ||
self.s.verify(self) | ||
} | ||
|
||
pub fn siwe_message(&self) -> Result<String, CacaoError> { | ||
self.caip122_message(Self::ETHEREUM) | ||
} | ||
|
||
pub fn caip122_message(&self, chain_name: &str) -> Result<String, CacaoError> { | ||
let mut message = format!( | ||
"{} wants you to sign in with your {} account:\n{}\n", | ||
self.p.domain, | ||
chain_name, | ||
self.p.address()? | ||
); | ||
|
||
if let Some(statement) = &self.p.statement { | ||
let _ = write!(message, "\n{}\n", statement); | ||
} | ||
|
||
let _ = write!( | ||
message, | ||
"\nURI: {}\nVersion: {}\nChain ID: {}\nNonce: {}\nIssued At: {}", | ||
self.p.aud, | ||
self.p.version, | ||
self.p.chain_id()?, | ||
self.p.nonce, | ||
self.p.iat | ||
); | ||
|
||
if let Some(exp) = &self.p.exp { | ||
let _ = write!(message, "\nExpiration Time: {}", exp); | ||
} | ||
|
||
if let Some(nbf) = &self.p.nbf { | ||
let _ = write!(message, "\nNot Before: {}", nbf); | ||
} | ||
|
||
if let Some(request_id) = &self.p.request_id { | ||
let _ = write!(message, "\nRequest ID: {}", request_id); | ||
} | ||
|
||
if let Some(resources) = &self.p.resources { | ||
if !resources.is_empty() { | ||
let _ = write!(message, "\nResources:"); | ||
resources.iter().for_each(|resource| { | ||
let _ = write!(message, "\n- {}", resource); | ||
}); | ||
} | ||
} | ||
|
||
Ok(message) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
use { | ||
super::{CacaoError, Version}, | ||
crate::auth::did::{extract_did_data, DID_METHOD_KEY}, | ||
serde::{Deserialize, Serialize}, | ||
}; | ||
|
||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] | ||
pub struct Payload { | ||
pub domain: String, | ||
pub iss: String, | ||
pub statement: Option<String>, | ||
pub aud: String, | ||
pub version: Version, | ||
pub nonce: String, | ||
pub iat: String, | ||
pub exp: Option<String>, | ||
pub nbf: Option<String>, | ||
pub request_id: Option<String>, | ||
pub resources: Option<Vec<String>>, | ||
} | ||
|
||
impl Payload { | ||
const ISS_DELIMITER: &'static str = ":"; | ||
const ISS_POSITION_OF_ADDRESS: usize = 4; | ||
const ISS_POSITION_OF_NAMESPACE: usize = 2; | ||
const ISS_POSITION_OF_REFERENCE: usize = 3; | ||
|
||
/// TODO: write valdation | ||
pub fn is_valid(&self) -> Result<bool, CacaoError> { | ||
Ok(true) | ||
} | ||
|
||
pub fn address(&self) -> Result<String, CacaoError> { | ||
self.iss | ||
.split(Self::ISS_DELIMITER) | ||
.nth(Self::ISS_POSITION_OF_ADDRESS) | ||
.ok_or(CacaoError::PayloadResources) | ||
.map(|s| s.to_string()) | ||
} | ||
|
||
pub fn namespace(&self) -> Result<String, CacaoError> { | ||
self.iss | ||
.split(Self::ISS_DELIMITER) | ||
.nth(Self::ISS_POSITION_OF_NAMESPACE) | ||
.ok_or(CacaoError::PayloadResources) | ||
.map(|s| s.to_string()) | ||
} | ||
|
||
pub fn chain_id_reference(&self) -> Result<String, CacaoError> { | ||
Ok(format!( | ||
"{}{}{}", | ||
self.namespace()?, | ||
Self::ISS_DELIMITER, | ||
self.chain_id()? | ||
)) | ||
} | ||
|
||
pub fn chain_id(&self) -> Result<String, CacaoError> { | ||
self.iss | ||
.split(Self::ISS_DELIMITER) | ||
.nth(Self::ISS_POSITION_OF_REFERENCE) | ||
.ok_or(CacaoError::PayloadResources) | ||
.map(|s| s.to_string()) | ||
} | ||
|
||
pub fn caip_10_address(&self) -> Result<String, CacaoError> { | ||
Ok(format!( | ||
"{}{}{}", | ||
self.chain_id_reference()?, | ||
Self::ISS_DELIMITER, | ||
self.address()? | ||
) | ||
.to_lowercase()) | ||
} | ||
|
||
pub fn identity_key(&self) -> Result<String, CacaoError> { | ||
let resources = self | ||
.resources | ||
.as_ref() | ||
.ok_or(CacaoError::PayloadResources)?; | ||
let did_key = resources.first().ok_or(CacaoError::PayloadIdentityKey)?; | ||
|
||
extract_did_data(did_key, DID_METHOD_KEY) | ||
.map(|data| data.to_string()) | ||
.map_err(|_| CacaoError::PayloadIdentityKey) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
use { | ||
super::{Cacao, CacaoError}, | ||
serde::{Deserialize, Serialize}, | ||
}; | ||
|
||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] | ||
pub struct Signature { | ||
pub t: String, | ||
pub s: String, | ||
} | ||
|
||
impl Signature { | ||
pub fn verify(&self, cacao: &Cacao) -> Result<bool, CacaoError> { | ||
match self.t.as_str() { | ||
"eip191" => Eip191.verify(&cacao.s.s, &cacao.p.address()?, &cacao.siwe_message()?), | ||
// "eip1271" => Eip1271.verify(), TODO: How to accces our RPC? | ||
_ => Err(CacaoError::UnsupportedSignature), | ||
} | ||
} | ||
} | ||
|
||
pub struct Eip191; | ||
|
||
impl Eip191 { | ||
pub fn eip191_bytes(&self, message: &str) -> Vec<u8> { | ||
format!( | ||
"\u{0019}Ethereum Signed Message:\n{}{}", | ||
message.as_bytes().len(), | ||
message | ||
) | ||
.into() | ||
} | ||
|
||
fn verify(&self, signature: &str, address: &str, message: &str) -> Result<bool, CacaoError> { | ||
use { | ||
k256::ecdsa::{RecoveryId, Signature as Sig, VerifyingKey}, | ||
sha3::{Digest, Keccak256}, | ||
}; | ||
|
||
let signature_bytes = hex::decode(guarantee_no_hex_prefix(signature)) | ||
.map_err(|_| CacaoError::Verification)?; | ||
|
||
let sig = Sig::try_from(&signature_bytes[..64]).map_err(|_| CacaoError::Verification)?; | ||
let recovery_id = RecoveryId::try_from(&signature_bytes[64] % 27) | ||
.map_err(|_| CacaoError::Verification)?; | ||
|
||
let recovered_key = VerifyingKey::recover_from_digest( | ||
Keccak256::new_with_prefix(&self.eip191_bytes(message)), | ||
&sig, | ||
recovery_id, | ||
) | ||
.map_err(|_| CacaoError::Verification)?; | ||
|
||
let add = &Keccak256::default() | ||
.chain_update(&recovered_key.to_encoded_point(false).as_bytes()[1..]) | ||
.finalize()[12..]; | ||
|
||
let address_encoded = hex::encode(add); | ||
|
||
if address_encoded.to_lowercase() != guarantee_no_hex_prefix(address).to_lowercase() { | ||
Err(CacaoError::Verification) | ||
} else { | ||
Ok(true) | ||
} | ||
} | ||
} | ||
|
||
/// Remove the 0x prefix from a hex string | ||
fn guarantee_no_hex_prefix(s: &str) -> &str { | ||
if let Some(stripped) = s.strip_prefix("0x") { | ||
stripped | ||
} else { | ||
s | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
use crate::auth::cacao::Cacao; | ||
|
||
/// Test that we can verify a Cacao | ||
#[test] | ||
fn cacao_verify_success() { | ||
let cacao_serialized = r#"{ | ||
"h": { | ||
"t": "eip4361" | ||
}, | ||
"p": { | ||
"iss": "did:pkh:eip155:1:0xf457f233ab23f863cabc383ebb37b29d8929a17a", | ||
"domain": "http://10.0.2.2:8080", | ||
"aud": "http://10.0.2.2:8080", | ||
"version": "1", | ||
"nonce": "[B@c3772c7", | ||
"iat": "2023-01-17T12:15:05+01:00", | ||
"resources": [ | ||
"did:key:z6MkkG9nM8ksS37sq5mgeoCn5kihLkWANcm9pza5WTkq3tWZ" | ||
] | ||
}, | ||
"s": { | ||
"t": "eip191", | ||
"s": "0x1b39982707c70c95f4676e7386052a07b47ecc073b3e9cf47b64b579687d3f68181d48fa9e926ad591ba6954f1a70c597d0772a800bed5fa906384fcd83bcf4f1b" | ||
} | ||
} "#; | ||
let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); | ||
let result = cacao.verify(); | ||
assert!(result.is_ok()); | ||
assert!(result.map_err(|_| false).unwrap()); | ||
|
||
let identity_key = cacao.p.identity_key(); | ||
assert!(identity_key.is_ok()); | ||
} | ||
|
||
/// Test that we can verify a Cacao with uppercase address | ||
#[test] | ||
fn cacao_without_lowercase_address_verify_success() { | ||
let cacao_serialized = r#"{"h":{"t":"eip4361"},"p":{"iss":"did:pkh:eip155:1:0xbD4D1935165012e7D29919dB8717A5e670a1a5b1","domain":"https://staging.keys.walletconnect.com","aud":"https://staging.keys.walletconnect.com","version":"1","nonce":"07487c09be5535dcbc341d8e35e5c9b4d3539a802089c42c5b1172dd9ed63c64","iat":"2023-01-25T15:08:36.846Z","statement":"Test","resources":["did:key:451cf9b97c64fcca05fbb0d4c40b886c94133653df5a2b6bd97bd29a0bbcdb37"]},"s":{"t":"eip191","s":"0x8496ad1dd1ddd5cb78ac26b62a6bd1c6cfff703ea3b11a9da29cfca112357ace75cac8ee28d114f9e166a6935ee9ed83151819a9e0ee738a0547116b1d978e351b"}}"#; | ||
let cacao: Cacao = serde_json::from_str(cacao_serialized).unwrap(); | ||
let result = cacao.verify(); | ||
assert!(result.is_ok()); | ||
assert!(result.map_err(|_| false).unwrap()); | ||
|
||
let identity_key = cacao.p.identity_key(); | ||
assert!(identity_key.is_ok()); | ||
} |
Oops, something went wrong.