openpay/ (+3 crates, ~8400 LOC, 162 tests)
op-iso8583/ ISO 8583 messages for direct acquirer / card-network
connectivity. MTIs, primary+secondary bitmaps, full
DE catalogue, BCD/ASCII/EBCDIC/LLVAR/LLLVAR codecs,
five dialects (Visa Base I, Mastercard MDS, Amex GNS,
Discover, JCB), network-management messages
(sign-on/echo/cutover), DE 55 EMV TLV.
op-mobile-wallets/ Apple Pay (ECDH-P256 + AES-256-GCM), Google Pay ECv2
(ECDH + HKDF + AES-256-CTR + HMAC), Samsung Pay. PAN
never leaves the decryption boundary as a String;
lands in VaultRef. No-PAN-leakage assertions per
wallet.
op-routing/ Least-cost routing + MCC-aware routing + intelligent
retry / soft-decline recovery. 231-MCC catalogue.
Real ISO 8583 DE-39 soft/hard decline taxonomies.
smart-byte/smart-byte-rs/ (+3 crates, ~7100 LOC, 96 tests)
smart-byte-vc/ W3C Verifiable Credentials 2.0: VCDM, presentations,
Data Integrity proofs (eddsa-jcs-2022), VC-JWT,
SD-JWT (selective disclosure), Bitstring Status
List 2021. Cargo-bridge maps VCs to Smart Byte
envelopes with SAID stability.
smart-byte-did/ W3C DID resolution: did:key (Ed25519/P-256/secp256k1),
did:web (HTTPS), did:peer (numalgo 2+4), did:jwk,
did:ion (feature-gated stub). DID URL dereferencer.
smart-byte-pq/ Post-quantum signatures: ML-DSA-44/65/87 (FIPS 204),
SLH-DSA across 12 parameter sets (FIPS 205), FN-DSA
(draft FIPS 206, feature-gated). Hybrid Ed25519 +
ML-DSA pattern for the transition. Secret-key
zeroize-on-drop.
eoc/eoc-rs/ (+3 crates, ~6450 LOC, 76 tests)
eoc-vendor-api/ NeuralBackend implementations: Anthropic, OpenAI,
Google, Mistral, Cohere, Groq, Together, Fireworks.
SSE streaming. Prompt-caching where supported.
Joule estimates per published energy coefficient
(12-model curated table).
eoc-embeddings/ 21 embedding models behind one Embedder trait:
OpenAI v3 (3), Cohere v3 (3), Voyage (5), Mistral,
Jina, plus local ONNX (BGE x3, nomic, mxbai, GTE,
E5) behind 'local' feature. Matryoshka-style
dimension projection. Per-content-hash cache.
eoc-route-learned/ RouteLLM-style matrix-factorisation router +
multinomial-logreg classifier + LinUCB / Thompson
bandits for online learning. LearnedCascade wraps
the rule-based cascade with a confidence-threshold
skip policy. Synthetic-eval accuracy: MF 0.935,
LogReg 0.980.
Quality:
- cargo check across all three workspaces: clean
- cargo clippy --all-targets -- -D warnings on every new crate: clean
- 334 new tests total; all passing
- #![forbid(unsafe_code)] across every new crate
- No unwrap/expect outside #[cfg(test)]
Rust 1.95.0 edition 2024 (inherited via root rust-toolchain.toml).
213 lines
7.2 KiB
Rust
213 lines
7.2 KiB
Rust
//! No-PAN-leakage assertions on the public API surface.
|
|
//!
|
|
//! The PCI-zero invariant for this crate: a cleartext device-PAN must
|
|
//! not appear as `String`, `&str`, `Vec<u8>`, or `&[u8]` on any public
|
|
//! method's return type after decryption. The only credential exit
|
|
//! point is [`op_core::VaultRef`].
|
|
//!
|
|
//! These tests *behaviorally* show that:
|
|
//!
|
|
//! 1. After a full successful decrypt, the public surface of
|
|
//! [`DecryptedToken`] does not contain the original cleartext DPAN.
|
|
//! We check this by serializing the entire `DecryptedToken` to JSON
|
|
//! and searching for the DPAN bytes — the rendered bytes must not
|
|
//! contain them.
|
|
//! 2. The `Debug` impl on the contained `VaultRef` masks its value
|
|
//! (only first 4 chars + length).
|
|
//! 3. Every wallet (Apple, Google, Samsung) round-trips through the
|
|
//! same invariant.
|
|
//!
|
|
//! Static (type-level) assertions are enforced by the absence of any
|
|
//! `pub fn` in this crate that returns the cleartext DPAN — see
|
|
//! `cargo doc -p op-mobile-wallets` for the rendered public surface.
|
|
|
|
use op_mobile_wallets::apple_pay::{ApplePayConfig, CertChain, ExpirationDate, test_support as ap_ts};
|
|
use op_mobile_wallets::google_pay::{GooglePayConfig, test_support as gp_ts};
|
|
use op_mobile_wallets::samsung_pay::{SamsungPayConfig, test_support as sp_ts};
|
|
use op_mobile_wallets::wallet::{IdentityVaultTokenizer, Sha256VaultTokenizer, Wallet, WalletConfig, decryptor_for};
|
|
use p256::SecretKey;
|
|
use p256::elliptic_curve::rand_core::OsRng;
|
|
use sha2::{Digest, Sha256};
|
|
use std::sync::Arc;
|
|
|
|
const DPAN: &str = "4012888888881881";
|
|
|
|
#[test]
|
|
fn apple_pay_no_pan_in_serialized_output() {
|
|
let priv_key = SecretKey::random(&mut OsRng);
|
|
let mut hash = [0u8; 32];
|
|
for (i, b) in b"merchant.com.openpay.test".iter().enumerate() {
|
|
hash[i % 32] ^= *b;
|
|
}
|
|
let chain = CertChain::new(true, 2_000_000_000, hash);
|
|
let cfg = ApplePayConfig {
|
|
merchant_id_hash: hash,
|
|
cert_chain: chain,
|
|
ephemeral_private_key: priv_key.clone(),
|
|
now_unix: 1_700_000_000,
|
|
};
|
|
let tokenizer: Arc<dyn op_mobile_wallets::wallet::VaultTokenizer> =
|
|
Arc::new(Sha256VaultTokenizer);
|
|
let wcfg = WalletConfig {
|
|
apple_pay: Some(cfg),
|
|
google_pay: None,
|
|
samsung_pay: None,
|
|
vault_tokenizer: tokenizer,
|
|
};
|
|
let decryptor = decryptor_for(Wallet::ApplePay, &wcfg).unwrap();
|
|
let token = ap_ts::build_token(
|
|
&priv_key,
|
|
&hash,
|
|
DPAN,
|
|
"271231",
|
|
"840",
|
|
1234,
|
|
"EMV",
|
|
"QUFBQQ==",
|
|
Some("05"),
|
|
"txn-001",
|
|
);
|
|
let raw = serde_json::to_vec(&token).unwrap();
|
|
let out = decryptor.decrypt(&raw).unwrap();
|
|
|
|
// 1. Vault ref does NOT contain the cleartext DPAN.
|
|
assert!(!out.application_primary_account_number.as_str().contains(DPAN));
|
|
// 2. Whole-struct JSON does NOT contain the cleartext DPAN.
|
|
let dbg_json = serde_json::to_string(&out).unwrap();
|
|
assert!(!dbg_json.contains(DPAN));
|
|
// 3. Debug rendering masks the vault ref.
|
|
let debug_str = format!("{:?}", out.application_primary_account_number);
|
|
assert!(!debug_str.contains(DPAN));
|
|
assert!(debug_str.starts_with("VaultRef("));
|
|
}
|
|
|
|
#[test]
|
|
fn google_pay_no_pan_in_serialized_output() {
|
|
let setup = gp_ts::fresh_setup();
|
|
let cfg = GooglePayConfig {
|
|
root_signing_keys: vec![setup.root_pub],
|
|
sender_id: "Google".into(),
|
|
recipient_id: "merchant:test".into(),
|
|
recipient_private_key: setup.recipient_priv.clone(),
|
|
now_unix_millis: 1_700_000_000_000,
|
|
};
|
|
let tokenizer: Arc<dyn op_mobile_wallets::wallet::VaultTokenizer> =
|
|
Arc::new(Sha256VaultTokenizer);
|
|
let wcfg = WalletConfig {
|
|
apple_pay: None,
|
|
google_pay: Some(cfg),
|
|
samsung_pay: None,
|
|
vault_tokenizer: tokenizer,
|
|
};
|
|
let decryptor = decryptor_for(Wallet::GooglePay, &wcfg).unwrap();
|
|
let token = gp_ts::build_token(
|
|
&setup,
|
|
"Google",
|
|
"merchant:test",
|
|
DPAN,
|
|
2030,
|
|
12,
|
|
"CRYPTOGRAM_3DS",
|
|
Some("QUFBQQ=="),
|
|
Some("05"),
|
|
"USD",
|
|
4242,
|
|
1_900_000_000_000,
|
|
1_900_000_000_000,
|
|
);
|
|
let raw = serde_json::to_vec(&token).unwrap();
|
|
let out = decryptor.decrypt(&raw).unwrap();
|
|
|
|
assert!(!out.application_primary_account_number.as_str().contains(DPAN));
|
|
let dbg_json = serde_json::to_string(&out).unwrap();
|
|
assert!(!dbg_json.contains(DPAN));
|
|
}
|
|
|
|
#[test]
|
|
fn samsung_pay_no_pan_in_serialized_output() {
|
|
let priv_key = SecretKey::random(&mut OsRng);
|
|
let merchant_id = "samsung-merchant-test".to_string();
|
|
let mut h = Sha256::new();
|
|
h.update(merchant_id.as_bytes());
|
|
let hash: [u8; 32] = h.finalize().into();
|
|
let chain = CertChain::new(true, 2_000_000_000, hash);
|
|
let cfg = SamsungPayConfig {
|
|
merchant_id: merchant_id.clone(),
|
|
cert_chain: chain,
|
|
recipient_private_key: priv_key.clone(),
|
|
now_unix: 1_700_000_000,
|
|
};
|
|
let tokenizer: Arc<dyn op_mobile_wallets::wallet::VaultTokenizer> =
|
|
Arc::new(Sha256VaultTokenizer);
|
|
let wcfg = WalletConfig {
|
|
apple_pay: None,
|
|
google_pay: None,
|
|
samsung_pay: Some(cfg),
|
|
vault_tokenizer: tokenizer,
|
|
};
|
|
let decryptor = decryptor_for(Wallet::SamsungPay, &wcfg).unwrap();
|
|
let token = sp_ts::build_token(
|
|
&priv_key,
|
|
&merchant_id,
|
|
DPAN,
|
|
"280630",
|
|
"840",
|
|
9999,
|
|
"EMV",
|
|
"QUFBQQ==",
|
|
Some("02"),
|
|
"txn-samsung",
|
|
);
|
|
let raw = serde_json::to_vec(&token).unwrap();
|
|
let out = decryptor.decrypt(&raw).unwrap();
|
|
|
|
assert!(!out.application_primary_account_number.as_str().contains(DPAN));
|
|
let dbg_json = serde_json::to_string(&out).unwrap();
|
|
assert!(!dbg_json.contains(DPAN));
|
|
}
|
|
|
|
#[test]
|
|
fn identity_tokenizer_still_keeps_dpan_off_string_surface() {
|
|
// Even with the identity tokenizer (which base64-encodes DPAN as
|
|
// the vault id), the DPAN ASCII bytes don't appear as a `String`
|
|
// — they appear inside a base64 blob inside a `VaultRef`. So a
|
|
// text-search for the DPAN must NOT find it.
|
|
let priv_key = SecretKey::random(&mut OsRng);
|
|
let mut hash = [0u8; 32];
|
|
for (i, b) in b"identity-merchant".iter().enumerate() {
|
|
hash[i % 32] ^= *b;
|
|
}
|
|
let chain = CertChain::new(true, 2_000_000_000, hash);
|
|
let cfg = ApplePayConfig {
|
|
merchant_id_hash: hash,
|
|
cert_chain: chain,
|
|
ephemeral_private_key: priv_key.clone(),
|
|
now_unix: 1_700_000_000,
|
|
};
|
|
let tokenizer: Arc<dyn op_mobile_wallets::wallet::VaultTokenizer> =
|
|
Arc::new(IdentityVaultTokenizer);
|
|
let wcfg = WalletConfig {
|
|
apple_pay: Some(cfg),
|
|
google_pay: None,
|
|
samsung_pay: None,
|
|
vault_tokenizer: tokenizer,
|
|
};
|
|
let decryptor = decryptor_for(Wallet::ApplePay, &wcfg).unwrap();
|
|
let token = ap_ts::build_token(
|
|
&priv_key,
|
|
&hash,
|
|
DPAN,
|
|
"271231",
|
|
"840",
|
|
1,
|
|
"EMV",
|
|
"QUFB",
|
|
None,
|
|
"id-txn",
|
|
);
|
|
let raw = serde_json::to_vec(&token).unwrap();
|
|
let out = decryptor.decrypt(&raw).unwrap();
|
|
let json = serde_json::to_string(&out).unwrap();
|
|
assert!(!json.contains(DPAN));
|
|
let _ = ExpirationDate::parse_yymmdd("271231").unwrap();
|
|
}
|