diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c0d3677..b1f2807c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added +- Function to verify a detached signature and get its creation time: +```go +func (keyRing *KeyRing) GetVerifiedSignatureTimestamp(message *PlainMessage, signature *PGPSignature, verifyTime int64) (int64, error) +``` + ## [2.3.1] 2021-12-15 ### Fixed - Fix the verification of PGP/MIME message signatures: diff --git a/crypto/keyring_message.go b/crypto/keyring_message.go index c6250618..4c2c9cd4 100644 --- a/crypto/keyring_message.go +++ b/crypto/keyring_message.go @@ -118,6 +118,44 @@ func (keyRing *KeyRing) VerifyDetachedEncrypted(message *PlainMessage, encrypted return keyRing.VerifyDetached(message, signature, verifyTime) } +// GetVerifiedSignatureTimestamp verifies a PlainMessage with a detached PGPSignature +// returns the creation time of the signature if it succeeds +// and returns a SignatureVerificationError if fails. +func (keyRing *KeyRing) GetVerifiedSignatureTimestamp(message *PlainMessage, signature *PGPSignature, verifyTime int64) (int64, error) { + packets := packet.NewReader(bytes.NewReader(signature.Data)) + var err error + var p packet.Packet + for { + p, err = packets.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + continue + } + sigPacket, ok := p.(*packet.Signature) + if !ok { + continue + } + var outBuf bytes.Buffer + err = sigPacket.Serialize(&outBuf) + if err != nil { + continue + } + err = verifySignature( + keyRing.entities, + message.NewReader(), + outBuf.Bytes(), + verifyTime, + ) + if err != nil { + continue + } + return sigPacket.CreationTime.Unix(), nil + } + return 0, errors.Wrap(err, "gopenpgp: can't verify any signature packets") +} + // ------ INTERNAL FUNCTIONS ------- // Core for encryption+signature (non-streaming) functions. diff --git a/crypto/message.go b/crypto/message.go index e91d9a3b..5ff3fce3 100644 --- a/crypto/message.go +++ b/crypto/message.go @@ -423,23 +423,23 @@ func (msg *PGPMessage) SeparateKeyAndData(estimatedLength, garbageCollector int) } // GetBinary returns the unarmored binary content of the signature as a []byte. -func (msg *PGPSignature) GetBinary() []byte { - return msg.Data +func (sig *PGPSignature) GetBinary() []byte { + return sig.Data } // GetArmored returns the armored signature as a string. -func (msg *PGPSignature) GetArmored() (string, error) { - return armor.ArmorWithType(msg.Data, constants.PGPSignatureHeader) +func (sig *PGPSignature) GetArmored() (string, error) { + return armor.ArmorWithType(sig.Data, constants.PGPSignatureHeader) } // GetSignatureKeyIDs Returns the key IDs of the keys to which the (readable) signature packets are encrypted to. -func (msg *PGPSignature) GetSignatureKeyIDs() ([]uint64, bool) { - return getSignatureKeyIDs(msg.Data) +func (sig *PGPSignature) GetSignatureKeyIDs() ([]uint64, bool) { + return getSignatureKeyIDs(sig.Data) } // GetHexSignatureKeyIDs Returns the key IDs of the keys to which the session key is encrypted. -func (msg *PGPSignature) GetHexSignatureKeyIDs() ([]string, bool) { - return getHexKeyIDs(msg.GetSignatureKeyIDs()) +func (sig *PGPSignature) GetHexSignatureKeyIDs() ([]string, bool) { + return getHexKeyIDs(sig.GetSignatureKeyIDs()) } // GetBinary returns the unarmored signed data as a []byte. diff --git a/crypto/signature_test.go b/crypto/signature_test.go index 3cf97b93..2395fc25 100644 --- a/crypto/signature_test.go +++ b/crypto/signature_test.go @@ -1,10 +1,14 @@ package crypto import ( + "bytes" "errors" + "io" + "io/ioutil" "regexp" "testing" + "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ProtonMail/gopenpgp/v2/constants" "github.com/stretchr/testify/assert" ) @@ -73,3 +77,120 @@ func TestVerifyBinDetachedSig(t *testing.T) { t.Fatal("Cannot verify binary signature:", verificationError) } } + +func Test_KeyRing_GetVerifiedSignatureTimestampSuccess(t *testing.T) { + message := NewPlainMessageFromString("Hello world!") + var time int64 = 1600000000 + pgp.latestServerTime = time + defer func() { + pgp.latestServerTime = testTime + }() + signature, err := keyRingTestPrivate.SignDetached(message) + if err != nil { + t.Errorf("Got an error while generating the signature: %v", err) + } + actualTime, err := keyRingTestPublic.GetVerifiedSignatureTimestamp(message, signature, 0) + if err != nil { + t.Errorf("Got an error while parsing the signature creation time: %v", err) + } + if time != actualTime { + t.Errorf("Expected creation time to be %d, got %d", time, actualTime) + } +} + +func Test_KeyRing_GetVerifiedSignatureWithTwoKeysTimestampSuccess(t *testing.T) { + publicKey1Armored, err := ioutil.ReadFile("testdata/signature/publicKey1") + if err != nil { + t.Errorf("Couldn't read the public key file: %v", err) + } + publicKey1 := parseKey(t, string(publicKey1Armored)) + publicKey2Armored, err := ioutil.ReadFile("testdata/signature/publicKey2") + if err != nil { + t.Errorf("Couldn't read the public key file: %v", err) + } + publicKey2 := parseKey(t, string(publicKey2Armored)) + message := NewPlainMessageFromString("hello world") + signatureArmored, err := ioutil.ReadFile("testdata/signature/detachedSigSignedTwice") + if err != nil { + t.Errorf("Couldn't read the signature file: %v", err) + } + signature, err := NewPGPSignatureFromArmored(string(signatureArmored)) + if err != nil { + t.Errorf("Got an error while parsing the signature: %v", err) + } + time1 := getTimestampOfIssuer(signature, publicKey1.GetKeyID()) + time2 := getTimestampOfIssuer(signature, publicKey2.GetKeyID()) + keyRing, err := NewKeyRing(publicKey1) + if err != nil { + t.Errorf("Got an error while building the key ring: %v", err) + } + err = keyRing.AddKey(publicKey2) + if err != nil { + t.Errorf("Got an error while adding key 2 to the key ring: %v", err) + } + actualTime, err := keyRing.GetVerifiedSignatureTimestamp(message, signature, 0) + if err != nil { + t.Errorf("Got an error while parsing the signature creation time: %v", err) + } + if time1 != actualTime { + t.Errorf("Expected creation time to be %d, got %d", time1, actualTime) + } + if time2 == actualTime { + t.Errorf("Expected creation time to be different from %d", time2) + } +} + +func parseKey(t *testing.T, keyArmored string) *Key { + key, err := NewKeyFromArmored(keyArmored) + if err != nil { + t.Errorf("Couldn't parse key: %v", err) + return nil + } + return key +} + +func getTimestampOfIssuer(signature *PGPSignature, keyID uint64) int64 { + packets := packet.NewReader(bytes.NewReader(signature.Data)) + var err error + var p packet.Packet + for { + p, err = packets.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + continue + } + sigPacket, ok := p.(*packet.Signature) + if !ok { + continue + } + var outBuf bytes.Buffer + err = sigPacket.Serialize(&outBuf) + if err != nil { + continue + } + if *sigPacket.IssuerKeyId == keyID { + return sigPacket.CreationTime.Unix() + } + } + return -1 +} + +func Test_KeyRing_GetVerifiedSignatureTimestampError(t *testing.T) { + message := NewPlainMessageFromString("Hello world!") + var time int64 = 1600000000 + pgp.latestServerTime = time + defer func() { + pgp.latestServerTime = testTime + }() + signature, err := keyRingTestPrivate.SignDetached(message) + if err != nil { + t.Errorf("Got an error while generating the signature: %v", err) + } + message_corrupted := NewPlainMessageFromString("Ciao world!") + _, err = keyRingTestPublic.GetVerifiedSignatureTimestamp(message_corrupted, signature, 0) + if err == nil { + t.Errorf("Expected an error while parsing the creation time of a wrong signature, got nil") + } +} diff --git a/crypto/testdata/signature/detachedSigSignedTwice b/crypto/testdata/signature/detachedSigSignedTwice new file mode 100644 index 00000000..40ca942d --- /dev/null +++ b/crypto/testdata/signature/detachedSigSignedTwice @@ -0,0 +1,10 @@ +-----BEGIN PGP SIGNATURE----- + +wnUEARYKAAYFAmCCo8gAIQkQyQtnL+EYbekWIQTopSabUSqDUEv/FMHJC2cv +4Rht6VeGAP4mUJl+WYN9nLE57YByTh95OmcZmwfgz5Z4R570YqTVngD/VBym +icc7YREcxij1gC6SSAe8kgKW6oVOWzxJ8HkOSQrCdQQBFgoABgUCYIK9vQAh +CRCGHCX3YYW5NRYhBErDPc6OYkUaNQCLhoYcJfdhhbk1W1QBAPhrkAjimO22 +jh1V2A8pRCOs53Ig/AMAFbN37BaAIEVKAP0SVMTL6zTxYJcxWNPog7Bv5lM4 +Px4G+hZ2Kia//qlgBg== +=0aeU +-----END PGP SIGNATURE----- \ No newline at end of file diff --git a/crypto/testdata/signature/publicKey1 b/crypto/testdata/signature/publicKey1 new file mode 100644 index 00000000..f3fa7b61 --- /dev/null +++ b/crypto/testdata/signature/publicKey1 @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xjMEYIKjXxYJKwYBBAHaRw8BAQdAbmODPSLO5tOI0GxfV+x5bgiiFriCcH3t +6lbJkS+OzKbNEHRlc3QgPHRlc3RAYS5pdD7CjAQQFgoAHQUCYIKjXwQLCQcI +AxUICgQWAAIBAhkBAhsDAh4BACEJEMkLZy/hGG3pFiEE6KUmm1Eqg1BL/xTB +yQtnL+EYbenOlAEAn7A7RXQJ9FUzhuiOHeKqczdslgOO5LFcng1LuSIWn1UB +ANWHrxnH63jnFLE82mfhpRZ5FYJ1fEXA9+3v6at3ZE8IzjgEYIKjXxIKKwYB +BAGXVQEFAQEHQA5moGr1AKlYvKI+JpyB6W640eXpQFNSiV6LBjuMteNbAwEI +B8J2BBgWCAAJBQJggqNfAhsMACEJEMkLZy/hGG3pFiEE6KUmm1Eqg1BL/xTB +yQtnL+EYben97QD4hf6DttxyczHGqxGbboatBZ3IufJgFm6r2xNf9d9lSAD3 +U12oHbxyYUhapbFFkSIBo7DWJqWvx3iUEPqzY6jIAA== +=ZWrn +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/crypto/testdata/signature/publicKey2 b/crypto/testdata/signature/publicKey2 new file mode 100644 index 00000000..c1f78d7a --- /dev/null +++ b/crypto/testdata/signature/publicKey2 @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xjMEYIKjgxYJKwYBBAHaRw8BAQdARyd9iDlrlozcTG144XFIjWozyWLz0KQv +fL4lqIrwM8XNEHRlc3QgPHRlc3RAYi5pdD7CjAQQFgoAHQUCYIKjgwQLCQcI +AxUICgQWAAIBAhkBAhsDAh4BACEJEIYcJfdhhbk1FiEESsM9zo5iRRo1AIuG +hhwl92GFuTVPRAD6A6//tK5pLPa1d7mgsoqyJ9BZyTAmnzxtbIgmOU9/TDcB +AI4cGBfCOLzRPw6L0il5Rt78TX1jz4Dlzu6YixJcJ2AFzjgEYIKjgxIKKwYB +BAGXVQEFAQEHQMjb0Q1FWvHzj0hyOiEN5ndChBDceUqxmQ0wOYDVqq8JAwEI +B8J4BBgWCAAJBQJggqODAhsMACEJEIYcJfdhhbk1FiEESsM9zo5iRRo1AIuG +hhwl92GFuTXz4AEAqn4L+ayYgphejF/ZTRIseHPK+t521CT6NZKoVaHnTWQA +/0+kMEB5d+CH3Mb54cUganYHPLj5utO2PexEJc3xARIG +=IEm4 +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file