facebook / opaque-ke

An implementation of the OPAQUE password-authenticated key exchange protocol
Apache License 2.0
291 stars 41 forks source link

Cannot get connection successful after server restart #343

Closed DorianCoding closed 11 months ago

DorianCoding commented 11 months ago

Hello and sorry if the question is evident.

I have implemented opaque and it works well when the server is up. However if I restart the server, I cannot connect anymore.

I have the same private key from ServerSetup using new_from_key and the public key is printed and is the same but the OsRng is different. Is it the problem ? if yes, how to save the OsRng parameters?

Of course, the password file is read succesfully each time.

Client :

target/debug/client -r
Username: dorian
Password:

Please confirm:

Connecting to remote host...
Register mode
Connected to [::1]:5501
Sending HELLO
Received HELLO, authentification phase.
Authentification calculation begin...
▰▰▰▰▰▰▰ Done                                                                                                                                                                                            

Registered

$ target/debug/client
Username: dorian
Password:

Connecting to remote host...
Login mode
Connected to [::1]:5502
Sending HELLO
Got hello, authentification phase
Got challenge
▰▰▰▰▰▰▰ Done                                                                                                                                                                                            

Shared Key is K******* and my key is 8**********

$ target/debug/client
Username: dorian
Password:

Connecting to remote host...
Login mode
Connected to [::1]:5502
Sending HELLO
Got hello, authentification phase
Got challenge
▰▰▰▰▰▰▰ Done                                                                                                                                                                                            

Error during calculation, definitely wrong password.

Server (your public key represents the key from ServerSetup) :

Your public key is: jhdHAQAVZNlJe-dkslkTg6OzWq5e6XIzNwtTeyZX8gs
Listening sockets
The username hash : 'p__G-L8e12ZRwUdWoGHWYvWA_03kO0n6gtgKS4D4Q0o' is not present, sending dummy login.
Error: No result, probably incorrect password. from [::1]:37692
Connection accepted with [::1]:36522.
Registration successful for user 'dorian'
VALID: Connection from [::1]:58732 - KEY: K********
^C
$ target/debug/server
Your public key is: jhdHAQAVZNlJe-dkslkTg6OzWq5e6XIzNwtTeyZX8gs
Listening sockets
Error: No result, probably incorrect password. from [::1]:51460

The so-called code :

let mut rng = OsRng;
let server_setupraw: ServerSetup<DefaultCipherSuite> = match fs::read_to_string(keyfile) {
        Ok(key) => {
            let mut keybuffer: &mut [u8] = &mut [0; 500];
            let keybuffer = func::returnhexstring(key, &mut keybuffer).unwrap();
            let key = KeyPair::from_private_key_slice(keybuffer).unwrap();
            ServerSetup::<DefaultCipherSuite>::new_with_key(&mut rng, key)
        }
        Err(_) => {
            let server_setupraw = ServerSetup::<DefaultCipherSuite>::new(&mut rng);
            let mut file = File::create(keyfile).unwrap();
            func::checkandlockfsordir(&Path::new(keyfile));
            let mut privatekey = server_setupraw.keypair().private().serialize();
            file.write_all(func::returnstring(privatekey).as_bytes())
                .unwrap();
            privatekey.zeroize();
            server_setupraw
        }
    };
    let publickey = server_setupraw.keypair().public().serialize();
    println!("Your public key is: {}", func::returnstring(publickey));

Thank you.

daxpedda commented 11 months ago

It would be helpful to provide a complete small reproducible example that only uses opaque-ke.

DorianCoding commented 11 months ago

cat Yes very sorry @daxpedda, I take the privilege to use the example (the example is complete but not small). I got this whereas I was expected three True. However I'm not an expert and maybe there is a parameter that I'm missing :

Registration finished
Result is true
Result is false
Result is false
//
// This source code is dual-licensed under either the MIT license found in the
// LICENSE-MIT file in the root directory of this source tree or the Apache
// License, Version 2.0 found in the LICENSE-APACHE file in the root directory
// of this source tree. You may select, at your option, one of the above-listed
// licenses.

//! Demonstrates a simple client-server password-based login protocol using
//! OPAQUE, over a command-line interface
//!
//! The client-server interactions are executed in a three-step protocol within
//! the account_registration (for password registration) and account_login (for
//! password login) functions. These steps must be performed in the specific
//! sequence outlined in each of these functions.
//!
//! The CipherSuite trait allows the application to configure the primitives
//! used by OPAQUE, but must be kept consistent across the steps of the
//! protocol.
//!
//! In a more realistic client-server interaction, the client must send messages
//! over "the wire" to the server. These bytes are serialized and explicitly
//! annotated in the below functions.

