diff --git a/.gitignore b/.gitignore index 7aad1878b..d7201f9d2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ # Noir and prover artifacts *.json +# Allow JSON files in csca_registry +!**/csca_registry/**/*.json *.gz *.bin *.nps diff --git a/playground/passport-input-gen/.cargo/katex-header.html b/playground/passport-input-gen/.cargo/katex-header.html new file mode 100644 index 000000000..0e76edd65 --- /dev/null +++ b/playground/passport-input-gen/.cargo/katex-header.html @@ -0,0 +1 @@ + diff --git a/playground/passport-input-gen/Cargo.toml b/playground/passport-input-gen/Cargo.toml index 4943e2b51..0a2487b32 100644 --- a/playground/passport-input-gen/Cargo.toml +++ b/playground/passport-input-gen/Cargo.toml @@ -7,7 +7,18 @@ description = "Passport input generator" [dependencies] rsa = { version = "0.9.8", features = ["sha2"] } sha2 = { version = "0.10", features = ["compress"] } +x509-parser = "0.16" +base64 = "0.22" +hex = "0.4" +rasn = "0.15" +rasn-pkix = "0.15" +rasn-cms = "0.15" +chrono = { version = "0.4", features = ["serde"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.143" +toml = "0.8" +noir-bignum-paramgen = "0.1.5" +thiserror = "2.0.16" +signature = "2.2" +lazy_static = "1.5.0" -[[bin]] -name = "passport-input-generator" -path = "src/main.rs" diff --git a/playground/passport-input-gen/README.md b/playground/passport-input-gen/README.md new file mode 100644 index 000000000..da843e60b --- /dev/null +++ b/playground/passport-input-gen/README.md @@ -0,0 +1,110 @@ +# Passport Input Generator + +A Rust crate for parsing passport data and generating circuit inputs for Noir Circuits. + +## Overview + +This crate provides functionality to: + +- Parse passport Machine Readable Zone (MRZ) data from DG1 and SOD +- Validate passport signatures using DSC and CSCA certificates +- Generate mock passport data for testing +- Convert passport data to circuit inputs for Noir zero-knowledge circuits + +### `PassportReader` + +Main structure for reading and validating passport data. + +**Structure:** + +```rust +pub struct PassportReader { + dg1: Binary, // DG1 (Machine Readable Zone) data + sod: SOD, // Security Object Document + mockdata: bool, // Flag indicating mock vs real passport data + csca_pubkey: Option, // Optional CSCA public key for mock data +} +``` + +**Key Behavior:** + +- When `mockdata: false`: The reader searches for existing CSCA keys from a predefined set. Currently supports USA CSCA keys loaded from the system. The `validate()` method iterates through all available USA CSCA keys to find one that successfully validates the passport signature. + +- When `mockdata: true`: The reader uses the provided `csca_pubkey` for validation. This is useful for testing with synthetic passport data generated using mock keys. + +**Methods:** + +- `validate() -> Result` - Validates the passport signatures and returns the CSCA key index used. For mock data, always returns index 0. For real data, returns the index of the USA CSCA key that successfully validated the passport. +- `to_circuit_inputs(current_date: u64, min_age_required: u8, max_age_required: u8, csca_key_index: usize) -> Result` - Converts passport data to circuit inputs + +#### `CircuitInputs` + +Contains all necessary inputs for Noir circuits. + +**Methods:** + +- `to_toml_string() -> String` - Converts circuit inputs to TOML format string +- `save_to_toml_file>(path: P) -> std::io::Result<()>` - Saves circuit inputs to a TOML file + +### Mock Data Generation + +#### `mock_generator` module + +**Functions:** + +- `dg1_bytes_with_birthdate_expiry_date(birthdate: &[u8; 6], expiry: &[u8; 6]) -> Vec` - Generates fake DG1 data with specified birth and expiry dates (format: YYMMDD) +- `generate_fake_sod(dg1: &[u8], dsc_priv: &RsaPrivateKey, dsc_pub: &RsaPublicKey, csca_priv: &RsaPrivateKey, _csca_pub: &RsaPublicKey) -> SOD` - Creates a synthetic SOD structure for testing + +#### `mock_keys` module + +**Constants:** + +- `MOCK_CSCA_PRIV_KEY_B64: &str` - Base64-encoded mock CSCA private key for testing +- `MOCK_DSC_PRIV_KEY_B64: &str` - Base64-encoded mock DSC private key for testing + +## Usage Example + +```rust +use passport_input_gen::{PassportReader, mock_generator, mock_keys}; +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use rsa::{RsaPrivateKey, pkcs8::DecodePrivateKey}; + +// Load mock keys +let csca_der = STANDARD.decode(mock_keys::MOCK_CSCA_PRIV_KEY_B64)?; +let csca_priv = RsaPrivateKey::from_pkcs8_der(&csca_der)?; +let csca_pub = csca_priv.to_public_key(); + +let dsc_der = STANDARD.decode(mock_keys::MOCK_DSC_PRIV_KEY_B64)?; +let dsc_priv = RsaPrivateKey::from_pkcs8_der(&dsc_der)?; +let dsc_pub = dsc_priv.to_public_key(); + +// Generate mock passport data +let dg1 = mock_generator::dg1_bytes_with_birthdate_expiry_date(b"900101", b"300101"); +let sod = mock_generator::generate_fake_sod(&dg1, &dsc_priv, &dsc_pub, &csca_priv, &csca_pub); + +// Create passport reader +let reader = PassportReader { + dg1: Binary::from_slice(&dg1), + sod, + mockdata: true, + csca_pubkey: Some(csca_pub), +}; + +// Validate passport +let csca_index = reader.validate()?; + +// Generate circuit inputs +let current_timestamp = chrono::Utc::now().timestamp() as u64; +let inputs = reader.to_circuit_inputs(current_timestamp, 18, 70, csca_index)?; + +// Export to TOML +inputs.save_to_toml_file("circuit_inputs.toml")?; +``` + +## Testing + +The crate includes tests for mock data generation and validation. Run tests with: + +```bash +cargo test +``` diff --git a/playground/passport-input-gen/csca_registry/csca_public_key.json b/playground/passport-input-gen/csca_registry/csca_public_key.json new file mode 100644 index 000000000..0e46cfbd7 --- /dev/null +++ b/playground/passport-input-gen/csca_registry/csca_public_key.json @@ -0,0 +1,52 @@ +{ + "USA": [ + { + "filename": "cert-00267-pubkey.pem", + "public_key": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAw4VGHnr3jsI//bv4fKBmGYLcrdcABkQNmVr8xkmlS7xxkDEQCohnzKSk8v0T+L0yp3h+WctO1ZwsaJfU0HDdMogDmEv3SeoNImOuqiofzkDLdH6/IWA/JfEwBYDz+1cn7/rbHuFCwo/dkPNJkeE7OrLGd9g6tsj8YibMZrB8N7nIVgEa9QOEiqXmUUV4KNKKRZJLu/RaWzZGNq+JqlXgANKvSwKWVW+aINb84UfkBIdbPfIkEq6JMdok+YIQyHemEP6nxnHAVQwOYRkgHidw9YHXZZkHgh75efixMoAy3LQxvoknECrlpZgxqIpdup+AwsEW8hoJZbpd3Dxeb27AAF3Rl9LBGPi1thd1A4Qb/AjVDtIyQaU62cHo7fEGcW5kVg/BLGhKH+RJKOqEZoG8b80X0jv/Z2VNNtDU/8flcKG9DkD+d+kgjMQAyPCOjC2achDcK5Zc1Q8/tq0T25x6GoRjbxadqR6MKrAYaP8KOLCrL0MJsjvcfD3M+hdyJXJPN/7TeCyKbK+dBvVCk9BePi6lBKmp6lEHjyyydgNBBzZPrTNf5l60Oh9eWpaIoTHcmToS6zKVffFb/dWnkCTF1BBuy1VgJNeIspn8n1PhY5Q23qvV8q+Q1Ta1X3TFGkvAb1SXGiPQW4Hd2JrFJEBo8Ah8nn8rQKX999PLI+An1acCAwEAAQ==", + "subject": "C=US, O=U.S. Government, OU=Department of State, OU=MRTD, OU=Certification Authorities, OU=U.S. Department of State MRTD CA", + "notBefore": "2014-12-18T16:21:01Z", + "notAfter": "2035-07-18T16:51:01Z", + "serial": "4E322929" + }, + { + "filename": "cert-00444-pubkey.pem", + "public_key": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4yGdJKQ7D4DCDPGseAdOMC6Dgj8Xa91oXgHgPtgLW7y3kGm2Z4u9yXLciMebRCoq55wfO4ZcGv+mnMHpkv7fnFWSnG+8pP1BRUEKCYhXAbeWSww+CpMFEagzP7miPZg1TsE4QKFhGyRAfMdJFuG1aGQTtEdjgAWkat7YovTSA0pC7V/PwMlagZZwTsz8OOiO0w0eS9uIHGJKylg8gi9RSQZ/Hm2r4oOYvx3G3Iqa4OlNXO/KKD9mwd19+BFjuJ1cdqfzcYejhMPothyvlUgbWeaYek3ZfXuqz9XjYBF3rnacVKE9w4ydncRpUpOrSV/qSwxZA46QBuKAGxCS3kOUjgEmAj74/dLI8IaqHhYfSwchQdiT7FhQQ+YZENkRI5Gm4QDVLrT01plMK5ytX7mo4F83Hb1uE+WGgjTk9VDEKfZsERscCiArR7oUoRWZyuH81aMScMVzDVpFxvOg6Md7rWk5k8hRwL75nZtkFilsNnIVGMtuQP7DSG0xcKJ3EZT69YsRH0oEAa4sfa5fm4/X/WVfkNl51tI71PmYvnzcO6UVt42pgGiAJGoy5YmdhKKmXBgj0aVmzFRxd/+Jx2SgyccLeiIIA6y5bn6av/KuUB2wi9NLPbFz0OxvdKYqtfqNL+E6WmpeHHiiGGCrUes15d7fs/65QCvamV7fFBn8+lcCAwEAAQ==", + "subject": "C=US, O=U.S. Government, OU=Department of State, OU=MRTD, OU=Certification Authorities, OU=U.S. Department of State MRTD CA", + "notBefore": "2024-09-30T16:38:20Z", + "notAfter": "2045-03-30T17:08:20Z", + "serial": "5DCE72E1" + }, + { + "filename": "cert-00443-pubkey.pem", + "public_key": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAozIONhYqXeLWTi2rhDm+X2ZX1OMUr/j7DZgc6zXZ7VzsYVB+6ARiIbok77SxPtk9ZeUIqujjW59Jyb+EUiI6giIZOu9zY7BA+NQfhZeywzr1XKXhfROd5mmUiv3Nz5ukMBFwBoCaOHUchoqwml35lCk+X7K3NOS++HWbnxvRQFqOItsTcaMg2jp9BF+vmHr0nbucsi4zA3BzUKWsrbvcV05M4H/yoyru/5uq8JktSxbxCzK3iKXVyi6SwRkOiJekbztVKouELQlVhlhGacUOQ8ub+PTa4gRWKVHth68YmYW5k12/2B52CrzAndxCGewfN5KvDy5woQEDXHcJ+O9s53liYjZFNU5ceL+R48jUoXLVPuCmXFQKqo1N/t++lWR6icWy8tllHZGsr1GDVX0+EY+0gJUULR+SPzsAqt5IgCq0BlQ/iz4iLUS8fAoHs46AU3u/yHNRapBu3XOqKwCmQH2HNUufUZyD56cVaIBzE0ILWKwY7HaEcP28y0D/++OpqPz44c8YhwLcSdoIYJIOb3FzBVsq+1B23VeqTZ8IxpDkI4wumBLwJS+eJ136XBUegL1hQQv5BRrcN/XgJ9wyC43QN+d8LfBWUZQody7kKXC0KtratgHwwazCdxl8PR3p0aeXbveUBbJmyRQS27jDykncrYUrzkG8WRWhrW0tUR0CAwEAAQ==", + "subject": "C=US, O=U.S. Government, OU=Department of State, OU=MRTD, OU=Certification Authorities, OU=U.S. Department of State MRTD CA", + "notBefore": "2019-11-14T16:37:12Z", + "notAfter": "2040-05-14T17:07:12Z", + "serial": "4E32D006" + }, + { + "filename": "cert-00265-pubkey.pem", + "public_key": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAw9Pm+vLMuE/h005EKy6NVXhI+afR9iaURn6ENVOrAy+QBNvol4vNRCCqEcHwrc21e8KmRoFqoN7a1/YidoZKIsBG/8HkitFqPX76FudVcY1npqzRFMlv+3kf5YANrhbzCqVlo1UCGXOfRJwZUKgJQ9Wb++OMSMmzSJwgXQP9bxRURHRlGl833CCudrdHajKfEF0f/f5WNx4/s2/DbfBaW+FK9LAeXIqjFkS8xDGv1La785gohpIDAk6dbKMhVWWT7YwPm3T5ox2E59IIN65qJzmKeKKdhaZdr7ZzrxhYVxVjce6KFTlcrN1fqtAnAjeETEEEnfIgw4tlV827Pm1S9dEZnHzkxWmusRo1UNeQw7pdO/sAuy34uVRA9CBhdETxTvRMpBrnzM1G5mB7H7ltsS0T2Gp8QiRxaXR9CnpJAI3Bil6lJJ/fKJwVyzr7xR9c2ws4DIJ/uh0Jgy8wkeVOie0ava6KEI7PCz0N2GNz0Jd2sAXUBDsfs/nNKw7K2k5HLJ/5bJz90d8TwoE6j5xaB2ObOhCwBFyTb0CsLrWnaIu/ZyJr0P/NqWYaZXhv4oIeousUHEl4qpvn7K9hVNKT1c7qAKH3ZEbLEnxV5DYh5qsRwImt4M9B5LJsFojJ28TqOE1DB+4Dqg9mhQ+CaX1s1Urw7PqOqA8u/euk0YxD86kCAwEAAQ==", + "subject": "C=US, O=U.S. Government, OU=Department of State, OU=MRTD, OU=Certification Authorities, OU=U.S. Department of State MRTD CA", + "notBefore": "2004-11-19T20:57:05Z", + "notAfter": "2025-06-19T21:27:05Z", + "serial": "419E6523" + }, + { + "filename": "cert-00266-pubkey.pem", + "public_key": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvbwV+zl/vBzLWAadrtTSpC9l/zOOzAQKbwhVXCGPKK0SfOjiqIJSVfboQ+Tvz67Fgxb8LDhUJzoUSxHjh7PvO8NYwGXMhQrw0JzAARjPqbGUST3qXO/DfEwAilFKx1NvmlrxZgAS64iIqUjl7IGwYApALE1Ok5jkEvYDTE16uCe5RQz0vuqaKcgirrhwIW6C4r2wx9G1xr7/piII7fo97D7h5y0206OwshClHAmQ0p4LSK+Nxexp6sWaDsb/E7jKjLcxkcKJLsayGF58edW4fnI92BuI3pAhPSJpMmCImdN8LvF32o0jnYmYPsRFlLsj2+UEAnH13bs1qS07vvRnx3CXmPYTGl6amlbmePaMDgV4qWM8e71ddVgj1jyZANgzx4fDT9B82A8+Iw13ZOpU/rBW9OZ3Rk2aSOmFC69Vq12cbYrfXobc3GXyQ/hb4yMi3tfNS2nqeesb2a5/GrxKhwOIiNrJcClnzTlqrdcjEHdZYCXT4kR6wpbGAQNruew/BWIuWrGugXPB8ah8f8ttLetGO55kMaPDbk+ZXY6DlDjDLznurF0bEzZiNcRmUQ8p2rpToRqmH/XntE0OgasGzBmcJX5oMNC+7zvELBJcc9YzMIgXX7R5VlI5/Gvnnn4x6lmiio5FWb3ksMc1MWYTN2bmk5tyyKxvQ6Z4rZdx+1UCAwEAAQ==", + "subject": "C=US, O=U.S. Government, OU=Department of State, OU=MRTD, OU=Certification Authorities, OU=U.S. Department of State MRTD CA", + "notBefore": "2010-01-08T16:06:27Z", + "notAfter": "2030-08-08T16:36:27Z", + "serial": "45DE28DD" + }, + { + "filename": "cert-00456-pubkey.pem", + "public_key": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAozIONhYqXeLWTi2rhDm+X2ZX1OMUr/j7DZgc6zXZ7VzsYVB+6ARiIbok77SxPtk9ZeUIqujjW59Jyb+EUiI6giIZOu9zY7BA+NQfhZeywzr1XKXhfROd5mmUiv3Nz5ukMBFwBoCaOHUchoqwml35lCk+X7K3NOS++HWbnxvRQFqOItsTcaMg2jp9BF+vmHr0nbucsi4zA3BzUKWsrbvcV05M4H/yoyru/5uq8JktSxbxCzK3iKXVyi6SwRkOiJekbztVKouELQlVhlhGacUOQ8ub+PTa4gRWKVHth68YmYW5k12/2B52CrzAndxCGewfN5KvDy5woQEDXHcJ+O9s53liYjZFNU5ceL+R48jUoXLVPuCmXFQKqo1N/t++lWR6icWy8tllHZGsr1GDVX0+EY+0gJUULR+SPzsAqt5IgCq0BlQ/iz4iLUS8fAoHs46AU3u/yHNRapBu3XOqKwCmQH2HNUufUZyD56cVaIBzE0ILWKwY7HaEcP28y0D/++OpqPz44c8YhwLcSdoIYJIOb3FzBVsq+1B23VeqTZ8IxpDkI4wumBLwJS+eJ136XBUegL1hQQv5BRrcN/XgJ9wyC43QN+d8LfBWUZQody7kKXC0KtratgHwwazCdxl8PR3p0aeXbveUBbJmyRQS27jDykncrYUrzkG8WRWhrW0tUR0CAwEAAQ==", + "subject": "C=US, O=U.S. Government, OU=Department of State, OU=MRTD, OU=Certification Authorities, OU=U.S. Department of State MRTD CA", + "notBefore": "2019-11-14T16:37:12Z", + "notAfter": "2040-05-14T17:07:12Z", + "serial": "4E32D03F" + } + ] +} diff --git a/playground/passport-input-gen/src/constants.rs b/playground/passport-input-gen/src/constants.rs deleted file mode 100644 index cc0b158e4..000000000 --- a/playground/passport-input-gen/src/constants.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! Part I: RSA params for the DSC_PUBKEY over SIGNED_ATTRIBUTES. -//! -//! Part II: RSA params + signature for the CSC_PUBKEY over DSC_CERT, which -//! contains within itself the DSC_PUBKEY (so we can check the country's -//! signature over a certificate containing the DSC). -pub const PASSPORT_SOD_SIZE: u64 = 64; -pub const SOD_CERT_SIZE: u64 = 32; - -// This is `n` -pub const DSC_RSA_PUBKEY_BYTES: [u8; 256] = [ - 192, 58, 60, 17, 52, 201, 97, 44, 238, 196, 29, 165, 93, 10, 196, 187, 214, 161, 26, 122, 165, - 122, 254, 7, 67, 159, 174, 26, 110, 70, 185, 63, 31, 134, 41, 31, 238, 180, 16, 42, 200, 115, - 160, 146, 83, 47, 130, 116, 92, 52, 44, 197, 1, 57, 228, 237, 218, 87, 123, 25, 76, 29, 50, 53, - 151, 38, 233, 181, 111, 232, 168, 55, 11, 40, 134, 69, 137, 216, 37, 192, 127, 33, 80, 163, 17, - 121, 60, 188, 97, 216, 13, 202, 217, 99, 56, 52, 41, 100, 107, 233, 243, 147, 209, 218, 30, 20, - 6, 201, 68, 27, 218, 225, 191, 241, 182, 58, 55, 90, 170, 250, 98, 198, 245, 45, 168, 206, 201, - 88, 42, 100, 207, 204, 125, 181, 43, 62, 94, 192, 217, 10, 97, 45, 37, 131, 190, 14, 248, 143, - 142, 249, 226, 134, 89, 10, 102, 160, 238, 165, 27, 78, 135, 167, 195, 214, 213, 123, 129, 4, - 27, 6, 115, 246, 101, 143, 141, 132, 221, 30, 25, 221, 162, 153, 175, 187, 26, 24, 8, 233, 54, - 83, 178, 115, 197, 140, 94, 141, 195, 161, 36, 170, 10, 243, 166, 76, 32, 0, 90, 83, 181, 242, - 91, 49, 198, 224, 65, 116, 205, 224, 136, 201, 106, 207, 149, 236, 121, 248, 162, 49, 60, 124, - 20, 21, 234, 157, 162, 22, 158, 167, 6, 149, 147, 64, 209, 233, 127, 54, 108, 141, 45, 183, 68, - 82, 150, 52, 10, 17, -]; - -// Primes the private key derivation -pub const DSC_P_BYTES: [u8; 128] = [ - 203, 233, 97, 37, 77, 135, 16, 25, 120, 207, 98, 216, 190, 7, 84, 1, 90, 53, 227, 194, 107, - 102, 54, 193, 43, 241, 68, 223, 190, 228, 205, 200, 47, 233, 196, 152, 188, 138, 24, 130, 131, - 158, 236, 107, 196, 232, 169, 137, 47, 123, 144, 84, 131, 190, 130, 40, 207, 136, 92, 234, 173, - 173, 247, 170, 12, 177, 98, 32, 151, 252, 128, 208, 24, 29, 133, 249, 101, 194, 234, 169, 195, - 216, 255, 254, 185, 77, 42, 42, 62, 36, 80, 88, 211, 68, 223, 102, 16, 28, 52, 144, 115, 193, - 172, 152, 221, 227, 214, 79, 33, 70, 44, 113, 17, 102, 122, 204, 137, 63, 100, 111, 117, 58, - 166, 224, 183, 161, 221, 29, -]; -pub const DSC_Q_BYTES: [u8; 128] = [ - 241, 84, 199, 169, 58, 250, 210, 199, 72, 176, 179, 105, 87, 142, 247, 166, 243, 99, 103, 200, - 173, 138, 94, 163, 240, 243, 61, 28, 13, 173, 159, 168, 226, 173, 107, 242, 149, 120, 233, 151, - 144, 113, 167, 170, 110, 123, 192, 11, 231, 54, 234, 153, 80, 188, 13, 108, 104, 88, 162, 51, - 74, 41, 168, 164, 83, 45, 225, 202, 119, 52, 181, 189, 43, 248, 181, 123, 163, 81, 43, 10, 172, - 232, 147, 243, 173, 103, 140, 189, 181, 143, 106, 131, 45, 12, 231, 208, 109, 14, 254, 178, - 160, 125, 120, 60, 195, 88, 253, 136, 32, 229, 107, 227, 142, 129, 111, 145, 109, 230, 112, - 154, 164, 108, 83, 102, 24, 118, 178, 133, -]; - -// This is for Barrett reduction. Note that the Noir version is actually -// just \lfloor \frac{b^{2k + 4}}{n} \rfloor rather than the usual -// \lfloor \frac{b^{2k}}{n} \rfloor. -pub const DSC_MU_BYTES: [u8; 257] = [ - 21, 78, 222, 215, 117, 196, 81, 199, 11, 168, 117, 238, 216, 140, 5, 35, 218, 135, 157, 202, - 207, 56, 100, 226, 86, 120, 215, 191, 30, 183, 145, 218, 114, 2, 89, 219, 84, 45, 28, 28, 30, - 62, 88, 92, 62, 73, 167, 47, 107, 93, 129, 152, 75, 41, 115, 134, 126, 66, 254, 51, 254, 179, - 58, 144, 234, 38, 251, 39, 24, 167, 111, 52, 72, 9, 54, 27, 136, 238, 212, 55, 104, 129, 220, - 83, 96, 199, 122, 33, 103, 225, 193, 240, 26, 157, 132, 46, 85, 151, 214, 253, 113, 14, 243, 5, - 45, 177, 61, 7, 8, 247, 9, 189, 16, 13, 220, 174, 181, 196, 47, 139, 112, 251, 200, 153, 50, - 145, 249, 39, 159, 143, 81, 146, 200, 234, 100, 73, 185, 243, 131, 124, 219, 217, 239, 153, 88, - 101, 158, 11, 77, 217, 200, 198, 202, 34, 24, 186, 204, 251, 64, 167, 121, 220, 102, 230, 36, - 117, 111, 192, 138, 146, 128, 95, 187, 236, 133, 172, 202, 254, 111, 199, 60, 224, 110, 5, 171, - 107, 124, 92, 141, 48, 58, 178, 229, 72, 80, 137, 35, 72, 134, 172, 28, 41, 247, 241, 207, 137, - 81, 118, 216, 208, 223, 90, 233, 150, 189, 89, 144, 238, 114, 151, 166, 225, 191, 188, 232, 24, - 154, 60, 176, 116, 140, 245, 115, 251, 123, 41, 161, 98, 140, 108, 41, 8, 214, 215, 26, 127, - 235, 150, 200, 158, 218, 103, 110, 157, 148, 122, 52, -]; - -// ------------------------- DSC_CERT stuff ------------------------- - -// This is the actual message to be signed -pub const DSC_CERT: [u8; 700] = [ - 192, 58, 60, 17, 52, 201, 97, 44, 238, 196, 29, 165, 93, 10, 196, 187, 214, 161, 26, 122, 165, - 122, 254, 7, 67, 159, 174, 26, 110, 70, 185, 63, 31, 134, 41, 31, 238, 180, 16, 42, 200, 115, - 160, 146, 83, 47, 130, 116, 92, 52, 44, 197, 1, 57, 228, 237, 218, 87, 123, 25, 76, 29, 50, 53, - 151, 38, 233, 181, 111, 232, 168, 55, 11, 40, 134, 69, 137, 216, 37, 192, 127, 33, 80, 163, 17, - 121, 60, 188, 97, 216, 13, 202, 217, 99, 56, 52, 41, 100, 107, 233, 243, 147, 209, 218, 30, 20, - 6, 201, 68, 27, 218, 225, 191, 241, 182, 58, 55, 90, 170, 250, 98, 198, 245, 45, 168, 206, 201, - 88, 42, 100, 207, 204, 125, 181, 43, 62, 94, 192, 217, 10, 97, 45, 37, 131, 190, 14, 248, 143, - 142, 249, 226, 134, 89, 10, 102, 160, 238, 165, 27, 78, 135, 167, 195, 214, 213, 123, 129, 4, - 27, 6, 115, 246, 101, 143, 141, 132, 221, 30, 25, 221, 162, 153, 175, 187, 26, 24, 8, 233, 54, - 83, 178, 115, 197, 140, 94, 141, 195, 161, 36, 170, 10, 243, 166, 76, 32, 0, 90, 83, 181, 242, - 91, 49, 198, 224, 65, 116, 205, 224, 136, 201, 106, 207, 149, 236, 121, 248, 162, 49, 60, 124, - 20, 21, 234, 157, 162, 22, 158, 167, 6, 149, 147, 64, 209, 233, 127, 54, 108, 141, 45, 183, 68, - 82, 150, 52, 10, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, -]; - -pub const CSC_PUBKEY: [u8; 256] = [ - 224, 180, 214, 135, 38, 18, 4, 58, 73, 171, 42, 173, 59, 213, 43, 83, 124, 174, 52, 186, 188, - 60, 225, 178, 33, 200, 177, 8, 57, 122, 114, 73, 57, 208, 142, 76, 52, 22, 58, 9, 77, 198, 29, - 29, 116, 12, 36, 135, 204, 95, 83, 108, 59, 139, 64, 196, 7, 189, 104, 122, 199, 149, 11, 212, - 11, 61, 71, 127, 243, 136, 11, 1, 214, 176, 197, 149, 165, 239, 24, 87, 122, 92, 31, 191, 182, - 114, 54, 202, 73, 169, 16, 252, 79, 35, 30, 59, 207, 11, 180, 40, 225, 98, 131, 35, 178, 75, - 159, 128, 37, 124, 15, 161, 80, 175, 110, 13, 248, 210, 84, 242, 28, 90, 22, 150, 114, 173, - 114, 139, 210, 146, 136, 146, 121, 91, 58, 105, 159, 194, 40, 206, 64, 104, 38, 23, 34, 31, 83, - 60, 94, 60, 161, 117, 255, 43, 171, 140, 13, 108, 210, 52, 39, 157, 209, 127, 153, 65, 245, 85, - 56, 115, 145, 130, 208, 219, 88, 32, 16, 173, 152, 177, 92, 10, 203, 23, 78, 64, 164, 139, 113, - 161, 7, 202, 225, 21, 59, 95, 211, 121, 193, 29, 81, 231, 94, 34, 89, 243, 173, 252, 199, 243, - 16, 51, 246, 4, 177, 183, 29, 234, 175, 230, 38, 38, 166, 232, 54, 34, 34, 218, 123, 71, 202, - 126, 209, 110, 122, 96, 204, 190, 45, 157, 209, 77, 102, 140, 209, 0, 154, 174, 131, 230, 175, - 201, 26, 186, 195, 97, -]; -pub const CSC_PUBKEY_MU: [u8; 257] = [ - 18, 58, 109, 78, 125, 158, 62, 131, 144, 126, 13, 128, 223, 13, 125, 210, 50, 147, 112, 59, - 155, 196, 101, 157, 158, 220, 16, 214, 247, 182, 66, 3, 212, 210, 119, 137, 109, 134, 171, 192, - 63, 233, 217, 62, 0, 155, 21, 86, 17, 117, 45, 238, 46, 93, 89, 197, 249, 63, 184, 217, 51, - 147, 90, 2, 29, 46, 145, 4, 107, 58, 139, 166, 135, 86, 161, 109, 170, 130, 243, 112, 152, 59, - 29, 28, 187, 156, 206, 95, 82, 222, 13, 127, 186, 196, 52, 31, 51, 209, 250, 85, 242, 55, 229, - 100, 173, 135, 180, 166, 247, 234, 23, 175, 211, 166, 206, 99, 235, 89, 106, 37, 104, 70, 67, - 69, 73, 14, 254, 250, 34, 90, 214, 119, 48, 96, 42, 36, 135, 117, 34, 120, 171, 106, 203, 109, - 186, 213, 12, 160, 24, 255, 44, 247, 247, 139, 31, 183, 202, 234, 97, 18, 67, 146, 42, 255, - 193, 1, 199, 102, 34, 126, 150, 138, 27, 128, 143, 210, 242, 195, 177, 4, 121, 134, 109, 245, - 63, 165, 107, 65, 220, 238, 108, 1, 195, 10, 163, 181, 77, 41, 24, 235, 88, 131, 39, 157, 24, - 232, 155, 185, 252, 51, 238, 95, 20, 207, 11, 91, 50, 36, 16, 13, 20, 174, 174, 248, 179, 224, - 219, 242, 63, 90, 229, 4, 234, 237, 237, 208, 33, 173, 29, 22, 243, 162, 101, 75, 158, 211, 78, - 53, 22, 140, 251, 20, 100, 27, 33, 199, 91, -]; -pub const CSC_P_BYTES: [u8; 128] = [ - 252, 167, 178, 185, 203, 240, 127, 45, 79, 211, 213, 150, 243, 41, 153, 7, 205, 201, 73, 137, - 124, 75, 62, 193, 145, 198, 198, 143, 37, 138, 56, 170, 237, 125, 140, 0, 162, 99, 53, 103, 15, - 64, 245, 62, 201, 126, 160, 212, 191, 58, 153, 203, 66, 213, 175, 247, 182, 24, 1, 205, 162, - 180, 80, 117, 54, 19, 76, 230, 57, 43, 69, 23, 133, 221, 22, 107, 185, 246, 35, 153, 72, 152, - 223, 203, 239, 129, 187, 107, 23, 2, 7, 186, 54, 62, 38, 115, 98, 243, 166, 199, 74, 136, 228, - 41, 31, 26, 21, 214, 102, 119, 56, 34, 143, 81, 246, 225, 133, 198, 75, 143, 96, 74, 243, 1, - 219, 44, 231, 249, -]; -pub const CSC_Q_BYTES: [u8; 128] = [ - 227, 174, 106, 117, 3, 88, 186, 84, 135, 35, 168, 66, 131, 201, 107, 22, 2, 3, 57, 63, 49, 28, - 144, 26, 41, 46, 37, 128, 122, 37, 255, 187, 74, 231, 128, 233, 133, 91, 223, 48, 63, 221, 120, - 90, 212, 34, 245, 232, 148, 251, 108, 183, 49, 166, 127, 91, 145, 195, 31, 97, 251, 237, 77, - 135, 145, 20, 140, 60, 70, 137, 231, 104, 157, 19, 101, 31, 61, 65, 110, 76, 18, 170, 31, 15, - 210, 146, 190, 178, 214, 228, 241, 239, 232, 230, 89, 125, 72, 79, 249, 201, 15, 3, 195, 61, - 178, 178, 19, 149, 23, 20, 208, 255, 136, 201, 172, 53, 241, 70, 188, 232, 74, 162, 103, 32, - 172, 106, 160, 169, -]; -pub const DSC_CERT_SIGNATURE_BYTES: [u8; 256] = [ - 10, 206, 98, 74, 100, 76, 221, 180, 183, 172, 153, 91, 69, 40, 127, 246, 154, 214, 50, 66, 197, - 55, 239, 218, 24, 20, 33, 244, 14, 20, 160, 200, 243, 254, 38, 234, 124, 188, 9, 110, 98, 60, - 205, 132, 99, 227, 137, 208, 178, 207, 78, 138, 101, 169, 132, 117, 127, 54, 250, 21, 5, 197, - 184, 173, 116, 21, 50, 140, 155, 149, 168, 244, 220, 82, 43, 58, 153, 160, 133, 25, 117, 106, - 127, 48, 10, 138, 16, 46, 80, 154, 173, 139, 203, 238, 4, 31, 241, 121, 138, 71, 30, 39, 188, - 130, 223, 45, 40, 113, 157, 171, 16, 151, 161, 40, 164, 78, 41, 141, 181, 11, 136, 152, 117, - 93, 222, 60, 59, 227, 206, 201, 192, 16, 162, 10, 197, 88, 183, 248, 91, 22, 113, 121, 223, - 244, 59, 241, 252, 248, 27, 120, 171, 163, 207, 6, 179, 160, 210, 180, 228, 25, 189, 213, 29, - 128, 8, 60, 209, 87, 91, 135, 128, 147, 23, 248, 33, 180, 187, 248, 110, 93, 166, 176, 31, 192, - 237, 216, 150, 33, 212, 205, 122, 94, 35, 9, 57, 135, 169, 230, 95, 188, 232, 200, 4, 234, 248, - 47, 145, 169, 230, 216, 245, 47, 19, 220, 233, 169, 26, 117, 155, 25, 63, 47, 86, 37, 24, 35, - 238, 138, 171, 57, 116, 215, 246, 124, 219, 23, 138, 121, 88, 189, 102, 88, 140, 133, 135, 184, - 224, 228, 135, 203, 107, 134, 142, 192, 117, 4, -]; diff --git a/playground/passport-input-gen/src/crypto.rs b/playground/passport-input-gen/src/crypto.rs deleted file mode 100644 index c5298901c..000000000 --- a/playground/passport-input-gen/src/crypto.rs +++ /dev/null @@ -1,79 +0,0 @@ -use { - rsa::{ - rand_core::OsRng, - traits::{PrivateKeyParts, PublicKeyParts}, - BigUint, Pkcs1v15Sign, RsaPrivateKey, - }, - sha2::{Digest, Sha256}, -}; - -// From Noir: -// `redc_param` = 2^{modulus_bits() * 2 + BARRETT_REDUCTION_OVERFLOW_BITS} / -// modulus -fn compute_redc_param_for_noir(n: &BigUint) -> BigUint { - const BARRETT_REDUCTION_OVERFLOW_BITS: usize = 4; - let k = n.bits(); - let b = BigUint::from(1u8) << (2 * k + BARRETT_REDUCTION_OVERFLOW_BITS); - &b / n -} - -/// Generates and prints a random set of RSA params, as bytes. -pub fn generate_random_rsa_params() { - let mut rng = OsRng; // rand@0.8 - let bits = 2048; - let private_key = RsaPrivateKey::new(&mut rng, bits).expect("failed to generate a key"); - let _mu = compute_redc_param_for_noir(private_key.n()); -} - -/// Returns the signature bytes. -pub fn generate_rsa_signature_pkcs_from_priv_key( - rsa_priv_key_p_bytes: &[u8; 128], - rsa_priv_key_q_bytes: &[u8; 128], - message_bytes: &[u8], -) -> Vec { - let prime_p = BigUint::from_bytes_be(&rsa_priv_key_p_bytes[..]); - let prime_q = BigUint::from_bytes_be(&rsa_priv_key_q_bytes[..]); - let e = BigUint::from(65537_u64); - let private_key = - RsaPrivateKey::from_p_q(prime_p, prime_q, e).expect("failed to read key from prime bytes"); - let public_key = private_key.to_public_key(); - let padding = Pkcs1v15Sign::new::(); // We explicitly want PKCSv1.15, not PSS - - let digest_in = Sha256::digest(message_bytes); - let signature_bytes = private_key - .sign(padding.clone(), &digest_in) - .expect("We should be able to sign"); - - public_key - .verify(padding, &digest_in, &signature_bytes) - .expect("Error: verification failed"); - - signature_bytes -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_rsa_signature_generation() { - let mut rng = OsRng; - let bits = 2048; - let private_key = RsaPrivateKey::new(&mut rng, bits).expect("failed to generate a key"); - - let p_bytes = private_key.primes()[0].to_bytes_be(); - let q_bytes = private_key.primes()[1].to_bytes_be(); - - let mut p_padded = [0u8; 128]; - let mut q_padded = [0u8; 128]; - p_padded[128 - p_bytes.len()..].copy_from_slice(&p_bytes); - q_padded[128 - q_bytes.len()..].copy_from_slice(&q_bytes); - - let message = b"test message"; - - let signature = generate_rsa_signature_pkcs_from_priv_key(&p_padded, &q_padded, message); - - assert!(!signature.is_empty()); - assert_eq!(signature.len(), 256); - } -} diff --git a/playground/passport-input-gen/src/generator.rs b/playground/passport-input-gen/src/generator.rs deleted file mode 100644 index 03b8db531..000000000 --- a/playground/passport-input-gen/src/generator.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::prover_config::{ - dg1_bytes_with_birthdate_expiry_date, - generate_prover_toml_string_from_custom_dg1_date_and_required_age, -}; - -pub fn generate_usa_passport_sample() -> String { - // USA passport MRZ example from the test file - // P YYMMDD format - // Expiry: 250101 (January 1, 2025) -> YYMMDD format - let birthdate_bytes = [b'9', b'0', b'0', b'1', b'0', b'1']; // Jan 1, 1990 - let expiry_bytes = [b'2', b'5', b'0', b'1', b'0', b'1']; // Jan 1, 2025 - let current_date = 20250101; // Current date for age verification - - // Generate DG1 with USA passport data - let usa_dg1_with_birthdate_expiry = - dg1_bytes_with_birthdate_expiry_date(&birthdate_bytes, &expiry_bytes); - - let usa_prover_toml = generate_prover_toml_string_from_custom_dg1_date_and_required_age( - &usa_dg1_with_birthdate_expiry, - 18, - 70, - current_date, - ); - - usa_prover_toml -} - -pub fn generate_age_testcases() -> Vec<(String, String)> { - let mut testcases = Vec::new(); - let current_date = 20250101; // January 1, 2025 - - // Test Case 1: Below 18 (17 years old - born January 2, 2007) - let birthdate_below_18 = [b'0', b'7', b'0', b'1', b'0', b'2']; // January 2, 2007 - let expiry_below_18 = [b'3', b'2', b'0', b'1', b'0', b'2']; // January 2, 2032 - let below_18_dg1 = dg1_bytes_with_birthdate_expiry_date(&birthdate_below_18, &expiry_below_18); - let below_18_toml = generate_prover_toml_string_from_custom_dg1_date_and_required_age( - &below_18_dg1, - 1, - 18, - current_date, - ); - testcases.push(("below_18".to_string(), below_18_toml)); - - // Test Case 2: Exactly 18 (born January 1, 2007) - let birthdate_exactly_18 = [b'0', b'7', b'0', b'1', b'0', b'1']; // January 1, 2007 - let expiry_exactly_18 = [b'3', b'2', b'0', b'1', b'0', b'1']; // January 1, 2032 - let exactly_18_dg1 = - dg1_bytes_with_birthdate_expiry_date(&birthdate_exactly_18, &expiry_exactly_18); - let exactly_18_toml = generate_prover_toml_string_from_custom_dg1_date_and_required_age( - &exactly_18_dg1, - 18, - 70, - current_date, - ); - testcases.push(("exactly_18".to_string(), exactly_18_toml)); - - // Test Case 3: Above 18 (19 years old - born December 31, 2005) - let birthdate_above_18 = [b'0', b'5', b'1', b'2', b'3', b'1']; // December 31, 2005 - let expiry_above_18 = [b'3', b'0', b'1', b'2', b'3', b'1']; // December 31, 2030 - let above_18_dg1 = dg1_bytes_with_birthdate_expiry_date(&birthdate_above_18, &expiry_above_18); - let above_18_toml = generate_prover_toml_string_from_custom_dg1_date_and_required_age( - &above_18_dg1, - 18, - 70, - current_date, - ); - testcases.push(("above_18".to_string(), above_18_toml)); - - testcases -} - -pub fn create_usa_dg1_from_mrz(mrz: &str) -> Option<[u8; 95]> { - // US MRZ format: - // P, +} + +/// Circuit inputs for Noir +pub struct CircuitInputs { + pub dg1: [u8; MAX_DG1_SIZE], + pub dg1_padded_length: usize, + pub current_date: u64, + pub min_age_required: u8, + pub max_age_required: u8, + pub passport_validity_contents: PassportValidityContent, +} + +/// Extracted validity contents from SOD +pub struct PassportValidityContent { + pub signed_attributes: [u8; MAX_SIGNED_ATTRIBUTES_SIZE], + pub signed_attributes_size: usize, + pub econtent: [u8; MAX_ECONTENT_SIZE], + pub econtent_len: usize, + pub dsc_pubkey: [u8; SIG_BYTES], + pub dsc_barrett_mu: [u8; SIG_BYTES + 1], + pub dsc_signature: [u8; SIG_BYTES], + pub dsc_rsa_exponent: u32, + pub csc_pubkey: [u8; SIG_BYTES * 2], + pub csc_barrett_mu: [u8; (SIG_BYTES * 2) + 1], + pub dsc_cert_signature: [u8; SIG_BYTES * 2], + pub csc_rsa_exponent: u32, + pub dg1_hash_offset: usize, + pub econtent_hash_offset: usize, + pub dsc_pubkey_offset_in_dsc_cert: usize, + pub dsc_cert: [u8; MAX_TBS_SIZE], + pub dsc_cert_len: usize, +} + +impl PassportReader { + /// Extract SignedAttributes (padded + size) + fn extract_signed_attrs( + &self, + ) -> Result<([u8; MAX_SIGNED_ATTRIBUTES_SIZE], usize), PassportError> { + let signed_attrs = self.sod.signer_info.signed_attrs.bytes.as_bytes(); + let size = signed_attrs.len(); + let padded = fit::(signed_attrs)?; + Ok((padded, size)) + } + + /// Extract eContent (padded + size + raw bytes) + fn extract_econtent(&self) -> Result<([u8; MAX_ECONTENT_SIZE], usize, &[u8]), PassportError> { + let econtent_bytes = self.sod.encap_content_info.e_content.bytes.as_bytes(); + let len = econtent_bytes.len(); + let padded = fit::(econtent_bytes)?; + Ok((padded, len, econtent_bytes)) + } + + /// Extract DSC public key, exponent, Barrett mu, and signature + fn extract_dsc( + &self, + ) -> Result<([u8; SIG_BYTES], u32, [u8; SIG_BYTES + 1], [u8; SIG_BYTES]), PassportError> { + let der = self + .sod + .certificate + .tbs + .subject_public_key_info + .subject_public_key + .as_bytes(); + let pubkey = + RsaPublicKey::from_pkcs1_der(der).map_err(|_| PassportError::DscPublicKeyInvalid)?; + + let modulus = to_fixed_array::(&pubkey.n().to_bytes_be(), "DSC modulus")?; + let exponent = to_u32(pubkey.e().to_bytes_be())?; + let barrett = to_fixed_array::<{ SIG_BYTES + 1 }>( + &compute_barrett_reduction_parameter(&BigUint::from_bytes_be(&modulus)).to_bytes_be(), + "DSC Barrett", + )?; + let signature = to_fixed_array::( + self.sod.signer_info.signature.as_bytes(), + "DSC signature", + )?; + + Ok((modulus, exponent, barrett, signature)) + } + + /// Extract CSCA public key, exponent, Barrett mu, and signature + fn extract_csca( + &self, + idx: usize, + ) -> Result< + ( + [u8; SIG_BYTES * 2], + u32, + [u8; SIG_BYTES * 2 + 1], + [u8; SIG_BYTES * 2], + ), + PassportError, + > { + let csca_keys = load_csca_public_keys().map_err(|_| PassportError::FailedToLoadCscaKeys)?; + let usa_csca = csca_keys.get("USA").ok_or(PassportError::NoUsaCsca)?; + let der = STANDARD + .decode(usa_csca[idx].public_key.as_bytes()) + .map_err(|e| PassportError::Base64DecodingFailed(e.to_string()))?; + let pubkey = RsaPublicKey::from_public_key_der(&der) + .map_err(|_| PassportError::CscaPublicKeyInvalid)?; + + let modulus = + to_fixed_array::<{ SIG_BYTES * 2 }>(&pubkey.n().to_bytes_be(), "CSCA modulus")?; + let exponent = to_u32(pubkey.e().to_bytes_be())?; + let barrett = to_fixed_array::<{ SIG_BYTES * 2 + 1 }>( + &compute_barrett_reduction_parameter(&BigUint::from_bytes_be(&modulus)).to_bytes_be(), + "CSCA Barrett", + )?; + let signature = to_fixed_array::<{ SIG_BYTES * 2 }>( + self.sod.certificate.signature.as_bytes(), + "CSCA signature", + )?; + + Ok((modulus, exponent, barrett, signature)) + } + + /// Extract CSCA data from an in-memory public key (used for mock data) + fn extract_csca_from_pubkey( + &self, + pubkey: &RsaPublicKey, + ) -> Result< + ( + [u8; SIG_BYTES * 2], + u32, + [u8; SIG_BYTES * 2 + 1], + [u8; SIG_BYTES * 2], + ), + PassportError, + > { + let modulus = + to_fixed_array::<{ SIG_BYTES * 2 }>(&pubkey.n().to_bytes_be(), "CSCA modulus")?; + let exponent = to_u32(pubkey.e().to_bytes_be())?; + let barrett = to_fixed_array::<{ SIG_BYTES * 2 + 1 }>( + &compute_barrett_reduction_parameter(&BigUint::from_bytes_be(&modulus)).to_bytes_be(), + "CSCA Barrett", + )?; + let signature = to_fixed_array::<{ SIG_BYTES * 2 }>( + self.sod.certificate.signature.as_bytes(), + "CSCA signature", + )?; + + Ok((modulus, exponent, barrett, signature)) + } + + /// Extract DSC certificate (padded + len + offset of modulus inside cert) + fn extract_dsc_cert( + &self, + dsc_modulus: &[u8; SIG_BYTES], + ) -> Result<([u8; MAX_TBS_SIZE], usize, usize), PassportError> { + let tbs_bytes = self.sod.certificate.tbs.bytes.as_bytes(); + let cert_len = tbs_bytes.len(); + let padded = fit::(tbs_bytes)?; + let pubkey_offset = find_offset(tbs_bytes, dsc_modulus, "DSC modulus in cert")?; + Ok((padded, cert_len, pubkey_offset)) + } + + /// Validate DG1, eContent, and signatures against DSC + CSCA + pub fn validate(&self) -> Result { + // 1. Check DG1 hash inside eContent + let dg1_hash = Sha256::digest(self.dg1.as_bytes()); + let dg1_from_econtent = self + .sod + .encap_content_info + .e_content + .data_group_hash_values + .values + .get(&1) + .ok_or(PassportError::MissingDg1Hash)? + .as_bytes(); + + if dg1_from_econtent != dg1_hash.as_slice() { + return Err(PassportError::Dg1HashMismatch); + } + + // 2. Check hash(eContent) inside SignedAttributes + let econtent_hash = Sha256::digest(self.sod.encap_content_info.e_content.bytes.as_bytes()); + let mut msg_digest = self.sod.signer_info.signed_attrs.message_digest.as_bytes(); + + if msg_digest.len() > ASN1_HEADER_LEN && msg_digest[0] == ASN1_OCTET_STRING_TAG { + msg_digest = &msg_digest[ASN1_HEADER_LEN..]; + } + + if econtent_hash.as_slice() != msg_digest { + return Err(PassportError::EcontentHashMismatch); + } + + // 3. Verify SignedAttributes signature with DSC + let signed_attr_hash = Sha256::digest(self.sod.signer_info.signed_attrs.bytes.as_bytes()); + let dsc_pubkey_bytes = self + .sod + .certificate + .tbs + .subject_public_key_info + .subject_public_key + .as_bytes(); + let dsc_pubkey = RsaPublicKey::from_pkcs1_der(dsc_pubkey_bytes) + .map_err(|_| PassportError::DscPublicKeyInvalid)?; + + let dsc_signature = self.sod.signer_info.signature.as_bytes(); + + let verify_result = match &self.sod.signer_info.signature_algorithm.name { + SignatureAlgorithmName::Sha256WithRsaEncryption + | SignatureAlgorithmName::RsaEncryption => dsc_pubkey.verify( + Pkcs1v15Sign::new::(), + signed_attr_hash.as_slice(), + dsc_signature, + ), + SignatureAlgorithmName::RsassaPss => dsc_pubkey.verify( + Pss::new::(), + signed_attr_hash.as_slice(), + dsc_signature, + ), + unsupported => { + return Err(PassportError::UnsupportedSignatureAlgorithm(format!( + "{:?}", + unsupported + ))) + } + }; + verify_result.map_err(|_| PassportError::DscSignatureInvalid)?; + + // 4. Verify DSC certificate signature with CSCA + let tbs_bytes = self.sod.certificate.tbs.bytes.as_bytes(); + let tbs_digest = Sha256::digest(tbs_bytes); + let csca_signature = self.sod.certificate.signature.as_bytes(); + + if let Some(key) = &self.csca_pubkey { + key.verify( + Pkcs1v15Sign::new::(), + tbs_digest.as_slice(), + csca_signature, + ) + .map_err(|_| PassportError::CscaSignatureInvalid)?; + return Ok(0); + } + + let all_csca = load_csca_public_keys().map_err(|_| PassportError::CscaKeysMissing)?; + let usa_csca = all_csca.get("USA").ok_or(PassportError::NoUsaCsca)?; + + for (i, csca) in usa_csca.iter().enumerate() { + let der = STANDARD + .decode(csca.public_key.as_bytes()) + .map_err(|e| PassportError::Base64DecodingFailed(e.to_string()))?; + let csca_pubkey = RsaPublicKey::from_public_key_der(&der) + .map_err(|_| PassportError::CscaPublicKeyInvalid)?; + if csca_pubkey + .verify( + Pkcs1v15Sign::new::(), + tbs_digest.as_slice(), + csca_signature, + ) + .is_ok() + { + return Ok(i); + } + } + Err(PassportError::CscaSignatureInvalid) + } + + /// Convert to circuit inputs for Noir Circuits + pub fn to_circuit_inputs( + &self, + current_date: u64, + min_age_required: u8, + max_age_required: u8, + csca_key_index: usize, + ) -> Result { + // === Step 1. DG1 === + let dg1_padded = fit::(self.dg1.as_bytes())?; + let dg1_len = self.dg1.len(); + + // === Step 2. SignedAttributes === + let (signed_attrs, signed_attributes_size) = self.extract_signed_attrs()?; + + // === Step 3. eContent === + let (econtent, econtent_len, econtent_bytes) = self.extract_econtent()?; + + // === Step 4. DSC === + let (dsc_modulus, dsc_exponent, dsc_barrett, dsc_signature) = self.extract_dsc()?; + + // === Step 5. CSCA === + let (csca_modulus, csca_exponent, csca_barrett, csca_signature) = if self.mockdata { + let key = self + .csca_pubkey + .as_ref() + .ok_or(PassportError::MissingCscaMockKey)?; + self.extract_csca_from_pubkey(key)? + } else { + self.extract_csca(csca_key_index)? + }; + + // === Step 6. Offsets === + let dg1_hash = Sha256::digest(self.dg1.as_bytes()); + let dg1_hash_offset = find_offset(econtent_bytes, dg1_hash.as_slice(), "DG1 hash")?; + + let econtent_hash = Sha256::digest(econtent_bytes); + let econtent_hash_offset = + find_offset(&signed_attrs, econtent_hash.as_slice(), "eContent hash")?; + + // === Step 7. DSC Certificate === + let (dsc_cert, dsc_cert_len, dsc_pubkey_offset) = self.extract_dsc_cert(&dsc_modulus)?; + + // === Step 8. Build CircuitInputs === + Ok(CircuitInputs { + dg1: dg1_padded, + dg1_padded_length: dg1_len, + current_date, + min_age_required, + max_age_required, + passport_validity_contents: PassportValidityContent { + signed_attributes: signed_attrs, + signed_attributes_size, + econtent, + econtent_len, + dsc_pubkey: dsc_modulus, + dsc_barrett_mu: dsc_barrett, + dsc_signature, + dsc_rsa_exponent: dsc_exponent, + csc_pubkey: csca_modulus, + csc_barrett_mu: csca_barrett, + dsc_cert_signature: csca_signature, + csc_rsa_exponent: csca_exponent, + dg1_hash_offset, + econtent_hash_offset, + dsc_pubkey_offset_in_dsc_cert: dsc_pubkey_offset, + dsc_cert, + dsc_cert_len, + }, + }) + } +} + +impl CircuitInputs { + pub fn to_toml_string(&self) -> String { + let mut out = String::new(); + let _ = writeln!(out, "dg1 = {:?}", self.dg1); + let _ = writeln!(out, "dg1_padded_length = {}", self.dg1_padded_length); + let _ = writeln!(out, "current_date = {}", self.current_date); + let _ = writeln!(out, "min_age_required = {}", self.min_age_required); + let _ = writeln!(out, "max_age_required = {}", self.max_age_required); + let _ = writeln!(out, "\n[passport_validity_contents]"); + + let pvc = &self.passport_validity_contents; + let _ = writeln!(out, "signed_attributes = {:?}", pvc.signed_attributes); + let _ = writeln!( + out, + "signed_attributes_size = {}", + pvc.signed_attributes_size + ); + let _ = writeln!(out, "econtent = {:?}", pvc.econtent); + let _ = writeln!(out, "econtent_len = {}", pvc.econtent_len); + let _ = writeln!(out, "dsc_signature = {:?}", pvc.dsc_signature); + let _ = writeln!(out, "dsc_rsa_exponent = {}", pvc.dsc_rsa_exponent); + let _ = writeln!(out, "dsc_pubkey = {:?}", pvc.dsc_pubkey); + let _ = writeln!(out, "dsc_barrett_mu = {:?}", pvc.dsc_barrett_mu); + let _ = writeln!(out, "csc_pubkey = {:?}", pvc.csc_pubkey); + let _ = writeln!(out, "csc_barrett_mu = {:?}", pvc.csc_barrett_mu); + let _ = writeln!(out, "dsc_cert_signature = {:?}", pvc.dsc_cert_signature); + let _ = writeln!(out, "csc_rsa_exponent = {}", pvc.csc_rsa_exponent); + let _ = writeln!(out, "dg1_hash_offset = {}", pvc.dg1_hash_offset); + let _ = writeln!(out, "econtent_hash_offset = {}", pvc.econtent_hash_offset); + let _ = writeln!( + out, + "dsc_pubkey_offset_in_dsc_cert = {}", + pvc.dsc_pubkey_offset_in_dsc_cert + ); + let _ = writeln!(out, "dsc_cert = {:?}", pvc.dsc_cert); + let _ = writeln!(out, "dsc_cert_len = {}", pvc.dsc_cert_len); + out + } + + pub fn save_to_toml_file>(&self, path: P) -> std::io::Result<()> { + std::fs::write(path, self.to_toml_string()) + } +} diff --git a/playground/passport-input-gen/src/main.rs b/playground/passport-input-gen/src/main.rs deleted file mode 100644 index 102007abd..000000000 --- a/playground/passport-input-gen/src/main.rs +++ /dev/null @@ -1,34 +0,0 @@ -pub mod constants; -pub mod crypto; -pub mod generator; -pub mod prover_config; - -use crate::{ - generator::{generate_age_testcases, generate_usa_passport_sample}, - prover_config::{ - dg1_bytes_with_birthdate_expiry_date, - generate_prover_toml_string_from_custom_dg1_date_and_required_age, - }, -}; - -fn main() { - println!("Generating age verification testcases..."); - - // Generate age testcases: below 18, exactly 18, above 18 (max age 70) - let testcases = generate_age_testcases(); - for (name, toml_content) in testcases { - let filename = format!("{}_Prover.toml", name); - let complete_age_check_path = format!( - "../../noir-examples/noir-passport-examples/complete_age_check/{}", - filename - ); - std::fs::write(&complete_age_check_path, toml_content) - .expect(&format!("Unable to write {}", complete_age_check_path)); - println!("Generated: {}", complete_age_check_path); - } - - println!("\nTestcases created:"); - println!("- below_18_Prover.toml (17 years old)"); - println!("- exactly_18_Prover.toml (18 years old)"); - println!("- above_18_Prover.toml (19 years old"); -} diff --git a/playground/passport-input-gen/src/mock_generator.rs b/playground/passport-input-gen/src/mock_generator.rs new file mode 100644 index 000000000..a5ca0baa4 --- /dev/null +++ b/playground/passport-input-gen/src/mock_generator.rs @@ -0,0 +1,198 @@ +use { + crate::parser::{ + binary::Binary, + dsc::{SubjectPublicKeyInfo, TbsCertificate, DSC}, + sod::SOD, + types::{ + DataGroupHashValues, DigestAlgorithm, EContent, EncapContentInfo, SignatureAlgorithm, + SignatureAlgorithmName, SignedAttrs, SignerIdentifier, SignerInfo, MAX_DG1_SIZE, + }, + }, + rsa::{ + pkcs1::EncodeRsaPublicKey, + pkcs1v15::SigningKey, + signature::{SignatureEncoding, Signer}, + RsaPrivateKey, RsaPublicKey, + }, + sha2::{Digest, Sha256}, + std::collections::HashMap, +}; + +/// Build a fake DG1 (MRZ) with given birthdate and expiry dates. +/// Birthdate and expiry are encoded as YYMMDD and inserted into the MRZ +/// positions. The rest of the bytes are filled with `<` characters and the +/// final two bytes are zeroed. +pub fn dg1_bytes_with_birthdate_expiry_date(birthdate: &[u8; 6], expiry: &[u8; 6]) -> Vec { + let mut dg1 = vec![b'<'; MAX_DG1_SIZE]; + let mrz_offset = 5; + dg1[mrz_offset + 57..mrz_offset + 57 + 6].copy_from_slice(birthdate); + dg1[mrz_offset + 65..mrz_offset + 65 + 6].copy_from_slice(expiry); + dg1[93] = 0; + dg1[94] = 0; + dg1 +} + +/// Generate a synthetic SOD structure for the given DG1 and key pairs. +pub fn generate_fake_sod( + dg1: &[u8], + dsc_priv: &RsaPrivateKey, + dsc_pub: &RsaPublicKey, + csca_priv: &RsaPrivateKey, + _csca_pub: &RsaPublicKey, +) -> SOD { + // Hash DG1 and build eContent + let dg1_hash = Sha256::digest(dg1); + let econtent_bytes = dg1_hash.to_vec(); + let mut dg_map = HashMap::new(); + dg_map.insert(1u32, Binary::from_slice(&dg1_hash)); + let data_group_hashes = DataGroupHashValues { values: dg_map }; + let econtent = EContent { + version: 0, + hash_algorithm: DigestAlgorithm::SHA256, + data_group_hash_values: data_group_hashes, + bytes: Binary::from_slice(&econtent_bytes), + }; + let encap_content_info = EncapContentInfo { + e_content_type: "mRTDSignatureData".to_string(), + e_content: econtent, + }; + + // Hash eContent and build SignedAttributes + let econtent_hash = Sha256::digest(&econtent_bytes); + let signed_attr_bytes = econtent_hash.to_vec(); + let signed_attrs = SignedAttrs { + content_type: "data".to_string(), + message_digest: Binary::from_slice(&econtent_hash), + signing_time: None, + bytes: Binary::from_slice(&signed_attr_bytes), + }; + + // Sign SignedAttributes with DSC private key + let dsc_signer = SigningKey::::new(dsc_priv.clone()); + let dsc_signature = dsc_signer.sign(&signed_attr_bytes).to_bytes(); + let signer_info = SignerInfo { + version: 1, + signed_attrs, + digest_algorithm: DigestAlgorithm::SHA256, + signature_algorithm: SignatureAlgorithm { + name: SignatureAlgorithmName::Sha256WithRsaEncryption, + parameters: None, + }, + signature: Binary::from_slice(&dsc_signature), + sid: SignerIdentifier { + issuer_and_serial_number: None, + subject_key_identifier: None, + }, + }; + + // Build fake DSC certificate (TBS = DER of DSC public key) + let dsc_pub_der = dsc_pub.to_pkcs1_der().expect("pkcs1 der").to_vec(); + let tbs_bytes = dsc_pub_der.clone(); + + let csca_signer = SigningKey::::new(csca_priv.clone()); + let csca_signature = csca_signer.sign(&tbs_bytes).to_bytes(); + + let dsc_cert = DSC { + tbs: TbsCertificate { + version: 1, + serial_number: Binary::from_slice(&[1]), + signature_algorithm: SignatureAlgorithm { + name: SignatureAlgorithmName::Sha256WithRsaEncryption, + parameters: None, + }, + issuer: "CSCA".to_string(), + validity_not_before: chrono::Utc::now() + - chrono::Duration::from_std(std::time::Duration::from_secs( + 5 * 365 * 24 * 60 * 60, + )) + .expect("valid duration before 5 years"), // before 5 year date + validity_not_after: chrono::Utc::now() + + chrono::Duration::from_std(std::time::Duration::from_secs( + 5 * 365 * 24 * 60 * 60, + )) + .expect("valid duration after 5 years"), // after 5 years + subject: "DSC".to_string(), + subject_public_key_info: SubjectPublicKeyInfo { + signature_algorithm: SignatureAlgorithm { + name: SignatureAlgorithmName::RsaEncryption, + parameters: None, + }, + subject_public_key: Binary::from_slice(&dsc_pub_der), + }, + issuer_unique_id: None, + subject_unique_id: None, + extensions: HashMap::new(), + bytes: Binary::from_slice(&tbs_bytes), + }, + signature_algorithm: SignatureAlgorithm { + name: SignatureAlgorithmName::Sha256WithRsaEncryption, + parameters: None, + }, + signature: Binary::from_slice(&csca_signature), + }; + + SOD { + version: 1, + digest_algorithms: vec![DigestAlgorithm::SHA256], + encap_content_info, + signer_info, + certificate: dsc_cert, + bytes: Binary::new(vec![]), + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::{ + mock_keys::{MOCK_CSCA_PRIV_KEY_B64, MOCK_DSC_PRIV_KEY_B64}, + PassportReader, + }, + base64::{engine::general_purpose::STANDARD, Engine as _}, + chrono::Utc, + rsa::pkcs8::DecodePrivateKey, + }; + + fn load_csca_mock_private_key() -> RsaPrivateKey { + let der = STANDARD + .decode(MOCK_CSCA_PRIV_KEY_B64) + .expect("decode CSCA private key"); + RsaPrivateKey::from_pkcs8_der(&der).expect("CSCA key") + } + + fn load_dsc_mock_private_key() -> RsaPrivateKey { + let der = STANDARD + .decode(MOCK_DSC_PRIV_KEY_B64) + .expect("decode DSC private key"); + RsaPrivateKey::from_pkcs8_der(&der).expect("DSC key") + } + + #[test] + fn test_generate_and_validate_sod() { + let csca_priv = load_csca_mock_private_key(); + let csca_pub = csca_priv.to_public_key(); + let dsc_priv = load_dsc_mock_private_key(); + let dsc_pub = dsc_priv.to_public_key(); + + let dg1 = dg1_bytes_with_birthdate_expiry_date(b"070101", b"320101"); + let sod = generate_fake_sod(&dg1, &dsc_priv, &dsc_pub, &csca_priv, &csca_pub); + let reader = PassportReader { + dg1: Binary::from_slice(&dg1), + sod, + mockdata: true, + csca_pubkey: Some(csca_pub), + }; + assert!(reader.validate().is_ok()); + + let current_date = Utc::now(); + let current_timestamp = current_date.timestamp() as u64; + + let inputs = reader + .to_circuit_inputs(current_timestamp, 18, 70, 0) + .expect("to circuit inputs"); + let _toml_output = inputs.to_toml_string(); + + println!("{}", _toml_output); + } +} diff --git a/playground/passport-input-gen/src/mock_keys.rs b/playground/passport-input-gen/src/mock_keys.rs new file mode 100644 index 000000000..66940a486 --- /dev/null +++ b/playground/passport-input-gen/src/mock_keys.rs @@ -0,0 +1,81 @@ +pub const MOCK_CSCA_PRIV_KEY_B64: &str = concat!( + "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", + "H+OeAUjqk4zLjY/dS6LoK6ouvEsT2xY=" +); + +pub const MOCK_DSC_PRIV_KEY_B64: &str = concat!( + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCQYBZiyhfuBrtT", + "9gqNlSc+ls8ZTP55n8EZEUDlcKqYXtTVBL8It+G41bXTZNI8mxoN2wt0VOwh1C8F", + "u+J4oTlhyPqui9irX7KUbQOJl/WONbH7SsoCnSE3Hr3v82W3K0T1xglabVltIWIg", + "rXnLAk9EloeeSEzfN0IeLSEQW5mef0DdH5fxXWnrmbCS3RTnjQKSTdEeWiEh6LCR", + "9OXdK2UK0jcyyGdXElI1wYJ8RWCzV/XLtc05Q7VQxjlll7NnyfM0RFt6idGNJ0RJ", + "9MjTfQKwDFBNUeGpItG71C84XNyfWeyFyNML7dmBc7/QJ8azEBw7eaAw71GQZqh6", + "njtTNlvTAgMBAAECggEAMDh3oZ1AKtcCwENAOROlHOl/2EJ4CCVMeFPj6f4cbHHY", + "AiHosD/CW1q9tRJKicWACTqs87jdeVsGLZMYTBQFN1zKJXw97Nc0rRBQCye+8zWJ", + "8ZWELSVQD7nr1HXl9iy0hqYTP6mgIavxu1pVgHGQCieAumQJkNBC/BM0pOMDRwVX", + "J7SyW8JdGW8tpKNoZd00iECvnLwjSJbe1Gzc9JfIbe4KG+J3lFX2GButvKpQR+sI", + "RqEJX5YaKQ+ck0xsYE+0XmrnNvO8UR2zqxORwonGVPFeeBUQUBzgZs3QlumTLLdD", + "WSllYrOj0NTkPEPpx3OeYAlnCTWPfYtlhiPFVM6QoQKBgQDGTXuCetOy3rXPltPA", + "hL7hlvW2FoynMCHQm+iCacbxfmItLe+acttOgHX1Jn+N+yoz08H5NQlBfgzsukqx", + "v0pHss9YDHboPXMZn/wWh9UyICaVm4u/pPF94fDX5dGZohhyS2V20TdIumTJI0Zn", + "UofXqeHK9kdpL3r+PxKJdyXF4wKBgQC6Ydf2uOaW8uxVbQBe3KD65XWmUUOibOFr", + "HvNhGzlaiRlXRIt6ETBg0HFXdk8FrGjXJyDABZdUDn1maWW7+vM8U/t3uv6y2QTY", + "BuxrEcfTkjRyLYc1B+7YVA4tOOfjVFWavHqOt5WXGWizjcHaX3+ahaSdeFqL3fIG", + "xZUsmIZ1UQKBgCyRM2XyxutXZFMgWbzV2LSIofZngPr+NUtWDt5oeX7C4bu3rDbx", + "A1rUQi5zlY1KWoTFXb3tboQamTsG70ydaktM446tVngBf4HN0/EYXBDk6ucKy1Cp", + "+GBLTD6pdv7pUEc3rLkUcjFVOtt9oxALm14b3xQnX4tDUYgcksT0UzfzAoGAEYwI", + "owfBXdC9A0Xh1Qx8c2KK8v+PmIkbp9QgIbJIcgeaRScE4pcfsN2u7gcLZYNX03mx", + "kaJ1HsrGb4/YrhvwLmvRrvIB7KCV3ii4tVPVNkv7eAxlQE7g46j2NLe4zSQxcwHv", + "n+QUx0bzHoRoDcET6F5Qoyqji6t3j7+mTa6GaiECgYEAro8UucwZXo8JRTXt5ylL", + "cwLA0O+U5dTwq2Pi9YcQ/pPHdqEMEp975QNcI9tmgSEBv+hppDC4ydgdGj1+pyTR", + "8nFvIYWjqcNAaxypyNladBlnsNpF0gby2BNAV4+HlprX5xFVzsgflHgB1SIHP4/Z", + "lxVcRfuIVt0q/b4RcHCPZZo=" +); diff --git a/playground/passport-input-gen/src/parser/binary.rs b/playground/passport-input-gen/src/parser/binary.rs new file mode 100644 index 000000000..2755ac7d5 --- /dev/null +++ b/playground/passport-input-gen/src/parser/binary.rs @@ -0,0 +1,62 @@ +use base64::{engine::general_purpose, Engine as _}; + +#[derive(Debug, Clone)] +pub struct Binary { + pub data: Vec, +} + +impl Binary { + pub fn new(data: Vec) -> Self { + Binary { data } + } + + pub fn from_slice(data: &[u8]) -> Self { + Binary { + data: data.to_vec(), + } + } + + pub fn from_base64(b64: &str) -> Result { + let data = general_purpose::STANDARD.decode(b64)?; + Ok(Binary::new(data)) + } + + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + pub fn slice(&self, start: usize, end: usize) -> Binary { + Binary::new(self.data[start..end].to_vec()) + } + + pub fn to_string_ascii(&self) -> String { + String::from_utf8_lossy(&self.data).to_string() + } + + pub fn as_bytes(&self) -> &[u8] { + &self.data + } + + pub fn to_hex(&self) -> String { + format!("0x{}", hex::encode(&self.data)) + } + + pub fn equals(&self, other: &Binary) -> bool { + self.data.eq(&other.data) + } + + pub fn from_hex(hex_str: &str) -> Result { + let data = hex::decode(hex_str)?; + Ok(Binary::new(data)) + } +} + +impl PartialEq for Binary { + fn eq(&self, other: &Self) -> bool { + self.data == other.data + } +} diff --git a/playground/passport-input-gen/src/parser/dsc.rs b/playground/passport-input-gen/src/parser/dsc.rs new file mode 100644 index 000000000..8c0c0fc6d --- /dev/null +++ b/playground/passport-input-gen/src/parser/dsc.rs @@ -0,0 +1,141 @@ +use { + crate::parser::{ + binary::Binary, + oid_registry::REGISTRY, + types::{PassportError, SignatureAlgorithm, SignatureAlgorithmName}, + utils::{get_oid_name, strip_length_prefix, OidEntry}, + }, + chrono::{DateTime, Utc}, + std::collections::HashMap, + x509_parser::{parse_x509_certificate, prelude::X509Certificate, x509::X509Name}, +}; + +#[derive(Debug, Clone)] +pub struct TbsCertificate { + pub version: u32, + pub serial_number: Binary, + pub signature_algorithm: SignatureAlgorithm, + pub issuer: String, + pub validity_not_before: DateTime, + pub validity_not_after: DateTime, + pub subject: String, + pub subject_public_key_info: SubjectPublicKeyInfo, + pub issuer_unique_id: Option, + pub subject_unique_id: Option, + pub extensions: HashMap, + pub bytes: Binary, +} + +#[derive(Debug, Clone)] +pub struct SubjectPublicKeyInfo { + pub signature_algorithm: SignatureAlgorithm, + pub subject_public_key: Binary, +} + +#[derive(Debug, Clone)] +pub struct DSC { + pub tbs: TbsCertificate, + pub signature_algorithm: SignatureAlgorithm, + pub signature: Binary, +} + +impl DSC { + /// Formats an X.509 Distinguished Name (DN) into a readable string. + fn format_name(name: &X509Name<'_>, registry: &HashMap<&'static str, OidEntry>) -> String { + name.iter_rdn() + .map(|rdn| { + rdn.iter() + .map(|attr| { + let oid_str = attr.attr_type().to_string(); + let field_name = get_oid_name(&oid_str, registry); + let value = attr + .as_str() + .map(String::from) + .unwrap_or_else(|_| hex::encode(attr.as_slice())); + format!("{}={}", field_name, value) + }) + .collect::>() + .join(", ") + }) + .collect::>() + .join(", ") + } + + /// Parses a DER-encoded X.509 certificate into a `DSC`. + pub fn from_der(binary: &Binary) -> Result { + let der = strip_length_prefix(binary); + let (_, cert) = parse_x509_certificate(&der.data).expect("X509 decode failed"); + Self::from_x509(cert) + } + + /// Converts a parsed `X509Certificate` into the internal `DSC` struct. + fn from_x509(cert: X509Certificate<'_>) -> Result { + let tbs = cert.tbs_certificate; + let tbs_bytes = Binary::from_slice(tbs.as_ref()); + + let not_before = tbs.validity.not_before.to_datetime(); + let not_before_utc = + DateTime::::from_timestamp(not_before.unix_timestamp(), not_before.nanosecond()) + .ok_or_else(|| PassportError::InvalidDate("Invalid not_before time".to_string()))?; + + let not_after = tbs.validity.not_after.to_datetime(); + let not_after_utc = + DateTime::::from_timestamp(not_after.unix_timestamp(), not_after.nanosecond()) + .ok_or_else(|| PassportError::InvalidDate("Invalid not_after time".to_string()))?; + + // Helper function to create SignatureAlgorithm from AlgorithmIdentifier + let create_signature_algorithm = |alg_id: &x509_parser::x509::AlgorithmIdentifier<'_>| -> Result { + let name = SignatureAlgorithmName::from_oid(&alg_id.algorithm.to_string()).ok_or_else( + || PassportError::UnsupportedSignatureAlgorithm(alg_id.algorithm.to_string()), + )?; + let parameters = alg_id + .parameters + .as_ref() + .map(|p| Binary::from_slice(p.data)); + Ok(SignatureAlgorithm { name, parameters }) + }; + + let tbs_signature_algorithm = create_signature_algorithm(&tbs.signature)?; + let cert_signature_algorithm = create_signature_algorithm(&cert.signature_algorithm)?; + let spki_algorithm = create_signature_algorithm(&tbs.subject_pki.algorithm)?; + + let subject_public_key_info = SubjectPublicKeyInfo { + signature_algorithm: spki_algorithm, + subject_public_key: Binary::from_slice(&tbs.subject_pki.subject_public_key.data), + }; + + let mut extensions = HashMap::new(); + for ext in tbs.extensions() { + let oid_str = ext.oid.to_string(); + let name = get_oid_name(&oid_str, ®ISTRY); + extensions.insert(name, (ext.critical, Binary::from_slice(ext.value))); + } + + let tbs_struct = TbsCertificate { + version: tbs.version().0, + serial_number: Binary::from_slice(tbs.raw_serial()), + signature_algorithm: tbs_signature_algorithm, + issuer: Self::format_name(&tbs.issuer, ®ISTRY), + validity_not_before: not_before_utc, + validity_not_after: not_after_utc, + subject: Self::format_name(&tbs.subject, ®ISTRY), + subject_public_key_info, + issuer_unique_id: tbs + .issuer_uid + .as_ref() + .map(|uid| Binary::from_slice(uid.0.as_ref())), + subject_unique_id: tbs + .subject_uid + .as_ref() + .map(|uid| Binary::from_slice(uid.0.as_ref())), + extensions, + bytes: tbs_bytes, + }; + + Ok(DSC { + tbs: tbs_struct, + signature_algorithm: cert_signature_algorithm, + signature: Binary::from_slice(&cert.signature_value.data), + }) + } +} diff --git a/playground/passport-input-gen/src/parser/mod.rs b/playground/passport-input-gen/src/parser/mod.rs new file mode 100644 index 000000000..e0c34d076 --- /dev/null +++ b/playground/passport-input-gen/src/parser/mod.rs @@ -0,0 +1,6 @@ +pub(crate) mod binary; +pub(crate) mod dsc; +mod oid_registry; +pub(crate) mod sod; +pub(crate) mod types; +pub(crate) mod utils; diff --git a/playground/passport-input-gen/src/parser/oid_registry.rs b/playground/passport-input-gen/src/parser/oid_registry.rs new file mode 100644 index 000000000..bc39c03a4 --- /dev/null +++ b/playground/passport-input-gen/src/parser/oid_registry.rs @@ -0,0 +1,204 @@ +use {crate::parser::utils::OidEntry, lazy_static::lazy_static, std::collections::HashMap}; + +lazy_static! { + pub static ref REGISTRY: HashMap<&'static str, OidEntry> = load_oids(); +} +/// Returns a lookup table for the Object Identifiers that are relevant to the +/// passport input generator. +/// +/// The previous version of this registry tried to mirror a “complete” OID +/// catalogue. That was hard to maintain and pulled in thousands of identifiers +/// that are never touched by the parser. The routines that consume this +/// registry only require a small set of entries to render human readable names +/// for: +/// +/// * CMS signed attributes that appear in SOD files (content type, message +/// digest, signing time). +/// * The hash algorithms that are supported by the parser (SHA-* family). +/// * Common X.509 RDN attributes so that certificate issuers/subjects remain +/// readable. +/// * Frequently used X.509 extensions such as key usage and authority key +/// identifiers. +/// * ICAO MRTD specific identifiers (e.g. `mRTDSignatureData`). +/// +/// Keeping the list focused makes it clear which identifiers we rely on and +/// avoids carrying around a huge hard-coded list that is difficult to audit. +fn load_oids() -> HashMap<&'static str, OidEntry> { + HashMap::from([ + // PKCS#9 signed attributes used in CMS / SOD structures + ("1.2.840.113549.1.9.3", OidEntry { + d: "contentType", + c: "PKCS #9", + w: false, + }), + ("1.2.840.113549.1.9.4", OidEntry { + d: "messageDigest", + c: "PKCS #9", + w: false, + }), + ("1.2.840.113549.1.9.5", OidEntry { + d: "signingTime", + c: "PKCS #9", + w: false, + }), + // CMS eContent type for ICAO LDS security objects + ("2.23.136.1.1.1", OidEntry { + d: "mRTDSignatureData", + c: "ICAO MRTD", + w: false, + }), + // Hash algorithms recognised by the parser + ("1.3.14.3.2.26", OidEntry { + d: "sha-1", + c: "NIST Algorithm", + w: false, + }), + ("2.16.840.1.101.3.4.2.1", OidEntry { + d: "sha-256", + c: "NIST Algorithm", + w: false, + }), + ("2.16.840.1.101.3.4.2.2", OidEntry { + d: "sha-384", + c: "NIST Algorithm", + w: false, + }), + ("2.16.840.1.101.3.4.2.3", OidEntry { + d: "sha-512", + c: "NIST Algorithm", + w: false, + }), + ("2.16.840.1.101.3.4.2.4", OidEntry { + d: "sha-224", + c: "NIST Algorithm", + w: false, + }), + // Common X.509 RDN attributes so issuer/subject strings stay readable + ("2.5.4.3", OidEntry { + d: "commonName", + c: "X.520 Distinguished Name", + w: false, + }), + ("2.5.4.4", OidEntry { + d: "surname", + c: "X.520 Distinguished Name", + w: false, + }), + ("2.5.4.5", OidEntry { + d: "serialNumber", + c: "X.520 Distinguished Name", + w: false, + }), + ("2.5.4.6", OidEntry { + d: "countryName", + c: "X.520 Distinguished Name", + w: false, + }), + ("2.5.4.7", OidEntry { + d: "localityName", + c: "X.520 Distinguished Name", + w: false, + }), + ("2.5.4.8", OidEntry { + d: "stateOrProvinceName", + c: "X.520 Distinguished Name", + w: false, + }), + ("2.5.4.9", OidEntry { + d: "streetAddress", + c: "X.520 Distinguished Name", + w: false, + }), + ("2.5.4.10", OidEntry { + d: "organizationName", + c: "X.520 Distinguished Name", + w: false, + }), + ("2.5.4.11", OidEntry { + d: "organizationalUnitName", + c: "X.520 Distinguished Name", + w: false, + }), + ("2.5.4.12", OidEntry { + d: "title", + c: "X.520 Distinguished Name", + w: false, + }), + ("2.5.4.13", OidEntry { + d: "description", + c: "X.520 Distinguished Name", + w: false, + }), + ("2.5.4.17", OidEntry { + d: "postalCode", + c: "X.520 Distinguished Name", + w: false, + }), + ("2.5.4.42", OidEntry { + d: "givenName", + c: "X.520 Distinguished Name", + w: false, + }), + ("2.5.4.43", OidEntry { + d: "initials", + c: "X.520 Distinguished Name", + w: false, + }), + ("2.5.4.46", OidEntry { + d: "dnQualifier", + c: "X.520 Distinguished Name", + w: false, + }), + ("2.5.4.65", OidEntry { + d: "pseudonym", + c: "X.520 Distinguished Name", + w: false, + }), + // Commonly encountered X.509 extensions + ("2.5.29.14", OidEntry { + d: "subjectKeyIdentifier", + c: "X.509 extension", + w: false, + }), + ("2.5.29.15", OidEntry { + d: "keyUsage", + c: "X.509 extension", + w: false, + }), + ("2.5.29.17", OidEntry { + d: "subjectAltName", + c: "X.509 extension", + w: false, + }), + ("2.5.29.19", OidEntry { + d: "basicConstraints", + c: "X.509 extension", + w: false, + }), + ("2.5.29.31", OidEntry { + d: "cRLDistributionPoints", + c: "X.509 extension", + w: false, + }), + ("2.5.29.32", OidEntry { + d: "certificatePolicies", + c: "X.509 extension", + w: false, + }), + ("2.5.29.32.0", OidEntry { + d: "anyPolicy", + c: "X.509 extension", + w: false, + }), + ("2.5.29.35", OidEntry { + d: "authorityKeyIdentifier", + c: "X.509 extension", + w: false, + }), + ("2.5.29.37", OidEntry { + d: "extKeyUsage", + c: "X.509 extension", + w: false, + }), + ]) +} diff --git a/playground/passport-input-gen/src/parser/sod.rs b/playground/passport-input-gen/src/parser/sod.rs new file mode 100644 index 000000000..877fcccbb --- /dev/null +++ b/playground/passport-input-gen/src/parser/sod.rs @@ -0,0 +1,431 @@ +use { + crate::parser::{ + binary::Binary, + dsc::DSC, + oid_registry::REGISTRY, + types::{ + DataGroupHashValues, DigestAlgorithm, EContent, EncapContentInfo, + IssuerAndSerialNumber, LDSSecurityObject, PassportError, SignatureAlgorithm, + SignatureAlgorithmName, SignedAttrs, SignerIdentifier, SignerInfo, + }, + utils::{ + get_hash_algo_name, get_oid_name, oid_to_string, strip_length_prefix, version_from, + OidEntry, + }, + }, + rasn::der, + rasn_cms::{Attribute, ContentInfo, SignedData}, + std::collections::{BTreeSet, HashMap}, +}; + +#[derive(Debug, Clone)] +pub struct SOD { + pub version: u32, + pub digest_algorithms: Vec, + pub encap_content_info: EncapContentInfo, + pub signer_info: SignerInfo, + pub certificate: DSC, + pub bytes: Binary, +} + +impl SOD { + /// Parses the `signedAttrs` field from a `SignerInfo`. + fn parse_signed_attrs( + signer_info_raw: &rasn_cms::SignerInfo, + registry: &HashMap<&'static str, OidEntry>, + ) -> Result { + let mut signed_attr_map: HashMap = HashMap::new(); + let mut reconstructed_signed_attrs: Vec = Vec::new(); + + let attrs = + signer_info_raw + .signed_attrs + .as_ref() + .ok_or(PassportError::MissingRequiredField( + "signedAttrs".to_string(), + ))?; + + for attr in attrs { + let oid_str = oid_to_string(&attr.r#type); + let name = get_oid_name(&oid_str, registry); + let val = attr + .values + .first() + .ok_or(PassportError::DataNotFound(format!( + "No value in attribute with OID: {}", + oid_str + )))? + .as_bytes(); + signed_attr_map.insert(name, Binary::from_slice(val)); + reconstructed_signed_attrs.push(attr.clone()); + } + + let signed_attrs_set = BTreeSet::from_iter(reconstructed_signed_attrs); + let reconstructed_block = der::encode(&signed_attrs_set) + .map_err(|e| PassportError::Asn1DecodingFailed(e.to_string()))?; + + let message_digest = signed_attr_map + .get("messageDigest") + .ok_or(PassportError::MissingRequiredField( + "messageDigest".to_string(), + ))? + .clone(); + + let signing_time = signed_attr_map + .get("signingTime") + .map(|time_attr| { + der::decode::(&time_attr.data) + .map_err(|e| PassportError::Asn1DecodingFailed(e.to_string())) + }) + .transpose()?; + + let content_type_bytes = + signed_attr_map + .get("contentType") + .ok_or(PassportError::MissingRequiredField( + "contentType".to_string(), + ))?; + + let content_type_oid: rasn::types::ObjectIdentifier = der::decode(&content_type_bytes.data) + .map_err(|e| PassportError::Asn1DecodingFailed(e.to_string()))?; + let oid_string = oid_to_string(&content_type_oid); + + Ok(SignedAttrs { + bytes: Binary::from_slice(&reconstructed_block), + content_type: get_oid_name(&oid_string, registry), + message_digest, + signing_time, + }) + } + + /// Extracts and parses the DSC (Document Signer Certificate) from a + /// `SignedData` structure. + fn parse_certificate(signed_data: &SignedData) -> Result { + let certificates = + signed_data + .certificates + .as_ref() + .ok_or(PassportError::MissingRequiredField( + "certificates".to_string(), + ))?; + if certificates.is_empty() { + return Err(PassportError::MissingRequiredField( + "DSC certificate".to_string(), + )); + } + + let dsc = certificates + .first() + .ok_or(PassportError::X509ParsingFailed( + "Failed to extract X.509 Certificate".to_string(), + ))?; + + let dsc_cert = match dsc { + rasn_cms::CertificateChoices::Certificate(c) => c, + _ => return Err(PassportError::InvalidCertificateType), + }; + let dsc_der = der::encode(&**dsc_cert) + .map_err(|e| PassportError::X509ParsingFailed(e.to_string()))?; + let dsc_binary = Binary::from_slice(&dsc_der); + DSC::from_der(&dsc_binary) + } + + /// Parses the encapsulated LDS Security Object (`encapContentInfo`) from + /// the SOD. + fn parse_encap_content_info( + signed_data: &SignedData, + registry: &HashMap<&'static str, OidEntry>, + ) -> Result { + let econtent_bytes = signed_data + .encap_content_info + .content + .as_ref() + .ok_or(PassportError::MissingRequiredField("eContent".to_string()))?; + + let econtent: LDSSecurityObject = der::decode(econtent_bytes) + .map_err(|e| PassportError::Asn1DecodingFailed(e.to_string()))?; + + let content_type = &signed_data.encap_content_info.content_type; + let econtent_oid = get_oid_name(&oid_to_string(content_type), registry); + let econtent_vec = signed_data.encap_content_info.content.clone().ok_or( + PassportError::MissingRequiredField("eContent data".to_string()), + )?; + let econtent_binary = Binary::from_slice(&econtent_vec); + let hash_algorithm_oid = oid_to_string(&econtent.hash_algorithm.algorithm); + let hash_algorithm_name = get_hash_algo_name(&hash_algorithm_oid, registry); + + let hash_algorithm = DigestAlgorithm::from_name(&hash_algorithm_name).ok_or( + PassportError::UnsupportedDigestAlgorithm(hash_algorithm_name), + )?; + + let mut data_group_hash_values_map = DataGroupHashValues { + values: HashMap::new(), + }; + + let mut sorted_data_groups: Vec<_> = econtent.data_group_hash_values.into_iter().collect(); + sorted_data_groups.sort_by_key(|dg| version_from(&dg.data_group_number)); + + for data_group in sorted_data_groups { + let dg_number = version_from(&data_group.data_group_number); + let hash_value = Binary::from_slice(&data_group.data_group_hash_value); + data_group_hash_values_map + .values + .insert(dg_number, hash_value); + } + + Ok(EncapContentInfo { + e_content_type: econtent_oid, + e_content: EContent { + version: version_from(&econtent.version), + hash_algorithm, + data_group_hash_values: data_group_hash_values_map, + bytes: econtent_binary, + }, + }) + } + + /// Parses a `SignerInfo` structure into a custom `SignerInfo` model. + fn parse_signer_info( + signer_info_raw: &rasn_cms::SignerInfo, + registry: &HashMap<&'static str, OidEntry>, + ) -> Result { + let signed_attrs = Self::parse_signed_attrs(signer_info_raw, registry)?; + let signer_version = version_from(&signer_info_raw.version); + + let digest_oid_str = oid_to_string(&signer_info_raw.digest_algorithm.algorithm); + let digest_name = get_oid_name(&digest_oid_str, registry); + let signed_digest_algorithm_oid = DigestAlgorithm::from_name(&digest_name) + .ok_or(PassportError::UnsupportedDigestAlgorithm(digest_name))?; + + let signature_algorithm_oid = oid_to_string(&signer_info_raw.signature_algorithm.algorithm); + let signature_algorithm = SignatureAlgorithmName::from_oid(&signature_algorithm_oid) + .ok_or(PassportError::UnsupportedSignatureAlgorithm( + signature_algorithm_oid, + ))?; + + let signature_parameters = signer_info_raw + .signature_algorithm + .parameters + .as_ref() + .map(|p| Binary::from_slice(p.as_bytes())); + + let signature = Binary::from_slice(&signer_info_raw.signature); + let signer_identifier = Self::parse_signer_identifier(signer_info_raw.sid.clone()); + let signing_time = signed_attrs.signing_time.and_then(|ut| { + let time_str = ut.to_string(); + chrono::DateTime::parse_from_rfc3339(&format!("{}T00:00:00Z", time_str)) + .ok() + .map(|dt| dt.with_timezone(&chrono::Utc)) + }); + Ok(SignerInfo { + version: signer_version, + signed_attrs: SignedAttrs { + content_type: signed_attrs.content_type, + message_digest: signed_attrs.message_digest, + signing_time, + bytes: signed_attrs.bytes, + }, + digest_algorithm: signed_digest_algorithm_oid, + signature_algorithm: SignatureAlgorithm { + name: signature_algorithm, + parameters: signature_parameters, + }, + signature, + sid: signer_identifier, + }) + } + + /// Parses the signer identifier (SID) from the `SignerInfo`. + fn parse_signer_identifier(sid: rasn_cms::SignerIdentifier) -> SignerIdentifier { + match sid { + rasn_cms::SignerIdentifier::IssuerAndSerialNumber(issuer_and_serial) => { + let rasn_pkix::Name::RdnSequence(rdn_sequence) = &issuer_and_serial.issuer; + let issuer_dn = rdn_sequence + .iter() + .flat_map(|rdn| rdn.iter()) + .map(|attr| { + let oid_str = oid_to_string(&attr.r#type); + let value_str = std::str::from_utf8(attr.value.as_bytes()) + .map(String::from) + .unwrap_or_else(|_| hex::encode(attr.value.as_bytes())); + let field_name = match oid_str.as_str() { + "2.5.4.3" => "CN", + "2.5.4.6" => "C", + "2.5.4.7" => "L", + "2.5.4.8" => "ST", + "2.5.4.9" => "STREET", + "2.5.4.10" => "O", + "2.5.4.11" => "OU", + _ => &oid_str, + }; + format!("{}={}", field_name, value_str) + }) + .collect::>() + .join(", "); + let serial_number = + Binary::from_slice(&issuer_and_serial.serial_number.to_bytes_be().1); + SignerIdentifier { + issuer_and_serial_number: Some(IssuerAndSerialNumber { + issuer: issuer_dn, + serial_number, + }), + subject_key_identifier: None, + } + } + rasn_cms::SignerIdentifier::SubjectKeyIdentifier(ski) => SignerIdentifier { + issuer_and_serial_number: None, + subject_key_identifier: Some(hex::encode(&ski)), + }, + } + } + + /// Entry point: parses a full SOD (Security Object Document) from raw DER + /// bytes. + pub fn from_der(binary: &mut Binary) -> Result { + *binary = strip_length_prefix(binary); + let content_info: ContentInfo = der::decode(&binary.data) + .map_err(|e| PassportError::CmsParsingFailed(e.to_string()))?; + let signed_data: SignedData = der::decode(content_info.content.as_bytes()) + .map_err(|e| PassportError::CmsParsingFailed(e.to_string()))?; + + if signed_data.signer_infos.is_empty() { + return Err(PassportError::DataNotFound( + "No SignerInfos found".to_string(), + )); + } + + let signer_info_raw = signed_data + .signer_infos + .first() + .ok_or(PassportError::DataNotFound( + "No SignerInfo found".to_string(), + ))? + .clone(); + + let digest_algorithms: Vec = signed_data + .digest_algorithms + .iter() + .filter_map(|alg| { + let oid_str = oid_to_string(&alg.algorithm); + let name = get_hash_algo_name(&oid_str, ®ISTRY); + DigestAlgorithm::from_name(&name) + }) + .collect(); + + let certificate = Self::parse_certificate(&signed_data)?; + let encap_content_info = Self::parse_encap_content_info(&signed_data, ®ISTRY)?; + let signer_info = Self::parse_signer_info(&signer_info_raw, ®ISTRY)?; + let sod_version = version_from(&signed_data.version); + + Ok(SOD { + version: sod_version, + digest_algorithms, + encap_content_info, + signer_info, + certificate, + bytes: binary.clone(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const FIXTURE_EF_SOD: &str = "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"; + + fn parse_sod() -> SOD { + let mut sod_bytes = Binary::from_base64(FIXTURE_EF_SOD).unwrap(); + SOD::from_der(&mut sod_bytes).unwrap() + } + + #[test] + fn should_parse_basic_sod_properties() { + let sod = parse_sod(); + assert_eq!(sod.version, 3); + assert_eq!(sod.digest_algorithms.len(), 1); + assert!(matches!(sod.digest_algorithms[0], DigestAlgorithm::SHA256)); + } + + #[test] + fn should_parse_econtent_data_correctly() { + let sod = parse_sod(); + let econtent = &sod.encap_content_info.e_content; + assert_eq!(econtent.version, 0); + assert!(matches!(econtent.hash_algorithm, DigestAlgorithm::SHA256)); + let dg_hashes = &econtent.data_group_hash_values.values; + assert_eq!(dg_hashes.len(), 5); + assert_eq!( + dg_hashes.get(&1).unwrap().to_hex(), + "0x4170ca879fce6a22ffef1567ff88079f415c66ead250ab5f23781ac2cdbf42b6" + ); + assert_eq!( + dg_hashes.get(&2).unwrap().to_hex(), + "0xa9a1b09dfd598087ab3fce4ae2ec65b1a1525bd258bfc27df4419f8a65e54745" + ); + } + + #[test] + fn should_parse_signer_info_correctly() { + let sod = parse_sod(); + let signer = &sod.signer_info; + assert_eq!(signer.version, 1); + assert!(matches!(signer.digest_algorithm, DigestAlgorithm::SHA256)); + assert!(matches!( + signer.signature_algorithm.name, + SignatureAlgorithmName::RsassaPss + )); + assert_eq!(signer.signed_attrs.content_type, "mRTDSignatureData"); + assert_eq!( + signer.signed_attrs.message_digest.to_hex(), + "0x0420b46a0d05e280f398efeeebff67e78c736add15e75670b1ad4c6c534e8187b9d6" + ); + } + + #[test] + fn should_parse_certificate_information_correctly() { + let sod = parse_sod(); + let cert = &sod.certificate; + let tbs = &cert.tbs; + + let expected_not_before = chrono::DateTime::parse_from_rfc3339("2013-12-16T21:43:18+00:00") + .unwrap() + .with_timezone(&chrono::Utc); + let expected_not_after = chrono::DateTime::parse_from_rfc3339("2014-12-11T21:43:18+00:00") + .unwrap() + .with_timezone(&chrono::Utc); + + assert_eq!(tbs.validity_not_before, expected_not_before); + assert_eq!(tbs.validity_not_after, expected_not_after); + assert_eq!( + tbs.issuer, + "countryName=DE, organizationName=HJP Consulting, organizationalUnitName=Country \ + Signer, commonName=HJP PB CS" + ); + assert_eq!( + tbs.subject, + "countryName=DE, organizationName=HJP Consulting, organizationalUnitName=Document \ + Signer, commonName=HJP PB DS" + ); + assert!(tbs.extensions.contains_key("keyUsage")); + assert!(tbs.extensions.contains_key("authorityKeyIdentifier")); + assert!(tbs.extensions.contains_key("subjectKeyIdentifier")); + assert!(tbs.extensions.get("keyUsage").unwrap().0); + } + + #[test] + fn should_parse_signature_algorithms_correctly() { + let sod = parse_sod(); + let cert = &sod.certificate; + assert!(matches!( + cert.signature_algorithm.name, + SignatureAlgorithmName::RsassaPss + )); + assert!(matches!( + cert.tbs.subject_public_key_info.signature_algorithm.name, + SignatureAlgorithmName::RsaEncryption + )); + assert!(!cert.signature.is_empty()); + assert!(!sod.signer_info.signature.is_empty()); + } +} diff --git a/playground/passport-input-gen/src/parser/types.rs b/playground/passport-input-gen/src/parser/types.rs new file mode 100644 index 000000000..fa3e93f85 --- /dev/null +++ b/playground/passport-input-gen/src/parser/types.rs @@ -0,0 +1,217 @@ +use { + crate::parser::binary::Binary, + chrono::{DateTime, Utc}, + rasn::{ + types::{Integer, OctetString, PrintableString, SequenceOf}, + AsnType, Decode, Encode, + }, + rasn_pkix::AlgorithmIdentifier, + std::collections::HashMap, + thiserror::Error, +}; + +pub const MAX_SIGNED_ATTRIBUTES_SIZE: usize = 200; +pub const MAX_DG1_SIZE: usize = 95; +pub const SIG_BYTES: usize = 256; +pub const MAX_ECONTENT_SIZE: usize = 200; +pub const MAX_TBS_SIZE: usize = 1500; + +#[derive(Debug, Clone)] +pub enum DigestAlgorithm { + SHA1, + SHA224, + SHA256, + SHA384, + SHA512, +} + +impl DigestAlgorithm { + pub fn from_name(name: &str) -> Option { + match name.to_uppercase().as_str() { + "SHA1" | "SHA-1" => Some(Self::SHA1), + "SHA224" | "SHA-224" => Some(Self::SHA224), + "SHA256" | "SHA-256" => Some(Self::SHA256), + "SHA384" | "SHA-384" => Some(Self::SHA384), + "SHA512" | "SHA-512" => Some(Self::SHA512), + _ => None, + } + } +} + +#[derive(Debug, Clone)] +pub struct DataGroupHashValues { + pub values: HashMap, +} + +#[derive(Debug, Clone)] +pub struct EContent { + pub version: u32, + pub hash_algorithm: DigestAlgorithm, + pub data_group_hash_values: DataGroupHashValues, + pub bytes: Binary, +} + +#[derive(Debug, Clone)] +pub struct EncapContentInfo { + pub e_content_type: String, + pub e_content: EContent, +} + +#[derive(Debug, Clone)] +pub struct SignerInfo { + pub version: u32, + pub signed_attrs: SignedAttrs, + pub digest_algorithm: DigestAlgorithm, + pub signature_algorithm: SignatureAlgorithm, + pub signature: Binary, + pub sid: SignerIdentifier, +} + +#[derive(Debug, Clone)] +pub struct SignedAttrs { + pub content_type: String, + pub message_digest: Binary, + pub signing_time: Option>, + pub bytes: Binary, +} + +#[derive(Debug, Clone)] +pub struct SignerIdentifier { + pub issuer_and_serial_number: Option, + pub subject_key_identifier: Option, +} + +#[derive(Debug, Clone)] +pub struct IssuerAndSerialNumber { + pub issuer: String, + pub serial_number: Binary, +} + +#[derive(Debug, Clone)] +pub struct SignatureAlgorithm { + pub name: SignatureAlgorithmName, + pub parameters: Option, +} + +#[derive(Debug, Clone)] +pub enum SignatureAlgorithmName { + Sha1WithRsaSignature, + Sha256WithRsaEncryption, + Sha384WithRsaEncryption, + Sha512WithRsaEncryption, + RsassaPss, + EcdsaWithSha1, + EcdsaWithSha256, + EcdsaWithSha384, + EcdsaWithSha512, + RsaEncryption, + EcPublicKey, +} + +impl SignatureAlgorithmName { + pub fn from_oid(oid: &str) -> Option { + match oid { + "1.2.840.113549.1.1.5" => Some(Self::Sha1WithRsaSignature), + "1.2.840.113549.1.1.11" => Some(Self::Sha256WithRsaEncryption), + "1.2.840.113549.1.1.12" => Some(Self::Sha384WithRsaEncryption), + "1.2.840.113549.1.1.13" => Some(Self::Sha512WithRsaEncryption), + "1.2.840.113549.1.1.10" => Some(Self::RsassaPss), + "1.2.840.10045.4.1" => Some(Self::EcdsaWithSha1), + "1.2.840.10045.4.3.2" => Some(Self::EcdsaWithSha256), + "1.2.840.10045.4.3.3" => Some(Self::EcdsaWithSha384), + "1.2.840.10045.4.3.4" => Some(Self::EcdsaWithSha512), + "1.2.840.113549.1.1.1" => Some(Self::RsaEncryption), + "1.2.840.10045.2.1" => Some(Self::EcPublicKey), + _ => None, + } + } +} + +/// DataGroupNumber ::= INTEGER (1..16) +pub type DataGroupNumber = Integer; + +/// DataGroupHash ::= SEQUENCE { +/// dataGroupNumber DataGroupNumber, +/// dataGroupHashValue OCTET STRING +/// } +#[derive(Debug, Clone, AsnType, Decode, Encode)] +pub struct DataGroupHash { + pub data_group_number: DataGroupNumber, + pub data_group_hash_value: OctetString, +} + +/// LDSVersionInfo ::= SEQUENCE { +/// ldsVersion PrintableString, +/// unicodeVersion PrintableString +/// } +#[derive(Debug, Clone, AsnType, Decode, Encode)] +pub struct LDSVersionInfo { + pub lds_version: PrintableString, + pub unicode_version: PrintableString, +} + +/// LDSSecurityObject ::= SEQUENCE { +/// version INTEGER { v0(0), v1(1), v2(2) }, +/// hashAlgorithm DigestAlgorithmIdentifier, +/// dataGroupHashValues SEQUENCE SIZE (2..ub-DataGroups) OF DataGroupHash, +/// ldsVersionInfo LDSVersionInfo OPTIONAL +/// } +#[derive(Debug, Clone, AsnType, Decode, Encode)] +pub struct LDSSecurityObject { + pub version: Integer, + pub hash_algorithm: AlgorithmIdentifier, + pub data_group_hash_values: SequenceOf, + pub lds_version_info: Option, +} + +#[derive(Debug, Error)] +pub enum PassportError { + #[error("DG1 hash mismatch in eContent")] + Dg1HashMismatch, + #[error("eContent hash mismatch in SignedAttributes")] + EcontentHashMismatch, + #[error("Invalid DSC public key")] + InvalidDscKey, + #[error("DSC signature verification failed")] + DscSignatureInvalid, + #[error("Failed to load CSCA keys")] + CscaKeysMissing, + #[error("No USA CSCA keys found")] + NoUsaCsca, + #[error("CSCA signature verification failed")] + CscaSignatureInvalid, + #[error("DSC Public key invalid")] + DscPublicKeyInvalid, + #[error("CSCA Public key invalid")] + CscaPublicKeyInvalid, + #[error("Data too large for buffer: {0}")] + BufferOverflow(String), + #[error("RSA exponent too large")] + RsaExponentTooLarge, + #[error("Required data not found: {0}")] + DataNotFound(String), + #[error("Unsupported signature algorithm: {0}")] + UnsupportedSignatureAlgorithm(String), + #[error("CMS parsing failed: {0}")] + CmsParsingFailed(String), + #[error("X.509 certificate parsing failed: {0}")] + X509ParsingFailed(String), + #[error("ASN.1 decoding failed: {0}")] + Asn1DecodingFailed(String), + #[error("Base64 decoding failed: {0}")] + Base64DecodingFailed(String), + #[error("Missing required field: {0}")] + MissingRequiredField(String), + #[error("Invalid certificate type")] + InvalidCertificateType, + #[error("Missing DG1 hash in eContent")] + MissingDg1Hash, + #[error("Missing CSCA public key for mock data")] + MissingCscaMockKey, + #[error("Failed to load CSCA public keys")] + FailedToLoadCscaKeys, + #[error("Invalid date: {0}")] + InvalidDate(String), + #[error("Unsupported digest algorithm: {0}")] + UnsupportedDigestAlgorithm(String), +} diff --git a/playground/passport-input-gen/src/parser/utils.rs b/playground/passport-input-gen/src/parser/utils.rs new file mode 100644 index 000000000..65b54f70e --- /dev/null +++ b/playground/passport-input-gen/src/parser/utils.rs @@ -0,0 +1,110 @@ +use { + crate::parser::{binary::Binary, types::PassportError}, + serde::Deserialize, + std::{collections::HashMap, fs}, +}; + +#[derive(Debug, Clone)] +pub struct OidEntry { + pub d: &'static str, + pub c: &'static str, + pub w: bool, +} + +pub fn get_oid_name(oid: &str, registry: &HashMap<&'static str, OidEntry>) -> String { + if let Some(entry) = registry.get(oid) { + entry.d.to_string() + } else { + oid.to_string() + } +} + +pub fn get_hash_algo_name(oid: &str, registry: &HashMap<&'static str, OidEntry>) -> String { + if let Some(entry) = registry.get(oid) { + entry.d.replace("-", "").to_uppercase() + } else { + oid.to_string() + } +} + +pub fn oid_to_string(oid: &rasn::types::ObjectIdentifier) -> String { + oid.iter() + .map(|v| v.to_string()) + .collect::>() + .join(".") +} + +pub fn strip_length_prefix(binary: &Binary) -> Binary { + if binary.slice(0, 2).equals(&Binary::new(vec![119, 130])) { + binary.slice(4, binary.len()) + } else { + binary.clone() + } +} + +pub fn version_from(value: &rasn::types::Integer) -> u32 { + value.to_u32_digits().1.first().copied().unwrap_or(0) +} + +pub fn fit(data: &[u8]) -> Result<[u8; N], PassportError> { + if data.len() > N { + return Err(PassportError::BufferOverflow(format!( + "data size {} exceeds buffer size {}", + data.len(), + N + ))); + } + let mut buf = [0u8; N]; + buf[..data.len()].copy_from_slice(data); + Ok(buf) +} + +#[derive(Deserialize)] +pub struct CscaKey { + #[serde(rename = "filename")] + pub _filename: String, + pub public_key: String, + // pub subject: String, + #[serde(rename = "notBefore")] + pub _not_before: String, + #[serde(rename = "notAfter")] + pub _not_after: String, + #[serde(rename = "serial")] + pub _serial: String, +} + +pub const ASN1_OCTET_STRING_TAG: u8 = 0x04; +pub const ASN1_HEADER_LEN: usize = 2; + +pub fn load_csca_public_keys() -> Result>, Box> +{ + let path = "csca_registry/csca_public_key.json"; + let file_content = fs::read_to_string(path)?; + let csca_keys: HashMap> = serde_json::from_str(&file_content)?; + Ok(csca_keys) +} + +pub fn to_fixed_array(bytes: &[u8], label: &str) -> Result<[u8; N], PassportError> { + bytes.try_into().map_err(|_| { + PassportError::BufferOverflow(format!( + "{label} must be exactly {N} bytes, got {}", + bytes.len() + )) + }) +} + +pub fn to_u32(bytes: Vec) -> Result { + if bytes.len() > 4 { + return Err(PassportError::RsaExponentTooLarge); + } + let mut buf = [0u8; 4]; + buf[4 - bytes.len()..].copy_from_slice(&bytes); + Ok(u32::from_be_bytes(buf)) +} + +pub fn find_offset(haystack: &[u8], needle: &[u8], label: &str) -> Result { + haystack + .windows(needle.len()) + .position(|w| w == needle) + .ok_or_else(|| PassportError::DataNotFound(label.to_string())) +} diff --git a/playground/passport-input-gen/src/prover_config.rs b/playground/passport-input-gen/src/prover_config.rs deleted file mode 100644 index 09f0780dc..000000000 --- a/playground/passport-input-gen/src/prover_config.rs +++ /dev/null @@ -1,115 +0,0 @@ -use { - crate::{ - constants::{ - CSC_PUBKEY, CSC_PUBKEY_MU, DSC_CERT, DSC_CERT_SIGNATURE_BYTES, DSC_MU_BYTES, - DSC_P_BYTES, DSC_Q_BYTES, DSC_RSA_PUBKEY_BYTES, PASSPORT_SOD_SIZE, SOD_CERT_SIZE, - }, - crypto::generate_rsa_signature_pkcs_from_priv_key, - }, - std::iter::repeat_n, -}; - -/// Assuming here that we use all the RSA keys from `zkpassport_constants`. -/// This essentially generates the struct `PassportValidityContents`. -pub fn generate_passport_validity_contents_prover_toml( - passport_sod: &[u8; 700], - sod_cert: &[u8; 200], - sod_cert_signature_bytes: &[u8; 256], -) -> String { - let mut prover_toml_str = String::from("[passport_validity_contents]\n\n"); - - // --- Purely passport DG1/SOD stuff --- - prover_toml_str += &format!("passport_sod = {:?}\n\n", passport_sod); - prover_toml_str += &format!("passport_sod_size = {:?}\n\n", PASSPORT_SOD_SIZE); - - // --- DSC signature over signed attributes stuff --- - prover_toml_str += &format!("sod_cert = {:?}\n\n", sod_cert); - prover_toml_str += &format!("sod_cert_size = {:?}\n\n", SOD_CERT_SIZE); - - prover_toml_str += &format!("dsc_pubkey = {:?}\n\n", DSC_RSA_PUBKEY_BYTES); - prover_toml_str += &format!("dsc_barrett_mu = {:?}\n\n", DSC_MU_BYTES); - - prover_toml_str += &format!("sod_cert_signature = {:?}\n\n", sod_cert_signature_bytes); - prover_toml_str += &format!("dsc_rsa_exponent = {:?}\n\n", 65537); - - // --- CSC signature over DSC cert stuff --- - prover_toml_str += &format!("dsc_pubkey_offset_in_dsc_cert = {:?}\n\n", 0); - prover_toml_str += &format!("dsc_cert = {:?}\n\n", DSC_CERT); - prover_toml_str += &format!("dsc_cert_len = {:?}\n\n", 256); - prover_toml_str += &format!("csc_pubkey = {:?}\n\n", CSC_PUBKEY); - prover_toml_str += &format!("csc_barrett_mu = {:?}\n\n", CSC_PUBKEY_MU); - prover_toml_str += &format!("dsc_cert_signature = {:?}\n\n", DSC_CERT_SIGNATURE_BYTES); - prover_toml_str += &format!("csc_rsa_exponent = {:?}\n\n", 65537); - - prover_toml_str -} - -/// Note: both `birthdate_bytes` and `expiry_bytes` are in the form "YYMMDD". -pub fn dg1_bytes_with_birthdate_expiry_date( - birthdate_bytes: &[u8; 6], - expiry_bytes: &[u8; 6], -) -> [u8; 95] { - let mut dg1_bytes = [1; 95]; - - // From Noir (we should double-check this with an actual passport): - // MRZ offset within DG1 is 5 - // Birthdate offset within MRZ is 57, with 6 bytes allocated - dg1_bytes[57 + 5..57 + 5 + 6].copy_from_slice(birthdate_bytes); - - // Expiry offset within MRZ is 65, with 6 bytes allocated - dg1_bytes[65 + 5..65 + 5 + 6].copy_from_slice(expiry_bytes); - - // Set final two bytes to be zero - dg1_bytes[93..].copy_from_slice(&[0, 0]); - - dg1_bytes -} - -/// Note: `current_date` format should be "YYYYMMDD" -pub fn generate_prover_toml_string_from_custom_dg1_date_and_required_age( - custom_dg1_bytes: &[u8; 95], - min_age_required: u8, - max_age_required: u8, - current_date: u64, -) -> String { - use sha2::{Digest, Sha256}; - - // Next, compute SHA-256 digest of this - let sha256_digest = Sha256::digest(custom_dg1_bytes); - - let passport_sod: Vec = sha256_digest - .into_iter() - .chain(repeat_n(0, 700 - 32)) - .collect(); - - let passport_sod_hash = Sha256::digest(&passport_sod); - let passport_signed_attributes: Vec = passport_sod_hash - .into_iter() - .chain(repeat_n(0, 200 - 32)) - .collect(); - - // Next, generate signature of the signed attributes - let passport_signed_attributes_signature_bytes = generate_rsa_signature_pkcs_from_priv_key( - &DSC_P_BYTES, - &DSC_Q_BYTES, - &passport_signed_attributes, - ); - - let mut prover_toml_str = format!("dg1_hash_offset_in_sod = {:?}\n\n", 0); - prover_toml_str += &format!("dg1 = {:?}\n\n", custom_dg1_bytes); - prover_toml_str += &format!("min_age_required = {:?}\n\n", min_age_required); - prover_toml_str += &format!("max_age_required = {:?}\n\n", max_age_required); - prover_toml_str += &format!("current_date = {:?}\n\n", current_date); - - // The DSC_CERT and everything afterwards should be deterministic, since - // we are not changing the DSC_KEY. - prover_toml_str += &generate_passport_validity_contents_prover_toml( - &passport_sod.try_into().unwrap(), - &passport_signed_attributes.try_into().unwrap(), - &passport_signed_attributes_signature_bytes - .try_into() - .unwrap(), - ); - - prover_toml_str -}