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
59 changes: 59 additions & 0 deletions openpgp/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,65 @@ 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].Serialize(serializedEntity)
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["Testt <test@exa>"]

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) != "2" {
t.Fatalf("got %s, expected 2", string(notations[0].Value))
}

if notations[1].Name != "[email protected]" {
t.Fatalf("got %s, expected [email protected]", notations[1].Name)
}

if string(notations[1].Value) != "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
32 changes: 32 additions & 0 deletions openpgp/keys_test_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,3 +518,35 @@ XLCBln+wdewpU4ChEffMUDRBfqfQco/YsMqWV7bHJHAO0eC/DMKCjyU90xdH7R/d
QgqsfguR1PqPuJxpXV4bSr6CGAAAAA==
=MSvh
-----END PGP PRIVATE KEY BLOCK-----`

const keyWithNotation = `-----BEGIN PGP PUBLIC KEY BLOCK-----

mQENBFzQOToBCADd0Pwh8edZ6gR3x49L1PaBPtiAQUr1QDUDWeNes8co5MTFl5hG
lHzptt+VD0JGucuIkvi34f5z2ZbInAV/xYDX3kSYefy6LB8XJD527I/o9bqY1P7T
PjtTZ4emcqNGkGhV2hNGV+hFcTevUS9Ty4vGg6P7X6RjfjeTrClHelJT8+9IiH+4
0h4X/Y1hwoijRWanYnZjuAUIrOXnG76iknXQRGc8th8iI0oIZfKQomfF0K5lXFhH
SU8Yvmik3vCTLHC6Ce0GVRCTIcU0/Xi2MK/Yrg9bGzSblHxomLU0NT6pee+2UjqR
BZXOAPLY66Lsh1oqxQ6ihVnOmbraU9glAGm1ABEBAAG0EFRlc3R0IDx0ZXN0QGV4
YT6JAYoEEwEIAHQCGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQQZ
jHIqS6wzbp2qrkRXnQGzq+FUDgUCXNA5VBoUgAAAAAAQAAF0ZXN0QGV4YW1wbGUu
Y29tMhoUgAAAAAAQAAF0ZXN0QGV4YW1wbGUuY29tMwAKCRBXnQGzq+FUDoLiCACn
ls1iy0hT59Xt3o3tmmxe1jLzkbQEprR6MMfZamtex5/BHViu2HPAu5i13mXyBRnJ
4Zvd/HUxJukP3tdQyJIlZFe8XwloMoRAA37KOZ5QGyKH8Jxq3LcAcQOOkFtWgr+Z
JbjUKF1IuqCsK6SYB8f7SVKgpZk/kqG3HE3gk72ONnqdvwOa9cIhAuZScdgZ+PLC
6W/0+IrnQIasvKeEWeK4u6/NYT35HUsUE/9Z6WKF+qxJnp5Pi2Q5cio6bFlGDNQb
+MiuiEb3Mzb3ev2PVg7WELBRXOg8QlCxrghqfi1SH791mmiyGK+GIQgnjRwMejTh
dNsnHYag/KAewag55AQvuQENBFzQOToBCADJD+auK+Opo1q3ZLxODMyw5//XHJH4
0vQPNawyBiOdBuneWHF3jfDwGa+lOftUx1abSwsq+Qs955THgLVSiJvivHWVy8pN
tPv0XLa9rMj2wh/OmckbcgzSMeJJIz09bTj095ONPGYW2D4AcpkOc+b5bkqV6r+N
yk9nopPJNCNqYYJtecTClDT5haRKBP5XjXRVsIXva/nHZGXKQLX8iWG2D5DOJNDP
ZkAEoIPg+7J85Q3u2iSFPnLPzKHlMAoQW8d9RAEYyJ6WqiILUIDShhvXg+RIkzri
wY/WkvhB/Kpj0r1SRbNhWRpmOWCR+0a2uHaLz9X0KTP7WMqQbmIdpRgZABEBAAGJ
ATwEGAEIACYWIQQZjHIqS6wzbp2qrkRXnQGzq+FUDgUCXNA5OgIbDAUJA8JnAAAK
CRBXnQGzq+FUDgI6B/9Far0CUR6rWvUiviBY4P5oe44I9P9P7ilWmum1cIQWxMyF
0sc5tRcVLpMomURlrDz0TR5GNs+nuGAHTRBfN7VO0Y+R/LyEd1Rf80ONObXOqzMp
vF9CdW3a7W4WicZwnGgUOImTICazR2VmR+RREdZshqrOCaOnuKmN3QwGH1zzFwJA
sTbLoNMdBv8SEARaRVOWPM1HwJ701mMYF48FqhHd5uinH/ZCeBhqrBfhmXa68FWx
xuyJz6ttl5Fp4nsB3waQdgPGZJ9NUrGfopLUZ44xDuJjBONd7rbYOh71TWbHd8wG
V+HOQJQxXJkVRYa3QrFUehiMzTeqqMdgC6ZqJy7+
=et/d
-----END PGP PUBLIC 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 (not *Notation) getData() []byte {
twiss marked this conversation as resolved.
Show resolved Hide resolved
nameData := []byte(not.Name)
nameLen := len(nameData)
valueLen := len(not.Value)

data := make([]byte, 8 + nameLen + valueLen)
if not.IsHumanReadable {
data[0] = 0x80
}

data[4] = byte(nameLen >> 8)
data[5] = byte(nameLen)
data[6] = byte(valueLen >> 8)
data[7] = byte(valueLen)

data = append(data, nameData...)
twiss marked this conversation as resolved.
Show resolved Hide resolved
return append(data, not.Value...)
}
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 _, not := range sig.Notations {
if not.IsCritical && !config.KnownNotation(not.Name) {
return errors.SignatureError("unknown critical notation: " + not.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-----`