use std::sync::Arc;
use structured_asn1::Parser;
use structured_asn1::memory::{Memory, MemoryAccount};
use structured_cms::cades::{Role, profile_faults};
use structured_cms::{ErrorCode, SignedData, oid};
fn memory(work: usize) -> Memory {
Memory::new(Arc::new(MemoryAccount::new(1_048_576, work)))
}
fn tlv(tag: u8, contents: &[u8]) -> Vec<u8> {
let mut encoded = vec![tag];
let length = contents.len();
if length < 0x80 {
encoded.push(u8::try_from(length).unwrap());
} else if length < 0x100 {
encoded.extend([0x81, u8::try_from(length).unwrap()]);
} else {
encoded.push(0x82);
encoded.extend(u16::try_from(length).unwrap().to_be_bytes());
}
encoded.extend_from_slice(contents);
encoded
}
fn attribute(kind: &[u8], values: &[Vec<u8>]) -> Vec<u8> {
let mut sorted = values.to_vec();
sorted.sort();
tlv(0x30, &[tlv(6, kind), tlv(0x31, &sorted.concat())].concat())
}
fn message(signed: &[Vec<u8>], unsigned: &[Vec<u8>]) -> Vec<u8> {
let mut fields = vec![2, 1, 3, 0x80, 1, 7];
fields.extend(tlv(0x30, &tlv(6, oid::SHA256.as_bytes())));
if !signed.is_empty() {
let mut sorted = signed.to_vec();
sorted.sort();
fields.extend(tlv(0xa0, &sorted.concat()));
}
fields.extend(tlv(0x30, &tlv(6, &[0x2b, 0x65, 0x70])));
fields.extend(tlv(4, &[8]));
if !unsigned.is_empty() {
fields.extend(tlv(0xa1, &unsigned.concat()));
}
let mut inside = vec![2, 1, 3, 0x31, 0];
inside.extend(tlv(0x30, &tlv(6, oid::DATA.as_bytes())));
inside.extend(tlv(0x31, &tlv(0x30, &fields)));
let envelope = [
tlv(6, oid::SIGNED_DATA.as_bytes()),
tlv(0xa0, &tlv(0x30, &inside)),
]
.concat();
tlv(0x30, &envelope)
}
const NONE: [(ErrorCode, &str); 0] = [];
fn codes(
protected: &[Vec<u8>],
unsigned: &[Vec<u8>],
role: Role,
) -> Vec<(ErrorCode, &'static str)> {
let encoded = message(protected, unsigned);
let parser = Parser::ber(memory(1_000_000));
let value = SignedData::read(&parser, &encoded, 0).unwrap();
let query = parser.read(&[], 0).unwrap();
let info = &value.signers()[0];
profile_faults(info, role, &query)
.unwrap()
.iter()
.map(|fault| (fault.code(), fault.field()))
.collect()
}
fn token() -> Vec<u8> {
tlv(0x30, &tlv(6, oid::SIGNED_DATA.as_bytes()))
}
#[test]
fn attributes_in_their_profile_sets_with_one_value_raise_nothing() {
let signed = [
attribute(
oid::COMMITMENT_TYPE_INDICATION.as_bytes(),
&[tlv(0x30, &tlv(6, &[0x2a]))],
),
attribute(
oid::CONTENT_HINTS.as_bytes(),
&[tlv(0x30, &tlv(6, &[0x2a]))],
),
attribute(oid::SIGNER_LOCATION.as_bytes(), &[tlv(0x30, &[])]),
];
let unsigned = [
attribute(oid::SIGNATURE_TIMESTAMP.as_bytes(), &[token()]),
attribute(oid::ARCHIVE_TIMESTAMP_V3.as_bytes(), &[token()]),
attribute(oid::ARCHIVE_TIMESTAMP_V3.as_bytes(), &[token()]),
];
assert_eq!(codes(&signed, &unsigned, Role::Signature), NONE);
}
#[test]
fn an_attribute_in_the_other_set_is_misplaced() {
let signed = [attribute(oid::SIGNATURE_TIMESTAMP.as_bytes(), &[token()])];
let unsigned = [
attribute(
oid::COMMITMENT_TYPE_INDICATION.as_bytes(),
&[tlv(0x30, &tlv(6, &[0x2a]))],
),
attribute(oid::MIME_TYPE.as_bytes(), &[tlv(12, b"text/plain")]),
];
assert_eq!(
codes(&signed, &unsigned, Role::Signature),
[
(ErrorCode::AttributePlacement, "commitmentTypeIndication"),
(ErrorCode::AttributePlacement, "mimeType"),
(ErrorCode::AttributePlacement, "signatureTimeStamp"),
]
);
}
#[test]
fn a_single_valued_attribute_with_another_count_is_refused() {
let signed = [attribute(
oid::MIME_TYPE.as_bytes(),
&[tlv(12, b"text/plain"), tlv(12, b"text/html")],
)];
let unsigned = [
attribute(oid::ARCHIVE_TIMESTAMP_V3.as_bytes(), &[token(), token()]),
attribute(oid::SIGNATURE_TIMESTAMP.as_bytes(), &[]),
];
assert_eq!(
codes(&signed, &unsigned, Role::Signature),
[
(ErrorCode::AttributeCardinality, "mimeType"),
(ErrorCode::AttributeCardinality, "signatureTimeStamp"),
(ErrorCode::AttributeCardinality, "archiveTimeStampV3"),
]
);
}
#[test]
fn content_description_is_forbidden_only_within_a_countersignature() {
let signed = [
attribute(
oid::CONTENT_HINTS.as_bytes(),
&[tlv(0x30, &tlv(6, &[0x2a]))],
),
attribute(oid::MIME_TYPE.as_bytes(), &[tlv(12, b"text/plain")]),
];
assert_eq!(codes(&signed, &[], Role::Signature), NONE);
assert_eq!(
codes(&signed, &[], Role::Countersignature),
[
(ErrorCode::AttributePlacement, "contentHints"),
(ErrorCode::AttributePlacement, "mimeType"),
]
);
}
#[test]
fn an_implied_signature_policy_is_refused() {
let implied = [attribute(
oid::SIGNATURE_POLICY_IDENTIFIER.as_bytes(),
&[vec![5, 0]],
)];
assert_eq!(
codes(&implied, &[], Role::Signature),
[(
ErrorCode::AttributeSyntax,
"signaturePolicyIdentifier.signaturePolicyImplied"
)]
);
let explicit = [attribute(
oid::SIGNATURE_POLICY_IDENTIFIER.as_bytes(),
&[tlv(0x30, &tlv(6, &[0x2a]))],
)];
assert_eq!(codes(&explicit, &[], Role::Signature), NONE);
}
#[test]
fn ess_signing_certificate_policies_are_refused() {
let identifier = tlv(0x30, &tlv(4, &[0; 32]));
let policies = tlv(0x30, &tlv(0x30, &tlv(6, &[0x55, 0x1d, 0x20, 0])));
let with = [attribute(
oid::SIGNING_CERTIFICATE_V2.as_bytes(),
&[tlv(0x30, &[tlv(0x30, &identifier), policies].concat())],
)];
assert_eq!(
codes(&with, &[], Role::Signature),
[(ErrorCode::AttributeSyntax, "signingCertificateV2.policies")]
);
let without = [attribute(
oid::SIGNING_CERTIFICATE_V2.as_bytes(),
&[tlv(0x30, &tlv(0x30, &identifier))],
)];
assert_eq!(codes(&without, &[], Role::Signature), NONE);
}
#[test]
fn a_spent_work_ceiling_refuses_instead_of_reporting_less() {
let protected = [attribute(
oid::MIME_TYPE.as_bytes(),
&[tlv(12, b"a"), tlv(12, b"b")],
)];
let encoded = message(&protected, &[]);
let account = memory(1_000_000);
let parser = Parser::ber(account.clone());
let value = SignedData::read(&parser, &encoded, 0).unwrap();
let query = parser.read(&[], 0).unwrap();
let info = &value.signers()[0];
let before = account.work_used();
assert_eq!(
profile_faults(info, Role::Signature, &query).unwrap().len(),
1
);
let needed = account.work_used() - before;
let spent = memory(1_000_000);
let parser = Parser::ber(spent.clone());
let value = SignedData::read(&parser, &encoded, 0).unwrap();
let query = parser.read(&[], 0).unwrap();
let info = &value.signers()[0];
spent
.charge_work((spent.work_remaining() - (needed - 1)) as u64)
.unwrap();
let error = profile_faults(info, Role::Signature, &query).unwrap_err();
assert!(error.is_resource());
}
#[test]
fn ess_content_attributes_carry_exactly_one_value() {
let signed = [
attribute(
oid::CONTENT_HINTS.as_bytes(),
&[tlv(0x30, &tlv(6, &[0x2a])), tlv(0x30, &tlv(6, &[0x2b]))],
),
attribute(oid::CONTENT_IDENTIFIER.as_bytes(), &[]),
attribute(
oid::CONTENT_REFERENCE.as_bytes(),
&[tlv(0x30, &[]), tlv(0x30, &[0x05, 0])],
),
];
assert_eq!(
codes(&signed, &[], Role::Signature),
[
(ErrorCode::AttributeCardinality, "contentHints"),
(ErrorCode::AttributeCardinality, "contentReference"),
(ErrorCode::AttributeCardinality, "contentIdentifier"),
]
);
}
#[test]
fn a_repeated_ess_content_attribute_is_refused() {
let reference = attribute(
oid::CONTENT_REFERENCE.as_bytes(),
&[tlv(0x30, &tlv(6, &[0x2a]))],
);
let identifier = attribute(oid::CONTENT_IDENTIFIER.as_bytes(), &[tlv(4, &[1])]);
let other = attribute(oid::CONTENT_IDENTIFIER.as_bytes(), &[tlv(4, &[2])]);
assert_eq!(
codes(
&[reference.clone(), reference, identifier, other],
&[],
Role::Signature
),
[
(ErrorCode::DuplicateAttribute, "contentReference"),
(ErrorCode::DuplicateAttribute, "contentIdentifier"),
]
);
let location = attribute(oid::SIGNER_LOCATION.as_bytes(), &[tlv(0x30, &[])]);
assert_eq!(
codes(&[location.clone(), location], &[], Role::Signature),
NONE
);
}
#[test]
fn a_claimed_saml_assertion_belongs_only_inside_signer_attributes() {
let saml = attribute(oid::CLAIMED_SAML_ASSERTION.as_bytes(), &[tlv(12, b"<a/>")]);
assert_eq!(
codes(
std::slice::from_ref(&saml),
std::slice::from_ref(&saml),
Role::Signature
),
[
(ErrorCode::AttributePlacement, "claimedSamlAssertion"),
(ErrorCode::AttributePlacement, "claimedSamlAssertion"),
]
);
}
#[test]
fn a_repeated_ats_hash_index_is_refused() {
let index = attribute(oid::ATS_HASH_INDEX_V3.as_bytes(), &[tlv(0x30, &[])]);
assert_eq!(
codes(&[], &[index.clone(), index], Role::ArchiveTimestamp),
[(ErrorCode::DuplicateAttribute, "atsHashIndexV3")]
);
}
#[test]
fn the_hash_index_belongs_only_to_an_archive_timestamp_signer() {
let index = attribute(oid::ATS_HASH_INDEX_V3.as_bytes(), &[tlv(0x30, &[])]);
let unsigned = std::slice::from_ref(&index);
assert_eq!(codes(&[], unsigned, Role::ArchiveTimestamp), NONE);
for role in [Role::Signature, Role::Countersignature] {
assert_eq!(
codes(&[], unsigned, role),
[(ErrorCode::AttributePlacement, "atsHashIndexV3")]
);
}
}
#[test]
fn every_policy_value_inspected_is_paid_for() {
let used = |values: usize, arcs: usize| {
let protected = [attribute(
oid::SIGNATURE_POLICY_IDENTIFIER.as_bytes(),
&(0..values)
.map(|ordinal| {
let mut identifier = vec![0x2a, u8::try_from(ordinal).unwrap()];
identifier.resize(2 + arcs, 1);
tlv(0x30, &tlv(6, &identifier))
})
.collect::<Vec<_>>(),
)];
let encoded = message(&protected, &[]);
let account = memory(1_000_000);
let parser = Parser::ber(account.clone());
let value = SignedData::read(&parser, &encoded, 0).unwrap();
let query = parser.read(&[], 0).unwrap();
let before = account.work_used();
profile_faults(&value.signers()[0], Role::Signature, &query).unwrap();
account.work_used() - before
};
let fewer = used(30, 6);
let more = used(60, 0);
assert!(
more >= fewer + 30,
"{more} work for 60 values, {fewer} for 30 of the same bytes"
);
}
#[test]
fn a_foreign_reader_is_refused_before_inspection() {
let encoded = message(
&[attribute(oid::MIME_TYPE.as_bytes(), &[tlv(12, b"a")])],
&[],
);
let parser = Parser::ber(memory(1_000_000));
let value = SignedData::read(&parser, &encoded, 0).unwrap();
let info = &value.signers()[0];
let foreign = Parser::ber(memory(1_000_000));
let other = foreign.read(&[], 0).unwrap();
let error = profile_faults(info, Role::Signature, &other).unwrap_err();
assert_eq!(
error.code(),
ErrorCode::Pkix(structured_pkix::ErrorCode::ForeignReader)
);
}