Add API to add contexts to detached signatures.

Using the notation data packets of signatures, we add a way to
set a context to detached signatures.
We also add a way to enforce that signatures have the right context
during verification.
This commit is contained in:
M. Thiercelin 2023-03-02 15:33:12 +01:00
parent 3152e50f92
commit 1ec90e34ea
No known key found for this signature in database
GPG key ID: 29581E7E24EBEC0A
10 changed files with 614 additions and 19 deletions

View file

@ -5,13 +5,23 @@ 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
# Changed
### Added
- API for adding context to detached signatures:
```go
sig, err := keyRing.SignDetachedWithContext(message, context)
```
- API to verify the context of detached signatures:
```go
err := keyRing.VerifyDetachedWithContext(message, signature, verifyTime, verificationContext)
```
### Changed
- Update `github.com/ProtonMail/go-crypto` to the latest version
- More strictly verify detached signatures: reject detached signatures from revoked and expired keys.
- In `GetVerifiedSignatureTimestamp`, use the new `VerifyDetachedSignatureAndHash` function to get the verified signature, instead of parsing the signature packets manually to get the timestamp.
## [2.5.2] 2022-01-25
# Changed
### Changed
- Update `github.com/ProtonMail/go-crypto` to the latest version
## [2.5.1] 2022-01-24

3
constants/context.go Normal file
View file

@ -0,0 +1,3 @@
package constants
const SignatureContextName = "context@proton.ch"

View file

