101 lines
3.3 KiB
Go
101 lines
3.3 KiB
Go
package usecase
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
)
|
|
|
|
// DkimKeyProvisioner is the storage side of key provisioning. It is declared
|
|
// here, next to its consumer, rather than in the shared port package because
|
|
// only this use case needs the write path.
|
|
type DkimKeyProvisioner interface {
|
|
FindPublicKey(ctx context.Context, domainName, algorithm string) (string, error)
|
|
Insert(ctx context.Context, domainName, selector, algorithm string, privateKeyPEM []byte, publicKey string) error
|
|
}
|
|
|
|
// DkimKeyGenerator produces a fresh key pair for an algorithm.
|
|
type DkimKeyGenerator func(algorithm string) (privateKeyPEM []byte, publicKeyBase64 string, err error)
|
|
|
|
// Selectors match the names published by the DNS reconciler
|
|
// (PLAN.md section 7.1, records 5 and 6).
|
|
const (
|
|
RsaDkimSelector = "default"
|
|
Ed25519DkimSelector = "default-ed"
|
|
|
|
rsaAlgorithm = "rsa2048"
|
|
ed25519Algorithm = "ed25519"
|
|
)
|
|
|
|
// ProvisionDkimKeysOutput carries the published halves, which the DNS
|
|
// reconciler needs to build the TXT records.
|
|
type ProvisionDkimKeysOutput struct {
|
|
RsaPublicKey string
|
|
Ed25519PublicKey string
|
|
Created []string
|
|
}
|
|
|
|
// ProvisionDkimKeysUseCase makes sure a domain has one active key per
|
|
// algorithm before it is asked to sign anything. It is idempotent: running it
|
|
// on a domain that already has keys returns the existing public halves and
|
|
// creates nothing.
|
|
type ProvisionDkimKeysUseCase struct {
|
|
keys DkimKeyProvisioner
|
|
generate DkimKeyGenerator
|
|
}
|
|
|
|
func NewProvisionDkimKeysUseCase(keys DkimKeyProvisioner, generate DkimKeyGenerator) *ProvisionDkimKeysUseCase {
|
|
return &ProvisionDkimKeysUseCase{keys: keys, generate: generate}
|
|
}
|
|
|
|
func (uc *ProvisionDkimKeysUseCase) Execute(ctx context.Context, domainName string) (*ProvisionDkimKeysOutput, error) {
|
|
if domainName == "" {
|
|
return nil, fmt.Errorf("provision dkim keys: domain name is required")
|
|
}
|
|
|
|
out := &ProvisionDkimKeysOutput{}
|
|
|
|
rsaKey, err := uc.ensure(ctx, domainName, rsaAlgorithm, RsaDkimSelector, out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out.RsaPublicKey = rsaKey
|
|
|
|
edKey, err := uc.ensure(ctx, domainName, ed25519Algorithm, Ed25519DkimSelector, out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out.Ed25519PublicKey = edKey
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func (uc *ProvisionDkimKeysUseCase) ensure(ctx context.Context, domainName, algorithm, selector string, out *ProvisionDkimKeysOutput) (string, error) {
|
|
existing, err := uc.keys.FindPublicKey(ctx, domainName, algorithm)
|
|
if err != nil {
|
|
return "", fmt.Errorf("look up %s key for %s: %w", algorithm, domainName, err)
|
|
}
|
|
if existing != "" {
|
|
return existing, nil
|
|
}
|
|
|
|
privateKeyPEM, publicKey, err := uc.generate(algorithm)
|
|
if err != nil {
|
|
return "", fmt.Errorf("generate %s key for %s: %w", algorithm, domainName, err)
|
|
}
|
|
|
|
if err := uc.keys.Insert(ctx, domainName, selector, algorithm, privateKeyPEM, publicKey); err != nil {
|
|
return "", fmt.Errorf("store %s key for %s: %w", algorithm, domainName, err)
|
|
}
|
|
|
|
// Re-read rather than trusting the generated value: a concurrent
|
|
// provisioner may have won the insert, and the DNS record has to carry
|
|
// whichever key actually became active.
|
|
stored, err := uc.keys.FindPublicKey(ctx, domainName, algorithm)
|
|
if err != nil {
|
|
return "", fmt.Errorf("confirm %s key for %s: %w", algorithm, domainName, err)
|
|
}
|
|
|
|
out.Created = append(out.Created, algorithm)
|
|
return stored, nil
|
|
}
|