Skip to content

Commit

Permalink
Add support for automatic forwarding (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
larabr authored and twiss committed Nov 12, 2024
1 parent d4666e7 commit e5928a3
Show file tree
Hide file tree
Showing 27 changed files with 3,129 additions and 30 deletions.
105 changes: 93 additions & 12 deletions openpgp/ecdh/ecdh.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,50 @@ import (
"io"

"github.com/ProtonMail/go-crypto/openpgp/aes/keywrap"
pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors"
"github.com/ProtonMail/go-crypto/openpgp/internal/algorithm"
"github.com/ProtonMail/go-crypto/openpgp/internal/ecc"
"github.com/ProtonMail/go-crypto/openpgp/internal/ecc/curve25519"
)

const (
KDFVersion1 = 1
KDFVersionForwarding = 255
)

type KDF struct {
Hash algorithm.Hash
Cipher algorithm.Cipher
Version int // Defaults to v1; 255 for forwarding
Hash algorithm.Hash
Cipher algorithm.Cipher
ReplacementFingerprint []byte // (forwarding only) fingerprint to use instead of recipient's (20 octets)
}

func (kdf *KDF) Serialize(w io.Writer) (err error) {
switch kdf.Version {
case 0, KDFVersion1: // Default to v1 if unspecified
return kdf.serializeForHash(w)
case KDFVersionForwarding:
// Length || Version || Hash || Cipher || Replacement Fingerprint
length := byte(3 + len(kdf.ReplacementFingerprint))
if _, err := w.Write([]byte{length, KDFVersionForwarding, kdf.Hash.Id(), kdf.Cipher.Id()}); err != nil {
return err
}
if _, err := w.Write(kdf.ReplacementFingerprint); err != nil {
return err
}

return nil
default:
return errors.New("ecdh: invalid KDF version")
}
}

func (kdf *KDF) serializeForHash(w io.Writer) (err error) {
// Length || Version || Hash || Cipher
if _, err := w.Write([]byte{3, KDFVersion1, kdf.Hash.Id(), kdf.Cipher.Id()}); err != nil {
return err
}
return nil
}

type PublicKey struct {
Expand All @@ -32,13 +69,10 @@ type PrivateKey struct {
D []byte
}

func NewPublicKey(curve ecc.ECDHCurve, kdfHash algorithm.Hash, kdfCipher algorithm.Cipher) *PublicKey {
func NewPublicKey(curve ecc.ECDHCurve, kdf KDF) *PublicKey {
return &PublicKey{
curve: curve,
KDF: KDF{
Hash: kdfHash,
Cipher: kdfCipher,
},
KDF: kdf,
}
}

Expand Down Expand Up @@ -149,21 +183,31 @@ func Decrypt(priv *PrivateKey, vsG, c, curveOID, fingerprint []byte) (msg []byte
}

func buildKey(pub *PublicKey, zb []byte, curveOID, fingerprint []byte, stripLeading, stripTrailing bool) ([]byte, error) {
// Param = curve_OID_len || curve_OID || public_key_alg_ID || 03
// || 01 || KDF_hash_ID || KEK_alg_ID for AESKeyWrap
// Param = curve_OID_len || curve_OID || public_key_alg_ID
// || KDF_params for AESKeyWrap
// || "Anonymous Sender " || recipient_fingerprint;
param := new(bytes.Buffer)
if _, err := param.Write(curveOID); err != nil {
return nil, err
}
algKDF := []byte{18, 3, 1, pub.KDF.Hash.Id(), pub.KDF.Cipher.Id()}
if _, err := param.Write(algKDF); err != nil {
algo := []byte{18}
if _, err := param.Write(algo); err != nil {
return nil, err
}

if err := pub.KDF.serializeForHash(param); err != nil {
return nil, err
}

if _, err := param.Write([]byte("Anonymous Sender ")); err != nil {
return nil, err
}
if _, err := param.Write(fingerprint[:]); err != nil {

if pub.KDF.ReplacementFingerprint != nil {
fingerprint = pub.KDF.ReplacementFingerprint
}

if _, err := param.Write(fingerprint); err != nil {
return nil, err
}

Expand Down Expand Up @@ -204,3 +248,40 @@ func buildKey(pub *PublicKey, zb []byte, curveOID, fingerprint []byte, stripLead
func Validate(priv *PrivateKey) error {
return priv.curve.ValidateECDH(priv.Point, priv.D)
}

func DeriveProxyParam(recipientKey, forwardeeKey *PrivateKey) (proxyParam []byte, err error) {
if recipientKey.GetCurve().GetCurveName() != "curve25519" {
return nil, pgperrors.InvalidArgumentError("recipient subkey is not curve25519")
}

if forwardeeKey.GetCurve().GetCurveName() != "curve25519" {
return nil, pgperrors.InvalidArgumentError("forwardee subkey is not curve25519")
}

c := ecc.NewCurve25519()

// Clamp and reverse two secrets
proxyParam, err = curve25519.DeriveProxyParam(c.MarshalByteSecret(recipientKey.D), c.MarshalByteSecret(forwardeeKey.D))

return proxyParam, err
}

func ProxyTransform(ephemeral, proxyParam []byte) ([]byte, error) {
c := ecc.NewCurve25519()

parsedEphemeral := c.UnmarshalBytePoint(ephemeral)
if parsedEphemeral == nil {
return nil, pgperrors.InvalidArgumentError("invalid ephemeral")
}

if len(proxyParam) != curve25519.ParamSize {
return nil, pgperrors.InvalidArgumentError("invalid proxy parameter")
}

transformed, err := curve25519.ProxyTransform(parsedEphemeral, proxyParam)
if err != nil {
return nil, err
}

return c.MarshalBytePoint(transformed), nil
}
36 changes: 35 additions & 1 deletion openpgp/ecdh/ecdh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func testMarshalUnmarshal(t *testing.T, priv *PrivateKey) {
p := priv.MarshalPoint()
d := priv.MarshalByteSecret()

parsed := NewPrivateKey(*NewPublicKey(priv.GetCurve(), priv.KDF.Hash, priv.KDF.Cipher))
parsed := NewPrivateKey(*NewPublicKey(priv.GetCurve(), priv.KDF))

if err := parsed.UnmarshalPoint(p); err != nil {
t.Fatalf("unable to unmarshal point: %s", err)
Expand All @@ -112,3 +112,37 @@ func testMarshalUnmarshal(t *testing.T, priv *PrivateKey) {
t.Fatal("failed to marshal/unmarshal correctly")
}
}

func TestKDFParamsWrite(t *testing.T) {
kdf := KDF{
Hash: algorithm.SHA512,
Cipher: algorithm.AES256,
}
byteBuffer := new(bytes.Buffer)

testFingerprint := make([]byte, 20)

expectBytesV1 := []byte{3, 1, kdf.Hash.Id(), kdf.Cipher.Id()}
kdf.Serialize(byteBuffer)
gotBytes := byteBuffer.Bytes()
if !bytes.Equal(gotBytes, expectBytesV1) {
t.Errorf("error serializing KDF params, got %x, want: %x", gotBytes, expectBytesV1)
}
byteBuffer.Reset()

kdfV2 := KDF{
Version: KDFVersionForwarding,
Hash: algorithm.SHA512,
Cipher: algorithm.AES256,
ReplacementFingerprint: testFingerprint,
}
expectBytesV2 := []byte{23, 0xFF, kdfV2.Hash.Id(), kdfV2.Cipher.Id()}
expectBytesV2 = append(expectBytesV2, testFingerprint...)

kdfV2.Serialize(byteBuffer)
gotBytes = byteBuffer.Bytes()
if !bytes.Equal(gotBytes, expectBytesV2) {
t.Errorf("error serializing KDF params v2, got %x, want: %x", gotBytes, expectBytesV2)
}
byteBuffer.Reset()
}
2 changes: 2 additions & 0 deletions openpgp/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func (i InvalidArgumentError) Error() string {
return "openpgp: invalid argument: " + string(i)
}

var InvalidForwardeeKeyError = InvalidArgumentError("invalid forwardee key")

// SignatureError indicates that a syntactically valid signature failed to
// validate.
type SignatureError string
Expand Down
163 changes: 163 additions & 0 deletions openpgp/forwarding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// 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 openpgp

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()
i := e.PrimaryIdentity()
if e.PrimaryKey.KeyExpired(i.SelfSignature, now) || // primary key has expired
i.SelfSignature.SigExpired(now) || // user ID self-signature has expired
e.Revoked(now) || // primary key has been revoked
i.Revoked(now) { // user ID has been revoked
return nil, nil, errors.InvalidArgumentError("primary key is expired")
}

// 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(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
}

// Filter expiration & revokal
if forwarderSubKey.PublicKey.KeyExpired(forwarderSubKey.Sig, now) ||
forwarderSubKey.Sig.SigExpired(now) ||
forwarderSubKey.Revoked(now) {
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]

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 forwarderSubKey.Sig.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.
forwardeeSubKey.Sig.FlagEncryptCommunications = false

// 0x08 - This key may be used to encrypt storage.
forwardeeSubKey.Sig.FlagEncryptStorage = false

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

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

// Re-sign subkey binding signature
err = forwardeeSubKey.Sig.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
}
Loading

0 comments on commit e5928a3

Please sign in to comment.