Expand description

An implementation of the OPAQUE asymmetric password authentication key exchange protocol

Note: This implementation is in sync with draft-irtf-cfrg-opaque-10, but this specification is subject to change, until the final version published by the IETF.

Minimum Supported Rust Version

Rust 1.57 or higher.

Overview

OPAQUE is a protocol between a client and a server. They must first agree on a collection of primitives to be kept consistent throughout protocol execution. These include:

  • a finite cyclic group along with a point representation
    • for the OPRF and
    • for the key exchange
  • a key exchange protocol,
  • a hashing function, and
  • a key stretching function.

We will use the following choices in this example:

use opaque_ke::CipherSuite;
struct Default;
impl CipherSuite for Default {
    type OprfCs = opaque_ke::Ristretto255;
    type KeGroup = opaque_ke::Ristretto255;
    type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh;
    type Ksf = opaque_ke::ksf::Identity;
}

See examples/simple_login.rs for a working example of a simple password-based login using OPAQUE.

Note that our choice of key stretching function in this example, Identity, is selected only to ensure that the tests execute quickly. A real application should use an actual key stretching function, such as Argon2, which can be enabled through the argon2 feature. See more details in the features section.

Setup

To set up the protocol, the server begins by creating a ServerSetup object:

use rand::rngs::OsRng;
use rand::RngCore;
let mut rng = OsRng;
let server_setup = ServerSetup::<Default>::new(&mut rng);

The server must persist an instance of ServerSetup for the registration and login steps.

Registration

The registration protocol between the client and server consists of four steps along with three messages: RegistrationRequest, RegistrationResponse, and RegistrationUpload. A successful execution of the registration protocol results in the server producing a password file corresponding to a server-side identifier for the client, along with the password provided by the client. This password file is typically stored in a key-value database, where the keys consist of these server-side identifiers for each client, and the values consist of their corresponding password files, to be retrieved upon future login attempts made by the client. It is your responsibility to ensure that the identifier used to form the initial RegistrationRequest, typically supplied by the client, matches the database key used in the final RegistrationUpload step.

Note that the RegistrationUpload message contains sensitive information (about as sensitive as a hash of the password), and hence should be protected with confidentiality guarantees by the consumer of this library.

Client Registration Start

In the first step of registration, the client chooses as input a registration password. The client runs ClientRegistration::start to produce a ClientRegistrationStartResult, which consists of a RegistrationRequest to be sent to the server and a ClientRegistration which must be persisted on the client for the final step of client registration.

use opaque_ke::ClientRegistration;
use rand::rngs::OsRng;
use rand::RngCore;
let mut client_rng = OsRng;
let client_registration_start_result =
    ClientRegistration::<Default>::start(&mut client_rng, b"password")?;

Server Registration Start

In the second step of registration, the server takes as input a persisted instance of ServerSetup, a RegistrationRequest from the client, and a server-side identifier for the client. The server runs ServerRegistration::start to produce a ServerRegistrationStartResult, which consists of a RegistrationResponse to be returned to the client.

use opaque_ke::ServerRegistration;
let server_registration_start_result = ServerRegistration::<Default>::start(
    &server_setup,
    client_registration_start_result.message,
    b"alice@example.com",
)?;

Client Registration Finish

In the third step of registration, the client takes as input a RegistrationResponse from the server, and a ClientRegistration from the first step of registration. The client runs ClientRegistration::finish to produce a ClientRegistrationFinishResult, which consists of a RegistrationUpload to be sent to the server and an export_key field which can be used optionally as described in the Export Key section.

let client_registration_finish_result = client_registration_start_result.state.finish(
    &mut client_rng,
    b"password",
    server_registration_start_result.message,
    ClientRegistrationFinishParameters::default(),
)?;

Server Registration Finish

In the fourth step of registration, the server takes as input a RegistrationUpload from the client, and a ServerRegistration from the second step. The server runs ServerRegistration::finish to produce a finalized ServerRegistration. At this point, the client can be considered as successfully registered, and the server can invoke ServerRegistration::serialize to store the password file for use during the login protocol.

