Skip to content

Commit

Permalink
feat: Add forwarding to v2 api
Browse files Browse the repository at this point in the history
  • Loading branch information
lubux committed Jul 16, 2024
1 parent 20a257b commit fcee26d
Show file tree
Hide file tree
Showing 2 changed files with 382 additions and 0 deletions.
159 changes: 159 additions & 0 deletions openpgp/v2/forwarding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package v2

import (
goerrors "errors"

"github.com/ProtonMail/go-crypto/openpgp/ecdh"
"github.com/ProtonMail/go-crypto/openpgp/errors"
"github.com/ProtonMail/go-crypto/openpgp/packet"
)

// NewForwardingEntity generates a new forwardee key and derives the proxy parameters from the entity e.
// If strict, it will return an error if encryption-capable non-revoked subkeys with a wrong algorithm are found,
// instead of ignoring them
func (e *Entity) NewForwardingEntity(
name, comment, email string, config *packet.Config, strict bool,
) (
forwardeeKey *Entity, instances []packet.ForwardingInstance, err error,
) {
if e.PrimaryKey.Version != 4 {
return nil, nil, errors.InvalidArgumentError("unsupported key version")
}

now := config.Now()

if _, err = e.VerifyPrimaryKey(now); err != nil {
return nil, nil, err
}

// Generate a new Primary key for the forwardee
config.Algorithm = packet.PubKeyAlgoEdDSA
config.Curve = packet.Curve25519
keyLifetimeSecs := config.KeyLifetime()

forwardeePrimaryPrivRaw, err := newSigner(config)
if err != nil {
return nil, nil, err
}

primary := packet.NewSignerPrivateKey(now, forwardeePrimaryPrivRaw)

forwardeeKey = &Entity{
PrimaryKey: &primary.PublicKey,
PrivateKey: primary,
Identities: make(map[string]*Identity),
Subkeys: []Subkey{},
}

err = forwardeeKey.addUserId(userIdData{name, comment, email}, config, now, keyLifetimeSecs, true)
if err != nil {
return nil, nil, err
}

// Init empty instances
instances = []packet.ForwardingInstance{}

// Handle all forwarder subkeys
for _, forwarderSubKey := range e.Subkeys {
// Filter flags
if !forwarderSubKey.PublicKey.PubKeyAlgo.CanEncrypt() {
continue
}

forwarderSubKeySelfSig, err := forwarderSubKey.Verify(now)
// Filter expiration & revokal
if err != nil {
continue
}

if forwarderSubKey.PublicKey.PubKeyAlgo != packet.PubKeyAlgoECDH {
if strict {
return nil, nil, errors.InvalidArgumentError("encryption subkey is not algorithm 18 (ECDH)")
} else {
continue
}
}

forwarderEcdhKey, ok := forwarderSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey)
if !ok {
return nil, nil, errors.InvalidArgumentError("malformed key")
}

err = forwardeeKey.addEncryptionSubkey(config, now, 0)
if err != nil {
return nil, nil, err
}

forwardeeSubKey := forwardeeKey.Subkeys[len(forwardeeKey.Subkeys)-1]
forwardeeSubKeySelfSig := forwardeeSubKey.Bindings[0].Packet

forwardeeEcdhKey, ok := forwardeeSubKey.PrivateKey.PrivateKey.(*ecdh.PrivateKey)
if !ok {
return nil, nil, goerrors.New("wrong forwarding sub key generation")
}

instance := packet.ForwardingInstance{
KeyVersion: 4,
ForwarderFingerprint: forwarderSubKey.PublicKey.Fingerprint,
}

instance.ProxyParameter, err = ecdh.DeriveProxyParam(forwarderEcdhKey, forwardeeEcdhKey)
if err != nil {
return nil, nil, err
}

kdf := ecdh.KDF{
Version: ecdh.KDFVersionForwarding,
Hash: forwarderEcdhKey.KDF.Hash,
Cipher: forwarderEcdhKey.KDF.Cipher,
}

// If deriving a forwarding key from a forwarding key
if forwarderSubKeySelfSig.FlagForward {
if forwarderEcdhKey.KDF.Version != ecdh.KDFVersionForwarding {
return nil, nil, goerrors.New("malformed forwarder key")
}
kdf.ReplacementFingerprint = forwarderEcdhKey.KDF.ReplacementFingerprint
} else {
kdf.ReplacementFingerprint = forwarderSubKey.PublicKey.Fingerprint
}

err = forwardeeSubKey.PublicKey.ReplaceKDF(kdf)
if err != nil {
return nil, nil, err
}

// Extract fingerprint after changing the KDF
instance.ForwardeeFingerprint = forwardeeSubKey.PublicKey.Fingerprint

// 0x04 - This key may be used to encrypt communications.
forwardeeSubKeySelfSig.FlagEncryptCommunications = true

// 0x08 - This key may be used to encrypt storage.
forwardeeSubKeySelfSig.FlagEncryptStorage = true

// 0x10 - The private component of this key may have been split by a secret-sharing mechanism.
forwardeeSubKeySelfSig.FlagSplitKey = true

// 0x40 - This key may be used for forwarded communications.
forwardeeSubKeySelfSig.FlagForward = true

err = forwardeeSubKeySelfSig.SignKey(forwardeeSubKey.PublicKey, forwardeeKey.PrivateKey, config)
if err != nil {
return nil, nil, err
}

// Append each valid instance to the list
instances = append(instances, instance)
}

