Repository navigation
Expand file tree
/
Copy pathcpace.go
More file actions
132 lines (113 loc) · 4.25 KB
/
Copy pathcpace.go
File metadata and controls
132 lines (113 loc) · 4.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2026 Fabrintek Engenharia Digital Ltda
package cpace
import (
"crypto/rand"
"crypto/sha512"
"errors"
"io"
"github.com/gtank/ristretto255"
)
// This file is CPace itself, the cipher suite CPACE-RISTRETTO255-SHA512 of
// draft-irtf-cfrg-cpace-21, section 8.3, word for word: every function here
// is named after the draft's and checked against its test vectors in
// appendices A.1 and B.3 (vectors_test.go). The group arithmetic is gtank/ristretto255,
// which implements RFC 9496 on filippo.io/edwards25519; nothing here does
// arithmetic of its own.
// dsi is G_Ristretto255.DSI.
var dsi = []byte("CPaceRistretto255")
// hashBlockSize is H.s_in_bytes for SHA-512: its input block size.
const hashBlockSize = 128
// groupSizeBytes is the length of a scalar and of an encoded element.
const groupSizeBytes = 32
// errIdentity is a peer's element that is not a valid encoding, or that
// leads to the identity: the draft says to abort on either.
var errIdentity = errors.New("cpace: the peer's element is invalid")
// prependLen is the draft's prepend_len: s with its length in front, encoded
// as LEB128.
func prependLen(s []byte) []byte {
n := len(s)
var out []byte
for {
b := byte(n & 0x7f)
n >>= 7
if n == 0 {
out = append(out, b)
break
}
out = append(out, b|0x80)
}
return append(out, s...)
}
// lvCat is the draft's lv_cat: each argument length-prefixed, concatenated.
func lvCat(args ...[]byte) []byte {
var out []byte
for _, a := range args {
out = append(out, prependLen(a)...)
}
return out
}
// generatorString is the draft's generator_string: DSI and PRS, zero-padded
// to fill the hash's first block, then CI and sid, all length-prefixed.
func generatorString(prs, ci, sid []byte) []byte {
zpad := hashBlockSize - 1 - len(prependLen(prs)) - len(prependLen(dsi))
if zpad < 0 {
zpad = 0
}
return lvCat(dsi, prs, make([]byte, zpad), ci, sid)
}
// calculateGenerator is G.calculate_generator for ristretto255: the
// generator string hashed to 64 bytes, then mapped into the group with
// RFC 9496's one-way map.
func calculateGenerator(prs, ci, sid []byte) *ristretto255.Element {
h := sha512.Sum512(generatorString(prs, ci, sid))
g, err := ristretto255.NewIdentityElement().SetUniformBytes(h[:])
if err != nil {
panic("cpace: SetUniformBytes refused 64 bytes") // cannot happen
}
return g
}
// sampleScalar is G.sample_scalar as the draft recommends it: random bytes
// with the bits above the group's 252 cleared, read little-endian. The result
// is below the group order, so it is always a canonical scalar.
func sampleScalar(rnd io.Reader) (*ristretto255.Scalar, error) {
var b [groupSizeBytes]byte
if _, err := io.ReadFull(rnd, b[:]); err != nil {
return nil, err
}
b[31] &= 0x0f
return ristretto255.NewScalar().SetCanonicalBytes(b[:])
}
// scalarMult is G.scalar_mult: y times the generator g, encoded.
func scalarMult(y *ristretto255.Scalar, g *ristretto255.Element) []byte {
return ristretto255.NewIdentityElement().ScalarMult(y, g).Bytes()
}
// identity is G.I, the encoding of the neutral element.
var identity = ristretto255.NewIdentityElement().Bytes()
// scalarMultVfy is G.scalar_mult_vfy: y times the element x encodes, or the
// identity if x does not decode.
func scalarMultVfy(y *ristretto255.Scalar, x []byte) []byte {
p, err := ristretto255.NewIdentityElement().SetCanonicalBytes(x)
if err != nil {
return append([]byte(nil), identity...)
}
return ristretto255.NewIdentityElement().ScalarMult(y, p).Bytes()
}
// transcriptIR is the draft's transcript_ir: the initiator's message, then
// the responder's. This package runs only the initiator-responder setting.
func transcriptIR(ya, ada, yb, adb []byte) []byte {
return append(lvCat(ya, ada), lvCat(yb, adb)...)
}
// isk is the draft's ISK, given K: the hash of the ISK domain separator, sid
// and K, then the transcript. It refuses K = G.I, as the draft requires.
func isk(sid, k, ya, ada, yb, adb []byte) ([]byte, error) {
if subtleEqual(k, identity) {
return nil, errIdentity
}
h := sha512.New()
h.Write(lvCat(append(append([]byte(nil), dsi...), "_ISK"...), sid, k))
h.Write(transcriptIR(ya, ada, yb, adb))
return h.Sum(nil), nil
}
// randReader is where scalars come from; tests replace it.
var randReader = rand.Reader