Skip to content

Commit

Permalink
feat: Add symmetric keys to v2
Browse files Browse the repository at this point in the history
  • Loading branch information
lubux committed Jul 16, 2024
1 parent 4017cb3 commit f27ab7d
Show file tree
Hide file tree
Showing 10 changed files with 358 additions and 7 deletions.
4 changes: 2 additions & 2 deletions openpgp/forwarding.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,10 @@ func (e *Entity) NewForwardingEntity(
instance.ForwardeeFingerprint = forwardeeSubKey.PublicKey.Fingerprint

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

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

// 0x10 - The private component of this key may have been split by a secret-sharing mechanism.
forwardeeSubKey.Sig.FlagSplitKey = true
Expand Down
2 changes: 1 addition & 1 deletion openpgp/packet/encrypted_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error {

var key []byte
switch priv.PubKeyAlgo {
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH:
case PubKeyAlgoRSA, PubKeyAlgoRSAEncryptOnly, PubKeyAlgoElGamal, PubKeyAlgoECDH, ExperimentalPubKeyAlgoAEAD:
keyOffset := 0
if e.Version < 6 {
e.CipherFunc = CipherFunction(b[0])
Expand Down
4 changes: 2 additions & 2 deletions openpgp/v2/forwarding.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ func (e *Entity) NewForwardingEntity(
instance.ForwardeeFingerprint = forwardeeSubKey.PublicKey.Fingerprint

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

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

// 0x10 - The private component of this key may have been split by a secret-sharing mechanism.
forwardeeSubKeySelfSig.FlagSplitKey = true
Expand Down
7 changes: 7 additions & 0 deletions openpgp/v2/key_generation.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/internal/algorithm"
"github.com/ProtonMail/go-crypto/openpgp/internal/ecc"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/ProtonMail/go-crypto/openpgp/symmetric"
"github.com/ProtonMail/go-crypto/openpgp/x25519"
"github.com/ProtonMail/go-crypto/openpgp/x448"
)
Expand Down Expand Up @@ -387,6 +388,9 @@ func newSigner(config *packet.Config) (signer interface{}, err error) {
return nil, err
}
return priv, nil
case packet.ExperimentalPubKeyAlgoHMAC:
hash := algorithm.HashById[hashToHashId(config.Hash())]
return symmetric.HMACGenerateKey(config.Random(), hash)
default:
return nil, errors.InvalidArgumentError("unsupported public key algorithm")
}
Expand Down Expand Up @@ -429,6 +433,9 @@ func newDecrypter(config *packet.Config) (decrypter interface{}, err error) {
return x25519.GenerateKey(config.Random())
case packet.PubKeyAlgoEd448, packet.PubKeyAlgoX448: // When passing Ed448, we generate an x448 subkey
return x448.GenerateKey(config.Random())
case packet.ExperimentalPubKeyAlgoAEAD:
cipher := algorithm.CipherFunction(config.Cipher())
return symmetric.AEADGenerateKey(config.Random(), cipher)
default:
return nil, errors.InvalidArgumentError("unsupported public key algorithm")
}
Expand Down
16 changes: 15 additions & 1 deletion openpgp/v2/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,10 @@ func (e *Entity) serializePrivate(w io.Writer, config *packet.Config, reSign boo
// Serialize writes the public part of the given Entity to w, including
// signatures from other entities. No private key material will be output.
func (e *Entity) Serialize(w io.Writer) error {
if e.PrimaryKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC ||
e.PrimaryKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD {
return errors.InvalidArgumentError("Can't serialize symmetric primary key")
}
if err := e.PrimaryKey.Serialize(w); err != nil {
return err
}
Expand All @@ -628,6 +632,16 @@ func (e *Entity) Serialize(w io.Writer) error {
}
}
for _, subkey := range e.Subkeys {
// The types of keys below are only useful as private keys. Thus, the
// public key packets contain no meaningful information and do not need
// to be serialized.
// Prevent public key export for forwarding keys, see forwarding section 4.1.
subKeySelfSig, err := subkey.LatestValidBindingSignature(time.Time{})
if subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoHMAC ||
subkey.PublicKey.PubKeyAlgo == packet.ExperimentalPubKeyAlgoAEAD ||
(err == nil && subKeySelfSig.FlagForward) {
continue
}
if err := subkey.Serialize(w, false); err != nil {
return err
}
Expand Down Expand Up @@ -782,5 +796,5 @@ func isValidCertificationKey(signature *packet.Signature, algo packet.PublicKeyA
func isValidEncryptionKey(signature *packet.Signature, algo packet.PublicKeyAlgorithm) bool {
return algo.CanEncrypt() &&
signature.FlagsValid &&
signature.FlagEncryptCommunications
(signature.FlagEncryptCommunications || signature.FlagForward || signature.FlagEncryptStorage)
}
220 changes: 220 additions & 0 deletions openpgp/v2/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/internal/algorithm"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/ProtonMail/go-crypto/openpgp/s2k"
"github.com/ProtonMail/go-crypto/openpgp/symmetric"
)

var hashes = []crypto.Hash{
Expand Down Expand Up @@ -2017,3 +2018,222 @@ NciH07RTRuMS/aRhRg4OB8PQROmTnZ+iZS0=
t.Fatal(err)
}
}

func TestAddHMACSubkey(t *testing.T) {
c := &packet.Config{
RSABits: 512,
Algorithm: packet.ExperimentalPubKeyAlgoHMAC,
}

entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024})
if err != nil {
t.Fatal(err)
}

err = entity.AddSigningSubkey(c)
if err != nil {
t.Fatal(err)
}

buf := bytes.NewBuffer(nil)
w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil)
if err := entity.SerializePrivate(w, nil); err != nil {
t.Errorf("failed to serialize entity: %s", err)
}
w.Close()

key, err := ReadArmoredKeyRing(buf)
if err != nil {
t.Error("could not read keyring", err)
}

generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey)
parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey)

generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey)
parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.HMACPublicKey)

if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) {
t.Error("parsed wrong key")
}
if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) {
t.Error("parsed wrong key in public part")
}
if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) {
t.Error("generated Public and Private Key differ")
}

if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) {
t.Error("parsed wrong hash seed")
}

if parsedPrivateKey.PublicKey.Hash != generatedPrivateKey.PublicKey.Hash {
t.Error("parsed wrong cipher id")
}
if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) {
t.Error("parsed wrong binding hash")
}
}

func TestSerializeSymmetricSubkeyError(t *testing.T) {
entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024})
if err != nil {
t.Fatal(err)
}

buf := bytes.NewBuffer(nil)
w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil)

entity.PrimaryKey.PubKeyAlgo = 100
err = entity.Serialize(w)
if err == nil {
t.Fatal(err)
}

entity.PrimaryKey.PubKeyAlgo = 101
err = entity.Serialize(w)
if err == nil {
t.Fatal(err)
}
}

func TestAddAEADSubkey(t *testing.T) {
c := &packet.Config{
RSABits: 512,
Algorithm: packet.ExperimentalPubKeyAlgoAEAD,
}
entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024})
if err != nil {
t.Fatal(err)
}

err = entity.AddEncryptionSubkey(c)
if err != nil {
t.Fatal(err)
}

generatedPrivateKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey)

buf := bytes.NewBuffer(nil)
w, _ := armor.Encode(buf, "PGP PRIVATE KEY BLOCK", nil)
if err := entity.SerializePrivate(w, nil); err != nil {
t.Errorf("failed to serialize entity: %s", err)
}
w.Close()

key, err := ReadArmoredKeyRing(buf)
if err != nil {
t.Error("could not read keyring", err)
}

parsedPrivateKey := key[0].Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey)

generatedPublicKey := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey)
parsedPublicKey := key[0].Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey)

