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. Config 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.
Config.SessionID must be non-empty by default. Config.AllowEmptySessionID is only for draft-21 compatibility tests or deliberately compatible profiles that accept weaker replay and transcript separation.
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.
Both parties must use the same role orientation: InitiatorID identifies the Start side, and ResponderID identifies the Respond side. 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() {
common := cpace.Config{
Password: []byte("correct horse battery staple"),
InitiatorID: []byte("client@example"),
ResponderID: []byte("server@example"),
Context: []byte("example protocol v1"),
SessionID: []byte("session-1234"),
}
initCfg := common
initCfg.AssociatedData = []byte("client hello")
initiator, msgA, err := cpace.Start(initCfg)
if err != nil {
panic(err)
}
respCfg := common
respCfg.AssociatedData = []byte("server hello")
responder, msgB, err := cpace.Respond(respCfg, msgA)
if err != nil {
panic(err)
}
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()))
}
Output: true true
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrInvalidInput reports invalid local configuration or parameters. ErrInvalidInput = errors.New("cpace: invalid input") // ErrEmptySessionID reports an empty Config.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") // ErrConfirmationFailed reports failed explicit key confirmation. ErrConfirmationFailed = errors.New("cpace: key confirmation failed") )
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
Password []byte
InitiatorID []byte
ResponderID []byte
Context []byte
SessionID []byte
AssociatedData []byte
AllowEmptySessionID bool
}
Config contains the local inputs for one CPace role.
Password, InitiatorID, and ResponderID must be non-empty. Context and AssociatedData may be empty. Both parties must use the same role orientation: InitiatorID is the party that called Start, and ResponderID is the party that called Respond. 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. The AssociatedData field is ADa for Start and ADb for Respond. Field lengths are capped at 4 KiB for Password and IDs, 1 KiB for Context and SessionID, and 64 KiB for AssociatedData. Inputs exceeding these caps are rejected before copying; accepted byte slices are copied by Start and Respond before use.
type Initiator ¶
type Initiator struct {
// contains filtered or unexported fields
}
Initiator is a single-use initiator state returned by Start.
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() {
common := exampleConfig("example-confirmation-failure")
initCfg := common
initCfg.AssociatedData = []byte("client hello")
initiator, msgA, err := cpace.Start(initCfg)
if err != nil {
panic(err)
}
respCfg := common
respCfg.Context = []byte("different protocol context")
respCfg.AssociatedData = []byte("server hello")
_, msgB, err := cpace.Respond(respCfg, msgA)
if err != nil {
panic(err)
}
_, _, err = initiator.Finish(msgB)
fmt.Println(errors.Is(err, cpace.ErrConfirmationFailed))
}
func exampleConfig(sessionID string) cpace.Config {
return cpace.Config{
Password: []byte("correct horse battery staple"),
InitiatorID: []byte("client@example"),
ResponderID: []byte("server@example"),
Context: []byte("example protocol v1"),
SessionID: []byte(sessionID),
}
}
Output: true
type Responder ¶
type Responder struct {
// contains filtered or unexported fields
}
Responder is a single-use responder state returned by Respond.
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. 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 exampleConfig(sessionID string) cpace.Config {
return cpace.Config{
Password: []byte("correct horse battery staple"),
InitiatorID: []byte("client@example"),
ResponderID: []byte("server@example"),
Context: []byte("example protocol v1"),
SessionID: []byte(sessionID),
}
}
func exampleConfirmedSessions(sessionID string) (*cpace.Session, *cpace.Session, error) {
common := exampleConfig(sessionID)
initCfg := common
initCfg.AssociatedData = []byte("client hello")
initiator, msgA, err := cpace.Start(initCfg)
if err != nil {
return nil, nil, err
}
respCfg := common
respCfg.AssociatedData = []byte("server hello")
responder, msgB, err := cpace.Respond(respCfg, msgA)
if err != nil {
return nil, nil, err
}
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)
}
}
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.
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 exampleConfig(sessionID string) cpace.Config {
return cpace.Config{
Password: []byte("correct horse battery staple"),
InitiatorID: []byte("client@example"),
ResponderID: []byte("server@example"),
Context: []byte("example protocol v1"),
SessionID: []byte(sessionID),
}
}
func exampleConfirmedSessions(sessionID string) (*cpace.Session, *cpace.Session, error) {
common := exampleConfig(sessionID)
initCfg := common
initCfg.AssociatedData = []byte("client hello")
initiator, msgA, err := cpace.Start(initCfg)
if err != nil {
return nil, nil, err
}
respCfg := common
respCfg.AssociatedData = []byte("server hello")
responder, msgB, err := cpace.Respond(respCfg, msgA)
if err != nil {
return nil, nil, err
}
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)
}
}
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 Config; 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 exampleConfig(sessionID string) cpace.Config {
return cpace.Config{
Password: []byte("correct horse battery staple"),
InitiatorID: []byte("client@example"),
ResponderID: []byte("server@example"),
Context: []byte("example protocol v1"),
SessionID: []byte(sessionID),
}
}
func exampleConfirmedSessions(sessionID string) (*cpace.Session, *cpace.Session, error) {
common := exampleConfig(sessionID)
initCfg := common
initCfg.AssociatedData = []byte("client hello")
initiator, msgA, err := cpace.Start(initCfg)
if err != nil {
return nil, nil, err
}
respCfg := common
respCfg.AssociatedData = []byte("server hello")
responder, msgB, err := cpace.Respond(respCfg, msgA)
if err != nil {
return nil, nil, err
}
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)
}
}
Output: 64 true
type Suite ¶
type Suite byte
Suite identifies a CPace ciphersuite in this package's wire framing.
const ( // DraftVersion identifies the CPace Internet-Draft revision implemented by // this package. DraftVersion = "draft-irtf-cfrg-cpace-21" // SuiteCPaceRistretto255SHA512 is the only suite implemented by v1 of this // package. SuiteCPaceRistretto255SHA512 Suite = 0x01 )