Skip to content

Commit

Permalink
feat: uses signing logic defined in the contract to sign claims (#39)
Browse files Browse the repository at this point in the history
roshaans authored Jan 8, 2024
1 parent cf740e9 commit 160ab76
Showing 6 changed files with 320 additions and 256 deletions.
5 changes: 2 additions & 3 deletions tx-signing-service/.env.test
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
ACCOUNT_ID=erika.near.service
SECRET_KEY=ed25519:4om21u2A7ZMPQj4giTcbgW9KvSzNmRkwYA8op93WQ51aV6wGgdqFvjXA93uFRTtQJca5SBHAiXXLvtcwGGEoWpp6
SECRET_KEY=DH/zxFoO2AIeM3y3kazgwnO7dEdBEsmcOwetjd/yE2qf+FNT1C5o+H8B4JCjZ+g4+KaTKhN0dnEc1NM06LtCOg
QUERY_API=https://near-queryapi.api.pagoda.co/v1/graphql
HEADER_ROLE=roshaan_near
HEADER_ROLE=roshaan_near
312 changes: 185 additions & 127 deletions tx-signing-service/Cargo.lock

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions tx-signing-service/Cargo.toml
Original file line number Diff line number Diff line change
@@ -9,11 +9,14 @@ edition = "2021"
warp = "0.3.6"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full", "test-util"] }
near-crypto = "0.17.0"
near-primitives = "0.17.0"
reqwest = { version = "0.11.22", features = ["json"] }
serde_json = "1.0.108"
serde_json = "1.0.91"
thiserror = "1.0.50"
dotenv = "0.15.0"
ed25519-dalek = "1"
rand = "^0.7"
base64 = "0.21.5"
borsh = "0.9"

[dev-dependencies]
22 changes: 14 additions & 8 deletions tx-signing-service/src/graphql_service.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::internal::sign_claim;
use crate::internal::{sign_claim, Claim};
use crate::{
QuestConditionQuery, QuestState, QuestStateError, QuestValidationInfo, QuestValidationRequest,
};
@@ -67,20 +67,26 @@ pub(crate) async fn check_quest(
Ok(data) if !data.is_empty() => data.get(0).unwrap().clone(),
_ => return Err(QuestStateError::QueryNotFound),
};
let parsed_quest_id = match quest_id.clone().parse::<u64>() {
Ok(id) => id,
Err(_) => {
return Err(QuestStateError::ClaimError(
"quest_id could not be parsed".to_string(),
))
}
};

match &quest_snapshot.is_completed {
true => {
let payload = serde_json::to_string(&QuestValidationInfo {
let claim = Claim {
account_id: account_id.clone(),
quest_id: quest_id.clone(),
})
.unwrap()
.into_bytes();
quest_id: parsed_quest_id,
};

let claim_response = sign_claim(&payload);
let claim_response = sign_claim(&claim, None);

match claim_response {
Ok(signature) => Ok(QuestState::Completed(
Ok((_, signature)) => Ok(QuestState::Completed(
"Quest has been completed!".to_string(),
signature,
)),
209 changes: 100 additions & 109 deletions tx-signing-service/src/internal.rs
Original file line number Diff line number Diff line change
@@ -1,138 +1,129 @@
use near_crypto::{InMemorySigner, SecretKey, Signature, Signer};
use near_primitives::types::AccountId;
use ed25519_dalek::Signature;
use ed25519_dalek::{Keypair, Signer};
use std::env;
use std::error::Error;
use std::str::FromStr;

/// given a payload, `sign_claim` pulls the Secret Key and Account ID from environment and uses
/// an in-memory signer
pub(crate) fn sign_claim(payload: &[u8]) -> Result<Signature, Box<dyn Error>> {
let secret_key = env::var("SECRET_KEY")?;
let secret_key = SecretKey::from_str(&secret_key)?;
use base64::{engine::general_purpose, Engine as _};
use borsh::{BorshDeserialize, BorshSerialize};

let account_id = env::var("ACCOUNT_ID").unwrap_or("v1.questverse.near".to_string());
let account_id = AccountId::from_str(&account_id)?;

let signer = InMemorySigner::from_secret_key(account_id, secret_key);
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Claim {
pub account_id: String,
pub quest_id: u64,
}

Ok(signer.sign(payload))
pub(crate) fn sign_claim(
claim: &Claim,
keypair: Option<Keypair>,
) -> Result<(String, Signature), Box<dyn Error>> {
let keypair = match keypair {
Some(kp) => kp,
None => {
let secret_key =
env::var("SECRET_KEY").expect("SECRET_KEY not found in the environment");
println!("secret_keysigned: {:?}", &secret_key);
let secret_key_decoded = general_purpose::STANDARD_NO_PAD
.decode(secret_key)
.expect("Failed to decode secret key");

Keypair::from_bytes(&secret_key_decoded).expect("Failed to create keypair from bytes")
}
};

let claim_bytes = claim.try_to_vec().unwrap();
let sig: Signature = keypair.sign(&claim_bytes);
let claim_base64 = general_purpose::STANDARD_NO_PAD.encode(claim_bytes.clone());
Ok((claim_base64, sig))
}

#[cfg(test)]
mod tests {
use super::*;
use crate::QuestValidationInfo;
use near_crypto::{KeyType, SecretKey};
use serde_json;
use base64::engine::general_purpose;
use ed25519_dalek::Keypair;
use ed25519_dalek::Verifier;
use std::path::Path;

fn setup() {
let dotenv_path = Path::new(".env.test");
dotenv::from_path(dotenv_path).ok();
fn sign_claim_test(claim: &Claim, keypair: Keypair) -> (String, Signature) {
sign_claim(&claim, Some(keypair)).unwrap()
}

fn mock_payload() -> Vec<u8> {
serde_json::to_string(&QuestValidationInfo {
account_id: "erika.near".to_string(),
quest_id: 477474.to_string(),
})
.unwrap()
.into_bytes()
fn generate_keypair() -> Keypair {
Keypair::generate(&mut rand::thread_rng())
}

const DEFAULT_ACCOUNT_ID: &str = "v1.questverse.near";

/// `generate_valid_secret_key` generates a random secret key
fn generate_valid_secret_key() -> String {
let secret_key = SecretKey::from_random(KeyType::ED25519);
secret_key.to_string()
/// copy of verify_claim method from the contract
pub fn verify_claim(claim_sig: &[u8; 64], claim: &[u8], public_key: &[u8; 32]) -> bool {
let public_key = ed25519_dalek::PublicKey::from_bytes(public_key).unwrap();
let sig: &[u8; 64] = claim_sig
.as_slice()
.try_into()
.expect("signature must be 64 bytes");
match Signature::from_bytes(sig) {
Ok(sig) => public_key.verify(claim, &sig).is_ok(),
Err(_) => false,
}
}

#[test]
fn test_sign_claim_with_valid_keys_and_correct_signer() {
setup();

let valid_secret_key = env::var("SECRET_KEY").expect("SECRET_KEY not found");
let valid_account_id = env::var("ACCOUNT_ID").expect("ACCOUNT_ID not found");

let payload = mock_payload();
let signature = sign_claim(&payload);
assert!(
signature.is_ok(),
"sign_claim should succeed with valid input"
);

let signature = signature.unwrap();

let secret_key =
SecretKey::from_str(&valid_secret_key).expect("Failed to parse secret key");
let account_id =
AccountId::from_str(&valid_account_id).expect("Failed to parse account ID");
let signer = InMemorySigner::from_secret_key(account_id, secret_key);

let is_valid = signer.verify(&payload, &signature);

assert!(is_valid, "The signature should be valid");
fn test_env() {
let dotenv_path = Path::new(".env.test");
dotenv::from_path(dotenv_path).ok();
}

#[test]
fn test_sign_claim_with_valid_keys_and_incorrect_signer() {
setup();

let valid_account_id = env::var("ACCOUNT_ID").expect("ACCOUNT_ID not found");

let payload = mock_payload();
let signature = sign_claim(&payload);
assert!(
signature.is_ok(),
"sign_claim should succeed with valid input"
);

let payload = mock_payload();
let signature = sign_claim(&payload);
assert!(
signature.is_ok(),
"sign_claim should succeed with valid input"
);
let signature = signature.unwrap();

let sk2 = generate_valid_secret_key();
let secret_key = SecretKey::from_str(&sk2).expect("Failed to parse secret key");
let account_id =
AccountId::from_str(&valid_account_id).expect("Failed to parse account ID");
let signer = InMemorySigner::from_secret_key(account_id, secret_key);

let is_valid = signer.verify(&payload, &signature);

assert!(
!is_valid,
"The signature should not be valid with a different secret key"
);
fn verify_claim_works() {
let keypair = generate_keypair();
let public_key = keypair.public.to_bytes();
let claim = Claim {
account_id: "alice.near".to_string(),
quest_id: 1,
};
let (claim_base64, sig) = sign_claim_test(&claim, keypair);

let decoded_claim = general_purpose::STANDARD_NO_PAD
.decode(claim_base64)
.unwrap();

let res = verify_claim(&sig.to_bytes(), &decoded_claim, &public_key);
assert!(res, "res {}", res);
}

#[test]
fn test_sign_claim_with_default_account_id() {
setup();

let valid_secret_key = env::var("SECRET_KEY").expect("SECRET_KEY not found");

let payload = mock_payload();
let signature = sign_claim(&payload);
assert!(
signature.is_ok(),
"sign_claim should succeed with default account ID"
fn verify_claim_false_signer() {
let keypair = generate_keypair();
let claim = Claim {
account_id: "alice.near".to_string(),
quest_id: 1,
};
let (claim_base64, sig) = sign_claim_test(&claim, keypair);
let decoded_claim = general_purpose::STANDARD_NO_PAD
.decode(claim_base64)
.unwrap();
let false_keypair = generate_keypair();
let res = verify_claim(
&sig.to_bytes(),
&decoded_claim,
&false_keypair.public.to_bytes(),
);
assert!(!res, "res {}", res);
}

let signature = signature.unwrap();

let secret_key =
SecretKey::from_str(&valid_secret_key).expect("Failed to parse secret key");
let account_id =
AccountId::from_str(DEFAULT_ACCOUNT_ID).expect("Failed to parse account id");
let signer = InMemorySigner::from_secret_key(account_id, secret_key);

let is_valid = signer.verify(&payload, &signature);

assert!(is_valid, "The signature should be valid");
#[test]
fn verify_claim_false_claim() {
let keypair = generate_keypair();
let public_key = keypair.public.to_bytes();
let claim = Claim {
account_id: "alice.near".to_string(),
quest_id: 1,
};
let false_claim = Claim {
account_id: "alice.near".to_string(),
quest_id: 2,
};
let (_, sig) = sign_claim_test(&claim, keypair);
let false_claim_bytes = false_claim.try_to_vec().unwrap();
let res = verify_claim(&sig.to_bytes(), &false_claim_bytes, &public_key);
assert!(!res, "res {}", res);
}
}
21 changes: 14 additions & 7 deletions tx-signing-service/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::graphql_service::check_quest;
use near_crypto::Signature;
use base64::{engine::general_purpose, Engine as _};
use ed25519_dalek::Signature;
use reqwest::{header::InvalidHeaderValue, Error as ReqwestError};
use serde::{Deserialize, Serialize};
use std::convert::Infallible;
@@ -74,7 +75,7 @@ struct ValidationResponse {
struct ClaimReceiptResponse {
message: Option<String>,
signed_receipt: String,
claim_data: QuestValidationInfo,
claim_data: String,
}

async fn validate_quest(info: QuestValidationInfo) -> Result<impl warp::Reply, Infallible> {
@@ -107,13 +108,19 @@ async fn generate_claim_receipt(
}
}

let sig_bytes = signed_receipt.into_bytes();
let sig_bytes_encoded = general_purpose::STANDARD_NO_PAD.encode(&sig_bytes.to_vec());
let claim_data_bytes = serde_json::to_vec(&QuestValidationInfo {
account_id: info.account_id,
quest_id: info.quest_id,
})
.unwrap();

let claim_data_encoded = general_purpose::STANDARD_NO_PAD.encode(claim_data_bytes);
Ok(warp::reply::json(&ClaimReceiptResponse {
message,
signed_receipt,
claim_data: QuestValidationInfo {
account_id: info.account_id,
quest_id: info.quest_id,
},
signed_receipt: sig_bytes_encoded,
claim_data: claim_data_encoded,
}))
}

0 comments on commit 160ab76

Please sign in to comment.