let password_file = ServerRegistration::<Default>::finish(
    client_registration_finish_result.message,
);

Login

The login protocol between a client and server also consists of four steps along with three messages: CredentialRequest, CredentialResponse, CredentialFinalization. The server is expected to have access to the password file corresponding to an output of the registration phase (see Dummy Server Login for handling the scenario where no password file is available). The login protocol will execute successfully only if the same password was used in the registration phase that produced the password file that the server is testing against.

Client Login Start

In the first step of login, the client chooses as input a login password. The client runs ClientLogin::start to produce an output consisting of a CredentialRequest to be sent to the server, and a ClientLogin which must be persisted on the client for the final step of client login.

use opaque_ke::ClientLogin;
let mut client_rng = OsRng;
let client_login_start_result = ClientLogin::<Default>::start(&mut client_rng, b"password")?;

Server Login Start

In the second step of login, the server takes as input a persisted instance of ServerSetup, the password file output from registration, a CredentialRequest from the client, and a server-side identifier for the client. The server runs ServerLogin::start to produce an output consisting of a CredentialResponse which is returned to the client, and a ServerLogin which must be persisted on the server for the final step of login.

use opaque_ke::{ServerLogin, ServerLoginStartParameters};
let password_file = ServerRegistration::<Default>::deserialize(&password_file_bytes)?;
let mut server_rng = OsRng;
let server_login_start_result = ServerLogin::start(
    &mut server_rng,
    &server_setup,
    Some(password_file),
    client_login_start_result.message,
    b"alice@example.com",
    ServerLoginStartParameters::default(),
)?;

Note that if there is no corresponding password file found for the user, the server can use None in place of Some(password_file) in order to generate a CredentialResponse that is indistinguishable from a valid CredentialResponse returned for a registered client. This allows the server to prevent leaking information about whether or not a client has previously registered with the server.

Client Login Finish

In the third step of login, the client takes as input a CredentialResponse from the server. The client runs ClientLogin::finish and produces an output consisting of a CredentialFinalization to be sent to the server to complete the protocol, the session_key sequence of bytes which will match the server’s session key upon a successful login.

let client_login_finish_result = client_login_start_result.state.finish(
    b"password",
    server_login_start_result.message,
    ClientLoginFinishParameters::default(),
)?;

Server Login Finish

In the fourth step of login, the server takes as input a CredentialFinalization from the client and runs ServerLogin::finish to produce an output consisting of the session_key sequence of bytes which will match the client’s session key upon a successful login.

let server_login_finish_result = server_login_start_result.state.finish(
    client_login_finish_result.message,
)?;

assert_eq!(
   client_login_finish_result.session_key,
   server_login_finish_result.session_key,
);

If the protocol completes successfully, then the server obtains a server_login_finish_result.session_key which is guaranteed to match client_login_finish_result.session_key (see the Session Key section). Otherwise, on failure, the ServerLogin::finish algorithm outputs the error InvalidLoginError.

Advanced Usage

This implementation offers support for several optional features of OPAQUE, described below. They are not critical to the execution of the main protocol, but can provide additional security benefits which can be suitable for various applications that rely on OPAQUE for authentication.

Session Key

Upon a successful completion of the OPAQUE protocol (the client runs login with the same password used during registration), the client and server have access to a session key, which is a pseudorandomly distributed 32-byte string which only the client and server know. Multiple login runs using the same password for the same client will produce different session keys, distributed as uniformly random strings. Thus, the session key can be used to establish a secure channel between the client and server.

The session key can be accessed from the session_key field of ClientLoginFinishResult and ServerLoginFinishResult. See the combination of Client Login Finish and Server Login Finish for example usage.

Checking Server Consistency

A ClientLoginFinishResult contains the server_s_pk field, which is represents the static public key of the server that is established during the setup phase. This can be used by the client to verify the authenticity of the server it engages with during the login phase. In particular, the client can check that the static public key of the server supplied during registration (with the server_s_pk field of ClientRegistrationFinishResult) matches this field during login.

