Add some initial tests for securejoin

This currently tests the setup-contact full flow and the shortcut
flow.  More securejoin tests are needed but this puts some
infrastructure in place to start writing these.
This commit is contained in:
Floris Bruynooghe
2020-09-04 23:23:53 +02:00
parent cdd1ccb458
commit cd925624a7
3 changed files with 363 additions and 2 deletions

View File

@@ -355,7 +355,7 @@ pub async fn store_self_keypair(
}
/// A key fingerprint
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct Fingerprint(Vec<u8>);
impl Fingerprint {
@@ -375,6 +375,14 @@ impl Fingerprint {
}
}
impl fmt::Debug for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Fingerprint")
.field("hex", &self.hex())
.finish()
}
}
/// Make a human-readable fingerprint.
impl fmt::Display for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {

View File

@@ -1088,3 +1088,222 @@ fn encrypted_and_signed(
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::peerstate::Peerstate;
use crate::test_utils::TestContext;
#[async_std::test]
async fn test_setup_contact() {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
// Generate QR-code, ChatId(0) indicates setup-contact
let qr = dc_get_securejoin_qr(&alice.ctx, ChatId::new(0))
.await
.unwrap();
// Bob scans QR-code, sends vc-request
let bob_chatid = dc_join_securejoin(&bob.ctx, &qr).await;
let sent = bob.pop_sent_msg().await;
assert_eq!(sent.id(), bob_chatid);
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vc-request");
assert!(msg.get(HeaderDef::SecureJoinInvitenumber).is_some());
// Alice receives vc-request, sends vc-auth-required
alice.recv_msg(&sent).await;
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vc-auth-required");
// Bob receives vc-auth-required, sends vc-request-with-auth
bob.recv_msg(&sent).await;
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-request-with-auth"
);
assert!(msg.get(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.fingerprint();
assert_eq!(
*msg.get(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
);
// Alice should not yet have Bob verified
let contact_bob_id =
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown).await;
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id)
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
VerifiedStatus::Unverified
);
// Alice receives vc-request-with-auth, sends vc-contact-confirm
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
VerifiedStatus::BidirectVerified
);
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm"
);
// Bob should not yet have Alice verified
let contact_alice_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown).await;
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id)
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&bob.ctx).await,
VerifiedStatus::Unverified
);
// Bob receives vc-contact-confirm, sends vc-contact-confirm-received
bob.recv_msg(&sent).await;
assert_eq!(
contact_alice.is_verified(&bob.ctx).await,
VerifiedStatus::BidirectVerified
);
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
}
#[async_std::test]
async fn test_setup_contact_bob_knows_alice() {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
// Ensure Bob knows Alice_FP
let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await.unwrap();
let peerstate = Peerstate {
context: &bob.ctx,
addr: "alice@example.com".into(),
last_seen: 10,
last_seen_autocrypt: 10,
prefer_encrypt: EncryptPreference::Mutual,
public_key: Some(alice_pubkey.clone()),
public_key_fingerprint: Some(alice_pubkey.fingerprint()),
gossip_key: Some(alice_pubkey.clone()),
gossip_timestamp: 10,
gossip_key_fingerprint: Some(alice_pubkey.fingerprint()),
verified_key: None,
verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
peerstate.save_to_db(&bob.ctx.sql, true).await.unwrap();
// Generate QR-code, ChatId(0) indicates setup-contact
let qr = dc_get_securejoin_qr(&alice.ctx, ChatId::new(0))
.await
.unwrap();
// Bob scans QR-code, sends vc-request-with-auth, skipping vc-request
dc_join_securejoin(&bob.ctx, &qr).await;
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-request-with-auth"
);
assert!(msg.get(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.fingerprint();
assert_eq!(
*msg.get(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
);
// Alice should not yet have Bob verified
let (contact_bob_id, _modified) = Contact::add_or_lookup(
&alice.ctx,
"Bob",
"bob@example.net",
Origin::ManuallyCreated,
)
.await
.unwrap();
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id)
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
VerifiedStatus::Unverified
);
// Alice receives vc-request-with-auth, sends vc-contact-confirm
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
VerifiedStatus::BidirectVerified
);
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm"
);
// Bob should not yet have Alice verified
let contact_alice_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown).await;
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id)
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&bob.ctx).await,
VerifiedStatus::Unverified
);
// Bob receives vc-contact-confirm, sends vc-contact-confirm-received
bob.recv_msg(&sent).await;
assert_eq!(
contact_alice.is_verified(&bob.ctx).await,
VerifiedStatus::BidirectVerified
);
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
}
}

View File

