Files
LambdaMail/services/protocols/internal/infrastructure/dkim/signer.go
T

88 lines
2.5 KiB
Go

package dkim
import (
"bytes"
"context"
"fmt"
"log"
msgauth "github.com/emersion/go-msgauth/dkim"
"lambdamail/protocols/internal/application/port"
)
// signedHeaders are the headers covered by the signature. Signing a header
// that is absent is harmless, but signing one that a forwarder may add later
// would break the signature, so the list stays close to RFC 6376 appendix D.
var signedHeaders = []string{
"From", "To", "Cc", "Subject", "Date", "Message-ID",
"MIME-Version", "Content-Type", "Content-Transfer-Encoding",
"In-Reply-To", "References",
}
// Signer applies every active key of a domain to an outbound message.
type Signer struct {
keys port.DkimKeyRepository
}
func NewSigner(keys port.DkimKeyRepository) *Signer {
return &Signer{keys: keys}
}
// Sign adds one DKIM-Signature header per active key. PLAN.md section 5 signs
// with Ed25519 and RSA at the same time so that receivers which do not yet
// validate Ed25519 still see a passing RSA signature.
//
// A key that fails to sign is logged and skipped rather than failing the
// send: delivering with one signature, or none, beats not delivering.
func (s *Signer) Sign(ctx context.Context, domainName string, message []byte) ([]byte, error) {
if s.keys == nil {
return message, nil
}
keys, err := s.keys.FindActiveKeys(ctx, domainName)
if err != nil {
return nil, fmt.Errorf("load dkim keys for %s: %w", domainName, err)
}
signed := message
for _, key := range keys {
next, err := signOnce(signed, key)
if err != nil {
log.Printf("dkim: skipping %s signature for %s: %v", key.Algorithm, domainName, err)
continue
}
signed = next
}
return signed, nil
}
func signOnce(message []byte, key port.DkimSigningKey) ([]byte, error) {
signer, err := ParsePrivateKey(key.PrivateKeyPEM)
if err != nil {
return nil, err
}
options := &msgauth.SignOptions{
Domain: key.DomainName,
Selector: key.Selector,
Signer: signer,
HeaderKeys: signedHeaders,
HeaderCanonicalization: msgauth.CanonicalizationRelaxed,
BodyCanonicalization: msgauth.CanonicalizationRelaxed,
}
// Hash is left at its zero value on purpose: SHA-256 is the only hash
// either algorithm admits (RFC 6376 for RSA, RFC 8463 for Ed25519), and
// the library selects it per key type.
var out bytes.Buffer
if err := msgauth.Sign(&out, bytes.NewReader(message), options); err != nil {
return nil, err
}
return out.Bytes(), nil
}
// Ensure port interface compliance
var _ port.DkimSigner = (*Signer)(nil)