Skip to content

sanlanka/opace2ee

Repository files navigation

opacE2EE

Open Protocol Auditor for End-to-End Encryption

opacE2EE is a framework that measures the security of E2EE messaging protocols across 9 dimensions, producing a scored audit report out of 100. Protocol authors implement a single Rust trait (ProtocolAdapter), run the audit, and get a deterministic, reproducible score.

All tests are fully transparent — anyone can read the test source, verify the scoring, and audit the results. No test depends on protocol-specific types or internal APIs. Every test operates exclusively through the ProtocolAdapter trait.

Quick Start

# Run the audit against the Signal protocol (default)
cargo run -- audit

# Run the audit against Signal with JSON and PDF reports
cargo run -- audit --json --pdf --output ./output/

# List available protocol adapters
cargo run -- list

The Signal adapter (libsignal-protocol) and the Olm/Megolm adapter (vodozemac) are included out of the box. Anyone can clone this repo and immediately run audits against either protocol.

# Run the audit against Olm/Megolm (Matrix)
cargo run -- audit --protocol olm

# Run the audit against Olm with JSON and PDF reports
cargo run -- audit --protocol olm --json --pdf --output ./output/

Using the Fourier adapter (optional)

The Fourier adapter requires the fourier crate (not included in this repo). If you have access to it:

cargo run --features fourier -- audit --protocol fourier

Audit Dimensions

opacE2EE measures protocols across 9 security dimensions. Each dimension is scored 0–10 and weighted to produce an overall score out of 100.

# Dimension Weight Tests What It Measures
1 Key Exchange 12% 5 X3DH session establishment, 3-DH fallback (no one-time prekey), signed prekey verification, session uniqueness
2 Forward Secrecy 15% 5 Chain key ratcheting per message, root key advancement on DH ratchet, per-message unique keys, key erasure after use
3 Post-Compromise Security 12% 4 DH ratchet heals compromised state, fresh keys after recovery, healing within one round-trip, state divergence
4 Server Trust 15% 5 Server cannot read plaintext, AEAD integrity, tampered messages rejected, replay rejection, no plaintext in metadata
5 Group Security 10% 4 Sender Keys encrypt/decrypt, non-member rejection, unique ciphertext per message, PCS limitations acknowledged
6 Metadata Protection 10% 5 Message size decorrelation, sender anonymity in messages, sealed sender, message padding, timing obfuscation
7 Cryptographic Primitives 8% 5 AEAD authenticated encryption, key derivation, digital signatures, X25519 DH agreement, key initialization
8 Identity Verification 10% 4 Safety number generation, re-key detection, safety number symmetry, key transparency
9 Multi-Device Security 8% 4 Per-device keys, independent sessions, device compromise isolation, cross-device sync

Scoring Formula

Overall = sum of (DimensionScore / 10.0 * DimensionWeight) for all 9 dimensions

Each dimension scores 0–10 based on tests passed. Weights sum to 100. A protocol scoring 10/10 on every dimension scores 100/100.

What Each Test Actually Verifies

Every test operates through the ProtocolAdapter trait API. Tests verify behavior, not implementation details:

  • Session snapshot tests verify that cryptographic state changes at the right times (after encrypt, after DH ratchet, etc.). For protocols with opaque session records (like libsignal), adapters use state fingerprinting — a SHA-256 hash of the serialized session state that changes whenever internal state changes.
  • Server view tests verify that the server cannot see plaintext, and that message metadata (ratchet keys, counters) is properly structured.
  • Padding tests use the pad_plaintext/unpad_plaintext trait methods. Protocols that implement padding override these methods; protocols that don't get the default no-op (and the test honestly fails).
  • Without-OTP tests use publish_prekey_bundle_without_otp(). Each adapter generates a real bundle without the one-time prekey component.

Adding Your Protocol

Step 1: Create your adapter

Create src/adapters/your_protocol.rs and implement the ProtocolAdapter trait. The Signal adapter (src/adapters/signal.rs) is the reference implementation — study it to understand the expected behavior.