@@ -2,12 +2,21 @@
//!
//! This module is only compiled for test runs.
use std::cell::RefCell;
use std::str::FromStr;
use async_std::path::PathBuf;
use tempfile::{tempdir, TempDir};
use crate::chat::ChatId;
use crate::config::Config;
use crate::context::Context;
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_tools::EmailAddress;
use crate::job::Action;
use crate::key::{self, DcKey};
use crate::mimeparser::MimeMessage;
use crate::param::{Param, Params};
/// A Context and temporary directory.
///
@@ -16,6 +25,8 @@ use crate::key::{self, DcKey};
pub(crate) struct TestContext {
pub ctx: Context,
pub dir: TempDir,
/// Counter for fake IMAP UIDs in [recv_msg], for private use in that function only.
recv_idx: RefCell<u32>,
}
impl TestContext {
@@ -35,7 +46,11 @@ impl TestContext {
let ctx = Context::new("FakeOS".into(), dbfile.into(), id)
.await
.unwrap();
Self { ctx, dir }
Self {
ctx,
dir,
recv_idx: RefCell::new(0),
}
}
/// Create a new configured [TestContext].
@@ -48,6 +63,19 @@ impl TestContext {
t
}
/// Create a new configured [TestContext].
///
/// This is a shortcut which configures bob@example.net with a fixed key.
pub async fn new_bob() -> Self {
let t = Self::new().await;
let keypair = bob_keypair();
t.configure_addr(&keypair.addr.to_string()).await;
key::store_self_keypair(&t.ctx, &keypair, key::KeyPairUse::Default)
.await
.expect("Failed to save Bob's key");
t
}
/// Configure with alice@example.com.
///
/// The context will be fake-configured as the alice user, with a pre-generated secret
@@ -76,6 +104,112 @@ impl TestContext {
.await
.unwrap();
}
/// Retrieve a sent message from the jobs table.
///
/// This retrieves and removes a message which has been scheduled to send from the jobs
/// table. Messages are returned in the order they have been sent.
///
/// Panics if there is no message or on any error.
pub async fn pop_sent_msg(&self) -> SentMessage {
let (rowid, foreign_id, raw_params) = self
.ctx
.sql
.query_row(
r#"
SELECT id, foreign_id, param
FROM jobs
WHERE action=?
ORDER BY desired_timestamp;
"#,
paramsv![Action::SendMsgToSmtp],
|row| {
let id: i64 = row.get(0)?;
let foreign_id: i64 = row.get(1)?;
let param: String = row.get(2)?;
Ok((id, foreign_id, param))
},
)
.await
.expect("no sent message found in jobs table");
let id = ChatId::new(foreign_id as u32);
let params = Params::from_str(&raw_params).unwrap();
let blob_path = params
.get_blob(Param::File, &self.ctx, false)
.await
.expect("failed to parse blob from param")
.expect("no Param::File found in Params")
.to_abs_path();
self.ctx
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![rowid])
.await
.expect("failed to remove job");
SentMessage {
id,
params,
blob_path,
}
}
/// Parse a message.
///
/// Parsing a message does not run the entire receive pipeline, but is not without
/// side-effects either. E.g. if the message includes autocrypt headers the relevant
/// peerstates will be updated. Later receiving the message using [recv_msg] is
/// unlikely to be affected as the peerstate would be processed again in exactly the
/// same way.
pub async fn parse_msg(&self, msg: &SentMessage) -> MimeMessage {
MimeMessage::from_bytes(&self.ctx, msg.payload().as_bytes())
.await
.unwrap()
}
/// Receive a message.
///
/// Receives a message using the `dc_receive_imf()` pipeline.
pub async fn recv_msg(&self, msg: &SentMessage) {
let mut idx = self.recv_idx.borrow_mut();
*idx += 1;
dc_receive_imf(&self.ctx, msg.payload().as_bytes(), "INBOX", *idx, false)
.await
.unwrap();
}
}
/// A raw message as it was scheduled to be sent.
///
/// This is a raw message, probably in the shape DC was planning to send it but not having
/// passed through a SMTP-IMAP pipeline.
#[derive(Debug, Clone)]
pub struct SentMessage {
id: ChatId,
params: Params,
blob_path: PathBuf,
}
impl SentMessage {
/// The ChatId the message belonged to.
pub fn id(&self) -> ChatId {
self.id
}
/// A recipient the message was destined for.
///
/// If there are multiple recipients this is just a random one, so is not very useful.
pub fn recipient(&self) -> EmailAddress {
let raw = self
.params
.get(Param::Recipients)
.expect("no recipients in params");
let rcpt = raw.split(' ').next().expect("no recipient found");
rcpt.parse().expect("failed to parse email address")
}
/// The raw message payload.
pub fn payload(&self) -> String {
std::fs::read_to_string(&self.blob_path).unwrap()
}
}
/// Load a pre-generated keypair for alice@example.com from disk.