Expand description
An implementation of the OPAQUE augmented password authentication key exchange protocol
Note: This implementation is in sync with draft-irtf-cfrg-opaque-16, but this specification is subject to change, until the final version published by the IETF.
§Minimum Supported Rust Version
Rust 1.74 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, and can use ServerSetup::serialize
and
ServerSetup::deserialize
to save and restore the instance.
§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 and runs ClientLogin::finish
on it.
If the authentication is successful, then the client obtains a
ClientLoginFinishResult
. Otherwise, on failure, the
algorithm outputs an
InvalidLoginError
error.
The resulting ClientLoginFinishResult
obtained by client in this step
contains, among other things, a CredentialFinalization
to be sent to the
server to complete the protocol, and a
session_key
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 byte
string (of length equal to the output size of voprf::CipherSuite::Hash
)
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 byte string
(of length equal to the output size of voprf::CipherSuite::Hash
) 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:
- The second login message, where the server can populate
ServerLoginStartParameters
, and - The third login message, where the client can populate
ClientLoginFinishParameters
.
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 onargon2
and implements theKsf
trait forArgon2
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 onksf::Identity
as done in the above example. The more computationally intensive theKsf
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
ristretto255
feature enables usingRistretto255
as aKeGroup
andOprfCs
. To select a specific backend see the curve25519-dalek documentation. -
The
curve25519
feature enables Curve25519 as aKeGroup
. To select a specific backend see the curve25519-dalek documentation. -
The
p256
feature enables the use ofp256::NistP256
as aKeGroup
and aOprfCs
forCipherSuite
. -
The
bench
feature is used only for running performance benchmarks for this implementation.
Re-exports§
pub use ciphersuite::CipherSuite;
pub use crate::key_exchange::group::curve25519::Curve25519;
curve25519
pub use crate::key_exchange::group::ristretto255::Ristretto255;
ristretto255
pub use rand;
Modules§
- ciphersuite
- Defines the
CipherSuite
trait to specify the underlying primitives for OPAQUE - errors
- A list of error types which are produced during an execution of the protocol
- hash
- A convenience trait for digest bounds used throughout the library
- key_
exchange - Includes instantiations of key exchange protocols used in the login step for OPAQUE
- keypair
- Contains the keypair types that must be supplied for the OPAQUE API
- ksf
- Trait specifying a key stretching function
Structs§
- Client
Login - The state elements the client holds to perform a login
- Client
Login Finish Parameters - Optional parameters for client login finish
- Client
Login Finish Result - Contains the fields that are returned by a client login finish
- Client
Login Start Result - Contains the fields that are returned by a client login start
- Client
Registration - The state elements the client holds to register itself
- Client
Registration Finish Parameters - Optional parameters for client registration finish
- Client
Registration Finish Result - Contains the fields that are returned by a client registration finish
- Client
Registration Start Result - Contains the fields that are returned by a client registration start
- Credential
Finalization - The answer sent by the client to the server, upon reception of the sealed envelope
- Credential
Request - The message sent by the user to the server, to initiate registration
- Credential
Response - The answer sent by the server to the user, upon reception of the login attempt
- Identifiers
- Options for specifying custom identifiers
- Registration
Request - The message sent by the client to the server, to initiate registration
- Registration
Response - The answer sent by the server to the user, upon reception of the registration attempt
- Registration
Upload - The final message from the client, containing sealed cryptographic identifiers
- Server
Login - The state elements the server holds to record a login
- Server
Login Finish Result - Contains the fields that are returned by a server login finish
- Server
Login Start Parameters - Optional parameters for server login start
- Server
Login Start Result - Contains the fields that are returned by a server login start
- Server
Registration - The state elements the server holds to record a registration
- Server
Registration Start Result - Contains the fields that are returned by a server registration start. Note that there is no state output in this step
- Server
Setup - The state elements the server holds upon setup
Type Aliases§
- Credential
Finalization Len - Length of
CredentialFinalization
in bytes for serialization. - Credential
Request Len - Length of
CredentialRequest
in bytes for serialization. - Credential
Response Len - Length of
CredentialResponse
in bytes for serialization. - Registration
Request Len - Length of
RegistrationRequest
in bytes for serialization. - Registration
Response Len - Length of
RegistrationResponse
in bytes for serialization. - Registration
Upload Len - Length of
RegistrationUpload
in bytes for serialization. - Server
Registration Len - Length of
ServerRegistration
in bytes for serialization.