use anyhow::Result;
use crate::traits::ProtocolAdapter;
use crate::types::{Identity, ServerView, SessionSnapshot};

pub struct YourProtocolAdapter {
    // Internal state: identity keys, sessions, prekeys, etc.
}

impl YourProtocolAdapter {
    pub fn new() -> Self {
        Self { /* ... */ }
    }
}

impl ProtocolAdapter for YourProtocolAdapter {
    fn protocol_name(&self) -> &str { "YourProtocol" }
    fn protocol_version(&self) -> &str { "1.0.0" }

    fn generate_identity(&mut self, id: &str) -> Result<Identity> { todo!() }

    fn publish_prekey_bundle(&mut self, identity_id: &str) -> Result<Vec<u8>> { todo!() }
    fn publish_prekey_bundle_without_otp(&mut self, identity_id: &str) -> Result<Vec<u8>> { todo!() }
    fn initiate_session(&mut self, initiator_id: &str, responder_bundle: &[u8]) -> Result<String> { todo!() }
    fn respond_to_session(&mut self, responder_id: &str, initiator_identity_key: &[u8; 32], initiator_ephemeral_key: &[u8; 32]) -> Result<String> { todo!() }

    fn encrypt(&mut self, session_id: &str, plaintext: &[u8]) -> Result<Vec<u8>> { todo!() }
    fn decrypt(&mut self, session_id: &str, ciphertext: &[u8]) -> Result<Vec<u8>> { todo!() }

    fn session_snapshot(&self, session_id: &str) -> Result<SessionSnapshot> { todo!() }
    fn server_view(&self, encrypted_message: &[u8]) -> Result<ServerView> { todo!() }

    fn create_group_session(&mut self, sender_id: &str, group_id: &str) -> Result<Vec<u8>> { todo!() }
    fn join_group_session(&mut self, receiver_id: &str, distribution_message: &[u8]) -> Result<String> { todo!() }
    fn group_encrypt(&mut self, sender_id: &str, group_id: &str, plaintext: &[u8]) -> Result<Vec<u8>> { todo!() }
    fn group_decrypt(&mut self, group_session_id: &str, ciphertext: &[u8]) -> Result<Vec<u8>> { todo!() }

    fn tamper_ciphertext(&self, encrypted_message: &[u8]) -> Result<Vec<u8>> { todo!() }
    fn safety_number(&self, id_a: &str, id_b: &str) -> Result<String> { todo!() }

    fn get_ephemeral_key(&self, session_id: &str) -> Result<[u8; 32]> { todo!() }
    fn get_identity_x25519(&self, id: &str) -> Result<[u8; 32]> { todo!() }

    // Optional — override if your protocol provides message padding:
    // fn pad_plaintext(&self, plaintext: &[u8]) -> Result<Vec<u8>> { ... }
    // fn unpad_plaintext(&self, padded: &[u8]) -> Result<Vec<u8>> { ... }
}

Step 2: Register your adapter

Add your module to src/adapters/mod.rs:

pub mod signal;
pub mod your_protocol;

Add the dispatch arm in src/lib.rs inside run_audit():

"your_protocol" => Box::new(YourProtocolAdapter::new()),

And add it to available_protocols():

pub fn available_protocols() -> Vec<&'static str> {
    let mut protocols = vec!["signal"];
    protocols.push("your_protocol");
    protocols
}

Step 3: Run the audit

cargo run -- audit --protocol your_protocol --json --pdf

Implementing SessionSnapshot

The session_snapshot() method returns internal cryptographic state for inspection. Tests use snapshots to verify that:

  • root_key changes after DH ratchet steps
  • chain_key_send / chain_key_recv advance after each encrypt/decrypt
  • dh_self_public / dh_peer_public rotate during DH ratchet
  • send_count / recv_count increment properly

