Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add Notation data support in subpackets #33

Merged
merged 11 commits into from
Jan 31, 2023
63 changes: 63 additions & 0 deletions openpgp/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>"]

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 != "[email protected]" {
t.Fatalf("got %s, expected [email protected]", 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 != "[email protected]" {
t.Fatalf("got %s, expected [email protected]", 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 {
Expand Down
18 changes: 18 additions & 0 deletions openpgp/keys_test_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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-----
`
11 changes: 11 additions & 0 deletions openpgp/packet/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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]
}
29 changes: 29 additions & 0 deletions openpgp/packet/notation.go
Original file line number Diff line number Diff line change
@@ -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
}
38 changes: 38 additions & 0 deletions openpgp/packet/notation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package packet

import (
"bytes"
"testing"
)

func TestNotationGetData(t *testing.T) {
notation := Notation{
Name: "[email protected]",
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: "[email protected]",
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)
}
}
31 changes: 31 additions & 0 deletions openpgp/packet/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -367,6 +369,24 @@ 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
nameLength := uint32(subpacket[4])<<8 | uint32(subpacket[5])
marinthiercelin marked this conversation as resolved.
Show resolved Hide resolved
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, notation)
case prefHashAlgosSubpacket:
// Preferred hash algorithms, section 5.2.3.8
if !isHashed {
Expand Down Expand Up @@ -938,6 +958,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)
Expand Down
21 changes: 17 additions & 4 deletions openpgp/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
57 changes: 57 additions & 0 deletions openpgp/read_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: [email protected]"
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{
"[email protected]": 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 {
Expand Down
33 changes: 33 additions & 0 deletions openpgp/read_write_test_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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-----`