use crate::content_certificate::ContentCertificateId;
use crate::ec::Point;
use crate::ecdsa::{verify as ecdsa_verify, Signature as EcdsaSignature};
use crate::error::AacsError;
pub const LIST_TYPE_FIRST_GEN: u8 = 0x0;
pub const RECORD_TYPE_CONTENT_CERTIFICATE_ID: u8 = 0x0;
pub const RECORD_TYPE_MANAGED_COPY_SERVER_ID: u8 = 0x1;
pub const RECORD_TYPE_RMRR_PART_1: u8 = 0x2;
pub const RECORD_TYPE_RMRR_PART_2: u8 = 0x3;
pub const RECORD_TYPE_RMRR_PART_3: u8 = 0x4;
pub const REVOCATION_RECORD_LEN: usize = 8;
pub const SEGMENT_SIGNATURE_LEN: usize = 40;
pub const CRL_HEADER_LEN: usize = 4;
pub const SEGMENT_1_SIZE_MAX: u32 = 128 * 1024 - CRL_HEADER_LEN as u32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ManagedCopyServerCertificateId(pub [u8; 6]);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum RecordableMediaType {
BdRecordable = 0,
HdDvdRecordable = 1,
DvdRecordable = 2,
PlusRecordable = 3,
Reserved(u8),
}
impl RecordableMediaType {
pub fn from_u3(v: u8) -> Self {
match v & 0x07 {
0 => Self::BdRecordable,
1 => Self::HdDvdRecordable,
2 => Self::DvdRecordable,
3 => Self::PlusRecordable,
other => Self::Reserved(other),
}
}
pub fn to_u3(self) -> u8 {
match self {
Self::BdRecordable => 0,
Self::HdDvdRecordable => 1,
Self::DvdRecordable => 2,
Self::PlusRecordable => 3,
Self::Reserved(v) => v & 0x07,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecordableMediaRevocation {
pub iccid: bool,
pub media_type: RecordableMediaType,
pub content_certificate_id: ContentCertificateId,
pub media_id: [u8; 16],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RevocationRecord {
ContentCertificateId {
range: u16,
id: ContentCertificateId,
},
ManagedCopyServerCertificateId {
range: u16,
id: ManagedCopyServerCertificateId,
},
RecordableMedia(RecordableMediaRevocation),
Unknown {
record_type: u8,
bytes: [u8; REVOCATION_RECORD_LEN],
},
}
impl RevocationRecord {
pub fn revokes_content_certificate_id(&self, query: ContentCertificateId) -> bool {
match self {
Self::ContentCertificateId { range, id } => id_in_range(query.0, id.0, *range),
Self::RecordableMedia(r) if !r.iccid => r.content_certificate_id == query,
_ => false,
}
}
pub fn revokes_managed_copy_server_id(&self, query: ManagedCopyServerCertificateId) -> bool {
match self {
Self::ManagedCopyServerCertificateId { range, id } => {
id_in_range(query.0, id.0, *range)
}
_ => false,
}
}
}
fn id_in_range(query: [u8; 6], start: [u8; 6], range: u16) -> bool {
let q = u64_from_6(query);
let s = u64_from_6(start);
let r = u64::from(range);
q >= s && q <= s.saturating_add(r)
}
fn u64_from_6(b: [u8; 6]) -> u64 {
((b[0] as u64) << 40)
| ((b[1] as u64) << 32)
| ((b[2] as u64) << 24)
| ((b[3] as u64) << 16)
| ((b[4] as u64) << 8)
| (b[5] as u64)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CrlSegment {
pub segment_size: u32,
pub records: Vec<RevocationRecord>,
pub signature: EcdsaSignature,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentRevocationList {
pub list_type: u8,
pub reserved_nibble: u8,
pub list_version: u16,
pub number_of_segments: u8,
pub segments: Vec<CrlSegment>,
}
impl ContentRevocationList {
pub fn parse(bytes: &[u8]) -> Result<Self, AacsError> {
if bytes.len() < CRL_HEADER_LEN {
return Err(AacsError::Truncated("CRL Header"));
}
let header_byte = bytes[0];
let list_type = (header_byte >> 4) & 0x0F;
let reserved_nibble = header_byte & 0x0F;
let list_version = u16::from_be_bytes([bytes[1], bytes[2]]);
let number_of_segments = bytes[3];
if number_of_segments == 0 {
return Err(AacsError::InvalidValue {
what: "CRL Number of Segments (must be ≥ 1)",
value: 0,
});
}
let mut cursor = CRL_HEADER_LEN;
let mut segments = Vec::with_capacity(usize::from(number_of_segments));
for segment_index in 0..usize::from(number_of_segments) {
if cursor.saturating_add(4) > bytes.len() {
return Err(AacsError::Truncated("CRL Segment Size"));
}
let segment_size = u32::from_be_bytes([
bytes[cursor],
bytes[cursor + 1],
bytes[cursor + 2],
bytes[cursor + 3],
]);
if segment_index == 0 && segment_size > SEGMENT_1_SIZE_MAX {
return Err(AacsError::InvalidValue {
what: "CRL Segment #1 Segment Size (must be ≤ 128 KiB − CRL Header)",
value: u64::from(segment_size),
});
}
let segment_size_usize = segment_size as usize;
let min_segment_len = 4usize
.checked_add(SEGMENT_SIGNATURE_LEN)
.ok_or(AacsError::Truncated("CRL Segment minimum length"))?;
if segment_size_usize < min_segment_len {
return Err(AacsError::InvalidValue {
what:
"CRL Segment Size (must accommodate 4-byte size field + 40-byte signature)",
value: u64::from(segment_size),
});
}
let segment_end = cursor
.checked_add(segment_size_usize)
.ok_or(AacsError::Truncated("CRL Segment end offset"))?;
if segment_end > bytes.len() {
return Err(AacsError::OversizedRecord {
what: "CRL Segment",
declared: segment_end,
available: bytes.len(),
});
}
let record_set_start = cursor + 4;
let record_set_end = segment_end - SEGMENT_SIGNATURE_LEN;
let record_set_bytes = &bytes[record_set_start..record_set_end];
let records = parse_revocation_record_set(record_set_bytes)?;
let mut signature = [0u8; SEGMENT_SIGNATURE_LEN];
signature.copy_from_slice(&bytes[record_set_end..segment_end]);
segments.push(CrlSegment {
segment_size,
records,
signature,
});
cursor = segment_end;
}
for (i, b) in bytes[cursor..].iter().enumerate() {
if *b != 0 {
return Err(AacsError::InvalidValue {
what: "CRL trailing byte (only 0x00 padding permitted)",
value: u64::from(*b) | (i as u64) << 8,
});
}
}
Ok(Self {
list_type,
reserved_nibble,
list_version,
number_of_segments,
segments,
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::new();
out.push((self.list_type << 4) | (self.reserved_nibble & 0x0F));
out.extend_from_slice(&self.list_version.to_be_bytes());
out.push(self.number_of_segments);
for seg in &self.segments {
out.extend_from_slice(&seg.segment_size.to_be_bytes());
for r in &seg.records {
out.extend_from_slice(&encode_revocation_record(r));
}
out.extend_from_slice(&seg.signature);
}
out
}
pub fn signed_range_for_segment(&self, k: usize) -> Option<Vec<u8>> {
if k >= self.segments.len() {
return None;
}
let mut out = Vec::new();
out.push((self.list_type << 4) | (self.reserved_nibble & 0x0F));
out.extend_from_slice(&self.list_version.to_be_bytes());
out.push(self.number_of_segments);
for (i, seg) in self.segments.iter().enumerate() {
out.extend_from_slice(&seg.segment_size.to_be_bytes());
for r in &seg.records {
out.extend_from_slice(&encode_revocation_record(r));
}
if i == k {
break;
}
out.extend_from_slice(&seg.signature);
}
Some(out)
}
pub fn verify_segment_signature(&self, k: usize, aacs_la_pub: &Point) -> Result<(), AacsError> {
let payload = self
.signed_range_for_segment(k)
.ok_or(AacsError::InvalidValue {
what: "CRL segment index",
value: k as u64,
})?;
let sig = &self.segments[k].signature;
if ecdsa_verify(aacs_la_pub, sig, &payload) {
Ok(())
} else {
Err(AacsError::MkbSignatureInvalid)
}
}
pub fn verify_last_segment_signature(&self, aacs_la_pub: &Point) -> Result<(), AacsError> {
if self.segments.is_empty() {
return Err(AacsError::InvalidValue {
what: "CRL Number of Segments (must be ≥ 1)",
value: 0,
});
}
self.verify_segment_signature(self.segments.len() - 1, aacs_la_pub)
}
pub fn verify_all_segments(&self, aacs_la_pub: &Point) -> Result<(), AacsError> {
for k in 0..self.segments.len() {
self.verify_segment_signature(k, aacs_la_pub)?;
}
Ok(())
}
pub fn records(&self) -> impl Iterator<Item = &RevocationRecord> + '_ {
self.segments.iter().flat_map(|s| s.records.iter())
}
pub fn is_content_certificate_id_revoked(&self, query: ContentCertificateId) -> bool {
self.records()
.any(|r| r.revokes_content_certificate_id(query))
}
pub fn is_managed_copy_server_id_revoked(&self, query: ManagedCopyServerCertificateId) -> bool {
self.records()
.any(|r| r.revokes_managed_copy_server_id(query))
}
pub fn recordable_media_revoked(
&self,
media_type: RecordableMediaType,
media_id: [u8; 16],
content_certificate_id: ContentCertificateId,
) -> bool {
self.records().any(|r| {
if let RevocationRecord::RecordableMedia(rmrr) = r {
if rmrr.media_type != media_type {
return false;
}
if rmrr.media_id != media_id {
return false;
}
if rmrr.iccid {
true
} else {
rmrr.content_certificate_id == content_certificate_id
}
} else {
false
}
})
}
}
fn parse_revocation_record_set(bytes: &[u8]) -> Result<Vec<RevocationRecord>, AacsError> {
if bytes.len() % REVOCATION_RECORD_LEN != 0 {
return Err(AacsError::InvalidValue {
what: "CRL Revocation Record Set length (must be a multiple of 8)",
value: bytes.len() as u64,
});
}
let mut records = Vec::with_capacity(bytes.len() / REVOCATION_RECORD_LEN);
let mut i = 0;
while i < bytes.len() {
let slot: [u8; REVOCATION_RECORD_LEN] = bytes[i..i + REVOCATION_RECORD_LEN]
.try_into()
.map_err(|_| AacsError::Truncated("CRL Revocation Record"))?;
let record_type = (slot[0] >> 4) & 0x0F;
match record_type {
RECORD_TYPE_CONTENT_CERTIFICATE_ID => {
let (range, id_bytes) = decode_range_and_id(&slot);
records.push(RevocationRecord::ContentCertificateId {
range,
id: ContentCertificateId(id_bytes),
});
i += REVOCATION_RECORD_LEN;
}
RECORD_TYPE_MANAGED_COPY_SERVER_ID => {
let (range, id_bytes) = decode_range_and_id(&slot);
records.push(RevocationRecord::ManagedCopyServerCertificateId {
range,
id: ManagedCopyServerCertificateId(id_bytes),
});
i += REVOCATION_RECORD_LEN;
}
RECORD_TYPE_RMRR_PART_1 => {
let have_3 = i + 3 * REVOCATION_RECORD_LEN <= bytes.len();
let part2_ok = have_3
&& (bytes[i + REVOCATION_RECORD_LEN] >> 4) & 0x0F == RECORD_TYPE_RMRR_PART_2;
let part3_ok = have_3
&& (bytes[i + 2 * REVOCATION_RECORD_LEN] >> 4) & 0x0F
== RECORD_TYPE_RMRR_PART_3;
if have_3 && part2_ok && part3_ok {
let rec1 = &bytes[i..i + REVOCATION_RECORD_LEN];
let rec2 = &bytes[i + REVOCATION_RECORD_LEN..i + 2 * REVOCATION_RECORD_LEN];
let rec3 = &bytes[i + 2 * REVOCATION_RECORD_LEN..i + 3 * REVOCATION_RECORD_LEN];
let rmrr = decode_rmrr(rec1, rec2, rec3);
records.push(RevocationRecord::RecordableMedia(rmrr));
i += 3 * REVOCATION_RECORD_LEN;
} else {
records.push(RevocationRecord::Unknown {
record_type,
bytes: slot,
});
i += REVOCATION_RECORD_LEN;
}
}
_ => {
records.push(RevocationRecord::Unknown {
record_type,
bytes: slot,
});
i += REVOCATION_RECORD_LEN;
}
}
}
Ok(records)
}
fn decode_range_and_id(slot: &[u8; REVOCATION_RECORD_LEN]) -> (u16, [u8; 6]) {
let range_hi = u16::from(slot[0] & 0x0F);
let range = (range_hi << 8) | u16::from(slot[1]);
let mut id = [0u8; 6];
id.copy_from_slice(&slot[2..8]);
(range, id)
}
fn decode_rmrr(rec1: &[u8], rec2: &[u8], rec3: &[u8]) -> RecordableMediaRevocation {
let iccid = (rec1[0] & 0x08) != 0;
let media_type = RecordableMediaType::from_u3(rec1[0] & 0x07);
let mut ccid = [0u8; 6];
ccid.copy_from_slice(&rec1[1..=6]);
let mut media_id = [0u8; 16];
media_id[0] = rec1[7];
media_id[1] = ((rec2[0] & 0x0F) << 4) | (rec3[0] & 0x0F);
media_id[2..=8].copy_from_slice(&rec2[1..=7]);
media_id[9..=15].copy_from_slice(&rec3[1..=7]);
RecordableMediaRevocation {
iccid,
media_type,
content_certificate_id: ContentCertificateId(ccid),
media_id,
}
}
fn encode_rmrr(rmrr: &RecordableMediaRevocation) -> [u8; 3 * REVOCATION_RECORD_LEN] {
let mut out = [0u8; 3 * REVOCATION_RECORD_LEN];
out[0] = (RECORD_TYPE_RMRR_PART_1 << 4)
| (if rmrr.iccid { 0x08 } else { 0x00 })
| (rmrr.media_type.to_u3() & 0x07);
out[1..=6].copy_from_slice(&rmrr.content_certificate_id.0);
out[7] = rmrr.media_id[0];
out[REVOCATION_RECORD_LEN] = (RECORD_TYPE_RMRR_PART_2 << 4) | ((rmrr.media_id[1] >> 4) & 0x0F);
out[REVOCATION_RECORD_LEN + 1..=REVOCATION_RECORD_LEN + 7]
.copy_from_slice(&rmrr.media_id[2..=8]);
out[2 * REVOCATION_RECORD_LEN] = (RECORD_TYPE_RMRR_PART_3 << 4) | (rmrr.media_id[1] & 0x0F);
out[2 * REVOCATION_RECORD_LEN + 1..=2 * REVOCATION_RECORD_LEN + 7]
.copy_from_slice(&rmrr.media_id[9..=15]);
out
}
fn encode_revocation_record(r: &RevocationRecord) -> Vec<u8> {
match r {
RevocationRecord::ContentCertificateId { range, id } => {
let mut out = vec![0u8; REVOCATION_RECORD_LEN];
out[0] = (RECORD_TYPE_CONTENT_CERTIFICATE_ID << 4) | ((*range >> 8) as u8 & 0x0F);
out[1] = (*range & 0xFF) as u8;
out[2..=7].copy_from_slice(&id.0);
out
}
RevocationRecord::ManagedCopyServerCertificateId { range, id } => {
let mut out = vec![0u8; REVOCATION_RECORD_LEN];
out[0] = (RECORD_TYPE_MANAGED_COPY_SERVER_ID << 4) | ((*range >> 8) as u8 & 0x0F);
out[1] = (*range & 0xFF) as u8;
out[2..=7].copy_from_slice(&id.0);
out
}
RevocationRecord::RecordableMedia(rmrr) => encode_rmrr(rmrr).to_vec(),
RevocationRecord::Unknown { bytes, .. } => bytes.to_vec(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::content_certificate::ContentCertificateId;
use crate::ec::{Point, U160};
use crate::ecdsa::sign;
fn small_scalar(v: u32) -> U160 {
U160 {
limbs: [v, 0, 0, 0, 0],
}
}
fn synth_la_keys() -> (U160, Point) {
let d = small_scalar(0x1234_abcd);
let q = Point::generator().mul_scalar(&d);
(d, q)
}
fn cc_id(b: u8) -> ContentCertificateId {
ContentCertificateId([
b,
b.wrapping_add(1),
b.wrapping_add(2),
b.wrapping_add(3),
b.wrapping_add(4),
b.wrapping_add(5),
])
}
fn build_signed_one_segment(
priv_key: &U160,
records: Vec<RevocationRecord>,
) -> ContentRevocationList {
let mut record_set_bytes = Vec::new();
for r in &records {
record_set_bytes.extend_from_slice(&encode_revocation_record(r));
}
let segment_size = 4u32 + record_set_bytes.len() as u32 + SEGMENT_SIGNATURE_LEN as u32;
let mut crl = ContentRevocationList {
list_type: LIST_TYPE_FIRST_GEN,
reserved_nibble: 0,
list_version: 0x0017,
number_of_segments: 1,
segments: vec![CrlSegment {
segment_size,
records,
signature: [0u8; SEGMENT_SIGNATURE_LEN],
}],
};
let payload = crl.signed_range_for_segment(0).unwrap();
crl.segments[0].signature = sign(priv_key, &payload);
crl
}
#[test]
fn round_trip_empty_crl_one_segment_no_records() {
let (priv_key, pub_key) = synth_la_keys();
let crl = build_signed_one_segment(&priv_key, vec![]);
let bytes = crl.to_bytes();
let reparsed = ContentRevocationList::parse(&bytes).unwrap();
assert_eq!(reparsed, crl);
crl.verify_segment_signature(0, &pub_key).unwrap();
crl.verify_last_segment_signature(&pub_key).unwrap();
}
#[test]
fn round_trip_content_certificate_id_records() {
let (priv_key, pub_key) = synth_la_keys();
let rec_a = RevocationRecord::ContentCertificateId {
range: 0,
id: cc_id(0x10),
};
let rec_b = RevocationRecord::ContentCertificateId {
range: 5,
id: cc_id(0x20),
};
let crl = build_signed_one_segment(&priv_key, vec![rec_a.clone(), rec_b.clone()]);
let bytes = crl.to_bytes();
let reparsed = ContentRevocationList::parse(&bytes).unwrap();
assert_eq!(reparsed, crl);
crl.verify_all_segments(&pub_key).unwrap();
assert!(crl.is_content_certificate_id_revoked(cc_id(0x10)));
assert!(!crl.is_content_certificate_id_revoked(cc_id(0x11)));
let base = cc_id(0x20).0;
let start = u64_from_6(base);
let mut mid = [0u8; 6];
let val = start + 3;
mid[0] = (val >> 40) as u8;
mid[1] = (val >> 32) as u8;
mid[2] = (val >> 24) as u8;
mid[3] = (val >> 16) as u8;
mid[4] = (val >> 8) as u8;
mid[5] = val as u8;
assert!(crl.is_content_certificate_id_revoked(ContentCertificateId(mid)));
let mut over = [0u8; 6];
let val = start + 6;
over[0] = (val >> 40) as u8;
over[1] = (val >> 32) as u8;
over[2] = (val >> 24) as u8;
over[3] = (val >> 16) as u8;
over[4] = (val >> 8) as u8;
over[5] = val as u8;
assert!(!crl.is_content_certificate_id_revoked(ContentCertificateId(over)));
}
#[test]
fn round_trip_managed_copy_server_records() {
let (priv_key, pub_key) = synth_la_keys();
let rec = RevocationRecord::ManagedCopyServerCertificateId {
range: 1,
id: ManagedCopyServerCertificateId([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]),
};
let crl = build_signed_one_segment(&priv_key, vec![rec.clone()]);
let bytes = crl.to_bytes();
let reparsed = ContentRevocationList::parse(&bytes).unwrap();
assert_eq!(reparsed, crl);
crl.verify_segment_signature(0, &pub_key).unwrap();
assert!(
crl.is_managed_copy_server_id_revoked(ManagedCopyServerCertificateId([
0x01, 0x02, 0x03, 0x04, 0x05, 0x06
]))
);
assert!(
crl.is_managed_copy_server_id_revoked(ManagedCopyServerCertificateId([
0x01, 0x02, 0x03, 0x04, 0x05, 0x07
]))
);
assert!(
!crl.is_managed_copy_server_id_revoked(ManagedCopyServerCertificateId([
0x01, 0x02, 0x03, 0x04, 0x05, 0x08
]))
);
assert!(
!crl.is_content_certificate_id_revoked(ContentCertificateId([
0x01, 0x02, 0x03, 0x04, 0x05, 0x06
]))
);
}
#[test]
fn round_trip_recordable_media_revocation() {
let (priv_key, pub_key) = synth_la_keys();
let media_id = [
0xDE, 0xAD, 0xBE, 0xEF, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x11, 0x22,
0x33, 0x44,
];
let rmrr_iccid_off = RecordableMediaRevocation {
iccid: false,
media_type: RecordableMediaType::BdRecordable,
content_certificate_id: cc_id(0x40),
media_id,
};
let rmrr_iccid_on = RecordableMediaRevocation {
iccid: true,
media_type: RecordableMediaType::DvdRecordable,
content_certificate_id: cc_id(0x50),
media_id: [0x55; 16],
};
let crl = build_signed_one_segment(
&priv_key,
vec![
RevocationRecord::RecordableMedia(rmrr_iccid_off),
RevocationRecord::RecordableMedia(rmrr_iccid_on),
],
);
let bytes = crl.to_bytes();
let reparsed = ContentRevocationList::parse(&bytes).unwrap();
assert_eq!(reparsed, crl);
crl.verify_all_segments(&pub_key).unwrap();
assert!(crl.recordable_media_revoked(
RecordableMediaType::BdRecordable,
media_id,
cc_id(0x40),
));
assert!(!crl.recordable_media_revoked(
RecordableMediaType::BdRecordable,
media_id,
cc_id(0x41),
));
assert!(crl.recordable_media_revoked(
RecordableMediaType::DvdRecordable,
[0x55; 16],
cc_id(0x99),
));
assert!(!crl.recordable_media_revoked(
RecordableMediaType::HdDvdRecordable,
media_id,
cc_id(0x40),
));
assert!(crl.is_content_certificate_id_revoked(cc_id(0x40)));
assert!(!crl.is_content_certificate_id_revoked(cc_id(0x50)));
}
#[test]
fn unknown_record_type_preserved_and_ignored_by_queries() {
let (priv_key, _pub_key) = synth_la_keys();
let unknown = RevocationRecord::Unknown {
record_type: 0xF,
bytes: [0xFA, 0xBC, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66],
};
let crl = build_signed_one_segment(&priv_key, vec![unknown.clone()]);
let bytes = crl.to_bytes();
let reparsed = ContentRevocationList::parse(&bytes).unwrap();
assert_eq!(reparsed, crl);
let any_cc = ContentCertificateId([0xFA, 0xBC, 0x11, 0x22, 0x33, 0x44]);
assert!(!crl.is_content_certificate_id_revoked(any_cc));
}
#[test]
fn malformed_rmrr_part1_without_parts_2_3_preserved_as_unknown() {
let (priv_key, _pub_key) = synth_la_keys();
let mut record_set = Vec::new();
record_set.push(RECORD_TYPE_RMRR_PART_1 << 4);
record_set.extend_from_slice(&[0; 7]);
record_set.push(RECORD_TYPE_CONTENT_CERTIFICATE_ID << 4);
record_set.extend_from_slice(&[0; 7]);
let segment_size = 4u32 + record_set.len() as u32 + SEGMENT_SIGNATURE_LEN as u32;
let mut crl = ContentRevocationList {
list_type: LIST_TYPE_FIRST_GEN,
reserved_nibble: 0,
list_version: 1,
number_of_segments: 1,
segments: vec![CrlSegment {
segment_size,
records: parse_revocation_record_set(&record_set).unwrap(),
signature: [0u8; SEGMENT_SIGNATURE_LEN],
}],
};
let payload = crl.signed_range_for_segment(0).unwrap();
crl.segments[0].signature = sign(&priv_key, &payload);
let bytes = crl.to_bytes();
let reparsed = ContentRevocationList::parse(&bytes).unwrap();
assert_eq!(reparsed, crl);
assert!(matches!(
crl.segments[0].records[0],
RevocationRecord::Unknown {
record_type: RECORD_TYPE_RMRR_PART_1,
..
}
));
assert!(matches!(
crl.segments[0].records[1],
RevocationRecord::ContentCertificateId { .. }
));
}
#[test]
fn multi_segment_signatures_use_cumulative_prefix() {
let (priv_key, pub_key) = synth_la_keys();
let records_a = vec![RevocationRecord::ContentCertificateId {
range: 0,
id: cc_id(0xA0),
}];
let records_b = vec![RevocationRecord::ContentCertificateId {
range: 0,
id: cc_id(0xB0),
}];
let seg_a_size = 4u32
+ (encode_revocation_record(&records_a[0]).len()) as u32
+ SEGMENT_SIGNATURE_LEN as u32;
let seg_b_size = 4u32
+ (encode_revocation_record(&records_b[0]).len()) as u32
+ SEGMENT_SIGNATURE_LEN as u32;
let mut crl = ContentRevocationList {
list_type: LIST_TYPE_FIRST_GEN,
reserved_nibble: 0,
list_version: 0x0023,
number_of_segments: 2,
segments: vec![
CrlSegment {
segment_size: seg_a_size,
records: records_a,
signature: [0u8; SEGMENT_SIGNATURE_LEN],
},
CrlSegment {
segment_size: seg_b_size,
records: records_b,
signature: [0u8; SEGMENT_SIGNATURE_LEN],
},
],
};
let payload_a = crl.signed_range_for_segment(0).unwrap();
crl.segments[0].signature = sign(&priv_key, &payload_a);
let payload_b = crl.signed_range_for_segment(1).unwrap();
crl.segments[1].signature = sign(&priv_key, &payload_b);
assert!(payload_b.len() > payload_a.len() + SEGMENT_SIGNATURE_LEN);
let bytes = crl.to_bytes();
let reparsed = ContentRevocationList::parse(&bytes).unwrap();
assert_eq!(reparsed, crl);
crl.verify_all_segments(&pub_key).unwrap();
crl.verify_last_segment_signature(&pub_key).unwrap();
assert!(crl.is_content_certificate_id_revoked(cc_id(0xA0)));
assert!(crl.is_content_certificate_id_revoked(cc_id(0xB0)));
}
#[test]
fn wrong_public_key_rejects_segment_signature() {
let (priv_key, _pub_key) = synth_la_keys();
let imposter = Point::generator().mul_scalar(&small_scalar(0xCAFE_F00D));
let crl = build_signed_one_segment(
&priv_key,
vec![RevocationRecord::ContentCertificateId {
range: 0,
id: cc_id(0x77),
}],
);
assert_eq!(
crl.verify_segment_signature(0, &imposter),
Err(AacsError::MkbSignatureInvalid)
);
}
#[test]
fn tampering_a_record_invalidates_segment_signature() {
let (priv_key, pub_key) = synth_la_keys();
let mut crl = build_signed_one_segment(
&priv_key,
vec![RevocationRecord::ContentCertificateId {
range: 0,
id: cc_id(0x88),
}],
);
crl.verify_segment_signature(0, &pub_key).unwrap();
if let RevocationRecord::ContentCertificateId { id, .. } = &mut crl.segments[0].records[0] {
id.0[3] ^= 0x80;
}
assert_eq!(
crl.verify_segment_signature(0, &pub_key),
Err(AacsError::MkbSignatureInvalid)
);
}
#[test]
fn invalid_list_type_rejected_via_header_round_trip() {
let bytes = [
0x10, 0x00, 0x01, 0x00, ];
assert!(matches!(
ContentRevocationList::parse(&bytes),
Err(AacsError::InvalidValue { .. })
));
}
#[test]
fn segment_size_too_small_rejected() {
let bytes = [
0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x04, ];
assert!(matches!(
ContentRevocationList::parse(&bytes),
Err(AacsError::InvalidValue { .. })
));
}
#[test]
fn first_segment_oversized_rejected() {
let mut bytes = Vec::new();
bytes.push(0x00); bytes.extend_from_slice(&1u16.to_be_bytes()); bytes.push(1); let oversized = 128u32 * 1024;
bytes.extend_from_slice(&oversized.to_be_bytes());
assert!(matches!(
ContentRevocationList::parse(&bytes),
Err(AacsError::InvalidValue { .. })
));
}
#[test]
fn trailing_zero_padding_is_tolerated() {
let (priv_key, _pub_key) = synth_la_keys();
let crl = build_signed_one_segment(
&priv_key,
vec![RevocationRecord::ContentCertificateId {
range: 0,
id: cc_id(0x55),
}],
);
let mut padded = crl.to_bytes();
padded.extend_from_slice(&[0u8; 17]); let reparsed = ContentRevocationList::parse(&padded).unwrap();
assert_eq!(reparsed, crl);
}
#[test]
fn trailing_non_zero_garbage_rejected() {
let (priv_key, _pub_key) = synth_la_keys();
let crl = build_signed_one_segment(
&priv_key,
vec![RevocationRecord::ContentCertificateId {
range: 0,
id: cc_id(0x56),
}],
);
let mut padded = crl.to_bytes();
padded.extend_from_slice(&[0xFF, 0x00, 0x00]);
assert!(matches!(
ContentRevocationList::parse(&padded),
Err(AacsError::InvalidValue { .. })
));
}
#[test]
fn rmrr_iccid_bit_round_trip() {
for iccid in [false, true] {
let rmrr = RecordableMediaRevocation {
iccid,
media_type: RecordableMediaType::HdDvdRecordable,
content_certificate_id: cc_id(0xCC),
media_id: [0; 16],
};
let bytes = encode_rmrr(&rmrr);
let decoded = decode_rmrr(
&bytes[..REVOCATION_RECORD_LEN],
&bytes[REVOCATION_RECORD_LEN..2 * REVOCATION_RECORD_LEN],
&bytes[2 * REVOCATION_RECORD_LEN..],
);
assert_eq!(decoded.iccid, iccid);
assert_eq!(decoded, rmrr);
}
}
#[test]
fn recordable_media_type_round_trip_including_reserved() {
for v in 0u8..=7 {
let mt = RecordableMediaType::from_u3(v);
assert_eq!(mt.to_u3(), v);
}
}
#[test]
fn signed_range_for_out_of_range_segment_returns_none() {
let (priv_key, _pub_key) = synth_la_keys();
let crl = build_signed_one_segment(&priv_key, vec![]);
assert!(crl.signed_range_for_segment(1).is_none());
assert!(matches!(
crl.verify_segment_signature(1, &Point::generator()),
Err(AacsError::InvalidValue { .. })
));
}
#[test]
fn rmrr_media_id_byte_layout_round_trip_full_pattern() {
let media_id = [
0x80, 0xAB, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
0x0D, 0x0E,
];
let rmrr = RecordableMediaRevocation {
iccid: false,
media_type: RecordableMediaType::PlusRecordable,
content_certificate_id: cc_id(0x71),
media_id,
};
let bytes = encode_rmrr(&rmrr);
let decoded = decode_rmrr(
&bytes[..REVOCATION_RECORD_LEN],
&bytes[REVOCATION_RECORD_LEN..2 * REVOCATION_RECORD_LEN],
&bytes[2 * REVOCATION_RECORD_LEN..],
);
assert_eq!(decoded.media_id, media_id);
assert_eq!(decoded, rmrr);
}
}