If your protocol exposes these values directly, return them. If your session record is opaque (e.g., libsignal's SessionRecord), you can use state fingerprinting: hash the serialized session state to produce values that change whenever internal state changes. The Signal adapter demonstrates this approach. The key requirement is that the returned values must be:

  1. Non-zero after session establishment
  2. Different between independent sessions
  3. Changed after operations that modify session state

Implementing ServerView

The server_view() method returns what a server relaying the message would see:

  • ciphertext — the encrypted payload bytes
  • ratchet_key — the ephemeral DH public key included in the message header (32 bytes)
  • message_number — the message counter/sequence number
  • associated_data — any unencrypted associated data
  • plaintext_visible — must be false (server should never see plaintext)

The ProtocolAdapter Trait

pub trait ProtocolAdapter: Send + Sync {
    // --- Protocol Info ---
    fn protocol_name(&self) -> &str;
    fn protocol_version(&self) -> &str;

    // --- Identity ---
    fn generate_identity(&mut self, id: &str) -> Result<Identity>;

    // --- Key Exchange ---
    fn publish_prekey_bundle(&mut self, identity_id: &str) -> Result<Vec<u8>>;
    fn publish_prekey_bundle_without_otp(&mut self, identity_id: &str) -> Result<Vec<u8>>;
    fn initiate_session(&mut self, initiator_id: &str, responder_bundle: &[u8]) -> Result<String>;
    fn respond_to_session(&mut self, responder_id: &str, ik: &[u8; 32], ek: &[u8; 32]) -> Result<String>;

    // --- 1:1 Encryption ---
    fn encrypt(&mut self, session_id: &str, plaintext: &[u8]) -> Result<Vec<u8>>;
    fn decrypt(&mut self, session_id: &str, ciphertext: &[u8]) -> Result<Vec<u8>>;

    // --- Session Inspection ---
    fn session_snapshot(&self, session_id: &str) -> Result<SessionSnapshot>;
    fn server_view(&self, encrypted_message: &[u8]) -> Result<ServerView>;

    // --- Group Encryption ---
    fn create_group_session(&mut self, sender_id: &str, group_id: &str) -> Result<Vec<u8>>;
    fn join_group_session(&mut self, receiver_id: &str, dist_msg: &[u8]) -> Result<String>;
    fn group_encrypt(&mut self, sender_id: &str, group_id: &str, pt: &[u8]) -> Result<Vec<u8>>;
    fn group_decrypt(&mut self, group_session_id: &str, ct: &[u8]) -> Result<Vec<u8>>;

    // --- Tamper Testing ---
    fn tamper_ciphertext(&self, encrypted_message: &[u8]) -> Result<Vec<u8>>;

    // --- Key Verification ---
    fn safety_number(&self, id_a: &str, id_b: &str) -> Result<String>;

    // --- Key Introspection ---
    fn get_ephemeral_key(&self, session_id: &str) -> Result<[u8; 32]>;
    fn get_identity_x25519(&self, id: &str) -> Result<[u8; 32]>;

    // --- Message Padding (optional, default: no-op) ---
    fn pad_plaintext(&self, plaintext: &[u8]) -> Result<Vec<u8>>;
    fn unpad_plaintext(&self, padded: &[u8]) -> Result<Vec<u8>>;
}

Measured Results

These scores come from actual adapter runs, not estimates.

Protocol Score Key Exchange Forward Secrecy PCS Server Trust Group Metadata Primitives Identity Multi-Device
Signal (libsignal) 87.5 10.0 10.0 10.0 10.0 10.0 2.0 10.0 7.5 7.5
Olm/Megolm (vodozemac) 87.5 10.0 10.0 10.0 10.0 10.0 2.0 10.0 7.5 7.5
Fourier 89.5 10.0 10.0 10.0 10.0 10.0 4.0 10.0 7.5 7.5

Differences explained:

  • Metadata Protection: Fourier provides a pad_message/unpad_message API that normalizes message sizes. Neither Signal's base protocol (libsignal-protocol) nor Olm/Megolm include padding — the Signal app adds PKCS#7-style padding at a higher layer, and Olm has no built-in padding mechanism.
  • Olm/Megolm vs Signal: Both score identically. Olm uses 3DH (simpler than Signal's X3DH) for key exchange and Megolm (shared ratchet) for group encryption instead of Signal's sender keys, but both approaches pass the same audit tests.
  • All protocols share the same limitations: no sealed sender at the crypto layer, no timing obfuscation, no key transparency, and no cross-device sync in the base protocol.

Fonts for PDF Reports

PDF report generation requires DejaVu fonts. The fonts/ directory is included in this repo. If fonts are missing, --pdf will return an error (not crash) and you can still use --json.

Idempotency

Audit scores are deterministic. Running the same protocol adapter produces the same score every time — no randomness in test evaluation, no external dependencies. The only values that change between runs are timestamp and run_id.

Publishing Reports

Each audit run produces a unique run_id (SHA-256 hash of timestamp + protocol + version + all test results). Use this to reference a specific audit.

# Generate shareable reports
cargo run -- audit --json --pdf --output ./output/

# Output files:
#   ./output/signal_audit.json
#   ./output/signal_audit.pdf

The JSON report contains every test result, scores, and metadata — suitable for automated comparison pipelines.

Bibliography

Protocol Specifications

  1. Marlinspike, M. & Perrin, T. (2016). The X3DH Key Agreement Protocol. Signal Foundation.
  2. Perrin, T. & Marlinspike, M. (2016). The Double Ratchet Algorithm. Signal Foundation.
  3. Marlinspike, M. & Perrin, T. (2017). The Sesame Algorithm. Signal Foundation.
  4. Barnes, R. et al. (2023). The Messaging Layer Security (MLS) Protocol. IETF RFC 9420.
  5. Marlinspike, M. (2017). Sealed Sender. Signal Blog.

Formal Security Analysis

  1. Cohn-Gordon, K. et al. (2020). A Formal Security Analysis of the Signal Messaging Protocol. Journal of Cryptology, 33(4).
  2. Alwen, J., Coretti, S., & Dodis, Y. (2019). The Double Ratchet: Security Notions, Proofs, and Modularization. EUROCRYPT 2019.
  3. Cremers, C., Hale, B., & Kohbrok, K. (2023). The Complexities of Healing in Secure Group Messaging. USENIX Security 2023.
  4. Bhargavan, K., Beurdouche, B., & Naldurg, P. (2017). A Formal Treatment of the Signal Messaging Protocol. ePrint 2017/1230.

Key Management & Identity

  1. Chase, M., Meiklejohn, S., & Zaverucha, G. (2020). Key Transparency and the Right to be Forgotten. ePrint 2020/534.
  2. Melara, M. et al. (2015). CONIKS: Bringing Key Transparency to End Users. USENIX Security 2015.

Metadata & Traffic Analysis

  1. Greschbach, B., Kreitz, G., & Buchegger, S. (2012). The Devil is in the Metadata. IEEE PerCom Workshops.
  2. Angel, S. & Setty, S. (2016). Unobservable Communication over Fully Untrusted Infrastructure. OSDI 2016.

Group Encryption

  1. Alwen, J. et al. (2020). Security Analysis and Improvements for the IETF MLS Standard. CRYPTO 2020.
  2. Cohn-Gordon, K. et al. (2018). On Ends-to-Ends Encryption. ACM CCS 2018.

Cryptographic Primitives

  1. Bernstein, D.J. (2008). ChaCha, a variant of Salsa20.
  2. Krawczyk, H. & Eronen, P. (2010). HKDF. IETF RFC 5869.
  3. Josefsson, S. & Liusvaara, I. (2017). EdDSA. IETF RFC 8032.

Further Reading

  • Unger, N. et al. (2015). SoK: Secure Messaging. IEEE S&P 2015.
  • Rosler, P., Mainka, C., & Schwenk, J. (2018). More is Less: On the End-to-End Security of Group Chats. IEEE EuroS&P 2018.

License

Apache-2.0 OR MIT (dual-licensed)

About

No description, website, or topics provided.

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages