Help us shape the future of Go! Take the 2026 Go Developer Survey

cpace

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 13, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

README

cpace

CPace, the balanced PAKE of draft-irtf-cfrg-cpace-21, in Go. Checked byte for byte against the draft's test vectors.

Go Reference CI Security License


Two parties who share a short password — a code read out over the phone — agree on a strong key over a channel they do not trust. Someone in the middle gets one guess per attempt, learns nothing it can test offline, and makes the exchange fail on both ends.

import "github.com/mendsec/cpace"

Status

  • Specification: draft-irtf-cfrg-cpace-21 (April 2026), cipher suite CPACE-RISTRETTO255-SHA512. CPace is a CFRG draft, not yet an RFC; when the draft changes, this package follows it, and the version says so.
  • Verification: every step is checked against the draft's own test vectors (below). The code has not had an independent audit. Read it — it is short — before you rely on it.
  • API: pre-1.0; it may change.

Usage

// The party who typed the code.
ini, start, err := cpace.NewInitiator(cpace.Config{
	PRS: []byte("5071"),                    // the shared password; never sent
	CI:  []byte("example.org support"),     // what this is for; never sent
	SID: sessionID,                         // unique to the session, known to both
	AD:  alicePublicKey,                    // sent, and authenticated
})

// The party who read it out.
res, reply, err := cpace.NewResponder(bobConfig, start)

// Back at the initiator: checks the responder's confirmation tag.
confirm, resultA, err := ini.Finish(reply) // cpace.ErrAuthFailed on a wrong code

// Back at the responder: checks the initiator's.
resultB, err := res.Finish(confirm)

// resultA.ISK == resultB.ISK; resultA.PeerAD is Bob's AD, resultB.PeerAD Alice's.

start, reply and confirm are opaque bytes: carry them over a WebSocket, a relay, a QR code, anything.

A typical use is binding public keys at first contact: each party puts its signing key in AD, and after Finish each knows the other's key came from whoever holds the same code. A carrier that swaps the keys makes the exchange fail. remotekit's pake package does exactly that for Chirp.

Using it well

  • Limit attempts and show every failure. A 4-digit code gives an attacker one chance in 10,000 per attempt — a good bound only if attempts are bounded.
  • Identities: put both in CI when both are known in advance (initiator first); otherwise put each party's own in its AD and check PeerAD (draft §10.1).
  • SID: unique to the session and known to both, such as randomness both contributed (§10.9).
  • ISK: run it through a KDF, such as HKDF, before use (§10.3).

The exchange

The draft's initiator-responder setting, with the explicit key confirmation of §10.4:

initiator                                   responder
NewInitiator   -- start:   Ya, ADa  -->     NewResponder
               <-- reply:  Yb, ADb, Tb --
Finish         -- confirm: Ta       -->     Finish
Value Definition
generator g ristretto255 one-way map of SHA-512(generator_string(DSI, PRS, CI, SID))
Ya, Yb ya·g, yb·g, with y sampled as the draft recommends (252 random bits)
K ya·Yb = yb·Ya; the exchange aborts if Y does not decode or K is the identity
ISK SHA-512(lv_cat(DSI‖"_ISK", SID, K) ‖ transcript_ir(Ya, ADa, Yb, ADb))
mac_key SHA-512("CPaceMac" ‖ SID ‖ ISK)
Ta, Tb HMAC-SHA-512(mac_key, lv_cat(Y, AD)) of each party's own message

Wire format: one tag byte (0x01 start, 0x02 reply, 0x03 confirm), then the fields, each prefixed with its length in LEB128 — the draft's own lv_cat. Fields are at most 64 KiB.

Correctness

The tests reproduce the draft's appendices, byte for byte:

Draft What is checked
A.1.2, A.1.4 prepend_len (LEB128, including the 127/128 boundary), lv_cat
B.3.1 generator_string, its SHA-512, the generator
B.3.2, B.3.3 Ya, Yb from the draft's scalars
B.3.4 K on both sides
B.3.5 transcript_ir and ISK
B.3.10, B.3.11 scalar_mult_vfy on a valid point, a non-canonical encoding and the identity

A whole exchange through the public API, with the draft's scalars, ends in the draft's ISK. Beyond the vectors, tests cover:

  • a wrong password, CI or SID, failing on both ends;
  • AD altered in either direction;
  • a man in the middle running each side under its own guess;
  • replayed confirmations;
  • malformed messages.

Both entry points that read untrusted bytes are fuzzed.

What it rests on

Group github.com/gtank/ristretto255 — RFC 9496, BSD-3-Clause
Field and curve filippo.io/edwards25519 — BSD-3-Clause
Hash, HMAC Go's standard library

This package does no arithmetic of its own, and all operations on secrets are constant-time in those libraries.

Why another implementation

When this was written (October 2026), none of the Go implementations followed the current draft:

  • bytemare/cpace builds the generator string and ISK differently and tests against vectors it generates itself.
  • filippo.io/cpace follows draft-haase-cpace-01 (2020).
  • schollz/pake is a SPAKE2 variant, not CPace.

The review is in barahn/remotekit#96.

Development

go test -race ./...
go test -run xxx -fuzz FuzzNewResponder -fuzztime 30s
golangci-lint run

The default branch is develop; releases are tagged from main. See CONTRIBUTING.md and SECURITY.md.

License

Apache-2.0. Copyright © 2026 Fabrintek Engenharia Digital Ltda. See LICENSE and NOTICE.

Documentation

Overview

Package cpace implements CPace, the balanced password-authenticated key exchange of draft-irtf-cfrg-cpace-21, in its CPACE-RISTRETTO255-SHA512 cipher suite.