if !bytes.Equal(parsedPrivateKey.Key, generatedPrivateKey.Key) {
t.Error("parsed wrong key")
}
if !bytes.Equal(parsedPublicKey.Key, generatedPrivateKey.Key) {
t.Error("parsed wrong key in public part")
}
if !bytes.Equal(generatedPublicKey.Key, generatedPrivateKey.Key) {
t.Error("generated Public and Private Key differ")
}

if !bytes.Equal(parsedPrivateKey.HashSeed[:], generatedPrivateKey.HashSeed[:]) {
t.Error("parsed wrong hash seed")
}

if parsedPrivateKey.PublicKey.Cipher.Id() != generatedPrivateKey.PublicKey.Cipher.Id() {
t.Error("parsed wrong cipher id")
}
if !bytes.Equal(parsedPrivateKey.PublicKey.BindingHash[:], generatedPrivateKey.PublicKey.BindingHash[:]) {
t.Error("parsed wrong binding hash")
}
}

func TestNoSymmetricKeySerialized(t *testing.T) {
aeadConfig := &packet.Config{
RSABits: 512,
DefaultHash: crypto.SHA512,
Algorithm: packet.ExperimentalPubKeyAlgoAEAD,
DefaultCipher: packet.CipherAES256,
}
hmacConfig := &packet.Config{
RSABits: 512,
DefaultHash: crypto.SHA512,
Algorithm: packet.ExperimentalPubKeyAlgoHMAC,
DefaultCipher: packet.CipherAES256,
}
entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", &packet.Config{RSABits: 1024})
if err != nil {
t.Fatal(err)
}

err = entity.AddEncryptionSubkey(aeadConfig)
if err != nil {
t.Fatal(err)
}
err = entity.AddSigningSubkey(hmacConfig)
if err != nil {
t.Fatal(err)
}

w := bytes.NewBuffer(nil)
entity.Serialize(w)

firstSymKey := entity.Subkeys[1].PrivateKey.PrivateKey.(*symmetric.AEADPrivateKey).Key
i := bytes.Index(w.Bytes(), firstSymKey)

secondSymKey := entity.Subkeys[2].PrivateKey.PrivateKey.(*symmetric.HMACPrivateKey).Key
k := bytes.Index(w.Bytes(), secondSymKey)

if (i > 0) || (k > 0) {
t.Error("Private key was serialized with public")
}

firstBindingHash := entity.Subkeys[1].PublicKey.PublicKey.(*symmetric.AEADPublicKey).BindingHash
i = bytes.Index(w.Bytes(), firstBindingHash[:])

secondBindingHash := entity.Subkeys[2].PublicKey.PublicKey.(*symmetric.HMACPublicKey).BindingHash
k = bytes.Index(w.Bytes(), secondBindingHash[:])
if (i > 0) || (k > 0) {
t.Errorf("Symmetric public key metadata exported %d %d", i, k)
}

}

