|
package main |
|
|
|
import ( |
|
"crypto/ed25519" |
|
"crypto/rand" |
|
"crypto/sha512" |
|
"encoding/hex" |
|
"errors" |
|
"fmt" |
|
"io" |
|
"os" |
|
|
|
"log/slog" |
|
|
|
"filippo.io/edwards25519" |
|
) |
|
|
|
// https://www.cossacklabs.com/blog/introducing_secure_comparator/ |
|
// https://www.cossacklabs.com/files/secure-comparator-paper-rev12.pdf |
|
|
|
type Server struct { |
|
a2, a3 ed25519.PrivateKey |
|
g2a, g3a ed25519.PublicKey |
|
pA, qA, pB, qB *edwards25519.Point |
|
} |
|
|
|
func (p *Server) Bootstrap() ([]byte, error) { |
|
// Generate a2, a3 |
|
var err error |
|
p.g2a, p.a2, err = ed25519.GenerateKey(rand.Reader) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to generate ephemeral key pair: %w", err) |
|
} |
|
p.g3a, p.a3, err = ed25519.GenerateKey(rand.Reader) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to generate ephemeral key pair: %w", err) |
|
} |
|
|
|
// G2a || SIGN(a2, G2a) || G3a || SIGN(a3, G3a) |
|
var out []byte |
|
out = append(out, p.g2a...) |
|
out = append(out, ed25519.Sign(p.a2, p.g2a)...) |
|
out = append(out, p.g3a...) |
|
out = append(out, ed25519.Sign(p.a3, p.g3a)...) |
|
|
|
return out, nil |
|
} |
|
|
|
func (p *Server) Prove(proof, secret []byte) ([]byte, error) { |
|
// Ensure expected length |
|
// G2b || G2b signature || G3b || G3b signature || Pb || Qb |
|
if len(proof) < 2*(ed25519.PublicKeySize+ed25519.SignatureSize)+2*ed25519.PublicKeySize { |
|
return nil, errors.New("message too short") |
|
} |
|
|
|
// Ensure valid signatures |
|
var ( |
|
g2b = proof[:ed25519.PublicKeySize] |
|
g2bSig = proof[ed25519.PublicKeySize : ed25519.PublicKeySize+ed25519.SignatureSize] |
|
g3b = proof[ed25519.PublicKeySize+ed25519.SignatureSize : ed25519.PublicKeySize+ed25519.SignatureSize+ed25519.PublicKeySize] |
|
g3bSig = proof[ed25519.PublicKeySize+ed25519.SignatureSize+ed25519.PublicKeySize : ed25519.PublicKeySize+ed25519.SignatureSize+ed25519.PublicKeySize+ed25519.SignatureSize] |
|
pb = proof[2*(ed25519.PublicKeySize+ed25519.SignatureSize) : 2*(ed25519.PublicKeySize+ed25519.SignatureSize)+ed25519.PublicKeySize] |
|
qb = proof[2*(ed25519.PublicKeySize+ed25519.SignatureSize)+ed25519.PublicKeySize : 2*(ed25519.PublicKeySize+ed25519.SignatureSize)+2*ed25519.PublicKeySize] |
|
) |
|
|
|
if !ed25519.Verify(ed25519.PublicKey(g2b), g2b, g2bSig) { |
|
return nil, errors.New("invalid proof of possession") |
|
} |
|
if !ed25519.Verify(ed25519.PublicKey(g3b), g3b, g3bSig) { |
|
return nil, errors.New("invalid proof of possession") |
|
} |
|
|
|
// Compute generator groups |
|
a2, err := privateToScalar(p.a2) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to initialize a scalar from ephemeral key: %w", err) |
|
} |
|
g2bp, err := (&edwards25519.Point{}).SetBytes(g2b) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to initialize a point from ephemeral public key: %w", err) |
|
} |
|
G2 := (&edwards25519.Point{}).ScalarMult(a2, g2bp) |
|
|
|
a3, err := privateToScalar(p.a3) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to initialize a scalar from ephemeral key: %w", err) |
|
} |
|
g3bp, err := (&edwards25519.Point{}).SetBytes(g3b) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to initialize a point from ephemeral public key: %w", err) |
|
} |
|
G3 := (&edwards25519.Point{}).ScalarMult(a3, g3bp) |
|
|
|
var challenge [32]byte |
|
if _, err := io.ReadFull(rand.Reader, challenge[:]); err != nil { |
|
return nil, fmt.Errorf("unable to generate random challenge: %w", err) |
|
} |
|
s, err := edwards25519.NewScalar().SetBytesWithClamping(challenge[:]) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to create a scalar from random challenge: %w", err) |
|
} |
|
|
|
// Compute secret |
|
h := sha512.Sum512_256(secret) |
|
x, err := edwards25519.NewScalar().SetBytesWithClamping(h[:]) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to prepare secret as scalar: %w", err) |
|
} |
|
|
|
// Convert to point for arithmetic |
|
p.qB, err = (&edwards25519.Point{}).SetBytes(qb) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to prepare secret public key as point: %w", err) |
|
} |
|
p.pB, err = (&edwards25519.Point{}).SetBytes(pb) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to prepare secret public key as point: %w", err) |
|
} |
|
|
|
// Pa = s * G3 |
|
p.pA = (&edwards25519.Point{}).ScalarMult(s, G3) |
|
// Qa = x * G2 + s * G |
|
p.qA = (&edwards25519.Point{}).VarTimeDoubleScalarBaseMult(x, G2, s) |
|
// Ra = a3 * (Qa - Qb) |
|
Ra := (&edwards25519.Point{}).ScalarMult(a3, (&edwards25519.Point{}).Subtract(p.qA, p.qB)) |
|
|
|
var out []byte |
|
out = append(out, p.pA.Bytes()...) |
|
out = append(out, p.qA.Bytes()...) |
|
out = append(out, Ra.Bytes()...) |
|
|
|
return out, nil |
|
} |
|
|
|
func (p *Server) Verify(proof []byte) (bool, error) { |
|
// Ensure expected length |
|
// Rb |
|
if len(proof) < ed25519.PublicKeySize { |
|
return false, errors.New("message too short") |
|
} |
|
|
|
a3, err := privateToScalar(p.a3) |
|
if err != nil { |
|
return false, fmt.Errorf("unable to initialize a scalar from ephemeral key: %w", err) |
|
} |
|
|
|
Rb, _ := (&edwards25519.Point{}).SetBytes(proof) |
|
// Rab = a3 * Rb |
|
Rab := (&edwards25519.Point{}).ScalarMult(a3, Rb) |
|
|
|
// CHECK( Rb == (Pa - Pb) ) |
|
if (&edwards25519.Point{}).Subtract(p.pA, p.pB).Equal(Rab) != 1 { |
|
return false, errors.New("invalid proof") |
|
} |
|
|
|
return true, nil |
|
} |
|
|
|
type Client struct { |
|
b2, b3 ed25519.PrivateKey |
|
g2b, g3b ed25519.PublicKey |
|
p, q *edwards25519.Point |
|
} |
|
|
|
func (v *Client) Proof(in, secret []byte) ([]byte, error) { |
|
// Ensure expected length |
|
// g2a || g2a signature || g3a || g3a signature |
|
if len(in) < 2*(ed25519.PublicKeySize+ed25519.SignatureSize) { |
|
return nil, errors.New("message too short") |
|
} |
|
|
|
// Ensure valid signatures |
|
var ( |
|
g2a = in[:ed25519.PublicKeySize] |
|
g2aSig = in[ed25519.PublicKeySize : ed25519.PublicKeySize+ed25519.SignatureSize] |
|
g3a = in[ed25519.PublicKeySize+ed25519.SignatureSize : ed25519.PublicKeySize+ed25519.SignatureSize+ed25519.PublicKeySize] |
|
g3aSig = in[ed25519.PublicKeySize+ed25519.SignatureSize+ed25519.PublicKeySize : ed25519.PublicKeySize+ed25519.SignatureSize+ed25519.PublicKeySize+ed25519.SignatureSize] |
|
) |
|
if !ed25519.Verify(ed25519.PublicKey(g2a), g2a, g2aSig) { |
|
return nil, errors.New("invalid proof of possession") |
|
} |
|
if !ed25519.Verify(ed25519.PublicKey(g3a), g3a, g3aSig) { |
|
return nil, errors.New("invalid proof of possession") |
|
} |
|
|
|
// Generate b2, b3 |
|
var err error |
|
v.g2b, v.b2, err = ed25519.GenerateKey(rand.Reader) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to generate ephemeral key pair: %w", err) |
|
} |
|
v.g3b, v.b3, err = ed25519.GenerateKey(rand.Reader) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to generate ephemeral key pair: %w", err) |
|
} |
|
|
|
// Convert to scalars for arithmetric |
|
b2, err := privateToScalar(v.b2) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to initialize a scalar from ephemeral key: %w", err) |
|
} |
|
g2ap, _ := (&edwards25519.Point{}).SetBytes(g2a) |
|
G2 := (&edwards25519.Point{}).ScalarMult(b2, g2ap) |
|
|
|
b3, err := privateToScalar(v.b3) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to initialize a scalar from ephemeral key: %w", err) |
|
} |
|
g3ap, _ := (&edwards25519.Point{}).SetBytes(g3a) |
|
G3 := (&edwards25519.Point{}).ScalarMult(b3, g3ap) |
|
|
|
var challenge [32]byte |
|
if _, err := io.ReadFull(rand.Reader, challenge[:]); err != nil { |
|
return nil, fmt.Errorf("unable to generate random challenge: %w", err) |
|
} |
|
r, _ := edwards25519.NewScalar().SetBytesWithClamping(challenge[:]) |
|
|
|
// Compute secret (use SH512/256) |
|
h := sha512.Sum512_256(secret) |
|
y, err := edwards25519.NewScalar().SetBytesWithClamping(h[:]) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to prepare secret as scalar: %w", err) |
|
} |
|
|
|
// Pb = r * G3 |
|
v.p = (&edwards25519.Point{}).ScalarMult(r, G3) |
|
// Qb = y * G2 + r * G |
|
v.q = (&edwards25519.Point{}).VarTimeDoubleScalarBaseMult(y, G2, r) |
|
|
|
var out []byte |
|
out = append(out, v.g2b...) |
|
out = append(out, ed25519.Sign(v.b2, v.g2b)...) |
|
out = append(out, v.g3b...) |
|
out = append(out, ed25519.Sign(v.b3, v.g3b)...) |
|
out = append(out, v.p.Bytes()...) |
|
out = append(out, v.q.Bytes()...) |
|
|
|
return out, nil |
|
} |
|
|
|
func (v *Client) Verify(proof []byte) ([]byte, error) { |
|
// Ensure expected length |
|
// Pa || Qa || Ra |
|
if len(proof) < 3*(ed25519.PublicKeySize) { |
|
return nil, errors.New("message too short") |
|
} |
|
|
|
// Ensure valid signatures |
|
var ( |
|
pa = proof[:ed25519.PublicKeySize] |
|
qa = proof[ed25519.PublicKeySize : 2*ed25519.PublicKeySize] |
|
ra = proof[2*ed25519.PublicKeySize : 3*ed25519.PublicKeySize] |
|
) |
|
|
|
b3, err := privateToScalar(v.b3) |
|
if err != nil { |
|
return nil, fmt.Errorf("unable to initialize a scalar from ephemeral key: %w", err) |
|
} |
|
|
|
Pa, _ := (&edwards25519.Point{}).SetBytes(pa) |
|
Qa, _ := (&edwards25519.Point{}).SetBytes(qa) |
|
Ra, _ := (&edwards25519.Point{}).SetBytes(ra) |
|
|
|
// Rb = b3 * (Qa - Qb) |
|
Rb := (&edwards25519.Point{}).ScalarMult(b3, (&edwards25519.Point{}).Subtract(Qa, v.q)) |
|
// Rab = b3 * Ra |
|
Rab := (&edwards25519.Point{}).ScalarMult(b3, Ra) |
|
|
|
// CHECK( Rab == (Pa - Pb) ) |
|
if (&edwards25519.Point{}).Subtract(Pa, v.p).Equal(Rab) != 1 { |
|
return nil, errors.New("invalid proof") |
|
} |
|
|
|
return Rb.Bytes(), nil |
|
} |
|
|
|
func privateToScalar(pk ed25519.PrivateKey) (*edwards25519.Scalar, error) { |
|
h := sha512.Sum512(pk.Seed()) |
|
s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32]) |
|
return s, err |
|
} |
|
|
|
func main() { |
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{})) |
|
slog.SetDefault(logger) |
|
|
|
c1 := &Client{} |
|
logger.Info("C -> S: Session boostrap") |
|
|
|
session := &Server{} |
|
step1, err := session.Bootstrap() |
|
if err != nil { |
|
logger.Error("unable to initialize prover", "error", err) |
|
return |
|
} |
|
|
|
logger.Info("S -> C: Session public keys") |
|
fmt.Println(hex.Dump(step1)) |
|
|
|
step2, err := c1.Proof(step1, []byte("very-secret-password")) |
|
if err != nil { |
|
logger.Error("unable to initialize prover", "error", err) |
|
return |
|
} |
|
|
|
logger.Info("C -> S: Client proof") |
|
fmt.Println(hex.Dump(step2)) |
|
|
|
step3, err := session.Prove(step2, []byte("very-secret-password")) |
|
if err != nil { |
|
logger.Error("unable to compute proof", "error", err) |
|
return |
|
} |
|
|
|
logger.Info("S -> C: Session proof") |
|
fmt.Println(hex.Dump(step3)) |
|
|
|
step4, err := c1.Verify(step3) |
|
if err != nil { |
|
logger.Error("unable to validate proof", "error", err) |
|
return |
|
} |
|
|
|
logger.Info("C -> S : Authenticate session proof") |
|
fmt.Println(hex.Dump(step4)) |
|
|
|
valid, err := session.Verify(step4) |
|
if err != nil { |
|
logger.Error("unable to authenticate proof", "error", err) |
|
return |
|
} |
|
|
|
logger.Info("S: Client Authenticated?") |
|
fmt.Println(valid) |
|
} |