use std::{
convert::Infallible,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use p256::ecdsa::{
Signature, SigningKey, VerifyingKey,
signature::{Signer as _, hazmat::PrehashSigner as _},
};
use serde::Serialize;
use serde_json::json;
use sha2::{Digest, Sha256};
use crate::{profile::Platform, sign::Signer, token::StaticKeys};
#[must_use]
pub fn test_key(seed: &str) -> SigningKey {
let scalar: [u8; 32] = Sha256::digest(seed.as_bytes()).into();
SigningKey::from_bytes(&scalar.into()).expect("hash output is a valid scalar")
}
#[must_use]
pub fn authenticator_data(app_id: &str, counter: u32) -> Vec<u8> {
let mut data = Sha256::digest(app_id.as_bytes()).to_vec();
data.push(0x01);
data.extend_from_slice(&counter.to_be_bytes());
data
}
#[derive(Debug, Clone)]
pub struct SoftwareSigner {
key: SigningKey,
platform: Platform,
}
impl SoftwareSigner {
#[must_use]
pub const fn new(key: SigningKey, platform: Platform) -> Self {
Self { key, platform }
}
#[must_use]
pub fn verifying_key(&self) -> VerifyingKey {
*self.key.verifying_key()
}
}
#[derive(Serialize)]
struct Assertion<'a> {
#[serde(with = "serde_bytes")]
signature: &'a [u8],
#[serde(rename = "authenticatorData", with = "serde_bytes")]
authenticator_data: &'a [u8],
}
impl Signer for SoftwareSigner {
type Error = Infallible;
fn platform(&self) -> Platform {
self.platform
}
fn sign(&self, client_data_hash: &[u8; 32]) -> Result<Vec<u8>, Infallible> {
Ok(match self.platform {
Platform::Android => sign_prehash(&self.key, client_data_hash)
.to_der()
.as_bytes()
.to_vec(),
Platform::Ios => {
let authenticator_data = authenticator_data("TEAMID1234.com.worldcoin.test", 1);
let nonce = Sha256::new()
.chain_update(&authenticator_data)
.chain_update(client_data_hash)
.finalize();
let signature = sign_prehash(&self.key, &Sha256::digest(nonce).into()).to_der();
let mut assertion = Vec::new();
ciborium::into_writer(
&Assertion {
signature: signature.as_bytes(),
authenticator_data: &authenticator_data,
},
&mut assertion,
)
.expect("writing CBOR to a Vec cannot fail");
assertion
}
})
}
}
fn sign_prehash(key: &SigningKey, digest: &[u8; 32]) -> Signature {
key.sign_prehash(digest)
.expect("a 32-byte prehash is always signable")
}
#[derive(Debug, Clone)]
pub struct TestIssuer {
pub issuer: String,
pub kid: String,
key: SigningKey,
}
#[derive(Debug, Clone)]
pub struct TestClaims {
pub audience: String,
pub platform: String,
pub device_key: VerifyingKey,
pub expires_at: SystemTime,
pub pass: Option<bool>,
}
impl TestClaims {
#[must_use]
pub fn valid(
audience: &str,
platform: Platform,
device_key: VerifyingKey,
now: SystemTime,
) -> Self {
Self {
audience: audience.to_owned(),
platform: platform.as_str().to_owned(),
device_key,
expires_at: now + Duration::from_secs(3600),
pass: Some(true),
}
}
}
impl TestIssuer {
#[must_use]
pub fn new(issuer: &str) -> Self {
Self {
issuer: issuer.to_owned(),
kid: "test-key-1".to_owned(),
key: test_key(issuer),
}
}
#[must_use]
pub fn keys(&self) -> StaticKeys {
StaticKeys::default().with_key(&self.kid, *self.key.verifying_key())
}
#[must_use]
pub fn jwks(&self) -> serde_json::Value {
let mut jwk = ec_jwk(self.key.verifying_key());
jwk["kid"] = json!(self.kid);
jwk["alg"] = json!("ES256");
jwk["use"] = json!("sig");
json!({ "keys": [jwk] })
}
#[must_use]
pub fn mint(&self, claims: &TestClaims) -> String {
let mut payload = json!({
"iss": self.issuer,
"aud": claims.audience,
"exp": unix_seconds(claims.expires_at),
"platform": claims.platform,
"cnf": { "jwk": ec_jwk(&claims.device_key) },
});
if let Some(pass) = claims.pass {
payload["pass"] = json!(pass);
}
self.sign_jws(
&json!({ "alg": "ES256", "typ": "JWT", "kid": self.kid }),
&payload,
)
}
#[must_use]
pub fn sign_jws(&self, header: &serde_json::Value, payload: &serde_json::Value) -> String {
let signing_input = format!(
"{}.{}",
URL_SAFE_NO_PAD.encode(header.to_string()),
URL_SAFE_NO_PAD.encode(payload.to_string()),
);
let signature: Signature = self.key.sign(signing_input.as_bytes());
format!(
"{signing_input}.{}",
URL_SAFE_NO_PAD.encode(signature.to_bytes())
)
}
}
fn ec_jwk(key: &VerifyingKey) -> serde_json::Value {
let point = key.to_encoded_point(false);
json!({
"kty": "EC",
"crv": "P-256",
"x": URL_SAFE_NO_PAD.encode(point.x().expect("uncompressed point")),
"y": URL_SAFE_NO_PAD.encode(point.y().expect("uncompressed point")),
})
}
fn unix_seconds(time: SystemTime) -> u64 {
time.duration_since(UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_secs())
}
#[derive(Debug, Clone)]
pub struct TestClient {
pub issuer: TestIssuer,
pub signer: SoftwareSigner,
pub audience: String,
pub authority: String,
pub now: SystemTime,
}
impl TestClient {
#[must_use]
pub fn new(platform: Platform, authority: &str) -> Self {
Self {
issuer: TestIssuer::new("https://attestation.example"),
signer: SoftwareSigner::new(test_key("test device"), platform),
audience: "test-audience".to_owned(),
authority: authority.to_owned(),
now: UNIX_EPOCH + Duration::from_secs(1_790_000_000),
}
}
#[must_use]
pub fn token(&self) -> String {
self.issuer.mint(&TestClaims::valid(
&self.audience,
self.signer.platform(),
self.signer.verifying_key(),
self.now,
))
}
#[must_use]
pub fn verifier(&self) -> crate::verify::VerifierBuilder {
let tokens = crate::token::TokenVerifier::new(
[(
self.issuer.issuer.clone(),
std::sync::Arc::new(self.issuer.keys()) as _,
)],
[self.audience.clone()],
)
.expect("issuer and audience are set");
crate::Verifier::builder(tokens, &self.authority)
.clock(std::sync::Arc::new(crate::verify::FixedClock(self.now)))
}
#[must_use]
pub fn sign(&self, method: &str, target: &str, body: &[u8]) -> crate::sign::SignedHeaders {
let (path, query) = target
.split_once('?')
.map_or((target, None), |(path, query)| (path, Some(query)));
let request =
crate::base::CanonicalRequest::new(method, "https", &self.authority, path, query, body)
.expect("valid authority");
let created = i64::try_from(unix_seconds(self.now)).expect("fits");
let nonce = crate::signature::generate_nonce().expect("OS randomness");
crate::sign::sign_request_at(&request, &self.token(), created, &nonce, &self.signer)
.expect("software signing cannot fail")
}
#[must_use]
pub fn request(&self, method: &str, target: &str, body: &[u8]) -> http::Request<Vec<u8>> {
let signed = self.sign(method, target, body);
let mut request = http::Request::builder().method(method).uri(target);
for (name, value) in signed.headers() {
request = request.header(name, value);
}
request.body(body.to_vec()).expect("valid request")
}
}