Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Alternatively, look at the [Cloudflare Go](https://github.com/cloudflare/go/tree
- [OT](./ot/simot): Simplest Oblivious Transfer ([ia.cr/2015/267]).
- [Threshold RSA](./tss/rsa) Signatures ([Shoup Eurocrypt 2000](https://www.iacr.org/archive/eurocrypt2000/1807/18070209-new.pdf)).
- [Prio3](./vdaf/prio3) Verifiable Distributed Aggregation Function ([draft-irtf-cfrg-vdaf](https://datatracker.ietf.org/doc/draft-irtf-cfrg-vdaf/)).
- [ECMR](./ecmr): McCallum-Relyea key exchange for Tang/Clevis.

### Post-Quantum Cryptography

Expand Down
137 changes: 137 additions & 0 deletions ecmr/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package ecmr

import (
"io"

"github.com/cloudflare/circl/group"
)

type Client struct{}

func NewClient() *Client {
return &Client{}
}

// Provision generates a new client key pair and computes the shared point
// with the server's public key.
func (c *Client) Provision(serverPub *PublicKey, rnd io.Reader) (*ProvisionResult, error) {
if serverPub == nil || serverPub.element == nil {
return nil, ErrNilKey
}
if rnd == nil {
return nil, ErrNilReader
}

clientScalar := group.P521.RandomNonZeroScalar(rnd)

clientPub := group.P521.NewElement().MulGen(clientScalar)
clientPubBytes, err := clientPub.MarshalBinary()
if err != nil {
return nil, ErrMalformedPoint
}

sharedPoint := group.P521.NewElement().Mul(serverPub.element, clientScalar)
sharedPointBytes, err := sharedPoint.MarshalBinary()
if err != nil {
return nil, ErrMalformedPoint
}

zeroScalar(clientScalar)

return &ProvisionResult{
ClientPublic: clientPubBytes,
SharedPoint: sharedPointBytes,
}, nil
}

// CreateRecoveryRequest creates a blinded recovery request using the stored
// client public key and a fresh ephemeral scalar.
func (c *Client) CreateRecoveryRequest(
clientPublicBytes []byte,
serverPub *PublicKey,
rnd io.Reader,
) (*RecoveryRequest, *RecoveryState, error) {
if serverPub == nil || serverPub.element == nil {
return nil, nil, ErrNilKey
}
if rnd == nil {
return nil, nil, ErrNilReader
}

if len(clientPublicBytes) != PublicKeySize {
return nil, nil, ErrMalformedPoint
}

clientPub := group.P521.NewElement()
if err := clientPub.UnmarshalBinary(clientPublicBytes); err != nil {
return nil, nil, ErrMalformedPoint
}
if clientPub.IsIdentity() {
return nil, nil, ErrIdentityPoint
}

ephemeral := group.P521.RandomNonZeroScalar(rnd)

ephemeralPub := group.P521.NewElement().MulGen(ephemeral)

blindedPoint := group.P521.NewElement().Add(clientPub, ephemeralPub)
blindedPointBytes, err := blindedPoint.MarshalBinary()
if err != nil {
zeroScalar(ephemeral)
return nil, nil, ErrMalformedPoint
}

state := &RecoveryState{
ephemeral: ephemeral,
serverPub: group.P521.NewElement().Set(serverPub.element),
}

return &RecoveryRequest{BlindedPoint: blindedPointBytes}, state, nil
}

// RecoverKey completes key recovery using the server's response.
// After calling this function, the RecoveryState is invalidated.
func (c *Client) RecoverKey(
state *RecoveryState,
response *RecoveryResponse,
) ([]byte, error) {
if state == nil || state.ephemeral == nil || state.serverPub == nil {
return nil, ErrNilKey
}

defer func() {
zeroScalar(state.ephemeral)
state.ephemeral = nil
state.serverPub = nil
}()

if response == nil || len(response.ProcessedPoint) != SharedPointSize {
return nil, ErrMalformedPoint
}

serverResponse := group.P521.NewElement()
if err := serverResponse.UnmarshalBinary(response.ProcessedPoint); err != nil {
return nil, ErrMalformedPoint
}
if serverResponse.IsIdentity() {
return nil, ErrIdentityPoint
}

blindingFactor := group.P521.NewElement().Mul(state.serverPub, state.ephemeral)

negBlindingFactor := group.P521.NewElement().Neg(blindingFactor)
sharedPoint := group.P521.NewElement().Add(serverResponse, negBlindingFactor)

sharedPointBytes, err := sharedPoint.MarshalBinary()
if err != nil {
return nil, ErrMalformedPoint
}

return sharedPointBytes, nil
}

func zeroScalar(s group.Scalar) {
if s != nil {
s.SetUint64(0)
}
}
26 changes: 26 additions & 0 deletions ecmr/compat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ecmr

import (
"github.com/cloudflare/circl/group"
)

// ExtractX extracts the x-coordinate from an uncompressed P-521 point.
// It validates the point is on-curve and not the identity before extracting.
func ExtractX(uncompressedPoint []byte) ([]byte, error) {
element := group.P521.NewElement()
if err := element.UnmarshalBinary(uncompressedPoint); err != nil {
return nil, ErrMalformedPoint
}
if element.IsIdentity() {
return nil, ErrIdentityPoint
}

canonical, err := element.MarshalBinary()
if err != nil {
return nil, ErrMalformedPoint
}

x := make([]byte, XCoordinateSize)
copy(x, canonical[1:1+XCoordinateSize])
return x, nil
}
45 changes: 45 additions & 0 deletions ecmr/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Package ecmr implements the McCallum-Relyea key exchange protocol for P-521.
//
// This protocol is used by Tang/Clevis for network-bound disk encryption (NBDE).
// It allows a client to derive a shared secret with a server's help, without
// the server ever learning the secret.
//
// # Timing Properties
//
// The scalar operations in this package (multiplication, addition) use CIRCL's
// group.P521, which delegates to Go's crypto/ecdh for constant-time scalar
// multiplication.
//
// IMPORTANT: Point serialization and validation are NOT constant-time due to
// limitations in the underlying group package:
// - MarshalBinary: calls big.Int.Mod and ecdsa.PublicKey.ECDH()
// - UnmarshalBinary: uses big.Int for coordinate parsing and curve checks
//
// Both operations may leak timing information about point coordinates. For
// Tang/Clevis deployments where the threat model is network-based key escrow,
// this is typically acceptable. Evaluate whether this meets your requirements.
//
// # Subgroup Membership
//
// P-521 is a prime-order curve (cofactor = 1). Every point validated as on-curve
// is automatically in the prime-order subgroup. No additional cofactor clearing
// or subgroup checks are needed.
//
// # Tang/Clevis Interoperability
//
// For Tang compatibility:
// 1. Call Provision or RecoverKey to get SharedPoint (133 bytes, uncompressed)
// 2. Extract x-coordinate: x, err := ecmr.ExtractX(sharedPoint)
// 3. Apply Concat KDF (RFC 7518 §4.6) with x as the shared secret
//
// ExtractX validates the point is on-curve before extracting, preventing
// corrupted stored state from producing invalid keys. Note that this validation
// uses variable-time operations (see Timing Properties above).
//
// # Supported Curves
//
// Only P-521 is supported. The API uses concrete types with no curve parameters.
// All key construction goes through GenerateKey or UnmarshalBinary, which
// exclusively use group.P521. Zero-value structs (e.g., &PublicKey{}) will fail
// at runtime with ErrNilKey.
package ecmr
42 changes: 42 additions & 0 deletions ecmr/ecmr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package ecmr

import (
"errors"

"github.com/cloudflare/circl/group"
)

const (
PublicKeySize = 133
PrivateKeySize = 66
SharedPointSize = 133
UncompressedPointSize = 133
XCoordinateSize = 66
)

var (
ErrMalformedPoint = errors.New("ecmr: malformed point encoding")
ErrIdentityPoint = errors.New("ecmr: identity point not allowed")
ErrMalformedScalar = errors.New("ecmr: malformed scalar encoding")
ErrZeroScalar = errors.New("ecmr: zero scalar not allowed")
ErrNilReader = errors.New("ecmr: nil random reader")
ErrNilKey = errors.New("ecmr: nil or uninitialized key")
)

type ProvisionResult struct {
ClientPublic []byte
SharedPoint []byte
}

type RecoveryRequest struct {
BlindedPoint []byte
}

type RecoveryResponse struct {
ProcessedPoint []byte
}

type RecoveryState struct {
ephemeral group.Scalar
serverPub group.Element
}
Loading