Files
data-mesher/pkg/config/signature_auth_test.go
brianmcgee b1df24ef80
buildbot/nix-eval Build done.
buildbot/nix-build Build done.
buildbot/nix-effects Build done.
Deploy docs / deploy (push) Successful in 1m14s
Merge pull request 'crypto,config: guard against nil PublicKey in decoded signatures' (#396) from fix/crypto-nil-handling into main
Reviewed-on: #396
2026-04-08 11:15:23 +00:00

486 lines
13 KiB
Go

package config_test
import (
"path"
"testing"
"time"
"git.clan.lol/clan/data-mesher/pkg/config"
"git.clan.lol/clan/data-mesher/pkg/crypto"
"git.clan.lol/clan/data-mesher/pkg/model"
"git.clan.lol/clan/data-mesher/test"
"github.com/stretchr/testify/require"
)
func newSignatureAuth(t *testing.T, network config.Network, extra config.ExtraNetworks) *config.SignatureAuth {
t.Helper()
cfg := &config.Config{
Network: network,
ExtraNetworks: extra,
}
return config.NewSignatureAuth(cfg)
}
func sig(t *testing.T, networkID *crypto.PublicKey, name string, signedBy *crypto.PublicKey) *model.Signature {
t.Helper()
return &model.Signature{
NetworkID: networkID,
Name: name,
SignedBy: signedBy,
}
}
func TestSignatureAuth_AuthorizedSigner(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 2)
signerKey, networkKey := allKeys[0], allKeys[1]
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
Files: config.Files{
"file_a": {signerKey.Public},
},
}, nil)
err := auth.Authorize(sig(t, networkKey.Public, "file_a", signerKey.Public))
as.NoError(err)
}
func TestSignatureAuth_NetworkIsImplicitSigner(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 2)
signerKey, networkKey := allKeys[0], allKeys[1]
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
Files: config.Files{
"file_a": {signerKey.Public},
},
}, nil)
// The network key itself should be an implicit signer for all files
err := auth.Authorize(sig(t, networkKey.Public, "file_a", networkKey.Public))
as.NoError(err)
}
func TestSignatureAuth_UnauthorizedSigner(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 3)
keys, networkKey := allKeys[:2], allKeys[2]
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
Files: config.Files{
"file_a": {keys[0].Public},
},
}, nil)
// keys[1] is not authorized for file_a
err := auth.Authorize(sig(t, networkKey.Public, "file_a", keys[1].Public))
as.ErrorIs(err, config.ErrUnauthorizedSigner)
}
func TestSignatureAuth_UnconfiguredFile(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 2)
signerKey, networkKey := allKeys[0], allKeys[1]
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
Files: config.Files{
"file_a": {signerKey.Public},
},
}, nil)
err := auth.Authorize(sig(t, networkKey.Public, "unknown_file", signerKey.Public))
as.ErrorIs(err, config.ErrUnauthorizedFile)
}
func TestSignatureAuth_UnknownNetwork(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 3)
signerKey, networkKey, unknownNetwork := allKeys[0], allKeys[1], allKeys[2]
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
Files: config.Files{
"file_a": {signerKey.Public},
},
}, nil)
err := auth.Authorize(sig(t, unknownNetwork.Public, "file_a", signerKey.Public))
as.ErrorIs(err, config.ErrUnauthorizedNetwork)
}
func TestSignatureAuth_ExtraNetwork(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 4)
keys, homeNetwork, extraNetwork := allKeys[:2], allKeys[2], allKeys[3]
auth := newSignatureAuth(t, config.Network{
ID: homeNetwork.Public,
Files: config.Files{
"home_file": {keys[0].Public},
},
}, config.ExtraNetworks{
"other": {
ID: extraNetwork.Public,
Files: config.Files{
"extra_file": {keys[1].Public},
},
},
})
// Home network signer authorized for home file
as.NoError(auth.Authorize(sig(t, homeNetwork.Public, "home_file", keys[0].Public)))
// Extra network signer authorized for extra file
as.NoError(auth.Authorize(sig(t, extraNetwork.Public, "extra_file", keys[1].Public)))
// Extra network key is implicit signer for its own files
as.NoError(auth.Authorize(sig(t, extraNetwork.Public, "extra_file", extraNetwork.Public)))
// Home signer not authorized for extra network file
as.ErrorIs(
auth.Authorize(sig(t, extraNetwork.Public, "extra_file", keys[0].Public)),
config.ErrUnauthorizedSigner,
)
// Extra signer not authorized for home network file
as.ErrorIs(
auth.Authorize(sig(t, homeNetwork.Public, "home_file", keys[1].Public)),
config.ErrUnauthorizedSigner,
)
// Home network key is not an implicit signer for extra network files
as.ErrorIs(
auth.Authorize(sig(t, extraNetwork.Public, "extra_file", homeNetwork.Public)),
config.ErrUnauthorizedSigner,
)
}
func TestSignatureAuth_MultipleSignersPerFile(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 4)
keys, networkKey := allKeys[:3], allKeys[3]
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
Files: config.Files{
"shared": {keys[0].Public, keys[1].Public},
},
}, nil)
as.NoError(auth.Authorize(sig(t, networkKey.Public, "shared", keys[0].Public)))
as.NoError(auth.Authorize(sig(t, networkKey.Public, "shared", keys[1].Public)))
as.ErrorIs(
auth.Authorize(sig(t, networkKey.Public, "shared", keys[2].Public)),
config.ErrUnauthorizedSigner,
)
}
func TestSignatureAuth_NoFiles(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 2)
signerKey, networkKey := allKeys[0], allKeys[1]
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
}, nil)
err := auth.Authorize(sig(t, networkKey.Public, "anything", signerKey.Public))
as.ErrorIs(err, config.ErrUnauthorizedFile)
}
// namespaceSig creates a signature for a namespace path with an optional certificate.
func namespaceSig(
t *testing.T,
networkKey *crypto.PrivateKey,
signerKey *crypto.PrivateKey,
namespace string,
cert *crypto.Certificate,
signedAt time.Time,
) *model.Signature {
t.Helper()
name := path.Join(namespace, signerKey.Public.URLEncoded())
return &model.Signature{
NetworkID: networkKey.Public,
Name: name,
SignedBy: signerKey.Public,
Certificate: cert,
SignedAt: signedAt,
}
}
func TestSignatureAuth_NamespaceAuthorized(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 2)
signerKey, networkKey := allKeys[0], allKeys[1]
now := time.Now()
cert, err := crypto.SignCertificate("peer1", signerKey.Public, networkKey, now.Add(-time.Hour), now.Add(time.Hour))
as.NoError(err)
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
Namespaces: []string{"dns"},
Files: config.Files{"placeholder": {signerKey.Public}},
}, nil)
err = auth.Authorize(namespaceSig(t, networkKey, signerKey, "dns", cert, now))
as.NoError(err)
}
func TestSignatureAuth_NamespaceRequiresCertificate(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 2)
signerKey, networkKey := allKeys[0], allKeys[1]
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
Namespaces: []string{"dns"},
Files: config.Files{"placeholder": {signerKey.Public}},
}, nil)
// no certificate provided
err := auth.Authorize(namespaceSig(t, networkKey, signerKey, "dns", nil, time.Now()))
as.ErrorIs(err, config.ErrUnauthorizedSigner)
as.ErrorContains(err, "certificate required")
}
func TestSignatureAuth_NamespaceKeyMismatch(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 3)
signerKeys, networkKey := allKeys[:2], allKeys[2]
now := time.Now()
cert, err := crypto.SignCertificate("peer1", signerKeys[0].Public, networkKey, now.Add(-time.Hour), now.Add(time.Hour))
as.NoError(err)
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
Namespaces: []string{"dns"},
Files: config.Files{"placeholder": {signerKeys[0].Public}},
}, nil)
// build a signature where the URL path has signerKeys[0] but SignedBy is signerKeys[1]
s := namespaceSig(t, networkKey, signerKeys[0], "dns", cert, now)
s.SignedBy = signerKeys[1].Public
err = auth.Authorize(s)
as.ErrorIs(err, config.ErrUnauthorizedSigner)
as.ErrorContains(err, "signer key does not match namespace path")
}
func TestSignatureAuth_NamespaceCertNotSignedByNetwork(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 3)
signerKey, networkKey, otherNetworkKey := allKeys[0], allKeys[1], allKeys[2]
now := time.Now()
// certificate signed by a different network key
cert, err := crypto.SignCertificate(
"peer1", signerKey.Public, otherNetworkKey, now.Add(-time.Hour), now.Add(time.Hour),
)
as.NoError(err)
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
Namespaces: []string{"dns"},
Files: config.Files{"placeholder": {signerKey.Public}},
}, nil)
err = auth.Authorize(namespaceSig(t, networkKey, signerKey, "dns", cert, now))
as.ErrorIs(err, config.ErrUnauthorizedSigner)
as.ErrorContains(err, "certificate verification failed")
}
func TestSignatureAuth_NamespaceNotConfigured(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 2)
signerKey, networkKey := allKeys[0], allKeys[1]
now := time.Now()
cert, err := crypto.SignCertificate("peer1", signerKey.Public, networkKey, now.Add(-time.Hour), now.Add(time.Hour))
as.NoError(err)
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
Namespaces: []string{"dns"},
Files: config.Files{"placeholder": {signerKey.Public}},
}, nil)
// "metrics" is not a configured namespace
err = auth.Authorize(namespaceSig(t, networkKey, signerKey, "metrics", cert, now))
as.ErrorIs(err, config.ErrUnauthorizedFile)
}
func TestSignatureAuth_NamespaceExpiredCertificate(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 2)
signerKey, networkKey := allKeys[0], allKeys[1]
now := time.Now()
// certificate valid from 2 hours ago to 1 hour ago (expired)
cert, err := crypto.SignCertificate("peer1", signerKey.Public, networkKey, now.Add(-2*time.Hour), now.Add(-time.Hour))
as.NoError(err)
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
Namespaces: []string{"dns"},
Files: config.Files{"placeholder": {signerKey.Public}},
}, nil)
// SignedAt is now, but certificate expired an hour ago — wall-clock check rejects it
err = auth.Authorize(namespaceSig(t, networkKey, signerKey, "dns", cert, now))
as.ErrorIs(err, config.ErrUnauthorizedSigner)
as.ErrorContains(err, "certificate verification failed")
}
func TestSignatureAuth_NamespaceExtraNetwork(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 3)
signerKey, homeKey, extraKey := allKeys[0], allKeys[1], allKeys[2]
now := time.Now()
// certificate signed by extra network
cert, err := crypto.SignCertificate("peer1", signerKey.Public, extraKey, now.Add(-time.Hour), now.Add(time.Hour))
as.NoError(err)
auth := newSignatureAuth(t, config.Network{
ID: homeKey.Public,
Files: config.Files{"home_file": {signerKey.Public}},
}, config.ExtraNetworks{
"extra": {
ID: extraKey.Public,
Namespaces: []string{"shared"},
Files: config.Files{"extra_file": {signerKey.Public}},
},
})
// namespace on extra network should work
err = auth.Authorize(namespaceSig(t, extraKey, signerKey, "shared", cert, now))
as.NoError(err)
// same namespace name on home network should fail (not configured there)
homeCert, err := crypto.SignCertificate("peer1", signerKey.Public, homeKey, now.Add(-time.Hour), now.Add(time.Hour))
as.NoError(err)
err = auth.Authorize(namespaceSig(t, homeKey, signerKey, "shared", homeCert, now))
as.ErrorIs(err, config.ErrUnauthorizedFile)
}
func TestSignatureAuth_StaticAndNamespaceBothWork(t *testing.T) {
t.Parallel()
as := require.New(t)
allKeys := test.GenerateKeys(t, 2)
signerKey, networkKey := allKeys[0], allKeys[1]
now := time.Now()
cert, err := crypto.SignCertificate("peer1", signerKey.Public, networkKey, now.Add(-time.Hour), now.Add(time.Hour))
as.NoError(err)
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
Files: config.Files{"static_file": {signerKey.Public}},
Namespaces: []string{"dynamic"},
}, nil)
// static file authorization works
as.NoError(auth.Authorize(sig(t, networkKey.Public, "static_file", signerKey.Public)))
// namespace authorization works
as.NoError(auth.Authorize(namespaceSig(t, networkKey, signerKey, "dynamic", cert, now)))
}
func TestSignatureAuth_NilNetworkID(t *testing.T) {
t.Parallel()
as := require.New(t)
keys := test.GenerateKeys(t, 2)
networkKey, signerKey := keys[0], keys[1]
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
Files: config.Files{
"file_a": {signerKey.Public},
},
}, nil)
err := auth.Authorize(sig(t, nil, "file_a", signerKey.Public))
as.ErrorIs(err, config.ErrUnauthorizedSigner)
}
func TestSignatureAuth_NilSignedBy(t *testing.T) {
t.Parallel()
as := require.New(t)
keys := test.GenerateKeys(t, 2)
networkKey := keys[0]
auth := newSignatureAuth(t, config.Network{
ID: networkKey.Public,
Files: config.Files{
"file_a": nil,
},
}, nil)
err := auth.Authorize(sig(t, networkKey.Public, "file_a", nil))
as.ErrorIs(err, config.ErrUnauthorizedSigner)
}