Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental: Symmetric Keys and Forwarding #141

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
branches: [ main, Proton ]

jobs:

Expand Down Expand Up @@ -44,4 +44,4 @@ jobs:
run: go test -v ./... -run RandomizeFast -count=512

- name: Randomized test suite 2
run: go test -v ./... -run RandomizeSlow -count=32
run: go test -v ./... -run RandomizeSlow -count=32
108 changes: 93 additions & 15 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,25 +183,32 @@ 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 := param.Write([]byte("Anonymous Sender ")); err != nil {

if err := pub.KDF.serializeForHash(param); err != nil {
return nil, err
}
if _, err := param.Write(fingerprint[:]); err != nil {

if _, err := param.Write([]byte("Anonymous Sender ")); err != nil {
return nil, err
}
if param.Len()-len(curveOID) != 45 {
return nil, errors.New("ecdh: malformed KDF Param")

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

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

// MB = Hash ( 00 || 00 || 00 || 01 || ZB || Param );
Expand Down Expand Up @@ -207,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
}
38 changes: 36 additions & 2 deletions openpgp/ecdh/ecdh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestCurves(t *testing.T) {
}

func testGenerate(t *testing.T, curve ecc.ECDHCurve) *PrivateKey {
kdf := KDF{
kdf := KDF {
Hash: algorithm.SHA512,
Cipher: algorithm.AES256,
}
Expand Down 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
wussler marked this conversation as resolved.
Show resolved Hide resolved

// 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
Loading