// During registration, the client obtains a ClientRegistrationFinishResult with
// a server_s_pk field
let client_registration_finish_result = client_registration_start_result.state.finish(
    &mut client_rng,
    b"password",
    server_registration_start_result.message,
    ClientRegistrationFinishParameters::default(),
)?;

// And then later, during login...
let client_login_finish_result = client_login_start_result.state.finish(
    b"password",
    server_login_start_result.message,
    ClientLoginFinishParameters::default(),
)?;

// Check that the server's static public key obtained from login matches what
// was obtained during registration
assert_eq!(
    &client_registration_finish_result.server_s_pk,
    &client_login_finish_result.server_s_pk,
);

Note that without this check over the consistency of the server’s static public key, a malicious actor could impersonate the registration server if it were able to copy the password file output during registration! Therefore, it is recommended to perform the following check in the application layer if the client can obtain a copy of the server’s static public key beforehand.

Export Key

The export key is a pseudorandomly distributed 32-byte string output by both the Client Registration Finish and Client Login Finish steps. The same export key string will be output by both functions only if the exact same password is passed to ClientRegistration::start and ClientLogin::start.

The export key retains as much secrecy as the password itself, and is similarly derived through an evaluation of the key stretching function. Hence, only the parties which know the password the client uses during registration and login can recover this secret, as it is never exposed to the server. As a result, the export key can be used (separately from the OPAQUE protocol) to provide confidentiality and integrity to other data which only the client should be able to process. For instance, if the server is expected to maintain any client-side secrets which require a password to access, then this export key can be used to encrypt these secrets so that they remain hidden from the server (see examples/digital_locker.rs for a working example).

You can access the export key from the export_key field of ClientRegistrationFinishResult and ClientLoginFinishResult.

// During registration...
let client_registration_finish_result = client_registration_start_result.state.finish(
    &mut client_rng,
    b"password",
    server_registration_start_result.message,
    ClientRegistrationFinishParameters::default()
)?;

// And then later, during login...
let client_login_finish_result = client_login_start_result.state.finish(
    b"password",
    server_login_start_result.message,
    ClientLoginFinishParameters::default(),
)?;

assert_eq!(
    client_registration_finish_result.export_key,
    client_login_finish_result.export_key,
);

Custom Identifiers

Typically when applications use OPAQUE to authenticate a client to a server, the client has a registered username which is sent to the server to identify the corresponding password file established during registration. This username may or may not coincide with the server-side identifier; however, this username must be known to both the client and the server (whereas the server-side identifier does not need to be exposed to the client). The server may also have an identifier corresponding to an entity (e.g. Facebook). By default, neither of these public identifiers need to be supplied to the OPAQUE protocol.

But, for applications that wish to cryptographically bind these identities to the registered password file as well as the session key output by the login phase, these custom identifiers can be specified through ClientRegistrationFinishParameters in Client Registration Finish:

let client_registration_finish_result = client_registration_start_result.state.finish(
    &mut client_rng,
    b"password",
    server_registration_start_result.message,
    ClientRegistrationFinishParameters::new(
        Identifiers {
            client: Some(b"Alice_the_Cryptographer"),
            server: Some(b"Facebook"),
        },
        None,
    ),
)?;

The same identifiers must also be supplied using ServerLoginStartParameters in Server Login Start:

let server_login_start_result = ServerLogin::start(
    &mut server_rng,
    &server_setup,
    Some(password_file),
    client_login_start_result.message,
    b"alice@example.com",
    ServerLoginStartParameters {
        context: None,
        identifiers: Identifiers {
            client: Some(b"Alice_the_Cryptographer"),
            server: Some(b"Facebook"),
        },
    },
)?;

as well as ClientLoginFinishParameters in Client Login Finish:

let client_login_finish_result = client_login_start_result.state.finish(
    b"password",
    server_login_start_result.message,
    ClientLoginFinishParameters::new(
        None,
        Identifiers {
            client: Some(b"Alice_the_Cryptographer"),
            server: Some(b"Facebook"),
        },
        None,
    ),
)?;

Failing to supply the same pair of custom identifiers in any of the three steps above will result in an error in attempting to complete the protocol!

Note that if only one of the client and server identifiers are present, then Identifiers can be used to specify them individually.

Key Exchange Context

A key exchange protocol typically allows for the specifying of shared “context” information between the two parties before the exchange is complete, so as to bind the integrity of application-specific data or configuration parameters to the security of the key exchange. During the login phase, the client and server can specify this context using:

For both of these messages, the WithContextAndIdentifiers variant can be used to specify these fields in addition to custom identifiers, with the ordering of the fields as WithContextAndIdentifiers(context, Identifiers::ClientAndServerIdentifiers(username, server_name)).

Dummy Server Login

For applications in which the server does not wish to reveal to the client whether an existing password file has been registered, the server can return a “dummy” credential response message to the client for an unregistered client, which is indistinguishable from the normal credential response message that the server would return for a registered client. The dummy message is created by passing a None to the password_file parameter for ServerLogin::start.

Remote Private Keys

Servers that want to store their private key in an external location (e.g. in an HSM or vault) can do so with the SecretKey trait. This allows ServerSetup to be constructed using an existing keypair without exposing the bytes of the private key to this library.

impl SecretKey<<Default as CipherSuite>::KeGroup> for YourRemoteKey {
    type Error = YourRemoteKeyError;
    type Len = U0;

    fn diffie_hellman(
        &self,
        pk: PublicKey<<Default as CipherSuite>::KeGroup>,
    ) -> Result<GenericArray<u8, <<Default as CipherSuite>::KeGroup as KeGroup>::PkLen>, InternalError<Self::Error>> {
        YourRemoteKey::diffie_hellman(self, &pk.serialize()).map_err(InternalError::Custom)
    }

    fn public_key(
        &self
    ) -> Result<PublicKey<<Default as CipherSuite>::KeGroup>, InternalError<Self::Error>> {
        PublicKey::deserialize(&YourRemoteKey::public_key(self).map_err(InternalError::Custom)?).map_err(InternalError::into_custom)
    }

    fn serialize(&self) -> GenericArray<u8, Self::Len> {
        // if you use Serde and the "serde" crate feature, you won't need this
        todo!()
    }

    fn deserialize(input: &[u8]) -> Result<Self, InternalError<Self::Error>> {
        // if you use Serde and the "serde" crate feature, you won't need this
        todo!()
    }
}

let keypair = KeyPair::from_private_key(remote_key).unwrap();
let server_setup = ServerSetup::<Default, YourRemoteKey>::new_with_key(&mut OsRng, keypair);

Custom KSF and Parameters

An application might want to use a custom KSF (Key Stretching Function) that’s not supported directly by this crate. The maintainer of the said KSF or of the application itself can implement the Ksf trait to use it with opaque-ke. scrypt is used for this example, but any KSF can be used.

#[derive(Default)]
struct CustomKsf(scrypt::Params);

// The Ksf trait must be implemented to be used in the ciphersuite.
impl opaque_ke::ksf::Ksf for CustomKsf {
    fn hash<L: generic_array::ArrayLength<u8>>(
        &self,
        input: GenericArray<u8, L>,
    ) -> Result<GenericArray<u8, L>, opaque_ke::errors::InternalError> {
        let mut output = GenericArray::<u8, L>::default();
        scrypt::scrypt(&input, &[], &self.0, &mut output)
            .map_err(|_| opaque_ke::errors::InternalError::KsfError)?;

        Ok(output)
    }
}

It is also possible to override the default derivation parameters that are used by the KSF during registration and login. This can be especially helpful if the Ksf trait is already implemented.

