use blake2::digest::consts::U28;
use blake2::digest::Digest;
use blake2::Blake2b;
use crate::cbor::CborValue;
use crate::cose::{
build_label309_sig_structure, build_sig_structure, encode_cose_sign1, CoseHeader,
CARDANO_POE_SIG_DOMAIN_PREFIX,
};
use crate::poe_standard::{chunk_bytes, encode_record_body_for_signing, PoeRecord, SigEntry};
const ED25519_PUBLIC_KEY_LENGTH: usize = 32;
const ED25519_SIGNATURE_LENGTH: usize = 64;
const COSE_HEADER_ALG_LABEL: i64 = 1;
const COSE_HEADER_KID_LABEL: i64 = 4;
const HASHED_MODE_HEADER_KEY: &str = "hashed";
const COSE_ALG_EDDSA: i64 = -8;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum OffHostSignError {
#[error("INVALID_PUBKEY_LENGTH: signerPubkey must be 32 bytes (Ed25519 raw public key)")]
InvalidPubkeyLength,
#[error("INVALID_SIGNATURE_LENGTH: signature must be 64 bytes (Ed25519 raw signature)")]
InvalidSignatureLength,
}
impl OffHostSignError {
#[must_use]
pub const fn code(self) -> &'static str {
match self {
OffHostSignError::InvalidPubkeyLength => "INVALID_PUBKEY_LENGTH",
OffHostSignError::InvalidSignatureLength => "INVALID_SIGNATURE_LENGTH",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PreparedSigStructure {
pub sig_structure_bytes: Vec<u8>,
pub protected_header_bytes: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PreparedSigStructureHashed {
pub sig_structure_bytes: Vec<u8>,
pub protected_header_bytes: Vec<u8>,
pub to_sign_hash_bytes: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssembledCoseSign1 {
pub cose_sign1_bytes: Vec<u8>,
pub sig_entry: SigEntry,
}
fn blake2b224(input: &[u8]) -> [u8; 28] {
Blake2b::<U28>::digest(input).into()
}
pub fn build_to_sign(record: &PoeRecord) -> Result<Vec<u8>, crate::cbor::CanonicalCborError> {
let body = encode_record_body_for_signing(record)?;
let prefix = CARDANO_POE_SIG_DOMAIN_PREFIX.as_bytes();
let mut out = Vec::with_capacity(prefix.len() + body.len());
out.extend_from_slice(prefix);
out.extend_from_slice(&body);
Ok(out)
}
fn path1_protected_header(signer_pubkey: &[u8]) -> CoseHeader {
CoseHeader::new()
.with_int(COSE_HEADER_ALG_LABEL, CborValue::int(COSE_ALG_EDDSA))
.with_int(
COSE_HEADER_KID_LABEL,
CborValue::bytes(signer_pubkey.to_vec()),
)
}
fn path1_protected_header_bytes(signer_pubkey: &[u8]) -> Vec<u8> {
path1_protected_header(signer_pubkey)
.encode_protected()
.expect("path-1 protected header encodes")
}
pub fn prepare_sig_structure(
record: &PoeRecord,
signer_pubkey: &[u8],
) -> Result<PreparedSigStructure, OffHostSignError> {
if signer_pubkey.len() != ED25519_PUBLIC_KEY_LENGTH {
return Err(OffHostSignError::InvalidPubkeyLength);
}
let protected_header_bytes = path1_protected_header_bytes(signer_pubkey);
let record_body_cbor = encode_record_body_for_signing(record).unwrap_or_default();
let sig_structure_bytes =
build_label309_sig_structure(&protected_header_bytes, &record_body_cbor);
Ok(PreparedSigStructure {
sig_structure_bytes,
protected_header_bytes,
})
}
pub fn assemble_cose_sign1(
record: &PoeRecord,
signer_pubkey: &[u8],
signature: &[u8],
) -> Result<AssembledCoseSign1, OffHostSignError> {
let _ = record;
if signer_pubkey.len() != ED25519_PUBLIC_KEY_LENGTH {
return Err(OffHostSignError::InvalidPubkeyLength);
}
if signature.len() != ED25519_SIGNATURE_LENGTH {
return Err(OffHostSignError::InvalidSignatureLength);
}
let protected_header = path1_protected_header(signer_pubkey);
let cose_sign1_bytes =
encode_cose_sign1(&protected_header, &CoseHeader::new(), None, signature)
.expect("COSE_Sign1 encodes");
let chunks = chunk_bytes(&cose_sign1_bytes);
Ok(AssembledCoseSign1 {
cose_sign1_bytes,
sig_entry: SigEntry {
cose_sign1: chunks,
cose_key: None,
},
})
}
pub fn prepare_sig_structure_hashed(
record: &PoeRecord,
signer_pubkey: &[u8],
) -> Result<PreparedSigStructureHashed, OffHostSignError> {
if signer_pubkey.len() != ED25519_PUBLIC_KEY_LENGTH {
return Err(OffHostSignError::InvalidPubkeyLength);
}
let protected_header_bytes = path1_protected_header_bytes(signer_pubkey);
let to_sign = build_to_sign(record).unwrap_or_default();
let to_sign_hash = blake2b224(&to_sign);
let sig_structure_bytes = build_sig_structure(&protected_header_bytes, &[], &to_sign_hash);
Ok(PreparedSigStructureHashed {
sig_structure_bytes,
protected_header_bytes,
to_sign_hash_bytes: to_sign_hash.to_vec(),
})
}
pub fn assemble_cose_sign1_hashed(
record: &PoeRecord,
signer_pubkey: &[u8],
signature: &[u8],
) -> Result<AssembledCoseSign1, OffHostSignError> {
let _ = record;
if signer_pubkey.len() != ED25519_PUBLIC_KEY_LENGTH {
return Err(OffHostSignError::InvalidPubkeyLength);
}
if signature.len() != ED25519_SIGNATURE_LENGTH {
return Err(OffHostSignError::InvalidSignatureLength);
}
let protected_header = path1_protected_header(signer_pubkey);
let unprotected_header =
CoseHeader::new().with_text(HASHED_MODE_HEADER_KEY, CborValue::Bool(true));
let cose_sign1_bytes =
encode_cose_sign1(&protected_header, &unprotected_header, None, signature)
.expect("COSE_Sign1 encodes");
let chunks = chunk_bytes(&cose_sign1_bytes);
Ok(AssembledCoseSign1 {
cose_sign1_bytes,
sig_entry: SigEntry {
cose_sign1: chunks,
cose_key: None,
},
})
}