diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index f8da781b..2158b70d 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -1045,6 +1045,46 @@ func (pk *PublicKey) VerifyDirectKeySignature(sig *Signature) (err error) { return pk.VerifySignature(h, sig) } +// VerifyUserAttributeSignature returns nil iff sig is a valid signature, made by this +// public key, that uat is an attribute of pub. +func (pk *PublicKey) VerifyUserAttributeSignature(pkt *UserAttribute, pub *PublicKey, sig *Signature) (err error) { + h, err := sig.PrepareVerify() + if err != nil { + return err + } + err = userAttributeSignatureHash(pkt, pub, h) + if err != nil { + return err + } + return pk.VerifySignature(h, sig) +} + +// userAttributeSignatureHash returns a Hash of the message that needs to be signed +// to assert that pk is a valid key for uat. +func userAttributeSignatureHash(uat *UserAttribute, pk *PublicKey, h hash.Hash) (err error) { + + // RFC 4880, section 5.2.4 + if err := pk.SerializeSignaturePrefix(h); err != nil { + return err + } + if err := pk.serializeWithoutHeaders(h); err != nil { + return err + } + + data := uat.data() + + var buf [5]byte + buf[0] = 0xd1 + buf[1] = byte(len(data) >> 24) + buf[2] = byte(len(data) >> 16) + buf[3] = byte(len(data) >> 8) + buf[4] = byte(len(data)) + h.Write(buf[:]) + h.Write(data) + + return +} + // KeyIdString returns the public key's fingerprint in capital hex // (e.g. "6C7EE1B8621CC013"). func (pk *PublicKey) KeyIdString() string { diff --git a/openpgp/packet/signature.go b/openpgp/packet/signature.go index 3a4b366d..cf09b202 100644 --- a/openpgp/packet/signature.go +++ b/openpgp/packet/signature.go @@ -1033,6 +1033,25 @@ func (sig *Signature) SignUserId(id string, pub *PublicKey, priv *PrivateKey, co return sig.Sign(prepareHash, priv, config) } +// SignUserAttribute computes a signature from priv, asserting that pub has +// user attribute uat. On success, the signature is stored in sig. Call +// Serialize to write it out. +// If config is nil, sensible defaults will be used. +func (sig *Signature) SignUserAttribute(uat *UserAttribute, pub *PublicKey, priv *PrivateKey, config *Config) error { + if priv.Dummy() { + return errors.ErrDummyPrivateKey("dummy key found") + } + prepareHash, err := sig.PrepareSign(config) + if err != nil { + return err + } + err = userAttributeSignatureHash(uat, pub, prepareHash) + if err != nil { + return err + } + return sig.Sign(prepareHash, priv, config) +} + // SignDirectKeyBinding computes a signature from priv // On success, the signature is stored in sig. // Call Serialize to write it out. diff --git a/openpgp/packet/userattribute.go b/openpgp/packet/userattribute.go index 63814ed1..270e57da 100644 --- a/openpgp/packet/userattribute.go +++ b/openpgp/packet/userattribute.go @@ -9,6 +9,8 @@ import ( "image" "image/jpeg" "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" ) const UserAttrImageSubpacket = 1 @@ -24,6 +26,29 @@ type UserAttribute struct { // NewUserAttributePhoto creates a user attribute packet // containing the given images. func NewUserAttributePhoto(photos ...image.Image) (uat *UserAttribute, err error) { + var imgBytes [][]byte + for _, photo := range photos { + var buf bytes.Buffer + if err = jpeg.Encode(&buf, photo, nil); err != nil { + return + } + imgBytes = append(imgBytes, buf.Bytes()) + } + return newUserAttributePhotoBytes(imgBytes) +} + +func NewUserAttributePhotoBytes(photos [][]byte) (uat *UserAttribute, err error) { + for _, photo := range photos { + //check jpeg + _, err = jpeg.Decode(bytes.NewBuffer(photo)) + if err != nil { + return nil, errors.InvalidArgumentError("jpeg data err") + } + } + return newUserAttributePhotoBytes(photos) +} + +func newUserAttributePhotoBytes(photos [][]byte) (uat *UserAttribute, err error) { uat = new(UserAttribute) for _, photo := range photos { var buf bytes.Buffer @@ -38,17 +63,15 @@ func NewUserAttributePhoto(photos ...image.Image) (uat *UserAttribute, err error if _, err = buf.Write(data); err != nil { return } - if err = jpeg.Encode(&buf, photo, nil); err != nil { - return - } + buf.Write(photo) - lengthBuf := make([]byte, 5) - n := serializeSubpacketLength(lengthBuf, len(buf.Bytes())+1) - lengthBuf = lengthBuf[:n] + encodedLength := make([]byte, 5) + n := serializeSubpacketLength(encodedLength, len(buf.Bytes())+1) + encodedLength = encodedLength[:n] uat.Contents = append(uat.Contents, &OpaqueSubpacket{ SubType: UserAttrImageSubpacket, - EncodedLength: lengthBuf, + EncodedLength: encodedLength, Contents: buf.Bytes(), }) } @@ -60,6 +83,14 @@ func NewUserAttribute(contents ...*OpaqueSubpacket) *UserAttribute { return &UserAttribute{Contents: contents} } +func (uat *UserAttribute) data() []byte { + buf := bytes.NewBuffer(nil) + for _, osp := range uat.Contents { + osp.Serialize(buf) + } + return buf.Bytes() +} + func (uat *UserAttribute) parse(r io.Reader) (err error) { // RFC 4880, section 5.13 b, err := io.ReadAll(r) diff --git a/openpgp/v2/keys.go b/openpgp/v2/keys.go index b4a7cc1e..eb29ccf8 100644 --- a/openpgp/v2/keys.go +++ b/openpgp/v2/keys.go @@ -31,6 +31,7 @@ type Entity struct { Revocations []*packet.VerifiableSignature DirectSignatures []*packet.VerifiableSignature // Direct-key self signature of the PrimaryKey (contains primary key properties in v6)} Subkeys []Subkey + Attributes []*Attribute } // A Key identifies a specific public key in an Entity. This is either the @@ -503,6 +504,10 @@ EachPacket: if err != nil { return nil, err } + case *packet.UserAttribute: + if err := addUserAttribute(e, packets, pkt); err != nil { + return nil, err + } case *packet.Signature: if pkt.SigType == packet.SigTypeKeyRevocation { e.Revocations = append(e.Revocations, packet.NewVerifiableSig(pkt)) @@ -592,6 +597,27 @@ func (e *Entity) serializePrivate(w io.Writer, config *packet.Config, reSign boo return err } } + for _, attr := range e.Attributes { + if reSign { + if attr.SelfSignature == nil { + return goerrors.New("openpgp: can't re-sign user attribute without valid self-signature") + } + err = attr.SelfSignature.SignUserAttribute(attr.UserAttribute, e.PrimaryKey, e.PrivateKey, config) + if err != nil { + return + } + } + err = attr.UserAttribute.Serialize(w) + if err != nil { + return + } + for _, sig := range attr.Signatures { + err = sig.Serialize(w) + if err != nil { + return err + } + } + } for _, subkey := range e.Subkeys { if reSign { if err := subkey.ReSign(config); err != nil { @@ -627,6 +653,18 @@ func (e *Entity) Serialize(w io.Writer) error { return err } } + for _, uat := range e.Attributes { + err := uat.UserAttribute.Serialize(w) + if err != nil { + return err + } + for _, sig := range uat.Signatures { + err = sig.Serialize(w) + if err != nil { + return err + } + } + } for _, subkey := range e.Subkeys { if err := subkey.Serialize(w, false); err != nil { return err diff --git a/openpgp/v2/read_write_test_data.go b/openpgp/v2/read_write_test_data.go index 2f0efc22..972d8461 100644 --- a/openpgp/v2/read_write_test_data.go +++ b/openpgp/v2/read_write_test_data.go @@ -740,3 +740,326 @@ NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91 xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE= =miES -----END PGP PRIVATE KEY BLOCK-----` + +const pgpPhotoPublicGPG2_3_6 = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +qzSeUQEzjQ7WX5tpXrluTLiyTVHejMy0T9JGp6TUuRldOnJNByP5hSuU5U09RZx4 +0RqG3wwNokxDZVwMJR9jBGMfdjxgGMZOcrK6gun04t37p9ms06babp4lhqqXKn6T +vkyjZsm35T8ZuUl3TcVwWWnXGiNBtJcJBFWWT9a/Q6jat0dmooxJW4nB8P8AJtK8 +mX3/AKBkEf7P71vJ6aknPclFdHwu3tuYmK0qS7KsSqommW/pjX87Y6+I9zWWn0uv +T6dmaiZpxSTOA6yEqXHPT2rUOklRLLhqJGRJR54YBKNIaXv4mP8AzCZ50q9Xy2FH +1sudJ5CU5WqOlNPCo683XLu46NnTNfzXLe+cW4nUpRl0cmS9HcfbZQlcWzamaajy +G5tBIYXS3zlbIiWSH+F6jKJKfKuJkIyti88FA/8AFAxul6ql4dZIokVw1OHTVpET +Dar2ynTEV8aBFdupttYJqoqHFENOVe9yTbohtTbmXqCVVyY+m6ys107Jl1uqYtk7 +0NdZntb3I/zbtbmpiMKcLzx+DSrKc8HC86fZ2ptH+us5bnOeiRuokXb/AC0VkYq2 +HMfcu57aGqSA7IXNk1FLzA/KREas7Ryzep61Faxd2yrKRvNCOrOJF0cZU1X9akrk +V+/oZze5FoxsceT2GGF/aRhktzhlk1H9Ppyc2AmdsamQTZVbP1Cfd2O5NFWFBj31 +mcyRQ0+8nVkw1ZWLlk9SVaTkahtjEeGmHEJaluU1aUp+5uevVper+rXIkp8XkfzI +QFdiFbVpMRE+Y7XNf1W5shbN7tg3yW5H4aqOr6dTUkXqpmobLpWaCqOfIUtMdubN +cu5T1O3Frn3UUlbVsHPmzJB2kzl86VKlm6lKCUv06ojgV4cLJW7GW5aMG4XYYaUI +SfwVDJKl6nc5rVazzZVr91nZJ/PfOYKpaKJDcNy5sIcNFfF1Baqfd09VdDGsZyK5 +p/m2UjepYdb6GM/EOA3NrFVddpnQpLmERJILE5rmN2TfLdeLthpSpa1oS3AY6WHc +N/0qFrMh/wDLbvp33slXWWt9K5bela0m29R2vSN6bpjeXMlt17MiQqymSkKNirp5 +aU1mh3lJl6AmPyf43FdUlJJ4rSHUeL6vWapCDSZ9n7oI/Ocl4QzdY6Ci8FH/ACW0 +xfJtoLyWXsqeWzITXVMOA5Yy4UCU6iZoybIfo9HtxR/GYfMaittpSW3uNIUzkPRC +WVrpdMgp1Q9DWaTLhgGQoFlvnfkYum/+PoWvwVLfMsbeEuTMbo5TztPoyVPW5oje +INMzEQhBIBkRjHdkZGeGAaMg2CMSKtqSmx0MhwSdJSmTdqJDJ9C4KlDkSTJffcSi +lsZ6Imi5raavQsaKlOnIaRHqo8cISSBnhkZ7s4CleVOeDX3rYS4F16QUGOYZrY6T +KM2CbIgRAuOe3IyMjI3A1hToNRmaG93s7SUCbwMqIcwcwEobhkZGRkZGRuG8G4Dc +G8eTMmchLZECLjgY9jA2jaMGPIyY3GNw3GNxjyNpgmzHKBNkCLuIh8d5d+BgYG0b +eOPYSD71y2m1FIQYJZAj4ZBdiQZYP2C4J78B2G06HyhJknXYJiS27LS8+hxye62t +Fv5TatmTUxp3gkK9kjBeymQ24NSMckRdQNOwtOuf39SfjkJsnW7FCSUXToMIitpV +gF8q+fYakkokObhnvwHmEuJsyXIXGreUvkodTNjPOPaagdVIQWOz9n7P/8QAJhEA +AgIBAwQDAQADAAAAAAAAAAECEQMQITEEEiBBEzBRMiJhcf/aAAgBAwEBPwHVarRI +YkMorwjHuJQ7fooooZRRBUjklh9xKKKKsoo7SihrVcCIyMmPa0fIlyfMiOeCHmg2 +LLAUovRjQ5JcjzfhCTGzHL0Q32M3TfJuuSeKUP61ewnZ3NCzTQup/SmKItFsY5Cd +ksakqZk6L3Anjlj5LUhc1q9Fz4Y+SEu1i/1pLGpLczdD2vugOKiK+7R+WPmxbo3i +LL+ikpcHJn6Xu3gOLUt9GLRb6w4HshS/SO5wLI0RmmZunjl/6ZMcsbqQxaLWC4Js +/pi/RS9mzKQm1wOUZrtyIzY1B7C0QhESTsih77HLpF0X7Ezus6mV0heEThCWj2Qv +8UUN76Tl2rclLud6LXG/Q+CtFzY2uWSyoeY+aRKTlyPROhSvTG6Y5oeWI8yHlbG7 +8X4WWX9LH9Vll6PzooryflHT19H/xAAoEQACAgEDAwUAAgMAAAAAAAAAAQIRAxAS +ISAiMQQTMEFRQmEyUnH/2gAIAQIBAT8B6nqixFl9GXKsaMeZZOpaIvRF6ZZb5HK5 +Mea+JFlljlQmWbi2WXoyUamSj+HghP6ZsbPbZl9POa4IYZRVM9uRtZyKzk8igTih +Iyw+y6MebbwyM1Pxrmn7cbMXqdzqRSZsiz2vwTL1qzJA8cEZuL4Iep/2Fki+UZPU +Rl2skql2mGWST7tFo+jIuCSsv90U2hbZcM27FwjBGW+xC6snKoapm3cbTx5LI5H4 +bMcXdt6J6vRD8kFuY4/xRkj9IqzahpmPM8f/AAhkjNWuub4ZiVI4iiX59jjt7UOO +1Ci5GyzbKEriYpuS5Fq9Jc8EVRJ8i7e9i7Vukc5GNfxQkvA426RCNdMuELmRfNH9 +C7pX9DfuSN1cRFHj+xMg19C6ci+yH+RfL0l2x2oqlUSGOjYe2hRUfGiKK0ycohF2 +bHyRhI2IrpQuivisXzrqsv4I9LLL+D//xAA6EAACAQIDBQQJAwMEAwAAAAABAgAD +ERIhMQQTMkFRICJhcRAUIzAzQEJSkXKBoQU0YiSCscFDYHP/2gAIAQEABj8C+W0h ++ePvBRoLib/iPSqrhdTYg/OU6S6ubRVtjqPxv1jUXoip3eK2Z/eVaCXKLoT82201 +ODZxf94GGSgw311EfeUUqDniEq+q08Gz/Tb5kLzJtKuxhSK7EEt90xGawrrRq6+B +ndtaodDDgWxXiX3FgLyzC3vwekX9Iip6NZUpVSSL3RjDivfQmYkybpM8u0hIB84x +Fr+HyFEsbAm0FiNL6zvNaDkoi53MbCAWtzhGLw8onraFqf8AjAaXtMr2xT+3E/tl +n9qk/tll0ogGEvTzPjMsa/vO5XYec9ntI/cTu4KnkZ3tlY+U79F18x7n2VBiOsov +XYY30UTvk4wPSLnKAhoa9MZ87CHFpDXFPe3FvKCoBhbRh7gVawLXNgo5wLVpPRU/ +VrFqU2Do2hE79NW8xO9sqea5S9Gq9I/mE0GSsPxPbbNUXxtM5ho0mc+EDbS+7H2r +BaiGb7mzlgLRaQ0pJAfTeBXMuTC1u42hEsM5em5pt4TDtiYrDJl5xWpsDcXt2tmp +fThv6F2es19lqG36D1lx2LMoYeMOPZwrfcmUC0kVB4ekmbTW5M5t5S/YBEAJm6fQ +5X6Qqwz6y9s5ZhN5RPmDzltrTdN1Gd4HpOHU9PTd2Cjxi+r7RTbaKfCA2sK1Vsw1 +BnUcjEpUtmxhAFxOdY1NqRpV1Fz07OTXMzEzlcJxlbD0HsiawVDmFFjbUeMYA3HW +HFkOU8J4S+z1ig53gXa0/wB6SptNAithXFZYz1WYg6KNBLglWHMQUq5FRgeM8UVV +OXWU/XduuSL4aa3l/wCn4Sp1bn2dfRr6T2biAQ9CIa6r3TrblOondOXY9jVanfpN +3Sws5zF4Hag6tzsModo/qVR9m+oL909c2imKdJB7GgcyfEyszIEvmoXlFOL2b8Sx +aiHErC49+qAXJOQiKWGOVKZ+oWvLPZjfIiW0b/KNfXxgI/Ho6zEO6ZVoK671+Fqm +doau2N6zVXQse7MVIAp0WF6tFgnWUq+0ZUbXsOcWnTFlXQe/rbU30Cwj4u8qLeEH +IQYlDrygxLhcfUIrUFLpoV5iaGZ5zFT7wGqy/PpAecFFDhI+qZ1WlXZq3tMQ4jML +X9m1vkEXnUN5tlY+U1lmN16GChstJm5YlGK/kIr1Qad/vGEyzqHUxm2cjCfpMbEh +U+MueLwl9Jp3uUGNBUHNGi1qVFfFekFOigRByHZsRbsXBz6TOHshOSC0c/cfRuKR +zbW0au+VYjNugi11qELTPdQ6HzhG4ahTIz711v4Q0Us9vqU6SzqGnd7phI7y+E7/ +AAjmZdefSCrRqZ8xMLez2gar17NwPd+co0usPWb19Z6vTPd+oxUXnE2Wgb1j05Te +1c67635Q7M11Gm9AvnFYtiNtdIwBvlLW3bD6hCKdPEvWMSPDOYsRDiLs21G1QZB+ +supuPceEH47BlNfGUU6Cf4CYV1luc37/ABW4RPXNozY5qDMCH2rfxPWaoyHDeLsO +y1MJPG45RV2XFlkamrPC+2EKzcK9BMbMFXqY2EpWTQ2lkJptLkXUaMs3dR2FE5db +QBu6Tp4+4t2BMXSMo14RDUOpmI6mesVuBc5i/wDCmkxdNBC78OpMFKj8Q5KIwv3m +4mg2qqLIvAp5zG7WAgoqxFInJI2I+LHrHeuq09jGp6Qot17uIluUZ6ZVzzwGJSRu +9T/iCGHs3g9Npijnligpj4aQDlE2Ojq2sF8m1MCJwA2E8hn4mNWbNjwjpBUq8P8A +zLmygTCOC+QgqP8AEI/E3NM+xX+YF/CxqCfEqfEP/Uq7VXyL97PpK+0iowxE2/eb +PvPibtcXnaND2rdPQPQz88EL/VCx55Q1W0TOVNpbhGYnq6cR1tN849o38TdL8ND+ +TBWrDLkIToBMCfDEFQkFEznq9I5nWGvXGXIfdN4fiHTwm8q/Bp5sTzm4pHDs66nr +Nk2YZ0lO8fy9N/cMfCIvUzdiKOU2elz4jFUa1WhqczBUqZ54jMCcbTG/w11nRRBs +mz5U/qaVEopeoRxc4FPxTym+rcC95pl8FMlEFJOuZi7FsmVJeJusy+Gv8xttrC1b +aNPBfSYe3nzMB1wxyNL2iDxlvtAmzUftWUNmHLMzetllimWeI2URaa8tTPV6J/UR +Mbj2rzG+vIRqj6a/pE3VPu0+nWJs9IYtpqa/4xKI+PV4j08IihDjqNhdunhFqbWP +9PTAIX7jABkB6TGHaAd8C84op5jlGf6zHl+glT9VoB0Kx/1YRE2Sn/uh2hh3jks3 +SH2rfxPWawuOV+cL1NeQ6zFUux5KOU3NFOI3aB/Uxfkxzh2hqo3755iUiGVcLZte +U3rrvmXPvaXmXZLgTPsiKX4ViDqY8qHyh/8Ap/3Hc8rGb2pyBaGvUFzUOUFRtQuS +9Zv9oDNc3sBAtDZt2vV4XqNvCZirKGaY92JYKLe5sReE08mhDoR2af6ZSPSOfKVJ +flivDuF3l15QYqJFopcbtF6xMVUvbrAoQZfIYXQMIWo93wmS3mdMzgMDYGZeYmCj +Qa1tWENM3wnqJhFXCDPaXdp8ETu0wJkPlMxOENM6a/iXFNfxOETT5vOZf+t4WcA+ +M4h873kBgo39q2WUFqrKBDgrhsuEQbwphvPhErflDipOMr3tM7iLhbi0+WIDZjlE +2lMnBhaocLjUSocsJ6ylUV+99spb2od2c8I5ejhEBCgH5Y5fvNxWJwodRqZjJvY2 +A6zEE5E4k5RmuSoF7t0hrVO8F69ff//EACcQAQACAgICAQQCAwEAAAAAAAEAESEx +EEFRYXGBkaGxwfAg0fHh/9oACAEBAAE/IRa3LZ3K4qVCbSs8EzygvMyFz1THDEHc +e/PJQaQksepUrjYl4+EP8TgIGZ3/AIgMWEWIsy4uBlFsyHHUSG/8HUeKl0B2uvZi +dZwa474d8bQuZMOQuCKidpk4M1AlROB4qK9RS/MWpQWGYyoJTgJxWUc5B4ripU2g +wWyoJcYrMcaERiSokDmoRU0hS6u9X1/M0RhVdSyRlidMWwlm7HDQ+DWfzyHAciyW +zFlfSYQuTWWhhTGd4vriZHARw6BBZaBLdgzqPvlYiDe8I2FpBW7JbhY7JgJs5ggS +oKXSwVeosYvDDOI1g/aUhvDHKkiVqI+I3LS4y2XxSu1ctjmVNuAzH3QKHJKh6B/B +5Q9DIN+4qEqummVIq98hNQWT4sJsgw1brjlVSKpA75isv7gdPCkMK4Ek7yCY9iKa +7VOwK9QJF7TDBIRsPaRQa3g7gfHYt5lOKfIE7g+XjM/8CBaP6cTiUL2JH6TR+NdK +WV6leYkL+mOzcvhk8sQ/1HJuWmYTKVEgzgt9QFbOxRCQVbOSu4xSKFrdktL3Etss +Jt7mXcmpgI9YVc4fiU8VYumKoi5OmEIECBKgRyh3eopwKvQPmFD612MoQ77YU1F6 +LS66CNJUutF2jLWHSxFoBPmEFrwxKg/lGHIl7UqTB0EudhD8uf8AU+MQErEWN0Nk +/NHCGBWYFc2ixNOGsSkG+dT6svnyldXqHJwDBhBFtdWT3c31jT6YmjHl/wCCEaWO +SBAhK1A6EMt7otBynoVAQk7GgLZQefoxg/Uv4uHhLidR7OxLRmsvsZTJGQmIjEew +xdJGdNqsoHiJq9VYUAWyxUB4lZQb+3UQLPofohZGUOmIIZch4gjNiWhuN0uoyz5h +BBwHoHol/AnzOofvGoR/yMuSYQcsrmNOiLghmXHQw/lC7gKG44CfQ2xYu7gitYP9 +QFh+kCx6MP8AqgL1Q2WjUbajb/DGZfWJSS81MPTxcY5a4tqW2Dr9pPEhZt/MCHLu +E7KU9oXxWqmAS0oMcGEgJKYzLKE+hElObzJbml1ExtWzqVUzivkhSKydnUw/7RY0 +Sm/UKIO2KFl6feypju9PY9SiqU/9zY5qfRDxL2DFdh9wFFoHibTEow5qVXNE2PAX +4WakatXBFhKZDpmGD6BOrYCi6QxDTU0Z+Dr1EtcOWFKxQ9PUXhVJe/Q1UaoS3Ucs +/ronwTx9NdQEioWalpbAJmBymoJ44Km13wEog6TIwzFCvDl4jx+xY9f2HpjUlnXU +dEmUlk8W92TeUaT9Qj8uR7MzNILepXLuOyZ0Que0pi6S4iMWzMS50wHNIDU3MIV7 +N8MoEIsRyhCmAeQbzkNTKoheEOvEaluhHT5SnAN6Zn5NkJfonV2B+MmynpujSyn4 +nk6UV1g18mC5YLln8CdzDhtaiSyiTO5fhmtigypXBvElgRxHhZXq3AcfhChe4SYF +kap2ygtCfKf88+JBDNT/AEiJFg32p3diUPT2QG2Vf6S6jSgG7Ajp+8Wl0rMcMz93 +DTAy10eY0NCgMyxF7X5TwwNGH0kwa3V/CVKlQJhkWXcYwUU13BD4m35IRUqgwXrM +t7FCew0v7Q0d8EqM6bzEFtNTv1DYVUrzKr8ncbTa0tfErAHh+EwThLY5fSVuhTNV +CrOUD+44L4ksBewaqCClpSxJv/Z6Fd+4fI2kZUCbVShR4bR2mKD7IHEIcTB5Y1fu +T8gMag2wO34A6hoLUygAnXfE2f8A8hOrjR6eYuXN8j5jiHZGsQomfbvq9RY1WO/d +7iAU21EsvW3cWdPI/uGzqoG/maYoXsmAw7XUVL9lSgplQMS0iW9OppDcWIPqTNtT +frAA/vzPRvRDn1xsHsdy7B63qIkVWj8y3vL0jxBuAKvR5mcX1L4iW2L7DzEdA+ZQ +AqD+5UZQXNqXC5wZ8JRCDoao9yiObd8Kd47zpFb90TWZWJTwOHENjcukyYltQaAw +Ylm8W3FCcVX1ZfGsMr8u00eox8RMyC1gdrUT9xatBc+4BmprwItXvdP94jxAsHiW +0dAdyoDIVi2G/L5eY280t+f2xQGSw8dQs9H4DqWwdCPa/wDZk3eQ+C5XJoMpWMMP +AxLrN8LI9b4TrjAfKRfKWnz/AF/ETZmW8swfld/v1lT3vPwRl8auhBNILL6S0d9S +D1p3e406PfxEaJ+b3MmJXthQ8xnjxMrRv2vEWZNboxg1e4niN4LzUUUcv+Gf78yq +MS+0pRJQsFPF3xVeHHFZ4NJSJrJV4yrf1/uXK6amM6ol7i1ah9D/AJ+Ya2M578TX +mWXuWqVhK6lIMlp7nVCRIWz5D/UpwQNLURC8vkscJ2Dqpil0nuDfbw9f8J4AAd+2 +U6z2+c6tJpkdff8A1GX5NX1xqh5IZYEG7hklA+yXKun7GYZwiZ/QX8zB+nHt/wCT +o+P4j+YmwrJZjtXpBMa1b8j5iC4XX6E6yNt9Efpb+Vl2ayy1HvrOm/dhsA7ZlD18 +z0Sx08JVyan4XSI7wb67b9Q2wFAdRZrBL3UdXGZk5qMDzZk+IgGCqRnTD+5X9J/M +G+b3+/eGnuz+hGQ0D6biUMlPiMf7lmtg18eJ5xwdECtOD4QSkWz2eY/WnvURXl8I +w6vY1v1DSo0YDoyPfEXFi2xLUFhtoN1KAFBLl8DKnyscRUCCVxUqm9wCGKy/MOin +8Mo8uP3NF6EFvy2P7aBLBfmX+sGYdgjdstrxKWu5YX1AGCUdH0nkbC9R9bMZJiZR +ARg6qBgYly4Fi+JcuW8R9TeyDH6oWA81FMkqHBEmwX3iSSrXP7R3Mw+onvlvvCFE +Yuo/MpQPedHyUlimYzZbjdQ6gCdJKE6g801iDLjwkygx0EZqHkl0bQytr4lIL9IH +v7Mv9OBIVwRX8SDz28BEnY1MP2VvOILR9mfqegVATIlw5DkYXQLVApjUOoQ4OKuB +YIxtL7I9Rn3FEd9CE/1TTggnG5cIGLBlw/xFXKvnjCbGJUri4QhCOkGDvSHsy8pU +N75iDhOGpwCCRbDzomzXC1SoHAghBgwYMuIYl9S3meqfVCCZBcTHOCx8L7hOrlMC +BCECWygSpUrgg8LgweaHhTxK+IRjuUSoQECBKhCEGOFSpUrlQJ9Kv7qP4f6xGkZb +BzwUuDLgt4g4IQhCHAOCZlSpUqVwsFl7SKJGpe8/9hJqjT0S85KjdyhUleeuoyHE +LyxW4IABnYfEKIvUyeC5vQ2+VQ4GioQISoSpUDmDLuVKlcBHBFBtD2hJ2Ls7hYw0 +O34gpqC7Z3EX52v47iGoxP26g9oRn/moKMaQhBqLsEIQhAgQIepuAIWgw4qBA4GD +l4bIkOxhu3Uu83IMODGRkexuAsXL66StvS8opBAgZlZgVBySoECBAgQOP//aAAwD +AQACAAMAAAAQ34AWULtuWnY3L/IqNHga6QfIezmVEPQl2IJZrEoXP5buU0J259YK +GRTfCP8Am847bkGNpGCciNkV1235955Qbi+RCGzWtypaLWg/PhxxgqA6VbS+DTtC +XGmLlhUjo46BCq/TPF31KSQuBmVLoWwGvmah4GCR1wG1P3G4Maxk8DfKDYRrR+uo +nyrhwB1He2uSxCTEW7HUPYggmsvW85q2h9raaPD2cccvGk7t2rZN/XeL8MW+4NYG +zDquGWee+y1z6rvWBanmD+x/m0hv/8QAHhEBAQEAAwEBAQEBAAAAAAAAAQARECEx +QVFhILH/2gAIAQMBAT8QPLLISWQxnuAh1lh5BeR8iy3fyXtZZBZBJsGTHgHA15BX +XRsvscTiPysnUUFnycwsR13Jak9suoFH3+S3BfxvcLcSPt4yQDAIL018DYzft3Wn +aedewqieCzh42+yPE30dvwy3cihDEJ7lrbXuI7ZJ2+xKdZ0E81hVyeORh7ZZLMya +Plh7mWWE/wAH8gMPYPtJGDY6Y9ng+nyRowtozEA7SY7gGXf/AGZYyeK04yNmyHid +ZizHZnXoyffDB6dfqxBC6RdHqHTbLsFo5CysNeWnbyEO+D1k5gQfDPqOD+Q1sit4 +rAz9tPPlv8CcYeannCNh8njEQ93dvW6NjrWXUesDugLrafxd7dhNhXjPnaDJ0y6P +YbratIZfhBdS2rhuyuH1CQ90U9gdzfC8KX1bLbN4t7vY6hEXT9n/AAzZP9nCOCLY +bbbSY8C7weDggGNeWrU9PCy8kx4Dn1YAhYHaVl2eWS//xAAfEQEBAQACAwADAQAA +AAAAAAABABEhMRBBUSBhgXH/2gAIAQIBAT8QYbZdtt2WkcMrcOZjC1coxnx+wXFH +flY4l24SskibLwXJv0x0oyd0xjDHcqbLtRtDh3Eh4jAtCZqfRNPtg0bb3IBxeEXq +R7Hw07hmPS+kh3Lg6sOM/wAb9cu+eVUzRoOSfUuvKzk5Lo7Bk4Y2bkK9WgVpxCdI +BDhmPXDYlGRdIlxerd8EtsfHcLrtbI8N33GXrt+4f0k5zDi2es8EF/QnULg6nHUc +uFjeL4BH5Qhsi7iCMPh7rQt0/rAAXM7YG225C7nvvxEliCGTZMYsGl0s1vc3p2sH +tY+S5j1OO5Ajc32T8XcNtggY2216LgexiN2MH6NsliMWd/S5rHE+M0ycLGeCPOfX +MOZ0myOiUvlZP6gDCZBhB9jzl2loZ5dymny2HYwvfMy65hvbauxGCDbkTFW5B4bC +chf9QDG2xYB6jx2RG43fhBsQBDDb4G22IweA/hn4ZZB4IZ7e4fGlqeGLR9xz57iJ +cQ7blts+LW6wl4sIw8f54Lb/xAAmEAEAAgIBAwMFAQEAAAAAAAABABEhMUFRYXGB +kaEQscHR8OHx/9oACAEBAAE/EOcRs3KpEpuJZDlKwIYalqZYyDeoZWA3UNtNPaM5 +e8OWXHMAbbekym3YQgRYt6sASrxL3rl0wV3uI9aC9GiWlqlpC7nEyJQzWtsfmMcJ +G2JiD6WSgMr0QqAeUNRhaMfuNkeJ6aZvPEeEsVkAggQLl69J5QwIlWZZ1h3gTNIY +hXL6EvrDCLJyUUXU4IxbRFImPU7+s01cC4wNR3FZluv6CpuOusdrEqt33lhzNf5n +QLlr4pYsX3m9ZmAi5vahd1BtxNxgrMcwsS+IeFgoJZifGqxwcAaPWFLEBRjQUsSj +N81Edg8Awjw1dX2lVj7QUqncyZU4zBWAyy9kWwbhXcocEutV8ShvUuwgEVS3JxFZ +zKTmKUsalOr9Y2YqGY4cRYVliHECQtNkMlK8gXoC9pQBlC15lduIipNghxX6PSWY +bAIoyXvUBo6A1CDK9bYmw3K5mH8w40RTvLGPtC4gtwKDvozqmuxE/n4ZcVpfEUkM +9yIGMxVK9mLBn1JpNPWFX2ihXrtFconiUHETpFVcydR50QKBUM9NyjowfKuiqSpg +mPZ1ln7bNuPEFO5wZqw+0YC/JDeodDfEZVCldH9qGVS+qx21FiB0+0HRrzMjBXUj +MTK8ExQDBswUL4aZnch7TYv3lWc+GpdguELJb1n3yqVLV3GDa9CYKyeZRhtXljSJ +ZCQ4PYjgMQ+RIj+y1LyL+5aIXwPWCsrWJKzw3nIkt+TNsH5VXzNwSSomMucVMlUu +h7dGICFyBJZe3aAQ3KuvrGjX4mA1zD5FPANcwBpKrEirHqQF8lnUlAqV0g9rITIA +d8xvkmxYqV8vhjneHzEyoeISse0JHUZM/TyqkJqhf395T24ILcXUMGji1r6fuUhE +QaXX+xQjG0sqPWAHLq6eENPpAHALuBx3O8EwCquVZ7+I7gxOxPUNdJW6vWYOA9dR +fpMZgu1IHHMCcRV1rSJfKV7hCEb4ASOWD0kl8BtYV95YehC/ED6AEmUVHqMxC3kc +RoREjQ3XpE71B2ZmgWsJjLwLZ/3Gj7M4BGBoq9UhDNM6wdelalYV/bDLdWrVmVoB +nCDcrWG/eVbgHYHXGZbjbWu416MVA+jIU2cPSFmurakLrqZMy1ahWM9ZlJfmBZ2J +ifSXmelHMya5uzrNMnmU9EFB2iLeAK07W7A74l1VyrAPkwS+8Jt6J8tkqyLoIxpm +Bao8vEKb1L9x4nGWY9Tt6QXoZQD2hnyVGh1fEWDwo9cko+WC61LLjEtaEh6LaL4h +YL0tkzzGIVubMZ+9/HEtnUgtmr3BGXyqvWotuCpUohXC6tvtA+NK0Hh6f5F6Mv4n +ahupbEIZ6MXye+AhiBLMB4IDFCxkQp0S0nrC0DEMiPMsI7BaxMQaECMer/xjeuNw +0K0DgALCNqkh91kdAZtiIlg3qfZEtzRT8kVvNs+1zuIwYBzcpSGmXEFAgG3Zs9BT +5jH2tIoLf2mUk1kdMRaqyrDZCfKSP+qzGOnzKXDjit/8hSDBnDn08fQgTGIn3AKD +3iUOsVDvftZB5KVoar/ZgOaXdpv4gxpihQFBq6lW4AtmFnByYc/RxSiDiDpOhYiO +XDijHmE9CxOo3xwKv5+JZnObWIDWcfcm7GsrPzLi+e0tgqaMtQALaGnMs6wUcYGK +JhL4GOnsMApB1w5g2syND2J1Iur1+mFjEOQLl3p5R82naMU2tMX1Yvan4ZHDjxGd +mVpZwcvO5b1EJByJqUi2HVUy3QYAUEPRs1XaFNFmgY3lXJLSUFIjo9niCd57IFwU +/uOKovVWKWuCK+SEvuBZRHpEusJEdsyzxjiIcSwftEDA8R7WKNsWlOcjC8XK6z0y +8QPhxL6A7FAhX8xgomxx/Kig2lmMl45KhsimE2meKzs0eCF2bFwVd2aZVXcDcVbp +7czmgML2NlkwZSgGnQc2pVd57Zs6fWXTiI2xQKrr+9Y1UBoDJ6jdwkJErFEVNneI +LG4c5lxYW6ykuamEgp9R3LDTEHHjmUWU+Z2pYJxCEO3Dr28xWjI7HQ7ywO0hV9Ce +GA9jIEu2nreM965jHQ2AR1PkrvFKzWl4tZXpCgDqAeL7QgjcqvqlyAWKH5JWv+7a +YJBFiRDTvrXEVLWUXZPARTVYUx2VGSLgzIBftKJCkA8eIElpFAHEryKZy6pgMITK +S2DS4e7Yw+hlh5lmkqa9YOVyd5bSpS2hzASVbNVu/UOQQbllK8TX3G17SsmwRtNn +SBFIB9CuL5O0vrnBlXmdkBzNh8Sj15+ZTMl0JW23pMISoFWXk64mdgGyg108w7SH +a1n/ALK2HC0I7HrmPXaAuk6wN7DKKo331BQq/ukoV4v4mMT3QzRFDaJwlRmUTKOG +G1vIpGU7Mqeg8xv0+g4WLbLEUk5VKYJkOq1sr/pDIqeDCYWtGB17eYt2Hmb8jSiq +VecG47eaWEY21qC1gWNX1E/syl77Ksxvyb34hIOK2Bxj24lQU4Iqj78QEAEGRLw6 +9SYKV0Ww6xkHY++UHR7xTC8i8wJnrcIH+/TgxtA4xBW0JM7dxrNX8RBXSo61HKRV +u1ZLj4/1KBBrMvO3XcQAtAJbWylasMsSjq1nnD8EUTdh6ykbrbNftLewppLh07vg +4iPD3NqJyWIneMBEznjhshLUvGIsUY1ECWxrbWE4jw2Ycl4w8RiNAVYWYtgOobpS +xhzXXvCxALVeFHtjmMKZl5awlFvLQOO68zBPLpgZzfetbnCMmUFdSqgGaNxhPX6T +eI5SWw4SZEy54gXCYHOoLFdI78yg0ZZTrn44zLxW+btnSgIcifuwBtpTysWnULZ8 ++/76Q9SpwDkfqSonvCrK28nQfqX9bLC5U8sq+Y6+SkNK6gfzA/GQo530eIsQ9CSu +7rnjPp0jIYBY10HI3rxNjGZAaO2bsx5O8MRzc97DPDZGLHT7lZrC+Yx8FhhBLerP +NdYPm7thnu+hSOLmWIOeJisS50cF2PVyTPY2LvyS54zLLLlAVVKAH5hksEuLwf8A +JdkMD7Z+GFWWljWNv92hx6LoDUC7FcqxfYcdLN9v15hiKIXP6aCCpyojnQ/if5HT +sYF873VCskKsHKXxQWvauYShBMLRyAbc9yHquPp4HzbtU1QmEPLAZwsCicPTP7gJ +9SyjWTuJniFIehk6X+IathTRbee0taEt42krp4hYZgy6+mb3Eyl37Q27y0S5+xM/ +uABmOuuaHMOqspb6yvLBRZy4+1x7pGjkUtY1EtHl/wCv2inrmjxDIAIMRGfb8eYh +XZ6KPyp/VAdEMRegEdjbV1wj9Te+ujVV5TzvbK022+/tFnUzZ4H2OP8AJmvgrRYA +jCoHND1te0SsxXglAfYO8ACaBLtpHlWivLxLcjaBAJ0bG/JxB8CGwMhyccxmj4SI +c16aiObM+0TeUMSq0ssJUNsS8S2nIMEDIgw4PtGMZVqpSBSCVgo9kxRzLqAF8l/6 +B6QK3GfFm2FZiSu1ge6kpRQFOTWvVinGhjjXofdlbwJUOcv9xvaEcUtHXoEtOOeT +QHpNo7ZCt1GRIMoAcQdUfYy2Ul9RaVdee/pGA1oHAb7g/wBlTm+Ly1VzF8gksgz7 +uX0OJbwhXRC09W8dyBZqMhZQOtYRt0rNt/IXBobseIg8C2aJD1mCx3LNJYtkrvUu +rcEYsNvQuW6buCz6cb2fQV9ISraqdrwflFjlEXI3+F9usWfPDWkPgXtGdioG64/r +oxr8lg30/XUJBiTlftf9uELpWLGj1rXvLJoCSkfqLw/s4AHxGdxltAHLpBwoNWOE +9/sxmRveRY932lIci1Scg7XlYFpmidEV0o17y9qL6juuc5YZEsMCnGuQ4OZTzyg0 +V0Ti6DzFQBQFBLSl+IPwRNTqmWsunowPlNm5YHqv7zL6hNGKj3f8ImXMB5IKymoY +2hPmLAWMuF259oQVgl1UB6fiMgpGGF3QKGglVK0Pf4ZR7EjYuD+6SvUCayOssteJ +j6ess7jQYAA1GxNULRb9E65QUEvxi8QIZWpLZrsGM/uZLwtUBo9ZviaDHC8dYYe6 +uQ25+TtL2rRi1bHpi7l4Vq4cP3/cfSQxyl+Fs10PoCgQblkOUlGrTLeL5g0xXmXE +u67IbNnGMoV+biVwG3A1RAojGmaP2EtYNhfQy/Azrug8c/kRUW7z1KH87w3aHE7f +k+EawLgYrHwQIdIR6/brBNBgOeeEKjYGlghSGoZ4z1/UOgqncvT/AGYvyy2Jb/cw +rpUBKT5PEF2EXgQ6IfmNhjybXQO8u30j9Kpo2Q81Ze1RdPGQME7C13CFRIdQDAEs +1Nn3guNkIywsTPojiDcP2gvioN9auzJx118wsih1Yn9mNaMZdjh8FxVloND4Zgjk +T3aPtHBAUe2CEbdI7K/Jib5AVR+yBTryocPVv07wImehnkfWMTGgPcitCgW77dQY +XXNSj0AfmBlcE4PDBh60USVR7Xn2lceFRD1D5l6cHuRyrtLOjODIbDfF1Nsw5UQc +PX3hImAAUHaPS4hd5jo4ZWJlIjpK0XvHoCPJMvXtDOVUbdYqAc0pXRFYrnfE5HQH +hY+Zibl+CEy5dWuv6I1Xn2D/ABEXFGzwYvA2IuXwHe2BJkBcDjPgKILunsHoHqeK +uZsLKBdh6HECeq2oaxAluVoDoHB2hv2oCgeZa66uqogmVoAqD0g7RJcyjRFQ/cxl +dxDtiCwTiBf3tuYYfALVlPeMhjyQfMsio1szD63mjNQgCxT4t9o6bIP2JhfVs+F/ +MN42VajamYUwoLMrBgIQhlXRPjcc9znQWVXXURhQUFgdjiCKAMdveEBh0IDkX4hq +HtBvwiCxbz1l3Bk5VuGi8OGUxreZR7ysbYIVkMMRDi6PIpbcs8QzF5ZxA+7uY03l +RKAVkueT1jstBYp59zHvzqqHM6ok0HFZ9CbWCVj/AFB4oFdT+4HTbtzYeE9gImAR +lkJUfEWYtcesWjc7scEaqWLAXTKxQeRth7D8w1WIoMowNQApIjHPUMzsQ0FJ10wH +BY5ptSmC2BqI4oIPgJQiG5rNG5bAw3Lu5CNm+ksMakcd5lBMwMLgGhtdIWM18sps +XduFoUhhhSK6gcR1H7QVPKEzHZbxNvY94Igh75hyG+iQTEUeYGtyyacxN1cHlgY7 +jKLuHwPrBL29IFZuEWFHxqbrmZ6rermDBpLcR2ir+4aavUWp/DK/2fSfcyInkFgF +iqBGAp1lJy7kQhgS9m/WagnpC5zcOgx9UNdqitpgdUkQyh5mBeU8ArRR2nblEwYD +1gss9iLGI3dbnGEBXvEnXy/SVENCd6WMGmoBDiZlDv5gmwwiNoC4jcD05gWr9YX/ +AMn8M7MyywhWAuCZ8e/0It+oeyZcQKl0XBYYLyAv2T6RsOF4DeMP2ht4ThGAjJLY +BLS4afQMrnMUdY6Xkm0NTaZfSJlO6UamKZCSty1Qt3h2TODjebjIQL0+2IjSGwhl +dvGF7+ItP32Wg8Zb89oaKpq7WrwaIX8a42yTXiFhFD2SnTuGO8y4eAWq3Lbb8ymm +EWO5KmNwKRyW9plMmudxCW+kGsfQEOHEHmGHMMNTMwEKbjsIw2McNmYWO30EBMkQ +y6I3G1jAelMCuYsLyH0qvWbZBqWtjm4uyhZGViuszlIsaZCU/rmCvrCrY7OY8iF5 +DMaCrV1hi9yz6IAJeX5Y4hGOZJ3UHtBfj6RcHvM0s+gwDbNCXLqL4dzU+/0lUC6n +GyZNTJ3jYzDojMBSdR4hp1dFZ1xRdsIsNpALCm/X0gwA4gEHZw/MzvHA+Rz2g3H7 +Sswdd0fiAZo4mnEoLXMyt3lIt7hKHSE4ZSzsy3iLKTmBU//ZiJkEExYKAEEWIQQt +/1rIQwoCvhlznp31J3BvaZrFogUCYwMEQwIbAwUJA8JnAAULCQgHAgIiAgYVCgkI +CwIEFgIDAQIeBwIXgAAKCRD1J3BvaZrFok/SAP9fP1jYvH1C0iIJzFjNDwyKVWA0 +lDyjDHZP4d0zjBz6FQD/e3c/nTnIMOLacxzBQZz3t3+UII2I0ci1dPd57DbSXQa4 +OARjAwQREgorBgEEAZdVAQUBAQdA8c7lzMCO8qModhUq0t7sgTwsMwY/A9JenOY6 +ncdGPzoDAQgHiH4EGBYKACYWIQQt/1rIQwoCvhlznp31J3BvaZrFogUCYwMEEQIb +DAUJA8JnAAAKCRD1J3BvaZrFol09AP9dlZ5yrouNEpfwpIGlkcZjtqa8fWnBBOMZ +z829BTssIwEA8JPOgk+yssCWu08ksLEY9rvQrVX6cQuSg2KihdLM3gQ= +=vfpo +-----END PGP PUBLIC KEY BLOCK----- +` diff --git a/openpgp/v2/uat.go b/openpgp/v2/uat.go new file mode 100644 index 00000000..a29446c9 --- /dev/null +++ b/openpgp/v2/uat.go @@ -0,0 +1,90 @@ +package v2 + +import ( + "io" + + "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func (t *Entity) AddPhotos(jpegBytes [][]byte, config *packet.Config) error { + creationTime := config.Now() + + uat, err := packet.NewUserAttributePhotoBytes(jpegBytes) + if err != nil { + return errors.InvalidArgumentError("add photo field contained invalid characters") + } + + primary := t.PrivateKey + + isPrimaryId := len(t.Attributes) == 0 + + selfSignature := &packet.Signature{ + Version: primary.PublicKey.Version, + SigType: packet.SigTypePositiveCert, + PubKeyAlgo: primary.PublicKey.PubKeyAlgo, + Hash: config.Hash(), + CreationTime: creationTime, + IssuerKeyId: &primary.PublicKey.KeyId, + IssuerFingerprint: primary.PublicKey.Fingerprint, + IsPrimaryId: &isPrimaryId, + } + + // User Attribute binding signature + err = selfSignature.SignUserAttribute(uat, &primary.PublicKey, primary, config) + if err != nil { + return err + } + userAttribute := &Attribute{ + UserAttribute: uat, + SelfSignature: selfSignature, + Signatures: []*packet.Signature{selfSignature}, + } + t.Attributes = append(t.Attributes, userAttribute) + return nil +} + +func addUserAttribute(e *Entity, packets *packet.Reader, pkt *packet.UserAttribute) error { + uat := new(Attribute) + uat.UserAttribute = pkt + e.Attributes = append(e.Attributes, uat) + + for { + p, err := packets.Next() + if err == io.EOF { + break + } else if err != nil { + return err + } + + sig, ok := p.(*packet.Signature) + if !ok { + packets.Unread(p) + break + } + + if sig.SigType != packet.SigTypeGenericCert && + sig.SigType != packet.SigTypePersonaCert && + sig.SigType != packet.SigTypeCasualCert && + sig.SigType != packet.SigTypePositiveCert && + sig.SigType != packet.SigTypeCertificationRevocation { + return errors.StructuralError("user attribute signature with wrong type") + } + + if sig.CheckKeyIdOrFingerprint(e.PrimaryKey) { + if err = e.PrimaryKey.VerifyUserAttributeSignature(pkt, e.PrimaryKey, sig); err != nil { + return errors.StructuralError("user attribute self-signature invalid: " + err.Error()) + } + if sig.SigType == packet.SigTypeCertificationRevocation { + uat.Revocations = append(uat.Revocations, sig) + } else if uat.SelfSignature == nil || sig.CreationTime.After(uat.SelfSignature.CreationTime) { + uat.SelfSignature = sig + } + uat.Signatures = append(uat.Signatures, sig) + } else { + uat.Signatures = append(uat.Signatures, sig) + } + } + + return nil +} diff --git a/openpgp/v2/uat_test.go b/openpgp/v2/uat_test.go new file mode 100644 index 00000000..fea15374 --- /dev/null +++ b/openpgp/v2/uat_test.go @@ -0,0 +1,52 @@ +package v2 + +import ( + "bytes" + "encoding/base64" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func TestUserattribute_CompatibleGPG2_3_6(t *testing.T) { + _, err := ReadArmoredKeyRing(bytes.NewBufferString(pgpPhotoPublicGPG2_3_6)) + if err != nil { + t.Fatal(err) + } +} + +func TestAddPhoto(t *testing.T) { + entity, err := NewEntity("Golang Gopher", "Test Key", "no-reply@golang.com", nil) + if err != nil { + t.Fatal(err) + } + + jpegBase64 := "/9j/4AAQSkZJRgABAQEAYABgAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAEAAAAAAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCAAyAEsDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9EKKKKACipLKzm1K/gtbeMy3F1KkMSAgF3ZgqrzxySBzxzXXXnwXm0+6kt7jxZ8P4LiFzHLFJrRV43U4ZWHlcEEEEdiKAONorrf8AhUf/AFOPw7/8HZ/+NUf8Kj/6nH4d/wDg7P8A8aoA5Kitrxh4Eu/BcdjLNdaVqFpqSSNbXWnXX2iCUxtsdQ2AcqxAPGMnGcggYtABRRRQAUUUUAa3w+/5KH4d/wCwtZ/+j0r1LwH4h8DaP4q8dR+KoLF719dvHjkurM3CmASt8qfK2GDbyQACcr1xx5b8Pv8Akofh3/sLWf8A6PSvX/hB8ILDxr8S/FmvakwuIdN8RXtvDaMvyPKspfe/qBvXC+oOc8CgDx3RvDVx448Wf2foNnNIbqVzbxO2TDFu4MjcgBVIy3r0ySAev+Lv7PmofDDTLfUI5v7SsNirdSqm37NL3yP+eZPRj06HsT9GeEvhzongS4vpdJsobOTUpfNmK/oq/wB1ByQowoLHAFbVzDHeQtFIqSRyAq6MAyuDwQR3BoA+RfGP/JHPh/8A9xb/ANKxXI11ni0/8WX+Hv01X/0rWuToAKKKKACiiigCbTdQm0fVLW8t2VbizmS4iLLuAdGDKSO/IHFddqXxP0PWb+a7vPAehXN3dSNNNKbmUeY7EszY7ZYk4964uigDrf8AhPPDP/RPNB/8CpaP+E88M/8ARPNB/wDAqWuSooA3vG3jr/hL7XTbWDTbPSdP0lJVtra3LNtMrh5CWY5OWAPbHPrWDRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQB//Z" + jpegBytes, err := base64.RawStdEncoding.DecodeString(jpegBase64) + if err != nil { + t.Fatal(err) + } + + err = entity.AddPhotos([][]byte{jpegBytes}, nil) + if err != nil { + t.Fatal(err) + } + + serializedEntity := bytes.NewBuffer(nil) + entity.SerializePrivate(serializedEntity, nil) + + entity2, err := ReadEntity(packet.NewReader(bytes.NewBuffer(serializedEntity.Bytes()))) + if err != nil { + t.Fatal(err) + } + + if len(entity2.Attributes) != 1 { + t.Fatal("data err, entity have no uat") + } + + imgBytes := entity2.Attributes[0].UserAttribute.ImageData() + if !bytes.Equal(imgBytes[0], jpegBytes) { + t.Fatal("image data not equal") + } + +} diff --git a/openpgp/v2/user.go b/openpgp/v2/user.go index 3da03bd7..c2ccc34b 100644 --- a/openpgp/v2/user.go +++ b/openpgp/v2/user.go @@ -19,6 +19,15 @@ type Identity struct { Revocations []*packet.VerifiableSignature } +// Attribute represents a user attribute (such as an image) claimed by an +// Entity and zero or more assertions by other entities about that claim. +type Attribute struct { + UserAttribute *packet.UserAttribute + SelfSignature *packet.Signature + Revocations []*packet.Signature + Signatures []*packet.Signature // all (potentially unverified) self-signatures, revocations, and third-party signatures +} + func readUser(e *Entity, packets *packet.Reader, pkt *packet.UserId) error { identity := Identity{ Primary: e,