diff --git a/openpgp/keys_test.go b/openpgp/keys_test.go index 35595e6b..124d55b8 100644 --- a/openpgp/keys_test.go +++ b/openpgp/keys_test.go @@ -960,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 numSigs, numExpected := len(notations), 2; numSigs != numExpected { + t.Fatalf("got %d Data Notation subpackets, expected %d", numSigs, numExpected) + } + + if notations[0].IsHumanReadable != true { + t.Fatalf("got false, expected true") + } + + if notations[0].Name != "text@example.com" { + t.Fatalf("got %s, expected test@example.com", notations[0].Name) + } + + if string(notations[0].Value) != "test" { + t.Fatalf("got %s, expected 2", 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 test@example.com", notations[1].Name) + } + + if !bytes.Equal(notations[1].Value, []byte{0, 1, 2, 3}) { + t.Fatalf("got %s, expected 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/config.go b/openpgp/packet/config.go index f9208158..4e2b27d1 100644 --- a/openpgp/packet/config.go +++ b/openpgp/packet/config.go @@ -94,6 +94,10 @@ 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 } func (c *Config) Random() io.Reader { @@ -202,3 +206,10 @@ func (c *Config) AllowUnauthenticatedMessages() bool { } return c.InsecureAllowUnauthenticatedMessages } + +func (c *Config) KnownNotation(notationName string) bool { + if c == nil { + return false + } + return c.KnownNotations[notationName] +} 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/signature.go b/openpgp/packet/signature.go index 73c705f6..df313c75 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -71,6 +71,7 @@ type Signature struct { 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 @@ -248,6 +249,7 @@ const ( keyExpirationSubpacket signatureSubpacketType = 9 prefSymmetricAlgosSubpacket signatureSubpacketType = 11 issuerSubpacket signatureSubpacketType = 16 + notationDataSubpacket signatureSubpacketType = 20 prefHashAlgosSubpacket signatureSubpacketType = 21 prefCompressionSubpacket signatureSubpacketType = 22 primaryUserIdSubpacket signatureSubpacketType = 25 @@ -367,6 +369,31 @@ func parseSignatureSubpacket(sig *Signature, subpacket []byte, isHashed bool) (r } sig.IssuerKeyId = new(uint64) *sig.IssuerKeyId = binary.BigEndian.Uint64(subpacket) + case notationDataSubpacket: + // Notation data, section 5.2.3.16 + if !isHashed { + return + } + 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 if !isHashed { @@ -938,6 +965,17 @@ 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) diff --git a/openpgp/read.go b/openpgp/read.go index 34d324c0..537aa4c1 100644 --- a/openpgp/read.go +++ b/openpgp/read.go @@ -515,12 +515,15 @@ func CheckArmoredDetachedSignature(keyring KeyRing, signed, signature io.Reader, } // 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 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 @@ -534,20 +537,30 @@ func CheckArmoredDetachedSignature(keyring KeyRing, signed, signature io.Reader, 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 - (key.PublicKey != key.Entity.PrimaryKey && key.Revoked(now)) || // subkey 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 key.PublicKey != key.Entity.PrimaryKey { + if signedBySubKey { if key.PublicKey.KeyExpired(key.SelfSignature, now) { // subkey is expired return errors.ErrKeyExpired } - sigsToCheck = append(sigsToCheck, key.SelfSignature, key.SelfSignature.EmbeddedSignature) } for _, sig := range sigsToCheck { if sig.SigExpired(now) { // any of the relevant signatures are expired diff --git a/openpgp/read_test.go b/openpgp/read_test.go index c9d1f334..10c92b78 100644 --- a/openpgp/read_test.go +++ b/openpgp/read_test.go @@ -435,6 +435,63 @@ func TestDetachedSignatureExpiredCrossSig(t *testing.T) { } } +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 d65a3215..a5224424 100644 --- a/openpgp/read_write_test_data.go +++ b/openpgp/read_write_test_data.go @@ -239,3 +239,36 @@ ITEG9mMgp3TGS9ZzSifMZ8UGtHdp9QdBg8NEVPFzDOMGxpc/Bftav7RRRuPiAER+ =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-----`