cadmpeg-codec-step 0.5.5

Read and write ISO 10303-21 STEP Part 21 files for AP203, AP214, and AP242.
Documentation
// SPDX-License-Identifier: Apache-2.0
#![allow(clippy::unwrap_used)]
#![allow(clippy::default_trait_access)]
use super::validate_detached_cms;
use base64::{engine::general_purpose::STANDARD, Engine as _};

#[test]
fn accepts_a_minimal_detached_signed_data_envelope() {
    let cms = [
        0x30, 0x5a, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02, 0xa0, 0x4d,
        0x30, 0x4b, 0x02, 0x01, 0x01, 0x31, 0x0d, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
        0x65, 0x03, 0x04, 0x02, 0x01, 0x30, 0x0b, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d,
        0x01, 0x07, 0x01, 0x31, 0x2a, 0x30, 0x28, 0x02, 0x01, 0x01, 0x30, 0x05, 0x30, 0x00, 0x02,
        0x01, 0x01, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
        0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00,
        0x04, 0x00,
    ];
    assert_eq!(validate_detached_cms(&cms), Ok(()));
}

const BER_CMS_INDEFINITE: &[u8] = &[
    0x30, 0x80, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02, 0xa0, 0x80, 0x30,
    0x80, 0x02, 0x01, 0x01, 0x31, 0x80, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03,
    0x04, 0x02, 0x01, 0x00, 0x00, 0x30, 0x80, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01,
    0x07, 0x01, 0x00, 0x00, 0x31, 0x80, 0x30, 0x80, 0x02, 0x01, 0x01, 0x30, 0x80, 0x30, 0x00, 0x02,
    0x01, 0x01, 0x00, 0x00, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02,
    0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00,
    0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];

#[test]
fn accepts_cms_ber_indefinite_lengths() {
    assert_eq!(validate_detached_cms(BER_CMS_INDEFINITE), Ok(()));
}

#[test]
fn accepts_ber_contextual_subject_key_identifier_and_octet_string() {
    assert_eq!(
        super::validate_signer_identifier(0x80, &[0x01, 0x02, 0x03]),
        Ok(())
    );
    assert_eq!(
        super::validate_octet_string(0x24, &[0x04, 0x01, 0xaa]),
        Ok(())
    );
}

#[test]
fn parser_retains_base64_encoded_ber_cms() {
    let payload = STANDARD.encode(BER_CMS_INDEFINITE);
    let source = format!(
        "ISO-10303-21;HEADER;FILE_DESCRIPTION(('BER CMS'),'4;2');FILE_NAME('','',(''),(''),'','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=ITEM();ENDSEC;END-ISO-10303-21;SIGNATURE;{payload}\nENDSEC;"
    );
    let (exchange, diagnostics) = crate::parse::parse(source.as_bytes()).expect("BER CMS witness");

    assert!(diagnostics.is_empty());
    assert_eq!(exchange.signature_sections[0].cms, BER_CMS_INDEFINITE);
}

#[test]
fn rejects_embedded_content() {
    let cms = [
        0x30, 0x5c, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02, 0xa0, 0x4f,
        0x30, 0x4d, 0x02, 0x01, 0x01, 0x31, 0x0d, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
        0x65, 0x03, 0x04, 0x02, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d,
        0x01, 0x07, 0x01, 0xa0, 0x02, 0x04, 0x00, 0x31, 0x2a, 0x30, 0x28, 0x02, 0x01, 0x01, 0x30,
        0x05, 0x30, 0x00, 0x02, 0x01, 0x01, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65,
        0x03, 0x04, 0x02, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01,
        0x01, 0x01, 0x05, 0x00, 0x04, 0x00,
    ];
    assert!(validate_detached_cms(&cms).is_err());
}