use std::collections::HashMap;
use std::process::exit;

use argon2::Argon2;
use generic_array::GenericArray;
use opaque_ke::ciphersuite::CipherSuite;
use opaque_ke::rand::rngs::OsRng;
use opaque_ke::{
    ClientLogin, ClientLoginFinishParameters, ClientRegistration,
    ClientRegistrationFinishParameters, CredentialFinalization, CredentialRequest,
    CredentialResponse, RegistrationRequest, RegistrationResponse, RegistrationUpload, ServerLogin,
    ServerLoginStartParameters, ServerRegistration, ServerRegistrationLen, ServerSetup,
};

// The ciphersuite trait allows to specify the underlying primitives that will
// be used in the OPAQUE protocol
#[allow(dead_code)]
struct DefaultCipherSuite;

#[cfg(feature = "ristretto255")]
impl CipherSuite for DefaultCipherSuite {
    type OprfCs = opaque_ke::Ristretto255;
    type KeGroup = opaque_ke::Ristretto255;
    type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh;

    type Ksf = Argon2<'static>;
}

#[cfg(not(feature = "ristretto255"))]
impl CipherSuite for DefaultCipherSuite {
    type OprfCs = p256::NistP256;
    type KeGroup = p256::NistP256;
    type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh;

    type Ksf = Argon2<'static>;
}

// Password-based registration between a client and server
fn account_registration(
    server_setup: &ServerSetup<DefaultCipherSuite>,
    username: String,
    password: String,
) -> GenericArray<u8, ServerRegistrationLen<DefaultCipherSuite>> {
    let mut client_rng = OsRng;
    let client_registration_start_result =
        ClientRegistration::<DefaultCipherSuite>::start(&mut client_rng, password.as_bytes())
            .unwrap();
    let registration_request_bytes = client_registration_start_result.message.serialize();

    // Client sends registration_request_bytes to server

    let server_registration_start_result = ServerRegistration::<DefaultCipherSuite>::start(
        server_setup,
        RegistrationRequest::deserialize(&registration_request_bytes).unwrap(),
        username.as_bytes(),
    )
    .unwrap();
    let registration_response_bytes = server_registration_start_result.message.serialize();

    // Server sends registration_response_bytes to client

    let client_finish_registration_result = client_registration_start_result
        .state
        .finish(
            &mut client_rng,
            password.as_bytes(),
            RegistrationResponse::deserialize(&registration_response_bytes).unwrap(),
            ClientRegistrationFinishParameters::default(),
        )
        .unwrap();
    let message_bytes = client_finish_registration_result.message.serialize();

    // Client sends message_bytes to server

    let password_file = ServerRegistration::finish(
        RegistrationUpload::<DefaultCipherSuite>::deserialize(&message_bytes).unwrap(),
    );
    password_file.serialize()
}

// Password-based login between a client and server
fn account_login(
    server_setup: &ServerSetup<DefaultCipherSuite>,
    username: String,
    password: String,
    password_file_bytes: &[u8],
) -> bool {
    let mut client_rng = OsRng;
    let client_login_start_result =
        ClientLogin::<DefaultCipherSuite>::start(&mut client_rng, password.as_bytes()).unwrap();
    let credential_request_bytes = client_login_start_result.message.serialize();

    // Client sends credential_request_bytes to server

    let password_file =
        ServerRegistration::<DefaultCipherSuite>::deserialize(password_file_bytes).unwrap();
    let mut server_rng = OsRng;
    let server_login_start_result = ServerLogin::start(
        &mut server_rng,
        server_setup,
        Some(password_file),
        CredentialRequest::deserialize(&credential_request_bytes).unwrap(),
        username.as_bytes(),
        ServerLoginStartParameters::default(),
    )
    .unwrap();
    let credential_response_bytes = server_login_start_result.message.serialize();

    // Server sends credential_response_bytes to client

    let result = client_login_start_result.state.finish(
        password.as_bytes(),
        CredentialResponse::deserialize(&credential_response_bytes).unwrap(),
        ClientLoginFinishParameters::default(),
    );

    if result.is_err() {
        // Client-detected login failure
        return false;
    }
    let client_login_finish_result = result.unwrap();
    let credential_finalization_bytes = client_login_finish_result.message.serialize();

    // Client sends credential_finalization_bytes to server

    let server_login_finish_result = server_login_start_result
        .state
        .finish(CredentialFinalization::deserialize(&credential_finalization_bytes).unwrap())
        .unwrap();

    client_login_finish_result.session_key == server_login_finish_result.session_key
}

