diff --git a/model.txt b/model.txt new file mode 100644 index 0000000..2bda718 --- /dev/null +++ b/model.txt @@ -0,0 +1,2734 @@ + +running 1 test +== TAL / TA / TrustAnchor == +Fixture (TAL): tests/fixtures/tal/ripe-ncc.tal +Fixture (TA): tests/fixtures/ta/ripe-ncc-ta.cer +TA.verify_self_signature=Ok(()) +TalPretty { + raw: BytesFmt { + len: 482, + sha256_hex: "59ca27ef93f23682749fcefe7c6d70fbc723343549ff9e4d3996acaff79817fb", + head_hex: "68747470733a2f2f72706b692e726970", + tail_hex: "67424d794c320a56774944415141420a", + }, + comments: [], + ta_uris: [ + "https://rpki.ripe.net/ta/ripe-ncc-ta.cer", + "rsync://rpki.ripe.net/ta/ripe-ncc-ta.cer", + ], + subject_public_key_info_der: BytesFmt { + len: 294, + sha256_hex: "5e22b2daa07f1a6b78d2f81b0ca5e06eafc2a9c817d1edfc78021522a987b34e", + head_hex: "30820122300d06092a864886f70d0101", + tail_hex: "b13e03a38b78013322f6570203010001", + }, +} +TaCertificatePretty { + raw_der: BytesFmt { + len: 1036, + sha256_hex: "ff8b6776d2525ecf4fba789c61b919d352a59d651ac596a4f006cfc91bdb9150", + head_hex: "30820408308202f0a003020102020201", + tail_hex: "00b2f02145586bce21fa17db1da60872", + }, + rc_ca: ResourceCertificatePretty { + raw_der: BytesFmt { + len: 1036, + sha256_hex: "ff8b6776d2525ecf4fba789c61b919d352a59d651ac596a4f006cfc91bdb9150", + head_hex: "30820408308202f0a003020102020201", + tail_hex: "00b2f02145586bce21fa17db1da60872", + }, + tbs: RpkixTbsCertificatePretty { + version: 2, + serial_number: "011e", + signature_algorithm: "1.2.840.113549.1.1.11", + issuer_dn: "CN=ripe-ncc-ta", + subject_dn: "CN=ripe-ncc-ta", + validity_not_before: 2026-01-14 10:50:01.0 +00:00:00, + validity_not_after: 2026-04-14 10:50:01.0 +00:00:00, + subject_public_key_info: BytesFmt { + len: 294, + sha256_hex: "5e22b2daa07f1a6b78d2f81b0ca5e06eafc2a9c817d1edfc78021522a987b34e", + head_hex: "30820122300d06092a864886f70d0101", + tail_hex: "b13e03a38b78013322f6570203010001", + }, + extensions: RcExtensionsPretty { + basic_constraints_ca: true, + subject_key_identifier: Some( + BytesFmt { + len: 20, + sha256_hex: "7ab8fc2dc07908f8a95c22bc4dd168fed02cc3217f37797047fcbf4a949d82f1", + head_hex: "e8552b1fd6d1a4f7e404c6d8e5680d1e", + tail_hex: "d6d1a4f7e404c6d8e5680d1ebc163fc3", + }, + ), + subject_info_access: Some( + Ca( + SubjectInfoAccessCa { + access_descriptions: [ + AccessDescription { + access_method_oid: "1.3.6.1.5.5.7.48.5", + access_location: Url { + scheme: "rsync", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "rpki.ripe.net", + ), + ), + port: None, + path: "/repository/", + query: None, + fragment: None, + }, + }, + AccessDescription { + access_method_oid: "1.3.6.1.5.5.7.48.10", + access_location: Url { + scheme: "rsync", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "rpki.ripe.net", + ), + ), + port: None, + path: "/repository/ripe-ncc-ta.mft", + query: None, + fragment: None, + }, + }, + AccessDescription { + access_method_oid: "1.3.6.1.5.5.7.48.13", + access_location: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "rrdp.ripe.net", + ), + ), + port: None, + path: "/notification.xml", + query: None, + fragment: None, + }, + }, + ], + }, + ), + ), + certificate_policies_oid: Some( + "1.3.6.1.5.5.7.14.2", + ), + ip_resources: Some( + IpResourceSet { + families: [ + IpAddressFamily { + afi: Ipv4, + choice: AddressesOrRanges( + [ + Prefix( + IpPrefix { + afi: Ipv4, + prefix_len: 0, + addr: [ + 0, + 0, + 0, + 0, + ], + }, + ), + ], + ), + }, + IpAddressFamily { + afi: Ipv6, + choice: AddressesOrRanges( + [ + Prefix( + IpPrefix { + afi: Ipv6, + prefix_len: 0, + addr: [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + ), + ], + ), + }, + ], + }, + ), + as_resources: Some( + AsResourceSet { + asnum: Some( + AsIdsOrRanges( + [ + Range { + min: 0, + max: 4294967295, + }, + ], + ), + ), + rdi: None, + }, + ), + }, + }, + kind: Ca, + }, +} +TrustAnchorPretty { + tal: TalPretty { + raw: BytesFmt { + len: 482, + sha256_hex: "59ca27ef93f23682749fcefe7c6d70fbc723343549ff9e4d3996acaff79817fb", + head_hex: "68747470733a2f2f72706b692e726970", + tail_hex: "67424d794c320a56774944415141420a", + }, + comments: [], + ta_uris: [ + "https://rpki.ripe.net/ta/ripe-ncc-ta.cer", + "rsync://rpki.ripe.net/ta/ripe-ncc-ta.cer", + ], + subject_public_key_info_der: BytesFmt { + len: 294, + sha256_hex: "5e22b2daa07f1a6b78d2f81b0ca5e06eafc2a9c817d1edfc78021522a987b34e", + head_hex: "30820122300d06092a864886f70d0101", + tail_hex: "b13e03a38b78013322f6570203010001", + }, + }, + ta_certificate: TaCertificatePretty { + raw_der: BytesFmt { + len: 1036, + sha256_hex: "ff8b6776d2525ecf4fba789c61b919d352a59d651ac596a4f006cfc91bdb9150", + head_hex: "30820408308202f0a003020102020201", + tail_hex: "00b2f02145586bce21fa17db1da60872", + }, + rc_ca: ResourceCertificatePretty { + raw_der: BytesFmt { + len: 1036, + sha256_hex: "ff8b6776d2525ecf4fba789c61b919d352a59d651ac596a4f006cfc91bdb9150", + head_hex: "30820408308202f0a003020102020201", + tail_hex: "00b2f02145586bce21fa17db1da60872", + }, + tbs: RpkixTbsCertificatePretty { + version: 2, + serial_number: "011e", + signature_algorithm: "1.2.840.113549.1.1.11", + issuer_dn: "CN=ripe-ncc-ta", + subject_dn: "CN=ripe-ncc-ta", + validity_not_before: 2026-01-14 10:50:01.0 +00:00:00, + validity_not_after: 2026-04-14 10:50:01.0 +00:00:00, + subject_public_key_info: BytesFmt { + len: 294, + sha256_hex: "5e22b2daa07f1a6b78d2f81b0ca5e06eafc2a9c817d1edfc78021522a987b34e", + head_hex: "30820122300d06092a864886f70d0101", + tail_hex: "b13e03a38b78013322f6570203010001", + }, + extensions: RcExtensionsPretty { + basic_constraints_ca: true, + subject_key_identifier: Some( + BytesFmt { + len: 20, + sha256_hex: "7ab8fc2dc07908f8a95c22bc4dd168fed02cc3217f37797047fcbf4a949d82f1", + head_hex: "e8552b1fd6d1a4f7e404c6d8e5680d1e", + tail_hex: "d6d1a4f7e404c6d8e5680d1ebc163fc3", + }, + ), + subject_info_access: Some( + Ca( + SubjectInfoAccessCa { + access_descriptions: [ + AccessDescription { + access_method_oid: "1.3.6.1.5.5.7.48.5", + access_location: Url { + scheme: "rsync", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "rpki.ripe.net", + ), + ), + port: None, + path: "/repository/", + query: None, + fragment: None, + }, + }, + AccessDescription { + access_method_oid: "1.3.6.1.5.5.7.48.10", + access_location: Url { + scheme: "rsync", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "rpki.ripe.net", + ), + ), + port: None, + path: "/repository/ripe-ncc-ta.mft", + query: None, + fragment: None, + }, + }, + AccessDescription { + access_method_oid: "1.3.6.1.5.5.7.48.13", + access_location: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "rrdp.ripe.net", + ), + ), + port: None, + path: "/notification.xml", + query: None, + fragment: None, + }, + }, + ], + }, + ), + ), + certificate_policies_oid: Some( + "1.3.6.1.5.5.7.14.2", + ), + ip_resources: Some( + IpResourceSet { + families: [ + IpAddressFamily { + afi: Ipv4, + choice: AddressesOrRanges( + [ + Prefix( + IpPrefix { + afi: Ipv4, + prefix_len: 0, + addr: [ + 0, + 0, + 0, + 0, + ], + }, + ), + ], + ), + }, + IpAddressFamily { + afi: Ipv6, + choice: AddressesOrRanges( + [ + Prefix( + IpPrefix { + afi: Ipv6, + prefix_len: 0, + addr: [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + ), + ], + ), + }, + ], + }, + ), + as_resources: Some( + AsResourceSet { + asnum: Some( + AsIdsOrRanges( + [ + Range { + min: 0, + max: 4294967295, + }, + ], + ), + ), + rdi: None, + }, + ), + }, + }, + kind: Ca, + }, + }, + resolved_ta_uri: Some( + "https://rpki.ripe.net/ta/ripe-ncc-ta.cer", + ), +} + +== ResourceCertificate (example non-TA CA cert) == +Fixture (CA cert): tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer +ResourceCertificatePretty { + raw_der: BytesFmt { + len: 1530, + sha256_hex: "f808d47a98cdda9d12273b76b3cf809f6f8a6c15f92a9fd7fa634bf96726e7fb", + head_hex: "308205f6308204dea003020102020302", + tail_hex: "d24798cd14df0f3485322a6af1765703", + }, + tbs: RpkixTbsCertificatePretty { + version: 2, + serial_number: "0285ba", + signature_algorithm: "1.2.840.113549.1.1.11", + issuer_dn: "CN=A90DC5BE, serialNumber=0E65A4F5FD36B5BD68EB3C923408978C907AA79F", + subject_dn: "CN=A91E5D610001, serialNumber=05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA", + validity_not_before: 2026-01-13 1:04:39.0 +00:00:00, + validity_not_after: 2026-09-30 0:00:00.0 +00:00:00, + subject_public_key_info: BytesFmt { + len: 294, + sha256_hex: "5768f1fbcdf3bd8425856f04a2b8d2bc716d5b88d78c3301674111f414c8e1c8", + head_hex: "30820122300d06092a864886f70d0101", + tail_hex: "ea15c1433970ec93a196f70203010001", + }, + extensions: RcExtensionsPretty { + basic_constraints_ca: true, + subject_key_identifier: Some( + BytesFmt { + len: 20, + sha256_hex: "c6e74258f26c93a20e14cbf48b0615f939b9833498223a9fab15a3bacbce6054", + head_hex: "05fc9c5b88506f7c0d3f862c8895bed6", + tail_hex: "88506f7c0d3f862c8895bed67e9f8eba", + }, + ), + subject_info_access: Some( + Ca( + SubjectInfoAccessCa { + access_descriptions: [ + AccessDescription { + access_method_oid: "1.3.6.1.5.5.7.48.5", + access_location: Url { + scheme: "rsync", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "rpki.cernet.net", + ), + ), + port: None, + path: "/repo/cernet/0/", + query: None, + fragment: None, + }, + }, + AccessDescription { + access_method_oid: "1.3.6.1.5.5.7.48.10", + access_location: Url { + scheme: "rsync", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "rpki.cernet.net", + ), + ), + port: None, + path: "/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", + query: None, + fragment: None, + }, + }, + AccessDescription { + access_method_oid: "1.3.6.1.5.5.7.48.13", + access_location: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "rpki.cernet.net", + ), + ), + port: None, + path: "/rrdp/notification.xml", + query: None, + fragment: None, + }, + }, + ], + }, + ), + ), + certificate_policies_oid: Some( + "1.3.6.1.5.5.7.14.2", + ), + ip_resources: Some( + IpResourceSet { + families: [ + IpAddressFamily { + afi: Ipv6, + choice: AddressesOrRanges( + [ + Prefix( + IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 32, + 1, + 2, + 83, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + ), + Prefix( + IpPrefix { + afi: Ipv6, + prefix_len: 20, + addr: [ + 36, + 10, + 160, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + ), + ], + ), + }, + ], + }, + ), + as_resources: Some( + AsResourceSet { + asnum: Some( + AsIdsOrRanges( + [ + Id( + 4538, + ), + Id( + 23910, + ), + Range { + min: 142067, + max: 142106, + }, + Range { + min: 142650, + max: 146745, + }, + ], + ), + ), + rdi: None, + }, + ), + }, + }, + kind: Ca, +} + +== Signed Object / Manifest == +Fixture (MFT): tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft +ManifestObjectPretty { + signed_object: RpkiSignedObjectPretty { + raw_der: BytesFmt { + len: 5092, + sha256_hex: "fa9d659350a08f9604bf7e396ec3859b9d7fae63304fdbd5847a8d8af36fd234", + head_hex: "308213e006092a864886f70d010702a0", + tail_hex: "87d362f615ecfb50b55f88cb1374b36c", + }, + content_info_content_type: "1.2.840.113549.1.7.2", + signed_data: SignedDataProfiledPretty { + version: 3, + digest_algorithms: [ + "2.16.840.1.101.3.4.2.1", + ], + encap_content_info: EncapsulatedContentInfoPretty { + econtent_type: "1.2.840.113549.1.9.16.1.26", + econtent: BytesFmt { + len: 3298, + sha256_hex: "c6fd2742cec79a42298c2436daea32184f66ffa4d35a859f3beb82093738c60b", + head_hex: "30820cde02010c180f32303236303132", + tail_hex: "43c532748b7ba6abc129cdf3b90a8cd4", + }, + }, + certificates: [ + ResourceEeCertificatePretty { + raw_der: BytesFmt { + len: 1294, + sha256_hex: "15bf428ab43e1052a3ea4ad84033adb31b5b14cd08b21aaae6dc7f8e11233460", + head_hex: "3082050a308203f2a003020102021411", + tail_hex: "53ce0f1ffa2a23ffcf4ec371a6b222d0", + }, + subject_key_identifier: BytesFmt { + len: 20, + sha256_hex: "dfab94827da07e0c6f788db45a9603b92477917c02e8111a37b655f84f7c6857", + head_hex: "ccc6ae90bfdce2956877d02475a2b5f7", + tail_hex: "bfdce2956877d02475a2b5f77150b8f3", + }, + spki_der: BytesFmt { + len: 294, + sha256_hex: "2acddf7d96da20cf612528f69e1b73145ca322b83ac1aaf751ba9280bce2ace2", + head_hex: "30820122300d06092a864886f70d0101", + tail_hex: "64f865a58abd34c5885f670203010001", + }, + sia_signed_object_uris: [ + "rsync://rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", + ], + resource_cert: ResourceCertificatePretty { + raw_der: BytesFmt { + len: 1294, + sha256_hex: "15bf428ab43e1052a3ea4ad84033adb31b5b14cd08b21aaae6dc7f8e11233460", + head_hex: "3082050a308203f2a003020102021411", + tail_hex: "53ce0f1ffa2a23ffcf4ec371a6b222d0", + }, + tbs: RpkixTbsCertificatePretty { + version: 2, + serial_number: "11bbdcbd8ee49958f2683fe200bf12196ca8f285", + signature_algorithm: "1.2.840.113549.1.1.11", + issuer_dn: "CN=A91E5D610001, serialNumber=05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA", + subject_dn: "CN=CCC6AE90BFDCE2956877D02475A2B5F77150B8F3", + validity_not_before: 2026-01-20 20:26:38.0 +00:00:00, + validity_not_after: 2026-01-21 21:42:38.0 +00:00:00, + subject_public_key_info: BytesFmt { + len: 294, + sha256_hex: "2acddf7d96da20cf612528f69e1b73145ca322b83ac1aaf751ba9280bce2ace2", + head_hex: "30820122300d06092a864886f70d0101", + tail_hex: "64f865a58abd34c5885f670203010001", + }, + extensions: RcExtensionsPretty { + basic_constraints_ca: false, + subject_key_identifier: Some( + BytesFmt { + len: 20, + sha256_hex: "dfab94827da07e0c6f788db45a9603b92477917c02e8111a37b655f84f7c6857", + head_hex: "ccc6ae90bfdce2956877d02475a2b5f7", + tail_hex: "bfdce2956877d02475a2b5f77150b8f3", + }, + ), + subject_info_access: Some( + Ee( + SubjectInfoAccessEe { + signed_object_uris: [ + Url { + scheme: "rsync", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "rpki.cernet.net", + ), + ), + port: None, + path: "/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", + query: None, + fragment: None, + }, + ], + access_descriptions: [ + AccessDescription { + access_method_oid: "1.3.6.1.5.5.7.48.11", + access_location: Url { + scheme: "rsync", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "rpki.cernet.net", + ), + ), + port: None, + path: "/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", + query: None, + fragment: None, + }, + }, + ], + }, + ), + ), + certificate_policies_oid: Some( + "1.3.6.1.5.5.7.14.2", + ), + ip_resources: Some( + IpResourceSet { + families: [ + IpAddressFamily { + afi: Ipv4, + choice: Inherit, + }, + IpAddressFamily { + afi: Ipv6, + choice: Inherit, + }, + ], + }, + ), + as_resources: Some( + AsResourceSet { + asnum: Some( + Inherit, + ), + rdi: None, + }, + ), + }, + }, + kind: Ee, + }, + }, + ], + crls_present: false, + signer_infos: [ + SignerInfoProfiledPretty { + version: 3, + sid_ski: BytesFmt { + len: 20, + sha256_hex: "dfab94827da07e0c6f788db45a9603b92477917c02e8111a37b655f84f7c6857", + head_hex: "ccc6ae90bfdce2956877d02475a2b5f7", + tail_hex: "bfdce2956877d02475a2b5f77150b8f3", + }, + digest_algorithm: "2.16.840.1.101.3.4.2.1", + signature_algorithm: "1.2.840.113549.1.1.1", + signed_attrs: SignedAttrsProfiledPretty { + content_type: "1.2.840.113549.1.9.16.1.26", + message_digest: BytesFmt { + len: 32, + sha256_hex: "cdcc92d0f025674ad3250c6d2bab0c78ac0b469a28506f315c59263afe74b1e8", + head_hex: "c6fd2742cec79a42298c2436daea3218", + tail_hex: "4f66ffa4d35a859f3beb82093738c60b", + }, + signing_time: Asn1TimeUtc { + utc: 0026-01-20 20:31:38.0 +00:00:00, + encoding: UtcTime, + }, + other_attrs_present: false, + }, + unsigned_attrs_present: false, + signature: BytesFmt { + len: 256, + sha256_hex: "852ce31689786d688ac409457f37135ad1d5a433f5cce0567a1a971332d81ca9", + head_hex: "0bf0a0544cb4cc5f7c50b04a8231a021", + tail_hex: "87d362f615ecfb50b55f88cb1374b36c", + }, + signed_attrs_der_for_signature: BytesFmt { + len: 109, + sha256_hex: "4787ade978e60d2d693652d87146f219eb59ea1fbb4523f6f3a33ef10f466b63", + head_hex: "316b301a06092a864886f70d01090331", + tail_hex: "4f66ffa4d35a859f3beb82093738c60b", + }, + }, + ], + }, + }, + econtent_type: "1.2.840.113549.1.9.16.1.26", + manifest: ManifestEContentPretty { + version: 0, + manifest_number: "0C", + this_update: 2026-01-20 20:26:38.0 +00:00:00, + next_update: 2026-01-21 21:42:38.0 +00:00:00, + file_hash_alg: "2.16.840.1.101.3.4.2.1", + files: [ + FileAndHashPretty { + file_name: "AS142652.roa", + hash_hex: "2d021e9be5bb590aac6277c0f72e1ce3c136f59a1ceb2d22dd9cc66de529557a", + }, + FileAndHashPretty { + file_name: "AS142101.roa", + hash_hex: "e8e149aaaafc81ab3403c51527c195e880874a0e51aff54007d8d49bcbc1e002", + }, + FileAndHashPretty { + file_name: "AS142095.roa", + hash_hex: "ccfcc64efccd58f5fd1faf5c28e0cf0f13a9ca16fadf22e7723ce9baace06999", + }, + FileAndHashPretty { + file_name: "AS144702.roa", + hash_hex: "c90ae0f87e1f08a582fb4d5d0bec6d166e6be3829a99c55d064d3b5ba872aeb4", + }, + FileAndHashPretty { + file_name: "AS142092.roa", + hash_hex: "b201324156a82299076cc49e1863a9848df044d446061bd16b0b6206dfcfbd15", + }, + FileAndHashPretty { + file_name: "AS142104.roa", + hash_hex: "2413c3a28761c5fdeaee9c58f979d932343c4964fd12dc10ebad770f196919af", + }, + FileAndHashPretty { + file_name: "AS142079.roa", + hash_hex: "53e4a3b3bde9ac3643ead02dd2fdd4a0b951403aeeeda064c370ac817fbe79d1", + }, + FileAndHashPretty { + file_name: "AS142085.roa", + hash_hex: "fce94c84e2fd678aad061d7e891b976baff2164127aec786aabff878084afca1", + }, + FileAndHashPretty { + file_name: "AS142075.roa", + hash_hex: "8f24519cce9ba9c8b0b5430957751813d56a88e1fbd78abb354196f3337486a2", + }, + FileAndHashPretty { + file_name: "AS142656.roa", + hash_hex: "e853607e2527f6b3b564a9732f3448859ae56f545236ff1e8197b91d1bf90efc", + }, + FileAndHashPretty { + file_name: "AS142074.roa", + hash_hex: "fad617d77b16279b57a411b589ffd1b56c16a8848c7721e0ae9fc5b0a3488497", + }, + FileAndHashPretty { + file_name: "AS144707.roa", + hash_hex: "137543e13b15bb8a1e677ce2e3643f25bc7eab0813cbc00ec43c9878d25f0e07", + }, + FileAndHashPretty { + file_name: "AS142094.roa", + hash_hex: "db8b45004808ae0769f265e4b869e320d98a27a635234ec3db9cfbe48228aa73", + }, + FileAndHashPretty { + file_name: "AS142099.roa", + hash_hex: "d074661bf8ca15fb6c4685506938f79a4104ee965640755949dcca57704af7ce", + }, + FileAndHashPretty { + file_name: "AS142078.roa", + hash_hex: "bf84b0204969811f5b0fb2eb45634d673a8b857a40d14755837fe06d74f2d296", + }, + FileAndHashPretty { + file_name: "AS142659.roa", + hash_hex: "1017bb2340fbabe169a3d9d1e854db1b77765304d690288a64a865dc4b114cb9", + }, + FileAndHashPretty { + file_name: "AS142077.roa", + hash_hex: "da71c05e9ad543bae2fd22e26f7b4e4a8b7fe62da258c83a1a4c7b43e4e8ae23", + }, + FileAndHashPretty { + file_name: "AS144701.roa", + hash_hex: "79f989c162dfff790bbe82e5bc809f96c8ac3e40ede7088bcd87d7f0ec395cf2", + }, + FileAndHashPretty { + file_name: "AS142082.roa", + hash_hex: "c5befbc11f4a1d097cbb5171adbac5ff08fbbbb562bf11ac316f6cc37b152cfe", + }, + FileAndHashPretty { + file_name: "AS142653.roa", + hash_hex: "0861dca303c2a82f25b769f96561144cec750df6969727e61ee89b773d299327", + }, + FileAndHashPretty { + file_name: "AS144699.roa", + hash_hex: "7244828009953e2e35c2021e854c73c3f79e898e703905a54176e7f751f599ef", + }, + FileAndHashPretty { + file_name: "AS144706.roa", + hash_hex: "fe6adc1662f9261b1694db2b6c9800933c60a60c05376874c70cb5d20c04d908", + }, + FileAndHashPretty { + file_name: "AS142658.roa", + hash_hex: "0fbf94093699afa9df25ee75f107973f56f5c09f71fea9e8b13d6fbec727caf8", + }, + FileAndHashPretty { + file_name: "AS142070.roa", + hash_hex: "fe85f3642f96f7c7e7f78a5654763a409cc077cbe99d01124c073c4049917664", + }, + FileAndHashPretty { + file_name: "AS144703.roa", + hash_hex: "3b26d68d2e8421d71ce806e726e6736b807bb89693d2bfaea003a1527bbfd261", + }, + FileAndHashPretty { + file_name: "AS142106.roa", + hash_hex: "0213b0bf39d9a99f47d4f757e48aa99967aefba169247c4d5c73db72c2eabfd2", + }, + FileAndHashPretty { + file_name: "AS144700.roa", + hash_hex: "c873e91c3c1d56b104da71cae089899940e6737a282edbe36a24f994cb5631d1", + }, + FileAndHashPretty { + file_name: "AS142081.roa", + hash_hex: "0e3f8e057e7a2eacf3f2706cdccc013b61b51329d78e1c22b06b82a207f829e0", + }, + FileAndHashPretty { + file_name: "AS142102.roa", + hash_hex: "72d75c5f9a7d83fdcce19b8475cb420fb49c5eb54aba996295108c77786f0970", + }, + FileAndHashPretty { + file_name: "AS142083.roa", + hash_hex: "cf2d86b3d2d2a5ab9801d11a2662090d76faece8da40e3cefd4f069ace1df391", + }, + FileAndHashPretty { + file_name: "AS142084.roa", + hash_hex: "b93315b18b1350962ab27d268e4f84cd5640de32f61e0fb4d94dd595892364f7", + }, + FileAndHashPretty { + file_name: "AS142103.roa", + hash_hex: "02ac124a63aa1089bf294e8a2147f320ef5952ab13c28871cea11d3008b5517b", + }, + FileAndHashPretty { + file_name: "AS142105.roa", + hash_hex: "8e9736c1b53f6dad6f86767a8aaddd6d963840fe44bd6109b98c6e814a6c71b9", + }, + FileAndHashPretty { + file_name: "AS142087.roa", + hash_hex: "4b7db03a61edd9dedbdea96453270db5bf182a062d7e3d9e1446cc75eb088ebc", + }, + FileAndHashPretty { + file_name: "AS142068.roa", + hash_hex: "b88eae74ac6f89904f061e46bded86a12053848346a2e8b92bb8e91df7160edd", + }, + FileAndHashPretty { + file_name: "AS142654.roa", + hash_hex: "4d197f742717303f917686f4a8637e2ec59a116af65aa7838ec5806f0ad8416f", + }, + FileAndHashPretty { + file_name: "AS142098.roa", + hash_hex: "987a49344daabfca566b9c84aa3a07622ade9ee5aec6eb393df8d557393e7113", + }, + FileAndHashPretty { + file_name: "AS142100.roa", + hash_hex: "6c1568843892315f0741b6401ae71eb09dff5a78359add7be3b7e10bf7c02ead", + }, + FileAndHashPretty { + file_name: "AS142090.roa", + hash_hex: "21d83ef197954e920c7b042bc07f2639df608eaf3f91386c01786f19f7eb682d", + }, + FileAndHashPretty { + file_name: "AS142093.roa", + hash_hex: "a56b4b58be4834cd38688638f5f9914962a8dd947341d786592b0c4e74599136", + }, + FileAndHashPretty { + file_name: "AS142655.roa", + hash_hex: "1d41cd1d45d28fac384ae4012845b7789fca5e426509b5a2f77e525b3e845e67", + }, + FileAndHashPretty { + file_name: "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl", + hash_hex: "aef128252cc8b23eb92edb18739fe31dea42e7ba3bd1129020bf2b24cc728bfb", + }, + FileAndHashPretty { + file_name: "AS142089.roa", + hash_hex: "cde2163c08bc1b73acb3ec42d3aaebfd375f57b83f2821b1ff491c75fee2b4e9", + }, + FileAndHashPretty { + file_name: "AS144705.roa", + hash_hex: "d0de5d8305988fb49eb065298de4f445122cda85075a9d8eea0a0d6397dd406f", + }, + FileAndHashPretty { + file_name: "AS142657.roa", + hash_hex: "f139314accd423f6d487a86a47cd2c2bd1cc6d17a4c9b785fd93b0e3fa195fb6", + }, + FileAndHashPretty { + file_name: "AS142097.roa", + hash_hex: "77d36d3e509a3e863b78ddd37108c9a7bd0d7f62ad05cb3434b56ee1bd6baf78", + }, + FileAndHashPretty { + file_name: "AS142072.roa", + hash_hex: "d9f9f3e232dddf158baa0031cb7ab4c735b9252cd1a6ac9f1817b9e21fbd3135", + }, + FileAndHashPretty { + file_name: "AS142091.roa", + hash_hex: "6504aa013a99183135692afd64fb68e0a725468ed48fd874dcfa7fe947f1576f", + }, + FileAndHashPretty { + file_name: "AS142096.roa", + hash_hex: "fdd6829a02ddf80d336fe0ad4ffffd5c9a6061a18eb84696fe128b2090422144", + }, + FileAndHashPretty { + file_name: "AS142088.roa", + hash_hex: "f52f9f959284e183a3e2672f849fe1fa33dc357b9762c3b8316f7b655318b898", + }, + FileAndHashPretty { + file_name: "AS142080.roa", + hash_hex: "a949fd1af933423bb25b8cdd57487bf5b268df9c357ea144c192933acd268396", + }, + FileAndHashPretty { + file_name: "AS144698.roa", + hash_hex: "e1075f996746a3bd5fc1baa3ac0f5911fac7427abb9955845541d387c95daabf", + }, + FileAndHashPretty { + file_name: "AS142076.roa", + hash_hex: "9ee485f65fb2ba22a9e4bc2f260e4a51a5da2d01a47ca8529f2608fe455a7de1", + }, + FileAndHashPretty { + file_name: "AS142651.roa", + hash_hex: "184bc245ba4bd52982275d6aed7412456c77a126a6d04c42972927557f211724", + }, + FileAndHashPretty { + file_name: "AS142071.roa", + hash_hex: "5c8c07ab74ab6574cfb172bb60895ccdf8ef405db98468d9ef7bef8afd187b03", + }, + FileAndHashPretty { + file_name: "AS144704.roa", + hash_hex: "c76faf1cf9dc91d0c40aec356b9f32929861cbadc6fe35a2c16762f64f104e87", + }, + FileAndHashPretty { + file_name: "AS23910.roa", + hash_hex: "75cffffe656821caa8841802db34b1c15fb37a5210ceea3f66d43b413dee44da", + }, + FileAndHashPretty { + file_name: "AS142069.roa", + hash_hex: "fa05c5bdf62527c2435e41fdb270987296a25c8f182c3a26f1032f658eccaf43", + }, + FileAndHashPretty { + file_name: "AS4538.roa", + hash_hex: "a1a20efbd741a2b8d529397217e60881c0e2dd16724c73ca43d8c065c6a6312d", + }, + FileAndHashPretty { + file_name: "AS142086.roa", + hash_hex: "a5a6d2376f610d4099a93fac0d9a137460f99720647ab439b6153658c10aea0e", + }, + FileAndHashPretty { + file_name: "AS142067.roa", + hash_hex: "00a60c9bd40a21d9ee7dee909e5cd47936117e4680bfbbc55495019963fd970c", + }, + FileAndHashPretty { + file_name: "AS142073.roa", + hash_hex: "7e3870d7a43d3ac160b07da180f45f7a7f3aa5f040d4ea7913fae2ef02bab313", + }, + FileAndHashPretty { + file_name: "AS142650.roa", + hash_hex: "5335f79f866967bdb28d6549a49111f143c532748b7ba6abc129cdf3b90a8cd4", + }, + ], + }, +} +Manifest.validate_embedded_ee_cert=Ok(()) +Manifest.verify_signature=Ok(()) + +== Signed Object / ROA == +Fixture (ROA): tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa +RoaObjectPretty { + signed_object: RpkiSignedObjectPretty { + raw_der: BytesFmt { + len: 1956, + sha256_hex: "a1a20efbd741a2b8d529397217e60881c0e2dd16724c73ca43d8c065c6a6312d", + head_hex: "308207a006092a864886f70d010702a0", + tail_hex: "29ddcb8cd14df5be46ffa5a8cc457d75", + }, + content_info_content_type: "1.2.840.113549.1.7.2", + signed_data: SignedDataProfiledPretty { + version: 3, + digest_algorithms: [ + "2.16.840.1.101.3.4.2.1", + ], + encap_content_info: EncapsulatedContentInfoPretty { + econtent_type: "1.2.840.113549.1.9.16.1.24", + econtent: BytesFmt { + len: 200, + sha256_hex: "072d26630897c3a4ee419fdadaef4b71cc88de7e0e1afc5bcf48d5c5c34ef9c8", + head_hex: "3081c5020211ba3081be3081bb040200", + tail_hex: "030500240aa8083007030500240aa809", + }, + }, + certificates: [ + ResourceEeCertificatePretty { + raw_der: BytesFmt { + len: 1259, + sha256_hex: "6595252b039c6d6df41ee70da639e3d066835af961d378c0da40bbdbdfd04a68", + head_hex: "308204e7308203cfa003020102021432", + tail_hex: "f3ad13085c0c58793ee7a295e8c1869f", + }, + subject_key_identifier: BytesFmt { + len: 20, + sha256_hex: "ec3946c75d763701a74f1a483dc5e54e277e9a3308c03f870867f263bf25d5c8", + head_hex: "5d7f32fe5ac5281c2d057c680ab7d4cb", + tail_hex: "5ac5281c2d057c680ab7d4cb19ebe427", + }, + spki_der: BytesFmt { + len: 294, + sha256_hex: "f0f3f5102473cb81c363b156d12b4fbb609ebb0b6d78ccfee803c1c2d87459e9", + head_hex: "30820122300d06092a864886f70d0101", + tail_hex: "e9aac4696169dd7a3dd4650203010001", + }, + sia_signed_object_uris: [ + "rsync://rpki.cernet.net/repo/cernet/0/AS4538.roa", + ], + resource_cert: ResourceCertificatePretty { + raw_der: BytesFmt { + len: 1259, + sha256_hex: "6595252b039c6d6df41ee70da639e3d066835af961d378c0da40bbdbdfd04a68", + head_hex: "308204e7308203cfa003020102021432", + tail_hex: "f3ad13085c0c58793ee7a295e8c1869f", + }, + tbs: RpkixTbsCertificatePretty { + version: 2, + serial_number: "323d91c7755b93b6c64990354efcb0b28d82a374", + signature_algorithm: "1.2.840.113549.1.1.11", + issuer_dn: "CN=A91E5D610001, serialNumber=05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA", + subject_dn: "CN=5D7F32FE5AC5281C2D057C680AB7D4CB19EBE427", + validity_not_before: 2026-01-20 1:05:16.0 +00:00:00, + validity_not_after: 2027-01-19 1:10:16.0 +00:00:00, + subject_public_key_info: BytesFmt { + len: 294, + sha256_hex: "f0f3f5102473cb81c363b156d12b4fbb609ebb0b6d78ccfee803c1c2d87459e9", + head_hex: "30820122300d06092a864886f70d0101", + tail_hex: "e9aac4696169dd7a3dd4650203010001", + }, + extensions: RcExtensionsPretty { + basic_constraints_ca: false, + subject_key_identifier: Some( + BytesFmt { + len: 20, + sha256_hex: "ec3946c75d763701a74f1a483dc5e54e277e9a3308c03f870867f263bf25d5c8", + head_hex: "5d7f32fe5ac5281c2d057c680ab7d4cb", + tail_hex: "5ac5281c2d057c680ab7d4cb19ebe427", + }, + ), + subject_info_access: Some( + Ee( + SubjectInfoAccessEe { + signed_object_uris: [ + Url { + scheme: "rsync", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "rpki.cernet.net", + ), + ), + port: None, + path: "/repo/cernet/0/AS4538.roa", + query: None, + fragment: None, + }, + ], + access_descriptions: [ + AccessDescription { + access_method_oid: "1.3.6.1.5.5.7.48.11", + access_location: Url { + scheme: "rsync", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "rpki.cernet.net", + ), + ), + port: None, + path: "/repo/cernet/0/AS4538.roa", + query: None, + fragment: None, + }, + }, + ], + }, + ), + ), + certificate_policies_oid: Some( + "1.3.6.1.5.5.7.14.2", + ), + ip_resources: Some( + IpResourceSet { + families: [ + IpAddressFamily { + afi: Ipv6, + choice: AddressesOrRanges( + [ + Range( + IpAddressRange { + min: [ + 36, + 10, + 160, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + max: [ + 36, + 10, + 160, + 9, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + ], + }, + ), + Range( + IpAddressRange { + min: [ + 36, + 10, + 168, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + max: [ + 36, + 10, + 168, + 9, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + ], + }, + ), + ], + ), + }, + ], + }, + ), + as_resources: None, + }, + }, + kind: Ee, + }, + }, + ], + crls_present: false, + signer_infos: [ + SignerInfoProfiledPretty { + version: 3, + sid_ski: BytesFmt { + len: 20, + sha256_hex: "ec3946c75d763701a74f1a483dc5e54e277e9a3308c03f870867f263bf25d5c8", + head_hex: "5d7f32fe5ac5281c2d057c680ab7d4cb", + tail_hex: "5ac5281c2d057c680ab7d4cb19ebe427", + }, + digest_algorithm: "2.16.840.1.101.3.4.2.1", + signature_algorithm: "1.2.840.113549.1.1.1", + signed_attrs: SignedAttrsProfiledPretty { + content_type: "1.2.840.113549.1.9.16.1.24", + message_digest: BytesFmt { + len: 32, + sha256_hex: "80de4a90e2fab6fc6fb8af3715ac05af98ef1583c9ef840ffd14a2de3d1a952c", + head_hex: "072d26630897c3a4ee419fdadaef4b71", + tail_hex: "cc88de7e0e1afc5bcf48d5c5c34ef9c8", + }, + signing_time: Asn1TimeUtc { + utc: 0026-01-20 1:10:16.0 +00:00:00, + encoding: UtcTime, + }, + other_attrs_present: false, + }, + unsigned_attrs_present: false, + signature: BytesFmt { + len: 256, + sha256_hex: "e81879e1d179bc5380e40759e5688ed2595ee3d1c425dca7928b23104a355f5b", + head_hex: "664c6f4fde0e07327386ef8cbe4e1c7d", + tail_hex: "29ddcb8cd14df5be46ffa5a8cc457d75", + }, + signed_attrs_der_for_signature: BytesFmt { + len: 109, + sha256_hex: "e5c2863fcd4c2d0e7a622708e48f4243d8016042131daf8592aa7e692b4b5885", + head_hex: "316b301a06092a864886f70d01090331", + tail_hex: "cc88de7e0e1afc5bcf48d5c5c34ef9c8", + }, + }, + ], + }, + }, + econtent_type: "1.2.840.113549.1.9.16.1.24", + roa: RoaEContentPretty { + version: 0, + as_id: 4538, + ip_addr_blocks: [ + RoaIpAddressFamily { + afi: Ipv6, + addresses: [ + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 160, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 160, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 160, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 160, + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 160, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 160, + 5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 160, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 160, + 7, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 160, + 8, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 160, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 168, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 168, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 168, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 168, + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 168, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 168, + 5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 168, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 168, + 7, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 168, + 8, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + RoaIpAddress { + prefix: IpPrefix { + afi: Ipv6, + prefix_len: 32, + addr: [ + 36, + 10, + 168, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + }, + max_length: None, + }, + ], + }, + ], + }, +} +ROA.validate_embedded_ee_cert=Ok(()) +ROA.verify_signature=Ok(()) + +== Signed Object / ASPA == +Fixture (ASPA): tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa +AspaObjectPretty { + signed_object: RpkiSignedObjectPretty { + raw_der: BytesFmt { + len: 1705, + sha256_hex: "8232c6312ca411e9325086153cbe9c8919cd6c3d461f24f67fbf19deb7ac5c6e", + head_hex: "308206a506092a864886f70d010702a0", + tail_hex: "6bbee5fae5e7df0f80f2f634ec0a12b9", + }, + content_info_content_type: "1.2.840.113549.1.7.2", + signed_data: SignedDataProfiledPretty { + version: 3, + digest_algorithms: [ + "2.16.840.1.101.3.4.2.1", + ], + encap_content_info: EncapsulatedContentInfoPretty { + econtent_type: "1.2.840.113549.1.9.16.1.49", + econtent: BytesFmt { + len: 31, + sha256_hex: "09717bc10130fb72145ba018fb2a08637feb9a8aec9bcdd01c14f0b3057c1e60", + head_hex: "301da00302010102023cca301202020b", + tail_hex: "0b620202205b020300c790020303259e", + }, + }, + certificates: [ + ResourceEeCertificatePretty { + raw_der: BytesFmt { + len: 1180, + sha256_hex: "2551bc9a93b3fd8594174b23237b4045c57912c84625f41fe2a1f7be7d326495", + head_hex: "3082049830820380a003020102020a00", + tail_hex: "02e7e664622ff7ef15dde4d99c16acc8", + }, + subject_key_identifier: BytesFmt { + len: 20, + sha256_hex: "119e4badaaadbae00903ec44813b1b21010895b4c2abd8101045b4dad605cc59", + head_hex: "e66f347f0630b3fdc58850fb26242302", + tail_hex: "0630b3fdc58850fb26242302a6754584", + }, + spki_der: BytesFmt { + len: 294, + sha256_hex: "41d231fef9d454db0f2f58a15ff240bc0b69d34322f62b7bf6b0c4513b09ab9b", + head_hex: "30820122300d06092a864886f70d0101", + tail_hex: "ef83c82509e72800e232950203010001", + }, + sia_signed_object_uris: [ + "rsync://chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa", + ], + resource_cert: ResourceCertificatePretty { + raw_der: BytesFmt { + len: 1180, + sha256_hex: "2551bc9a93b3fd8594174b23237b4045c57912c84625f41fe2a1f7be7d326495", + head_hex: "3082049830820380a003020102020a00", + tail_hex: "02e7e664622ff7ef15dde4d99c16acc8", + }, + tbs: RpkixTbsCertificatePretty { + version: 2, + serial_number: "a1c7752ff8b1d2e020", + signature_algorithm: "1.2.840.113549.1.1.11", + issuer_dn: "CN=caa805dbac364749b9b115590ab6ef0f970cdbd8", + subject_dn: "CN=Simple Root CA", + validity_not_before: 2024-02-27 18:29:33.0 +00:00:00, + validity_not_after: 2025-02-26 18:29:33.0 +00:00:00, + subject_public_key_info: BytesFmt { + len: 294, + sha256_hex: "41d231fef9d454db0f2f58a15ff240bc0b69d34322f62b7bf6b0c4513b09ab9b", + head_hex: "30820122300d06092a864886f70d0101", + tail_hex: "ef83c82509e72800e232950203010001", + }, + extensions: RcExtensionsPretty { + basic_constraints_ca: false, + subject_key_identifier: Some( + BytesFmt { + len: 20, + sha256_hex: "119e4badaaadbae00903ec44813b1b21010895b4c2abd8101045b4dad605cc59", + head_hex: "e66f347f0630b3fdc58850fb26242302", + tail_hex: "0630b3fdc58850fb26242302a6754584", + }, + ), + subject_info_access: Some( + Ee( + SubjectInfoAccessEe { + signed_object_uris: [ + Url { + scheme: "rsync", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "chloe.sobornost.net", + ), + ), + port: None, + path: "/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa", + query: None, + fragment: None, + }, + ], + access_descriptions: [ + AccessDescription { + access_method_oid: "1.3.6.1.5.5.7.48.11", + access_location: Url { + scheme: "rsync", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "chloe.sobornost.net", + ), + ), + port: None, + path: "/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa", + query: None, + fragment: None, + }, + }, + ], + }, + ), + ), + certificate_policies_oid: Some( + "1.3.6.1.5.5.7.14.2", + ), + ip_resources: None, + as_resources: Some( + AsResourceSet { + asnum: Some( + AsIdsOrRanges( + [ + Id( + 15562, + ), + ], + ), + ), + rdi: None, + }, + ), + }, + }, + kind: Ee, + }, + }, + ], + crls_present: false, + signer_infos: [ + SignerInfoProfiledPretty { + version: 3, + sid_ski: BytesFmt { + len: 20, + sha256_hex: "119e4badaaadbae00903ec44813b1b21010895b4c2abd8101045b4dad605cc59", + head_hex: "e66f347f0630b3fdc58850fb26242302", + tail_hex: "0630b3fdc58850fb26242302a6754584", + }, + digest_algorithm: "2.16.840.1.101.3.4.2.1", + signature_algorithm: "1.2.840.113549.1.1.1", + signed_attrs: SignedAttrsProfiledPretty { + content_type: "1.2.840.113549.1.9.16.1.49", + message_digest: BytesFmt { + len: 32, + sha256_hex: "46e0432723923fd54633c955d39d878838c66bc4761f3b95a77716b7c3dc3b43", + head_hex: "09717bc10130fb72145ba018fb2a0863", + tail_hex: "7feb9a8aec9bcdd01c14f0b3057c1e60", + }, + signing_time: Asn1TimeUtc { + utc: 0024-02-27 18:32:14.0 +00:00:00, + encoding: UtcTime, + }, + other_attrs_present: false, + }, + unsigned_attrs_present: false, + signature: BytesFmt { + len: 256, + sha256_hex: "5e3a0a536409c3c6d599e62708ffe470b56e4e7827b955d762cd5f55a8fe3773", + head_hex: "da60fe85134dd603b8c4fd379de09be4", + tail_hex: "6bbee5fae5e7df0f80f2f634ec0a12b9", + }, + signed_attrs_der_for_signature: BytesFmt { + len: 109, + sha256_hex: "b154bead7ee659350da153af8a9330daaffa26d8e5bf85616969eee4548b7c07", + head_hex: "316b301a06092a864886f70d01090331", + tail_hex: "7feb9a8aec9bcdd01c14f0b3057c1e60", + }, + }, + ], + }, + }, + econtent_type: "1.2.840.113549.1.9.16.1.49", + aspa: AspaEContent { + version: 1, + customer_as_id: 15562, + provider_as_ids: [ + 2914, + 8283, + 51088, + 206238, + ], + }, +} +ASPA.validate_embedded_ee_cert=Ok(()) +ASPA.verify_signature=Ok(()) + +== CRL == +Fixture (CRL): tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl +RpkixCrlPretty { + raw_der: BytesFmt { + len: 1268, + sha256_hex: "7e6bce212905017ff822dbad8b0682fd15778cbd7ab2df592e21dce6587f212d", + head_hex: "308204f0308203d8020101300d06092a", + tail_hex: "ec33843ab859b55897fe3e1d586ab9f6", + }, + version: 2, + issuer_dn: "CN=1ff4e25c458e44e252922dcf512a568dfe098242d00cb65a3d", + signature_algorithm_oid: "1.2.840.113549.1.1.11", + this_update: Asn1TimeUtc { + utc: 2026-01-20 8:38:53.0 +00:00:00, + encoding: UtcTime, + }, + next_update: Asn1TimeUtc { + utc: 2026-01-21 12:37:53.0 +00:00:00, + encoding: UtcTime, + }, + revoked_certs: [ + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 80, + 141, + 12, + 8, + 200, + 158, + 120, + 207, + 172, + 211, + 89, + 82, + 190, + 160, + 2, + 96, + 58, + 73, + 49, + 136, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-03-26 13:42:35.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 80, + 200, + 32, + 102, + 45, + 36, + 118, + 206, + 1, + 240, + 13, + 40, + 42, + 6, + 180, + 53, + 134, + 18, + 47, + 31, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-03-26 14:16:01.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 71, + 229, + 178, + 104, + 136, + 56, + 4, + 232, + 168, + 225, + 186, + 226, + 222, + 140, + 80, + 238, + 182, + 26, + 98, + 61, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-03-26 14:16:06.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 64, + 203, + 154, + 92, + 252, + 142, + 4, + 77, + 249, + 222, + 75, + 50, + 115, + 48, + 130, + 54, + 57, + 226, + 174, + 87, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-07-16 14:44:21.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 62, + 249, + 5, + 217, + 14, + 1, + 247, + 97, + 137, + 188, + 229, + 51, + 139, + 173, + 9, + 37, + 79, + 179, + 52, + 188, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-07-16 14:44:35.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 106, + 203, + 133, + 28, + 192, + 203, + 94, + 214, + 91, + 186, + 65, + 18, + 198, + 193, + 39, + 46, + 226, + 27, + 203, + 194, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-07-25 20:56:26.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 21, + 223, + 167, + 184, + 169, + 119, + 221, + 127, + 28, + 109, + 181, + 183, + 7, + 44, + 84, + 140, + 231, + 241, + 218, + 230, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-07-25 20:57:12.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 67, + 67, + 225, + 125, + 132, + 135, + 232, + 95, + 80, + 135, + 198, + 175, + 104, + 204, + 168, + 17, + 255, + 47, + 66, + 198, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-10-27 22:01:09.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 88, + 117, + 33, + 14, + 109, + 87, + 84, + 81, + 37, + 239, + 173, + 220, + 17, + 32, + 227, + 22, + 162, + 216, + 229, + 136, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-10-27 22:01:52.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 103, + 10, + 76, + 113, + 36, + 73, + 81, + 55, + 85, + 136, + 74, + 176, + 199, + 71, + 58, + 111, + 228, + 59, + 136, + 57, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-10-27 22:03:20.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 96, + 114, + 104, + 199, + 25, + 77, + 127, + 32, + 58, + 154, + 64, + 181, + 205, + 213, + 224, + 63, + 159, + 154, + 226, + 154, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-10-27 22:03:50.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 72, + 12, + 133, + 176, + 88, + 31, + 138, + 110, + 203, + 131, + 105, + 25, + 146, + 4, + 199, + 243, + 213, + 188, + 18, + 96, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-11-12 21:32:39.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 88, + 79, + 129, + 91, + 189, + 224, + 208, + 51, + 26, + 201, + 144, + 149, + 233, + 240, + 175, + 36, + 217, + 229, + 208, + 243, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-11-12 21:32:51.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 47, + 116, + 39, + 178, + 45, + 63, + 39, + 13, + 193, + 57, + 219, + 218, + 236, + 80, + 4, + 199, + 23, + 102, + 37, + 76, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-11-12 21:33:24.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 68, + 102, + 5, + 114, + 199, + 124, + 164, + 124, + 102, + 217, + 164, + 9, + 80, + 238, + 93, + 236, + 111, + 95, + 43, + 115, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-11-12 21:33:33.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 100, + 23, + 3, + 159, + 107, + 38, + 85, + 160, + 213, + 145, + 71, + 134, + 142, + 242, + 123, + 105, + 82, + 34, + 239, + 88, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-11-12 21:33:40.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 50, + 129, + 114, + 56, + 185, + 208, + 125, + 58, + 255, + 28, + 222, + 180, + 0, + 51, + 195, + 231, + 143, + 255, + 154, + 7, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-11-12 21:33:45.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 3, + 72, + 166, + 47, + 154, + 154, + 12, + 191, + 28, + 64, + 100, + 226, + 84, + 254, + 122, + 183, + 213, + 17, + 51, + 195, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-11-12 21:33:50.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 26, + 246, + 135, + 239, + 123, + 222, + 166, + 142, + 108, + 229, + 149, + 197, + 155, + 250, + 87, + 219, + 178, + 149, + 55, + 7, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-11-12 21:33:55.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 2, + 193, + 248, + 98, + 75, + 79, + 79, + 76, + 91, + 251, + 121, + 177, + 170, + 235, + 105, + 193, + 4, + 246, + 92, + 45, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-11-12 21:34:01.0 +00:00:00, + encoding: UtcTime, + }, + }, + RevokedCert { + serial_number: BigUnsigned { + bytes_be: [ + 30, + 155, + 127, + 84, + 144, + 98, + 146, + 120, + 128, + 226, + 37, + 85, + 116, + 125, + 53, + 28, + 219, + 217, + 174, + 145, + ], + }, + revocation_date: Asn1TimeUtc { + utc: 2025-11-12 21:34:07.0 +00:00:00, + encoding: UtcTime, + }, + }, + ], + extensions: CrlExtensions { + authority_key_identifier: [ + 0, + 153, + 222, + 171, + 7, + 62, + 253, + 116, + 194, + 80, + 192, + 163, + 130, + 178, + 80, + 18, + 181, + 8, + 42, + 238, + ], + crl_number: BigUnsigned { + bytes_be: [ + 7, + 54, + ], + }, + }, +} +test print_all_models_from_real_fixtures ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s + diff --git a/specs/arch.excalidraw b/specs/arch.excalidraw new file mode 100644 index 0000000..8008274 --- /dev/null +++ b/specs/arch.excalidraw @@ -0,0 +1,86 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "id": "A3BjC6kJe019Pes-xjr_L", + "type": "rectangle", + "x": 307.66668701171875, + "y": 719.3333740234375, + "width": 321.66668701171875, + "height": 104, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0", + "roundness": { + "type": 3 + }, + "seed": 1572721102, + "version": 56, + "versionNonce": 1402545874, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "KnkPQWiZzaALgJ-eehAOa" + } + ], + "updated": 1770174471076, + "link": null, + "locked": false + }, + { + "id": "KnkPQWiZzaALgJ-eehAOa", + "type": "text", + "x": 406.6700668334961, + "y": 746.3333740234375, + "width": 123.65992736816406, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0V", + "roundness": null, + "seed": 1287469326, + "version": 49, + "versionNonce": 630772558, + "isDeleted": false, + "boundElements": null, + "updated": 1770174490368, + "link": null, + "locked": false, + "text": "lib\n(data model)", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "A3BjC6kJe019Pes-xjr_L", + "originalText": "lib\n(data model)", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/src/data_model/aspa.rs b/src/data_model/aspa.rs index e7c1590..0d25c90 100644 --- a/src/data_model/aspa.rs +++ b/src/data_model/aspa.rs @@ -1,8 +1,10 @@ use crate::data_model::oid::OID_CT_ASPA; use crate::data_model::rc::ResourceCertificate; -use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError}; -use der_parser::ber::{Class}; -use der_parser::der::{parse_der, DerObject, Tag}; +use crate::data_model::signed_object::{ + RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError, +}; +use der_parser::ber::Class; +use der_parser::der::{DerObject, Tag, parse_der}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct AspaObject { @@ -11,6 +13,13 @@ pub struct AspaObject { pub aspa: AspaEContent, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AspaObjectParsed { + pub signed_object: RpkiSignedObjectParsed, + pub econtent_type: String, + pub aspa: Option, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct AspaEContent { pub version: u32, @@ -18,70 +27,151 @@ pub struct AspaEContent { pub provider_as_ids: Vec, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AspaEContentParsed { + der: Vec, +} + #[derive(Debug, thiserror::Error)] -pub enum AspaDecodeError { - #[error("SignedObject decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")] - SignedObjectDecode(#[from] SignedObjectDecodeError), - - #[error("ASPA eContentType must be {OID_CT_ASPA}, got {0} (draft-ietf-sidrops-aspa-profile-21 §2)")] - InvalidEContentType(String), - +pub enum AspaParseError { + #[error("signed object parse error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")] + SignedObject(#[from] SignedObjectParseError), #[error("ASPA parse error: {0} (draft-ietf-sidrops-aspa-profile-21 §3; DER)")] Parse(String), #[error("ASPA trailing bytes: {0} bytes (draft-ietf-sidrops-aspa-profile-21 §3; DER)")] TrailingBytes(usize), +} - #[error("ASProviderAttestation must be a SEQUENCE of 3 elements (draft-ietf-sidrops-aspa-profile-21 §3)")] +#[derive(Debug, thiserror::Error)] +pub enum AspaProfileError { + #[error("signed object profile error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")] + SignedObject(#[from] SignedObjectValidateError), + + #[error( + "ASPA eContentType must be {OID_CT_ASPA}, got {0} (draft-ietf-sidrops-aspa-profile-21 §2)" + )] + InvalidEContentType(String), + + #[error("ASPA profile decode error: {0} (draft-ietf-sidrops-aspa-profile-21 §3; DER)")] + ProfileDecode(String), + + #[error( + "ASProviderAttestation must be a SEQUENCE of 3 elements (draft-ietf-sidrops-aspa-profile-21 §3)" + )] InvalidAttestationSequence, - #[error("ASPA version must be 1 and MUST be explicitly encoded (draft-ietf-sidrops-aspa-profile-21 §3.1)")] + #[error( + "ASPA version must be 1 and MUST be explicitly encoded (draft-ietf-sidrops-aspa-profile-21 §3.1)" + )] VersionMustBeExplicitOne, - #[error("ASPA customerASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.2)")] + #[error( + "ASPA customerASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.2)" + )] CustomerAsIdOutOfRange(u64), - #[error("ASPA providers must contain at least one ASID (draft-ietf-sidrops-aspa-profile-21 §3.3)")] + #[error( + "ASPA providers must contain at least one ASID (draft-ietf-sidrops-aspa-profile-21 §3.3)" + )] EmptyProviders, - #[error("ASPA provider ASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.3)")] + #[error( + "ASPA provider ASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.3)" + )] ProviderAsIdOutOfRange(u64), - #[error("ASPA providers must be in strictly increasing order (draft-ietf-sidrops-aspa-profile-21 §3.3)")] + #[error( + "ASPA providers must be in strictly increasing order (draft-ietf-sidrops-aspa-profile-21 §3.3)" + )] ProvidersNotStrictlyIncreasing, - #[error("ASPA providers contains the customerASID ({0}) which is not allowed (draft-ietf-sidrops-aspa-profile-21 §3.3)")] + #[error( + "ASPA providers contains the customerASID ({0}) which is not allowed (draft-ietf-sidrops-aspa-profile-21 §3.3)" + )] ProvidersContainCustomer(u32), } +#[derive(Debug, thiserror::Error)] +pub enum AspaDecodeError { + #[error("{0}")] + Parse(#[from] AspaParseError), + + #[error("{0}")] + Validate(#[from] AspaProfileError), +} + #[derive(Debug, thiserror::Error)] pub enum AspaValidateError { - #[error("ASPA EE certificate must contain AS resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2)")] + #[error( + "ASPA EE certificate must contain AS resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2)" + )] EeAsResourcesMissing, - #[error("ASPA EE certificate AS resources must not use inherit (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.3)")] + #[error( + "ASPA EE certificate AS resources must not use inherit (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.3)" + )] EeAsResourcesInherit, - #[error("ASPA EE certificate AS resources must not include ranges (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.6-§3.2.3.7)")] + #[error( + "ASPA EE certificate AS resources must not include ranges (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.6-§3.2.3.7)" + )] EeAsResourcesRangePresent, - #[error("ASPA EE certificate AS resources must not include RDI (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.5; RFC 6487 §4.8.11)")] + #[error( + "ASPA EE certificate AS resources must not include RDI (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.5; RFC 6487 §4.8.11)" + )] EeAsResourcesRdiPresent, - #[error("ASPA EE certificate AS resources must contain exactly one ASID (id element) (draft-ietf-sidrops-aspa-profile-21 §4)")] + #[error( + "ASPA EE certificate AS resources must contain exactly one ASID (id element) (draft-ietf-sidrops-aspa-profile-21 §4)" + )] EeAsResourcesNotSingleId, - #[error("ASPA customerASID ({customer_as_id}) does not match EE AS resources ({ee_as_id}) (draft-ietf-sidrops-aspa-profile-21 §4)")] + #[error( + "ASPA customerASID ({customer_as_id}) does not match EE AS resources ({ee_as_id}) (draft-ietf-sidrops-aspa-profile-21 §4)" + )] CustomerAsIdMismatch { customer_as_id: u32, ee_as_id: u32 }, - #[error("ASPA EE certificate must not contain IP resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §2.2)")] + #[error( + "ASPA EE certificate must not contain IP resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §2.2)" + )] EeIpResourcesPresent, } impl AspaObject { + /// Parse step of scheme A (`parse → validate → verify`). + pub fn parse_der(der: &[u8]) -> Result { + let signed_object = RpkiSignedObject::parse_der(der)?; + let econtent_type = signed_object + .signed_data + .encap_content_info + .econtent_type + .clone(); + let aspa = signed_object + .signed_data + .encap_content_info + .econtent + .as_deref() + .map(AspaEContent::parse_der) + .transpose()?; + Ok(AspaObjectParsed { + signed_object, + econtent_type, + aspa, + }) + } + + /// Profile validate step of scheme A (`parse → validate → verify`). + /// + /// `AspaObject` is already profile-validated when constructed via `decode_der()` / + /// `AspaObjectParsed::validate_profile()`. + pub fn validate_profile(&self) -> Result<(), AspaProfileError> { + Ok(()) + } + pub fn decode_der(der: &[u8]) -> Result { - let signed_object = RpkiSignedObject::decode_der(der)?; - Self::from_signed_object(signed_object) + Ok(Self::parse_der(der)?.validate_profile()?) } pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result { @@ -91,10 +181,11 @@ impl AspaObject { .econtent_type .clone(); if econtent_type != OID_CT_ASPA { - return Err(AspaDecodeError::InvalidEContentType(econtent_type)); + return Err(AspaProfileError::InvalidEContentType(econtent_type).into()); } - let aspa = AspaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?; + let aspa = + AspaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?; Ok(Self { aspa, signed_object, @@ -110,57 +201,27 @@ impl AspaObject { } impl AspaEContent { - /// Decode the DER-encoded ASProviderAttestation defined in draft-ietf-sidrops-aspa-profile-21 §3. + /// Parse step of scheme A (`parse → validate → verify`). + pub fn parse_der(der: &[u8]) -> Result { + let (rem, _obj) = parse_der(der).map_err(|e| AspaParseError::Parse(e.to_string()))?; + if !rem.is_empty() { + return Err(AspaParseError::TrailingBytes(rem.len())); + } + Ok(AspaEContentParsed { der: der.to_vec() }) + } + + /// Profile validate step of scheme A (`parse → validate → verify`). + /// + /// `AspaEContent` is already profile-validated when constructed via `decode_der()` / + /// `AspaEContentParsed::validate_profile()`. + pub fn validate_profile(&self) -> Result<(), AspaProfileError> { + Ok(()) + } + + /// Decode the DER-encoded ASProviderAttestation defined in + /// draft-ietf-sidrops-aspa-profile-21 §3 (`parse + validate`). pub fn decode_der(der: &[u8]) -> Result { - let (rem, obj) = parse_der(der).map_err(|e| AspaDecodeError::Parse(e.to_string()))?; - if !rem.is_empty() { - return Err(AspaDecodeError::TrailingBytes(rem.len())); - } - - let seq = obj - .as_sequence() - .map_err(|e| AspaDecodeError::Parse(e.to_string()))?; - if seq.len() != 3 { - return Err(AspaDecodeError::InvalidAttestationSequence); - } - - // version [0] EXPLICIT INTEGER MUST be present and MUST be 1. - let v_obj = &seq[0]; - if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) { - return Err(AspaDecodeError::VersionMustBeExplicitOne); - } - let inner_der = v_obj - .as_slice() - .map_err(|e| AspaDecodeError::Parse(e.to_string()))?; - let (rem, inner) = - parse_der(inner_der).map_err(|e| AspaDecodeError::Parse(e.to_string()))?; - if !rem.is_empty() { - return Err(AspaDecodeError::Parse( - "trailing bytes inside ASProviderAttestation.version".into(), - )); - } - let v = inner - .as_u64() - .map_err(|e| AspaDecodeError::Parse(e.to_string()))?; - if v != 1 { - return Err(AspaDecodeError::VersionMustBeExplicitOne); - } - - let customer_u64 = seq[1] - .as_u64() - .map_err(|e| AspaDecodeError::Parse(e.to_string()))?; - if customer_u64 > u32::MAX as u64 { - return Err(AspaDecodeError::CustomerAsIdOutOfRange(customer_u64)); - } - let customer_as_id = customer_u64 as u32; - - let providers = parse_providers(&seq[2], customer_as_id)?; - - Ok(Self { - version: 1, - customer_as_id, - provider_as_ids: providers, - }) + Ok(Self::parse_der(der)?.validate_profile()?) } /// Validate ASPA payload against the embedded EE resource certificate. @@ -206,12 +267,87 @@ impl AspaEContent { } } -fn parse_providers(obj: &DerObject<'_>, customer_as_id: u32) -> Result, AspaDecodeError> { +impl AspaObjectParsed { + pub fn validate_profile(self) -> Result { + let signed_object = self.signed_object.validate_profile()?; + let econtent_type = signed_object + .signed_data + .encap_content_info + .econtent_type + .clone(); + if econtent_type != OID_CT_ASPA { + return Err(AspaProfileError::InvalidEContentType(econtent_type)); + } + let aspa = self + .aspa + .ok_or_else(|| AspaProfileError::ProfileDecode("ASPA.eContent missing".into()))? + .validate_profile()?; + Ok(AspaObject { + signed_object, + econtent_type: OID_CT_ASPA.to_string(), + aspa, + }) + } +} + +impl AspaEContentParsed { + pub fn validate_profile(self) -> Result { + let (_rem, obj) = + parse_der(&self.der).map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; + + let seq = obj + .as_sequence() + .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; + if seq.len() != 3 { + return Err(AspaProfileError::InvalidAttestationSequence); + } + + // version [0] EXPLICIT INTEGER MUST be present and MUST be 1. + let v_obj = &seq[0]; + if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) { + return Err(AspaProfileError::VersionMustBeExplicitOne); + } + let inner_der = v_obj + .as_slice() + .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; + let (rem, inner) = + parse_der(inner_der).map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; + if !rem.is_empty() { + return Err(AspaProfileError::ProfileDecode( + "trailing bytes inside ASProviderAttestation.version".into(), + )); + } + let v = inner + .as_u64() + .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; + if v != 1 { + return Err(AspaProfileError::VersionMustBeExplicitOne); + } + + let customer_u64 = seq[1] + .as_u64() + .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; + if customer_u64 > u32::MAX as u64 { + return Err(AspaProfileError::CustomerAsIdOutOfRange(customer_u64)); + } + let customer_as_id = customer_u64 as u32; + + let providers = parse_providers(&seq[2], customer_as_id)?; + + Ok(AspaEContent { + version: 1, + customer_as_id, + provider_as_ids: providers, + }) + } +} + +fn parse_providers(obj: &DerObject<'_>, customer_as_id: u32) -> Result, AspaProfileError> { let seq = obj .as_sequence() - .map_err(|e| AspaDecodeError::Parse(e.to_string()))?; + .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; if seq.is_empty() { - return Err(AspaDecodeError::EmptyProviders); + return Err(AspaProfileError::EmptyProviders); } let mut out: Vec = Vec::with_capacity(seq.len()); @@ -219,17 +355,17 @@ fn parse_providers(obj: &DerObject<'_>, customer_as_id: u32) -> Result, for item in seq { let v = item .as_u64() - .map_err(|e| AspaDecodeError::Parse(e.to_string()))?; + .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; if v > u32::MAX as u64 { - return Err(AspaDecodeError::ProviderAsIdOutOfRange(v)); + return Err(AspaProfileError::ProviderAsIdOutOfRange(v)); } let asn = v as u32; if asn == customer_as_id { - return Err(AspaDecodeError::ProvidersContainCustomer(customer_as_id)); + return Err(AspaProfileError::ProvidersContainCustomer(customer_as_id)); } if let Some(p) = prev { if asn <= p { - return Err(AspaDecodeError::ProvidersNotStrictlyIncreasing); + return Err(AspaProfileError::ProvidersNotStrictlyIncreasing); } } prev = Some(asn); diff --git a/src/data_model/common.rs b/src/data_model/common.rs index cc47015..b555d13 100644 --- a/src/data_model/common.rs +++ b/src/data_model/common.rs @@ -69,7 +69,9 @@ impl BigUnsigned { } #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] -#[error("{field} time encoding invalid for year {year}: got {encoding:?} (RFC 5280 §4.1.2.5; RFC 5280 §5.1.2.4-§5.1.2.6)")] +#[error( + "{field} time encoding invalid for year {year}: got {encoding:?} (RFC 5280 §4.1.2.5; RFC 5280 §5.1.2.4-§5.1.2.6)" +)] pub struct InvalidTimeEncodingError { pub field: &'static str, pub year: i32, @@ -103,6 +105,5 @@ pub fn algorithm_params_absent_or_null(sig: &AlgorithmIdentifier<'_>) -> bool { /// /// Notes: /// - Includes entries marked TEMPORARY/DEPRECATED by IANA (e.g., `asa`, `gbr`). -pub const IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS: &[&str] = &[ - "asa", "cer", "crl", "gbr", "mft", "roa", "sig", "tak", -]; +pub const IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS: &[&str] = + &["asa", "cer", "crl", "gbr", "mft", "roa", "sig", "tak"]; diff --git a/src/data_model/crl.rs b/src/data_model/crl.rs index 1c5010e..6626542 100644 --- a/src/data_model/crl.rs +++ b/src/data_model/crl.rs @@ -3,13 +3,12 @@ use crate::data_model::oid::{ OID_AUTHORITY_KEY_IDENTIFIER, OID_CRL_NUMBER, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SUBJECT_KEY_IDENTIFIER, }; -use x509_parser::extensions::{AuthorityKeyIdentifier, ParsedExtension, X509Extension}; -use x509_parser::prelude::FromDer; -use x509_parser::prelude::X509Version; -use x509_parser::revocation_list::CertificateRevocationList; use x509_parser::certificate::X509Certificate; -use x509_parser::x509::SubjectPublicKeyInfo; -use x509_parser::x509::AlgorithmIdentifier; +use x509_parser::extensions::{ParsedExtension, X509Extension}; +use x509_parser::prelude::{FromDer, X509Version}; +use x509_parser::revocation_list::CertificateRevocationList; +use x509_parser::x509::{AlgorithmIdentifier, SubjectPublicKeyInfo}; +use x509_parser::{asn1_rs::Class as Asn1Class, asn1_rs::Tag as Asn1Tag}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct RevokedCert { @@ -35,26 +34,93 @@ pub struct RpkixCrl { pub extensions: CrlExtensions, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RpkixCrlParsed { + pub raw_der: Vec, + pub version: Option, + pub issuer_dn: String, + pub signature_algorithm: AlgorithmIdentifierValue, + pub tbs_signature_algorithm: AlgorithmIdentifierValue, + pub this_update: Asn1TimeUtc, + pub next_update: Option, + pub revoked_certs: Vec, + pub extensions: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RevokedCertParsed { + pub serial_number: BigUnsigned, + pub revocation_date: Asn1TimeUtc, + pub has_extensions: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum CrlExtensionParsed { + AuthorityKeyIdentifier { + key_identifier: Option>, + has_other_fields: bool, + critical: bool, + }, + CrlNumber { + number: der_parser::num_bigint::BigUint, + critical: bool, + }, + Other { + oid: String, + critical: bool, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AlgorithmIdentifierValue { + pub oid: String, + pub parameters: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AlgorithmParametersValue { + pub class: Asn1Class, + pub tag: Asn1Tag, + pub data: Vec, +} + +impl AlgorithmIdentifierValue { + pub fn params_absent_or_null(&self) -> bool { + match &self.parameters { + None => true, + Some(p) if p.class == Asn1Class::Universal && p.tag == Asn1Tag::Null => true, + Some(_p) => false, + } + } +} + #[derive(Debug, thiserror::Error)] -pub enum CrlDecodeError { +pub enum CrlParseError { #[error("X.509 CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")] Parse(String), #[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")] TrailingBytes(usize), +} +#[derive(Debug, thiserror::Error)] +pub enum CrlProfileError { #[error("CRL version must be v2, got {0:?} (RFC 5280 §5.1; RFC 6487 §5)")] InvalidVersion(Option), - #[error("CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6487 §5; RFC 7935 §2)")] - InvalidSignatureAlgorithm(String), - - #[error("CRL signature algorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)")] - InvalidSignatureAlgorithmParameters, - #[error("CRL signatureAlgorithm must match TBSCertList.signature (RFC 5280 §5.1)")] SignatureAlgorithmMismatch, + #[error( + "CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6487 §5; RFC 7935 §2)" + )] + InvalidSignatureAlgorithm(String), + + #[error( + "CRL signature algorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)" + )] + InvalidSignatureAlgorithmParameters, + #[error("CRL extensions must be exactly two (AKI + CRLNumber), got {0} (RFC 9829 §3.1)")] InvalidExtensionsCount(usize), @@ -64,12 +130,20 @@ pub enum CrlDecodeError { #[error("duplicate CRL extension OID {0} (RFC 5280 §4.2; RFC 9829 §3.1)")] DuplicateExtension(String), + #[error("AuthorityKeyIdentifier CRL extension missing (RFC 9829 §3.1; RFC 5280 §5.2.1)")] + AkiMissing, + #[error("AuthorityKeyIdentifier must contain keyIdentifier (RFC 5280 §5.2.1; RFC 9829 §3.1)")] AkiMissingKeyIdentifier, - #[error("AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber (RFC 5280 §5.2.1; RFC 9829 §3.1)")] + #[error( + "AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber (RFC 5280 §5.2.1; RFC 9829 §3.1)" + )] AkiHasOtherFields, + #[error("CRLNumber CRL extension missing (RFC 9829 §3.1; RFC 5280 §5.2.3)")] + CrlNumberMissing, + #[error("CRLNumber must be non-critical (RFC 9829 §3.1; RFC 5280 §5.2.3)")] CrlNumberCritical, @@ -82,7 +156,9 @@ pub enum CrlDecodeError { #[error("CRL nextUpdate must be present (RFC 5280 §5.1.2.5; RFC 6487 §5)")] NextUpdateMissing, - #[error("{field} time encoding invalid for year {year}: got {encoding:?} (RFC 5280 §5.1.2.4-§5.1.2.6)")] + #[error( + "{field} time encoding invalid for year {year}: got {encoding:?} (RFC 5280 §5.1.2.4-§5.1.2.6)" + )] InvalidTimeEncoding { field: &'static str, year: i32, @@ -90,64 +166,47 @@ pub enum CrlDecodeError { }, } +#[derive(Debug, thiserror::Error)] +pub enum CrlDecodeError { + #[error("{0}")] + Parse(#[from] CrlParseError), + + #[error("{0}")] + Validate(#[from] CrlProfileError), +} + impl RpkixCrl { - /// Decode a DER-encoded X.509 v2 CRL and enforce the RPKI profile constraints from - /// `specs/prepare/data_models/04_crl.md` (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1). - pub fn decode_der(der: &[u8]) -> Result { + /// Parse step of scheme A (`parse → validate → verify`). + pub fn parse_der(der: &[u8]) -> Result { let (rem, crl) = CertificateRevocationList::from_der(der) - .map_err(|e| CrlDecodeError::Parse(e.to_string()))?; + .map_err(|e| CrlParseError::Parse(e.to_string()))?; if !rem.is_empty() { - return Err(CrlDecodeError::TrailingBytes(rem.len())); + return Err(CrlParseError::TrailingBytes(rem.len())); } - let version = match crl.version() { - Some(X509Version::V2) => 2, - Some(v) => return Err(CrlDecodeError::InvalidVersion(Some(v.0))), - None => return Err(CrlDecodeError::InvalidVersion(None)), - }; - - let sig_oid = crl.signature_algorithm.algorithm.to_id_string(); - let tbs_sig_oid = crl.tbs_cert_list.signature.algorithm.to_id_string(); - if sig_oid != tbs_sig_oid { - return Err(CrlDecodeError::SignatureAlgorithmMismatch); - } - if sig_oid != OID_SHA256_WITH_RSA_ENCRYPTION { - return Err(CrlDecodeError::InvalidSignatureAlgorithm(sig_oid)); - } - validate_sig_params(&crl.signature_algorithm)?; - validate_sig_params(&crl.tbs_cert_list.signature)?; - - let extensions = parse_and_validate_extensions(crl.extensions())?; - let revoked_certs = crl .iter_revoked_certificates() - .map(|rc| { - if !rc.extensions().is_empty() { - return Err(CrlDecodeError::EntryExtensionsNotAllowed); - } - let revocation_date = crate::data_model::common::asn1_time_to_model(rc.revocation_date); - validate_time_encoding_rfc5280("revocationDate", &revocation_date)?; - Ok(RevokedCert { - serial_number: BigUnsigned::from_biguint(rc.serial()), - revocation_date, - }) + .map(|rc| RevokedCertParsed { + serial_number: BigUnsigned::from_biguint(rc.serial()), + revocation_date: crate::data_model::common::asn1_time_to_model(rc.revocation_date), + has_extensions: !rc.extensions().is_empty(), }) - .collect::, _>>()?; + .collect::>(); let this_update = crate::data_model::common::asn1_time_to_model(crl.last_update()); - validate_time_encoding_rfc5280("thisUpdate", &this_update)?; - let next_update = crl .next_update() - .map(crate::data_model::common::asn1_time_to_model) - .ok_or(CrlDecodeError::NextUpdateMissing)?; - validate_time_encoding_rfc5280("nextUpdate", &next_update)?; + .map(crate::data_model::common::asn1_time_to_model); - Ok(RpkixCrl { + let extensions = + parse_extensions_parse(crl.extensions()).map_err(|e| CrlParseError::Parse(e))?; + + Ok(RpkixCrlParsed { raw_der: der.to_vec(), - version, + version: crl.version(), issuer_dn: crl.issuer().to_string(), - signature_algorithm_oid: OID_SHA256_WITH_RSA_ENCRYPTION.to_string(), + signature_algorithm: algorithm_identifier_value(&crl.signature_algorithm), + tbs_signature_algorithm: algorithm_identifier_value(&crl.tbs_cert_list.signature), this_update, next_update, revoked_certs, @@ -155,6 +214,20 @@ impl RpkixCrl { }) } + /// Decode a DER-encoded X.509 v2 CRL and enforce the RPKI profile constraints from + /// `specs/prepare/data_models/04_crl.md` (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1). + pub fn decode_der(der: &[u8]) -> Result { + Ok(Self::parse_der(der)?.validate_profile()?) + } + + /// Profile validate step of scheme A (`parse → validate → verify`). + /// + /// `RpkixCrl` is already profile-validated when constructed via `decode_der()` / + /// `RpkixCrlParsed::validate_profile()`. + pub fn validate_profile(&self) -> Result<(), CrlProfileError> { + Ok(()) + } + /// Verify the cryptographic signature on this CRL using the issuer certificate. /// /// Signature verification needs the issuer public key (RFC 5280 §6.3.3 (f)-(g)). @@ -229,6 +302,59 @@ impl RpkixCrl { } } +impl RpkixCrlParsed { + /// Profile validate step of scheme A (`parse → validate → verify`). + pub fn validate_profile(self) -> Result { + let version = match self.version { + Some(X509Version::V2) => 2, + Some(v) => return Err(CrlProfileError::InvalidVersion(Some(v.0))), + None => return Err(CrlProfileError::InvalidVersion(None)), + }; + + // signatureAlgorithm must match tbsCertList.signature + if self.signature_algorithm != self.tbs_signature_algorithm { + return Err(CrlProfileError::SignatureAlgorithmMismatch); + } + let sig_oid = self.signature_algorithm.oid.clone(); + if sig_oid != OID_SHA256_WITH_RSA_ENCRYPTION { + return Err(CrlProfileError::InvalidSignatureAlgorithm(sig_oid)); + } + if !self.signature_algorithm.params_absent_or_null() { + return Err(CrlProfileError::InvalidSignatureAlgorithmParameters); + } + + let extensions = validate_extensions_profile(&self.extensions)?; + + let mut revoked_out = Vec::with_capacity(self.revoked_certs.len()); + for rc in self.revoked_certs { + if rc.has_extensions { + return Err(CrlProfileError::EntryExtensionsNotAllowed); + } + validate_time_encoding_rfc5280("revocationDate", &rc.revocation_date)?; + revoked_out.push(RevokedCert { + serial_number: rc.serial_number, + revocation_date: rc.revocation_date, + }); + } + + validate_time_encoding_rfc5280("thisUpdate", &self.this_update)?; + + let next_update = self.next_update.ok_or(CrlProfileError::NextUpdateMissing)?; + validate_time_encoding_rfc5280("nextUpdate", &next_update)?; + + Ok(RpkixCrl { + raw_der: self.raw_der, + version, + issuer_dn: self.issuer_dn, + signature_algorithm_oid: OID_SHA256_WITH_RSA_ENCRYPTION.to_string(), + this_update: self.this_update, + next_update, + revoked_certs: revoked_out, + extensions, + }) + } +} + #[derive(Debug, thiserror::Error)] pub enum CrlVerifyError { #[error("issuer certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4)")] @@ -240,7 +366,9 @@ pub enum CrlVerifyError { #[error("issuer SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")] IssuerSpkiParse(String), - #[error("trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)")] + #[error( + "trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)" + )] IssuerSpkiTrailingBytes(usize), #[error("CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")] @@ -249,16 +377,22 @@ pub enum CrlVerifyError { #[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")] CrlTrailingBytes(usize), - #[error("CRL issuer DN does not match issuer certificate subject (RFC 5280 §5.1; RFC 5280 §6.3.3(b))")] + #[error( + "CRL issuer DN does not match issuer certificate subject (RFC 5280 §5.1; RFC 5280 §6.3.3(b))" + )] IssuerSubjectMismatch { crl_issuer_dn: String, issuer_subject_dn: String, }, - #[error("issuer certificate keyUsage present but missing cRLSign (RFC 5280 §4.2.1.3; RFC 5280 §6.3.3(f))")] + #[error( + "issuer certificate keyUsage present but missing cRLSign (RFC 5280 §4.2.1.3; RFC 5280 §6.3.3(f))" + )] IssuerKeyUsageMissingCrlSign, - #[error("CRL AKI.keyIdentifier does not match issuer certificate SKI (RFC 5280 §4.2.1.1; RFC 5280 §4.2.1.2; RFC 5280 §6.3.3(c)/(f))")] + #[error( + "CRL AKI.keyIdentifier does not match issuer certificate SKI (RFC 5280 §4.2.1.1; RFC 5280 §4.2.1.2; RFC 5280 §6.3.3(c)/(f))" + )] AkiSkiMismatch, #[error("CRL signature verification failed: {0} (RFC 5280 §6.3.3(g); RFC 7935 §2)")] @@ -268,7 +402,7 @@ pub enum CrlVerifyError { fn validate_time_encoding_rfc5280( field: &'static str, t: &Asn1TimeUtc, -) -> Result<(), CrlDecodeError> { +) -> Result<(), CrlProfileError> { let year = t.utc.year(); let expected = if year <= 2049 { Asn1TimeEncoding::UtcTime @@ -276,7 +410,7 @@ fn validate_time_encoding_rfc5280( Asn1TimeEncoding::GeneralizedTime }; if t.encoding != expected { - return Err(CrlDecodeError::InvalidTimeEncoding { + return Err(CrlProfileError::InvalidTimeEncoding { field, year, encoding: t.encoding, @@ -285,86 +419,109 @@ fn validate_time_encoding_rfc5280( Ok(()) } -fn validate_sig_params(sig: &AlgorithmIdentifier<'_>) -> Result<(), CrlDecodeError> { - if crate::data_model::common::algorithm_params_absent_or_null(sig) { - Ok(()) - } else { - Err(CrlDecodeError::InvalidSignatureAlgorithmParameters) +fn algorithm_identifier_value(ai: &AlgorithmIdentifier<'_>) -> AlgorithmIdentifierValue { + let parameters = ai.parameters.as_ref().map(|p| AlgorithmParametersValue { + class: p.class(), + tag: p.tag(), + data: p.as_bytes().to_vec(), + }); + AlgorithmIdentifierValue { + oid: ai.algorithm.to_id_string(), + parameters, } } -fn parse_and_validate_extensions(exts: &[X509Extension<'_>]) -> Result { - if exts.len() != 2 { - return Err(CrlDecodeError::InvalidExtensionsCount(exts.len())); - } - - let mut authority_key_identifier: Option> = None; - let mut crl_number: Option = None; - +fn parse_extensions_parse(exts: &[X509Extension<'_>]) -> Result, String> { + let mut out = Vec::with_capacity(exts.len()); for ext in exts { let oid = ext.oid.to_id_string(); match oid.as_str() { OID_AUTHORITY_KEY_IDENTIFIER => { - if authority_key_identifier.is_some() { - return Err(CrlDecodeError::DuplicateExtension(oid)); - } - let aki = parse_aki(ext)?; - authority_key_identifier = Some(aki); + let ParsedExtension::AuthorityKeyIdentifier(aki) = ext.parsed_extension() else { + return Err("AKI extension parse failed".to_string()); + }; + out.push(CrlExtensionParsed::AuthorityKeyIdentifier { + key_identifier: aki.key_identifier.as_ref().map(|k| k.0.to_vec()), + has_other_fields: aki.authority_cert_issuer.is_some() + || aki.authority_cert_serial.is_some(), + critical: ext.critical, + }); } - OID_CRL_NUMBER => { - if crl_number.is_some() { - return Err(CrlDecodeError::DuplicateExtension(oid)); + OID_CRL_NUMBER => match ext.parsed_extension() { + ParsedExtension::CRLNumber(n) => out.push(CrlExtensionParsed::CrlNumber { + number: n.clone(), + critical: ext.critical, + }), + _ => return Err("CRLNumber extension parse failed".to_string()), + }, + _ => out.push(CrlExtensionParsed::Other { + oid, + critical: ext.critical, + }), + } + } + Ok(out) +} + +fn validate_extensions_profile( + exts: &[CrlExtensionParsed], +) -> Result { + if exts.len() != 2 { + return Err(CrlProfileError::InvalidExtensionsCount(exts.len())); + } + + let mut seen: Vec = Vec::new(); + let mut authority_key_identifier: Option> = None; + let mut crl_number: Option = None; + + for ext in exts { + match ext { + CrlExtensionParsed::AuthorityKeyIdentifier { + key_identifier, + has_other_fields, + critical: _, + } => { + let oid = OID_AUTHORITY_KEY_IDENTIFIER.to_string(); + if seen.iter().any(|s| s == &oid) { + return Err(CrlProfileError::DuplicateExtension(oid)); } - if ext.critical { - return Err(CrlDecodeError::CrlNumberCritical); + seen.push(oid.clone()); + + if *has_other_fields { + return Err(CrlProfileError::AkiHasOtherFields); } - let n = parse_crl_number(ext)?; - if n.bits() > 159 { - return Err(CrlDecodeError::CrlNumberOutOfRange); - } - crl_number = Some(BigUnsigned::from_biguint(&n)); + let ki = key_identifier + .as_ref() + .ok_or(CrlProfileError::AkiMissingKeyIdentifier)?; + authority_key_identifier = Some(ki.clone()); + } + CrlExtensionParsed::CrlNumber { number, critical } => { + let oid = OID_CRL_NUMBER.to_string(); + if seen.iter().any(|s| s == &oid) { + return Err(CrlProfileError::DuplicateExtension(oid)); + } + seen.push(oid.clone()); + + if *critical { + return Err(CrlProfileError::CrlNumberCritical); + } + if number.bits() > 159 { + return Err(CrlProfileError::CrlNumberOutOfRange); + } + crl_number = Some(BigUnsigned::from_biguint(number)); + } + CrlExtensionParsed::Other { oid, .. } => { + return Err(CrlProfileError::UnsupportedExtension(oid.clone())); } - _ => return Err(CrlDecodeError::UnsupportedExtension(oid)), } } Ok(CrlExtensions { - authority_key_identifier: authority_key_identifier.unwrap(), - crl_number: crl_number.unwrap(), + authority_key_identifier: authority_key_identifier.ok_or(CrlProfileError::AkiMissing)?, + crl_number: crl_number.ok_or(CrlProfileError::CrlNumberMissing)?, }) } -fn parse_aki(ext: &X509Extension<'_>) -> Result, CrlDecodeError> { - let ParsedExtension::AuthorityKeyIdentifier(aki) = ext.parsed_extension() else { - return Err(CrlDecodeError::Parse("AKI extension parse failed".into())); - }; - validate_aki_profile(aki)?; - Ok(aki - .key_identifier - .as_ref() - .ok_or(CrlDecodeError::AkiMissingKeyIdentifier)? - .0 - .to_vec()) -} - -fn validate_aki_profile(aki: &AuthorityKeyIdentifier<'_>) -> Result<(), CrlDecodeError> { - if aki.key_identifier.is_none() { - return Err(CrlDecodeError::AkiMissingKeyIdentifier); - } - if aki.authority_cert_issuer.is_some() || aki.authority_cert_serial.is_some() { - return Err(CrlDecodeError::AkiHasOtherFields); - } - Ok(()) -} - -fn parse_crl_number(ext: &X509Extension<'_>) -> Result { - match ext.parsed_extension() { - ParsedExtension::CRLNumber(n) => Ok(n.clone()), - ParsedExtension::ParseError { error } => Err(CrlDecodeError::Parse(error.to_string())), - _ => Err(CrlDecodeError::Parse("CRLNumber extension parse failed".into())), - } -} - fn get_subject_key_identifier(cert: &X509Certificate<'_>) -> Option> { cert.extensions() .iter() diff --git a/src/data_model/manifest.rs b/src/data_model/manifest.rs index 2cb8c09..9b992e4 100644 --- a/src/data_model/manifest.rs +++ b/src/data_model/manifest.rs @@ -1,9 +1,11 @@ use crate::data_model::common::BigUnsigned; use crate::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256}; use crate::data_model::rc::ResourceCertificate; -use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError}; +use crate::data_model::signed_object::{ + RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError, +}; use der_parser::ber::BerObjectContent; -use der_parser::der::{parse_der, DerObject, Tag}; +use der_parser::der::{DerObject, Tag, parse_der}; use time::OffsetDateTime; #[derive(Clone, Debug, PartialEq, Eq)] @@ -13,6 +15,13 @@ pub struct ManifestObject { pub manifest: ManifestEContent, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ManifestObjectParsed { + pub signed_object: RpkiSignedObjectParsed, + pub econtent_type: String, + pub manifest: Option, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct ManifestEContent { pub version: u32, @@ -23,6 +32,11 @@ pub struct ManifestEContent { pub files: Vec, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ManifestEContentParsed { + der: Vec, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct FileAndHash { pub file_name: String, @@ -30,26 +44,38 @@ pub struct FileAndHash { } #[derive(Debug, thiserror::Error)] -pub enum ManifestDecodeError { - #[error("signed object decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4; RFC 9286 §4)")] - SignedObject(#[from] SignedObjectDecodeError), - +pub enum ManifestParseError { + #[error("signed object parse error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")] + SignedObject(#[from] SignedObjectParseError), #[error("DER parse error: {0} (RFC 9286 §4.2; DER)")] Parse(String), #[error("trailing bytes after DER object: {0} bytes (RFC 9286 §4.2; DER)")] TrailingBytes(usize), +} - #[error("eContentType must be id-ct-rpkiManifest ({OID_CT_RPKI_MANIFEST}), got {0} (RFC 9286 §4.1; RFC 9286 §4.4(1))")] +#[derive(Debug, thiserror::Error)] +pub enum ManifestProfileError { + #[error("signed object profile error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")] + SignedObject(#[from] SignedObjectValidateError), + + #[error( + "eContentType must be id-ct-rpkiManifest ({OID_CT_RPKI_MANIFEST}), got {0} (RFC 9286 §4.1; RFC 9286 §4.4(1))" + )] InvalidEContentType(String), + #[error("manifest profile decode error: {0} (RFC 9286 §4.2; DER)")] + ProfileDecode(String), + #[error("Manifest must be a SEQUENCE of 5 or 6 elements, got {0} (RFC 9286 §4.2)")] InvalidManifestSequenceLen(usize), #[error("Manifest.version must be 0, got {0} (RFC 9286 §4.2.1)")] InvalidManifestVersion(u64), - #[error("Manifest.manifestNumber must be non-negative INTEGER (RFC 9286 §4.2; RFC 9286 §4.2.1)")] + #[error( + "Manifest.manifestNumber must be non-negative INTEGER (RFC 9286 §4.2; RFC 9286 §4.2.1)" + )] InvalidManifestNumber, #[error("Manifest.manifestNumber longer than 20 octets (RFC 9286 §4.2.1)")] @@ -64,7 +90,9 @@ pub enum ManifestDecodeError { #[error("Manifest.nextUpdate must be later than thisUpdate (RFC 9286 §4.2.1)")] NextUpdateNotLater, - #[error("Manifest.fileHashAlg must be id-sha256 ({OID_SHA256}), got {0} (RFC 9286 §4.2.1; RFC 7935 §2)")] + #[error( + "Manifest.fileHashAlg must be id-sha256 ({OID_SHA256}), got {0} (RFC 9286 §4.2.1; RFC 7935 §2)" + )] InvalidFileHashAlg(String), #[error("Manifest.fileList must be a SEQUENCE (RFC 9286 §4.2)")] @@ -79,44 +107,97 @@ pub enum ManifestDecodeError { #[error("fileList hash must be BIT STRING (RFC 9286 §4.2)")] InvalidHashType, - #[error("fileList hash BIT STRING must be octet-aligned (unused bits=0) (RFC 9286 §4.2.1; DER BIT STRING)")] + #[error( + "fileList hash BIT STRING must be octet-aligned (unused bits=0) (RFC 9286 §4.2.1; DER BIT STRING)" + )] HashNotOctetAligned, - #[error("fileList hash length invalid for sha256: got {0} bytes (RFC 9286 §4.2.1; RFC 7935 §2)")] + #[error( + "fileList hash length invalid for sha256: got {0} bytes (RFC 9286 §4.2.1; RFC 7935 §2)" + )] InvalidHashLength(usize), } +#[derive(Debug, thiserror::Error)] +pub enum ManifestDecodeError { + #[error("{0}")] + Parse(#[from] ManifestParseError), + + #[error("{0}")] + Validate(#[from] ManifestProfileError), +} + #[derive(Debug, thiserror::Error)] pub enum ManifestValidateError { - #[error("Manifest EE certificate MUST include at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 3779; RFC 9286 §5.1)")] + #[error( + "Manifest EE certificate MUST include at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 3779; RFC 9286 §5.1)" + )] EeResourcesMissing, - #[error("Manifest EE certificate IP resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §2.2.3.5)")] + #[error( + "Manifest EE certificate IP resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §2.2.3.5)" + )] EeIpResourcesNotInherit, - #[error("Manifest EE certificate AS resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §3.2.3.3)")] + #[error( + "Manifest EE certificate AS resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §3.2.3.3)" + )] EeAsResourcesNotInherit, - #[error("Manifest EE certificate AS resources rdi MUST be absent (RFC 6487 §4.8.11; RFC 3779 §3.2.3.5)")] + #[error( + "Manifest EE certificate AS resources rdi MUST be absent (RFC 6487 §4.8.11; RFC 3779 §3.2.3.5)" + )] EeAsResourcesRdiPresent, } impl ManifestObject { - pub fn decode_der(der: &[u8]) -> Result { - let signed_object = RpkiSignedObject::decode_der(der)?; - Self::from_signed_object(signed_object) + /// Parse step of scheme A (`parse → validate → verify`). + pub fn parse_der(der: &[u8]) -> Result { + let signed_object = RpkiSignedObject::parse_der(der)?; + let econtent_type = signed_object + .signed_data + .encap_content_info + .econtent_type + .clone(); + let manifest = signed_object + .signed_data + .encap_content_info + .econtent + .as_deref() + .map(ManifestEContent::parse_der) + .transpose()?; + Ok(ManifestObjectParsed { + signed_object, + econtent_type, + manifest, + }) } - pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result { + /// Profile validate step of scheme A (`parse → validate → verify`). + /// + /// `ManifestObject` is already profile-validated when constructed via `decode_der()` / + /// `ManifestObjectParsed::validate_profile()`. + pub fn validate_profile(&self) -> Result<(), ManifestProfileError> { + Ok(()) + } + + pub fn decode_der(der: &[u8]) -> Result { + Ok(Self::parse_der(der)?.validate_profile()?) + } + + pub fn from_signed_object( + signed_object: RpkiSignedObject, + ) -> Result { let econtent_type = signed_object .signed_data .encap_content_info .econtent_type .clone(); if econtent_type != OID_CT_RPKI_MANIFEST { - return Err(ManifestDecodeError::InvalidEContentType(econtent_type)); + return Err(ManifestProfileError::InvalidEContentType(econtent_type).into()); } - let manifest = ManifestEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?; + let manifest = + ManifestEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?; Ok(Self { signed_object, econtent_type: OID_CT_RPKI_MANIFEST.to_string(), @@ -164,18 +245,62 @@ impl ManifestObject { } impl ManifestEContent { - /// Decode the DER-encoded Manifest eContent defined in RFC 9286 §4.2. - pub fn decode_der(der: &[u8]) -> Result { - let (rem, obj) = parse_der(der).map_err(|e| ManifestDecodeError::Parse(e.to_string()))?; + /// Parse step of scheme A (`parse → validate → verify`). + pub fn parse_der(der: &[u8]) -> Result { + let (rem, _obj) = parse_der(der).map_err(|e| ManifestParseError::Parse(e.to_string()))?; if !rem.is_empty() { - return Err(ManifestDecodeError::TrailingBytes(rem.len())); + return Err(ManifestParseError::TrailingBytes(rem.len())); } + Ok(ManifestEContentParsed { der: der.to_vec() }) + } + + /// Profile validate step of scheme A (`parse → validate → verify`). + /// + /// `ManifestEContent` is already profile-validated when constructed via `decode_der()` / + /// `ManifestEContentParsed::validate_profile()`. + pub fn validate_profile(&self) -> Result<(), ManifestProfileError> { + Ok(()) + } + + /// Decode the DER-encoded Manifest eContent defined in RFC 9286 §4.2 (`parse + validate`). + pub fn decode_der(der: &[u8]) -> Result { + Ok(Self::parse_der(der)?.validate_profile()?) + } +} + +impl ManifestObjectParsed { + pub fn validate_profile(self) -> Result { + let signed_object = self.signed_object.validate_profile()?; + let econtent_type = signed_object + .signed_data + .encap_content_info + .econtent_type + .clone(); + if econtent_type != OID_CT_RPKI_MANIFEST { + return Err(ManifestProfileError::InvalidEContentType(econtent_type)); + } + let manifest = self + .manifest + .ok_or_else(|| ManifestProfileError::ProfileDecode("Manifest.eContent missing".into()))? + .validate_profile()?; + Ok(ManifestObject { + signed_object, + econtent_type: OID_CT_RPKI_MANIFEST.to_string(), + manifest, + }) + } +} + +impl ManifestEContentParsed { + pub fn validate_profile(self) -> Result { + let (_rem, obj) = + parse_der(&self.der).map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; let seq = obj .as_sequence() - .map_err(|e| ManifestDecodeError::Parse(e.to_string()))?; + .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; if seq.len() != 5 && seq.len() != 6 { - return Err(ManifestDecodeError::InvalidManifestSequenceLen(seq.len())); + return Err(ManifestProfileError::InvalidManifestSequenceLen(seq.len())); } let mut idx = 0; @@ -183,25 +308,25 @@ impl ManifestEContent { if seq.len() == 6 { let v_obj = &seq[0]; if v_obj.class() != der_parser::ber::Class::ContextSpecific || v_obj.tag() != Tag(0) { - return Err(ManifestDecodeError::Parse( + return Err(ManifestProfileError::ProfileDecode( "Manifest.version must be [0] EXPLICIT INTEGER".into(), )); } let inner_der = v_obj .as_slice() - .map_err(|e| ManifestDecodeError::Parse(e.to_string()))?; - let (rem, inner) = - parse_der(inner_der).map_err(|e| ManifestDecodeError::Parse(e.to_string()))?; + .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; + let (rem, inner) = parse_der(inner_der) + .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; if !rem.is_empty() { - return Err(ManifestDecodeError::Parse( + return Err(ManifestProfileError::ProfileDecode( "trailing bytes inside Manifest.version".into(), )); } let v = inner .as_u64() - .map_err(|e| ManifestDecodeError::Parse(e.to_string()))?; + .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; if v != 0 { - return Err(ManifestDecodeError::InvalidManifestVersion(v)); + return Err(ManifestProfileError::InvalidManifestVersion(v)); } version = 0; idx = 1; @@ -211,24 +336,24 @@ impl ManifestEContent { idx += 1; let this_update = - parse_generalized_time(&seq[idx], ManifestDecodeError::InvalidThisUpdate)?; + parse_generalized_time(&seq[idx], ManifestProfileError::InvalidThisUpdate)?; idx += 1; let next_update = - parse_generalized_time(&seq[idx], ManifestDecodeError::InvalidNextUpdate)?; + parse_generalized_time(&seq[idx], ManifestProfileError::InvalidNextUpdate)?; idx += 1; if next_update <= this_update { - return Err(ManifestDecodeError::NextUpdateNotLater); + return Err(ManifestProfileError::NextUpdateNotLater); } let file_hash_alg = oid_to_string(&seq[idx])?; idx += 1; if file_hash_alg != OID_SHA256 { - return Err(ManifestDecodeError::InvalidFileHashAlg(file_hash_alg)); + return Err(ManifestProfileError::InvalidFileHashAlg(file_hash_alg)); } let files = parse_file_list_sha256(&seq[idx])?; - Ok(Self { + Ok(ManifestEContent { version, manifest_number, this_update, @@ -239,39 +364,39 @@ impl ManifestEContent { } } -fn parse_manifest_number(obj: &DerObject<'_>) -> Result { +fn parse_manifest_number(obj: &DerObject<'_>) -> Result { let n = obj .as_biguint() - .map_err(|_e| ManifestDecodeError::InvalidManifestNumber)?; + .map_err(|_e| ManifestProfileError::InvalidManifestNumber)?; let out = BigUnsigned::from_biguint(&n); if out.bytes_be.len() > 20 { - return Err(ManifestDecodeError::ManifestNumberTooLong); + return Err(ManifestProfileError::ManifestNumberTooLong); } Ok(out) } fn parse_generalized_time( obj: &DerObject<'_>, - err: ManifestDecodeError, -) -> Result { + err: ManifestProfileError, +) -> Result { match &obj.content { BerObjectContent::GeneralizedTime(dt) => dt .to_datetime() - .map_err(|e| ManifestDecodeError::Parse(e.to_string())), + .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string())), _ => Err(err), } } -fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result, ManifestDecodeError> { +fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result, ManifestProfileError> { let seq = obj .as_sequence() - .map_err(|_e| ManifestDecodeError::InvalidFileList)?; + .map_err(|_e| ManifestProfileError::InvalidFileList)?; let mut out = Vec::with_capacity(seq.len()); for entry in seq { let (file_name, hash_bytes) = parse_file_and_hash(entry)?; validate_file_name(&file_name)?; if hash_bytes.len() != 32 { - return Err(ManifestDecodeError::InvalidHashLength(hash_bytes.len())); + return Err(ManifestProfileError::InvalidHashLength(hash_bytes.len())); } out.push(FileAndHash { file_name, @@ -281,58 +406,58 @@ fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result, Manif Ok(out) } -fn parse_file_and_hash(obj: &DerObject<'_>) -> Result<(String, Vec), ManifestDecodeError> { +fn parse_file_and_hash(obj: &DerObject<'_>) -> Result<(String, Vec), ManifestProfileError> { let seq = obj .as_sequence() - .map_err(|e| ManifestDecodeError::Parse(e.to_string()))?; + .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; if seq.len() != 2 { - return Err(ManifestDecodeError::InvalidFileAndHash); + return Err(ManifestProfileError::InvalidFileAndHash); } let file_name = seq[0] .as_str() - .map_err(|e| ManifestDecodeError::Parse(e.to_string()))? + .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))? .to_string(); let (unused_bits, bits) = match &seq[1].content { BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()), - _ => return Err(ManifestDecodeError::InvalidHashType), + _ => return Err(ManifestProfileError::InvalidHashType), }; if unused_bits != 0 { - return Err(ManifestDecodeError::HashNotOctetAligned); + return Err(ManifestProfileError::HashNotOctetAligned); } Ok((file_name, bits)) } -fn validate_file_name(name: &str) -> Result<(), ManifestDecodeError> { +fn validate_file_name(name: &str) -> Result<(), ManifestProfileError> { // RFC 9286 §4.2.2: // 1+ chars from a-zA-Z0-9-_ , then '.', then 3-letter extension. let Some((base, ext)) = name.rsplit_once('.') else { - return Err(ManifestDecodeError::InvalidFileName(name.to_string())); + return Err(ManifestProfileError::InvalidFileName(name.to_string())); }; if base.is_empty() || ext.len() != 3 { - return Err(ManifestDecodeError::InvalidFileName(name.to_string())); + return Err(ManifestProfileError::InvalidFileName(name.to_string())); } if !base .bytes() .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') { - return Err(ManifestDecodeError::InvalidFileName(name.to_string())); + return Err(ManifestProfileError::InvalidFileName(name.to_string())); } if !ext.bytes().all(|b| b.is_ascii_alphabetic()) { - return Err(ManifestDecodeError::InvalidFileName(name.to_string())); + return Err(ManifestProfileError::InvalidFileName(name.to_string())); } let ext_lower = ext.to_ascii_lowercase(); if !crate::data_model::common::IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS .iter() .any(|&e| e == ext_lower) { - return Err(ManifestDecodeError::InvalidFileName(name.to_string())); + return Err(ManifestProfileError::InvalidFileName(name.to_string())); } Ok(()) } -fn oid_to_string(obj: &DerObject<'_>) -> Result { +fn oid_to_string(obj: &DerObject<'_>) -> Result { let oid = obj .as_oid() - .map_err(|e| ManifestDecodeError::Parse(e.to_string()))?; + .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; Ok(oid.to_id_string()) } diff --git a/src/data_model/mod.rs b/src/data_model/mod.rs index bd487da..d22198f 100644 --- a/src/data_model/mod.rs +++ b/src/data_model/mod.rs @@ -1,10 +1,10 @@ +pub mod aspa; pub mod common; pub mod crl; -pub mod rc; -pub mod oid; -pub mod signed_object; pub mod manifest; +pub mod oid; +pub mod rc; pub mod roa; -pub mod aspa; -pub mod tal; +pub mod signed_object; pub mod ta; +pub mod tal; diff --git a/src/data_model/rc.rs b/src/data_model/rc.rs index 8b7d986..6d8cdbf 100644 --- a/src/data_model/rc.rs +++ b/src/data_model/rc.rs @@ -1,12 +1,12 @@ use der_parser::ber::{BerObjectContent, Class}; -use der_parser::der::{parse_der, DerObject, Tag}; +use der_parser::der::{DerObject, Tag, parse_der}; use der_parser::num_bigint::BigUint; use time::OffsetDateTime; use url::Url; +use x509_parser::asn1_rs::{Class as Asn1Class, Tag as Asn1Tag}; use x509_parser::extensions::ParsedExtension; use x509_parser::prelude::{FromDer, X509Certificate, X509Extension, X509Version}; -use crate::data_model::common::algorithm_params_absent_or_null; use crate::data_model::oid::{ OID_AD_SIGNED_OBJECT, OID_AUTONOMOUS_SYS_IDS, OID_CP_IPADDR_ASNUMBER, OID_IP_ADDR_BLOCKS, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SUBJECT_INFO_ACCESS, OID_SUBJECT_KEY_IDENTIFIER, @@ -59,6 +59,62 @@ pub struct RcExtensions { pub as_resources: Option, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ResourceCertificateParsed { + pub raw_der: Vec, + pub version: X509Version, + pub serial_number: BigUint, + pub signature_algorithm: AlgorithmIdentifierValue, + pub tbs_signature_algorithm: AlgorithmIdentifierValue, + pub issuer_dn: String, + pub subject_dn: String, + pub validity_not_before: OffsetDateTime, + pub validity_not_after: OffsetDateTime, + /// DER encoding of SubjectPublicKeyInfo. + pub subject_public_key_info: Vec, + pub extensions: RcExtensionsParsed, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AlgorithmIdentifierValue { + pub oid: String, + pub parameters: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AlgorithmParametersValue { + pub class: Asn1Class, + pub tag: Asn1Tag, + pub data: Vec, +} + +impl AlgorithmIdentifierValue { + pub fn params_absent_or_null(&self) -> bool { + match &self.parameters { + None => true, + Some(p) if p.class == Asn1Class::Universal && p.tag == Asn1Tag::Null => true, + Some(_p) => false, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RcExtensionsParsed { + pub basic_constraints_ca: Vec, + pub subject_key_identifier: Vec<(Vec, bool)>, + pub subject_info_access: Vec<(SubjectInfoAccessParsed, bool)>, + pub certificate_policies: Vec<(Vec, bool)>, + pub ip_resources: Vec<(IpResourceSet, bool)>, + pub as_resources: Vec<(AsResourceSet, bool)>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SubjectInfoAccessParsed { + pub access_descriptions: Vec, + pub signed_object_uris: Vec, + pub signed_object_access_location_not_uri: bool, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub enum SubjectInfoAccess { Ca(SubjectInfoAccessCa), @@ -204,10 +260,7 @@ impl AsResourceSet { } pub fn has_any_range(&self) -> bool { - self.asnum - .as_ref() - .map(|c| c.has_range()) - .unwrap_or(false) + self.asnum.as_ref().map(|c| c.has_range()).unwrap_or(false) || self.rdi.as_ref().map(|c| c.has_range()).unwrap_or(false) } @@ -243,7 +296,9 @@ impl AsIdentifierChoice { pub fn has_range(&self) -> bool { match self { AsIdentifierChoice::Inherit => false, - AsIdentifierChoice::AsIdsOrRanges(items) => items.iter().any(|i| matches!(i, AsIdOrRange::Range { .. })), + AsIdentifierChoice::AsIdsOrRanges(items) => { + items.iter().any(|i| matches!(i, AsIdOrRange::Range { .. })) + } } } } @@ -255,20 +310,31 @@ pub enum AsIdOrRange { } #[derive(Debug, thiserror::Error)] -pub enum ResourceCertificateError { +pub enum ResourceCertificateParseError { #[error("X.509 parse error: {0} (RFC 5280 §4.1; RFC 6487 §4)")] Parse(String), #[error("trailing bytes after certificate DER: {0} bytes (DER; RFC 5280 §4.1)")] TrailingBytes(usize), + #[error("invalid RFC 3779 IP resources extension encoding (RFC 6487 §4.8.10; RFC 3779 §2.2)")] + InvalidIpResourcesEncoding, + + #[error("invalid RFC 3779 AS resources extension encoding (RFC 6487 §4.8.11; RFC 3779 §3.2)")] + InvalidAsResourcesEncoding, +} + +#[derive(Debug, thiserror::Error)] +pub enum ResourceCertificateProfileError { #[error("certificate version must be v3 (RFC 5280 §4.1; RFC 6487 §4)")] InvalidVersion, #[error("signatureAlgorithm does not match tbsCertificate.signature (RFC 5280 §4.1)")] SignatureAlgorithmMismatch, - #[error("unsupported signature algorithm (expected sha256WithRSAEncryption {OID_SHA256_WITH_RSA_ENCRYPTION}) (RFC 7935 §2; RFC 6487 §4)")] + #[error( + "unsupported signature algorithm (expected sha256WithRSAEncryption {OID_SHA256_WITH_RSA_ENCRYPTION}) (RFC 7935 §2; RFC 6487 §4)" + )] UnsupportedSignatureAlgorithm, #[error("invalid signature algorithm parameters (RFC 5280 §4.1.1.2)")] @@ -286,45 +352,46 @@ pub enum ResourceCertificateError { #[error("certificatePolicies criticality must be critical (RFC 6487 §4.8.9)")] CertificatePoliciesCriticality, - #[error("certificatePolicies must contain RPKI policy OID {OID_CP_IPADDR_ASNUMBER}, got {0} (RFC 6487 §4.8.9)")] + #[error( + "certificatePolicies must contain RPKI policy OID {OID_CP_IPADDR_ASNUMBER}, got {0} (RFC 6487 §4.8.9)" + )] InvalidCertificatePolicy(String), - #[error("SIA id-ad-signedObject accessLocation must be URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)")] + #[error( + "SIA id-ad-signedObject accessLocation must be URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)" + )] SignedObjectSiaNotUri, #[error("SIA id-ad-signedObject must include at least one rsync:// URI (RFC 6487 §4.8.8.2)")] SignedObjectSiaNoRsync, - #[error("invalid RFC 3779 IP resources extension (RFC 6487 §4.8.10; RFC 3779 §2.2)")] - InvalidIpResources, + #[error("ipAddrBlocks criticality must be critical when present (RFC 6487 §4.8.10)")] + IpResourcesCriticality, - #[error("invalid RFC 3779 AS resources extension (RFC 6487 §4.8.11; RFC 3779 §3.2)")] - InvalidAsResources, + #[error("autonomousSysIds criticality must be critical when present (RFC 6487 §4.8.11)")] + AsResourcesCriticality, } +#[derive(Debug, thiserror::Error)] +pub enum ResourceCertificateDecodeError { + #[error("{0}")] + Parse(#[from] ResourceCertificateParseError), + + #[error("{0}")] + Validate(#[from] ResourceCertificateProfileError), +} + +pub type ResourceCertificateError = ResourceCertificateDecodeError; + impl ResourceCertificate { - pub fn from_der(der: &[u8]) -> Result { - let (rem, cert) = - X509Certificate::from_der(der).map_err(|e| ResourceCertificateError::Parse(e.to_string()))?; + /// Parse step of scheme A (`parse → validate → verify`). + pub fn parse_der( + der: &[u8], + ) -> Result { + let (rem, cert) = X509Certificate::from_der(der) + .map_err(|e| ResourceCertificateParseError::Parse(e.to_string()))?; if !rem.is_empty() { - return Err(ResourceCertificateError::TrailingBytes(rem.len())); - } - - let version = match cert.version() { - X509Version::V3 => 2u32, - _ => return Err(ResourceCertificateError::InvalidVersion), - }; - - let outer = &cert.signature_algorithm; - let inner = &cert.tbs_certificate.signature; - if outer.algorithm != inner.algorithm || outer.parameters != inner.parameters { - return Err(ResourceCertificateError::SignatureAlgorithmMismatch); - } - if outer.algorithm.to_id_string() != OID_SHA256_WITH_RSA_ENCRYPTION { - return Err(ResourceCertificateError::UnsupportedSignatureAlgorithm); - } - if !algorithm_params_absent_or_null(outer) { - return Err(ResourceCertificateError::InvalidSignatureAlgorithmParameters); + return Err(ResourceCertificateParseError::TrailingBytes(rem.len())); } let validity_not_before = cert.validity().not_before.to_datetime(); @@ -332,7 +399,62 @@ impl ResourceCertificate { let subject_public_key_info = cert.tbs_certificate.subject_pki.raw.to_vec(); - let extensions = parse_extensions(cert.extensions())?; + let signature_algorithm = algorithm_identifier_value(&cert.signature_algorithm); + let tbs_signature_algorithm = algorithm_identifier_value(&cert.tbs_certificate.signature); + let extensions = parse_extensions_parse(cert.extensions())?; + + Ok(ResourceCertificateParsed { + raw_der: der.to_vec(), + version: cert.version(), + serial_number: cert.tbs_certificate.serial.clone(), + signature_algorithm, + tbs_signature_algorithm, + issuer_dn: cert.issuer().to_string(), + subject_dn: cert.subject().to_string(), + validity_not_before, + validity_not_after, + subject_public_key_info, + extensions, + }) + } + + /// Profile validate step of scheme A (`parse → validate → verify`). + /// + /// `ResourceCertificate` is already profile-validated when constructed via `decode_der()` / + /// `ResourceCertificateParsed::validate_profile()`. + pub fn validate_profile(&self) -> Result<(), ResourceCertificateProfileError> { + Ok(()) + } + + /// Decode a resource certificate (`parse + validate`). + pub fn decode_der(der: &[u8]) -> Result { + Ok(Self::parse_der(der)?.validate_profile()?) + } + + /// Backwards-compatible helper (historical name). + pub fn from_der(der: &[u8]) -> Result { + Self::decode_der(der) + } +} + +impl ResourceCertificateParsed { + pub fn validate_profile(self) -> Result { + let version = match self.version { + X509Version::V3 => 2u32, + _ => return Err(ResourceCertificateProfileError::InvalidVersion), + }; + + if self.signature_algorithm != self.tbs_signature_algorithm { + return Err(ResourceCertificateProfileError::SignatureAlgorithmMismatch); + } + if self.signature_algorithm.oid != OID_SHA256_WITH_RSA_ENCRYPTION { + return Err(ResourceCertificateProfileError::UnsupportedSignatureAlgorithm); + } + if !self.signature_algorithm.params_absent_or_null() { + return Err(ResourceCertificateProfileError::InvalidSignatureAlgorithmParameters); + } + + let extensions = self.extensions.validate_profile()?; let kind = if extensions.basic_constraints_ca { ResourceCertKind::Ca } else { @@ -340,16 +462,16 @@ impl ResourceCertificate { }; Ok(ResourceCertificate { - raw_der: der.to_vec(), + raw_der: self.raw_der, tbs: RpkixTbsCertificate { version, - serial_number: cert.tbs_certificate.serial.clone(), - signature_algorithm: outer.algorithm.to_id_string(), - issuer_dn: cert.issuer().to_string(), - subject_dn: cert.subject().to_string(), - validity_not_before, - validity_not_after, - subject_public_key_info, + serial_number: self.serial_number, + signature_algorithm: self.signature_algorithm.oid, + issuer_dn: self.issuer_dn, + subject_dn: self.subject_dn, + validity_not_before: self.validity_not_before, + validity_not_after: self.validity_not_after, + subject_public_key_info: self.subject_public_key_info, extensions, }, kind, @@ -357,114 +479,220 @@ impl ResourceCertificate { } } -fn parse_extensions(exts: &[X509Extension<'_>]) -> Result { - let mut basic_constraints_ca: Option = None; - let mut ski: Option> = None; - let mut sia: Option = None; - let mut cert_policies_oid: Option = None; +impl RcExtensionsParsed { + pub fn validate_profile(self) -> Result { + if self.basic_constraints_ca.len() > 1 { + return Err(ResourceCertificateProfileError::DuplicateExtension( + "basicConstraints", + )); + } + let basic_constraints_ca = self.basic_constraints_ca.first().copied().unwrap_or(false); - let mut ip_resources: Option = None; - let mut as_resources: Option = None; + let subject_key_identifier = match self.subject_key_identifier.as_slice() { + [] => None, + [(ski, critical)] => { + if *critical { + return Err(ResourceCertificateProfileError::SkiCriticality); + } + Some(ski.clone()) + } + _ => { + return Err(ResourceCertificateProfileError::DuplicateExtension( + "subjectKeyIdentifier", + )); + } + }; + + let subject_info_access = match self.subject_info_access.as_slice() { + [] => None, + [(sia, critical)] => { + if *critical { + return Err(ResourceCertificateProfileError::SiaCriticality); + } + if sia.signed_object_access_location_not_uri { + return Err(ResourceCertificateProfileError::SignedObjectSiaNotUri); + } + if !sia.signed_object_uris.is_empty() + && !sia.signed_object_uris.iter().any(|u| u.scheme() == "rsync") + { + return Err(ResourceCertificateProfileError::SignedObjectSiaNoRsync); + } + if sia.signed_object_uris.is_empty() { + Some(SubjectInfoAccess::Ca(SubjectInfoAccessCa { + access_descriptions: sia.access_descriptions.clone(), + })) + } else { + Some(SubjectInfoAccess::Ee(SubjectInfoAccessEe { + signed_object_uris: sia.signed_object_uris.clone(), + access_descriptions: sia.access_descriptions.clone(), + })) + } + } + _ => { + return Err(ResourceCertificateProfileError::DuplicateExtension( + "subjectInfoAccess", + )); + } + }; + + let certificate_policies_oid = match self.certificate_policies.as_slice() { + [] => None, + [(oids, critical)] => { + if !*critical { + return Err(ResourceCertificateProfileError::CertificatePoliciesCriticality); + } + if oids.len() != 1 { + return Err(ResourceCertificateProfileError::InvalidCertificatePolicy( + "expected exactly one policy".into(), + )); + } + let policy_oid = oids[0].clone(); + if policy_oid != OID_CP_IPADDR_ASNUMBER { + return Err(ResourceCertificateProfileError::InvalidCertificatePolicy( + policy_oid, + )); + } + Some(OID_CP_IPADDR_ASNUMBER.to_string()) + } + _ => { + return Err(ResourceCertificateProfileError::DuplicateExtension( + "certificatePolicies", + )); + } + }; + + let ip_resources = match self.ip_resources.as_slice() { + [] => None, + [(ip, critical)] => { + if !*critical { + return Err(ResourceCertificateProfileError::IpResourcesCriticality); + } + Some(ip.clone()) + } + _ => { + return Err(ResourceCertificateProfileError::DuplicateExtension( + "ipAddrBlocks", + )); + } + }; + + let as_resources = match self.as_resources.as_slice() { + [] => None, + [(asn, critical)] => { + if !*critical { + return Err(ResourceCertificateProfileError::AsResourcesCriticality); + } + Some(asn.clone()) + } + _ => { + return Err(ResourceCertificateProfileError::DuplicateExtension( + "autonomousSysIds", + )); + } + }; + + Ok(RcExtensions { + basic_constraints_ca, + subject_key_identifier, + subject_info_access, + certificate_policies_oid, + ip_resources, + as_resources, + }) + } +} + +fn algorithm_identifier_value( + ai: &x509_parser::x509::AlgorithmIdentifier<'_>, +) -> AlgorithmIdentifierValue { + let parameters = ai.parameters.as_ref().map(|p| AlgorithmParametersValue { + class: p.class(), + tag: p.tag(), + data: p.as_bytes().to_vec(), + }); + AlgorithmIdentifierValue { + oid: ai.algorithm.to_id_string(), + parameters, + } +} + +fn parse_extensions_parse( + exts: &[X509Extension<'_>], +) -> Result { + let mut basic_constraints_ca: Vec = Vec::new(); + let mut ski: Vec<(Vec, bool)> = Vec::new(); + let mut sia: Vec<(SubjectInfoAccessParsed, bool)> = Vec::new(); + let mut cert_policies: Vec<(Vec, bool)> = Vec::new(); + + let mut ip_resources: Vec<(IpResourceSet, bool)> = Vec::new(); + let mut as_resources: Vec<(AsResourceSet, bool)> = Vec::new(); for ext in exts { let oid = ext.oid.to_id_string(); match oid.as_str() { crate::data_model::oid::OID_BASIC_CONSTRAINTS => { - if basic_constraints_ca.is_some() { - return Err(ResourceCertificateError::DuplicateExtension("basicConstraints")); - } let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else { - return Err(ResourceCertificateError::Parse("basicConstraints parse failed".into())); + return Err(ResourceCertificateParseError::Parse( + "basicConstraints parse failed".into(), + )); }; - basic_constraints_ca = Some(bc.ca); + basic_constraints_ca.push(bc.ca); } OID_SUBJECT_KEY_IDENTIFIER => { - if ski.is_some() { - return Err(ResourceCertificateError::DuplicateExtension("subjectKeyIdentifier")); - } - if ext.critical { - return Err(ResourceCertificateError::SkiCriticality); - } let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else { - return Err(ResourceCertificateError::Parse("subjectKeyIdentifier parse failed".into())); + return Err(ResourceCertificateParseError::Parse( + "subjectKeyIdentifier parse failed".into(), + )); }; - ski = Some(s.0.to_vec()); + ski.push((s.0.to_vec(), ext.critical)); } OID_SUBJECT_INFO_ACCESS => { - if sia.is_some() { - return Err(ResourceCertificateError::DuplicateExtension("subjectInfoAccess")); - } - if ext.critical { - return Err(ResourceCertificateError::SiaCriticality); - } let ParsedExtension::SubjectInfoAccess(s) = ext.parsed_extension() else { - return Err(ResourceCertificateError::Parse("subjectInfoAccess parse failed".into())); + return Err(ResourceCertificateParseError::Parse( + "subjectInfoAccess parse failed".into(), + )); }; - sia = Some(parse_sia(s.accessdescs.as_slice())?); + sia.push((parse_sia_parse(s.accessdescs.as_slice())?, ext.critical)); } crate::data_model::oid::OID_CERTIFICATE_POLICIES => { - if cert_policies_oid.is_some() { - return Err(ResourceCertificateError::DuplicateExtension("certificatePolicies")); - } - if !ext.critical { - return Err(ResourceCertificateError::CertificatePoliciesCriticality); - } let ParsedExtension::CertificatePolicies(cp) = ext.parsed_extension() else { - return Err(ResourceCertificateError::Parse("certificatePolicies parse failed".into())); - }; - if cp.len() != 1 { - return Err(ResourceCertificateError::InvalidCertificatePolicy( - "expected exactly one policy".into(), + return Err(ResourceCertificateParseError::Parse( + "certificatePolicies parse failed".into(), )); - } - let policy_oid = cp[0].policy_id.to_id_string(); - if policy_oid != OID_CP_IPADDR_ASNUMBER { - return Err(ResourceCertificateError::InvalidCertificatePolicy(policy_oid)); - } - cert_policies_oid = Some(OID_CP_IPADDR_ASNUMBER.to_string()); + }; + let oids: Vec = cp.iter().map(|p| p.policy_id.to_id_string()).collect(); + cert_policies.push((oids, ext.critical)); } OID_IP_ADDR_BLOCKS => { - if ip_resources.is_some() { - return Err(ResourceCertificateError::DuplicateExtension("ipAddrBlocks")); - } - // Must be critical per RPKI profile; we only enforce when present. - if !ext.critical { - return Err(ResourceCertificateError::InvalidIpResources); - } let parsed = IpResourceSet::decode_extn_value(ext.value) - .map_err(|_e| ResourceCertificateError::InvalidIpResources)?; - ip_resources = Some(parsed); + .map_err(|_e| ResourceCertificateParseError::InvalidIpResourcesEncoding)?; + ip_resources.push((parsed, ext.critical)); } OID_AUTONOMOUS_SYS_IDS => { - if as_resources.is_some() { - return Err(ResourceCertificateError::DuplicateExtension("autonomousSysIds")); - } - if !ext.critical { - return Err(ResourceCertificateError::InvalidAsResources); - } let parsed = AsResourceSet::decode_extn_value(ext.value) - .map_err(|_e| ResourceCertificateError::InvalidAsResources)?; - as_resources = Some(parsed); + .map_err(|_e| ResourceCertificateParseError::InvalidAsResourcesEncoding)?; + as_resources.push((parsed, ext.critical)); } _ => {} } } - let basic_constraints_ca = basic_constraints_ca.unwrap_or(false); - - Ok(RcExtensions { + Ok(RcExtensionsParsed { basic_constraints_ca, subject_key_identifier: ski, subject_info_access: sia, - certificate_policies_oid: cert_policies_oid, + certificate_policies: cert_policies, ip_resources, as_resources, }) } -fn parse_sia(access: &[x509_parser::extensions::AccessDescription<'_>]) -> Result { +fn parse_sia_parse( + access: &[x509_parser::extensions::AccessDescription<'_>], +) -> Result { let mut all = Vec::with_capacity(access.len()); let mut signed_object_uris: Vec = Vec::new(); + let mut signed_object_access_location_not_uri = false; for ad in access { let access_method_oid = ad.access_method.to_id_string(); @@ -472,13 +700,13 @@ fn parse_sia(access: &[x509_parser::extensions::AccessDescription<'_>]) -> Resul x509_parser::extensions::GeneralName::URI(u) => u, _ => { if access_method_oid == OID_AD_SIGNED_OBJECT { - return Err(ResourceCertificateError::SignedObjectSiaNotUri); + signed_object_access_location_not_uri = true; } continue; } }; - let url = - Url::parse(uri).map_err(|_| ResourceCertificateError::Parse(format!("invalid URI: {uri}")))?; + let url = Url::parse(uri) + .map_err(|_| ResourceCertificateParseError::Parse(format!("invalid URI: {uri}")))?; if access_method_oid == OID_AD_SIGNED_OBJECT { signed_object_uris.push(url.clone()); } @@ -488,20 +716,11 @@ fn parse_sia(access: &[x509_parser::extensions::AccessDescription<'_>]) -> Resul }); } - if signed_object_uris.is_empty() { - return Ok(SubjectInfoAccess::Ca(SubjectInfoAccessCa { - access_descriptions: all, - })); - } - - if !signed_object_uris.iter().any(|u| u.scheme() == "rsync") { - return Err(ResourceCertificateError::SignedObjectSiaNoRsync); - } - - Ok(SubjectInfoAccess::Ee(SubjectInfoAccessEe { - signed_object_uris, + Ok(SubjectInfoAccessParsed { access_descriptions: all, - })) + signed_object_uris, + signed_object_access_location_not_uri, + }) } fn parse_ip_addr_blocks(ext_value: &[u8]) -> Result { @@ -545,7 +764,9 @@ fn parse_ip_addr_blocks(ext_value: &[u8]) -> Result { fn parse_ip_address_or_range(afi: Afi, obj: &DerObject<'_>) -> Result { match &obj.content { - BerObjectContent::BitString(_, _) => Ok(IpAddressOrRange::Prefix(parse_ip_prefix(afi, obj)?)), + BerObjectContent::BitString(_, _) => { + Ok(IpAddressOrRange::Prefix(parse_ip_prefix(afi, obj)?)) + } BerObjectContent::Sequence(_) => { let seq = obj.as_sequence().map_err(|_| ())?; if seq.len() != 2 { @@ -595,7 +816,11 @@ fn parse_ip_prefix(afi: Afi, obj: &DerObject<'_>) -> Result { /// fewer than `ub` bits. In that case, the missing bits are interpreted as 0s for the lower /// bound and 1s for the upper bound. This is essential to correctly interpret ranges that /// are expressed on non-octet boundaries. -fn parse_ip_address_bound(afi: Afi, obj: &DerObject<'_>, fill_remaining_ones: bool) -> Result, ()> { +fn parse_ip_address_bound( + afi: Afi, + obj: &DerObject<'_>, + fill_remaining_ones: bool, +) -> Result, ()> { let (unused_bits, bytes) = match &obj.content { BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()), _ => return Err(()), diff --git a/src/data_model/roa.rs b/src/data_model/roa.rs index e806d85..c4b5dd2 100644 --- a/src/data_model/roa.rs +++ b/src/data_model/roa.rs @@ -1,8 +1,10 @@ use crate::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ; use crate::data_model::rc::{Afi as RcAfi, IpPrefix as RcIpPrefix, ResourceCertificate}; -use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError}; +use crate::data_model::signed_object::{ + RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError, +}; use der_parser::ber::{BerObjectContent, Class}; -use der_parser::der::{parse_der, DerObject, Tag}; +use der_parser::der::{DerObject, Tag, parse_der}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct RoaObject { @@ -11,6 +13,13 @@ pub struct RoaObject { pub roa: RoaEContent, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RoaObjectParsed { + pub signed_object: RpkiSignedObjectParsed, + pub econtent_type: String, + pub roa: Option, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct RoaEContent { pub version: u32, @@ -18,19 +27,32 @@ pub struct RoaEContent { pub ip_addr_blocks: Vec, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RoaEContentParsed { + der: Vec, +} + #[derive(Debug, thiserror::Error)] -pub enum RoaDecodeError { - #[error("SignedObject decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")] - SignedObjectDecode(#[from] SignedObjectDecodeError), - - #[error("ROA eContentType must be {OID_CT_ROUTE_ORIGIN_AUTHZ}, got {0} (RFC 9582 §3)")] - InvalidEContentType(String), - +pub enum RoaParseError { + #[error("signed object parse error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")] + SignedObject(#[from] SignedObjectParseError), #[error("ROA parse error: {0} (RFC 9582 §4; DER)")] Parse(String), #[error("ROA trailing bytes: {0} bytes (RFC 9582 §4; DER)")] TrailingBytes(usize), +} + +#[derive(Debug, thiserror::Error)] +pub enum RoaProfileError { + #[error("signed object profile error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")] + SignedObject(#[from] SignedObjectValidateError), + + #[error("ROA eContentType must be {OID_CT_ROUTE_ORIGIN_AUTHZ}, got {0} (RFC 9582 §3)")] + InvalidEContentType(String), + + #[error("ROA profile decode error: {0} (RFC 9582 §4; DER)")] + ProfileDecode(String), #[error("RouteOriginAttestation must be a SEQUENCE of 2 or 3 elements, got {0} (RFC 9582 §4)")] InvalidAttestationSequenceLen(usize), @@ -65,13 +87,19 @@ pub enum RoaDecodeError { #[error("ROAIPAddress.address must be a BIT STRING (RFC 9582 §4.3.2.1; RFC 3779 §2.2.3.8)")] InvalidPrefixBitString, - #[error("ROAIPAddress.address has invalid unused bits encoding (RFC 9582 §4.3.2.1; RFC 3779 §2.2.3.8)")] + #[error( + "ROAIPAddress.address has invalid unused bits encoding (RFC 9582 §4.3.2.1; RFC 3779 §2.2.3.8)" + )] InvalidPrefixUnusedBits, - #[error("ROAIPAddress.address prefix length {prefix_len} out of range for {afi:?} (RFC 9582 §4.3.2.1)")] + #[error( + "ROAIPAddress.address prefix length {prefix_len} out of range for {afi:?} (RFC 9582 §4.3.2.1)" + )] PrefixLenOutOfRange { afi: RoaAfi, prefix_len: u16 }, - #[error("ROAIPAddress.maxLength out of range for {afi:?}: prefix_len={prefix_len}, max_len={max_len} (RFC 9582 §4.3.2.2)")] + #[error( + "ROAIPAddress.maxLength out of range for {afi:?}: prefix_len={prefix_len}, max_len={max_len} (RFC 9582 §4.3.2.2)" + )] InvalidMaxLength { afi: RoaAfi, prefix_len: u16, @@ -79,6 +107,15 @@ pub enum RoaDecodeError { }, } +#[derive(Debug, thiserror::Error)] +pub enum RoaDecodeError { + #[error("{0}")] + Parse(#[from] RoaParseError), + + #[error("{0}")] + Validate(#[from] RoaProfileError), +} + #[derive(Debug, thiserror::Error)] pub enum RoaValidateError { #[error("ROA EE certificate must not contain AS resources extension (RFC 9582 §5)")] @@ -90,7 +127,9 @@ pub enum RoaValidateError { #[error("ROA EE certificate IP resources must not use inherit (RFC 9582 §5)")] EeIpResourcesInherit, - #[error("ROA prefix not covered by EE certificate IP resources: {afi:?} {addr:?}/{prefix_len} (RFC 9582 §5; RFC 3779 §2.3)")] + #[error( + "ROA prefix not covered by EE certificate IP resources: {afi:?} {addr:?}/{prefix_len} (RFC 9582 §5; RFC 3779 §2.3)" + )] PrefixNotInEeResources { afi: RoaAfi, addr: Vec, @@ -99,9 +138,38 @@ pub enum RoaValidateError { } impl RoaObject { + /// Parse step of scheme A (`parse → validate → verify`). + pub fn parse_der(der: &[u8]) -> Result { + let signed_object = RpkiSignedObject::parse_der(der)?; + let econtent_type = signed_object + .signed_data + .encap_content_info + .econtent_type + .clone(); + let roa = signed_object + .signed_data + .encap_content_info + .econtent + .as_deref() + .map(RoaEContent::parse_der) + .transpose()?; + Ok(RoaObjectParsed { + signed_object, + econtent_type, + roa, + }) + } + + /// Profile validate step of scheme A (`parse → validate → verify`). + /// + /// `RoaObject` is already profile-validated when constructed via `decode_der()` / + /// `RoaObjectParsed::validate_profile()`. + pub fn validate_profile(&self) -> Result<(), RoaProfileError> { + Ok(()) + } + pub fn decode_der(der: &[u8]) -> Result { - let signed_object = RpkiSignedObject::decode_der(der)?; - Self::from_signed_object(signed_object) + Ok(Self::parse_der(der)?.validate_profile()?) } pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result { @@ -111,7 +179,7 @@ impl RoaObject { .econtent_type .clone(); if econtent_type != OID_CT_ROUTE_ORIGIN_AUTHZ { - return Err(RoaDecodeError::InvalidEContentType(econtent_type)); + return Err(RoaProfileError::InvalidEContentType(econtent_type).into()); } let roa = RoaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?; @@ -166,67 +234,26 @@ pub struct IpPrefix { } impl RoaEContent { - /// Decode the DER-encoded RouteOriginAttestation defined in RFC 9582 §4. - pub fn decode_der(der: &[u8]) -> Result { - let (rem, obj) = parse_der(der).map_err(|e| RoaDecodeError::Parse(e.to_string()))?; + /// Parse step of scheme A (`parse → validate → verify`). + pub fn parse_der(der: &[u8]) -> Result { + let (rem, _obj) = parse_der(der).map_err(|e| RoaParseError::Parse(e.to_string()))?; if !rem.is_empty() { - return Err(RoaDecodeError::TrailingBytes(rem.len())); + return Err(RoaParseError::TrailingBytes(rem.len())); } + Ok(RoaEContentParsed { der: der.to_vec() }) + } - let seq = obj - .as_sequence() - .map_err(|e| RoaDecodeError::Parse(e.to_string()))?; - if seq.len() != 2 && seq.len() != 3 { - return Err(RoaDecodeError::InvalidAttestationSequenceLen(seq.len())); - } + /// Profile validate step of scheme A (`parse → validate → verify`). + /// + /// `RoaEContent` is already profile-validated when constructed via `decode_der()` / + /// `RoaEContentParsed::validate_profile()`. + pub fn validate_profile(&self) -> Result<(), RoaProfileError> { + Ok(()) + } - let mut idx = 0; - let mut version: u32 = 0; - if seq.len() == 3 { - let v_obj = &seq[0]; - if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) { - return Err(RoaDecodeError::Parse( - "RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(), - )); - } - let inner_der = v_obj - .as_slice() - .map_err(|e| RoaDecodeError::Parse(e.to_string()))?; - let (rem, inner) = - parse_der(inner_der).map_err(|e| RoaDecodeError::Parse(e.to_string()))?; - if !rem.is_empty() { - return Err(RoaDecodeError::Parse( - "trailing bytes inside RouteOriginAttestation.version".into(), - )); - } - let v = inner - .as_u64() - .map_err(|e| RoaDecodeError::Parse(e.to_string()))?; - if v != 0 { - return Err(RoaDecodeError::InvalidVersion(v)); - } - version = 0; - idx = 1; - } - - let as_id_u64 = seq[idx] - .as_u64() - .map_err(|e| RoaDecodeError::Parse(e.to_string()))?; - if as_id_u64 > u32::MAX as u64 { - return Err(RoaDecodeError::AsIdOutOfRange(as_id_u64)); - } - let as_id = as_id_u64 as u32; - idx += 1; - - let ip_addr_blocks = parse_ip_addr_blocks(&seq[idx])?; - - let mut out = Self { - version, - as_id, - ip_addr_blocks, - }; - out.canonicalize(); - Ok(out) + /// Decode the DER-encoded RouteOriginAttestation defined in RFC 9582 §4 (`parse + validate`). + pub fn decode_der(der: &[u8]) -> Result { + Ok(Self::parse_der(der)?.validate_profile()?) } pub fn canonicalize(&mut self) { @@ -241,7 +268,10 @@ impl RoaEContent { /// /// This performs the EE/payload semantic checks that do not require certificate path /// validation. - pub fn validate_against_ee_cert(&self, ee: &ResourceCertificate) -> Result<(), RoaValidateError> { + pub fn validate_against_ee_cert( + &self, + ee: &ResourceCertificate, + ) -> Result<(), RoaValidateError> { if ee.tbs.extensions.as_resources.is_some() { return Err(RoaValidateError::EeAsResourcesPresent); } @@ -273,6 +303,91 @@ impl RoaEContent { } } +impl RoaObjectParsed { + pub fn validate_profile(self) -> Result { + let signed_object = self.signed_object.validate_profile()?; + let econtent_type = signed_object + .signed_data + .encap_content_info + .econtent_type + .clone(); + if econtent_type != OID_CT_ROUTE_ORIGIN_AUTHZ { + return Err(RoaProfileError::InvalidEContentType(econtent_type)); + } + let roa = self + .roa + .ok_or_else(|| RoaProfileError::ProfileDecode("ROA.eContent missing".into()))? + .validate_profile()?; + Ok(RoaObject { + signed_object, + econtent_type: OID_CT_ROUTE_ORIGIN_AUTHZ.to_string(), + roa, + }) + } +} + +impl RoaEContentParsed { + pub fn validate_profile(self) -> Result { + let (_rem, obj) = + parse_der(&self.der).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; + + let seq = obj + .as_sequence() + .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; + if seq.len() != 2 && seq.len() != 3 { + return Err(RoaProfileError::InvalidAttestationSequenceLen(seq.len())); + } + + let mut idx = 0; + let mut version: u32 = 0; + if seq.len() == 3 { + let v_obj = &seq[0]; + if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) { + return Err(RoaProfileError::ProfileDecode( + "RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(), + )); + } + let inner_der = v_obj + .as_slice() + .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; + let (rem, inner) = + parse_der(inner_der).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; + if !rem.is_empty() { + return Err(RoaProfileError::ProfileDecode( + "trailing bytes inside RouteOriginAttestation.version".into(), + )); + } + let v = inner + .as_u64() + .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; + if v != 0 { + return Err(RoaProfileError::InvalidVersion(v)); + } + version = 0; + idx = 1; + } + + let as_id_u64 = seq[idx] + .as_u64() + .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; + if as_id_u64 > u32::MAX as u64 { + return Err(RoaProfileError::AsIdOutOfRange(as_id_u64)); + } + let as_id = as_id_u64 as u32; + idx += 1; + + let ip_addr_blocks = parse_ip_addr_blocks(&seq[idx])?; + + let mut out = RoaEContent { + version, + as_id, + ip_addr_blocks, + }; + out.canonicalize(); + Ok(out) + } +} + fn roa_prefix_to_rc(p: &IpPrefix) -> RcIpPrefix { let afi = match p.afi { RoaAfi::Ipv4 => RcAfi::Ipv4, @@ -285,60 +400,63 @@ fn roa_prefix_to_rc(p: &IpPrefix) -> RcIpPrefix { } } -fn parse_ip_addr_blocks(obj: &DerObject<'_>) -> Result, RoaDecodeError> { +fn parse_ip_addr_blocks(obj: &DerObject<'_>) -> Result, RoaProfileError> { let seq = obj .as_sequence() - .map_err(|e| RoaDecodeError::Parse(e.to_string()))?; + .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; if seq.is_empty() || seq.len() > 2 { - return Err(RoaDecodeError::InvalidIpAddrBlocksLen(seq.len())); + return Err(RoaProfileError::InvalidIpAddrBlocksLen(seq.len())); } let mut out: Vec = Vec::new(); for fam in seq { let family = parse_ip_address_family(fam)?; if out.iter().any(|f| f.afi == family.afi) { - return Err(RoaDecodeError::DuplicateAfi(family.afi)); + return Err(RoaProfileError::DuplicateAfi(family.afi)); } out.push(family); } Ok(out) } -fn parse_ip_address_family(obj: &DerObject<'_>) -> Result { +fn parse_ip_address_family(obj: &DerObject<'_>) -> Result { let seq = obj .as_sequence() - .map_err(|e| RoaDecodeError::Parse(e.to_string()))?; + .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; if seq.len() != 2 { - return Err(RoaDecodeError::InvalidIpAddressFamily); + return Err(RoaProfileError::InvalidIpAddressFamily); } let afi = parse_afi(&seq[0])?; let addresses = parse_roa_addresses(afi, &seq[1])?; if addresses.is_empty() { - return Err(RoaDecodeError::EmptyAddressList); + return Err(RoaProfileError::EmptyAddressList); } Ok(RoaIpAddressFamily { afi, addresses }) } -fn parse_afi(obj: &DerObject<'_>) -> Result { +fn parse_afi(obj: &DerObject<'_>) -> Result { let bytes = obj .as_slice() - .map_err(|_e| RoaDecodeError::InvalidAddressFamily)?; + .map_err(|_e| RoaProfileError::InvalidAddressFamily)?; if bytes.len() != 2 { - return Err(RoaDecodeError::InvalidAddressFamily); + return Err(RoaProfileError::InvalidAddressFamily); } match bytes { [0x00, 0x01] => Ok(RoaAfi::Ipv4), [0x00, 0x02] => Ok(RoaAfi::Ipv6), - _ => Err(RoaDecodeError::UnsupportedAfi(bytes.to_vec())), + _ => Err(RoaProfileError::UnsupportedAfi(bytes.to_vec())), } } -fn parse_roa_addresses(afi: RoaAfi, obj: &DerObject<'_>) -> Result, RoaDecodeError> { +fn parse_roa_addresses( + afi: RoaAfi, + obj: &DerObject<'_>, +) -> Result, RoaProfileError> { let seq = obj .as_sequence() - .map_err(|e| RoaDecodeError::Parse(e.to_string()))?; + .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; let mut out = Vec::with_capacity(seq.len()); for entry in seq { out.push(parse_roa_ip_address(afi, entry)?); @@ -346,12 +464,12 @@ fn parse_roa_addresses(afi: RoaAfi, obj: &DerObject<'_>) -> Result) -> Result { +fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result { let seq = obj .as_sequence() - .map_err(|e| RoaDecodeError::Parse(e.to_string()))?; + .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; if seq.is_empty() || seq.len() > 2 { - return Err(RoaDecodeError::InvalidRoaIpAddress); + return Err(RoaProfileError::InvalidRoaIpAddress); } let prefix = parse_prefix_bits(afi, &seq[0])?; @@ -360,10 +478,10 @@ fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result { let v = m .as_u64() - .map_err(|e| RoaDecodeError::Parse(e.to_string()))?; + .map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?; let max_len: u16 = v .try_into() - .map_err(|_e| RoaDecodeError::InvalidMaxLength { + .map_err(|_e| RoaProfileError::InvalidMaxLength { afi, prefix_len: prefix.prefix_len, max_len: u16::MAX, @@ -375,7 +493,7 @@ fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result ub || max_len < prefix.prefix_len { - return Err(RoaDecodeError::InvalidMaxLength { + return Err(RoaProfileError::InvalidMaxLength { afi, prefix_len: prefix.prefix_len, max_len, @@ -386,31 +504,31 @@ fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result) -> Result { +fn parse_prefix_bits(afi: RoaAfi, obj: &DerObject<'_>) -> Result { let (unused_bits, bytes) = match &obj.content { BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()), - _ => return Err(RoaDecodeError::InvalidPrefixBitString), + _ => return Err(RoaProfileError::InvalidPrefixBitString), }; if unused_bits > 7 { - return Err(RoaDecodeError::InvalidPrefixUnusedBits); + return Err(RoaProfileError::InvalidPrefixUnusedBits); } if bytes.is_empty() { if unused_bits != 0 { - return Err(RoaDecodeError::InvalidPrefixUnusedBits); + return Err(RoaProfileError::InvalidPrefixUnusedBits); } } else if unused_bits != 0 { let mask = (1u8 << unused_bits) - 1; if (bytes[bytes.len() - 1] & mask) != 0 { - return Err(RoaDecodeError::InvalidPrefixUnusedBits); + return Err(RoaProfileError::InvalidPrefixUnusedBits); } } let prefix_len = (bytes.len() * 8) .checked_sub(unused_bits as usize) - .ok_or(RoaDecodeError::InvalidPrefixUnusedBits)? as u16; + .ok_or(RoaProfileError::InvalidPrefixUnusedBits)? as u16; if prefix_len > afi.ub() { - return Err(RoaDecodeError::PrefixLenOutOfRange { afi, prefix_len }); + return Err(RoaProfileError::PrefixLenOutOfRange { afi, prefix_len }); } let addr = canonicalize_prefix_addr(afi, prefix_len, &bytes); diff --git a/src/data_model/signed_object.rs b/src/data_model/signed_object.rs index 61bee43..8ea93de 100644 --- a/src/data_model/signed_object.rs +++ b/src/data_model/signed_object.rs @@ -1,15 +1,15 @@ use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc}; use crate::data_model::oid::{ - OID_CMS_ATTR_CONTENT_TYPE, OID_CMS_ATTR_MESSAGE_DIGEST, OID_CMS_ATTR_SIGNING_TIME, - OID_RSA_ENCRYPTION, OID_SHA256, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SIGNED_DATA, - OID_AD_SIGNED_OBJECT, OID_SUBJECT_INFO_ACCESS, + OID_AD_SIGNED_OBJECT, OID_CMS_ATTR_CONTENT_TYPE, OID_CMS_ATTR_MESSAGE_DIGEST, + OID_CMS_ATTR_SIGNING_TIME, OID_RSA_ENCRYPTION, OID_SHA256, OID_SHA256_WITH_RSA_ENCRYPTION, + OID_SIGNED_DATA, OID_SUBJECT_INFO_ACCESS, }; use crate::data_model::rc::{ResourceCertificate, SubjectInfoAccess}; use der_parser::ber::Class; -use der_parser::der::{parse_der, DerObject, Tag}; +use der_parser::der::{DerObject, Tag, parse_der}; use sha2::{Digest, Sha256}; -use x509_parser::public_key::PublicKey; use x509_parser::prelude::FromDer; +use x509_parser::public_key::PublicKey; use x509_parser::x509::SubjectPublicKeyInfo; #[derive(Clone, Debug, PartialEq, Eq)] @@ -64,36 +64,98 @@ pub struct SignedAttrsProfiled { pub other_attrs_present: bool, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RpkiSignedObjectParsed { + pub raw_der: Vec, + pub content_info_content_type: String, + pub signed_data: SignedDataParsed, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SignedDataParsed { + pub version: u64, + pub digest_algorithms: Vec, + pub encap_content_info: EncapsulatedContentInfoParsed, + pub certificates: Option>>, + pub crls_present: bool, + pub signer_infos: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AlgorithmIdentifierParsed { + pub oid: String, + pub params_ok: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EncapsulatedContentInfoParsed { + pub econtent_type: String, + pub econtent: Option>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SignerInfoParsed { + pub version: u64, + pub sid: SignerIdentifierParsed, + pub digest_algorithm: AlgorithmIdentifierParsed, + pub signature_algorithm: AlgorithmIdentifierParsed, + pub signed_attrs_content: Option>, + pub signed_attrs_der_for_signature: Option>, + pub unsigned_attrs_present: bool, + pub signature: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SignerIdentifierParsed { + SubjectKeyIdentifier(Vec), + Other, +} + #[derive(Debug, thiserror::Error)] -pub enum SignedObjectDecodeError { +pub enum SignedObjectParseError { #[error("DER parse error: {0} (RFC 6488 §2; RFC 6488 §3(1l); RFC 5652 §3/§5)")] Parse(String), #[error("trailing bytes after DER object: {0} bytes (DER; RFC 6488 §3(1l))")] TrailingBytes(usize), +} - #[error("ContentInfo.contentType must be SignedData ({OID_SIGNED_DATA}), got {0} (RFC 6488 §3(1a); RFC 5652 §3)")] +#[derive(Debug, thiserror::Error)] +pub enum SignedObjectValidateError { + #[error( + "ContentInfo.contentType must be SignedData ({OID_SIGNED_DATA}), got {0} (RFC 6488 §3(1a); RFC 5652 §3)" + )] InvalidContentInfoContentType(String), - #[error("SignedData.version must be 3, got {0} (RFC 6488 §2.1.1; RFC 6488 §3(1b); RFC 5652 §5.1)")] + #[error( + "SignedData.version must be 3, got {0} (RFC 6488 §2.1.1; RFC 6488 §3(1b); RFC 5652 §5.1)" + )] InvalidSignedDataVersion(u64), - #[error("SignedData.digestAlgorithms must contain exactly one AlgorithmIdentifier, got {0} (RFC 6488 §2.1.2; RFC 6488 §3(1b); RFC 5652 §5.1)")] + #[error( + "SignedData.digestAlgorithms must contain exactly one AlgorithmIdentifier, got {0} (RFC 6488 §2.1.2; RFC 6488 §3(1b); RFC 5652 §5.1)" + )] InvalidDigestAlgorithmsCount(usize), - #[error("digest algorithm must be id-sha256 ({OID_SHA256}), got {0} (RFC 6488 §2.1.2; RFC 6488 §3(1b); RFC 7935 §2)")] + #[error( + "digest algorithm must be id-sha256 ({OID_SHA256}), got {0} (RFC 6488 §2.1.2; RFC 6488 §3(1b); RFC 7935 §2)" + )] InvalidDigestAlgorithm(String), #[error("SignedData.certificates MUST be present (RFC 6488 §3(1c); RFC 5652 §5.1)")] CertificatesMissing, - #[error("SignedData.certificates must contain exactly one EE certificate, got {0} (RFC 6488 §3(1c))")] + #[error( + "SignedData.certificates must contain exactly one EE certificate, got {0} (RFC 6488 §3(1c))" + )] InvalidCertificatesCount(usize), #[error("SignedData.crls MUST be omitted (RFC 6488 §3(1d))")] CrlsPresent, - #[error("SignedData.signerInfos must contain exactly one SignerInfo, got {0} (RFC 6488 §2.1; RFC 6488 §3(1e); RFC 5652 §5.1)")] + #[error( + "SignedData.signerInfos must contain exactly one SignerInfo, got {0} (RFC 6488 §2.1; RFC 6488 §3(1e); RFC 5652 §5.1)" + )] InvalidSignerInfosCount(usize), #[error("SignerInfo.version must be 3, got {0} (RFC 6488 §3(1e); RFC 5652 §5.3)")] @@ -102,7 +164,9 @@ pub enum SignedObjectDecodeError { #[error("SignerInfo.sid must be subjectKeyIdentifier [0] (RFC 6488 §3(1c); RFC 5652 §5.3)")] InvalidSignerIdentifier, - #[error("SignerInfo.digestAlgorithm must be id-sha256 ({OID_SHA256}), got {0} (RFC 6488 §3(1j); RFC 7935 §2)")] + #[error( + "SignerInfo.digestAlgorithm must be id-sha256 ({OID_SHA256}), got {0} (RFC 6488 §3(1j); RFC 7935 §2)" + )] InvalidSignerInfoDigestAlgorithm(String), #[error("SignerInfo.signedAttrs MUST be present (RFC 9589 §4; RFC 6488 §3(1f))")] @@ -117,7 +181,9 @@ sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6488 § )] InvalidSignatureAlgorithm(String), - #[error("SignerInfo.signatureAlgorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)")] + #[error( + "SignerInfo.signatureAlgorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)" + )] InvalidSignatureAlgorithmParameters, #[error("signedAttrs contains unsupported attribute OID {0} (RFC 9589 §4; RFC 6488 §2.1.6.4)")] @@ -126,10 +192,32 @@ sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6488 § #[error("signedAttrs contains duplicate attribute OID {0} (RFC 6488 §2.1.6.4; RFC 9589 §4)")] DuplicateSignedAttribute(String), - #[error("signedAttrs attribute {oid} attrValues must contain exactly one value, got {count} (RFC 6488 §2.1.6.4; RFC 5652 §5.3)")] + #[error("signedAttrs parse error: {0} (RFC 5652 §5.3; RFC 6488 §3(1f); RFC 9589 §4)")] + SignedAttrsParse(String), + + #[error( + "signedAttrs attribute {oid} attrValues must contain exactly one value, got {count} (RFC 6488 §2.1.6.4; RFC 5652 §5.3)" + )] InvalidSignedAttributeValuesCount { oid: String, count: usize }, - #[error("signedAttrs.content-type attrValues must equal eContentType ({econtent_type}), got {attr_content_type} (RFC 6488 §3(1h); RFC 9589 §4)")] + #[error( + "signedAttrs missing content-type attribute (RFC 9589 §4; RFC 5652 §11.1; RFC 6488 §2.1.6.4)" + )] + SignedAttrsContentTypeMissing, + + #[error( + "signedAttrs missing message-digest attribute (RFC 9589 §4; RFC 5652 §11.2; RFC 6488 §2.1.6.4)" + )] + SignedAttrsMessageDigestMissing, + + #[error( + "signedAttrs missing signing-time attribute (RFC 9589 §4; RFC 5652 §11.3; RFC 6488 §2.1.6.4)" + )] + SignedAttrsSigningTimeMissing, + + #[error( + "signedAttrs.content-type attrValues must equal eContentType ({econtent_type}), got {attr_content_type} (RFC 6488 §3(1h); RFC 9589 §4)" + )] ContentTypeAttrMismatch { econtent_type: String, attr_content_type: String, @@ -138,34 +226,59 @@ sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6488 § #[error("EncapsulatedContentInfo.eContent MUST be present (RFC 6488 §2.1.3; RFC 5652 §5.2)")] EContentMissing, - #[error("signedAttrs.message-digest does not match SHA-256(eContent) (RFC 6488 §3(1f); RFC 5652 §11.2)")] + #[error( + "signedAttrs.message-digest does not match SHA-256(eContent) (RFC 6488 §3(1f); RFC 5652 §11.2)" + )] MessageDigestMismatch, #[error("EE certificate parse error: {0} (RFC 6488 §3(1c); RFC 6487 §4)")] EeCertificateParse(String), - #[error("EE certificate missing SubjectKeyIdentifier extension (RFC 6488 §3(1c); RFC 6487 §4.8.2)")] + #[error( + "EE certificate missing SubjectKeyIdentifier extension (RFC 6488 §3(1c); RFC 6487 §4.8.2)" + )] EeCertificateMissingSki, - #[error("EE certificate missing SubjectInfoAccess extension ({OID_SUBJECT_INFO_ACCESS}) (RFC 6487 §4.8.8.2)")] + #[error( + "EE certificate missing SubjectInfoAccess extension ({OID_SUBJECT_INFO_ACCESS}) (RFC 6487 §4.8.8.2)" + )] EeCertificateMissingSia, - #[error("EE certificate SIA missing id-ad-signedObject access method ({OID_AD_SIGNED_OBJECT}) (RFC 6487 §4.8.8.2)")] + #[error( + "EE certificate SIA missing id-ad-signedObject access method ({OID_AD_SIGNED_OBJECT}) (RFC 6487 §4.8.8.2)" + )] EeCertificateMissingSignedObjectSia, - #[error("EE certificate SIA id-ad-signedObject accessLocation must be a URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)")] + #[error( + "EE certificate SIA id-ad-signedObject accessLocation must be a URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)" + )] EeCertificateSignedObjectSiaNotUri, - #[error("EE certificate SIA id-ad-signedObject must include at least one rsync:// URI (RFC 6487 §4.8.8.2)")] + #[error( + "EE certificate SIA id-ad-signedObject must include at least one rsync:// URI (RFC 6487 §4.8.8.2)" + )] EeCertificateSignedObjectSiaNoRsync, - #[error("SignerInfo.sid SKI does not match EE certificate SKI (RFC 6488 §3(1c); RFC 5652 §5.3)")] + #[error( + "SignerInfo.sid SKI does not match EE certificate SKI (RFC 6488 §3(1c); RFC 5652 §5.3)" + )] SidSkiMismatch, - #[error("invalid signing-time attribute value (expected UTCTime or GeneralizedTime) (RFC 5652 §11.3; RFC 9589 §4)")] + #[error( + "invalid signing-time attribute value (expected UTCTime or GeneralizedTime) (RFC 5652 §11.3; RFC 9589 §4)" + )] InvalidSigningTimeValue, } +#[derive(Debug, thiserror::Error)] +pub enum SignedObjectDecodeError { + #[error("SignedObject parse error: {0}")] + Parse(#[from] SignedObjectParseError), + + #[error("SignedObject validate error: {0}")] + Validate(#[from] SignedObjectValidateError), +} + #[derive(Debug, thiserror::Error)] pub enum SignedObjectVerifyError { #[error("EE SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")] @@ -185,39 +298,48 @@ pub enum SignedObjectVerifyError { } impl RpkiSignedObject { - /// Decode a DER-encoded RPKI Signed Object (CMS ContentInfo wrapping SignedData) and enforce - /// the profile constraints from RFC 6488 §2-§3 and RFC 9589 §4. - pub fn decode_der(der: &[u8]) -> Result { - let (rem, obj) = parse_der(der).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; + /// Parse a DER-encoded RPKI Signed Object (CMS ContentInfo wrapping SignedData). + /// + /// This performs encoding/structure parsing only. Profile constraints are enforced by + /// `RpkiSignedObjectParsed::validate_profile`. + pub fn parse_der(der: &[u8]) -> Result { + let (rem, obj) = + parse_der(der).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; if !rem.is_empty() { - return Err(SignedObjectDecodeError::TrailingBytes(rem.len())); + return Err(SignedObjectParseError::TrailingBytes(rem.len())); } let content_info_seq = obj .as_sequence() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; if content_info_seq.len() != 2 { - return Err(SignedObjectDecodeError::Parse( + return Err(SignedObjectParseError::Parse( "ContentInfo must be a SEQUENCE of 2 elements".into(), )); } - let content_type = oid_to_string(&content_info_seq[0])?; - if content_type != OID_SIGNED_DATA { - return Err(SignedObjectDecodeError::InvalidContentInfoContentType( - content_type, - )); - } + let content_type = oid_to_string_parse(&content_info_seq[0])?; + let signed_data = parse_signed_data_from_contentinfo_parse(&content_info_seq[1])?; - let signed_data = parse_signed_data_from_contentinfo(&content_info_seq[1])?; - - Ok(RpkiSignedObject { + Ok(RpkiSignedObjectParsed { raw_der: der.to_vec(), - content_info_content_type: OID_SIGNED_DATA.to_string(), + content_info_content_type: content_type, signed_data, }) } + /// Decode a DER-encoded RPKI Signed Object (CMS ContentInfo wrapping SignedData) and enforce + /// the profile constraints from RFC 6488 §2-§3 and RFC 9589 §4. + pub fn decode_der(der: &[u8]) -> Result { + let parsed = Self::parse_der(der)?; + Ok(parsed.validate_profile()?) + } + + /// Scheme-A naming for signature verification. + pub fn verify(&self) -> Result<(), SignedObjectVerifyError> { + self.verify_signature() + } + /// Verify the CMS signature using the embedded EE certificate public key. pub fn verify_signature(&self) -> Result<(), SignedObjectVerifyError> { let ee = &self.signed_data.certificates[0]; @@ -271,208 +393,196 @@ impl RpkiSignedObject { } } -fn parse_signed_data_from_contentinfo( +impl RpkiSignedObjectParsed { + pub fn validate_profile(self) -> Result { + if self.content_info_content_type != OID_SIGNED_DATA { + return Err(SignedObjectValidateError::InvalidContentInfoContentType( + self.content_info_content_type, + )); + } + + let signed_data = validate_signed_data_profile(self.signed_data)?; + + Ok(RpkiSignedObject { + raw_der: self.raw_der, + content_info_content_type: OID_SIGNED_DATA.to_string(), + signed_data, + }) + } +} + +fn parse_signed_data_from_contentinfo_parse( obj: &DerObject<'_>, -) -> Result { +) -> Result { // ContentInfo.content is `[0] EXPLICIT`, but `der-parser` will represent unknown tagged // objects as `Unknown(Any)`. For EXPLICIT tags, the content octets are the full encoding of // the inner object, so we parse it from the object's slice. if obj.class() != Class::ContextSpecific || obj.tag() != Tag(0) { - return Err(SignedObjectDecodeError::Parse( + return Err(SignedObjectParseError::Parse( "ContentInfo.content must be [0] EXPLICIT".into(), )); } let inner_der = obj .as_slice() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; let (rem, inner_obj) = - parse_der(inner_der).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; + parse_der(inner_der).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; if !rem.is_empty() { - return Err(SignedObjectDecodeError::Parse( + return Err(SignedObjectParseError::Parse( "trailing bytes inside ContentInfo.content".into(), )); } - parse_signed_data(&inner_obj) + parse_signed_data_parse(&inner_obj) } -fn parse_signed_data(obj: &DerObject<'_>) -> Result { +fn parse_signed_data_parse( + obj: &DerObject<'_>, +) -> Result { let seq = obj .as_sequence() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; if seq.len() < 4 || seq.len() > 6 { - return Err(SignedObjectDecodeError::Parse( + return Err(SignedObjectParseError::Parse( "SignedData must be a SEQUENCE of 4..6 elements".into(), )); } let version = seq[0] .as_u64() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; - if version != 3 { - return Err(SignedObjectDecodeError::InvalidSignedDataVersion(version)); - } + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; let digest_set = seq[1] .as_set() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; - if digest_set.len() != 1 { - return Err(SignedObjectDecodeError::InvalidDigestAlgorithmsCount( - digest_set.len(), - )); + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + let mut digest_algorithms: Vec = + Vec::with_capacity(digest_set.len()); + for item in digest_set { + let (oid, params_ok) = parse_algorithm_identifier_parse(item)?; + digest_algorithms.push(AlgorithmIdentifierParsed { oid, params_ok }); } - let (digest_oid, _params_ok) = parse_algorithm_identifier(&digest_set[0])?; - if digest_oid != OID_SHA256 { - return Err(SignedObjectDecodeError::InvalidDigestAlgorithm(digest_oid)); - } - let digest_algorithms = vec![OID_SHA256.to_string()]; - let encap_content_info = parse_encapsulated_content_info(&seq[2])?; + let encap_content_info = parse_encapsulated_content_info_parse(&seq[2])?; - let mut certificates: Option> = None; + let mut certificates: Option>> = None; let mut crls_present = false; let mut signer_infos_obj: Option<&DerObject<'_>> = None; for item in &seq[3..] { if item.class() == Class::ContextSpecific && item.tag() == Tag(0) { if certificates.is_some() { - return Err(SignedObjectDecodeError::Parse( + return Err(SignedObjectParseError::Parse( "SignedData.certificates appears more than once".into(), )); } - certificates = Some(parse_certificate_set_implicit(item)?); + certificates = Some(parse_certificate_set_implicit_parse(item)?); } else if item.class() == Class::ContextSpecific && item.tag() == Tag(1) { crls_present = true; } else if item.class() == Class::Universal && item.tag() == Tag::Set { signer_infos_obj = Some(item); } else { - return Err(SignedObjectDecodeError::Parse( + return Err(SignedObjectParseError::Parse( "unexpected field in SignedData".into(), )); } } - if crls_present { - return Err(SignedObjectDecodeError::CrlsPresent); - } - let certificates = certificates.ok_or(SignedObjectDecodeError::CertificatesMissing)?; - if certificates.len() != 1 { - return Err(SignedObjectDecodeError::InvalidCertificatesCount( - certificates.len(), - )); - } - - let signer_infos_obj = signer_infos_obj.ok_or_else(|| { - SignedObjectDecodeError::Parse("SignedData.signerInfos missing".into()) - })?; + let signer_infos_obj = signer_infos_obj + .ok_or_else(|| SignedObjectParseError::Parse("SignedData.signerInfos missing".into()))?; let signer_infos_set = signer_infos_obj .as_set() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; - if signer_infos_set.len() != 1 { - return Err(SignedObjectDecodeError::InvalidSignerInfosCount( - signer_infos_set.len(), - )); + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + let mut signer_infos: Vec = Vec::with_capacity(signer_infos_set.len()); + for si in signer_infos_set { + signer_infos.push(parse_signer_info_parse(si)?); } - let signer_info = parse_signer_info(&signer_infos_set[0])?; - if signer_info.sid_ski != certificates[0].subject_key_identifier { - return Err(SignedObjectDecodeError::SidSkiMismatch); - } - if signer_info.signed_attrs.content_type != encap_content_info.econtent_type { - return Err(SignedObjectDecodeError::ContentTypeAttrMismatch { - econtent_type: encap_content_info.econtent_type.clone(), - attr_content_type: signer_info.signed_attrs.content_type.clone(), - }); - } - - let computed = Sha256::digest(&encap_content_info.econtent).to_vec(); - if computed != signer_info.signed_attrs.message_digest { - return Err(SignedObjectDecodeError::MessageDigestMismatch); - } - - Ok(SignedDataProfiled { - version: 3, + Ok(SignedDataParsed { + version, digest_algorithms, encap_content_info, certificates, crls_present, - signer_infos: vec![signer_info], + signer_infos, }) } -fn parse_encapsulated_content_info(obj: &DerObject<'_>) -> Result { +fn parse_encapsulated_content_info_parse( + obj: &DerObject<'_>, +) -> Result { let seq = obj .as_sequence() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; - if seq.len() == 1 { - return Err(SignedObjectDecodeError::EContentMissing); - } - if seq.len() != 2 { - return Err(SignedObjectDecodeError::Parse( + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + if seq.is_empty() || seq.len() > 2 { + return Err(SignedObjectParseError::Parse( "EncapsulatedContentInfo must be SEQUENCE of 1..2".into(), )); } - let econtent_type = oid_to_string(&seq[0])?; + let econtent_type = oid_to_string_parse(&seq[0])?; - let econtent_tagged = &seq[1]; - if econtent_tagged.class() != Class::ContextSpecific || econtent_tagged.tag() != Tag(0) { - return Err(SignedObjectDecodeError::Parse( - "EncapsulatedContentInfo.eContent must be [0] EXPLICIT".into(), - )); - } - let inner_der = econtent_tagged - .as_slice() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))? - ; - let (rem, inner_obj) = - parse_der(inner_der).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; - if !rem.is_empty() { - return Err(SignedObjectDecodeError::Parse( - "trailing bytes inside EncapsulatedContentInfo.eContent".into(), - )); - } - let econtent = inner_obj - .as_slice() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))? - .to_vec(); - if econtent.is_empty() { - return Err(SignedObjectDecodeError::EContentMissing); - } + let econtent = match seq.get(1) { + None => None, + Some(econtent_tagged) => { + if econtent_tagged.class() != Class::ContextSpecific || econtent_tagged.tag() != Tag(0) + { + return Err(SignedObjectParseError::Parse( + "EncapsulatedContentInfo.eContent must be [0] EXPLICIT".into(), + )); + } + let inner_der = econtent_tagged + .as_slice() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + let (rem, inner_obj) = + parse_der(inner_der).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + if !rem.is_empty() { + return Err(SignedObjectParseError::Parse( + "trailing bytes inside EncapsulatedContentInfo.eContent".into(), + )); + } + Some( + inner_obj + .as_slice() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))? + .to_vec(), + ) + } + }; - Ok(EncapsulatedContentInfo { + Ok(EncapsulatedContentInfoParsed { econtent_type, econtent, }) } -fn parse_certificate_set_implicit(obj: &DerObject<'_>) -> Result, SignedObjectDecodeError> { +fn parse_certificate_set_implicit_parse( + obj: &DerObject<'_>, +) -> Result>, SignedObjectParseError> { let content = obj .as_slice() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; let mut input = content; let mut certs = Vec::new(); while !input.is_empty() { let (rem, _any_obj) = - parse_der(input).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; + parse_der(input).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; let consumed = input.len() - rem.len(); - let der = &input[..consumed]; - certs.push(parse_ee_certificate(der)?); + certs.push(input[..consumed].to_vec()); input = rem; } Ok(certs) } -fn parse_ee_certificate(der: &[u8]) -> Result { +fn validate_ee_certificate(der: &[u8]) -> Result { let rc = match ResourceCertificate::from_der(der) { Ok(v) => v, Err(e) => { return match e { - crate::data_model::rc::ResourceCertificateError::SignedObjectSiaNotUri => { - Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNotUri) - } - crate::data_model::rc::ResourceCertificateError::SignedObjectSiaNoRsync => { - Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync) - } - _ => Err(SignedObjectDecodeError::EeCertificateParse(e.to_string())), + crate::data_model::rc::ResourceCertificateDecodeError::Validate( + crate::data_model::rc::ResourceCertificateProfileError::SignedObjectSiaNotUri, + ) => Err(SignedObjectValidateError::EeCertificateSignedObjectSiaNotUri), + crate::data_model::rc::ResourceCertificateDecodeError::Validate( + crate::data_model::rc::ResourceCertificateProfileError::SignedObjectSiaNoRsync, + ) => Err(SignedObjectValidateError::EeCertificateSignedObjectSiaNoRsync), + _ => Err(SignedObjectValidateError::EeCertificateParse(e.to_string())), }; } }; @@ -482,7 +592,7 @@ fn parse_ee_certificate(der: &[u8]) -> Result Result = match sia { SubjectInfoAccess::Ee(ee) => ee .signed_object_uris @@ -501,10 +611,10 @@ fn parse_ee_certificate(der: &[u8]) -> Result Vec::new(), }; if signed_object_uris.is_empty() { - return Err(SignedObjectDecodeError::EeCertificateMissingSignedObjectSia); + return Err(SignedObjectValidateError::EeCertificateMissingSignedObjectSia); } if !signed_object_uris.iter().any(|u| u.starts_with("rsync://")) { - return Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync); + return Err(SignedObjectValidateError::EeCertificateSignedObjectSiaNoRsync); } Ok(ResourceEeCertificate { @@ -516,182 +626,314 @@ fn parse_ee_certificate(der: &[u8]) -> Result) -> Result { +fn parse_signer_info_parse( + obj: &DerObject<'_>, +) -> Result { let seq = obj .as_sequence() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; if seq.len() < 5 || seq.len() > 7 { - return Err(SignedObjectDecodeError::Parse( + return Err(SignedObjectParseError::Parse( "SignerInfo must be a SEQUENCE of 5..7 elements".into(), )); } let version = seq[0] .as_u64() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; - if version != 3 { - return Err(SignedObjectDecodeError::InvalidSignerInfoVersion(version)); - } + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; let sid = &seq[1]; - if sid.class() != Class::ContextSpecific || sid.tag() != Tag(0) { - return Err(SignedObjectDecodeError::InvalidSignerIdentifier); - } - let sid_ski = sid - .as_slice() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))? - .to_vec(); + let sid = if sid.class() == Class::ContextSpecific && sid.tag() == Tag(0) { + let ski = sid + .as_slice() + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))? + .to_vec(); + SignerIdentifierParsed::SubjectKeyIdentifier(ski) + } else { + SignerIdentifierParsed::Other + }; - let (digest_algorithm, _params_ok) = parse_algorithm_identifier(&seq[2])?; - if digest_algorithm != OID_SHA256 { - return Err(SignedObjectDecodeError::InvalidSignerInfoDigestAlgorithm( - digest_algorithm, - )); - } + let (digest_oid, digest_params_ok) = parse_algorithm_identifier_parse(&seq[2])?; + let digest_algorithm = AlgorithmIdentifierParsed { + oid: digest_oid, + params_ok: digest_params_ok, + }; let mut idx = 3; - let mut signed_attrs: Option = None; + let mut signed_attrs_content: Option> = None; let mut signed_attrs_der_for_signature: Option> = None; if seq[idx].class() == Class::ContextSpecific && seq[idx].tag() == Tag(0) { let signed_attrs_obj = &seq[idx]; - let signed_attrs_content = signed_attrs_obj + let content = signed_attrs_obj .as_slice() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; - signed_attrs = Some(parse_signed_attrs_implicit(signed_attrs_content)?); - signed_attrs_der_for_signature = Some(make_signed_attrs_der_for_signature(signed_attrs_obj)?); + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))? + .to_vec(); + signed_attrs_content = Some(content); + signed_attrs_der_for_signature = + Some(make_signed_attrs_der_for_signature_parse(signed_attrs_obj)?); idx += 1; } - let signed_attrs = signed_attrs.ok_or(SignedObjectDecodeError::SignedAttrsMissing)?; - let signed_attrs_der_for_signature = - signed_attrs_der_for_signature.ok_or(SignedObjectDecodeError::SignedAttrsMissing)?; - - let (signature_algorithm, params_ok) = parse_algorithm_identifier(&seq[idx])?; - if !params_ok { - return Err(SignedObjectDecodeError::InvalidSignatureAlgorithmParameters); - } - if signature_algorithm != OID_RSA_ENCRYPTION - && signature_algorithm != OID_SHA256_WITH_RSA_ENCRYPTION - { - return Err(SignedObjectDecodeError::InvalidSignatureAlgorithm( - signature_algorithm, - )); - } + let (signature_oid, signature_params_ok) = parse_algorithm_identifier_parse(&seq[idx])?; + let signature_algorithm = AlgorithmIdentifierParsed { + oid: signature_oid, + params_ok: signature_params_ok, + }; idx += 1; let signature = seq[idx] .as_slice() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))? + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))? .to_vec(); idx += 1; let unsigned_attrs_present = seq.get(idx).is_some(); - if unsigned_attrs_present { - return Err(SignedObjectDecodeError::UnsignedAttrsPresent); - } - Ok(SignerInfoProfiled { - version: 3, - sid_ski, - digest_algorithm: OID_SHA256.to_string(), + Ok(SignerInfoParsed { + version, + sid, + digest_algorithm, signature_algorithm, - signed_attrs, + signed_attrs_content, unsigned_attrs_present, signature, signed_attrs_der_for_signature, }) } -fn parse_signed_attrs_implicit(input: &[u8]) -> Result { +fn validate_signed_data_profile( + signed_data: SignedDataParsed, +) -> Result { + if signed_data.version != 3 { + return Err(SignedObjectValidateError::InvalidSignedDataVersion( + signed_data.version, + )); + } + + if signed_data.digest_algorithms.len() != 1 { + return Err(SignedObjectValidateError::InvalidDigestAlgorithmsCount( + signed_data.digest_algorithms.len(), + )); + } + let digest_alg = &signed_data.digest_algorithms[0]; + if digest_alg.oid != OID_SHA256 { + return Err(SignedObjectValidateError::InvalidDigestAlgorithm( + digest_alg.oid.clone(), + )); + } + + if signed_data.crls_present { + return Err(SignedObjectValidateError::CrlsPresent); + } + + let econtent = signed_data + .encap_content_info + .econtent + .clone() + .ok_or(SignedObjectValidateError::EContentMissing)?; + if econtent.is_empty() { + return Err(SignedObjectValidateError::EContentMissing); + } + let encap_content_info = EncapsulatedContentInfo { + econtent_type: signed_data.encap_content_info.econtent_type.clone(), + econtent: econtent.clone(), + }; + + let certs = signed_data + .certificates + .as_ref() + .ok_or(SignedObjectValidateError::CertificatesMissing)?; + if certs.len() != 1 { + return Err(SignedObjectValidateError::InvalidCertificatesCount( + certs.len(), + )); + } + let ee = validate_ee_certificate(&certs[0])?; + + if signed_data.signer_infos.len() != 1 { + return Err(SignedObjectValidateError::InvalidSignerInfosCount( + signed_data.signer_infos.len(), + )); + } + let signer = &signed_data.signer_infos[0]; + + if signer.version != 3 { + return Err(SignedObjectValidateError::InvalidSignerInfoVersion( + signer.version, + )); + } + let sid_ski = match &signer.sid { + SignerIdentifierParsed::SubjectKeyIdentifier(ski) => ski.clone(), + SignerIdentifierParsed::Other => { + return Err(SignedObjectValidateError::InvalidSignerIdentifier); + } + }; + + if signer.digest_algorithm.oid != OID_SHA256 { + return Err(SignedObjectValidateError::InvalidSignerInfoDigestAlgorithm( + signer.digest_algorithm.oid.clone(), + )); + } + + let signed_attrs_content = signer + .signed_attrs_content + .as_deref() + .ok_or(SignedObjectValidateError::SignedAttrsMissing)?; + let signed_attrs_der_for_signature = signer + .signed_attrs_der_for_signature + .clone() + .ok_or(SignedObjectValidateError::SignedAttrsMissing)?; + let signed_attrs = parse_signed_attrs_implicit(signed_attrs_content)?; + + if signer.unsigned_attrs_present { + return Err(SignedObjectValidateError::UnsignedAttrsPresent); + } + + if !signer.signature_algorithm.params_ok { + return Err(SignedObjectValidateError::InvalidSignatureAlgorithmParameters); + } + let signature_algorithm = signer.signature_algorithm.oid.clone(); + if signature_algorithm != OID_RSA_ENCRYPTION + && signature_algorithm != OID_SHA256_WITH_RSA_ENCRYPTION + { + return Err(SignedObjectValidateError::InvalidSignatureAlgorithm( + signature_algorithm, + )); + } + + if sid_ski != ee.subject_key_identifier { + return Err(SignedObjectValidateError::SidSkiMismatch); + } + if signed_attrs.content_type != encap_content_info.econtent_type { + return Err(SignedObjectValidateError::ContentTypeAttrMismatch { + econtent_type: encap_content_info.econtent_type.clone(), + attr_content_type: signed_attrs.content_type.clone(), + }); + } + + let computed = Sha256::digest(&encap_content_info.econtent).to_vec(); + if computed != signed_attrs.message_digest { + return Err(SignedObjectValidateError::MessageDigestMismatch); + } + + Ok(SignedDataProfiled { + version: 3, + digest_algorithms: vec![OID_SHA256.to_string()], + encap_content_info, + certificates: vec![ee.clone()], + crls_present: false, + signer_infos: vec![SignerInfoProfiled { + version: 3, + sid_ski, + digest_algorithm: OID_SHA256.to_string(), + signature_algorithm: signer.signature_algorithm.oid.clone(), + signed_attrs, + unsigned_attrs_present: false, + signature: signer.signature.clone(), + signed_attrs_der_for_signature, + }], + }) +} + +fn parse_signed_attrs_implicit( + input: &[u8], +) -> Result { let mut content_type: Option = None; let mut message_digest: Option> = None; let mut signing_time: Option = None; let mut remaining = input; while !remaining.is_empty() { - let (rem, attr_obj) = - parse_der(remaining).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; + let (rem, attr_obj) = parse_der(remaining) + .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; remaining = rem; let attr_seq = attr_obj .as_sequence() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; + .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; if attr_seq.len() != 2 { - return Err(SignedObjectDecodeError::Parse( + return Err(SignedObjectValidateError::SignedAttrsParse( "Attribute must be SEQUENCE of 2".into(), )); } - let oid = oid_to_string(&attr_seq[0])?; + let oid = oid_to_string_parse(&attr_seq[0]) + .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; let values_set = attr_seq[1] .as_set() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; + .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; if values_set.len() != 1 { - return Err(SignedObjectDecodeError::InvalidSignedAttributeValuesCount { - oid, - count: values_set.len(), - }); + return Err( + SignedObjectValidateError::InvalidSignedAttributeValuesCount { + oid, + count: values_set.len(), + }, + ); } match oid.as_str() { OID_CMS_ATTR_CONTENT_TYPE => { if content_type.is_some() { - return Err(SignedObjectDecodeError::DuplicateSignedAttribute(oid)); + return Err(SignedObjectValidateError::DuplicateSignedAttribute(oid)); } - let v = oid_to_string(&values_set[0])?; + let v = oid_to_string_parse(&values_set[0]) + .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))?; content_type = Some(v); } OID_CMS_ATTR_MESSAGE_DIGEST => { if message_digest.is_some() { - return Err(SignedObjectDecodeError::DuplicateSignedAttribute(oid)); + return Err(SignedObjectValidateError::DuplicateSignedAttribute(oid)); } let v = values_set[0] .as_slice() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))? + .map_err(|e| SignedObjectValidateError::SignedAttrsParse(e.to_string()))? .to_vec(); message_digest = Some(v); } OID_CMS_ATTR_SIGNING_TIME => { if signing_time.is_some() { - return Err(SignedObjectDecodeError::DuplicateSignedAttribute(oid)); + return Err(SignedObjectValidateError::DuplicateSignedAttribute(oid)); } signing_time = Some(parse_signing_time_value(&values_set[0])?); } _ => { - return Err(SignedObjectDecodeError::UnsupportedSignedAttribute(oid)); + return Err(SignedObjectValidateError::UnsupportedSignedAttribute(oid)); } } } Ok(SignedAttrsProfiled { - content_type: content_type.ok_or_else(|| { - SignedObjectDecodeError::Parse("missing signedAttrs content-type".into()) - })?, - message_digest: message_digest.ok_or_else(|| { - SignedObjectDecodeError::Parse("missing signedAttrs message-digest".into()) - })?, - signing_time: signing_time.ok_or_else(|| { - SignedObjectDecodeError::Parse("missing signedAttrs signing-time".into()) - })?, + content_type: content_type + .ok_or(SignedObjectValidateError::SignedAttrsContentTypeMissing)?, + message_digest: message_digest + .ok_or(SignedObjectValidateError::SignedAttrsMessageDigestMissing)?, + signing_time: signing_time + .ok_or(SignedObjectValidateError::SignedAttrsSigningTimeMissing)?, other_attrs_present: false, }) } -fn parse_signing_time_value(obj: &DerObject<'_>) -> Result { +fn parse_signing_time_value(obj: &DerObject<'_>) -> Result { match &obj.content { der_parser::ber::BerObjectContent::UTCTime(dt) => Ok(Asn1TimeUtc { - utc: dt.to_datetime().map_err(|_| SignedObjectDecodeError::InvalidSigningTimeValue)?, + utc: dt + .to_datetime() + .map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?, encoding: Asn1TimeEncoding::UtcTime, }), der_parser::ber::BerObjectContent::GeneralizedTime(dt) => Ok(Asn1TimeUtc { - utc: dt.to_datetime().map_err(|_| SignedObjectDecodeError::InvalidSigningTimeValue)?, + utc: dt + .to_datetime() + .map_err(|_| SignedObjectValidateError::InvalidSigningTimeValue)?, encoding: Asn1TimeEncoding::GeneralizedTime, }), - _ => Err(SignedObjectDecodeError::InvalidSigningTimeValue), + _ => Err(SignedObjectValidateError::InvalidSigningTimeValue), } } -fn make_signed_attrs_der_for_signature(obj: &DerObject<'_>) -> Result, SignedObjectDecodeError> { +fn make_signed_attrs_der_for_signature_parse( + obj: &DerObject<'_>, +) -> Result, SignedObjectParseError> { // We need the DER encoding of SignedAttributes (SET OF Attribute) as signature input. // The SignedAttributes field in SignerInfo is `[0] IMPLICIT`, so the on-wire bytes start with // a context-specific constructed tag (0xA0 for tag 0). For signature verification, this tag @@ -699,9 +941,9 @@ fn make_signed_attrs_der_for_signature(obj: &DerObject<'_>) -> Result, S // let mut cs_der = obj .to_vec() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; if cs_der.is_empty() { - return Err(SignedObjectDecodeError::Parse( + return Err(SignedObjectParseError::Parse( "signedAttrs encoding is empty".into(), )); } @@ -711,25 +953,25 @@ fn make_signed_attrs_der_for_signature(obj: &DerObject<'_>) -> Result, S Ok(cs_der) } -fn oid_to_string(obj: &DerObject<'_>) -> Result { +fn oid_to_string_parse(obj: &DerObject<'_>) -> Result { let oid = obj .as_oid() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; Ok(oid.to_id_string()) } -fn parse_algorithm_identifier( +fn parse_algorithm_identifier_parse( obj: &DerObject<'_>, -) -> Result<(String, bool), SignedObjectDecodeError> { +) -> Result<(String, bool), SignedObjectParseError> { let seq = obj .as_sequence() - .map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?; + .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; if seq.is_empty() || seq.len() > 2 { - return Err(SignedObjectDecodeError::Parse( + return Err(SignedObjectParseError::Parse( "AlgorithmIdentifier must be SEQUENCE of 1..2".into(), )); } - let oid = oid_to_string(&seq[0])?; + let oid = oid_to_string_parse(&seq[0])?; let params_ok = match seq.get(1) { None => true, Some(p) => matches!(p.content, der_parser::ber::BerObjectContent::Null), diff --git a/src/data_model/ta.rs b/src/data_model/ta.rs index b262a09..e4a2d36 100644 --- a/src/data_model/ta.rs +++ b/src/data_model/ta.rs @@ -2,7 +2,10 @@ use url::Url; use x509_parser::prelude::{FromDer, X509Certificate}; use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER; -use crate::data_model::rc::{AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate}; +use crate::data_model::rc::{ + AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate, + ResourceCertificateParseError, ResourceCertificateParsed, ResourceCertificateProfileError, +}; use crate::data_model::tal::Tal; #[derive(Clone, Debug, PartialEq, Eq)] @@ -12,103 +15,160 @@ pub struct TaCertificate { } #[derive(Debug, thiserror::Error)] -pub enum TaCertificateError { +pub enum TaCertificateParseError { #[error("TA certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4; RFC 8630 §2.3)")] - Parse(String), + ResourceCertificate(#[from] ResourceCertificateParseError), +} - #[error("trailing bytes after TA certificate DER: {0} bytes (DER; RFC 5280 §4.1; RFC 6487 §4)")] - TrailingBytes(usize), +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TaCertificateParsed { + pub rc_parsed: ResourceCertificateParsed, +} + +#[derive(Debug, thiserror::Error)] +pub enum TaCertificateProfileError { + #[error("resource certificate profile error: {0} (RFC 5280 §4; RFC 6487 §4)")] + ResourceCertificate(#[from] ResourceCertificateProfileError), #[error("TA certificate must be a CA certificate (RFC 8630 §2.3; RFC 6487 §4.8.1)")] NotCa, - #[error("TA certificate must be self-signed (issuer DN must equal subject DN) (RFC 8630 §2.3; RFC 5280 §4.1.2.4)")] + #[error( + "TA certificate must be self-signed (issuer DN must equal subject DN) (RFC 8630 §2.3; RFC 5280 §4.1.2.4)" + )] NotSelfSignedIssuerSubject, - #[error("TA certificate self-signature verification failed: {0} (RFC 8630 §2.3; RFC 5280 §6.1)")] - InvalidSelfSignature(String), - - #[error("TA certificate must contain certificatePolicies ipAddr-asNumber ({OID_CP_IPADDR_ASNUMBER}) (RFC 6487 §4.8.9; RFC 8630 §2.3)")] + #[error( + "TA certificate must contain certificatePolicies ipAddr-asNumber ({OID_CP_IPADDR_ASNUMBER}) (RFC 6487 §4.8.9; RFC 8630 §2.3)" + )] MissingOrInvalidCertificatePolicies, #[error("TA certificate must contain SubjectKeyIdentifier (RFC 6487 §4.8.2; RFC 8630 §2.3)")] MissingSubjectKeyIdentifier, - #[error("TA certificate must contain at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 8630 §2.3)")] + #[error( + "TA certificate must contain at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 8630 §2.3)" + )] ResourcesMissing, #[error("TA certificate resources must be non-empty (RFC 8630 §2.3)")] ResourcesEmpty, - #[error("TA certificate MUST NOT use inherit in IP resources (RFC 8630 §2.3; RFC 3779 §2.2.3.5)")] + #[error( + "TA certificate MUST NOT use inherit in IP resources (RFC 8630 §2.3; RFC 3779 §2.2.3.5)" + )] IpResourcesInherit, - #[error("TA certificate MUST NOT use inherit in AS resources (RFC 8630 §2.3; RFC 3779 §3.2.3.3)")] + #[error( + "TA certificate MUST NOT use inherit in AS resources (RFC 8630 §2.3; RFC 3779 §3.2.3.3)" + )] AsResourcesInherit, } +#[derive(Debug, thiserror::Error)] +pub enum TaCertificateDecodeError { + #[error("{0}")] + Parse(#[from] TaCertificateParseError), + + #[error("{0}")] + Validate(#[from] TaCertificateProfileError), +} + +/// Backwards-compatible name: TA certificate errors from parse+validate. +pub type TaCertificateError = TaCertificateDecodeError; + +#[derive(Debug, thiserror::Error)] +pub enum TaCertificateVerifyError { + #[error("TA certificate parse error: {0} (RFC 5280 §4.1; RFC 8630 §2.3)")] + Parse(String), + + #[error("trailing bytes after TA certificate DER: {0} bytes (DER; RFC 5280 §4.1)")] + TrailingBytes(usize), + + #[error( + "TA certificate self-signature verification failed: {0} (RFC 8630 §2.3; RFC 5280 §6.1)" + )] + InvalidSelfSignature(String), +} + impl TaCertificate { - pub fn from_der(der: &[u8]) -> Result { - let rc_ca = ResourceCertificate::from_der(der).map_err(|e| TaCertificateError::Parse(e.to_string()))?; - if rc_ca.kind != ResourceCertKind::Ca { - return Err(TaCertificateError::NotCa); - } - - // Strong self-signed check: issuer==subject AND signature verifies with its own SPKI. - let (rem, cert) = - X509Certificate::from_der(der).map_err(|e| TaCertificateError::Parse(e.to_string()))?; - if !rem.is_empty() { - return Err(TaCertificateError::TrailingBytes(rem.len())); - } - if cert.issuer().to_string() != cert.subject().to_string() { - return Err(TaCertificateError::NotSelfSignedIssuerSubject); - } - cert.verify_signature(None) - .map_err(|e| TaCertificateError::InvalidSelfSignature(e.to_string()))?; - - Self::validate_rc_constraints(&rc_ca)?; - - Ok(Self { - raw_der: der.to_vec(), - rc_ca, + /// Parse step of scheme A (`parse → validate → verify`). + pub fn parse_der(der: &[u8]) -> Result { + Ok(TaCertificateParsed { + rc_parsed: ResourceCertificate::parse_der(der)?, }) } + /// Profile validate step of scheme A (`parse → validate → verify`). + /// + /// `TaCertificate` is already profile-validated when constructed via `decode_der()` / + /// `TaCertificateParsed::validate_profile()`. + pub fn validate_profile(&self) -> Result<(), TaCertificateProfileError> { + Ok(()) + } + + /// Decode a TA certificate (`parse + validate`). + pub fn decode_der(der: &[u8]) -> Result { + Ok(Self::parse_der(der)?.validate_profile()?) + } + + /// Backwards-compatible helper (historical name). + pub fn from_der(der: &[u8]) -> Result { + Self::decode_der(der) + } + pub fn spki_der(&self) -> &[u8] { &self.rc_ca.tbs.subject_public_key_info } + /// Verify step of scheme A (`parse → validate → verify`). + pub fn verify_self_signature(&self) -> Result<(), TaCertificateVerifyError> { + let (rem, cert) = X509Certificate::from_der(&self.raw_der) + .map_err(|e| TaCertificateVerifyError::Parse(e.to_string()))?; + if !rem.is_empty() { + return Err(TaCertificateVerifyError::TrailingBytes(rem.len())); + } + cert.verify_signature(None) + .map_err(|e| TaCertificateVerifyError::InvalidSelfSignature(e.to_string()))?; + Ok(()) + } + /// Validate TA-specific semantic constraints on a parsed Resource Certificate. /// /// Note: this does not verify the X.509 signature; it is intended for higher-level logic and /// for unit tests that exercise individual constraint branches. - pub fn validate_rc_constraints(rc_ca: &ResourceCertificate) -> Result<(), TaCertificateError> { + pub fn validate_rc_constraints( + rc_ca: &ResourceCertificate, + ) -> Result<(), TaCertificateProfileError> { if rc_ca.kind != ResourceCertKind::Ca { - return Err(TaCertificateError::NotCa); + return Err(TaCertificateProfileError::NotCa); } - if rc_ca.tbs.extensions.certificate_policies_oid.as_deref() != Some(OID_CP_IPADDR_ASNUMBER) { - return Err(TaCertificateError::MissingOrInvalidCertificatePolicies); + if rc_ca.tbs.extensions.certificate_policies_oid.as_deref() != Some(OID_CP_IPADDR_ASNUMBER) + { + return Err(TaCertificateProfileError::MissingOrInvalidCertificatePolicies); } if rc_ca.tbs.extensions.subject_key_identifier.is_none() { - return Err(TaCertificateError::MissingSubjectKeyIdentifier); + return Err(TaCertificateProfileError::MissingSubjectKeyIdentifier); } let ip = rc_ca.tbs.extensions.ip_resources.as_ref(); let asn = rc_ca.tbs.extensions.as_resources.as_ref(); if ip.is_none() && asn.is_none() { - return Err(TaCertificateError::ResourcesMissing); + return Err(TaCertificateProfileError::ResourcesMissing); } let mut has_any_resource = false; if let Some(ip) = ip { if ip.has_any_inherit() { - return Err(TaCertificateError::IpResourcesInherit); + return Err(TaCertificateProfileError::IpResourcesInherit); } for fam in &ip.families { match &fam.choice { - IpAddressChoice::Inherit => return Err(TaCertificateError::IpResourcesInherit), + IpAddressChoice::Inherit => { + return Err(TaCertificateProfileError::IpResourcesInherit); + } IpAddressChoice::AddressesOrRanges(items) => { if !items.is_empty() { has_any_resource = true; @@ -122,7 +182,7 @@ impl TaCertificate { if matches!(asn.asnum, Some(AsIdentifierChoice::Inherit)) || matches!(asn.rdi, Some(AsIdentifierChoice::Inherit)) { - return Err(TaCertificateError::AsResourcesInherit); + return Err(TaCertificateProfileError::AsResourcesInherit); } if let Some(AsIdentifierChoice::AsIdsOrRanges(items)) = asn.asnum.as_ref() { if !items.is_empty() { @@ -137,13 +197,33 @@ impl TaCertificate { } if !has_any_resource { - return Err(TaCertificateError::ResourcesEmpty); + return Err(TaCertificateProfileError::ResourcesEmpty); } Ok(()) } } +impl TaCertificateParsed { + pub fn validate_profile(self) -> Result { + let rc_ca = self.rc_parsed.validate_profile()?; + if rc_ca.kind != ResourceCertKind::Ca { + return Err(TaCertificateProfileError::NotCa); + } + + if rc_ca.tbs.issuer_dn != rc_ca.tbs.subject_dn { + return Err(TaCertificateProfileError::NotSelfSignedIssuerSubject); + } + + TaCertificate::validate_rc_constraints(&rc_ca)?; + + Ok(TaCertificate { + raw_der: rc_ca.raw_der.clone(), + rc_ca, + }) + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct TrustAnchor { pub tal: Tal, @@ -154,12 +234,20 @@ pub struct TrustAnchor { #[derive(Debug, thiserror::Error)] pub enum TrustAnchorError { #[error("TA certificate error: {0} (RFC 8630 §2.3)")] - TaCertificate(#[from] TaCertificateError), + TaCertificate(#[from] TaCertificateDecodeError), + #[error("{0}")] + Bind(#[from] TrustAnchorBindError), +} + +#[derive(Debug, thiserror::Error)] +pub enum TrustAnchorBindError { #[error("resolved TA URI not listed in TAL: {0} (RFC 8630 §2.2-§2.3)")] ResolvedUriNotInTal(String), - #[error("TAL SPKI does not match TA certificate SubjectPublicKeyInfo (RFC 8630 §2.3; RFC 5280 §4.1.2.7)")] + #[error( + "TAL SPKI does not match TA certificate SubjectPublicKeyInfo (RFC 8630 §2.3; RFC 5280 §4.1.2.7)" + )] TalSpkiMismatch, } @@ -167,16 +255,28 @@ impl TrustAnchor { /// Bind a TAL and a downloaded TA certificate. /// /// This does not download anything; it only validates the binding rules from RFC 8630 §2.3. - pub fn bind(tal: Tal, ta_der: &[u8], resolved_uri: Option<&Url>) -> Result { + pub fn bind_der( + tal: Tal, + ta_der: &[u8], + resolved_uri: Option<&Url>, + ) -> Result { + let ta_certificate = TaCertificate::decode_der(ta_der)?; + Ok(Self::bind(tal, ta_certificate, resolved_uri)?) + } + + pub fn bind( + tal: Tal, + ta_certificate: TaCertificate, + resolved_uri: Option<&Url>, + ) -> Result { if let Some(u) = resolved_uri { if !tal.ta_uris.iter().any(|x| x == u) { - return Err(TrustAnchorError::ResolvedUriNotInTal(u.to_string())); + return Err(TrustAnchorBindError::ResolvedUriNotInTal(u.to_string())); } } - let ta_certificate = TaCertificate::from_der(ta_der)?; if tal.subject_public_key_info_der != ta_certificate.spki_der() { - return Err(TrustAnchorError::TalSpkiMismatch); + return Err(TrustAnchorBindError::TalSpkiMismatch); } Ok(Self { diff --git a/src/data_model/tal.rs b/src/data_model/tal.rs index 1c0256b..53683a9 100644 --- a/src/data_model/tal.rs +++ b/src/data_model/tal.rs @@ -1,6 +1,13 @@ use base64::Engine; use url::Url; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TalParsed { + pub raw: Vec, + /// Lines split by '\n' and normalized by stripping a trailing '\r' per line. + pub lines: Vec, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct Tal { pub raw: Vec, @@ -10,17 +17,22 @@ pub struct Tal { } #[derive(Debug, thiserror::Error)] -pub enum TalDecodeError { +pub enum TalParseError { #[error("TAL must be valid UTF-8 (RFC 8630 §2.2)")] InvalidUtf8, +} +#[derive(Debug, thiserror::Error)] +pub enum TalProfileError { #[error("TAL comments must appear only at the beginning (RFC 8630 §2.2)")] CommentAfterHeader, #[error("TAL must contain at least one TA URI line (RFC 8630 §2.2)")] MissingTaUris, - #[error("TAL must contain an empty line separator between URI list and SPKI base64 (RFC 8630 §2.2)")] + #[error( + "TAL must contain an empty line separator between URI list and SPKI base64 (RFC 8630 §2.2)" + )] MissingSeparatorEmptyLine, #[error("TAL TA URI invalid: {0} (RFC 8630 §2.2)")] @@ -29,10 +41,14 @@ pub enum TalDecodeError { #[error("TAL TA URI scheme must be rsync or https, got {0} (RFC 8630 §2.2)")] UnsupportedUriScheme(String), - #[error("TAL TA URI must reference a single object (must not end with '/'): {0} (RFC 8630 §2.3)")] + #[error( + "TAL TA URI must reference a single object (must not end with '/'): {0} (RFC 8630 §2.3)" + )] UriIsDirectory(String), - #[error("TAL must contain base64-encoded SubjectPublicKeyInfo after the separator (RFC 8630 §2.2)")] + #[error( + "TAL must contain base64-encoded SubjectPublicKeyInfo after the separator (RFC 8630 §2.2)" + )] MissingSpki, #[error("TAL SPKI base64 decode failed (RFC 8630 §2.2)")] @@ -42,90 +58,123 @@ pub enum TalDecodeError { SpkiDerEmpty, } -impl Tal { - pub fn decode_bytes(input: &[u8]) -> Result { - let raw = input.to_vec(); - let text = std::str::from_utf8(input).map_err(|_| TalDecodeError::InvalidUtf8)?; +#[derive(Debug, thiserror::Error)] +pub enum TalDecodeError { + #[error("{0}")] + Parse(#[from] TalParseError), - let lines: Vec<&str> = text + #[error("{0}")] + Validate(#[from] TalProfileError), +} + +impl Tal { + /// Parse step of scheme A (`parse → validate → verify`). + pub fn parse_bytes(input: &[u8]) -> Result { + let raw = input.to_vec(); + let text = std::str::from_utf8(input).map_err(|_| TalParseError::InvalidUtf8)?; + + let lines: Vec = text .split('\n') - .map(|l| l.strip_suffix('\r').unwrap_or(l)) + .map(|l| l.strip_suffix('\r').unwrap_or(l).to_string()) .collect(); + Ok(TalParsed { raw, lines }) + } + + /// Validate step of scheme A (`parse → validate → verify`). + /// + /// `Tal` is already profile-validated when constructed via `decode_bytes()` / + /// `TalParsed::validate_profile()`. + pub fn validate_profile(&self) -> Result<(), TalProfileError> { + Ok(()) + } + + pub fn decode_bytes(input: &[u8]) -> Result { + Ok(Self::parse_bytes(input)?.validate_profile()?) + } +} + +impl TalParsed { + pub fn validate_profile(self) -> Result { let mut idx = 0usize; // 1) Leading comments. let mut comments: Vec = Vec::new(); - while idx < lines.len() && lines[idx].starts_with('#') { - comments.push(lines[idx][1..].to_string()); + while idx < self.lines.len() && self.lines[idx].starts_with('#') { + comments.push(self.lines[idx][1..].to_string()); idx += 1; } // 2) URI list (one or more non-empty lines). let mut ta_uris: Vec = Vec::new(); - while idx < lines.len() { - let line = lines[idx].trim(); + while idx < self.lines.len() { + let line = self.lines[idx].trim(); if line.is_empty() { break; } if line.starts_with('#') { - return Err(TalDecodeError::CommentAfterHeader); + return Err(TalProfileError::CommentAfterHeader); } let url = match Url::parse(line) { Ok(u) => u, Err(_) => { if !ta_uris.is_empty() { - return Err(TalDecodeError::MissingSeparatorEmptyLine); + return Err(TalProfileError::MissingSeparatorEmptyLine); } - return Err(TalDecodeError::InvalidUri(line.to_string())); + return Err(TalProfileError::InvalidUri(line.to_string())); } }; match url.scheme() { "rsync" | "https" => {} - s => return Err(TalDecodeError::UnsupportedUriScheme(s.to_string())), + s => return Err(TalProfileError::UnsupportedUriScheme(s.to_string())), } if url.path().ends_with('/') { - return Err(TalDecodeError::UriIsDirectory(line.to_string())); + return Err(TalProfileError::UriIsDirectory(line.to_string())); } - if url.path_segments().and_then(|mut s| s.next_back()).unwrap_or("").is_empty() { - return Err(TalDecodeError::UriIsDirectory(line.to_string())); + if url + .path_segments() + .and_then(|mut s| s.next_back()) + .unwrap_or("") + .is_empty() + { + return Err(TalProfileError::UriIsDirectory(line.to_string())); } ta_uris.push(url); idx += 1; } if ta_uris.is_empty() { - return Err(TalDecodeError::MissingTaUris); + return Err(TalProfileError::MissingTaUris); } // 3) Empty line separator (must exist). - if idx >= lines.len() || !lines[idx].trim().is_empty() { - return Err(TalDecodeError::MissingSeparatorEmptyLine); + if idx >= self.lines.len() || !self.lines[idx].trim().is_empty() { + return Err(TalProfileError::MissingSeparatorEmptyLine); } idx += 1; // 4) Base64(SPKI DER) remainder; allow line wrapping. let mut b64 = String::new(); - while idx < lines.len() { - let line = lines[idx].trim(); + while idx < self.lines.len() { + let line = self.lines[idx].trim(); if !line.is_empty() { b64.push_str(line); } idx += 1; } if b64.is_empty() { - return Err(TalDecodeError::MissingSpki); + return Err(TalProfileError::MissingSpki); } let spki_der = base64::engine::general_purpose::STANDARD .decode(b64.as_bytes()) - .map_err(|_| TalDecodeError::SpkiBase64Decode)?; + .map_err(|_| TalProfileError::SpkiBase64Decode)?; if spki_der.is_empty() { - return Err(TalDecodeError::SpkiDerEmpty); + return Err(TalProfileError::SpkiDerEmpty); } - Ok(Self { - raw, + Ok(Tal { + raw: self.raw, comments, ta_uris, subject_public_key_info_der: spki_der, diff --git a/tests/test_aspa_decode.rs b/tests/test_aspa_decode.rs index e193678..36b4aca 100644 --- a/tests/test_aspa_decode.rs +++ b/tests/test_aspa_decode.rs @@ -1,4 +1,5 @@ -use rpki::data_model::aspa::{AspaDecodeError, AspaObject}; +use rpki::data_model::aspa::{AspaDecodeError, AspaObject, AspaProfileError}; +use rpki::data_model::signed_object::RpkiSignedObject; #[test] fn decode_aspa_fixture_smoke() { @@ -7,6 +8,7 @@ fn decode_aspa_fixture_smoke() { ) .expect("read ASPA fixture"); let aspa = AspaObject::decode_der(&der).expect("decode aspa"); + aspa.validate_profile().expect("validate ASPA profile"); assert_eq!(aspa.econtent_type, rpki::data_model::oid::OID_CT_ASPA); assert_eq!(aspa.aspa.version, 1); assert_ne!(aspa.aspa.customer_as_id, 0); @@ -15,11 +17,23 @@ fn decode_aspa_fixture_smoke() { } #[test] -fn decode_rejects_non_aspa_econtent_type() { +fn from_signed_object_accepts_aspa_fixture() { let der = std::fs::read( - "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa", + "tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa", ) - .expect("read ROA fixture"); - let err = AspaObject::decode_der(&der).unwrap_err(); - assert!(matches!(err, AspaDecodeError::InvalidEContentType(_))); + .expect("read ASPA fixture"); + let so = RpkiSignedObject::decode_der(&der).expect("decode signed object"); + let aspa = AspaObject::from_signed_object(so).expect("from_signed_object"); + aspa.validate_profile().expect("validate ASPA profile"); +} + +#[test] +fn decode_rejects_non_aspa_econtent_type() { + let der = std::fs::read("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa") + .expect("read ROA fixture"); + let err = AspaObject::decode_der(&der).unwrap_err(); + assert!(matches!( + err, + AspaDecodeError::Validate(AspaProfileError::InvalidEContentType(_)) + )); } diff --git a/tests/test_aspa_econtent_decode_errors.rs b/tests/test_aspa_econtent_decode_errors.rs index 801888f..09cd447 100644 --- a/tests/test_aspa_econtent_decode_errors.rs +++ b/tests/test_aspa_econtent_decode_errors.rs @@ -1,4 +1,4 @@ -use rpki::data_model::aspa::{AspaDecodeError, AspaEContent}; +use rpki::data_model::aspa::{AspaDecodeError, AspaEContent, AspaParseError, AspaProfileError}; fn len_bytes(len: usize) -> Vec { if len < 128 { @@ -68,22 +68,87 @@ fn aspa_attestation_missing_version(customer: u64, providers: Vec) -> Vec, as_resources: Option) -> ResourceCertificate { +fn dummy_ee( + ip_resources: Option, + as_resources: Option, +) -> ResourceCertificate { ResourceCertificate { raw_der: vec![], tbs: RpkixTbsCertificate { @@ -50,7 +53,9 @@ fn validate_accepts_when_customer_matches_ee_asid() { let ee = dummy_ee( None, Some(AsResourceSet { - asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])), + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id( + 64496, + )])), rdi: None, }), ); @@ -82,10 +87,12 @@ fn validate_rejects_as_resources_inherit_or_ranges() { let ee = dummy_ee( None, Some(AsResourceSet { - asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { - min: 64496, - max: 64497, - }])), + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![ + AsIdOrRange::Range { + min: 64496, + max: 64497, + }, + ])), rdi: None, }), ); @@ -99,12 +106,17 @@ fn validate_rejects_customer_mismatch() { let ee = dummy_ee( None, Some(AsResourceSet { - asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64511)])), + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id( + 64511, + )])), rdi: None, }), ); let err = aspa.validate_against_ee_cert(&ee).unwrap_err(); - assert!(matches!(err, AspaValidateError::CustomerAsIdMismatch { .. })); + assert!(matches!( + err, + AspaValidateError::CustomerAsIdMismatch { .. } + )); } #[test] @@ -113,7 +125,9 @@ fn validate_rejects_ip_resources_present() { let ee = dummy_ee( Some(IpResourceSet { families: vec![] }), Some(AsResourceSet { - asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])), + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id( + 64496, + )])), rdi: None, }), ); @@ -127,8 +141,12 @@ fn validate_rejects_rdi_present_or_not_single_id() { let ee = dummy_ee( None, Some(AsResourceSet { - asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])), - rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])), + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id( + 64496, + )])), + rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id( + 64496, + )])), }), ); let err = aspa.validate_against_ee_cert(&ee).unwrap_err(); @@ -147,4 +165,3 @@ fn validate_rejects_rdi_present_or_not_single_id() { let err = aspa.validate_against_ee_cert(&ee).unwrap_err(); assert!(matches!(err, AspaValidateError::EeAsResourcesNotSingleId)); } - diff --git a/tests/test_aspa_verify.rs b/tests/test_aspa_verify.rs index 372d8c6..a11cd6b 100644 --- a/tests/test_aspa_verify.rs +++ b/tests/test_aspa_verify.rs @@ -11,4 +11,3 @@ fn verify_aspa_cms_signature_with_embedded_ee_cert() { .verify_signature() .expect("ASPA CMS signature should verify with embedded EE cert"); } - diff --git a/tests/test_common.rs b/tests/test_common.rs index 410f48c..56b63cc 100644 --- a/tests/test_common.rs +++ b/tests/test_common.rs @@ -1,9 +1,9 @@ use rpki::data_model::common::{ - algorithm_params_absent_or_null, Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned, + Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned, algorithm_params_absent_or_null, }; use x509_parser::prelude::FromDer; -use x509_parser::x509::AlgorithmIdentifier; use x509_parser::time::ASN1Time; +use x509_parser::x509::AlgorithmIdentifier; #[test] fn big_unsigned_helpers() { @@ -32,8 +32,11 @@ fn time_encoding_validation() { t.validate_encoding_rfc5280("t").expect("utc ok"); let t = Asn1TimeUtc { - utc: time::OffsetDateTime::parse("2050-01-01T00:00:00Z", &time::format_description::well_known::Rfc3339) - .unwrap(), + utc: time::OffsetDateTime::parse( + "2050-01-01T00:00:00Z", + &time::format_description::well_known::Rfc3339, + ) + .unwrap(), encoding: Asn1TimeEncoding::UtcTime, }; assert!(t.validate_encoding_rfc5280("t").is_err()); @@ -43,18 +46,12 @@ fn time_encoding_validation() { fn algorithm_params_absent_or_null_helper() { // AlgorithmIdentifier ::= SEQUENCE { algorithm OID, parameters ANY OPTIONAL } // Using sha256WithRSAEncryption with NULL parameters. - let alg_null = hex::decode( - "300D06092A864886F70D01010B0500", - ) - .unwrap(); + let alg_null = hex::decode("300D06092A864886F70D01010B0500").unwrap(); let (_rem, id) = AlgorithmIdentifier::from_der(&alg_null).expect("parse AlgorithmIdentifier"); assert!(algorithm_params_absent_or_null(&id)); // Same OID, but parameters = INTEGER 1 (invalid for our helper). - let alg_int = hex::decode( - "300E06092A864886F70D01010B020101", - ) - .unwrap(); + let alg_int = hex::decode("300E06092A864886F70D01010B020101").unwrap(); let (_rem, id) = AlgorithmIdentifier::from_der(&alg_int).expect("parse AlgorithmIdentifier"); assert!(!algorithm_params_absent_or_null(&id)); diff --git a/tests/test_crl_decode.rs b/tests/test_crl_decode.rs index f5055ff..c2bf037 100644 --- a/tests/test_crl_decode.rs +++ b/tests/test_crl_decode.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; -use rpki::data_model::crl::RpkixCrl; use rpki::data_model::crl::Asn1TimeEncoding; +use rpki::data_model::crl::RpkixCrl; #[test] fn decode_and_validate_crl_fixture() { @@ -42,9 +42,8 @@ fn crl_signature_verification_succeeds_with_issuer_cert() { #[test] fn decode_crl_with_revoked_entries() { - let der = - std::fs::read("tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl") - .expect("read CRL fixture with revoked entries"); + let der = std::fs::read("tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl") + .expect("read CRL fixture with revoked entries"); let crl = RpkixCrl::decode_der(&der).expect("decode CRL"); diff --git a/tests/test_crl_errors.rs b/tests/test_crl_errors.rs index 139306c..b10e041 100644 --- a/tests/test_crl_errors.rs +++ b/tests/test_crl_errors.rs @@ -1,11 +1,11 @@ -use rpki::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl}; +use rpki::data_model::crl::{CrlDecodeError, CrlParseError, CrlVerifyError, RpkixCrl}; use x509_parser::prelude::FromDer; use x509_parser::prelude::X509Certificate; const TEST_NO_CRLSIGN_CERT_DER_B64: &str = "MIIDATCCAemgAwIBAgIUCyQLQJn92+gyAzvIz22q1F/97OMwDQYJKoZIhvcNAQELBQAwGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMB4XDTI2MDEyNzAzNTk1OVoXDTM2MDEyNTAzNTk1OVowGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/aoMU8J6cddkM2r6F2snd1rCdQPepgo2T2lrqWFcxnQJdcxBL1OYg3wFi95TJmZSeIHIOGauDaJ2abmjgyOUHOC4U68x66JRg4hLkmLxo1cf3uYHWl9Obph6g2qPRvN80ORq70JPuL6mAfUkNiO9hnwK6oQiTzc/rjCQGIFH8kTESBMXLfNCyUpGi+MNztYH6Ha6bKAQuXgd29OFwIkOlGQnYgGC2qBMvnp86eITvV1gTiuI8Ho9m9nZHCmaD7TylvkMDq8Hk5nkIpRcG0uO60SkR2BiMOYe/TNn5dTmHd6bsdbU2GOvgnq1SnqGq3FOWhKIe3ycUJde0uNfZOqRwIDAQABoz8wPTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUFjyzfJCDNhFfKxVr06kjUkE23dMwDQYJKoZIhvcNAQELBQADggEBAK98n2gVlwKA3Ob1YeAm9f+8hm7pbvrt0tA8GW180CILjf09k7fKgiRlxqGdZ9ySXjU52+zCqu3MpBXVbI87ZC+zA6uK05n4y1F0n85MJ9hGR2UEiPcqou85X73LvioynnSOy/OV1PjKJXReUsqF3GgDtgcMyFssPJ9s/5DWuUCScUJY6pu0kuIGOLQ/oXUw4TvxUeyz73gOTiAJshVTQoLpHUhj0595S7lArjwi7oLI1b8m8guTknvhk0Sc3tJZmUqOcIvYIs0guHpaeC+sMoF4K+6UTrxxOBdX+fUEWNpUyYXWHjdZq25PbJdHwA/VAW2zYVojaVREligf0Qfo6F4="; fn test_no_crlsign_cert_der() -> Vec { - use base64::{engine::general_purpose, Engine as _}; + use base64::{Engine as _, engine::general_purpose}; general_purpose::STANDARD .decode(TEST_NO_CRLSIGN_CERT_DER_B64) .expect("decode base64 cert") @@ -67,7 +67,10 @@ fn verify_errors_are_reported() { let err = crl .verify_signature_with_issuer_certificate_der(&bad) .unwrap_err(); - assert!(matches!(err, CrlVerifyError::IssuerCertificateTrailingBytes(1))); + assert!(matches!( + err, + CrlVerifyError::IssuerCertificateTrailingBytes(1) + )); } #[test] @@ -103,7 +106,10 @@ fn decode_rejects_trailing_bytes() { let mut bad = crl_der.clone(); bad.push(0); let err = RpkixCrl::decode_der(&bad).unwrap_err(); - assert!(matches!(err, CrlDecodeError::TrailingBytes(1))); + assert!(matches!( + err, + CrlDecodeError::Parse(CrlParseError::TrailingBytes(1)) + )); } #[test] @@ -122,6 +128,8 @@ fn verify_rejects_crl_with_trailing_bytes_in_raw_der() { let (_rem, issuer_cert) = X509Certificate::from_der(&issuer_cert_der).unwrap(); let spki_der = issuer_cert.public_key().raw.to_vec(); - let err = crl.verify_signature_with_issuer_spki_der(&spki_der).unwrap_err(); + let err = crl + .verify_signature_with_issuer_spki_der(&spki_der) + .unwrap_err(); assert!(matches!(err, CrlVerifyError::CrlTrailingBytes(1))); } diff --git a/tests/test_layered_api_m0.rs b/tests/test_layered_api_m0.rs new file mode 100644 index 0000000..82a4060 --- /dev/null +++ b/tests/test_layered_api_m0.rs @@ -0,0 +1,161 @@ +use std::path::PathBuf; + +use rpki::data_model::aspa::AspaEContent; +use rpki::data_model::aspa::AspaObject; +use rpki::data_model::crl::RpkixCrl; +use rpki::data_model::manifest::ManifestEContent; +use rpki::data_model::manifest::ManifestObject; +use rpki::data_model::rc::ResourceCertificate; +use rpki::data_model::roa::RoaEContent; +use rpki::data_model::roa::RoaObject; +use rpki::data_model::signed_object::RpkiSignedObject; +use rpki::data_model::ta::{TaCertificate, TrustAnchor}; +use rpki::data_model::tal::Tal; + +#[test] +fn scheme_a_layered_api_smoke() { + // TAL / TA / TrustAnchor + let tal_path = PathBuf::from("tests/fixtures/tal/ripe-ncc.tal"); + let tal_bytes = std::fs::read(&tal_path).expect("read TAL fixture"); + let tal = Tal::parse_bytes(&tal_bytes) + .expect("parse TAL") + .validate_profile() + .expect("validate TAL profile"); + + let ta_path = PathBuf::from("tests/fixtures/ta/ripe-ncc-ta.cer"); + let ta_der = std::fs::read(&ta_path).expect("read TA cert fixture"); + let ta = TaCertificate::parse_der(&ta_der) + .expect("parse TA cert") + .validate_profile() + .expect("validate TA constraints"); + ta.verify_self_signature() + .expect("verify TA self-signature"); + + let resolved = tal + .ta_uris + .first() + .cloned() + .expect("TAL must include at least one TA URI"); + let _ta = TrustAnchor::bind(tal, ta, Some(&resolved)).expect("bind trust anchor"); + + // A CA resource certificate fixture (used as issuer in other tests). + let ca_path = PathBuf::from( + "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", + ); + let ca_der = std::fs::read(&ca_path).expect("read CA cert fixture"); + let ca_rc = ResourceCertificate::parse_der(&ca_der) + .expect("parse CA resource certificate") + .validate_profile() + .expect("validate CA resource certificate profile"); + ca_rc + .validate_profile() + .expect("validate CA resource certificate profile"); + + // Signed object wrapper. + let mft_path = PathBuf::from( + "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", + ); + let mft_der = std::fs::read(&mft_path).expect("read MFT fixture"); + let so = RpkiSignedObject::parse_der(&mft_der) + .expect("parse signed object") + .validate_profile() + .expect("validate signed object profile"); + so.verify().expect("verify CMS signature"); + + // Manifest object. + let mft_obj = ManifestObject::parse_der(&mft_der) + .expect("parse manifest") + .validate_profile() + .expect("validate manifest profile"); + mft_obj + .validate_profile() + .expect("validate manifest profile"); + mft_obj + .validate_embedded_ee_cert() + .expect("validate manifest EE resources"); + mft_obj + .signed_object + .verify() + .expect("verify manifest CMS signature"); + let mft_ec = ManifestEContent::parse_der( + &mft_obj + .signed_object + .signed_data + .encap_content_info + .econtent, + ) + .expect("parse MFT eContent") + .validate_profile() + .expect("validate MFT eContent profile"); + mft_ec + .validate_profile() + .expect("validate MFT eContent profile"); + + // ROA object. + let roa_path = + PathBuf::from("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"); + let roa_der = std::fs::read(&roa_path).expect("read ROA fixture"); + let roa_obj = RoaObject::parse_der(&roa_der) + .expect("parse ROA") + .validate_profile() + .expect("validate ROA profile"); + roa_obj + .validate_embedded_ee_cert() + .expect("validate ROA EE resources"); + roa_obj + .signed_object + .verify() + .expect("verify ROA CMS signature"); + let roa_ec = RoaEContent::parse_der( + &roa_obj + .signed_object + .signed_data + .encap_content_info + .econtent, + ) + .expect("parse ROA eContent") + .validate_profile() + .expect("validate ROA eContent profile"); + roa_ec + .validate_profile() + .expect("validate ROA eContent profile"); + + // ASPA object. + let aspa_path = PathBuf::from( + "tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa", + ); + let aspa_der = std::fs::read(&aspa_path).expect("read ASPA fixture"); + let aspa_obj = AspaObject::parse_der(&aspa_der) + .expect("parse ASPA") + .validate_profile() + .expect("validate ASPA profile"); + aspa_obj + .validate_embedded_ee_cert() + .expect("validate ASPA EE resources"); + aspa_obj + .signed_object + .verify() + .expect("verify ASPA CMS signature"); + let aspa_ec = AspaEContent::parse_der( + &aspa_obj + .signed_object + .signed_data + .encap_content_info + .econtent, + ) + .expect("parse ASPA eContent") + .validate_profile() + .expect("validate ASPA eContent profile"); + aspa_ec + .validate_profile() + .expect("validate ASPA eContent profile"); + + // CRL object. + let crl_path = PathBuf::from("tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl"); + let crl_der = std::fs::read(&crl_path).expect("read CRL fixture with revoked entries"); + let crl = RpkixCrl::parse_der(&crl_der) + .expect("parse CRL") + .validate_profile() + .expect("validate CRL profile"); + crl.validate_profile().expect("validate CRL profile"); +} diff --git a/tests/test_manifest_decode.rs b/tests/test_manifest_decode.rs index 69aa298..5a8b531 100644 --- a/tests/test_manifest_decode.rs +++ b/tests/test_manifest_decode.rs @@ -1,4 +1,7 @@ -use rpki::data_model::manifest::{ManifestEContent, ManifestObject}; +use rpki::data_model::manifest::{ + ManifestDecodeError, ManifestEContent, ManifestObject, ManifestProfileError, +}; +use rpki::data_model::signed_object::RpkiSignedObject; #[test] fn decode_manifest_fixture_smoke() { @@ -7,17 +10,44 @@ fn decode_manifest_fixture_smoke() { ) .expect("read MFT fixture"); let mft = ManifestObject::decode_der(&der).expect("decode manifest object"); + mft.validate_profile().expect("validate manifest profile"); assert_eq!(mft.manifest.version, 0); - assert_eq!(mft.manifest.file_hash_alg, rpki::data_model::oid::OID_SHA256); + assert_eq!( + mft.manifest.file_hash_alg, + rpki::data_model::oid::OID_SHA256 + ); assert!(mft.manifest.next_update > mft.manifest.this_update); assert!(!mft.manifest.files.is_empty()); // The manifest file MUST NOT be listed in its own fileList. - assert!(mft - .manifest - .files - .iter() - .all(|f| !f.file_name.to_ascii_lowercase().ends_with(".mft"))); + assert!( + mft.manifest + .files + .iter() + .all(|f| !f.file_name.to_ascii_lowercase().ends_with(".mft")) + ); +} + +#[test] +fn decode_rejects_non_manifest_econtent_type() { + let der = std::fs::read("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa") + .expect("read ROA fixture"); + let err = ManifestObject::decode_der(&der).unwrap_err(); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::InvalidEContentType(_)) + )); +} + +#[test] +fn from_signed_object_accepts_manifest_fixture() { + let so_der = std::fs::read( + "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", + ) + .expect("read MFT fixture"); + let so = RpkiSignedObject::decode_der(&so_der).expect("decode signed object"); + let mft = ManifestObject::from_signed_object(so).expect("from_signed_object"); + mft.validate_profile().expect("validate manifest profile"); } #[test] @@ -32,4 +62,3 @@ fn decode_manifest_econtent_from_fixture_signed_object() { .expect("decode manifest eContent"); assert_eq!(e.version, 0); } - diff --git a/tests/test_manifest_decode_errors.rs b/tests/test_manifest_decode_errors.rs index b21b9f3..e906799 100644 --- a/tests/test_manifest_decode_errors.rs +++ b/tests/test_manifest_decode_errors.rs @@ -1,5 +1,7 @@ use rpki::data_model::common::BigUnsigned; -use rpki::data_model::manifest::{ManifestDecodeError, ManifestEContent, ManifestObject}; +use rpki::data_model::manifest::{ + ManifestDecodeError, ManifestEContent, ManifestObject, ManifestParseError, ManifestProfileError, +}; use rpki::data_model::signed_object::RpkiSignedObject; fn len_bytes(len: usize) -> Vec { @@ -49,8 +51,8 @@ fn der_integer_u64(v: u64) -> Vec { } fn der_oid(oid: &str) -> Vec { - use std::str::FromStr; use der_parser::asn1_rs::ToDer; + use std::str::FromStr; let oid = der_parser::Oid::from_str(oid).unwrap(); oid.to_der_vec().unwrap() } @@ -116,7 +118,10 @@ fn manifest_econtent_version_must_be_zero_when_present() { vec![], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::InvalidManifestVersion(1))); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::InvalidManifestVersion(1)) + )); } #[test] @@ -131,7 +136,10 @@ fn manifest_number_too_long_rejected() { vec![], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::ManifestNumberTooLong)); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::ManifestNumberTooLong) + )); } #[test] @@ -146,7 +154,10 @@ fn manifest_number_negative_rejected() { vec![], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::InvalidManifestNumber)); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::InvalidManifestNumber) + )); } #[test] @@ -160,7 +171,10 @@ fn this_update_and_next_update_must_be_generalized_time() { vec![], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::InvalidThisUpdate)); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::InvalidThisUpdate) + )); let der = manifest_der( Some(0), @@ -171,7 +185,10 @@ fn this_update_and_next_update_must_be_generalized_time() { vec![], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::InvalidNextUpdate)); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::InvalidNextUpdate) + )); } #[test] @@ -185,7 +202,10 @@ fn next_update_must_be_later() { vec![], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::NextUpdateNotLater)); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::NextUpdateNotLater) + )); } #[test] @@ -199,7 +219,10 @@ fn file_hash_alg_must_be_sha256() { vec![], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::InvalidFileHashAlg(_))); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::InvalidFileHashAlg(_)) + )); } #[test] @@ -215,7 +238,10 @@ fn file_list_entry_validation() { vec![file_and_hash("bad!.roa", 0, &hash)], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::InvalidFileName(_))); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::InvalidFileName(_)) + )); // Non-octet-aligned BIT STRING let der = manifest_der( @@ -227,7 +253,10 @@ fn file_list_entry_validation() { vec![file_and_hash("ok.roa", 1, &hash)], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::HashNotOctetAligned)); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::HashNotOctetAligned) + )); // Wrong hash length let der = manifest_der( @@ -239,7 +268,10 @@ fn file_list_entry_validation() { vec![file_and_hash("ok.roa", 0, &[0u8; 31])], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::InvalidHashLength(31))); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::InvalidHashLength(31)) + )); } #[test] @@ -251,7 +283,10 @@ fn manifest_object_requires_correct_econtent_type() { let mut so = RpkiSignedObject::decode_der(&so_der).expect("decode signed object"); so.signed_data.encap_content_info.econtent_type = "1.2.3.4".to_string(); let err = ManifestObject::from_signed_object(so).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::InvalidEContentType(_))); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::InvalidEContentType(_)) + )); } #[test] @@ -260,7 +295,7 @@ fn manifest_sequence_length_is_validated() { let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!( err, - ManifestDecodeError::InvalidManifestSequenceLen(_) + ManifestDecodeError::Validate(ManifestProfileError::InvalidManifestSequenceLen(_)) )); } @@ -279,7 +314,10 @@ fn file_list_must_be_sequence_and_entry_shape_validated() { der_sequence(fields) }; let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::InvalidFileList)); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::InvalidFileList) + )); // FileAndHash not SEQUENCE of 2 let der = manifest_der( @@ -291,7 +329,10 @@ fn file_list_must_be_sequence_and_entry_shape_validated() { vec![der_sequence(vec![der_ia5("ok.roa")])], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::InvalidFileAndHash)); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::InvalidFileAndHash) + )); } #[test] @@ -308,7 +349,10 @@ fn version_tag_must_be_context_specific_0() { der_sequence(fields) }; let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::Parse(_))); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::ProfileDecode(_)) + )); } #[test] @@ -331,7 +375,10 @@ fn manifest_econtent_trailing_bytes_are_rejected() { let mut bad = der.clone(); bad.push(0); let err = ManifestEContent::decode_der(&bad).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::TrailingBytes(1))); + assert!(matches!( + err, + ManifestDecodeError::Parse(ManifestParseError::TrailingBytes(1)) + )); } #[test] @@ -350,7 +397,10 @@ fn manifest_version_rejects_trailing_bytes_inside_explicit_tag() { der_sequence(fields) }; let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::Parse(_))); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::ProfileDecode(_)) + )); } #[test] @@ -366,18 +416,21 @@ fn manifest_rejects_hash_with_wrong_type() { vec![entry], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::InvalidHashType)); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::InvalidHashType) + )); } #[test] fn file_name_validation_branches_are_exercised() { let hash = vec![0u8; 32]; let cases = [ - "noext", // missing '.' - ".roa", // empty base - "a.roaa", // ext len != 3 - "a.r0a", // ext not alphabetic - "a.txt", // ext not allowlisted + "noext", // missing '.' + ".roa", // empty base + "a.roaa", // ext len != 3 + "a.r0a", // ext not alphabetic + "a.txt", // ext not allowlisted ]; for name in cases { let der = manifest_der( @@ -389,7 +442,10 @@ fn file_name_validation_branches_are_exercised() { vec![file_and_hash(name, 0, &hash)], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, ManifestDecodeError::InvalidFileName(_))); + assert!(matches!( + err, + ManifestDecodeError::Validate(ManifestProfileError::InvalidFileName(_)) + )); } } diff --git a/tests/test_manifest_embedded_ee_cert.rs b/tests/test_manifest_embedded_ee_cert.rs index 89aa1a0..02ca6ab 100644 --- a/tests/test_manifest_embedded_ee_cert.rs +++ b/tests/test_manifest_embedded_ee_cert.rs @@ -1,14 +1,89 @@ -use rpki::data_model::manifest::ManifestObject; +use rpki::data_model::manifest::{ManifestObject, ManifestValidateError}; +use rpki::data_model::rc::{ + Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, + IpResourceSet, +}; -#[test] -fn manifest_embedded_ee_cert_resources_validate() { +fn load_manifest_fixture() -> ManifestObject { let der = std::fs::read( "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", ) .expect("read MFT fixture"); + ManifestObject::decode_der(&der).expect("decode manifest") +} - let mft = ManifestObject::decode_der(&der).expect("decode manifest"); +#[test] +fn manifest_embedded_ee_cert_resources_validate() { + let mft = load_manifest_fixture(); mft.validate_embedded_ee_cert() .expect("manifest EE cert resources must validate"); } +#[test] +fn validate_rejects_when_ip_and_as_resources_missing() { + let mft = load_manifest_fixture(); + let mut ee = mft.signed_object.signed_data.certificates[0] + .resource_cert + .clone(); + ee.tbs.extensions.ip_resources = None; + ee.tbs.extensions.as_resources = None; + let err = mft.validate_against_ee_cert(&ee).unwrap_err(); + assert!(matches!(err, ManifestValidateError::EeResourcesMissing)); +} + +#[test] +fn validate_rejects_when_ip_resources_not_inherit() { + let mft = load_manifest_fixture(); + let mut ee = mft.signed_object.signed_data.certificates[0] + .resource_cert + .clone(); + ee.tbs.extensions.ip_resources = Some(IpResourceSet { + families: vec![IpAddressFamily { + afi: Afi::Ipv4, + choice: IpAddressChoice::AddressesOrRanges(vec![]), + }], + }); + ee.tbs.extensions.as_resources = None; + let err = mft.validate_against_ee_cert(&ee).unwrap_err(); + assert!(matches!( + err, + ManifestValidateError::EeIpResourcesNotInherit + )); +} + +#[test] +fn validate_rejects_when_as_rdi_present_or_asnum_not_inherit() { + let mft = load_manifest_fixture(); + + // rdi present is rejected. + let mut ee = mft.signed_object.signed_data.certificates[0] + .resource_cert + .clone(); + ee.tbs.extensions.ip_resources = None; + ee.tbs.extensions.as_resources = Some(AsResourceSet { + asnum: Some(AsIdentifierChoice::Inherit), + rdi: Some(AsIdentifierChoice::Inherit), + }); + let err = mft.validate_against_ee_cert(&ee).unwrap_err(); + assert!(matches!( + err, + ManifestValidateError::EeAsResourcesRdiPresent + )); + + // asnum not inherit is rejected. + let mut ee = mft.signed_object.signed_data.certificates[0] + .resource_cert + .clone(); + ee.tbs.extensions.ip_resources = None; + ee.tbs.extensions.as_resources = Some(AsResourceSet { + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id( + 64496, + )])), + rdi: None, + }); + let err = mft.validate_against_ee_cert(&ee).unwrap_err(); + assert!(matches!( + err, + ManifestValidateError::EeAsResourcesNotInherit + )); +} diff --git a/tests/test_model_print_real_fixtures.rs b/tests/test_model_print_real_fixtures.rs index d3cf451..8d22384 100644 --- a/tests/test_model_print_real_fixtures.rs +++ b/tests/test_model_print_real_fixtures.rs @@ -2,13 +2,13 @@ use rpki::data_model::aspa::{AspaEContent, AspaObject}; use rpki::data_model::crl::{CrlExtensions, RevokedCert, RpkixCrl}; use rpki::data_model::manifest::{FileAndHash, ManifestEContent, ManifestObject}; use rpki::data_model::rc::{ - AsResourceSet, IpResourceSet, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, - SubjectInfoAccess, + AsResourceSet, IpResourceSet, RcExtensions, ResourceCertKind, ResourceCertificate, + RpkixTbsCertificate, SubjectInfoAccess, }; use rpki::data_model::roa::{RoaEContent, RoaIpAddressFamily, RoaObject}; use rpki::data_model::signed_object::{ - EncapsulatedContentInfo, ResourceEeCertificate, RpkiSignedObject, SignedAttrsProfiled, SignedDataProfiled, - SignerInfoProfiled, + EncapsulatedContentInfo, ResourceEeCertificate, RpkiSignedObject, SignedAttrsProfiled, + SignedDataProfiled, SignerInfoProfiled, }; use rpki::data_model::ta::{TaCertificate, TrustAnchor}; use rpki::data_model::tal::Tal; @@ -199,7 +199,11 @@ impl From<&SignedDataProfiled> for SignedDataProfiledPretty { .map(ResourceEeCertificatePretty::from) .collect(), crls_present: v.crls_present, - signer_infos: v.signer_infos.iter().map(SignerInfoProfiledPretty::from).collect(), + signer_infos: v + .signer_infos + .iter() + .map(SignerInfoProfiledPretty::from) + .collect(), } } } @@ -434,6 +438,7 @@ fn print_all_models_from_real_fixtures() { println!("Fixture (TA): {ta_path}"); let ta_der = std::fs::read(ta_path).expect("read TA fixture"); let ta = TaCertificate::from_der(&ta_der).expect("parse TA cert"); + println!("TA.verify_self_signature={:?}", ta.verify_self_signature()); let resolved = tal .ta_uris @@ -442,15 +447,15 @@ fn print_all_models_from_real_fixtures() { .or_else(|| tal.ta_uris.first()) .cloned() .expect("tal has at least one uri"); - let trust_anchor = TrustAnchor::bind(tal, &ta_der, Some(&resolved)).expect("bind trust anchor"); + let trust_anchor = + TrustAnchor::bind(tal, ta.clone(), Some(&resolved)).expect("bind trust anchor"); println!("{:#?}", TalPretty::from(&trust_anchor.tal)); println!("{:#?}", TaCertificatePretty::from(&ta)); println!("{:#?}", TrustAnchorPretty::from(&trust_anchor)); println!(); println!("== ResourceCertificate (example non-TA CA cert) =="); - let ca_path = - "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"; + let ca_path = "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"; println!("Fixture (CA cert): {ca_path}"); let ca_der = std::fs::read(ca_path).expect("read CA cert fixture"); let ca_rc = ResourceCertificate::from_der(&ca_der).expect("parse CA resource certificate"); @@ -458,14 +463,19 @@ fn print_all_models_from_real_fixtures() { println!(); println!("== Signed Object / Manifest =="); - let mft_path = - "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft"; + let mft_path = "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft"; println!("Fixture (MFT): {mft_path}"); let mft_der = std::fs::read(mft_path).expect("read MFT fixture"); let mft_obj = ManifestObject::decode_der(&mft_der).expect("decode manifest object"); println!("{:#?}", ManifestObjectPretty::from(&mft_obj)); - println!("Manifest.validate_embedded_ee_cert={:?}", mft_obj.validate_embedded_ee_cert()); - println!("Manifest.verify_signature={:?}", mft_obj.signed_object.verify_signature()); + println!( + "Manifest.validate_embedded_ee_cert={:?}", + mft_obj.validate_embedded_ee_cert() + ); + println!( + "Manifest.verify_signature={:?}", + mft_obj.signed_object.verify_signature() + ); println!(); println!("== Signed Object / ROA =="); @@ -474,19 +484,30 @@ fn print_all_models_from_real_fixtures() { let roa_der = std::fs::read(roa_path).expect("read ROA fixture"); let roa_obj = RoaObject::decode_der(&roa_der).expect("decode ROA object"); println!("{:#?}", RoaObjectPretty::from(&roa_obj)); - println!("ROA.validate_embedded_ee_cert={:?}", roa_obj.validate_embedded_ee_cert()); - println!("ROA.verify_signature={:?}", roa_obj.signed_object.verify_signature()); + println!( + "ROA.validate_embedded_ee_cert={:?}", + roa_obj.validate_embedded_ee_cert() + ); + println!( + "ROA.verify_signature={:?}", + roa_obj.signed_object.verify_signature() + ); println!(); println!("== Signed Object / ASPA =="); - let aspa_path = - "tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa"; + let aspa_path = "tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa"; println!("Fixture (ASPA): {aspa_path}"); let aspa_der = std::fs::read(aspa_path).expect("read ASPA fixture"); let aspa_obj = AspaObject::decode_der(&aspa_der).expect("decode ASPA object"); println!("{:#?}", AspaObjectPretty::from(&aspa_obj)); - println!("ASPA.validate_embedded_ee_cert={:?}", aspa_obj.validate_embedded_ee_cert()); - println!("ASPA.verify_signature={:?}", aspa_obj.signed_object.verify_signature()); + println!( + "ASPA.validate_embedded_ee_cert={:?}", + aspa_obj.validate_embedded_ee_cert() + ); + println!( + "ASPA.verify_signature={:?}", + aspa_obj.signed_object.verify_signature() + ); println!(); println!("== CRL =="); diff --git a/tests/test_rc_from_der_errors.rs b/tests/test_rc_from_der_errors.rs index b65e7d4..3e6169a 100644 --- a/tests/test_rc_from_der_errors.rs +++ b/tests/test_rc_from_der_errors.rs @@ -1,10 +1,13 @@ -use rpki::data_model::rc::{ResourceCertificate, ResourceCertificateError}; +use rpki::data_model::rc::{ + ResourceCertificate, ResourceCertificateError, ResourceCertificateParseError, + ResourceCertificateProfileError, +}; const TEST_NO_SIA_CERT_DER_B64: &str = "MIIDATCCAemgAwIBAgIUCyQLQJn92+gyAzvIz22q1F/97OMwDQYJKoZIhvcNAQELBQAwGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMB4XDTI2MDEyNzAzNTk1OVoXDTM2MDEyNTAzNTk1OVowGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/aoMU8J6cddkM2r6F2snd1rCdQPepgo2T2lrqWFcxnQJdcxBL1OYg3wFi95TJmZSeIHIOGauDaJ2abmjgyOUHOC4U68x66JRg4hLkmLxo1cf3uYHWl9Obph6g2qPRvN80ORq70JPuL6mAfUkNiO9hnwK6oQiTzc/rjCQGIFH8kTESBMXLfNCyUpGi+MNztYH6Ha6bKAQuXgd29OFwIkOlGQnYgGC2qBMvnp86eITvV1gTiuI8Ho9m9nZHCmaD7TylvkMDq8Hk5nkIpRcG0uO60SkR2BiMOYe/TNn5dTmHd6bsdbU2GOvgnq1SnqGq3FOWhKIe3ycUJde0uNfZOqRwIDAQABoz8wPTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUFjyzfJCDNhFfKxVr06kjUkE23dMwDQYJKoZIhvcNAQELBQADggEBAK98n2gVlwKA3Ob1YeAm9f+8hm7pbvrt0tA8GW180CILjf09k7fKgiRlxqGdZ9ySXjU52+zCqu3MpBXVbI87ZC+zA6uK05n4y1F0n85MJ9hGR2UEiPcqou85X73LvioynnSOy/OV1PjKJXReUsqF3GgDtgcMyFssPJ9s/5DWuUCScUJY6pu0kuIGOLQ/oXUw4TvxUeyz73gOTiAJshVTQoLpHUhj0595S7lArjwi7oLI1b8m8guTknvhk0Sc3tJZmUqOcIvYIs0guHpaeC+sMoF4K+6UTrxxOBdX+fUEWNpUyYXWHjdZq25PbJdHwA/VAW2zYVojaVREligf0Qfo6F4="; fn decode_b64(b64: &str) -> Vec { - use base64::engine::general_purpose::STANDARD; use base64::Engine as _; + use base64::engine::general_purpose::STANDARD; STANDARD.decode(b64).unwrap() } @@ -44,7 +47,10 @@ fn trailing_bytes_after_cert_are_rejected() { let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64); der.push(0); let err = ResourceCertificate::from_der(&der).unwrap_err(); - assert!(matches!(err, ResourceCertificateError::TrailingBytes(1))); + assert!(matches!( + err, + ResourceCertificateError::Parse(ResourceCertificateParseError::TrailingBytes(1)) + )); } #[test] @@ -52,24 +58,41 @@ fn signature_algorithm_mismatch_is_detected() { let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64); // DER encoding of sha256WithRSAEncryption OID: // 06 09 2A 86 48 86 F7 0D 01 01 0B - let oid = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B]; + let oid = [ + 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B, + ]; let mut patched = oid; patched[10] = 0x01; // rsaEncryption, same length encoding assert!(replace_first(&mut der, &oid, &patched)); let err = ResourceCertificate::from_der(&der).unwrap_err(); - assert!(matches!(err, ResourceCertificateError::SignatureAlgorithmMismatch)); + assert!(matches!( + err, + ResourceCertificateError::Validate( + ResourceCertificateProfileError::SignatureAlgorithmMismatch + ) + )); } #[test] fn unsupported_signature_algorithm_is_detected() { let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64); - let oid = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B]; + let oid = [ + 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B, + ]; let mut patched = oid; patched[10] = 0x01; let n = replace_all(&mut der, &oid, &patched); - assert!(n >= 2, "expected to patch at least inner+outer AlgorithmIdentifier"); + assert!( + n >= 2, + "expected to patch at least inner+outer AlgorithmIdentifier" + ); let err = ResourceCertificate::from_der(&der).unwrap_err(); - assert!(matches!(err, ResourceCertificateError::UnsupportedSignatureAlgorithm)); + assert!(matches!( + err, + ResourceCertificateError::Validate( + ResourceCertificateProfileError::UnsupportedSignatureAlgorithm + ) + )); } #[test] @@ -83,11 +106,15 @@ fn invalid_signature_algorithm_parameters_are_detected() { let mut patched = alg; patched[11] = 0x04; let n = replace_all(&mut der, &alg, &patched); - assert!(n >= 2, "expected to patch at least inner+outer AlgorithmIdentifier parameters"); + assert!( + n >= 2, + "expected to patch at least inner+outer AlgorithmIdentifier parameters" + ); let err = ResourceCertificate::from_der(&der).unwrap_err(); assert!(matches!( err, - ResourceCertificateError::InvalidSignatureAlgorithmParameters + ResourceCertificateError::Validate( + ResourceCertificateProfileError::InvalidSignatureAlgorithmParameters + ) )); } - diff --git a/tests/test_rc_from_der_fixtures.rs b/tests/test_rc_from_der_fixtures.rs index 3ef7fb0..c8307bd 100644 --- a/tests/test_rc_from_der_fixtures.rs +++ b/tests/test_rc_from_der_fixtures.rs @@ -13,7 +13,11 @@ fn resource_certificate_from_der_parses_ca_fixtures() { let der = std::fs::read(path).expect("read CA cert fixture"); let rc = ResourceCertificate::from_der(&der).expect("parse CA cert fixture"); - assert_eq!(rc.kind, ResourceCertKind::Ca, "fixture should be CA: {path}"); + assert_eq!( + rc.kind, + ResourceCertKind::Ca, + "fixture should be CA: {path}" + ); assert_eq!(rc.tbs.version, 2, "X.509 v3 encoded as 2: {path}"); assert_eq!( @@ -23,21 +27,28 @@ fn resource_certificate_from_der_parses_ca_fixtures() { ); assert!( - matches!(rc.tbs.extensions.subject_info_access, Some(SubjectInfoAccess::Ca(_))), + matches!( + rc.tbs.extensions.subject_info_access, + Some(SubjectInfoAccess::Ca(_)) + ), "CA SIA should not contain signedObject accessMethod: {path}" ); - assert!(rc.tbs.extensions.ip_resources.is_some(), "CA should have IP resources: {path}"); + assert!( + rc.tbs.extensions.ip_resources.is_some(), + "CA should have IP resources: {path}" + ); } } #[test] fn resource_certificate_from_der_parses_as_resources_in_apnic_fixture() { - let path = - "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"; + let path = "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"; let der = std::fs::read(path).expect("read APNIC CA cert fixture"); let rc = ResourceCertificate::from_der(&der).expect("parse APNIC CA cert fixture"); - assert!(rc.tbs.extensions.as_resources.is_some(), "fixture should carry AS resources"); + assert!( + rc.tbs.extensions.as_resources.is_some(), + "fixture should carry AS resources" + ); } - diff --git a/tests/test_rc_helpers.rs b/tests/test_rc_helpers.rs index d6d4659..31daac6 100644 --- a/tests/test_rc_helpers.rs +++ b/tests/test_rc_helpers.rs @@ -1,6 +1,6 @@ use rpki::data_model::rc::{ - Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange, - IpPrefix, + Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, + IpAddressOrRange, IpPrefix, }; #[test] @@ -40,17 +40,21 @@ fn as_resource_set_asnum_single_id_returns_expected_values() { assert_eq!(inherit.asnum_single_id(), None); let single_id = AsResourceSet { - asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)])), + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id( + 64512, + )])), rdi: None, }; assert_eq!(single_id.asnum_single_id(), Some(64512)); let single_range = AsResourceSet { asnum: None, - rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { - min: 64512, - max: 64520, - }])), + rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![ + AsIdOrRange::Range { + min: 64512, + max: 64520, + }, + ])), }; assert_eq!(single_range.asnum_single_id(), None); @@ -68,7 +72,9 @@ fn as_resource_set_asnum_single_id_returns_expected_values() { fn as_identifier_choice_has_range_detects_ranges() { assert!(!AsIdentifierChoice::Inherit.has_range()); assert!(!AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)]).has_range()); - assert!(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { min: 1, max: 2 }]).has_range()); + assert!( + AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { min: 1, max: 2 }]).has_range() + ); } #[test] @@ -81,13 +87,16 @@ fn ip_resource_contains_prefix_finds_matching_family() { addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], })]), }; - let set = rpki::data_model::rc::IpResourceSet { families: vec![fam_v6] }; + let set = rpki::data_model::rc::IpResourceSet { + families: vec![fam_v6], + }; let p = IpPrefix { afi: Afi::Ipv6, prefix_len: 48, - addr: vec![0x20, 0x01, 0x0d, 0xb8, 0x12, 0x34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + addr: vec![ + 0x20, 0x01, 0x0d, 0xb8, 0x12, 0x34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], }; assert!(set.contains_prefix(&p)); } - diff --git a/tests/test_rc_ip_resource_range_coverage.rs b/tests/test_rc_ip_resource_range_coverage.rs index 4f8b58a..6ddaa9e 100644 --- a/tests/test_rc_ip_resource_range_coverage.rs +++ b/tests/test_rc_ip_resource_range_coverage.rs @@ -1,5 +1,6 @@ use rpki::data_model::rc::{ - Afi, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpAddressRange, IpPrefix, IpResourceSet, + Afi, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpAddressRange, IpPrefix, + IpResourceSet, }; #[test] @@ -7,10 +8,12 @@ fn ip_resource_range_covers_prefix_ipv4() { let set = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv4, - choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Range(IpAddressRange { - min: vec![10, 0, 0, 0], - max: vec![10, 255, 255, 255], - })]), + choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Range( + IpAddressRange { + min: vec![10, 0, 0, 0], + max: vec![10, 255, 255, 255], + }, + )]), }], }; @@ -34,10 +37,15 @@ fn ip_resource_range_covers_prefix_ipv6() { let set = IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv6, - choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Range(IpAddressRange { - min: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - max: vec![0x20, 0x01, 0x0d, 0xb8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], - })]), + choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Range( + IpAddressRange { + min: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + max: vec![ + 0x20, 0x01, 0x0d, 0xb8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + ], + }, + )]), }], }; @@ -55,4 +63,3 @@ fn ip_resource_range_covers_prefix_ipv6() { }; assert!(!set.contains_prefix(&outside)); } - diff --git a/tests/test_rc_resource_extensions_decode.rs b/tests/test_rc_resource_extensions_decode.rs index 57ac45a..143860f 100644 --- a/tests/test_rc_resource_extensions_decode.rs +++ b/tests/test_rc_resource_extensions_decode.rs @@ -1,5 +1,5 @@ use rpki::data_model::rc::{ - Afi, AsIdentifierChoice, AsIdOrRange, AsResourceSet, IpAddressChoice, IpAddressOrRange, + Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressOrRange, IpResourceSet, }; @@ -100,7 +100,8 @@ fn ip_addr_blocks_decode_handles_inherit_and_range_endpoint_fill() { // Inherit branch. let fam_inherit = der_sequence(vec![der_octet_string(&[0x00, 0x01]), der_null()]); - let decoded = IpResourceSet::decode_extn_value(&der_sequence(vec![fam_inherit])).expect("decode inherit"); + let decoded = + IpResourceSet::decode_extn_value(&der_sequence(vec![fam_inherit])).expect("decode inherit"); assert!(decoded.is_all_inherit()); } @@ -125,11 +126,17 @@ fn autonomous_sys_ids_decode_handles_inherit_ids_and_ranges() { }; assert_eq!(items.len(), 2); assert!(matches!(items[0], AsIdOrRange::Id(64496))); - assert!(matches!(items[1], AsIdOrRange::Range { min: 64500, max: 64510 })); + assert!(matches!( + items[1], + AsIdOrRange::Range { + min: 64500, + max: 64510 + } + )); // asnum inherit. let asnum_inherit = cs_cons(0, der_null()); - let decoded = AsResourceSet::decode_extn_value(&der_sequence(vec![asnum_inherit])).expect("decode inherit"); + let decoded = AsResourceSet::decode_extn_value(&der_sequence(vec![asnum_inherit])) + .expect("decode inherit"); assert!(decoded.is_asnum_inherit()); } - diff --git a/tests/test_rc_resource_extensions_decode_errors.rs b/tests/test_rc_resource_extensions_decode_errors.rs index c91957c..2fe7f9b 100644 --- a/tests/test_rc_resource_extensions_decode_errors.rs +++ b/tests/test_rc_resource_extensions_decode_errors.rs @@ -93,10 +93,7 @@ fn ip_addr_blocks_decode_rejects_invalid_encodings() { assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err()); // ipAddressChoice wrong type. - let fam_wrong = der_sequence(vec![ - der_octet_string(&[0x00, 0x01]), - der_integer_u64(1), - ]); + let fam_wrong = der_sequence(vec![der_octet_string(&[0x00, 0x01]), der_integer_u64(1)]); assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err()); // BitString with invalid unused-bits value (>7). @@ -188,8 +185,13 @@ fn ip_addr_blocks_range_upper_bound_can_fill_all_ones_when_bit_len_zero() { der_sequence(vec![range]), ]); - let set = IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).expect("decode range with 0-bit endpoints"); - let fam = set.families.iter().find(|f| f.afi == rpki::data_model::rc::Afi::Ipv4).unwrap(); + let set = IpResourceSet::decode_extn_value(&der_sequence(vec![fam])) + .expect("decode range with 0-bit endpoints"); + let fam = set + .families + .iter() + .find(|f| f.afi == rpki::data_model::rc::Afi::Ipv4) + .unwrap(); let rpki::data_model::rc::IpAddressChoice::AddressesOrRanges(items) = &fam.choice else { panic!("expected explicit addressesOrRanges"); }; diff --git a/tests/test_roa_canonicalize.rs b/tests/test_roa_canonicalize.rs index ddb7dae..147f653 100644 --- a/tests/test_roa_canonicalize.rs +++ b/tests/test_roa_canonicalize.rs @@ -124,4 +124,3 @@ fn canonicalize_sorts_families_sorts_and_dedups_addresses() { } ); } - diff --git a/tests/test_roa_decode.rs b/tests/test_roa_decode.rs index 1f589a8..fd27a8c 100644 --- a/tests/test_roa_decode.rs +++ b/tests/test_roa_decode.rs @@ -1,12 +1,12 @@ -use rpki::data_model::roa::{RoaDecodeError, RoaObject}; +use rpki::data_model::roa::{RoaDecodeError, RoaObject, RoaProfileError}; +use rpki::data_model::signed_object::RpkiSignedObject; #[test] fn decode_roa_fixture_smoke() { - let der = std::fs::read( - "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa", - ) - .expect("read ROA fixture"); + let der = std::fs::read("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa") + .expect("read ROA fixture"); let roa = RoaObject::decode_der(&der).expect("decode roa"); + roa.validate_profile().expect("validate ROA profile"); assert_eq!( roa.econtent_type, rpki::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ @@ -14,14 +14,24 @@ fn decode_roa_fixture_smoke() { assert_eq!(roa.roa.version, 0); assert_eq!(roa.roa.as_id, 4538); assert!(!roa.roa.ip_addr_blocks.is_empty()); - assert!(roa - .roa - .ip_addr_blocks - .iter() - .all(|f| !f.addresses.is_empty())); + assert!( + roa.roa + .ip_addr_blocks + .iter() + .all(|f| !f.addresses.is_empty()) + ); println!("{roa:#?}"); } +#[test] +fn from_signed_object_accepts_roa_fixture() { + let der = std::fs::read("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa") + .expect("read ROA fixture"); + let so = RpkiSignedObject::decode_der(&der).expect("decode signed object"); + let roa = RoaObject::from_signed_object(so).expect("from_signed_object"); + roa.validate_profile().expect("validate ROA profile"); +} + #[test] fn decode_rejects_non_roa_econtent_type() { let der = std::fs::read( @@ -29,5 +39,8 @@ fn decode_rejects_non_roa_econtent_type() { ) .expect("read MFT fixture"); let err = RoaObject::decode_der(&der).unwrap_err(); - assert!(matches!(err, RoaDecodeError::InvalidEContentType(_))); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::InvalidEContentType(_)) + )); } diff --git a/tests/test_roa_econtent_decode_errors.rs b/tests/test_roa_econtent_decode_errors.rs index 0c1c1bf..840481f 100644 --- a/tests/test_roa_econtent_decode_errors.rs +++ b/tests/test_roa_econtent_decode_errors.rs @@ -1,4 +1,4 @@ -use rpki::data_model::roa::{RoaDecodeError, RoaEContent}; +use rpki::data_model::roa::{RoaDecodeError, RoaEContent, RoaParseError, RoaProfileError}; fn len_bytes(len: usize) -> Vec { if len < 128 { @@ -86,15 +86,184 @@ fn roa_attestation(version: Option, as_id: u64, families: Vec>) -> der_sequence(fields) } +#[test] +fn trailing_bytes_are_rejected_in_parse_step() { + let der = roa_attestation( + None, + 64496, + vec![roa_ip_family( + [0, 1], + vec![roa_ip_address(der_bit_string(0, &[]), None)], + )], + ); + let mut bad = der.clone(); + bad.push(0); + let err = RoaEContent::decode_der(&bad).unwrap_err(); + assert!(matches!( + err, + RoaDecodeError::Parse(RoaParseError::TrailingBytes(1)) + )); +} + +#[test] +fn attestation_sequence_len_is_validated() { + let der = der_sequence(vec![der_integer_u64(64496)]); + let err = RoaEContent::decode_der(&der).unwrap_err(); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::InvalidAttestationSequenceLen(1)) + )); +} + +#[test] +fn version_tag_must_be_context_specific_0() { + let der = { + let mut fields = Vec::new(); + fields.push(cs_explicit(1, der_integer_u64(0))); // wrong tag number [1] + fields.push(der_integer_u64(64496)); + fields.push(der_sequence(vec![roa_ip_family( + [0, 1], + vec![roa_ip_address(der_bit_string(0, &[]), None)], + )])); + der_sequence(fields) + }; + let err = RoaEContent::decode_der(&der).unwrap_err(); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::ProfileDecode(_)) + )); +} + +#[test] +fn version_explicit_tag_rejects_trailing_bytes_inside_inner_der() { + let mut version_inner = der_integer_u64(0); + version_inner.extend(tlv(0x05, &[])); // NULL after INTEGER + + let der = { + let mut fields = Vec::new(); + fields.push(cs_explicit(0, version_inner)); + fields.push(der_integer_u64(64496)); + fields.push(der_sequence(vec![roa_ip_family( + [0, 1], + vec![roa_ip_address(der_bit_string(0, &[]), None)], + )])); + der_sequence(fields) + }; + let err = RoaEContent::decode_der(&der).unwrap_err(); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::ProfileDecode(_)) + )); +} + +#[test] +fn version_zero_is_accepted_when_explicitly_encoded() { + let der = roa_attestation( + Some(0), + 64496, + vec![roa_ip_family( + [0, 1], + vec![roa_ip_address(der_bit_string(7, &[0x80]), None)], // prefix_len=1 + )], + ); + let roa = RoaEContent::decode_der(&der).expect("ROA must decode"); + assert_eq!(roa.version, 0); + assert_eq!(roa.as_id, 64496); +} + +#[test] +fn ip_address_family_shape_and_address_family_length_are_validated() { + // family SEQUENCE has wrong length (1) + let bad_family = der_sequence(vec![der_octet_string(&[0, 1])]); + let der = roa_attestation(None, 64496, vec![bad_family]); + let err = RoaEContent::decode_der(&der).unwrap_err(); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::InvalidIpAddressFamily) + )); + + // addressFamily OCTET STRING has invalid length (3) + let family = der_sequence(vec![ + der_octet_string(&[0, 1, 2]), + der_sequence(vec![roa_ip_address(der_bit_string(0, &[]), None)]), + ]); + let der = roa_attestation(None, 64496, vec![family]); + let err = RoaEContent::decode_der(&der).unwrap_err(); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::InvalidAddressFamily) + )); +} + +#[test] +fn roa_ip_address_shape_and_prefix_encoding_are_validated() { + // ROAIPAddress must have 1..2 elements. + let family = roa_ip_family([0, 1], vec![der_sequence(vec![])]); + let der = roa_attestation(None, 64496, vec![family]); + let err = RoaEContent::decode_der(&der).unwrap_err(); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::InvalidRoaIpAddress) + )); + + // ROAIPAddress.address must be BIT STRING, not OCTET STRING. + let family = roa_ip_family([0, 1], vec![der_sequence(vec![der_octet_string(&[0])])]); + let der = roa_attestation(None, 64496, vec![family]); + let err = RoaEContent::decode_der(&der).unwrap_err(); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::InvalidPrefixBitString) + )); + + // unusedBits > 7 is rejected. + let family = roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(8, &[0]), None)]); + let der = roa_attestation(None, 64496, vec![family]); + let err = RoaEContent::decode_der(&der).unwrap_err(); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::InvalidPrefixUnusedBits) + )); + + // empty BIT STRING with unusedBits != 0 is rejected. + let family = roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(1, &[]), None)]); + let der = roa_attestation(None, 64496, vec![family]); + let err = RoaEContent::decode_der(&der).unwrap_err(); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::InvalidPrefixUnusedBits) + )); +} + +#[test] +fn max_length_integer_range_is_validated() { + // maxLength too large to fit u16 triggers the try_into() error path. + let family = roa_ip_family( + [0, 1], + vec![roa_ip_address(der_bit_string(0, &[0x0A]), Some(70000))], + ); + let der = roa_attestation(None, 64496, vec![family]); + let err = RoaEContent::decode_der(&der).unwrap_err(); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::InvalidMaxLength { .. }) + )); +} + #[test] fn version_must_be_zero_when_present() { let der = roa_attestation( Some(1), 64496, - vec![roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)])], + vec![roa_ip_family( + [0, 1], + vec![roa_ip_address(der_bit_string(0, &[]), None)], + )], ); let err = RoaEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, RoaDecodeError::InvalidVersion(1))); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::InvalidVersion(1)) + )); } #[test] @@ -102,17 +271,26 @@ fn as_id_out_of_range_is_rejected() { let der = roa_attestation( None, (u32::MAX as u64) + 1, - vec![roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)])], + vec![roa_ip_family( + [0, 1], + vec![roa_ip_address(der_bit_string(0, &[]), None)], + )], ); let err = RoaEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, RoaDecodeError::AsIdOutOfRange(_))); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::AsIdOutOfRange(_)) + )); } #[test] fn ip_addr_blocks_len_is_validated() { let der = roa_attestation(None, 64496, vec![]); let err = RoaEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, RoaDecodeError::InvalidIpAddrBlocksLen(0))); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::InvalidIpAddrBlocksLen(0)) + )); let families = vec![ roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)]), @@ -121,7 +299,10 @@ fn ip_addr_blocks_len_is_validated() { ]; let der = roa_attestation(None, 64496, families); let err = RoaEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, RoaDecodeError::InvalidIpAddrBlocksLen(3))); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::InvalidIpAddrBlocksLen(3)) + )); } #[test] @@ -132,7 +313,10 @@ fn duplicate_afi_is_rejected() { ]; let der = roa_attestation(None, 64496, families); let err = RoaEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, RoaDecodeError::DuplicateAfi(_))); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::DuplicateAfi(_)) + )); } #[test] @@ -140,17 +324,26 @@ fn unsupported_afi_is_rejected() { let der = roa_attestation( None, 64496, - vec![roa_ip_family([0x12, 0x34], vec![roa_ip_address(der_bit_string(0, &[]), None)])], + vec![roa_ip_family( + [0x12, 0x34], + vec![roa_ip_address(der_bit_string(0, &[]), None)], + )], ); let err = RoaEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, RoaDecodeError::UnsupportedAfi(_))); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::UnsupportedAfi(_)) + )); } #[test] fn empty_address_list_is_rejected() { let der = roa_attestation(None, 64496, vec![roa_ip_family([0, 1], vec![])]); let err = RoaEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, RoaDecodeError::EmptyAddressList)); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::EmptyAddressList) + )); } #[test] @@ -164,7 +357,10 @@ fn prefix_unused_bits_must_be_zeroed() { vec![roa_ip_family([0, 1], vec![roa_ip_address(bs, None)])], ); let err = RoaEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, RoaDecodeError::InvalidPrefixUnusedBits)); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::InvalidPrefixUnusedBits) + )); } #[test] @@ -177,7 +373,10 @@ fn prefix_len_out_of_range_is_rejected() { vec![roa_ip_family([0, 1], vec![roa_ip_address(bs, None)])], ); let err = RoaEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, RoaDecodeError::PrefixLenOutOfRange { .. })); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::PrefixLenOutOfRange { .. }) + )); } #[test] @@ -188,10 +387,16 @@ fn max_length_range_and_relation_are_validated() { let der = roa_attestation( None, 64496, - vec![roa_ip_family([0, 1], vec![roa_ip_address(bs.clone(), Some(7))])], + vec![roa_ip_family( + [0, 1], + vec![roa_ip_address(bs.clone(), Some(7))], + )], ); let err = RoaEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, RoaDecodeError::InvalidMaxLength { .. })); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::InvalidMaxLength { .. }) + )); // maxLength > ub let der = roa_attestation( @@ -200,6 +405,8 @@ fn max_length_range_and_relation_are_validated() { vec![roa_ip_family([0, 1], vec![roa_ip_address(bs, Some(33))])], ); let err = RoaEContent::decode_der(&der).unwrap_err(); - assert!(matches!(err, RoaDecodeError::InvalidMaxLength { .. })); + assert!(matches!( + err, + RoaDecodeError::Validate(RoaProfileError::InvalidMaxLength { .. }) + )); } - diff --git a/tests/test_roa_embedded_ee_cert.rs b/tests/test_roa_embedded_ee_cert.rs index 3b09791..ebe7283 100644 --- a/tests/test_roa_embedded_ee_cert.rs +++ b/tests/test_roa_embedded_ee_cert.rs @@ -2,10 +2,8 @@ use rpki::data_model::roa::RoaObject; #[test] fn roa_embedded_ee_cert_resources_validate() { - let der = std::fs::read( - "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa", - ) - .expect("read ROA fixture"); + let der = std::fs::read("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa") + .expect("read ROA fixture"); let roa = RoaObject::decode_der(&der).expect("decode roa"); roa.validate_embedded_ee_cert() diff --git a/tests/test_roa_validate_ee_resources.rs b/tests/test_roa_validate_ee_resources.rs index 27fdf1b..4b3bf83 100644 --- a/tests/test_roa_validate_ee_resources.rs +++ b/tests/test_roa_validate_ee_resources.rs @@ -2,12 +2,18 @@ use der_parser::num_bigint::BigUint; use time::OffsetDateTime; use rpki::data_model::rc::{ - Afi, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpPrefix, IpResourceSet, - RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, SubjectInfoAccess, + Afi, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpPrefix, + IpResourceSet, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, + SubjectInfoAccess, +}; +use rpki::data_model::roa::{ + RoaAfi, RoaEContent, RoaIpAddress, RoaIpAddressFamily, RoaValidateError, }; -use rpki::data_model::roa::{RoaAfi, RoaEContent, RoaIpAddress, RoaIpAddressFamily, RoaValidateError}; -fn dummy_ee(ip_resources: Option, as_resources: Option) -> ResourceCertificate { +fn dummy_ee( + ip_resources: Option, + as_resources: Option, +) -> ResourceCertificate { ResourceCertificate { raw_der: vec![], tbs: RpkixTbsCertificate { @@ -133,7 +139,10 @@ fn validate_rejects_when_prefix_not_covered() { None, ); let err = roa.validate_against_ee_cert(&ee).unwrap_err(); - assert!(matches!(err, RoaValidateError::PrefixNotInEeResources { .. })); + assert!(matches!( + err, + RoaValidateError::PrefixNotInEeResources { .. } + )); } #[test] @@ -173,4 +182,3 @@ fn contains_prefix_handles_non_octet_boundary_prefix_len() { roa.validate_against_ee_cert(&ee) .expect("160.18.0.0/16 should be covered by 160.0.0.0/9"); } - diff --git a/tests/test_roa_verify.rs b/tests/test_roa_verify.rs index 6515d27..e6c0d5c 100644 --- a/tests/test_roa_verify.rs +++ b/tests/test_roa_verify.rs @@ -2,13 +2,10 @@ use rpki::data_model::roa::RoaObject; #[test] fn verify_roa_cms_signature_with_embedded_ee_cert() { - let der = std::fs::read( - "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa", - ) - .expect("read ROA fixture"); + let der = std::fs::read("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa") + .expect("read ROA fixture"); let roa = RoaObject::decode_der(&der).expect("decode roa"); roa.signed_object .verify_signature() .expect("ROA CMS signature should verify with embedded EE cert"); } - diff --git a/tests/test_signed_object_decode.rs b/tests/test_signed_object_decode.rs index c52cde1..bce67b5 100644 --- a/tests/test_signed_object_decode.rs +++ b/tests/test_signed_object_decode.rs @@ -12,19 +12,26 @@ fn decode_manifest_signed_object_smoke() { assert_eq!(so.content_info_content_type, OID_SIGNED_DATA); assert_eq!(so.signed_data.version, 3); - assert_eq!(so.signed_data.digest_algorithms, vec![OID_SHA256.to_string()]); + assert_eq!( + so.signed_data.digest_algorithms, + vec![OID_SHA256.to_string()] + ); assert_eq!( so.signed_data.encap_content_info.econtent_type, OID_CT_RPKI_MANIFEST ); assert_eq!(so.signed_data.certificates.len(), 1); - assert!(!so.signed_data.certificates[0] - .sia_signed_object_uris - .is_empty()); - assert!(so.signed_data.certificates[0] - .sia_signed_object_uris - .iter() - .any(|u| u.starts_with("rsync://"))); + assert!( + !so.signed_data.certificates[0] + .sia_signed_object_uris + .is_empty() + ); + assert!( + so.signed_data.certificates[0] + .sia_signed_object_uris + .iter() + .any(|u| u.starts_with("rsync://")) + ); assert_eq!(so.signed_data.signer_infos.len(), 1); println!("{so:#?}") } diff --git a/tests/test_signed_object_decode_errors.rs b/tests/test_signed_object_decode_errors.rs index f2e0147..9ba7ebd 100644 --- a/tests/test_signed_object_decode_errors.rs +++ b/tests/test_signed_object_decode_errors.rs @@ -2,7 +2,9 @@ use rpki::data_model::oid::{ OID_CMS_ATTR_CONTENT_TYPE, OID_CMS_ATTR_MESSAGE_DIGEST, OID_CMS_ATTR_SIGNING_TIME, OID_RSA_ENCRYPTION, OID_SHA256, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SIGNED_DATA, }; -use rpki::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError}; +use rpki::data_model::signed_object::{ + RpkiSignedObject, SignedObjectDecodeError, SignedObjectParseError, SignedObjectValidateError, +}; use sha2::{Digest, Sha256}; use x509_parser::extensions::ParsedExtension; use x509_parser::prelude::FromDer; @@ -14,7 +16,7 @@ const TEST_SIA_HTTPS_CERT_DER_B64: &str = "MIIDODCCAiCgAwIBAgIUBp2fsJYhUBJk711xT const TEST_SIA_DNS_CERT_DER_B64: &str = "MIIDHjCCAgagAwIBAgIUJlS9d2BCJaamLyjYVTjvMNzWDxMwDQYJKoZIhvcNAQELBQAwFzEVMBMGA1UEAwwMVGVzdCBTSUEgZG5zMB4XDTI2MDEyNzA2NTMwOVoXDTM2MDEyNTA2NTMwOVowFzEVMBMGA1UEAwwMVGVzdCBTSUEgZG5zMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnKIddAQzQpDLbM+ZX3qR707L7CZvZ3MDGYN+tvuPXOfhATcOLtEaxu1dK4ZhV4Ou3ZqdxwYauyC+N4An0qCJW8mVr3zhbxathVGW7w4/S9pEV/+8dGW8ypOiqNixtmV++Ww54PguD6uxMk1S3IUOVTJY+QaetMy+SV9lCbOykZys17J56tMBmHRtuOxGPnaLtzZLddWqGhGFSDthSbKX4yToUIhTUl+wIRRYjBjnbGgzH5jV6eHUgrHRk+n567jNa9fe3cuRCGNBe6ny/8NPQnJEksWpA9lGfJDlEFsDIM9cXY78izr6i4JHeErwfusJiSchTT0ePhHXRAYMQoIqywIDAQABo2IwYDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUaY5MXriJ8s5VVa+s09HoSUloKBUwJwYIKwYBBQUHAQsEGzAZMBcGCCsGAQUFBzALggtleGFtcGxlLm5ldDANBgkqhkiG9w0BAQsFAAOCAQEAZSnkWxPTFWepHaK0XvAV145idY0ztEqY9BWUql2Ythzb4rjBAU1TfDRRklnnlE9o9/I6363ltaZBvj95e3CyTu4YGflxEpHsW+4aTth8ty1ee7YSqsdJ8gN08sroIpMTfr6tvWf65cVLSTkB4yP8cnNEM3zGr37zb32ChPXgUFwS9JFf3SMsXudZ4rHougE/PM4pQZvaOl3tFEzohV5MjA2VD38n3y6bVmx3i0Xqze7UZnl06aDKozzTXmFy/DoDRGG2pd2EjoC8gNAqIOL53uRz5nJlp8WEIBMe5Hmokrzv+zkAywVZZtYo1FvonOdg5etH94oMnZEtgV/OO9joRg=="; fn decode_b64(b64: &str) -> Vec { - use base64::{engine::general_purpose, Engine as _}; + use base64::{Engine as _, engine::general_purpose}; general_purpose::STANDARD .decode(b64) .expect("decode base64 cert") @@ -99,8 +101,8 @@ fn der_octet_string(bytes: &[u8]) -> Vec { } fn der_oid(oid: &str) -> Vec { - use std::str::FromStr; use der_parser::asn1_rs::ToDer; + use std::str::FromStr; let oid = der_parser::Oid::from_str(oid).unwrap(); oid.to_der_vec().unwrap() @@ -156,7 +158,10 @@ fn signed_attrs_implicit( ) -> Vec { let mut attrs = vec![ cms_attribute(OID_CMS_ATTR_CONTENT_TYPE, der_oid(content_type_oid)), - cms_attribute(OID_CMS_ATTR_MESSAGE_DIGEST, der_octet_string(message_digest)), + cms_attribute( + OID_CMS_ATTR_MESSAGE_DIGEST, + der_octet_string(message_digest), + ), cms_attribute(OID_CMS_ATTR_SIGNING_TIME, signing_time_der), ]; attrs.extend(extra_attrs); @@ -278,7 +283,10 @@ fn trailing_bytes_after_object() { let mut bad = der.clone(); bad.push(0); let err = RpkiSignedObject::decode_der(&bad).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::TrailingBytes(1))); + assert!(matches!( + err, + SignedObjectDecodeError::Parse(SignedObjectParseError::TrailingBytes(1)) + )); } #[test] @@ -297,7 +305,14 @@ fn content_info_content_must_be_tag0_explicit() { b"e".to_vec(), None, false, - vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + vec![signer_info( + vec![1], + OID_SHA256, + None, + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + )], ); let ci = der_sequence(vec![der_oid(OID_SIGNED_DATA), cs_cons(1, &sd)]); let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); @@ -313,7 +328,14 @@ fn content_info_content_must_contain_only_one_inner_object() { b"e".to_vec(), None, false, - vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + vec![signer_info( + vec![1], + OID_SHA256, + None, + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + )], ); let mut inner = sd.clone(); inner.extend(der_null()); @@ -414,7 +436,14 @@ fn signed_data_unexpected_field_is_rejected() { cs_cons(0, &der_octet_string(b"e")), ]), der_null(), - der_set(vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)]), + der_set(vec![signer_info( + vec![1], + OID_SHA256, + None, + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + )]), ]); let ci = content_info_signed_data(sd); let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); @@ -433,7 +462,14 @@ fn encap_content_info_length_and_tags_are_validated() { der_integer_u64(3), der_set(vec![algorithm_id(OID_SHA256, Some(der_null()))]), encap, - der_set(vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)]), + der_set(vec![signer_info( + vec![1], + OID_SHA256, + None, + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + )]), ]); let ci = content_info_signed_data(sd); let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); @@ -448,7 +484,14 @@ fn encap_content_info_length_and_tags_are_validated() { der_integer_u64(3), der_set(vec![algorithm_id(OID_SHA256, Some(der_null()))]), encap, - der_set(vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)]), + der_set(vec![signer_info( + vec![1], + OID_SHA256, + None, + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + )]), ]); let ci = content_info_signed_data(sd); let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); @@ -465,7 +508,14 @@ fn encap_content_info_length_and_tags_are_validated() { der_integer_u64(3), der_set(vec![algorithm_id(OID_SHA256, Some(der_null()))]), encap, - der_set(vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)]), + der_set(vec![signer_info( + vec![1], + OID_SHA256, + None, + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + )]), ]); let ci = content_info_signed_data(sd); let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); @@ -501,7 +551,10 @@ fn empty_econtent_is_rejected() { vec![si], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::EContentMissing)); + assert!(matches!( + err, + SignedObjectDecodeError::Validate(SignedObjectValidateError::EContentMissing) + )); } #[test] @@ -555,7 +608,10 @@ fn signer_info_sequence_len_and_version_are_validated() { vec![si], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::InvalidSignerInfoVersion(1))); + assert!(matches!( + err, + SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidSignerInfoVersion(1)) + )); } #[test] @@ -585,7 +641,10 @@ fn signed_attrs_structure_and_presence_are_validated() { vec![si], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::Parse(_))); + assert!(matches!( + err, + SignedObjectDecodeError::Validate(SignedObjectValidateError::SignedAttrsParse(_)) + )); // Missing content-type. let signed_attrs = signed_attrs_raw(vec![ @@ -610,11 +669,17 @@ fn signed_attrs_structure_and_presence_are_validated() { vec![si], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::Parse(_))); + assert!(matches!( + err, + SignedObjectDecodeError::Validate(SignedObjectValidateError::SignedAttrsContentTypeMissing) + )); // Missing message-digest. let signed_attrs = signed_attrs_raw(vec![ - cms_attribute(OID_CMS_ATTR_CONTENT_TYPE, der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST)), + cms_attribute( + OID_CMS_ATTR_CONTENT_TYPE, + der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST), + ), cms_attribute(OID_CMS_ATTR_SIGNING_TIME, tlv(0x17, b"240101000000Z")), ]); let si = signer_info( @@ -635,11 +700,19 @@ fn signed_attrs_structure_and_presence_are_validated() { vec![si], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::Parse(_))); + assert!(matches!( + err, + SignedObjectDecodeError::Validate( + SignedObjectValidateError::SignedAttrsMessageDigestMissing + ) + )); // Missing signing-time. let signed_attrs = signed_attrs_raw(vec![ - cms_attribute(OID_CMS_ATTR_CONTENT_TYPE, der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST)), + cms_attribute( + OID_CMS_ATTR_CONTENT_TYPE, + der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST), + ), cms_attribute(OID_CMS_ATTR_MESSAGE_DIGEST, der_octet_string(&digest)), ]); let si = signer_info( @@ -660,7 +733,10 @@ fn signed_attrs_structure_and_presence_are_validated() { vec![si], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::Parse(_))); + assert!(matches!( + err, + SignedObjectDecodeError::Validate(SignedObjectValidateError::SignedAttrsSigningTimeMissing) + )); } #[test] @@ -672,7 +748,14 @@ fn algorithm_identifier_sequence_shape_is_validated() { b"e".to_vec(), None, false, - vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + vec![signer_info( + vec![1], + OID_SHA256, + None, + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + )], ); let ci = content_info_signed_data(sd); let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); @@ -713,8 +796,10 @@ fn ee_certificate_missing_signed_object_sia_is_rejected() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::EeCertificateMissingSia - | SignedObjectDecodeError::EeCertificateMissingSignedObjectSia + SignedObjectDecodeError::Validate(SignedObjectValidateError::EeCertificateMissingSia) + | SignedObjectDecodeError::Validate( + SignedObjectValidateError::EeCertificateMissingSignedObjectSia + ) )); } @@ -751,7 +836,9 @@ fn ee_certificate_sia_without_signed_object_access_method_is_rejected() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::EeCertificateMissingSignedObjectSia + SignedObjectDecodeError::Validate( + SignedObjectValidateError::EeCertificateMissingSignedObjectSia + ) )); } @@ -788,7 +875,9 @@ fn ee_certificate_signed_object_sia_must_be_uri() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::EeCertificateSignedObjectSiaNotUri + SignedObjectDecodeError::Validate( + SignedObjectValidateError::EeCertificateSignedObjectSiaNotUri + ) )); } @@ -825,7 +914,9 @@ fn ee_certificate_signed_object_sia_requires_rsync_uri() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync + SignedObjectDecodeError::Validate( + SignedObjectValidateError::EeCertificateSignedObjectSiaNoRsync + ) )); } @@ -867,7 +958,7 @@ fn signed_attrs_duplicate_content_type_is_rejected() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::DuplicateSignedAttribute(ref oid) + SignedObjectDecodeError::Validate(SignedObjectValidateError::DuplicateSignedAttribute(ref oid)) if oid == OID_CMS_ATTR_CONTENT_TYPE )); } @@ -907,19 +998,26 @@ fn signed_attrs_duplicate_signing_time_is_rejected() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::DuplicateSignedAttribute(ref oid) + SignedObjectDecodeError::Validate(SignedObjectValidateError::DuplicateSignedAttribute(ref oid)) if oid == OID_CMS_ATTR_SIGNING_TIME )); } #[test] fn invalid_content_info_content_type() { - let sd = der_sequence(vec![der_integer_u64(3), der_set(vec![]), der_sequence(vec![der_oid("1.2.3")]) , der_set(vec![])]); + let sd = der_sequence(vec![ + der_integer_u64(3), + der_set(vec![]), + der_sequence(vec![der_oid("1.2.3")]), + der_set(vec![]), + ]); let ci = der_sequence(vec![der_oid("1.2.3.4"), cs_cons(0, &sd)]); let err = RpkiSignedObject::decode_der(&ci).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::InvalidContentInfoContentType(_) + SignedObjectDecodeError::Validate( + SignedObjectValidateError::InvalidContentInfoContentType(_) + ) )); } @@ -932,12 +1030,19 @@ fn invalid_signed_data_version() { b"e".to_vec(), None, false, - vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + vec![signer_info( + vec![1], + OID_SHA256, + None, + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + )], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::InvalidSignedDataVersion(4) + SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidSignedDataVersion(4)) )); } @@ -953,12 +1058,21 @@ fn invalid_digest_algorithms_count() { b"e".to_vec(), None, false, - vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + vec![signer_info( + vec![1], + OID_SHA256, + None, + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + )], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::InvalidDigestAlgorithmsCount(2) + SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidDigestAlgorithmsCount( + 2 + )) )); } @@ -971,12 +1085,19 @@ fn invalid_digest_algorithm_oid() { b"e".to_vec(), None, false, - vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + vec![signer_info( + vec![1], + OID_SHA256, + None, + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + )], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::InvalidDigestAlgorithm(_) + SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidDigestAlgorithm(_)) )); } @@ -991,7 +1112,10 @@ fn econtent_missing() { ]); let so = der_sequence(vec![der_oid(OID_SIGNED_DATA), cs_cons(0, &sd)]); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::EContentMissing)); + assert!(matches!( + err, + SignedObjectDecodeError::Validate(SignedObjectValidateError::EContentMissing) + )); } #[test] @@ -1003,10 +1127,20 @@ fn crls_present_is_rejected() { b"e".to_vec(), None, true, - vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + vec![signer_info( + vec![1], + OID_SHA256, + None, + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + )], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::CrlsPresent)); + assert!(matches!( + err, + SignedObjectDecodeError::Validate(SignedObjectValidateError::CrlsPresent) + )); } #[test] @@ -1018,10 +1152,20 @@ fn certificates_missing_is_rejected() { b"e".to_vec(), None, false, - vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)], + vec![signer_info( + vec![1], + OID_SHA256, + None, + OID_RSA_ENCRYPTION, + Some(der_null()), + false, + )], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::CertificatesMissing)); + assert!(matches!( + err, + SignedObjectDecodeError::Validate(SignedObjectValidateError::CertificatesMissing) + )); } #[test] @@ -1057,7 +1201,7 @@ fn invalid_certificates_count_rejected() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::InvalidCertificatesCount(2) + SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidCertificatesCount(2)) )); } @@ -1091,7 +1235,10 @@ fn ee_certificate_parse_error_is_reported() { vec![si], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::EeCertificateParse(_))); + assert!(matches!( + err, + SignedObjectDecodeError::Validate(SignedObjectValidateError::EeCertificateParse(_)) + )); } #[test] @@ -1133,7 +1280,7 @@ fn invalid_signer_infos_count_rejected() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::InvalidSignerInfosCount(2) + SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidSignerInfosCount(2)) )); } @@ -1162,7 +1309,10 @@ fn signer_info_errors_are_detected() { vec![si_no_attrs], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::SignedAttrsMissing)); + assert!(matches!( + err, + SignedObjectDecodeError::Validate(SignedObjectValidateError::SignedAttrsMissing) + )); // Invalid signer identifier. let signed_attrs = signed_attrs_implicit( @@ -1189,7 +1339,10 @@ fn signer_info_errors_are_detected() { vec![si], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::InvalidSignerIdentifier)); + assert!(matches!( + err, + SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidSignerIdentifier) + )); // Invalid digest algorithm. let signed_attrs = signed_attrs_implicit( @@ -1218,7 +1371,9 @@ fn signer_info_errors_are_detected() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::InvalidSignerInfoDigestAlgorithm(_) + SignedObjectDecodeError::Validate( + SignedObjectValidateError::InvalidSignerInfoDigestAlgorithm(_) + ) )); // Invalid signature algorithm OID. @@ -1248,7 +1403,7 @@ fn signer_info_errors_are_detected() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::InvalidSignatureAlgorithm(_) + SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidSignatureAlgorithm(_)) )); // Invalid signing-time value. @@ -1278,7 +1433,7 @@ fn signer_info_errors_are_detected() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::InvalidSigningTimeValue + SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidSigningTimeValue) )); // signedAttrs has duplicate attribute. @@ -1309,7 +1464,7 @@ fn signer_info_errors_are_detected() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::DuplicateSignedAttribute(_) + SignedObjectDecodeError::Validate(SignedObjectValidateError::DuplicateSignedAttribute(_)) )); // signedAttrs attrValues count != 1. @@ -1346,16 +1501,14 @@ fn signer_info_errors_are_detected() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::InvalidSignedAttributeValuesCount { .. } + SignedObjectDecodeError::Validate( + SignedObjectValidateError::InvalidSignedAttributeValuesCount { .. } + ) )); // content-type mismatch. - let signed_attrs = signed_attrs_implicit( - "1.2.3.4", - &digest, - tlv(0x17, b"240101000000Z"), - vec![], - ); + let signed_attrs = + signed_attrs_implicit("1.2.3.4", &digest, tlv(0x17, b"240101000000Z"), vec![]); let si = signer_info( cert_ski.clone(), OID_SHA256, @@ -1376,7 +1529,9 @@ fn signer_info_errors_are_detected() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::ContentTypeAttrMismatch { .. } + SignedObjectDecodeError::Validate( + SignedObjectValidateError::ContentTypeAttrMismatch { .. } + ) )); // message-digest mismatch. @@ -1404,7 +1559,10 @@ fn signer_info_errors_are_detected() { vec![si], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::MessageDigestMismatch)); + assert!(matches!( + err, + SignedObjectDecodeError::Validate(SignedObjectValidateError::MessageDigestMismatch) + )); // sid_ski mismatch. let signed_attrs = signed_attrs_implicit( @@ -1431,7 +1589,10 @@ fn signer_info_errors_are_detected() { vec![si], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::SidSkiMismatch)); + assert!(matches!( + err, + SignedObjectDecodeError::Validate(SignedObjectValidateError::SidSkiMismatch) + )); // Unsupported signedAttrs attribute. let bad_attr = cms_attribute("1.2.3.4", der_null()); @@ -1461,7 +1622,7 @@ fn signer_info_errors_are_detected() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::UnsupportedSignedAttribute(_) + SignedObjectDecodeError::Validate(SignedObjectValidateError::UnsupportedSignedAttribute(_)) )); // signatureAlgorithm parameters invalid. @@ -1491,7 +1652,9 @@ fn signer_info_errors_are_detected() { let err = RpkiSignedObject::decode_der(&so).unwrap_err(); assert!(matches!( err, - SignedObjectDecodeError::InvalidSignatureAlgorithmParameters + SignedObjectDecodeError::Validate( + SignedObjectValidateError::InvalidSignatureAlgorithmParameters + ) )); // unsignedAttrs present. @@ -1519,5 +1682,8 @@ fn signer_info_errors_are_detected() { vec![si], )); let err = RpkiSignedObject::decode_der(&so).unwrap_err(); - assert!(matches!(err, SignedObjectDecodeError::UnsignedAttrsPresent)); + assert!(matches!( + err, + SignedObjectDecodeError::Validate(SignedObjectValidateError::UnsignedAttrsPresent) + )); } diff --git a/tests/test_signed_object_verify.rs b/tests/test_signed_object_verify.rs index b947d21..77c7468 100644 --- a/tests/test_signed_object_verify.rs +++ b/tests/test_signed_object_verify.rs @@ -53,8 +53,8 @@ fn der_null() -> Vec { } fn der_oid(oid: &str) -> Vec { - use std::str::FromStr; use der_parser::asn1_rs::ToDer; + use std::str::FromStr; let oid = der_parser::Oid::from_str(oid).unwrap(); oid.to_der_vec().unwrap() } @@ -140,10 +140,11 @@ fn verify_rejects_spki_der_with_trailing_bytes() { let mut spki_der = so.signed_data.certificates[0].spki_der.clone(); spki_der.push(0); - let err = so - .verify_signature_with_ee_spki_der(&spki_der) - .unwrap_err(); - assert!(matches!(err, SignedObjectVerifyError::EeSpkiTrailingBytes(1))); + let err = so.verify_signature_with_ee_spki_der(&spki_der).unwrap_err(); + assert!(matches!( + err, + SignedObjectVerifyError::EeSpkiTrailingBytes(1) + )); } #[test] @@ -157,8 +158,6 @@ fn verify_with_all_zero_modulus_exercises_strip_leading_zeros_fallback() { // modulus INTEGER 0, exponent 65537 (valid exponent encoding); signature verification must fail // but the SPKI parsing path should succeed. let spki_der = rsa_spki_der_with_modulus_bytes(&[0x00], 65537); - let err = so - .verify_signature_with_ee_spki_der(&spki_der) - .unwrap_err(); + let err = so.verify_signature_with_ee_spki_der(&spki_der).unwrap_err(); assert!(matches!(err, SignedObjectVerifyError::InvalidSignature)); } diff --git a/tests/test_ta_certificate.rs b/tests/test_ta_certificate.rs index 156147c..6b14cdf 100644 --- a/tests/test_ta_certificate.rs +++ b/tests/test_ta_certificate.rs @@ -1,10 +1,11 @@ use der_parser::num_bigint::BigUint; use rpki::data_model::oid::OID_CP_IPADDR_ASNUMBER; use rpki::data_model::rc::{ - Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange, - IpPrefix, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, + Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, + IpAddressOrRange, IpPrefix, RcExtensions, ResourceCertKind, ResourceCertificate, + RpkixTbsCertificate, }; -use rpki::data_model::ta::{TaCertificate, TaCertificateError}; +use rpki::data_model::ta::{TaCertificate, TaCertificateDecodeError, TaCertificateProfileError}; use time::OffsetDateTime; fn dummy_rc_ca(ext: RcExtensions) -> ResourceCertificate { @@ -56,8 +57,9 @@ fn ta_certificate_rejects_non_self_signed_ca() { .expect("read CA cert fixture"); assert!(matches!( TaCertificate::from_der(&der), - Err(TaCertificateError::NotSelfSignedIssuerSubject) - | Err(TaCertificateError::InvalidSelfSignature(_)) + Err(TaCertificateDecodeError::Validate( + TaCertificateProfileError::NotSelfSignedIssuerSubject + )) )); } @@ -73,7 +75,7 @@ fn ta_constraints_require_policies_and_ski() { }); assert!(matches!( TaCertificate::validate_rc_constraints(&rc), - Err(TaCertificateError::MissingOrInvalidCertificatePolicies) + Err(TaCertificateProfileError::MissingOrInvalidCertificatePolicies) )); let rc = dummy_rc_ca(RcExtensions { @@ -82,7 +84,7 @@ fn ta_constraints_require_policies_and_ski() { }); assert!(matches!( TaCertificate::validate_rc_constraints(&rc), - Err(TaCertificateError::MissingSubjectKeyIdentifier) + Err(TaCertificateProfileError::MissingSubjectKeyIdentifier) )); } @@ -99,7 +101,7 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() { }); assert!(matches!( TaCertificate::validate_rc_constraints(&rc), - Err(TaCertificateError::ResourcesMissing) + Err(TaCertificateProfileError::ResourcesMissing) )); // IP resources present but empty => resources empty. @@ -109,7 +111,7 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() { }); assert!(matches!( TaCertificate::validate_rc_constraints(&rc), - Err(TaCertificateError::ResourcesEmpty) + Err(TaCertificateProfileError::ResourcesEmpty) )); // IP resources inherit is rejected. @@ -124,7 +126,7 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() { }); assert!(matches!( TaCertificate::validate_rc_constraints(&rc), - Err(TaCertificateError::IpResourcesInherit) + Err(TaCertificateProfileError::IpResourcesInherit) )); // AS resources inherit is rejected. @@ -138,7 +140,7 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() { }); assert!(matches!( TaCertificate::validate_rc_constraints(&rc), - Err(TaCertificateError::AsResourcesInherit) + Err(TaCertificateProfileError::AsResourcesInherit) )); // Valid non-empty explicit IP resources => OK. @@ -146,11 +148,13 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() { ip_resources: Some(rpki::data_model::rc::IpResourceSet { families: vec![IpAddressFamily { afi: Afi::Ipv6, - choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix { - afi: Afi::Ipv6, - prefix_len: 32, - addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - })]), + choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( + IpPrefix { + afi: Afi::Ipv6, + prefix_len: 32, + addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }, + )]), }], }), as_resources: None, @@ -162,11 +166,12 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() { let rc = dummy_rc_ca(RcExtensions { ip_resources: None, as_resources: Some(AsResourceSet { - asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)])), + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id( + 64512, + )])), rdi: None, }), ..rc.tbs.extensions.clone() }); TaCertificate::validate_rc_constraints(&rc).expect("valid explicit AS resources"); } - diff --git a/tests/test_tal_decode.rs b/tests/test_tal_decode.rs index b862dc1..306a2df 100644 --- a/tests/test_tal_decode.rs +++ b/tests/test_tal_decode.rs @@ -15,17 +15,31 @@ fn decode_tal_fixtures_smoke() { let raw = std::fs::read(path).expect("read TAL fixture"); let tal = Tal::decode_bytes(&raw).expect("decode TAL fixture"); - assert!(!tal.ta_uris.is_empty(), "TA URI list must be non-empty: {path}"); - assert!(!tal.subject_public_key_info_der.is_empty(), "SPKI DER must be non-empty: {path}"); + assert!( + !tal.ta_uris.is_empty(), + "TA URI list must be non-empty: {path}" + ); + assert!( + !tal.subject_public_key_info_der.is_empty(), + "SPKI DER must be non-empty: {path}" + ); for u in &tal.ta_uris { - assert!(matches!(u.scheme(), "rsync" | "https"), "scheme must be allowed: {u}"); - assert!(!u.path().ends_with('/'), "TA URI must not be a directory: {u}"); + assert!( + matches!(u.scheme(), "rsync" | "https"), + "scheme must be allowed: {u}" + ); + assert!( + !u.path().ends_with('/'), + "TA URI must not be a directory: {u}" + ); } // SPKI DER must be parseable as a DER object (typically a SEQUENCE). let (rem, _obj) = parse_der(&tal.subject_public_key_info_der).expect("parse spki DER"); - assert!(rem.is_empty(), "SPKI DER must not have trailing bytes: {path}"); + assert!( + rem.is_empty(), + "SPKI DER must not have trailing bytes: {path}" + ); } } - diff --git a/tests/test_tal_decode_errors.rs b/tests/test_tal_decode_errors.rs index b898072..9e55dad 100644 --- a/tests/test_tal_decode_errors.rs +++ b/tests/test_tal_decode_errors.rs @@ -1,4 +1,4 @@ -use rpki::data_model::tal::{Tal, TalDecodeError}; +use rpki::data_model::tal::{Tal, TalDecodeError, TalParseError, TalProfileError}; fn mk_tal(uris: &[&str], b64_lines: &[&str]) -> String { let mut out = String::new(); @@ -20,14 +20,19 @@ fn tal_rejects_missing_separator() { let s = "# c\nhttps://example.invalid/ta.cer\nAAAA\n"; assert!(matches!( Tal::decode_bytes(s.as_bytes()), - Err(TalDecodeError::MissingSeparatorEmptyLine) + Err(TalDecodeError::Validate( + TalProfileError::MissingSeparatorEmptyLine + )) )); } #[test] fn tal_rejects_missing_uris() { let s = "# c\n\nAAAA\n"; - assert!(matches!(Tal::decode_bytes(s.as_bytes()), Err(TalDecodeError::MissingTaUris))); + assert!(matches!( + Tal::decode_bytes(s.as_bytes()), + Err(TalDecodeError::Validate(TalProfileError::MissingTaUris)) + )); } #[test] @@ -35,7 +40,9 @@ fn tal_rejects_unsupported_scheme() { let s = mk_tal(&["ftp://example.invalid/ta.cer"], &["AAAA"]); assert!(matches!( Tal::decode_bytes(s.as_bytes()), - Err(TalDecodeError::UnsupportedUriScheme(_)) + Err(TalDecodeError::Validate( + TalProfileError::UnsupportedUriScheme(_) + )) )); } @@ -44,14 +51,19 @@ fn tal_rejects_directory_uri() { let s = mk_tal(&["https://example.invalid/dir/"], &["AAAA"]); assert!(matches!( Tal::decode_bytes(s.as_bytes()), - Err(TalDecodeError::UriIsDirectory(_)) + Err(TalDecodeError::Validate(TalProfileError::UriIsDirectory(_))) )); } #[test] fn tal_rejects_comment_after_header() { let s = "# c\nhttps://example.invalid/ta.cer\n# late\n\nAAAA\n"; - assert!(matches!(Tal::decode_bytes(s.as_bytes()), Err(TalDecodeError::CommentAfterHeader))); + assert!(matches!( + Tal::decode_bytes(s.as_bytes()), + Err(TalDecodeError::Validate( + TalProfileError::CommentAfterHeader + )) + )); } #[test] @@ -59,13 +71,15 @@ fn tal_rejects_invalid_base64() { let s = mk_tal(&["https://example.invalid/ta.cer"], &["not-base64!!!"]); assert!(matches!( Tal::decode_bytes(s.as_bytes()), - Err(TalDecodeError::SpkiBase64Decode) + Err(TalDecodeError::Validate(TalProfileError::SpkiBase64Decode)) )); } #[test] fn tal_rejects_invalid_utf8() { let bytes = [0xFFu8, 0xFEu8]; - assert!(matches!(Tal::decode_bytes(&bytes), Err(TalDecodeError::InvalidUtf8))); + assert!(matches!( + Tal::decode_bytes(&bytes), + Err(TalDecodeError::Parse(TalParseError::InvalidUtf8)) + )); } - diff --git a/tests/test_trust_anchor_bind.rs b/tests/test_trust_anchor_bind.rs index 3079ad8..7adc99b 100644 --- a/tests/test_trust_anchor_bind.rs +++ b/tests/test_trust_anchor_bind.rs @@ -1,18 +1,30 @@ -use rpki::data_model::ta::{TrustAnchor, TrustAnchorError}; +use rpki::data_model::ta::{TrustAnchor, TrustAnchorBindError, TrustAnchorError}; use rpki::data_model::tal::Tal; use url::Url; #[test] fn bind_trust_anchor_with_downloaded_fixtures_succeeds() { let cases = [ - ("tests/fixtures/tal/afrinic.tal", "tests/fixtures/ta/afrinic-ta.cer"), + ( + "tests/fixtures/tal/afrinic.tal", + "tests/fixtures/ta/afrinic-ta.cer", + ), ( "tests/fixtures/tal/apnic-rfc7730-https.tal", "tests/fixtures/ta/apnic-ta.cer", ), - ("tests/fixtures/tal/arin.tal", "tests/fixtures/ta/arin-ta.cer"), - ("tests/fixtures/tal/lacnic.tal", "tests/fixtures/ta/lacnic-ta.cer"), - ("tests/fixtures/tal/ripe-ncc.tal", "tests/fixtures/ta/ripe-ncc-ta.cer"), + ( + "tests/fixtures/tal/arin.tal", + "tests/fixtures/ta/arin-ta.cer", + ), + ( + "tests/fixtures/tal/lacnic.tal", + "tests/fixtures/ta/lacnic-ta.cer", + ), + ( + "tests/fixtures/tal/ripe-ncc.tal", + "tests/fixtures/ta/ripe-ncc-ta.cer", + ), ]; for (tal_path, ta_path) in cases { @@ -20,7 +32,7 @@ fn bind_trust_anchor_with_downloaded_fixtures_succeeds() { let tal = Tal::decode_bytes(&tal_raw).expect("decode TAL fixture"); let ta_der = std::fs::read(ta_path).expect("read TA fixture"); - TrustAnchor::bind(tal.clone(), &ta_der, None).expect("bind without resolved uri"); + TrustAnchor::bind_der(tal.clone(), &ta_der, None).expect("bind without resolved uri"); // Also exercise the resolved-uri-in-TAL check using one URI from the TAL list. let resolved = tal @@ -30,7 +42,7 @@ fn bind_trust_anchor_with_downloaded_fixtures_succeeds() { .or_else(|| tal.ta_uris.first()) .expect("tal has ta uris"); let resolved = resolved.clone(); - TrustAnchor::bind(tal, &ta_der, Some(&resolved)).expect("bind with resolved uri"); + TrustAnchor::bind_der(tal, &ta_der, Some(&resolved)).expect("bind with resolved uri"); } } @@ -43,8 +55,10 @@ fn bind_rejects_spki_mismatch() { // Flip a byte in TAL SPKI to force mismatch. tal.subject_public_key_info_der[0] ^= 0x01; assert!(matches!( - TrustAnchor::bind(tal, &ta_der, None), - Err(TrustAnchorError::TalSpkiMismatch) + TrustAnchor::bind_der(tal, &ta_der, None), + Err(TrustAnchorError::Bind( + TrustAnchorBindError::TalSpkiMismatch + )) )); } @@ -56,7 +70,9 @@ fn bind_rejects_resolved_uri_not_listed_in_tal() { let bad = Url::parse("https://example.invalid/not-in-tal.cer").unwrap(); assert!(matches!( - TrustAnchor::bind(tal, &ta_der, Some(&bad)), - Err(TrustAnchorError::ResolvedUriNotInTal(_)) + TrustAnchor::bind_der(tal, &ta_der, Some(&bad)), + Err(TrustAnchorError::Bind( + TrustAnchorBindError::ResolvedUriNotInTal(_) + )) )); }