Skip to content

Commit

Permalink
feat: move cacao to Rust sdk (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
Rakowskiii committed Mar 23, 2023
1 parent 7f08502 commit 1fb0f78
Show file tree
Hide file tree
Showing 8 changed files with 396 additions and 0 deletions.
3 changes: 3 additions & 0 deletions relay_rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ chrono = { version = "0.4", default-features = false, features = ["std", "clock"
regex = "1.7"
once_cell = "1.16"
jsonwebtoken = "8.1"
k256 = "0.13.0"
sha3 = "0.10.6"
hex = "0.4.3"
3 changes: 3 additions & 0 deletions relay_rpc/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#[cfg(test)]
mod tests;

pub mod cacao;
pub mod did;

use {
crate::domain::{AuthSubject, ClientId, ClientIdDecodingError, DecodedClientId},
chrono::{DateTime, Utc},
Expand Down
18 changes: 18 additions & 0 deletions relay_rpc/src/auth/cacao/header.rs
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),
}
}
}
133 changes: 133 additions & 0 deletions relay_rpc/src/auth/cacao/mod.rs
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;
87 changes: 87 additions & 0 deletions relay_rpc/src/auth/cacao/payload.rs
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)
}
}
75 changes: 75 additions & 0 deletions relay_rpc/src/auth/cacao/signature.rs
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
}
}
46 changes: 46 additions & 0 deletions relay_rpc/src/auth/cacao/tests.rs
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());
}
Loading

0 comments on commit 1fb0f78

Please sign in to comment.