mod types;
pub use types::{
CoseEncrypt0, CoseSign1, CritHeader, EmptyHeader, EncProtectedHeader, EncStructure,
EncapKeyHeader, HEADER_TIMESTAMP, SigProtectedHeader, SigStructure,
};
use web_time::{SystemTime, UNIX_EPOCH};
use crate::cbor::{self, Decode, Encode, Raw};
use crate::{xdsa, xhpke};
pub const DOMAIN_PREFIX: &[u8] = b"dark-bio-v1:";
#[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],
) -> Vec<u8> {
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],
) -> Vec<u8> {
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,
) -> Vec<u8> {
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(
&SigStructure {
context: "Signature1",
protected: &protected,
external_aad: &aad,
payload: &[],
}
.encode_cbor(),
);
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,
) -> Vec<u8> {
let msg_to_embed = 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 signature = signer.sign(
&SigStructure {
context: "Signature1",
protected: &protected,
external_aad: &aad,
payload: &msg_to_embed,
}
.encode_cbor(),
);
cbor::encode(&CoseSign1 {
protected,
unprotected: EmptyHeader {},
payload: Some(msg_to_embed),
signature,
})
}
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 = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before Unix epoch")
.as_secs() as i64;
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 - header.timestamp).unsigned_abs();
if drift > max {
return Err(Error::StaleSignature(drift, max));
}
}
let blob = SigStructure {
context: "Signature1",
protected: &sign1.protected,
external_aad: &aad,
payload: &[],
}
.encode_cbor();
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 = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before Unix epoch")
.as_secs() as i64;
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 = 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 - header.timestamp).unsigned_abs();
if drift > max {
return Err(Error::StaleSignature(drift, max));
}
}
let blob = SigStructure {
context: "Signature1",
protected: &sign1.protected,
external_aad: &aad,
payload: &payload,
}
.encode_cbor();
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)?;
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 = 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_seal = cbor::encode(msg_to_seal);
let msg_to_auth = cbor::encode(msg_to_auth);
let signed = sign_at(
Raw(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 info = [DOMAIN_PREFIX, domain].concat();
let (encap_key, ciphertext) = recipient
.seal(
sign1,
&EncStructure {
context: "Encrypt0",
protected: &protected,
external_aad: &msg_to_auth,
}
.encode_cbor(),
&info,
)
.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 = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before Unix epoch")
.as_secs() as i64;
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 = 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)?;
Ok(cbor::decode(&raw.0)?)
}
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 info = [DOMAIN_PREFIX, domain].concat();
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,
&EncStructure {
context: "Encrypt0",
protected: &encrypt0.protected,
external_aad: &msg_to_auth,
}
.encode_cbor(),
&info,
)
.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)
}
#[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,
&test.verifier_msg_to_auth.to_vec(),
&verifier,
test.verifier_domain,
test.max_drift,
);
if test.want_ok {
let recovered = result.expect(&format!("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_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.expect(&format!("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");
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);
}
}