const OID_PKCS7_DATA: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01];
const OID_PKCS7_SIGNED_DATA: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02];
const TAG_OCTET_STRING: u8 = 0x04;
const TAG_OID: u8 = 0x06;
const TAG_SEQUENCE: u8 = 0x10;
#[cfg(test)]
const DER_SEQUENCE_BYTE: u8 = 0x30;
const MAX_DEPTH: u8 = 24;
#[must_use]
pub(crate) fn is_signed_data(bytes: &[u8]) -> bool {
let Some(start) = bytes.iter().position(|byte| !byte.is_ascii_whitespace()) else {
return false;
};
let bytes = &bytes[start..];
let Some(outer) = read(bytes, 0) else {
return false;
};
if outer.tag != TAG_SEQUENCE || !outer.constructed {
return false;
}
read(outer.contents, 0)
.is_some_and(|first| first.tag == TAG_OID && first.contents == OID_PKCS7_SIGNED_DATA)
}
struct Element<'a> {
tag: u8,
constructed: bool,
contents: &'a [u8],
end: usize,
}
fn read(bytes: &[u8], offset: usize) -> Option<Element<'_>> {
let tag = *bytes.get(offset)?;
if tag & 0x1f == 0x1f {
return None;
}
let first_length = *bytes.get(offset + 1)?;
let (length, header) = if first_length & 0x80 == 0 {
(usize::from(first_length), 2)
} else {
let count = usize::from(first_length & 0x7f);
if count == 0 || count > 4 {
return None;
}
let mut length = 0usize;
for index in 0..count {
let byte = *bytes.get(offset + 2 + index)?;
length = length.checked_mul(256)?.checked_add(usize::from(byte))?;
}
(length, 2 + count)
};
let start = offset.checked_add(header)?;
let end = start.checked_add(length)?;
if end > bytes.len() {
return None;
}
Some(Element {
tag: tag & 0x1f,
constructed: tag & 0x20 != 0,
contents: &bytes[start..end],
end,
})
}
fn octet_string(element: &Element<'_>, depth: u8) -> Option<Vec<u8>> {
if !element.constructed {
return Some(element.contents.to_vec());
}
if depth == 0 {
return None;
}
let mut out = Vec::new();
let mut offset = 0;
while offset < element.contents.len() {
let child = read(element.contents, offset)?;
if child.tag != TAG_OCTET_STRING {
return None;
}
out.extend_from_slice(&octet_string(&child, depth - 1)?);
offset = child.end;
}
Some(out)
}
fn find_payload(bytes: &[u8], depth: u8) -> Option<Vec<u8>> {
if depth == 0 {
return None;
}
let mut offset = 0;
while offset < bytes.len() {
let element = read(bytes, offset)?;
if element.tag == TAG_SEQUENCE && element.constructed {
if let Some(first) = read(element.contents, 0) {
if first.tag == TAG_OID && first.contents == OID_PKCS7_DATA {
if let Some(wrapper) = read(element.contents, first.end) {
if let Some(inner) = read(wrapper.contents, 0) {
if inner.tag == TAG_OCTET_STRING {
if let Some(payload) = octet_string(&inner, MAX_DEPTH) {
if !payload.is_empty() {
return Some(payload);
}
}
}
}
}
}
}
}
if element.constructed {
if let Some(found) = find_payload(element.contents, depth - 1) {
return Some(found);
}
}
offset = element.end;
}
None
}
#[must_use]
pub(crate) fn extract_payload(bytes: &[u8]) -> Option<Vec<u8>> {
let start = bytes.iter().position(|byte| !byte.is_ascii_whitespace())?;
find_payload(&bytes[start..], MAX_DEPTH)
}
#[cfg(test)]
mod tests {
use super::*;
fn wrap(payload: &[u8]) -> Vec<u8> {
fn tlv(tag: u8, contents: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
let length = contents.len();
if length < 0x80 {
#[allow(clippy::cast_possible_truncation)]
out.push(length as u8);
} else if length < 0x100 {
#[allow(clippy::cast_possible_truncation)]
out.extend_from_slice(&[0x81, length as u8]);
} else {
#[allow(clippy::cast_possible_truncation)]
out.extend_from_slice(&[0x82, (length >> 8) as u8, (length & 0xff) as u8]);
}
out.extend_from_slice(contents);
out
}
let econtent = tlv(0xa0, &tlv(TAG_OCTET_STRING, payload));
let mut encap = tlv(TAG_OID, OID_PKCS7_DATA);
encap.extend_from_slice(&econtent);
let encap = tlv(DER_SEQUENCE_BYTE, &encap);
let mut signed_data = tlv(0x02, &[0x01]); signed_data.extend_from_slice(&tlv(0x31, &[])); signed_data.extend_from_slice(&encap);
let signed_data = tlv(DER_SEQUENCE_BYTE, &signed_data);
let mut content_info = tlv(
TAG_OID,
&[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02],
);
content_info.extend_from_slice(&tlv(0xa0, &signed_data));
tlv(DER_SEQUENCE_BYTE, &content_info)
}
#[test]
fn json_is_not_mistaken_for_signed_data() {
assert!(!is_signed_data(br#"{"applinks":{}}"#));
assert!(!is_signed_data(b" \n{}"));
assert!(!is_signed_data(b""));
assert!(!is_signed_data(b"0"));
assert!(!is_signed_data(b"01"));
assert!(!is_signed_data(b"0.5"));
assert!(!is_signed_data(&[
0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02
]));
}
#[test]
fn a_signed_payload_round_trips() {
let payload = br#"{"applinks":{"details":[]}}"#;
let signed = wrap(payload);
assert!(is_signed_data(&signed));
assert_eq!(extract_payload(&signed).as_deref(), Some(&payload[..]));
}
#[test]
fn a_long_payload_round_trips() {
let payload = format!(r#"{{"comment":"{}"}}"#, "x".repeat(1000));
let signed = wrap(payload.as_bytes());
assert_eq!(
extract_payload(&signed).as_deref(),
Some(payload.as_bytes())
);
}
#[test]
fn malformed_der_never_panics() {
let payload = wrap(br#"{"a":1}"#);
for cut in 0..payload.len() {
let _ = extract_payload(&payload[..cut]);
}
for index in 0..payload.len() {
let mut damaged = payload.clone();
damaged[index] ^= 0xff;
let _ = extract_payload(&damaged);
}
let _ = extract_payload(&[0x30, 0x80]);
let _ = extract_payload(&[0x30, 0x84, 0xff, 0xff, 0xff, 0xff]);
}
}