diff --git a/go.mod b/go.mod index d8766e1b..f41adc8c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module github.com/ProtonMail/go-crypto go 1.13 require ( - github.com/cloudflare/circl v1.1.0 // indirect + github.com/cloudflare/circl v1.1.0 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 ) diff --git a/go.sum b/go.sum index 7fe4589a..6586a6e0 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= diff --git a/openpgp/clearsign/clearsign.go b/openpgp/clearsign/clearsign.go index ec66699c..8f70b373 100644 --- a/openpgp/clearsign/clearsign.go +++ b/openpgp/clearsign/clearsign.go @@ -307,6 +307,10 @@ func (d *dashEscaper) Close() (err error) { sig.Hash = d.hashType sig.CreationTime = t sig.IssuerKeyId = &k.KeyId + sig.IssuerFingerprint = k.Fingerprint + sig.Notations = d.config.Notations() + sigLifetimeSecs := d.config.SigLifetime() + sig.SigLifetimeSecs = &sigLifetimeSecs if err = sig.Sign(d.hashers[i], k, d.config); err != nil { return @@ -421,12 +425,6 @@ func (b *Block) VerifySignature(keyring openpgp.KeyRing, config *packet.Config) // if the name isn't known. See RFC 4880, section 9.4. func nameOfHash(h crypto.Hash) string { switch h { - case crypto.MD5: - return "MD5" - case crypto.SHA1: - return "SHA1" - case crypto.RIPEMD160: - return "RIPEMD160" case crypto.SHA224: return "SHA224" case crypto.SHA256: @@ -435,6 +433,10 @@ func nameOfHash(h crypto.Hash) string { return "SHA384" case crypto.SHA512: return "SHA512" + case crypto.SHA3_256: + return "SHA3-256" + case crypto.SHA3_512: + return "SHA3-512" } return "" } @@ -443,12 +445,8 @@ func nameOfHash(h crypto.Hash) string { // if the name isn't known. See RFC 4880, section 9.4. func nameToHash(h string) crypto.Hash { switch h { - case "MD5": - return crypto.MD5 case "SHA1": return crypto.SHA1 - case "RIPEMD160": - return crypto.RIPEMD160 case "SHA224": return crypto.SHA224 case "SHA256": @@ -457,6 +455,10 @@ func nameToHash(h string) crypto.Hash { return crypto.SHA384 case "SHA512": return crypto.SHA512 + case "SHA3-256": + return crypto.SHA3_256 + case "SHA3-512": + return crypto.SHA3_512 } return crypto.Hash(0) } diff --git a/openpgp/integration_tests/end_to_end_test.go b/openpgp/integration_tests/end_to_end_test.go index 37d25a6a..bda40ade 100644 --- a/openpgp/integration_tests/end_to_end_test.go +++ b/openpgp/integration_tests/end_to_end_test.go @@ -308,7 +308,7 @@ func signVerifyTest( } if otherSigner.PrimaryKey.KeyId != skFrom[0].PrimaryKey.KeyId { t.Errorf( - "wrong signer got:%x want:%x", otherSigner.PrimaryKey.KeyId, 0) + "wrong signer: got %x, expected %x", otherSigner.PrimaryKey.KeyId, 0) } } @@ -333,7 +333,7 @@ func signVerifyTest( } if otherSigner.PrimaryKey.KeyId != skFrom[0].PrimaryKey.KeyId { t.Errorf( - "wrong signer got:%x want:%x", + "wrong signer: got %x, expected %x", skFrom[0].PrimaryKey.KeyId, skFrom[0].PrimaryKey.KeyId, ) diff --git a/openpgp/integration_tests/utils_test.go b/openpgp/integration_tests/utils_test.go index d08a6fb0..c0084e0a 100644 --- a/openpgp/integration_tests/utils_test.go +++ b/openpgp/integration_tests/utils_test.go @@ -237,9 +237,9 @@ func randConfig() *packet.Config { pkAlgo := pkAlgos[mathrand.Intn(len(pkAlgos))] aeadModes := []packet.AEADMode{ - packet.AEADModeEAX, packet.AEADModeOCB, - packet.AEADModeExperimentalGCM, + packet.AEADModeEAX, + packet.AEADModeGCM, } var aeadConf = packet.AEADConfig{ DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], diff --git a/openpgp/internal/algorithm/aead.go b/openpgp/internal/algorithm/aead.go index 17a1bfe9..d0670651 100644 --- a/openpgp/internal/algorithm/aead.go +++ b/openpgp/internal/algorithm/aead.go @@ -16,7 +16,7 @@ type AEADMode uint8 const ( AEADModeEAX = AEADMode(1) AEADModeOCB = AEADMode(2) - AEADModeGCM = AEADMode(100) + AEADModeGCM = AEADMode(3) ) // TagLength returns the length in bytes of authentication tags. diff --git a/openpgp/internal/algorithm/hash.go b/openpgp/internal/algorithm/hash.go index 3f1b61b8..82e43d67 100644 --- a/openpgp/internal/algorithm/hash.go +++ b/openpgp/internal/algorithm/hash.go @@ -32,26 +32,25 @@ type Hash interface { // The following vars mirror the crypto/Hash supported hash functions. var ( - MD5 Hash = cryptoHash{1, crypto.MD5} SHA1 Hash = cryptoHash{2, crypto.SHA1} - RIPEMD160 Hash = cryptoHash{3, crypto.RIPEMD160} SHA256 Hash = cryptoHash{8, crypto.SHA256} SHA384 Hash = cryptoHash{9, crypto.SHA384} SHA512 Hash = cryptoHash{10, crypto.SHA512} SHA224 Hash = cryptoHash{11, crypto.SHA224} + SHA3_256 Hash = cryptoHash{12, crypto.SHA3_256} + SHA3_512 Hash = cryptoHash{14, crypto.SHA3_512} ) // HashById represents the different hash functions specified for OpenPGP. See // http://www.iana.org/assignments/pgp-parameters/pgp-parameters.xhtml#pgp-parameters-14 var ( HashById = map[uint8]Hash{ - MD5.Id(): MD5, - SHA1.Id(): SHA1, - RIPEMD160.Id(): RIPEMD160, SHA256.Id(): SHA256, SHA384.Id(): SHA384, SHA512.Id(): SHA512, SHA224.Id(): SHA224, + SHA3_256.Id(): SHA3_256, + SHA3_512.Id(): SHA3_512, } ) @@ -68,13 +67,12 @@ func (h cryptoHash) Id() uint8 { } var hashNames = map[uint8]string{ - MD5.Id(): "MD5", - SHA1.Id(): "SHA1", - RIPEMD160.Id(): "RIPEMD160", SHA256.Id(): "SHA256", SHA384.Id(): "SHA384", SHA512.Id(): "SHA512", SHA224.Id(): "SHA224", + SHA3_256.Id(): "SHA3-256", + SHA3_512.Id(): "SHA3-512", } func (h cryptoHash) String() string { @@ -84,3 +82,62 @@ func (h cryptoHash) String() string { } return s } + +// HashIdToHash returns a crypto.Hash which corresponds to the given OpenPGP +// hash id. +func HashIdToHash(id byte) (h crypto.Hash, ok bool) { + if hash, ok := HashById[id]; ok { + return hash.HashFunc(), true + } + return 0, false +} + +// HashIdToHashWithSha1 returns a crypto.Hash which corresponds to the given OpenPGP +// hash id, allowing sha1. +func HashIdToHashWithSha1(id byte) (h crypto.Hash, ok bool) { + if hash, ok := HashById[id]; ok { + return hash.HashFunc(), true + } + + if id == SHA1.Id() { + return SHA1.HashFunc(), true + } + + return 0, false +} + +// HashIdToString returns the name of the hash function corresponding to the +// given OpenPGP hash id. +func HashIdToString(id byte) (name string, ok bool) { + if hash, ok := HashById[id]; ok { + return hash.String(), true + } + return "", false +} + +// HashToHashId returns an OpenPGP hash id which corresponds the given Hash. +func HashToHashId(h crypto.Hash) (id byte, ok bool) { + for id, hash := range HashById { + if hash.HashFunc() == h { + return id, true + } + } + + return 0, false +} + +// HashToHashIdWithSha1 returns an OpenPGP hash id which corresponds the given Hash, +// allowing instances of SHA1 +func HashToHashIdWithSha1(h crypto.Hash) (id byte, ok bool) { + for id, hash := range HashById { + if hash.HashFunc() == h { + return id, true + } + } + + if h == SHA1.HashFunc() { + return SHA1.Id(), true + } + + return 0, false +} diff --git a/openpgp/key_generation.go b/openpgp/key_generation.go index 118dd656..0e71934c 100644 --- a/openpgp/key_generation.go +++ b/openpgp/key_generation.go @@ -82,27 +82,24 @@ func (t *Entity) addUserId(name, comment, email string, config *packet.Config, c isPrimaryId := len(t.Identities) == 0 - selfSignature := &packet.Signature{ - Version: primary.PublicKey.Version, - SigType: packet.SigTypePositiveCert, - PubKeyAlgo: primary.PublicKey.PubKeyAlgo, - Hash: config.Hash(), - CreationTime: creationTime, - KeyLifetimeSecs: &keyLifetimeSecs, - IssuerKeyId: &primary.PublicKey.KeyId, - IssuerFingerprint: primary.PublicKey.Fingerprint, - IsPrimaryId: &isPrimaryId, - FlagsValid: true, - FlagSign: true, - FlagCertify: true, - MDC: true, // true by default, see 5.8 vs. 5.14 - AEAD: config.AEAD() != nil, - V5Keys: config != nil && config.V5Keys, - } + selfSignature := createSignaturePacket(&primary.PublicKey, packet.SigTypePositiveCert, config) + selfSignature.CreationTime = creationTime + selfSignature.KeyLifetimeSecs = &keyLifetimeSecs + selfSignature.IsPrimaryId = &isPrimaryId + selfSignature.FlagsValid = true + selfSignature.FlagSign = true + selfSignature.FlagCertify = true + selfSignature.SEIPDv1 = true // true by default, see 5.8 vs. 5.14 + selfSignature.SEIPDv2 = config.AEAD() != nil // Set the PreferredHash for the SelfSignature from the packet.Config. // If it is not the must-implement algorithm from rfc4880bis, append that. - selfSignature.PreferredHash = []uint8{hashToHashId(config.Hash())} + hash, ok := algorithm.HashToHashId(config.Hash()) + if !ok { + return errors.UnsupportedError("unsupported preferred hash function") + } + + selfSignature.PreferredHash = []uint8{hash} if config.Hash() != crypto.SHA256 { selfSignature.PreferredHash = append(selfSignature.PreferredHash, hashToHashId(crypto.SHA256)) } @@ -123,9 +120,16 @@ func (t *Entity) addUserId(name, comment, email string, config *packet.Config, c } // And for DefaultMode. - selfSignature.PreferredAEAD = []uint8{uint8(config.AEAD().Mode())} - if config.AEAD().Mode() != packet.AEADModeEAX { - selfSignature.PreferredAEAD = append(selfSignature.PreferredAEAD, uint8(packet.AEADModeEAX)) + modes := []uint8{uint8(config.AEAD().Mode())} + if config.AEAD().Mode() != packet.AEADModeOCB { + modes = append(modes, uint8(packet.AEADModeOCB)) + } + + // For preferred (AES256, GCM), we'll generate (AES256, GCM), (AES256, OCB), (AES128, GCM), (AES128, OCB) + for _, cipher := range selfSignature.PreferredSymmetric { + for _, mode := range modes { + selfSignature.PreferredCipherSuites = append(selfSignature.PreferredCipherSuites, [2]uint8{cipher, mode}) + } } // User ID binding signature @@ -153,42 +157,30 @@ func (e *Entity) AddSigningSubkey(config *packet.Config) error { return err } sub := packet.NewSignerPrivateKey(creationTime, subPrivRaw) + sub.IsSubkey = true + if config != nil && config.V5Keys { + sub.UpgradeToV5() + } subkey := Subkey{ PublicKey: &sub.PublicKey, PrivateKey: sub, - Sig: &packet.Signature{ - Version: e.PrimaryKey.Version, - CreationTime: creationTime, - KeyLifetimeSecs: &keyLifetimeSecs, - SigType: packet.SigTypeSubkeyBinding, - PubKeyAlgo: e.PrimaryKey.PubKeyAlgo, - Hash: config.Hash(), - FlagsValid: true, - FlagSign: true, - IssuerKeyId: &e.PrimaryKey.KeyId, - EmbeddedSignature: &packet.Signature{ - Version: e.PrimaryKey.Version, - CreationTime: creationTime, - SigType: packet.SigTypePrimaryKeyBinding, - PubKeyAlgo: sub.PublicKey.PubKeyAlgo, - Hash: config.Hash(), - IssuerKeyId: &e.PrimaryKey.KeyId, - }, - }, - } - if config != nil && config.V5Keys { - subkey.PublicKey.UpgradeToV5() } + subkey.Sig = createSignaturePacket(e.PrimaryKey, packet.SigTypeSubkeyBinding, config) + subkey.Sig.CreationTime = creationTime + subkey.Sig.KeyLifetimeSecs = &keyLifetimeSecs + subkey.Sig.FlagsValid = true + subkey.Sig.FlagSign = true + subkey.Sig.EmbeddedSignature = createSignaturePacket(subkey.PublicKey, packet.SigTypePrimaryKeyBinding, config) + subkey.Sig.EmbeddedSignature.CreationTime = creationTime err = subkey.Sig.EmbeddedSignature.CrossSignKey(subkey.PublicKey, e.PrimaryKey, subkey.PrivateKey, config) if err != nil { return err } - subkey.PublicKey.IsSubkey = true - subkey.PrivateKey.IsSubkey = true - if err = subkey.Sig.SignKey(subkey.PublicKey, e.PrivateKey, config); err != nil { + err = subkey.Sig.SignKey(subkey.PublicKey, e.PrivateKey, config) + if err != nil { return err } @@ -210,30 +202,24 @@ func (e *Entity) addEncryptionSubkey(config *packet.Config, creationTime time.Ti return err } sub := packet.NewDecrypterPrivateKey(creationTime, subPrivRaw) + sub.IsSubkey = true + if config != nil && config.V5Keys { + sub.UpgradeToV5() + } subkey := Subkey{ PublicKey: &sub.PublicKey, PrivateKey: sub, - Sig: &packet.Signature{ - Version: e.PrimaryKey.Version, - CreationTime: creationTime, - KeyLifetimeSecs: &keyLifetimeSecs, - SigType: packet.SigTypeSubkeyBinding, - PubKeyAlgo: e.PrimaryKey.PubKeyAlgo, - Hash: config.Hash(), - FlagsValid: true, - FlagEncryptStorage: true, - FlagEncryptCommunications: true, - IssuerKeyId: &e.PrimaryKey.KeyId, - }, } - if config != nil && config.V5Keys { - subkey.PublicKey.UpgradeToV5() - } - - subkey.PublicKey.IsSubkey = true - subkey.PrivateKey.IsSubkey = true - if err = subkey.Sig.SignKey(subkey.PublicKey, e.PrivateKey, config); err != nil { + subkey.Sig = createSignaturePacket(e.PrimaryKey, packet.SigTypeSubkeyBinding, config) + subkey.Sig.CreationTime = creationTime + subkey.Sig.KeyLifetimeSecs = &keyLifetimeSecs + subkey.Sig.FlagsValid = true + subkey.Sig.FlagEncryptStorage = true + subkey.Sig.FlagEncryptCommunications = true + + err = subkey.Sig.SignKey(subkey.PublicKey, e.PrivateKey, config) + if err != nil { return err } diff --git a/openpgp/keys.go b/openpgp/keys.go index 48278e6e..120f081a 100644 --- a/openpgp/keys.go +++ b/openpgp/keys.go @@ -751,18 +751,7 @@ func (e *Entity) SignIdentity(identity string, signer *Entity, config *packet.Co return errors.InvalidArgumentError("given identity string not found in Entity") } - sig := &packet.Signature{ - Version: certificationKey.PrivateKey.Version, - SigType: packet.SigTypeGenericCert, - PubKeyAlgo: certificationKey.PrivateKey.PubKeyAlgo, - Hash: config.Hash(), - CreationTime: config.Now(), - IssuerKeyId: &certificationKey.PrivateKey.KeyId, - } - - if config.SigLifetime() != 0 { - sig.SigLifetimeSecs = &config.SigLifetimeSecs - } + sig := createSignaturePacket(certificationKey.PublicKey, packet.SigTypeGenericCert, config) signingUserID := config.SigningUserId() if signingUserID != "" { @@ -783,16 +772,9 @@ func (e *Entity) SignIdentity(identity string, signer *Entity, config *packet.Co // specified reason code and text (RFC4880 section-5.2.3.23). // If config is nil, sensible defaults will be used. func (e *Entity) RevokeKey(reason packet.ReasonForRevocation, reasonText string, config *packet.Config) error { - revSig := &packet.Signature{ - Version: e.PrimaryKey.Version, - CreationTime: config.Now(), - SigType: packet.SigTypeKeyRevocation, - PubKeyAlgo: e.PrimaryKey.PubKeyAlgo, - Hash: config.Hash(), - RevocationReason: &reason, - RevocationReasonText: reasonText, - IssuerKeyId: &e.PrimaryKey.KeyId, - } + revSig := createSignaturePacket(e.PrimaryKey, packet.SigTypeKeyRevocation, config) + revSig.RevocationReason = &reason + revSig.RevocationReasonText = reasonText if err := revSig.RevokeKey(e.PrimaryKey, e.PrivateKey, config); err != nil { return err @@ -809,16 +791,9 @@ func (e *Entity) RevokeSubkey(sk *Subkey, reason packet.ReasonForRevocation, rea return errors.InvalidArgumentError("given subkey is not associated with this key") } - revSig := &packet.Signature{ - Version: e.PrimaryKey.Version, - CreationTime: config.Now(), - SigType: packet.SigTypeSubkeyRevocation, - PubKeyAlgo: e.PrimaryKey.PubKeyAlgo, - Hash: config.Hash(), - RevocationReason: &reason, - RevocationReasonText: reasonText, - IssuerKeyId: &e.PrimaryKey.KeyId, - } + revSig := createSignaturePacket(e.PrimaryKey, packet.SigTypeSubkeyRevocation, config) + revSig.RevocationReason = &reason + revSig.RevocationReasonText = reasonText if err := revSig.RevokeSubkey(sk.PublicKey, e.PrivateKey, config); err != nil { return err diff --git a/openpgp/keys_test.go b/openpgp/keys_test.go index c1c8e825..5e2820cf 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -28,6 +28,8 @@ var hashes = []crypto.Hash{ crypto.SHA256, crypto.SHA384, crypto.SHA512, + crypto.SHA3_256, + crypto.SHA3_512, } var ciphers = []packet.CipherFunction{ @@ -39,8 +41,9 @@ var ciphers = []packet.CipherFunction{ } var aeadModes = []packet.AEADMode{ - packet.AEADModeEAX, packet.AEADModeOCB, + packet.AEADModeEAX, + packet.AEADModeGCM, } func TestKeyExpiry(t *testing.T) { @@ -799,6 +802,13 @@ func TestNewEntityWithDefaultHash(t *testing.T) { DefaultHash: hash, } entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", c) + if hash == crypto.SHA1 { + if err == nil { + t.Fatal("should fail on SHA1 key creation") + } + continue + } + if err != nil { t.Fatal(err) } @@ -897,14 +907,20 @@ func TestNewEntityWithDefaultAead(t *testing.T) { } for _, identity := range entity.Identities { - if len(identity.SelfSignature.PreferredAEAD) == 0 { + if len(identity.SelfSignature.PreferredCipherSuites) == 0 { t.Fatal("didn't find a preferred mode in self signature") } - mode := identity.SelfSignature.PreferredAEAD[0] + cipher := identity.SelfSignature.PreferredCipherSuites[0][0] + if cipher != uint8(cfg.Cipher()) { + t.Fatalf("Expected preferred cipher to be %d, got %d", + uint8(cfg.Cipher()), + identity.SelfSignature.PreferredCipherSuites[0][0]) + } + mode := identity.SelfSignature.PreferredCipherSuites[0][1] if mode != uint8(cfg.AEAD().DefaultMode) { t.Fatalf("Expected preferred mode to be %d, got %d", uint8(cfg.AEAD().DefaultMode), - identity.SelfSignature.PreferredAEAD[0]) + identity.SelfSignature.PreferredCipherSuites[0][1]) } } } @@ -944,6 +960,69 @@ func TestNewEntityPrivateSerialization(t *testing.T) { } } +func TestNotationPacket(t *testing.T) { + keys, err := ReadArmoredKeyRing(bytes.NewBufferString(keyWithNotation)) + if err != nil { + t.Fatal(err) + } + + assertNotationPackets(t, keys) + + serializedEntity := bytes.NewBuffer(nil) + err = keys[0].SerializePrivate(serializedEntity, nil) + if err != nil { + t.Fatal(err) + } + + keys, err = ReadKeyRing(serializedEntity) + if err != nil { + t.Fatal(err) + } + + assertNotationPackets(t, keys) +} + +func assertNotationPackets(t *testing.T, keys EntityList) { + if len(keys) != 1 { + t.Errorf("Failed to accept key, %d", len(keys)) + } + + identity := keys[0].Identities["Test "] + + if numSigs, numExpected := len(identity.Signatures), 1; numSigs != numExpected { + t.Fatalf("got %d signatures, expected %d", numSigs, numExpected) + } + + notations := identity.Signatures[0].Notations + if numNotations, numExpected := len(notations), 2; numNotations != numExpected { + t.Fatalf("got %d Notation Data subpackets, expected %d", numNotations, numExpected) + } + + if notations[0].IsHumanReadable != true { + t.Fatalf("got false, expected true") + } + + if notations[0].Name != "text@example.com" { + t.Fatalf("got %s, expected text@example.com", notations[0].Name) + } + + if string(notations[0].Value) != "test" { + t.Fatalf("got %s, expected \"test\"", string(notations[0].Value)) + } + + if notations[1].IsHumanReadable != false { + t.Fatalf("got true, expected false") + } + + if notations[1].Name != "binary@example.com" { + t.Fatalf("got %s, expected binary@example.com", notations[1].Name) + } + + if !bytes.Equal(notations[1].Value, []byte{0, 1, 2, 3}) { + t.Fatalf("got %s, expected {0, 1, 2, 3}", string(notations[1].Value)) + } +} + func TestEntityPrivateSerialization(t *testing.T) { keys, err := ReadArmoredKeyRing(bytes.NewBufferString(armoredPrivateKeyBlock)) if err != nil { diff --git a/openpgp/keys_test_data.go b/openpgp/keys_test_data.go index 4bcfd5fd..108fd096 100644 --- a/openpgp/keys_test_data.go +++ b/openpgp/keys_test_data.go @@ -518,3 +518,21 @@ XLCBln+wdewpU4ChEffMUDRBfqfQco/YsMqWV7bHJHAO0eC/DMKCjyU90xdH7R/d QgqsfguR1PqPuJxpXV4bSr6CGAAAAA== =MSvh -----END PGP PRIVATE KEY BLOCK-----` + +const keyWithNotation = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEY9gIshYJKwYBBAHaRw8BAQdAF25fSM8OpFlXZhop4Qpqo5ywGZ4jgWlR +ppjhIKDthREAAQC+LFpzFcMJYcjxGKzBGHN0Px2jU4d04YSRnFAik+lVVQ6u +zRdUZXN0IDx0ZXN0QGV4YW1wbGUuY29tPsLACgQQFgoAfAUCY9gIsgQLCQcI +CRD/utJOCym8pR0UgAAAAAAQAAR0ZXh0QGV4YW1wbGUuY29tdGVzdB8UAAAA +AAASAARiaW5hcnlAZXhhbXBsZS5jb20AAQIDAxUICgQWAAIBAhkBAhsDAh4B +FiEEEMCQTUVGKgCX5rDQ/7rSTgspvKUAAPl5AP9Npz90LxzrB97Qr2DrGwfG +wuYn4FSYwtuPfZHHeoIabwD/QEbvpQJ/NBb9EAZuow4Rirlt1yv19mmnF+j5 +8yUzhQjHXQRj2AiyEgorBgEEAZdVAQUBAQdARXAo30DmKcyUg6co7OUm0RNT +z9iqFbDBzA8A47JEt1MDAQgHAAD/XKK3lBm0SqMR558HLWdBrNG6NqKuqb5X +joCML987ZNgRD8J4BBgWCAAqBQJj2AiyCRD/utJOCym8pQIbDBYhBBDAkE1F +RioAl+aw0P+60k4LKbylAADRxgEAg7UfBDiDPp5LHcW9D+SgFHk6+GyEU4ev +VppQxdtxPvAA/34snHBX7Twnip1nMt7P4e2hDiw/hwQ7oqioOvc6jMkP +=Z8YJ +-----END PGP PRIVATE KEY BLOCK----- +` diff --git a/openpgp/packet/aead_config.go b/openpgp/packet/aead_config.go index 7350974e..fec41a0e 100644 --- a/openpgp/packet/aead_config.go +++ b/openpgp/packet/aead_config.go @@ -4,6 +4,14 @@ package packet import "math/bits" +// CipherSuite contains a combination of Cipher and Mode +type CipherSuite struct { + // The cipher function + Cipher CipherFunction + // The AEAD mode of operation. + Mode AEADMode +} + // AEADConfig collects a number of AEAD parameters along with sensible defaults. // A nil AEADConfig is valid and results in all default values. type AEADConfig struct { @@ -15,12 +23,13 @@ type AEADConfig struct { // Mode returns the AEAD mode of operation. func (conf *AEADConfig) Mode() AEADMode { + // If no preference is specified, OCB is used (which is mandatory to implement). if conf == nil || conf.DefaultMode == 0 { - return AEADModeEAX + return AEADModeOCB } + mode := conf.DefaultMode - if mode != AEADModeEAX && mode != AEADModeOCB && - mode != AEADModeExperimentalGCM { + if mode != AEADModeEAX && mode != AEADModeOCB && mode != AEADModeGCM { panic("AEAD mode unsupported") } return mode @@ -28,6 +37,8 @@ func (conf *AEADConfig) Mode() AEADMode { // ChunkSizeByte returns the byte indicating the chunk size. The effective // chunk size is computed with the formula uint64(1) << (chunkSizeByte + 6) +// limit to 16 = 4 MiB +// https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-5.13.2 func (conf *AEADConfig) ChunkSizeByte() byte { if conf == nil || conf.ChunkSize == 0 { return 12 // 1 << (12 + 6) == 262144 bytes @@ -38,8 +49,8 @@ func (conf *AEADConfig) ChunkSizeByte() byte { switch { case exponent < 6: exponent = 6 - case exponent > 27: - exponent = 27 + case exponent > 16: + exponent = 16 } return byte(exponent - 6) diff --git a/openpgp/packet/aead_crypter.go b/openpgp/packet/aead_crypter.go new file mode 100644 index 00000000..a82b040b --- /dev/null +++ b/openpgp/packet/aead_crypter.go @@ -0,0 +1,265 @@ +// Copyright (C) 2019 ProtonTech AG + +package packet + +import ( + "bytes" + "crypto/cipher" + "encoding/binary" + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" +) + +// aeadCrypter is an AEAD opener/sealer, its configuration, and data for en/decryption. +type aeadCrypter struct { + aead cipher.AEAD + chunkSize int + initialNonce []byte + associatedData []byte // Chunk-independent associated data + chunkIndex []byte // Chunk counter + packetTag packetType + bytesProcessed int // Amount of plaintext bytes encrypted/decrypted + buffer bytes.Buffer // Buffered bytes across chunks +} + +// computeNonce takes the incremental index and computes an eXclusive OR with +// the least significant 8 bytes of the receivers' initial nonce (see sec. +// 5.16.1 and 5.16.2). It returns the resulting nonce. +func (wo *aeadCrypter) computeNextNonce() (nonce []byte) { + if wo.packetTag == packetTypeSymmetricallyEncryptedIntegrityProtected { + return append(wo.initialNonce, wo.chunkIndex...) + } + + nonce = make([]byte, len(wo.initialNonce)) + copy(nonce, wo.initialNonce) + offset := len(wo.initialNonce) - 8 + for i := 0; i < 8; i++ { + nonce[i+offset] ^= wo.chunkIndex[i] + } + return +} + +// incrementIndex performs an integer increment by 1 of the integer represented by the +// slice, modifying it accordingly. +func (wo *aeadCrypter) incrementIndex() error { + index := wo.chunkIndex + if len(index) == 0 { + return errors.AEADError("Index has length 0") + } + for i := len(index) - 1; i >= 0; i-- { + if index[i] < 255 { + index[i]++ + return nil + } + index[i] = 0 + } + return errors.AEADError("cannot further increment index") +} + +// aeadDecrypter reads and decrypts bytes. It buffers extra decrypted bytes when +// necessary, similar to aeadEncrypter. +type aeadDecrypter struct { + aeadCrypter // Embedded ciphertext opener + reader io.Reader // 'reader' is a partialLengthReader + peekedBytes []byte // Used to detect last chunk + eof bool +} + +// Read decrypts bytes and reads them into dst. It decrypts when necessary and +// buffers extra decrypted bytes. It returns the number of bytes copied into dst +// and an error. +func (ar *aeadDecrypter) Read(dst []byte) (n int, err error) { + // Return buffered plaintext bytes from previous calls + if ar.buffer.Len() > 0 { + return ar.buffer.Read(dst) + } + + // Return EOF if we've previously validated the final tag + if ar.eof { + return 0, io.EOF + } + + // Read a chunk + tagLen := ar.aead.Overhead() + cipherChunkBuf := new(bytes.Buffer) + _, errRead := io.CopyN(cipherChunkBuf, ar.reader, int64(ar.chunkSize + tagLen)) + cipherChunk := cipherChunkBuf.Bytes() + if errRead != nil && errRead != io.EOF { + return 0, errRead + } + decrypted, errChunk := ar.openChunk(cipherChunk) + if errChunk != nil { + return 0, errChunk + } + + // Return decrypted bytes, buffering if necessary + if len(dst) < len(decrypted) { + n = copy(dst, decrypted[:len(dst)]) + ar.buffer.Write(decrypted[len(dst):]) + } else { + n = copy(dst, decrypted) + } + + // Check final authentication tag + if errRead == io.EOF { + errChunk := ar.validateFinalTag(ar.peekedBytes) + if errChunk != nil { + return n, errChunk + } + ar.eof = true // Mark EOF for when we've returned all buffered data + } + return +} + +// Close is noOp. The final authentication tag of the stream was already +// checked in the last Read call. In the future, this function could be used to +// wipe the reader and peeked, decrypted bytes, if necessary. +func (ar *aeadDecrypter) Close() (err error) { + return nil +} + +// openChunk decrypts and checks integrity of an encrypted chunk, returning +// the underlying plaintext and an error. It accesses peeked bytes from next +// chunk, to identify the last chunk and decrypt/validate accordingly. +func (ar *aeadDecrypter) openChunk(data []byte) ([]byte, error) { + tagLen := ar.aead.Overhead() + // Restore carried bytes from last call + chunkExtra := append(ar.peekedBytes, data...) + // 'chunk' contains encrypted bytes, followed by an authentication tag. + chunk := chunkExtra[:len(chunkExtra)-tagLen] + ar.peekedBytes = chunkExtra[len(chunkExtra)-tagLen:] + + adata := ar.associatedData + if ar.aeadCrypter.packetTag == packetTypeAEADEncrypted { + adata = append(ar.associatedData, ar.chunkIndex...) + } + + nonce := ar.computeNextNonce() + plainChunk, err := ar.aead.Open(nil, nonce, chunk, adata) + if err != nil { + return nil, err + } + ar.bytesProcessed += len(plainChunk) + if err = ar.aeadCrypter.incrementIndex(); err != nil { + return nil, err + } + return plainChunk, nil +} + +// Checks the summary tag. It takes into account the total decrypted bytes into +// the associated data. It returns an error, or nil if the tag is valid. +func (ar *aeadDecrypter) validateFinalTag(tag []byte) error { + // Associated: tag, version, cipher, aead, chunk size, ... + amountBytes := make([]byte, 8) + binary.BigEndian.PutUint64(amountBytes, uint64(ar.bytesProcessed)) + + adata := ar.associatedData + if ar.aeadCrypter.packetTag == packetTypeAEADEncrypted { + // ... index ... + adata = append(ar.associatedData, ar.chunkIndex...) + } + + // ... and total number of encrypted octets + adata = append(adata, amountBytes...) + nonce := ar.computeNextNonce() + _, err := ar.aead.Open(nil, nonce, tag, adata) + if err != nil { + return err + } + return nil +} + +// aeadEncrypter encrypts and writes bytes. It encrypts when necessary according +// to the AEAD block size, and buffers the extra encrypted bytes for next write. +type aeadEncrypter struct { + aeadCrypter // Embedded plaintext sealer + writer io.WriteCloser // 'writer' is a partialLengthWriter +} + + +// Write encrypts and writes bytes. It encrypts when necessary and buffers extra +// plaintext bytes for next call. When the stream is finished, Close() MUST be +// called to append the final tag. +func (aw *aeadEncrypter) Write(plaintextBytes []byte) (n int, err error) { + // Append plaintextBytes to existing buffered bytes + n, err = aw.buffer.Write(plaintextBytes) + if err != nil { + return n, err + } + // Encrypt and write chunks + for aw.buffer.Len() >= aw.chunkSize { + plainChunk := aw.buffer.Next(aw.chunkSize) + encryptedChunk, err := aw.sealChunk(plainChunk) + if err != nil { + return n, err + } + _, err = aw.writer.Write(encryptedChunk) + if err != nil { + return n, err + } + } + return +} + +// Close encrypts and writes the remaining buffered plaintext if any, appends +// the final authentication tag, and closes the embedded writer. This function +// MUST be called at the end of a stream. +func (aw *aeadEncrypter) Close() (err error) { + // Encrypt and write a chunk if there's buffered data left, or if we haven't + // written any chunks yet. + if aw.buffer.Len() > 0 || aw.bytesProcessed == 0 { + plainChunk := aw.buffer.Bytes() + lastEncryptedChunk, err := aw.sealChunk(plainChunk) + if err != nil { + return err + } + _, err = aw.writer.Write(lastEncryptedChunk) + if err != nil { + return err + } + } + // Compute final tag (associated data: packet tag, version, cipher, aead, + // chunk size... + adata := aw.associatedData + + if aw.aeadCrypter.packetTag == packetTypeAEADEncrypted { + // ... index ... + adata = append(aw.associatedData, aw.chunkIndex...) + } + + // ... and total number of encrypted octets + amountBytes := make([]byte, 8) + binary.BigEndian.PutUint64(amountBytes, uint64(aw.bytesProcessed)) + adata = append(adata, amountBytes...) + + nonce := aw.computeNextNonce() + finalTag := aw.aead.Seal(nil, nonce, nil, adata) + _, err = aw.writer.Write(finalTag) + if err != nil { + return err + } + return aw.writer.Close() +} + +// sealChunk Encrypts and authenticates the given chunk. +func (aw *aeadEncrypter) sealChunk(data []byte) ([]byte, error) { + if len(data) > aw.chunkSize { + return nil, errors.AEADError("chunk exceeds maximum length") + } + if aw.associatedData == nil { + return nil, errors.AEADError("can't seal without headers") + } + adata := aw.associatedData + if aw.aeadCrypter.packetTag == packetTypeAEADEncrypted { + adata = append(aw.associatedData, aw.chunkIndex...) + } + + nonce := aw.computeNextNonce() + encrypted := aw.aead.Seal(nil, nonce, data, adata) + aw.bytesProcessed += len(data) + if err := aw.aeadCrypter.incrementIndex(); err != nil { + return nil, err + } + return encrypted, nil +} diff --git a/openpgp/packet/aead_encrypted.go b/openpgp/packet/aead_encrypted.go index 862b1ac0..98bd876b 100644 --- a/openpgp/packet/aead_encrypted.go +++ b/openpgp/packet/aead_encrypted.go @@ -3,17 +3,14 @@ package packet import ( - "bytes" - "crypto/cipher" - "crypto/rand" - "encoding/binary" "io" "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" ) -// AEADEncrypted represents an AEAD Encrypted Packet (tag 20, RFC4880bis-5.16). +// AEADEncrypted represents an AEAD Encrypted Packet. +// See https://www.ietf.org/archive/id/draft-koch-openpgp-2015-rfc4880bis-00.html#name-aead-encrypted-data-packet-t type AEADEncrypted struct { cipher CipherFunction mode AEADMode @@ -25,33 +22,6 @@ type AEADEncrypted struct { // Only currently defined version const aeadEncryptedVersion = 1 -// An AEAD opener/sealer, its configuration, and data for en/decryption. -type aeadCrypter struct { - aead cipher.AEAD - chunkSize int - initialNonce []byte - associatedData []byte // Chunk-independent associated data - chunkIndex []byte // Chunk counter - bytesProcessed int // Amount of plaintext bytes encrypted/decrypted - buffer bytes.Buffer // Buffered bytes across chunks -} - -// aeadEncrypter encrypts and writes bytes. It encrypts when necessary according -// to the AEAD block size, and buffers the extra encrypted bytes for next write. -type aeadEncrypter struct { - aeadCrypter // Embedded plaintext sealer - writer io.WriteCloser // 'writer' is a partialLengthWriter -} - -// aeadDecrypter reads and decrypts bytes. It buffers extra decrypted bytes when -// necessary, similar to aeadEncrypter. -type aeadDecrypter struct { - aeadCrypter // Embedded ciphertext opener - reader io.Reader // 'reader' is a partialLengthReader - peekedBytes []byte // Used to detect last chunk - eof bool -} - func (ae *AEADEncrypted) parse(buf io.Reader) error { headerData := make([]byte, 4) if n, err := io.ReadFull(buf, headerData); n < 4 { @@ -59,10 +29,14 @@ func (ae *AEADEncrypted) parse(buf io.Reader) error { } // Read initial nonce mode := AEADMode(headerData[2]) - nonceLen := mode.NonceLength() - if nonceLen == 0 { + nonceLen := mode.IvLength() + + // This packet supports only EAX and OCB + // https://www.ietf.org/archive/id/draft-koch-openpgp-2015-rfc4880bis-00.html#name-aead-encrypted-data-packet-t + if nonceLen == 0 || mode > AEADModeOCB { return errors.AEADError("unknown mode") } + initialNonce := make([]byte, nonceLen) if n, err := io.ReadFull(buf, initialNonce); n < nonceLen { return errors.AEADError("could not read aead nonce:" + err.Error()) @@ -75,7 +49,7 @@ func (ae *AEADEncrypted) parse(buf io.Reader) error { } ae.cipher = CipherFunction(c) ae.mode = mode - ae.chunkSizeByte = byte(headerData[3]) + ae.chunkSizeByte = headerData[3] return nil } @@ -105,225 +79,13 @@ func (ae *AEADEncrypted) decrypt(key []byte) (io.ReadCloser, error) { initialNonce: ae.initialNonce, associatedData: ae.associatedData(), chunkIndex: make([]byte, 8), + packetTag: packetTypeAEADEncrypted, }, reader: ae.Contents, peekedBytes: peekedBytes}, nil } -// Read decrypts bytes and reads them into dst. It decrypts when necessary and -// buffers extra decrypted bytes. It returns the number of bytes copied into dst -// and an error. -func (ar *aeadDecrypter) Read(dst []byte) (n int, err error) { - // Return buffered plaintext bytes from previous calls - if ar.buffer.Len() > 0 { - return ar.buffer.Read(dst) - } - - // Return EOF if we've previously validated the final tag - if ar.eof { - return 0, io.EOF - } - - // Read a chunk - tagLen := ar.aead.Overhead() - cipherChunkBuf := new(bytes.Buffer) - _, errRead := io.CopyN(cipherChunkBuf, ar.reader, int64(ar.chunkSize + tagLen)) - cipherChunk := cipherChunkBuf.Bytes() - if errRead != nil && errRead != io.EOF { - return 0, errRead - } - decrypted, errChunk := ar.openChunk(cipherChunk) - if errChunk != nil { - return 0, errChunk - } - - // Return decrypted bytes, buffering if necessary - if len(dst) < len(decrypted) { - n = copy(dst, decrypted[:len(dst)]) - ar.buffer.Write(decrypted[len(dst):]) - } else { - n = copy(dst, decrypted) - } - - // Check final authentication tag - if errRead == io.EOF { - errChunk := ar.validateFinalTag(ar.peekedBytes) - if errChunk != nil { - return n, errChunk - } - ar.eof = true // Mark EOF for when we've returned all buffered data - } - return -} - -// Close is noOp. The final authentication tag of the stream was already -// checked in the last Read call. In the future, this function could be used to -// wipe the reader and peeked, decrypted bytes, if necessary. -func (ar *aeadDecrypter) Close() (err error) { - return nil -} - -// SerializeAEADEncrypted initializes the aeadCrypter and returns a writer. -// This writer encrypts and writes bytes (see aeadEncrypter.Write()). -func SerializeAEADEncrypted(w io.Writer, key []byte, cipher CipherFunction, mode AEADMode, config *Config) (io.WriteCloser, error) { - writeCloser := noOpCloser{w} - writer, err := serializeStreamHeader(writeCloser, packetTypeAEADEncrypted) - if err != nil { - return nil, err - } - - // Data for en/decryption: tag, version, cipher, aead mode, chunk size - aeadConf := config.AEAD() - prefix := []byte{ - 0xD4, - aeadEncryptedVersion, - byte(config.Cipher()), - byte(aeadConf.Mode()), - aeadConf.ChunkSizeByte(), - } - n, err := writer.Write(prefix[1:]) - if err != nil || n < 4 { - return nil, errors.AEADError("could not write AEAD headers") - } - // Sample nonce - nonceLen := aeadConf.Mode().NonceLength() - nonce := make([]byte, nonceLen) - n, err = rand.Read(nonce) - if err != nil { - panic("Could not sample random nonce") - } - _, err = writer.Write(nonce) - if err != nil { - return nil, err - } - blockCipher := CipherFunction(config.Cipher()).new(key) - alg := AEADMode(aeadConf.Mode()).new(blockCipher) - - chunkSize := decodeAEADChunkSize(aeadConf.ChunkSizeByte()) - return &aeadEncrypter{ - aeadCrypter: aeadCrypter{ - aead: alg, - chunkSize: chunkSize, - associatedData: prefix, - chunkIndex: make([]byte, 8), - initialNonce: nonce, - }, - writer: writer}, nil -} - -// Write encrypts and writes bytes. It encrypts when necessary and buffers extra -// plaintext bytes for next call. When the stream is finished, Close() MUST be -// called to append the final tag. -func (aw *aeadEncrypter) Write(plaintextBytes []byte) (n int, err error) { - // Append plaintextBytes to existing buffered bytes - n, err = aw.buffer.Write(plaintextBytes) - if err != nil { - return n, err - } - // Encrypt and write chunks - for aw.buffer.Len() >= aw.chunkSize { - plainChunk := aw.buffer.Next(aw.chunkSize) - encryptedChunk, err := aw.sealChunk(plainChunk) - if err != nil { - return n, err - } - _, err = aw.writer.Write(encryptedChunk) - if err != nil { - return n, err - } - } - return -} - -// Close encrypts and writes the remaining buffered plaintext if any, appends -// the final authentication tag, and closes the embedded writer. This function -// MUST be called at the end of a stream. -func (aw *aeadEncrypter) Close() (err error) { - // Encrypt and write a chunk if there's buffered data left, or if we haven't - // written any chunks yet. - if aw.buffer.Len() > 0 || aw.bytesProcessed == 0 { - plainChunk := aw.buffer.Bytes() - lastEncryptedChunk, err := aw.sealChunk(plainChunk) - if err != nil { - return err - } - _, err = aw.writer.Write(lastEncryptedChunk) - if err != nil { - return err - } - } - // Compute final tag (associated data: packet tag, version, cipher, aead, - // chunk size, index, total number of encrypted octets). - adata := append(aw.associatedData[:], aw.chunkIndex[:]...) - adata = append(adata, make([]byte, 8)...) - binary.BigEndian.PutUint64(adata[13:], uint64(aw.bytesProcessed)) - nonce := aw.computeNextNonce() - finalTag := aw.aead.Seal(nil, nonce, nil, adata) - _, err = aw.writer.Write(finalTag) - if err != nil { - return err - } - return aw.writer.Close() -} - -// sealChunk Encrypts and authenticates the given chunk. -func (aw *aeadEncrypter) sealChunk(data []byte) ([]byte, error) { - if len(data) > aw.chunkSize { - return nil, errors.AEADError("chunk exceeds maximum length") - } - if aw.associatedData == nil { - return nil, errors.AEADError("can't seal without headers") - } - adata := append(aw.associatedData, aw.chunkIndex...) - nonce := aw.computeNextNonce() - encrypted := aw.aead.Seal(nil, nonce, data, adata) - aw.bytesProcessed += len(data) - if err := aw.aeadCrypter.incrementIndex(); err != nil { - return nil, err - } - return encrypted, nil -} - -// openChunk decrypts and checks integrity of an encrypted chunk, returning -// the underlying plaintext and an error. It access peeked bytes from next -// chunk, to identify the last chunk and decrypt/validate accordingly. -func (ar *aeadDecrypter) openChunk(data []byte) ([]byte, error) { - tagLen := ar.aead.Overhead() - // Restore carried bytes from last call - chunkExtra := append(ar.peekedBytes, data...) - // 'chunk' contains encrypted bytes, followed by an authentication tag. - chunk := chunkExtra[:len(chunkExtra)-tagLen] - ar.peekedBytes = chunkExtra[len(chunkExtra)-tagLen:] - adata := append(ar.associatedData, ar.chunkIndex...) - nonce := ar.computeNextNonce() - plainChunk, err := ar.aead.Open(nil, nonce, chunk, adata) - if err != nil { - return nil, err - } - ar.bytesProcessed += len(plainChunk) - if err = ar.aeadCrypter.incrementIndex(); err != nil { - return nil, err - } - return plainChunk, nil -} - -// Checks the summary tag. It takes into account the total decrypted bytes into -// the associated data. It returns an error, or nil if the tag is valid. -func (ar *aeadDecrypter) validateFinalTag(tag []byte) error { - // Associated: tag, version, cipher, aead, chunk size, index, and octets - amountBytes := make([]byte, 8) - binary.BigEndian.PutUint64(amountBytes, uint64(ar.bytesProcessed)) - adata := append(ar.associatedData, ar.chunkIndex...) - adata = append(adata, amountBytes...) - nonce := ar.computeNextNonce() - _, err := ar.aead.Open(nil, nonce, tag, adata) - if err != nil { - return err - } - return nil -} - -// Associated data for chunks: tag, version, cipher, mode, chunk size byte +// associatedData for chunks: tag, version, cipher, mode, chunk size byte func (ae *AEADEncrypted) associatedData() []byte { return []byte{ 0xD4, @@ -332,33 +94,3 @@ func (ae *AEADEncrypted) associatedData() []byte { byte(ae.mode), ae.chunkSizeByte} } - -// computeNonce takes the incremental index and computes an eXclusive OR with -// the least significant 8 bytes of the receivers' initial nonce (see sec. -// 5.16.1 and 5.16.2). It returns the resulting nonce. -func (wo *aeadCrypter) computeNextNonce() (nonce []byte) { - nonce = make([]byte, len(wo.initialNonce)) - copy(nonce, wo.initialNonce) - offset := len(wo.initialNonce) - 8 - for i := 0; i < 8; i++ { - nonce[i+offset] ^= wo.chunkIndex[i] - } - return -} - -// incrementIndex performs an integer increment by 1 of the integer represented by the -// slice, modifying it accordingly. -func (wo *aeadCrypter) incrementIndex() error { - index := wo.chunkIndex - if len(index) == 0 { - return errors.AEADError("Index has length 0") - } - for i := len(index) - 1; i >= 0; i-- { - if index[i] < 255 { - index[i]++ - return nil - } - index[i] = 0 - } - return errors.AEADError("cannot further increment index") -} diff --git a/openpgp/packet/aead_encrypted_test.go b/openpgp/packet/aead_encrypted_test.go index 93cc5526..f5827579 100644 --- a/openpgp/packet/aead_encrypted_test.go +++ b/openpgp/packet/aead_encrypted_test.go @@ -9,6 +9,8 @@ import ( "io" mathrand "math/rand" "testing" + + "github.com/ProtonMail/go-crypto/openpgp/errors" ) // Note: This implementation does not produce packets with chunk sizes over @@ -16,10 +18,7 @@ import ( // them within limits of the running system. See RFC4880bis, sec 5.16. var maxChunkSizeExp = 62 -const ( - keyLength = 16 - maxPlaintextLength = 1 << 18 -) +const maxPlaintextLength = 1 << 18 func TestAeadRFCParse(t *testing.T) { for _, sample := range samplesAeadEncryptedDataPacket { @@ -274,9 +273,7 @@ func TestAeadUnclosedStreamRandomizeSlow(t *testing.T) { } // 'writeCloser' encrypts and writes the plaintext bytes. rawCipher := bytes.NewBuffer(nil) - writeCloser, err := SerializeAEADEncrypted( - rawCipher, key, config.Cipher(), config.AEAD().Mode(), config, - ) + writeCloser, err := SerializeAEADEncrypted(rawCipher, key, config) if err != nil { t.Error(err) } @@ -330,7 +327,6 @@ func randomConfig() *Config { var modes = []AEADMode{ AEADModeEAX, AEADModeOCB, - AEADModeExperimentalGCM, } // Random chunk size @@ -361,9 +357,7 @@ func randomStream(key []byte, ptLen int, config *Config) (*bytes.Buffer, []byte, // 'writeCloser' encrypts and writes the plaintext bytes. rawCipher := bytes.NewBuffer(nil) - writeCloser, err := SerializeAEADEncrypted( - rawCipher, key, config.Cipher(), config.AEAD().Mode(), config, - ) + writeCloser, err := SerializeAEADEncrypted(rawCipher, key, config) if err != nil { return nil, nil, err } @@ -399,3 +393,54 @@ func readDecryptedStream(rc io.ReadCloser) (got []byte, err error) { } return got, err } + +// SerializeAEADEncrypted initializes the aeadCrypter and returns a writer. +// This writer encrypts and writes bytes (see aeadEncrypter.Write()). +// This funcion is moved to the test suite to prevent it from creating this deprecated package +func SerializeAEADEncrypted(w io.Writer, key []byte, config *Config) (io.WriteCloser, error) { + writeCloser := noOpCloser{w} + writer, err := serializeStreamHeader(writeCloser, packetTypeAEADEncrypted) + if err != nil { + return nil, err + } + + // Data for en/decryption: tag, version, cipher, aead mode, chunk size + aeadConf := config.AEAD() + prefix := []byte{ + 0xD4, + aeadEncryptedVersion, + byte(config.Cipher()), + byte(aeadConf.Mode()), + aeadConf.ChunkSizeByte(), + } + n, err := writer.Write(prefix[1:]) + if err != nil || n < 4 { + return nil, errors.AEADError("could not write AEAD headers") + } + // Sample nonce + nonceLen := aeadConf.Mode().IvLength() + nonce := make([]byte, nonceLen) + n, err = rand.Read(nonce) + if err != nil { + panic("Could not sample random nonce") + } + _, err = writer.Write(nonce) + if err != nil { + return nil, err + } + blockCipher := CipherFunction(config.Cipher()).new(key) + alg := aeadConf.Mode().new(blockCipher) + + chunkSize := decodeAEADChunkSize(aeadConf.ChunkSizeByte()) + return &aeadEncrypter{ + aeadCrypter: aeadCrypter{ + aead: alg, + chunkSize: chunkSize, + associatedData: prefix, + chunkIndex: make([]byte, 8), + initialNonce: nonce, + packetTag: packetTypeAEADEncrypted, + }, + writer: writer, + }, nil +} diff --git a/openpgp/packet/config.go b/openpgp/packet/config.go index f9208158..82ae5399 100644 --- a/openpgp/packet/config.go +++ b/openpgp/packet/config.go @@ -94,6 +94,12 @@ type Config struct { // might be no other way than to tolerate the missing MDC. Setting this flag, allows this // mode of operation. It should be considered a measure of last resort. InsecureAllowUnauthenticatedMessages bool + // KnownNotations is a map of Notation Data names to bools, which controls + // the notation names that are allowed to be present in critical Notation Data + // signature subpackets. + KnownNotations map[string]bool + // SignatureNotations is a list of Notations to be added to any signatures. + SignatureNotations []*Notation } func (c *Config) Random() io.Reader { @@ -202,3 +208,17 @@ func (c *Config) AllowUnauthenticatedMessages() bool { } return c.InsecureAllowUnauthenticatedMessages } + +func (c *Config) KnownNotation(notationName string) bool { + if c == nil { + return false + } + return c.KnownNotations[notationName] +} + +func (c *Config) Notations() []*Notation { + if c == nil { + return nil + } + return c.SignatureNotations +} diff --git a/openpgp/packet/encrypted_key.go b/openpgp/packet/encrypted_key.go index 801aec92..eeff2902 100644 --- a/openpgp/packet/encrypted_key.go +++ b/openpgp/packet/encrypted_key.go @@ -25,7 +25,7 @@ const encryptedKeyVersion = 3 type EncryptedKey struct { KeyId uint64 Algo PublicKeyAlgorithm - CipherFunc CipherFunction // only valid after a successful Decrypt + CipherFunc CipherFunction // only valid after a successful Decrypt for a v3 packet Key []byte // only valid after a successful Decrypt encryptedMPI1, encryptedMPI2 encoding.Field @@ -123,6 +123,10 @@ func (e *EncryptedKey) Decrypt(priv *PrivateKey, config *Config) error { } e.CipherFunc = CipherFunction(b[0]) + if !e.CipherFunc.IsSupported() { + return errors.UnsupportedError("unsupported encryption function") + } + e.Key = b[1 : len(b)-2] expectedChecksum := uint16(b[len(b)-2])<<8 | uint16(b[len(b)-1]) checksum := checksumKeyMaterial(e.Key) diff --git a/openpgp/packet/notation.go b/openpgp/packet/notation.go new file mode 100644 index 00000000..2c3e3f50 --- /dev/null +++ b/openpgp/packet/notation.go @@ -0,0 +1,29 @@ +package packet + +// Notation type represents a Notation Data subpacket +// see https://tools.ietf.org/html/rfc4880#section-5.2.3.16 +type Notation struct { + Name string + Value []byte + IsCritical bool + IsHumanReadable bool +} + +func (notation *Notation) getData() []byte { + nameData := []byte(notation.Name) + nameLen := len(nameData) + valueLen := len(notation.Value) + + data := make([]byte, 8+nameLen+valueLen) + if notation.IsHumanReadable { + data[0] = 0x80 + } + + data[4] = byte(nameLen >> 8) + data[5] = byte(nameLen) + data[6] = byte(valueLen >> 8) + data[7] = byte(valueLen) + copy(data[8:8+nameLen], nameData) + copy(data[8+nameLen:], notation.Value) + return data +} diff --git a/openpgp/packet/notation_test.go b/openpgp/packet/notation_test.go new file mode 100644 index 00000000..9b12daf9 --- /dev/null +++ b/openpgp/packet/notation_test.go @@ -0,0 +1,38 @@ +package packet + +import ( + "bytes" + "testing" +) + +func TestNotationGetData(t *testing.T) { + notation := Notation{ + Name: "test@proton.me", + Value: []byte("test-value"), + IsCritical: true, + IsHumanReadable: true, + } + expected := []byte{0x80, 0, 0, 0, 0, 14, 0, 10} + expected = append(expected, []byte(notation.Name)...) + expected = append(expected, []byte(notation.Value)...) + data := notation.getData() + if !bytes.Equal(expected, data) { + t.Fatalf("Expected %s, got %s", expected, data) + } +} + +func TestNotationGetDataNotHumanReadable(t *testing.T) { + notation := Notation{ + Name: "test@proton.me", + Value: []byte("test-value"), + IsCritical: true, + IsHumanReadable: false, + } + expected := []byte{0, 0, 0, 0, 0, 14, 0, 10} + expected = append(expected, []byte(notation.Name)...) + expected = append(expected, []byte(notation.Value)...) + data := notation.getData() + if !bytes.Equal(expected, data) { + t.Fatalf("Expected %s, got %s", expected, data) + } +} diff --git a/openpgp/packet/one_pass_signature.go b/openpgp/packet/one_pass_signature.go index 41c35de2..fff119e6 100644 --- a/openpgp/packet/one_pass_signature.go +++ b/openpgp/packet/one_pass_signature.go @@ -8,7 +8,7 @@ import ( "crypto" "encoding/binary" "github.com/ProtonMail/go-crypto/openpgp/errors" - "github.com/ProtonMail/go-crypto/openpgp/s2k" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "io" "strconv" ) @@ -37,7 +37,7 @@ func (ops *OnePassSignature) parse(r io.Reader) (err error) { } var ok bool - ops.Hash, ok = s2k.HashIdToHash(buf[2]) + ops.Hash, ok = algorithm.HashIdToHashWithSha1(buf[2]) if !ok { return errors.UnsupportedError("hash function: " + strconv.Itoa(int(buf[2]))) } @@ -55,7 +55,7 @@ func (ops *OnePassSignature) Serialize(w io.Writer) error { buf[0] = onePassSignatureVersion buf[1] = uint8(ops.SigType) var ok bool - buf[2], ok = s2k.HashToHashId(ops.Hash) + buf[2], ok = algorithm.HashToHashId(ops.Hash) if !ok { return errors.UnsupportedError("hash type: " + strconv.Itoa(int(ops.Hash))) } diff --git a/openpgp/packet/packet.go b/openpgp/packet/packet.go index c570bfca..f73f6f40 100644 --- a/openpgp/packet/packet.go +++ b/openpgp/packet/packet.go @@ -302,21 +302,21 @@ func consumeAll(r io.Reader) (n int64, err error) { type packetType uint8 const ( - packetTypeEncryptedKey packetType = 1 - packetTypeSignature packetType = 2 - packetTypeSymmetricKeyEncrypted packetType = 3 - packetTypeOnePassSignature packetType = 4 - packetTypePrivateKey packetType = 5 - packetTypePublicKey packetType = 6 - packetTypePrivateSubkey packetType = 7 - packetTypeCompressed packetType = 8 - packetTypeSymmetricallyEncrypted packetType = 9 - packetTypeLiteralData packetType = 11 - packetTypeUserId packetType = 13 - packetTypePublicSubkey packetType = 14 - packetTypeUserAttribute packetType = 17 - packetTypeSymmetricallyEncryptedMDC packetType = 18 - packetTypeAEADEncrypted packetType = 20 + packetTypeEncryptedKey packetType = 1 + packetTypeSignature packetType = 2 + packetTypeSymmetricKeyEncrypted packetType = 3 + packetTypeOnePassSignature packetType = 4 + packetTypePrivateKey packetType = 5 + packetTypePublicKey packetType = 6 + packetTypePrivateSubkey packetType = 7 + packetTypeCompressed packetType = 8 + packetTypeSymmetricallyEncrypted packetType = 9 + packetTypeLiteralData packetType = 11 + packetTypeUserId packetType = 13 + packetTypePublicSubkey packetType = 14 + packetTypeUserAttribute packetType = 17 + packetTypeSymmetricallyEncryptedIntegrityProtected packetType = 18 + packetTypeAEADEncrypted packetType = 20 ) // EncryptedDataPacket holds encrypted data. It is currently implemented by @@ -361,9 +361,9 @@ func Read(r io.Reader) (p Packet, err error) { p = new(UserId) case packetTypeUserAttribute: p = new(UserAttribute) - case packetTypeSymmetricallyEncryptedMDC: + case packetTypeSymmetricallyEncryptedIntegrityProtected: se := new(SymmetricallyEncrypted) - se.MDC = true + se.IntegrityProtected = true p = se case packetTypeAEADEncrypted: p = new(AEADEncrypted) @@ -455,6 +455,11 @@ func (cipher CipherFunction) KeySize() int { return algorithm.CipherFunction(cipher).KeySize() } +// IsSupported returns true if the cipher is supported from the library +func (cipher CipherFunction) IsSupported() bool { + return algorithm.CipherFunction(cipher).KeySize() > 0 +} + // blockSize returns the block size, in bytes, of cipher. func (cipher CipherFunction) blockSize() int { return algorithm.CipherFunction(cipher).BlockSize() @@ -490,15 +495,16 @@ const ( // AEADMode represents the different Authenticated Encryption with Associated // Data specified for OpenPGP. +// See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 type AEADMode algorithm.AEADMode const ( - AEADModeEAX AEADMode = 1 - AEADModeOCB AEADMode = 2 - AEADModeExperimentalGCM AEADMode = 100 + AEADModeEAX AEADMode = 1 + AEADModeOCB AEADMode = 2 + AEADModeGCM AEADMode = 3 ) -func (mode AEADMode) NonceLength() int { +func (mode AEADMode) IvLength() int { return algorithm.AEADMode(mode).NonceLength() } @@ -537,3 +543,9 @@ const ( CurveBrainpoolP384 Curve = "BrainpoolP384" CurveBrainpoolP512 Curve = "BrainpoolP512" ) + +// TrustLevel represents a trust level per RFC4880 5.2.3.13 +type TrustLevel uint8 + +// TrustAmount represents a trust amount per RFC4880 5.2.3.13 +type TrustAmount uint8 diff --git a/openpgp/packet/private_key.go b/openpgp/packet/private_key.go index 009f0ef1..2898fa74 100644 --- a/openpgp/packet/private_key.go +++ b/openpgp/packet/private_key.go @@ -179,6 +179,9 @@ func (pk *PrivateKey) parse(r io.Reader) (err error) { return } pk.cipher = CipherFunction(buf[0]) + if pk.cipher != 0 && !pk.cipher.IsSupported() { + return errors.UnsupportedError("unsupported cipher function in private key") + } pk.s2kParams, err = s2k.ParseIntoParams(r) if err != nil { return diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index 6d2a61ce..9f0b1b19 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -17,8 +17,8 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/ecdsa" "github.com/ProtonMail/go-crypto/openpgp/eddsa" "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/internal/encoding" - "github.com/ProtonMail/go-crypto/openpgp/s2k" ) const ( @@ -66,11 +66,24 @@ type Signature struct { SigLifetimeSecs, KeyLifetimeSecs *uint32 PreferredSymmetric, PreferredHash, PreferredCompression []uint8 - PreferredAEAD []uint8 + PreferredCipherSuites [][2]uint8 IssuerKeyId *uint64 IssuerFingerprint []byte SignerUserId *string IsPrimaryId *bool + Notations []*Notation + + // TrustLevel and TrustAmount can be set by the signer to assert that + // the key is not only valid but also trustworthy at the specified + // level. + // See RFC 4880, section 5.2.3.13 for details. + TrustLevel TrustLevel + TrustAmount TrustAmount + + // TrustRegularExpression can be used in conjunction with trust Signature + // packets to limit the scope of the trust that is extended. + // See RFC 4880, section 5.2.3.14 for details. + TrustRegularExpression *string // PolicyURI can be set to the URI of a document that describes the // policy under which the signature was issued. See RFC 4880, section @@ -89,8 +102,8 @@ type Signature struct { // In a self-signature, these flags are set there is a features subpacket // indicating that the issuer implementation supports these features - // (section 5.2.5.25). - MDC, AEAD, V5Keys bool + // see https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-crypto-refresh#features-subpacket + SEIPDv1, SEIPDv2 bool // EmbeddedSignature, if non-nil, is a signature of the parent key, by // this key. This prevents an attacker from claiming another's signing @@ -126,7 +139,13 @@ func (sig *Signature) parse(r io.Reader) (err error) { } var ok bool - sig.Hash, ok = s2k.HashIdToHash(buf[2]) + + if sig.Version < 5 { + sig.Hash, ok = algorithm.HashIdToHashWithSha1(buf[2]) + } else { + sig.Hash, ok = algorithm.HashIdToHash(buf[2]) + } + if !ok { return errors.UnsupportedError("hash function " + strconv.Itoa(int(buf[2]))) } @@ -137,7 +156,11 @@ func (sig *Signature) parse(r io.Reader) (err error) { if err != nil { return } - sig.buildHashSuffix(hashedSubpackets) + err = sig.buildHashSuffix(hashedSubpackets) + if err != nil { + return + } + err = parseSignatureSubpackets(sig, hashedSubpackets, true) if err != nil { return @@ -221,9 +244,12 @@ type signatureSubpacketType uint8 const ( creationTimeSubpacket signatureSubpacketType = 2 signatureExpirationSubpacket signatureSubpacketType = 3 + trustSubpacket signatureSubpacketType = 5 + regularExpressionSubpacket signatureSubpacketType = 6 keyExpirationSubpacket signatureSubpacketType = 9 prefSymmetricAlgosSubpacket signatureSubpacketType = 11 issuerSubpacket signatureSubpacketType = 16 + notationDataSubpacket signatureSubpacketType = 20 prefHashAlgosSubpacket signatureSubpacketType = 21 prefCompressionSubpacket signatureSubpacketType = 22 primaryUserIdSubpacket signatureSubpacketType = 25 @@ -234,7 +260,7 @@ const ( featuresSubpacket signatureSubpacketType = 30 embeddedSignatureSubpacket signatureSubpacketType = 32 issuerFingerprintSubpacket signatureSubpacketType = 33 - prefAeadAlgosSubpacket signatureSubpacketType = 34 + prefCipherSuitesSubpacket signatureSubpacketType = 39 ) // parseSignatureSubpacket parses a single subpacket. len(subpacket) is >= 1. @@ -278,12 +304,14 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r isCritical = subpacket[0]&0x80 == 0x80 subpacket = subpacket[1:] sig.rawSubpackets = append(sig.rawSubpackets, outputSubpacket{isHashed, packetType, isCritical, subpacket}) + if !isHashed && + packetType != issuerSubpacket && + packetType != issuerFingerprintSubpacket && + packetType != embeddedSignatureSubpacket { + return + } switch packetType { case creationTimeSubpacket: - if !isHashed { - err = errors.StructuralError("signature creation time in non-hashed area") - return - } if len(subpacket) != 4 { err = errors.StructuralError("signature creation time not four bytes") return @@ -292,20 +320,27 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r sig.CreationTime = time.Unix(int64(t), 0) case signatureExpirationSubpacket: // Signature expiration time, section 5.2.3.10 - if !isHashed { - return - } if len(subpacket) != 4 { err = errors.StructuralError("expiration subpacket with bad length") return } sig.SigLifetimeSecs = new(uint32) *sig.SigLifetimeSecs = binary.BigEndian.Uint32(subpacket) - case keyExpirationSubpacket: - // Key expiration time, section 5.2.3.6 - if !isHashed { + case trustSubpacket: + // Trust level and amount, section 5.2.3.13 + sig.TrustLevel = TrustLevel(subpacket[0]) + sig.TrustAmount = TrustAmount(subpacket[1]) + case regularExpressionSubpacket: + // Trust regular expression, section 5.2.3.14 + // RFC specifies the string should be null-terminated; remove a null byte from the end + if subpacket[len(subpacket)-1] != 0x00 { + err = errors.StructuralError("expected regular expression to be null-terminated") return } + trustRegularExpression := string(subpacket[:len(subpacket)-1]) + sig.TrustRegularExpression = &trustRegularExpression + case keyExpirationSubpacket: + // Key expiration time, section 5.2.3.6 if len(subpacket) != 4 { err = errors.StructuralError("key expiration subpacket with bad length") return @@ -314,41 +349,52 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r *sig.KeyLifetimeSecs = binary.BigEndian.Uint32(subpacket) case prefSymmetricAlgosSubpacket: // Preferred symmetric algorithms, section 5.2.3.7 - if !isHashed { - return - } sig.PreferredSymmetric = make([]byte, len(subpacket)) copy(sig.PreferredSymmetric, subpacket) case issuerSubpacket: + // Issuer, section 5.2.3.5 if sig.Version > 4 { err = errors.StructuralError("issuer subpacket found in v5 key") + return } - // Issuer, section 5.2.3.5 if len(subpacket) != 8 { err = errors.StructuralError("issuer subpacket with bad length") return } sig.IssuerKeyId = new(uint64) *sig.IssuerKeyId = binary.BigEndian.Uint64(subpacket) - case prefHashAlgosSubpacket: - // Preferred hash algorithms, section 5.2.3.8 - if !isHashed { + case notationDataSubpacket: + // Notation data, section 5.2.3.16 + if len(subpacket) < 8 { + err = errors.StructuralError("notation data subpacket with bad length") + return + } + + nameLength := uint32(subpacket[4])<<8 | uint32(subpacket[5]) + valueLength := uint32(subpacket[6])<<8 | uint32(subpacket[7]) + if len(subpacket) != int(nameLength) + int(valueLength) + 8 { + err = errors.StructuralError("notation data subpacket with bad length") return } + + notation := Notation{ + IsHumanReadable: (subpacket[0] & 0x80) == 0x80, + Name: string(subpacket[8: (nameLength + 8)]), + Value: subpacket[(nameLength + 8) : (valueLength + nameLength + 8)], + IsCritical: isCritical, + } + + sig.Notations = append(sig.Notations, ¬ation) + case prefHashAlgosSubpacket: + // Preferred hash algorithms, section 5.2.3.8 sig.PreferredHash = make([]byte, len(subpacket)) copy(sig.PreferredHash, subpacket) case prefCompressionSubpacket: // Preferred compression algorithms, section 5.2.3.9 - if !isHashed { - return - } sig.PreferredCompression = make([]byte, len(subpacket)) copy(sig.PreferredCompression, subpacket) case primaryUserIdSubpacket: // Primary User ID, section 5.2.3.19 - if !isHashed { - return - } if len(subpacket) != 1 { err = errors.StructuralError("primary user id subpacket with bad length") return @@ -359,9 +405,6 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r } case keyFlagsSubpacket: // Key flags, section 5.2.3.21 - if !isHashed { - return - } if len(subpacket) == 0 { err = errors.StructuralError("empty key flags subpacket") return @@ -393,9 +436,6 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r sig.SignerUserId = &userId case reasonForRevocationSubpacket: // Reason For Revocation, section 5.2.3.23 - if !isHashed { - return - } if len(subpacket) == 0 { err = errors.StructuralError("empty revocation reason subpacket") return @@ -407,18 +447,13 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r // Features subpacket, section 5.2.3.24 specifies a very general // mechanism for OpenPGP implementations to signal support for new // features. - if !isHashed { - return - } if len(subpacket) > 0 { if subpacket[0]&0x01 != 0 { - sig.MDC = true - } - if subpacket[0]&0x02 != 0 { - sig.AEAD = true + sig.SEIPDv1 = true } - if subpacket[0]&0x04 != 0 { - sig.V5Keys = true + // 0x02 and 0x04 are reserved + if subpacket[0]&0x08 != 0 { + sig.SEIPDv2 = true } } case embeddedSignatureSubpacket: @@ -441,9 +476,6 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r } case policyUriSubpacket: // Policy URI, section 5.2.3.20 - if !isHashed { - return - } sig.PolicyURI = string(subpacket) case issuerFingerprintSubpacket: v, l := subpacket[0], len(subpacket[1:]) @@ -458,13 +490,19 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r } else { *sig.IssuerKeyId = binary.BigEndian.Uint64(subpacket[13:21]) } - case prefAeadAlgosSubpacket: - // Preferred symmetric algorithms, section 5.2.3.8 - if !isHashed { + case prefCipherSuitesSubpacket: + // Preferred AEAD cipher suites + // See https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#name-preferred-aead-ciphersuites + if len(subpacket) % 2 != 0 { + err = errors.StructuralError("invalid aead cipher suite length") return } - sig.PreferredAEAD = make([]byte, len(subpacket)) - copy(sig.PreferredAEAD, subpacket) + + sig.PreferredCipherSuites = make([][2]byte, len(subpacket) / 2) + + for i := 0; i < len(subpacket) / 2; i++ { + sig.PreferredCipherSuites[i] = [2]uint8{subpacket[2*i], subpacket[2*i+1]} + } default: if isCritical { err = errors.UnsupportedError("unknown critical signature subpacket type " + strconv.Itoa(int(packetType))) @@ -562,7 +600,15 @@ func (sig *Signature) SigExpired(currentTime time.Time) bool { // buildHashSuffix constructs the HashSuffix member of sig in preparation for signing. func (sig *Signature) buildHashSuffix(hashedSubpackets []byte) (err error) { - hash, ok := s2k.HashToHashId(sig.Hash) + var hashId byte + var ok bool + + if sig.Version < 5 { + hashId, ok = algorithm.HashToHashIdWithSha1(sig.Hash) + } else { + hashId, ok = algorithm.HashToHashId(sig.Hash) + } + if !ok { sig.HashSuffix = nil return errors.InvalidArgumentError("hash cannot be represented in OpenPGP: " + strconv.Itoa(int(sig.Hash))) @@ -572,7 +618,7 @@ func (sig *Signature) buildHashSuffix(hashedSubpackets []byte) (err error) { uint8(sig.Version), uint8(sig.SigType), uint8(sig.PubKeyAlgo), - uint8(hash), + uint8(hashId), uint8(len(hashedSubpackets) >> 8), uint8(len(hashedSubpackets)), }) @@ -885,23 +931,40 @@ func (sig *Signature) buildSubpackets(issuer PublicKey) (subpackets []outputSubp subpackets = append(subpackets, outputSubpacket{true, keyFlagsSubpacket, false, []byte{flags}}) } + for _, notation := range sig.Notations { + subpackets = append( + subpackets, + outputSubpacket{ + true, + notationDataSubpacket, + notation.IsCritical, + notation.getData(), + }) + } + // The following subpackets may only appear in self-signatures. var features = byte(0x00) - if sig.MDC { + if sig.SEIPDv1 { features |= 0x01 } - if sig.AEAD { - features |= 0x02 - } - if sig.V5Keys { - features |= 0x04 + if sig.SEIPDv2 { + features |= 0x08 } if features != 0x00 { subpackets = append(subpackets, outputSubpacket{true, featuresSubpacket, false, []byte{features}}) } + if sig.TrustLevel != 0 { + subpackets = append(subpackets, outputSubpacket{true, trustSubpacket, true, []byte{byte(sig.TrustLevel), byte(sig.TrustAmount)}}) + } + + if sig.TrustRegularExpression != nil { + // RFC specifies the string should be null-terminated; add a null byte to the end + subpackets = append(subpackets, outputSubpacket{true, regularExpressionSubpacket, true, []byte(*sig.TrustRegularExpression + "\000")}) + } + if sig.KeyLifetimeSecs != nil && *sig.KeyLifetimeSecs != 0 { keyLifetime := make([]byte, 4) binary.BigEndian.PutUint32(keyLifetime, *sig.KeyLifetimeSecs) @@ -928,8 +991,13 @@ func (sig *Signature) buildSubpackets(issuer PublicKey) (subpackets []outputSubp subpackets = append(subpackets, outputSubpacket{true, policyUriSubpacket, false, []uint8(sig.PolicyURI)}) } - if len(sig.PreferredAEAD) > 0 { - subpackets = append(subpackets, outputSubpacket{true, prefAeadAlgosSubpacket, false, sig.PreferredAEAD}) + if len(sig.PreferredCipherSuites) > 0 { + serialized := make([]byte, len(sig.PreferredCipherSuites)*2) + for i, cipherSuite := range sig.PreferredCipherSuites { + serialized[2*i] = cipherSuite[0] + serialized[2*i+1] = cipherSuite[1] + } + subpackets = append(subpackets, outputSubpacket{true, prefCipherSuitesSubpacket, false, serialized}) } // Revocation reason appears only in revocation signatures and is serialized as per section 5.2.3.23. diff --git a/openpgp/packet/signature_test.go b/openpgp/packet/signature_test.go index 56fb8e64..c4569a56 100644 --- a/openpgp/packet/signature_test.go +++ b/openpgp/packet/signature_test.go @@ -208,6 +208,69 @@ func TestSignatureWithPolicyURI(t *testing.T) { } } +func TestSignatureWithTrust(t *testing.T) { + packet, err := Read(readerFromHex(signatureWithTrustDataHex)) + if err != nil { + t.Error(err) + return + } + sig, ok := packet.(*Signature) + if !ok || sig.SigType != SigTypeGenericCert || sig.PubKeyAlgo != PubKeyAlgoRSA || sig.Hash != crypto.SHA256 || sig.TrustLevel != 0x01 || sig.TrustAmount != 0x03C { + t.Errorf("failed to parse, got: %#v", packet) + } + + out := new(bytes.Buffer) + err = sig.Serialize(out) + if err != nil { + t.Errorf("error reserializing: %s", err) + return + } + + expected, _ := hex.DecodeString(signatureWithTrustDataHex) + if !bytes.Equal(expected, out.Bytes()) { + t.Errorf("output doesn't match input (got vs expected):\n%s\n%s", hex.Dump(out.Bytes()), hex.Dump(expected)) + } +} + +func TestSignatureWithTrustAndRegex(t *testing.T) { + packet, err := Read(readerFromHex(signatureWithTrustRegexHex)) + if err != nil { + t.Error(err) + return + } + sig, ok := packet.(*Signature) + if !ok || sig.SigType != SigTypeGenericCert || sig.PubKeyAlgo != PubKeyAlgoRSA || sig.Hash != crypto.SHA256 || sig.TrustLevel != 0x01 || sig.TrustAmount != 0x3C || *sig.TrustRegularExpression != "*.example.com" { + t.Errorf("failed to parse, got: %#v", packet) + } + + out := new(bytes.Buffer) + err = sig.Serialize(out) + if err != nil { + t.Errorf("error reserializing: %s", err) + return + } + + expected, _ := hex.DecodeString(signatureWithTrustRegexHex) + if !bytes.Equal(expected, out.Bytes()) { + t.Errorf("output doesn't match input (got vs expected):\n%s\n%s", hex.Dump(out.Bytes()), hex.Dump(expected)) + } + + // ensure we fail if the regular expression is not null-terminated + packet, err = Read(readerFromHex(signatureWithBadTrustRegexHex)) + if err == nil { + t.Errorf("did not receive an error when expected") + } + if err.Error() != "openpgp: invalid data: expected regular expression to be null-terminated" { + t.Errorf("unexpected error while parsing: %v", err) + } +} + const signatureDataHex = "c2c05c04000102000605024cb45112000a0910ab105c91af38fb158f8d07ff5596ea368c5efe015bed6e78348c0f033c931d5f2ce5db54ce7f2a7e4b4ad64db758d65a7a71773edeab7ba2a9e0908e6a94a1175edd86c1d843279f045b021a6971a72702fcbd650efc393c5474d5b59a15f96d2eaad4c4c426797e0dcca2803ef41c6ff234d403eec38f31d610c344c06f2401c262f0993b2e66cad8a81ebc4322c723e0d4ba09fe917e8777658307ad8329adacba821420741009dfe87f007759f0982275d028a392c6ed983a0d846f890b36148c7358bdb8a516007fac760261ecd06076813831a36d0459075d1befa245ae7f7fb103d92ca759e9498fe60ef8078a39a3beda510deea251ea9f0a7f0df6ef42060f20780360686f3e400e" +const signatureWithTrustDataHex = "c2ad0410010800210502886e09001621040f0bfb42b3b08bece556fffcc181c053de849bf20385013c000035d803ff405c3c10211d680d3f5192e44d5acf7a25068a9938b5e5b1337735658ef8916e6878735ddfe15679c4868fcf46f02890104a5fb7caffa8e628a202deeda8376d58e586d60c1759e667fa49d87c7564c83b88f59db2631dc7e68535fd4a13b6096f91b05f7bb9989ddb36fc7e6e35dcc2f493468320cbe66e27895744eab2ae4b" + +const signatureWithTrustRegexHex = "c2bd0410010800310502886e09001621040f0bfb42b3b08bece556fffcc181c053de849bf20385013c0f862a2e6578616d706c652e636f6d000000620603ff7e405020cdbf82ac30f6ad11f82690d3c2fa2107130f80a66fc48a4b6cc426b90585670d8cb8e258f9c1fa35c62381074fd9b740aaebd96a3265c96d145620d7c24265c8e258a2f9a2229e4edb8076e27d5e229cf676135dde4dad54271e061adea05302e81ff412c55742b15c8b20fe3bee4c6b96cd9dfff44da9cc5df328ab" + +const signatureWithBadTrustRegexHex = "c2bc0410010800300502886e09001621040f0bfb42b3b08bece556fffcc181c053de849bf20385013c0e862a2e6578616d706c652e636f6d00007e7103fe3fa66963f7a91ceb297286f57bab38446ba591215a9d6589ab6ec0d930438a4d79f80a52440e017dc6dd03f7425ccc1e059edda2b32f4975501eacc5676f216e56c568b75442c3efc750425f0d5276c7611ef838ce3f015f4de0969b4710aac8a76fcf2d48dd0749e937099b55ab77d93132e9777ba3b8cf89f908c2dbfff838" + const positiveCertSignatureDataHex = "c2c0b304130108005d050b0908070206150a09080b020416020301021e010217802418686b70733a2f2f686b70732e706f6f6c2e736b732d6b6579736572766572732e6e65741621045ef9b8a44d89b32f94f3e9333679666422d0f62605025b2cc122021b2f000a09103679666422d0f62668e1080098b71f59ce893769ccb603344290e89df8f12d6ea906cc1c2b166c61a02679070744565f8280712b4e6bdfd482b758ef935655f1674c8f3633ab173d27cbe31e46368a8255134ecc5249ad66324cc4f6a79f160459b326711cfdc35032aac0903657a934f80f79768786ddd6554aa8d385c03adbee17c4e3e2831752d4910077da3b1f5562d267a57540a1c2b0dd2d96ed055c06098599b2390d61cfa37c6d19d9d63749fb3c3cfe0036fd959ba616eb23486216563fed8fdd19f96f5da9943db1698705fb688c1354c379ef01de307c4a0ac016e6385324cb0a7b49cfeee8961a289c8fa4c81d0e24e00969039db223a9835e8b86a8d85df645175f8aa0f8f2" diff --git a/openpgp/packet/symmetric_key_encrypted.go b/openpgp/packet/symmetric_key_encrypted.go index d5b6a87f..bc2caf0e 100644 --- a/openpgp/packet/symmetric_key_encrypted.go +++ b/openpgp/packet/symmetric_key_encrypted.go @@ -7,15 +7,17 @@ package packet import ( "bytes" "crypto/cipher" + "crypto/sha256" "io" "strconv" "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/s2k" + "golang.org/x/crypto/hkdf" ) -// This is the largest session key that we'll support. Since no 512-bit cipher -// has even been seriously used, this is comfortably large. +// This is the largest session key that we'll support. Since at most 256-bit cipher +// is supported in OpenPGP, this is large enough to contain also the auth tag. const maxSessionKeySizeInBytes = 64 // SymmetricKeyEncrypted represents a passphrase protected session key. See RFC @@ -25,13 +27,16 @@ type SymmetricKeyEncrypted struct { CipherFunc CipherFunction Mode AEADMode s2k func(out, in []byte) - aeadNonce []byte - encryptedKey []byte + iv []byte + encryptedKey []byte // Contains also the authentication tag for AEAD } +// parse parses an SymmetricKeyEncrypted packet as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#name-symmetric-key-encrypted-ses func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error { - // RFC 4880, section 5.3. - var buf [2]byte + var buf [1]byte + + // Version if _, err := readFull(r, buf[:]); err != nil { return err } @@ -39,17 +44,34 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error { if ske.Version != 4 && ske.Version != 5 { return errors.UnsupportedError("unknown SymmetricKeyEncrypted version") } - ske.CipherFunc = CipherFunction(buf[1]) - if ske.CipherFunc.KeySize() == 0 { - return errors.UnsupportedError("unknown cipher: " + strconv.Itoa(int(buf[1]))) + + if ske.Version == 5 { + // Scalar octet count + if _, err := readFull(r, buf[:]); err != nil { + return err + } + } + + // Cipher function + if _, err := readFull(r, buf[:]); err != nil { + return err + } + ske.CipherFunc = CipherFunction(buf[0]) + if !ske.CipherFunc.IsSupported() { + return errors.UnsupportedError("unknown cipher: " + strconv.Itoa(int(buf[0]))) } if ske.Version == 5 { - mode := make([]byte, 1) - if _, err := r.Read(mode); err != nil { + // AEAD mode + if _, err := readFull(r, buf[:]); err != nil { return errors.StructuralError("cannot read AEAD octet from packet") } - ske.Mode = AEADMode(mode[0]) + ske.Mode = AEADMode(buf[0]) + + // Scalar octet count + if _, err := readFull(r, buf[:]); err != nil { + return err + } } var err error @@ -61,13 +83,14 @@ func (ske *SymmetricKeyEncrypted) parse(r io.Reader) error { } if ske.Version == 5 { - // AEAD nonce - nonce := make([]byte, ske.Mode.NonceLength()) - _, err := readFull(r, nonce) - if err != nil && err != io.ErrUnexpectedEOF { - return err + // AEAD IV + iv := make([]byte, ske.Mode.IvLength()) + _, err := readFull(r, iv) + if err != nil { + return errors.StructuralError("cannot read AEAD IV") } - ske.aeadNonce = nonce + + ske.iv = iv } encryptedKey := make([]byte, maxSessionKeySizeInBytes) @@ -128,11 +151,10 @@ func (ske *SymmetricKeyEncrypted) decryptV4(key []byte) ([]byte, CipherFunction, } func (ske *SymmetricKeyEncrypted) decryptV5(key []byte) ([]byte, error) { - blockCipher := CipherFunction(ske.CipherFunc).new(key) - aead := ske.Mode.new(blockCipher) - adata := []byte{0xc3, byte(5), byte(ske.CipherFunc), byte(ske.Mode)} - plaintextKey, err := aead.Open(nil, ske.aeadNonce, ske.encryptedKey, adata) + aead := getEncryptedKeyAeadInstance(ske.CipherFunc, ske.Mode, key, adata) + + plaintextKey, err := aead.Open(nil, ske.iv, ske.encryptedKey, adata) if err != nil { return nil, err } @@ -142,17 +164,12 @@ func (ske *SymmetricKeyEncrypted) decryptV5(key []byte) ([]byte, error) { // SerializeSymmetricKeyEncrypted serializes a symmetric key packet to w. // The packet contains a random session key, encrypted by a key derived from // the given passphrase. The session key is returned and must be passed to -// SerializeSymmetricallyEncrypted or SerializeAEADEncrypted, depending on -// whether config.AEADConfig != nil. +// SerializeSymmetricallyEncrypted. // If config is nil, sensible defaults will be used. func SerializeSymmetricKeyEncrypted(w io.Writer, passphrase []byte, config *Config) (key []byte, err error) { cipherFunc := config.Cipher() - keySize := cipherFunc.KeySize() - if keySize == 0 { - return nil, errors.UnsupportedError("unknown cipher: " + strconv.Itoa(int(cipherFunc))) - } - sessionKey := make([]byte, keySize) + sessionKey := make([]byte, cipherFunc.KeySize()) _, err = io.ReadFull(config.Random(), sessionKey) if err != nil { return @@ -169,9 +186,8 @@ func SerializeSymmetricKeyEncrypted(w io.Writer, passphrase []byte, config *Conf // SerializeSymmetricKeyEncryptedReuseKey serializes a symmetric key packet to w. // The packet contains the given session key, encrypted by a key derived from -// the given passphrase. The session key must be passed to -// SerializeSymmetricallyEncrypted or SerializeAEADEncrypted, depending on -// whether config.AEADConfig != nil. +// the given passphrase. The returned session key must be passed to +// SerializeSymmetricallyEncrypted. // If config is nil, sensible defaults will be used. func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, passphrase []byte, config *Config) (err error) { var version int @@ -181,11 +197,12 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass version = 4 } cipherFunc := config.Cipher() - keySize := cipherFunc.KeySize() - if keySize == 0 { - return errors.UnsupportedError("unknown cipher: " + strconv.Itoa(int(cipherFunc))) + // cipherFunc must be AES + if !cipherFunc.IsSupported() || cipherFunc < CipherAES128 || cipherFunc > CipherAES256 { + return errors.UnsupportedError("unsupported cipher: " + strconv.Itoa(int(cipherFunc))) } + keySize := cipherFunc.KeySize() s2kBuf := new(bytes.Buffer) keyEncryptingKey := make([]byte, keySize) // s2k.Serialize salts and stretches the passphrase, and writes the @@ -201,24 +218,32 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass case 4: packetLength = 2 /* header */ + len(s2kBytes) + 1 /* cipher type */ + keySize case 5: - nonceLen := config.AEAD().Mode().NonceLength() + ivLen := config.AEAD().Mode().IvLength() tagLen := config.AEAD().Mode().TagLength() - packetLength = 3 + len(s2kBytes) + nonceLen + keySize + tagLen + packetLength = 5 + len(s2kBytes) + ivLen + keySize + tagLen } err = serializeHeader(w, packetTypeSymmetricKeyEncrypted, packetLength) if err != nil { return } - buf := make([]byte, 2) // Symmetric Key Encrypted Version - buf[0] = byte(version) + buf := []byte{byte(version)} + + if version == 5 { + // Scalar octet count + buf = append(buf, byte(3 + len(s2kBytes) + config.AEAD().Mode().IvLength())) + } + // Cipher function - buf[1] = byte(cipherFunc) + buf = append(buf, byte(cipherFunc)) if version == 5 { // AEAD mode buf = append(buf, byte(config.AEAD().Mode())) + + // Scalar octet count + buf = append(buf, byte(len(s2kBytes))) } _, err = w.Write(buf) if err != nil { @@ -241,19 +266,20 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass return } case 5: - blockCipher := cipherFunc.new(keyEncryptingKey) mode := config.AEAD().Mode() - aead := mode.new(blockCipher) - // Sample nonce using random reader - nonce := make([]byte, config.AEAD().Mode().NonceLength()) - _, err = io.ReadFull(config.Random(), nonce) + adata := []byte{0xc3, byte(5), byte(cipherFunc), byte(mode)} + aead := getEncryptedKeyAeadInstance(cipherFunc, mode, keyEncryptingKey, adata) + + // Sample iv using random reader + iv := make([]byte, config.AEAD().Mode().IvLength()) + _, err = io.ReadFull(config.Random(), iv) if err != nil { return } // Seal and write (encryptedData includes auth. tag) - adata := []byte{0xc3, byte(5), byte(cipherFunc), byte(mode)} - encryptedData := aead.Seal(nil, nonce, sessionKey, adata) - _, err = w.Write(nonce) + + encryptedData := aead.Seal(nil, iv, sessionKey, adata) + _, err = w.Write(iv) if err != nil { return } @@ -265,3 +291,13 @@ func SerializeSymmetricKeyEncryptedReuseKey(w io.Writer, sessionKey []byte, pass return } + +func getEncryptedKeyAeadInstance(c CipherFunction, mode AEADMode, inputKey, associatedData []byte) (aead cipher.AEAD) { + hkdfReader := hkdf.New(sha256.New, inputKey, []byte{}, associatedData) + + encryptionKey := make([]byte, c.KeySize()) + _, _ = readFull(hkdfReader, encryptionKey) + + blockCipher := c.new(encryptionKey) + return mode.new(blockCipher) +} diff --git a/openpgp/packet/symmetric_key_encrypted_data_test.go b/openpgp/packet/symmetric_key_encrypted_data_test.go index 59353308..957bab31 100644 --- a/openpgp/packet/symmetric_key_encrypted_data_test.go +++ b/openpgp/packet/symmetric_key_encrypted_data_test.go @@ -1,8 +1,7 @@ package packet -// These test vectors contain V4 or V4 symmetric key encrypted packets followed -// by an integrity protected packet (either a SymmetricallyEncrypted (with MDC) -// or AEADEncrypted packet. +// These test vectors contain V4 or V5 symmetric key encrypted packets followed +// by an integrity protected packet (SEIPD v1 or v2). type packetSequence struct { password string @@ -10,18 +9,24 @@ type packetSequence struct { contents string } -var keyAndIpePackets = []*packetSequence{symEncTest, aeadEaxRFC, aeadOcbRFC} +var keyAndIpePackets = []*packetSequence{symEncTest, aeadEaxRFC, aeadOcbRFC, aeadGcmRFC} var aeadEaxRFC = &packetSequence{ password: "password", - packets: "c33e0507010308cd5a9f70fbe0bc6590bc669e34e500dcaedc5b32aa2dab02359dee19d07c3446c4312a34ae1967a2fb7e928ea5b4fa8012bd456d1738c63c36d44a0107010eb732379f73c4928de25facfe6517ec105dc11a81dc0cb8a2f6f3d90016384a56fc821ae11ae8dbcb49862655dea88d06a81486801b0ff387bd2eab013de1259586906eab2476", - contents: "cb1462000000000048656c6c6f2c20776f726c64210a", + packets: "c340051e07010b0308a5ae579d1fc5d82bff69224f919993b3506fa3b59a6a73cff8da746b88e357e8ae54eb87e1d70575d72f60232990523e9a59094922406be1c3d269020701069ff90e3b321964f3a42913c8dcc6619325015227efb7eaeaa49f04c2e674175d4a3d226ed6afcb9ca9ac122c1470e11c63d4c0ab241c6a938ad48bf99a5a99b90bba8325de61047540258ab7959a95ad051dda96eb15431dfef5f5e2255ca78261546e339a", + contents: "cb1362000000000048656c6c6f2c20776f726c6421d50eae5bf0cd6705500355816cb0c8ff", } var aeadOcbRFC = &packetSequence{ password: "password", - packets: "c33d05070203089f0b7da3e5ea64779099e326e5400a90936cefb4e8eba08c6773716d1f2714540a38fcac529949dac529d3de31e15b4aeb729e330033dbedd4490107020e5ed2bc1e470abe8f1d644c7a6c8a567b0f7701196611a154ba9c2574cd056284a8ef68035c623d93cc708a43211bb6eaf2b27f7c18d571bcd83b20add3a08b73af15b9a098", - contents: "cb1462000000000048656c6c6f2c20776f726c64210a", + packets: "c33f051d07020b030856a298d2f5e36453ffcfcc5c11664edb9db42590d7dc46b078c5c0419cc51b3a4687cb32e5b7031ce7c66975765b5c21d92aef4cc05c3fead2690207020620a661f731fc9a3032b5623326027e3a5d8db5748ebeff0b0c5910d09ecdd641ff9fd38562758035bc49754ce1bf3fffa7dad0a3b8104f5133cf42a4100a83eef4ca1b4801a8846bf42bcda7c8ce9d65e212f301cbcd98fdcade694a877ad4247323f6e857", + contents: "cb1362000000000048656c6c6f2c20776f726c6421d50eae6aa1649b56aa835b2613902bd2", +} + +var aeadGcmRFC = &packetSequence{ + password: "password", + packets: "c33c051a07030b0308e9d39785b2070008ffb42e7c483ef4884457cb37260c0c4bf3f2cd6cb7b6e38b5bf33467c1c71944dd590346662f5ade61ff84bce0d26902070306fcb94490bcb98bbdc9d106c6090266940f72e89edc21b5596b1576b101ed0f9ffc6fc6d65bbfd24dcd0790966e6d1e85a30053784cb1d8b6a0699ef12155a7b2ad6258531b57651fd7777912fa95e35d9b40216f69a4c248db28ff4331f1632907399e6ff9", + contents: "cb1362000000000048656c6c6f2c20776f726c6421d50e1ce2269a9eddef81032172b7ed7c", } var symEncTest = &packetSequence{ @@ -29,3 +34,4 @@ var symEncTest = &packetSequence{ packets: "c32e04090308f9f479ee0862ee8700a86d5cce4c166b5a7d664dcbe0f0eb2696a3e8a815fe8913251605ad79cc865f15d24301c3da8f5003383b9bd62c673589e2292d990902227311905ff4a7f694727578468e15d9f1aadb41572c4b2a789d7f93896661249200b64af9fbf6abf001f5498d036a", contents: "cb1875076d73672e7478745cafc23e636f6e74656e74732e0d0a", } + diff --git a/openpgp/packet/symmetric_key_encrypted_test.go b/openpgp/packet/symmetric_key_encrypted_test.go index 28f3f4cd..8d6e4c35 100644 --- a/openpgp/packet/symmetric_key_encrypted_test.go +++ b/openpgp/packet/symmetric_key_encrypted_test.go @@ -65,104 +65,101 @@ func TestDecryptSymmetricKeyAndEncryptedDataPacket(t *testing.T) { } } -func TestRandomSerializeSymmetricKeyEncryptedV5RandomizeSlow(t *testing.T) { - var ciphers = []CipherFunction{ - CipherAES128, - CipherAES192, - CipherAES256, - } - var modes = []AEADMode{ - AEADModeEAX, - AEADModeOCB, - AEADModeExperimentalGCM, +func TestSerializeSymmetricKeyEncryptedV5RandomizeSlow(t *testing.T) { + ciphers := map[string] CipherFunction { + "AES128": CipherAES128, + "AES192": CipherAES192, + "AES256": CipherAES256, } - var buf bytes.Buffer - passphrase := make([]byte, mathrand.Intn(maxPassLen)) - _, err := rand.Read(passphrase) - if err != nil { - panic(err) - } - aeadConf := AEADConfig{ - DefaultMode: modes[mathrand.Intn(len(modes))], - } - config := &Config{ - DefaultCipher: ciphers[mathrand.Intn(len(ciphers))], - AEADConfig: &aeadConf, - } - key, err := SerializeSymmetricKeyEncrypted(&buf, passphrase, config) - p, err := Read(&buf) - if err != nil { - t.Errorf("failed to reparse %s", err) - } - ske, ok := p.(*SymmetricKeyEncrypted) - if !ok { - t.Errorf("parsed a different packet type: %#v", p) + modes := map[string] AEADMode { + "EAX": AEADModeEAX, + "OCB": AEADModeOCB, + "GCM": AEADModeGCM, } - parsedKey, _, err := ske.Decrypt(passphrase) - if err != nil { - t.Errorf("failed to decrypt reparsed SKE: %s", err) - } - if !bytes.Equal(key, parsedKey) { - t.Errorf("keys don't match after Decrypt: %x (original) vs %x (parsed)", key, parsedKey) + for cipherName, cipher := range ciphers { + t.Run(cipherName, func(t *testing.T) { + for modeName, mode := range modes { + t.Run(modeName, func(t *testing.T) { + var buf bytes.Buffer + passphrase := randomKey(mathrand.Intn(maxPassLen)) + + config := &Config{ + DefaultCipher: cipher, + AEADConfig: &AEADConfig{DefaultMode: mode}, + } + + key, err := SerializeSymmetricKeyEncrypted(&buf, passphrase, config) + p, err := Read(&buf) + if err != nil { + t.Errorf("failed to reparse %s", err) + } + ske, ok := p.(*SymmetricKeyEncrypted) + if !ok { + t.Errorf("parsed a different packet type: %#v", p) + } + + parsedKey, _, err := ske.Decrypt(passphrase) + if err != nil { + t.Errorf("failed to decrypt reparsed SKE: %s", err) + } + if !bytes.Equal(key, parsedKey) { + t.Errorf("keys don't match after Decrypt: %x (original) vs %x (parsed)", key, parsedKey) + } + }) + } + }) } } func TestSerializeSymmetricKeyEncryptedCiphersV4(t *testing.T) { - tests := [...]struct { - cipherFunc CipherFunction - name string - }{ - {Cipher3DES, "Cipher3DES"}, - {CipherCAST5, "CipherCAST5"}, - {CipherAES128, "CipherAES128"}, - {CipherAES192, "CipherAES192"}, - {CipherAES256, "CipherAES256"}, + tests := map[string] CipherFunction { + "AES128": CipherAES128, + "AES192": CipherAES192, + "AES256": CipherAES256, } - for _, test := range tests { - var buf bytes.Buffer - passphrase := make([]byte, mathrand.Intn(maxPassLen)) - if _, err := rand.Read(passphrase); err != nil { - panic(err) - } - config := &Config{ - DefaultCipher: test.cipherFunc, - } + for cipherName, cipher := range tests { + t.Run(cipherName, func(t *testing.T) { + var buf bytes.Buffer + passphrase := make([]byte, mathrand.Intn(maxPassLen)) + if _, err := rand.Read(passphrase); err != nil { + panic(err) + } + config := &Config{ + DefaultCipher: cipher, + } - key, err := SerializeSymmetricKeyEncrypted(&buf, passphrase, config) - if err != nil { - t.Errorf("cipher(%s) failed to serialize: %s", test.name, err) - continue - } + key, err := SerializeSymmetricKeyEncrypted(&buf, passphrase, config) + if err != nil { + t.Fatalf("failed to serialize: %s", err) + } - p, err := Read(&buf) - if err != nil { - t.Errorf("cipher(%s) failed to reparse: %s", test.name, err) - continue - } + p, err := Read(&buf) + if err != nil { + t.Fatalf("failed to reparse: %s", err) + } - ske, ok := p.(*SymmetricKeyEncrypted) - if !ok { - t.Errorf("cipher(%s) parsed a different packet type: %#v", test.name, p) - continue - } + ske, ok := p.(*SymmetricKeyEncrypted) + if !ok { + t.Fatalf("parsed a different packet type: %#v", p) + } - if ske.CipherFunc != config.DefaultCipher { - t.Errorf("cipher(%s) SKE cipher function is %d (expected %d)", test.name, ske.CipherFunc, config.DefaultCipher) - } - parsedKey, parsedCipherFunc, err := ske.Decrypt(passphrase) - if err != nil { - t.Errorf("cipher(%s) failed to decrypt reparsed SKE: %s", test.name, err) - continue - } - if !bytes.Equal(key, parsedKey) { - t.Errorf("cipher(%s) keys don't match after Decrypt: %x (original) vs %x (parsed)", test.name, key, parsedKey) - } - if parsedCipherFunc != test.cipherFunc { - t.Errorf("cipher(%s) cipher function doesn't match after Decrypt: %d (original) vs %d (parsed)", - test.name, test.cipherFunc, parsedCipherFunc) - } + if ske.CipherFunc != config.DefaultCipher { + t.Fatalf("SKE cipher function is %d (expected %d)", ske.CipherFunc, config.DefaultCipher) + } + parsedKey, parsedCipherFunc, err := ske.Decrypt(passphrase) + if err != nil { + t.Fatalf("failed to decrypt reparsed SKE: %s", err) + } + if !bytes.Equal(key, parsedKey) { + t.Fatalf("keys don't match after Decrypt: %x (original) vs %x (parsed)", key, parsedKey) + } + if parsedCipherFunc != cipher { + t.Fatalf("cipher function doesn't match after Decrypt: %d (original) vs %d (parsed)", + cipher, parsedCipherFunc) + } + }) } } diff --git a/openpgp/packet/symmetrically_encrypted.go b/openpgp/packet/symmetrically_encrypted.go index 8b84de17..dc1a2403 100644 --- a/openpgp/packet/symmetrically_encrypted.go +++ b/openpgp/packet/symmetrically_encrypted.go @@ -5,36 +5,55 @@ package packet import ( - "crypto/cipher" - "crypto/sha1" - "crypto/subtle" - "hash" "io" - "strconv" "github.com/ProtonMail/go-crypto/openpgp/errors" ) +const aeadSaltSize = 32 + // SymmetricallyEncrypted represents a symmetrically encrypted byte string. The // encrypted Contents will consist of more OpenPGP packets. See RFC 4880, // sections 5.7 and 5.13. type SymmetricallyEncrypted struct { - MDC bool // true iff this is a type 18 packet and thus has an embedded MAC. - Contents io.Reader + Version int + Contents io.Reader // contains tag for version 2 + IntegrityProtected bool // If true it is type 18 (with MDC or AEAD). False is packet type 9 + + // Specific to version 1 prefix []byte + + // Specific to version 2 + cipher CipherFunction + mode AEADMode + chunkSizeByte byte + salt [aeadSaltSize]byte } -const symmetricallyEncryptedVersion = 1 +const ( + symmetricallyEncryptedVersionMdc = 1 + symmetricallyEncryptedVersionAead = 2 +) + func (se *SymmetricallyEncrypted) parse(r io.Reader) error { - if se.MDC { + if se.IntegrityProtected { // See RFC 4880, section 5.13. var buf [1]byte _, err := readFull(r, buf[:]) if err != nil { return err } - if buf[0] != symmetricallyEncryptedVersion { + + switch buf[0] { + case symmetricallyEncryptedVersionMdc: + se.Version = symmetricallyEncryptedVersionMdc + case symmetricallyEncryptedVersionAead: + se.Version = symmetricallyEncryptedVersionAead + if err := se.parseAead(r); err != nil { + return err + } + default: return errors.UnsupportedError("unknown SymmetricallyEncrypted version") } } @@ -46,245 +65,27 @@ func (se *SymmetricallyEncrypted) parse(r io.Reader) error { // packet can be read. An incorrect key will only be detected after trying // to decrypt the entire data. func (se *SymmetricallyEncrypted) Decrypt(c CipherFunction, key []byte) (io.ReadCloser, error) { - keySize := c.KeySize() - if keySize == 0 { - return nil, errors.UnsupportedError("unknown cipher: " + strconv.Itoa(int(c))) - } - if len(key) != keySize { - return nil, errors.InvalidArgumentError("SymmetricallyEncrypted: incorrect key length") - } - - if se.prefix == nil { - se.prefix = make([]byte, c.blockSize()+2) - _, err := readFull(se.Contents, se.prefix) - if err != nil { - return nil, err - } - } else if len(se.prefix) != c.blockSize()+2 { - return nil, errors.InvalidArgumentError("can't try ciphers with different block lengths") - } - - ocfbResync := OCFBResync - if se.MDC { - // MDC packets use a different form of OCFB mode. - ocfbResync = OCFBNoResync - } - - s := NewOCFBDecrypter(c.new(key), se.prefix, ocfbResync) - - plaintext := cipher.StreamReader{S: s, R: se.Contents} - - if se.MDC { - // MDC packets have an embedded hash that we need to check. - h := sha1.New() - h.Write(se.prefix) - return &seMDCReader{in: plaintext, h: h}, nil - } - - // Otherwise, we just need to wrap plaintext so that it's a valid ReadCloser. - return seReader{plaintext}, nil -} - -// seReader wraps an io.Reader with a no-op Close method. -type seReader struct { - in io.Reader -} - -func (ser seReader) Read(buf []byte) (int, error) { - return ser.in.Read(buf) -} - -func (ser seReader) Close() error { - return nil -} - -const mdcTrailerSize = 1 /* tag byte */ + 1 /* length byte */ + sha1.Size - -// An seMDCReader wraps an io.Reader, maintains a running hash and keeps hold -// of the most recent 22 bytes (mdcTrailerSize). Upon EOF, those bytes form an -// MDC packet containing a hash of the previous Contents which is checked -// against the running hash. See RFC 4880, section 5.13. -type seMDCReader struct { - in io.Reader - h hash.Hash - trailer [mdcTrailerSize]byte - scratch [mdcTrailerSize]byte - trailerUsed int - error bool - eof bool -} - -func (ser *seMDCReader) Read(buf []byte) (n int, err error) { - if ser.error { - err = io.ErrUnexpectedEOF - return - } - if ser.eof { - err = io.EOF - return - } - - // If we haven't yet filled the trailer buffer then we must do that - // first. - for ser.trailerUsed < mdcTrailerSize { - n, err = ser.in.Read(ser.trailer[ser.trailerUsed:]) - ser.trailerUsed += n - if err == io.EOF { - if ser.trailerUsed != mdcTrailerSize { - n = 0 - err = io.ErrUnexpectedEOF - ser.error = true - return - } - ser.eof = true - n = 0 - return - } - - if err != nil { - n = 0 - return - } - } - - // If it's a short read then we read into a temporary buffer and shift - // the data into the caller's buffer. - if len(buf) <= mdcTrailerSize { - n, err = readFull(ser.in, ser.scratch[:len(buf)]) - copy(buf, ser.trailer[:n]) - ser.h.Write(buf[:n]) - copy(ser.trailer[:], ser.trailer[n:]) - copy(ser.trailer[mdcTrailerSize-n:], ser.scratch[:]) - if n < len(buf) { - ser.eof = true - err = io.EOF - } - return + if se.Version == symmetricallyEncryptedVersionAead { + return se.decryptAead(key) } - n, err = ser.in.Read(buf[mdcTrailerSize:]) - copy(buf, ser.trailer[:]) - ser.h.Write(buf[:n]) - copy(ser.trailer[:], buf[n:]) - - if err == io.EOF { - ser.eof = true - } - return -} - -// This is a new-format packet tag byte for a type 19 (MDC) packet. -const mdcPacketTagByte = byte(0x80) | 0x40 | 19 - -func (ser *seMDCReader) Close() error { - if ser.error { - return errors.ErrMDCMissing - } - - for !ser.eof { - // We haven't seen EOF so we need to read to the end - var buf [1024]byte - _, err := ser.Read(buf[:]) - if err == io.EOF { - break - } - if err != nil { - return errors.ErrMDCMissing - } - } - - ser.h.Write(ser.trailer[:2]) - - final := ser.h.Sum(nil) - if subtle.ConstantTimeCompare(final, ser.trailer[2:]) != 1 { - return errors.ErrMDCHashMismatch - } - // The hash already includes the MDC header, but we still check its value - // to confirm encryption correctness - if ser.trailer[0] != mdcPacketTagByte || ser.trailer[1] != sha1.Size { - return errors.ErrMDCMissing - } - return nil -} - -// An seMDCWriter writes through to an io.WriteCloser while maintains a running -// hash of the data written. On close, it emits an MDC packet containing the -// running hash. -type seMDCWriter struct { - w io.WriteCloser - h hash.Hash -} - -func (w *seMDCWriter) Write(buf []byte) (n int, err error) { - w.h.Write(buf) - return w.w.Write(buf) -} - -func (w *seMDCWriter) Close() (err error) { - var buf [mdcTrailerSize]byte - - buf[0] = mdcPacketTagByte - buf[1] = sha1.Size - w.h.Write(buf[:2]) - digest := w.h.Sum(nil) - copy(buf[2:], digest) - - _, err = w.w.Write(buf[:]) - if err != nil { - return - } - return w.w.Close() -} - -// noOpCloser is like an ioutil.NopCloser, but for an io.Writer. -type noOpCloser struct { - w io.Writer -} - -func (c noOpCloser) Write(data []byte) (n int, err error) { - return c.w.Write(data) -} - -func (c noOpCloser) Close() error { - return nil + return se.decryptMdc(c, key) } // SerializeSymmetricallyEncrypted serializes a symmetrically encrypted packet // to w and returns a WriteCloser to which the to-be-encrypted packets can be // written. // If config is nil, sensible defaults will be used. -func SerializeSymmetricallyEncrypted(w io.Writer, c CipherFunction, key []byte, config *Config) (Contents io.WriteCloser, err error) { - if c.KeySize() != len(key) { - return nil, errors.InvalidArgumentError("SymmetricallyEncrypted.Serialize: bad key length") - } +func SerializeSymmetricallyEncrypted(w io.Writer, c CipherFunction, aeadSupported bool, cipherSuite CipherSuite, key []byte, config *Config) (Contents io.WriteCloser, err error) { writeCloser := noOpCloser{w} - ciphertext, err := serializeStreamHeader(writeCloser, packetTypeSymmetricallyEncryptedMDC) - if err != nil { - return - } - - _, err = ciphertext.Write([]byte{symmetricallyEncryptedVersion}) + ciphertext, err := serializeStreamHeader(writeCloser, packetTypeSymmetricallyEncryptedIntegrityProtected) if err != nil { return } - block := c.new(key) - blockSize := block.BlockSize() - iv := make([]byte, blockSize) - _, err = config.Random().Read(iv) - if err != nil { - return - } - s, prefix := NewOCFBEncrypter(block, iv, OCFBNoResync) - _, err = ciphertext.Write(prefix) - if err != nil { - return + if aeadSupported { + return serializeSymmetricallyEncryptedAead(ciphertext, cipherSuite, config.AEADConfig.ChunkSizeByte(), config.Random(), key) } - plaintext := cipher.StreamWriter{S: s, W: ciphertext} - h := sha1.New() - h.Write(iv) - h.Write(iv[blockSize-2:]) - Contents = &seMDCWriter{w: plaintext, h: h} - return + return serializeSymmetricallyEncryptedMdc(ciphertext, c, key, config) } diff --git a/openpgp/packet/symmetrically_encrypted_aead.go b/openpgp/packet/symmetrically_encrypted_aead.go new file mode 100644 index 00000000..241800c0 --- /dev/null +++ b/openpgp/packet/symmetrically_encrypted_aead.go @@ -0,0 +1,155 @@ +// Copyright 2023 Proton AG. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package packet + +import ( + "crypto/cipher" + "crypto/sha256" + "github.com/ProtonMail/go-crypto/openpgp/errors" + "golang.org/x/crypto/hkdf" + "io" +) + +// parseAead parses a V2 SEIPD packet (AEAD) as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-5.13.2 +func (se *SymmetricallyEncrypted) parseAead(r io.Reader) error { + headerData := make([]byte, 3) + if n, err := io.ReadFull(r, headerData); n < 3 { + return errors.StructuralError("could not read aead header: " + err.Error()) + } + + // Cipher + se.cipher = CipherFunction(headerData[0]) + // cipherFunc must have block size 16 to use AEAD + if se.cipher.blockSize() != 16 { + return errors.UnsupportedError("invalid aead cipher: " + string(se.cipher)) + } + + // Mode + se.mode = AEADMode(headerData[1]) + if se.mode.TagLength() == 0 { + return errors.UnsupportedError("unknown aead mode: " + string(se.mode)) + } + + // Chunk size + se.chunkSizeByte = headerData[2] + if se.chunkSizeByte > 16 { + return errors.UnsupportedError("invalid aead chunk size byte: " + string(se.chunkSizeByte)) + } + + // Salt + if n, err := io.ReadFull(r, se.salt[:]); n < aeadSaltSize { + return errors.StructuralError("could not read aead salt: " + err.Error()) + } + + return nil +} + +// associatedData for chunks: tag, version, cipher, mode, chunk size byte +func (se *SymmetricallyEncrypted) associatedData() []byte { + return []byte{ + 0xD2, + symmetricallyEncryptedVersionAead, + byte(se.cipher), + byte(se.mode), + se.chunkSizeByte, + } +} + +// decryptAead decrypts a V2 SEIPD packet (AEAD) as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-5.13.2 +func (se *SymmetricallyEncrypted) decryptAead(inputKey []byte) (io.ReadCloser, error) { + aead, nonce := getSymmetricallyEncryptedAeadInstance(se.cipher, se.mode, inputKey, se.salt[:], se.associatedData()) + + // Carry the first tagLen bytes + tagLen := se.mode.TagLength() + peekedBytes := make([]byte, tagLen) + n, err := io.ReadFull(se.Contents, peekedBytes) + if n < tagLen || (err != nil && err != io.EOF) { + return nil, errors.StructuralError("not enough data to decrypt:" + err.Error()) + } + + return &aeadDecrypter{ + aeadCrypter: aeadCrypter{ + aead: aead, + chunkSize: decodeAEADChunkSize(se.chunkSizeByte), + initialNonce: nonce, + associatedData: se.associatedData(), + chunkIndex: make([]byte, 8), + packetTag: packetTypeSymmetricallyEncryptedIntegrityProtected, + }, + reader: se.Contents, + peekedBytes: peekedBytes, + }, nil +} + +// serializeSymmetricallyEncryptedAead encrypts to a writer a V2 SEIPD packet (AEAD) as specified in +// https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-5.13.2 +func serializeSymmetricallyEncryptedAead(ciphertext io.WriteCloser, cipherSuite CipherSuite, chunkSizeByte byte, rand io.Reader, inputKey []byte) (Contents io.WriteCloser, err error) { + // cipherFunc must have block size 16 to use AEAD + if cipherSuite.Cipher.blockSize() != 16 { + return nil, errors.InvalidArgumentError("invalid aead cipher function") + } + + if cipherSuite.Cipher.KeySize() != len(inputKey) { + return nil, errors.InvalidArgumentError("error in aead serialization: bad key length") + } + + // Data for en/decryption: tag, version, cipher, aead mode, chunk size + prefix := []byte{ + 0xD2, + symmetricallyEncryptedVersionAead, + byte(cipherSuite.Cipher), + byte(cipherSuite.Mode), + chunkSizeByte, + } + + // Write header (that correspond to prefix except first byte) + n, err := ciphertext.Write(prefix[1:]) + if err != nil || n < 4 { + return nil, err + } + + // Random salt + salt := make([]byte, aeadSaltSize) + if _, err := rand.Read(salt); err != nil { + return nil, err + } + + if _, err := ciphertext.Write(salt); err != nil { + return nil, err + } + + aead, nonce := getSymmetricallyEncryptedAeadInstance(cipherSuite.Cipher, cipherSuite.Mode, inputKey, salt, prefix) + + return &aeadEncrypter{ + aeadCrypter: aeadCrypter{ + aead: aead, + chunkSize: decodeAEADChunkSize(chunkSizeByte), + associatedData: prefix, + chunkIndex: make([]byte, 8), + initialNonce: nonce, + packetTag: packetTypeSymmetricallyEncryptedIntegrityProtected, + }, + writer: ciphertext, + }, nil +} + +func getSymmetricallyEncryptedAeadInstance(c CipherFunction, mode AEADMode, inputKey, salt, associatedData []byte) (aead cipher.AEAD, nonce []byte) { + hkdfReader := hkdf.New(sha256.New, inputKey, salt, associatedData) + + encryptionKey := make([]byte, c.KeySize()) + _, _ = readFull(hkdfReader, encryptionKey) + + // Last 64 bits of nonce are the counter + nonce = make([]byte, mode.IvLength() - 8) + + _, _ = readFull(hkdfReader, nonce) + + blockCipher := c.new(encryptionKey) + aead = mode.new(blockCipher) + + return +} diff --git a/openpgp/packet/symmetrically_encrypted_mdc.go b/openpgp/packet/symmetrically_encrypted_mdc.go new file mode 100644 index 00000000..3e070f8b --- /dev/null +++ b/openpgp/packet/symmetrically_encrypted_mdc.go @@ -0,0 +1,256 @@ +// 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 packet + +import ( + "crypto/cipher" + "crypto/sha1" + "crypto/subtle" + "hash" + "io" + "strconv" + + "github.com/ProtonMail/go-crypto/openpgp/errors" +) + +// seMdcReader wraps an io.Reader with a no-op Close method. +type seMdcReader struct { + in io.Reader +} + +func (ser seMdcReader) Read(buf []byte) (int, error) { + return ser.in.Read(buf) +} + +func (ser seMdcReader) Close() error { + return nil +} + +func (se *SymmetricallyEncrypted) decryptMdc(c CipherFunction, key []byte) (io.ReadCloser, error) { + if !c.IsSupported() { + return nil, errors.UnsupportedError("unsupported cipher: " + strconv.Itoa(int(c))) + } + + if len(key) != c.KeySize() { + return nil, errors.InvalidArgumentError("SymmetricallyEncrypted: incorrect key length") + } + + if se.prefix == nil { + se.prefix = make([]byte, c.blockSize()+2) + _, err := readFull(se.Contents, se.prefix) + if err != nil { + return nil, err + } + } else if len(se.prefix) != c.blockSize()+2 { + return nil, errors.InvalidArgumentError("can't try ciphers with different block lengths") + } + + ocfbResync := OCFBResync + if se.IntegrityProtected { + // MDC packets use a different form of OCFB mode. + ocfbResync = OCFBNoResync + } + + s := NewOCFBDecrypter(c.new(key), se.prefix, ocfbResync) + + plaintext := cipher.StreamReader{S: s, R: se.Contents} + + if se.IntegrityProtected { + // IntegrityProtected packets have an embedded hash that we need to check. + h := sha1.New() + h.Write(se.prefix) + return &seMDCReader{in: plaintext, h: h}, nil + } + + // Otherwise, we just need to wrap plaintext so that it's a valid ReadCloser. + return seMdcReader{plaintext}, nil +} + +const mdcTrailerSize = 1 /* tag byte */ + 1 /* length byte */ + sha1.Size + +// An seMDCReader wraps an io.Reader, maintains a running hash and keeps hold +// of the most recent 22 bytes (mdcTrailerSize). Upon EOF, those bytes form an +// MDC packet containing a hash of the previous Contents which is checked +// against the running hash. See RFC 4880, section 5.13. +type seMDCReader struct { + in io.Reader + h hash.Hash + trailer [mdcTrailerSize]byte + scratch [mdcTrailerSize]byte + trailerUsed int + error bool + eof bool +} + +func (ser *seMDCReader) Read(buf []byte) (n int, err error) { + if ser.error { + err = io.ErrUnexpectedEOF + return + } + if ser.eof { + err = io.EOF + return + } + + // If we haven't yet filled the trailer buffer then we must do that + // first. + for ser.trailerUsed < mdcTrailerSize { + n, err = ser.in.Read(ser.trailer[ser.trailerUsed:]) + ser.trailerUsed += n + if err == io.EOF { + if ser.trailerUsed != mdcTrailerSize { + n = 0 + err = io.ErrUnexpectedEOF + ser.error = true + return + } + ser.eof = true + n = 0 + return + } + + if err != nil { + n = 0 + return + } + } + + // If it's a short read then we read into a temporary buffer and shift + // the data into the caller's buffer. + if len(buf) <= mdcTrailerSize { + n, err = readFull(ser.in, ser.scratch[:len(buf)]) + copy(buf, ser.trailer[:n]) + ser.h.Write(buf[:n]) + copy(ser.trailer[:], ser.trailer[n:]) + copy(ser.trailer[mdcTrailerSize-n:], ser.scratch[:]) + if n < len(buf) { + ser.eof = true + err = io.EOF + } + return + } + + n, err = ser.in.Read(buf[mdcTrailerSize:]) + copy(buf, ser.trailer[:]) + ser.h.Write(buf[:n]) + copy(ser.trailer[:], buf[n:]) + + if err == io.EOF { + ser.eof = true + } + return +} + +// This is a new-format packet tag byte for a type 19 (Integrity Protected) packet. +const mdcPacketTagByte = byte(0x80) | 0x40 | 19 + +func (ser *seMDCReader) Close() error { + if ser.error { + return errors.ErrMDCMissing + } + + for !ser.eof { + // We haven't seen EOF so we need to read to the end + var buf [1024]byte + _, err := ser.Read(buf[:]) + if err == io.EOF { + break + } + if err != nil { + return errors.ErrMDCMissing + } + } + + ser.h.Write(ser.trailer[:2]) + + final := ser.h.Sum(nil) + if subtle.ConstantTimeCompare(final, ser.trailer[2:]) != 1 { + return errors.ErrMDCHashMismatch + } + // The hash already includes the MDC header, but we still check its value + // to confirm encryption correctness + if ser.trailer[0] != mdcPacketTagByte || ser.trailer[1] != sha1.Size { + return errors.ErrMDCMissing + } + return nil +} + +// An seMDCWriter writes through to an io.WriteCloser while maintains a running +// hash of the data written. On close, it emits an MDC packet containing the +// running hash. +type seMDCWriter struct { + w io.WriteCloser + h hash.Hash +} + +func (w *seMDCWriter) Write(buf []byte) (n int, err error) { + w.h.Write(buf) + return w.w.Write(buf) +} + +func (w *seMDCWriter) Close() (err error) { + var buf [mdcTrailerSize]byte + + buf[0] = mdcPacketTagByte + buf[1] = sha1.Size + w.h.Write(buf[:2]) + digest := w.h.Sum(nil) + copy(buf[2:], digest) + + _, err = w.w.Write(buf[:]) + if err != nil { + return + } + return w.w.Close() +} + +// noOpCloser is like an ioutil.NopCloser, but for an io.Writer. +type noOpCloser struct { + w io.Writer +} + +func (c noOpCloser) Write(data []byte) (n int, err error) { + return c.w.Write(data) +} + +func (c noOpCloser) Close() error { + return nil +} + +func serializeSymmetricallyEncryptedMdc(ciphertext io.WriteCloser, c CipherFunction, key []byte, config *Config) (Contents io.WriteCloser, err error) { + // Disallow old cipher suites + if !c.IsSupported() || c < CipherAES128 { + return nil, errors.InvalidArgumentError("invalid mdc cipher function") + } + + if c.KeySize() != len(key) { + return nil, errors.InvalidArgumentError("error in mdc serialization: bad key length") + } + + _, err = ciphertext.Write([]byte{symmetricallyEncryptedVersionMdc}) + if err != nil { + return + } + + block := c.new(key) + blockSize := block.BlockSize() + iv := make([]byte, blockSize) + _, err = config.Random().Read(iv) + if err != nil { + return + } + s, prefix := NewOCFBEncrypter(block, iv, OCFBNoResync) + _, err = ciphertext.Write(prefix) + if err != nil { + return + } + plaintext := cipher.StreamWriter{S: s, W: ciphertext} + + h := sha1.New() + h.Write(iv) + h.Write(iv[blockSize-2:]) + Contents = &seMDCWriter{w: plaintext, h: h} + return +} diff --git a/openpgp/packet/symmetrically_encrypted_test.go b/openpgp/packet/symmetrically_encrypted_test.go index 78962499..36f0a1d4 100644 --- a/openpgp/packet/symmetrically_encrypted_test.go +++ b/openpgp/packet/symmetrically_encrypted_test.go @@ -6,12 +6,15 @@ package packet import ( "bytes" + "crypto/rand" "crypto/sha1" "encoding/hex" - "github.com/ProtonMail/go-crypto/openpgp/errors" + goerrors "errors" "io" "io/ioutil" "testing" + + "github.com/ProtonMail/go-crypto/openpgp/errors" ) // TestReader wraps a []byte and returns reads of a specific length. @@ -28,17 +31,21 @@ func (t *testReader) Read(buf []byte) (n int, err error) { if n > len(buf) { n = len(buf) } - copy(buf, t.data) + + copy(buf[:n], t.data) t.data = t.data[n:] + if len(t.data) == 0 { err = io.EOF } + return } -func testMDCReader(t *testing.T) { - mdcPlaintext, _ := hex.DecodeString(mdcPlaintextHex) +const mdcPlaintextHex = "cb1362000000000048656c6c6f2c20776f726c6421d314c23d643f478a9a2098811fcb191e7b24b80966a1" +func TestMDCReader(t *testing.T) { + mdcPlaintext, _ := hex.DecodeString(mdcPlaintextHex) for stride := 1; stride < len(mdcPlaintext)/2; stride++ { r := &testReader{data: mdcPlaintext, stride: stride} mdcReader := &seMDCReader{in: r, h: sha1.New()} @@ -70,19 +77,22 @@ func testMDCReader(t *testing.T) { err = mdcReader.Close() if err == nil { t.Error("corruption: no error") - } else if _, ok := err.(*errors.SignatureError); !ok { + } else if !goerrors.Is(err, errors.ErrMDCHashMismatch) { t.Errorf("corruption: expected SignatureError, got: %s", err) } } -const mdcPlaintextHex = "a302789c3b2d93c4e0eb9aba22283539b3203335af44a134afb800c849cb4c4de10200aff40b45d31432c80cb384299a0655966d6939dfdeed1dddf980" - -func TestSerialize(t *testing.T) { +func TestSerializeMdc(t *testing.T) { buf := bytes.NewBuffer(nil) c := CipherAES128 key := make([]byte, c.KeySize()) - w, err := SerializeSymmetricallyEncrypted(buf, c, key, nil) + cipherSuite := CipherSuite{ + Cipher: c, + Mode: AEADModeOCB, + } + + w, err := SerializeSymmetricallyEncrypted(buf, c, false, cipherSuite, key, nil) if err != nil { t.Errorf("error from SerializeSymmetricallyEncrypted: %s", err) return @@ -121,3 +131,167 @@ func TestSerialize(t *testing.T) { t.Errorf("contents not equal got: %x want: %x", contentsCopy.Bytes(), contents) } } + +const aeadHexKey = "1936fc8568980274bb900d8319360c77" +const aeadHexSeipd = "d26902070306fcb94490bcb98bbdc9d106c6090266940f72e89edc21b5596b1576b101ed0f9ffc6fc6d65bbfd24dcd0790966e6d1e85a30053784cb1d8b6a0699ef12155a7b2ad6258531b57651fd7777912fa95e35d9b40216f69a4c248db28ff4331f1632907399e6ff9" +const aeadHexPlainText = "cb1362000000000048656c6c6f2c20776f726c6421d50e1ce2269a9eddef81032172b7ed7c" +const aeadExpectedSalt = "fcb94490bcb98bbdc9d106c6090266940f72e89edc21b5596b1576b101ed0f9f" + +func TestAeadRfcVector(t *testing.T) { + key, err := hex.DecodeString(aeadHexKey) + if err != nil { + t.Errorf("error in decoding key: %s", err) + } + + packet, err := hex.DecodeString(aeadHexSeipd) + if err != nil { + t.Errorf("error in decoding packet: %s", err) + } + + plainText, err := hex.DecodeString(aeadHexPlainText) + if err != nil { + t.Errorf("error in decoding plaintext: %s", err) + } + + expectedSalt, err := hex.DecodeString(aeadExpectedSalt) + if err != nil { + t.Errorf("error in decoding salt: %s", err) + } + + buf := bytes.NewBuffer(packet) + p, err := Read(buf) + if err != nil { + t.Errorf("error from Read: %s", err) + return + } + + se, ok := p.(*SymmetricallyEncrypted) + if !ok { + t.Errorf("didn't read a *SymmetricallyEncrypted") + return + } + + if se.Version != symmetricallyEncryptedVersionAead { + t.Errorf("found wrong version, want: %d, got: %d", symmetricallyEncryptedVersionAead, se.Version) + } + + if se.cipher != CipherAES128 { + t.Errorf("found wrong cipher, want: %d, got: %d", CipherAES128, se.cipher) + } + + if se.mode != AEADModeGCM { + t.Errorf("found wrong mode, want: %d, got: %d", AEADModeGCM, se.mode) + } + + if !bytes.Equal(se.salt[:], expectedSalt) { + t.Errorf("found wrong salt, want: %x, got: %x", expectedSalt, se.salt) + } + + if se.chunkSizeByte != 0x06 { + t.Errorf("found wrong chunk size byte, want: %d, got: %d", 0x06, se.chunkSizeByte) + } + + aeadReader, err := se.Decrypt(CipherFunction(0), key) + if err != nil { + t.Errorf("error from Decrypt: %s", err) + return + } + + decrypted, err := ioutil.ReadAll(aeadReader) + if err != nil { + t.Errorf("error when reading: %s", err) + return + } + + err = aeadReader.Close() + if err != nil { + t.Errorf("error when closing reader: %s", err) + return + } + + if !bytes.Equal(decrypted, plainText) { + t.Errorf("contents not equal got: %x want: %x", decrypted, plainText) + } +} + +func TestAeadEncryptDecrypt(t *testing.T) { + ciphers := map[string] CipherFunction { + "AES128": CipherAES128, + "AES192": CipherAES192, + "AES256": CipherAES256, + } + + modes := map[string] AEADMode { + "EAX": AEADModeEAX, + "OCB": AEADModeOCB, + "GCM": AEADModeGCM, + } + + for cipherName, cipher := range ciphers { + t.Run(cipherName, func(t *testing.T) { + for modeName, mode := range modes { + t.Run(modeName, func(t *testing.T) { + testSerializeAead(t, CipherSuite{Cipher: cipher, Mode: mode}) + }) + } + }) + } +} + +func testSerializeAead(t *testing.T, cipherSuite CipherSuite) { + buf := bytes.NewBuffer(nil) + key := make([]byte, cipherSuite.Cipher.KeySize()) + _, _ = rand.Read(key) + + w, err := SerializeSymmetricallyEncrypted(buf, CipherFunction(0), true, cipherSuite, key, &Config{AEADConfig: &AEADConfig{}}) + if err != nil { + t.Errorf("error from SerializeSymmetricallyEncrypted: %s", err) + return + } + + contents := []byte("hello world\n") + + w.Write(contents) + w.Close() + + p, err := Read(buf) + if err != nil { + t.Errorf("error from Read: %s", err) + return + } + + se, ok := p.(*SymmetricallyEncrypted) + if !ok { + t.Errorf("didn't read a *SymmetricallyEncrypted") + return + } + + if se.Version != symmetricallyEncryptedVersionAead { + t.Errorf("found wrong version, want: %d, got: %d", symmetricallyEncryptedVersionAead, se.Version) + } + + if se.cipher != cipherSuite.Cipher { + t.Errorf("found wrong cipher, want: %d, got: %d", cipherSuite.Cipher, se.cipher) + } + + if se.mode != cipherSuite.Mode { + t.Errorf("found wrong mode, want: %d, got: %d", cipherSuite.Mode, se.mode) + } + + + r, err := se.Decrypt(CipherFunction(0), key) + if err != nil { + t.Errorf("error from Decrypt: %s", err) + return + } + + contentsCopy := bytes.NewBuffer(nil) + _, err = io.Copy(contentsCopy, r) + if err != nil { + t.Errorf("error from io.Copy: %s", err) + return + } + if !bytes.Equal(contentsCopy.Bytes(), contents) { + t.Errorf("contents not equal got: %x want: %x", contentsCopy.Bytes(), contents) + } +} \ No newline at end of file diff --git a/openpgp/read.go b/openpgp/read.go index f9233f6d..e910e184 100644 --- a/openpgp/read.go +++ b/openpgp/read.go @@ -8,13 +8,16 @@ package openpgp // import "github.com/ProtonMail/go-crypto/openpgp" import ( "crypto" _ "crypto/sha256" + _ "crypto/sha512" "hash" "io" "strconv" "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/packet" + _ "golang.org/x/crypto/sha3" ) // SignatureType is the armor type for a PGP signature. @@ -131,8 +134,8 @@ ParsePackets: } } case *packet.SymmetricallyEncrypted: - if !p.MDC && !config.AllowUnauthenticatedMessages() { - return nil, errors.UnsupportedError("message is not authenticated") + if !p.IntegrityProtected && !config.AllowUnauthenticatedMessages() { + return nil, errors.UnsupportedError("message is not integrity protected") } edp = p break ParsePackets @@ -208,13 +211,11 @@ FindKey: if len(symKeys) != 0 && passphrase != nil { for _, s := range symKeys { key, cipherFunc, err := s.Decrypt(passphrase) - // On wrong passphrase, session key decryption is very likely to result in an invalid cipherFunc: + // In v4, on wrong passphrase, session key decryption is very likely to result in an invalid cipherFunc: // only for < 5% of cases we will proceed to decrypt the data if err == nil { decrypted, err = edp.Decrypt(cipherFunc, key) - // TODO: ErrKeyIncorrect is no longer thrown on SEIP decryption, - // but it might still be relevant for when we implement AEAD decryption (otherwise, remove?) - if err != nil && err != errors.ErrKeyIncorrect { + if err != nil { return nil, err } if decrypted != nil { @@ -304,14 +305,14 @@ FindLiteralData: // should be preprocessed (i.e. to normalize line endings). Thus this function // returns two hashes. The second should be used to hash the message itself and // performs any needed preprocessing. -func hashForSignature(hashId crypto.Hash, sigType packet.SignatureType) (hash.Hash, hash.Hash, error) { - if hashId == crypto.MD5 { - return nil, nil, errors.UnsupportedError("insecure hash algorithm: MD5") +func hashForSignature(hashFunc crypto.Hash, sigType packet.SignatureType) (hash.Hash, hash.Hash, error) { + if _, ok := algorithm.HashToHashIdWithSha1(hashFunc); !ok { + return nil, nil, errors.UnsupportedError("unsupported hash function") } - if !hashId.Available() { - return nil, nil, errors.UnsupportedError("hash not available: " + strconv.Itoa(int(hashId))) + if !hashFunc.Available() { + return nil, nil, errors.UnsupportedError("hash not available: " + strconv.Itoa(int(hashFunc))) } - h := hashId.New() + h := hashFunc.New() switch sigType { case packet.SigTypeBinary: @@ -383,19 +384,7 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) { key := scr.md.SignedBy signatureError := key.PublicKey.VerifySignature(scr.h, sig) if signatureError == nil { - now := scr.config.Now() - if key.Revoked(now) || - key.Entity.Revoked(now) || // primary key is revoked (redundant if key is the primary key) - key.Entity.PrimaryIdentity().Revoked(now) { - signatureError = errors.ErrKeyRevoked - } - if sig.SigExpired(now) { - signatureError = errors.ErrSignatureExpired - } - if key.PublicKey.KeyExpired(key.SelfSignature, now) || - key.SelfSignature.SigExpired(now) { - signatureError = errors.ErrKeyExpired - } + signatureError = checkSignatureDetails(key, sig, scr.config) } scr.md.Signature = sig scr.md.SignatureError = signatureError @@ -434,8 +423,24 @@ func (scr *signatureCheckReader) Read(buf []byte) (int, error) { return n, nil } +// VerifyDetachedSignature takes a signed file and a detached signature and +// returns the signature packet and the entity the signature was signed by, +// if any, and a possible signature verification error. +// If the signer isn't known, ErrUnknownIssuer is returned. +func VerifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) { + var expectedHashes []crypto.Hash + return verifyDetachedSignature(keyring, signed, signature, expectedHashes, config) +} + +// VerifyDetachedSignatureAndHash performs the same actions as +// VerifyDetachedSignature and checks that the expected hash functions were used. +func VerifyDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) { + return verifyDetachedSignature(keyring, signed, signature, expectedHashes, config) +} + // CheckDetachedSignature takes a signed file and a detached signature and -// returns the signer if the signature is valid. If the signer isn't known, +// returns the entity the signature was signed by, if any, and a possible +// signature verification error. If the signer isn't known, // ErrUnknownIssuer is returned. func CheckDetachedSignature(keyring KeyRing, signed, signature io.Reader, config *packet.Config) (signer *Entity, err error) { var expectedHashes []crypto.Hash @@ -445,6 +450,11 @@ func CheckDetachedSignature(keyring KeyRing, signed, signature io.Reader, config // CheckDetachedSignatureAndHash performs the same actions as // CheckDetachedSignature and checks that the expected hash functions were used. func CheckDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, config *packet.Config) (signer *Entity, err error) { + _, signer, err = verifyDetachedSignature(keyring, signed, signature, expectedHashes, config) + return +} + +func verifyDetachedSignature(keyring KeyRing, signed, signature io.Reader, expectedHashes []crypto.Hash, config *packet.Config) (sig *packet.Signature, signer *Entity, err error) { var issuerKeyId uint64 var hashFunc crypto.Hash var sigType packet.SignatureType @@ -453,23 +463,22 @@ func CheckDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, expectedHashesLen := len(expectedHashes) packets := packet.NewReader(signature) - var sig *packet.Signature for { p, err = packets.Next() if err == io.EOF { - return nil, errors.ErrUnknownIssuer + return nil, nil, errors.ErrUnknownIssuer } if err != nil { - return nil, err + return nil, nil, err } var ok bool sig, ok = p.(*packet.Signature) if !ok { - return nil, errors.StructuralError("non signature packet found") + return nil, nil, errors.StructuralError("non signature packet found") } if sig.IssuerKeyId == nil { - return nil, errors.StructuralError("signature doesn't have an issuer") + return nil, nil, errors.StructuralError("signature doesn't have an issuer") } issuerKeyId = *sig.IssuerKeyId hashFunc = sig.Hash @@ -480,7 +489,7 @@ func CheckDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, break } if i+1 == expectedHashesLen { - return nil, errors.StructuralError("hash algorithm mismatch with cleartext message headers") + return nil, nil, errors.StructuralError("hash algorithm mismatch with cleartext message headers") } } @@ -496,34 +505,21 @@ func CheckDetachedSignatureAndHash(keyring KeyRing, signed, signature io.Reader, h, wrappedHash, err := hashForSignature(hashFunc, sigType) if err != nil { - return nil, err + return nil, nil, err } if _, err := io.Copy(wrappedHash, signed); err != nil && err != io.EOF { - return nil, err + return nil, nil, err } for _, key := range keys { err = key.PublicKey.VerifySignature(h, sig) if err == nil { - now := config.Now() - if key.Revoked(now) || - key.Entity.Revoked(now) || // primary key is revoked (redundant if key is the primary key) - key.Entity.PrimaryIdentity().Revoked(now) { - return key.Entity, errors.ErrKeyRevoked - } - if sig.SigExpired(now) { - return key.Entity, errors.ErrSignatureExpired - } - if key.PublicKey.KeyExpired(key.SelfSignature, now) || - key.SelfSignature.SigExpired(now) { - return key.Entity, errors.ErrKeyExpired - } - return key.Entity, nil + return sig, key.Entity, checkSignatureDetails(&key, sig, config) } } - return nil, err + return nil, nil, err } // CheckArmoredDetachedSignature performs the same actions as @@ -536,3 +532,59 @@ func CheckArmoredDetachedSignature(keyring KeyRing, signed, signature io.Reader, return CheckDetachedSignature(keyring, signed, body, config) } + +// checkSignatureDetails returns an error if: +// - The signature (or one of the binding signatures mentioned below) +// has a unknown critical notation data subpacket +// - The primary key of the signing entity is revoked +// The signature was signed by a subkey and: +// - The signing subkey is revoked +// - The primary identity is revoked +// - The signature is expired +// - The primary key of the signing entity is expired according to the +// primary identity binding signature +// The signature was signed by a subkey and: +// - The signing subkey is expired according to the subkey binding signature +// - The signing subkey binding signature is expired +// - The signing subkey cross-signature is expired +// NOTE: The order of these checks is important, as the caller may choose to +// ignore ErrSignatureExpired or ErrKeyExpired errors, but should never +// ignore any other errors. +// TODO: Also return an error if: +// - The primary key is expired according to a direct-key signature +// - (For V5 keys only:) The direct-key signature (exists and) is expired +func checkSignatureDetails(key *Key, signature *packet.Signature, config *packet.Config) error { + now := config.Now() + primaryIdentity := key.Entity.PrimaryIdentity() + signedBySubKey := key.PublicKey != key.Entity.PrimaryKey + sigsToCheck := []*packet.Signature{ signature, primaryIdentity.SelfSignature } + if signedBySubKey { + sigsToCheck = append(sigsToCheck, key.SelfSignature, key.SelfSignature.EmbeddedSignature) + } + for _, sig := range sigsToCheck { + for _, notation := range sig.Notations { + if notation.IsCritical && !config.KnownNotation(notation.Name) { + return errors.SignatureError("unknown critical notation: " + notation.Name) + } + } + } + if key.Entity.Revoked(now) || // primary key is revoked + (signedBySubKey && key.Revoked(now)) || // subkey is revoked + primaryIdentity.Revoked(now) { // primary identity is revoked + return errors.ErrKeyRevoked + } + if key.Entity.PrimaryKey.KeyExpired(primaryIdentity.SelfSignature, now) { // primary key is expired + return errors.ErrKeyExpired + } + if signedBySubKey { + if key.PublicKey.KeyExpired(key.SelfSignature, now) { // subkey is expired + return errors.ErrKeyExpired + } + } + for _, sig := range sigsToCheck { + if sig.SigExpired(now) { // any of the relevant signatures are expired + return errors.ErrSignatureExpired + } + } + return nil +} diff --git a/openpgp/read_test.go b/openpgp/read_test.go index 664a939d..36083d66 100644 --- a/openpgp/read_test.go +++ b/openpgp/read_test.go @@ -353,7 +353,7 @@ func testDetachedSignature(t *testing.T, kring KeyRing, signature io.Reader, sig return } if signer.PrimaryKey.KeyId != expectedSignerKeyId { - t.Errorf("%s: wrong signer got:%x want:%x", tag, signer.PrimaryKey.KeyId, expectedSignerKeyId) + t.Errorf("%s: wrong signer: got %x, expected %x", tag, signer.PrimaryKey.KeyId, expectedSignerKeyId) } } @@ -423,6 +423,75 @@ func TestRSASignatureBadMPILength(t *testing.T) { } } +func TestDetachedSignatureExpiredCrossSig(t *testing.T) { + kring, _ := ReadArmoredKeyRing(bytes.NewBufferString(keyWithExpiredCrossSig)) + config := &packet.Config{} + _, err := CheckArmoredDetachedSignature(kring, bytes.NewBufferString("Hello World :)"), bytes.NewBufferString(sigFromKeyWithExpiredCrossSig), config) + if err == nil { + t.Fatal("Signature from key with expired subkey binding embedded signature was accepted") + } + if err != errors.ErrSignatureExpired { + t.Fatalf("Unexpected class of error: %s", err) + } +} + +func TestSignatureUnknownNotation(t *testing.T) { + el, err := ReadArmoredKeyRing(bytes.NewBufferString(criticalNotationSigner)) + if err != nil { + t.Error(err) + } + raw, err := armor.Decode(strings.NewReader(signedMessageWithCriticalNotation)) + if err != nil { + t.Error(err) + return + } + md, err := ReadMessage(raw.Body, el, nil, nil) + if err != nil { + t.Error(err) + return + } + _, err = ioutil.ReadAll(md.UnverifiedBody) + if err != nil { + t.Error(err) + return + } + const expectedErr string = "openpgp: invalid signature: unknown critical notation: test@example.com" + if md.SignatureError == nil || md.SignatureError.Error() != expectedErr { + t.Errorf("Expected error '%s', but got error '%s'", expectedErr, md.SignatureError) + } +} + +func TestSignatureKnownNotation(t *testing.T) { + el, err := ReadArmoredKeyRing(bytes.NewBufferString(criticalNotationSigner)) + if err != nil { + t.Error(err) + } + raw, err := armor.Decode(strings.NewReader(signedMessageWithCriticalNotation)) + if err != nil { + t.Error(err) + return + } + config := &packet.Config{ + KnownNotations: map[string]bool{ + "test@example.com": true, + }, + } + md, err := ReadMessage(raw.Body, el, nil, config) + if err != nil { + t.Error(err) + return + } + _, err = ioutil.ReadAll(md.UnverifiedBody) + if err != nil { + t.Error(err) + return + } + if md.SignatureError != nil { + t.Error(md.SignatureError) + return + } +} + func TestReadingArmoredPrivateKey(t *testing.T) { el, err := ReadArmoredKeyRing(bytes.NewBufferString(armoredPrivateKeyBlock)) if err != nil { diff --git a/openpgp/read_write_test_data.go b/openpgp/read_write_test_data.go index 117a1464..db6dad5c 100644 --- a/openpgp/read_write_test_data.go +++ b/openpgp/read_write_test_data.go @@ -1,8 +1,8 @@ package openpgp -const testKey1KeyId = 0xA34D7E18C20C31BB -const testKey3KeyId = 0x338934250CCC0360 -const testKeyP256KeyId = 0xd44a2c495918513e +const testKey1KeyId uint64 = 0xA34D7E18C20C31BB +const testKey3KeyId uint64 = 0x338934250CCC0360 +const testKeyP256KeyId uint64 = 0xd44a2c495918513e const signedInput = "Signed message\nline 2\nline 3\n" const signedTextInput = "Signed message\r\nline 2\r\nline 3\r\n" @@ -106,7 +106,7 @@ const unknownHashFunctionHex = `8a00000040040001990006050253863c24000a09103b4fe6 const rsaSignatureBadMPIlength = `8a00000040040001030006050253863c24000a09103b4fe6acc0b21f32ffff0101010101010101010101010101010101010101010101010101010101010101010101010101` -const missingHashFunctionHex = `8a00000040040001030006050253863c24000a09103b4fe6acc0b21f32ffff0101010101010101010101010101010101010101010101010101010101010101010101` +const missingHashFunctionHex = `8a00000040040001030006050253863c24000a09103b4fe6acc0b21f32ffff0101010101010101010101010101010101010101010101010101010101010101010101010101` const campbellQuine = `a0b001000300fcffa0b001000d00f2ff000300fcffa0b001000d00f2ff8270a01c00000500faff8270a01c00000500faff000500faff001400ebff8270a01c00000500faff000500faff001400ebff428821c400001400ebff428821c400001400ebff428821c400001400ebff428821c400001400ebff428821c400000000ffff000000ffff000b00f4ff428821c400000000ffff000000ffff000b00f4ff0233214c40000100feff000233214c40000100feff0000` @@ -171,3 +171,104 @@ y29VPonFXqi2zKkpZrvyvZxg+n5e8Nt9wNbuxeCd3QD/TtO2s+JvjrE4Siwv UQdl5MlBka1QSNbMq2Bz7XwNPg4= =6lbM -----END PGP MESSAGE-----` + +const keyWithExpiredCrossSig = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +hW1Hj9AO8lzggBQ= +=Nt+N +-----END PGP PUBLIC KEY BLOCK----- +` + +const sigFromKeyWithExpiredCrossSig = `-----BEGIN PGP SIGNATURE----- + +wsDzBAABCgAGBYJfLAFsACEJEHwvqk35PDeyFiEEHdzhXwkhfO4vOzdgfC+qTfk8 +N7KiqwwAts4QGB7v9bABCC2qkTxJhmStC0wQMcHRcjL/qAiVnmasQWmvE9KVsdm3 +AaXd8mIx4a37/RRvr9dYrY2eE4uw72cMqPxNja2tvVXkHQvk1oEUqfkvbXs4ypKI +NyeTWjXNOTZEbg0hbm3nMy+Wv7zgB1CEvAsEboLDJlhGqPcD+X8a6CJGrBGUBUrv +KVmZr3U6vEzClz3DBLpoddCQseJRhT4YM1nKmBlZ5quh2LFgTSpajv5OsZheqt9y +EZAPbqmLhDmWRQwGzkWHKceKS7nZ/ox2WK6OS7Ob8ZGZkM64iPo6/EGj5Yc19vQN +AGiIaPEGszBBWlOpHTPhNm0LB0nMWqqaT87oNYwP8CQuuxDb6rKJ2lffCmZH27Lb +UbQZcH8J+0UhpeaiadPZxH5ATJAcenmVtVVMLVOFnm+eIlxzov9ntpgGYt8hLdXB +ITEG9mMgp3TGS9ZzSifMZ8UGtHdp9QdBg8NEVPFzDOMGxpc/Bftav7RRRuPiAER+ +7A5CBid5 +=aQkm +-----END PGP SIGNATURE----- +` + +const signedMessageWithCriticalNotation = `-----BEGIN PGP MESSAGE----- + +owGbwMvMwMH4oOW7S46CznTG09xJDDE3Wl1KUotLuDousDAwcjBYiSmyXL+48d6x +U1PSGUxcj8IUszKBVMpMaWAAAgEGZpAeh9SKxNyCnFS95PzcytRiBi5OAZjyXXzM +f8WYLqv7TXP61Sa4rqT12CI3xaN73YS2pt089f96odCKaEPnWJ3iSGmzJaW/ug10 +2Zo8Wj2k4s7t8wt4H3HtTu+y5UZfV3VOO+l//sdE/o+Lsub8FZH7/eOq7OnbNp4n +vwjE8mqJXetNMfj8r2SCyvkEnlVRYR+/mnge+ib56FdJ8uKtqSxyvgA= +=fRXs +-----END PGP MESSAGE-----` + +const criticalNotationSigner = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mI0EUmEvTgEEANyWtQQMOybQ9JltDqmaX0WnNPJeLILIM36sw6zL0nfTQ5zXSS3+ +fIF6P29lJFxpblWk02PSID5zX/DYU9/zjM2xPO8Oa4xo0cVTOTLj++Ri5mtr//f5 +GLsIXxFrBJhD/ghFsL3Op0GXOeLJ9A5bsOn8th7x6JucNKuaRB6bQbSPABEBAAG0 +JFRlc3QgTWNUZXN0aW5ndG9uIDx0ZXN0QGV4YW1wbGUuY29tPoi5BBMBAgAjBQJS +YS9OAhsvBwsJCAcDAgEGFQgCCQoLBBYCAwECHgECF4AACgkQSmNhOk1uQJQwDAP6 +AgrTyqkRlJVqz2pb46TfbDM2TDF7o9CBnBzIGoxBhlRwpqALz7z2kxBDmwpQa+ki +Bq3jZN/UosY9y8bhwMAlnrDY9jP1gdCo+H0sD48CdXybblNwaYpwqC8VSpDdTndf +9j2wE/weihGp/DAdy/2kyBCaiOY1sjhUfJ1GogF49rC4jQRSYS9OAQQA6R/PtBFa +JaT4jq10yqASk4sqwVMsc6HcifM5lSdxzExFP74naUMMyEsKHP53QxTF0Grqusag +Qg/ZtgT0CN1HUM152y7ACOdp1giKjpMzOTQClqCoclyvWOFB+L/SwGEIJf7LSCEr +woBuJifJc8xAVr0XX0JthoW+uP91eTQ3XpsAEQEAAYkBPQQYAQIACQUCUmEvTgIb +LgCoCRBKY2E6TW5AlJ0gBBkBAgAGBQJSYS9OAAoJEOCE90RsICyXuqIEANmmiRCA +SF7YK7PvFkieJNwzeK0V3F2lGX+uu6Y3Q/Zxdtwc4xR+me/CSBmsURyXTO29OWhP +GLszPH9zSJU9BdDi6v0yNprmFPX/1Ng0Abn/sCkwetvjxC1YIvTLFwtUL/7v6NS2 +bZpsUxRTg9+cSrMWWSNjiY9qUKajm1tuzPDZXAUEAMNmAN3xXN/Kjyvj2OK2ck0X +W748sl/tc3qiKPMJ+0AkMF7Pjhmh9nxqE9+QCEl7qinFqqBLjuzgUhBU4QlwX1GD +AtNTq6ihLMD5v1d82ZC7tNatdlDMGWnIdvEMCv2GZcuIqDQ9rXWs49e7tq1NncLY +hz3tYjKhoFTKEIq3y3Pp +=h/aX +-----END PGP PUBLIC KEY BLOCK-----` diff --git a/openpgp/s2k/s2k.go b/openpgp/s2k/s2k.go index 14f58548..d0b85834 100644 --- a/openpgp/s2k/s2k.go +++ b/openpgp/s2k/s2k.go @@ -172,7 +172,7 @@ func Iterated(out []byte, h hash.Hash, in []byte, salt []byte, count int) { // Generate generates valid parameters from given configuration. // It will enforce salted + hashed s2k method func Generate(rand io.Reader, c *Config) (*Params, error) { - hashId, ok := HashToHashId(c.Hash) + hashId, ok := algorithm.HashToHashId(c.Hash) if !ok { return nil, errors.UnsupportedError("no such hash") } @@ -262,7 +262,7 @@ func (params *Params) Function() (f func(out, in []byte), err error) { if params.Dummy() { return nil, errors.ErrDummyPrivateKey("dummy key found") } - hashObj, ok := HashIdToHash(params.hashId) + hashObj, ok := algorithm.HashIdToHashWithSha1(params.hashId) if !ok { return nil, errors.UnsupportedError("hash for S2K function: " + strconv.Itoa(int(params.hashId))) } @@ -337,31 +337,3 @@ func Serialize(w io.Writer, key []byte, rand io.Reader, passphrase []byte, c *Co f(key, passphrase) return nil } - -// HashIdToHash returns a crypto.Hash which corresponds to the given OpenPGP -// hash id. -func HashIdToHash(id byte) (h crypto.Hash, ok bool) { - if hash, ok := algorithm.HashById[id]; ok { - return hash.HashFunc(), true - } - return 0, false -} - -// HashIdToString returns the name of the hash function corresponding to the -// given OpenPGP hash id. -func HashIdToString(id byte) (name string, ok bool) { - if hash, ok := algorithm.HashById[id]; ok { - return hash.String(), true - } - return "", false -} - -// HashIdToHash returns an OpenPGP hash id which corresponds the given Hash. -func HashToHashId(h crypto.Hash) (id byte, ok bool) { - for id, hash := range algorithm.HashById { - if hash.HashFunc() == h { - return id, true - } - } - return 0, false -} diff --git a/openpgp/s2k/s2k_test.go b/openpgp/s2k/s2k_test.go index 80aa12d5..10071173 100644 --- a/openpgp/s2k/s2k_test.go +++ b/openpgp/s2k/s2k_test.go @@ -15,6 +15,7 @@ import ( "testing" _ "golang.org/x/crypto/ripemd160" + _ "golang.org/x/crypto/sha3" ) var saltedTests = []struct { @@ -136,7 +137,8 @@ func TestParseIntoParams(t *testing.T) { } func TestSerializeOK(t *testing.T) { - hashes := []crypto.Hash{crypto.SHA1, crypto.RIPEMD160, crypto.SHA256, crypto.SHA384, crypto.SHA512, crypto.SHA224} + hashes := []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512, crypto.SHA224, crypto.SHA3_256, + crypto.SHA3_512} testCounts := []int{-1, 0, 1024, 65536, 4063232, 65011712} for _, h := range hashes { for _, c := range testCounts { diff --git a/openpgp/test_data/aead-sym-message.asc b/openpgp/test_data/aead-sym-message.asc index d9967a69..6a60c94c 100644 --- a/openpgp/test_data/aead-sym-message.asc +++ b/openpgp/test_data/aead-sym-message.asc @@ -1,369 +1,889 @@ -----BEGIN PGP MESSAGE----- -Version: OpenPGP.js v4.6.2 -Comment: https://openpgpjs.org -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 -TvQZExKZfrZt -=KQtL +w0wFGgkDCwMI+dXx1CfpVvvgy6H6JtSY8VqxV/gNy/9u4vpd+d3XoYfKNhdt +zl9AEISh5t87kbVblkPu/sZHHDcd0LCQa6qqEgURdeIG0u8CCQMMe74+wWB1 +4qTfCeAosDexaXW7NioncStFuU6xTUhggx6mVVIKUJl1FYX22aXIpPcO5CLv +p+0iVfo8DcUNXjpbMz2AGJCHoAz0RNDNxveM5goGWzVNWH5Cy/yjcUbYZlqo +eJFZ93BuNSCLOfmVsixncpT/gitBam2l13K57/1PvHOgfEw8LmiOpOhEep6w +MmKPkSjz0nvU3EyE2vxidtA5ncReDMT9avtyH77+2ofB47YQccrvyhx0lEgi +OpXBxiLXBiQlh752xW5a8u1y2fvro4xnySaGKwQyVc6xZEv44h0k3l4MueMu +J8AU63WR2tTDvGcBXYirpfYS/M7nmeuQ2pS7AoR/ukwllpk6NEyUlEpx1GrB +yBNTiBZQ3iT35uYJrzvTKay86ejksnI89mZGA6ZZQoTULqaGqQk6rornHdFW +U7j2NIzu98TG+S3/Dw3vDi+MS+ONSvsplUmsqQxPRU8qdZToF95g83pJsX9T +pSa6vjNoiM1+KuJmnGbtTM4+wUZmihZOvINJ/1sNQ37zjpgC1GijLQX/I8Wa +ubwLOQtgqdq2xgg7uD7d9pIfImlGPbDMjDP9t7CpXaVdBKKqAtqCLmBZUxLA +CtWMfwgkB/hJa+UaysYvCbMUcuKbjKjFajegrgaDAxnv7Yp4s3QLIkmyC/oQ +IeClQSwESXFrGz0ljqQQxQM3RAmC36M7bgcZFdAtrfnM2JwTzsxnhefVVkCZ +w6FHm6Xx1PinlzRLJ/XqJcYr3+lQk6uG17TpDEwDETmiVXdTKneBjQpuq+yP +a8Chkn5D55Kh8LuAzPnCTrqNmXSpQxv+iGqnsWKjXjghOjwnnDxOrd4OpV0u +nlv5iuMpsuzAKotYtzTYzjDJWhSeJzpkU7FEMfKbSvzknhYdSDfCGWS/HX0p +NnMrYs5y2Xd6OZZn1rIh5Z9NPyIdOgjutT2USQgUpcPMmAh9ROdEZAfV+gMP +kc6VSKVDJfUKItLXk20w+xKoNlMYcCfptV1GUYL9MJj+yTDMsnrVu+uamau0 +cXms26IrZ9NrAaEdmW0CiUbb4Nznb0KEkVq+ZZETBj5BSk++MIFtomMgAGte +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 -----END PGP MESSAGE----- diff --git a/openpgp/write.go b/openpgp/write.go index 23a24f20..b3ae72f7 100644 --- a/openpgp/write.go +++ b/openpgp/write.go @@ -13,8 +13,8 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/internal/algorithm" "github.com/ProtonMail/go-crypto/openpgp/packet" - "github.com/ProtonMail/go-crypto/openpgp/s2k" ) // DetachSign signs message with the private key from signer (which must @@ -70,15 +70,11 @@ func detachSign(w io.Writer, signer *Entity, message io.Reader, sigType packet.S if signingKey.PrivateKey.Encrypted { return errors.InvalidArgumentError("signing key is encrypted") } + if _, ok := algorithm.HashToHashId(config.Hash()); !ok { + return errors.InvalidArgumentError("invalid hash function") + } - sig := new(packet.Signature) - sig.SigType = sigType - sig.PubKeyAlgo = signingKey.PrivateKey.PubKeyAlgo - sig.Hash = config.Hash() - sig.CreationTime = config.Now() - sigLifetimeSecs := config.SigLifetime() - sig.SigLifetimeSecs = &sigLifetimeSecs - sig.IssuerKeyId = &signingKey.PrivateKey.KeyId + sig := createSignaturePacket(signingKey.PublicKey, sigType, config) h, wrappedHash, err := hashForSignature(sig.Hash, sig.SigType) if err != nil { @@ -125,16 +121,13 @@ func SymmetricallyEncrypt(ciphertext io.Writer, passphrase []byte, hints *FileHi } var w io.WriteCloser - if config.AEAD() != nil { - w, err = packet.SerializeAEADEncrypted(ciphertext, key, config.Cipher(), config.AEAD().Mode(), config) - if err != nil { - return - } - } else { - w, err = packet.SerializeSymmetricallyEncrypted(ciphertext, config.Cipher(), key, config) - if err != nil { - return - } + cipherSuite := packet.CipherSuite{ + Cipher: config.Cipher(), + Mode: config.AEAD().Mode(), + } + w, err = packet.SerializeSymmetricallyEncrypted(ciphertext, config.Cipher(), config.AEAD() != nil, cipherSuite, key, config) + if err != nil { + return } literalData := w @@ -173,8 +166,25 @@ func intersectPreferences(a []uint8, b []uint8) (intersection []uint8) { return a[:j] } +// intersectPreferences mutates and returns a prefix of a that contains only +// the values in the intersection of a and b. The order of a is preserved. +func intersectCipherSuites(a [][2]uint8, b [][2]uint8) (intersection [][2]uint8) { + var j int + for _, v := range a { + for _, v2 := range b { + if v[0] == v2[0] && v[1] == v2[1] { + a[j] = v + j++ + break + } + } + } + + return a[:j] +} + func hashToHashId(h crypto.Hash) uint8 { - v, ok := s2k.HashToHashId(h) + v, ok := algorithm.HashToHashId(h) if !ok { panic("tried to convert unknown hash") } @@ -240,7 +250,7 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit var hash crypto.Hash for _, hashId := range candidateHashes { - if h, ok := s2k.HashIdToHash(hashId); ok && h.Available() { + if h, ok := algorithm.HashIdToHash(hashId); ok && h.Available() { hash = h break } @@ -249,7 +259,7 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit // If the hash specified by config is a candidate, we'll use that. if configuredHash := config.Hash(); configuredHash.Available() { for _, hashId := range candidateHashes { - if h, ok := s2k.HashIdToHash(hashId); ok && h == configuredHash { + if h, ok := algorithm.HashIdToHash(hashId); ok && h == configuredHash { hash = h break } @@ -258,7 +268,7 @@ func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entit if hash == 0 { hashId := candidateHashes[0] - name, ok := s2k.HashIdToString(hashId) + name, ok := algorithm.HashIdToString(hashId) if !ok { name = "#" + strconv.Itoa(int(hashId)) } @@ -329,39 +339,39 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En // These are the possible ciphers that we'll use for the message. candidateCiphers := []uint8{ - uint8(packet.CipherAES128), uint8(packet.CipherAES256), - uint8(packet.CipherCAST5), + uint8(packet.CipherAES128), } + // These are the possible hash functions that we'll use for the signature. candidateHashes := []uint8{ hashToHashId(crypto.SHA256), hashToHashId(crypto.SHA384), hashToHashId(crypto.SHA512), - hashToHashId(crypto.SHA1), - hashToHashId(crypto.RIPEMD160), + hashToHashId(crypto.SHA3_256), + hashToHashId(crypto.SHA3_512), } - candidateAeadModes := []uint8{ - uint8(packet.AEADModeEAX), - uint8(packet.AEADModeOCB), - uint8(packet.AEADModeExperimentalGCM), + + // Prefer GCM if everyone supports it + candidateCipherSuites := [][2]uint8{ + {uint8(packet.CipherAES256), uint8(packet.AEADModeGCM)}, + {uint8(packet.CipherAES256), uint8(packet.AEADModeEAX)}, + {uint8(packet.CipherAES256), uint8(packet.AEADModeOCB)}, + {uint8(packet.CipherAES128), uint8(packet.AEADModeGCM)}, + {uint8(packet.CipherAES128), uint8(packet.AEADModeEAX)}, + {uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}, } + candidateCompression := []uint8{ uint8(packet.CompressionNone), uint8(packet.CompressionZIP), uint8(packet.CompressionZLIB), } - // In the event that a recipient doesn't specify any supported ciphers - // or hash functions, these are the ones that we assume that every - // implementation supports. - defaultCiphers := candidateCiphers[0:1] - defaultHashes := candidateHashes[0:1] - defaultAeadModes := candidateAeadModes[0:1] - defaultCompression := candidateCompression[0:1] encryptKeys := make([]Key, len(to)) - // AEAD is used only if every key supports it. - aeadSupported := true + + // AEAD is used only if config enables it and every key supports it + aeadSupported := config.AEAD() != nil for i := range to { var ok bool @@ -371,38 +381,37 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En } sig := to[i].PrimaryIdentity().SelfSignature - if sig.AEAD == false { + if sig.SEIPDv2 == false { aeadSupported = false } - preferredSymmetric := sig.PreferredSymmetric - if len(preferredSymmetric) == 0 { - preferredSymmetric = defaultCiphers - } - preferredHashes := sig.PreferredHash - if len(preferredHashes) == 0 { - preferredHashes = defaultHashes - } - preferredAeadModes := sig.PreferredAEAD - if len(preferredAeadModes) == 0 { - preferredAeadModes = defaultAeadModes - } - preferredCompression := sig.PreferredCompression - if len(preferredCompression) == 0 { - preferredCompression = defaultCompression - } - candidateCiphers = intersectPreferences(candidateCiphers, preferredSymmetric) - candidateHashes = intersectPreferences(candidateHashes, preferredHashes) - candidateAeadModes = intersectPreferences(candidateAeadModes, preferredAeadModes) - candidateCompression = intersectPreferences(candidateCompression, preferredCompression) + candidateCiphers = intersectPreferences(candidateCiphers, sig.PreferredSymmetric) + candidateHashes = intersectPreferences(candidateHashes, sig.PreferredHash) + candidateCipherSuites = intersectCipherSuites(candidateCipherSuites, sig.PreferredCipherSuites) + candidateCompression = intersectPreferences(candidateCompression, sig.PreferredCompression) } - if len(candidateCiphers) == 0 || len(candidateHashes) == 0 || len(candidateAeadModes) == 0 { - return nil, errors.InvalidArgumentError("cannot encrypt because recipient set shares no common algorithms") + // In the event that the intersection of supported algorithms is empty we use the ones + // labelled as MUST that every implementation supports. + if len(candidateCiphers) == 0 { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.3 + candidateCiphers = []uint8{uint8(packet.CipherAES128)} + } + if len(candidateHashes) == 0 { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#hash-algos + candidateHashes = []uint8{hashToHashId(crypto.SHA256)} + } + if len(candidateCipherSuites) == 0 { + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.6 + candidateCipherSuites = [][2]uint8{{uint8(packet.CipherAES128), uint8(packet.AEADModeOCB)}} } cipher := packet.CipherFunction(candidateCiphers[0]) - mode := packet.AEADMode(candidateAeadModes[0]) + aeadCipherSuite := packet.CipherSuite{ + Cipher: packet.CipherFunction(candidateCipherSuites[0][0]), + Mode: packet.AEADMode(candidateCipherSuites[0][1]), + } + // If the cipher specified by config is a candidate, we'll use that. configuredCipher := config.Cipher() for _, c := range candidateCiphers { @@ -425,17 +434,11 @@ func encrypt(keyWriter io.Writer, dataWriter io.Writer, to []*Entity, signed *En } var payload io.WriteCloser - if config.AEAD() != nil && aeadSupported { - payload, err = packet.SerializeAEADEncrypted(dataWriter, symKey, cipher, mode, config) - if err != nil { - return - } - } else { - payload, err = packet.SerializeSymmetricallyEncrypted(dataWriter, cipher, symKey, config) - if err != nil { + payload, err = packet.SerializeSymmetricallyEncrypted(dataWriter, cipher, aeadSupported, aeadCipherSuite, symKey, config) + if err != nil { return } - } + payload, err = handleCompression(payload, candidateCompression, config) if err != nil { return nil, err @@ -458,8 +461,8 @@ func Sign(output io.Writer, signed *Entity, hints *FileHints, config *packet.Con hashToHashId(crypto.SHA256), hashToHashId(crypto.SHA384), hashToHashId(crypto.SHA512), - hashToHashId(crypto.SHA1), - hashToHashId(crypto.RIPEMD160), + hashToHashId(crypto.SHA3_256), + hashToHashId(crypto.SHA3_512), } defaultHashes := candidateHashes[0:1] preferredHashes := signed.PrimaryIdentity().SelfSignature.PreferredHash @@ -502,15 +505,9 @@ func (s signatureWriter) Write(data []byte) (int, error) { } func (s signatureWriter) Close() error { - sig := &packet.Signature{ - Version: s.signer.Version, - SigType: s.sigType, - PubKeyAlgo: s.signer.PubKeyAlgo, - Hash: s.hashType, - CreationTime: s.config.Now(), - IssuerKeyId: &s.signer.KeyId, - Metadata: s.metadata, - } + sig := createSignaturePacket(&s.signer.PublicKey, s.sigType, s.config) + sig.Hash = s.hashType + sig.Metadata = s.metadata if err := sig.Sign(s.h, s.signer, s.config); err != nil { return err @@ -524,6 +521,21 @@ func (s signatureWriter) Close() error { return s.encryptedData.Close() } +func createSignaturePacket(signer *packet.PublicKey, sigType packet.SignatureType, config *packet.Config) *packet.Signature { + sigLifetimeSecs := config.SigLifetime() + return &packet.Signature{ + Version: signer.Version, + SigType: sigType, + PubKeyAlgo: signer.PubKeyAlgo, + Hash: config.Hash(), + CreationTime: config.Now(), + IssuerKeyId: &signer.KeyId, + IssuerFingerprint: signer.Fingerprint, + Notations: config.Notations(), + SigLifetimeSecs: &sigLifetimeSecs, + } +} + // noOpCloser is like an ioutil.NopCloser, but for an io.Writer. // TODO: we have two of these in OpenPGP packages alone. This probably needs // to be promoted somewhere more common. @@ -545,6 +557,9 @@ func handleCompression(compressed io.WriteCloser, candidateCompression []uint8, if confAlgo == packet.CompressionNone { return } + + // Set algorithm labelled as MUST as fallback + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#section-9.4 finalAlgo := packet.CompressionNone // if compression specified by config available we will use it for _, c := range candidateCompression { diff --git a/openpgp/write_test.go b/openpgp/write_test.go index a50aadfa..324471c0 100644 --- a/openpgp/write_test.go +++ b/openpgp/write_test.go @@ -72,6 +72,111 @@ func TestSignDetachedP256(t *testing.T) { testDetachedSignature(t, kring, out, signedInput, "check", testKeyP256KeyId) } +func TestSignDetachedWithNotation(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) + signature := bytes.NewBuffer(nil) + message := bytes.NewBufferString(signedInput) + config := &packet.Config{ + SignatureNotations: []*packet.Notation{ + { + Name: "test@example.com", + Value: []byte("test"), + IsHumanReadable: true, + }, + }, + } + err := DetachSign(signature, kring[0], message, config) + if err != nil { + t.Error(err) + } + + signed := bytes.NewBufferString(signedInput) + config = &packet.Config{} + sig, signer, err := VerifyDetachedSignature(kring, signed, signature, config) + if err != nil { + t.Errorf("signature error: %s", err) + return + } + if sig == nil { + t.Errorf("sig is nil") + return + } + if numNotations, numExpected := len(sig.Notations), 1; numNotations != numExpected { + t.Fatalf("got %d Notation Data subpackets, expected %d", numNotations, numExpected) + } + if sig.Notations[0].IsHumanReadable != true { + t.Fatalf("got false, expected true") + } + if sig.Notations[0].Name != "test@example.com" { + t.Fatalf("got %s, expected test@example.com", sig.Notations[0].Name) + } + if string(sig.Notations[0].Value) != "test" { + t.Fatalf("got %s, expected \"test\"", string(sig.Notations[0].Value)) + } + if signer == nil { + t.Errorf("signer is nil") + return + } + if signer.PrimaryKey.KeyId != testKey1KeyId { + t.Errorf("wrong signer: got %x, expected %x", signer.PrimaryKey.KeyId, testKey1KeyId) + } +} + +func TestSignDetachedWithCriticalNotation(t *testing.T) { + kring, _ := ReadKeyRing(readerFromHex(testKeys1And2PrivateHex)) + signature := bytes.NewBuffer(nil) + message := bytes.NewBufferString(signedInput) + config := &packet.Config{ + SignatureNotations: []*packet.Notation{ + { + Name: "test@example.com", + Value: []byte("test"), + IsHumanReadable: true, + IsCritical: true, + }, + }, + } + err := DetachSign(signature, kring[0], message, config) + if err != nil { + t.Error(err) + } + + signed := bytes.NewBufferString(signedInput) + config = &packet.Config{ + KnownNotations: map[string]bool{ + "test@example.com": true, + }, + } + sig, signer, err := VerifyDetachedSignature(kring, signed, signature, config) + if err != nil { + t.Errorf("signature error: %s", err) + return + } + if sig == nil { + t.Errorf("sig is nil") + return + } + if numNotations, numExpected := len(sig.Notations), 1; numNotations != numExpected { + t.Fatalf("got %d Notation Data subpackets, expected %d", numNotations, numExpected) + } + if sig.Notations[0].IsHumanReadable != true { + t.Fatalf("got false, expected true") + } + if sig.Notations[0].Name != "test@example.com" { + t.Fatalf("got %s, expected test@example.com", sig.Notations[0].Name) + } + if string(sig.Notations[0].Value) != "test" { + t.Fatalf("got %s, expected \"test\"", string(sig.Notations[0].Value)) + } + if signer == nil { + t.Errorf("signer is nil") + return + } + if signer.PrimaryKey.KeyId != testKey1KeyId { + t.Errorf("wrong signer: got %x, expected %x", signer.PrimaryKey.KeyId, testKey1KeyId) + } +} + func TestNewEntity(t *testing.T) { // Check bit-length with no config. @@ -234,13 +339,8 @@ func TestSymmetricEncryption(t *testing.T) { } func TestSymmetricEncryptionV5RandomizeSlow(t *testing.T) { - var modes = []packet.AEADMode{ - packet.AEADModeEAX, - packet.AEADModeOCB, - packet.AEADModeExperimentalGCM, - } aeadConf := packet.AEADConfig{ - DefaultMode: modes[mathrand.Intn(len(modes))], + DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], } config := &packet.Config{AEADConfig: &aeadConf} buf := new(bytes.Buffer) @@ -280,12 +380,15 @@ func TestSymmetricEncryptionV5RandomizeSlow(t *testing.T) { default: t.Errorf("Didn't find a SymmetricKeyEncrypted packet (found %T instead)", tp) } - // Then an AEADEncrypted packet + // Then an SymmetricallyEncrypted packet version 2 p, err = packets.Next() switch tp := p.(type) { - case *packet.AEADEncrypted: + case *packet.SymmetricallyEncrypted: + if tp.Version != 2 { + t.Errorf("Wrong packet version, expected 2, found %d", tp.Version) + } default: - t.Errorf("Didn't find an AEADEncrypted packet (found %T instead)", tp) + t.Errorf("Didn't find an SymmetricallyEncrypted packet (found %T instead)", tp) } promptFunc := func(keys []Key, symmetric bool) ([]byte, error) { @@ -369,16 +472,11 @@ func TestEncryption(t *testing.T) { DefaultCompressionAlgo: compAlgo, CompressionConfig: compConf, } - // Flip coin to enable AEAD mode - var modes = []packet.AEADMode{ - packet.AEADModeEAX, - packet.AEADModeOCB, - packet.AEADModeExperimentalGCM, - } + // Flip coin to enable AEAD mode if mathrand.Int()%2 == 0 { aeadConf := packet.AEADConfig{ - DefaultMode: modes[mathrand.Intn(len(modes))], + DefaultMode: aeadModes[mathrand.Intn(len(aeadModes))], } config.AEADConfig = &aeadConf }