func TestSymmetricKeys(t *testing.T) {
data := `-----BEGIN PGP PRIVATE KEY BLOCK-----
xWoEYs7w5mUIcFvlmkuricX26x138uvHGlwIaxWIbRnx1+ggPcveTcwA4zSZ
n6XcD0Q5aLe6dTEBwCyfUecZ/nA0W8Pl9xBHfjIjQuxcUBnIqxZ061RZPjef
D/XIQga1ftLDelhylQwL7R3TzQ1TeW1tZXRyaWMgS2V5wmkEEGUIAB0FAmLO
8OYECwkHCAMVCAoEFgACAQIZAQIbAwIeAQAhCRCRTKq2ObiQKxYhBMHTTXXF
ULQ2M2bYNJFMqrY5uJArIawgJ+5RSsN8VNuZTKJbG88TIedU05wwKjW3wqvT
X6Z7yfbHagRizvDmZAluL/kJo6hZ1kFENpQkWD/Kfv1vAG3nbxhsVEzBQ6a1
OAD24BaKJz6gWgj4lASUNK5OuXnLc3J79Bt1iRGkSbiPzRs/bplB4TwbILeC
ZLeDy9kngZDosgsIk5sBgGEqS9y5HiHCVQQYZQgACQUCYs7w5gIbDAAhCRCR
TKq2ObiQKxYhBMHTTXXFULQ2M2bYNJFMqrY5uJArENkgL0Bc+OI/1na0XWqB
TxGVotQ4A/0u0VbOMEUfnrI8Fms=
=RdCW
-----END PGP PRIVATE KEY BLOCK-----
`
keys, err := ReadArmoredKeyRing(strings.NewReader(data))
if err != nil {
t.Fatal(err)
}
if len(keys) != 1 {
t.Errorf("Expected 1 symmetric key, got %d", len(keys))
}
if keys[0].PrivateKey.PubKeyAlgo != packet.ExperimentalPubKeyAlgoHMAC {
t.Errorf("Expected HMAC primary key")
}
if len(keys[0].Subkeys) != 1 {
t.Errorf("Expected 1 symmetric subkey, got %d", len(keys[0].Subkeys))
}
if keys[0].Subkeys[0].PrivateKey.PubKeyAlgo != packet.ExperimentalPubKeyAlgoAEAD {
t.Errorf("Expected AEAD subkey")
}
}
2 changes: 1 addition & 1 deletion openpgp/v2/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ ParsePackets:
switch p.Algo {
case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly,
packet.PubKeyAlgoElGamal, packet.PubKeyAlgoECDH,
packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448:
packet.PubKeyAlgoX25519, packet.PubKeyAlgoX448, packet.ExperimentalPubKeyAlgoAEAD:
break
default:
continue
Expand Down
7 changes: 7 additions & 0 deletions openpgp/v2/read_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1002,3 +1002,10 @@ func testMalformedMessage(t *testing.T, keyring EntityList, message string) {
return
}
}

func TestReadKeyRingWithSymmetricSubkey(t *testing.T) {
_, err := ReadArmoredKeyRing(strings.NewReader(keyWithAEADSubkey))
if err != nil {
t.Error("could not read keyring", err)
}
}
18 changes: 18 additions & 0 deletions openpgp/v2/read_write_test_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -740,3 +740,21 @@ NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91
xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=
=miES
-----END PGP PRIVATE KEY BLOCK-----`

// A key that contains a persistent AEAD subkey
const keyWithAEADSubkey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
xVgEYs/4KxYJKwYBBAHaRw8BAQdA7tIsntXluwloh/H62PJMqasjP00M86fv
/Pof9A968q8AAQDYcgkPKUdWAxsDjDHJfouPS4q5Me3ks+umlo5RJdwLZw4k
zQ1TeW1tZXRyaWMgS2V5wowEEBYKAB0FAmLP+CsECwkHCAMVCAoEFgACAQIZ
AQIbAwIeAQAhCRDkNhFDvaU8vxYhBDJNoyEFquVOCf99d+Q2EUO9pTy/5XQA
/1F2YPouv0ydBDJU3EOS/4bmPt7yqvzciWzeKVEOkzYuAP9OsP7q/5ccqOPX
mmRUKwd82/cNjdzdnWZ8Tq89XMwMAMdqBGLP+CtkCfFyZxOMF0BWLwAE8pLy
RVj2n2K7k6VvrhyuTqDkFDUFALiSLrEfnmTKlsPYS3/YzsODF354ccR63q73
3lmCrvFRyaf6AHvVrBYPbJR+VhuTjZTwZKvPPKv0zVdSqi5JDEQiocJ4BBgW
CAAJBQJiz/grAhsMACEJEOQ2EUO9pTy/FiEEMk2jIQWq5U4J/3135DYRQ72l
PL+fEQEA7RaRbfa+AtiRN7a4GuqVEDZi3qtQZ2/Qcb27/LkAD0sA/3r9drYv
jyu46h1fdHHyo0HS2MiShZDZ8u60JnDltloD
=8TxH
-----END PGP PRIVATE KEY BLOCK-----
`
Loading

0 comments on commit f27ab7d

Please sign in to comment.