Some checks are pending
Security Checks / security (push) Waiting to run
精确更新证书、Nginx、TLS、重定向和续期演练用例,统一使用 cdis.cn 与 web.cdis.cn。
99 lines
4.5 KiB
Python
99 lines
4.5 KiB
Python
"""仅检查远程传输层,不登录、不发信、不读写业务数据库。"""
|
|
|
|
from datetime import datetime, timezone
|
|
import hashlib
|
|
import http.client
|
|
import json
|
|
from pathlib import Path
|
|
import smtplib
|
|
import socket
|
|
import ssl
|
|
import subprocess
|
|
|
|
|
|
HOST = 'web.cdis.cn'
|
|
CONTEXT = ssl.create_default_context()
|
|
|
|
|
|
def request(port, path='/', host=HOST, tls=True):
|
|
raw = socket.create_connection(('127.0.0.1', port), timeout=10)
|
|
stream = CONTEXT.wrap_socket(raw, server_hostname=host) if tls else raw
|
|
try:
|
|
stream.sendall(f'GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n'.encode())
|
|
response = http.client.HTTPResponse(stream)
|
|
response.begin()
|
|
body = response.read()
|
|
return response.status, dict(response.getheaders()), body
|
|
finally:
|
|
stream.close()
|
|
|
|
|
|
def check_redirects(results):
|
|
expected = 'https://' + HOST + '/acceptance?x=1'
|
|
failures = []
|
|
for port in (80, 8001):
|
|
status, headers, _ = request(port, '/acceptance?x=1', tls=False)
|
|
location = headers.get('Location')
|
|
results['checks'].append({'port': port, 'redirect': location, 'status': status})
|
|
if status != 308 or location != expected:
|
|
failures.append({'port': port, 'check': 'public_redirect',
|
|
'expectedStatus': 308, 'actualStatus': status,
|
|
'expectedLocation': expected, 'actualLocation': location})
|
|
return failures
|
|
|
|
|
|
def main():
|
|
results = {'time': datetime.now(timezone.utc).isoformat(), 'checks': []}
|
|
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), (443, 'cdis.cn'), (443, 'mail.cdis.cn'),
|
|
(8002, HOST), (465, 'smtp.cdis.cn'),
|
|
(993, 'imap.cdis.cn'), (995, 'pop.cdis.cn')]:
|
|
with socket.create_connection(('127.0.0.1', port), timeout=10) as raw:
|
|
with CONTEXT.wrap_socket(raw, server_hostname=host) as stream:
|
|
actual = hashlib.sha256(stream.getpeercert(binary_form=True)).hexdigest()
|
|
assert actual == expected, (port, 'certificate mismatch')
|
|
results['checks'].append({'port': port, 'hostname': host, 'tls': stream.version(),
|
|
'systemTrust': True, 'sameCertificate': True})
|
|
for port in (25, 587):
|
|
with smtplib.SMTP(timeout=10) as client:
|
|
assert client.connect('127.0.0.1', port)[0] == 220
|
|
client._host = 'smtp.cdis.cn'
|
|
assert client.ehlo('localhost')[0] == 250
|
|
assert not client.has_extn('auth'), 'plaintext AUTH advertised'
|
|
assert client.has_extn('starttls')
|
|
assert client.starttls(context=CONTEXT)[0] == 220
|
|
assert hashlib.sha256(client.sock.getpeercert(binary_form=True)).hexdigest() == expected
|
|
assert client.ehlo('localhost')[0] == 250
|
|
results['checks'].append({'port': port, 'systemTrust': True, 'starttls': True,
|
|
'plaintextAuthHidden': True})
|
|
for port in (443, 8002):
|
|
status, headers, body = request(port)
|
|
assert status == 200 and b'<html' in body.lower(), (port, status)
|
|
assert 'Content-Security-Policy' in headers
|
|
results['checks'].append({'port': port, 'pageStatus': status, 'pageBytes': len(body)})
|
|
status, _, body = request(port, '/api/ping')
|
|
assert status == 200
|
|
results['failures'] = check_redirects(results)
|
|
assert request(80, host='untrusted.invalid', tls=False)[0] == 404
|
|
results['checks'].append({'unknownHttpHost': 'rejected'})
|
|
try:
|
|
with socket.create_connection(('127.0.0.1', 443), timeout=10) as raw:
|
|
CONTEXT.wrap_socket(raw, server_hostname='untrusted.invalid')
|
|
except ssl.SSLError:
|
|
results['checks'].append({'unknownSNI': 'rejected'})
|
|
else:
|
|
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://web.cdis.cn']
|
|
results['fingerprintSHA256'] = expected
|
|
results['status'] = 'failed' if results['failures'] else 'passed'
|
|
print(json.dumps(results, ensure_ascii=False, indent=2))
|
|
return int(bool(results['failures']))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|