mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 08:16:39 +00:00
861 lines
21 KiB
Go
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)
|
|
}
|