mod types;
pub use types::{
CoseEncrypt0, CoseSign1, CritHeader, EmptyHeader, EncProtectedHeader, EncStructure,
EncapKeyHeader, HEADER_TIMESTAMP, SigProtectedHeader, SigStructure,
};
use web_time::{SystemTime, UNIX_EPOCH};
use zeroize::{Zeroize, Zeroizing};
use crate::cbor::{self, Decode, Encode, Raw};
use crate::{xdsa, xhpke};
pub const DOMAIN_PREFIX: &[u8] = crate::xhpke::DOMAIN_PREFIX;
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error("cbor: {0}")]
CborError(#[from] cbor::Error),
#[error("unexpected algorithm: have {0}, want {1}")]
UnexpectedAlgorithm(i64, i64),
#[error("unexpected signing key: have {0:x?}, want {1:x?}")]
UnexpectedSigningKey(xdsa::Fingerprint, xdsa::Fingerprint),
#[error("signature verification failed: {0}")]
InvalidSignature(String),
#[error("signature stale: time drift {0}s exceeds max {1}s")]
StaleSignature(u64, u64),
#[error("unexpected payload in detached signature")]
UnexpectedPayload,
#[error("missing payload in embedded signature")]
MissingPayload,
#[error("unexpected encryption key: have {0:x?}, want {1:x?}")]
UnexpectedEncryptionKey(xhpke::Fingerprint, xhpke::Fingerprint),
#[error("invalid encapsulated key size: {0}, expected {1}")]
InvalidEncapKeySize(usize, usize),
#[error("decryption failed: {0}")]
DecryptionFailed(String),
}
pub const ALGORITHM_ID_XDSA: i64 = -70000;
pub const ALGORITHM_ID_XHPKE: i64 = -70001;
pub fn sign_detached<A: Encode>(
msg_to_auth: A,
signer: &xdsa::SecretKey,
domain: &[u8],
) -> Result<Vec<u8>, Error> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before Unix epoch")
.as_secs() as i64;
sign_detached_at(msg_to_auth, signer, domain, timestamp)
}
pub fn sign<E: Encode, A: Encode>(
msg_to_embed: E,
msg_to_auth: A,
signer: &xdsa::SecretKey,
domain: &[u8],
) -> Result<Vec<u8>, Error> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before Unix epoch")
.as_secs() as i64;
sign_at(msg_to_embed, msg_to_auth, signer, domain, timestamp)
}
pub fn sign_detached_at<A: Encode>(
msg_to_auth: A,
signer: &xdsa::SecretKey,
domain: &[u8],
timestamp: i64,
) -> Result<Vec<u8>, Error> {
let info = [DOMAIN_PREFIX, domain].concat();
let aad = cbor::encode(&(&info, msg_to_auth))?;
let protected = cbor::encode(&SigProtectedHeader {
algorithm: ALGORITHM_ID_XDSA,
crit: CritHeader {
timestamp: HEADER_TIMESTAMP,
},
kid: signer.fingerprint(),
timestamp,
})?;
let signature = signer.sign(&cbor::encode(SigStructure {
context: "Signature1",
protected: &protected,
external_aad: &aad,
payload: &[],
})?);
Ok(cbor::encode(&CoseSign1 {
protected,
unprotected: EmptyHeader {},
payload: None,
signature,
})?)
}
pub fn sign_at<E: Encode, A: Encode>(
msg_to_embed: E,
msg_to_auth: A,
signer: &xdsa::SecretKey,
domain: &[u8],
timestamp: i64,
) -> Result<Vec<u8>, Error> {
let mut msg_to_embed = Zeroizing::new(cbor::encode(msg_to_embed)?);
let info = [DOMAIN_PREFIX, domain].concat();
let aad = cbor::encode(&(&info, msg_to_auth))?;
let protected = cbor::encode(&SigProtectedHeader {
algorithm: ALGORITHM_ID_XDSA,
crit: CritHeader {
timestamp: HEADER_TIMESTAMP,
},
kid: signer.fingerprint(),
timestamp,
})?;
let blob = encode_wiped(
SigStructure {
context: "Signature1",
protected: &protected,
external_aad: &aad,
payload: &msg_to_embed,
},
protected.len() + aad.len() + msg_to_embed.len(),
)?;
let signature = signer.sign(&blob);
let len = protected.len() + msg_to_embed.len() + xdsa::SIGNATURE_SIZE;
let mut sign1 = CoseSign1 {
protected,
unprotected: EmptyHeader {},
payload: Some(std::mem::take(&mut *msg_to_embed)),
signature,
};
let encoded = encode_wiped(&sign1, len);
sign1.payload.zeroize();
Ok(std::mem::take(&mut *encoded?))
}
pub fn verify_detached<A: Encode>(
msg_to_check: &[u8],
msg_to_auth: A,
verifier: &xdsa::PublicKey,
domain: &[u8],
max_drift: Option<u64>,
) -> Result<(), Error> {
let now = match max_drift {
Some(_) => SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before Unix epoch")
.as_secs() as i64,
None => 0,
};
verify_detached_at(msg_to_check, msg_to_auth, verifier, domain, max_drift, now)
}
pub fn verify_detached_at<A: Encode>(
msg_to_check: &[u8],
msg_to_auth: A,
verifier: &xdsa::PublicKey,
domain: &[u8],
max_drift: Option<u64>,
now: i64,
) -> Result<(), Error> {
let info = [DOMAIN_PREFIX, domain].concat();
let aad = cbor::encode(&(&info, msg_to_auth))?;
let sign1: CoseSign1 = cbor::decode(msg_to_check)?;
if sign1.payload.is_some() {
return Err(Error::UnexpectedPayload);
}
let header = verify_sig_protected_header(&sign1.protected, ALGORITHM_ID_XDSA, verifier)?;
if let Some(max) = max_drift {
let drift = now.abs_diff(header.timestamp);
if drift > max {
return Err(Error::StaleSignature(drift, max));
}
}
let blob = cbor::encode(SigStructure {
context: "Signature1",
protected: &sign1.protected,
external_aad: &aad,
payload: &[],
})?;
verifier
.verify(&blob, &sign1.signature)
.map_err(|e| Error::InvalidSignature(e.to_string()))?;
Ok(())
}
pub fn verify<E: Decode, A: Encode>(
msg_to_check: &[u8],
msg_to_auth: A,
verifier: &xdsa::PublicKey,
domain: &[u8],
max_drift: Option<u64>,
) -> Result<E, Error> {
let now = match max_drift {
Some(_) => SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before Unix epoch")
.as_secs() as i64,
None => 0,
};
verify_at(msg_to_check, msg_to_auth, verifier, domain, max_drift, now)
}
pub fn verify_at<E: Decode, A: Encode>(
msg_to_check: &[u8],
msg_to_auth: A,
verifier: &xdsa::PublicKey,
domain: &[u8],
max_drift: Option<u64>,
now: i64,
) -> Result<E, Error> {
let info = [DOMAIN_PREFIX, domain].concat();
let aad = cbor::encode(&(&info, msg_to_auth))?;
let sign1: CoseSign1 = cbor::decode(msg_to_check)?;
let payload = Zeroizing::new(sign1.payload.ok_or(Error::MissingPayload)?);
let header = verify_sig_protected_header(&sign1.protected, ALGORITHM_ID_XDSA, verifier)?;
if let Some(max) = max_drift {
let drift = now.abs_diff(header.timestamp);
if drift > max {
return Err(Error::StaleSignature(drift, max));
}
}
let blob = encode_wiped(
SigStructure {
context: "Signature1",
protected: &sign1.protected,
external_aad: &aad,
payload: &payload,
},
sign1.protected.len() + aad.len() + payload.len(),
)?;
verifier
.verify(&blob, &sign1.signature)
.map_err(|e| Error::InvalidSignature(e.to_string()))?;
Ok(cbor::decode(&payload)?)
}
pub fn signer(signature: &[u8]) -> Result<xdsa::Fingerprint, Error> {
let sign1: CoseSign1 = cbor::decode(signature)?;
drop(Zeroizing::new(sign1.payload));
let header: SigProtectedHeader = cbor::decode(&sign1.protected)?;
Ok(header.kid)
}
pub fn peek<E: Decode>(signature: &[u8]) -> Result<E, Error> {
let sign1: CoseSign1 = cbor::decode(signature)?;
let payload = Zeroizing::new(sign1.payload.ok_or(Error::MissingPayload)?);
Ok(cbor::decode(&payload)?)
}
pub fn seal<E: Encode, A: Encode>(
msg_to_seal: E,
msg_to_auth: A,
signer: &xdsa::SecretKey,
recipient: &xhpke::PublicKey,
domain: &[u8],
) -> Result<Vec<u8>, Error> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before Unix epoch")
.as_secs() as i64;
seal_at(
msg_to_seal,
msg_to_auth,
signer,
recipient,
domain,
timestamp,
)
}
pub fn seal_at<E: Encode, A: Encode>(
msg_to_seal: E,
msg_to_auth: A,
signer: &xdsa::SecretKey,
recipient: &xhpke::PublicKey,
domain: &[u8],
timestamp: i64,
) -> Result<Vec<u8>, Error> {
let msg_to_auth = cbor::encode(msg_to_auth)?;
let signed = Zeroizing::new(sign_at(
msg_to_seal,
Raw(msg_to_auth.clone()),
signer,
domain,
timestamp,
)?);
encrypt(&signed, Raw(msg_to_auth), recipient, domain)
}
pub fn encrypt<A: Encode>(
sign1: &[u8],
msg_to_auth: A,
recipient: &xhpke::PublicKey,
domain: &[u8],
) -> Result<Vec<u8>, Error> {
let msg_to_auth = cbor::encode(msg_to_auth)?;
let protected = cbor::encode(&EncProtectedHeader {
algorithm: ALGORITHM_ID_XHPKE,
kid: recipient.fingerprint(),
})?;
let (encap_key, ciphertext) = recipient
.seal(
sign1,
&cbor::encode(EncStructure {
context: "Encrypt0",
protected: &protected,
external_aad: &msg_to_auth,
})?,
domain,
)
.map_err(|e| Error::DecryptionFailed(e.to_string()))?;
Ok(cbor::encode(&CoseEncrypt0 {
protected,
unprotected: EncapKeyHeader {
encap_key: encap_key.to_vec(),
},
ciphertext,
})?)
}
pub fn open<E: Decode, A: Encode + Clone>(
msg_to_open: &[u8],
msg_to_auth: A,
recipient: &xhpke::SecretKey,
sender: &xdsa::PublicKey,
domain: &[u8],
max_drift: Option<u64>,
) -> Result<E, Error> {
let now = match max_drift {
Some(_) => SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before Unix epoch")
.as_secs() as i64,
None => 0,
};
open_at(
msg_to_open,
msg_to_auth,
recipient,
sender,
domain,
max_drift,
now,
)
}
pub fn open_at<E: Decode, A: Encode + Clone>(
msg_to_open: &[u8],
msg_to_auth: A,
recipient: &xhpke::SecretKey,
sender: &xdsa::PublicKey,
domain: &[u8],
max_drift: Option<u64>,
now: i64,
) -> Result<E, Error> {
let sign1 = Zeroizing::new(decrypt(
msg_to_open,
msg_to_auth.clone(),
recipient,
domain,
)?);
let raw: Raw = verify_at::<Raw, _>(&sign1, &msg_to_auth, sender, domain, max_drift, now)?;
let payload = Zeroizing::new(raw.0);
Ok(cbor::decode(&payload)?)
}
pub fn decrypt<A: Encode>(
msg_to_open: &[u8],
msg_to_auth: A,
recipient: &xhpke::SecretKey,
domain: &[u8],
) -> Result<Vec<u8>, Error> {
let msg_to_auth = cbor::encode(msg_to_auth)?;
let encrypt0: CoseEncrypt0 = cbor::decode(msg_to_open)?;
verify_enc_protected_header(&encrypt0.protected, ALGORITHM_ID_XHPKE, recipient)?;
let encap_key: &[u8; xhpke::ENCAP_KEY_SIZE] = encrypt0
.unprotected
.encap_key
.as_slice()
.try_into()
.map_err(|_| {
Error::InvalidEncapKeySize(encrypt0.unprotected.encap_key.len(), xhpke::ENCAP_KEY_SIZE)
})?;
let decrypted = recipient
.open(
encap_key,
&encrypt0.ciphertext,
&cbor::encode(EncStructure {
context: "Encrypt0",
protected: &encrypt0.protected,
external_aad: &msg_to_auth,
})?,
domain,
)
.map_err(|e| Error::DecryptionFailed(e.to_string()))?;
Ok(decrypted)
}
pub fn recipient(ciphertext: &[u8]) -> Result<xhpke::Fingerprint, Error> {
let encrypt0: CoseEncrypt0 = cbor::decode(ciphertext)?;
let header: EncProtectedHeader = cbor::decode(&encrypt0.protected)?;
Ok(header.kid)
}
fn verify_sig_protected_header(
bytes: &[u8],
exp_algo: i64,
verifier: &xdsa::PublicKey,
) -> Result<SigProtectedHeader, Error> {
let header: SigProtectedHeader = cbor::decode(bytes)?;
if header.algorithm != exp_algo {
return Err(Error::UnexpectedAlgorithm(header.algorithm, exp_algo));
}
if header.crit.timestamp != HEADER_TIMESTAMP {
return Err(Error::UnexpectedAlgorithm(
header.crit.timestamp,
HEADER_TIMESTAMP,
));
}
if header.kid != verifier.fingerprint() {
return Err(Error::UnexpectedSigningKey(
header.kid,
verifier.fingerprint(),
));
}
Ok(header)
}
fn verify_enc_protected_header(
bytes: &[u8],
exp_algo: i64,
recipient: &xhpke::SecretKey,
) -> Result<EncProtectedHeader, Error> {
let header: EncProtectedHeader = cbor::decode(bytes)?;
if header.algorithm != exp_algo {
return Err(Error::UnexpectedAlgorithm(header.algorithm, exp_algo));
}
if header.kid != recipient.fingerprint() {
return Err(Error::UnexpectedEncryptionKey(
header.kid,
recipient.fingerprint(),
));
}
Ok(header)
}
const FRAMING_OVERHEAD: usize = 64;
fn encode_wiped<T: Encode>(value: T, len: usize) -> Result<Zeroizing<Vec<u8>>, Error> {
let mut buf = Zeroizing::new(Vec::with_capacity(len + FRAMING_OVERHEAD));
value.encode_cbor_to(&mut buf)?;
debug_assert!(buf.len() <= len + FRAMING_OVERHEAD);
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sign_verify() {
struct TestCase {
msg_to_sign: &'static [u8],
msg_to_auth: &'static [u8],
verifier_msg_to_auth: &'static [u8],
domain: &'static [u8],
verifier_domain: &'static [u8],
timestamp: Option<i64>,
max_drift: Option<u64>,
wrong_key: bool,
want_ok: bool,
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let tests = [
TestCase {
msg_to_sign: b"foo",
msg_to_auth: b"bar",
verifier_msg_to_auth: b"bar",
domain: b"baz",
verifier_domain: b"baz",
timestamp: None,
max_drift: None,
wrong_key: false,
want_ok: true,
},
TestCase {
msg_to_sign: b"foo",
msg_to_auth: b"",
verifier_msg_to_auth: b"",
domain: b"baz",
verifier_domain: b"baz",
timestamp: None,
max_drift: None,
wrong_key: false,
want_ok: true,
},
TestCase {
msg_to_sign: b"foo",
msg_to_auth: b"bar",
verifier_msg_to_auth: b"bar",
domain: b"baz",
verifier_domain: b"baz",
timestamp: Some(now),
max_drift: None,
wrong_key: false,
want_ok: true,
},
TestCase {
msg_to_sign: b"foo",
msg_to_auth: b"bar",
verifier_msg_to_auth: b"bar",
domain: b"baz",
verifier_domain: b"baz",
timestamp: Some(now - 30),
max_drift: Some(60),
wrong_key: false,
want_ok: true,
},
TestCase {
msg_to_sign: b"foo",
msg_to_auth: b"bar",
verifier_msg_to_auth: b"bar",
domain: b"baz",
verifier_domain: b"baz",
timestamp: Some(now - 120),
max_drift: Some(60),
wrong_key: false,
want_ok: false,
},
TestCase {
msg_to_sign: b"foo",
msg_to_auth: b"bar",
verifier_msg_to_auth: b"bar",
domain: b"baz",
verifier_domain: b"baz",
timestamp: Some(now + 120),
max_drift: Some(60),
wrong_key: false,
want_ok: false,
},
TestCase {
msg_to_sign: b"foo",
msg_to_auth: b"bar",
verifier_msg_to_auth: b"bar",
domain: b"baz",
verifier_domain: b"baz2",
timestamp: Some(now + 120),
max_drift: Some(60),
wrong_key: false,
want_ok: false,
},
TestCase {
msg_to_sign: b"foo",
msg_to_auth: b"bar",
verifier_msg_to_auth: b"bar2",
domain: b"baz",
verifier_domain: b"baz",
timestamp: None,
max_drift: None,
wrong_key: false,
want_ok: false,
},
TestCase {
msg_to_sign: b"foo",
msg_to_auth: b"",
verifier_msg_to_auth: b"",
domain: b"baz",
verifier_domain: b"baz",
timestamp: None,
max_drift: None,
wrong_key: true,
want_ok: false,
},
];
for (i, test) in tests.iter().enumerate() {
let alice = xdsa::SecretKey::generate();
let bobby = xdsa::SecretKey::generate();
let signed = match test.timestamp {
Some(ts) => sign_at(
test.msg_to_sign.to_vec(),
test.msg_to_auth.to_vec(),
&alice,
test.domain,
ts,
),
None => sign(
test.msg_to_sign.to_vec(),
test.msg_to_auth.to_vec(),
&alice,
test.domain,
),
};
let verifier = if test.wrong_key {
bobby.public_key()
} else {
alice.public_key()
};
let result: Result<Vec<u8>, _> = verify(
&signed.unwrap(),
test.verifier_msg_to_auth.to_vec(),
&verifier,
test.verifier_domain,
test.max_drift,
);
if test.want_ok {
let recovered = result.unwrap_or_else(|_| panic!("test {}: expected success", i));
assert_eq!(recovered, test.msg_to_sign, "test {}: payload mismatch", i);
} else {
assert!(result.is_err(), "test {}: expected error", i);
}
}
}
#[test]
fn test_drift_range() {
let signer = xdsa::SecretKey::generate();
let tests: [(&str, i64, i64, u64, bool); 5] = [
(
"wrapping distance",
-9223372036854775740,
9223372036854775683,
209,
false,
),
("full range within max", i64::MIN, i64::MAX, u64::MAX, true),
(
"full range over max",
i64::MIN,
i64::MAX,
u64::MAX - 1,
false,
),
("near the maximum", i64::MAX - 5, i64::MAX, 5, true),
("near the minimum", i64::MIN + 5, i64::MIN, 5, true),
];
for (name, timestamp, now, max_drift, want_ok) in tests {
let signed = sign_at(
b"payload".as_slice(),
b"".as_slice(),
&signer,
b"",
timestamp,
)
.unwrap();
let result = verify_at::<Vec<u8>, _>(
&signed,
b"".as_slice(),
&signer.public_key(),
b"",
Some(max_drift),
now,
);
assert_eq!(result.is_ok(), want_ok, "{name}");
assert!(
want_ok || matches!(result, Err(Error::StaleSignature(..))),
"{name}"
);
let signed = sign_detached_at(b"".as_slice(), &signer, b"", timestamp).unwrap();
let result = verify_detached_at(
&signed,
b"".as_slice(),
&signer.public_key(),
b"",
Some(max_drift),
now,
);
assert_eq!(result.is_ok(), want_ok, "{name}");
assert!(
want_ok || matches!(result, Err(Error::StaleSignature(..))),
"{name}"
);
}
}
#[test]
fn test_seal_open() {
struct TestCase {
msg_to_seal: &'static [u8],
msg_to_auth: &'static [u8],
opener_msg_to_auth: &'static [u8],
domain: &'static [u8],
opener_domain: &'static [u8],
timestamp: Option<i64>,
max_drift: Option<u64>,
wrong_signer: bool,
want_ok: bool,
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let tests = [
TestCase {
msg_to_seal: b"foo",
msg_to_auth: b"bar",
opener_msg_to_auth: b"bar",
domain: b"baz",
opener_domain: b"baz",
timestamp: None,
max_drift: None,
wrong_signer: false,
want_ok: true,
},
TestCase {
msg_to_seal: b"foo",
msg_to_auth: b"",
opener_msg_to_auth: b"",
domain: b"baz",
opener_domain: b"baz",
timestamp: None,
max_drift: None,
wrong_signer: false,
want_ok: true,
},
TestCase {
msg_to_seal: b"foo",
msg_to_auth: b"bar",
opener_msg_to_auth: b"bar",
domain: b"baz",
opener_domain: b"baz",
timestamp: Some(now),
max_drift: None,
wrong_signer: false,
want_ok: true,
},
TestCase {
msg_to_seal: b"foo",
msg_to_auth: b"bar",
opener_msg_to_auth: b"bar",
domain: b"baz",
opener_domain: b"baz",
timestamp: Some(now - 30),
max_drift: Some(60),
wrong_signer: false,
want_ok: true,
},
TestCase {
msg_to_seal: b"foo",
msg_to_auth: b"",
opener_msg_to_auth: b"",
domain: b"baz",
opener_domain: b"baz2",
timestamp: None,
max_drift: None,
wrong_signer: false,
want_ok: false,
},
TestCase {
msg_to_seal: b"foo",
msg_to_auth: b"bar",
opener_msg_to_auth: b"bar2",
domain: b"baz",
opener_domain: b"baz",
timestamp: None,
max_drift: None,
wrong_signer: false,
want_ok: false,
},
TestCase {
msg_to_seal: b"foo",
msg_to_auth: b"",
opener_msg_to_auth: b"",
domain: b"baz",
opener_domain: b"baz",
timestamp: None,
max_drift: None,
wrong_signer: true,
want_ok: false,
},
TestCase {
msg_to_seal: b"foo",
msg_to_auth: b"bar",
opener_msg_to_auth: b"bar",
domain: b"baz",
opener_domain: b"baz",
timestamp: Some(now - 120),
max_drift: Some(60),
wrong_signer: false,
want_ok: false,
},
TestCase {
msg_to_seal: b"foo",
msg_to_auth: b"bar",
opener_msg_to_auth: b"bar",
domain: b"baz",
opener_domain: b"baz",
timestamp: Some(now + 120),
max_drift: Some(60),
wrong_signer: false,
want_ok: false,
},
];
for (i, test) in tests.iter().enumerate() {
let alice = xdsa::SecretKey::generate();
let bobby = xdsa::SecretKey::generate();
let carol = xhpke::SecretKey::generate();
let sealed = match test.timestamp {
Some(ts) => seal_at(
test.msg_to_seal.to_vec(),
test.msg_to_auth.to_vec(),
&alice,
&carol.public_key(),
test.domain,
ts,
)
.unwrap(),
None => seal(
test.msg_to_seal.to_vec(),
test.msg_to_auth.to_vec(),
&alice,
&carol.public_key(),
test.domain,
)
.unwrap(),
};
let verifier = if test.wrong_signer {
bobby.public_key()
} else {
alice.public_key()
};
let result: Result<Vec<u8>, _> = open(
&sealed,
&test.opener_msg_to_auth.to_vec(),
&carol,
&verifier,
test.opener_domain,
test.max_drift,
);
if test.want_ok {
let recovered = result.unwrap_or_else(|_| panic!("test {}: expected success", i));
assert_eq!(recovered, test.msg_to_seal, "test {}: payload mismatch", i);
} else {
assert!(result.is_err(), "test {}: expected error", i);
}
}
}
#[test]
fn test_sign_verify_typed() {
let alice = xdsa::SecretKey::generate();
let payload = (42u64, "foo".to_string());
let aad = ("bar".to_string(),);
let signed = sign(&payload, &aad, &alice, b"baz").unwrap();
let recovered: (u64, String) =
verify(&signed, &aad, &alice.public_key(), b"baz", None).unwrap();
assert_eq!(recovered, payload);
}
#[test]
fn test_seal_open_typed() {
let alice = xdsa::SecretKey::generate();
let carol = xhpke::SecretKey::generate();
let payload = (123u64, "foo".to_string());
let aad = ("bar".to_string(),);
let sealed = seal(&payload, &aad, &alice, &carol.public_key(), b"baz").unwrap();
let recovered: (u64, String) =
open(&sealed, &aad, &carol, &alice.public_key(), b"baz", None).unwrap();
assert_eq!(recovered, payload);
}
#[test]
fn test_signer_peek() {
let alice = xdsa::SecretKey::generate();
let signed = sign_at(b"foo".as_slice(), b"bar".as_slice(), &alice, b"baz", 0).unwrap();
assert_eq!(signer(&signed).unwrap(), alice.fingerprint());
assert_eq!(peek::<Vec<u8>>(&signed).unwrap(), b"foo");
let truncated = &signed[..signed.len() - 1];
let trailing = [signed.as_slice(), &[0]].concat();
for input in [truncated, &trailing] {
assert!(signer(input).is_err());
assert!(peek::<Vec<u8>>(input).is_err());
}
let signed = sign_detached_at(b"bar".as_slice(), &alice, b"baz", 0).unwrap();
assert_eq!(signer(&signed).unwrap(), alice.fingerprint());
assert!(matches!(
peek::<Vec<u8>>(&signed),
Err(Error::MissingPayload)
));
}
const FIXTURES: &str = include_str!("testdata/v0.16.json");
fn fixture(corpus: &serde_json::Value, key: &str) -> Vec<u8> {
hex::decode(corpus[key].as_str().unwrap()).unwrap()
}
#[test]
fn test_v0_16_fixtures() {
let corpus: serde_json::Value = serde_json::from_str(FIXTURES).unwrap();
let xdsa_seed: [u8; 64] = fixture(&corpus, "xdsa_seed").try_into().unwrap();
let xhpke_seed: [u8; 32] = fixture(&corpus, "xhpke_seed").try_into().unwrap();
let signer = xdsa::SecretKey::from_bytes(&xdsa_seed);
let recipient = xhpke::SecretKey::from_bytes(&xhpke_seed);
let domain = fixture(&corpus, "domain");
let payload = fixture(&corpus, "payload");
let aad = fixture(&corpus, "aad");
let sign1 = fixture(&corpus, "sign1");
let encrypt0 = fixture(&corpus, "encrypt0");
let got: Vec<u8> = verify_at(
&sign1,
aad.as_slice(),
&signer.public_key(),
&domain,
None,
0,
)
.unwrap();
assert_eq!(got, payload);
assert_eq!(super::signer(&sign1).unwrap(), signer.fingerprint());
assert_eq!(peek::<Vec<u8>>(&sign1).unwrap(), payload);
let bad = verify_at::<Vec<u8>, _>(
&sign1,
aad.as_slice(),
&signer.public_key(),
b"wrong",
None,
0,
);
assert!(bad.is_err());
let mut tampered = sign1.clone();
*tampered.last_mut().unwrap() ^= 1;
let bad = verify_at::<Vec<u8>, _>(
&tampered,
aad.as_slice(),
&signer.public_key(),
&domain,
None,
0,
);
assert!(bad.is_err());
let got: Vec<u8> = open_at(
&encrypt0,
aad.as_slice(),
&recipient,
&signer.public_key(),
&domain,
None,
0,
)
.unwrap();
assert_eq!(got, payload);
let mut tampered = encrypt0.clone();
*tampered.last_mut().unwrap() ^= 1;
let bad = open_at::<Vec<u8>, _>(
&tampered,
aad.as_slice(),
&recipient,
&signer.public_key(),
&domain,
None,
0,
);
assert!(bad.is_err());
}
}