@ -62,12 +62,26 @@ func (keyRing *KeyRing) Decrypt(
// SignDetached generates and returns a PGPSignature for a given PlainMessage.
func (keyRing *KeyRing) SignDetached(message *PlainMessage) (*PGPSignature, error) {
return keyRing.SignDetachedWithContext(message, nil)
}
// SignDetachedWithContext generates and returns a PGPSignature for a given PlainMessage.
// If a context is provided, it is added to the signature as notation data
// with the name set in `constants.SignatureContextName`.
func (keyRing *KeyRing) SignDetachedWithContext(message *PlainMessage, context *SigningContext) (*PGPSignature, error) {
signEntity, err := keyRing.getSigningEntity()
if err != nil {
return nil, err
}
config := &packet.Config{DefaultHash: crypto.SHA512, Time: getTimeGenerator()}
var signatureNotations []*packet.Notation
if context != nil {
signatureNotations = []*packet.Notation{context.getNotation()}
}
config := &packet.Config{
DefaultHash: crypto.SHA512,
Time: getTimeGenerator(),
SignatureNotations: signatureNotations,
}
var outBuf bytes.Buffer
if message.IsBinary() {
err = openpgp.DetachSign(&outBuf, signEntity, message.NewReader(), config)
@ -89,6 +103,22 @@ func (keyRing *KeyRing) VerifyDetached(message *PlainMessage, signature *PGPSign
message.NewReader(),
signature.GetBinary(),
verifyTime,
nil,
)
return err
}
// VerifyDetachedWithContext verifies a PlainMessage with a detached PGPSignature
// and returns a SignatureVerificationError if fails.
// If a context is provided, it verifies that the signature is valid in the given context, using
// the signature notation with name the name set in `constants.SignatureContextName`.
func (keyRing *KeyRing) VerifyDetachedWithContext(message *PlainMessage, signature *PGPSignature, verifyTime int64, verificationContext *VerificationContext) error {
_, err := verifySignature(
keyRing.entities,
message.NewReader(),
signature.GetBinary(),
verifyTime,
verificationContext,
)
return err
}
@ -132,6 +162,31 @@ func (keyRing *KeyRing) GetVerifiedSignatureTimestamp(message *PlainMessage, sig
message.NewReader(),
signature.GetBinary(),
verifyTime,
nil,
)
if err != nil {
return 0, err
}
return sigPacket.CreationTime.Unix(), nil
}
// GetVerifiedSignatureTimestampWithContext verifies a PlainMessage with a detached PGPSignature
// returns the creation time of the signature if it succeeds
// and returns a SignatureVerificationError if fails.
// If a context is provided, it verifies that the signature is valid in the given context, using
// the signature notation with name the name set in `constants.SignatureContextName`.
func (keyRing *KeyRing) GetVerifiedSignatureTimestampWithContext(
message *PlainMessage,
signature *PGPSignature,
verifyTime int64,
verificationContext *VerificationContext,
) (int64, error) {
sigPacket, err := verifySignature(
keyRing.entities,
message.NewReader(),
signature.GetBinary(),
verifyTime,
verificationContext,
)
if err != nil {
return 0, err

View file

@ -329,6 +329,27 @@ func (keyRing *KeyRing) VerifyDetachedStream(
message,
signature.GetBinary(),
verifyTime,
nil,
)
return err
}
// VerifyDetachedStreamWithContext verifies a message reader with a detached PGPSignature
// and returns a SignatureVerificationError if fails.
// If a context is provided, it verifies that the signature is valid in the given context, using
// the signature notations.
func (keyRing *KeyRing) VerifyDetachedStreamWithContext(
message Reader,
signature *PGPSignature,
verifyTime int64,
verificationContext *VerificationContext,
) error {
_, err := verifySignature(
keyRing.entities,
message,
signature.GetBinary(),
verifyTime,
verificationContext,
)
return err
}

View file

@ -118,8 +118,96 @@ func verifyDetailsSignature(md *openpgp.MessageDetails, verifierKey *KeyRing) er
return nil
}
// SigningContext gives the context that will be
// included in the signature's notation data.
type SigningContext struct {
Value string
IsCritical bool
}
// NewSigningContext creates a new signing context.
// The value is set to the notation data.
// isCritical controls whether the notation is flagged as a critical packet.
func NewSigningContext(value string, isCritical bool) *SigningContext {
return &SigningContext{Value: value, IsCritical: isCritical}
}
func (context *SigningContext) getNotation() *packet.Notation {
return &packet.Notation{
Name: constants.SignatureContextName,
Value: []byte(context.Value),
IsCritical: context.IsCritical,
IsHumanReadable: true,
}
}
// VerificationContext gives the context that will be
// used to verify the signature.
type VerificationContext struct {
Value string
IsRequired bool
RequiredAfter int64
}
// NewVerificationContext creates a new verification context.
// The value is checked against the signature's notation data.
// If isRequired is false, the signature is allowed to have no context set.
// If requiredAfter is != 0, the signature is allowed to have no context set if it
// was created before the unix time set in requiredAfter.
func NewVerificationContext(value string, isRequired bool, requiredAfter int64) *VerificationContext {
return &VerificationContext{
Value: value,
IsRequired: isRequired,
RequiredAfter: requiredAfter,
}
}
func (context *VerificationContext) isRequiredAtTime(signatureTime time.Time) bool {
return context.IsRequired &&
(context.RequiredAfter == 0 || signatureTime.After(time.Unix(context.RequiredAfter, 0)))
}
func findContext(notations []*packet.Notation) (string, error) {
context := ""
for _, notation := range notations {
if notation.Name == constants.SignatureContextName {
if context != "" {
return "", errors.New("gopenpgp: signature has multiple context notations")
}
if !notation.IsHumanReadable {
return "", errors.New("gopenpgp: context notation was not set as human-readable")
}
context = string(notation.Value)
}
}
return context, nil
}
func (context *VerificationContext) verifyContext(sig *packet.Signature) error {
signatureContext, err := findContext(sig.Notations)
if err != nil {
return err
}
if signatureContext != context.Value {
contextRequired := context.isRequiredAtTime(sig.CreationTime)
if contextRequired {
return errors.New("gopenpgp: signature did not have the required context")
} else if signatureContext != "" {
return errors.New("gopenpgp: signature had a wrong context")
}
}
return nil
}
// verifySignature verifies if a signature is valid with the entity list.
func verifySignature(pubKeyEntries openpgp.EntityList, origText io.Reader, signature []byte, verifyTime int64) (*packet.Signature, error) {
func verifySignature(
pubKeyEntries openpgp.EntityList,
origText io.Reader,
signature []byte,
verifyTime int64,
verificationContext *VerificationContext,
) (*packet.Signature, error) {
config := &packet.Config{}
if verifyTime == 0 {
config.Time = func() time.Time {
@ -130,15 +218,18 @@ func verifySignature(pubKeyEntries openpgp.EntityList, origText io.Reader, signa
return time.Unix(verifyTime+internal.CreationTimeOffset, 0)
}
}
if verificationContext != nil {
config.KnownNotations = map[string]bool{constants.SignatureContextName: true}
}
signatureReader := bytes.NewReader(signature)
sig, signer, err := openpgp.VerifyDetachedSignatureAndHash(pubKeyEntries, origText, signatureReader, allowedHashes, config)
if sig != nil && signer != nil && (errors.Is(err, pgpErrors.ErrSignatureExpired) || errors.Is(err, pgpErrors.ErrKeyExpired)) {
if verifyTime == 0 { // Expiration check disabled
return sig, nil
}
err = nil
} else {
// Maybe the creation time offset pushed it over the edge
// Retry with the actual verification time
config.Time = func() time.Time {
@ -152,10 +243,18 @@ func verifySignature(pubKeyEntries openpgp.EntityList, origText io.Reader, signa
sig, signer, err = openpgp.VerifyDetachedSignatureAndHash(pubKeyEntries, origText, signatureReader, allowedHashes, config)
}
}
if err != nil || sig == nil || signer == nil {
return nil, newSignatureFailed()
}
if verificationContext != nil {
err := verificationContext.verifyContext(sig)
if err != nil {
return nil, newSignatureFailed()
}
}
return sig, nil
}

View file

@ -13,6 +13,8 @@ import (
"github.com/stretchr/testify/assert"
)
const testMessage = "Hello world!"
const signedPlainText = "Signed message\n"
var textSignature, binSignature *PGPSignature
@ -119,7 +121,7 @@ func TestVerifyBinDetachedSig(t *testing.T) {
}
func Test_KeyRing_GetVerifiedSignatureTimestampSuccess(t *testing.T) {
message := NewPlainMessageFromString("Hello world!")
message := NewPlainMessageFromString(testMessage)
var time int64 = 1600000000
pgp.latestServerTime = time
defer func() {
@ -218,7 +220,7 @@ func getTimestampOfIssuer(signature *PGPSignature, keyID uint64) int64 {
}
func Test_KeyRing_GetVerifiedSignatureTimestampError(t *testing.T) {
message := NewPlainMessageFromString("Hello world!")
message := NewPlainMessageFromString(testMessage)
var time int64 = 1600000000
pgp.latestServerTime = time
defer func() {
@ -234,3 +236,352 @@ func Test_KeyRing_GetVerifiedSignatureTimestampError(t *testing.T) {
t.Errorf("Expected an error while parsing the creation time of a wrong signature, got nil")
}
}
func Test_SignDetachedWithNonCriticalContext(t *testing.T) {
// given
context := NewSigningContext(
"test-context",
false,
)
// when
signature, err := keyRingTestPrivate.SignDetachedWithContext(
NewPlainMessage([]byte(testMessage)),
context,
)
// then
if err != nil {
t.Fatal(err)
}
p, err := packet.Read(bytes.NewReader(signature.Data))
if err != nil {
t.Fatal(err)
}
sig, ok := p.(*packet.Signature)
if !ok {
t.Fatal("Packet was not a signature")
}
notations := sig.Notations
if len(notations) != 1 {
t.Fatal("Wrong number of notations")
}
notation := notations[0]
if notation.Name != constants.SignatureContextName {
t.Fatalf("Expected notation name to be %s, got %s", constants.SignatureContextName, notation.Name)
}
if string(notation.Value) != context.Value {
t.Fatalf("Expected notation value to be %s, got %s", context.Value, notation.Value)
}
if notation.IsCritical {
t.Fatal("Expected notation to be non critical")
}
if !notation.IsHumanReadable {
t.Fatal("Expected notation to be human readable")
}
}
func Test_SignDetachedWithCriticalContext(t *testing.T) {
// given
context := NewSigningContext(
"test-context",
true,
)
// when
signature, err := keyRingTestPrivate.SignDetachedWithContext(
NewPlainMessage([]byte(testMessage)),
context,
)
// then
if err != nil {
t.Fatal(err)
}
p, err := packet.Read(bytes.NewReader(signature.Data))
if err != nil {
t.Fatal(err)
}
sig, ok := p.(*packet.Signature)
if !ok {
t.Fatal("Packet was not a signature")
}
notations := sig.Notations
if len(notations) != 1 {
t.Fatal("Wrong number of notations")
}
notation := notations[0]
if notation.Name != constants.SignatureContextName {
t.Fatalf("Expected notation name to be %s, got %s", constants.SignatureContextName, notation.Name)
}
if string(notation.Value) != context.Value {
t.Fatalf("Expected notation value to be %s, got %s", context.Value, notation.Value)
}
if !notation.IsCritical {
t.Fatal("Expected notation to be critical")
}
if !notation.IsHumanReadable {
t.Fatal("Expected notation to be human readable")
}
}
func Test_VerifyDetachedWithUnknownCriticalContext(t *testing.T) {
// given
signatureArmored, err := ioutil.ReadFile("testdata/signature/critical_context_detached_sig")
if err != nil {
t.Fatal(err)
}
sig, err := NewPGPSignatureFromArmored(string(signatureArmored))
if err != nil {
t.Fatal(err)
}
// when
err = keyRingTestPublic.VerifyDetached(
NewPlainMessage([]byte(testMessage)),
sig,
0,
)
// then
if err == nil || !errors.Is(err, newSignatureFailed()) {
t.Fatalf("Expected a verification error")
}
}
func Test_VerifyDetachedWithUnKnownNonCriticalContext(t *testing.T) {
// given
signatureArmored, err := ioutil.ReadFile("testdata/signature/non_critical_context_detached_sig")
if err != nil {
t.Fatal(err)
}
sig, err := NewPGPSignatureFromArmored(string(signatureArmored))
if err != nil {
t.Fatal(err)
}
// when
err = keyRingTestPublic.VerifyDetached(
NewPlainMessage([]byte(testMessage)),
sig,
0,
)
// then
if err != nil {
t.Fatalf("Expected no verification error, got %v", err)
}
}
func Test_VerifyDetachedWithKnownCriticalContext(t *testing.T) {
// given
signatureArmored, err := ioutil.ReadFile("testdata/signature/critical_context_detached_sig")
if err != nil {
t.Fatal(err)
}
sig, err := NewPGPSignatureFromArmored(string(signatureArmored))
if err != nil {
t.Fatal(err)
}
verificationContext := NewVerificationContext(
"test-context",
false,
0,
)
// when
err = keyRingTestPublic.VerifyDetachedWithContext(
NewPlainMessage([]byte(testMessage)),
sig,
0,
verificationContext,
)
// then
if err != nil {
t.Fatalf("Expected no verification error, got %v", err)
}
}
func Test_VerifyDetachedWithWrongContext(t *testing.T) {
// given
signatureArmored, err := ioutil.ReadFile("testdata/signature/critical_context_detached_sig")
if err != nil {
t.Fatal(err)
}
sig, err := NewPGPSignatureFromArmored(string(signatureArmored))
if err != nil {
t.Fatal(err)
}
verificationContext := NewVerificationContext(
"another-test-context",
false,
0,
)
// when
err = keyRingTestPublic.VerifyDetachedWithContext(
NewPlainMessage([]byte(testMessage)),
sig,
0,
verificationContext,
)
// then
if err == nil || !errors.Is(err, newSignatureFailed()) {
t.Fatalf("Expected a verification error")
}
}
func Test_VerifyDetachedWithMissingNonRequiredContext(t *testing.T) {
// given
signatureArmored, err := ioutil.ReadFile("testdata/signature/no_context_detached_sig")
if err != nil {
t.Fatal(err)
}
sig, err := NewPGPSignatureFromArmored(string(signatureArmored))
if err != nil {
t.Fatal(err)
}
verificationContext := NewVerificationContext(
"test-context",
false,
0,
)
// when
err = keyRingTestPublic.VerifyDetachedWithContext(
NewPlainMessage([]byte(testMessage)),
sig,
0,
verificationContext,
)
// then
if err != nil {
t.Fatalf("Expected no verification error, got %v", err)
}
}
func Test_VerifyDetachedWithMissingRequiredContext(t *testing.T) {
// given
signatureArmored, err := ioutil.ReadFile("testdata/signature/no_context_detached_sig")
if err != nil {
t.Fatal(err)
}
sig, err := NewPGPSignatureFromArmored(string(signatureArmored))
if err != nil {
t.Fatal(err)
}
verificationContext := NewVerificationContext(
"test-context",
true,
0,
)
// when
err = keyRingTestPublic.VerifyDetachedWithContext(
NewPlainMessage([]byte(testMessage)),
sig,
0,
verificationContext,
)
// then
if err == nil || !errors.Is(err, newSignatureFailed()) {
t.Fatalf("Expected a verification error")
}
}
func Test_VerifyDetachedWithMissingRequiredContextBeforeCutoff(t *testing.T) {
// given
signatureArmored, err := ioutil.ReadFile("testdata/signature/no_context_detached_sig")
if err != nil {
t.Fatal(err)
}
sig, err := NewPGPSignatureFromArmored(string(signatureArmored))
if err != nil {
t.Fatal(err)
}
p, err := packet.Read(bytes.NewReader(sig.Data))
if err != nil {
t.Fatal(err)
}
sigPacket, ok := p.(*packet.Signature)
if !ok {
t.Fatal("Packet was not a signature")
}
verificationContext := NewVerificationContext(
"test-context",
true,
sigPacket.CreationTime.Unix()+10000,
)
// when
err = keyRingTestPublic.VerifyDetachedWithContext(
NewPlainMessage([]byte(testMessage)),
sig,
0,
verificationContext,
)
// then
if err != nil {
t.Fatalf("Expected no verification error, got %v", err)
}
}
func Test_VerifyDetachedWithMissingRequiredContextAfterCutoff(t *testing.T) {
// given
signatureArmored, err := ioutil.ReadFile("testdata/signature/no_context_detached_sig")
if err != nil {
t.Fatal(err)
}
sig, err := NewPGPSignatureFromArmored(string(signatureArmored))
if err != nil {
t.Fatal(err)
}
p, err := packet.Read(bytes.NewReader(sig.Data))
if err != nil {
t.Fatal(err)
}
sigPacket, ok := p.(*packet.Signature)
if !ok {
t.Fatal("Packet was not a signature")
}
verificationContext := NewVerificationContext(
"test-context",
true,
sigPacket.CreationTime.Unix()-10000,
)
// when
err = keyRingTestPublic.VerifyDetachedWithContext(
NewPlainMessage([]byte(testMessage)),
sig,
0,
verificationContext,
)
// then
if err == nil || !errors.Is(err, newSignatureFailed()) {
t.Fatalf("Expected a verification error")
}
}
func Test_VerifyDetachedWithDoubleContext(t *testing.T) {
// given
signatureArmored, err := ioutil.ReadFile("testdata/signature/double_critical_context_detached_sig")
if err != nil {
t.Fatal(err)
}
sig, err := NewPGPSignatureFromArmored(string(signatureArmored))
if err != nil {
t.Fatal(err)
}
verificationContext := NewVerificationContext(
"test-context",
true,
0,
)
// when
err = keyRingTestPublic.VerifyDetachedWithContext(
NewPlainMessage([]byte(testMessage)),
sig,
0,
verificationContext,
)
// then
if err == nil || !errors.Is(err, newSignatureFailed()) {
t.Fatalf("Expected a verification error")
}
}

View file

@ -0,0 +1,14 @@
-----BEGIN PGP SIGNATURE-----
Version: GopenPGP 2.5.2
Comment: https://gopenpgp.org
wsCaBAABCgBOBQJkBdTjCZA+tiWe3yHfJBYhBG6LoimwzMr2li+XlT62JZ7fId8k
JpSAAAAAABEADGNvbnRleHRAcHJvdG9uLmNodGVzdC1jb250ZXh0AACmMwgAmhVy
MIOgqeidOgNUQrOren3m53sA48dO0xmSRMd1HZa4uv5gDDisl+j98l7iawpvnQ1m
GqMvvrxyCV66h3W1efjGCW8lbGMKjaSZL4iUteRrAYCfsBq2l7yMDqFn+Kqns9f5
c29eh5mSxiGtmJsGSoJVFw7ZfDS+QpIw1yEsdYcyKLqdxmFS5pNQwY8uGuCrPaya
4iHLP52kGRt9pTSQTf8flwjb1bjTTJ/dOd3C2AVXtH7NmOgtLeLuc2bT6WKJFPwd
BYgCnD0r/6bcRqzqdhcV2lK3WtG1AitH0kKweXhPbtv9OGD36//04zGAeZY7BK8+
4J2lzLNX+pYtHPbnRw==
=XIJE
-----END PGP SIGNATURE-----

View file

@ -0,0 +1,15 @@
-----BEGIN PGP SIGNATURE-----
Version: GopenPGP 2.5.2
Comment: https://gopenpgp.org
wsDDBAABCgB3BQJkBd5+CZA+tiWe3yHfJBYhBG6LoimwzMr2li+XlT62JZ7fId8k
JpSAAAAAABEADGNvbnRleHRAcHJvdG9uLmNodGVzdC1jb250ZXh0KJSAAAAAABEA
DmNvbnRleHRAcHJvdG9uLmNodGVzdC1jb250ZXh0LTIAAGnMB/9Z9Bd0z9Q6gvBB
xxh2v/p9PleBwytUbUMaPrL4gzRsfsKF/9kShY9ZCYpzFeTVtTHGG2C8rEiCPLev
01xr3wxVq5N8iyyF9H839qwsAKomkNrqpuAtHHF76uE/vpnqRLQ+2eCiTyOh/BSH
syizwNBRaYeVtabZVXGW5ofWFoq/sgmO4Pr63hPiTmhFbIWDOZVadN1rHOVaLBPW
mlcxb7vK2FUdcyIpwsQMH9ReDNe2FiCLy/lTWyKFYO43/6VnzHtd5Gn1MXLm2tuN
zMTEGii2WFPH8u0e6sQCcmkhJS44J8y2MFv6BKrZKcRpZfzOLPzktBvFg7Q9Pvo3
1qxluPe5
=KiYX
-----END PGP SIGNATURE-----

View file

@ -0,0 +1,13 @@
-----BEGIN PGP SIGNATURE-----
Version: GopenPGP 2.5.2
Comment: https://gopenpgp.org
wsBzBAABCgAnBQJkBdkOCZA+tiWe3yHfJBYhBG6LoimwzMr2li+XlT62JZ7fId8k
AAAwOgf/U+wgABHyfI6Bd/1xPdUyy3FTaEY+Nj8NYi/PKez66OmLubgMEj0DfD7M
2P4SL3ZR0Y9iEtCKpncvLtlvA0sss0SZMaXH0bpJZS62cc98gLBuhE9mP1aWUu1u
+1AKVIvJKzhJC+MjKrVwMO03JrEb97ZDJylqoF2UvTeQomIY6qo5l4khDeZRVgsn
wqmq7+FLGHG75bhrW4dSOCKrNdKwodml/3l4/R8OPhRL6882egXfBtF0i0yhnX2s
4watN2OKQE8b9gfkrDWp0vA/hLLXx8IdIiuAkj55Dj6ciVXy6fTfKqcK4/IIX4MO
y5KD4MLQbmTja5KoK82mavsbhwXM7A==
=k4Xq
-----END PGP SIGNATURE-----

View file

@ -0,0 +1,14 @@
-----BEGIN PGP SIGNATURE-----
Version: GopenPGP 2.5.2
Comment: https://gopenpgp.org
wsCaBAABCgBOBQJkBdcDCZA+tiWe3yHfJBYhBG6LoimwzMr2li+XlT62JZ7fId8k
JhSAAAAAABEADGNvbnRleHRAcHJvdG9uLmNodGVzdC1jb250ZXh0AAAWswgAmtfD
vf7yNlc2umZ4p8ddlcQGhkpwQgiTuaYIeJytAytPtzzSAuMUcACeBCXCTt9iXaak
ImnZULdBW6T5n/o5zVTVO5yGniOeswpXqERnp+Qmsowjd5fU+XRBnkx0cSVIrVo5
tB4gf5nxAnojusQekELnNINd8nXrWYHiDFM+aos+pTxqzWlcJv32LtQ4yuxWSzIL
9dJMIpqL+1jk2QI6E+6iTM6NkwNhYjJ7emMGJXyzPmXj4pmpJ1lYo50uHRlwirnI
VXcOkUKUwGdibnCjUv+XFoG7Qv2ilDuk/TxTKSjW7ajGjv6KAOde/pOtmpiwcWKi
OzIkiswXw5vOtLkrew==
=Ub8I
-----END PGP SIGNATURE-----