Replies: 3 comments 1 reply
-
Alternatively, an accessor for This would result in a smaller change to the existing implementation of Here's the full diff of taking this approach instead of implementing serde methods on diff --git a/internal/ake/server.go b/internal/ake/server.go
index 29525e9..f8d89fb 100644
--- a/internal/ake/server.go
+++ b/internal/ake/server.go
@@ -84,3 +84,8 @@ func (s *Server) Finalize(p *internal.Parameters, ke3 *message.KE3) bool {
func (s *Server) SessionKey() []byte {
return s.sessionSecret
}
+
+// ClientMAC returns the expected client MAC if a previous call to Finalize() was successful.
+func (s *Server) ClientMAC() []byte {
+ return s.clientMac
+}
diff --git a/tests/opaque_test.go b/tests/opaque_test.go
index dc67319..1a48dfa 100644
--- a/tests/opaque_test.go
+++ b/tests/opaque_test.go
@@ -10,6 +10,7 @@ package opaque_test
import (
"bytes"
+ "errors"
"testing"
"github.com/bytemare/cryptotools/utils"
@@ -152,6 +153,8 @@ func testAuthentication(t *testing.T, p *testParams, record *opaque.ClientRecord
// Server
var m5s []byte
+ var expectedMAC []byte
+ var serverKey []byte
{
server := p.Server()
m4, err := server.DeserializeKE1(m4s)
@@ -165,6 +168,9 @@ func testAuthentication(t *testing.T, p *testParams, record *opaque.ClientRecord
}
m5s = ke2.Serialize()
+
+ expectedMAC = server.Ake.ClientMAC()
+ serverKey = server.SessionKey()
}
// Client
@@ -188,7 +194,6 @@ func testAuthentication(t *testing.T, p *testParams, record *opaque.ClientRecord
}
// Server
- var serverKey []byte
{
server := p.Server()
m6, err := server.DeserializeKE3(m6s)
@@ -196,11 +201,9 @@ func testAuthentication(t *testing.T, p *testParams, record *opaque.ClientRecord
t.Fatalf(dbgErr, p.Mode, err)
}
- if err := server.Finish(m6); err != nil {
- t.Fatalf(dbgErr, p.Mode, err)
+ if !server.MAC.Equal(expectedMAC, m6.Mac) {
+ t.Fatalf(dbgErr, p.Mode, errors.New("failed to authenticate client: invalid client mac"))
}
-
- serverKey = server.SessionKey()
}
if !bytes.Equal(clientKey, serverKey) { |
Beta Was this translation helpful? Give feedback.
-
Hi 👋 First off: thank you for your question :) Indeed, this is a desirable feature and something that we've been discussing in the spec. I'm just back from vacation and will tackle this in the coming days. I'm thinking about something very simple, where the server can export its state in a serialized form, and import an existing state to complete the AKE (which is just the mac and session key). |
Beta Was this translation helpful? Give feedback.
-
Merged in #8 |
Beta Was this translation helpful? Give feedback.
-
TL;DR
Please provide
func (s Server) Serialize() []byte
andfunc (s *Server) Deserialize(data []byte) error
inpackage ake
so that state can be persisted externally to theake.Server
instance between steps in a protocol run.If you're open to it, I would be happy to implement these methods and submit a PR.
Rationale
The OPAQUE protocol is comprised of two flows--registration and authentication--both of which have four steps. Both the client and the server need to maintain some state between steps in each flow.
Clients generally are implemented to be run on a single endpoint (web browser, mobile device, etc.) where state is easily maintained between steps in a protocol run.
Servers in production environments, however, are typically implemented to be as stateless as possible, sharing state via some persistent store. For a fleet of servers implemented in this way, it's not currently possible to complete the authentication flow because necessary state cannot be persisted server-side between steps of the flow. Even a single HTTP server cannot trivially maintain the state between HTTP requests correlating to steps in the flow.
Focusing on the authentication flow:
Step 1: the client begins the flow with
ke1 := client.Init(password)
.ke1
is sent to the server, probably along with ausername
. The client's only input so far ispassword
and maybeusername
.Step 2: the server does
ke2, err := server.Init(ke1, serverIdentity, serverSecretKey, serverPublicKey, oprfSeed, record)
. All of the parameters passed here are externally known.ke1
was provided by the client,serverIdentity
,serverSecretKey
,serverPublicKey
, andoprfSeed
all come from configuration, andrecord
can be fetched from a persistent store using ausername
that the client probably provided. As a side effect ofserver.Init
, some state is set internally toserver.Ake
.ke2
is passed back to the client.Step 3: the client does
ke3, exportKey, err := client.Finish(idc, ids, ke2)
using theke2
it got back from the server, and the client and server identities ostensibly already known to the client.ke3
is sent to the server.Step 4: the server attempts
err := server.Finish(ke3)
. The only input available here iske3
, which comes from the client. There's an implicit dependency on state internal toserver
which was set in Step 2. If the instance ofserver
being used here is not the same instance ofserver
being used in Step 2, then that internal state is not available and this step will fail.An implementation of an OPAQUE server can store the server identity, server public and private keys, and OPRF seed(s) in a secure configuration store, and it can store client records in a database. If there was a way to serialize the internal state created in Step 2 and deserialize that state in Step 4, then the implementation can move all of its state into persistent stores.
Focusing on
TestFull
The existing
TestFull
in./tests
is helpful in illustrating this request.Stateful
server
The current test suite uses the same instance of
server
for both server-side steps of the registration flow and the authentication flow.Segmenting
func testRegistration
Using bare scopes, we can segment the flow of
func testRegistration
to clearly identify state that carries over from one step to another.client
will be running on a user's endpoint, and it's reasonable to expect that it will track its own state. Therefore,client
will live in the top-level scope of the function.The first step in the registration process is
client.RegistrationInit
. The only output that is not internal toclient
here is them1s
message. So, we createm1s
in the function's top-level scope, and assign it within a bare scope once its value is known.In this round,
server
will need to keep some internal state, so it will exist as a single instance in the function's top-level scope.server
will recieve them1s
message sent fromclient
, and it will respond with them2s
message.server
will also generate a randomcredID
which it needs to remember in a later step. Therefore, bothm2s
andcredID
are created in the function's top-level scope before continuing into a bare scope whereserver.RegistrationResponse
is called.Back to the
client
for the next step whereclient.RegistrationFinalize
will happen. In this step,client
will finish by outputtingm3s
for the server. Also in this step,client
will learn the value ofexportKeyReg
, which is one of the return values of the function. Bothm3s
andexportKeyReg
are created in the function's top-level scope, and assigned within the following bare scope.Finally,
server
recievesm3s
and can store aClientRecord
. There is nothing new learned in this step, but this is whenserver
persists theClientRecord
to complete the registration process. Note thatserver
here is usingcredID
which it had to remember from earlier.Collapsing the bare scopes, it's easy to identify that the three registration messages (
m1s
,m2s
, andm3s
) must persist between steps.credID
also must persist becauseserver
must remember it between steps 2 and 4. Finally,exportKeyReg
must persist because it is a return value of the function.The complete `func testRegistration`
Segmenting
func testAuthentication
The same exercise is performed for
func testAuthentication
.Once again,
client
will exist in the function's top-level scope so that it can track its state.In the first step,
client
createsm4s
which is sent toserver
.Again for this round,
server
will exist in the same scope asclient
.In step 2,
server
recievesm4s
and doesserver.Init
. Other inputs toserver.Init
includep.serverSecretKey
andp.oprfSeed
, which are known externally and passed in as parameters, andrecord
whichserver
ostensibly would retrieve from a persistent store. The output of this step ism5s
, which is sent toclient
. There is also some internal state created inserver.Ake
.In step 3,
client
will createm6s
to be sent toserver
. Also,client
will learnexportKeyLogin
andclientKey
, both of which are used at the end of the function to validate that the protocol has run successfully; therefore, those values persist between steps.In step 4,
server
recievesm6s
and doesserver.Finish
. Here,server
will learnserverKey
, which also is used at the end of the function to validate that the protocol has run successfully, so it will persist beyond this step.Finally, the test asserts the equality of
clientKey
andserverKey
, and returnsexportKeyLogin
for its caller to compare toexportKeyReg
.With this segmentation in place, the test suite still passes.
Stateless
server
If different instances of
opaque.Server
are used for step 2 and step 4 of the registration flow, registration will still complete successfully as long as those two instances are configured in the same way (i.e., using the same OPAQUE configuration) because there is no state carried internally toserver
between the steps of the registration flow.Stateless Registration
func testRegistration
is modified so that a new instance ofserver
is created within the bare scope of each server-side step in the flow.With this change, the test suite still completes successfully.
Stateless Authentication
func testAuthentication
is modified so that a new instance ofserver
is created within the bare scope of each server-side step in the flow.With this change, the test suite fails.
This failure is from a call to
server.Finish
which then callsserver.Ake.Finalize
, which callsp.MAC.Equal(server.Ake.clientMac, ke3.Mac)
(wherep
isserver
'sParameters
specifying the MAC to use).server.Ake.clientMac
is set during step 2 as a side effect ofserver.Init
, so the original value was lost when execution exited the bare scope of step 2.Making it Work
ake.Server
is composed of two unexported fields,clientMac
andsessionSecret
, both of which are[]byte
. In order to persist this state externally from the instance ofake.Server
(and theopaque.Server
that referencs it), it's necessary to usereflect
andunsafe
.AKE Server State Serde
The first goal is to extract the state so that it can be persisted elsewhere. This is accomplished by using
reflect
to access the unexported fields, andencoding/gob
to copy the values of those fields into a[]byte
returned to the caller.The second goal is to rehydrate an
ake.Server
by deserializing the state onto a new instance. This is not easily done, since the target fields are unexported and therefore not settable byreflect
. However, since they are[]byte
, which is a reference type,unsafe
provides a solution. For each field, the state is decoded intovar b []byte
, and the field's address in memory is updated to point to the decoded value.Update
func testAuthentication
to Persist StateIn step 2, a new value
state
will persist at the end of the step. Within the step's scope, it is assigned asstate = serializeAkeServerState(server.Ake)
. Then, in step 4,state
is applied to the new instance ofserver
like this:Full patch
With these changes in place, the test is now passing.
Beta Was this translation helpful? Give feedback.
All reactions