This commit is contained in:
parent
2436894589
commit
eb5758bd3b
@ -17,7 +17,10 @@ import time
|
||||
import uuid
|
||||
|
||||
|
||||
DOMAINS = ('solit.cn', 'mail.solit.cn', 'smtp.solit.cn', 'imap.solit.cn', 'pop.solit.cn')
|
||||
DOMAINS = ('solit.cn', '*.solit.cn')
|
||||
VERIFY_HOSTNAMES = ('solit.cn', 'web.solit.cn', 'mail.solit.cn', 'smtp.solit.cn',
|
||||
'imap.solit.cn', 'pop.solit.cn')
|
||||
CERTIFICATE_SNI = 'web.solit.cn'
|
||||
LINEAGE = Path('/var/lib/smail-acme-tls/certificates')
|
||||
TARGET = Path('/opt/smail/ssl')
|
||||
BACKUPS = Path('/var/backups')
|
||||
@ -96,7 +99,13 @@ def replace_file(path, data, uid, gid):
|
||||
|
||||
|
||||
def validate(directory):
|
||||
for domain in DOMAINS:
|
||||
names = run('openssl', 'x509', '-in', str(directory / 'candidate.crt'), '-noout',
|
||||
'-ext', 'subjectAltName').decode('utf-8')
|
||||
certificate_domains = {item.strip()[4:] for line in names.splitlines()
|
||||
for item in line.split(',') if item.strip().startswith('DNS:')}
|
||||
if not set(DOMAINS).issubset(certificate_domains):
|
||||
raise RuntimeError('证书必须同时覆盖根域名和泛域名')
|
||||
for domain in VERIFY_HOSTNAMES:
|
||||
run('openssl', 'verify', '-CAfile', CA_FILE, '-purpose', 'sslserver',
|
||||
'-untrusted', str(directory / 'candidate.issuer.crt'), '-verify_hostname', domain,
|
||||
str(directory / 'candidate.crt'))
|
||||
@ -119,7 +128,7 @@ def verify_live_certificate(fingerprint):
|
||||
while True:
|
||||
try:
|
||||
with socket.create_connection(('127.0.0.1', port), timeout=2) as connection:
|
||||
with context.wrap_socket(connection, server_hostname='mail.solit.cn') as secure:
|
||||
with context.wrap_socket(connection, server_hostname=CERTIFICATE_SNI) as secure:
|
||||
actual = hashlib.sha256(secure.getpeercert(binary_form=True)).hexdigest()
|
||||
if actual == fingerprint:
|
||||
break
|
||||
|
||||
@ -11,7 +11,7 @@ import deploy_certificate as deploy
|
||||
|
||||
|
||||
ROOT = Path('/var/lib/smail-acme-tls')
|
||||
DOMAINS = ('solit.cn', 'mail.solit.cn', 'smtp.solit.cn', 'imap.solit.cn', 'pop.solit.cn')
|
||||
DOMAINS = deploy.DOMAINS
|
||||
RENEW_TIMEOUT = 600
|
||||
WEBROOT = Path('/var/lib/smail-acme')
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ server {
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name solit.cn mail.solit.cn smtp.solit.cn imap.solit.cn pop.solit.cn;
|
||||
server_name solit.cn mail.solit.cn web.solit.cn smtp.solit.cn imap.solit.cn pop.solit.cn;
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /var/lib/smail-acme;
|
||||
@ -17,7 +17,7 @@ server {
|
||||
}
|
||||
|
||||
location / {
|
||||
return 308 https://mail.solit.cn$request_uri;
|
||||
return 308 https://web.solit.cn$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,16 +31,16 @@ server {
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name solit.cn smtp.solit.cn imap.solit.cn pop.solit.cn;
|
||||
server_name solit.cn mail.solit.cn smtp.solit.cn imap.solit.cn pop.solit.cn;
|
||||
ssl_certificate /opt/smail/ssl/custom.crt;
|
||||
ssl_certificate_key /opt/smail/ssl/custom.key;
|
||||
return 308 https://mail.solit.cn$request_uri;
|
||||
return 308 https://web.solit.cn$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name mail.solit.cn;
|
||||
server_name web.solit.cn;
|
||||
ssl_certificate /opt/smail/ssl/custom.crt;
|
||||
ssl_certificate_key /opt/smail/ssl/custom.key;
|
||||
|
||||
@ -50,10 +50,11 @@ server {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Connection "";
|
||||
proxy_ssl_server_name on;
|
||||
proxy_ssl_name mail.solit.cn;
|
||||
proxy_ssl_name web.solit.cn;
|
||||
proxy_ssl_verify on;
|
||||
proxy_ssl_verify_depth 3;
|
||||
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
|
||||
|
||||
@ -264,7 +264,7 @@ class DeploymentTests(unittest.TestCase):
|
||||
|
||||
def test_wrong_domain_does_not_change_live_files(self):
|
||||
with patch.object(deploy, 'DOMAINS', ('wrong.example',)), \
|
||||
self.assertRaises(subprocess.CalledProcessError):
|
||||
self.assertRaisesRegex(RuntimeError, '根域名和泛域名'):
|
||||
deploy.main()
|
||||
self.assert_old_untouched()
|
||||
|
||||
|
||||
28
test/deployment/test_domain_acceptance.py
Normal file
28
test/deployment/test_domain_acceptance.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""验证 Smail 的后台域名、别名跳转和根域名泛域名证书约束。"""
|
||||
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
NGINX = (ROOT / 'deploy/nginx/smail.conf').read_text(encoding='utf-8')
|
||||
CERTIFICATE = (ROOT / 'deploy/nginx/deploy_certificate.py').read_text(encoding='utf-8')
|
||||
RENEWAL = (ROOT / 'deploy/nginx/renew_certificate.py').read_text(encoding='utf-8')
|
||||
|
||||
|
||||
class DomainAcceptanceTest(unittest.TestCase):
|
||||
def test_web_domain_is_the_only_public_web_proxy(self):
|
||||
self.assertIn('server_name web.solit.cn;', NGINX)
|
||||
self.assertIn('proxy_ssl_name web.solit.cn;', NGINX)
|
||||
self.assertIn('return 308 https://web.solit.cn$request_uri;', NGINX)
|
||||
self.assertNotIn('return 308 https://mail.solit.cn$request_uri;', NGINX)
|
||||
|
||||
def test_certificate_requests_root_and_wildcard(self):
|
||||
self.assertIn("DOMAINS = ('solit.cn', '*.solit.cn')", CERTIFICATE)
|
||||
self.assertIn("CERTIFICATE_SNI = 'web.solit.cn'", CERTIFICATE)
|
||||
self.assertIn("'web.solit.cn'", CERTIFICATE)
|
||||
self.assertIn('DOMAINS = deploy.DOMAINS', RENEWAL)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
||||
@ -11,7 +11,7 @@ import verify_nginx_tls as verify
|
||||
|
||||
|
||||
class RedirectAcceptanceTest(unittest.TestCase):
|
||||
expected = 'https://mail.solit.cn/acceptance?x=1'
|
||||
expected = 'https://web.solit.cn/acceptance?x=1'
|
||||
|
||||
def check(self, first, second):
|
||||
results = {'checks': []}
|
||||
@ -29,12 +29,12 @@ class RedirectAcceptanceTest(unittest.TestCase):
|
||||
self.assertEqual(self.check(self.response(), self.response()), [])
|
||||
|
||||
def test_internal_https_port_is_failure(self):
|
||||
failures = self.check(self.response(), self.response('https://mail.solit.cn:8002/acceptance?x=1'))
|
||||
failures = self.check(self.response(), self.response('https://web.solit.cn:8002/acceptance?x=1'))
|
||||
self.assertEqual(len(failures), 1)
|
||||
self.assertEqual(failures[0]['port'], 8001)
|
||||
|
||||
def test_public_port_does_not_skip_validation(self):
|
||||
failures = self.check(self.response('http://mail.solit.cn/acceptance?x=1'), self.response())
|
||||
failures = self.check(self.response('http://web.solit.cn/acceptance?x=1'), self.response())
|
||||
self.assertEqual([item['port'] for item in failures], [80])
|
||||
|
||||
def test_missing_location_records_failure(self):
|
||||
@ -46,7 +46,7 @@ class RedirectAcceptanceTest(unittest.TestCase):
|
||||
self.assertEqual(failures[0]['actualStatus'], 200)
|
||||
|
||||
def test_path_and_query_must_be_preserved(self):
|
||||
for location in ('https://mail.solit.cn/', 'https://mail.solit.cn/acceptance',
|
||||
for location in ('https://web.solit.cn/', 'https://web.solit.cn/acceptance',
|
||||
'https://other.invalid/acceptance?x=1'):
|
||||
with self.subTest(location=location):
|
||||
self.assertTrue(self.check(self.response(), self.response(location)))
|
||||
|
||||
@ -11,7 +11,7 @@ import ssl
|
||||
import subprocess
|
||||
|
||||
|
||||
HOST = 'mail.solit.cn'
|
||||
HOST = 'web.solit.cn'
|
||||
CONTEXT = ssl.create_default_context()
|
||||
|
||||
|
||||
@ -47,7 +47,8 @@ def main():
|
||||
leaf = subprocess.run(['openssl', 'x509', '-in', '/opt/smail/ssl/custom.crt', '-outform', 'DER'],
|
||||
check=True, capture_output=True).stdout
|
||||
expected = hashlib.sha256(leaf).hexdigest()
|
||||
for port, host in [(443, HOST), (8002, HOST), (465, 'smtp.solit.cn'),
|
||||
for port, host in [(443, HOST), (443, 'solit.cn'), (443, 'mail.solit.cn'),
|
||||
(8002, HOST), (465, 'smtp.solit.cn'),
|
||||
(993, 'imap.solit.cn'), (995, 'pop.solit.cn')]:
|
||||
with socket.create_connection(('127.0.0.1', port), timeout=10) as raw:
|
||||
with CONTEXT.wrap_socket(raw, server_hostname=host) as stream:
|
||||
@ -86,7 +87,7 @@ def main():
|
||||
raise AssertionError('unknown SNI accepted')
|
||||
root = json.loads(Path('/opt/smail/config/config.json').read_text())
|
||||
assert root['http']['port'] == 8001 and root['http']['httpsPort'] == 8002
|
||||
assert root['security']['webAuthnRpOrigins'] == ['https://mail.solit.cn']
|
||||
assert root['security']['webAuthnRpOrigins'] == ['https://web.solit.cn']
|
||||
results['fingerprintSHA256'] = expected
|
||||
results['status'] = 'failed' if results['failures'] else 'passed'
|
||||
print(json.dumps(results, ensure_ascii=False, indent=2))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user