fn main() {
    let mut rng = OsRng;
    let server_setup = ServerSetup::<DefaultCipherSuite>::new(&mut rng);
    let result = account_registration(
        &server_setup,
        String::from("dorian"),
        String::from("coding"),
    );
    println!("Registration finished");
    println!(
        "Result is {}",
        account_login(
            &server_setup,
            String::from("dorian"),
            String::from("coding"),
            result.as_slice()
        )
    );
    let server_setup2 =
        ServerSetup::<DefaultCipherSuite>::new_with_key(&mut rng, server_setup.keypair().clone());
    println!(
        "Result is {}",
        account_login(
            &server_setup2,
            String::from("dorian"),
            String::from("coding"),
            result.as_slice()
        )
    );
    rng = OsRng;
    let server_setup2 =
        ServerSetup::<DefaultCipherSuite>::new_with_key(&mut rng, server_setup.keypair().clone());
    println!(
        "Result is {}",
        account_login(
            &server_setup2,
            String::from("dorian"),
            String::from("coding"),
            result.as_slice()
        )
    );
}
daxpedda commented 11 months ago

I believe the documentation on this is actually poor now that I look at it. The ServerSetup also contains an OPRF seed, which has to stay the same between registration and logins.

Instead of ServerSetup::keypair() + ServerSetup::new_with_key() you actually have to use ServerSetup::serialize() + ServerSetup::deserialize().

ServerSetup::new_with_key() is only good if you want to use a certain key, and not one that is generated by opaque-ke, or you want to use the same key but different OPRF seeds between clients.

DorianCoding commented 11 months ago

The ServerSetup also contains an OPRF seed, which has to stay the same between registration and logins.

Okay this is what I was afraid of. It did work with the use of your commands. However I'm wondering now : how (this means get OPRF seed only) and what are the benefits of changing OPRF between clients? Is there any risk when sharing the same parameters between clients? Because in the draft it is written :

The oprf_seed value SHOULD be used for all clients; see Section 10.9.

This being said, I think an information in the documentation would be welcomed.

daxpedda commented 11 months ago

However I'm wondering now : how (this means get OPRF seed only) and what are the benefits of changing OPRF between clients? Is there any risk when sharing the same parameters between clients?

I was just about to link to the relevant part of the spec, but I found actually inconsistent recommendations, so I will open an issue to let them clarify it.

In any case, it's probably best if @kevinlewi chimes in, the actual expert here.

daxpedda commented 11 months ago

Because in the draft it is written :

The oprf_seed value SHOULD be used for all clients; see Section 10.9.

https://cfrg.github.io/draft-irtf-cfrg-opaque/draft-irtf-cfrg-opaque.html#section-3.1-1:

The server can use server_private_key and server_public_key with multiple clients and can opt to use multiple seeds (so long as they are kept consistent for each client).

This is the inconsistency I was talking about!

daxpedda commented 11 months ago

https://github.com/cfrg/draft-irtf-cfrg-opaque/issues/426

DorianCoding commented 11 months ago

Yes I'm waiting for the answer but what I have understand is oprf_seed must be the same if you use dummy login as you cannot know what oprf_seed to send in case the user does not exist but if the server is compromised, it must be changed. And if the server key follows this faith and is trasmitted before to clients to avoid MITM attacks at login, this will break. Moreover if this is true, I doubt the new_from_key function is useful. Moreover for the compromisation of the recent password change, it seems way easier to detect this as pointed out in another issue if the seed changes each time rather than being the same. For this reason the change if both server key and OPRF in case of compromise and keep both when everything is fine should be prioritized.

In the event of a server compromise that results in a re-registration of credentials for all compromised clients, the oprf_seed value MUST be resampled, resulting in a change in the oprf_key value for each client. Although this change can be detected by an adversary, it is only leaked upon password rotation after the exposure of the credential files, and equally affects all registered clients.

daxpedda commented 11 months ago

But if this is true, I doubt the new_from_key function is useful.

It's still useful for users who want to use a specific key, but it's not useful to rebuild the ServerSetup for login.

kevinlewi commented 11 months ago

Right, so ultimately we want to use the serialize and deserialize functions to persist the instance of ServerSetup. The new_with_key() and new() functions won't work for this. I put up a PR to clarify this in the docs. And thanks @daxpedda for chiming in :)

kevinlewi commented 11 months ago

Closed by https://github.com/facebook/opaque-ke/pull/344