From 587a62a2362af151de4f672cef04602fd541ef56 Mon Sep 17 00:00:00 2001 From: Jan-nku Date: Thu, 5 Oct 2023 20:25:09 +0800 Subject: [PATCH] HPAKE --- LICENSE | 21 + README.md | 47 ++ SECURITY.md | 11 + client.go | 282 +++++++++ deserializer.go | 232 ++++++++ examples_test.go | 455 +++++++++++++++ go.mod | 18 + go.sum | 18 + internal/ake/3dh.go | 193 +++++++ internal/ake/client.go | 91 +++ internal/ake/server.go | 123 ++++ internal/configuration.go | 56 ++ internal/encoding/encoding.go | 70 +++ internal/encoding/i2osp.go | 77 +++ internal/encoding/misc.go | 53 ++ internal/hash.go | 119 ++++ internal/keyrecovery/envelope.go | 128 +++++ internal/keyrecovery/keyrec.go | 39 ++ internal/masking/masking.go | 94 +++ internal/oprf/client.go | 63 +++ internal/oprf/oprf.go | 142 +++++ internal/oprf/server.go | 199 +++++++ internal/tag/strings.go | 80 +++ message/credentials.go | 59 ++ message/login.go | 58 ++ message/registration.go | 58 ++ opaque.go | 287 ++++++++++ server.go | 418 ++++++++++++++ tests/client_test.go | 345 +++++++++++ tests/deserializer_test.go | 216 +++++++ tests/encoding_test.go | 217 +++++++ tests/fuzz_test.go | 453 +++++++++++++++ tests/helper_test.go | 236 ++++++++ tests/opaque_test.go | 502 ++++++++++++++++ tests/oprfVectors.json | 137 +++++ tests/oprf_test.go | 268 +++++++++ tests/server_test.go | 304 ++++++++++ ...z-a1ae7e291f3d65e6ffdc8acb9f39f3538e3e0ab4 | 9 + ...z-a1ae7e291f3d65e6ffdc8acb9f39f3538e3e0ab4 | 9 + tests/vectors.json | 322 +++++++++++ tests/vectors_test.go | 534 ++++++++++++++++++ 41 files changed, 7043 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 client.go create mode 100644 deserializer.go create mode 100644 examples_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/ake/3dh.go create mode 100644 internal/ake/client.go create mode 100644 internal/ake/server.go create mode 100644 internal/configuration.go create mode 100644 internal/encoding/encoding.go create mode 100644 internal/encoding/i2osp.go create mode 100644 internal/encoding/misc.go create mode 100644 internal/hash.go create mode 100644 internal/keyrecovery/envelope.go create mode 100644 internal/keyrecovery/keyrec.go create mode 100644 internal/masking/masking.go create mode 100644 internal/oprf/client.go create mode 100644 internal/oprf/oprf.go create mode 100644 internal/oprf/server.go create mode 100644 internal/tag/strings.go create mode 100644 message/credentials.go create mode 100644 message/login.go create mode 100644 message/registration.go create mode 100644 opaque.go create mode 100644 server.go create mode 100644 tests/client_test.go create mode 100644 tests/deserializer_test.go create mode 100644 tests/encoding_test.go create mode 100644 tests/fuzz_test.go create mode 100644 tests/helper_test.go create mode 100644 tests/opaque_test.go create mode 100644 tests/oprfVectors.json create mode 100644 tests/oprf_test.go create mode 100644 tests/server_test.go create mode 100644 tests/testdata/fuzz/FuzzDeserializeKE1/fuzzbuzz-a1ae7e291f3d65e6ffdc8acb9f39f3538e3e0ab4 create mode 100644 tests/testdata/fuzz/FuzzDeserializeKE2/fuzzbuzz-a1ae7e291f3d65e6ffdc8acb9f39f3538e3e0ab4 create mode 100644 tests/vectors.json create mode 100644 tests/vectors_test.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a45ba7d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Daniel Bourdrez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b4393e --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# OPAQUE +[![OPAQUE](https://github.com/Jan-nku/opaque/actions/workflows/ci.yml/badge.svg)](https://github.com/Jan-nku/opaque/actions/workflows/ci.yml) +[![Go Reference](https://pkg.go.dev/badge/github.com/Jan-nku/opaque.svg)](https://pkg.go.dev/github.com/Jan-nku/opaque) +[![codecov](https://codecov.io/gh/Jan-nku/opaque/branch/main/graph/badge.svg?token=5bQfB0OctA)](https://codecov.io/gh/Jan-nku/opaque) + +``` + import "github.com/Jan-nku/opaque" +``` + +This package implements [OPAQUE](https://datatracker.ietf.org/doc/draft-irtf-cfrg-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. + +This implementation is developed by one of the authors of the RFC [Internet Draft](https://github.com/cfrg/draft-irtf-cfrg-opaque). +The main branch is in sync with the latest developments of the draft, and [the releases](https://github.com/Jan-nku/opaque/releases) +correspond to the [official draft versions](https://datatracker.ietf.org/doc/draft-irtf-cfrg-opaque). + +#### What is OPAQUE? + +> OPAQUE is a PKI-free secure 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 +- [The original paper](https://eprint.iacr.org/2018/163.pdf) from Jarecki, Krawczyk, and Xu. +- [OPAQUE is used in WhatsApp](https://www.whatsapp.com/security/WhatsApp_Security_Encrypted_Backups_Whitepaper.pdf) to enable end-to-end encrypted backups. +- [The GitHub repo](https://github.com/cfrg/draft-irtf-cfrg-opaque) where the draft is being specified. + +## Documentation [![Go Reference](https://pkg.go.dev/badge/github.com/Jan-nku/opaque.svg)](https://pkg.go.dev/github.com/Jan-nku/opaque) + +You can find the documentation and usage examples in [the package doc](https://pkg.go.dev/github.com/Jan-nku/opaque) and [the project wiki](https://github.com/Jan-nku/opaque/wiki) . + +## Versioning + +[SemVer](http://semver.org) is used for versioning. For the versions available, see the [tags on the repository](https://github.com/Jan-nku/opaque/tags). + +Minor v0.x versions match the corresponding CFRG draft version, the master branch implements the latest changes of [the draft development](https://github.com/cfrg/draft-irtf-cfrg-opaque). + +## Contributing + +Please read [CONTRIBUTING.md](.github/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](LICENSE) file for details. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..cd25faf --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported Versions + +The OPAQUE protocol is still in the process of specification. Therefore, this implementation evolves with the draft. +Only the latest version will be benefit from security fixes. Maintainers of projects using this implementation of OPAQUE are invited to update their dependency. + +## Reporting a Vulnerability + +Vulnerabilities can be reported through Github issues, here: https://github.com/Jan-nku/opaque/security/advisories +If the issue is sensitive enough that the reporter thinks the discussion needs more confidentiality, we can discuss options there (e.g. On a Security Advisory or per e-mail). diff --git a/client.go b/client.go new file mode 100644 index 0000000..888d08b --- /dev/null +++ b/client.go @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package opaque + +import ( + "errors" + "fmt" + + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/internal/ake" + "github.com/Jan-nku/opaque/internal/encoding" + "github.com/Jan-nku/opaque/internal/keyrecovery" + "github.com/Jan-nku/opaque/internal/masking" + "github.com/Jan-nku/opaque/internal/oprf" + "github.com/Jan-nku/opaque/internal/tag" + "github.com/Jan-nku/opaque/message" +) + +var ( + // errInvalidMaskedLength happens when unmasking a masked response. + errInvalidMaskedLength = errors.New("invalid masked response length") + + // errKe1Missing happens when LoginFinish is called and the client has no Ke1 in state. + errKe1Missing = errors.New("missing KE1 in client state") +) + +// Client represents an OPAQUE Client, exposing its functions and holding its state. +type Client struct { + Deserialize *Deserializer + OPRF *oprf.Client + Ake *ake.Client + conf *internal.Configuration +} + +// NewClient returns a new Client instantiation given the application Configuration. +func NewClient(c *Configuration) (*Client, error) { + if c == nil { + c = DefaultConfiguration() + } + + conf, err := c.toInternal() + if err != nil { + return nil, err + } + + return &Client{ + OPRF: conf.OPRF.Client(), + Ake: ake.NewClient(), + Deserialize: &Deserializer{conf: conf}, + conf: conf, + }, nil +} + +// GetConf returns the internal configuration. +func (c *Client) GetConf() *internal.Configuration { + return c.conf +} + +// TODO: buildPRK +// buildPRK derives the randomized password from the OPRF output. +func (c *Client) buildPRK(evaluation *group.Element) []byte { + //output = h(pw, h'(pw)^ku) []byte + output := c.OPRF.Finalize(evaluation) + // Harden函数做一个延展? + stretched := c.conf.KSF.Harden(output, nil, c.conf.OPRF.Group().ElementLength()) + //Extract + return c.conf.KDF.Extract(nil, encoding.Concat(output, stretched)) +} + +// ClientRegistrationInitOptions enables setting internal client values for the client registration. +type ClientRegistrationInitOptions struct { + // OPRFBlind: optional + OPRFBlind *group.Scalar +} + +func getClientRegistrationInitBlind(options []ClientRegistrationInitOptions) *group.Scalar { + if len(options) == 0 { + return nil + } + + return options[0].OPRFBlind +} + +// TODO: RegistrationInit +// RegistrationInit returns a RegistrationRequest message blinding the given password. +func (c *Client) RegistrationInit( + password []byte, + username []byte, + options ...ClientRegistrationInitOptions, +) *message.RegistrationRequest { + m := c.OPRF.Blind(password, getClientRegistrationInitBlind(options)) + + return &message.RegistrationRequest{ + BlindedMessage: m, + UserName: username, + } +} + +// TODO: ClientRegistrationFinalizeOptions contain ClientIdentity, ServerIdentity +// ClientRegistrationFinalizeOptions enables setting optional client values for the client registration. +type ClientRegistrationFinalizeOptions struct { + // ClientIdentity: optional + ClientIdentity []byte + // ServerIdentity: optional + ServerIdentity []byte + // EnvelopeNonce : optional + EnvelopeNonce []byte +} + +func initClientRegistrationFinalizeOptions(options []ClientRegistrationFinalizeOptions) *keyrecovery.Credentials { + if len(options) == 0 { + return &keyrecovery.Credentials{ + ClientIdentity: nil, + ServerIdentity: nil, + EnvelopeNonce: nil, + } + } + + return &keyrecovery.Credentials{ + ClientIdentity: options[0].ClientIdentity, + ServerIdentity: options[0].ServerIdentity, + EnvelopeNonce: options[0].EnvelopeNonce, + } +} + +// TODO: RegistrationFinalize +// RegistrationFinalize returns a RegistrationRecord message given the identities and the server's RegistrationResponse. +func (c *Client) RegistrationFinalize( + resp *message.RegistrationResponse, + options ...ClientRegistrationFinalizeOptions, +) (record *message.RegistrationRecord, exportKey []byte) { + credentials := initClientRegistrationFinalizeOptions(options) + // generate randomizedPwd from resp.EvaluatedMessage, related to h(pw, h'(pw)^ku) + randomizedPwd := c.buildPRK(resp.EvaluatedMessage) + + // generate maskingKey from hash(randomizedPwd) + maskingKey := c.conf.KDF.Expand(randomizedPwd, []byte(tag.MaskingKey), c.conf.KDF.Size()) + envelope, clientPublicKey, exportKey := keyrecovery.Store(c.conf, randomizedPwd, resp.Pks, credentials) + + //envelope 存储 在 RegistrationRecord 中 + return &message.RegistrationRecord{ + PublicKey: clientPublicKey, + MaskingKey: maskingKey, + Envelope: envelope.Serialize(), + }, exportKey +} + +// ClientLoginInitOptions enables setting optional values for the session, which default to secure random values if not +// set. +type ClientLoginInitOptions struct { + // Blind: optional + Blind *group.Scalar + // EphemeralSecretKey: optional + EphemeralSecretKey *group.Scalar + // Nonce: optional + Nonce []byte + // NonceLength: optional + NonceLength uint +} + +func (c ClientLoginInitOptions) get() (*group.Scalar, ake.Options) { + return c.Blind, ake.Options{ + EphemeralSecretKey: c.EphemeralSecretKey, + Nonce: c.Nonce, + NonceLength: c.NonceLength, + } +} + +func getClientLoginInitOptions(options []ClientLoginInitOptions) (*group.Scalar, ake.Options) { + if len(options) != 0 { + return options[0].get() + } + + return nil, ake.Options{ + EphemeralSecretKey: nil, + Nonce: nil, + NonceLength: internal.NonceLength, + } +} + +// TODO: LoginInit(password) +// LoginInit initiates the authentication process, returning a KE1 message blinding the given password. +func (c *Client) LoginInit(password, username []byte, options ...ClientLoginInitOptions) *message.KE1 { + blind, akeOptions := getClientLoginInitOptions(options) + m := c.OPRF.Blind(password, blind) + + //TODO: start initiates the 3dh protocol, call setOptions(g, options) function + ke1 := c.Ake.Start(c.conf.Group, akeOptions) + ke1.CredentialRequest = message.NewCredentialRequest(c.conf.OPRF, m) + ke1.UserName = make([]byte, len(username)) + copy(ke1.UserName, username) + c.Ake.Ke1 = ke1.Serialize() + + return ke1 +} + +// ClientLoginFinishOptions enables setting optional client values for the client registration. +type ClientLoginFinishOptions struct { + // ClientIdentity: optional + ClientIdentity []byte + // ServerIdentity: optional + ServerIdentity []byte +} + +func initClientLoginFinishOptions(options []ClientLoginFinishOptions) *ake.Identities { + if len(options) == 0 { + return &ake.Identities{ + ClientIdentity: nil, + ServerIdentity: nil, + } + } + + return &ake.Identities{ + ClientIdentity: options[0].ClientIdentity, + ServerIdentity: options[0].ServerIdentity, + } +} + +// LoginFinish returns a KE3 message given the server's KE2 response message and the identities. If the idc +// or ids parameters are nil, the client and server's public keys are taken as identities for both. +func (c *Client) LoginFinish( + ke2 *message.KE2, options ...ClientLoginFinishOptions, +) (ke3 *message.KE3, exportKey []byte, err error) { + if len(c.Ake.Ke1) == 0 { + return nil, nil, errKe1Missing + } + + // This test is very important as it avoids buffer overflows in subsequent parsing. + if len(ke2.MaskedResponse) != c.conf.Group.ElementLength()+c.conf.EnvelopeSize { + return nil, nil, errInvalidMaskedLength + } + + identities := initClientLoginFinishOptions(options) + + // Finalize the OPRF. + randomizedPwd := c.buildPRK(ke2.EvaluatedMessage) + + // Decrypt the masked response. + serverPublicKey, serverPublicKeyBytes, + envelope, err := masking.Unmask(c.conf, randomizedPwd, ke2.MaskingNonce, ke2.MaskedResponse) + if err != nil { + return nil, nil, fmt.Errorf("unmasking: %w", err) + } + + // Recover the client keys. + clientSecretKey, clientPublicKey, + exportKey, err := keyrecovery.Recover( + c.conf, + randomizedPwd, + serverPublicKeyBytes, + identities.ClientIdentity, + identities.ServerIdentity, + envelope) + if err != nil { + return nil, nil, fmt.Errorf("key recovery: %w", err) + } + + // Finalize the AKE. + // SetIdentities sets the client and server identities to their respective public key if not set. + identities.SetIdentities(clientPublicKey, serverPublicKeyBytes) + + // produce ke3 + ke3, err = c.Ake.Finalize(c.conf, identities, clientSecretKey, serverPublicKey, ke2) + if err != nil { + return nil, nil, fmt.Errorf("finalizing AKE: %w", err) + } + + return ke3, exportKey, nil +} + +// SessionKey returns the session key if the previous call to LoginFinish() was successful. +func (c *Client) SessionKey() []byte { + return c.Ake.SessionKey() +} diff --git a/deserializer.go b/deserializer.go new file mode 100644 index 0000000..b3466f3 --- /dev/null +++ b/deserializer.go @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package opaque + +import ( + "errors" + "fmt" + + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/message" +) + +var ( + errInvalidMessageLength = errors.New("invalid message length for the configuration") + errInvalidBlindedData = errors.New("blinded data is an invalid point") + errInvalidClientEPK = errors.New("invalid ephemeral client public key") + errInvalidEvaluatedData = errors.New("invalid OPRF evaluation") + errInvalidServerEPK = errors.New("invalid ephemeral server public key") + errInvalidServerPK = errors.New("invalid server public key") + errInvalidClientPK = errors.New("invalid client public key") +) + +// Deserializer exposes the message deserialization functions. +type Deserializer struct { + conf *internal.Configuration +} + +// RegistrationRequest takes a serialized RegistrationRequest message and returns a deserialized +// RegistrationRequest structure. +func (d *Deserializer) RegistrationRequest(registrationRequest []byte) (*message.RegistrationRequest, error) { + //if len(registrationRequest) != d.conf.OPRF.Group().ElementLength() { + // return nil, errInvalidMessageLength + //} + + blindedMessage := d.conf.OPRF.Group().NewElement() + if err := blindedMessage.Decode(registrationRequest[:d.conf.OPRF.Group().ElementLength()]); err != nil { + return nil, errInvalidBlindedData + } + + username := make([]byte, len(registrationRequest)-d.conf.OPRF.Group().ElementLength()) + copy(username, registrationRequest[d.conf.OPRF.Group().ElementLength():]) + return &message.RegistrationRequest{BlindedMessage: blindedMessage, UserName: username}, nil +} + +func (d *Deserializer) registrationResponseLength() int { + return d.conf.OPRF.Group().ElementLength() + d.conf.Group.ElementLength() +} + +// RegistrationResponse takes a serialized RegistrationResponse message and returns a deserialized +// RegistrationResponse structure. +func (d *Deserializer) RegistrationResponse(registrationResponse []byte) (*message.RegistrationResponse, error) { + if len(registrationResponse) != d.registrationResponseLength() { + return nil, errInvalidMessageLength + } + + evaluatedMessage := d.conf.OPRF.Group().NewElement() + if err := evaluatedMessage.Decode(registrationResponse[:d.conf.OPRF.Group().ElementLength()]); err != nil { + return nil, errInvalidEvaluatedData + } + + pks := d.conf.Group.NewElement() + if err := pks.Decode(registrationResponse[d.conf.OPRF.Group().ElementLength():]); err != nil { + return nil, errInvalidServerPK + } + + return &message.RegistrationResponse{ + EvaluatedMessage: evaluatedMessage, + Pks: pks, + }, nil +} + +func (d *Deserializer) recordLength() int { + return d.conf.Group.ElementLength() + d.conf.Hash.Size() + d.conf.EnvelopeSize +} + +// RegistrationRecord takes a serialized RegistrationRecord message and returns a deserialized +// RegistrationRecord structure. +func (d *Deserializer) RegistrationRecord(record []byte) (*message.RegistrationRecord, error) { + if len(record) != d.recordLength() { + return nil, errInvalidMessageLength + } + + pk := record[:d.conf.Group.ElementLength()] + maskingKey := record[d.conf.Group.ElementLength() : d.conf.Group.ElementLength()+d.conf.Hash.Size()] + env := record[d.conf.Group.ElementLength()+d.conf.Hash.Size():] + + pku := d.conf.Group.NewElement() + if err := pku.Decode(pk); err != nil { + return nil, errInvalidClientPK + } + + return &message.RegistrationRecord{ + PublicKey: pku, + MaskingKey: maskingKey, + Envelope: env, + }, nil +} + +func (d *Deserializer) deserializeCredentialRequest(input []byte) (*message.CredentialRequest, error) { + blindedMessage := d.conf.OPRF.Group().NewElement() + if err := blindedMessage.Decode(input[:d.conf.OPRF.Group().ElementLength()]); err != nil { + return nil, errInvalidBlindedData + } + + return message.NewCredentialRequest(d.conf.OPRF, blindedMessage), nil +} + +func (d *Deserializer) deserializeCredentialResponse( + input []byte, + maxResponseLength int, +) (*message.CredentialResponse, error) { + data := d.conf.OPRF.Group().NewElement() + if err := data.Decode(input[:d.conf.OPRF.Group().ElementLength()]); err != nil { + return nil, errInvalidEvaluatedData + } + + return message.NewCredentialResponse(data, + input[d.conf.OPRF.Group().ElementLength():d.conf.OPRF.Group().ElementLength()+d.conf.NonceLen], + input[d.conf.OPRF.Group().ElementLength()+d.conf.NonceLen:maxResponseLength]), nil +} + +//func (d *Deserializer) ke1Length() int { +// return d.conf.OPRF.Group().ElementLength() + d.conf.NonceLen + d.conf.Group.ElementLength() +//} + +// KE1 takes a serialized KE1 message and returns a deserialized KE1 structure. +func (d *Deserializer) KE1(ke1 []byte) (*message.KE1, error) { + //if len(ke1) != d.ke1Length() { + // return nil, errInvalidMessageLength + //} + + request, err := d.deserializeCredentialRequest(ke1) + if err != nil { + return nil, err + } + + nonceU := ke1[d.conf.OPRF.Group().ElementLength() : d.conf.OPRF.Group().ElementLength()+d.conf.NonceLen] + + epku := d.conf.Group.NewElement() + if err := epku.Decode(ke1[d.conf.OPRF.Group().ElementLength()+d.conf.NonceLen : 2*d.conf.OPRF.Group().ElementLength()+d.conf.NonceLen]); err != nil { + return nil, errInvalidClientEPK + } + + username := make([]byte, len(ke1)-(2*d.conf.OPRF.Group().ElementLength()+d.conf.NonceLen)) + copy(username, ke1[2*d.conf.OPRF.Group().ElementLength()+d.conf.NonceLen:]) + + return &message.KE1{ + CredentialRequest: request, + NonceU: nonceU, + EpkU: epku, + UserName: username, + }, nil +} + +func (d *Deserializer) ke2LengthWithoutCreds() int { + return d.conf.NonceLen + d.conf.Group.ElementLength() + d.conf.MAC.Size() +} + +func (d *Deserializer) credentialResponseLength() int { + return d.conf.OPRF.Group().ElementLength() + d.conf.NonceLen + d.conf.Group.ElementLength() + d.conf.EnvelopeSize +} + +// KE2 takes a serialized KE2 message and returns a deserialized KE2 structure. +func (d *Deserializer) KE2(ke2 []byte) (*message.KE2, error) { + // size of credential response + maxResponseLength := d.credentialResponseLength() + + // Verify it matches the size of a legal KE2 + if len(ke2) != maxResponseLength+d.ke2LengthWithoutCreds() { + return nil, errInvalidMessageLength + } + + cresp, err := d.deserializeCredentialResponse(ke2, maxResponseLength) + if err != nil { + return nil, err + } + + nonceS := ke2[maxResponseLength : maxResponseLength+d.conf.NonceLen] + offset := maxResponseLength + d.conf.NonceLen + epk := ke2[offset : offset+d.conf.Group.ElementLength()] + offset += d.conf.Group.ElementLength() + mac := ke2[offset:] + + epks := d.conf.Group.NewElement() + if err := epks.Decode(epk); err != nil { + return nil, errInvalidServerEPK + } + + return &message.KE2{ + CredentialResponse: cresp, + NonceS: nonceS, + EpkS: epks, + Mac: mac, + }, nil +} + +// KE3 takes a serialized KE3 message and returns a deserialized KE3 structure. +func (d *Deserializer) KE3(ke3 []byte) (*message.KE3, error) { + if len(ke3) != d.conf.MAC.Size() { + return nil, errInvalidMessageLength + } + + return &message.KE3{Mac: ke3}, nil +} + +// DecodeAkePrivateKey takes a serialized private key (a scalar) and attempts to return it's decoded form. +func (d *Deserializer) DecodeAkePrivateKey(encoded []byte) (*group.Scalar, error) { + sk := d.conf.Group.NewScalar() + if err := sk.Decode(encoded); err != nil { + return nil, fmt.Errorf("invalid private key: %w", err) + } + + return sk, nil +} + +// DecodeAkePublicKey takes a serialized public key (a point) and attempts to return it's decoded form. +func (d *Deserializer) DecodeAkePublicKey(encoded []byte) (*group.Element, error) { + pk := d.conf.Group.NewElement() + if err := pk.Decode(encoded); err != nil { + return nil, fmt.Errorf("invalid public key: %w", err) + } + + return pk, nil +} diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..dc4253d --- /dev/null +++ b/examples_test.go @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package opaque_test + +import ( + "bytes" + "crypto" + "encoding/hex" + "fmt" + "log" + "reflect" + + "github.com/bytemare/ksf" + + "github.com/Jan-nku/opaque" +) + +var ( + exampleClientRecord *opaque.ClientRecord + secretOprfSeed, serverPrivateKey, serverPublicKey []byte +) + +func isSameConf(a, b *opaque.Configuration) bool { + if a.OPRF != b.OPRF || + a.KDF != b.KDF || + a.MAC != b.MAC || + a.Hash != b.Hash || + a.KSF != b.KSF || + a.AKE != b.AKE { + return false + } + + return bytes.Equal(a.Context, b.Context) +} + +// 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. +func Example_configuration() { + // 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, + KDF: crypto.SHA512, + MAC: crypto.SHA512, + Hash: crypto.SHA512, + KSF: ksf.Argon2id, + AKE: opaque.RistrettoSha512, + Context: nil, + } + + if !isSameConf(defaultConf, customConf) { + // isSameConf() this is just a demo function to check equality. + 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 !isSameConf(defaultConf, conf) { + log.Fatalln("Oh no! Something went wrong in decoding the configuration!") + } + + fmt.Println("OPAQUE configuration is easy!") + + // Output: Encoded Configuration: 0107070701010000 + // OPAQUE configuration is easy! +} + +// 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() { + // 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() + secretOprfSeed = conf.GenerateOPRFSeed() + serverPrivateKey, serverPublicKey = conf.KeyGen() + + if serverPrivateKey == nil || serverPublicKey == nil || secretOprfSeed == 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) + } + + if err := server.SetKeyMaterial(serverID, serverPrivateKey, serverPublicKey, secretOprfSeed); err != nil { + log.Fatalln(err) + } + + fmt.Println("OPAQUE server initialized.") + + // Output: OPAQUE server initialized. +} + +// 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. +func Example_deserialization() { + // 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_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() { + // The server must have been set up with its long term values once. So we're calling this, here, for the demo. + { + Example_serverSetup() + } + + // Secret client information. + password := []byte("password") + + // Information shared by both client and server. + serverID := []byte("server") + clientID := []byte("username") + conf := opaque.DefaultConfiguration() + + // Runtime instantiation for the client and server. + client, err := conf.Client() + if err != nil { + log.Fatalln(err) + } + + server, err := conf.Server() + 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 := client.RegistrationInit(password) + 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) + pks, err := server.Deserialize.DecodeAkePublicKey(serverPublicKey) + if err != nil { + log.Fatalln(err) + } + + // The server uses its public key and secret OPRF seed created at the setup. + response := server.RegistrationResponse(request, pks, credID, secretOprfSeed) + + // 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, _ := client.RegistrationFinalize(response, opaque.ClientRegistrationFinalizeOptions{ + ClientIdentity: clientID, + ServerIdentity: serverID, + }) + 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 server values initialized. + // OPAQUE registration is easy! +} + +// Example_LoginKeyExchange 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. +func Example_loginKeyExchange() { + // For the purpose of this demo, we consider the following registration has already happened. + { + Example_registration() + } + + // Secret client information. + password := []byte("password") + + // Information shared by both client and server. + serverID := []byte("server") + clientID := []byte("username") + conf := opaque.DefaultConfiguration() + + // Runtime instantiation for the client and server. + client, err := conf.Client() + if err != nil { + log.Fatalln(err) + } + + server, err := conf.Server() + if err != nil { + log.Fatalln(err) + } + + if err := server.SetKeyMaterial(serverID, serverPrivateKey, serverPublicKey, secretOprfSeed); 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 := client.LoginInit(password) + message1 = ke1.Serialize() + } + + // The server interprets ke1, and sends back ke2. + { + ke1, err := server.Deserialize.KE1(message1) + if err != nil { + log.Fatalln(err) + } + + ke2, err := server.LoginInit(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. + { + 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, _, err := client.LoginFinish(ke2, opaque.ClientLoginFinishOptions{ + ClientIdentity: clientID, + ServerIdentity: serverID, + }) + if err != nil { + log.Fatalln(err) + } + + message3 = ke3.Serialize() + + // If no error occurred, the server can be trusted, and the client can use the session key. + clientSessionKey = client.SessionKey() + } + + // 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); err != nil { + log.Fatalln(err) + } + + // If no error occurred at this point, the server can trust the client and safely extract the shared session key. + serverSessionKey = server.SessionKey() + } + + // The following test does not exist in the real world and simply proves the point that the keys match. + 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 server values initialized. + // OPAQUE registration is easy! + // OPAQUE is much awesome! +} + +// 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 LoginInit 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. +func Example_fakeResponse() { + // 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, and return it when no valid record was found. 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("fake_client")) + if 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 + { + serverID := []byte("server") + server, err := conf.Server() + if err != nil { + log.Fatalln(err) + } + + if err := server.SetKeyMaterial(serverID, serverPrivateKey, serverPublicKey, secretOprfSeed); err != nil { + log.Fatalln(err) + } + + ke1, err := server.Deserialize.KE1(message1) + if err != nil { + log.Fatalln(err) + } + + ke2, err := server.LoginInit(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 values initialized. + // Thwarting OPAQUE client enumeration is easy! +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ec6d9d2 --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module github.com/Jan-nku/opaque + +go 1.20 + +require ( + github.com/bytemare/crypto v0.4.3 + github.com/bytemare/hash v0.1.5 + github.com/bytemare/ksf v0.1.0 +) + +require ( + filippo.io/edwards25519 v1.0.0 // indirect + filippo.io/nistec v0.0.2 // indirect + github.com/bytemare/hash2curve v0.1.3 // indirect + github.com/gtank/ristretto255 v0.1.2 // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/sys v0.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..42699e7 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= +filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +filippo.io/nistec v0.0.2 h1:/NIXTUimcHIh0E2DsYucHlICvUisgj28/XEnKSEptUs= +filippo.io/nistec v0.0.2/go.mod h1:84fxC9mi+MhC2AERXI4LSa8cmSVOzrFikg6hZ4IfCyw= +github.com/bytemare/crypto v0.4.3 h1:k9sdSOcC9PdeyB6FMUUIv9wbI4A0D7vhySoWZrI/FDE= +github.com/bytemare/crypto v0.4.3/go.mod h1:UA6K3SBPZ0C2VHQXc/9LT93rWTBwXxXNZFNL4uwapPo= +github.com/bytemare/hash v0.1.5 h1:VW+X1YQ2b3chjRFHkRUnO42uclsQjXimdBCPOgIobR4= +github.com/bytemare/hash v0.1.5/go.mod h1:+QmWXTky/2b63ngqM5IYezGydn9UTFDhpX7mLYwYxCA= +github.com/bytemare/hash2curve v0.1.3 h1:BOqV8BF5dF+BbPZgIyoeAVTwd4m7jmw4LwacD1GFBvU= +github.com/bytemare/hash2curve v0.1.3/go.mod h1:Wma3DmJdn8kqiK9j120hkWvC3tQVKS1PyA8ZzyG23BI= +github.com/bytemare/ksf v0.1.0 h1:t0VobAtBVSb1SyX1RltuZ+c4gVVHLKQnUN5oYd3o3qc= +github.com/bytemare/ksf v0.1.0/go.mod h1:wKBp7KmpY482R8lOfcGFh01MsJEU0vZHw8qFEMzNoRU= +github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= +github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/ake/3dh.go b/internal/ake/3dh.go new file mode 100644 index 0000000..ed145e1 --- /dev/null +++ b/internal/ake/3dh.go @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +// Package ake provides high-level functions for the 3DH AKE. +package ake + +import ( + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/internal/encoding" + "github.com/Jan-nku/opaque/internal/tag" + "github.com/Jan-nku/opaque/message" +) + +// TODO: keyGen() +// KeyGen returns private and public keys in the group. +func KeyGen(id group.Group) (privateKey, publicKey []byte) { + scalar := id.NewScalar().Random() + point := id.Base().Multiply(scalar) + + return scalar.Encode(), point.Encode() +} + +// Identities holds the client and server identities. +type Identities struct { + ClientIdentity []byte + ServerIdentity []byte +} + +// SetIdentities sets the client and server identities to their respective public key if not set. +func (id *Identities) SetIdentities(clientPublicKey *group.Element, serverPublicKey []byte) *Identities { + if id.ClientIdentity == nil { + id.ClientIdentity = clientPublicKey.Encode() + } + + if id.ServerIdentity == nil { + id.ServerIdentity = serverPublicKey + } + + return id +} + +// Options enables setting optional ephemeral values, which default to secure random values if not set. +type Options struct { + // EphemeralSecretKey: optional + EphemeralSecretKey *group.Scalar + // Nonce: optional + Nonce []byte + // NonceLength: optional + NonceLength uint +} + +// TODO: Start finally call the func initOptions +func initOptions(g group.Group, options *Options) { + /* This is currently unreachable as the callers correctly call this function with non-nil options + if options == nil { + options = &Options{ + EphemeralSecretKey: nil, + Nonce: nil, + NonceLength: 0, + } + } + */ + if options.EphemeralSecretKey == nil { + options.EphemeralSecretKey = g.NewScalar().Random() + } + + if options.NonceLength == 0 { + options.NonceLength = internal.NonceLength + } + + if len(options.Nonce) == 0 { + options.Nonce = internal.RandomBytes(int(options.NonceLength)) + } +} + +type values struct { + ephemeralSecretKey *group.Scalar + nonce []byte +} + +// GetEphemeralSecretKey returns the state's ephemeral secret key. +func (v *values) GetEphemeralSecretKey() *group.Scalar { + return v.ephemeralSecretKey +} + +// GetNonce returns the secret nonce. +func (v *values) GetNonce() []byte { + return v.nonce +} + +func (v *values) flush() { + if v.ephemeralSecretKey != nil { + v.ephemeralSecretKey.Zero() + v.ephemeralSecretKey = nil + } + + v.nonce = nil +} + +// setOptions sets optional values. +// There's no effect if ephemeralSecretKey and nonce have already been set in a previous call. +func (v *values) setOptions(g group.Group, options Options) *group.Element { + //call initOptions func, init EphemeralSecretKey、Nonce + initOptions(g, &options) + + if v.ephemeralSecretKey == nil || + (options.EphemeralSecretKey != nil && options.EphemeralSecretKey != v.ephemeralSecretKey) { + v.ephemeralSecretKey = options.EphemeralSecretKey + } + + if v.nonce == nil { + v.nonce = options.Nonce + } + + return g.Base().Multiply(v.ephemeralSecretKey) +} + +func k3dh( + p1 *group.Element, + s1 *group.Scalar, + p2 *group.Element, + s2 *group.Scalar, + p3 *group.Element, + s3 *group.Scalar, +) []byte { + e1 := p1.Copy().Multiply(s1).Encode() + e2 := p2.Copy().Multiply(s2).Encode() + e3 := p3.Copy().Multiply(s3).Encode() + + return encoding.Concat3(e1, e2, e3) +} + +// TODO: core3DH func +func core3DH( + conf *internal.Configuration, identities *Identities, ikm, ke1 []byte, ke2 *message.KE2, +) (sessionSecret, macS, macC []byte) { + //将一些关键信息按照特定的顺序拼接并计算哈希值,用于记录和验证加密通信协议中的会话状态和重要参数 + initTranscript(conf, identities, ke1, ke2) + + serverMacKey, clientMacKey, sessionSecret := deriveKeys(conf.KDF, ikm, conf.Hash.Sum()) // preamble + serverMac := conf.MAC.MAC(serverMacKey, conf.Hash.Sum()) // transcript2 + conf.Hash.Write(serverMac) + transcript3 := conf.Hash.Sum() + clientMac := conf.MAC.MAC(clientMacKey, transcript3) + + return sessionSecret, serverMac, clientMac +} + +func buildLabel(length int, label, context []byte) []byte { + return encoding.Concat3( + encoding.I2OSP(length, 2), + encoding.EncodeVectorLen(append([]byte(tag.LabelPrefix), label...), 1), + encoding.EncodeVectorLen(context, 1)) +} + +func expand(h *internal.KDF, secret, hkdfLabel []byte) []byte { + return h.Expand(secret, hkdfLabel, h.Size()) +} + +func expandLabel(h *internal.KDF, secret, label, context []byte) []byte { + hkdfLabel := buildLabel(h.Size(), label, context) + return expand(h, secret, hkdfLabel) +} + +func deriveSecret(h *internal.KDF, secret, label, context []byte) []byte { + return expandLabel(h, secret, label, context) +} + +func initTranscript(conf *internal.Configuration, identities *Identities, ke1 []byte, ke2 *message.KE2) { + encodedClientID := encoding.EncodeVector(identities.ClientIdentity) + encodedServerID := encoding.EncodeVector(identities.ServerIdentity) + //将一些关键信息按照特定的顺序拼接并计算哈希值,用于记录和验证加密通信协议中的会话状态和重要参数 + conf.Hash.Write(encoding.Concatenate([]byte(tag.VersionTag), encoding.EncodeVector(conf.Context), + encodedClientID, ke1, + encodedServerID, ke2.CredentialResponse.Serialize(), ke2.NonceS, ke2.EpkS.Encode())) +} + +func deriveKeys(h *internal.KDF, ikm, context []byte) (serverMacKey, clientMacKey, sessionSecret []byte) { + prk := h.Extract(nil, ikm) + handshakeSecret := deriveSecret(h, prk, []byte(tag.Handshake), context) + sessionSecret = deriveSecret(h, prk, []byte(tag.SessionKey), context) + serverMacKey = expandLabel(h, handshakeSecret, []byte(tag.MacServer), nil) + clientMacKey = expandLabel(h, handshakeSecret, []byte(tag.MacClient), nil) + + return serverMacKey, clientMacKey, sessionSecret +} diff --git a/internal/ake/client.go b/internal/ake/client.go new file mode 100644 index 0000000..f6206e1 --- /dev/null +++ b/internal/ake/client.go @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package ake + +import ( + "errors" + + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/message" +) + +var errAkeInvalidServerMac = errors.New("invalid server mac") + +// Client exposes the client's AKE functions and holds its state. +type Client struct { + values + Ke1 []byte + sessionSecret []byte +} + +// TODO: ake.NewClient() +// NewClient returns a new, empty, 3DH client. +func NewClient() *Client { + return &Client{ + values: values{ + ephemeralSecretKey: nil, + nonce: nil, + }, + Ke1: nil, + sessionSecret: nil, + } +} + +// Start initiates the 3DH protocol, and returns a KE1 message with clientInfo. +func (c *Client) Start(cs group.Group, options Options) *message.KE1 { + //epk = g ^ sk + epk := c.values.setOptions(cs, options) + + return &message.KE1{ + CredentialRequest: nil, + NonceU: c.nonce, + EpkU: epk, + } +} + +// Finalize verifies and responds to KE3. If the handshake is successful, the session key is stored and this functions +// returns a KE3 message. +func (c *Client) Finalize( + conf *internal.Configuration, + identities *Identities, + clientSecretKey *group.Scalar, + serverPublicKey *group.Element, + ke2 *message.KE2, +) (*message.KE3, error) { + ikm := k3dh( + ke2.EpkS, + c.ephemeralSecretKey, + serverPublicKey, + c.ephemeralSecretKey, + ke2.EpkS, + clientSecretKey, + ) + sessionSecret, serverMac, clientMac := core3DH(conf, identities, ikm, c.Ke1, ke2) + + if !conf.MAC.Equal(serverMac, ke2.Mac) { + return nil, errAkeInvalidServerMac + } + + c.sessionSecret = sessionSecret + + return &message.KE3{Mac: clientMac}, nil +} + +// SessionKey returns the secret shared session key if a previous call to Finalize() was successful. +func (c *Client) SessionKey() []byte { + return c.sessionSecret +} + +// Flush sets all the client's session related internal AKE values to nil. +func (c *Client) Flush() { + c.values.flush() + c.sessionSecret = nil +} diff --git a/internal/ake/server.go b/internal/ake/server.go new file mode 100644 index 0000000..04bc74f --- /dev/null +++ b/internal/ake/server.go @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package ake + +import ( + "errors" + + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/message" +) + +var errStateNotEmpty = errors.New("existing state is not empty") + +// Server exposes the server's AKE functions and holds its state. +type Server struct { + values + clientMac []byte + sessionSecret []byte +} + +// NewServer returns a new, empty, 3DH server. +func NewServer() *Server { + return &Server{ + values: values{ + ephemeralSecretKey: nil, + nonce: nil, + }, + clientMac: nil, + sessionSecret: nil, + } +} + +// TODO: Response +// Response produces a 3DH server response message. +func (s *Server) Response( + conf *internal.Configuration, + identities *Identities, + serverSecretKey *group.Scalar, + clientPublicKey *group.Element, + ke1 *message.KE1, //口令盲化值、EpkU、NonceU + response *message.CredentialResponse, //OPRF二次加密值、加密服务器公钥和envelope使用的随机数和加密结果 + options Options, //nil +) *message.KE2 { + //设置values(ephemeralSecretKey, nonce), 返回epk + epk := s.values.setOptions(conf.Group, options) + + ke2 := &message.KE2{ + CredentialResponse: response, + NonceS: s.nonce, + EpkS: epk, + Mac: nil, + } + + //调用3dh协议生成sessionSecret 以及 mac(serverMac, clientMac) + //ikm = EpkU ^ s.ephemeralSecretKey || EpkU ^ serverSecretKey || clientPublicKey ^ s.ephemeralSecretKey + //ikm = g ^ esku ^ esks || g ^ esku ^ sks ^ || g ^ sku ^ esks + ikm := k3dh( + ke1.EpkU, + s.ephemeralSecretKey, + ke1.EpkU, + serverSecretKey, + clientPublicKey, + s.ephemeralSecretKey, + ) + sessionSecret, serverMac, clientMac := core3DH(conf, identities, ikm, ke1.Serialize(), ke2) + s.sessionSecret = sessionSecret + s.clientMac = clientMac + ke2.Mac = serverMac + + return ke2 +} + +// Finalize verifies the authentication tag contained in ke3. +func (s *Server) Finalize(conf *internal.Configuration, ke3 *message.KE3) bool { + return conf.MAC.Equal(s.clientMac, ke3.Mac) +} + +// SessionKey returns the secret shared session key if a previous call to Response() was successful. +func (s *Server) SessionKey() []byte { + return s.sessionSecret +} + +// ExpectedMAC returns the expected client MAC if a previous call to Response() was successful. +func (s *Server) ExpectedMAC() []byte { + return s.clientMac +} + +// SerializeState will return a []byte containing internal state of the Server. +func (s *Server) SerializeState() []byte { + state := make([]byte, len(s.clientMac)+len(s.sessionSecret)) + + i := copy(state, s.clientMac) + copy(state[i:], s.sessionSecret) + + return state +} + +// SetState will set the given clientMac and sessionSecret in the server's internal state. +func (s *Server) SetState(clientMac, sessionSecret []byte) error { + if len(s.clientMac) != 0 || len(s.sessionSecret) != 0 { + return errStateNotEmpty + } + + s.clientMac = clientMac + s.sessionSecret = sessionSecret + + return nil +} + +// Flush sets all the server's session related internal AKE values to nil. +func (s *Server) Flush() { + s.values.flush() + s.clientMac = nil + s.sessionSecret = nil +} diff --git a/internal/configuration.go b/internal/configuration.go new file mode 100644 index 0000000..93a789a --- /dev/null +++ b/internal/configuration.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +// Package internal provides values, structures, and functions to operate OPAQUE that are not part of the public API. +package internal + +import ( + cryptorand "crypto/rand" + "errors" + "fmt" + + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal/oprf" +) + +const ( + // NonceLength is the default length used for nonces. + NonceLength = 32 + + // SeedLength is the default length used for seeds. + SeedLength = 32 +) + +// ErrConfigurationInvalidLength happens when deserializing a configuration of invalid length. +var ErrConfigurationInvalidLength = errors.New("invalid encoded configuration length") + +// TODO: +// Configuration is the internal representation of the instance runtime parameters. +type Configuration struct { + KDF *KDF + MAC *Mac + Hash *Hash + KSF *KSF + OPRF oprf.Identifier + Context []byte + NonceLen int + EnvelopeSize int + Group group.Group +} + +// RandomBytes returns random bytes of length len (wrapper for crypto/rand). +func RandomBytes(length int) []byte { + r := make([]byte, length) + if _, err := cryptorand.Read(r); err != nil { + // We can as well not panic and try again in a loop and a counter to stop. + panic(fmt.Errorf("unexpected error in generating random bytes : %w", err)) + } + + return r +} diff --git a/internal/encoding/encoding.go b/internal/encoding/encoding.go new file mode 100644 index 0000000..95091d4 --- /dev/null +++ b/internal/encoding/encoding.go @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +// Package encoding provides encoding utilities. +package encoding + +import ( + "encoding/base64" + "errors" +) + +var ( + errI2OSPLength = errors.New("requested size is too big") + errHeaderLength = errors.New("insufficient header length for decoding") + errTotalLength = errors.New("insufficient total length for decoding") +) + +// EncodeVectorLen returns the input prepended with a byte encoding of its length. +func EncodeVectorLen(input []byte, length int) []byte { + if length != 1 && length != 2 { + panic(errI2OSPLength) + } + + return append(I2OSP(len(input), length), input...) +} + +// EncodeVector returns the input with a two-byte encoding of its length. +func EncodeVector(input []byte) []byte { + return EncodeVectorLen(input, 2) +} + +func decodeVectorLen(in []byte, size int) (data []byte, offset int, err error) { + if len(in) < size { + return nil, 0, errHeaderLength + } + + dataLen := OS2IP(in[0:size]) + offset = size + dataLen + + if len(in) < offset { + return nil, 0, errTotalLength + } + + return in[size:offset], offset, nil +} + +// DecodeVector returns the byte-slice of length indexed in the first two bytes. +func DecodeVector(in []byte) (data []byte, offset int, err error) { + return decodeVectorLen(in, 2) +} + +// New: ByteArrayToBase64String +func ByteArrayToBase64String(input []byte) string { + encoded := base64.StdEncoding.EncodeToString(input) + result := "1:" + encoded + return result +} + +// New: Base64StringToByteArray +func Base64StringToByteArray(input string) []byte { + //Remove the leading 1: or 0: + trimmedInput := input[2:] + decoded, _ := base64.StdEncoding.DecodeString(trimmedInput) + return decoded +} diff --git a/internal/encoding/i2osp.go b/internal/encoding/i2osp.go new file mode 100644 index 0000000..702540f --- /dev/null +++ b/internal/encoding/i2osp.go @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package encoding + +import ( + "encoding/binary" + "errors" +) + +var ( + errInputNegative = errors.New("negative input") + errInputLarge = errors.New("input is too high for length") + errLengthNegative = errors.New("length is negative or 0") + errLengthTooBig = errors.New("requested length is > 4") + + errInputEmpty = errors.New("nil or empty input") + errInputTooLarge = errors.New("input too large for integer") +) + +// I2OSP 32-bit Integer to Octet Stream Primitive on maximum 4 bytes. +func I2OSP(value, length int) []byte { + if length <= 0 { + panic(errLengthNegative) + } + + i2ospMax := 4 + if length > i2ospMax { + panic(errLengthTooBig) + } + + out := make([]byte, i2ospMax) + + switch v := value; { + case v < 0: + panic(errInputNegative) + case v >= 1<<(8*length): + panic(errInputLarge) + case length == 1: + binary.BigEndian.PutUint16(out, uint16(v)) + return out[1:2] + case length == 2: + binary.BigEndian.PutUint16(out, uint16(v)) + return out[:2] + case length == 3: + binary.BigEndian.PutUint32(out, uint32(v)) + return out[1:] + default: // length == 4 + binary.BigEndian.PutUint32(out, uint32(v)) + return out + } +} + +// OS2IP Octet Stream to Integer Primitive on maximum 4 bytes / 32 bits. +func OS2IP(input []byte) int { + switch length := len(input); { + case length == 0: + panic(errInputEmpty) + case length == 1: + b := []byte{0, input[0]} + return int(binary.BigEndian.Uint16(b)) + case length == 2: + return int(binary.BigEndian.Uint16(input)) + case length == 3: + b := append([]byte{0}, input...) + return int(binary.BigEndian.Uint32(b)) + case length == 4: + return int(binary.BigEndian.Uint32(input)) + default: + panic(errInputTooLarge) + } +} diff --git a/internal/encoding/misc.go b/internal/encoding/misc.go new file mode 100644 index 0000000..a84e426 --- /dev/null +++ b/internal/encoding/misc.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package encoding + +// Concat returns the concatenation of the two input byte strings. +func Concat(a, b []byte) []byte { + e := make([]byte, 0, len(a)+len(b)) + e = append(e, a...) + e = append(e, b...) + + return e +} + +// Concat3 returns the concatenation of the three input byte strings. +func Concat3(a, b, c []byte) []byte { + e := make([]byte, 0, len(a)+len(b)+len(c)) + e = append(e, a...) + e = append(e, b...) + e = append(e, c...) + + return e +} + +// SuffixString returns the concatenation of the input byte string and the string argument. +func SuffixString(a []byte, b string) []byte { + e := make([]byte, 0, len(a)+len(b)) + e = append(e, a...) + e = append(e, b...) + + return e +} + +// Concatenate takes the variadic array of input and returns a concatenation of it. +func Concatenate(input ...[]byte) []byte { + length := 0 + for _, b := range input { + length += len(b) + } + + buf := make([]byte, 0, length) + + for _, in := range input { + buf = append(buf, in...) + } + + return buf +} diff --git a/internal/hash.go b/internal/hash.go new file mode 100644 index 0000000..a7275d2 --- /dev/null +++ b/internal/hash.go @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package internal + +import ( + "crypto" + "crypto/hmac" + + "github.com/bytemare/hash" + "github.com/bytemare/ksf" +) + +// NewKDF returns a newly instantiated KDF. +func NewKDF(id crypto.Hash) *KDF { + return &KDF{h: hash.FromCrypto(id).Get()} +} + +// KDF wraps a hash function and exposes KDF methods. +type KDF struct { + h *hash.Hash +} + +// Extract exposes an Extract only KDF method. +func (k *KDF) Extract(salt, ikm []byte) []byte { + return k.h.HKDFExtract(ikm, salt) +} + +// Expand exposes an Expand only KDF method. +func (k *KDF) Expand(key, info []byte, length int) []byte { + return k.h.HKDFExpand(key, info, length) +} + +// Size returns the output size of the Extract method. +func (k *KDF) Size() int { + return k.h.OutputSize() +} + +// NewMac returns a newly instantiated Mac. +func NewMac(id crypto.Hash) *Mac { + return &Mac{h: hash.FromCrypto(id).Get()} +} + +// Mac wraps a hash function and exposes Message Authentication Code methods. +type Mac struct { + h *hash.Hash +} + +// Equal returns a constant-time comparison of the input. +func (m *Mac) Equal(a, b []byte) bool { + return hmac.Equal(a, b) +} + +// MAC computes a MAC over the message using key. +func (m *Mac) MAC(key, message []byte) []byte { + return m.h.Hmac(message, key) +} + +// Size returns the MAC's output length. +func (m *Mac) Size() int { + return m.h.OutputSize() +} + +// NewHash returns a newly instantiated Hash. +func NewHash(id crypto.Hash) *Hash { + return &Hash{h: hash.FromCrypto(id).Get()} +} + +// Hash wraps a hash function and exposes only necessary hashing methods. +type Hash struct { + h *hash.Hash +} + +// Size returns the output size of the hashing function. +func (h *Hash) Size() int { + return h.h.OutputSize() +} + +// Sum returns the current hash of the running state. +func (h *Hash) Sum() []byte { + return h.h.Sum(nil) +} + +// Write adds input to the running state. +func (h *Hash) Write(p []byte) { + _, _ = h.h.Write(p) +} + +// NewKSF returns a newly instantiated KSF. +func NewKSF(id ksf.Identifier) *KSF { + if id == 0 { + return &KSF{&IdentityKSF{}} + } + + return &KSF{id.Get()} +} + +// KSF wraps a key stretching function and exposes its functions. +type KSF struct { + ksfInterface +} + +type ksfInterface interface { + // Harden uses default parameters for the key derivation function over the input password and salt. + Harden(password, salt []byte, length int) []byte +} + +// IdentityKSF represents a KSF with no operations. +type IdentityKSF struct{} + +// Harden returns the password as is. +func (i IdentityKSF) Harden(password, _ []byte, _ int) []byte { + return password +} diff --git a/internal/keyrecovery/envelope.go b/internal/keyrecovery/envelope.go new file mode 100644 index 0000000..e1574d4 --- /dev/null +++ b/internal/keyrecovery/envelope.go @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +// Package keyrecovery provides utility functions and structures allowing credential management. +package keyrecovery + +import ( + "errors" + + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/internal/encoding" + "github.com/Jan-nku/opaque/internal/tag" +) + +var errEnvelopeInvalidMac = errors.New("invalid envelope authentication tag") + +// Credentials structure is currently used for testing purposes. +type Credentials struct { + ClientIdentity, ServerIdentity []byte + EnvelopeNonce []byte // testing: integrated to support testing +} + +// Envelope represents the OPAQUE envelope. +type Envelope struct { + Nonce []byte + AuthTag []byte +} + +// Serialize returns the byte serialization of the envelope. +func (e *Envelope) Serialize() []byte { + return encoding.Concat(e.Nonce, e.AuthTag) +} + +func exportKey(conf *internal.Configuration, randomizedPwd, nonce []byte) []byte { + return conf.KDF.Expand(randomizedPwd, encoding.SuffixString(nonce, tag.ExportKey), conf.KDF.Size()) +} + +func authTag(conf *internal.Configuration, randomizedPwd, nonce, ctc []byte) []byte { + authKey := conf.KDF.Expand(randomizedPwd, encoding.SuffixString(nonce, tag.AuthKey), conf.KDF.Size()) + return conf.MAC.MAC(authKey, encoding.Concat(nonce, ctc)) +} + +// cleartextCredentials assumes that clientPublicKey, serverPublicKey are non-nil valid group elements. +func cleartextCredentials(clientPublicKey, serverPublicKey, clientIdentity, serverIdentity []byte) []byte { + if clientIdentity == nil { + clientIdentity = clientPublicKey + } + + if serverIdentity == nil { + serverIdentity = serverPublicKey + } + + return encoding.Concat3( + serverPublicKey, + encoding.EncodeVector(serverIdentity), + encoding.EncodeVector(clientIdentity), + ) +} + +// Store returns the client's Envelope, the masking key for the registration, and the additional export key. +func Store( + conf *internal.Configuration, + randomizedPwd []byte, serverPublicKey *group.Element, + creds *Credentials, +) (env *Envelope, pku *group.Element, export []byte) { + // testing: integrated to support testing with set nonce + + //nonce := creds.EnvelopeNonce = nil + nonce := creds.EnvelopeNonce + if nonce == nil { + nonce = internal.RandomBytes(conf.NonceLen) + } + + //getPubkey 获取客户端公钥, randomizedPwd + nonce --> sk + pku = getPubkey(conf, randomizedPwd, nonce) + //ctc 凭证 + ctc := cleartextCredentials( + pku.Encode(), + serverPublicKey.Encode(), + creds.ClientIdentity, + creds.ServerIdentity, + ) + //auth = MAC_authkey(nonce, ctc), authkey = hash(randomizedPwd, nonce) + auth := authTag(conf, randomizedPwd, nonce, ctc) + + //randomizedPwd, nonce --> export + export = exportKey(conf, randomizedPwd, nonce) + + //nonce 存储在 env 中 + env = &Envelope{ + Nonce: nonce, + AuthTag: auth, + } + + return env, pku, export +} + +// Recover returns the client's private and public key, as well as the secret export key. +func Recover( + conf *internal.Configuration, + randomizedPwd, serverPublicKey, clientIdentity, serverIdentity []byte, + envelope *Envelope, +) (clientSecretKey *group.Scalar, clientPublicKey *group.Element, export []byte, err error) { + clientSecretKey, clientPublicKey = recoverKeys(conf, randomizedPwd, envelope.Nonce) + ctc := cleartextCredentials( + clientPublicKey.Encode(), + serverPublicKey, + clientIdentity, + serverIdentity, + ) + + // verify mac + expectedTag := authTag(conf, randomizedPwd, envelope.Nonce, ctc) + if !conf.MAC.Equal(expectedTag, envelope.AuthTag) { + return nil, nil, nil, errEnvelopeInvalidMac + } + + export = exportKey(conf, randomizedPwd, envelope.Nonce) + + return clientSecretKey, clientPublicKey, export, nil +} diff --git a/internal/keyrecovery/keyrec.go b/internal/keyrecovery/keyrec.go new file mode 100644 index 0000000..c40f69a --- /dev/null +++ b/internal/keyrecovery/keyrec.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package keyrecovery + +import ( + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/internal/encoding" + "github.com/Jan-nku/opaque/internal/oprf" + "github.com/Jan-nku/opaque/internal/tag" +) + +// TODO: randomizedPwd, nonce --> seed -->sk --> pk +func deriveAuthKeyPair(conf *internal.Configuration, randomizedPwd, nonce []byte) (*group.Scalar, *group.Element) { + seed := conf.KDF.Expand(randomizedPwd, encoding.SuffixString(nonce, tag.ExpandPrivateKey), internal.SeedLength) + sk := oprf.IDFromGroup(conf.Group).DeriveKey(seed, []byte(tag.DerivePrivateKey)) + + return sk, conf.Group.Base().Multiply(sk) +} + +func getPubkey(conf *internal.Configuration, randomizedPwd, nonce []byte) *group.Element { + //getPubkey 调用 deriveAuthKeyPair 函数, 私钥丢弃 + _, pk := deriveAuthKeyPair(conf, randomizedPwd, nonce) + return pk +} + +func recoverKeys( + conf *internal.Configuration, + randomizedPwd, nonce []byte, +) (clientSecretKey *group.Scalar, clientPublicKey *group.Element) { + return deriveAuthKeyPair(conf, randomizedPwd, nonce) +} diff --git a/internal/masking/masking.go b/internal/masking/masking.go new file mode 100644 index 0000000..b11b80f --- /dev/null +++ b/internal/masking/masking.go @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +// Package masking provides the credential masking mechanism. +package masking + +import ( + "errors" + + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/internal/encoding" + "github.com/Jan-nku/opaque/internal/keyrecovery" + "github.com/Jan-nku/opaque/internal/tag" +) + +// errUnmaskInvalidPKS happens when the client reads an invalid public key while unmasking. +var errUnmaskInvalidPKS = errors.New("invalid server public key in masked response") + +// Keys contains all the output keys from the masking mechanism. +type Keys struct { + ClientSecretKey *group.Scalar + ClientPublicKey, ServerPublicKey *group.Element + ExportKey, ServerPublicKeyBytes []byte +} + +// TODO: maskingNonce, maskedResponse := masking.Mask(s.conf, maskingNonce, record.MaskingKey, serverPublicKey, record.Envelope) +// Mask encrypts the serverPublicKey and the envelope under nonceIn and the maskingKey. +func Mask( + conf *internal.Configuration, + nonceIn, maskingKey, serverPublicKey, envelope []byte, +) (nonce, maskedResponse []byte) { + // testing: integrated to support testing, to force values. + nonce = nonceIn + if len(nonce) == 0 { + nonce = internal.RandomBytes(conf.NonceLen) + } + + clear := encoding.Concat(serverPublicKey, envelope) + //maskedResponse = Expand(maskingKey + maskingNonce) xor Concat(serverPublicKey + envelope(nonce + authtag)) + maskedResponse = xorResponse(conf, maskingKey, nonce, clear) + + return nonce, maskedResponse +} + +// Unmask decrypts the maskedResponse and returns the server's public key and the client key on success. +// This function assumes that maskedResponse has been checked to be of length pointLength + envelope size. +func Unmask( + conf *internal.Configuration, + randomizedPwd, nonce, maskedResponse []byte, +) (serverPublicKey *group.Element, serverPublicKeyBytes []byte, envelope *keyrecovery.Envelope, err error) { + maskingKey := conf.KDF.Expand(randomizedPwd, []byte(tag.MaskingKey), conf.Hash.Size()) + clear := xorResponse(conf, maskingKey, nonce, maskedResponse) + serverPublicKeyBytes = clear[:conf.Group.ElementLength()] + env := clear[conf.Group.ElementLength():] + envelope = &keyrecovery.Envelope{ + Nonce: env[:conf.NonceLen], + AuthTag: env[conf.NonceLen:], + } + + serverPublicKey = conf.Group.NewElement() + if err = serverPublicKey.Decode(serverPublicKeyBytes); err != nil { + return nil, nil, nil, errUnmaskInvalidPKS + } + + return serverPublicKey, serverPublicKeyBytes, envelope, nil +} + +// TODO: xorResponse(conf, maskingKey, nonce, encoding.Concat(serverPublicKey, envelope)) +// // xorResponse is used to encrypt and decrypt the response in KE2. +// // It returns a new byte slice containing the byte-by-byte xor-ing of the in argument and a constructed pad, +// // which must be of the same length. +func xorResponse(c *internal.Configuration, key, nonce, in []byte) []byte { + pad := c.KDF.Expand( + key, + encoding.SuffixString(nonce, tag.CredentialResponsePad), + c.Group.ElementLength()+c.EnvelopeSize, + ) + + dst := make([]byte, len(pad)) + + // if the size is fixed, we could unroll the loop + for i, r := range pad { + dst[i] = r ^ in[i] + } + + return dst +} diff --git a/internal/oprf/client.go b/internal/oprf/client.go new file mode 100644 index 0000000..4237dbb --- /dev/null +++ b/internal/oprf/client.go @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package oprf + +import ( + "errors" + + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal/encoding" + "github.com/Jan-nku/opaque/internal/tag" +) + +var errInvalidInput = errors.New("invalid input - OPRF input deterministically maps to the group identity element") + +// Client implements the OPRF client and holds its state. +type Client struct { + blind *group.Scalar + Identifier + input []byte +} + +// TODO: Blind存储input(password)和c.blind, 输出盲化值 +// Blind masks the input. +func (c *Client) Blind(input []byte, blind *group.Scalar) *group.Element { + if blind != nil { + c.blind = blind.Copy() + } else { + c.blind = c.Group().NewScalar().Random() + } + + p := c.Group().HashToGroup(input, c.dst(tag.OPRFPointPrefix)) + if p.IsIdentity() { + panic(errInvalidInput) + } + + c.input = input + + return p.Multiply(c.blind) +} + +func (c *Client) hashTranscript(input, unblinded []byte) []byte { + encInput := encoding.EncodeVector(input) + encElement := encoding.EncodeVector(unblinded) + encDST := []byte(tag.OPRFFinalize) + + return c.Identifier.hash(encInput, encElement, encDST) +} + +// Finalize terminates the OPRF by unblinding the evaluation and hashing the transcript. +func (c *Client) Finalize(evaluation *group.Element) []byte { + invert := c.blind.Copy().Invert() + // u = h(pw) ^ ku + u := evaluation.Copy().Multiply(invert).Encode() + + return c.hashTranscript(c.input, u) +} diff --git a/internal/oprf/oprf.go b/internal/oprf/oprf.go new file mode 100644 index 0000000..fbdc049 --- /dev/null +++ b/internal/oprf/oprf.go @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +// Package oprf implements the Elliptic Curve Oblivious Pseudorandom Function (EC-OPRF) from +// https://tools.ietf.org/html/draft-irtf-cfrg-voprf. +package oprf + +import ( + "crypto" + + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal/encoding" + "github.com/Jan-nku/opaque/internal/tag" +) + +// Identifier of the OPRF compatible cipher suite to be used. +type Identifier string + +const ( + // Ristretto255Sha512 is the OPRF cipher suite of the Ristretto255 group and SHA-512. + Ristretto255Sha512 Identifier = "ristretto255-SHA512" + + // Decaf448Sha512 is the OPRF cipher suite of the Decaf448 group and SHA-512. + // decaf448Sha512 Identifier = "decaf448-SHAKE256". + + // P256Sha256 is the OPRF cipher suite of the NIST P-256 group and SHA-256. + P256Sha256 Identifier = "P256-SHA256" + + // P384Sha384 is the OPRF cipher suite of the NIST P-384 group and SHA-384. + P384Sha384 Identifier = "P384-SHA384" + + // P521Sha512 is the OPRF cipher suite of the NIST P-512 group and SHA-512. + P521Sha512 Identifier = "P521-SHA512" + + nbIDs = 4 + maxDeriveKeyPairTries = 255 +) + +var ( + suites = make(map[group.Group]Identifier, nbIDs) + groups = make(map[Identifier]group.Group, nbIDs) + hashes = make(map[Identifier]crypto.Hash, nbIDs) +) + +func init() { + Ristretto255Sha512.register(group.Ristretto255Sha512, crypto.SHA512) + P256Sha256.register(group.P256Sha256, crypto.SHA256) + P384Sha384.register(group.P384Sha384, crypto.SHA384) + P521Sha512.register(group.P521Sha512, crypto.SHA512) +} + +func (i Identifier) register(g group.Group, h crypto.Hash) { + suites[g] = i + groups[i] = g + hashes[i] = h +} + +func (i Identifier) dst(prefix string) []byte { + return encoding.Concat([]byte(prefix), i.contextString()) +} + +func (i Identifier) contextString() []byte { + return encoding.Concatenate([]byte(tag.OPRFVersionPrefix), []byte(i)) +} + +func (i Identifier) hash(input ...[]byte) []byte { + h := hashes[i].New() + h.Reset() + + for _, i := range input { + _, _ = h.Write(i) + } + + return h.Sum(nil) +} + +// Available returns whether the Identifier has been registered of not. +func (i Identifier) Available() bool { + // Check for invalid identifiers + switch i { + case Ristretto255Sha512, P256Sha256, P384Sha384, P521Sha512: + break + default: + return false + } + + return true +} + +// IDFromGroup returns the OPRF identifier corresponding to the input group. +func IDFromGroup(g group.Group) Identifier { + return suites[g] +} + +// Group returns the Group identifier for the cipher suite. +func (i Identifier) Group() group.Group { + return groups[i] +} + +// DeriveKey returns a scalar mapped from the input. +func (i Identifier) DeriveKey(seed, info []byte) *group.Scalar { + dst := encoding.Concat([]byte(tag.DeriveKeyPairInternal), i.contextString()) + deriveInput := encoding.Concat(seed, encoding.EncodeVector(info)) + + var counter uint8 + var s *group.Scalar + + for s == nil || s.IsZero() { + if counter > maxDeriveKeyPairTries { + panic("DeriveKeyPairError") + } + + s = i.Group().HashToScalar(encoding.Concat(deriveInput, []byte{counter}), dst) + counter++ + } + + return s +} + +// TODO: oprf.Client() +// Client returns an OPRF client. +func (i Identifier) Client() *Client { + return &Client{ + Identifier: i, + input: nil, + blind: nil, + } +} + +// New: add Server +func (i Identifier) Server() *Server { + return &Server{ + Identifier: i, + blind: nil, + } +} diff --git a/internal/oprf/server.go b/internal/oprf/server.go new file mode 100644 index 0000000..1fcb7ce --- /dev/null +++ b/internal/oprf/server.go @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package oprf + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "github.com/Jan-nku/opaque/internal/encoding" + group "github.com/bytemare/crypto" + "math/big" + "net/http" +) + +// New: add oprf Server +type Server struct { + blind *group.Scalar + Identifier +} + +// Evaluate evaluates the blinded input with the given key. +func (i Identifier) Evaluate(privateKey *group.Scalar, blindedElement *group.Element) *group.Element { + return blindedElement.Copy().Multiply(privateKey) +} + +// New: add server blind func +func (s *Server) Blind(blindedElement *group.Element) *group.Element { + if s.blind == nil { + s.blind = s.Group().NewScalar().Random() + } + return blindedElement.Copy().Multiply(s.blind) +} + +// New: add server unblind func +func (s *Server) UnBlind(blindedElement *group.Element) *group.Element { + invert := s.blind.Copy().Invert() + u := blindedElement.Copy().Multiply(invert) + return u +} + +func (s *Server) ServiceReg(a1 *group.Element, a2 *group.Element, client *http.Client) *group.Element { + str_a1 := encoding.ByteArrayToBase64String(a1.Encode()) + str_a2 := encoding.ByteArrayToBase64String(a2.Encode()) + + url := "https://123.249.125.222/reg?a1=" + str_a1 + "&a2=" + str_a2 + + // 发送HTTPS GET请求 + response, err := client.Get(url) + if err != nil { + fmt.Println("无法发送请求:", err) + } + defer response.Body.Close() + + // 检查响应的状态码 + if response.StatusCode != http.StatusOK { + fmt.Println("请求失败,状态码:", response.StatusCode) + } + + // 解码JSON响应 + var data map[string]interface{} + decoder := json.NewDecoder(response.Body) + if err := decoder.Decode(&data); err != nil { + fmt.Println("解码JSON失败:", err) + } + + blindedMessage := s.Group().NewElement() + if err := blindedMessage.Decode(encoding.Base64StringToByteArray(data["beta"].(string))); err != nil { + return nil + } + + //ZKP Verify process, h = g ^ kc1 + h := s.Group().NewElement() + if err := h.Decode(encoding.Base64StringToByteArray(data["h"].(string))); err != nil { + return nil + } + x1 := s.Group().NewElement() + if err := x1.Decode(encoding.Base64StringToByteArray(data["x1"].(string))); err != nil { + return nil + } + x2 := s.Group().NewElement() + if err := x2.Decode(encoding.Base64StringToByteArray(data["x2"].(string))); err != nil { + return nil + } + k1 := s.Group().NewScalar() + if err := k1.Decode(encoding.Base64StringToByteArray(data["k1"].(string))); err != nil { + return nil + } + k2 := s.Group().NewScalar() + if err := k2.Decode(encoding.Base64StringToByteArray(data["k2"].(string))); err != nil { + return nil + } + + //compute hash result c + str := "1:AwpljT4HsBVSyWx1Pem9S9cgMuJ9FSuAboqjPOWhGvS3" + data["h"].(string) + str_a1 + str_a2 + data["beta"].(string) + data["x1"].(string) + data["x2"].(string) + sha256Hash := sha256.Sum256([]byte(str)) + hashInt := new(big.Int) + hashInt.SetBytes(sha256Hash[:]) + c := s.Group().NewScalar() + if err := c.SetInt(hashInt); err != nil { + return nil + } + + left := a1.Copy().Multiply(k1).Add(a2.Copy().Multiply(k2)) + right := x1.Add(x2).Add(blindedMessage.Copy().Multiply(c)) + /* if left.Equal(right) == 1 { + fmt.Println("Zero knowledge proof is correct.") + } else { + fmt.Println("Zero knowledge proof is not correct.") + }*/ + if left.Equal(right) != 1 { + return nil + } + + return blindedMessage +} + +func (s *Server) ServiceLogin(a1, a2, tau1, tau2 *group.Element, client *http.Client) *group.Element { + str_a1 := encoding.ByteArrayToBase64String(a1.Encode()) + str_a2 := encoding.ByteArrayToBase64String(a2.Encode()) + str_tau1 := encoding.ByteArrayToBase64String(tau1.Encode()) + str_tau2 := encoding.ByteArrayToBase64String(tau2.Encode()) + + url := "https://123.249.125.222/login?a1=" + str_a1 + "&a2=" + str_a2 + "&t1=" + str_tau1 + "&t2=" + str_tau2 + + // 发送HTTPS GET请求 + response, err := client.Get(url) + if err != nil { + fmt.Println("无法发送请求:", err) + } + defer response.Body.Close() + + // 检查响应的状态码 + if response.StatusCode != http.StatusOK { + fmt.Println("请求失败,状态码:", response.StatusCode) + } + + // 解码JSON响应 + var data map[string]interface{} + decoder := json.NewDecoder(response.Body) + if err := decoder.Decode(&data); err != nil { + fmt.Println("解码JSON失败:", err) + } + + blindedMessage := s.Group().NewElement() + if err := blindedMessage.Decode(encoding.Base64StringToByteArray(data["beta"].(string))); err != nil { + return nil + } + + //ZKP Verify process, h = g ^ kc1 + h := s.Group().NewElement() + if err := h.Decode(encoding.Base64StringToByteArray(data["h"].(string))); err != nil { + return nil + } + x1 := s.Group().NewElement() + if err := x1.Decode(encoding.Base64StringToByteArray(data["x1"].(string))); err != nil { + return nil + } + x2 := s.Group().NewElement() + if err := x2.Decode(encoding.Base64StringToByteArray(data["x2"].(string))); err != nil { + return nil + } + k1 := s.Group().NewScalar() + if err := k1.Decode(encoding.Base64StringToByteArray(data["k1"].(string))); err != nil { + return nil + } + k2 := s.Group().NewScalar() + if err := k2.Decode(encoding.Base64StringToByteArray(data["k2"].(string))); err != nil { + return nil + } + + //compute hash result c + str := "1:AwpljT4HsBVSyWx1Pem9S9cgMuJ9FSuAboqjPOWhGvS3" + data["h"].(string) + str_a1 + str_a2 + data["beta"].(string) + data["x1"].(string) + data["x2"].(string) + sha256Hash := sha256.Sum256([]byte(str)) + hashInt := new(big.Int) + hashInt.SetBytes(sha256Hash[:]) + c := s.Group().NewScalar() + if err := c.SetInt(hashInt); err != nil { + return nil + } + + left := a1.Copy().Multiply(k1).Add(a2.Copy().Multiply(k2)) + right := x1.Add(x2).Add(blindedMessage.Copy().Multiply(c)) + /* if left.Equal(right) == 1 { + fmt.Println("Zero knowledge proof is correct.") + } else { + fmt.Println("Zero knowledge proof is not correct.") + }*/ + if left.Equal(right) != 1 { + return nil + } + + return blindedMessage +} diff --git a/internal/tag/strings.go b/internal/tag/strings.go new file mode 100644 index 0000000..3c23998 --- /dev/null +++ b/internal/tag/strings.go @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +// Package tag provides the static tag strings to OPAQUE. +package tag + +// These strings are the static tags and labels used throughout the protocol. +const ( + // OPRF tags. + + // OPRFVersionPrefix is a string explicitly stating the version name. + OPRFVersionPrefix = "OPRFV1-\x00-" + + // DeriveKeyPairInternal is the internal DeriveKeyPair tag as defined in VOPRF. + DeriveKeyPairInternal = "DeriveKeyPair" + + // OPRFPointPrefix is the DST prefix to use for HashToGroup operations. + OPRFPointPrefix = "HashToGroup-" + + // OPRFFinalize is the DST suffix used in the client transcript. + OPRFFinalize = "Finalize" + + // Envelope tags. + + // AuthKey is the envelope's MAC key's KDF dst. + AuthKey = "AuthKey" + + // ExportKey is the export key's KDF dst. + ExportKey = "ExportKey" + + // MaskingKey is the masking key's creation KDF dst. + MaskingKey = "MaskingKey" + + // DerivePrivateKey is the client's private key hash-to-scalar dst. + DerivePrivateKey = "OPAQUE-DeriveAuthKeyPair" + + // ExpandPrivateKey is the client's private key seed KDF dst. + ExpandPrivateKey = "PrivateKey" + + // 3DH tags. + + // VersionTag indicates the protocol RFC identifier for the AKE transcript prefix. + VersionTag = "RFCXXXX" + + // LabelPrefix is the 3DH secret KDF dst prefix. + LabelPrefix = "OPAQUE-" + + // Handshake is the 3DH HandshakeSecret dst. + Handshake = "HandshakeSecret" + + // SessionKey is the 3DH session secret dst. + SessionKey = "SessionKey" + + // MacServer is 3DH server's MAC key KDF dst. + MacServer = "ServerMAC" + + // MacClient is 3DH server's MAC key KDF dst. + MacClient = "ClientMAC" + + // Client tags. + + // CredentialResponsePad is the masking keys KDF dst to expand to the input. + CredentialResponsePad = "CredentialResponsePad" + + // Server tags. + + // ExpandOPRF is the server's OPRF key seed KDF dst. + ExpandOPRF = "OprfKey" + + // DeriveKeyPair is the server's OPRF hash-to-scalar dst. + DeriveKeyPair = "OPAQUE-DeriveKeyPair" + + // DeriveKeyPair is the server's OPRF hash-to-scalar dst. + GenSeed = "OPAQUE-GenSeed" +) diff --git a/message/credentials.go b/message/credentials.go new file mode 100644 index 0000000..6343e2a --- /dev/null +++ b/message/credentials.go @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package message + +import ( + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal/encoding" + "github.com/Jan-nku/opaque/internal/oprf" +) + +// TODO: CredentialRequest +// CredentialRequest represents credential request message. +type CredentialRequest struct { + BlindedMessage *group.Element `json:"blindedMessage"` +} + +// NewCredentialRequest returns a populated CredentialRequest. +func NewCredentialRequest(ciphersuite oprf.Identifier, message *group.Element) *CredentialRequest { + return &CredentialRequest{ + BlindedMessage: message, + } +} + +// Serialize returns the byte encoding of CredentialRequest. +func (c *CredentialRequest) Serialize() []byte { + return c.BlindedMessage.Encode() +} + +// TODO: CredentialResponse +// CredentialResponse represents credential response message. +type CredentialResponse struct { + EvaluatedMessage *group.Element `json:"evaluatedMessage"` + MaskingNonce []byte `json:"maskingNonce"` + MaskedResponse []byte `json:"maskedResponse"` +} + +// NewCredentialResponse returns a populated CredentialResponse. +func NewCredentialResponse( + message *group.Element, + nonce, response []byte, +) *CredentialResponse { + return &CredentialResponse{ + EvaluatedMessage: message, + MaskingNonce: nonce, + MaskedResponse: response, + } +} + +// Serialize returns the byte encoding of CredentialResponse. +func (c *CredentialResponse) Serialize() []byte { + return encoding.Concat3(c.EvaluatedMessage.Encode(), c.MaskingNonce, c.MaskedResponse) +} diff --git a/message/login.go b/message/login.go new file mode 100644 index 0000000..b0ccc2f --- /dev/null +++ b/message/login.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +// Package message provides message structures for the OPAQUE protocol. +package message + +import ( + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal/encoding" +) + +// TODO: KE1 +// KE1 is the first message of the login flow, created by the client and sent to the server. +type KE1 struct { + *CredentialRequest + EpkU *group.Element `json:"clientEphemeralPublicKey"` + NonceU []byte `json:"clientNonce"` + UserName []byte `json:"clientUserName"` +} + +// Serialize returns the byte encoding of KE1. +func (m *KE1) Serialize() []byte { + return encoding.Concat(encoding.Concat3(m.CredentialRequest.Serialize(), m.NonceU, m.EpkU.Encode()), m.UserName) +} + +// TODO: KE2 +// KE2 is the second message of the login flow, created by the server and sent to the client. +type KE2 struct { + *CredentialResponse + EpkS *group.Element `json:"serverEphemeralPublicKey"` + NonceS []byte `json:"serverNonce"` + Mac []byte `json:"serverMac"` +} + +// Serialize returns the byte encoding of KE2. +func (m *KE2) Serialize() []byte { + return encoding.Concat( + m.CredentialResponse.Serialize(), + encoding.Concat3(m.NonceS, m.EpkS.Encode(), m.Mac), + ) +} + +// TODO: KE3 +// KE3 is the third and last message of the login flow, created by the client and sent to the server. +type KE3 struct { + Mac []byte `json:"clientMac"` +} + +// Serialize returns the byte encoding of KE3. +func (k KE3) Serialize() []byte { + return k.Mac +} diff --git a/message/registration.go b/message/registration.go new file mode 100644 index 0000000..e6e2169 --- /dev/null +++ b/message/registration.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package message + +import ( + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal/encoding" +) + +// TODO: RegistrationRequest is the first message of the registration flow +// RegistrationRequest is the first message of the registration flow, created by the client and sent to the server. +type RegistrationRequest struct { + BlindedMessage *group.Element `json:"blindedMessage"` + UserName []byte `json:"username"` +} + +// Serialize returns the byte encoding of RegistrationRequest. +func (r *RegistrationRequest) Serialize() []byte { + //return r.BlindedMessage.Encode() + return encoding.Concat(r.BlindedMessage.Encode(), r.UserName) +} + +// TODO: RegistrationResponse +// RegistrationResponse is the second message of the registration flow, created by the server and sent to the client. +type RegistrationResponse struct { + EvaluatedMessage *group.Element `json:"evaluatedMessage"` + Pks *group.Element `json:"serverPublicKey"` +} + +// Serialize returns the byte encoding of RegistrationResponse. +func (r *RegistrationResponse) Serialize() []byte { + return encoding.Concat(r.EvaluatedMessage.Encode(), r.Pks.Encode()) +} + +// TODO: RegistrationRecord +// RegistrationRecord represents the client record sent as the last registration message by the client to the server. +type RegistrationRecord struct { + PublicKey *group.Element `json:"clientPublicKey"` + MaskingKey []byte `json:"maskingKey"` + Envelope []byte `json:"envelope"` +} + +// Serialize returns the byte encoding of RegistrationRecord. +func (r *RegistrationRecord) Serialize() []byte { + return encoding.Concat3(r.PublicKey.Encode(), r.MaskingKey, r.Envelope) +} + +// Serialize returns the byte encoding of RegistrationRecord. +//func (r *RegistrationRecord) Deserialize() []byte { +// return encoding.Concat3(r.PublicKey.Encode(), r.MaskingKey, r.Envelope) +//} diff --git a/opaque.go b/opaque.go new file mode 100644 index 0000000..f4a9eae --- /dev/null +++ b/opaque.go @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +// 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). +package opaque + +import ( + "crypto" + "errors" + "fmt" + + group "github.com/bytemare/crypto" + "github.com/bytemare/hash" + "github.com/bytemare/ksf" + + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/internal/ake" + "github.com/Jan-nku/opaque/internal/encoding" + "github.com/Jan-nku/opaque/internal/oprf" + "github.com/Jan-nku/opaque/message" +) + +// TODO: group +// Group identifies the prime-order group with hash-to-curve capability to use in OPRF and AKE. +type Group byte + +const ( + // RistrettoSha512 identifies the Ristretto255 group and SHA-512. + RistrettoSha512 = Group(group.Ristretto255Sha512) + + // decaf448Shake256 identifies the Decaf448 group and Shake-256. + // decaf448Shake256 = 2. + + // P256Sha256 identifies the NIST P-256 group and SHA-256. + P256Sha256 = Group(group.P256Sha256) + + // P384Sha512 identifies the NIST P-384 group and SHA-384. + P384Sha512 = Group(group.P384Sha384) + + // P521Sha512 identifies the NIST P-512 group and SHA-512. + P521Sha512 = Group(group.P521Sha512) +) + +// 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 Group. +func (g Group) Available() bool { + return g == RistrettoSha512 || + g == P256Sha256 || + g == P384Sha512 || + g == P521Sha512 +} + +// OPRF returns the OPRF Identifier used in the Ciphersuite. +func (g Group) OPRF() oprf.Identifier { + return oprf.IDFromGroup(g.Group()) +} + +// Group returns the EC Group used in the Ciphersuite. +func (g Group) Group() group.Group { + return group.Group(g) +} + +const confLength = 6 + +var ( + errInvalidOPRFid = errors.New("invalid OPRF group id") + errInvalidKDFid = errors.New("invalid KDF id") + errInvalidMACid = errors.New("invalid MAC id") + errInvalidHASHid = errors.New("invalid Hash id") + errInvalidKSFid = errors.New("invalid KSF id") + errInvalidAKEid = errors.New("invalid AKE group id") +) + +// Configuration represents an OPAQUE configuration. Note that OprfGroup and AKEGroup are recommended to be the same, +// as well as KDF, MAC, Hash should be the same. +type Configuration struct { + Context []byte + KDF crypto.Hash `json:"kdf"` + MAC crypto.Hash `json:"mac"` + Hash crypto.Hash `json:"hash"` + OPRF Group `json:"oprf"` + KSF ksf.Identifier `json:"ksf"` + AKE Group `json:"group"` +} + +// TODO: conf := opaque.DefaultConfiguration() +// DefaultConfiguration returns a default configuration with strong parameters. +func DefaultConfiguration() *Configuration { + return &Configuration{ + //RistrettoSha512 --> P256Sha256(aka NIST P-256) + OPRF: P256Sha256, + KDF: crypto.SHA256, + MAC: crypto.SHA256, + Hash: crypto.SHA256, + KSF: ksf.Argon2id, + AKE: P256Sha256, + Context: nil, + } +} + +// TODO: client, err := conf.Client() +// Client returns a newly instantiated Client from the Configuration. +func (c *Configuration) Client() (*Client, error) { + return NewClient(c) +} + +// TODO: server, err := conf.Server() +// Server returns a newly instantiated Server from the Configuration. +func (c *Configuration) Server() (*Server, error) { + return NewServer(c) +} + +// GenerateOPRFSeed returns a OPRF seed valid in the given configuration. +func (c *Configuration) GenerateOPRFSeed() []byte { + return RandomBytes(c.Hash.Size()) +} + +// KeyGen returns a key pair in the AKE group. +func (c *Configuration) KeyGen() (secretKey, publicKey []byte) { + return ake.KeyGen(group.Group(c.AKE)) +} + +func (c *Configuration) Generate() (t1, t2, gs *group.Element) { + scalar1 := c.OPRF.Group().NewScalar().Random() + t1 = c.OPRF.Group().Base().Multiply(scalar1) + scalar2 := c.OPRF.Group().NewScalar().Random() + t2 = c.OPRF.Group().Base().Multiply(scalar2) + scalar3 := c.OPRF.Group().NewScalar().Random() + gs = c.OPRF.Group().Base().Multiply(scalar3) + + return t1, t2, gs +} + +// verify returns an error on the first non-compliant parameter, nil otherwise. +func (c *Configuration) verify() error { + if !c.OPRF.Available() || !c.OPRF.OPRF().Available() { + return errInvalidOPRFid + } + + if !c.AKE.Available() || !c.AKE.Group().Available() { + return errInvalidAKEid + } + + if !hash.Hashing(c.KDF).Available() { + return errInvalidKDFid + } + + if !hash.Hashing(c.MAC).Available() { + return errInvalidMACid + } + + if !hash.Hashing(c.Hash).Available() { + return errInvalidHASHid + } + + if c.KSF != 0 && !c.KSF.Available() { + return errInvalidKSFid + } + + return nil +} + +// TODO: opaque.Configuration converts to internal.Configuration +// toInternal builds the internal representation of the configuration parameters. +func (c *Configuration) toInternal() (*internal.Configuration, error) { + if err := c.verify(); err != nil { + return nil, err + } + + g := c.AKE.Group() + o := c.OPRF.OPRF() + mac := internal.NewMac(c.MAC) + ip := &internal.Configuration{ + OPRF: o, + KDF: internal.NewKDF(c.KDF), + MAC: mac, + Hash: internal.NewHash(c.Hash), + KSF: internal.NewKSF(c.KSF), + NonceLen: internal.NonceLength, + EnvelopeSize: internal.NonceLength + mac.Size(), + Group: g, + Context: c.Context, + } + + return ip, nil +} + +// Deserializer returns a pointer to a Deserializer structure allowing deserialization of messages in the given +// configuration. +func (c *Configuration) Deserializer() (*Deserializer, error) { + conf, err := c.toInternal() + if err != nil { + return nil, err + } + + return &Deserializer{conf: conf}, nil +} + +// Serialize returns the byte encoding of the Configuration structure. +func (c *Configuration) Serialize() []byte { + b := []byte{ + byte(c.OPRF), + byte(c.KDF), + byte(c.MAC), + byte(c.Hash), + byte(c.KSF), + byte(c.AKE), + } + + return encoding.Concat(b, encoding.EncodeVector(c.Context)) +} + +// GetFakeRecord creates a fake Client record to be used when no existing client record exists, +// to defend against client enumeration techniques. +func (c *Configuration) GetFakeRecord(credentialIdentifier []byte) (*ClientRecord, error) { + i, err := c.toInternal() + if err != nil { + return nil, err + } + + scalar := i.Group.NewScalar().Random() + publicKey := i.Group.Base().Multiply(scalar) + + regRecord := &message.RegistrationRecord{ + PublicKey: publicKey, + MaskingKey: RandomBytes(i.KDF.Size()), + Envelope: make([]byte, internal.NonceLength+i.MAC.Size()), + } + + return &ClientRecord{ + CredentialIdentifier: credentialIdentifier, + ClientIdentity: nil, + RegistrationRecord: regRecord, + TestMaskNonce: nil, + }, nil +} + +// DeserializeConfiguration decodes the input and returns a Parameter structure. +func DeserializeConfiguration(encoded []byte) (*Configuration, error) { + if len(encoded) < confLength+2 { // corresponds to the configuration length + 2-byte encoding of empty context + return nil, internal.ErrConfigurationInvalidLength + } + + ctx, _, err := encoding.DecodeVector(encoded[confLength:]) + if err != nil { + return nil, fmt.Errorf("decoding the configuration context: %w", err) + } + + c := &Configuration{ + OPRF: Group(encoded[0]), + KDF: crypto.Hash(encoded[1]), + MAC: crypto.Hash(encoded[2]), + Hash: crypto.Hash(encoded[3]), + KSF: ksf.Identifier(encoded[4]), + AKE: Group(encoded[5]), + Context: ctx, + } + + if err := c.verify(); err != nil { + return nil, err + } + + return c, nil +} + +// ClientRecord is a server-side structure enabling the storage of user relevant information. +type ClientRecord struct { + CredentialIdentifier []byte + ClientIdentity []byte + *message.RegistrationRecord + + // testing + TestMaskNonce []byte +} + +// RandomBytes returns random bytes of length len (wrapper for crypto/rand). +func RandomBytes(length int) []byte { + return internal.RandomBytes(length) +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..83babe7 --- /dev/null +++ b/server.go @@ -0,0 +1,418 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package opaque + +import ( + "errors" + "fmt" + "github.com/Jan-nku/opaque/internal/oprf" + "net/http" + + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/internal/ake" + "github.com/Jan-nku/opaque/internal/encoding" + "github.com/Jan-nku/opaque/internal/masking" + "github.com/Jan-nku/opaque/internal/tag" + "github.com/Jan-nku/opaque/message" +) + +var ( + // ErrNoServerKeyMaterial indicates that the server's key material has not been set. + ErrNoServerKeyMaterial = errors.New("key material not set: call SetKeyMaterial() to set values") + + // ErrAkeInvalidClientMac indicates that the MAC contained in the KE3 message is not valid in the given session. + ErrAkeInvalidClientMac = errors.New("failed to authenticate client: invalid client mac") + + // ErrInvalidState indicates that the given state is not valid due to a wrong length. + ErrInvalidState = errors.New("invalid state length") + + // ErrInvalidEnvelopeLength indicates the envelope contained in the record is of invalid length. + ErrInvalidEnvelopeLength = errors.New("record has invalid envelope length") + + // ErrInvalidPksLength indicates the input public key is not of right length. + ErrInvalidPksLength = errors.New("input server public key's length is invalid") + + // ErrInvalidOPRFSeedLength indicates that the OPRF seed is not of right length. + ErrInvalidOPRFSeedLength = errors.New("input OPRF seed length is invalid (must be of hash output length)") + + // ErrZeroSKS indicates that the server's private key is a zero scalar. + ErrZeroSKS = errors.New("server private key is zero") +) + +// Server represents an OPAQUE Server, exposing its functions and holding its state. +type Server struct { + Deserialize *Deserializer + conf *internal.Configuration + OPRF *oprf.Server + Ake *ake.Server + *keyMaterial +} + +type keyMaterial struct { + serverIdentity []byte + serverSecretKey *group.Scalar + serverPublicKey []byte + oprfSeed []byte + t1 *group.Element + t2 *group.Element + gs *group.Element +} + +// TODO: NewServer() +// NewServer returns a Server instantiation given the application Configuration. +func NewServer(c *Configuration) (*Server, error) { + if c == nil { + c = DefaultConfiguration() + } + + conf, err := c.toInternal() + if err != nil { + return nil, err + } + + return &Server{ + Deserialize: &Deserializer{conf: conf}, + conf: conf, + OPRF: conf.OPRF.Server(), + Ake: ake.NewServer(), + keyMaterial: nil, + }, nil +} + +// GetConf return the internal configuration. +func (s *Server) GetConf() *internal.Configuration { + return s.conf +} + +// TODO: oprfResponse +func (s *Server) oprfResponse(element *group.Element, oprfSeed, credentialIdentifier []byte) *group.Element { + //oprfSeed, credentialIdentifier --> seed + seed := s.conf.KDF.Expand( + oprfSeed, + encoding.SuffixString(credentialIdentifier, tag.ExpandOPRF), + internal.SeedLength, + ) + //seed(oprfSeed + credID --> expand), info(const str) --> ku + //两次调用,生成ku都是相同的 + ku := s.conf.OPRF.DeriveKey(seed, []byte(tag.DeriveKeyPair)) + //Evaluate call the func multiply + return s.conf.OPRF.Evaluate(ku, element) +} + +// TODO: Modify RegistrationResponse func to support hpake +// RegistrationResponse returns a RegistrationResponse message to the input RegistrationRequest message and given +// identifiers. +func (s *Server) HpakeRegistrationResponse( + req *message.RegistrationRequest, + serverPublicKey *group.Element, + credentialIdentifier, oprfSeed []byte, + client *http.Client, +) *message.RegistrationResponse { + //oprfseed, credentialIdentifier --> x. in fact, x = 1/(ku + h(uid)) + seed := s.conf.KDF.Expand( + oprfSeed, + encoding.SuffixString(credentialIdentifier, tag.ExpandOPRF), + internal.SeedLength, + ) + x := s.conf.OPRF.DeriveKey(seed, []byte(tag.DeriveKeyPair)) + + //New: m = h(pw) ^ blindU ^ blindS, a1 = m, a2 = a1 ^ x + a1 := s.OPRF.Blind(req.BlindedMessage) + a2 := a1.Copy().Multiply(x) + + //New: Cryptor Service + b := s.OPRF.ServiceReg(a1, a2, client) + z := s.OPRF.UnBlind(b) + + return &message.RegistrationResponse{ + EvaluatedMessage: z, + Pks: serverPublicKey, + } +} + +// RegistrationResponse returns a RegistrationResponse message to the input RegistrationRequest message and given +// identifiers. +func (s *Server) RegistrationResponse( + req *message.RegistrationRequest, + serverPublicKey *group.Element, + credentialIdentifier, oprfSeed []byte, +) *message.RegistrationResponse { + z := s.oprfResponse(req.BlindedMessage, oprfSeed, credentialIdentifier) + + return &message.RegistrationResponse{ + EvaluatedMessage: z, + Pks: serverPublicKey, + } +} + +func (s *Server) hpakeCredentialResponse( + req *message.CredentialRequest, + serverPublicKey []byte, + record *message.RegistrationRecord, + credentialIdentifier, oprfSeed, maskingNonce []byte, + client *http.Client, +) *message.CredentialResponse { + + seed := s.conf.KDF.Expand( + oprfSeed, + encoding.SuffixString(credentialIdentifier, tag.ExpandOPRF), + internal.SeedLength, + ) + x := s.conf.OPRF.DeriveKey(seed, []byte(tag.DeriveKeyPair)) + + //New: m = h(pw) ^ blindU ^ blindS, a1 = m, a2 = a1 ^ x + a1 := s.OPRF.Blind(req.BlindedMessage) + a2 := a1.Copy().Multiply(x) + tau1 := s.t1.Copy().Multiply(x) + o := s.conf.OPRF.DeriveKey(seed, []byte(tag.GenSeed)) + tau2 := (s.gs.Copy().Multiply(o).Add(s.t2)).Multiply(x) + + //New: Cryptor Service Map + b := s.OPRF.ServiceLogin(a1, a2, tau1, tau2, client) + z := s.OPRF.UnBlind(b) + + //z := s.oprfResponse(req.BlindedMessage, oprfSeed, credentialIdentifier) + + maskingNonce, maskedResponse := masking.Mask( + s.conf, + maskingNonce, // record.TestMaskNonce = nil + record.MaskingKey, + serverPublicKey, + record.Envelope, + ) + + return message.NewCredentialResponse(z, maskingNonce, maskedResponse) +} + +func (s *Server) credentialResponse( + req *message.CredentialRequest, + serverPublicKey []byte, + record *message.RegistrationRecord, + credentialIdentifier, oprfSeed, maskingNonce []byte, +) *message.CredentialResponse { + z := s.oprfResponse(req.BlindedMessage, oprfSeed, credentialIdentifier) + + maskingNonce, maskedResponse := masking.Mask( + s.conf, + maskingNonce, + record.MaskingKey, + serverPublicKey, + record.Envelope, + ) + + return message.NewCredentialResponse(z, maskingNonce, maskedResponse) +} + +// ServerLoginInitOptions enables setting optional values for the session, which default to secure random values if not +// set. +type ServerLoginInitOptions struct { + // EphemeralSecretKey: optional + EphemeralSecretKey *group.Scalar + // Nonce: optional + Nonce []byte + // NonceLength: optional + NonceLength uint +} + +func getServerLoginInitOptions(options []ServerLoginInitOptions) *ake.Options { + var op ake.Options + + if len(options) != 0 { + op.EphemeralSecretKey = options[0].EphemeralSecretKey + op.Nonce = options[0].Nonce + op.NonceLength = options[0].NonceLength + } + + return &op +} + +// SetKeyMaterial set the server's identity and mandatory key material to be used during LoginInit(). +// All these values must be the same as used during client registration and remain the same across protocol execution +// for a given registered client. +// +// - serverIdentity can be nil, in which case it will be set to serverPublicKey. +// - serverSecretKey is the server's secret AKE key. +// - serverPublicKey is the server's public AKE key to the serverSecretKey. +// - oprfSeed is the long-term OPRF input seed. +func (s *Server) HpakeSetKeyMaterial(serverIdentity, serverSecretKey, serverPublicKey, oprfSeed []byte, t1, t2, gs *group.Element) error { + sks := s.conf.Group.NewScalar() + if err := sks.Decode(serverSecretKey); err != nil { + return fmt.Errorf("invalid server AKE secret key: %w", err) + } + + if sks.IsZero() { + return ErrZeroSKS + } + + if len(oprfSeed) != s.conf.Hash.Size() { + return ErrInvalidOPRFSeedLength + } + + if len(serverPublicKey) != s.conf.Group.ElementLength() { + return ErrInvalidPksLength + } + + if err := s.conf.Group.NewElement().Decode(serverPublicKey); err != nil { + return fmt.Errorf("invalid server public key: %w", err) + } + + s.keyMaterial = &keyMaterial{ + serverIdentity: serverIdentity, + serverSecretKey: sks, + serverPublicKey: serverPublicKey, + oprfSeed: oprfSeed, + t1: t1, + t2: t2, + gs: gs, + } + + return nil +} + +func (s *Server) SetKeyMaterial(serverIdentity, serverSecretKey, serverPublicKey, oprfSeed []byte) error { + sks := s.conf.Group.NewScalar() + if err := sks.Decode(serverSecretKey); err != nil { + return fmt.Errorf("invalid server AKE secret key: %w", err) + } + + if sks.IsZero() { + return ErrZeroSKS + } + + if len(oprfSeed) != s.conf.Hash.Size() { + return ErrInvalidOPRFSeedLength + } + + if len(serverPublicKey) != s.conf.Group.ElementLength() { + return ErrInvalidPksLength + } + + if err := s.conf.Group.NewElement().Decode(serverPublicKey); err != nil { + return fmt.Errorf("invalid server public key: %w", err) + } + + s.keyMaterial = &keyMaterial{ + serverIdentity: serverIdentity, + serverSecretKey: sks, + serverPublicKey: serverPublicKey, + oprfSeed: oprfSeed, + } + + return nil +} + +// TODO: Modify LoginInit(ke1, record) to support hpake +// LoginInit responds to a KE1 message with a KE2 message a client record. +func (s *Server) HpakeLoginInit( + ke1 *message.KE1, + record *ClientRecord, + client *http.Client, + options ...ServerLoginInitOptions, +) (*message.KE2, error) { + if s.keyMaterial == nil { + return nil, ErrNoServerKeyMaterial + } + + if len(record.Envelope) != s.conf.EnvelopeSize { + return nil, ErrInvalidEnvelopeLength + } + + // We've checked that the server's public key and the client's envelope are of correct length, + // thus ensuring that the subsequent xor-ing input is the same length as the encryption pad. + + op := getServerLoginInitOptions(options) // op = nil + + //TODO: credentialResponse func return credential response message + //New: modify the Calculation method of z + response := s.hpakeCredentialResponse(ke1.CredentialRequest, s.keyMaterial.serverPublicKey, + record.RegistrationRecord, record.CredentialIdentifier, s.keyMaterial.oprfSeed, record.TestMaskNonce, client) + + identities := ake.Identities{ + ClientIdentity: record.ClientIdentity, + ServerIdentity: s.keyMaterial.serverIdentity, + } + //SetIdentities sets the client and server identities to their respective public key if not set. + identities.SetIdentities(record.PublicKey, s.keyMaterial.serverPublicKey) + //TODO: Response func produce message ke2 + ke2 := s.Ake.Response(s.conf, &identities, s.keyMaterial.serverSecretKey, record.PublicKey, ke1, response, *op) + + return ke2, nil +} + +// LoginInit responds to a KE1 message with a KE2 message a client record. +func (s *Server) LoginInit( + ke1 *message.KE1, + record *ClientRecord, + options ...ServerLoginInitOptions, +) (*message.KE2, error) { + if s.keyMaterial == nil { + return nil, ErrNoServerKeyMaterial + } + + if len(record.Envelope) != s.conf.EnvelopeSize { + return nil, ErrInvalidEnvelopeLength + } + + // We've checked that the server's public key and the client's envelope are of correct length, + // thus ensuring that the subsequent xor-ing input is the same length as the encryption pad. + + op := getServerLoginInitOptions(options) + + response := s.credentialResponse(ke1.CredentialRequest, s.keyMaterial.serverPublicKey, + record.RegistrationRecord, record.CredentialIdentifier, s.keyMaterial.oprfSeed, record.TestMaskNonce) + + identities := ake.Identities{ + ClientIdentity: record.ClientIdentity, + ServerIdentity: s.keyMaterial.serverIdentity, + } + identities.SetIdentities(record.PublicKey, s.keyMaterial.serverPublicKey) + + ke2 := s.Ake.Response(s.conf, &identities, s.keyMaterial.serverSecretKey, record.PublicKey, ke1, response, *op) + + return ke2, nil +} + +// LoginFinish returns an error if the KE3 received from the client holds an invalid mac, and nil if correct. +func (s *Server) LoginFinish(ke3 *message.KE3) error { + if !s.Ake.Finalize(s.conf, ke3) { + return ErrAkeInvalidClientMac + } + + return nil +} + +// SessionKey returns the session key if the previous call to LoginInit() was successful. +func (s *Server) SessionKey() []byte { + return s.Ake.SessionKey() +} + +// ExpectedMAC returns the expected client MAC if the previous call to LoginInit() was successful. +func (s *Server) ExpectedMAC() []byte { + return s.Ake.ExpectedMAC() +} + +// SetAKEState sets the internal state of the AKE server from the given bytes. +func (s *Server) SetAKEState(state []byte) error { + if len(state) != s.conf.MAC.Size()+s.conf.KDF.Size() { + return ErrInvalidState + } + + if err := s.Ake.SetState(state[:s.conf.MAC.Size()], state[s.conf.MAC.Size():]); err != nil { + return fmt.Errorf("setting AKE state: %w", err) + } + + return nil +} + +// SerializeState returns the internal state of the AKE server serialized to bytes. +func (s *Server) SerializeState() []byte { + return s.Ake.SerializeState() +} diff --git a/tests/client_test.go b/tests/client_test.go new file mode 100644 index 0000000..f9e3f66 --- /dev/null +++ b/tests/client_test.go @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package opaque_test + +import ( + "log" + "strings" + "testing" + + "github.com/Jan-nku/opaque" + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/internal/encoding" + "github.com/Jan-nku/opaque/internal/tag" +) + +/* + The following tests look for failing conditions. +*/ + +func TestClientRegistrationFinalize_InvalidPks(t *testing.T) { + /* + Invalid data sent to the client + */ + credID := internal.RandomBytes(32) + + testAll(t, func(t2 *testing.T, conf *configuration) { + client, err := conf.conf.Client() + if err != nil { + t.Fatal(err) + } + + server, err := conf.conf.Server() + if err != nil { + t.Fatal(err) + } + + _, pks := conf.conf.KeyGen() + oprfSeed := internal.RandomBytes(conf.conf.Hash.Size()) + r1 := client.RegistrationInit([]byte("yo")) + + pk := server.GetConf().Group.NewElement() + if err := pk.Decode(pks); err != nil { + panic(err) + } + r2 := server.RegistrationResponse(r1, pk, credID, oprfSeed) + + // message length + badr2 := internal.RandomBytes(15) + expected := "invalid message length" + if _, err := client.Deserialize.RegistrationResponse(badr2); err == nil || + !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error for empty server public key - got %v", err) + } + + // invalid data + badr2 = encoding.Concat(getBadElement(t, conf), pks) + expected = "invalid OPRF evaluation" + if _, err := client.Deserialize.RegistrationResponse(badr2); err == nil || + !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error for empty server public key - got %v", err) + } + + // nil pks + expected = "invalid server public key" + badr2 = encoding.Concat(r2.Serialize()[:client.GetConf().OPRF.Group().ElementLength()], getBadElement(t, conf)) + if _, err := client.Deserialize.RegistrationResponse(badr2); err == nil || + !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error for invalid server public key - got %v", err) + } + }) +} + +func TestClientFinish_BadEvaluation(t *testing.T) { + /* + Oprf finalize : evaluation deserialization // element decoding + */ + testAll(t, func(t2 *testing.T, conf *configuration) { + client, err := conf.conf.Client() + if err != nil { + t.Fatal(err) + } + + _ = client.LoginInit([]byte("yo")) + r2 := encoding.Concat( + getBadElement(t, conf), + internal.RandomBytes( + client.GetConf().NonceLen+client.GetConf().Group.ElementLength()+client.GetConf().EnvelopeSize, + ), + ) + badKe2 := encoding.Concat( + r2, + internal.RandomBytes( + client.GetConf().NonceLen+client.GetConf().Group.ElementLength()+client.GetConf().MAC.Size(), + ), + ) + + expected := "invalid OPRF evaluation" + if _, err := client.Deserialize.KE2(badKe2); err == nil || !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error for invalid evaluated element - got %v", err) + } + }) +} + +func TestClientFinish_BadMaskedResponse(t *testing.T) { + /* + The masked response is of invalid length. + */ + credID := internal.RandomBytes(32) + + testAll(t, func(t2 *testing.T, conf *configuration) { + client, err := conf.conf.Client() + if err != nil { + t.Fatal(err) + } + + server, err := conf.conf.Server() + if err != nil { + t.Fatal(err) + } + + sks, pks := conf.conf.KeyGen() + oprfSeed := internal.RandomBytes(conf.conf.Hash.Size()) + + if err := server.SetKeyMaterial(nil, sks, pks, oprfSeed); err != nil { + t.Fatal(err) + } + + rec := buildRecord(credID, oprfSeed, []byte("yo"), pks, client, server) + + ke1 := client.LoginInit([]byte("yo")) + ke2, _ := server.LoginInit(ke1, rec) + + goodLength := client.GetConf().Group.ElementLength() + client.GetConf().EnvelopeSize + expected := "invalid masked response length" + + // too short + ke2.MaskedResponse = internal.RandomBytes(goodLength - 1) + if _, _, err := client.LoginFinish(ke2); err == nil || !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error for short response - got %v", err) + } + + // too long + ke2.MaskedResponse = internal.RandomBytes(goodLength + 1) + if _, _, err := client.LoginFinish(ke2); err == nil || !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error for long response - got %v", err) + } + }) +} + +func TestClientFinish_InvalidEnvelopeTag(t *testing.T) { + /* + Invalid envelope tag + */ + credID := internal.RandomBytes(32) + + testAll(t, func(t2 *testing.T, conf *configuration) { + client, err := conf.conf.Client() + if err != nil { + t.Fatal(err) + } + + server, err := conf.conf.Server() + if err != nil { + t.Fatal(err) + } + + sks, pks := conf.conf.KeyGen() + oprfSeed := internal.RandomBytes(conf.conf.Hash.Size()) + + if err := server.SetKeyMaterial(nil, sks, pks, oprfSeed); err != nil { + t.Fatal(err) + } + + rec := buildRecord(credID, oprfSeed, []byte("yo"), pks, client, server) + + ke1 := client.LoginInit([]byte("yo")) + ke2, _ := server.LoginInit(ke1, rec) + + env, _, err := getEnvelope(client, ke2) + if err != nil { + t.Fatal(err) + } + + // tamper the envelope + env.AuthTag = internal.RandomBytes(client.GetConf().MAC.Size()) + clear := encoding.Concat(pks, env.Serialize()) + ke2.MaskedResponse = xorResponse(server.GetConf(), rec.MaskingKey, ke2.MaskingNonce, clear) + + expected := "key recovery: invalid envelope authentication tag" + if _, _, err := client.LoginFinish(ke2); err == nil || !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error = %q for invalid envelope mac - got %v", expected, err) + } + }) +} + +func cleartextCredentials(clientPublicKey, serverPublicKey, idc, ids []byte) []byte { + if ids == nil { + ids = serverPublicKey + } + + if idc == nil { + idc = clientPublicKey + } + + return encoding.Concat3(serverPublicKey, encoding.EncodeVector(ids), encoding.EncodeVector(idc)) +} + +func TestClientFinish_InvalidKE2KeyEncoding(t *testing.T) { + /* + Tamper KE2 values + */ + credID := internal.RandomBytes(32) + + testAll(t, func(t2 *testing.T, conf *configuration) { + client, err := conf.conf.Client() + if err != nil { + t.Fatal(err) + } + + server, err := conf.conf.Server() + if err != nil { + t.Fatal(err) + } + + sks, pks := conf.conf.KeyGen() + oprfSeed := internal.RandomBytes(conf.conf.Hash.Size()) + + if err := server.SetKeyMaterial(nil, sks, pks, oprfSeed); err != nil { + t.Fatal(err) + } + + rec := buildRecord(credID, oprfSeed, []byte("yo"), pks, client, server) + + ke1 := client.LoginInit([]byte("yo")) + ke2, _ := server.LoginInit(ke1, rec) + // epks := ke2.EpkS + + // tamper epks + offset := client.GetConf().Group.ElementLength() + client.GetConf().MAC.Size() + encoded := ke2.Serialize() + badKe2 := encoding.Concat3(encoded[:len(encoded)-offset], getBadElement(t, conf), ke2.Mac) + expected := "invalid ephemeral server public key" + if _, err := client.Deserialize.KE2(badKe2); err == nil || !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error for invalid epks encoding - got %q", err) + } + + // tamper PKS + // ke2.EpkS = server.Group.NewElement().Mult(server.Group.NewScalar().Random()) + env, randomizedPwd, err := getEnvelope(client, ke2) + if err != nil { + t.Fatal(err) + } + + badpks := getBadElement(t, conf) + + ctc := cleartextCredentials( + rec.RegistrationRecord.PublicKey.Encode(), + badpks, + nil, + nil, + ) + authKey := client.GetConf().KDF.Expand( + randomizedPwd, + encoding.SuffixString(env.Nonce, tag.AuthKey), + client.GetConf().KDF.Size(), + ) + authTag := client.GetConf().MAC.MAC(authKey, encoding.Concat(env.Nonce, ctc)) + env.AuthTag = authTag + + clear := encoding.Concat(badpks, env.Serialize()) + ke2.MaskedResponse = xorResponse(server.GetConf(), rec.MaskingKey, ke2.MaskingNonce, clear) + + expected = "unmasking: invalid server public key in masked response" + if _, _, err := client.LoginFinish(ke2); err == nil || !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error %q for invalid envelope mac - got %q", expected, err) + } + + // replace PKS + group := server.GetConf().Group + fakepks := group.Base().Multiply(group.NewScalar().Random()).Encode() + clear = encoding.Concat(fakepks, env.Serialize()) + ke2.MaskedResponse = xorResponse(server.GetConf(), rec.MaskingKey, ke2.MaskingNonce, clear) + + expected = "key recovery: invalid envelope authentication tag" + if _, _, err := client.LoginFinish(ke2); err == nil || !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error %q for invalid envelope mac - got %q", expected, err) + } + }) +} + +func TestClientFinish_InvalidKE2Mac(t *testing.T) { + /* + Invalid server ke2 mac + */ + credID := internal.RandomBytes(32) + + testAll(t, func(t2 *testing.T, conf *configuration) { + client, err := conf.conf.Client() + if err != nil { + t.Fatal(err) + } + + server, err := conf.conf.Server() + if err != nil { + t.Fatal(err) + } + + sks, pks := conf.conf.KeyGen() + oprfSeed := internal.RandomBytes(conf.conf.Hash.Size()) + + if err := server.SetKeyMaterial(nil, sks, pks, oprfSeed); err != nil { + log.Fatal(err) + } + + rec := buildRecord(credID, oprfSeed, []byte("yo"), pks, client, server) + + ke1 := client.LoginInit([]byte("yo")) + ke2, _ := server.LoginInit(ke1, rec) + + ke2.Mac = internal.RandomBytes(client.GetConf().MAC.Size()) + expected := "finalizing AKE: invalid server mac" + if _, _, err := client.LoginFinish(ke2); err == nil || !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error %q for invalid epks encoding - got %q", expected, err) + } + }) +} + +func TestClientFinish_MissingKe1(t *testing.T) { + expectedError := "missing KE1 in client state" + conf := opaque.DefaultConfiguration() + client, _ := conf.Client() + if _, _, err := client.LoginFinish(nil); err == nil || !strings.EqualFold(err.Error(), expectedError) { + t.Fatalf( + "expected error when calling LoginFinish without pre-existing KE1, want %q, got %q", + expectedError, + err, + ) + } +} diff --git a/tests/deserializer_test.go b/tests/deserializer_test.go new file mode 100644 index 0000000..32be4cc --- /dev/null +++ b/tests/deserializer_test.go @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package opaque_test + +import ( + "encoding/hex" + "errors" + "testing" + + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque" + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/internal/encoding" +) + +const testErrValidConf = "unexpected error on valid configuration: %v" + +var errInvalidMessageLength = errors.New("invalid message length for the configuration") + +/* + Message Deserialization +*/ + +func TestDeserializer(t *testing.T) { + // Test valid configurations + testAll(t, func(t2 *testing.T, conf *configuration) { + if _, err := conf.conf.Deserializer(); err != nil { + t.Fatalf(testErrValidConf, err) + } + }) + + // Test for an invalid configuration. + conf := &opaque.Configuration{ + OPRF: 0, + KDF: 0, + MAC: 0, + Hash: 0, + KSF: 0, + AKE: 0, + Context: nil, + } + + if _, err := conf.Deserializer(); err == nil { + t.Fatal("expected error on invalid configuration") + } +} + +func TestDeserializeRegistrationRequest(t *testing.T) { + c := opaque.DefaultConfiguration() + + server, _ := c.Server() + conf := server.GetConf() + length := conf.OPRF.Group().ElementLength() + 1 + if _, err := server.Deserialize.RegistrationRequest(internal.RandomBytes(length)); err == nil || + err.Error() != errInvalidMessageLength.Error() { + t.Fatalf("Expected error for DeserializeRegistrationRequest. want %q, got %q", errInvalidMessageLength, err) + } + + client, _ := c.Client() + if _, err := client.Deserialize.RegistrationRequest(internal.RandomBytes(length)); err == nil || + err.Error() != errInvalidMessageLength.Error() { + t.Fatalf("Expected error for DeserializeRegistrationRequest. want %q, got %q", errInvalidMessageLength, err) + } +} + +func TestDeserializeRegistrationResponse(t *testing.T) { + c := opaque.DefaultConfiguration() + + server, _ := c.Server() + conf := server.GetConf() + length := conf.OPRF.Group().ElementLength() + conf.Group.ElementLength() + 1 + if _, err := server.Deserialize.RegistrationResponse(internal.RandomBytes(length)); err == nil || + err.Error() != errInvalidMessageLength.Error() { + t.Fatalf("Expected error for DeserializeRegistrationRequest. want %q, got %q", errInvalidMessageLength, err) + } + + client, _ := c.Client() + if _, err := client.Deserialize.RegistrationResponse(internal.RandomBytes(length)); err == nil || + err.Error() != errInvalidMessageLength.Error() { + t.Fatalf("Expected error for DeserializeRegistrationRequest. want %q, got %q", errInvalidMessageLength, err) + } +} + +func TestDeserializeRegistrationRecord(t *testing.T) { + testAll(t, func(t2 *testing.T, conf *configuration) { + server, err := conf.conf.Server() + if err != nil { + t.Fatal(err) + } + c := server.GetConf() + length := c.Group.ElementLength() + c.Hash.Size() + c.EnvelopeSize + 1 + if _, err := server.Deserialize.RegistrationRecord(internal.RandomBytes(length)); err == nil || + err.Error() != errInvalidMessageLength.Error() { + t.Fatalf("Expected error for DeserializeRegistrationRequest. want %q, got %q", errInvalidMessageLength, err) + } + + badPKu := getBadElement(t, conf) + rec := encoding.Concat(badPKu, internal.RandomBytes(c.Hash.Size()+c.EnvelopeSize)) + + expect := "invalid client public key" + if _, err := server.Deserialize.RegistrationRecord(rec); err == nil || err.Error() != expect { + t.Fatalf("Expected error for DeserializeRegistrationRequest. want %q, got %q", expect, err) + } + + client, err := conf.conf.Client() + if err != nil { + t.Fatal(err) + } + if _, err := client.Deserialize.RegistrationRecord(internal.RandomBytes(length)); err == nil || + err.Error() != errInvalidMessageLength.Error() { + t.Fatalf("Expected error for DeserializeRegistrationRequest. want %q, got %q", errInvalidMessageLength, err) + } + }) +} + +func TestDeserializeKE1(t *testing.T) { + c := opaque.DefaultConfiguration() + g := group.Group(c.AKE) + ke1Length := g.ElementLength() + internal.NonceLength + g.ElementLength() + + server, _ := c.Server() + if _, err := server.Deserialize.KE1(internal.RandomBytes(ke1Length + 1)); err == nil || + err.Error() != errInvalidMessageLength.Error() { + t.Fatalf("Expected error for DeserializeKE1. want %q, got %q", errInvalidMessageLength, err) + } + + client, _ := c.Client() + if _, err := client.Deserialize.KE1(internal.RandomBytes(ke1Length + 1)); err == nil || + err.Error() != errInvalidMessageLength.Error() { + t.Fatalf("Expected error for DeserializeKE1. want %q, got %q", errInvalidMessageLength, err) + } +} + +func TestDeserializeKE2(t *testing.T) { + c := opaque.DefaultConfiguration() + + client, _ := c.Client() + conf := client.GetConf() + ke2Length := conf.OPRF.Group(). + ElementLength() + + 2*conf.NonceLen + 2*conf.Group.ElementLength() + conf.EnvelopeSize + conf.MAC.Size() + if _, err := client.Deserialize.KE2(internal.RandomBytes(ke2Length + 1)); err == nil || + err.Error() != errInvalidMessageLength.Error() { + t.Fatalf("Expected error for DeserializeKE1. want %q, got %q", errInvalidMessageLength, err) + } + + server, _ := c.Server() + conf = server.GetConf() + ke2Length = conf.OPRF.Group(). + ElementLength() + + 2*conf.NonceLen + 2*conf.Group.ElementLength() + conf.EnvelopeSize + conf.MAC.Size() + if _, err := server.Deserialize.KE2(internal.RandomBytes(ke2Length + 1)); err == nil || + err.Error() != errInvalidMessageLength.Error() { + t.Fatalf("Expected error for DeserializeKE1. want %q, got %q", errInvalidMessageLength, err) + } +} + +func TestDeserializeKE3(t *testing.T) { + c := opaque.DefaultConfiguration() + ke3Length := c.MAC.Size() + + server, _ := c.Server() + if _, err := server.Deserialize.KE3(internal.RandomBytes(ke3Length + 1)); err == nil || + err.Error() != errInvalidMessageLength.Error() { + t.Fatalf("Expected error for DeserializeKE1. want %q, got %q", errInvalidMessageLength, err) + } + + client, _ := c.Client() + if _, err := client.Deserialize.KE3(internal.RandomBytes(ke3Length + 1)); err == nil || + err.Error() != errInvalidMessageLength.Error() { + t.Fatalf("Expected error for DeserializeKE1. want %q, got %q", errInvalidMessageLength, err) + } +} + +func TestDecodeAkePrivateKey(t *testing.T) { + testAll(t, func(t2 *testing.T, conf *configuration) { + badKey := getBadScalar(t, conf) + + des, err := conf.conf.Deserializer() + if err != nil { + t.Fatalf(testErrValidConf, err) + } + + if _, err := des.DecodeAkePrivateKey(badKey); err == nil { + t.Fatalf("expect error on invalid private key. Group %v, key %v", + conf.conf.AKE, + hex.EncodeToString(badKey), + ) + } + }) +} + +func TestDecodeAkePublicKey(t *testing.T) { + testAll(t, func(t2 *testing.T, conf *configuration) { + badKey := getBadElement(t, conf) + + des, err := conf.conf.Deserializer() + if err != nil { + t.Fatalf(testErrValidConf, err) + } + + if _, err := des.DecodeAkePublicKey(badKey); err == nil { + t.Fatalf("expect error on invalid public key. Group %v, key %v", + conf.conf.AKE, + hex.EncodeToString(badKey), + ) + } + }) +} diff --git a/tests/encoding_test.go b/tests/encoding_test.go new file mode 100644 index 0000000..6e934cc --- /dev/null +++ b/tests/encoding_test.go @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package opaque_test + +import ( + "bytes" + "encoding/hex" + "fmt" + "testing" + + "github.com/Jan-nku/opaque/internal/encoding" +) + +func TestEncodeVectorLenPanic(t *testing.T) { + /* + EncodeVectorLen with size > 2 + */ + defer func() { + recover() + }() + + encoding.EncodeVectorLen(nil, 3) + t.Fatal("no panic with exceeding encoding length") +} + +func TestDecodeVector(t *testing.T) { + /* + DecodeVector with invalid header and payload + */ + + badHeader := []byte{0} + if _, _, err := encoding.DecodeVector(badHeader); err == nil || + err.Error() != "insufficient header length for decoding" { + t.Fatalf("expected error for short input. Got %q", err) + } + + badPayload := []byte{0, 3, 0, 0} + if _, _, err := encoding.DecodeVector(badPayload); err == nil || + err.Error() != "insufficient total length for decoding" { + t.Fatalf("expected error for short input. Got %q", err) + } +} + +type i2ospTest struct { + encoded []byte + value int + size int +} + +var I2OSPVectors = []i2ospTest{ + { + []byte{0}, 0, 1, + }, + { + []byte{1}, 1, 1, + }, + { + []byte{0xff}, 255, 1, + }, + { + []byte{0x01, 0x00}, 256, 2, + }, + { + []byte{0xff, 0xff}, 65535, 2, + }, + { + []byte{0xff, 0xe3, 0xd0}, 16770000, 3, + }, + { + []byte{0xff, 0xff, 0xe3, 0x80}, 4294960000, 4, + }, +} + +func TestI2OSP(t *testing.T) { + for i, v := range I2OSPVectors { + t.Run(fmt.Sprintf("%d - %d - %v", v.value, v.size, v.encoded), func(t *testing.T) { + r := encoding.I2OSP(v.value, v.size) + + if !bytes.Equal(r, v.encoded) { + t.Fatalf( + "invalid encoding for %d. Expected '%s', got '%v'", + i, + hex.EncodeToString(v.encoded), + hex.EncodeToString(r), + ) + } + + value := encoding.OS2IP(v.encoded) + if v.value != value { + t.Errorf("invalid decoding for %d. Expected %d, got %d", i, v.value, value) + } + }) + } + + length := -1 + if hasPanic, err := expectPanic(nil, func() { + _ = encoding.I2OSP(1, length) + }); !hasPanic { + t.Fatalf("expected panic with with negative length: %v", err) + } + + length = 0 + if hasPanic, err := expectPanic(nil, func() { + _ = encoding.I2OSP(1, length) + }); !hasPanic { + t.Fatalf("expected panic with with 0 length: %v", err) + } + + length = 5 + if hasPanic, err := expectPanic(nil, func() { + _ = encoding.I2OSP(1, length) + }); !hasPanic { + t.Fatalf("expected panic with length too big: %v", err) + } + + negative := -1 + if hasPanic, err := expectPanic(nil, func() { + _ = encoding.I2OSP(negative, 4) + }); !hasPanic { + t.Fatalf("expected panic with negative input: %v", err) + } + + tooLarge := 1 << 32 + length = 1 + if hasPanic, err := expectPanic(nil, func() { + _ = encoding.I2OSP(tooLarge, length) + }); !hasPanic { + t.Fatalf("expected panic with exceeding value for the length: %v", err) + } + + lengths := map[int]int{ + 100: 1, + 1 << 8: 2, + 1 << 16: 3, + (1 << 32) - 1: 4, + } + + for k, v := range lengths { + r := encoding.I2OSP(k, v) + + if len(r) != v { + t.Fatalf("invalid length for %d. Expected '%d', got '%d' (%v)", k, v, len(r), r) + } + } +} + +func TestOS2IP(t *testing.T) { + // No input + if hasPanic, _ := expectPanic(nil, func() { + _ = encoding.OS2IP(nil) + }); !hasPanic { + t.Fatal("expected panic with nil input") + } + + // Empty input + if hasPanic, _ := expectPanic(nil, func() { + _ = encoding.OS2IP([]byte("")) + }); !hasPanic { + t.Fatal("expected panic with empty input") + } + + // Exceeding input + input := "12345" + if hasPanic, _ := expectPanic(nil, func() { + _ = encoding.OS2IP([]byte(input)) + }); !hasPanic { + t.Fatal("expected panic with big input") + } +} + +func hasPanic(f func()) (has bool, err error) { + err = nil + var report interface{} + func() { + defer func() { + if report = recover(); report != nil { + has = true + } + }() + + f() + }() + + if has { + err = fmt.Errorf("%v", report) + } + + return +} + +func expectPanic(expectedError error, f func()) (bool, string) { + hasPanic, err := hasPanic(f) + + if !hasPanic { + return false, "no panic" + } + + if expectedError == nil { + return true, "" + } + + if err == nil { + return false, "panic but no message" + } + + if err.Error() != expectedError.Error() { + return false, fmt.Sprintf("expected %q, got %q", expectedError, err) + } + + return true, "" +} diff --git a/tests/fuzz_test.go b/tests/fuzz_test.go new file mode 100644 index 0000000..613b0ac --- /dev/null +++ b/tests/fuzz_test.go @@ -0,0 +1,453 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package opaque_test + +import ( + "crypto" + "encoding/json" + "errors" + "fmt" + "log" + "os" + "strings" + "testing" + + "github.com/bytemare/hash" + "github.com/bytemare/ksf" + + "github.com/Jan-nku/opaque" + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/internal/oprf" +) + +const ( + fmtGotValidInput = "got %q but input is valid" +) + +func fuzzTestConfigurationError(t *testing.T, c *opaque.Configuration, err error) { + // Errors tested for + var ( + errInvalidKDFid = errors.New("invalid KDF id") + errInvalidMACid = errors.New("invalid MAC id") + errInvalidHASHid = errors.New("invalid Hash id") + errInvalidKSFid = errors.New("invalid KSF id") + errInvalidOPRFid = errors.New("invalid OPRF group id") + errInvalidAKEid = errors.New("invalid AKE group id") + ) + + if strings.Contains(err.Error(), errInvalidKDFid.Error()) { + if hash.Hashing(c.KDF).Available() { + t.Fatalf("got %q but input is valid: %q", errInvalidKDFid, c.KDF) + } + t.Skip() + } + if strings.Contains(err.Error(), errInvalidMACid.Error()) { + if hash.Hashing(c.MAC).Available() { + t.Fatalf("got %q but input is valid: %q", errInvalidMACid, c.MAC) + } + t.Skip() + } + if strings.Contains(err.Error(), errInvalidHASHid.Error()) { + if hash.Hashing(c.Hash).Available() { + t.Fatalf("got %q but input is valid: %q", errInvalidHASHid, c.Hash) + } + t.Skip() + } + if strings.Contains(err.Error(), errInvalidKSFid.Error()) { + if c.KSF.Available() { + t.Fatalf("got %q but input is valid: %q", errInvalidKSFid, c.KSF) + } + t.Skip() + } + if strings.Contains(err.Error(), errInvalidOPRFid.Error()) { + if c.OPRF.OPRF().Available() { + t.Fatalf("got %q but input is valid: %q", errInvalidOPRFid, c.OPRF) + } + t.Skip() + } + if strings.Contains(err.Error(), errInvalidAKEid.Error()) { + if c.AKE.Group().Available() { + t.Fatalf("got %q but input is valid: %q", errInvalidAKEid, c.AKE) + } + t.Skip() + } + + t.Fatalf("Unrecognized error: %q", err) +} + +func fuzzClientConfiguration(t *testing.T, c *opaque.Configuration) *opaque.Client { + client, err := c.Client() + if err != nil { + fuzzTestConfigurationError(t, c, err) + } + if client == nil { + t.Fatal("server is nil") + } + + return client +} + +func fuzzServerConfiguration(t *testing.T, c *opaque.Configuration) *opaque.Server { + server, err := c.Server() + if err != nil { + fuzzTestConfigurationError(t, c, err) + } + if server == nil { + t.Fatal("server is nil") + } + + return server +} + +func fuzzLoadVectors(path string) ([]*vector, error) { + contents, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("no vectors to read: %v", err) + } + + var v []*vector + err = json.Unmarshal(contents, &v) + if err != nil { + return nil, fmt.Errorf("no vectors to read: %v", err) + } + + return v, nil +} + +func FuzzConfiguration(f *testing.F) { + // seed corpus + loadVectorSeedCorpus(f, "") + + f.Fuzz(func(t *testing.T, ke1, context []byte, kdf, mac, h uint, o []byte, ksfID, ake byte) { + c := inputToConfig(context, kdf, mac, h, o, ksfID, ake) + _ = fuzzServerConfiguration(t, c) + _ = fuzzClientConfiguration(t, c) + }) +} + +func loadVectorSeedCorpus(f *testing.F, stage string) { + // seed corpus + vectors, err := fuzzLoadVectors("vectors.json") + if err != nil { + log.Fatal(err) + } + + for _, v := range vectors { + var input ByteToHex + switch stage { + case "": + input = nil + case "RegistrationRequest": + input = v.Outputs.RegistrationRequest + case "RegistrationResponse": + input = v.Outputs.RegistrationResponse + case "RegistrationRecord": + input = v.Outputs.RegistrationRecord + case "KE1": + input = v.Outputs.KE1 + case "KE2": + input = v.Outputs.KE2 + case "KE3": + input = v.Outputs.KE3 + default: + panic(nil) + } + + f.Add([]byte(input), + []byte(v.Config.Context), + uint(kdfToHash(v.Config.KDF)), + uint(macToHash(v.Config.MAC)), + uint(hashToHash(v.Config.Hash)), + []byte(v.Config.OPRF), + byte(ksfToKSF(v.Config.KSF)), + byte(groupToGroup(v.Config.Group)), + ) + } + + // previous crashers + f.Add([]byte("0"), []byte(""), uint(7), uint(37), uint(7), []byte{'\x05'}, byte('\x02'), byte('\x05')) + f.Add([]byte("0"), []byte("0"), uint(13), uint(5), uint(5), []byte{'\x03'}, byte('\r'), byte('\x03')) + f.Add([]byte("0"), []byte("0"), uint(13), uint(5), uint(5), []byte{'\a'}, byte('\x04'), byte('\x03')) + f.Add( + []byte("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + []byte("0"), + uint(7), + uint(7), + uint(7), + []byte{'\x01'}, + byte('\x03'), + byte('\x01'), + ) + f.Add( + []byte("00000000000000000000000000000000"), + []byte("0"), + uint(7), + uint(7), + uint(7), + []byte{'\x01'}, + byte('\x03'), + byte('\x06'), + ) +} + +func inputToConfig(context []byte, kdf, mac, h uint, o []byte, ksfID, ake byte) *opaque.Configuration { + return &opaque.Configuration{ + Context: context, + KDF: crypto.Hash(kdf), + MAC: crypto.Hash(mac), + Hash: crypto.Hash(h), + OPRF: oprfToGroup(oprf.Identifier(o)), + KSF: ksf.Identifier(ksfID), + AKE: opaque.Group(ake), + } +} + +func FuzzDeserializeRegistrationRequest(f *testing.F) { + // Errors tested for + var ( + errInvalidMessageLength = errors.New("invalid message length for the configuration") + errInvalidBlindedData = errors.New("blinded data is an invalid point") + ) + + loadVectorSeedCorpus(f, "RegistrationRequest") + + f.Fuzz(func(t *testing.T, r1, context []byte, kdf, mac, h uint, oprf []byte, ksfID, ake byte) { + c := inputToConfig(context, kdf, mac, h, oprf, ksfID, ake) + server, err := c.Server() + if err != nil { + t.Skip() + } + + _, err = server.Deserialize.RegistrationRequest(r1) + if err != nil { + conf := server.GetConf() + if strings.Contains(err.Error(), errInvalidMessageLength.Error()) && + len(r1) == conf.OPRF.Group().ElementLength() { + t.Fatalf("got %q but input length is valid", errInvalidMessageLength) + } + + if strings.Contains(err.Error(), errInvalidBlindedData.Error()) { + if err := isValidOPRFPoint(conf, r1[:conf.OPRF.Group().ElementLength()], errInvalidBlindedData); err != nil { + t.Fatal(err) + } + } + } + }) +} + +func FuzzDeserializeRegistrationResponse(f *testing.F) { + // Errors tested for + var ( + errInvalidMessageLength = errors.New("invalid message length for the configuration") + errInvalidEvaluatedData = errors.New("invalid OPRF evaluation") + errInvalidServerPK = errors.New("invalid server public key") + ) + + loadVectorSeedCorpus(f, "RegistrationResponse") + + f.Fuzz(func(t *testing.T, r2, context []byte, kdf, mac, h uint, oprf []byte, ksfID, ake byte) { + c := inputToConfig(context, kdf, mac, h, oprf, ksfID, ake) + client, err := c.Client() + if err != nil { + t.Skip() + } + + _, err = client.Deserialize.RegistrationResponse(r2) + if err != nil { + conf := client.GetConf() + maxResponseLength := conf.OPRF.Group().ElementLength() + conf.Group.ElementLength() + + if strings.Contains(err.Error(), errInvalidMessageLength.Error()) && len(r2) == maxResponseLength { + t.Fatalf(fmtGotValidInput, errInvalidMessageLength) + } + + if strings.Contains(err.Error(), errInvalidEvaluatedData.Error()) { + if err := isValidOPRFPoint(conf, r2[:conf.OPRF.Group().ElementLength()], errInvalidEvaluatedData); err != nil { + t.Fatal(err) + } + } + + if strings.Contains(err.Error(), errInvalidServerPK.Error()) { + if err := isValidAKEPoint(conf, r2[conf.OPRF.Group().ElementLength():], errInvalidServerPK); err != nil { + t.Fatal(err) + } + } + } + }) +} + +func FuzzDeserializeRegistrationRecord(f *testing.F) { + // Errors tested for + var ( + errInvalidMessageLength = errors.New("invalid message length for the configuration") + errInvalidClientPK = errors.New("invalid client public key") + ) + + loadVectorSeedCorpus(f, "RegistrationRecord") + + f.Fuzz(func(t *testing.T, r3, context []byte, kdf, mac, h uint, oprf []byte, ksfID, ake byte) { + c := inputToConfig(context, kdf, mac, h, oprf, ksfID, ake) + server, err := c.Server() + if err != nil { + t.Skip() + } + + conf := server.GetConf() + + _, err = server.Deserialize.RegistrationRecord(r3) + if err != nil { + maxMessageLength := conf.Group.ElementLength() + conf.Hash.Size() + conf.EnvelopeSize + + if strings.Contains(err.Error(), errInvalidMessageLength.Error()) && len(r3) == maxMessageLength { + t.Fatalf(fmtGotValidInput, errInvalidMessageLength) + } + + if strings.Contains(err.Error(), errInvalidClientPK.Error()) { + if err := isValidAKEPoint(conf, r3[:conf.Group.ElementLength()], errInvalidClientPK); err != nil { + t.Fatal(err) + } + } + } + }) +} + +func FuzzDeserializeKE1(f *testing.F) { + // Errors tested for + var ( + errInvalidMessageLength = errors.New("invalid message length for the configuration") + errInvalidBlindedData = errors.New("blinded data is an invalid point") + errInvalidClientEPK = errors.New("invalid ephemeral client public key") + ) + + loadVectorSeedCorpus(f, "KE1") + + f.Fuzz(func(t *testing.T, ke1, context []byte, kdf, mac, h uint, oprf []byte, ksfID, ake byte) { + c := inputToConfig(context, kdf, mac, h, oprf, ksfID, ake) + server, err := c.Server() + if err != nil { + t.Skip() + } + + _, err = server.Deserialize.KE1(ke1) + if err != nil { + conf := server.GetConf() + if strings.Contains(err.Error(), errInvalidMessageLength.Error()) && + len(ke1) == conf.OPRF.Group().ElementLength()+conf.NonceLen+conf.Group.ElementLength() { + t.Fatalf("got %q but input length is valid", errInvalidMessageLength) + } + + if strings.Contains(err.Error(), errInvalidBlindedData.Error()) { + if err := isValidOPRFPoint(conf, ke1[:conf.OPRF.Group().ElementLength()], errInvalidBlindedData); err != nil { + t.Fatal(err) + } + } + + if strings.Contains(err.Error(), errInvalidClientEPK.Error()) { + if err := isValidOPRFPoint(conf, ke1[conf.OPRF.Group().ElementLength()+conf.NonceLen:], errInvalidClientEPK); err != nil { + t.Fatal(err) + } + } + } + }) +} + +func isValidAKEPoint(conf *internal.Configuration, input []byte, err error) error { + e := conf.Group.NewElement() + if _err := e.Decode(input); _err == nil { + if e.IsIdentity() { + return errors.New("point is identity/infinity") + } + + return fmt.Errorf("got %q but point is valid", err) + } + + return nil +} + +func isValidOPRFPoint(conf *internal.Configuration, input []byte, err error) error { + e := conf.OPRF.Group().NewElement() + if _err := e.Decode(input); _err == nil { + if e.IsIdentity() { + return errors.New("point is identity/infinity") + } + + return fmt.Errorf("got %q but point is valid", err) + } + + return nil +} + +func FuzzDeserializeKE2(f *testing.F) { + // Errors tested for + var ( + errInvalidMessageLength = errors.New("invalid message length for the configuration") + errInvalidEvaluatedData = errors.New("invalid OPRF evaluation") + errInvalidServerEPK = errors.New("invalid ephemeral server public key") + ) + + loadVectorSeedCorpus(f, "KE2") + + f.Fuzz(func(t *testing.T, ke2, context []byte, kdf, mac, h uint, oprf []byte, ksfID, ake byte) { + c := inputToConfig(context, kdf, mac, h, oprf, ksfID, ake) + client, err := c.Client() + if err != nil { + t.Skip() + } + + _, err = client.Deserialize.KE2(ke2) + if err != nil { + conf := client.GetConf() + maxResponseLength := conf.OPRF.Group(). + ElementLength() + + conf.NonceLen + conf.Group.ElementLength() + conf.EnvelopeSize + + if strings.Contains(err.Error(), errInvalidMessageLength.Error()) && + len(ke2) == maxResponseLength+conf.NonceLen+conf.Group.ElementLength()+conf.MAC.Size() { + t.Fatalf(fmtGotValidInput, errInvalidMessageLength) + } + + if strings.Contains(err.Error(), errInvalidEvaluatedData.Error()) { + if err := isValidOPRFPoint(conf, ke2[:conf.OPRF.Group().ElementLength()], errInvalidEvaluatedData); err != nil { + t.Fatal(err) + } + } + + if strings.Contains(err.Error(), errInvalidServerEPK.Error()) { + if err := isValidAKEPoint(conf, ke2[conf.OPRF.Group().ElementLength()+conf.NonceLen:], errInvalidServerEPK); err != nil { + t.Fatal(err) + } + } + } + }) +} + +func FuzzDeserializeKE3(f *testing.F) { + // Error tested for + errInvalidMessageLength := errors.New("invalid message length for the configuration") + + loadVectorSeedCorpus(f, "KE3") + + f.Fuzz(func(t *testing.T, ke3, context []byte, kdf, mac, h uint, oprf []byte, ksfID, ake byte) { + c := inputToConfig(context, kdf, mac, h, oprf, ksfID, ake) + server, err := c.Server() + if err != nil { + t.Skip() + } + + _, err = server.Deserialize.KE3(ke3) + if err != nil { + conf := server.GetConf() + maxMessageLength := conf.MAC.Size() + + if strings.Contains(err.Error(), errInvalidMessageLength.Error()) && len(ke3) == maxMessageLength { + t.Fatalf(fmtGotValidInput, errInvalidMessageLength) + } + } + }) +} diff --git a/tests/helper_test.go b/tests/helper_test.go new file mode 100644 index 0000000..60fc09f --- /dev/null +++ b/tests/helper_test.go @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package opaque_test + +import ( + "crypto" + "crypto/elliptic" + "encoding/hex" + "fmt" + "log" + "math/big" + "testing" + + group "github.com/bytemare/crypto" + "github.com/bytemare/ksf" + + "github.com/Jan-nku/opaque" + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/internal/encoding" + "github.com/Jan-nku/opaque/internal/keyrecovery" + "github.com/Jan-nku/opaque/internal/oprf" + "github.com/Jan-nku/opaque/internal/tag" + "github.com/Jan-nku/opaque/message" +) + +func init() { + log.SetFlags(log.LstdFlags | log.Lshortfile) +} + +// helper functions + +type configuration struct { + curve elliptic.Curve + conf *opaque.Configuration + name string +} + +var configurationTable = []configuration{ + { + name: "Ristretto255", + conf: opaque.DefaultConfiguration(), + curve: nil, + }, + { + name: "P256Sha256", + conf: &opaque.Configuration{ + OPRF: opaque.P256Sha256, + KDF: crypto.SHA256, + MAC: crypto.SHA256, + Hash: crypto.SHA256, + KSF: ksf.Scrypt, + AKE: opaque.P256Sha256, + }, + curve: elliptic.P256(), + }, + { + name: "P384Sha512", + conf: &opaque.Configuration{ + OPRF: opaque.P384Sha512, + KDF: crypto.SHA512, + MAC: crypto.SHA512, + Hash: crypto.SHA512, + KSF: ksf.Scrypt, + AKE: opaque.P384Sha512, + }, + curve: elliptic.P384(), + }, + { + name: "P521Sha512", + conf: &opaque.Configuration{ + OPRF: opaque.P521Sha512, + KDF: crypto.SHA512, + MAC: crypto.SHA512, + Hash: crypto.SHA512, + KSF: ksf.Scrypt, + AKE: opaque.P521Sha512, + }, + curve: elliptic.P521(), + }, +} + +func testAll(t *testing.T, f func(*testing.T, *configuration)) { + for _, test := range configurationTable { + t.Run(test.name, func(t *testing.T) { + f(t, &test) + }) + } +} + +func getBadRistrettoScalar() []byte { + a := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + decoded, _ := hex.DecodeString(a) + + return decoded +} + +func getBadRistrettoElement() []byte { + a := "2a292df7e32cababbd9de088d1d1abec9fc0440f637ed2fba145094dc14bea08" + decoded, _ := hex.DecodeString(a) + + return decoded +} + +func getBad25519Element() []byte { + a := "efffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f" + decoded, _ := hex.DecodeString(a) + + return decoded +} + +func getBad25519Scalar() []byte { + a := "ecd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000011" + decoded, _ := hex.DecodeString(a) + + return decoded +} + +func badScalar(t *testing.T, g group.Group, curve elliptic.Curve) []byte { + order := curve.Params().P + exceeded := new(big.Int).Add(order, big.NewInt(2)).Bytes() + + err := g.NewScalar().Decode(exceeded) + if err == nil { + t.Errorf("Exceeding order did not yield an error for group %s", g) + } + + return exceeded +} + +func getBadNistElement(t *testing.T, id group.Group) []byte { + size := id.ElementLength() + element := internal.RandomBytes(size) + // detag compression + element[0] = 4 + + // test if invalid compression is detected + err := id.NewElement().Decode(element) + if err == nil { + t.Errorf("detagged compressed point did not yield an error for group %s", id) + } + + return element +} + +func getBadElement(t *testing.T, c *configuration) []byte { + switch c.conf.AKE { + case opaque.RistrettoSha512: + return getBadRistrettoElement() + default: + return getBadNistElement(t, group.Group(c.conf.AKE)) + } +} + +func getBadScalar(t *testing.T, c *configuration) []byte { + switch c.conf.AKE { + case opaque.RistrettoSha512: + return getBadRistrettoScalar() + default: + return badScalar(t, oprf.IDFromGroup(group.Group(c.conf.AKE)).Group(), c.curve) + } +} + +func buildRecord( + credID, oprfSeed, password, pks []byte, + client *opaque.Client, + server *opaque.Server, +) *opaque.ClientRecord { + conf := server.GetConf() + r1 := client.RegistrationInit(password) + + pk := conf.Group.NewElement() + if err := pk.Decode(pks); err != nil { + panic(err) + } + + r2 := server.RegistrationResponse(r1, pk, credID, oprfSeed) + r3, _ := client.RegistrationFinalize(r2) + + return &opaque.ClientRecord{ + CredentialIdentifier: credID, + ClientIdentity: nil, + RegistrationRecord: r3, + TestMaskNonce: nil, + } +} + +func xorResponse(c *internal.Configuration, key, nonce, in []byte) []byte { + pad := c.KDF.Expand( + key, + encoding.SuffixString(nonce, tag.CredentialResponsePad), + c.Group.ElementLength()+c.EnvelopeSize, + ) + + dst := make([]byte, len(pad)) + + // if the size is fixed, we could unroll the loop + for i, r := range pad { + dst[i] = r ^ in[i] + } + + return dst +} + +func buildPRK(client *opaque.Client, evaluation *group.Element) ([]byte, error) { + conf := client.GetConf() + unblinded := client.OPRF.Finalize(evaluation) + hardened := conf.KSF.Harden(unblinded, nil, conf.OPRF.Group().ElementLength()) + + return conf.KDF.Extract(nil, encoding.Concat(unblinded, hardened)), nil +} + +func getEnvelope(client *opaque.Client, ke2 *message.KE2) (*keyrecovery.Envelope, []byte, error) { + conf := client.GetConf() + + randomizedPwd, err := buildPRK(client, ke2.EvaluatedMessage) + if err != nil { + return nil, nil, fmt.Errorf("finalizing OPRF : %w", err) + } + + maskingKey := conf.KDF.Expand(randomizedPwd, []byte(tag.MaskingKey), conf.KDF.Size()) + clear := xorResponse(conf, maskingKey, ke2.MaskingNonce, ke2.MaskedResponse) + e := clear[conf.Group.ElementLength():] + + env := &keyrecovery.Envelope{ + Nonce: e[:conf.NonceLen], + AuthTag: e[conf.NonceLen:], + } + + return env, randomizedPwd, nil +} diff --git a/tests/opaque_test.go b/tests/opaque_test.go new file mode 100644 index 0000000..fbf2a85 --- /dev/null +++ b/tests/opaque_test.go @@ -0,0 +1,502 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package opaque_test + +import ( + "bytes" + "crypto" + "errors" + "reflect" + "strings" + "testing" + + group "github.com/bytemare/crypto" + "github.com/bytemare/ksf" + + "github.com/Jan-nku/opaque" + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/internal/oprf" +) + +const dbgErr = "%v" + +type testParams struct { + *opaque.Configuration + username, userID, serverID, password, serverSecretKey, serverPublicKey, oprfSeed []byte +} + +func TestFull(t *testing.T) { + ids := []byte("server") + username := []byte("client") + password := []byte("password") + + conf := opaque.DefaultConfiguration() + conf.Context = []byte("OPAQUETest") + + test := &testParams{ + Configuration: conf, + username: username, + userID: username, + serverID: ids, + password: password, + oprfSeed: conf.GenerateOPRFSeed(), + } + + serverSecretKey, pks := conf.KeyGen() + test.serverSecretKey = serverSecretKey + test.serverPublicKey = pks + + /* + Registration + */ + _, _, record, exportKeyReg := testRegistration(t, test) + + /* + Login + */ + _, _, exportKeyLogin := testAuthentication(t, test, record) + + // Check values + if !bytes.Equal(exportKeyReg, exportKeyLogin) { + t.Errorf("export keys differ") + } +} + +func testRegistration(t *testing.T, p *testParams) (*opaque.Client, *opaque.Server, *opaque.ClientRecord, []byte) { + // Client + client, _ := p.Client() + + var m1s []byte + { + reqReg := client.RegistrationInit(p.password) + m1s = reqReg.Serialize() + } + + // Server + var m2s []byte + var credID []byte + { + server, _ := p.Server() + m1, err := server.Deserialize.RegistrationRequest(m1s) + if err != nil { + t.Fatalf(dbgErr, err) + } + + credID = internal.RandomBytes(32) + pks, err := server.Deserialize.DecodeAkePublicKey(p.serverPublicKey) + if err != nil { + t.Fatalf(dbgErr, err) + } + + respReg := server.RegistrationResponse(m1, pks, credID, p.oprfSeed) + + m2s = respReg.Serialize() + } + + // Client + var m3s []byte + var exportKeyReg []byte + { + m2, err := client.Deserialize.RegistrationResponse(m2s) + if err != nil { + t.Fatalf(dbgErr, err) + } + + upload, key := client.RegistrationFinalize(m2, opaque.ClientRegistrationFinalizeOptions{ + ClientIdentity: p.username, + ServerIdentity: p.serverID, + }) + exportKeyReg = key + + m3s = upload.Serialize() + } + + // Server + { + server, _ := p.Server() + m3, err := server.Deserialize.RegistrationRecord(m3s) + if err != nil { + t.Fatalf(dbgErr, err) + } + + return client, server, &opaque.ClientRecord{ + CredentialIdentifier: credID, + ClientIdentity: p.username, + RegistrationRecord: m3, + }, exportKeyReg + } +} + +func testAuthentication( + t *testing.T, + p *testParams, + record *opaque.ClientRecord, +) (*opaque.Client, *opaque.Server, []byte) { + // Client + client, _ := p.Client() + + var m4s []byte + { + ke1 := client.LoginInit(p.password) + m4s = ke1.Serialize() + } + + // Server + var m5s []byte + var state []byte + server, _ := p.Server() + { + if err := server.SetKeyMaterial(p.serverID, p.serverSecretKey, p.serverPublicKey, p.oprfSeed); err != nil { + t.Fatal(err) + } + + m4, err := server.Deserialize.KE1(m4s) + if err != nil { + t.Fatalf(dbgErr, err) + } + + ke2, err := server.LoginInit(m4, record) + if err != nil { + t.Fatalf(dbgErr, err) + } + + state = server.SerializeState() + + m5s = ke2.Serialize() + } + + // Client + var m6s []byte + var exportKeyLogin []byte + var clientKey []byte + { + m5, err := client.Deserialize.KE2(m5s) + if err != nil { + t.Fatalf(dbgErr, err) + } + + ke3, key, err := client.LoginFinish(m5, opaque.ClientLoginFinishOptions{ + ClientIdentity: p.username, + ServerIdentity: p.serverID, + }) + if err != nil { + t.Fatalf(dbgErr, err) + } + exportKeyLogin = key + + m6s = ke3.Serialize() + clientKey = client.SessionKey() + } + + // Server + var serverKey []byte + { + // here we spawn a new server instance to test setting the state + resumedServer, _ := p.Server() + if err := resumedServer.SetAKEState(state); err != nil { + t.Fatalf(dbgErr, err) + } + + m6, err := resumedServer.Deserialize.KE3(m6s) + if err != nil { + t.Fatalf(dbgErr, err) + } + + if err := resumedServer.LoginFinish(m6); err != nil { + t.Fatalf(dbgErr, err) + } + + serverKey = resumedServer.SessionKey() + } + + if !bytes.Equal(clientKey, serverKey) { + t.Fatalf("session keys differ") + } + + return client, server, exportKeyLogin +} + +func isSameConf(a, b *opaque.Configuration) bool { + if a.OPRF != b.OPRF { + return false + } + if a.KDF != b.KDF { + return false + } + if a.MAC != b.MAC { + return false + } + if a.Hash != b.Hash { + return false + } + if a.KSF != b.KSF { + return false + } + if a.AKE != b.AKE { + return false + } + + return bytes.Equal(a.Context, b.Context) +} + +func TestConfiguration_Deserialization(t *testing.T) { + conf := opaque.DefaultConfiguration() + ser := conf.Serialize() + + conf2, err := opaque.DeserializeConfiguration(ser) + if err != nil { + t.Fatalf("unexpected error on valid configuration: %v", err) + } + + if !isSameConf(conf, conf2) { + t.Fatalf("Unexpected inequality:\n\t%v\n\t%v", conf, conf2) + } +} + +func TestFlush(t *testing.T) { + ids := []byte("server") + username := []byte("client") + password := []byte("password") + + conf := opaque.DefaultConfiguration() + conf.Context = []byte("OPAQUETest") + + test := &testParams{ + Configuration: conf, + username: username, + userID: username, + serverID: ids, + password: password, + oprfSeed: conf.GenerateOPRFSeed(), + } + + serverSecretKey, pks := conf.KeyGen() + test.serverSecretKey = serverSecretKey + test.serverPublicKey = pks + + /* + Registration + */ + _, _, record, _ := testRegistration(t, test) + + /* + Login + */ + client, server, _ := testAuthentication(t, test, record) + + client.Ake.Flush() + if client.SessionKey() != nil { + t.Fatalf("client flush failed, the session key is non-nil: %v", client.SessionKey()) + } + + if client.Ake.GetEphemeralSecretKey() != nil { + t.Fatalf("client flush failed, the ephemeral session key is non-nil: %v", client.SessionKey()) + } + + if client.Ake.GetNonce() != nil { + t.Fatalf("client flush failed, the nonce is non-nil: %v", client.SessionKey()) + } + + server.Ake.Flush() + if server.SessionKey() != nil { + t.Fatalf("server flush failed, the session key is non-nil: %v", client.SessionKey()) + } + + if server.Ake.GetEphemeralSecretKey() != nil { + t.Fatalf("server flush failed, the ephemeral session key is non-nil: %v", client.SessionKey()) + } + + if server.Ake.GetNonce() != nil { + t.Fatalf("server flush failed, the nonce is non-nil: %v", client.SessionKey()) + } + + if server.ExpectedMAC() != nil { + t.Fatalf("server flush failed, the expected client mac is non-nil: %v", client.SessionKey()) + } +} + +/* + The following tests look for failing conditions. +*/ + +func TestNilConfiguration(t *testing.T) { + def := opaque.DefaultConfiguration() + g := group.Group(def.AKE) + defaultConfiguration := &internal.Configuration{ + KDF: internal.NewKDF(def.KDF), + MAC: internal.NewMac(def.MAC), + Hash: internal.NewHash(def.Hash), + KSF: internal.NewKSF(def.KSF), + NonceLen: internal.NonceLength, + Group: g, + OPRF: oprf.IDFromGroup(g), + Context: def.Context, + } + + s, _ := opaque.NewServer(nil) + if reflect.DeepEqual(s.GetConf(), defaultConfiguration) { + t.Errorf("server did not default to correct configuration") + } + + c, _ := opaque.NewClient(nil) + if reflect.DeepEqual(c.GetConf(), defaultConfiguration) { + t.Errorf("client did not default to correct configuration") + } +} + +func TestDeserializeConfiguration_Short(t *testing.T) { + r9 := internal.RandomBytes(7) + + if _, err := opaque.DeserializeConfiguration(r9); !errors.Is(err, internal.ErrConfigurationInvalidLength) { + t.Errorf("DeserializeConfiguration did not return the appropriate error for vector r9. want %q, got %q", + internal.ErrConfigurationInvalidLength, err) + } +} + +func TestDeserializeConfiguration_InvalidContextHeader(t *testing.T) { + d := opaque.DefaultConfiguration().Serialize() + d[7] = 3 + + expected := "decoding the configuration context: " + if _, err := opaque.DeserializeConfiguration(d); err == nil || !strings.HasPrefix(err.Error(), expected) { + t.Errorf( + "DeserializeConfiguration did not return the appropriate error for vector invalid header. want %q, got %q", + expected, + err, + ) + } +} + +func TestBadConfiguration(t *testing.T) { + setBadValue := func(pos, val int) []byte { + b := opaque.DefaultConfiguration().Serialize() + b[pos] = byte(val) + return b + } + + tests := []struct { + name string + makeBad func() []byte + error string + }{ + { + name: "Bad OPRF", + makeBad: func() []byte { + return setBadValue(0, 0) + }, + error: "invalid OPRF group id", + }, + { + name: "Bad KDF", + makeBad: func() []byte { + return setBadValue(1, 0) + }, + error: "invalid KDF id", + }, + { + name: "Bad MAC", + makeBad: func() []byte { + return setBadValue(2, 0) + }, + error: "invalid MAC id", + }, + { + name: "Bad Hash", + makeBad: func() []byte { + return setBadValue(3, 0) + }, + error: "invalid Hash id", + }, + { + name: "Bad KSF", + makeBad: func() []byte { + return setBadValue(4, 10) + }, + error: "invalid KSF id", + }, + { + name: "Bad AKE", + makeBad: func() []byte { + return setBadValue(5, 0) + }, + error: "invalid AKE group id", + }, + } + + convertToBadConf := func(encoded []byte) *opaque.Configuration { + return &opaque.Configuration{ + OPRF: opaque.Group(encoded[0]), + KDF: crypto.Hash(encoded[1]), + MAC: crypto.Hash(encoded[2]), + Hash: crypto.Hash(encoded[3]), + KSF: ksf.Identifier(encoded[4]), + AKE: opaque.Group(encoded[5]), + Context: encoded[5:], + } + } + + for _, badConf := range tests { + t.Run(badConf.name, func(t *testing.T) { + // Test Deserialization for bad conf + badEncoded := badConf.makeBad() + _, err := opaque.DeserializeConfiguration(badEncoded) + if err == nil || !strings.EqualFold(err.Error(), badConf.error) { + t.Fatalf( + "Expected error for %s. Want %q, got %q.\n\tEncoded: %v", + badConf.name, + badConf.error, + err, + badEncoded, + ) + } + + // Test bad configuration for client, server, and deserializer setup + bad := convertToBadConf(badEncoded) + + _, err = bad.Client() + if err == nil || !strings.EqualFold(err.Error(), badConf.error) { + t.Fatalf("Expected error for %s / client. Want %q, got %q", badConf.name, badConf.error, err) + } + + _, err = bad.Server() + if err == nil || !strings.EqualFold(err.Error(), badConf.error) { + t.Fatalf("Expected error for %s / server. Want %q, got %q", badConf.name, badConf.error, err) + } + + _, err = bad.Deserializer() + if err == nil || !strings.EqualFold(err.Error(), badConf.error) { + t.Fatalf("Expected error for %s / deserializer. Want %q, got %q", badConf.name, badConf.error, err) + } + }) + } +} + +func TestFakeRecord(t *testing.T) { + // Test valid configurations + testAll(t, func(t2 *testing.T, conf *configuration) { + if _, err := conf.conf.GetFakeRecord(nil); err != nil { + t.Fatalf("unexpected error on valid configuration: %v", err) + } + }) + + // Test for an invalid configuration. + conf := &opaque.Configuration{ + OPRF: 0, + KDF: 0, + MAC: 0, + Hash: 0, + KSF: 0, + AKE: 0, + Context: nil, + } + + if _, err := conf.GetFakeRecord(nil); err == nil { + t.Fatal("expected error on invalid configuration") + } +} diff --git a/tests/oprfVectors.json b/tests/oprfVectors.json new file mode 100644 index 0000000..c52060f --- /dev/null +++ b/tests/oprfVectors.json @@ -0,0 +1,137 @@ +[ + { + "groupDST": "48617368546f47726f75702d4f50524656312d002d72697374726574746f3235352d534841353132", + "hash": "SHA512", + "identifier": "ristretto255-SHA512", + "keyInfo": "74657374206b6579", + "mode": 0, + "seed": "a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3", + "skSm": "5ebcea5ee37023ccb9fc2d2019f9d7737be85591ae8652ffa9ef0f4d37063b0e", + "vectors": [ + { + "Batch": 1, + "Blind": "64d37aed22a27f5191de1c1d69fadb899d8862b58eb4220029e036ec4c1f6706", + "BlindedElement": "609a0ae68c15a3cf6903766461307e5c8bb2f95e7e6550e1ffa2dc99e412803c", + "EvaluationElement": "7ec6578ae5120958eb2db1745758ff379e77cb64fe77b0b2d8cc917ea0869c7e", + "Input": "00", + "Output": "527759c3d9366f277d8c6020418d96bb393ba2afb20ff90df23fb7708264e2f3ab9135e3bd69955851de4b1f9fe8a0973396719b7912ba9ee8aa7d0b5e24bcf6" + }, + { + "Batch": 1, + "Blind": "64d37aed22a27f5191de1c1d69fadb899d8862b58eb4220029e036ec4c1f6706", + "BlindedElement": "da27ef466870f5f15296299850aa088629945a17d1f5b7f5ff043f76b3c06418", + "EvaluationElement": "b4cbf5a4f1eeda5a63ce7b77c7d23f461db3fcab0dd28e4e17cecb5c90d02c25", + "Input": "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", + "Output": "f4a74c9c592497375e796aa837e907b1a045d34306a749db9f34221f7e750cb4f2a6413a6bf6fa5e19ba6348eb673934a722a7ede2e7621306d18951e7cf2c73" + } + ] + }, + { + "groupDST": "48617368546f47726f75702d4f50524656312d002d64656361663434382d5348414b45323536", + "hash": "SHAKE_256", + "identifier": "decaf448-SHAKE256", + "keyInfo": "74657374206b6579", + "mode": 0, + "seed": "a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3", + "skSm": "e8b1375371fd11ebeb224f832dcc16d371b4188951c438f751425699ed29ecc80c6c13e558ccd67634fd82eac94aa8d1f0d7fee990695d1e", + "vectors": [ + { + "Batch": 1, + "Blind": "64d37aed22a27f5191de1c1d69fadb899d8862b58eb4220029e036ec65fa3833a26e9388336361686ff1f83df55046504dfecad8549ba112", + "BlindedElement": "e0ae01c4095f08e03b19baf47ffdc19cb7d98e583160522a3c7d6a0b2111cd93a126a46b7b41b730cd7fc943d4e28e590ed33ae475885f6c", + "EvaluationElement": "50ce4e60eed006e22e7027454b5a4b8319eb2bc8ced609eb19eb3ad42fb19e06ba12d382cbe7ae342a0cad6ead0ef8f91f00bb7f0cd9c0a2", + "Input": "00", + "Output": "37d3f7922d9388a15b561de5829bbf654c4089ede89c0ce0f3f85bcdba09e382ce0ab3507e021f9e79706a1798ffeac68ebd5cf62e5eb9838c7068351d97ae37" + }, + { + "Batch": 1, + "Blind": "64d37aed22a27f5191de1c1d69fadb899d8862b58eb4220029e036ec65fa3833a26e9388336361686ff1f83df55046504dfecad8549ba112", + "BlindedElement": "86a88dc5c6331ecfcb1d9aacb50a68213803c462e377577cacc00af28e15f0ddbc2e3d716f2f39ef95f3ec1314a2c64d940a9f295d8f13bb", + "EvaluationElement": "162e9fa6e9d527c3cd734a31bf122a34dbd5bcb7bb23651f1768a7a9274cc116c03b58afa6f0dede3994a60066c76370e7328e7062fd5819", + "Input": "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", + "Output": "a2a652290055cb0f6f8637a249ee45e32ef4667db0b4c80c0a70d2a64164d01525cfdad5d870a694ec77972b9b6ec5d2596a5223e5336913f945101f0137f55e" + } + ] + }, + { + "groupDST": "48617368546f47726f75702d4f50524656312d002d503235362d534841323536", + "hash": "SHA256", + "identifier": "P256-SHA256", + "keyInfo": "74657374206b6579", + "mode": 0, + "seed": "a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3", + "skSm": "159749d750713afe245d2d39ccfaae8381c53ce92d098a9375ee70739c7ac0bf", + "vectors": [ + { + "Batch": 1, + "Blind": "3338fa65ec36e0290022b48eb562889d89dbfa691d1cde91517fa222ed7ad364", + "BlindedElement": "03723a1e5c09b8b9c18d1dcbca29e8007e95f14f4732d9346d490ffc195110368d", + "EvaluationElement": "030de02ffec47a1fd53efcdd1c6faf5bdc270912b8749e783c7ca75bb412958832", + "Input": "00", + "Output": "a0b34de5fa4c5b6da07e72af73cc507cceeb48981b97b7285fc375345fe495dd" + }, + { + "Batch": 1, + "Blind": "3338fa65ec36e0290022b48eb562889d89dbfa691d1cde91517fa222ed7ad364", + "BlindedElement": "03cc1df781f1c2240a64d1c297b3f3d16262ef5d4cf102734882675c26231b0838", + "EvaluationElement": "03a0395fe3828f2476ffcd1f4fe540e5a8489322d398be3c4e5a869db7fcb7c52c", + "Input": "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", + "Output": "c748ca6dd327f0ce85f4ae3a8cd6d4d5390bbb804c9e12dcf94f853fece3dcce" + } + ] + }, + { + "groupDST": "48617368546f47726f75702d4f50524656312d002d503338342d534841333834", + "hash": "SHA384", + "identifier": "P384-SHA384", + "keyInfo": "74657374206b6579", + "mode": 0, + "seed": "a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3", + "skSm": "dfe7ddc41a4646901184f2b432616c8ba6d452f9bcd0c4f75a5150ef2b2ed02ef40b8b92f60ae591bcabd72a6518f188", + "vectors": [ + { + "Batch": 1, + "Blind": "504650f53df8f16f6861633388936ea23338fa65ec36e0290022b48eb562889d89dbfa691d1cde91517fa222ed7ad364", + "BlindedElement": "02a36bc90e6db34096346eaf8b7bc40ee1113582155ad3797003ce614c835a874343701d3f2debbd80d97cbe45de6e5f1f", + "EvaluationElement": "03af2a4fc94770d7a7bf3187ca9cc4faf3732049eded2442ee50fbddda58b70ae2999366f72498cdbc43e6f2fc184afe30", + "Input": "00", + "Output": "ed84ad3f31a552f0456e58935fcc0a3039db42e7f356dcb32aa6d487b6b815a07d5813641fb1398c03ddab5763874357" + }, + { + "Batch": 1, + "Blind": "504650f53df8f16f6861633388936ea23338fa65ec36e0290022b48eb562889d89dbfa691d1cde91517fa222ed7ad364", + "BlindedElement": "02def6f418e3484f67a124a2ce1bfb19de7a4af568ede6a1ebb2733882510ddd43d05f2b1ab5187936a55e50a847a8b900", + "EvaluationElement": "034e9b9a2960b536f2ef47d8608b21597ba400d5abfa1825fd21c36b75f927f396bf3716c96129d1fa4a77fa1d479c8d7b", + "Input": "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", + "Output": "dd4f29da869ab9355d60617b60da0991e22aaab243a3460601e48b075859d1c526d36597326f1b985778f781a1682e75" + } + ] + }, + { + "groupDST": "48617368546f47726f75702d4f50524656312d002d503532312d534841353132", + "hash": "SHA512", + "identifier": "P521-SHA512", + "keyInfo": "74657374206b6579", + "mode": 0, + "seed": "a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3", + "skSm": "0153441b8faedb0340439036d6aed06d1217b34c42f17f8db4c5cc610a4a955d698a688831b16d0dc7713a1aa3611ec60703bffc7dc9c84e3ed673b3dbe1d5fccea6", + "vectors": [ + { + "Batch": 1, + "Blind": "00d1dccf7a51bafaf75d4a866d53d8cafe4d504650f53df8f16f6861633388936ea23338fa65ec36e0290022b48eb562889d89dbfa691d1cde91517fa222ed7ad364", + "BlindedElement": "0300e78bf846b0e1e1a3c320e353d758583cd876df56100a3a1e62bacba470fa6e0991be1be80b721c50c5fd0c672ba764457acc18c6200704e9294fbf28859d916351", + "EvaluationElement": "030166371cf827cb2fb9b581f97907121a16e2dc5d8b10ce9f0ede7f7d76a0d047657735e8ad07bcda824907b3e5479bd72cdef6b839b967ba5c58b118b84d26f2ba07", + "Input": "00", + "Output": "26232de6fff83f812adadadb6cc05d7bbeee5dca043dbb16b03488abb9981d0a1ef4351fad52dbd7e759649af393348f7b9717566c19a6b8856284d69375c809" + }, + { + "Batch": 1, + "Blind": "00d1dccf7a51bafaf75d4a866d53d8cafe4d504650f53df8f16f6861633388936ea23338fa65ec36e0290022b48eb562889d89dbfa691d1cde91517fa222ed7ad364", + "BlindedElement": "0300c28e57e74361d87e0c1874e5f7cc1cc796d61f9cad50427cf54655cdb455613368d42b27f94bf66f59f53c816db3e95e68e1b113443d66a99b3693bab88afb556b", + "EvaluationElement": "0301ad453607e12d0cc11a3359332a40c3a254eaa1afc64296528d55bed07ba322e72e22cf3bcb50570fd913cb54f7f09c17aff8787af75f6a7faf5640cbb2d9620a6e", + "Input": "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", + "Output": "ad1f76ef939042175e007738906ac0336bbd1d51e287ebaa66901abdd324ea3ffa40bfc5a68e7939c2845e0fd37a5a6e76dadb9907c6cc8579629757fd4d04ba" + } + ] + } +] \ No newline at end of file diff --git a/tests/oprf_test.go b/tests/oprf_test.go new file mode 100644 index 0000000..80f3f29 --- /dev/null +++ b/tests/oprf_test.go @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package opaque_test + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "strings" + "testing" + + group "github.com/bytemare/crypto" + + "github.com/Jan-nku/opaque/internal/encoding" + "github.com/Jan-nku/opaque/internal/oprf" + "github.com/Jan-nku/opaque/internal/tag" +) + +type oprfVector struct { + DST string `json:"groupDST"` + Hash string `json:"hash"` + KeyInfo string `json:"keyInfo"` + Seed string `json:"seed"` + SkSm string `json:"skSm"` + SuiteName string `json:"suiteName"` + SuiteID oprf.Identifier `json:"identifier"` + Vectors []testVector `json:"vectors"` + Mode byte `json:"mode"` +} + +type test struct { + Blind [][]byte + BlindedElement [][]byte + EvaluationElement [][]byte + Input [][]byte + Output [][]byte + Batch int +} + +type testVectors []oprfVector + +type testVector struct { + Blind string `json:"Blind"` + BlindedElement string `json:"BlindedElement"` + EvaluationElement string `json:"EvaluationElement"` + Input string `json:"Input"` + Output string `json:"Output"` + Batch int `json:"Batch"` +} + +func decodeBatch(nb int, in string) ([][]byte, error) { + v := strings.Split(in, ",") + if len(v) != nb { + return nil, fmt.Errorf("incoherent number of values in batch %d/%d", len(v), nb) + } + + out := make([][]byte, nb) + + for i, s := range v { + dec, err := hex.DecodeString(s) + if err != nil { + return nil, fmt.Errorf("hex decoding errored with %q", err) + } + out[i] = dec + } + + return out, nil +} + +func (tv *testVector) Decode() (*test, error) { + blind, err := decodeBatch(tv.Batch, tv.Blind) + // blind, err := hex.DecodeString(tv.Blind) + if err != nil { + return nil, fmt.Errorf(" Blind decoding errored with %q", err) + } + + blinded, err := decodeBatch(tv.Batch, tv.BlindedElement) + // blinded, err := hex.DecodeString(tv.BlindedElement) + if err != nil { + return nil, fmt.Errorf(" BlindedElement decoding errored with %q", err) + } + + evaluationElement, err := decodeBatch(tv.Batch, tv.EvaluationElement) + if err != nil { + return nil, fmt.Errorf(" EvaluationElement decoding errored with %q", err) + } + + input, err := decodeBatch(tv.Batch, tv.Input) + // input, err := hex.DecodeString(tv.Input) + if err != nil { + return nil, fmt.Errorf(" Input decoding errored with %q", err) + } + + output, err := decodeBatch(tv.Batch, tv.Output) + // output, err := hex.DecodeString(tv.Output) + if err != nil { + return nil, fmt.Errorf(" Output decoding errored with %q", err) + } + + return &test{ + Batch: tv.Batch, + Blind: blind, + BlindedElement: blinded, + EvaluationElement: evaluationElement, + Input: input, + Output: output, + }, nil +} + +func testBlind(t *testing.T, c oprf.Identifier, test *test) { + client := c.Client() + for i := 0; i < len(test.Input); i++ { + s := c.Group().NewScalar() + if err := s.Decode(test.Blind[i]); err != nil { + t.Fatal(fmt.Errorf("blind decoding to scalar in suite %v errored with %q", c, err)) + } + + blinded := client.Blind(test.Input[i], s).Encode() + + if !bytes.Equal(test.BlindedElement[i], blinded) { + t.Fatal("unexpected blinded output") + } + } +} + +func testEvaluation(t *testing.T, c oprf.Identifier, privKey *group.Scalar, test *test) { + for i := 0; i < len(test.BlindedElement); i++ { + b := c.Group().NewElement() + if err := b.Decode(test.BlindedElement[i]); err != nil { + t.Fatal(fmt.Errorf("blind decoding to element in suite %v errored with %q", c, err)) + } + + ev := c.Evaluate(privKey, b) + if !bytes.Equal(test.EvaluationElement[i], ev.Encode()) { + t.Fatal("unexpected evaluation") + } + } +} + +func testFinalization(t *testing.T, c oprf.Identifier, test *test) { + client := c.Client() + for i := 0; i < len(test.EvaluationElement); i++ { + ev := c.Group().NewElement() + if err := ev.Decode(test.EvaluationElement[i]); err != nil { + t.Fatal(fmt.Errorf("blind decoding to element in suite %v errored with %q", c, err)) + } + + s := c.Group().NewScalar() + if err := s.Decode(test.Blind[i]); err != nil { + t.Fatal(fmt.Errorf("blind decoding to scalar in suite %v errored with %q", c, err)) + } + + client.Blind(test.Input[i], s) + + output := client.Finalize(ev) + if !bytes.Equal(test.Output[i], output) { + t.Fatal("unexpected output") + } + } +} + +func getDST(prefix []byte, c oprf.Identifier) []byte { + return encoding.Concatenate(prefix, []byte(tag.OPRFVersionPrefix), []byte(c)) +} + +func (v oprfVector) test(t *testing.T) { + s, err := hex.DecodeString(v.SkSm) + if err != nil { + t.Fatalf("private key decoding errored with %q\nfor sksm %v\n", err, v.SkSm) + } + + privKey := v.SuiteID.Group().NewScalar() + if err := privKey.Decode(s); err != nil { + t.Fatal(fmt.Errorf("private key decoding to scalar in suite %v errored with %q", v.SuiteID, err)) + } + + decSeed, err := hex.DecodeString(v.Seed) + if err != nil { + t.Fatalf("decoding errored with %q\nfor seed %v\n", err, v.Seed) + } + + decKeyInfo, err := hex.DecodeString(v.KeyInfo) + if err != nil { + t.Fatalf("decoding errored with %q\nfor key info %v\n", err, v.KeyInfo) + } + + sks := v.SuiteID.DeriveKey(decSeed, decKeyInfo) + + if !sks.Subtract(privKey).IsZero() { + t.Fatalf(" DeriveKeyPair did not yield the expected key %v\n", hex.EncodeToString(sks.Encode())) + } + + dst, err := hex.DecodeString(v.DST) + if err != nil { + t.Fatalf("hex decoding errored with %q", err) + } + + dst2 := getDST([]byte(tag.OPRFPointPrefix), v.SuiteID) + if !bytes.Equal(dst, dst2) { + t.Fatalf( + "GroupDST output is not valid.\n\twant: %v\n\tgot : %v", + hex.EncodeToString(dst), + hex.EncodeToString(dst2), + ) + } + + for i, tv := range v.Vectors { + t.Run(fmt.Sprintf("Vector %d", i), func(t *testing.T) { + test, err := tv.Decode() + if err != nil { + t.Fatal(fmt.Sprintf("batches : %v Failed %v\n", tv.Batch, err)) + } + + // Test Blinding + testBlind(t, v.SuiteID, test) + + // Server evaluating + testEvaluation(t, v.SuiteID, privKey, test) + + // Client finalize + testFinalization(t, v.SuiteID, test) + }) + } +} + +func loadVOPRFVectors(filepath string) (testVectors, error) { + contents, err := os.ReadFile(filepath) + if err != nil { + return nil, err + } + + var v testVectors + errJSON := json.Unmarshal(contents, &v) + if errJSON != nil { + return nil, errJSON + } + + return v, nil +} + +func TestVOPRFVectors(t *testing.T) { + vectorFile := "oprfVectors.json" + + v, err := loadVOPRFVectors(vectorFile) + if err != nil || v == nil { + t.Fatal(err) + } + + for _, tv := range v { + if tv.Mode != 0x00 { + continue + } + + if tv.SuiteID == "decaf448-SHAKE256" { + continue + } + + t.Run(string(tv.Mode)+" - "+string(tv.SuiteID), tv.test) + } +} diff --git a/tests/server_test.go b/tests/server_test.go new file mode 100644 index 0000000..f2cffef --- /dev/null +++ b/tests/server_test.go @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package opaque_test + +import ( + "errors" + "strings" + "testing" + + "github.com/Jan-nku/opaque" + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/internal/encoding" +) + +var ( + errInvalidStateLength = errors.New("invalid state length") + errStateExists = errors.New("setting AKE state: existing state is not empty") +) + +/* + The following tests look for failing conditions. +*/ + +func TestServer_BadRegistrationRequest(t *testing.T) { + /* + Error in OPRF + - client blinded element invalid point encoding + */ + err1 := "invalid message length" + err2 := "blinded data is an invalid point" + + testAll(t, func(t2 *testing.T, conf *configuration) { + server, err := conf.conf.Server() + if err != nil { + t.Fatal(err) + } + if _, err := server.Deserialize.RegistrationRequest(nil); err == nil || !strings.HasPrefix(err.Error(), err1) { + t.Fatalf("expected error. Got %v", err) + } + + bad := getBadElement(t, conf) + if _, err := server.Deserialize.RegistrationRequest(bad); err == nil || !strings.HasPrefix(err.Error(), err2) { + t.Fatalf("expected error. Got %v", err) + } + }) +} + +func TestServerInit_InvalidPublicKey(t *testing.T) { + /* + Nil and invalid server public key + */ + testAll(t, func(t2 *testing.T, conf *configuration) { + server, err := conf.conf.Server() + if err != nil { + t.Fatal(err) + } + sk, _ := conf.conf.KeyGen() + oprfSeed := internal.RandomBytes(conf.conf.Hash.Size()) + + expected := "input server public key's length is invalid" + if err := server.SetKeyMaterial(nil, sk, nil, oprfSeed); err == nil || + !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error on nil pubkey - got %s", err) + } + + expected = "invalid server public key: " + if err := server.SetKeyMaterial(nil, sk, getBadElement(t, conf), oprfSeed); err == nil || + !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error on bad secret key - got %s", err) + } + }) +} + +func TestServerInit_InvalidOPRFSeedLength(t *testing.T) { + /* + Nil and invalid server public key + */ + testAll(t, func(t2 *testing.T, conf *configuration) { + server, err := conf.conf.Server() + if err != nil { + t.Fatal(err) + } + sk, pk := conf.conf.KeyGen() + expected := opaque.ErrInvalidOPRFSeedLength + + if err := server.SetKeyMaterial(nil, sk, pk, nil); err == nil || !errors.Is(err, expected) { + t.Fatalf("expected error on nil seed - got %s", err) + } + + seed := internal.RandomBytes(conf.conf.Hash.Size() - 1) + if err := server.SetKeyMaterial(nil, sk, pk, seed); err == nil || !errors.Is(err, expected) { + t.Fatalf("expected error on bad seed - got %s", err) + } + + seed = internal.RandomBytes(conf.conf.Hash.Size() + 1) + if err := server.SetKeyMaterial(nil, sk, pk, seed); err == nil || !errors.Is(err, expected) { + t.Fatalf("expected error on bad seed - got %s", err) + } + }) +} + +func TestServerInit_NilSecretKey(t *testing.T) { + /* + Nil server secret key + */ + testAll(t, func(t2 *testing.T, conf *configuration) { + server, err := conf.conf.Server() + if err != nil { + t.Fatal(err) + } + _, pk := conf.conf.KeyGen() + expected := "invalid server AKE secret key: " + + if err := server.SetKeyMaterial(nil, nil, pk, nil); err == nil || + !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error on nil secret key - got %s", err) + } + }) +} + +func TestServerInit_ZeroSecretKey(t *testing.T) { + /* + Nil server secret key + */ + testAll(t, func(t2 *testing.T, conf *configuration) { + server, err := conf.conf.Server() + if err != nil { + t.Fatal(err) + } + sk := [32]byte{} + expected := "server private key is zero" + + if err := server.SetKeyMaterial(nil, sk[:], nil, nil); err == nil || + !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error on nil secret key - got %s", err) + } + }) +} + +func TestServerInit_NoKeyMaterial(t *testing.T) { + /* + SetKeyMaterial has not been called or was not successful + */ + testAll(t, func(t2 *testing.T, conf *configuration) { + server, err := conf.conf.Server() + if err != nil { + t.Fatal(err) + } + expected := "key material not set: call SetKeyMaterial() to set values" + + if _, err := server.LoginInit(nil, nil); err == nil || + !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error not calling SetKeyMaterial - got %s", err) + } + }) +} + +func TestServerInit_InvalidEnvelope(t *testing.T) { + /* + Record envelope of invalid length + */ + testAll(t, func(t2 *testing.T, conf *configuration) { + server, err := conf.conf.Server() + if err != nil { + t.Fatal(err) + } + sk, pk := conf.conf.KeyGen() + oprfSeed := internal.RandomBytes(conf.conf.Hash.Size()) + + if err := server.SetKeyMaterial(nil, sk, pk, oprfSeed); err != nil { + t.Fatal(err) + } + + client, err := conf.conf.Client() + if err != nil { + t.Fatal(err) + } + rec := buildRecord(internal.RandomBytes(32), oprfSeed, []byte("yo"), pk, client, server) + rec.Envelope = internal.RandomBytes(15) + + expected := "record has invalid envelope length" + if _, err := server.LoginInit(nil, rec); err == nil || + !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error on nil secret key - got %s", err) + } + }) +} + +func TestServerInit_InvalidData(t *testing.T) { + /* + Invalid OPRF data in KE1 + */ + testAll(t, func(t2 *testing.T, conf *configuration) { + server, err := conf.conf.Server() + if err != nil { + t.Fatal(err) + } + ke1 := encoding.Concatenate( + getBadElement(t, conf), + internal.RandomBytes(server.GetConf().NonceLen), + internal.RandomBytes(server.GetConf().Group.ElementLength()), + ) + expected := "blinded data is an invalid point" + if _, err := server.Deserialize.KE1(ke1); err == nil || !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error on bad oprf request - got %s", err) + } + }) +} + +func TestServerInit_InvalidEPKU(t *testing.T) { + /* + Invalid EPKU in KE1 + */ + testAll(t, func(t2 *testing.T, conf *configuration) { + server, err := conf.conf.Server() + if err != nil { + t.Fatal(err) + } + client, err := conf.conf.Client() + if err != nil { + t.Fatal(err) + } + ke1 := client.LoginInit([]byte("yo")).Serialize() + badke1 := encoding.Concat( + ke1[:server.GetConf().OPRF.Group().ElementLength()+server.GetConf().NonceLen], + getBadElement(t, conf), + ) + expected := "invalid ephemeral client public key" + if _, err := server.Deserialize.KE1(badke1); err == nil || !strings.HasPrefix(err.Error(), expected) { + t.Fatalf("expected error on bad epku - got %s", err) + } + }) +} + +func TestServerFinish_InvalidKE3Mac(t *testing.T) { + /* + ke3 mac is invalid + */ + password := []byte("yo") + conf := opaque.DefaultConfiguration() + credId := internal.RandomBytes(32) + oprfSeed := internal.RandomBytes(conf.Hash.Size()) + client, _ := conf.Client() + server, _ := conf.Server() + sk, pk := conf.KeyGen() + if err := server.SetKeyMaterial(nil, sk, pk, oprfSeed); err != nil { + t.Fatal(err) + } + rec := buildRecord(credId, oprfSeed, password, pk, client, server) + ke1 := client.LoginInit(password) + ke2, err := server.LoginInit(ke1, rec) + if err != nil { + t.Fatal(err) + } + ke3, _, err := client.LoginFinish(ke2) + if err != nil { + t.Fatal(err) + } + ke3.Mac[0] = ^ke3.Mac[0] + + expected := opaque.ErrAkeInvalidClientMac + if err := server.LoginFinish(ke3); err == nil || err.Error() != expected.Error() { + t.Fatalf("expected error on invalid mac - got %v", err) + } +} + +func TestServerSetAKEState_InvalidInput(t *testing.T) { + conf := opaque.DefaultConfiguration() + + /* + Test an invalid state + */ + + buf := internal.RandomBytes(conf.MAC.Size() + conf.KDF.Size() + 1) + + server, _ := conf.Server() + if err := server.SetAKEState(buf); err == nil || err.Error() != errInvalidStateLength.Error() { + t.Fatalf("Expected error for SetAKEState. want %q, got %q", errInvalidStateLength, err) + } + + /* + A state already exists. + */ + password := []byte("yo") + credId := internal.RandomBytes(32) + seed := internal.RandomBytes(conf.Hash.Size()) + client, _ := conf.Client() + server, _ = conf.Server() + sk, pk := conf.KeyGen() + rec := buildRecord(credId, seed, password, pk, client, server) + ke1 := client.LoginInit(password) + _ = server.SetKeyMaterial(nil, sk, pk, seed) + _, _ = server.LoginInit(ke1, rec) + state := server.SerializeState() + if err := server.SetAKEState(state); err == nil || err.Error() != errStateExists.Error() { + t.Fatalf("Expected error for SetAKEState. want %q, got %q", errStateExists, err) + } +} diff --git a/tests/testdata/fuzz/FuzzDeserializeKE1/fuzzbuzz-a1ae7e291f3d65e6ffdc8acb9f39f3538e3e0ab4 b/tests/testdata/fuzz/FuzzDeserializeKE1/fuzzbuzz-a1ae7e291f3d65e6ffdc8acb9f39f3538e3e0ab4 new file mode 100644 index 0000000..ffb6e82 --- /dev/null +++ b/tests/testdata/fuzz/FuzzDeserializeKE1/fuzzbuzz-a1ae7e291f3d65e6ffdc8acb9f39f3538e3e0ab4 @@ -0,0 +1,9 @@ +go test fuzz v1 +[]byte("000000000x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000") +[]byte("0") +uint(7) +uint(7) +uint(6) +[]byte("\x01") +byte('\x00') +byte('\x06') diff --git a/tests/testdata/fuzz/FuzzDeserializeKE2/fuzzbuzz-a1ae7e291f3d65e6ffdc8acb9f39f3538e3e0ab4 b/tests/testdata/fuzz/FuzzDeserializeKE2/fuzzbuzz-a1ae7e291f3d65e6ffdc8acb9f39f3538e3e0ab4 new file mode 100644 index 0000000..0f75731 --- /dev/null +++ b/tests/testdata/fuzz/FuzzDeserializeKE2/fuzzbuzz-a1ae7e291f3d65e6ffdc8acb9f39f3538e3e0ab4 @@ -0,0 +1,9 @@ +go test fuzz v1 +[]byte("20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") +[]byte("0") +uint(7) +uint(7) +uint(7) +[]byte("\x01") +byte('\x00') +byte('\x06') diff --git a/tests/vectors.json b/tests/vectors.json new file mode 100644 index 0000000..26d13b9 --- /dev/null +++ b/tests/vectors.json @@ -0,0 +1,322 @@ +[ + { + "config": { + "Context": "4f50415155452d504f43", + "Fake": "False", + "Group": "ristretto255", + "Hash": "SHA512", + "KDF": "HKDF-SHA512", + "KSF": "Identity", + "MAC": "HMAC-SHA512", + "Name": "3DH", + "Nh": "64", + "Nm": "64", + "Nok": "32", + "Npk": "32", + "Nsk": "32", + "Nx": "64", + "OPRF": "ristretto255-SHA512" + }, + "inputs": { + "blind_login": "6ecc102d2e7a7cf49617aad7bbe188556792d4acd60a1a8a8d2b65d4b0790308", + "blind_registration": "76cfbfe758db884bebb33582331ba9f159720ca8784a2a070a265d9c2d6abe01", + "client_keyshare": "0c3a00c961fead8a16f818929cc976f0475e4f723519318b96f4947a7a5f9663", + "client_nonce": "da7e07376d6d6f034cfa9bb537d11b8c6b4238c334333d1f0aebb380cae6a6cc", + "client_private_keyshare": "22c919134c9bdd9dc0c5ef3450f18b54820f43f646a95223bf4a85b2018c2001", + "credential_identifier": "31323334", + "envelope_nonce": "ac13171b2f17bc2c74997f0fce1e1f35bec6b91fe2e12dbd323d23ba7a38dfec", + "masking_nonce": "38fe59af0df2c79f57b8780278f5ae47355fe1f817119041951c80f612fdfc6d", + "oprf_seed": "f433d0227b0b9dd54f7c4422b600e764e47fb503f1f9a0f0a47c6606b054a7fdc65347f1a08f277e22358bbabe26f823fca82c7848e9a75661f4ec5d5c1989ef", + "password": "436f7272656374486f72736542617474657279537461706c65", + "server_keyshare": "c8c39f573135474c51660b02425bca633e339cec4e1acc69c94dd48497fe4028", + "server_nonce": "71cd9960ecef2fe0d0f7494986fa3d8b2bb01963537e60efb13981e138e3d4a1", + "server_private_key": "47451a85372f8b3537e249d7b54188091fb18edde78094b43e2ba42b5eb89f0d", + "server_private_keyshare": "2e842960258a95e28bcfef489cffd19d8ec99cc1375d840f96936da7dbb0b40d", + "server_public_key": "b2fe7af9f48cc502d016729d2fe25cdd433f2c4bc904660b2a382c9b79df1a78" + }, + "intermediates": { + "auth_key": "6cd32316f18d72a9a927a83199fa030663a38ce0c11fbaef82aa90037730494fc555c4d49506284516edd1628c27965b7555a4ebfed2223199f6c67966dde822", + "client_mac_key": "f2d019bad603b45b2ac50376279a0a37d097723b5405aa4fb20a59f60cdbdd52ec043372cedcdbbdb634c54483e1be51a88d13a5798180acb84c10b1297069fd", + "client_public_key": "2ec892bdbf9b3e2ea834be9eb11f5d187e64ba661ec041c0a3b66db8b7d6cc30", + "envelope": "ac13171b2f17bc2c74997f0fce1e1f35bec6b91fe2e12dbd323d23ba7a38dfecb9dbe7d48cf714fc3533becab6faf60b783c94d258477eb74ecc453413bf61c53fd58f0fb3c1175410b674c02e1b59b2d729a865b709db3dc4ee2bb45703d5a8", + "handshake_secret": "562564da0d4efdc73cb6efbb454388dabfa5052d4e7e83f4d0240c5afd8352881e762755c2f1a9110e36b05fe770f0f48658489c9730dcd365e6c2d4049c8fe3", + "masking_key": "1ac5844383c7708077dea41cbefe2fa15724f449e535dd7dd562e66f5ecfb95864eadddec9db5874959905117dad40a4524111849799281fefe3c51fa82785c5", + "oprf_key": "5d4c6a8b7c7138182afb4345d1fae6a9f18a1744afbcc3854f8f5a2b4b4c6d05", + "randomized_pwd": "aac48c25ab036e30750839d31d6e73007344cb1155289fb7d329beb932e9adeea73d5d5c22a0ce1952f8aba6d66007615cd1698d4ac85ef1fcf150031d1435d9", + "server_mac_key": "59473632c53a647f9f4ab4d6c3b81e241dd9cb19ca05f0eabed7e593f0407ff57e7f060621e5e48d5291be600a1959fbecbc26d4a7157bd227a993c37b645f73" + }, + "outputs": { + "KE1": "c4dedb0ba6ed5d965d6f250fbe554cd45cba5dfcce3ce836e4aee778aa3cd44dda7e07376d6d6f034cfa9bb537d11b8c6b4238c334333d1f0aebb380cae6a6cc0c3a00c961fead8a16f818929cc976f0475e4f723519318b96f4947a7a5f9663", + "KE2": "7e308140890bcde30cbcea28b01ea1ecfbd077cff62c4def8efa075aabcbb47138fe59af0df2c79f57b8780278f5ae47355fe1f817119041951c80f612fdfc6dd6ec60bcdb26dc455ddf3e718f1020490c192d70dfc7e403981179d8073d1146a4f9aa1ced4e4cd984c657eb3b54ced3848326f70331953d91b02535af44d9fe0610f003be80cb2098357928c8ea17bb065af33095f39d4e0b53b1687f02d522d96bad4ca354293d5c401177ccbd302cf565b96c327f71bc9eaf2890675d2fbb71cd9960ecef2fe0d0f7494986fa3d8b2bb01963537e60efb13981e138e3d4a1c8c39f573135474c51660b02425bca633e339cec4e1acc69c94dd48497fe40287f33611c2cf0eef57adbf48942737d9421e6b20e4b9d6e391d4168bf4bf96ea57aa42ad41c977605e027a9ef706a349f4b2919fe3562c8e86c4eeecf2f9457d4", + "KE3": "df9a13cd256091f90f0fcb2ef6b3411e4aebff07bb0813299c0ec7f5dedd33a7681231a001a82f1dece1777921f42abfeee551ee34392e1c9743c5cc1dc1ef8c", + "export_key": "1ef15b4fa99e8a852412450ab78713aad30d21fa6966c9b8c9fb3262a970dc62950d4dd4ed62598229b1b72794fc0335199d9f7fcc6eaedde92cc04870e63f16", + "registration_request": "5059ff249eb1551b7ce4991f3336205bde44a105a032e747d21bf382e75f7a71", + "registration_response": "7408a268083e03abc7097fc05b587834539065e86fb0c7b6342fcf5e01e5b019b2fe7af9f48cc502d016729d2fe25cdd433f2c4bc904660b2a382c9b79df1a78", + "registration_upload": "2ec892bdbf9b3e2ea834be9eb11f5d187e64ba661ec041c0a3b66db8b7d6cc301ac5844383c7708077dea41cbefe2fa15724f449e535dd7dd562e66f5ecfb95864eadddec9db5874959905117dad40a4524111849799281fefe3c51fa82785c5ac13171b2f17bc2c74997f0fce1e1f35bec6b91fe2e12dbd323d23ba7a38dfecb9dbe7d48cf714fc3533becab6faf60b783c94d258477eb74ecc453413bf61c53fd58f0fb3c1175410b674c02e1b59b2d729a865b709db3dc4ee2bb45703d5a8", + "session_key": "8a0f9f4928fc0c3b5bb261c4b7b3997600405424a8128632e85a5667b4b742484ed791933971be6d3fcf2b23c56b8e8f7e7edcae19a03b8fd87f5999fce129d2" + } + }, + { + "config": { + "Context": "4f50415155452d504f43", + "Fake": "False", + "Group": "ristretto255", + "Hash": "SHA512", + "KDF": "HKDF-SHA512", + "KSF": "Identity", + "MAC": "HMAC-SHA512", + "Name": "3DH", + "Nh": "64", + "Nm": "64", + "Nok": "32", + "Npk": "32", + "Nsk": "32", + "Nx": "64", + "OPRF": "ristretto255-SHA512" + }, + "inputs": { + "blind_login": "6ecc102d2e7a7cf49617aad7bbe188556792d4acd60a1a8a8d2b65d4b0790308", + "blind_registration": "76cfbfe758db884bebb33582331ba9f159720ca8784a2a070a265d9c2d6abe01", + "client_identity": "616c696365", + "client_keyshare": "0c3a00c961fead8a16f818929cc976f0475e4f723519318b96f4947a7a5f9663", + "client_nonce": "da7e07376d6d6f034cfa9bb537d11b8c6b4238c334333d1f0aebb380cae6a6cc", + "client_private_keyshare": "22c919134c9bdd9dc0c5ef3450f18b54820f43f646a95223bf4a85b2018c2001", + "credential_identifier": "31323334", + "envelope_nonce": "ac13171b2f17bc2c74997f0fce1e1f35bec6b91fe2e12dbd323d23ba7a38dfec", + "masking_nonce": "38fe59af0df2c79f57b8780278f5ae47355fe1f817119041951c80f612fdfc6d", + "oprf_seed": "f433d0227b0b9dd54f7c4422b600e764e47fb503f1f9a0f0a47c6606b054a7fdc65347f1a08f277e22358bbabe26f823fca82c7848e9a75661f4ec5d5c1989ef", + "password": "436f7272656374486f72736542617474657279537461706c65", + "server_identity": "626f62", + "server_keyshare": "c8c39f573135474c51660b02425bca633e339cec4e1acc69c94dd48497fe4028", + "server_nonce": "71cd9960ecef2fe0d0f7494986fa3d8b2bb01963537e60efb13981e138e3d4a1", + "server_private_key": "47451a85372f8b3537e249d7b54188091fb18edde78094b43e2ba42b5eb89f0d", + "server_private_keyshare": "2e842960258a95e28bcfef489cffd19d8ec99cc1375d840f96936da7dbb0b40d", + "server_public_key": "b2fe7af9f48cc502d016729d2fe25cdd433f2c4bc904660b2a382c9b79df1a78" + }, + "intermediates": { + "auth_key": "6cd32316f18d72a9a927a83199fa030663a38ce0c11fbaef82aa90037730494fc555c4d49506284516edd1628c27965b7555a4ebfed2223199f6c67966dde822", + "client_mac_key": "156e4ab0b9f71ef994bbbb73928e6d14d7335cf9561f113d61ac6b41fab35f9c72fe827d3c4d7dd91d8398ee619810e4f9286e6b32f329eb6b1476ce18fa8500", + "client_public_key": "2ec892bdbf9b3e2ea834be9eb11f5d187e64ba661ec041c0a3b66db8b7d6cc30", + "envelope": "ac13171b2f17bc2c74997f0fce1e1f35bec6b91fe2e12dbd323d23ba7a38dfec1ac902dc5589e9a5f0de56ad685ea8486210ef41449cd4d8712828913c5d2b680b2b3af4a26c765cff329bfb66d38ecf1d6cfa9e7a73c222c6efe0d9520f7d7c", + "handshake_secret": "bc2abaa979af9cbb6859856b7d5d201a038fbdfa7e10f11d131d3f8f6fc3b263bde4db6d2d9207d4648ff80415a276d5f157f9d37a3eade559db2e5f3fa026b2", + "masking_key": "1ac5844383c7708077dea41cbefe2fa15724f449e535dd7dd562e66f5ecfb95864eadddec9db5874959905117dad40a4524111849799281fefe3c51fa82785c5", + "oprf_key": "5d4c6a8b7c7138182afb4345d1fae6a9f18a1744afbcc3854f8f5a2b4b4c6d05", + "randomized_pwd": "aac48c25ab036e30750839d31d6e73007344cb1155289fb7d329beb932e9adeea73d5d5c22a0ce1952f8aba6d66007615cd1698d4ac85ef1fcf150031d1435d9", + "server_mac_key": "2420461c589866700b08c8818cbf390c872629a14cf32a264dad3375f85f33188c8f04bdb71880b2d4613187a0e416808ab62b45858b88319882602371ef5f75" + }, + "outputs": { + "KE1": "c4dedb0ba6ed5d965d6f250fbe554cd45cba5dfcce3ce836e4aee778aa3cd44dda7e07376d6d6f034cfa9bb537d11b8c6b4238c334333d1f0aebb380cae6a6cc0c3a00c961fead8a16f818929cc976f0475e4f723519318b96f4947a7a5f9663", + "KE2": "7e308140890bcde30cbcea28b01ea1ecfbd077cff62c4def8efa075aabcbb47138fe59af0df2c79f57b8780278f5ae47355fe1f817119041951c80f612fdfc6dd6ec60bcdb26dc455ddf3e718f1020490c192d70dfc7e403981179d8073d1146a4f9aa1ced4e4cd984c657eb3b54ced3848326f70331953d91b02535af44d9fea502150b67fe36795dd8914f164e49f81c7688a38928372134b7dccd50e09f8fed9518b7b2f94835b3c4fe4c8475e7513f20eb97ff0568a39caee3fd6251876f71cd9960ecef2fe0d0f7494986fa3d8b2bb01963537e60efb13981e138e3d4a1c8c39f573135474c51660b02425bca633e339cec4e1acc69c94dd48497fe4028c463164503598ea84fab9005b9cd51b7bb3206fb22a412e8a86b9cb6ffca18f5ea6b4c24fdc94865e8bf74248e6be15b85b1604140ffad2175f9518452d381af", + "KE3": "a86ece659d90525e2476aa1756d313b067581cb7b0643b97be6b8ab8d0f1084357e514ecfaff9dc18f6cca37da630545f0048393f16bc175eb819653ebc45b60", + "export_key": "1ef15b4fa99e8a852412450ab78713aad30d21fa6966c9b8c9fb3262a970dc62950d4dd4ed62598229b1b72794fc0335199d9f7fcc6eaedde92cc04870e63f16", + "registration_request": "5059ff249eb1551b7ce4991f3336205bde44a105a032e747d21bf382e75f7a71", + "registration_response": "7408a268083e03abc7097fc05b587834539065e86fb0c7b6342fcf5e01e5b019b2fe7af9f48cc502d016729d2fe25cdd433f2c4bc904660b2a382c9b79df1a78", + "registration_upload": "2ec892bdbf9b3e2ea834be9eb11f5d187e64ba661ec041c0a3b66db8b7d6cc301ac5844383c7708077dea41cbefe2fa15724f449e535dd7dd562e66f5ecfb95864eadddec9db5874959905117dad40a4524111849799281fefe3c51fa82785c5ac13171b2f17bc2c74997f0fce1e1f35bec6b91fe2e12dbd323d23ba7a38dfec1ac902dc5589e9a5f0de56ad685ea8486210ef41449cd4d8712828913c5d2b680b2b3af4a26c765cff329bfb66d38ecf1d6cfa9e7a73c222c6efe0d9520f7d7c", + "session_key": "0968e91efeb702d6aa09023a9a79803332d8bd3442a79b8ad09490b9267161013bf475bed945238a5e976ef7d7de7ff41ae30439fe2fc39758fb3e56f2683e60" + } + }, + { + "config": { + "Context": "4f50415155452d504f43", + "Fake": "False", + "Group": "P256_XMD:SHA-256_SSWU_RO_", + "Hash": "SHA256", + "KDF": "HKDF-SHA256", + "KSF": "Identity", + "MAC": "HMAC-SHA256", + "Name": "3DH", + "Nh": "32", + "Nm": "32", + "Nok": "32", + "Npk": "33", + "Nsk": "32", + "Nx": "32", + "OPRF": "P256-SHA256" + }, + "inputs": { + "blind_login": "c497fddf6056d241e6cf9fb7ac37c384f49b357a221eb0a802c989b9942256c1", + "blind_registration": "411bf1a62d119afe30df682b91a0a33d777972d4f2daa4b34ca527d597078153", + "client_keyshare": "03493f36ca12467d1f5eaaabea67ca31377c4869c1e9a62346b6f01a991624b95d", + "client_nonce": "ab3d33bde0e93eda72392346a7a73051110674bbf6b1b7ffab8be4f91fdaeeb1", + "client_private_keyshare": "89d5a7e18567f255748a86beac13913df755a5adf776d69e143147b545d22134", + "credential_identifier": "31323334", + "envelope_nonce": "a921f2a014513bd8a90e477a629794e89fec12d12206dde662ebdcf65670e51f", + "masking_nonce": "38fe59af0df2c79f57b8780278f5ae47355fe1f817119041951c80f612fdfc6d", + "oprf_seed": "62f60b286d20ce4fd1d64809b0021dad6ed5d52a2c8cf27ae6582543a0a8dce2", + "password": "436f7272656374486f72736542617474657279537461706c65", + "server_keyshare": "020e67941e94deba835214421d2d8c90de9b0f7f925d11e2032ce19b1832ae8e0f", + "server_nonce": "71cd9960ecef2fe0d0f7494986fa3d8b2bb01963537e60efb13981e138e3d4a1", + "server_private_key": "c36139381df63bfc91c850db0b9cfbec7a62e86d80040a41aa7725bf0e79d5e5", + "server_private_keyshare": "9addab838c920fa7044f3a46b91ecaea24b0e72039928ee7d4c37a5b9bc17349", + "server_public_key": "035f40ff9cf88aa1f5cd4fe5fd3da9ea65a4923a5594f84fd9f2092d6067784874" + }, + "intermediates": { + "auth_key": "5bd4be1602516092dc5078f8d699f5721dc1720a49fb80d8e5c16377abd0987b", + "client_mac_key": "7329ffd54df21db5532fce8794fca78b505fef9397aad28a424f6ea3f97c51ca", + "client_public_key": "02dc91b178ba2c4bbf9b9403fca25457b906a7f507e59b6e703031e09114ba2be0", + "envelope": "a921f2a014513bd8a90e477a629794e89fec12d12206dde662ebdcf65670e51fe155412cb432898eda63529c3b2633521f770cccbd25d7548a4e20665a45e65a", + "handshake_secret": "c59197dd9269abfdb3037ea1c203a97627e2c0aa142000d1c3f06a2c8713077d", + "masking_key": "7f0ed53532d3ae8e505ecc70d42d2b814b6b0e48156def71ea029148b2803aaf", + "oprf_key": "2dfb5cb9aa1476093be74ca0d43e5b02862a05f5d6972614d7433acdc66f7f31", + "randomized_pwd": "06be0a1a51d56557a3adad57ba29c5510565dcd8b5078fa319151b9382258fb0", + "server_mac_key": "a431a5c1d3cb5772cbc66af0c2851e23dd9ad153a0c8b99081c7d0d543173fde" + }, + "outputs": { + "KE1": "037342f0bcb3ecea754c1e67576c86aa90c1de3875f390ad599a26686cdfee6e07ab3d33bde0e93eda72392346a7a73051110674bbf6b1b7ffab8be4f91fdaeeb103493f36ca12467d1f5eaaabea67ca31377c4869c1e9a62346b6f01a991624b95d", + "KE2": "0246da9fe4d41d5ba69faa6c509a1d5bafd49a48615a47a8dd4b0823cc1476481138fe59af0df2c79f57b8780278f5ae47355fe1f817119041951c80f612fdfc6d2f0c547f70deaeca54d878c14c1aa5e1ab405dec833777132eea905c2fbb12504a67dcbe0e66740c76b62c13b04a38a77926e19072953319ec65e41f9bfd2ae2687bd3348bfe33cb0bb9864fdb3b307f7dd68a17f3f150074a0bfc830ab889717d71cd9960ecef2fe0d0f7494986fa3d8b2bb01963537e60efb13981e138e3d4a1020e67941e94deba835214421d2d8c90de9b0f7f925d11e2032ce19b1832ae8e0fb5166145361a2c344d9737dd5c826fede3bbfafa418ad379ce4fa65fbb15db6e", + "KE3": "272d04758b2b436bf0239ba7b9bd0a1686a9b6542ceaaf08732054beda956498", + "export_key": "c3c9a1b0e33ac84dd83d0b7e8af6794e17e7a3caadff289fbd9dc769a853c64b", + "registration_request": "029e949a29cfa0bf7c1287333d2fb3dc586c41aa652f5070d26a5315a1b50229f8", + "registration_response": "0350d3694c00978f00a5ce7cd08a00547e4ab5fb5fc2b2f6717cdaa6c89136efef035f40ff9cf88aa1f5cd4fe5fd3da9ea65a4923a5594f84fd9f2092d6067784874", + "registration_upload": "02dc91b178ba2c4bbf9b9403fca25457b906a7f507e59b6e703031e09114ba2be07f0ed53532d3ae8e505ecc70d42d2b814b6b0e48156def71ea029148b2803aafa921f2a014513bd8a90e477a629794e89fec12d12206dde662ebdcf65670e51fe155412cb432898eda63529c3b2633521f770cccbd25d7548a4e20665a45e65a", + "session_key": "a224790a010afc0a3f37e23c1b7a5cb7f9e73e3d9a924116510d97d80e2a1e0c" + } + }, + { + "config": { + "Context": "4f50415155452d504f43", + "Fake": "False", + "Group": "P256_XMD:SHA-256_SSWU_RO_", + "Hash": "SHA256", + "KDF": "HKDF-SHA256", + "KSF": "Identity", + "MAC": "HMAC-SHA256", + "Name": "3DH", + "Nh": "32", + "Nm": "32", + "Nok": "32", + "Npk": "33", + "Nsk": "32", + "Nx": "32", + "OPRF": "P256-SHA256" + }, + "inputs": { + "blind_login": "c497fddf6056d241e6cf9fb7ac37c384f49b357a221eb0a802c989b9942256c1", + "blind_registration": "411bf1a62d119afe30df682b91a0a33d777972d4f2daa4b34ca527d597078153", + "client_identity": "616c696365", + "client_keyshare": "03493f36ca12467d1f5eaaabea67ca31377c4869c1e9a62346b6f01a991624b95d", + "client_nonce": "ab3d33bde0e93eda72392346a7a73051110674bbf6b1b7ffab8be4f91fdaeeb1", + "client_private_keyshare": "89d5a7e18567f255748a86beac13913df755a5adf776d69e143147b545d22134", + "credential_identifier": "31323334", + "envelope_nonce": "a921f2a014513bd8a90e477a629794e89fec12d12206dde662ebdcf65670e51f", + "masking_nonce": "38fe59af0df2c79f57b8780278f5ae47355fe1f817119041951c80f612fdfc6d", + "oprf_seed": "62f60b286d20ce4fd1d64809b0021dad6ed5d52a2c8cf27ae6582543a0a8dce2", + "password": "436f7272656374486f72736542617474657279537461706c65", + "server_identity": "626f62", + "server_keyshare": "020e67941e94deba835214421d2d8c90de9b0f7f925d11e2032ce19b1832ae8e0f", + "server_nonce": "71cd9960ecef2fe0d0f7494986fa3d8b2bb01963537e60efb13981e138e3d4a1", + "server_private_key": "c36139381df63bfc91c850db0b9cfbec7a62e86d80040a41aa7725bf0e79d5e5", + "server_private_keyshare": "9addab838c920fa7044f3a46b91ecaea24b0e72039928ee7d4c37a5b9bc17349", + "server_public_key": "035f40ff9cf88aa1f5cd4fe5fd3da9ea65a4923a5594f84fd9f2092d6067784874" + }, + "intermediates": { + "auth_key": "5bd4be1602516092dc5078f8d699f5721dc1720a49fb80d8e5c16377abd0987b", + "client_mac_key": "e48e2064cf570dbd18eb42550d4459c58ac4ae4e28881d1aefbabd668f7f1df9", + "client_public_key": "02dc91b178ba2c4bbf9b9403fca25457b906a7f507e59b6e703031e09114ba2be0", + "envelope": "a921f2a014513bd8a90e477a629794e89fec12d12206dde662ebdcf65670e51f4d7773a36a208a866301dbb2858e40dc5638017527cf91aef32d3848eebe0971", + "handshake_secret": "0ee4a82c4a34992f72bfbcb5d2ce64044477dfe200b9d8c92bf1759b219b3485", + "masking_key": "7f0ed53532d3ae8e505ecc70d42d2b814b6b0e48156def71ea029148b2803aaf", + "oprf_key": "2dfb5cb9aa1476093be74ca0d43e5b02862a05f5d6972614d7433acdc66f7f31", + "randomized_pwd": "06be0a1a51d56557a3adad57ba29c5510565dcd8b5078fa319151b9382258fb0", + "server_mac_key": "77ebd7511216a51e9c2f3368ce6c1e40513f24b6f42085ef18e7f737b427aab5" + }, + "outputs": { + "KE1": "037342f0bcb3ecea754c1e67576c86aa90c1de3875f390ad599a26686cdfee6e07ab3d33bde0e93eda72392346a7a73051110674bbf6b1b7ffab8be4f91fdaeeb103493f36ca12467d1f5eaaabea67ca31377c4869c1e9a62346b6f01a991624b95d", + "KE2": "0246da9fe4d41d5ba69faa6c509a1d5bafd49a48615a47a8dd4b0823cc1476481138fe59af0df2c79f57b8780278f5ae47355fe1f817119041951c80f612fdfc6d2f0c547f70deaeca54d878c14c1aa5e1ab405dec833777132eea905c2fbb12504a67dcbe0e66740c76b62c13b04a38a77926e19072953319ec65e41f9bfd2ae268d7f106042021c80300e4c6f585980cf39fc51a4a6bba41b0729f9b240c729e5671cd9960ecef2fe0d0f7494986fa3d8b2bb01963537e60efb13981e138e3d4a1020e67941e94deba835214421d2d8c90de9b0f7f925d11e2032ce19b1832ae8e0fdca637d2a5390f4c809a67b46977c536fe9f643f703178a17a413d14e4bb523c", + "KE3": "298cd0077d018f122bc95d706e5fef06537814c567f08d5e40b0c0ae918f9287", + "export_key": "c3c9a1b0e33ac84dd83d0b7e8af6794e17e7a3caadff289fbd9dc769a853c64b", + "registration_request": "029e949a29cfa0bf7c1287333d2fb3dc586c41aa652f5070d26a5315a1b50229f8", + "registration_response": "0350d3694c00978f00a5ce7cd08a00547e4ab5fb5fc2b2f6717cdaa6c89136efef035f40ff9cf88aa1f5cd4fe5fd3da9ea65a4923a5594f84fd9f2092d6067784874", + "registration_upload": "02dc91b178ba2c4bbf9b9403fca25457b906a7f507e59b6e703031e09114ba2be07f0ed53532d3ae8e505ecc70d42d2b814b6b0e48156def71ea029148b2803aafa921f2a014513bd8a90e477a629794e89fec12d12206dde662ebdcf65670e51f4d7773a36a208a866301dbb2858e40dc5638017527cf91aef32d3848eebe0971", + "session_key": "0c59872e9bcdde274f4f52f6ba0fd1acca211d6eb7db98677b457a739ef1f0d8" + } + }, + { + "config": { + "Context": "4f50415155452d504f43", + "Fake": "True", + "Group": "ristretto255", + "Hash": "SHA512", + "KDF": "HKDF-SHA512", + "KSF": "Identity", + "MAC": "HMAC-SHA512", + "Name": "3DH", + "Nh": "64", + "Nm": "64", + "Nok": "32", + "Npk": "32", + "Nsk": "32", + "Nx": "64", + "OPRF": "ristretto255-SHA512" + }, + "inputs": { + "blind_login": "941e66e98aa88cb92cdd615829f11b4b7dc174f073c5b864c79a6efd7a284806", + "client_identity": "616c696365", + "client_keyshare": "0e4ed8bcc15f3dd01a30365c97c0c0de0a3dd3fbf5d3cbec55fb6ac1d3bf740f", + "client_nonce": "42d4e61ed3f8d64cdd3b9d153343eca15b9b0d5e388232793c6376bd2d9cfd0a", + "client_private_key": "2b98980aa95ab53a0f39f0291903d2fdf04b00c167f0814169922df873002409", + "client_private_keyshare": "bb9b7b2955983320deaaf92b374ed74caace85246ccac62d3362ec76c462740d", + "client_public_key": "84f43f9492e19c22d8bdaa4447cc3d4db1cdb5427a9f852c4707921212c36251", + "credential_identifier": "31323334", + "masking_key": "39ebd51f0e39a07a1c2d2431995b0399bca9996c5d10014d6ebab4453dc10ce5cef38ed3df6e56bfff40c2d8dd4671c2b4cf63c3d54860f31fe40220d690bb71", + "masking_nonce": "9c035896a043e70f897d87180c543e7a063b83c1bb728fbd189c619e27b6e5a6", + "oprf_seed": "743fc168d1f826ad43738933e5adb23da6fb95f95a1b069f0daa0522d0a78b617f701fc6aa46d3e7981e70de7765dfcd6b1e13e3369a582eb8dc456b10aa53b0", + "password": "436f7272656374486f72736542617474657279537461706c65", + "server_identity": "626f62", + "server_keyshare": "5236e2e06d49f0b496db2a786f6ee1016f15b4fd6c0dbd95d6b117055d914157", + "server_nonce": "1e10f6eeab2a7a420bf09da9b27a4639645622c46358de9cf7ae813055ae2d12", + "server_private_key": "c788585ae8b5ba2942b693b849be0c0426384e41977c18d2e81fbe30fd7c9f06", + "server_private_keyshare": "6d8fba9741a357584770f85294430bce2252fe212a8a372152a73c7ffe414503", + "server_public_key": "825f832667480f08b0c9069da5083ac4d0e9ee31b49c4e0310031fea04d52966" + }, + "intermediates": {}, + "outputs": { + "KE1": "b0a26dcaca2230b8f5e4b1bcab9c84b586140221bb8b2848486874b0be44890542d4e61ed3f8d64cdd3b9d153343eca15b9b0d5e388232793c6376bd2d9cfd0a0e4ed8bcc15f3dd01a30365c97c0c0de0a3dd3fbf5d3cbec55fb6ac1d3bf740f", + "KE2": "928f79ad8df21963e91411b9f55165ba833dea918f441db967cdc09521d229259c035896a043e70f897d87180c543e7a063b83c1bb728fbd189c619e27b6e5a632b5ab1bff96636144faa4f9f9afaac75dd88ea99cf5175902ae3f3b2195693f165f11929ba510a5978e64dcdabecbd7ee1e4380ce270e58fea58e6462d92964a1aaef72698bca1c673baeb04cc2bf7de5f3c2f5553464552d3a0f7698a9ca7f9c5e70c6cb1f706b2f175ab9d04bbd13926e816b6811a50b4aafa9799d5ed7971e10f6eeab2a7a420bf09da9b27a4639645622c46358de9cf7ae813055ae2d125236e2e06d49f0b496db2a786f6ee1016f15b4fd6c0dbd95d6b117055d914157cb5e11625c701e642293ad32bfcf88da653c9b6e71efc8a89607fd46ed5e7b9bf7cc7dbb997a4fd41194a04bcd0c5d88052e080a2f02c68d8d9e9c0ce15c92ff" + } + }, + { + "config": { + "Context": "4f50415155452d504f43", + "Fake": "True", + "Group": "P256_XMD:SHA-256_SSWU_RO_", + "Hash": "SHA256", + "KDF": "HKDF-SHA256", + "KSF": "Identity", + "MAC": "HMAC-SHA256", + "Name": "3DH", + "Nh": "32", + "Nm": "32", + "Nok": "32", + "Npk": "33", + "Nsk": "32", + "Nx": "32", + "OPRF": "P256-SHA256" + }, + "inputs": { + "blind_login": "324143d1fd6e9ac764b8c573f074c17d4b1bf1295861dd2cb98ca88ae9661e94", + "client_identity": "616c696365", + "client_keyshare": "03994d4f1221bfd205063469e92ea4d492f7cc76a327223633ab74590c30cf7285", + "client_nonce": "42d4e61ed3f8d64cdd3b9d153343eca15b9b0d5e388232793c6376bd2d9cfd0a", + "client_private_key": "d423b87899fc61d014fc8330a4e26190fcfa470a3afe5924324294af7dbbc1dd", + "client_private_keyshare": "a270dc715dc2b4612bc7864312a05c3e9788ee1bad1f276d1e15bdeb4c355e95", + "client_public_key": "03b81708eae026a9370616c22e1e8542fe9dbebd36ce8a2661b708e9628f4a57fc", + "credential_identifier": "31323334", + "masking_key": "caecc6ccb4cae27cb54d8f3a1af1bac52a3d53107ce08497cdd362b1992e4e5e", + "masking_nonce": "9c035896a043e70f897d87180c543e7a063b83c1bb728fbd189c619e27b6e5a6", + "oprf_seed": "bb1cd59e16ac09bc0cb6d528541695d7eba2239b1613a3db3ade77b36280f725", + "password": "436f7272656374486f72736542617474657279537461706c65", + "server_identity": "626f62", + "server_keyshare": "03f42965d5bcba2a590a49eb2418061effe40b5c29a34b8e5163e0ef32044b2e4c", + "server_nonce": "1e10f6eeab2a7a420bf09da9b27a4639645622c46358de9cf7ae813055ae2d12", + "server_private_key": "34fbe7e830be1fe8d2187c97414e3826040cbe49b893b64229bab5e85a5888c7", + "server_private_keyshare": "1a2a0ff27f3ca75221378a2a21fe5222ce0b439452f870475857a34197ba8f6d", + "server_public_key": "0221e034c0e202fe883dcfc96802a7624166fed4cfcab4ae30cf5f3290d01c88bf" + }, + "intermediates": {}, + "outputs": { + "KE1": "0396875da2b4f7749bba411513aea02dc514a48d169d8a9531bd61d3af3fa9baae42d4e61ed3f8d64cdd3b9d153343eca15b9b0d5e388232793c6376bd2d9cfd0a03994d4f1221bfd205063469e92ea4d492f7cc76a327223633ab74590c30cf7285", + "KE2": "0201198dcd13f9792eb75dcfa815f61b049abfe2e3e9456d4bbbceec5f442efd049c035896a043e70f897d87180c543e7a063b83c1bb728fbd189c619e27b6e5a6facda65ce0a97b9085e7af07f61fd3fdd046d257cbf2183ce8766090b8041a8bf28d79dd4c9031ddc75bb6ddb4c291e639937840e3d39fc0d5a3d6e7723c09f7945df485bcf9aefe3fe82d149e84049e259bb5b33d6a2ff3b25e4bfb7eff0962821e10f6eeab2a7a420bf09da9b27a4639645622c46358de9cf7ae813055ae2d1203f42965d5bcba2a590a49eb2418061effe40b5c29a34b8e5163e0ef32044b2e4c196137813ed8ec48627f0b0d90d9427f4ec137f8360769df167c25836eae5d91" + } + } +] \ No newline at end of file diff --git a/tests/vectors_test.go b/tests/vectors_test.go new file mode 100644 index 0000000..737682f --- /dev/null +++ b/tests/vectors_test.go @@ -0,0 +1,534 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2020-2022 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package opaque_test + +import ( + "bytes" + "crypto" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "strings" + "testing" + + "github.com/bytemare/hash" + "github.com/bytemare/ksf" + + "github.com/Jan-nku/opaque" + "github.com/Jan-nku/opaque/internal" + "github.com/Jan-nku/opaque/internal/encoding" + "github.com/Jan-nku/opaque/internal/oprf" +) + +type ByteToHex []byte + +func (j ByteToHex) MarshalJSON() ([]byte, error) { + return json.Marshal(hex.EncodeToString(j)) +} + +func (j *ByteToHex) UnmarshalJSON(b []byte) error { + bs := strings.Trim(string(b), "\"") + + dst, err := hex.DecodeString(bs) + if err != nil { + return err + } + + *j = dst + return nil +} + +/* + Test test vectors +*/ + +type config struct { + Fake string `json:"Fake"` + Group string `json:"Group"` + Hash string `json:"Hash"` + KDF string `json:"KDF"` + MAC string `json:"MAC"` + KSF string `json:"KSF"` + Name string `json:"Name"` + OPRF oprf.Identifier `json:"OPRF"` + Context ByteToHex `json:"Context"` +} + +type inputs struct { + BlindLogin ByteToHex `json:"blind_login"` + BlindRegistration ByteToHex `json:"blind_registration"` + ClientIdentity ByteToHex `json:"client_identity,omitempty"` + Context ByteToHex `json:"context"` + ClientKeyshare ByteToHex `json:"client_keyshare"` + ClientNonce ByteToHex `json:"client_nonce"` + ClientPrivateKeyshare ByteToHex `json:"client_private_keyshare"` + CredentialIdentifier ByteToHex `json:"credential_identifier"` + EnvelopeNonce ByteToHex `json:"envelope_nonce"` + MaskingNonce ByteToHex `json:"masking_nonce"` + OprfKey ByteToHex `json:"oprf_key"` + OprfSeed ByteToHex `json:"oprf_seed"` + Password ByteToHex `json:"password"` + ServerIdentity ByteToHex `json:"server_identity,omitempty"` + ServerKeyshare ByteToHex `json:"server_keyshare"` + ServerNonce ByteToHex `json:"server_nonce"` + ServerPrivateKey ByteToHex `json:"server_private_key"` + ServerPrivateKeyshare ByteToHex `json:"server_private_keyshare"` + ServerPublicKey ByteToHex `json:"server_public_key"` + ClientPublicKey ByteToHex `json:"client_public_key"` // Used for fake credentials tests + MaskingKey ByteToHex `json:"masking_key"` // Used for fake credentials tests +} + +type intermediates struct { + AuthKey ByteToHex `json:"auth_key"` // + ClientMacKey ByteToHex `json:"client_mac_key"` // + ClientPublicKey ByteToHex `json:"client_public_key"` + Envelope ByteToHex `json:"envelope"` // + HandshakeSecret ByteToHex `json:"handshake_secret"` // + MaskingKey ByteToHex `json:"masking_key"` + RandomPWD ByteToHex `json:"randomized_pwd"` // + ServerMacKey ByteToHex `json:"server_mac_key"` // +} + +type outputs struct { + KE1 ByteToHex `json:"KE1"` // + KE2 ByteToHex `json:"KE2"` // + KE3 ByteToHex `json:"KE3"` // + ExportKey ByteToHex `json:"export_key"` // + RegistrationRequest ByteToHex `json:"registration_request"` // + RegistrationResponse ByteToHex `json:"registration_response"` // + RegistrationRecord ByteToHex `json:"registration_upload"` // + SessionKey ByteToHex `json:"session_key"` // +} + +type vector struct { + Config config `json:"config"` + Inputs inputs `json:"inputs"` + Intermediates intermediates `json:"intermediates"` + Outputs outputs `json:"outputs"` +} + +func (v *vector) testRegistration(conf *opaque.Configuration, t *testing.T) { + // Client + client, _ := conf.Client() + + g := conf.OPRF.Group() + blind := g.NewScalar() + if err := blind.Decode(v.Inputs.BlindRegistration); err != nil { + panic(err) + } + + regReq := client.RegistrationInit(v.Inputs.Password, opaque.ClientRegistrationInitOptions{OPRFBlind: blind}) + + if !bytes.Equal(v.Outputs.RegistrationRequest, regReq.Serialize()) { + t.Fatalf( + "registration requests do not match\nwant: %v\ngot : %v", + hex.EncodeToString(v.Outputs.RegistrationRequest), + hex.EncodeToString(regReq.Serialize()), + ) + } + + // Server + server, _ := conf.Server() + pks, err := server.Deserialize.DecodeAkePublicKey(v.Inputs.ServerPublicKey) + if err != nil { + panic(err) + } + + regResp := server.RegistrationResponse(regReq, pks, v.Inputs.CredentialIdentifier, v.Inputs.OprfSeed) + + vRegResp, err := client.Deserialize.RegistrationResponse(v.Outputs.RegistrationResponse) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(vRegResp.EvaluatedMessage.Encode(), regResp.EvaluatedMessage.Encode()) { + t.Logf("%v\n%v", vRegResp.EvaluatedMessage.Encode(), regResp.EvaluatedMessage.Encode()) + t.Fatal("registration response data do not match") + } + + if !bytes.Equal(vRegResp.Pks.Encode(), regResp.Pks.Encode()) { + t.Fatal("registration response serverPublicKey do not match") + } + + if !bytes.Equal(v.Outputs.RegistrationResponse, regResp.Serialize()) { + t.Fatal("registration responses do not match") + } + + // Client + upload, exportKey := client.RegistrationFinalize( + regResp, + opaque.ClientRegistrationFinalizeOptions{ + ClientIdentity: v.Inputs.ClientIdentity, + ServerIdentity: v.Inputs.ServerIdentity, + EnvelopeNonce: v.Inputs.EnvelopeNonce, + }, + ) + + if !bytes.Equal(v.Outputs.ExportKey, exportKey) { + t.Fatalf("exportKey do not match\nexpected %v,\ngot %v", v.Outputs.ExportKey, exportKey) + } + + if !bytes.Equal(v.Intermediates.Envelope, upload.Envelope) { + t.Fatalf("envelopes do not match\nexpected %v,\ngot %v", v.Intermediates.Envelope, upload.Envelope) + } + + if !bytes.Equal(v.Outputs.RegistrationRecord, upload.Serialize()) { + t.Fatalf("registration upload do not match") + } +} + +func getFakeEnvelope(c *opaque.Configuration) []byte { + if !hash.Hashing(c.MAC).Available() { + panic(nil) + } + + envelopeSize := internal.NonceLength + internal.NewMac(c.MAC).Size() + + return make([]byte, envelopeSize) +} + +func (v *vector) testLogin(conf *opaque.Configuration, t *testing.T) { + // Client + client, _ := conf.Client() + + if !isFake(v.Config.Fake) { + g := conf.OPRF.Group() + blind := g.NewScalar() + if err := blind.Decode(v.Inputs.BlindLogin); err != nil { + panic(err) + } + + esk, err := client.Deserialize.DecodeAkePrivateKey(v.Inputs.ClientPrivateKeyshare) + if err != nil { + t.Fatal(err) + } + + KE1 := client.LoginInit(v.Inputs.Password, opaque.ClientLoginInitOptions{ + Blind: blind, + EphemeralSecretKey: esk, + Nonce: v.Inputs.ClientNonce, + NonceLength: internal.NonceLength, + }) + + if !bytes.Equal(v.Outputs.KE1, KE1.Serialize()) { + t.Fatalf("KE1 do not match") + } + } + + // Server + server, _ := conf.Server() + + record := &opaque.ClientRecord{} + if !isFake(v.Config.Fake) { + upload, err := server.Deserialize.RegistrationRecord(v.Outputs.RegistrationRecord) + if err != nil { + t.Fatal(err) + } + + record.RegistrationRecord = upload + } else { + rec, err := server.Deserialize.RegistrationRecord(encoding.Concat3(v.Inputs.ClientPublicKey, v.Inputs.MaskingKey, getFakeEnvelope(conf))) + if err != nil { + t.Fatal(err) + } + + record.RegistrationRecord = rec + } + + record.CredentialIdentifier = v.Inputs.CredentialIdentifier + record.ClientIdentity = v.Inputs.ClientIdentity + record.TestMaskNonce = v.Inputs.MaskingNonce + + v.loginResponse(t, server, record) + + if isFake(v.Config.Fake) { + return + } + + // Client + cke2, err := client.Deserialize.KE2(v.Outputs.KE2) + if err != nil { + t.Fatal(err) + } + + ke3, exportKey, err := client.LoginFinish( + cke2, + opaque.ClientLoginFinishOptions{ + ClientIdentity: v.Inputs.ClientIdentity, + ServerIdentity: v.Inputs.ServerIdentity, + }, + ) + if err != nil { + t.Fatal(err) + } + + //if !bytes.Equal(v.Intermediates.ClientMacKey, client.Ake.ClientMacKey) { + // t.Fatal("client mac keys do not match") + //} + + if !bytes.Equal(v.Outputs.ExportKey, exportKey) { + t.Fatal("Client export keys do not match") + } + + if !bytes.Equal(v.Outputs.SessionKey, client.SessionKey()) { + t.Fatal("Client session keys do not match") + } + + if !bytes.Equal(v.Outputs.KE3, ke3.Serialize()) { + t.Fatal("KE3 do not match") + } + + if err := server.LoginFinish(ke3); err != nil { + t.Fatal(err) + } + + if !bytes.Equal(v.Outputs.SessionKey, server.SessionKey()) { + t.Fatal("Server session keys do not match") + } +} + +func oprfToGroup(oprf oprf.Identifier) opaque.Group { + switch oprf { + case "ristretto255-SHA512": + return opaque.RistrettoSha512 + case "P256-SHA256": + return opaque.P256Sha256 + default: + return 0 + } +} + +func (v *vector) test(t *testing.T) { + p := &opaque.Configuration{ + OPRF: oprfToGroup(v.Config.OPRF), + Hash: hashToHash(v.Config.Hash), + KDF: kdfToHash(v.Config.KDF), + MAC: macToHash(v.Config.MAC), + KSF: ksfToKSF(v.Config.KSF), + AKE: groupToGroup(v.Config.Group), + Context: []byte(v.Config.Context), + } + + // Registration + if !isFake(v.Config.Fake) { + v.testRegistration(p, t) + } + + // Login + v.testLogin(p, t) +} + +func (v *vector) loginResponse(t *testing.T, s *opaque.Server, record *opaque.ClientRecord) { + sks, err := s.Deserialize.DecodeAkePrivateKey(v.Inputs.ServerPrivateKeyshare) + if err != nil { + t.Fatal(err) + } + + ke1, err := s.Deserialize.KE1(v.Outputs.KE1) + if err != nil { + t.Fatal(err) + } + + if err := s.SetKeyMaterial( + v.Inputs.ServerIdentity, + v.Inputs.ServerPrivateKey, + v.Inputs.ServerPublicKey, + v.Inputs.OprfSeed); err != nil { + t.Fatal(err) + } + + ke2, err := s.LoginInit( + ke1, + record, + opaque.ServerLoginInitOptions{ + EphemeralSecretKey: sks, + Nonce: v.Inputs.ServerNonce, + NonceLength: internal.NonceLength, + }, + ) + if err != nil { + t.Fatal(err) + } + + //if !bytes.Equal(v.Intermediates.HandshakeSecret, s.Ake.HandshakeSecret) { + // t.Fatalf("HandshakeSecrets do not match : %v", s.Ake.HandshakeSecret) + //} + + //if !bytes.Equal(v.Intermediates.ServerMacKey, s.Ake.ServerMacKey) { + // t.Fatalf("ServerMacs do not match.expected %v,\ngot %v", v.Intermediates.ServerMacKey, s.Ake.ServerMacKey) + //} + + //if !bytes.Equal(v.Intermediates.ClientMacKey, s.Ake.Keys.ClientMacKey) { + // t.Fatal("ClientMacs do not match") + //} + + if !isFake(v.Config.Fake) { + vectorKE3, err := s.Deserialize.KE3(v.Outputs.KE3) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(vectorKE3.Mac, s.ExpectedMAC()) { + t.Fatalf("Expected client MACs do not match : %v", s.ExpectedMAC()) + } + + if !bytes.Equal(v.Outputs.SessionKey, s.SessionKey()) { + t.Fatalf("Server's session key is invalid : %v", v.Outputs.SessionKey) + } + } + + vectorKE2, err := s.Deserialize.KE2(v.Outputs.KE2) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal( + vectorKE2.CredentialResponse.EvaluatedMessage.Encode(), + ke2.CredentialResponse.EvaluatedMessage.Encode(), + ) { + t.Fatal("data do not match") + } + + if !bytes.Equal(vectorKE2.CredentialResponse.MaskingNonce, ke2.CredentialResponse.MaskingNonce) { + t.Fatal("serverPublicKey do not match") + } + + if !bytes.Equal(vectorKE2.CredentialResponse.MaskedResponse, ke2.CredentialResponse.MaskedResponse) { + t.Fatal("MaskedResponse do not match") + } + + if !bytes.Equal(vectorKE2.CredentialResponse.Serialize(), ke2.CredentialResponse.Serialize()) { + t.Fatal("CredResp do not match") + } + + if !bytes.Equal(vectorKE2.NonceS, ke2.NonceS) { + t.Fatal("nonces do not match") + } + + if !bytes.Equal(vectorKE2.EpkS.Encode(), ke2.EpkS.Encode()) { + t.Fatal("epks do not match") + } + + if !bytes.Equal(vectorKE2.Mac, ke2.Mac) { + t.Fatalf("server macs do not match") + } + + if !bytes.Equal(v.Outputs.KE2, ke2.Serialize()) { + t.Fatalf("KE2 do not match") + } + + if !isFake(v.Config.Fake) && !bytes.Equal(v.Outputs.SessionKey, s.Ake.SessionKey()) { + t.Fatalf("Server SessionKey do not match:\n%v\n%v", v.Outputs.SessionKey, s.Ake.SessionKey()) + } +} + +func isFake(f string) bool { + switch f { + case "True": + return true + case "False": + return false + default: + panic("'Fake' parameter not recognised") + } +} + +func hashToHash(h string) crypto.Hash { + switch h { + case "SHA256": + return crypto.SHA256 + case "SHA512": + return crypto.SHA512 + default: + return 0 + } +} + +func kdfToHash(h string) crypto.Hash { + switch h { + case "HKDF-SHA256": + return crypto.SHA256 + case "HKDF-SHA512": + return crypto.SHA512 + default: + return 0 + } +} + +func macToHash(h string) crypto.Hash { + switch h { + case "HMAC-SHA256": + return crypto.SHA256 + case "HMAC-SHA512": + return crypto.SHA512 + default: + return 0 + } +} + +func ksfToKSF(h string) ksf.Identifier { + switch h { + case "Identity": + return 0 + case "Scrypt": + return ksf.Scrypt + default: + return 0 + } +} + +func groupToGroup(g string) opaque.Group { + switch g { + case "ristretto255": + return opaque.RistrettoSha512 + case "decaf448": + panic("group not supported") + case "P256_XMD:SHA-256_SSWU_RO_": + return opaque.P256Sha256 + case "P384_XMD:SHA-384_SSWU_RO_": + return opaque.P384Sha512 + case "P521_XMD:SHA-512_SSWU_RO_": + return opaque.P521Sha512 + // case "curve25519_XMD:SHA-512_ELL2_RO_": + // return opaque.Curve25519Sha512 + default: + panic("group not recognised") + } +} + +type draftVectors []*vector + +func loadOpaqueVectors(filepath string) (draftVectors, error) { + contents, err := os.ReadFile(filepath) + if err != nil { + return nil, err + } + + var v draftVectors + errJSON := json.Unmarshal(contents, &v) + if errJSON != nil { + return nil, errJSON + } + + return v, nil +} + +func TestOpaqueVectors(t *testing.T) { + vectorFile := "vectors.json" + + v, err := loadOpaqueVectors(vectorFile) + if err != nil || v == nil { + t.Fatal(err) + } + + for _, tv := range v { + t.Run(fmt.Sprintf("%s - %s - Fake:%s", tv.Config.Name, tv.Config.Group, tv.Config.Fake), tv.test) + } +}