Skip to content

Commit

Permalink
Merge pull request #157 from ProtonMail/feat/signature_creation_time
Browse files Browse the repository at this point in the history
Add APIs to get the creation time of verified detached signatures
  • Loading branch information
wussler committed Dec 21, 2021
2 parents eec2885 + d08315a commit e8c7fa3
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 8 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
38 changes: 38 additions & 0 deletions crypto/keyring_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 8 additions & 8 deletions crypto/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
121 changes: 121 additions & 0 deletions crypto/signature_test.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand Down Expand Up @@ -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")
}
}
10 changes: 10 additions & 0 deletions crypto/testdata/signature/detachedSigSignedTwice
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-----BEGIN PGP SIGNATURE-----

wnUEARYKAAYFAmCCo8gAIQkQyQtnL+EYbekWIQTopSabUSqDUEv/FMHJC2cv
4Rht6VeGAP4mUJl+WYN9nLE57YByTh95OmcZmwfgz5Z4R570YqTVngD/VBym
icc7YREcxij1gC6SSAe8kgKW6oVOWzxJ8HkOSQrCdQQBFgoABgUCYIK9vQAh
CRCGHCX3YYW5NRYhBErDPc6OYkUaNQCLhoYcJfdhhbk1W1QBAPhrkAjimO22
jh1V2A8pRCOs53Ig/AMAFbN37BaAIEVKAP0SVMTL6zTxYJcxWNPog7Bv5lM4
Px4G+hZ2Kia//qlgBg==
=0aeU
-----END PGP SIGNATURE-----
13 changes: 13 additions & 0 deletions crypto/testdata/signature/publicKey1
Original file line number Diff line number Diff line change
@@ -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-----
13 changes: 13 additions & 0 deletions crypto/testdata/signature/publicKey2
Original file line number Diff line number Diff line change
@@ -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-----

0 comments on commit e8c7fa3

Please sign in to comment.