opaque

package module
v0.18.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 24, 2026 License: MIT Imports: 20 Imported by: 3

README

OPAQUE

CI Go Reference codecov SLSA 3 OpenSSF Scorecard

  import "github.com/bytemare/opaque"

This package implements OPAQUE (RFC 9807), the augmented password-authenticated key exchange (aPAKE) protocol that is secure against pre-computation attacks. It enables a client to authenticate to a server without ever revealing its password to the server.

This implementation is developed and maintained by one of the authors of the RFC. It has not been independently audited, and even though great care about security and performance has been taken, it comes with no warranty.

What is OPAQUE?

OPAQUE is an aPAKE that is secure against pre-computation attacks. OPAQUE provides forward secrecy with respect to password leakage while also hiding the password from the server, even during password registration. OPAQUE allows applications to increase the difficulty of offline dictionary attacks via iterated hashing or other key stretching schemes. OPAQUE is also extensible, allowing clients to safely store and retrieve arbitrary application data on servers using only their password.

References

Documentation Go Reference

You can find the documentation and usage examples in the package doc.

Security and Operational Notes

  • Use a confidential transport (e.g., TLS 1.3+) to protect client identities on the wire.
  • Keep the OPRF global seed single and stable across clients, and derive unique per-client OPRF keys using a stable, unique credential identifier.
  • Ensure consistent configuration (groups, hashes, KSF) across registration and subsequent logins.
  • The server must always verify KE3 with LoginFinish before using the session secret.
  • For unknown users, return a fake record (GetFakeRecord) to reduce user-enumeration signals.
  • Rate limiting and replay tracking are application layer responsibilities. OPAQUE authenticates the transcripts, and session management is up to you.
  • The server is concurrency-safe for typical use. Avoid a hidden global state in your app layer.
  • Store ServerKeyMaterial securely. Treat secrets (e.g., private key and OPRF seed) appropriately.
  • Client.ClearState() is a best-effort to clear ephemeral material, but zeroization has language/runtime limits.

Versioning

SemVer is used for versioning. For the versions available, see the tags on the repository.

Release Integrity (SLSA Level 3)

Releases are built with the reusable bytemare/slsa workflow and ship the evidence required for SLSA Level 3 compliance:

  • 📦 Artifacts are uploaded to the release page, and include the deterministic source archive plus subjects.sha256, signed SBOM (sbom.cdx.json), GitHub provenance (*.intoto.jsonl), a reproducibility report (verification.json), and a signed Verification Summary Attestation (verification-summary.attestation.json[.bundle]).
  • ✍️ All artifacts are signed using Sigstore with transparency via Rekor.
  • ✅ Verification (or see the latest docs at bytemare/slsa):
curl -sSL https://raw.githubusercontent.com/bytemare/slsa/main/verify-release.sh -o verify-release.sh
chmod +x verify-release.sh
./verify-release.sh --repo <owner>/<repo> --tag <tag> --mode full --signer-repo bytemare/slsa

Run again with --mode reproduce to build in a container, or --mode vsa to validate just the verification summary.

Contributing

Please read CONTRIBUTING.md for details on the code of conduct, and the process for submitting pull requests.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Overview