#[test]
fn parser_retains_multiple_signature_sections_after_exchange_terminator() {
    let source = b"ISO-10303-21;HEADER;FILE_DESCRIPTION(('signatures'),'4;2');FILE_NAME('','',(''),(''),'','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=ITEM();ENDSEC;END-ISO-10303-21;SIGNATURE;MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\nENDSEC;SIGNATURE;MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\nENDSEC;";
    let (exchange, diagnostics) = crate::parse::parse(source).expect("multiple signatures");

    assert!(diagnostics.is_empty());
    assert_eq!(exchange.signatures.len(), 2);
    assert!(source[exchange.signatures[0].clone()]
        .windows(b"MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=".len())
        .any(|bytes| bytes == b"MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA="));
    assert!(source[exchange.signatures[1].clone()]
        .windows(b"MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=".len())
        .any(|bytes| bytes == b"MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA="));
    assert_eq!(exchange.signature_sections.len(), 2);
    let first = &exchange.signature_sections[0];
    let second = &exchange.signature_sections[1];
    assert_eq!(first.signed.end, first.span.start);
    assert_eq!(second.signed.end, second.span.start);
    let first_section = &source[first.span.clone()];
    let second_signed = &source[second.signed.clone()];
    assert!(second_signed
        .windows(first_section.len())
        .any(|window| window == first_section));
}

#[test]
fn parser_accepts_signature_edge_separators_and_keeps_each_boundary() {
    let payload = "MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=";
    let source = format!(
        "ISO-10303-21;HEADER;FILE_DESCRIPTION(('signatures'),'4;2');FILE_NAME('','',(''),(''),'','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=ITEM();ENDSEC;END-ISO-10303-21;SIGNATURE;/* decoy ENDSEC; */\\N\\{payload}\\F\\/* trailing ENDSEC; */EN\nDSEC;SIGNATURE;\\F\\{payload}\\N\\ENDSEC;"
    );
    let (exchange, diagnostics) = crate::parse::parse(source.as_bytes()).expect("edge separators");

    assert!(diagnostics.is_empty());
    assert_eq!(exchange.signature_sections.len(), 2);
    assert_eq!(exchange.signatures.len(), 2);
    assert!(source.as_bytes()[exchange.signatures[0].clone()].ends_with(b"DSEC;"));
    assert!(source.as_bytes()[exchange.signatures[0].clone()]
        .windows(b"EN\nDSEC;".len())
        .any(|bytes| bytes == b"EN\nDSEC;"));
    assert!(source.as_bytes()[exchange.signatures[1].clone()].ends_with(b"ENDSEC;"));
    assert_eq!(
        exchange.signature_sections[0].signed.end,
        exchange.signatures[0].start
    );
    assert_eq!(
        exchange.signature_sections[1].signed.end,
        exchange.signatures[1].start
    );
    assert_eq!(exchange.signatures[1].start, exchange.signatures[0].end);
    assert_eq!(exchange.signature_sections[0].cms.len(), 92);
    assert_eq!(exchange.signature_sections[1].cms.len(), 92);
}

#[test]
fn parser_does_not_treat_unseparated_endsec_text_as_a_signature_boundary() {
    let payload = "MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=";
    let source = format!(
        "ISO-10303-21;HEADER;FILE_DESCRIPTION(('signature'),'4;2');FILE_NAME('','',(''),(''),'','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=ITEM();ENDSEC;END-ISO-10303-21;SIGNATURE;{payload}ENDSEC;AAAA\nENDSEC;"
    );
    let error = crate::parse::parse(source.as_bytes()).expect_err("unseparated ENDSEC text");

    assert!(matches!(
        error,
        crate::parse::ParseError::Lex(crate::lex::LexError { message, .. })
            if message == "invalid SIGNATURE base64 padding"
    ));
}

