163 lines
4.4 KiB
Go
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
|
|
}
|