if len(instances) == 0 {
return nil, nil, errors.InvalidArgumentError("no valid subkey found")
}

return forwardeeKey, instances, nil
}
223 changes: 223 additions & 0 deletions openpgp/v2/forwarding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package v2

import (
"bytes"
"crypto/rand"
goerrors "errors"
"io"
"io/ioutil"
"strings"
"testing"

"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet"
)

const forwardeeKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
xVgEZQRXoxYJKwYBBAHaRw8BAQdAhxdzZ8ZP1M4UcauXSGbts38KhhAZxHNRcChs
9H7danMAAQC4tHykQmFpnlvhLYJDDc4MJm68mUB9qUls34GgKkqKNw6FzRtjaGFy
bGVzIDxjaGFybGVzQHByb3Rvbi5tZT7CiwQTFggAPQUCZQRXowkQizX+kwlYIwMW
IQTYm4qmQoyzTnG0eZKLNf6TCVgjAwIbAwIeAQIZAQILBwIVCAIWAAMnBwIAAMsQ
AQD9UHMIU418Z10UQrymhbjkGq/PHCytaaneaq5oycpN/QD/UiK3aA4+HxWhX/F2
VrvEKL5a2xyd1AKKQ2DInF3xUg3HcQRlBFejEgorBgEEAZdVAQUBAQdAep7x8ncL
ShzEgKL6h9MAJbgX2z3BBgSLeAdg/rczKngX/woJjSg9O4DzqQOtAvdhYkDoOCNf
QgUAAP9OMqK0IwNmshCtktDy1/RTeyPKT8ItHDFAZ1ReKMA5CA63wngEGBYIACoF
AmUEV6MJEIs1/pMJWCMDFiEE2JuKpkKMs05xtHmSizX+kwlYIwMCG1wAAC5EAP9s
AbYBf9NGv1NxJvU0n0K++k3UIGkw9xgGJa3VFHFKvwEAx0DZpTVpCkJmiOFAOcfu
cSvjlMyQwsC/hAAzQpcqvwE=
=8LJg
-----END PGP PRIVATE KEY BLOCK-----`

const forwardedMessage = `-----BEGIN PGP MESSAGE-----
wV4DKsXbtIU9/JMSAQdA/6+foCjeUhS7Xto3fimUi6pfMQ/Ft3caHkK/1i767isw
NvG8xRbjQ0sAE1IZVGE1MBcVhCIbHhqp0h2J479Zmfn/iP7hfomYxrkJ/6UMnlEo
0kABKyyfO3QVAzBBNeq6hH27uqzwLgjWVrpgY7dmWPv0goSSaqHUda0lm+8JNUuF
wssOJTwrSwQrX3ezy5D/h/E6
=okS+
-----END PGP MESSAGE-----`

const forwardedPlaintext = "Message for Bob"

func TestForwardingStatic(t *testing.T) {
charlesKey, err := ReadArmoredKeyRing(bytes.NewBufferString(forwardeeKey))
if err != nil {
t.Error(err)
return
}

ciphertext, err := armor.Decode(strings.NewReader(forwardedMessage))
if err != nil {
t.Error(err)
return
}

m, err := ReadMessage(ciphertext.Body, charlesKey, nil, nil)
if err != nil {
t.Fatal(err)
}

dec, err := ioutil.ReadAll(m.decrypted)

if !bytes.Equal(dec, []byte(forwardedPlaintext)) {
t.Fatal("forwarded decrypted does not match original")
}
}

func TestForwardingFull(t *testing.T) {
keyConfig := &packet.Config{
Algorithm: packet.PubKeyAlgoEdDSA,
Curve: packet.Curve25519,
}

plaintext := make([]byte, 1024)
rand.Read(plaintext)

bobEntity, err := NewEntity("bob", "", "bob@proton.me", keyConfig)
if err != nil {
t.Fatal(err)
}

charlesEntity, instances, err := bobEntity.NewForwardingEntity("charles", "", "charles@proton.me", keyConfig, true)
if err != nil {
t.Fatal(err)
}

charlesEntity = serializeAndParseForwardeeKey(t, charlesEntity)

if len(instances) != 1 {
t.Fatalf("invalid number of instances, expected 1 got %d", len(instances))
}

if !bytes.Equal(instances[0].ForwarderFingerprint, bobEntity.Subkeys[0].PublicKey.Fingerprint) {
t.Fatalf("invalid forwarder key ID, expected: %x, got: %x", bobEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwarderFingerprint)
}

if !bytes.Equal(instances[0].ForwardeeFingerprint, charlesEntity.Subkeys[0].PublicKey.Fingerprint) {
t.Fatalf("invalid forwardee key ID, expected: %x, got: %x", charlesEntity.Subkeys[0].PublicKey.Fingerprint, instances[0].ForwardeeFingerprint)
}

// Encrypt message
buf := bytes.NewBuffer(nil)
w, err := Encrypt(buf, []*Entity{bobEntity}, nil, nil, nil, nil)
if err != nil {
t.Fatal(err)
}

_, err = w.Write(plaintext)
if err != nil {
t.Fatal(err)
}

err = w.Close()
if err != nil {
t.Fatal(err)
}

encrypted := buf.Bytes()

// Decrypt message for Bob
m, err := ReadMessage(bytes.NewBuffer(encrypted), EntityList([]*Entity{bobEntity}), nil, nil)
if err != nil {
t.Fatal(err)
}
dec, err := ioutil.ReadAll(m.decrypted)

if !bytes.Equal(dec, plaintext) {
t.Fatal("decrypted does not match original")
}

// Forward message
transformed := transformTestMessage(t, encrypted, instances[0])

// Decrypt forwarded message for Charles
m, err = ReadMessage(bytes.NewBuffer(transformed), EntityList([]*Entity{charlesEntity}), nil /* no prompt */, nil)
if err != nil {
t.Fatal(err)
}

dec, err = ioutil.ReadAll(m.decrypted)

if !bytes.Equal(dec, plaintext) {
t.Fatal("forwarded decrypted does not match original")
}

// Setup further forwarding
danielEntity, secondForwardInstances, err := charlesEntity.NewForwardingEntity("Daniel", "", "daniel@proton.me", keyConfig, true)
if err != nil {
t.Fatal(err)
}

danielEntity = serializeAndParseForwardeeKey(t, danielEntity)

secondTransformed := transformTestMessage(t, transformed, secondForwardInstances[0])

// Decrypt forwarded message for Charles
m, err = ReadMessage(bytes.NewBuffer(secondTransformed), EntityList([]*Entity{danielEntity}), nil /* no prompt */, nil)
if err != nil {
t.Fatal(err)
}

dec, err = ioutil.ReadAll(m.decrypted)

if !bytes.Equal(dec, plaintext) {
t.Fatal("forwarded decrypted does not match original")
}
}

func transformTestMessage(t *testing.T, encrypted []byte, instance packet.ForwardingInstance) []byte {
bytesReader := bytes.NewReader(encrypted)
packets := packet.NewReader(bytesReader)
splitPoint := int64(0)
transformedEncryptedKey := bytes.NewBuffer(nil)

Loop:
for {
p, err := packets.Next()
if goerrors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatalf("error in parsing message: %s", err)
}
switch p := p.(type) {
case *packet.EncryptedKey:
tp, err := p.ProxyTransform(instance)
if err != nil {
t.Fatalf("error transforming PKESK: %s", err)
}

splitPoint = bytesReader.Size() - int64(bytesReader.Len())

err = tp.Serialize(transformedEncryptedKey)
if err != nil {
t.Fatalf("error serializing transformed PKESK: %s", err)
}
break Loop
}
}

transformed := transformedEncryptedKey.Bytes()
transformed = append(transformed, encrypted[splitPoint:]...)

return transformed
}

func serializeAndParseForwardeeKey(t *testing.T, key *Entity) *Entity {
serializedEntity := bytes.NewBuffer(nil)
err := key.SerializePrivateWithoutSigning(serializedEntity, nil)
if err != nil {
t.Fatalf("Error in serializing forwardee key: %s", err)
}
el, err := ReadKeyRing(serializedEntity)
if err != nil {
t.Fatalf("Error in reading forwardee key: %s", err)
}

if len(el) != 1 {
t.Fatalf("Wrong number of entities in parsing, expected 1, got %d", len(el))
}

return el[0]
}

0 comments on commit fcee26d

Please sign in to comment.