use alloc::vec::Vec;
use crate::artifacts::{SigningKeyExt, VerifyingKey};
use crate::identity::Policy;
use ibs::gg::{Signature, Signer, Verifier};
use rand::{CryptoRng, RngCore};
pub const CHALLENGE_DOMAIN: &[u8] = b"postguard/challenge/v1";
fn challenge_message(context: &str, challenge: &[u8]) -> Vec<u8> {
let mut msg = Vec::with_capacity(
CHALLENGE_DOMAIN.len() + 2 * core::mem::size_of::<u64>() + context.len() + challenge.len(),
);
msg.extend_from_slice(CHALLENGE_DOMAIN);
msg.extend_from_slice(&(context.len() as u64).to_be_bytes());
msg.extend_from_slice(context.as_bytes());
msg.extend_from_slice(&(challenge.len() as u64).to_be_bytes());
msg.extend_from_slice(challenge);
msg
}
pub fn sign_challenge<R: RngCore + CryptoRng>(
key: &SigningKeyExt,
context: &str,
challenge: &[u8],
rng: &mut R,
) -> Signature {
Signer::new()
.chain(challenge_message(context, challenge))
.sign(&key.key.0, rng)
}
pub fn verify_challenge(
vk: &VerifyingKey,
pol: &Policy,
context: &str,
challenge: &[u8],
sig: &Signature,
) -> bool {
let Ok(id) = pol.derive_ibs() else {
return false;
};
Verifier::default()
.chain(challenge_message(context, challenge))
.verify(&vk.0, sig, &id)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::Attribute;
use crate::test::TestSetup;
use alloc::vec;
const CONTEXT: &str = "cryptify/upload";
const CHALLENGE: &[u8] = b"a verifier-chosen challenge";
#[test]
fn test_challenge_roundtrip() {
let mut rng = rand::thread_rng();
let setup = TestSetup::new(&mut rng);
let key = &setup.signing_keys[0];
let sig = sign_challenge(key, CONTEXT, CHALLENGE, &mut rng);
assert!(verify_challenge(
&setup.ibs_pk,
&key.policy,
CONTEXT,
CHALLENGE,
&sig
));
}
#[test]
fn test_challenge_wrong_identity_fails() {
let mut rng = rand::thread_rng();
let setup = TestSetup::new(&mut rng);
let key_a = &setup.signing_keys[0];
let pol_b = &setup.signing_keys[1].policy;
let sig = sign_challenge(key_a, CONTEXT, CHALLENGE, &mut rng);
assert!(!verify_challenge(
&setup.ibs_pk,
pol_b,
CONTEXT,
CHALLENGE,
&sig
));
}
#[test]
fn test_challenge_wrong_challenge_fails() {
let mut rng = rand::thread_rng();
let setup = TestSetup::new(&mut rng);
let key = &setup.signing_keys[0];
let sig = sign_challenge(key, CONTEXT, b"challenge X", &mut rng);
assert!(!verify_challenge(
&setup.ibs_pk,
&key.policy,
CONTEXT,
b"challenge Y",
&sig
));
}
#[test]
fn test_challenge_wrong_context_fails() {
let mut rng = rand::thread_rng();
let setup = TestSetup::new(&mut rng);
let key = &setup.signing_keys[0];
let sig = sign_challenge(key, "a", CHALLENGE, &mut rng);
assert!(!verify_challenge(
&setup.ibs_pk,
&key.policy,
"b",
CHALLENGE,
&sig
));
}
#[test]
fn test_challenge_split_is_unambiguous() {
let mut rng = rand::thread_rng();
let setup = TestSetup::new(&mut rng);
let key = &setup.signing_keys[0];
let sig = sign_challenge(key, "ab", b"c", &mut rng);
assert!(!verify_challenge(
&setup.ibs_pk,
&key.policy,
"a",
b"bc",
&sig
));
assert_ne!(challenge_message("ab", b"c"), challenge_message("a", b"bc"));
}
#[test]
fn test_challenge_domain_separates_from_undomained_signature() {
let mut rng = rand::thread_rng();
let setup = TestSetup::new(&mut rng);
let key = &setup.signing_keys[0];
let sig = Signer::new().chain(CHALLENGE).sign(&key.key.0, &mut rng);
assert!(!verify_challenge(
&setup.ibs_pk,
&key.policy,
CONTEXT,
CHALLENGE,
&sig
));
}
#[test]
fn test_challenge_verifies_under_non_canonical_policy() {
let mut rng = rand::thread_rng();
let setup = TestSetup::new(&mut rng);
let key = &setup.signing_keys[0];
let non_canonical = Policy {
timestamp: key.policy.timestamp,
con: vec![Attribute::new(
"pbdf.sidn-pbdf.email.email",
Some("Alice@Example.COM"),
)],
};
assert_ne!(non_canonical, key.policy);
let sig = sign_challenge(key, CONTEXT, CHALLENGE, &mut rng);
assert!(verify_challenge(
&setup.ibs_pk,
&non_canonical,
CONTEXT,
CHALLENGE,
&sig
));
}
}