Skip to content

Commit da0a0cf

Browse files
committed
feat: Add forwarding to v2 api
1 parent 0e6a359 commit da0a0cf

File tree

2 files changed

+382
-0
lines changed

2 files changed

+382
-0
lines changed

Diff for: openpgp/v2/forwarding.go

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright 2011 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 v2
6+
7+
import (
8+
goerrors "errors"
9+
10+
"github.com/ProtonMail/go-crypto/openpgp/ecdh"
11+
"github.com/ProtonMail/go-crypto/openpgp/errors"
12+
"github.com/ProtonMail/go-crypto/openpgp/packet"
13+
)
14+
15+
// NewForwardingEntity generates a new forwardee key and derives the proxy parameters from the entity e.
16+
// If strict, it will return an error if encryption-capable non-revoked subkeys with a wrong algorithm are found,
17+
// instead of ignoring them
18+
func (e *Entity) NewForwardingEntity(
19+
name, comment, email string, config *packet.Config, strict bool,
20+
) (
21+
forwardeeKey *Entity, instances []packet.ForwardingInstance, err error,
22+
) {
23+
if e.PrimaryKey.Version != 4 {
24+
return nil, nil, errors.InvalidArgumentError("unsupported key version")
25+
}
26+
27+
now := config.Now()
28+
29+
if _, err = e.VerifyPrimaryKey(now); err != nil {
30+
return nil, nil, err
31+
}
32+
33+
// Generate a new Primary key for the forwardee
34+
config.Algorithm = packet.PubKeyAlgoEdDSA
35+
config.Curve = packet.Curve25519
36+
keyLifetimeSecs := config.KeyLifetime()
37+
38+
forwardeePrimaryPrivRaw, err := newSigner(config)
39+
if err != nil {
40+
return nil, nil, err
41+
}
42+
43+
primary := packet.NewSignerPrivateKey(now, forwardeePrimaryPrivRaw)
44+
45+
forwardeeKey = &Entity{
46+
PrimaryKey: &primary.PublicKey,
47+
PrivateKey: primary,
48+
Identities: make(map[string]*Identity),
49+
Subkeys: []Subkey{},
50+
}
51+
52+
err = forwardeeKey.addUserId(userIdData{name, comment, email}, config, now, keyLifetimeSecs, true)
53+
if err != nil {
54+
return nil, nil, err
55+
}
56+
57+
// Init empty instances
58+
instances = []packet.ForwardingInstance{}
59+
60+
// Handle all forwarder subkeys
61+
for _, forwarderSubKey := range e.Subkeys {
62+
// Filter flags
63+
if !forwarderSubKey.PublicKey.PubKeyAlgo.CanEncrypt() {
64+
continue
65+
}
66+
67+
forwarderSubKeySelfSig, err := forwarderSubKey.Verify(now)
68+
// Filter expiration & revokal
69+
if err != nil {
70+
continue
71+
}
72+
73+
if forwarderSubKey.PublicKey.PubKeyAlgo != packet.PubKeyAlgoECDH {
74+
if strict {
75+
return nil, nil, errors.InvalidArgumentError("encryption subkey is not algorithm 18 (ECDH)")
76+
} else {
77+
continue
78+
}
79+
}
80+
81+
forwarderEcdhKey, ok := forwarderSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey)
82+
if !ok {
83+
return nil, nil, errors.InvalidArgumentError("malformed key")
84+
}
85+
86+
err = forwardeeKey.addEncryptionSubkey(config, now, 0)
87+
if err != nil {
88+
return nil, nil, err
89+
}
90+
91+
forwardeeSubKey := forwardeeKey.Subkeys[len(forwardeeKey.Subkeys)-1]
92+
forwardeeSubKeySelfSig := forwardeeSubKey.Bindings[0].Packet
93+
94+
forwardeeEcdhKey, ok := forwardeeSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey)
95+
if !ok {
96+
return nil, nil, goerrors.New("wrong forwarding sub key generation")
97+
}
98+
99+
instance := packet.ForwardingInstance{
100+
KeyVersion: 4,
101+
ForwarderFingerprint: forwarderSubKey.PublicKey.Fingerprint,
102+
}
103+
104+
instance.ProxyParameter, err = ecdh.DeriveProxyParam(forwarderEcdhKey, forwardeeEcdhKey)
105+
if err != nil {
106+
return nil, nil, err
107+
}
108+
109+
kdf := ecdh.KDF{
110+
Version: ecdh.KDFVersionForwarding,
111+
Hash: forwarderEcdhKey.KDF.Hash,
112+
Cipher: forwarderEcdhKey.KDF.Cipher,
113+
}
114+
115+
// If deriving a forwarding key from a forwarding key
116+
if forwarderSubKeySelfSig.FlagForward {
117+
if forwarderEcdhKey.KDF.Version != ecdh.KDFVersionForwarding {
118+
return nil, nil, goerrors.New("malformed forwarder key")
119+
}
120+
kdf.ReplacementFingerprint = forwarderEcdhKey.KDF.ReplacementFingerprint
121+
} else {
122+
kdf.ReplacementFingerprint = forwarderSubKey.PublicKey.Fingerprint
123+
}
124+
125+
err = forwardeeSubKey.PublicKey.ReplaceKDF(kdf)
126+
if err != nil {
127+
return nil, nil, err
128+
}
129+
130+
// Extract fingerprint after changing the KDF
131+
instance.ForwardeeFingerprint = forwardeeSubKey.PublicKey.Fingerprint
132+
133+
// 0x04 - This key may be used to encrypt communications.
134+
forwardeeSubKeySelfSig.FlagEncryptCommunications = true
135+
136+
// 0x08 - This key may be used to encrypt storage.
137+
forwardeeSubKeySelfSig.FlagEncryptStorage = true
138+
139+
// 0x10 - The private component of this key may have been split by a secret-sharing mechanism.
140+
forwardeeSubKeySelfSig.FlagSplitKey = true
141+
142+
// 0x40 - This key may be used for forwarded communications.
143+
forwardeeSubKeySelfSig.FlagForward = true
144+
145+
err = forwardeeSubKeySelfSig.SignKey(forwardeeSubKey.PublicKey, forwardeeKey.PrivateKey, config)
146+
if err != nil {
147+
return nil, nil, err
148+
}
149+
150+
// Append each valid instance to the list
151+
instances = append(instances, instance)
152+
}
153+
154+
if len(instances) == 0 {
155+
return nil, nil, errors.InvalidArgumentError("no valid subkey found")
156+
}
157+
158+
return forwardeeKey, instances, nil
159+
}

