use crate::cht::HASH_VALUE_SIZE;
use crate::ec::Point;
use crate::ecdsa::{sha1, verify, Signature, SHA1_LEN};
use crate::error::AacsError;
pub const CERTIFICATE_TYPE_FIRST_GEN: u8 = 0x00;
pub const CONTENT_HASH_TABLE_DIGEST_LEN: usize = HASH_VALUE_SIZE;
pub const SIGNATURE_DATA_LEN: usize = 40;
const HEADER_FIXED_LEN: usize = 26;
const OFFSET_LENGTH_FORMAT_SPECIFIC_SECTION: usize = 24;
const BD_FORMAT_SPECIFIC_FIXED_LEN: usize = SHA1_LEN + SHA1_LEN + 2;
const HASH_VALUE_OF_CPS_UNIT_USAGE_FILE_LEN: usize = SHA1_LEN;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ContentCertificateId(pub [u8; 6]);
impl ContentCertificateId {
pub fn from_components(applicant_id: [u8; 2], content_sequence_number: [u8; 4]) -> Self {
let mut out = [0u8; 6];
out[..2].copy_from_slice(&applicant_id);
out[2..].copy_from_slice(&content_sequence_number);
Self(out)
}
pub fn applicant_id(&self) -> [u8; 2] {
[self.0[0], self.0[1]]
}
pub fn content_sequence_number(&self) -> [u8; 4] {
[self.0[2], self.0[3], self.0[4], self.0[5]]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContentSequenceNumber {
pub ccss_id: u8,
pub timestamp: u16,
pub sequence_number: u16,
}
impl ContentSequenceNumber {
pub fn from_be_bytes(b: [u8; 4]) -> Self {
let ccss_id = b[0] >> 2;
let seq1 = ((u16::from(b[0] & 0x03)) << 2) | (u16::from(b[1]) >> 6);
let timestamp =
((u16::from(b[1] & 0x3F)) << 9) | (u16::from(b[2]) << 1) | (u16::from(b[3]) >> 7);
let seq2 = u16::from(b[3] & 0x7F);
let sequence_number = (seq1 << 7) | seq2;
Self {
ccss_id,
timestamp,
sequence_number,
}
}
pub fn to_be_bytes(self) -> [u8; 4] {
let ccss_id = self.ccss_id & 0x3F;
let seq1 = (self.sequence_number >> 7) & 0x0F;
let seq2 = self.sequence_number & 0x7F;
let timestamp = self.timestamp & 0x7FFF;
let b0 = (ccss_id << 2) | ((seq1 >> 2) as u8 & 0x03);
let b1 = (((seq1 & 0x03) as u8) << 6) | ((timestamp >> 9) as u8 & 0x3F);
let b2 = ((timestamp >> 1) & 0xFF) as u8;
let b3 = (((timestamp & 0x01) as u8) << 7) | (seq2 as u8 & 0x7F);
[b0, b1, b2, b3]
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BdFormatSpecificSection {
pub hash_value_of_mc_manifest_file: [u8; SHA1_LEN],
pub hash_value_of_bdj_root_cert: [u8; SHA1_LEN],
pub hash_value_of_cps_unit_usage_files: Vec<[u8; SHA1_LEN]>,
}
impl BdFormatSpecificSection {
pub fn parse(bytes: &[u8]) -> Result<Self, AacsError> {
if bytes.len() < BD_FORMAT_SPECIFIC_FIXED_LEN {
return Err(AacsError::Truncated(
"Content Certificate BD Format-Specific Section",
));
}
let mut mc = [0u8; SHA1_LEN];
mc.copy_from_slice(&bytes[0..SHA1_LEN]);
let mut bdj = [0u8; SHA1_LEN];
bdj.copy_from_slice(&bytes[SHA1_LEN..2 * SHA1_LEN]);
let num_of_cps_unit =
u16::from_be_bytes([bytes[2 * SHA1_LEN], bytes[2 * SHA1_LEN + 1]]) as usize;
let required = BD_FORMAT_SPECIFIC_FIXED_LEN
.checked_add(
num_of_cps_unit
.checked_mul(HASH_VALUE_OF_CPS_UNIT_USAGE_FILE_LEN)
.ok_or(AacsError::Truncated(
"Content Certificate CPS Unit Usage hash array",
))?,
)
.ok_or(AacsError::Truncated(
"Content Certificate BD Format-Specific Section",
))?;
if bytes.len() < required {
return Err(AacsError::OversizedRecord {
what: "Content Certificate BD Format-Specific Section",
declared: required,
available: bytes.len(),
});
}
let mut usage = Vec::with_capacity(num_of_cps_unit);
let mut cursor = BD_FORMAT_SPECIFIC_FIXED_LEN;
for _ in 0..num_of_cps_unit {
let mut h = [0u8; SHA1_LEN];
h.copy_from_slice(&bytes[cursor..cursor + SHA1_LEN]);
usage.push(h);
cursor += SHA1_LEN;
}
Ok(Self {
hash_value_of_mc_manifest_file: mc,
hash_value_of_bdj_root_cert: bdj,
hash_value_of_cps_unit_usage_files: usage,
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let j = self.hash_value_of_cps_unit_usage_files.len();
let mut out = Vec::with_capacity(BD_FORMAT_SPECIFIC_FIXED_LEN + j * SHA1_LEN);
out.extend_from_slice(&self.hash_value_of_mc_manifest_file);
out.extend_from_slice(&self.hash_value_of_bdj_root_cert);
out.extend_from_slice(&(j as u16).to_be_bytes());
for h in &self.hash_value_of_cps_unit_usage_files {
out.extend_from_slice(h);
}
out
}
pub fn num_of_cps_unit(&self) -> u16 {
self.hash_value_of_cps_unit_usage_files.len() as u16
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentCertificate {
pub certificate_type: u8,
pub bee: bool,
pub total_number_of_hash_units: u32,
pub total_number_of_layers: u8,
pub layer_number: u8,
pub number_of_hash_units: u32,
pub number_of_digests: u16,
pub applicant_id: [u8; 2],
pub content_sequence_number: [u8; 4],
pub minimum_crl_version: u16,
pub format_specific_section: Vec<u8>,
pub content_hash_table_digests: Vec<[u8; CONTENT_HASH_TABLE_DIGEST_LEN]>,
pub signature_data: Signature,
}
impl ContentCertificate {
pub fn parse(bytes: &[u8]) -> Result<Self, AacsError> {
if bytes.len() < HEADER_FIXED_LEN {
return Err(AacsError::Truncated("Content Certificate header"));
}
let certificate_type = bytes[0];
if certificate_type != CERTIFICATE_TYPE_FIRST_GEN {
return Err(AacsError::InvalidValue {
what: "Content Certificate Type",
value: u64::from(certificate_type),
});
}
let bee = (bytes[1] & 0x80) != 0;
let total_number_of_hash_units =
u32::from_be_bytes([bytes[2], bytes[3], bytes[4], bytes[5]]);
let total_number_of_layers = bytes[6];
let layer_number = bytes[7];
let number_of_hash_units = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
let number_of_digests = u16::from_be_bytes([bytes[12], bytes[13]]);
let applicant_id = [bytes[14], bytes[15]];
let content_sequence_number = [bytes[16], bytes[17], bytes[18], bytes[19]];
let minimum_crl_version = u16::from_be_bytes([bytes[20], bytes[21]]);
let length_format_specific_section = u16::from_be_bytes([
bytes[OFFSET_LENGTH_FORMAT_SPECIFIC_SECTION],
bytes[OFFSET_LENGTH_FORMAT_SPECIFIC_SECTION + 1],
]) as usize;
if length_format_specific_section != 0 && (length_format_specific_section + 2) % 4 != 0 {
return Err(AacsError::InvalidValue {
what: "Content Certificate Length_Format_Specific_Section (must be 0 or ≡ 2 mod 4)",
value: length_format_specific_section as u64,
});
}
let format_specific_section_len = if length_format_specific_section == 0 {
2
} else {
length_format_specific_section
};
let digests_offset = HEADER_FIXED_LEN + format_specific_section_len;
let n_digests = number_of_digests as usize;
let digests_len =
n_digests
.checked_mul(CONTENT_HASH_TABLE_DIGEST_LEN)
.ok_or(AacsError::Truncated(
"Content Certificate digest-array length",
))?;
let signature_offset = digests_offset
.checked_add(digests_len)
.ok_or(AacsError::Truncated("Content Certificate signature offset"))?;
let total = signature_offset
.checked_add(SIGNATURE_DATA_LEN)
.ok_or(AacsError::Truncated("Content Certificate total length"))?;
if bytes.len() < total {
return Err(AacsError::OversizedRecord {
what: "Content Certificate",
declared: total,
available: bytes.len(),
});
}
let mut format_specific_section = Vec::with_capacity(length_format_specific_section);
if length_format_specific_section > 0 {
format_specific_section.extend_from_slice(
&bytes[HEADER_FIXED_LEN..HEADER_FIXED_LEN + length_format_specific_section],
);
}
let mut content_hash_table_digests = Vec::with_capacity(n_digests);
for i in 0..n_digests {
let off = digests_offset + i * CONTENT_HASH_TABLE_DIGEST_LEN;
let mut d = [0u8; CONTENT_HASH_TABLE_DIGEST_LEN];
d.copy_from_slice(&bytes[off..off + CONTENT_HASH_TABLE_DIGEST_LEN]);
content_hash_table_digests.push(d);
}
let mut signature_data = [0u8; SIGNATURE_DATA_LEN];
signature_data
.copy_from_slice(&bytes[signature_offset..signature_offset + SIGNATURE_DATA_LEN]);
Ok(Self {
certificate_type,
bee,
total_number_of_hash_units,
total_number_of_layers,
layer_number,
number_of_hash_units,
number_of_digests,
applicant_id,
content_sequence_number,
minimum_crl_version,
format_specific_section,
content_hash_table_digests,
signature_data,
})
}
pub fn content_certificate_id(&self) -> ContentCertificateId {
ContentCertificateId::from_components(self.applicant_id, self.content_sequence_number)
}
pub fn content_sequence_number_decoded(&self) -> ContentSequenceNumber {
ContentSequenceNumber::from_be_bytes(self.content_sequence_number)
}
pub fn bd_format_specific_section(&self) -> Result<BdFormatSpecificSection, AacsError> {
BdFormatSpecificSection::parse(&self.format_specific_section)
}
pub fn number_of_digests_stored(&self) -> usize {
self.content_hash_table_digests.len()
}
pub fn is_layer_zero(&self) -> bool {
self.layer_number == 0
}
pub fn to_bytes(&self) -> Vec<u8> {
let length_field = self.format_specific_section.len() as u16;
let format_specific_padded_len = if self.format_specific_section.is_empty() {
2
} else {
self.format_specific_section.len()
};
let mut out = Vec::with_capacity(
HEADER_FIXED_LEN
+ format_specific_padded_len
+ self.content_hash_table_digests.len() * CONTENT_HASH_TABLE_DIGEST_LEN
+ SIGNATURE_DATA_LEN,
);
out.push(self.certificate_type);
out.push(if self.bee { 0x80 } else { 0x00 });
out.extend_from_slice(&self.total_number_of_hash_units.to_be_bytes());
out.push(self.total_number_of_layers);
out.push(self.layer_number);
out.extend_from_slice(&self.number_of_hash_units.to_be_bytes());
out.extend_from_slice(&self.number_of_digests.to_be_bytes());
out.extend_from_slice(&self.applicant_id);
out.extend_from_slice(&self.content_sequence_number);
out.extend_from_slice(&self.minimum_crl_version.to_be_bytes());
out.extend_from_slice(&[0u8; 2]); out.extend_from_slice(&length_field.to_be_bytes());
if self.format_specific_section.is_empty() {
out.extend_from_slice(&[0u8; 2]);
} else {
out.extend_from_slice(&self.format_specific_section);
}
for d in &self.content_hash_table_digests {
out.extend_from_slice(d);
}
out.extend_from_slice(&self.signature_data);
out
}
pub fn signed_range_bytes(&self) -> Vec<u8> {
let mut bytes = self.to_bytes();
bytes.truncate(bytes.len() - SIGNATURE_DATA_LEN);
bytes
}
pub fn verify_signature(&self, aacs_cc_pub: &Point) -> Result<(), AacsError> {
let payload = self.signed_range_bytes();
if verify(aacs_cc_pub, &self.signature_data, &payload) {
Ok(())
} else {
Err(AacsError::MkbSignatureInvalid)
}
}
pub fn content_hash_table_digest(cht_bytes: &[u8]) -> [u8; CONTENT_HASH_TABLE_DIGEST_LEN] {
let d = sha1(cht_bytes);
let mut out = [0u8; CONTENT_HASH_TABLE_DIGEST_LEN];
out.copy_from_slice(&d[SHA1_LEN - CONTENT_HASH_TABLE_DIGEST_LEN..]);
out
}
pub fn verify_content_hash_table_digest(
&self,
digest_index: usize,
cht_bytes: &[u8],
) -> Result<(), AacsError> {
let stored =
self.content_hash_table_digests
.get(digest_index)
.ok_or(AacsError::InvalidValue {
what: "Content Certificate digest index",
value: digest_index as u64,
})?;
let recomputed = Self::content_hash_table_digest(cht_bytes);
if &recomputed == stored {
Ok(())
} else {
Err(AacsError::ContentHashMismatch {
index: digest_index,
})
}
}
}
pub fn usage_rules_hash(usage_rules: &[u8]) -> [u8; SHA1_LEN] {
sha1(usage_rules)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ec::{Point, U160};
use crate::ecdsa::sign;
fn small_scalar(v: u32) -> U160 {
U160 {
limbs: [v, 0, 0, 0, 0],
}
}
fn synth_bd_format_specific_section(seed: u8, j: usize) -> BdFormatSpecificSection {
let mut mc = [0u8; SHA1_LEN];
for (i, b) in mc.iter_mut().enumerate() {
*b = (i as u8).wrapping_add(seed);
}
let mut bdj = [0u8; SHA1_LEN];
for (i, b) in bdj.iter_mut().enumerate() {
*b = (i as u8).wrapping_add(seed).wrapping_mul(3);
}
let mut usage = Vec::with_capacity(j);
for u in 0..j {
let mut h = [0u8; SHA1_LEN];
for (i, b) in h.iter_mut().enumerate() {
*b = (i as u8)
.wrapping_add(seed)
.wrapping_add(u as u8)
.wrapping_mul(7);
}
usage.push(h);
}
BdFormatSpecificSection {
hash_value_of_mc_manifest_file: mc,
hash_value_of_bdj_root_cert: bdj,
hash_value_of_cps_unit_usage_files: usage,
}
}
fn synth_certificate_template() -> ContentCertificate {
let bd_section = synth_bd_format_specific_section(0x11, 2);
let format_specific = bd_section.to_bytes();
let digests = vec![[0u8; 8], [0u8; 8]];
ContentCertificate {
certificate_type: CERTIFICATE_TYPE_FIRST_GEN,
bee: true,
total_number_of_hash_units: 0x0001_0000,
total_number_of_layers: 2,
layer_number: 0,
number_of_hash_units: 0x0000_8000,
number_of_digests: digests.len() as u16,
applicant_id: [0x00, 0x42],
content_sequence_number: ContentSequenceNumber {
ccss_id: 5,
timestamp: 1234,
sequence_number: 0x123,
}
.to_be_bytes(),
minimum_crl_version: 7,
format_specific_section: format_specific,
content_hash_table_digests: digests,
signature_data: [0u8; SIGNATURE_DATA_LEN],
}
}
#[test]
fn content_certificate_id_concatenates_applicant_and_sequence() {
let cert = synth_certificate_template();
let id = cert.content_certificate_id();
assert_eq!(id.applicant_id(), [0x00, 0x42]);
assert_eq!(id.content_sequence_number(), cert.content_sequence_number);
assert_eq!(id.0[0..2], cert.applicant_id);
assert_eq!(id.0[2..6], cert.content_sequence_number);
}
#[test]
fn content_sequence_number_round_trips() {
let cases = [
ContentSequenceNumber {
ccss_id: 0,
timestamp: 0,
sequence_number: 0,
},
ContentSequenceNumber {
ccss_id: 0x3F,
timestamp: 0x7FFF,
sequence_number: 0x7FF,
},
ContentSequenceNumber {
ccss_id: 7,
timestamp: 1234,
sequence_number: 0x456,
},
ContentSequenceNumber {
ccss_id: 33,
timestamp: 0x4321,
sequence_number: 0x321,
},
];
for c in cases {
let bytes = c.to_be_bytes();
let decoded = ContentSequenceNumber::from_be_bytes(bytes);
assert_eq!(decoded, c, "round-trip failed for {c:?}");
}
}
#[test]
fn bd_format_specific_section_round_trips() {
let original = synth_bd_format_specific_section(0x42, 3);
let bytes = original.to_bytes();
assert_eq!(bytes.len(), BD_FORMAT_SPECIFIC_FIXED_LEN + 3 * SHA1_LEN);
let parsed = BdFormatSpecificSection::parse(&bytes).unwrap();
assert_eq!(parsed, original);
assert_eq!(parsed.num_of_cps_unit(), 3);
}
#[test]
fn bd_format_specific_section_rejects_truncated_prefix() {
let truncated = [0u8; BD_FORMAT_SPECIFIC_FIXED_LEN - 1];
assert!(matches!(
BdFormatSpecificSection::parse(&truncated),
Err(AacsError::Truncated(_))
));
}
#[test]
fn bd_format_specific_section_rejects_short_trailer() {
let mut buf = vec![0u8; BD_FORMAT_SPECIFIC_FIXED_LEN + SHA1_LEN];
buf[2 * SHA1_LEN] = 0x00;
buf[2 * SHA1_LEN + 1] = 0x02;
assert!(matches!(
BdFormatSpecificSection::parse(&buf),
Err(AacsError::OversizedRecord { .. })
));
}
#[test]
fn parse_round_trips_certificate_bytes() {
let mut cert = synth_certificate_template();
cert.content_hash_table_digests[0] = [1, 2, 3, 4, 5, 6, 7, 8];
cert.content_hash_table_digests[1] = [9, 10, 11, 12, 13, 14, 15, 16];
for (i, b) in cert.signature_data.iter_mut().enumerate() {
*b = i as u8;
}
let on_disc = cert.to_bytes();
let parsed = ContentCertificate::parse(&on_disc).unwrap();
assert_eq!(parsed, cert);
assert_eq!(parsed.to_bytes(), on_disc);
}
#[test]
fn parse_rejects_unknown_certificate_type() {
let mut cert = synth_certificate_template();
cert.certificate_type = 0x42;
let mut bytes = cert.to_bytes();
bytes[0] = 0x42;
assert!(matches!(
ContentCertificate::parse(&bytes),
Err(AacsError::InvalidValue { what, .. }) if what.contains("Certificate Type")
));
}
#[test]
fn parse_rejects_length_field_violating_4byte_alignment() {
let cert = synth_certificate_template();
let mut bytes = cert.to_bytes();
let bad = 63u16.to_be_bytes();
bytes[OFFSET_LENGTH_FORMAT_SPECIFIC_SECTION] = bad[0];
bytes[OFFSET_LENGTH_FORMAT_SPECIFIC_SECTION + 1] = bad[1];
assert!(matches!(
ContentCertificate::parse(&bytes),
Err(AacsError::InvalidValue { .. })
));
}
#[test]
fn parse_rejects_truncated_bytes() {
let cert = synth_certificate_template();
let bytes = cert.to_bytes();
let short = &bytes[..bytes.len() - 1];
assert!(matches!(
ContentCertificate::parse(short),
Err(AacsError::OversizedRecord { .. })
));
}
#[test]
fn signed_range_excludes_only_the_signature() {
let cert = synth_certificate_template();
let full = cert.to_bytes();
let signed = cert.signed_range_bytes();
assert_eq!(signed.len(), full.len() - SIGNATURE_DATA_LEN);
assert_eq!(&signed[..], &full[..full.len() - SIGNATURE_DATA_LEN]);
}
#[test]
fn verify_signature_round_trips_with_synthetic_key() {
let priv_key = small_scalar(0x9be3_1f5d);
let pub_key = Point::generator().mul_scalar(&priv_key);
let mut cert = synth_certificate_template();
cert.content_hash_table_digests[0] = [0xaa; 8];
cert.content_hash_table_digests[1] = [0xbb; 8];
let payload = cert.signed_range_bytes();
let sig = sign(&priv_key, &payload);
cert.signature_data = sig;
cert.verify_signature(&pub_key).unwrap();
let mut tampered = cert.clone();
tampered.content_hash_table_digests[0][0] ^= 0x01;
assert_eq!(
tampered.verify_signature(&pub_key),
Err(AacsError::MkbSignatureInvalid)
);
let other_priv = small_scalar(0x1234_5678);
let other_pub = Point::generator().mul_scalar(&other_priv);
assert_eq!(
cert.verify_signature(&other_pub),
Err(AacsError::MkbSignatureInvalid)
);
}
#[test]
fn cht_digest_is_lsb_64_of_sha1() {
let cht_bytes: Vec<u8> = (0..2000u32).map(|i| (i & 0xFF) as u8).collect();
let digest = ContentCertificate::content_hash_table_digest(&cht_bytes);
let full = sha1(&cht_bytes);
assert_eq!(&digest[..], &full[12..20]);
}
#[test]
fn verify_content_hash_table_digest_round_trips_and_detects_tampering() {
let cht_a: Vec<u8> = (0..1024u32).map(|i| (i & 0xFF) as u8).collect();
let cht_b: Vec<u8> = (0..2048u32).map(|i| ((i * 3) & 0xFF) as u8).collect();
let mut cert = synth_certificate_template();
cert.content_hash_table_digests[0] = ContentCertificate::content_hash_table_digest(&cht_a);
cert.content_hash_table_digests[1] = ContentCertificate::content_hash_table_digest(&cht_b);
cert.verify_content_hash_table_digest(0, &cht_a).unwrap();
cert.verify_content_hash_table_digest(1, &cht_b).unwrap();
let mut tampered = cht_a.clone();
tampered[7] ^= 0xFF;
assert_eq!(
cert.verify_content_hash_table_digest(0, &tampered),
Err(AacsError::ContentHashMismatch { index: 0 })
);
assert!(matches!(
cert.verify_content_hash_table_digest(99, &cht_a),
Err(AacsError::InvalidValue { .. })
));
}
#[test]
fn usage_rules_hash_is_sha1() {
let usage_rules = b"BDMV usage rules payload (synthetic for test)";
let h = usage_rules_hash(usage_rules);
assert_eq!(h, sha1(usage_rules));
}
#[test]
fn bd_format_specific_round_trip_through_full_certificate() {
let cert = synth_certificate_template();
let on_disc = cert.to_bytes();
let parsed = ContentCertificate::parse(&on_disc).unwrap();
let bd = parsed.bd_format_specific_section().unwrap();
assert_eq!(bd.num_of_cps_unit(), 2);
assert_eq!(bd.hash_value_of_cps_unit_usage_files.len(), 2);
}
#[test]
fn zero_length_format_specific_section_uses_alignment_bytes() {
let mut cert = synth_certificate_template();
cert.format_specific_section.clear();
cert.number_of_digests = 1;
cert.content_hash_table_digests = vec![[0x77; 8]];
let on_disc = cert.to_bytes();
assert_eq!(on_disc.len(), HEADER_FIXED_LEN + 2 + 8 + SIGNATURE_DATA_LEN);
let parsed = ContentCertificate::parse(&on_disc).unwrap();
assert!(parsed.format_specific_section.is_empty());
assert_eq!(parsed.content_hash_table_digests, vec![[0x77; 8]]);
}
}