Files
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

163 lines
4.4 KiB
Go

package config
import (
"errors"
"fmt"
"slices"
"git.clan.lol/clan/data-mesher/pkg/model"
)
var (
ErrUnauthorizedFile = errors.New("file not found")
ErrUnauthorizedNetwork = errors.New("network not found")
ErrUnauthorizedSigner = errors.New("unauthorized signer")
)
type signersByFile map[string][]string
type signersByNetwork map[string]signersByFile
// namespacesByNetwork maps networkID (string) -> set of namespace names.
type namespacesByNetwork map[string]map[string]bool
type SignatureAuth struct {
// map of network ids to a superset of signers across all configured files
signers signersByNetwork
// map of network ids to configured namespace names
namespaces namespacesByNetwork
}
func NewSignatureAuth(cfg *Config) *SignatureAuth {
// index the set of signers by network id
// we convert everything to string to make comparison easier later
signers := make(signersByNetwork)
namespaces := make(namespacesByNetwork)
// start with our home network
signers[cfg.Network.ID.String()] = signersForNetwork(&cfg.Network)
namespaces[cfg.Network.ID.String()] = namespacesForNetwork(&cfg.Network)
// do the same for each extra network
for _, network := range cfg.ExtraNetworks {
signers[network.ID.String()] = signersForNetwork(&network)
namespaces[network.ID.String()] = namespacesForNetwork(&network)
}
return &SignatureAuth{
signers: signers,
namespaces: namespaces,
}
}
func (sf *SignatureAuth) Authorize(sig *model.Signature) error {
if sig == nil {
return errors.New("signature is nil")
}
if sig.NetworkID == nil || sig.SignedBy == nil {
return ErrUnauthorizedSigner
}
// try static file authorization first
err := sf.authorizeStatic(sig)
if err == nil {
return nil
}
// if the file wasn't found in static config, try namespace authorization
if errors.Is(err, ErrUnauthorizedFile) {
return sf.authorizeNamespace(sig)
}
return err
}
func (sf *SignatureAuth) authorizeStatic(sig *model.Signature) error {
forNetwork, ok := sf.signers[sig.NetworkID.String()]
if !ok {
return ErrUnauthorizedNetwork
}
forFile, ok := forNetwork[sig.Name]
if !ok {
return ErrUnauthorizedFile
}
if !slices.Contains(forFile, sig.SignedBy.String()) {
return ErrUnauthorizedSigner
}
return nil
}
func (sf *SignatureAuth) authorizeNamespace(sig *model.Signature) error {
// parse the name as a namespace path
namespace, signerKey, err := model.ParseNamespacePath(sig.Name)
if err != nil {
return ErrUnauthorizedFile
}
// look up namespaces for this network
forNetwork, ok := sf.namespaces[sig.NetworkID.String()]
if !ok {
return ErrUnauthorizedNetwork
}
// check the namespace is configured on this network
if !forNetwork[namespace] {
return ErrUnauthorizedFile
}
// the URL-encoded key in the path must match the signer
if !signerKey.Equal(sig.SignedBy) {
return fmt.Errorf("%w: signer key does not match namespace path", ErrUnauthorizedSigner)
}
// certificate is required for namespace authorization
if sig.Certificate == nil {
return fmt.Errorf("%w: certificate required for namespace files", ErrUnauthorizedSigner)
}
// the certificate's identity key must match the signer
if !sig.Certificate.IdentityKey.Equal(sig.SignedBy) {
return fmt.Errorf("%w: certificate identity key does not match signer", ErrUnauthorizedSigner)
}
// the certificate must be signed by the network and currently valid
if err := sig.Certificate.Verify(sig.NetworkID); err != nil {
return fmt.Errorf("%w: certificate verification failed: %w", ErrUnauthorizedSigner, err)
}
// the signing time must be within the certificate's validity period
if sig.SignedAt.Before(sig.Certificate.NotBefore) || sig.SignedAt.After(sig.Certificate.NotAfter) {
return fmt.Errorf("%w: signature time outside certificate validity", ErrUnauthorizedSigner)
}
return nil
}
func signersForNetwork(network *Network) signersByFile {
signersForFile := make(signersByFile)
for name, fileSigners := range network.Files {
for _, fileSigner := range fileSigners {
signersForFile[name] = append(signersForFile[name], fileSigner.String())
}
// network id is an implicit signer of all files
signersForFile[name] = append(signersForFile[name], network.ID.String())
}
return signersForFile
}
func namespacesForNetwork(network *Network) map[string]bool {
result := make(map[string]bool, len(network.Namespaces))
for _, ns := range network.Namespaces {
result[ns] = true
}
return result
}