use aes_gcm::aead::{Aead, KeyInit, Payload};
use aes_gcm::{Aes256Gcm, Nonce};
use ciborium::value::{Integer, Value};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use crate::model;
use crate::wire;
const ALG: i64 = 1;
const KID: i64 = 4;
const IV: i64 = 5;
const ALG_EDDSA: i64 = -8;
const ALG_A256GCM: i64 = 3;
const TAG_SIGN1: u64 = 18;
const TAG_ENCRYPT0: u64 = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SigStatus {
Valid,
Invalid,
Unverified,
}
fn protected_header() -> Vec<u8> {
wire::encode(&Value::Map(vec![(
Value::Integer(Integer::from(ALG)),
Value::Integer(Integer::from(ALG_EDDSA)),
)]))
}
fn sig_structure(protected: &[u8], frame_id: &[u8]) -> Vec<u8> {
wire::encode(&Value::Array(vec![
Value::Text("Signature1".to_string()),
Value::Bytes(protected.to_vec()),
Value::Bytes(Vec::new()),
Value::Bytes(frame_id.to_vec()),
]))
}
pub fn sign_id(frame_id: &[u8], signing_key: &SigningKey, kid: &str) -> Vec<u8> {
let protected = protected_header();
let signature: Signature = signing_key.sign(&sig_structure(&protected, frame_id));
let cose = Value::Tag(
TAG_SIGN1,
Box::new(Value::Array(vec![
Value::Bytes(protected),
Value::Map(vec![(
Value::Integer(Integer::from(KID)),
Value::Bytes(kid.as_bytes().to_vec()),
)]),
Value::Null,
Value::Bytes(signature.to_bytes().to_vec()),
])),
);
wire::encode(&cose)
}
pub fn parse(sig: &[u8]) -> Option<(String, Vec<u8>, [u8; 64])> {
let value: Value = ciborium::de::from_reader(sig).ok()?;
let body = match value {
Value::Tag(_, inner) => *inner,
other => other,
};
let array = body.as_array()?;
if array.len() != 4 {
return None;
}
let protected = array[0].as_bytes()?.clone();
let unprotected = array[1].as_map()?;
let signature: [u8; 64] = array[3].as_bytes()?.as_slice().try_into().ok()?;
let kid_target = Integer::from(KID);
let kid = unprotected.iter().find_map(|(k, v)| match (k, v) {
(Value::Integer(i), Value::Bytes(b)) if *i == kid_target => {
String::from_utf8(b.clone()).ok()
}
_ => None,
})?;
Some((kid, protected, signature))
}
pub fn signature_kid(sig: &[u8]) -> Option<String> {
parse(sig).map(|(kid, _, _)| kid)
}
pub fn verify_sig(sig: &[u8], frame_id: &[u8], public: &VerifyingKey) -> SigStatus {
let Some((_kid, protected, signature)) = parse(sig) else {
return SigStatus::Invalid;
};
let signature = Signature::from_bytes(&signature);
match public.verify(&sig_structure(&protected, frame_id), &signature) {
Ok(()) => SigStatus::Valid,
Err(_) => SigStatus::Invalid,
}
}
pub fn verify_signatures(
signatures: &mut [model::Signature],
resolve: impl Fn(&str) -> Option<VerifyingKey>,
) {
for sig in signatures.iter_mut() {
let Some(cose) = sig.cose.clone() else {
continue;
};
let kid = signature_kid(&cose);
sig.kid.clone_from(&kid);
sig.status = match kid.as_deref().and_then(&resolve) {
Some(key) => match verify_sig(&cose, &sig.frame_id, &key) {
SigStatus::Valid => "valid",
_ => "invalid",
},
None => "unverified",
}
.to_string();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Encrypt0Error {
Malformed,
MissingKey,
AuthFailed,
}
impl std::fmt::Display for Encrypt0Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = match self {
Self::Malformed => "malformed COSE_Encrypt0",
Self::MissingKey => "no content key for recipient",
Self::AuthFailed => "authentication failed (AES-GCM tag mismatch)",
};
f.write_str(msg)
}
}
impl std::error::Error for Encrypt0Error {}
fn encrypt0_protected() -> Vec<u8> {
wire::encode(&Value::Map(vec![(
Value::Integer(Integer::from(ALG)),
Value::Integer(Integer::from(ALG_A256GCM)),
)]))
}
fn enc_structure(protected: &[u8]) -> Vec<u8> {
wire::encode(&Value::Array(vec![
Value::Text("Encrypt0".to_string()),
Value::Bytes(protected.to_vec()),
Value::Bytes(Vec::new()),
]))
}
pub fn encrypt0_with_iv(plaintext: &[u8], kid: &str, key: &[u8; 32], iv: &[u8; 12]) -> Vec<u8> {
let protected = encrypt0_protected();
let aad = enc_structure(&protected);
let cipher = Aes256Gcm::new(key.into());
let ciphertext = cipher
.encrypt(
Nonce::from_slice(iv),
Payload {
msg: plaintext,
aad: &aad,
},
)
.expect("AES-256-GCM encryption cannot fail for a valid key/nonce");
let cose = Value::Tag(
TAG_ENCRYPT0,
Box::new(Value::Array(vec![
Value::Bytes(protected),
Value::Map(vec![
(
Value::Integer(Integer::from(KID)),
Value::Bytes(kid.as_bytes().to_vec()),
),
(Value::Integer(Integer::from(IV)), Value::Bytes(iv.to_vec())),
]),
Value::Bytes(ciphertext),
])),
);
wire::canonical(&cose)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn encrypt0(plaintext: &[u8], kid: &str, key: &[u8; 32]) -> Vec<u8> {
let mut iv = [0u8; 12];
getrandom::getrandom(&mut iv).expect("OS CSPRNG is available");
encrypt0_with_iv(plaintext, kid, key, &iv)
}
struct Encrypt0Parts {
kid: String,
protected: Vec<u8>,
iv: Vec<u8>,
ciphertext: Vec<u8>,
}
fn parse_encrypt0(blob: &[u8]) -> Option<Encrypt0Parts> {
let value: Value = ciborium::de::from_reader(blob).ok()?;
let body = match value {
Value::Tag(_, inner) => *inner,
other => other,
};
let array = body.as_array()?;
if array.len() != 3 {
return None;
}
let protected = array[0].as_bytes()?.clone();
let unprotected = array[1].as_map()?;
let ciphertext = array[2].as_bytes()?.clone();
let kid_target = Integer::from(KID);
let iv_target = Integer::from(IV);
let kid = unprotected.iter().find_map(|(k, v)| match (k, v) {
(Value::Integer(i), Value::Bytes(b)) if *i == kid_target => {
String::from_utf8(b.clone()).ok()
}
_ => None,
})?;
let iv = unprotected.iter().find_map(|(k, v)| match (k, v) {
(Value::Integer(i), Value::Bytes(b)) if *i == iv_target => Some(b.clone()),
_ => None,
})?;
Some(Encrypt0Parts {
kid,
protected,
iv,
ciphertext,
})
}
pub fn recipient_kid(blob: &[u8]) -> Option<String> {
parse_encrypt0(blob).map(|p| p.kid)
}
pub fn decrypt0(
blob: &[u8],
resolve: impl Fn(&str) -> Option<[u8; 32]>,
) -> Result<Vec<u8>, Encrypt0Error> {
let parts = parse_encrypt0(blob).ok_or(Encrypt0Error::Malformed)?;
let key = resolve(&parts.kid).ok_or(Encrypt0Error::MissingKey)?;
if parts.iv.len() != 12 {
return Err(Encrypt0Error::Malformed);
}
let aad = enc_structure(&parts.protected);
let cipher = Aes256Gcm::new((&key).into());
cipher
.decrypt(
Nonce::from_slice(&parts.iv),
Payload {
msg: &parts.ciphertext,
aad: &aad,
},
)
.map_err(|_| Encrypt0Error::AuthFailed)
}