-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
bytemare
authored and
bytemare
committed
Nov 21, 2020
1 parent
d986f82
commit a45864b
Showing
8 changed files
with
1,288 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,3 +13,5 @@ | |
|
||
# Dependency directories (remove the comment below to include it) | ||
# vendor/ | ||
|
||
.idea |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
# CPace | ||
|
||
CPace implements the CFRG recommended balanced Password Authentication Key Exchange. | ||
|
||
**!!! WARNING : THIS IMPLEMENTATION IS PROOF OF CONCEPT AND BASED ON THE LATEST INTERNET DRAFT. | ||
THERE ARE ABSOLUTELY NO WARRANTIES. !!!** | ||
|
||
CPace allows two parties sharing a common secret or password to securely agree on a session key for secure communication. | ||
It's a dead simple protocol with only two messages, yet state of the art key exchange based on a shared secret. | ||
|
||
Note: The registration of the secret password is not in the scope of the protocol or this implementation. | ||
|
||
# Get it | ||
|
||
go get github.com/bytemare/cpace | ||
|
||
# Use it | ||
|
||
The API is really minimal and very easy to use. | ||
|
||
- New() returns the structure through which the steps are made. It is the same struct type for the client and the server. | ||
- Authenticate() takes a message and returns one. If this function returns a message, it should be sent to the peer. | ||
- SessionKey() returns the fresh session key, if everything went fine. | ||
|
||
Messages are of type message.Kex, from the companion pake package. These message have built-in methods for encoding and decoding to different formats like Gob and Json. | ||
|
||
<details> | ||
<summary>Example:</summary> | ||
|
||
``` | ||
serverID := []byte("server") | ||
username := []byte("client") | ||
password := []byte("password") | ||
var ad = []byte("myAuth") | ||
// Set up the initiator, let's call it the client | ||
client, err := New(pake.Initiator, username, serverID, password, nil, ad, nil) | ||
if err != nil { panic(err) } | ||
// Start the protocol. | ||
// message1 must then be sent to the responder | ||
message1, err := client.Authenticate(nil) | ||
if err != nil { panic(err) } | ||
// Set up the responder, let's call it the server | ||
server, err := New(pake.Responder, serverID, username, password, nil, ad, nil) | ||
if err != nil { panic(err) } | ||
// Handle the initiator's message, and send back message2. At this point the session key can already be derived. | ||
message2, err := server.Authenticate(message1) | ||
if err != nil { panic(err) } | ||
// Give the initiator the responder's answer. Since we're in implicit authentication, no message comes out here. | ||
// After this, the initiator can derive the session key. | ||
_, err = client.Authenticate(message2) | ||
if err != nil { panic(err) } | ||
``` | ||
</details> | ||
|
||
# Under the hood | ||
|
||
All cryptographic operations can be found in the pake package, which itself uses either the standard library or tested and proved external libraries. | ||
The Ristretto255 group is used for the mathematical heavy lifting and performance. | ||
|
||
Hash operations use SHA3 and Shake. | ||
|
||
Default password key derivation is Argon2id. | ||
|
||
# Deploy it | ||
|
||
Don't, yet. | ||
|
||
## Work on it | ||
|
||
WIP |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,246 @@ | ||
// Package cpace provides an easy to use CPace implementation | ||
package cpace | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
|
||
"golang.org/x/crypto/cryptobyte" | ||
|
||
"github.com/bytemare/cryptotools" | ||
"github.com/bytemare/cryptotools/encoding" | ||
"github.com/bytemare/cryptotools/hashtogroup" | ||
"github.com/bytemare/cryptotools/utils" | ||
"github.com/bytemare/pake" | ||
"github.com/bytemare/pake/message" | ||
) | ||
|
||
const ( | ||
Protocol = "CPace" | ||
Version = "0.0.0" | ||
maxIDLength = 1<<16 - 1 | ||
) | ||
|
||
var errInternalKexAssertion = errors.New("internal: something went wrong in type assertion to Kex message") | ||
|
||
// Parameters groups a party's input parameters | ||
type Parameters struct { | ||
// ID is own identity | ||
ID []byte | ||
|
||
// PeerID identifies the remote peer | ||
PeerID []byte | ||
|
||
// Secret is the shared secret, e.g. the password | ||
Secret []byte | ||
|
||
// SID is the session identifier, a unique random byte array of at least 16 bytes | ||
SID []byte | ||
|
||
// AD | ||
AD []byte | ||
|
||
// Encoding specifies which encoding should be used for outgoing and incoming messages | ||
Encoding encoding.Encoding | ||
} | ||
|
||
// CPace wraps the core CPace session and state info and enriches them with a more abstract API | ||
type CPace struct { | ||
session | ||
state | ||
} | ||
|
||
func validateCI(id, peerID, ad []byte) error { | ||
switch { | ||
case len(id) > maxIDLength: | ||
return errSetupLongID | ||
case len(peerID) > maxIDLength: | ||
return errSetupLongPeerID | ||
case len(ad) > maxIDLength: | ||
return errSetupLongAD | ||
default: | ||
return nil | ||
} | ||
} | ||
|
||
func assembleCI(role pake.Role, id, peerID, ad []byte) ([]byte, error) { | ||
ci := make([]byte, 0, len(id)+len(peerID)+len(ad)) | ||
b := cryptobyte.NewBuilder(ci) | ||
|
||
switch role { | ||
case pake.Initiator: | ||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(id) }) | ||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(peerID) }) | ||
case pake.Responder: | ||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(peerID) }) | ||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(id) }) | ||
default: | ||
return nil, fmt.Errorf("assembleCI : %w", errInternalInvalidRole) | ||
} | ||
|
||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(ad) }) | ||
|
||
return b.BytesOrPanic(), nil | ||
} | ||
|
||
func buildDST(identifier hashtogroup.Ciphersuite, in int) []byte { | ||
return []byte(fmt.Sprintf("%s%s-%d", Protocol, identifier, in)) | ||
} | ||
|
||
func newCPace(role pake.Role, parameters *Parameters, csp *cryptotools.Parameters) (*CPace, error) { | ||
if err := parameters.Encoding.Available(); err != nil { | ||
return nil, err | ||
} | ||
|
||
// Verify session id and generate one for the initiator if none provided | ||
switch l := len(parameters.SID); { | ||
case l == 0: | ||
// If none is given for the Responder, we'll take it from the initiator's first message | ||
if role == pake.Initiator { | ||
parameters.SID = utils.RandomBytes(minSidLength) | ||
} | ||
case l < minSidLength: | ||
return nil, errSetupSIDTooShort | ||
} | ||
|
||
if err := validateCI(parameters.ID, parameters.PeerID, parameters.AD); err != nil { | ||
return nil, err | ||
} | ||
|
||
ci, err := assembleCI(role, parameters.ID, parameters.PeerID, parameters.AD) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// meta := pake.MetaData() | ||
|
||
pakeCore, err := pake.KeyExchange.New(Protocol, Version, parameters.Encoding, csp, role, parameters.PeerID) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &CPace{ | ||
session: session{ | ||
role: role, | ||
sid: parameters.SID, | ||
cid: ci, | ||
}, | ||
state: state{ | ||
password: parameters.Secret, | ||
core: pakeCore, | ||
dsi1: buildDST(pakeCore.Crypto.Parameters.Group, 1), | ||
dsi2: buildDST(pakeCore.Crypto.Parameters.Group, 2), | ||
}, | ||
}, nil | ||
} | ||
|
||
// Client returns a new CPace client instance | ||
func Client(parameters *Parameters, csp *cryptotools.Parameters) (pake.Pake, error) { | ||
return newCPace(pake.Initiator, parameters, csp) | ||
} | ||
|
||
// Server returns a new CPace server instance | ||
func Server(parameters *Parameters, csp *cryptotools.Parameters) (pake.Pake, error) { | ||
return newCPace(pake.Responder, parameters, csp) | ||
} | ||
|
||
// AuthenticateKex interprets the key exchange message and operates the consecutive steps of the protocol. | ||
func (c *CPace) AuthenticateKex(m *message.Kex) (*message.Kex, error) { | ||
switch c.role { | ||
case pake.Initiator: | ||
if c.core.Expect != message.StageResponse { | ||
return nil, errInternalUnexpectedMessage | ||
} | ||
|
||
// We should only enter here when initialising the initiator, i.e. with a nil message | ||
if m == nil { | ||
// If own public element has already been set, it means we've already initiated the client | ||
if c.publicElement != nil { | ||
return nil, errInitReInit | ||
} | ||
|
||
// We want to start the protocol | ||
elem, sid, err := c.initiate() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &message.Kex{ | ||
Element: elem, | ||
Auth: sid, | ||
}, nil | ||
} | ||
|
||
return nil, c.finish(m.Element) | ||
|
||
case pake.Responder: | ||
if c.core.Expect != message.StageStart { | ||
return nil, errInternalUnexpectedMessage | ||
} | ||
|
||
if m == nil { | ||
return nil, errRespNilMsg | ||
} | ||
|
||
elem, err := c.response(m.Element, m.Auth) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &message.Kex{ | ||
Element: elem, | ||
Auth: nil, | ||
}, nil | ||
} | ||
|
||
return nil, errInternalInvalidRole | ||
} | ||
|
||
// Authenticate decodes the payload as a Kex message and operates the consecutive steps of the protocol. | ||
func (c *CPace) Authenticate(m []byte) ([]byte, error) { | ||
kex, err := decodeKeyExchange(m, c.core.Encoding()) | ||
if err != nil { | ||
return nil, errPeerEncoding // todo : should we wrap the error ? | ||
} | ||
|
||
r, err := c.AuthenticateKex(kex) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// On success, the initiator returns nil | ||
if r == nil { | ||
return nil, nil | ||
} | ||
|
||
return r.Encode(c.core.Encoding()) | ||
} | ||
|
||
// SessionKey returns the session's intermediary secret session key | ||
func (c *CPace) SessionKey() []byte { | ||
return c.iSessionKey | ||
} | ||
|
||
// EncodedParameters returns the 4-byte encoding of the ciphersuite parameters | ||
func (c *CPace) EncodedParameters() cryptotools.CiphersuiteEncoding { | ||
return c.core.Crypto.Parameters.Encode() | ||
} | ||
|
||
// decodeKeyExchange decodes messages in the simple Kex format, returns nil on nil message. | ||
func decodeKeyExchange(m []byte, enc encoding.Encoding) (*message.Kex, error) { | ||
if m == nil { | ||
return nil, nil | ||
} | ||
|
||
k, err := enc.Decode(m, &message.Kex{}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
kex, ok := k.(*message.Kex) | ||
if !ok { | ||
return nil, errInternalKexAssertion | ||
} | ||
|
||
return kex, nil | ||
} |
Oops, something went wrong.