Refuse a signature made when its key was not valid (closes #209)
check / check (push) Waiting to run

verifySignature lets a key that has expired since verify a signature,
because a manifest outlives its key, but it checked the key only as of
now. It accepted a signature dated after its key expired, after the
key's self-signature or the signing subkey's binding signature expired,
or before the key was made. It now verifies a second time with the
library's clock set to the signature's creation time and refuses the
signature if the key or subkey was not valid then. docs/FORMAT.md
states the rule.

Judgement call: the tests make the refused signatures with the
library's packet API, because openpgp.DetachSign will not sign with a
key that is not valid at its clock's time.

Model: opus-5-5
This commit is contained in:
2026-10-13 18:35:09 +00:00
parent 24496880c5
commit 1bd1455a7b
3 changed files with 155 additions and 8 deletions
+7 -3
View File
@@ -174,9 +174,13 @@ also refuses a manifest whose `signingPubKey` holds a DSA key or subkey, or any
secret key or subkey, since checking the self-signatures of a DSA key or the secret key or subkey, since checking the self-signatures of a DSA key or the
numbers of a secret key can take hours when those numbers are very large. numbers of a secret key can take hours when those numbers are very large.
A signing key that has expired does not make the signature bad, because a A signature is good only if the key that made it was valid at the signature's
manifest outlives its key. A signature past its own expiry time is not good, creation time, meaning the key had been made by then and neither it nor its
whether or not the key has expired too. self-signature had expired. When a subkey made the signature, the same must hold
of the primary key, and of the subkey and its binding signature. A key that has
expired since does not make the signature bad, because a manifest outlives its
key. A signature past its own expiry time is not good, whether or not the key
has expired too.
## Deterministic Serialization ## Deterministic Serialization
+22 -3
View File
@@ -57,7 +57,9 @@ var (
"signature must hold exactly one signature") "signature must hold exactly one signature")
errNotOneArmoredBlock = errors.New( errNotOneArmoredBlock = errors.New(
"must be exactly one armored block and nothing else") "must be exactly one armored block and nothing else")
errMalformedArmor = errors.New("armor is malformed") errMalformedArmor = errors.New("armor is malformed")
errKeyNotValidWhenSigned = errors.New(
"signature was made when its key was not valid")
) )
// SigningOptions holds the key a manifest is signed with. // SigningOptions holds the key a manifest is signed with.
@@ -245,8 +247,9 @@ func fingerprint(key *openpgp.Entity) string {
// verifySignature checks that signature is one good, unexpired OpenPGP // verifySignature checks that signature is one good, unexpired OpenPGP
// signature over data, made by the one primary key in pubKey or one of its // signature over data, made by the one primary key in pubKey or one of its
// subkeys, and returns that primary key's fingerprint. signature and pubKey // subkeys while that key was valid, and returns that primary key's
// may each be armored or binary. // fingerprint. The key may have expired since. signature and pubKey may
// each be armored or binary.
func verifySignature(data, signature, pubKey []byte) (string, error) { func verifySignature(data, signature, pubKey []byte) (string, error) {
key, err := readOneKey(pubKey, "embedded public key block", true) key, err := readOneKey(pubKey, "embedded public key block", true)
if err != nil { if err != nil {
@@ -281,6 +284,22 @@ func verifySignature(data, signature, pubKey []byte) (string, error) {
return "", fmt.Errorf("verify signature: %w", err) return "", fmt.Errorf("verify signature: %w", err)
} }
// The library checks whether the key is valid at the time its config
// gives, which above is now. Verifying again as of the signature's
// creation time refuses a signature made before the key or subkey that
// made it was made, after it expired, or after its self-signature or
// binding signature expired.
signedAt := &packet.Config{
Time: func() time.Time { return sig.CreationTime },
}
_, _, err = openpgp.VerifyDetachedSignature(openpgp.EntityList{key},
bytes.NewReader(data), bytes.NewReader(sigData), signedAt)
if err != nil {
return "", fmt.Errorf("verify signature: %w: %w",
errKeyNotValidWhenSigned, err)
}
return fingerprint(key), nil return fingerprint(key), nil
} }
+126 -2
View File
@@ -4,6 +4,7 @@ package mfer
import ( import (
"bytes" "bytes"
"context" "context"
"crypto"
"crypto/dsa" //nolint:staticcheck // SA1019: tests need a DSA key to refuse "crypto/dsa" //nolint:staticcheck // SA1019: tests need a DSA key to refuse
"io" "io"
"math/big" "math/big"
@@ -419,8 +420,9 @@ func TestVerifySignature(t *testing.T) {
// TestVerifySignatureKeyExpiredSince verifies two signatures made in 2020 // TestVerifySignatureKeyExpiredSince verifies two signatures made in 2020
// by a key that expired a day after it was made. A signature with no // by a key that expired a day after it was made. A signature with no
// expiry time is still good. One that expired an hour after it was made is // expiry time is still good, as is one made then by a signing subkey that
// refused, although the key has expired too. // expired a day after it was made. One that expired an hour after it was
// made is refused, although the key has expired too.
func TestVerifySignatureKeyExpiredSince(t *testing.T) { func TestVerifySignatureKeyExpiredSince(t *testing.T) {
t.Parallel() t.Parallel()
@@ -448,10 +450,132 @@ func TestVerifySignatureKeyExpiredSince(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, fingerprint(key), signer) assert.Equal(t, fingerprint(key), signer)
withSubkey := newTestKey(t, &packet.Config{Time: config.Time})
require.NoError(t, withSubkey.AddSigningSubkey(&packet.Config{
Algorithm: packet.PubKeyAlgoEdDSA, Time: config.Time,
KeyLifetimeSecs: config.KeyLifetimeSecs,
}))
var subkeySig bytes.Buffer
require.NoError(t, openpgp.ArmoredDetachSign(&subkeySig, withSubkey,
bytes.NewReader(data), config))
subkeyPubKey, err := armoredPublicKey(withSubkey)
require.NoError(t, err)
_, err = verifySignature(data, subkeySig.Bytes(), subkeyPubKey)
require.NoError(t, err)
_, err = verifySignature(data, expiringSig.Bytes(), pubKey) _, err = verifySignature(data, expiringSig.Bytes(), pubKey)
require.ErrorIs(t, err, pgperrors.ErrSignatureExpired) require.ErrorIs(t, err, pgperrors.ErrSignatureExpired)
} }
// signDated returns a detached signature over data by signer, dated when.
// openpgp.DetachSign signs only with a key that is valid at the time its
// config gives, so this makes the signature packet without that check.
func signDated(t *testing.T, signer *packet.PrivateKey, data []byte,
when time.Time,
) []byte {
t.Helper()
sig := &packet.Signature{
SigType: packet.SigTypeBinary,
PubKeyAlgo: signer.PubKeyAlgo,
Hash: crypto.SHA256,
CreationTime: when,
IssuerKeyId: &signer.KeyId,
}
h, err := sig.PrepareSign(nil)
require.NoError(t, err)
_, err = h.Write(data)
require.NoError(t, err)
require.NoError(t, sig.Sign(h, signer, nil))
var buf bytes.Buffer
require.NoError(t, sig.Serialize(&buf))
return buf.Bytes()
}
// TestVerifySignatureRefusesKeyNotValidWhenSigned verifies signatures
// dated when the key or subkey that made them was not valid: after the key
// expired; after the key's self-signature, or the signing subkey's binding
// signature, expired, the key or subkey having expired since as well; and
// before the key was made, also by a key made in 2100. Each must be
// refused.
func TestVerifySignatureRefusesKeyNotValidWhenSigned(t *testing.T) {
t.Parallel()
made := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
twoDaysLater := made.Add(48 * time.Hour)
day := uint32((24 * time.Hour).Seconds())
clock := func(when time.Time) func() time.Time {
return func() time.Time { return when }
}
expiredAfterADay := newTestKey(t, &packet.Config{
Time: clock(made), KeyLifetimeSecs: day,
})
selfSignatureExpiredAfterADay := newTestKey(t, &packet.Config{
Time: clock(made), KeyLifetimeSecs: 30 * day, SigLifetimeSecs: day,
})
withSubkey := newTestKey(t, &packet.Config{Time: clock(made)})
require.NoError(t, withSubkey.AddSigningSubkey(&packet.Config{
Algorithm: packet.PubKeyAlgoEdDSA, Time: clock(made),
KeyLifetimeSecs: 30 * day, SigLifetimeSecs: day,
}))
subkey := withSubkey.Subkeys[len(withSubkey.Subkeys)-1].PrivateKey
madeADayLater := newTestKey(t, &packet.Config{
Time: clock(made.Add(24 * time.Hour)),
})
madeIn2100 := newTestKey(t, &packet.Config{
Time: clock(time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC)),
})
for name, test := range map[string]struct {
key *openpgp.Entity
signer *packet.PrivateKey
when time.Time
}{
"after the key expired": {
expiredAfterADay, expiredAfterADay.PrivateKey, twoDaysLater,
},
"after the self-signature expired": {
selfSignatureExpiredAfterADay,
selfSignatureExpiredAfterADay.PrivateKey, twoDaysLater,
},
"after the binding signature expired": {
withSubkey, subkey, twoDaysLater,
},
"before the key was made": {
madeADayLater, madeADayLater.PrivateKey, made,
},
"before a key made in 2100": {
madeIn2100, madeIn2100.PrivateKey, made,
},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
data := []byte("signed in 2020")
pubKey, err := armoredPublicKey(test.key)
require.NoError(t, err)
_, err = verifySignature(data,
signDated(t, test.signer, data, test.when), pubKey)
require.ErrorIs(t, err, errKeyNotValidWhenSigned)
})
}
}
func TestManifestSignatureVerification(t *testing.T) { func TestManifestSignatureVerification(t *testing.T) {
t.Parallel() t.Parallel()