// Create an Argon2 instance with the specified parameters
let argon2_params = argon2::Params::new(131072, 2, 4, None).unwrap();
let argon2_params = argon2::Argon2::new(
    argon2::Algorithm::Argon2id,
    argon2::Version::V0x13,
    argon2_params,
);

// Override the default parameters with the custom ones
let hash_params = ClientRegistrationFinishParameters {
    ksf: Some(&argon2_params),
    ..Default::default()
};

let client_registration_finish_result = client_registration_start_result
    .state
    .finish(
        &mut rng,
        password,
        server_registration_start_result.message,
        hash_params,
    )
    .unwrap();

Features

  • The argon2 feature, when enabled, introduces a dependency on argon2 and implements the Ksf trait for Argon2 with a set of default parameters. In general, secure instantiations should choose to invoke a memory-hard password hashing function when the client’s password is expected to have low entropy, instead of relying on ksf::Identity as done in the above example. The more computationally intensive the Ksf function is, the more resistant the server’s password file records will be against offline dictionary and precomputation attacks; see the OPAQUE paper for more details.

  • The serde feature, enabled by default, provides convenience functions for serializing and deserializing with serde.

  • The backend features are re-exported from curve25519-dalek and allow for selecting the corresponding backend for the curve arithmetic used. The ristretto255-u64 feature is included as the default. Other features are mapped as ristretto255-u32, ristretto255-fiat-u64 and ristretto255-fiat-u32. Any ristretto255-* backend feature will enable the ristretto255 feature, which can be used too, but keep in mind that curve25519-dalek will fail to compile without a selected backend. This enables the use of Ristretto255 as a KeGroup and OprfCs.

  • The x25519 feature is similar to the ristretto255 feature and requires to select a backend like x25519-u64, other backends are the same as in ristretto255-*. This enables X25519 as a KeGroup.

  • The ristretto255-simd feature is re-exported from curve25519-dalek and enables parallel formulas, using either AVX2 or AVX512-IFMA. This will automatically enable the ristretto255-u64 feature and requires Rust nightly.

  • The p256 feature enables the use of p256::NistP256 as a KeGroup and a OprfCs for CipherSuite.

  • The bench feature is used only for running performance benchmarks for this implementation.

Re-exports

pub use ciphersuite::CipherSuite;
pub use rand;
pub use crate::key_exchange::group::ristretto255::Ristretto255;
pub use crate::key_exchange::group::x25519::X25519;

Modules

Defines the CipherSuite trait to specify the underlying primitives for OPAQUE
A list of error types which are produced during an execution of the protocol
A convenience trait for digest bounds used throughout the library
Includes instantiations of key exchange protocols used in the login step for OPAQUE
Contains the keypair types that must be supplied for the OPAQUE API
Trait specifying a key stretching function

Structs

The state elements the client holds to perform a login
Optional parameters for client login finish
Contains the fields that are returned by a client login finish
Contains the fields that are returned by a client login start
The state elements the client holds to register itself
Optional parameters for client registration finish
Contains the fields that are returned by a client registration finish
Contains the fields that are returned by a client registration start
The answer sent by the client to the server, upon reception of the sealed envelope
The message sent by the user to the server, to initiate registration
The answer sent by the server to the user, upon reception of the login attempt
Options for specifying custom identifiers
The message sent by the client to the server, to initiate registration
The answer sent by the server to the user, upon reception of the registration attempt
The final message from the client, containing sealed cryptographic identifiers
The state elements the server holds to record a login
Contains the fields that are returned by a server login finish
Optional parameters for server login start
Contains the fields that are returned by a server login start
The state elements the server holds to record a registration
Contains the fields that are returned by a server registration start. Note that there is no state output in this step
The state elements the server holds upon setup

Type Definitions

Length of CredentialFinalization in bytes for serialization.
Length of CredentialRequest in bytes for serialization.
Length of CredentialResponse in bytes for serialization.
Length of RegistrationRequest in bytes for serialization.
Length of RegistrationResponse in bytes for serialization.
Length of RegistrationUpload in bytes for serialization.
Length of ServerRegistration in bytes for serialization.