Skip to content

Commit 8f9e683

Browse files
committed
cmd/age,tag: implement age1tag1.../p256tag recipients
See C2SP/C2SP#156
1 parent 15153e6 commit 8f9e683

File tree

7 files changed

+680
-0
lines changed

7 files changed

+680
-0
lines changed

cmd/age/parse.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"filippo.io/age/agessh"
1717
"filippo.io/age/armor"
1818
"filippo.io/age/plugin"
19+
"filippo.io/age/tag"
1920
"golang.org/x/crypto/cryptobyte"
2021
"golang.org/x/crypto/ssh"
2122
)
@@ -30,6 +31,8 @@ func (gitHubRecipientError) Error() string {
3031

3132
func parseRecipient(arg string) (age.Recipient, error) {
3233
switch {
34+
case strings.HasPrefix(arg, "age1tag1"):
35+
return tag.ParseRecipient(arg)
3336
case strings.HasPrefix(arg, "age1") && strings.Count(arg, "1") > 1:
3437
return plugin.NewRecipient(arg, pluginTerminalUI)
3538
case strings.HasPrefix(arg, "age1"):

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.19
44

55
require (
66
filippo.io/edwards25519 v1.1.0
7+
filippo.io/nistec v0.0.3
78
golang.org/x/crypto v0.24.0
89
golang.org/x/sys v0.21.0
910
golang.org/x/term v0.21.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3I
22
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w=
33
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
44
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
5+
filippo.io/nistec v0.0.3 h1:h336Je2jRDZdBCLy2fLDUd9E2unG32JLwcJi0JQE9Cw=
6+
filippo.io/nistec v0.0.3/go.mod h1:84fxC9mi+MhC2AERXI4LSa8cmSVOzrFikg6hZ4IfCyw=
57
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
68
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
79
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=

tag/internal/hpke/hpke.go

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package hpke
6+
7+
import (
8+
"crypto/cipher"
9+
"crypto/ecdh"
10+
"crypto/hkdf"
11+
"crypto/rand"
12+
"crypto/sha256"
13+
"encoding/binary"
14+
"errors"
15+
"hash"
16+
"math/bits"
17+
18+
"golang.org/x/crypto/chacha20poly1305"
19+
)
20+
21+
type KEMSender interface {
22+
Encap() (sharedSecret, enc []byte, err error)
23+
ID() uint16
24+
}
25+
26+
type KEMRecipient interface {
27+
Decap(enc []byte) (sharedSecret []byte, err error)
28+
ID() uint16
29+
}
30+
31+
type dhKEM struct {
32+
kdf KDF
33+
id uint16
34+
nSecret uint16
35+
}
36+
37+
func (dh *dhKEM) extractAndExpand(dhKey, kemContext []byte) ([]byte, error) {
38+
suiteID := binary.BigEndian.AppendUint16([]byte("KEM"), dh.id)
39+
eaePRK, err := dh.kdf.LabeledExtract(suiteID, nil, "eae_prk", dhKey)
40+
if err != nil {
41+
return nil, err
42+
}
43+
return dh.kdf.LabeledExpand(suiteID, eaePRK, "shared_secret", kemContext, dh.nSecret)
44+
}
45+
46+
func (dh *dhKEM) ID() uint16 {
47+
return dh.id
48+
}
49+
50+
type dhkemSender struct {
51+
dhKEM
52+
pub *ecdh.PublicKey
53+
}
54+
55+
// DHKEMSender returns a KEMSender implementing DHKEM(P-256, HKDF-SHA256).
56+
func DHKEMSender(pub *ecdh.PublicKey) (KEMSender, error) {
57+
switch pub.Curve() {
58+
case ecdh.P256():
59+
return &dhkemSender{
60+
pub: pub,
61+
dhKEM: dhKEM{
62+
kdf: HKDFSHA256(),
63+
id: 0x0010,
64+
nSecret: 32,
65+
},
66+
}, nil
67+
default:
68+
return nil, errors.New("unsupported curve")
69+
}
70+
}
71+
72+
// testingOnlyGenerateKey is only used during testing, to provide
73+
// a fixed test key to use when checking the RFC 9180 vectors.
74+
var testingOnlyGenerateKey func() *ecdh.PrivateKey
75+
76+
func (dh *dhkemSender) Encap() (sharedSecret []byte, encapPub []byte, err error) {
77+
privEph, err := dh.pub.Curve().GenerateKey(rand.Reader)
78+
if err != nil {
79+
return nil, nil, err
80+
}
81+
if testingOnlyGenerateKey != nil {
82+
privEph = testingOnlyGenerateKey()
83+
}
84+
dhVal, err := privEph.ECDH(dh.pub)
85+
if err != nil {
86+
return nil, nil, err
87+
}
88+
encPubEph := privEph.PublicKey().Bytes()
89+
90+
encPubRecip := dh.pub.Bytes()
91+
kemContext := append(encPubEph, encPubRecip...)
92+
sharedSecret, err = dh.extractAndExpand(dhVal, kemContext)
93+
if err != nil {
94+
return nil, nil, err
95+
}
96+
return sharedSecret, encPubEph, nil
97+
}
98+
99+
type dhkemRecipient struct {
100+
dhKEM
101+
priv *ecdh.PrivateKey
102+
}
103+
104+
// DHKEMRecipient returns a KEMRecipient implementing DHKEM(P-256, HKDF-SHA256).
105+
func DHKEMRecipient(priv *ecdh.PrivateKey) (KEMRecipient, error) {
106+
switch priv.Curve() {
107+
case ecdh.P256():
108+
return &dhkemRecipient{
109+
priv: priv,
110+
dhKEM: dhKEM{
111+
kdf: HKDFSHA256(),
112+
id: 0x0010,
113+
nSecret: 32,
114+
},
115+
}, nil
116+
default:
117+
return nil, errors.New("unsupported curve")
118+
}
119+
}
120+
121+
func (dh *dhkemRecipient) Decap(encPubEph []byte) ([]byte, error) {
122+
pubEph, err := dh.priv.Curve().NewPublicKey(encPubEph)
123+
if err != nil {
124+
return nil, err
125+
}
126+
dhVal, err := dh.priv.ECDH(pubEph)
127+
if err != nil {
128+
return nil, err
129+
}
130+
kemContext := append(encPubEph, dh.priv.PublicKey().Bytes()...)
131+
return dh.extractAndExpand(dhVal, kemContext)
132+
}
133+
134+
type KDF interface {
135+
LabeledExtract(sid, salt []byte, label string, inputKey []byte) ([]byte, error)
136+
LabeledExpand(suiteID, randomKey []byte, label string, info []byte, length uint16) ([]byte, error)
137+
ID() uint16
138+
}
139+
140+
type hkdfKDF struct {
141+
hash func() hash.Hash
142+
id uint16
143+
}
144+
145+
func HKDFSHA256() KDF {
146+
return &hkdfKDF{hash: sha256.New, id: 0x0001}
147+
}
148+
149+
func (kdf *hkdfKDF) ID() uint16 {
150+
return kdf.id
151+
}
152+
153+
func (kdf *hkdfKDF) LabeledExtract(sid []byte, salt []byte, label string, inputKey []byte) ([]byte, error) {
154+
labeledIKM := make([]byte, 0, 7+len(sid)+len(label)+len(inputKey))
155+
labeledIKM = append(labeledIKM, []byte("HPKE-v1")...)
156+
labeledIKM = append(labeledIKM, sid...)
157+
labeledIKM = append(labeledIKM, label...)
158+
labeledIKM = append(labeledIKM, inputKey...)
159+
return hkdf.Extract(kdf.hash, labeledIKM, salt)
160+
}
161+
162+
func (kdf *hkdfKDF) LabeledExpand(suiteID []byte, randomKey []byte, label string, info []byte, length uint16) ([]byte, error) {
163+
labeledInfo := make([]byte, 0, 2+7+len(suiteID)+len(label)+len(info))
164+
labeledInfo = binary.BigEndian.AppendUint16(labeledInfo, length)
165+
labeledInfo = append(labeledInfo, []byte("HPKE-v1")...)
166+
labeledInfo = append(labeledInfo, suiteID...)
167+
labeledInfo = append(labeledInfo, label...)
168+
labeledInfo = append(labeledInfo, info...)
169+
return hkdf.Expand(kdf.hash, randomKey, string(labeledInfo), int(length))
170+
}
171+
172+
type AEAD interface {
173+
AEAD(key []byte) (cipher.AEAD, error)
174+
KeySize() int
175+
NonceSize() int
176+
ID() uint16
177+
}
178+
179+
type aead struct {
180+
keySize int
181+
nonceSize int
182+
aead func([]byte) (cipher.AEAD, error)
183+
id uint16
184+
}
185+
186+
func ChaCha20Poly1305() AEAD {
187+
return &aead{
188+
keySize: chacha20poly1305.KeySize,
189+
nonceSize: chacha20poly1305.NonceSize,
190+
aead: chacha20poly1305.New,
191+
id: 0x0003,
192+
}
193+
}
194+
195+
func (a *aead) ID() uint16 {
196+
return a.id
197+
}
198+
199+
func (a *aead) AEAD(key []byte) (cipher.AEAD, error) {
200+
if len(key) != a.keySize {
201+
return nil, errors.New("invalid key size")
202+
}
203+
return a.aead(key)
204+
}
205+
206+
func (a *aead) KeySize() int {
207+
return a.keySize
208+
}
209+
210+
func (a *aead) NonceSize() int {
211+
return a.nonceSize
212+
}
213+
214+
type context struct {
215+
aead cipher.AEAD
216+
suiteID []byte
217+
218+
key []byte
219+
baseNonce []byte
220+
221+
seqNum uint128
222+
}
223+
224+
type Sender struct {
225+
*context
226+
}
227+
228+
type Recipient struct {
229+
*context
230+
}
231+
232+
func newContext(sharedSecret []byte, kemID uint16, kdf KDF, aead AEAD, info []byte) (*context, error) {
233+
sid := suiteID(kemID, kdf.ID(), aead.ID())
234+
235+
pskIDHash, err := kdf.LabeledExtract(sid, nil, "psk_id_hash", nil)
236+
if err != nil {
237+
return nil, err
238+
}
239+
infoHash, err := kdf.LabeledExtract(sid, nil, "info_hash", info)
240+
if err != nil {
241+
return nil, err
242+
}
243+
ksContext := append([]byte{0}, pskIDHash...)
244+
ksContext = append(ksContext, infoHash...)
245+
246+
secret, err := kdf.LabeledExtract(sid, sharedSecret, "secret", nil)
247+
if err != nil {
248+
return nil, err
249+
}
250+
key, err := kdf.LabeledExpand(sid, secret, "key", ksContext, uint16(aead.KeySize()))
251+
if err != nil {
252+
return nil, err
253+
}
254+
baseNonce, err := kdf.LabeledExpand(sid, secret, "base_nonce", ksContext, uint16(aead.NonceSize()))
255+
if err != nil {
256+
return nil, err
257+
}
258+
259+
a, err := aead.AEAD(key)
260+
if err != nil {
261+
return nil, err
262+
}
263+
264+
return &context{
265+
aead: a,
266+
suiteID: sid,
267+
key: key,
268+
baseNonce: baseNonce,
269+
}, nil
270+
}
271+
272+
func SetupSender(kem KEMSender, kdf KDF, aead AEAD, info []byte) ([]byte, *Sender, error) {
273+
sharedSecret, encapsulatedKey, err := kem.Encap()
274+
if err != nil {
275+
return nil, nil, err
276+
}
277+
context, err := newContext(sharedSecret, kem.ID(), kdf, aead, info)
278+
if err != nil {
279+
return nil, nil, err
280+
}
281+
return encapsulatedKey, &Sender{context}, nil
282+
}
283+
284+
func SetupRecipient(kem KEMRecipient, kdf KDF, aead AEAD, info, enc []byte) (*Recipient, error) {
285+
sharedSecret, err := kem.Decap(enc)
286+
if err != nil {
287+
return nil, err
288+
}
289+
context, err := newContext(sharedSecret, kem.ID(), kdf, aead, info)
290+
if err != nil {
291+
return nil, err
292+
}
293+
return &Recipient{context}, nil
294+
}
295+
296+
func (ctx *context) nextNonce() []byte {
297+
nonce := ctx.seqNum.bytes()[16-ctx.aead.NonceSize():]
298+
for i := range ctx.baseNonce {
299+
nonce[i] ^= ctx.baseNonce[i]
300+
}
301+
return nonce
302+
}
303+
304+
func (ctx *context) incrementNonce() {
305+
ctx.seqNum = ctx.seqNum.addOne()
306+
}
307+
308+
func (s *Sender) Seal(aad, plaintext []byte) ([]byte, error) {
309+
ciphertext := s.aead.Seal(nil, s.nextNonce(), plaintext, aad)
310+
s.incrementNonce()
311+
return ciphertext, nil
312+
}
313+
314+
func (r *Recipient) Open(aad, ciphertext []byte) ([]byte, error) {
315+
plaintext, err := r.aead.Open(nil, r.nextNonce(), ciphertext, aad)
316+
if err != nil {
317+
return nil, err
318+
}
319+
r.incrementNonce()
320+
return plaintext, nil
321+
}
322+
323+
func suiteID(kemID, kdfID, aeadID uint16) []byte {
324+
suiteID := make([]byte, 0, 4+2+2+2)
325+
suiteID = append(suiteID, []byte("HPKE")...)
326+
suiteID = binary.BigEndian.AppendUint16(suiteID, kemID)
327+
suiteID = binary.BigEndian.AppendUint16(suiteID, kdfID)
328+
suiteID = binary.BigEndian.AppendUint16(suiteID, aeadID)
329+
return suiteID
330+
}
331+
332+
type uint128 struct {
333+
hi, lo uint64
334+
}
335+
336+
func (u uint128) addOne() uint128 {
337+
lo, carry := bits.Add64(u.lo, 1, 0)
338+
return uint128{u.hi + carry, lo}
339+
}
340+
341+
func (u uint128) bytes() []byte {
342+
b := make([]byte, 16)
343+
binary.BigEndian.PutUint64(b[0:], u.hi)
344+
binary.BigEndian.PutUint64(b[8:], u.lo)
345+
return b
346+
}

0 commit comments

Comments
 (0)