#[test]
fn parser_exposes_the_detached_signature_contract() {
    let source = b" /* leading trivia */ ISO-10303-21;HEADER;FILE_DESCRIPTION(('signature'),'4;2');FILE_NAME('','',(''),(''),'','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=ITEM();ENDSEC;END-ISO-10303-21;SIGNATURE;MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\nENDSEC;";
    let (exchange, diagnostics) = crate::parse::parse(source).expect("signature contract");

    assert!(diagnostics.is_empty());
    let section = &exchange.signature_sections[0];
    let signed = &source[section.signed.clone()];
    assert!(signed.starts_with(b"ISO-10303-21;"));
    assert!(signed.ends_with(b"END-ISO-10303-21;"));
    assert_eq!(
        &source[section.payload.clone()],
        b"MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\n"
    );
    assert_eq!(section.cms.len(), 92);
    let signed_alphabet = section
        .signed_alphabet_bytes(source)
        .expect("signed source range");
    assert!(!signed_alphabet.contains(&b'\n'));
    assert!(signed_alphabet.ends_with(b"END-ISO-10303-21;"));
    assert_eq!(
        &source[section.span.clone()],
        b"SIGNATURE;MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\nENDSEC;"
    );
}

#[test]
fn parser_does_not_verify_detached_content_during_structural_admission() {
    let source = include_bytes!("tests/data/sg01_signature_method_selection.p21");
    let (original, original_diagnostics) = crate::parse::parse(source).expect("original signature");
    let mut tampered_source = source.to_vec();
    let marker = b"SG-01 CMS method witness";
    let marker_start = tampered_source
        .windows(marker.len())
        .position(|window| window == marker)
        .expect("signature witness marker");
    tampered_source[marker_start] = b'T';
    let (tampered, tampered_diagnostics) =
        crate::parse::parse(&tampered_source).expect("structural signature admission");

    assert!(original_diagnostics.is_empty());
    assert!(tampered_diagnostics.is_empty());
    assert_eq!(
        original.signature_sections[0].cms,
        tampered.signature_sections[0].cms
    );
    assert_ne!(
        original.signature_sections[0]
            .signed_alphabet_bytes(source)
            .expect("original signed content"),
        tampered.signature_sections[0]
            .signed_alphabet_bytes(&tampered_source)
            .expect("tampered signed content")
    );
}

#[test]
fn real_detached_cms_witness_remains_structural_after_source_tampering() {
    let source = include_bytes!("tests/data/sg04_openssl_detached.p21");
    let (original, original_diagnostics) = crate::parse::parse(source).expect("real CMS witness");
    let mut tampered_source = source.to_vec();
    let marker = b"SG-04 OpenSSL detached CMS witness";
    let marker_start = tampered_source
        .windows(marker.len())
        .position(|window| window == marker)
        .expect("real CMS witness marker");
    tampered_source[marker_start] = b'X';
    let (tampered, tampered_diagnostics) =
        crate::parse::parse(&tampered_source).expect("tampered CMS structure");

    assert!(original_diagnostics.is_empty());
    assert!(tampered_diagnostics.is_empty());
    assert_eq!(original.signature_sections[0].cms.len(), 1324);
    assert_eq!(
        super::validate_detached_cms(&original.signature_sections[0].cms),
        Ok(())
    );
    assert_eq!(
        original.signature_sections[0].cms,
        tampered.signature_sections[0].cms
    );
    assert_ne!(
        original.signature_sections[0]
            .signed_alphabet_bytes(source)
            .expect("original signed content"),
        tampered.signature_sections[0]
            .signed_alphabet_bytes(&tampered_source)
            .expect("tampered signed content")
    );
}

#[test]
fn signature_method_and_parameters_are_inside_cms_payload() {
    let source = include_bytes!("tests/data/sg01_signature_method_selection.p21");
    let (exchange, diagnostics) = crate::parse::parse(source).expect("signature method witness");

    assert!(diagnostics.is_empty());
    assert_eq!(exchange.signature_sections.len(), 1);
    let section = &exchange.signature_sections[0];
    assert_eq!(
        &source[section.span.clone()],
        b"SIGNATURE;\nMFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\nENDSEC;"
    );
    assert_eq!(
        &source[section.span.start..section.payload.start],
        b"SIGNATURE;"
    );
    assert_eq!(&source[section.payload.end..section.span.end], b"ENDSEC;");
    assert_eq!(section.cms.len(), 92);
}

