Refuse a signature made when its key was not valid (closes #209)
check / check (push) Waiting to run
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:
+7
-3
@@ -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
@@ -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
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user