mirror of
https://github.com/netbirdio/netbird.git
synced 2026-03-31 06:24:18 -04:00
Auto-update logic moved out of the UI into a dedicated updatemanager.Manager service that runs in the connection layer. The UI no longer polls or checks for updates independently. The update manager supports three modes driven by the management server's auto-update policy: No policy set by mgm: checks GitHub for the latest version and notifies the user (previous behavior, now centralized) mgm enforces update: the "About" menu triggers installation directly instead of just downloading the file — user still initiates the action mgm forces update: installation proceeds automatically without user interaction updateManager lifecycle is now owned by daemon, giving the daemon server direct control via a new TriggerUpdate RPC Introduces EngineServices struct to group external service dependencies passed to NewEngine, reducing its argument count from 11 to 4
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)
|
|
}
|