mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-19 00:36:38 +00:00
477 lines
13 KiB
Go
477 lines
13 KiB
Go
package reposign
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Test RootKey.String()
|
|
|
|
func TestRootKey_String(t *testing.T) {
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
createdAt := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)
|
|
expiresAt := time.Date(2034, 1, 15, 10, 30, 0, 0, time.UTC)
|
|
|
|
rk := RootKey{
|
|
PrivateKey{
|
|
Key: priv,
|
|
Metadata: KeyMetadata{
|
|
ID: computeKeyID(pub),
|
|
CreatedAt: createdAt,
|
|
ExpiresAt: expiresAt,
|
|
},
|
|
},
|
|
}
|
|
|
|
str := rk.String()
|
|
assert.Contains(t, str, "RootKey")
|
|
assert.Contains(t, str, computeKeyID(pub).String())
|
|
assert.Contains(t, str, "2024-01-15")
|
|
assert.Contains(t, str, "2034-01-15")
|
|
}
|
|
|
|
func TestRootKey_String_NoExpiration(t *testing.T) {
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
createdAt := time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)
|
|
|
|
rk := RootKey{
|
|
PrivateKey{
|
|
Key: priv,
|
|
Metadata: KeyMetadata{
|
|
ID: computeKeyID(pub),
|
|
CreatedAt: createdAt,
|
|
ExpiresAt: time.Time{}, // No expiration
|
|
},
|
|
},
|
|
}
|
|
|
|
str := rk.String()
|
|
assert.Contains(t, str, "RootKey")
|
|
assert.Contains(t, str, "0001-01-01") // Zero time format
|
|
}
|
|
|
|
// Test GenerateRootKey
|
|
|
|
func TestGenerateRootKey_Valid(t *testing.T) {
|
|
expiration := 10 * 365 * 24 * time.Hour // 10 years
|
|
|
|
rk, privPEM, pubPEM, err := GenerateRootKey(expiration)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, rk)
|
|
assert.NotEmpty(t, privPEM)
|
|
assert.NotEmpty(t, pubPEM)
|
|
|
|
// Verify the key has correct metadata
|
|
assert.False(t, rk.Metadata.CreatedAt.IsZero())
|
|
assert.False(t, rk.Metadata.ExpiresAt.IsZero())
|
|
assert.True(t, rk.Metadata.ExpiresAt.After(rk.Metadata.CreatedAt))
|
|
|
|
// Verify expiration is approximately correct
|
|
expectedExpiration := time.Now().Add(expiration)
|
|
timeDiff := rk.Metadata.ExpiresAt.Sub(expectedExpiration)
|
|
assert.True(t, timeDiff < time.Minute && timeDiff > -time.Minute)
|
|
}
|
|
|
|
func TestGenerateRootKey_ShortExpiration(t *testing.T) {
|
|
expiration := 24 * time.Hour // 1 day
|
|
|
|
rk, _, _, err := GenerateRootKey(expiration)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, rk)
|
|
|
|
// Verify expiration
|
|
expectedExpiration := time.Now().Add(expiration)
|
|
timeDiff := rk.Metadata.ExpiresAt.Sub(expectedExpiration)
|
|
assert.True(t, timeDiff < time.Minute && timeDiff > -time.Minute)
|
|
}
|
|
|
|
func TestGenerateRootKey_ZeroExpiration(t *testing.T) {
|
|
rk, _, _, err := GenerateRootKey(0)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, rk)
|
|
|
|
// With zero expiration, ExpiresAt should be equal to CreatedAt
|
|
assert.Equal(t, rk.Metadata.CreatedAt, rk.Metadata.ExpiresAt)
|
|
}
|
|
|
|
func TestGenerateRootKey_PEMFormat(t *testing.T) {
|
|
rk, privPEM, pubPEM, err := GenerateRootKey(365 * 24 * time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
// Verify private key PEM
|
|
privBlock, _ := pem.Decode(privPEM)
|
|
require.NotNil(t, privBlock)
|
|
assert.Equal(t, tagRootPrivate, privBlock.Type)
|
|
|
|
var privKey PrivateKey
|
|
err = json.Unmarshal(privBlock.Bytes, &privKey)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, rk.Key, privKey.Key)
|
|
|
|
// Verify public key PEM
|
|
pubBlock, _ := pem.Decode(pubPEM)
|
|
require.NotNil(t, pubBlock)
|
|
assert.Equal(t, tagRootPublic, pubBlock.Type)
|
|
|
|
var pubKey PublicKey
|
|
err = json.Unmarshal(pubBlock.Bytes, &pubKey)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, rk.Metadata.ID, pubKey.Metadata.ID)
|
|
}
|
|
|
|
func TestGenerateRootKey_KeySize(t *testing.T) {
|
|
rk, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
// Ed25519 private key should be 64 bytes
|
|
assert.Equal(t, ed25519.PrivateKeySize, len(rk.Key))
|
|
|
|
// Ed25519 public key should be 32 bytes
|
|
pubKey := rk.Key.Public().(ed25519.PublicKey)
|
|
assert.Equal(t, ed25519.PublicKeySize, len(pubKey))
|
|
}
|
|
|
|
func TestGenerateRootKey_UniqueKeys(t *testing.T) {
|
|
rk1, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
rk2, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
// Different keys should have different IDs
|
|
assert.NotEqual(t, rk1.Metadata.ID, rk2.Metadata.ID)
|
|
assert.NotEqual(t, rk1.Key, rk2.Key)
|
|
}
|
|
|
|
// Test ParseRootKey
|
|
|
|
func TestParseRootKey_Valid(t *testing.T) {
|
|
original, privPEM, _, err := GenerateRootKey(365 * 24 * time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
parsed, err := ParseRootKey(privPEM)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, parsed)
|
|
|
|
// Verify the parsed key matches the original
|
|
assert.Equal(t, original.Key, parsed.Key)
|
|
assert.Equal(t, original.Metadata.ID, parsed.Metadata.ID)
|
|
assert.Equal(t, original.Metadata.CreatedAt.Unix(), parsed.Metadata.CreatedAt.Unix())
|
|
assert.Equal(t, original.Metadata.ExpiresAt.Unix(), parsed.Metadata.ExpiresAt.Unix())
|
|
}
|
|
|
|
func TestParseRootKey_InvalidPEM(t *testing.T) {
|
|
_, err := ParseRootKey([]byte("not a valid PEM"))
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "failed to parse")
|
|
}
|
|
|
|
func TestParseRootKey_EmptyData(t *testing.T) {
|
|
_, err := ParseRootKey([]byte{})
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestParseRootKey_WrongType(t *testing.T) {
|
|
// Generate an artifact key instead of root key
|
|
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
artifactKey, privPEM, _, _, err := GenerateArtifactKey(rootKey, 30*24*time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
// Try to parse artifact key as root key
|
|
_, err = ParseRootKey(privPEM)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "PEM type")
|
|
|
|
// Just to use artifactKey to avoid unused variable warning
|
|
_ = artifactKey
|
|
}
|
|
|
|
func TestParseRootKey_CorruptedJSON(t *testing.T) {
|
|
// Create PEM with corrupted JSON
|
|
corruptedPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: tagRootPrivate,
|
|
Bytes: []byte("corrupted json data"),
|
|
})
|
|
|
|
_, err := ParseRootKey(corruptedPEM)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestParseRootKey_InvalidKeySize(t *testing.T) {
|
|
// Create a key with invalid size
|
|
invalidKey := PrivateKey{
|
|
Key: []byte{0x01, 0x02, 0x03}, // Too short
|
|
Metadata: KeyMetadata{
|
|
ID: KeyID{},
|
|
CreatedAt: time.Now().UTC(),
|
|
},
|
|
}
|
|
|
|
privJSON, err := json.Marshal(invalidKey)
|
|
require.NoError(t, err)
|
|
|
|
invalidPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: tagRootPrivate,
|
|
Bytes: privJSON,
|
|
})
|
|
|
|
_, err = ParseRootKey(invalidPEM)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "incorrect Ed25519 private key size")
|
|
}
|
|
|
|
func TestParseRootKey_Roundtrip(t *testing.T) {
|
|
// Generate a key
|
|
original, privPEM, _, err := GenerateRootKey(365 * 24 * time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
// Parse it
|
|
parsed, err := ParseRootKey(privPEM)
|
|
require.NoError(t, err)
|
|
|
|
// Generate PEM again from parsed key
|
|
privJSON2, err := json.Marshal(parsed.PrivateKey)
|
|
require.NoError(t, err)
|
|
|
|
privPEM2 := pem.EncodeToMemory(&pem.Block{
|
|
Type: tagRootPrivate,
|
|
Bytes: privJSON2,
|
|
})
|
|
|
|
// Parse again
|
|
parsed2, err := ParseRootKey(privPEM2)
|
|
require.NoError(t, err)
|
|
|
|
// Should still match original
|
|
assert.Equal(t, original.Key, parsed2.Key)
|
|
assert.Equal(t, original.Metadata.ID, parsed2.Metadata.ID)
|
|
}
|
|
|
|
// Test SignArtifactKey
|
|
|
|
func TestSignArtifactKey_Valid(t *testing.T) {
|
|
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
data := []byte("test data to sign")
|
|
sigData, err := SignArtifactKey(*rootKey, data)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, sigData)
|
|
|
|
// Parse and verify signature
|
|
sig, err := ParseSignature(sigData)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, sig.Signature)
|
|
assert.Equal(t, rootKey.Metadata.ID, sig.KeyID)
|
|
assert.Equal(t, "ed25519", sig.Algorithm)
|
|
assert.Equal(t, "sha512", sig.HashAlgo)
|
|
assert.False(t, sig.Timestamp.IsZero())
|
|
}
|
|
|
|
func TestSignArtifactKey_EmptyData(t *testing.T) {
|
|
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
sigData, err := SignArtifactKey(*rootKey, []byte{})
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, sigData)
|
|
|
|
// Should still be able to parse
|
|
sig, err := ParseSignature(sigData)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, sig.Signature)
|
|
}
|
|
|
|
func TestSignArtifactKey_Verify(t *testing.T) {
|
|
rootKey, _, pubPEM, err := GenerateRootKey(365 * 24 * time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
// Parse public key
|
|
pubKey, _, err := parsePublicKey(pubPEM, tagRootPublic)
|
|
require.NoError(t, err)
|
|
|
|
// Sign some data
|
|
data := []byte("test data for verification")
|
|
sigData, err := SignArtifactKey(*rootKey, data)
|
|
require.NoError(t, err)
|
|
|
|
// Parse signature
|
|
sig, err := ParseSignature(sigData)
|
|
require.NoError(t, err)
|
|
|
|
// Reconstruct message
|
|
msg := make([]byte, 0, len(data)+8)
|
|
msg = append(msg, data...)
|
|
msg = binary.LittleEndian.AppendUint64(msg, uint64(sig.Timestamp.Unix()))
|
|
|
|
// Verify signature
|
|
valid := ed25519.Verify(pubKey.Key, msg, sig.Signature)
|
|
assert.True(t, valid)
|
|
}
|
|
|
|
func TestSignArtifactKey_DifferentData(t *testing.T) {
|
|
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
data1 := []byte("data1")
|
|
data2 := []byte("data2")
|
|
|
|
sig1, err := SignArtifactKey(*rootKey, data1)
|
|
require.NoError(t, err)
|
|
|
|
sig2, err := SignArtifactKey(*rootKey, data2)
|
|
require.NoError(t, err)
|
|
|
|
// Different data should produce different signatures
|
|
assert.NotEqual(t, sig1, sig2)
|
|
}
|
|
|
|
func TestSignArtifactKey_MultipleSignatures(t *testing.T) {
|
|
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
data := []byte("test data")
|
|
|
|
// Sign twice with a small delay
|
|
sig1, err := SignArtifactKey(*rootKey, data)
|
|
require.NoError(t, err)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
sig2, err := SignArtifactKey(*rootKey, data)
|
|
require.NoError(t, err)
|
|
|
|
// Signatures should be different due to different timestamps
|
|
assert.NotEqual(t, sig1, sig2)
|
|
|
|
// Parse both signatures
|
|
parsed1, err := ParseSignature(sig1)
|
|
require.NoError(t, err)
|
|
|
|
parsed2, err := ParseSignature(sig2)
|
|
require.NoError(t, err)
|
|
|
|
// Timestamps should be different
|
|
assert.True(t, parsed2.Timestamp.After(parsed1.Timestamp))
|
|
}
|
|
|
|
func TestSignArtifactKey_LargeData(t *testing.T) {
|
|
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
// Create 1MB of data
|
|
largeData := make([]byte, 1024*1024)
|
|
for i := range largeData {
|
|
largeData[i] = byte(i % 256)
|
|
}
|
|
|
|
sigData, err := SignArtifactKey(*rootKey, largeData)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, sigData)
|
|
|
|
// Verify signature can be parsed
|
|
sig, err := ParseSignature(sigData)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, sig.Signature)
|
|
}
|
|
|
|
func TestSignArtifactKey_TimestampInSignature(t *testing.T) {
|
|
rootKey, _, _, err := GenerateRootKey(365 * 24 * time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
beforeSign := time.Now().UTC()
|
|
data := []byte("test data")
|
|
sigData, err := SignArtifactKey(*rootKey, data)
|
|
require.NoError(t, err)
|
|
afterSign := time.Now().UTC()
|
|
|
|
sig, err := ParseSignature(sigData)
|
|
require.NoError(t, err)
|
|
|
|
// Timestamp should be between before and after
|
|
assert.True(t, sig.Timestamp.After(beforeSign.Add(-time.Second)))
|
|
assert.True(t, sig.Timestamp.Before(afterSign.Add(time.Second)))
|
|
}
|
|
|
|
// Integration test
|
|
|
|
func TestRootKey_FullWorkflow(t *testing.T) {
|
|
// Step 1: Generate root key
|
|
rootKey, privPEM, pubPEM, err := GenerateRootKey(10 * 365 * 24 * time.Hour)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, rootKey)
|
|
assert.NotEmpty(t, privPEM)
|
|
assert.NotEmpty(t, pubPEM)
|
|
|
|
// Step 2: Parse the private key back
|
|
parsedRootKey, err := ParseRootKey(privPEM)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, rootKey.Key, parsedRootKey.Key)
|
|
assert.Equal(t, rootKey.Metadata.ID, parsedRootKey.Metadata.ID)
|
|
|
|
// Step 3: Generate an artifact key using root key
|
|
artifactKey, _, artifactPubPEM, artifactSig, err := GenerateArtifactKey(rootKey, 30*24*time.Hour)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, artifactKey)
|
|
|
|
// Step 4: Verify the artifact key signature
|
|
pubKey, _, err := parsePublicKey(pubPEM, tagRootPublic)
|
|
require.NoError(t, err)
|
|
|
|
sig, err := ParseSignature(artifactSig)
|
|
require.NoError(t, err)
|
|
|
|
artifactPubKey, _, err := parsePublicKey(artifactPubPEM, tagArtifactPublic)
|
|
require.NoError(t, err)
|
|
|
|
// Reconstruct message - SignArtifactKey signs the PEM, not the JSON
|
|
msg := make([]byte, 0, len(artifactPubPEM)+8)
|
|
msg = append(msg, artifactPubPEM...)
|
|
msg = binary.LittleEndian.AppendUint64(msg, uint64(sig.Timestamp.Unix()))
|
|
|
|
// Verify with root public key
|
|
valid := ed25519.Verify(pubKey.Key, msg, sig.Signature)
|
|
assert.True(t, valid, "Artifact key signature should be valid")
|
|
|
|
// Step 5: Use artifact key to sign data
|
|
testData := []byte("This is test artifact data")
|
|
dataSig, err := SignData(*artifactKey, testData)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, dataSig)
|
|
|
|
// Step 6: Verify the artifact data signature
|
|
dataSigParsed, err := ParseSignature(dataSig)
|
|
require.NoError(t, err)
|
|
|
|
err = ValidateArtifact([]PublicKey{artifactPubKey}, testData, *dataSigParsed)
|
|
assert.NoError(t, err, "Artifact data signature should be valid")
|
|
}
|
|
|
|
func TestRootKey_ExpiredKeyWorkflow(t *testing.T) {
|
|
// Generate a root key that expires very soon
|
|
rootKey, _, _, err := GenerateRootKey(1 * time.Millisecond)
|
|
require.NoError(t, err)
|
|
|
|
// Wait for expiration
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
// Try to generate artifact key with expired root key
|
|
_, _, _, _, err = GenerateArtifactKey(rootKey, 30*24*time.Hour)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "expired")
|
|
}
|