Diff for: openpgp/v2/forwarding_test.go

+223
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package v2
2+
3+
import (
4+
"bytes"
5+
"crypto/rand"
6+
goerrors "errors"
7+
"io"
8+
"io/ioutil"
9+
"strings"
10+
"testing"
11+
12+
"github.com/ProtonMail/go-crypto/openpgp/armor"
13+
"github.com/ProtonMail/go-crypto/openpgp/packet"
14+
)
15+
16+
const forwardeeKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
17+
18+
xVgEZQRXoxYJKwYBBAHaRw8BAQdAhxdzZ8ZP1M4UcauXSGbts38KhhAZxHNRcChs
19+
9H7danMAAQC4tHykQmFpnlvhLYJDDc4MJm68mUB9qUls34GgKkqKNw6FzRtjaGFy
20+
bGVzIDxjaGFybGVzQHByb3Rvbi5tZT7CiwQTFggAPQUCZQRXowkQizX+kwlYIwMW
21+
IQTYm4qmQoyzTnG0eZKLNf6TCVgjAwIbAwIeAQIZAQILBwIVCAIWAAMnBwIAAMsQ
22+
AQD9UHMIU418Z10UQrymhbjkGq/PHCytaaneaq5oycpN/QD/UiK3aA4+HxWhX/F2
23+
VrvEKL5a2xyd1AKKQ2DInF3xUg3HcQRlBFejEgorBgEEAZdVAQUBAQdAep7x8ncL
24+
ShzEgKL6h9MAJbgX2z3BBgSLeAdg/rczKngX/woJjSg9O4DzqQOtAvdhYkDoOCNf
25+
QgUAAP9OMqK0IwNmshCtktDy1/RTeyPKT8ItHDFAZ1ReKMA5CA63wngEGBYIACoF
26+
AmUEV6MJEIs1/pMJWCMDFiEE2JuKpkKMs05xtHmSizX+kwlYIwMCG1wAAC5EAP9s
27+
AbYBf9NGv1NxJvU0n0K++k3UIGkw9xgGJa3VFHFKvwEAx0DZpTVpCkJmiOFAOcfu
28+
cSvjlMyQwsC/hAAzQpcqvwE=
29+
=8LJg
30+
-----END PGP PRIVATE KEY BLOCK-----`
31+
32+
const forwardedMessage = `-----BEGIN PGP MESSAGE-----
33+
34+
wV4DKsXbtIU9/JMSAQdA/6+foCjeUhS7Xto3fimUi6pfMQ/Ft3caHkK/1i767isw
35+
NvG8xRbjQ0sAE1IZVGE1MBcVhCIbHhqp0h2J479Zmfn/iP7hfomYxrkJ/6UMnlEo
36+
0kABKyyfO3QVAzBBNeq6hH27uqzwLgjWVrpgY7dmWPv0goSSaqHUda0lm+8JNUuF
37+
wssOJTwrSwQrX3ezy5D/h/E6
38+
=okS+
39+
-----END PGP MESSAGE-----`
40+
41+
const forwardedPlaintext = "Message for Bob"
42+
43+
func TestForwardingStatic(t *testing.T) {
44+
charlesKey, err := ReadArmoredKeyRing(bytes.NewBufferString(forwardeeKey))
45+
if err != nil {
46+
t.Error(err)
47+
return
48+
}
49+
50+
ciphertext, err := armor.Decode(strings.NewReader(forwardedMessage))
51+
if err != nil {
52+
t.Error(err)
53+
return
54+
}
55+
56+
m, err := ReadMessage(ciphertext.Body, charlesKey, nil, nil)
57+
if err != nil {
58+
t.Fatal(err)
59+
}
60+
61+
dec, err := ioutil.ReadAll(m.decrypted)
62+
63+
if !bytes.Equal(dec, []byte(forwardedPlaintext)) {
64+
t.Fatal("forwarded decrypted does not match original")
65+
}
66+
}
67+
68+
func TestForwardingFull(t *testing.T) {
69+
keyConfig := &packet.Config{
70+
Algorithm: packet.PubKeyAlgoEdDSA,
71+
Curve: packet.Curve25519,
72+
}
73+
74+
plaintext := make([]byte, 1024)
75+
rand.Read(plaintext)
76+
77+
bobEntity, err := NewEntity("bob", "", "[email protected]", keyConfig)
78+
if err != nil {
79+
t.Fatal(err)
80+
}
81+
82+
charlesEntity, instances, err := bobEntity.NewForwardingEntity("charles", "", "[email protected]", keyConfig, true)
83+
if err != nil {
84+
t.Fatal(err)
85+
}
86+
87+
charlesEntity = serializeAndParseForwardeeKey(t, charlesEntity)
88+
89+
if len(instances) != 1 {
90+
t.Fatalf("invalid number of instances, expected 1 got %d", len(instances))
91+
}
92+
93+
if !bytes.Equal(instances[0].ForwarderFingerprint, bobEntity.Subkeys[0].PublicKey.Fingerprint) {
94+
t.Fatalf("invalid forwarder key ID, expected: %x, got: %x", bobEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwarderFingerprint)
95+
}
96+
97+
if !bytes.Equal(instances[0].ForwardeeFingerprint, charlesEntity.Subkeys[0].PublicKey.Fingerprint) {
98+
t.Fatalf("invalid forwardee key ID, expected: %x, got: %x", charlesEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwardeeFingerprint)
99+
}
100+
101+
// Encrypt message
102+
buf := bytes.NewBuffer(nil)
103+
w, err := Encrypt(buf, []*Entity{bobEntity}, nil, nil, nil, nil)
104+
if err != nil {
105+
t.Fatal(err)
106+
}
107+
108+
_, err = w.Write(plaintext)
109+
if err != nil {
110+
t.Fatal(err)
111+
}
112+
113+
err = w.Close()
114+
if err != nil {
115+
t.Fatal(err)
116+
}
117+
118+
encrypted := buf.Bytes()
119+
120+
// Decrypt message for Bob
121+
m, err := ReadMessage(bytes.NewBuffer(encrypted), EntityList([]*Entity{bobEntity}), nil, nil)
122+
if err != nil {
123+
t.Fatal(err)
124+
}
125+
dec, err := ioutil.ReadAll(m.decrypted)
126+
127+
if !bytes.Equal(dec, plaintext) {
128+
t.Fatal("decrypted does not match original")
129+
}
130+
131+
// Forward message
132+
transformed := transformTestMessage(t, encrypted, instances[0])
133+
134+
// Decrypt forwarded message for Charles
135+
m, err = ReadMessage(bytes.NewBuffer(transformed), EntityList([]*Entity{charlesEntity}), nil /* no prompt */, nil)
136+
if err != nil {
137+
t.Fatal(err)
138+
}
139+
140+
dec, err = ioutil.ReadAll(m.decrypted)
141+
142+
if !bytes.Equal(dec, plaintext) {
143+
t.Fatal("forwarded decrypted does not match original")
144+
}
145+
146+
// Setup further forwarding
147+
danielEntity, secondForwardInstances, err := charlesEntity.NewForwardingEntity("Daniel", "", "[email protected]", keyConfig, true)
148+
if err != nil {
149+
t.Fatal(err)
150+
}
151+
152+
danielEntity = serializeAndParseForwardeeKey(t, danielEntity)
153+
154+
secondTransformed := transformTestMessage(t, transformed, secondForwardInstances[0])
155+
156+
// Decrypt forwarded message for Charles
157+
m, err = ReadMessage(bytes.NewBuffer(secondTransformed), EntityList([]*Entity{danielEntity}), nil /* no prompt */, nil)
158+
if err != nil {
159+
t.Fatal(err)
160+
}
161+
162+
dec, err = ioutil.ReadAll(m.decrypted)
163+
164+
if !bytes.Equal(dec, plaintext) {
165+
t.Fatal("forwarded decrypted does not match original")
166+
}
167+
}
168+
169+
func transformTestMessage(t *testing.T, encrypted []byte, instance packet.ForwardingInstance) []byte {
170+
bytesReader := bytes.NewReader(encrypted)
171+
packets := packet.NewReader(bytesReader)
172+
splitPoint := int64(0)
173+
transformedEncryptedKey := bytes.NewBuffer(nil)
174+
175+
Loop:
176+
for {
177+
p, err := packets.Next()
178+
if goerrors.Is(err, io.EOF) {
179+
break
180+
}
181+
if err != nil {
182+
t.Fatalf("error in parsing message: %s", err)
183+
}
184+
switch p := p.(type) {
185+
case *packet.EncryptedKey:
186+
tp, err := p.ProxyTransform(instance)
187+
if err != nil {
188+
t.Fatalf("error transforming PKESK: %s", err)
189+
}
190+
191+
splitPoint = bytesReader.Size() - int64(bytesReader.Len())
192+
193+
err = tp.Serialize(transformedEncryptedKey)
194+
if err != nil {
195+
t.Fatalf("error serializing transformed PKESK: %s", err)
196+
}
197+
break Loop
198+
}
199+
}
200+
201+
transformed := transformedEncryptedKey.Bytes()
202+
transformed = append(transformed, encrypted[splitPoint:]...)
203+
204+
return transformed
205+
}
206+
207+
func serializeAndParseForwardeeKey(t *testing.T, key *Entity) *Entity {
208+
serializedEntity := bytes.NewBuffer(nil)
209+
err := key.SerializePrivateWithoutSigning(serializedEntity, nil)
210+
if err != nil {
211+
t.Fatalf("Error in serializing forwardee key: %s", err)
212+
}
213+
el, err := ReadKeyRing(serializedEntity)
214+
if err != nil {
215+
t.Fatalf("Error in reading forwardee key: %s", err)
216+
}
217+
218+
if len(el) != 1 {
219+
t.Fatalf("Wrong number of entities in parsing, expected 1, got %d", len(el))
220+
}
221+
222+
return el[0]
223+
}

0 commit comments

Comments
 (0)