Package opaque implements OPAQUE, an asymmetric password-authenticated key exchange protocol that is secure against pre-computation attacks. It enables a client to authenticate to a server without ever revealing its password to the server. Protocol details can be found on the IETF RFC page (https://datatracker.ietf.org/doc/draft-irtf-cfrg-opaque) and on the GitHub specification repository (https://github.com/cfrg/draft-irtf-cfrg-opaque).

Example (Authentication)

Example_authentication demonstrates in a single function the interactions between a client and a server for the login phase. This is of course a proof-of-concept demonstration, as client and server execute separately.

package main

import (
	"bytes"
	"fmt"
	"log"

	"github.com/bytemare/ecc"

	"github.com/bytemare/opaque"
	"github.com/bytemare/opaque/message"
)

var (
	exampleClientRecord  *opaque.ClientRecord
	secretGlobalOprfSeed []byte
	serverPrivateKey     *ecc.Scalar
	serverPublicKey      *ecc.Element
)

// Example_Registration demonstrates in a single function the interactions between a client and a server for the
// registration phase. This is of course a proof-of-concept demonstration, as client and server execute separately.
// The server outputs a ClientRecord and the credential identifier. The latter is a unique identifier for  a given
// client (e.g. database entry ID), and that must absolutely stay the same for the whole client existence and
// never be reused.
func Example_registration() {

	serverID := []byte("server")
	clientID := []byte("username")
	conf := opaque.DefaultConfiguration()

	password := []byte("password")

	secretGlobalOprfSeed = conf.GenerateOPRFSeed()
	serverPrivateKey, serverPublicKey = conf.KeyGen()

	skm := &opaque.ServerKeyMaterial{
		Identity:       serverID,
		PrivateKey:     serverPrivateKey,
		PublicKeyBytes: serverPublicKey.Encode(),
		OPRFGlobalSeed: secretGlobalOprfSeed,
	}

	server, err := conf.Server()
	if err != nil {
		log.Fatalln(err)
	}

	if err = server.SetKeyMaterial(skm); err != nil {
		log.Fatalln(err)
	}

	client, err := conf.Client()
	if err != nil {
		log.Fatalln(err)
	}

	var message1, message2, message3 []byte
	var credID []byte

	{
		c1, err := client.RegistrationInit(password)
		if err != nil {
			log.Fatalln(err)
		}

		message1 = c1.Serialize()
	}

	{
		request, err := server.Deserialize.RegistrationRequest(message1)
		if err != nil {
			log.Fatalln(err)
		}

		credID = opaque.RandomBytes(64)

		response, err := server.RegistrationResponse(request, credID, nil)
		if err != nil {
			log.Fatalln(err)
		}

		message2 = response.Serialize()
	}

	{
		response, err := client.Deserialize.RegistrationResponse(message2)
		if err != nil {
			log.Fatalln(err)
		}

		record, _, err := client.RegistrationFinalize(response, clientID, serverID)
		if err != nil {
			log.Fatalln(err)
		}

		message3 = record.Serialize()
	}

	{
		record, err := server.Deserialize.RegistrationRecord(message3)
		if err != nil {
			log.Fatalln(err)
		}

		exampleClientRecord = &opaque.ClientRecord{
			CredentialIdentifier: credID,
			ClientIdentity:       clientID,
			RegistrationRecord:   record,
		}

		fmt.Println("OPAQUE registration is easy!")
	}

}

func main() {
	// For the purpose of this demo, we consider the following registration has already happened.
	{
		Example_registration()
	}

	// Information shared by both client and server.
	serverID := []byte("server")
	clientID := []byte("username")
	conf := opaque.DefaultConfiguration()

	// Secret client information.
	password := []byte("password")

	// Secret server information.
	// This can be encoded and backed up with the Encode() and Hex() methods, and later recovered.
	skm := &opaque.ServerKeyMaterial{
		Identity:       serverID,
		PrivateKey:     serverPrivateKey,
		PublicKeyBytes: serverPublicKey.Encode(),
		OPRFGlobalSeed: secretGlobalOprfSeed,
	}

	// Runtime instantiation of the server.
	server, err := conf.Server()
	if err != nil {
		log.Fatalln(err)
	}

	if err = server.SetKeyMaterial(skm); err != nil {
		log.Fatalln(err)
	}

	// Runtime instantiation of the client.
	client, err := conf.Client()
	if err != nil {
		log.Fatalln(err)
	}

	// These are the 3 login messages that will be exchanged,
	// and the respective sessions keys for the client and server.
	var message1, message2, message3 []byte
	var clientSessionKey, serverSessionKey []byte

	// The client initiates the ball and sends the serialized ke1 to the server.
	{
		ke1, err := client.GenerateKE1(password)
		if err != nil {
			log.Fatalln(err)
		}

		message1 = ke1.Serialize()
	}

	// The server interprets ke1, and sends back ke2.
	var serverOutput *opaque.ServerOutput
	{
		var (
			ke1 *message.KE1
			ke2 *message.KE2
		)

		ke1, err = server.Deserialize.KE1(message1)
		if err != nil {
			log.Fatalln(err)
		}

		ke2, serverOutput, err = server.GenerateKE2(ke1, exampleClientRecord)
		if err != nil {
			log.Fatalln(err)
		}

		message2 = ke2.Serialize()
	}

	// The client interprets ke2. If everything went fine, the server is considered trustworthy and the client
	// can use the shared session key and secret export key.
	{
		var ke3 *message.KE3
		ke2, err := client.Deserialize.KE2(message2)
		if err != nil {
			log.Fatalln(err)
		}

		// In this example, we don't use the secret export key. The client sends the serialized ke3 to the server.
		ke3, clientSessionKey, _, err = client.GenerateKE3(ke2, clientID, serverID)
		if err != nil {
			log.Fatalln(err)
		}

		message3 = ke3.Serialize()

		// If no error occurred here, the server can be trusted, and the client can use the session key.
		fmt.Println("OPAQUE succeeded from the client perspective, which can now safely use the session key.")
	}

	// The server must absolutely validate this last message to authenticate the client and continue. If this message
	// does not return successfully, the server must not send any secret or sensitive information and immediately cease
	// the connection.
	{
		ke3, err := server.Deserialize.KE3(message3)
		if err != nil {
			log.Fatalln(err)
		}

		if err = server.LoginFinish(ke3, serverOutput.ClientMAC); err != nil {
			log.Fatalln(err)
		}

		// If no error occurred here, the client can be trusted, and the server can use the session key.
		fmt.Println("OPAQUE succeeded from the server perspective, which can now safely use the session key.")

		// If no error occurred at this point, the server can trust the client and safely extract the shared session key.
		serverSessionKey = serverOutput.SessionSecret
	}

	// The following test does not exist in the real world and simply proves the point that the keys match for the demo.
	if !bytes.Equal(clientSessionKey, serverSessionKey) {
		log.Fatalln("Oh no! Abort! The shared session keys don't match!")
	}

	fmt.Println("OPAQUE is much awesome!")
}
Output:
OPAQUE registration is easy!
OPAQUE succeeded from the client perspective, which can now safely use the session key.
OPAQUE succeeded from the server perspective, which can now safely use the session key.
OPAQUE is much awesome!
Example (Configuration)

Example_Configuration shows how to instantiate a configuration, which is used to initialize clients and servers from. Configurations MUST remain the same for a given client between sessions, or the client won't be able to execute the protocol. Configurations can be serialized and deserialized, if you need to save, hardcode, or transmit it.

package main

import (
	"crypto"
	"encoding/hex"
	"fmt"
	"log"

	"github.com/bytemare/ksf"

	"github.com/bytemare/opaque"
)

func main() {
	// You can compose your own configuration or choose a recommended default configuration.
	// The two following configuration setups are the same.
	defaultConf := opaque.DefaultConfiguration()

	customConf := &opaque.Configuration{
		OPRF:    opaque.RistrettoSha512,
		AKE:     opaque.RistrettoSha512,
		KSF:     ksf.Argon2id,
		KDF:     crypto.SHA512,
		MAC:     crypto.SHA512,
		Hash:    crypto.SHA512,
		Context: nil,
	}

	if err := defaultConf.Equals(customConf); err != nil {
		// This is just a demo to show that the two configurations are the same.
		// In practice, you would just pick one way to create a configuration.
		// Recommended is to use DefaultConfiguration() or one of the other defaults.
		log.Fatalln("Oh no! Configurations differ!")
	}

	// A configuration can be saved encoded and saved, and later loaded and decoded at runtime.
	// Any additional 'Context' is also included.
	encoded := defaultConf.Serialize()
	fmt.Printf("Encoded Configuration: %s\n", hex.EncodeToString(encoded))

	// This how you decode that configuration.
	conf, err := opaque.DeserializeConfiguration(encoded)
	if err != nil {
		log.Fatalf("Oh no! Decoding the configurations failed! %v", err)
	}

	if err = defaultConf.Equals(conf); err != nil {
		log.Fatalln("Oh no! Something went wrong in decoding the configuration!")
	}

	fmt.Println("OPAQUE configuration is easy!")

}
Output:
Encoded Configuration: 0101010707070000
OPAQUE configuration is easy!
Example (Deserialization)

Example_Deserialization demonstrates a couple of ways to deserialize OPAQUE protocol messages. Message interpretation depends on the configuration context it's exchanged in. Hence, we need the corresponding configuration. We can then directly deserialize messages from a Configuration or pass them to Client or Server instances which can do it as well. You must know in advance what message you are expecting, and call the appropriate deserialization function.

package main

import (
	"encoding/hex"
	"fmt"
	"log"
	"reflect"

	"github.com/bytemare/opaque"
)

func main() {
	// Let's say we have this RegistrationRequest message we received on the wire.
	registrationMessage, _ := hex.DecodeString("9857e1694af550c515e56a9103292ad07a014b020708d3df57ac4b151f58d323")

	// Pick your configuration.
	conf := opaque.DefaultConfiguration()

	// You can directly deserialize and test the message's validity in that configuration by getting a deserializer.
	deserializer, err := conf.Deserializer()
	if err != nil {
		log.Fatalln(err)
	}

	requestD, err := deserializer.RegistrationRequest(registrationMessage)
	if err != nil {
		log.Fatalln(err)
	}

	// Or if you already have a Server instance, you can use that also.
	server, err := conf.Server()
	if err != nil {
		log.Fatalln(err)
	}

	requestS, err := server.Deserialize.RegistrationRequest(registrationMessage)
	if err != nil {
		// The error message will tell us what's wrong.
		log.Fatalln(err)
	}

	// Alternatively, a Client instance can do that as well.
	client, err := conf.Client()
	if err != nil {
		// The error message will tell us what's wrong.
		log.Fatalln(err)
	}

	requestC, err := client.Deserialize.RegistrationRequest(registrationMessage)
	if err != nil {
		// The error message will tell us what's wrong.
		log.Fatalln(err)
	}

	// All these yield the same message. The following is just a test to proof that point.
	{
		if !reflect.DeepEqual(requestD, requestS) ||
			!reflect.DeepEqual(requestD, requestC) ||
			!reflect.DeepEqual(requestS, requestC) {
			log.Fatalf("Unexpected divergent RegistrationMessages:\n\t- %v\n\t- %v\n\t- %v",
				hex.EncodeToString(requestD.Serialize()),
				hex.EncodeToString(requestS.Serialize()),
				hex.EncodeToString(requestC.Serialize()))
		}

		fmt.Println("OPAQUE messages deserialization is easy!")
	}

}
Output:
OPAQUE messages deserialization is easy!
Example (ErrorHandling)

Example: handling high-level errors and specific causes.

package main

import (
	"errors"
	"fmt"

	"github.com/bytemare/opaque"
	"github.com/bytemare/opaque/internal"
)

func main() {
	// Simulate an error chain
	err := opaque.ErrAuthentication.Join(internal.ErrServerAuthentication, internal.ErrInvalidServerMac)

	switch {
	case errors.Is(err, opaque.ErrAuthentication):
		// top-level class
		fmt.Println("auth error: abort and do not use keys")
		// handle specific cause
		if errors.Is(err, internal.ErrInvalidServerMac) {
			fmt.Println("server MAC invalid: report generic failure")
		}
	case errors.Is(err, opaque.ErrRegistration):
		fmt.Println("registration error: notify client")
	default:
		fmt.Println("unexpected error")
	}
}
Output:
auth error: abort and do not use keys
server MAC invalid: report generic failure
Example (FakeResponse)

Example_FakeResponse shows how to counter some client enumeration attacks by faking an existing client entry. Precompute the fake client record, and return it when no valid record was found. Use this with the server's GenerateKE2 function whenever a client wants to retrieve an envelope but a client entry does not exist. Failing to do so results in an attacker being able to enumerate users.

package main

import (
	"encoding/hex"
	"fmt"
	"log"

	"github.com/bytemare/ecc"

	"github.com/bytemare/opaque"
)

var (
	secretGlobalOprfSeed []byte
	serverPrivateKey     *ecc.Scalar
	serverPublicKey      *ecc.Element
)

// Example_ServerSetup shows how to set up the long term values for the OPAQUE server.
// - The secret OPRF seed can be unique for each client or the same for all, but must be
// the same for a given client between registration and all login sessions.
// - The AKE key pair can also be the same for all clients or unique, but must be
// the same for a given client between registration and all login sessions.
func Example_serverSetup() {

	serverID := []byte("server-identity")
	conf := opaque.DefaultConfiguration()
	secretGlobalOprfSeed = conf.GenerateOPRFSeed()
	serverPrivateKey, serverPublicKey = conf.KeyGen()

	if serverPrivateKey == nil || serverPublicKey == nil || secretGlobalOprfSeed == nil {
		log.Fatalf("Oh no! Something went wrong setting up the server secrets!")
	}

	server, err := conf.Server()
	if err != nil {
		log.Fatalln(err)
	}

	skm := &opaque.ServerKeyMaterial{
		Identity:       serverID,
		PrivateKey:     serverPrivateKey,
		PublicKeyBytes: serverPublicKey.Encode(),
		OPRFGlobalSeed: secretGlobalOprfSeed,
	}

	if err = server.SetKeyMaterial(skm); err != nil {
		log.Fatalln(err)
	}

	fmt.Println("OPAQUE server initialized.")

}

func main() {
	// The server must have been set up with its long term values once. So we're calling this, here, for the demo.
	{
		Example_serverSetup()
	}

	// Precompute the fake client record, store it, and use it when no valid record was found for the requested client
	// id. The malicious client will purposefully fail, but can't determine the difference with an existing
	// client record. Choose the same configuration as in your app.
	conf := opaque.DefaultConfiguration()
	fakeRecord, err := conf.GetFakeRecord([]byte("id_to_the_fake_client"))
	if err != nil {
		log.Fatalln(err)
	}

	// Runtime instantiation of the server. Note that there's no change to the regular server setup.
	server, err := conf.Server()
	if err != nil {
		log.Fatalln(err)
	}

	skm := &opaque.ServerKeyMaterial{
		PrivateKey:     serverPrivateKey,
		PublicKeyBytes: serverPublicKey.Encode(),
		OPRFGlobalSeed: secretGlobalOprfSeed,
	}

	if err = server.SetKeyMaterial(skm); err != nil {
		log.Fatalln(err)
	}

	// Later, during protocol execution, let's say this is the fraudulent login message we received,
	// for which no client entry exists.
	message1, _ := hex.DecodeString("b4d366645e7ae380f9d476e1319e67c1821f7a5d3dfbfc4e26c7898351979139" +
		"0ea528fc609b4393b0353e85fdbb20c6067c11919f40d93d8bb229967fc2878c" +
		"209786ef4b960bfbfe10481c1fd301300fc72dc4234a1e829b556c720f904d30")

	// Continue as usual, using the fake record in lieu of the (non-)existing one. The server the sends
	// back the serialized ke2 message message2.
	var message2 []byte
	{
		ke1, err := server.Deserialize.KE1(message1)
		if err != nil {
			log.Fatalln(err)
		}

		ke2, _, err := server.GenerateKE2(ke1, fakeRecord)
		if err != nil {
			log.Fatalln(err)
		}

		message2 = ke2.Serialize()
	}

	// The following is just a test to check everything went fine.
	{
		if len(message2) == 0 {
			log.Fatalln("Fake KE2 is unexpectedly empty.")
		}

		fmt.Println("Thwarting OPAQUE client enumeration is easy!")
	}

}
Output:
OPAQUE server initialized.
Thwarting OPAQUE client enumeration is easy!
Example (Registration)

Example_Registration demonstrates in a single function the interactions between a client and a server for the registration phase. This is of course a proof-of-concept demonstration, as client and server execute separately. The server outputs a ClientRecord and the credential identifier. The latter is a unique identifier for a given client (e.g. database entry ID), and that must absolutely stay the same for the whole client existence and never be reused.

package main

import (
	"fmt"
	"log"

	"github.com/bytemare/ecc"

	"github.com/bytemare/opaque"
)

var (
	exampleClientRecord  *opaque.ClientRecord
	secretGlobalOprfSeed []byte
	serverPrivateKey     *ecc.Scalar
	serverPublicKey      *ecc.Element
)

func main() {
	// Information shared by both client and server.
	serverID := []byte("server")
	clientID := []byte("username")
	conf := opaque.DefaultConfiguration()

	// Secret client information.
	password := []byte("password")

	// Secret server information, this is set up once and for all, and must be the same for a given client.
	// This can be encoded and backed up with the Encode() and Hex() methods, and later recovered.
	secretGlobalOprfSeed = conf.GenerateOPRFSeed()
	serverPrivateKey, serverPublicKey = conf.KeyGen()

	skm := &opaque.ServerKeyMaterial{
		Identity:       serverID,
		PrivateKey:     serverPrivateKey,
		PublicKeyBytes: serverPublicKey.Encode(),
		OPRFGlobalSeed: secretGlobalOprfSeed,
	}

	// Runtime instantiation of the server.
	server, err := conf.Server()
	if err != nil {
		log.Fatalln(err)
	}

	if err = server.SetKeyMaterial(skm); err != nil {
		log.Fatalln(err)
	}

	// Runtime instantiation of the client.
	client, err := conf.Client()
	if err != nil {
		log.Fatalln(err)
	}

	// These are the 3 registration messages that will be exchanged.
	// The credential identifier credID is a unique identifier for a given client (e.g. database entry ID), and that
	// must absolutely stay the same for the whole client existence and never be reused.
	var message1, message2, message3 []byte
	var credID []byte

	// The client starts, serializes the message, and sends it to the server.
	{
		c1, err := client.RegistrationInit(password)
		if err != nil {
			log.Fatalln(err)
		}

		message1 = c1.Serialize()
	}

	// The server receives the encoded message, decodes it, interprets it, and returns its response.
	{
		request, err := server.Deserialize.RegistrationRequest(message1)
		if err != nil {
			log.Fatalln(err)
		}

		// The server creates a database entry for the client and creates a credential identifier that must absolutely
		// be unique among all clients.
		credID = opaque.RandomBytes(64)

		// The server uses its public key and secret OPRF seed created at the setup.
		response, err := server.RegistrationResponse(request, credID, nil)
		if err != nil {
			log.Fatalln(err)
		}

		// The server responds with its serialized response.
		message2 = response.Serialize()
	}

	// The client deserializes the responses, and sends back its final client record containing the envelope.
	{
		response, err := client.Deserialize.RegistrationResponse(message2)
		if err != nil {
			log.Fatalln(err)
		}

		// The client produces its record and a client-only-known secret export_key, that the client can use for other purposes (e.g. encrypt
		// information to store on the server, and that the server can't decrypt). We don't use in the example here.
		record, _, err := client.RegistrationFinalize(response, clientID, serverID)
		if err != nil {
			log.Fatalln(err)
		}

		message3 = record.Serialize()
	}

	// Server registers the client record.
	{
		record, err := server.Deserialize.RegistrationRecord(message3)
		if err != nil {
			log.Fatalln(err)
		}

		exampleClientRecord = &opaque.ClientRecord{
			CredentialIdentifier: credID,
			ClientIdentity:       clientID,
			RegistrationRecord:   record,
		}

		fmt.Println("OPAQUE registration is easy!")
	}

}
Output:
OPAQUE registration is easy!
Example (ServerSetup)

Example_ServerSetup shows how to set up the long term values for the OPAQUE server. - The secret OPRF seed can be unique for each client or the same for all, but must be the same for a given client between registration and all login sessions. - The AKE key pair can also be the same for all clients or unique, but must be the same for a given client between registration and all login sessions.

package main

import (
	"fmt"
	"log"

	"github.com/bytemare/ecc"

	"github.com/bytemare/opaque"
)

var (
	secretGlobalOprfSeed []byte
	serverPrivateKey     *ecc.Scalar
	serverPublicKey      *ecc.Element
)

func main() {
	// This a straightforward way to use a secure and efficient configuration.
	// They have to be run only once in the application's lifecycle, and the output values must be stored appropriately.
	serverID := []byte("server-identity")
	conf := opaque.DefaultConfiguration()
	secretGlobalOprfSeed = conf.GenerateOPRFSeed()
	serverPrivateKey, serverPublicKey = conf.KeyGen()

	if serverPrivateKey == nil || serverPublicKey == nil || secretGlobalOprfSeed == nil {
		log.Fatalf("Oh no! Something went wrong setting up the server secrets!")
	}

	// Server setup
	server, err := conf.Server()
	if err != nil {
		log.Fatalln(err)
	}

	skm := &opaque.ServerKeyMaterial{
		Identity:       serverID,
		PrivateKey:     serverPrivateKey,
		PublicKeyBytes: serverPublicKey.Encode(),
		OPRFGlobalSeed: secretGlobalOprfSeed,
	}

	if err = server.SetKeyMaterial(skm); err != nil {
		log.Fatalln(err)
	}

	fmt.Println("OPAQUE server initialized.")

}
Output:
OPAQUE server initialized.

Index

Examples

Constants

View Source
const (
	// RistrettoSha512 identifies the Ristretto255 group and SHA-512.
	RistrettoSha512 = Group(ecc.Ristretto255Sha512)

	// P256Sha256 identifies the NIST P-256 group and SHA-256.
	P256Sha256 = Group(ecc.P256Sha256)

	// P384Sha512 identifies the NIST P-384 group and SHA-384.
	P384Sha512 = Group(ecc.P384Sha384)

	// P521Sha512 identifies the NIST P-512 group and SHA-512.
	P521Sha512 = Group(ecc.P521Sha512)
)

Variables

View Source
var (
	// ErrConfiguration indicates that the configuration is invalid.
	ErrConfiguration = ErrCodeConfiguration.New("invalid configuration")

	// ErrRegistration indicates that the registration process failed.
	ErrRegistration = ErrCodeRegistration.New("registration failed")

	// ErrAuthentication indicates that the authentication process failed.
	ErrAuthentication = ErrCodeAuthentication.New("authentication failed")

	// ErrRegistrationRequest indicates an error with a registration request.
	ErrRegistrationRequest = ErrCodeMessage.New("invalid registration request")

	// ErrRegistrationResponse indicates an error with a registration response.
	ErrRegistrationResponse = ErrCodeMessage.New("invalid registration response")

	// ErrRegistrationRecord indicates an error with a registration record.
	ErrRegistrationRecord = ErrCodeMessage.New("invalid registration record")

	// ErrKE1 indicates an error with a KE1 message.
	ErrKE1 = ErrCodeMessage.New("invalid KE1 message")

	// ErrKE2 indicates an error with a KE2 message.
	ErrKE2 = ErrCodeMessage.New("invalid KE2 message")

	// ErrKE3 indicates an error with a KE3 message.
	ErrKE3 = ErrCodeMessage.New("invalid KE3 message")

	// ErrServerKeyMaterial indicates that the server's key material is invalid.
	ErrServerKeyMaterial = ErrCodeServerKeyMaterial.New("invalid server key material")

	// ErrServerOptions indicates that the provided server options are invalid.
	ErrServerOptions = ErrCodeServerOptions.New("invalid server options")

	// ErrClientRecord indicates that the client record is invalid.
	ErrClientRecord = ErrCodeClientRecord.New("invalid client record")

	// ErrClientState indicates that the client state is invalid.
	ErrClientState = ErrCodeClientState.New("invalid client state")

	// ErrClientOptions indicates that the client options are invalid.
	ErrClientOptions = ErrCodeClientOptions.New("invalid client options")

	// ErrCriticalAbort - todo: use this if tampering has been detected?
	ErrCriticalAbort = errors.New("critical - abort protocol")
)

Functions

func DeserializeElement added in v0.18.0

func DeserializeElement(g ecc.Group, input []byte) (*ecc.Element, error)

DeserializeElement takes a byte slice and attempts to decode it into an Element of the given group, and returns an error if the decoding fails or if the element is the identity element (point at infinity).

func DeserializeScalar added in v0.18.0

func DeserializeScalar(g ecc.Group, input []byte) (*ecc.Scalar, error)

DeserializeScalar takes a byte slice and attempts to decode it into a Scalar of the given group, and returns an error if the decoding fails or if the scalar is zero.

func IsValidElement added in v0.18.0

func IsValidElement(g ecc.Group, e *ecc.Element) error

IsValidElement checks if the provided element is valid. It must be: - non-nil - non-identity (point at infinity) - part of the correct group.

func IsValidScalar added in v0.18.0

func IsValidScalar(g ecc.Group, s *ecc.Scalar) error

IsValidScalar checks if the provided scalar is valid. It must be: - non-nil - non-zero - part of the correct group.

func RandomBytes added in v0.8.1

func RandomBytes(length int) []byte

RandomBytes returns random bytes of length len (wrapper for crypto/rand).

Types

type AKEOptions added in v0.18.0

type AKEOptions struct {
	SecretKeyShare     *ecc.Scalar
	SecretKeyShareSeed []byte
	Nonce              []byte
}

AKEOptions override the secure default values or internally generated values. Only use this if you know what you're doing. Reusing seeds and nonces across sessions is a security risk, and breaks forward secrecy. If the SecretKeyShare is provided, SecretKeyShareSeed is ignored.

type Client

type Client struct {
	Deserialize *Deserializer
	// contains filtered or unexported fields
}

Client represents an OPAQUE Client, exposing its methods and holding its state. The state includes the OPRF blind, during a registration or authentication session, and the ephemeral secret key share during an authentication session.

func NewClient

func NewClient(c *Configuration) (*Client, error)

NewClient returns a new Client instantiation given the application Configuration.

func (*Client) ClearState added in v0.18.0

func (c *Client) ClearState()

ClearState attempts to zero out the client's secret material and state, and sets them to nil. It is strongly recommended to call this method after the client is done with the protocol, to avoid leaking sensitive key material.

func (*Client) GenerateKE1 added in v0.18.0

func (c *Client) GenerateKE1(password []byte, options ...*ClientOptions) (*message.KE1, error)

GenerateKE1 initiates the authentication process, returning a KE1 message, blinding the given password. This method initiates a state, so the same client instance should be used to call GenerateKE3() later on. To complete GenerateKE3() from a different client instance for the same message, ClientOptions must provide ResumePassword, OPRFBlind, ResumeKE1, and AKE.SecretKeyShare or AKE.SecretKeyShareSeed.

func (*Client) GenerateKE3 added in v0.18.0

func (c *Client) GenerateKE3(
	ke2 *message.KE2,
	clientIdentity, serverIdentity []byte,
	options ...*ClientOptions,
) (ke3 *message.KE3, sessionKey, exportKey []byte, err error)

GenerateKE3 returns a KE3 message given the server's KE2 response message and the identities. If the client or server identity parameters are nil, the client and server's public keys are taken as identities for both.

func (*Client) RegistrationFinalize

func (c *Client) RegistrationFinalize(
	resp *message.RegistrationResponse,
	clientIdentity, serverIdentity []byte,
	options ...*ClientOptions,
) (record *message.RegistrationRecord, exportKey []byte, err error)

RegistrationFinalize returns a RegistrationRecord message given the identities and the server's RegistrationResponse, and the export key, that the client can use for other means. If a different client instance is used than the one that called RegistrationInit, ClientOptions must provide both ResumePassword and OPRFBlind.

func (*Client) RegistrationInit

func (c *Client) RegistrationInit(
	password []byte,
	options ...*ClientOptions,
) (*message.RegistrationRequest, error)

RegistrationInit returns a RegistrationRequest message blinding the given password. This will initiate a state, so the same client instance should be used to call RegistrationFinalize() later on. Optionally, that value can be overridden by providing a ClientOptions with an OPRF Blind value, at your own risks.

type ClientOptions added in v0.18.0

type ClientOptions struct {
	OPRFBlind *ecc.Scalar
	// ResumePassword is only used in RegistrationFinalize and GenerateKE3, to complete a run from a different
	// client instance that does not hold the prior state.
	ResumePassword []byte
	AKE            *AKEOptions
	// ResumeKE1 is only used in GenerateKE3, to complete a run from a different client instance that does not hold
	// the prior state.
	ResumeKE1 []byte
	KSFSalt   []byte
	// RegistrationEnvelopeNonce is only used in RegistrationFinalize.
	RegistrationEnvelopeNonce []byte
	KDFSalt                   []byte
	KSFParameters             []uint64
	// RegistrationEnvelopeNonceLength is only used in RegistrationFinalize.
	RegistrationEnvelopeNonceLength int
	KSFLength                       int // If 0 or not set, will default to the OPRF length.
}

ClientOptions override the secure default values or internally generated values. Only use this if you know what you're doing. Reusing seeds and nonces across sessions is a security risk, and breaks forward secrecy.

type ClientRecord

type ClientRecord struct {
	*message.RegistrationRecord
	CredentialIdentifier []byte
	ClientIdentity       []byte
}

ClientRecord is a server-side structure enabling the storage of user relevant information.

type Configuration

type Configuration struct {
	Context []byte
	KDF     crypto.Hash    `json:"kdf"`
	MAC     crypto.Hash    `json:"mac"`
	Hash    crypto.Hash    `json:"hash"`
	KSF     ksf.Identifier `json:"ksf"`
	OPRF    Group          `json:"oprf"`
	AKE     Group          `json:"group"`
}

Configuration represents an OPAQUE configuration. OPRF and AKE can use different groups. Matching KDF, MAC, and Hash selections remain recommended but are not required.

func DefaultConfiguration

func DefaultConfiguration() *Configuration

DefaultConfiguration returns a default configuration with strong parameters.

func DeserializeConfiguration

func DeserializeConfiguration(encoded []byte) (*Configuration, error)

DeserializeConfiguration decodes the input and returns a Parameter structure.

func (*Configuration) Client

func (c *Configuration) Client() (*Client, error)

Client returns a newly instantiated Client from the Configuration.

func (*Configuration) DecodeServerKeyMaterial added in v0.18.0

func (c *Configuration) DecodeServerKeyMaterial(data []byte) (*ServerKeyMaterial, error)

DecodeServerKeyMaterial decodes the server key material from a byte slice.

func (*Configuration) DecodeServerKeyMaterialHex added in v0.18.0

func (c *Configuration) DecodeServerKeyMaterialHex(data string) (*ServerKeyMaterial, error)

DecodeServerKeyMaterialHex decodes the server key material from a hex string.

func (*Configuration) Deserializer added in v0.8.2

func (c *Configuration) Deserializer() (*Deserializer, error)

Deserializer returns a pointer to a Deserializer structure allowing deserialization of messages in the given configuration.

func (*Configuration) Equals added in v0.18.0

func (c *Configuration) Equals(other *Configuration) error

Equals compares two configurations and returns nil if they are equal, or an error otherwise.

func (*Configuration) GenerateOPRFSeed added in v0.8.1

func (c *Configuration) GenerateOPRFSeed() []byte

GenerateOPRFSeed returns a OPRF seed valid in the given configuration.

func (*Configuration) GetFakeRecord added in v0.8.2

func (c *Configuration) GetFakeRecord(credentialIdentifier []byte) (*ClientRecord, error)

GetFakeRecord creates a fake Client record to be used when no existing client record exists, to defend against client enumeration techniques.

func (*Configuration) KeyGen added in v0.8.1

func (c *Configuration) KeyGen() (secretKey *ecc.Scalar, publicKey *ecc.Element)

KeyGen returns a key pair in the AKE ecc.

func (*Configuration) Serialize

func (c *Configuration) Serialize() []byte

Serialize returns the byte encoding of the Configuration structure.

func (*Configuration) Server

func (c *Configuration) Server() (*Server, error)

Server returns a newly instantiated Server from the Configuration.

type Deserializer added in v0.8.1

type Deserializer struct {
	// contains filtered or unexported fields
}

Deserializer exposes the message deserialization methods.

func (*Deserializer) DecodePrivateKey added in v0.18.0

func (d *Deserializer) DecodePrivateKey(encoded []byte) (*ecc.Scalar, error)

DecodePrivateKey takes a serialized private key (a scalar) and attempts to return it's decoded form.

func (*Deserializer) DecodePublicKey added in v0.18.0

func (d *Deserializer) DecodePublicKey(encoded []byte) (*ecc.Element, error)

DecodePublicKey takes a serialized public key (a point) and attempts to return it's decoded form.

func (*Deserializer) KE1 added in v0.8.1

func (d *Deserializer) KE1(ke1 []byte) (*message.KE1, error)

KE1 takes a serialized KE1 message and returns a deserialized KE1 structure.

func (*Deserializer) KE2 added in v0.8.1

func (d *Deserializer) KE2(ke2 []byte) (*message.KE2, error)

KE2 takes a serialized KE2 message and returns a deserialized KE2 structure.

func (*Deserializer) KE3 added in v0.8.1

func (d *Deserializer) KE3(ke3 []byte) (*message.KE3, error)

KE3 takes a serialized KE3 message and returns a deserialized KE3 structure.

func (*Deserializer) RegistrationRecord added in v0.8.1

func (d *Deserializer) RegistrationRecord(record []byte) (*message.RegistrationRecord, error)

RegistrationRecord takes a serialized RegistrationRecord message and returns a deserialized RegistrationRecord structure.

func (*Deserializer) RegistrationRequest added in v0.8.1

func (d *Deserializer) RegistrationRequest(registrationRequest []byte) (*message.RegistrationRequest, error)

RegistrationRequest takes a serialized RegistrationRequest message and returns a deserialized RegistrationRequest structure.

func (*Deserializer) RegistrationResponse added in v0.8.1

func (d *Deserializer) RegistrationResponse(registrationResponse []byte) (*message.RegistrationResponse, error)

RegistrationResponse takes a serialized RegistrationResponse message and returns a deserialized RegistrationResponse structure.

type Error added in v0.18.0

type Error struct {
	Err     error
	Message string
	Code    ErrorCode
}

Error represents an error in the OPAQUE protocol.

func (*Error) As added in v0.18.0

func (e *Error) As(target any) bool

As implements the errors.As method for the Error type. It allows type assertion to specific error types.

func (*Error) Error added in v0.18.0

func (e *Error) Error() string

Error implements the error interface for the Error type. By convention, we return only the concise form of the current error, without the cause. The cause can be retrieved with the Unwrap() method.

func (*Error) Format added in v0.18.0

func (e *Error) Format(f fmt.State, verb rune)

Format implements the fmt.Formatter interface for the Error type. It allows formatting the error in different ways.

func (*Error) Is added in v0.18.0

func (e *Error) Is(target error) bool

Is implements the errors.Is method for the Error type. It allows checking if the error is of a specific ErrorCode.

func (*Error) Join added in v0.18.0

func (e *Error) Join(errs ...error) error

Join wraps the provided error to the current error.

func (*Error) LogValue added in v0.18.0

func (e *Error) LogValue() slog.Value

LogValue implements the slog.LogValuer interface for the Error type.

func (*Error) Unwrap added in v0.18.0

func (e *Error) Unwrap() error

Unwrap implements the errors.Unwrap method for the Error type. It allows retrieving the underlying error, if any.

type ErrorCode added in v0.18.0

type ErrorCode byte //nolint:errname // This is an error code, not an error type.

ErrorCode represents the type of error in the OPAQUE protocol. It is used to categorize errors and provide a consistent way to handle error conditions.

const (
	// ErrCodeUnknown represents an unknown error.
	ErrCodeUnknown ErrorCode = iota

	// ErrCodeConfiguration represents an error related to the configuration.
	ErrCodeConfiguration

	// ErrCodeRegistration represents an error related to the registration phase.
	ErrCodeRegistration

	// ErrCodeAuthentication represents an error related to the authentication phase.
	ErrCodeAuthentication

	// ErrCodeMessage represents an error related to message processing.
	ErrCodeMessage

	// ErrCodeServerKeyMaterial represents an error related to the server's key material.
	ErrCodeServerKeyMaterial

	// ErrCodeServerOptions represents an error related to the server's optional arguments.
	ErrCodeServerOptions

	// ErrCodeClientRecord represents an error related to the clients record.
	ErrCodeClientRecord

	// ErrCodeClientState represents an error related to the client's state.
	ErrCodeClientState

	// ErrCodeClientOptions represents an error related to the client's optional arguments.
	ErrCodeClientOptions
)

func (ErrorCode) As added in v0.18.0

func (c ErrorCode) As(target any) bool

As implements the errors.As method for the Error type. It allows type assertion to specific error types.

func (ErrorCode) Error added in v0.18.0

func (c ErrorCode) Error() string

Error implements the error interface for the ErrorCode type. It returns a string representation of the error code.

func (ErrorCode) Is added in v0.18.0

func (c ErrorCode) Is(target error) bool

Is implements the errors.Is method for the ErrorCode type. It allows checking if the error is of a specific ErrorCode.

func (ErrorCode) New added in v0.18.0

func (c ErrorCode) New(message string, errs ...error) *Error

New creates a new Error with the given message and errors.

func (ErrorCode) String added in v0.18.0

func (c ErrorCode) String() string

String returns the string representation of the ErrorCode. If the code is not recognized, it returns "unknown_error".

type Group

type Group byte

Group identifies a supported group configuration for OPRF or AKE.

func (Group) Available added in v0.9.0

func (g Group) Available() bool

Available returns whether the Group byte is recognized in this implementation. This allows to fail early when working with multiple versions not using the same configuration and ecc.

func (Group) Group added in v0.9.1

func (g Group) Group() ecc.Group

Group returns the EC Group used in the Ciphersuite.

func (Group) OPRF added in v0.9.1

func (g Group) OPRF() oprf.Identifier

OPRF returns the OPRF Identifier used in the Ciphersuite.

type Server

type Server struct {
	ServerKeyMaterial *ServerKeyMaterial
	Deserialize       *Deserializer
	// contains filtered or unexported fields
}

Server represents an OPAQUE Server, exposing its functions and holding its key material. The server is thread-safe and can be used concurrently by multiple goroutines to serve clients, given the same key material. The server's key material must be set before using the registration and login functions.

func NewServer

func NewServer(c *Configuration) (*Server, error)

NewServer returns a Server instantiation given the application Configuration.

func (*Server) GenerateKE2 added in v0.18.0

func (s *Server) GenerateKE2(
	ke1 *message.KE1,
	record *ClientRecord,
	options ...*ServerOptions,
) (*message.KE2, *ServerOutput, error)

GenerateKE2 responds to a KE1 message with a KE2 message a client record. The ServerKeyMaterial must be set before calling this method using SetKeyMaterial. This method can be used concurrently by multiple goroutines for different clients.

func (*Server) LoginFinish added in v0.8.0

func (s *Server) LoginFinish(ke3 *message.KE3, expectedClientMac []byte) error

LoginFinish verifies whether the KE3 message holds the client MAC that matches expectedClientMac. If this method returns an error, the session secret must not be used. If it returns nil, the session secret can be used.

func (*Server) RegistrationResponse

func (s *Server) RegistrationResponse(
	req *message.RegistrationRequest,
	clientCredentialIdentifier []byte,
	clientOPRFKey *ecc.Scalar,

) (*message.RegistrationResponse, error)

RegistrationResponse computes the server’s response to a client’s RegistrationRequest.

It client-specific OPRF key on the client input and returns a RegistrationResponse message in response to the client's RegistrationRequest message req.

Parameters:

  • clientCredentialIdentifier: application-defined, stable identifier for this client. If clientOPRFKey is nil, the OPRF key is derived from this identifier and the server’s global OPRF seed provided in the server key material. The identifier MUST be unique per client and stable for the credential’s lifetime from registration to all subsequent logins. It MUST NOT be empty if clientOPRFKey is nil.
  • clientOPRFKey: optional explicit client OPRF secret key. If non-nil, it is used directly instead of deriving it from the credentialIdentifier and globalOPRFSeed.

Preconditions:

  • s.SetKeyMaterial has been called. The server’s AKE public key (ServerKeyMaterial.PublicKeyBytes) is required to populate the response.
  • If clientOPRFKey is nil, the global OPRF seed (ServerKeyMaterial.OPRFGlobalSeed) MUST be set.

Security and usage notes:

  • Using a single global OPRF seed together with unique clientCredentialIdentifier values prevents client enumeration by deriving per-client OPRF keys.
  • The clientCredentialIdentifier MUST be consistent across registration and subsequent logins for a given client.
  • When the OPRF key is derived internally, it is zeroed after use.

func (*Server) SetKeyMaterial added in v0.10.0

func (s *Server) SetKeyMaterial(skm *ServerKeyMaterial) error

SetKeyMaterial sets the server's key material. The ServerKeyMaterial must be set before calling RegistrationResponse or GenerateKE2. It validates the key material and returns an error if it is invalid.

type ServerKeyMaterial added in v0.18.0

type ServerKeyMaterial struct {
	// The server's long-term secret key. Required only for Login, and not used in Registration.
	PrivateKey *ecc.Scalar

	// The server's public key in bytes. Must be provided during registration and login.
	PublicKeyBytes []byte

	// The seed to derive the OPRF key for the clients with. Required if client OPRF keys won't be provided directly.
	OPRFGlobalSeed []byte

	// The server's identity. If empty, the server's public key will be used as the identity.
	Identity []byte
}

ServerKeyMaterial holds the server's long-term identity and key material for OPAQUE registration and authentication sessions. Note that, depending on the setup, these values are not client specific and can be reused across clients.

func (*ServerKeyMaterial) Encode added in v0.18.0

func (s *ServerKeyMaterial) Encode() []byte

Encode encodes the server key material into a byte slice.

func (*ServerKeyMaterial) Flush added in v0.18.0

func (s *ServerKeyMaterial) Flush()

Flush does a best-effort attempt to clear the server key material from memory. It is not guaranteed that the contents are correctly wiped from memory.

func (*ServerKeyMaterial) Hex added in v0.18.0

func (s *ServerKeyMaterial) Hex() string

Hex encodes the server key material into a hex string.

type ServerOptions added in v0.18.0

type ServerOptions struct {
	ClientOPRFKey *ecc.Scalar
	AKE           *AKEOptions
	MaskingNonce  []byte
}

ServerOptions override the secure default values or internally generated values. Only use this if you know what you're doing. Reusing seeds and nonces across sessions is a security risk, and breaks forward secrecy.

type ServerOutput added in v0.18.0

type ServerOutput struct {
	ClientMAC     []byte
	SessionSecret []byte
}

ServerOutput is the result of a successful GenerateKE2 call, containing the client MAC and session secret. The SessionSecret must not be used before the client's KE3 message has been verified against the ClientMAC.

Directories

Path Synopsis
Package internal provides values, structures, and functions to operate OPAQUE that are not part of the public API.
Package internal provides values, structures, and functions to operate OPAQUE that are not part of the public API.
ake
Package ake provides high-level functions for the 3DH AKE.
Package ake provides high-level functions for the 3DH AKE.
encoding
Package encoding provides encoding utilities.
Package encoding provides encoding utilities.
envelope
Package envelope provides utility functions for the envelope credential management..
Package envelope provides utility functions for the envelope credential management..
ksf
Package ksf provides the Key Stretching Functions.
Package ksf provides the Key Stretching Functions.
masking
Package masking provides the credential masking mechanism.
Package masking provides the credential masking mechanism.
oprf
Package oprf implements the Elliptic Curve Oblivious Pseudorandom Function (EC-OPRF) from https://tools.ietf.org/html/draft-irtf-cfrg-voprf.
Package oprf implements the Elliptic Curve Oblivious Pseudorandom Function (EC-OPRF) from https://tools.ietf.org/html/draft-irtf-cfrg-voprf.
tag
Package tag provides the static tag strings to OPAQUE.
Package tag provides the static tag strings to OPAQUE.
Package message provides message structures for the OPAQUE protocol.
Package message provides message structures for the OPAQUE protocol.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL