#16: implement DKIM key provisioning, Ed25519 & RSA dual signatures and rotation
This commit is contained in:
1 parent
68799acea0
commit
b96be4a2a5
8 files changed
+792
No files matched your search
@@ -0,0 +1,87 @@
|
||||
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)
|
||||
Reference in new issue
Block a user