#[test]
fn parser_projects_each_signature_to_preceding_alphabet_bytes() {
    let source = include_bytes!("tests/data/sg02_signed_byte_sequence.p21");
    let (exchange, diagnostics) = crate::parse::parse(source).expect("signed byte witness");

    assert!(diagnostics.is_empty());
    assert_eq!(exchange.signature_sections.len(), 2);
    let first = &exchange.signature_sections[0];
    let second = &exchange.signature_sections[1];
    assert_eq!(first.signed.start, 0);
    assert_eq!(first.signed.end, first.span.start);
    assert_eq!(second.signed.start, 0);
    assert_eq!(second.signed.end, second.span.start);
    assert_eq!(
        first.signed_alphabet_bytes(source).expect("first signed bytes"),
        b"ISO-10303-21;HEADER;FILE_DESCRIPTION(('SG-02 signed byte sequence witness'),'4;2');FILE_NAME('sg02_signed_byte_sequence','2026-08-16T00:00:00',('cadmpeg'),('cadmpeg'),'cadmpeg-step','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=PRODUCT('P1','Part','',());#2=(LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.));#3=(NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.));#4=(GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNIT_ASSIGNED_CONTEXT((#2,#3)) REPRESENTATION_CONTEXT('model','3D'));ENDSEC;END-ISO-10303-21;/*FIRST*/\\N\\"
    );
    let mut expected_second = first
        .signed_alphabet_bytes(source)
        .expect("first signed bytes")
        .clone();
    expected_second.extend_from_slice(
        b"SIGNATURE;MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=ENDSEC;/*SECOND*/\\F\\",
    );
    assert_eq!(
        second
            .signed_alphabet_bytes(source)
            .expect("second signed bytes"),
        expected_second
    );
    assert_eq!(
        &source[first.span.clone()],
        b"SIGNATURE;\nMFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\nENDSEC;"
    );
}

#[test]
fn parser_ignores_controls_inside_signature_terminators() {
    let source = b"ISO-10303-21;HEADER;FILE_DESCRIPTION(('signature'),'4;2');FILE_NAME('','',(''),(''),'','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=ITEM();ENDSEC;END-ISO-10303-21;SIGNATURE;MFoGCSqGSIb3DQEHAqBNMEsCAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBMSowKAIBATAFMAACAQEwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABAA=\nEN\nDSEC;";
    let (exchange, _) = crate::parse::parse(source).expect("split signature terminator");
    assert_eq!(exchange.signatures.len(), 1);
}

#[test]
fn parser_rejects_invalid_signature_base64() {
    for (payload, expected_message) in [
        ("YWJjZA==!", "invalid SIGNATURE base64 padding"),
        ("YWJjZA==AAAA", "invalid SIGNATURE base64 padding"),
        ("YWJjZA=", "SIGNATURE base64 content has incomplete quantum"),
        ("YWJj ZA==", "invalid SIGNATURE base64 character"),
    ] {
        let source = format!(
            "ISO-10303-21;HEADER;FILE_DESCRIPTION(('signature'),'4;2');FILE_NAME('','',(''),(''),'','','');FILE_SCHEMA(('AP242'));ENDSEC;DATA;#1=ITEM();ENDSEC;END-ISO-10303-21;SIGNATURE;{payload}\nENDSEC;"
        );
        let error = crate::parse::parse(source.as_bytes()).expect_err("invalid signature");
        assert!(matches!(
            error,
            crate::parse::ParseError::Lex(crate::lex::LexError { message, .. })
                if message == expected_message
        ));
    }
}