use std::time::{SystemTime, UNIX_EPOCH};
use alloy::hex;
use base64ct::{Base64UrlUnpadded, Encoding};
use ed25519_dalek::{SigningKey, ed25519::signature::SignerMut};
use serde::{Deserialize, Serialize};
use crate::{
constants::CRYPTO_JWT_TTL,
utils::{encode_iss, random_bytes32},
};
pub struct RelayAuth {
client_seed: [u8; 32],
}
impl RelayAuth {
pub fn new(client_seed: [u8; 32]) -> Self {
Self { client_seed }
}
pub fn get_client_id(&self) -> String {
let seed = self.client_seed;
let key_pair = Keypair::from_seed(seed);
encode_iss(&key_pair.public_key)
}
pub fn sign_jwt(&self, aud: &str) -> String {
let keypair = Keypair::from_seed(self.client_seed);
let sub = random_bytes32(); let ttl = CRYPTO_JWT_TTL;
sign_jwt(&hex::encode(sub), aud, ttl, &keypair, None)
}
}
#[derive(Debug, Clone)]
pub struct Keypair {
pub seed: [u8; 32],
pub secret_key: [u8; 64],
pub public_key: [u8; 32],
}
impl Keypair {
pub fn generate() -> Self {
Keypair::from_seed(random_bytes32())
}
pub fn sign(&self, data: &[u8]) -> [u8; 64] {
let mut signing_key = SigningKey::from(self.seed);
signing_key.sign(data).to_bytes()
}
pub fn from_bytes64_secret(secret_key: [u8; 64]) -> Self {
let mut seed = [0u8; 32];
let mut public_key = [0u8; 32];
seed.copy_from_slice(&secret_key[..32]);
public_key.copy_from_slice(&secret_key[32..]);
let signing_key = SigningKey::from_bytes(&seed);
let calculated_public_key = signing_key.verifying_key().to_bytes();
assert_eq!(
public_key, calculated_public_key,
"Public key does not match the signing key"
);
Keypair {
seed,
secret_key,
public_key,
}
}
pub fn from_seed(seed: [u8; 32]) -> Keypair {
let signing_key = SigningKey::from_bytes(&seed);
let public_key = signing_key.verifying_key().to_bytes();
let mut secret_key = [0u8; 64];
secret_key[..32].copy_from_slice(&seed);
secret_key[32..].copy_from_slice(&public_key);
Keypair {
seed,
secret_key,
public_key,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct IridiumJWTHeader {
pub alg: &'static str,
pub typ: &'static str,
}
#[derive(Serialize, Deserialize)]
pub struct IridiumJWTPayload {
pub iss: String,
pub sub: String,
pub aud: String,
pub iat: u64,
pub exp: u64,
}
#[derive(Serialize)]
pub struct IridiumJWTSigned<'a> {
pub header: &'a IridiumJWTHeader,
pub payload: &'a IridiumJWTPayload,
pub signature: String,
}
fn encode_json<T: ?Sized + Serialize>(val: &T) -> String {
Base64UrlUnpadded::encode_string(
serde_json::to_string(val).unwrap().as_bytes(),
)
}
fn encode_data(
header: &IridiumJWTHeader,
payload: &IridiumJWTPayload,
) -> (Vec<u8>, String) {
let h = encode_json(header);
let p = encode_json(payload);
let joined = format!("{h}.{p}");
(joined.as_bytes().to_vec(), joined)
}
pub fn sign_jwt(
sub: &str,
aud: &str,
ttl: u64,
keypair: &Keypair,
iat_opt: Option<u64>,
) -> String {
let iat = iat_opt.unwrap_or_else(|| {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
});
let exp = iat + ttl;
let header = IridiumJWTHeader {
alg: "EdDSA",
typ: "JWT",
};
let iss = encode_iss(&keypair.public_key);
let payload = IridiumJWTPayload {
iss,
sub: sub.to_string(),
aud: aud.to_string(),
iat,
exp,
};
let (data, jwt_head_payload) = encode_data(&header, &payload);
let signature = keypair.sign(&data);
let sig_encoded = Base64UrlUnpadded::encode_string(&signature);
format!("{jwt_head_payload}.{sig_encoded}")
}
#[cfg(test)]
mod test {
use super::RelayAuth;
#[test]
fn test_1() {
let wk = RelayAuth::new([0; 32]);
let client_id = wk.get_client_id();
assert_eq!(
client_id,
"did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp"
);
}
#[test]
fn test_2() {
let wk = RelayAuth::new([
23, 113, 199, 94, 246, 41, 119, 10, 250, 248, 253, 136, 173, 241,
191, 149, 165, 249, 17, 42, 46, 189, 120, 175, 78, 88, 53, 83, 254,
16, 32, 150,
]);
let client_id = wk.get_client_id();
assert_eq!(
client_id,
"did:key:z6MkriJMhx6cLMiwwfuJ3NCGw8C8UjB9KoVHB7QSBaBxMx3y"
);
}
}