Two parties who share a low-entropy password -- a short code read out over the phone -- agree on a strong shared key over a channel they do not trust. Someone in the middle gets one guess at the password per attempt, learns nothing it can test offline, and makes the exchange fail on both ends.

The exchange

This package runs the initiator-responder setting of the draft, with the explicit key confirmation of its section 10.4, in three messages:

initiator                                 responder
NewInitiator  -- start:   Ya, ADa  -->    NewResponder
              <-- reply:  Yb, ADb, Tb --
Finish        -- confirm: Ta       -->    Finish

The messages are opaque bytes; carry them however you like. The password (PRS), the channel identifier (CI) and the session identifier (SID) never travel; the associated data (AD) travels in the clear and is authenticated. A party that finishes knows that the other holds the same PRS, CI and SID, and has the other's AD as it was sent. Confirmation tags are checked before anything is returned, so a wrong password fails on both ends: the initiator on the reply, the responder on the confirmation.

What it rests on

The group is ristretto255 (RFC 9496) from github.com/gtank/ristretto255, on filippo.io/edwards25519; this package does no arithmetic of its own. Every step -- prepend_len, lv_cat, the generator string, the generator, Ya, Yb, K, transcript_ir, ISK, and the handling of invalid points -- is checked byte for byte against the draft's test vectors (appendices A.1 and B.3), and so is a whole exchange run with the draft's scalars.

Using it well

  • Limit attempts and show every failure: with a 4-digit password, each attempt is one chance in 10,000 for an attacker.
  • Put both parties' identities in CI when both know them in advance (initiator first); otherwise put each party's identity, or public key, in its own AD and check what Result.PeerAD says (section 10.1).
  • Use a SID unique to the session that both parties know, such as randomness both contributed (section 10.9).
  • Run Result.ISK through a KDF before use (section 10.3).
Example
package main

import (
	"bytes"
	"fmt"

	"github.com/mendsec/cpace"
)

func main() {
	ci := []byte("example.org remote support")
	sid := []byte("3f6c9a1d2b7e4058")

	// The party who typed the code starts.
	alice := cpace.Config{PRS: []byte("5071"), CI: ci, SID: sid, AD: []byte("alice's public key")}
	ini, start, err := cpace.NewInitiator(alice)
	if err != nil {
		panic(err)
	}

	// The party who read it out answers. start, reply and confirm are what
	// travel between them.
	bob := cpace.Config{PRS: []byte("5071"), CI: ci, SID: sid, AD: []byte("bob's public key")}
	res, reply, err := cpace.NewResponder(bob, start)
	if err != nil {
		panic(err)
	}

	confirm, a, err := ini.Finish(reply)
	if err != nil {
		panic(err) // ErrAuthFailed: a wrong code, or someone in the middle
	}
	b, err := res.Finish(confirm)
	if err != nil {
		panic(err)
	}

	fmt.Println(bytes.Equal(a.ISK, b.ISK))
	fmt.Println(string(a.PeerAD))
	fmt.Println(string(b.PeerAD))
}
Output:
true
bob's public key
alice's public key

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrAuthFailed means the other party did not prove it holds the same
	// password, channel identifier and session identifier: the password
	// was mistyped, or someone in the middle tried to take part. The two
	// cases cannot be told apart, on purpose.
	ErrAuthFailed = errors.New("cpace: the other party does not hold the same password")
	// ErrMalformed means a message is not one of this protocol's, or is
	// out of place.
	ErrMalformed = errors.New("cpace: malformed message")
	// ErrNoPassword means a Config has no password.
	ErrNoPassword = errors.New("cpace: no password")
)

Errors a caller can act on.

Functions

This section is empty.

Types

type Config

type Config struct {
	// PRS is the password-related string both parties share. It never
	// travels.
	PRS []byte
	// CI is the channel identifier: what this exchange is for, and the
	// parties' identities when both know them in advance, initiator first
	// (draft section 10.1). It is hashed, never sent.
	CI []byte
	// SID is the session identifier. It should be unique to the session
	// and known to both parties (draft section 10.9); it may be empty.
	SID []byte
	// AD is this party's associated data. It is sent in the clear and
	// authenticated: the other party learns it as Result.PeerAD only if
	// the exchange succeeds. A party's own identity or public key goes
	// here when CI cannot carry it.
	AD []byte
}

Config is one party's inputs. Both parties must give the same PRS, CI and SID, or the exchange fails as a wrong password does.

type Initiator

type Initiator struct {
	// contains filtered or unexported fields
}

Initiator is the party that speaks first.

func NewInitiator

func NewInitiator(cfg Config) (*Initiator, []byte, error)

NewInitiator starts an exchange and returns the first message, for the responder.

func (*Initiator) Finish

func (i *Initiator) Finish(reply []byte) ([]byte, Result, error)

Finish reads the responder's reply. If the responder proves it holds the same password, Finish returns the last message, for the responder, and the result; otherwise ErrAuthFailed, and nothing is to be sent. An Initiator finishes once.

type Responder

type Responder struct {
	// contains filtered or unexported fields
}

Responder is the party that answers.

func NewResponder

func NewResponder(cfg Config, start []byte) (*Responder, []byte, error)

NewResponder answers the initiator's first message and returns the reply, for the initiator. The exchange is not authenticated until Finish accepts the initiator's confirmation.

func (*Responder) Finish

func (r *Responder) Finish(confirm []byte) (Result, error)

Finish reads the initiator's confirmation. It returns the result if the initiator proves it holds the same password, and ErrAuthFailed otherwise. A Responder finishes once; a second call fails.

type Result

type Result struct {
	// ISK is the 64-byte intermediate session key. Run it through a KDF
	// before use (draft section 10.3).
	ISK []byte
	// PeerAD is the other party's associated data, authenticated.
	PeerAD []byte
}

Result is what a successful exchange gives each party.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL