88 lines
2.5 KiB
Go
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)
|