Documentation
¶
Overview ¶
Package cpace implements an auditable draft-irtf-cfrg-cpace-21 CPACE-RISTR255-SHA512 initiator-responder flow.
This module is an Internet-Draft implementation. It is not independently audited and must not be treated as production-ready cryptographic software.
The public API intentionally exposes only an initiator-responder flow with mandatory explicit key confirmation. A session is returned only after both sides have confirmed possession of the same intermediate session key. Respond success alone is not authentication.
Scalar randomness always comes from crypto/rand.Reader; the package does not accept caller-supplied randomness through the public API. Input and wire fields have package-owned per-field size caps; associated data is capped at 64 KiB and smaller identity/context fields are capped more tightly.
Input.SessionID must be non-empty by default. Input.AllowEmptySessionID is only for draft-21 compatibility tests or deliberately compatible profiles that accept weaker replay and transcript separation.
Initiator and Responder are single-use state. Finish consumes them on success and failure; Close releases their local persistent secret material when an exchange is abandoned before Finish. Close is nil-safe and idempotent, and value copies share terminal state.
Session.Close performs best-effort cleanup of session key material and makes future Export calls fail. PeerAssociatedData and PeerID expose copied, non-secret metadata bound into the confirmed exchange.
Callers provide role-local Input: Start maps SelfID to the initiator identity and PeerID to the responder identity, while Respond maps SelfID to the responder identity and PeerID to the initiator identity. Applications that negotiate PAKE versions, suites, or protocol modes outside this package must provide their own downgrade protection for that outer negotiation.
Example ¶
package main
import (
"bytes"
"fmt"
"github.com/the-sarge/cpace"
)
func main() {
initCfg := exampleInitiatorInput("session-1234")
initCfg.LocalAssociatedData = []byte("client hello")
initiator, msgA, err := cpace.Start(initCfg)
if err != nil {
panic(err)
}
defer closeExampleInitiator(initiator)
respCfg := exampleResponderInput("session-1234")
respCfg.LocalAssociatedData = []byte("server hello")
responder, msgB, err := cpace.Respond(respCfg, msgA)
if err != nil {
panic(err)
}
defer closeExampleResponder(responder)
msgC, initSession, err := initiator.Finish(msgB)
if err != nil {
panic(err)
}
respSession, err := responder.Finish(msgC)
if err != nil {
panic(err)
}
defer func() {
if err := initSession.Close(); err != nil {
panic(err)
}
}()
defer func() {
if err := respSession.Close(); err != nil {
panic(err)
}
}()
initKey, _ := initSession.Export([]byte("application key"), nil, 32)
respKey, _ := respSession.Export([]byte("application key"), nil, 32)
fmt.Println(bytes.Equal(initKey, respKey))
fmt.Println(bytes.Equal(initSession.TranscriptID(), respSession.TranscriptID()))
}
func exampleInitiatorInput(sessionID string) cpace.Input {
return cpace.Input{
Password: []byte("correct horse battery staple"),
SelfID: []byte("client@example"),
PeerID: []byte("server@example"),
Context: []byte("example protocol v1"),
SessionID: []byte(sessionID),
}
}
func exampleResponderInput(sessionID string) cpace.Input {
return cpace.Input{
Password: []byte("correct horse battery staple"),
SelfID: []byte("server@example"),
PeerID: []byte("client@example"),
Context: []byte("example protocol v1"),
SessionID: []byte(sessionID),
}
}
func closeExampleInitiator(initiator *cpace.Initiator) {
if err := initiator.Close(); err != nil {
panic(err)
}
}
func closeExampleResponder(responder *cpace.Responder) {
if err := responder.Close(); err != nil {
panic(err)
}
}
Output: true true
Index ¶
Examples ¶
Constants ¶
const ( // DraftVersion identifies the CPace Internet-Draft revision implemented by // this package. DraftVersion = "draft-irtf-cfrg-cpace-21" )
Variables ¶
var ( // ErrInvalidInput reports invalid local configuration or parameters. ErrInvalidInput = errors.New("cpace: invalid input") // ErrEmptySessionID reports an empty Input.SessionID without the explicit // AllowEmptySessionID compatibility opt-in. The returned error also wraps // ErrInvalidInput. ErrEmptySessionID = errors.New("cpace: empty session id") // ErrRandomness reports randomness-related failures, including random // source read failures and repeated unusable scalar samples. ErrRandomness = errors.New("cpace: randomness failure") // ErrMessage reports malformed or unexpected wire messages. ErrMessage = errors.New("cpace: invalid message") // ErrStateUsed reports an attempt to reuse a single-use protocol state. ErrStateUsed = errors.New("cpace: state already used") // ErrSessionClosed reports an attempt to export key material from a closed // Session. ErrSessionClosed = errors.New("cpace: session closed") // ErrAbort reports a draft abort condition such as an invalid point or // neutral-element Diffie-Hellman result. ErrAbort = errors.New("cpace: protocol abort") // canonical Ristretto255 encoding. The returned error also wraps // ErrAbort. ErrPeerShareEncoding = errors.New("cpace: peer share encoding") // Ristretto255 identity element. The returned error also wraps ErrAbort. ErrPeerShareIdentity = errors.New("cpace: peer share identity") // ErrConfirmationFailed reports failed explicit key confirmation. ErrConfirmationFailed = errors.New("cpace: key confirmation failed") )
Functions ¶
This section is empty.
Types ¶
type Initiator ¶
type Initiator struct {
// contains filtered or unexported fields
}
Initiator is a single-use initiator state returned by Start.
func (*Initiator) Close ¶ added in v0.1.3
Close releases the persistent secret material held by the initiator state when an exchange is abandoned before Finish. Close is idempotent and nil-safe; calling Close on a nil *Initiator returns nil. Copies of an Initiator share the same terminal state, so closing one copy closes them all.
func (*Initiator) Finish ¶
Finish consumes message B, verifies the responder confirmation tag, and returns message C plus an authenticated session. The initiator state is consumed even when message parsing or confirmation fails.
Example (ConfirmationFailure) ¶
package main
import (
"errors"
"fmt"
"github.com/the-sarge/cpace"
)
func main() {
initCfg := exampleInitiatorInput("example-confirmation-failure")
initCfg.LocalAssociatedData = []byte("client hello")
initiator, msgA, err := cpace.Start(initCfg)
if err != nil {
panic(err)
}
defer closeExampleInitiator(initiator)
respCfg := exampleResponderInput("example-confirmation-failure")
respCfg.Context = []byte("different protocol context")
respCfg.LocalAssociatedData = []byte("server hello")
responder, msgB, err := cpace.Respond(respCfg, msgA)
if err != nil {
panic(err)
}
defer closeExampleResponder(responder)
_, _, err = initiator.Finish(msgB)
fmt.Println(errors.Is(err, cpace.ErrConfirmationFailed))
}
func exampleInitiatorInput(sessionID string) cpace.Input {
return cpace.Input{
Password: []byte("correct horse battery staple"),
SelfID: []byte("client@example"),
PeerID: []byte("server@example"),
Context: []byte("example protocol v1"),
SessionID: []byte(sessionID),
}
}
func exampleResponderInput(sessionID string) cpace.Input {
return cpace.Input{
Password: []byte("correct horse battery staple"),
SelfID: []byte("server@example"),
PeerID: []byte("client@example"),
Context: []byte("example protocol v1"),
SessionID: []byte(sessionID),
}
}
func closeExampleInitiator(initiator *cpace.Initiator) {
if err := initiator.Close(); err != nil {
panic(err)
}
}
func closeExampleResponder(responder *cpace.Responder) {
if err := responder.Close(); err != nil {
panic(err)
}
}
Output: true
type Input ¶ added in v0.1.3
type Input struct {
Password []byte
SelfID []byte
PeerID []byte
Context []byte
SessionID []byte
LocalAssociatedData []byte
AllowEmptySessionID bool
}
Input contains the local inputs for one CPace role.
Password, SelfID, and PeerID must be non-empty. Context and LocalAssociatedData may be empty. Password, Context, and SessionID are shared session values both parties supply identically. SelfID, PeerID, and LocalAssociatedData are role-local values: Start treats SelfID as the initiator identity and PeerID as the responder identity; Respond treats SelfID as the responder identity and PeerID as the initiator identity. SessionID must be a fresh, non-secret, parties-agree-on value for every session. Empty SessionID values are rejected by default because they weaken replay and transcript separation properties. Set AllowEmptySessionID only for draft-21 compatibility tests or profiles that have deliberately accepted the weaker empty-sid behavior. Scalar randomness always comes from crypto/rand.Reader. Field lengths are capped at 4 KiB for Password and IDs, 1 KiB for Context and SessionID, and 64 KiB for LocalAssociatedData. Inputs exceeding these caps are rejected before copying; accepted byte slices are copied by Start and Respond before use.
type Responder ¶
type Responder struct {
// contains filtered or unexported fields
}
Responder is a single-use responder state returned by Respond.
func Respond ¶
Respond consumes message A, creates responder state, and returns message B. Message B includes the responder's explicit key-confirmation tag. A nil error from Respond does not authenticate the initiator; authentication is established only by successful Finish calls.
func (*Responder) Close ¶ added in v0.1.3
Close releases the persistent secret material held by the responder state when an exchange is abandoned before Finish. Close is idempotent and nil-safe; calling Close on a nil *Responder returns nil. Copies of a Responder share the same terminal state, so closing one copy closes them all.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is an explicitly confirmed CPace session. Copies of a Session share the same close state and secret key material.
func (*Session) Close ¶
Close releases the secret key material held by the Session. Close is idempotent and nil-safe; calling Close on a nil *Session returns nil. It performs best-effort in-memory key cleanup, but Go does not provide guaranteed secure memory erasure and the runtime or compiler may make additional copies. Non-secret metadata such as TranscriptID, PeerAssociatedData, and PeerID remains available after Close.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/the-sarge/cpace"
)
func main() {
initSession, respSession, err := exampleConfirmedSessions("example-close")
if err != nil {
panic(err)
}
defer closeExampleSession(respSession)
if err := initSession.Close(); err != nil {
panic(err)
}
_, err = initSession.Export([]byte("application key"), nil, 32)
fmt.Println(errors.Is(err, cpace.ErrSessionClosed))
fmt.Println(len(initSession.TranscriptID()) > 0)
}
func exampleInitiatorInput(sessionID string) cpace.Input {
return cpace.Input{
Password: []byte("correct horse battery staple"),
SelfID: []byte("client@example"),
PeerID: []byte("server@example"),
Context: []byte("example protocol v1"),
SessionID: []byte(sessionID),
}
}
func exampleResponderInput(sessionID string) cpace.Input {
return cpace.Input{
Password: []byte("correct horse battery staple"),
SelfID: []byte("server@example"),
PeerID: []byte("client@example"),
Context: []byte("example protocol v1"),
SessionID: []byte(sessionID),
}
}
func exampleConfirmedSessions(sessionID string) (*cpace.Session, *cpace.Session, error) {
initCfg := exampleInitiatorInput(sessionID)
initCfg.LocalAssociatedData = []byte("client hello")
initiator, msgA, err := cpace.Start(initCfg)
if err != nil {
return nil, nil, err
}
defer closeExampleInitiator(initiator)
respCfg := exampleResponderInput(sessionID)
respCfg.LocalAssociatedData = []byte("server hello")
responder, msgB, err := cpace.Respond(respCfg, msgA)
if err != nil {
return nil, nil, err
}
defer closeExampleResponder(responder)
msgC, initSession, err := initiator.Finish(msgB)
if err != nil {
return nil, nil, err
}
respSession, err := responder.Finish(msgC)
if err != nil {
_ = initSession.Close()
return nil, nil, err
}
return initSession, respSession, nil
}
func closeExampleSession(session *cpace.Session) {
if err := session.Close(); err != nil {
panic(err)
}
}
func closeExampleInitiator(initiator *cpace.Initiator) {
if err := initiator.Close(); err != nil {
panic(err)
}
}
func closeExampleResponder(responder *cpace.Responder) {
if err := responder.Close(); err != nil {
panic(err)
}
}
Output: true true
func (*Session) Export ¶
Export derives deterministic application key material from the confirmed ISK using HKDF-SHA512. The label and context are prefix-free encoded into HKDF info. Export output is not fresh randomness or a randomness pool; use separate, domain-specific labels and contexts for each application purpose. length must be in the range [0, 16320] (255 * 64, the HKDF-SHA512 maximum). A length of zero returns a result with length 0; callers must not distinguish nil from empty output for this case. Negative values and values exceeding the maximum are rejected with a wrapped ErrInvalidInput.
Example ¶
package main
import (
"bytes"
"fmt"
"github.com/the-sarge/cpace"
)
func main() {
initSession, respSession, err := exampleConfirmedSessions("example-export")
if err != nil {
panic(err)
}
defer closeExampleSession(initSession)
defer closeExampleSession(respSession)
trafficKey, err := initSession.Export([]byte("traffic key"), []byte("initiator to responder"), 32)
if err != nil {
panic(err)
}
headerKey, err := initSession.Export([]byte("header key"), []byte("initiator to responder"), 32)
if err != nil {
panic(err)
}
peerTrafficKey, err := respSession.Export([]byte("traffic key"), []byte("initiator to responder"), 32)
if err != nil {
panic(err)
}
fmt.Println(len(trafficKey))
fmt.Println(bytes.Equal(trafficKey, headerKey))
fmt.Println(bytes.Equal(trafficKey, peerTrafficKey))
}
func exampleInitiatorInput(sessionID string) cpace.Input {
return cpace.Input{
Password: []byte("correct horse battery staple"),
SelfID: []byte("client@example"),
PeerID: []byte("server@example"),
Context: []byte("example protocol v1"),
SessionID: []byte(sessionID),
}
}
func exampleResponderInput(sessionID string) cpace.Input {
return cpace.Input{
Password: []byte("correct horse battery staple"),
SelfID: []byte("server@example"),
PeerID: []byte("client@example"),
Context: []byte("example protocol v1"),
SessionID: []byte(sessionID),
}
}
func exampleConfirmedSessions(sessionID string) (*cpace.Session, *cpace.Session, error) {
initCfg := exampleInitiatorInput(sessionID)
initCfg.LocalAssociatedData = []byte("client hello")
initiator, msgA, err := cpace.Start(initCfg)
if err != nil {
return nil, nil, err
}
defer closeExampleInitiator(initiator)
respCfg := exampleResponderInput(sessionID)
respCfg.LocalAssociatedData = []byte("server hello")
responder, msgB, err := cpace.Respond(respCfg, msgA)
if err != nil {
return nil, nil, err
}
defer closeExampleResponder(responder)
msgC, initSession, err := initiator.Finish(msgB)
if err != nil {
return nil, nil, err
}
respSession, err := responder.Finish(msgC)
if err != nil {
_ = initSession.Close()
return nil, nil, err
}
return initSession, respSession, nil
}
func closeExampleSession(session *cpace.Session) {
if err := session.Close(); err != nil {
panic(err)
}
}
func closeExampleInitiator(initiator *cpace.Initiator) {
if err := initiator.Close(); err != nil {
panic(err)
}
}
func closeExampleResponder(responder *cpace.Responder) {
if err := responder.Close(); err != nil {
panic(err)
}
}
Output: 32 false true
func (*Session) PeerAssociatedData ¶
PeerAssociatedData returns the peer associated data that was bound into the confirmed exchange. The returned slice is a copy and remains available after Close.
func (*Session) PeerID ¶
PeerID returns the caller-configured peer identity that was bound into CI and confirmed by the completed exchange. The value is copied from Input; it is not parsed from peer-controlled wire data. The returned slice is a copy and remains available after Close.
func (*Session) TranscriptID ¶
TranscriptID returns the draft CPaceSidOutput value for the confirmed initiator-responder CPace transcript. It is not a complete channel binding for any outer version, suite, or application-protocol negotiation. TranscriptID remains available after Close.
Example ¶
package main
import (
"bytes"
"fmt"
"github.com/the-sarge/cpace"
)
func main() {
initSession, respSession, err := exampleConfirmedSessions("example-transcript")
if err != nil {
panic(err)
}
defer closeExampleSession(initSession)
defer closeExampleSession(respSession)
fmt.Println(len(initSession.TranscriptID()))
fmt.Println(bytes.Equal(initSession.TranscriptID(), respSession.TranscriptID()))
}
func exampleInitiatorInput(sessionID string) cpace.Input {
return cpace.Input{
Password: []byte("correct horse battery staple"),
SelfID: []byte("client@example"),
PeerID: []byte("server@example"),
Context: []byte("example protocol v1"),
SessionID: []byte(sessionID),
}
}
func exampleResponderInput(sessionID string) cpace.Input {
return cpace.Input{
Password: []byte("correct horse battery staple"),
SelfID: []byte("server@example"),
PeerID: []byte("client@example"),
Context: []byte("example protocol v1"),
SessionID: []byte(sessionID),
}
}
func exampleConfirmedSessions(sessionID string) (*cpace.Session, *cpace.Session, error) {
initCfg := exampleInitiatorInput(sessionID)
initCfg.LocalAssociatedData = []byte("client hello")
initiator, msgA, err := cpace.Start(initCfg)
if err != nil {
return nil, nil, err
}
defer closeExampleInitiator(initiator)
respCfg := exampleResponderInput(sessionID)
respCfg.LocalAssociatedData = []byte("server hello")
responder, msgB, err := cpace.Respond(respCfg, msgA)
if err != nil {
return nil, nil, err
}
defer closeExampleResponder(responder)
msgC, initSession, err := initiator.Finish(msgB)
if err != nil {
return nil, nil, err
}
respSession, err := responder.Finish(msgC)
if err != nil {
_ = initSession.Close()
return nil, nil, err
}
return initSession, respSession, nil
}
func closeExampleSession(session *cpace.Session) {
if err := session.Close(); err != nil {
panic(err)
}
}
func closeExampleInitiator(initiator *cpace.Initiator) {
if err := initiator.Close(); err != nil {
panic(err)
}
}
func closeExampleResponder(responder *cpace.Responder) {
if err := responder.Close(); err != nil {
panic(err)
}
}
Output: 64 true