Inital Commit
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
import configparser
|
||||
import argparse
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
CONFIG_PATH = "/etc/pyarc/milter.conf"
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Schlüsselgenerator für den Custom ARC Milter")
|
||||
parser.add_argument("domain", help="Die Domain, für die ein ARC-Schlüsselpaar generiert werden soll")
|
||||
args = parser.parse_args()
|
||||
|
||||
domain = args.domain.lower().strip()
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
if not os.path.exists(CONFIG_PATH):
|
||||
print(f"Fehler: Konfigurationsdatei {CONFIG_PATH} nicht gefunden.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config.read(CONFIG_PATH)
|
||||
|
||||
if not config.has_section(domain):
|
||||
print(f"Fehler: Domain '{domain}' ist nicht in der {CONFIG_PATH} eingetragen.", file=sys.stderr)
|
||||
print("Bitte lege zuerst die Sektion für die Domain in der Config an. Beispiel:", file=sys.stderr)
|
||||
print(f"\n[{domain}]\nselector = arc2026\nprivate_key_path = /etc/pyarc/certs/{domain}.private.key\n", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
selector = config.get(domain, "selector", fallback="arc2026")
|
||||
key_path = config.get(domain, "private_key_path")
|
||||
|
||||
if os.path.exists(key_path):
|
||||
print(f"Abbruch: Key-Datei existiert bereits unter: {key_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Generiere 2048-Bit RSA Schlüssel für {domain}...")
|
||||
|
||||
try:
|
||||
private_key = rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=2048
|
||||
)
|
||||
|
||||
log_dir = os.path.dirname(key_path)
|
||||
if log_dir:
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
pem = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
|
||||
with open(key_path, "wb") as f:
|
||||
f.write(pem)
|
||||
|
||||
os.chmod(key_path, 0o640)
|
||||
|
||||
if os.path.exists("/etc/pyarc"):
|
||||
stat_info = os.stat("/etc/pyarc")
|
||||
os.chown(key_path, stat_info.st_uid, stat_info.st_gid)
|
||||
|
||||
print(f"✔ Private Key erfolgreich gespeichert unter: {key_path}")
|
||||
|
||||
public_key = private_key.public_key()
|
||||
pub_pem = public_key.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
|
||||
pub_lines = pub_pem.decode('utf-8').splitlines()
|
||||
pub_key_clean = "".join(pub_lines[1:-1])
|
||||
|
||||
print("\n" + "="*60)
|
||||
print(f"DEIN DNS-TXT-RECORD FÜR {domain}:")
|
||||
print("="*60)
|
||||
print(f"{selector}._domainkey.{domain}. IN TXT \"v=DKIM1; k=rsa; p={pub_key_clean}\"")
|
||||
print("="*60 + "\n")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ein unerwarteter Fehler ist aufgetreten: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
import Milter
|
||||
import dkim
|
||||
import authres
|
||||
import sys
|
||||
import configparser
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
from io import BytesIO
|
||||
|
||||
CONFIG_PATH = "/etc/pyarc/milter.conf"
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
if not os.path.exists(CONFIG_PATH):
|
||||
print(f"Fehler: Konfigurationsdatei nicht gefunden unter {CONFIG_PATH}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config.read(CONFIG_PATH)
|
||||
|
||||
# --- Logging Setup ---
|
||||
LOG_ENABLED = config.getboolean("logging", "file_logging", fallback=False)
|
||||
LOG_FILE = config.get("logging", "log_file", fallback="/var/log/pyarc/pyarc.log")
|
||||
|
||||
logger = logging.getLogger("pyarc_milter")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
stdout_handler = logging.StreamHandler(sys.stdout)
|
||||
stdout_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||
stdout_handler.setFormatter(stdout_formatter)
|
||||
logger.addHandler(stdout_handler)
|
||||
|
||||
if LOG_ENABLED:
|
||||
log_dir = os.path.dirname(LOG_FILE)
|
||||
if not os.path.exists(log_dir):
|
||||
try:
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Konnte Log-Verzeichnis {log_dir} nicht erstellen: {e}")
|
||||
|
||||
if os.path.exists(log_dir):
|
||||
try:
|
||||
file_handler = logging.FileHandler(LOG_FILE)
|
||||
file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - [%(process)d] - %(message)s')
|
||||
file_handler.setFormatter(file_formatter)
|
||||
logger.addHandler(file_handler)
|
||||
except Exception as e:
|
||||
logger.error(f"Konnte Log-Datei {LOG_FILE} nicht öffnen: {e}")
|
||||
|
||||
# --- Allgemeine Config laden ---
|
||||
AUTH_SERV_ID = config.get("general", "auth_serv_id", fallback="mx01.domain.tld")
|
||||
LISTEN_SOCKET = config.get("general", "listen_socket", fallback="inet:8899@127.0.0.1")
|
||||
REJECT_ON_FAIL = config.getboolean("validation", "reject_on_fail", fallback=False)
|
||||
|
||||
DOMAIN_REGEX = re.compile(r'@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})')
|
||||
|
||||
|
||||
class ArcMilter(Milter.Base):
|
||||
def __init__(self):
|
||||
self.id = Milter.uniqueID()
|
||||
self.headers = []
|
||||
self.body_buffer = BytesIO()
|
||||
self.from_domain = None
|
||||
|
||||
def header(self, name, hval):
|
||||
self.headers.append((name.encode('utf-8'), hval.encode('utf-8')))
|
||||
if name.lower() == 'from':
|
||||
match = DOMAIN_REGEX.search(hval)
|
||||
if match:
|
||||
self.from_domain = match.group(1).lower().strip()
|
||||
return Milter.CONTINUE
|
||||
|
||||
def body(self, chunk):
|
||||
self.body_buffer.write(chunk)
|
||||
return Milter.CONTINUE
|
||||
|
||||
def eom(self):
|
||||
self.body_buffer.seek(0)
|
||||
full_mail = b"".join([b"%s: %s\r\n" % (k, v) for k, v in self.headers]) + b"\r\n" + self.body_buffer.read()
|
||||
|
||||
try:
|
||||
# --- 1. Eingehend: ARC Validierung ---
|
||||
arc_verifier = dkim.ARC(full_mail)
|
||||
cv, results, comment = arc_verifier.verify()
|
||||
cv_str = cv.decode('utf-8') if isinstance(cv, bytes) else str(cv)
|
||||
|
||||
logger.info(f"[{self.id}] Validierung für Domain '{self.from_domain}': CV={cv_str} ({comment})")
|
||||
self.addheader('Authentication-Results', f"{AUTH_SERV_ID}; arc={cv_str}")
|
||||
|
||||
is_arc_valid = (cv_str == "pass")
|
||||
|
||||
if cv_str == "fail" and REJECT_ON_FAIL:
|
||||
logger.warning(f"[{self.id}] Mail abgewiesen: ARC Validation fehlgeschlagen.")
|
||||
self.setreply('550', '5.7.1', 'ARC validation failed')
|
||||
return Milter.REJECT
|
||||
|
||||
# --- 2. Ausgehend: Dynamische ARC Signierung ---
|
||||
if self.from_domain and config.has_section(self.from_domain):
|
||||
selector = config.get(self.from_domain, "selector")
|
||||
key_path = config.get(self.from_domain, "private_key_path")
|
||||
|
||||
if os.path.exists(key_path):
|
||||
with open(key_path, "rb") as f:
|
||||
private_key = f.read()
|
||||
|
||||
sig_headers = dkim.arc_sign(
|
||||
full_mail,
|
||||
selector=selector.encode('utf-8'),
|
||||
domain=self.from_domain.encode('utf-8'),
|
||||
privkey=private_key,
|
||||
srv_id=AUTH_SERV_ID.encode('utf-8')
|
||||
)
|
||||
|
||||
for header_line in sig_headers:
|
||||
if b':' in header_line:
|
||||
name, val = header_line.split(b':', 1)
|
||||
self.addheader(name.decode('utf-8').strip(), val.decode('utf-8').strip())
|
||||
|
||||
logger.info(f"[{self.id}] Mail für {self.from_domain} erfolgreich signiert (Selector: {selector}).")
|
||||
is_arc_valid = True
|
||||
else:
|
||||
logger.error(f"[{self.id}] Key für {self.from_domain} nicht gefunden unter {key_path}")
|
||||
|
||||
self.addheader('X-ARC', 'TRUE' if is_arc_valid else 'FALSE')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.id}] Fehler beim ARC-Processing: {e}", exc_info=True)
|
||||
|
||||
return Milter.CONTINUE
|
||||
|
||||
def main():
|
||||
Milter.factory = ArcMilter
|
||||
logger.info(f"Multidomain ARC Milter startet auf {LISTEN_SOCKET}...")
|
||||
Milter.runmilter("ArcMilter", LISTEN_SOCKET, timeout=30)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user