Files
netbird/client/internal/updatemanager/reposign/revocation_test.go
2025-12-19 19:57:39 +01:00

861 lines
21 KiB
Go

package reposign
import (
"crypto/ed25519"
"crypto/rand"
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Test RevocationList marshaling/unmarshaling
func TestRevocationList_MarshalJSON(t *testing.T) {
pub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
keyID := computeKeyID(pub)
revokedTime := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)
lastUpdated := time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC)
expiresAt := time.Date(2024, 4, 15, 11, 0, 0, 0, time.UTC)
rl := &RevocationList{
Revoked: map[KeyID]time.Time{
keyID: revokedTime,
},
LastUpdated: lastUpdated,
ExpiresAt: expiresAt,
}
jsonData, err := json.Marshal(rl)
require.NoError(t, err)
// Verify it can be unmarshaled back
var decoded map[string]interface{}
err = json.Unmarshal(jsonData, &decoded)
require.NoError(t, err)
assert.Contains(t, decoded, "revoked")
assert.Contains(t, decoded, "last_updated")
assert.Contains(t, decoded, "expires_at")
}
func TestRevocationList_UnmarshalJSON(t *testing.T) {
pub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
keyID := computeKeyID(pub)
revokedTime := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)
lastUpdated := time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC)
jsonData := map[string]interface{}{
"revoked": map[string]string{
keyID.String(): revokedTime.Format(time.RFC3339),
},
"last_updated": lastUpdated.Format(time.RFC3339),
}
jsonBytes, err := json.Marshal(jsonData)
require.NoError(t, err)
var rl RevocationList
err = json.Unmarshal(jsonBytes, &rl)
require.NoError(t, err)
assert.Len(t, rl.Revoked, 1)
assert.Contains(t, rl.Revoked, keyID)
assert.Equal(t, lastUpdated.Unix(), rl.LastUpdated.Unix())
}
func TestRevocationList_MarshalUnmarshal_Roundtrip(t *testing.T) {
pub1, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
pub2, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
keyID1 := computeKeyID(pub1)
keyID2 := computeKeyID(pub2)
original := &RevocationList{
Revoked: map[KeyID]time.Time{
keyID1: time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC),
keyID2: time.Date(2024, 2, 20, 14, 45, 0, 0, time.UTC),
},
LastUpdated: time.Date(2024, 2, 20, 15, 0, 0, 0, time.UTC),
}
// Marshal
jsonData, err := original.MarshalJSON()
require.NoError(t, err)
// Unmarshal
var decoded RevocationList
err = decoded.UnmarshalJSON(jsonData)
require.NoError(t, err)
// Verify
assert.Len(t, decoded.Revoked, 2)
assert.Equal(t, original.Revoked[keyID1].Unix(), decoded.Revoked[keyID1].Unix())
assert.Equal(t, original.Revoked[keyID2].Unix(), decoded.Revoked[keyID2].Unix())
assert.Equal(t, original.LastUpdated.Unix(), decoded.LastUpdated.Unix())
}
func TestRevocationList_UnmarshalJSON_InvalidKeyID(t *testing.T) {
jsonData := []byte(`{
"revoked": {
"invalid_key_id": "2024-01-15T10:30:00Z"
},
"last_updated": "2024-01-15T11:00:00Z"
}`)
var rl RevocationList
err := json.Unmarshal(jsonData, &rl)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse KeyID")
}
func TestRevocationList_EmptyRevoked(t *testing.T) {
rl := &RevocationList{
Revoked: make(map[KeyID]time.Time),
LastUpdated: time.Now().UTC(),
}
jsonData, err := rl.MarshalJSON()
require.NoError(t, err)
var decoded RevocationList
err = decoded.UnmarshalJSON(jsonData)
require.NoError(t, err)
assert.Empty(t, decoded.Revoked)
assert.NotNil(t, decoded.Revoked)
}
// Test ParseRevocationList
func TestParseRevocationList_Valid(t *testing.T) {
pub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
keyID := computeKeyID(pub)
revokedTime := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)
lastUpdated := time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC)
rl := RevocationList{
Revoked: map[KeyID]time.Time{
keyID: revokedTime,
},
LastUpdated: lastUpdated,
ExpiresAt: time.Date(2025, 2, 20, 14, 45, 0, 0, time.UTC),
}
jsonData, err := rl.MarshalJSON()
require.NoError(t, err)
parsed, err := ParseRevocationList(jsonData)
require.NoError(t, err)
assert.NotNil(t, parsed)
assert.Len(t, parsed.Revoked, 1)
assert.Equal(t, lastUpdated.Unix(), parsed.LastUpdated.Unix())
}
func TestParseRevocationList_InvalidJSON(t *testing.T) {
invalidJSON := []byte("not valid json")
_, err := ParseRevocationList(invalidJSON)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to unmarshal")
}
func TestParseRevocationList_MissingLastUpdated(t *testing.T) {
jsonData := []byte(`{
"revoked": {}
}`)
_, err := ParseRevocationList(jsonData)
assert.Error(t, err)
assert.Contains(t, err.Error(), "missing last_updated")
}
func TestParseRevocationList_EmptyObject(t *testing.T) {
jsonData := []byte(`{}`)
_, err := ParseRevocationList(jsonData)
assert.Error(t, err)
assert.Contains(t, err.Error(), "missing last_updated")
}
func TestParseRevocationList_NilRevoked(t *testing.T) {
lastUpdated := time.Now().UTC()
expiresAt := lastUpdated.Add(90 * 24 * time.Hour)
jsonData := []byte(`{
"last_updated": "` + lastUpdated.Format(time.RFC3339) + `",
"expires_at": "` + expiresAt.Format(time.RFC3339) + `"
}`)
parsed, err := ParseRevocationList(jsonData)
require.NoError(t, err)
assert.NotNil(t, parsed.Revoked)
assert.Empty(t, parsed.Revoked)
}
func TestParseRevocationList_MissingExpiresAt(t *testing.T) {
lastUpdated := time.Now().UTC()
jsonData := []byte(`{
"revoked": {},
"last_updated": "` + lastUpdated.Format(time.RFC3339) + `"
}`)
_, err := ParseRevocationList(jsonData)
assert.Error(t, err)
assert.Contains(t, err.Error(), "missing expires_at")
}
// Test ValidateRevocationList
func TestValidateRevocationList_Valid(t *testing.T) {
// Generate root key
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
rootKey := RootKey{
PrivateKey{
Key: rootPriv,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
rootKeys := []PublicKey{
{
Key: rootPub,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
// Create revocation list
rlData, sigData, err := CreateRevocationList(rootKey, defaultRevocationListExpiration)
require.NoError(t, err)
signature, err := ParseSignature(sigData)
require.NoError(t, err)
// Validate
rl, err := ValidateRevocationList(rootKeys, rlData, *signature)
require.NoError(t, err)
assert.NotNil(t, rl)
assert.Empty(t, rl.Revoked)
}
func TestValidateRevocationList_InvalidSignature(t *testing.T) {
// Generate root key
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
rootKey := RootKey{
PrivateKey{
Key: rootPriv,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
rootKeys := []PublicKey{
{
Key: rootPub,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
// Create revocation list
rlData, _, err := CreateRevocationList(rootKey, defaultRevocationListExpiration)
require.NoError(t, err)
// Create invalid signature
invalidSig := Signature{
Signature: make([]byte, 64),
Timestamp: time.Now().UTC(),
KeyID: computeKeyID(rootPub),
Algorithm: "ed25519",
HashAlgo: "sha512",
}
// Validate should fail
_, err = ValidateRevocationList(rootKeys, rlData, invalidSig)
assert.Error(t, err)
assert.Contains(t, err.Error(), "verification failed")
}
func TestValidateRevocationList_FutureTimestamp(t *testing.T) {
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
rootKey := RootKey{
PrivateKey{
Key: rootPriv,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
rootKeys := []PublicKey{
{
Key: rootPub,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
rlData, sigData, err := CreateRevocationList(rootKey, defaultRevocationListExpiration)
require.NoError(t, err)
signature, err := ParseSignature(sigData)
require.NoError(t, err)
// Modify timestamp to be in the future
signature.Timestamp = time.Now().UTC().Add(10 * time.Minute)
_, err = ValidateRevocationList(rootKeys, rlData, *signature)
assert.Error(t, err)
assert.Contains(t, err.Error(), "in the future")
}
func TestValidateRevocationList_TooOld(t *testing.T) {
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
rootKey := RootKey{
PrivateKey{
Key: rootPriv,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
rootKeys := []PublicKey{
{
Key: rootPub,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
rlData, sigData, err := CreateRevocationList(rootKey, defaultRevocationListExpiration)
require.NoError(t, err)
signature, err := ParseSignature(sigData)
require.NoError(t, err)
// Modify timestamp to be too old
signature.Timestamp = time.Now().UTC().Add(-20 * 365 * 24 * time.Hour)
_, err = ValidateRevocationList(rootKeys, rlData, *signature)
assert.Error(t, err)
assert.Contains(t, err.Error(), "too old")
}
func TestValidateRevocationList_InvalidJSON(t *testing.T) {
rootPub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
rootKeys := []PublicKey{
{
Key: rootPub,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
signature := Signature{
Signature: make([]byte, 64),
Timestamp: time.Now().UTC(),
KeyID: computeKeyID(rootPub),
Algorithm: "ed25519",
HashAlgo: "sha512",
}
_, err = ValidateRevocationList(rootKeys, []byte("invalid json"), signature)
assert.Error(t, err)
}
func TestValidateRevocationList_FutureLastUpdated(t *testing.T) {
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
rootKey := RootKey{
PrivateKey{
Key: rootPriv,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
rootKeys := []PublicKey{
{
Key: rootPub,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
// Create revocation list with future LastUpdated
rl := RevocationList{
Revoked: make(map[KeyID]time.Time),
LastUpdated: time.Now().UTC().Add(10 * time.Minute),
ExpiresAt: time.Now().UTC().Add(365 * 24 * time.Hour),
}
rlData, err := json.Marshal(rl)
require.NoError(t, err)
// Sign it
sig, err := signRevocationList(rootKey, rl)
require.NoError(t, err)
_, err = ValidateRevocationList(rootKeys, rlData, *sig)
assert.Error(t, err)
assert.Contains(t, err.Error(), "LastUpdated is in the future")
}
func TestValidateRevocationList_TimestampMismatch(t *testing.T) {
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
rootKey := RootKey{
PrivateKey{
Key: rootPriv,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
rootKeys := []PublicKey{
{
Key: rootPub,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
// Create revocation list with LastUpdated far in the past
rl := RevocationList{
Revoked: make(map[KeyID]time.Time),
LastUpdated: time.Now().UTC().Add(-1 * time.Hour),
ExpiresAt: time.Now().UTC().Add(365 * 24 * time.Hour),
}
rlData, err := json.Marshal(rl)
require.NoError(t, err)
// Sign it with current timestamp
sig, err := signRevocationList(rootKey, rl)
require.NoError(t, err)
// Modify signature timestamp to differ too much from LastUpdated
sig.Timestamp = time.Now().UTC()
_, err = ValidateRevocationList(rootKeys, rlData, *sig)
assert.Error(t, err)
assert.Contains(t, err.Error(), "differs too much")
}
func TestValidateRevocationList_Expired(t *testing.T) {
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
rootKey := RootKey{
PrivateKey{
Key: rootPriv,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
rootKeys := []PublicKey{
{
Key: rootPub,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
// Create revocation list that expired in the past
now := time.Now().UTC()
rl := RevocationList{
Revoked: make(map[KeyID]time.Time),
LastUpdated: now.Add(-100 * 24 * time.Hour),
ExpiresAt: now.Add(-10 * 24 * time.Hour), // Expired 10 days ago
}
rlData, err := json.Marshal(rl)
require.NoError(t, err)
// Sign it
sig, err := signRevocationList(rootKey, rl)
require.NoError(t, err)
// Adjust signature timestamp to match LastUpdated
sig.Timestamp = rl.LastUpdated
_, err = ValidateRevocationList(rootKeys, rlData, *sig)
assert.Error(t, err)
assert.Contains(t, err.Error(), "expired")
}
func TestValidateRevocationList_ExpiresAtTooFarInFuture(t *testing.T) {
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
rootKey := RootKey{
PrivateKey{
Key: rootPriv,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
rootKeys := []PublicKey{
{
Key: rootPub,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
// Create revocation list with ExpiresAt too far in the future (beyond maxRevocationSignatureAge)
now := time.Now().UTC()
rl := RevocationList{
Revoked: make(map[KeyID]time.Time),
LastUpdated: now,
ExpiresAt: now.Add(15 * 365 * 24 * time.Hour), // 15 years in the future
}
rlData, err := json.Marshal(rl)
require.NoError(t, err)
// Sign it
sig, err := signRevocationList(rootKey, rl)
require.NoError(t, err)
_, err = ValidateRevocationList(rootKeys, rlData, *sig)
assert.Error(t, err)
assert.Contains(t, err.Error(), "too far in the future")
}
// Test CreateRevocationList
func TestCreateRevocationList_Valid(t *testing.T) {
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
rootKey := RootKey{
PrivateKey{
Key: rootPriv,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
rlData, sigData, err := CreateRevocationList(rootKey, defaultRevocationListExpiration)
require.NoError(t, err)
assert.NotEmpty(t, rlData)
assert.NotEmpty(t, sigData)
// Verify it can be parsed
rl, err := ParseRevocationList(rlData)
require.NoError(t, err)
assert.Empty(t, rl.Revoked)
assert.False(t, rl.LastUpdated.IsZero())
// Verify signature can be parsed
sig, err := ParseSignature(sigData)
require.NoError(t, err)
assert.NotEmpty(t, sig.Signature)
}
// Test ExtendRevocationList
func TestExtendRevocationList_AddKey(t *testing.T) {
// Generate root key
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
rootKey := RootKey{
PrivateKey{
Key: rootPriv,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
// Create empty revocation list
rlData, _, err := CreateRevocationList(rootKey, defaultRevocationListExpiration)
require.NoError(t, err)
rl, err := ParseRevocationList(rlData)
require.NoError(t, err)
assert.Empty(t, rl.Revoked)
// Generate a key to revoke
revokedPub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
revokedKeyID := computeKeyID(revokedPub)
// Extend the revocation list
newRLData, newSigData, err := ExtendRevocationList(rootKey, *rl, revokedKeyID, defaultRevocationListExpiration)
require.NoError(t, err)
// Verify the new list
newRL, err := ParseRevocationList(newRLData)
require.NoError(t, err)
assert.Len(t, newRL.Revoked, 1)
assert.Contains(t, newRL.Revoked, revokedKeyID)
// Verify signature
sig, err := ParseSignature(newSigData)
require.NoError(t, err)
assert.NotEmpty(t, sig.Signature)
}
func TestExtendRevocationList_MultipleKeys(t *testing.T) {
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
rootKey := RootKey{
PrivateKey{
Key: rootPriv,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
// Create empty revocation list
rlData, _, err := CreateRevocationList(rootKey, defaultRevocationListExpiration)
require.NoError(t, err)
rl, err := ParseRevocationList(rlData)
require.NoError(t, err)
// Add first key
key1Pub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
key1ID := computeKeyID(key1Pub)
rlData, _, err = ExtendRevocationList(rootKey, *rl, key1ID, defaultRevocationListExpiration)
require.NoError(t, err)
rl, err = ParseRevocationList(rlData)
require.NoError(t, err)
assert.Len(t, rl.Revoked, 1)
// Add second key
key2Pub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
key2ID := computeKeyID(key2Pub)
rlData, _, err = ExtendRevocationList(rootKey, *rl, key2ID, defaultRevocationListExpiration)
require.NoError(t, err)
rl, err = ParseRevocationList(rlData)
require.NoError(t, err)
assert.Len(t, rl.Revoked, 2)
assert.Contains(t, rl.Revoked, key1ID)
assert.Contains(t, rl.Revoked, key2ID)
}
func TestExtendRevocationList_DuplicateKey(t *testing.T) {
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
rootKey := RootKey{
PrivateKey{
Key: rootPriv,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
// Create empty revocation list
rlData, _, err := CreateRevocationList(rootKey, defaultRevocationListExpiration)
require.NoError(t, err)
rl, err := ParseRevocationList(rlData)
require.NoError(t, err)
// Add a key
keyPub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
keyID := computeKeyID(keyPub)
rlData, _, err = ExtendRevocationList(rootKey, *rl, keyID, defaultRevocationListExpiration)
require.NoError(t, err)
rl, err = ParseRevocationList(rlData)
require.NoError(t, err)
firstRevocationTime := rl.Revoked[keyID]
// Wait a bit
time.Sleep(10 * time.Millisecond)
// Add the same key again
rlData, _, err = ExtendRevocationList(rootKey, *rl, keyID, defaultRevocationListExpiration)
require.NoError(t, err)
rl, err = ParseRevocationList(rlData)
require.NoError(t, err)
assert.Len(t, rl.Revoked, 1)
// The revocation time should be updated
secondRevocationTime := rl.Revoked[keyID]
assert.True(t, secondRevocationTime.After(firstRevocationTime) || secondRevocationTime.Equal(firstRevocationTime))
}
func TestExtendRevocationList_UpdatesLastUpdated(t *testing.T) {
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
rootKey := RootKey{
PrivateKey{
Key: rootPriv,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
// Create revocation list
rlData, _, err := CreateRevocationList(rootKey, defaultRevocationListExpiration)
require.NoError(t, err)
rl, err := ParseRevocationList(rlData)
require.NoError(t, err)
firstLastUpdated := rl.LastUpdated
// Wait a bit
time.Sleep(10 * time.Millisecond)
// Extend list
keyPub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
keyID := computeKeyID(keyPub)
rlData, _, err = ExtendRevocationList(rootKey, *rl, keyID, defaultRevocationListExpiration)
require.NoError(t, err)
rl, err = ParseRevocationList(rlData)
require.NoError(t, err)
// LastUpdated should be updated
assert.True(t, rl.LastUpdated.After(firstLastUpdated))
}
// Integration test
func TestRevocationList_FullWorkflow(t *testing.T) {
// Create root key
rootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
rootKey := RootKey{
PrivateKey{
Key: rootPriv,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
rootKeys := []PublicKey{
{
Key: rootPub,
Metadata: KeyMetadata{
ID: computeKeyID(rootPub),
CreatedAt: time.Now().UTC(),
},
},
}
// Step 1: Create empty revocation list
rlData, sigData, err := CreateRevocationList(rootKey, defaultRevocationListExpiration)
require.NoError(t, err)
// Step 2: Validate it
sig, err := ParseSignature(sigData)
require.NoError(t, err)
rl, err := ValidateRevocationList(rootKeys, rlData, *sig)
require.NoError(t, err)
assert.Empty(t, rl.Revoked)
// Step 3: Revoke a key
revokedPub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
revokedKeyID := computeKeyID(revokedPub)
rlData, sigData, err = ExtendRevocationList(rootKey, *rl, revokedKeyID, defaultRevocationListExpiration)
require.NoError(t, err)
// Step 4: Validate the extended list
sig, err = ParseSignature(sigData)
require.NoError(t, err)
rl, err = ValidateRevocationList(rootKeys, rlData, *sig)
require.NoError(t, err)
assert.Len(t, rl.Revoked, 1)
assert.Contains(t, rl.Revoked, revokedKeyID)
// Step 5: Verify the revocation time is reasonable
revTime := rl.Revoked[revokedKeyID]
now := time.Now().UTC()
assert.True(t, revTime.Before(now) || revTime.Equal(now))
assert.True(t, now.Sub(revTime) < time.Minute)
}