lib.tls
1import hashlib 2import re 3import shlex 4import ssl 5 6from SRE.lib_sre import Grade0, NetScheme0 7 8 9def _cert_fingerprint_sha256(pem: str) -> str: 10 """Return the SHA256 fingerprint of a PEM certificate as XX:XX:... uppercase hex.""" 11 der = ssl.PEM_cert_to_DER_cert(pem.strip()) 12 digest = hashlib.sha256(der).hexdigest() 13 return ':'.join(digest[i:i + 2].upper() for i in range(0, len(digest), 2)) 14 15 16def _val(line, prefix): 17 m = re.search(rf'{prefix}=(.+)', line) 18 return m.group(1).strip() if m else '' 19 20 21def _cert_dict(subject_out, issuer_out, dates_out, serial_out, fingerprint_out): 22 not_before_m = re.search(r'notBefore=(.+)', dates_out) 23 not_after_m = re.search(r'notAfter=(.+)', dates_out) 24 subject = _val(subject_out, "subject") 25 cn_m = re.search(r'(?:^|[,/]\s*)CN\s*=\s*([^,/]+)', subject) 26 return { 27 "subject": subject, 28 "issuer": _val(issuer_out, "issuer"), 29 "common_name": cn_m.group(1).strip() if cn_m else '', 30 "not_before": not_before_m.group(1).strip() if not_before_m else '', 31 "not_after": not_after_m.group(1).strip() if not_after_m else '', 32 "serial": _val(serial_out, "serial"), 33 "fingerprint": _val(fingerprint_out, "SHA256 Fingerprint"), 34 } 35 36 37def eval_rsa_private_key(grade: Grade0, machine_name: str, key_file: str, 38 password: str | None = None, bits: int = 4096, 39 cipher: str = "AES-256-CBC", 40 step: int = 1) -> bool: 41 """Check that key_file is an RSA private key with the expected properties. 42 43 Verifies: 44 - The file is an RSA private key (decryptable with password if provided). 45 - The key size matches bits. 46 - The encryption cipher in the PEM header matches cipher (case-insensitive). 47 This applies to traditional PEM format (BEGIN RSA PRIVATE KEY); for 48 PKCS#8 (BEGIN ENCRYPTED PRIVATE KEY) the cipher check is skipped. 49 The cipher check is also skipped when password is None. 50 51 Args: 52 grade: the Grade0 instance. 53 machine_name: name of the virtual machine to inspect. 54 key_file: absolute path to the private key file on the machine. 55 password: passphrase protecting the private key, or None if unencrypted. 56 bits: expected RSA key size (default: 4096). 57 cipher: expected PEM encryption cipher (default: 'AES-256-CBC'). 58 step: step number passed to grade.test() (default: 1). 59 60 Returns: 61 True if all checks pass, False otherwise. 62 """ 63 passin = f"-passin {shlex.quote(f'pass:{password}')}" if password is not None else "" 64 q_key_file = shlex.quote(key_file) 65 key_text, key_code = grade.test( 66 machine_name=machine_name, 67 command=f"openssl rsa -in {q_key_file} {passin} -noout -text 2>&1", 68 step=step, 69 allow_error=True, 70 ) 71 dek_info, _ = grade.test( 72 machine_name=machine_name, 73 command=f"grep 'DEK-Info' {q_key_file}", 74 step=step, 75 allow_error=True, 76 ) 77 78 if key_code != 0: 79 return False 80 81 bits_m = re.search(r'Private-Key:\s*\((\d+)\s*bit', key_text) 82 if not bits_m or int(bits_m.group(1)) != bits: 83 return False 84 85 # DEK-Info line only present in traditional PEM format; skip cipher check for PKCS#8 86 if dek_info.strip(): 87 dek_m = re.search(r'DEK-Info:\s*([^,\s]+)', dek_info) 88 if not dek_m or dek_m.group(1).upper() != cipher.upper(): 89 return False 90 91 return True 92 93 94def set_rsa_private_key(net_scheme: NetScheme0, machine_name: str, key_file: str, 95 password: str, bits: int = 4096, 96 cipher: str = "AES-256-CBC"): 97 """Generate an RSA private key on machine_name. 98 99 Args: 100 net_scheme: the NetScheme0 instance. 101 machine_name: name of the virtual machine. 102 key_file: absolute path where the key will be written on the machine. 103 password: passphrase to protect the key. 104 bits: RSA key size in bits (default: 4096). 105 cipher: PEM encryption cipher (default: 'AES-256-CBC'). 106 """ 107 net_scheme.cmd(machine_name, 108 f"openssl genrsa -{shlex.quote(cipher.lower())} -passout {shlex.quote(f'pass:{password}')} -out {shlex.quote(key_file)} {bits}") 109 110 111def eval_self_signed_certificate(grade: Grade0, machine_name: str, 112 key_file: str, cert_file: str, 113 password: str, 114 step: int = 1) -> dict | None: 115 """Check that key_file and cert_file are a password-protected TLS key and 116 a matching self-signed certificate on machine_name. 117 118 Verifies: 119 - key_file is a valid PEM private key decryptable with password. 120 - cert_file is a valid PEM certificate whose Issuer equals its Subject 121 (self-signed). 122 - The public key in the certificate matches the private key. 123 124 Args: 125 grade: the Grade0 instance. 126 machine_name: name of the virtual machine to inspect. 127 key_file: absolute path to the private key file on the machine. 128 cert_file: absolute path to the certificate file on the machine. 129 password: passphrase protecting the private key. 130 step: step number passed to grade.test() (default: 1). 131 132 Returns: 133 A dict with certificate fields (subject, issuer, not_before, not_after, 134 serial, fingerprint) if all checks pass, None otherwise. 135 """ 136 # All grade.test() calls must be made unconditionally so they are registered 137 # in the first (registration) pass and carry real results in the second pass. 138 q_key_file = shlex.quote(key_file) 139 q_cert_file = shlex.quote(cert_file) 140 q_passin = shlex.quote(f'pass:{password}') 141 _, key_code = grade.test( 142 machine_name=machine_name, 143 command=f"openssl pkey -in {q_key_file} -passin {q_passin} -noout", 144 step=step, 145 allow_error=True, 146 ) 147 cert_text, cert_code = grade.test( 148 machine_name=machine_name, 149 command=f"openssl x509 -in {q_cert_file} -noout -text -fingerprint -sha256", 150 step=step, 151 allow_error=True, 152 ) 153 cert_pubkey, cert_pubkey_code = grade.test( 154 machine_name=machine_name, 155 command=f"openssl x509 -in {q_cert_file} -noout -pubkey", 156 step=step, 157 allow_error=True, 158 ) 159 key_pubkey, key_pubkey_code = grade.test( 160 machine_name=machine_name, 161 command=f"openssl pkey -in {q_key_file} -passin {q_passin} -pubout", 162 step=step, 163 allow_error=True, 164 ) 165 subject_out, _ = grade.test( 166 machine_name=machine_name, 167 command=f"openssl x509 -in {q_cert_file} -noout -subject", 168 step=step, 169 allow_error=True, 170 ) 171 issuer_out, _ = grade.test( 172 machine_name=machine_name, 173 command=f"openssl x509 -in {q_cert_file} -noout -issuer", 174 step=step, 175 allow_error=True, 176 ) 177 dates_out, _ = grade.test( 178 machine_name=machine_name, 179 command=f"openssl x509 -in {q_cert_file} -noout -dates", 180 step=step, 181 allow_error=True, 182 ) 183 serial_out, _ = grade.test( 184 machine_name=machine_name, 185 command=f"openssl x509 -in {q_cert_file} -noout -serial", 186 step=step, 187 allow_error=True, 188 ) 189 fingerprint_out, _ = grade.test( 190 machine_name=machine_name, 191 command=f"openssl x509 -in {q_cert_file} -noout -fingerprint -sha256", 192 step=step, 193 allow_error=True, 194 ) 195 196 if key_code != 0 or cert_code != 0 or cert_pubkey_code != 0 or key_pubkey_code != 0: 197 return None 198 199 if cert_pubkey.strip() != key_pubkey.strip(): 200 return None 201 202 # Check self-signed: Subject must equal Issuer 203 subject_m = re.search(r'Subject:\s*(.+)', cert_text) 204 issuer_m = re.search(r'Issuer:\s*(.+)', cert_text) 205 if not subject_m or not issuer_m: 206 return None 207 if subject_m.group(1).strip() != issuer_m.group(1).strip(): 208 return None 209 210 return _cert_dict(subject_out, issuer_out, dates_out, serial_out, fingerprint_out) 211 212 213def eval_certificate(grade: Grade0, machine_name: str, 214 key_file: str, cert_file: str, 215 step: int = 1) -> dict | None: 216 """Check that key_file and cert_file are a matching TLS key pair on machine_name. 217 218 Verifies: 219 - cert_file is a valid PEM certificate. 220 - The public key in the certificate matches the private key in key_file. 221 222 Args: 223 grade: the Grade0 instance. 224 machine_name: name of the virtual machine to inspect. 225 key_file: absolute path to the private key file on the machine. 226 cert_file: absolute path to the certificate file on the machine. 227 step: step number passed to grade.test() (default: 1). 228 229 Returns: 230 A dict with certificate fields (subject, issuer, common_name, 231 not_before, not_after, serial, fingerprint) if all checks pass, 232 None otherwise. 233 """ 234 # All grade.test() calls must be made unconditionally so they are registered 235 # in the first (registration) pass and carry real results in the second pass. 236 q_key_file = shlex.quote(key_file) 237 q_cert_file = shlex.quote(cert_file) 238 _, cert_code = grade.test( 239 machine_name=machine_name, 240 command=f"openssl x509 -in {q_cert_file} -noout -text -fingerprint -sha256", 241 step=step, 242 allow_error=True, 243 ) 244 cert_pubkey, cert_pubkey_code = grade.test( 245 machine_name=machine_name, 246 command=f"openssl x509 -in {q_cert_file} -noout -pubkey", 247 step=step, 248 allow_error=True, 249 ) 250 key_pubkey, key_pubkey_code = grade.test( 251 machine_name=machine_name, 252 command=f"openssl pkey -in {q_key_file} -pubout", 253 step=step, 254 allow_error=True, 255 ) 256 subject_out, _ = grade.test( 257 machine_name=machine_name, 258 command=f"openssl x509 -in {q_cert_file} -noout -subject", 259 step=step, 260 allow_error=True, 261 ) 262 issuer_out, _ = grade.test( 263 machine_name=machine_name, 264 command=f"openssl x509 -in {q_cert_file} -noout -issuer", 265 step=step, 266 allow_error=True, 267 ) 268 dates_out, _ = grade.test( 269 machine_name=machine_name, 270 command=f"openssl x509 -in {q_cert_file} -noout -dates", 271 step=step, 272 allow_error=True, 273 ) 274 serial_out, _ = grade.test( 275 machine_name=machine_name, 276 command=f"openssl x509 -in {q_cert_file} -noout -serial", 277 step=step, 278 allow_error=True, 279 ) 280 fingerprint_out, _ = grade.test( 281 machine_name=machine_name, 282 command=f"openssl x509 -in {q_cert_file} -noout -fingerprint -sha256", 283 step=step, 284 allow_error=True, 285 ) 286 287 if cert_code != 0 or cert_pubkey_code != 0 or key_pubkey_code != 0: 288 return None 289 290 if cert_pubkey.strip() != key_pubkey.strip(): 291 return None 292 293 return _cert_dict(subject_out, issuer_out, dates_out, serial_out, fingerprint_out) 294 295 296def eval_certificate_validity(grade: Grade0, machine_name: str, 297 cert_file: str, ca_cert_file: str, 298 step: int = 1) -> bool: 299 """Check that cert_file is signed by ca_cert_file on machine_name. 300 301 Args: 302 grade: the Grade0 instance. 303 machine_name: name of the virtual machine to inspect. 304 cert_file: absolute path to the certificate file on the machine. 305 ca_cert_file: absolute path to the CA certificate file on the machine. 306 step: step number passed to grade.test() (default: 1). 307 308 Returns: 309 True if cert_file is a valid certificate signed by ca_cert_file, 310 False otherwise. 311 """ 312 _, verify_code = grade.test( 313 machine_name=machine_name, 314 command=f"openssl verify -CAfile {shlex.quote(ca_cert_file)} {shlex.quote(cert_file)}", 315 step=step, 316 allow_error=True, 317 ) 318 return verify_code == 0 319 320 321def eval_https_server(grade: Grade0, machine_name: str, url: str, 322 server_ip: str, cert: str, 323 server_port: int = 443, step: int = 1) -> bool: 324 """Check that an HTTPS server at server_ip responds correctly and presents 325 the expected certificate. 326 327 Verifies: 328 - A GET request to url (routed to server_ip:server_port) succeeds (2xx). 329 - The certificate presented by the server matches cert. 330 331 Args: 332 grade: the Grade0 instance. 333 machine_name: name of the virtual machine from which to connect. 334 url: full URL to request (e.g. https://myserver/index.html). 335 server_ip: IP address of the HTTPS server to connect to. 336 cert: PEM certificate content to verify against the server. 337 server_port: HTTPS port (default: 443). 338 step: step number passed to grade.test() (default: 1). 339 340 Returns: 341 True if all checks pass, False otherwise. 342 """ 343 # All grade.test() calls must be made unconditionally so they are registered 344 # in the first (registration) pass and carry real results in the second pass. 345 _, http_code = grade.test( 346 machine_name=machine_name, 347 command=f"curl -k -L --fail --connect-to ::{server_ip}:{server_port}" 348 f" -s -o /dev/null {url}", 349 step=step, 350 allow_error=True, 351 ) 352 server_fp, server_fp_code = grade.test( 353 machine_name=machine_name, 354 command=f"openssl s_client -connect {server_ip}:{server_port}" 355 f" </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256", 356 step=step, 357 allow_error=True, 358 ) 359 360 if http_code != 0: 361 return False 362 if server_fp_code != 0: 363 return False 364 365 try: 366 cert_fp_val = _cert_fingerprint_sha256(cert) 367 except Exception: 368 return False 369 370 server_fp_val = _val(server_fp, "SHA256 Fingerprint") 371 return bool(server_fp_val) and server_fp_val == cert_fp_val
38def eval_rsa_private_key(grade: Grade0, machine_name: str, key_file: str, 39 password: str | None = None, bits: int = 4096, 40 cipher: str = "AES-256-CBC", 41 step: int = 1) -> bool: 42 """Check that key_file is an RSA private key with the expected properties. 43 44 Verifies: 45 - The file is an RSA private key (decryptable with password if provided). 46 - The key size matches bits. 47 - The encryption cipher in the PEM header matches cipher (case-insensitive). 48 This applies to traditional PEM format (BEGIN RSA PRIVATE KEY); for 49 PKCS#8 (BEGIN ENCRYPTED PRIVATE KEY) the cipher check is skipped. 50 The cipher check is also skipped when password is None. 51 52 Args: 53 grade: the Grade0 instance. 54 machine_name: name of the virtual machine to inspect. 55 key_file: absolute path to the private key file on the machine. 56 password: passphrase protecting the private key, or None if unencrypted. 57 bits: expected RSA key size (default: 4096). 58 cipher: expected PEM encryption cipher (default: 'AES-256-CBC'). 59 step: step number passed to grade.test() (default: 1). 60 61 Returns: 62 True if all checks pass, False otherwise. 63 """ 64 passin = f"-passin {shlex.quote(f'pass:{password}')}" if password is not None else "" 65 q_key_file = shlex.quote(key_file) 66 key_text, key_code = grade.test( 67 machine_name=machine_name, 68 command=f"openssl rsa -in {q_key_file} {passin} -noout -text 2>&1", 69 step=step, 70 allow_error=True, 71 ) 72 dek_info, _ = grade.test( 73 machine_name=machine_name, 74 command=f"grep 'DEK-Info' {q_key_file}", 75 step=step, 76 allow_error=True, 77 ) 78 79 if key_code != 0: 80 return False 81 82 bits_m = re.search(r'Private-Key:\s*\((\d+)\s*bit', key_text) 83 if not bits_m or int(bits_m.group(1)) != bits: 84 return False 85 86 # DEK-Info line only present in traditional PEM format; skip cipher check for PKCS#8 87 if dek_info.strip(): 88 dek_m = re.search(r'DEK-Info:\s*([^,\s]+)', dek_info) 89 if not dek_m or dek_m.group(1).upper() != cipher.upper(): 90 return False 91 92 return True
Check that key_file is an RSA private key with the expected properties.
Verifies:
- The file is an RSA private key (decryptable with password if provided).
- The key size matches bits.
- The encryption cipher in the PEM header matches cipher (case-insensitive). This applies to traditional PEM format (BEGIN RSA PRIVATE KEY); for PKCS#8 (BEGIN ENCRYPTED PRIVATE KEY) the cipher check is skipped. The cipher check is also skipped when password is None.
Arguments:
- grade: the Grade0 instance.
- machine_name: name of the virtual machine to inspect.
- key_file: absolute path to the private key file on the machine.
- password: passphrase protecting the private key, or None if unencrypted.
- bits: expected RSA key size (default: 4096).
- cipher: expected PEM encryption cipher (default: 'AES-256-CBC').
- step: step number passed to grade.test() (default: 1).
Returns:
True if all checks pass, False otherwise.
95def set_rsa_private_key(net_scheme: NetScheme0, machine_name: str, key_file: str, 96 password: str, bits: int = 4096, 97 cipher: str = "AES-256-CBC"): 98 """Generate an RSA private key on machine_name. 99 100 Args: 101 net_scheme: the NetScheme0 instance. 102 machine_name: name of the virtual machine. 103 key_file: absolute path where the key will be written on the machine. 104 password: passphrase to protect the key. 105 bits: RSA key size in bits (default: 4096). 106 cipher: PEM encryption cipher (default: 'AES-256-CBC'). 107 """ 108 net_scheme.cmd(machine_name, 109 f"openssl genrsa -{shlex.quote(cipher.lower())} -passout {shlex.quote(f'pass:{password}')} -out {shlex.quote(key_file)} {bits}")
Generate an RSA private key on machine_name.
Arguments:
- net_scheme: the NetScheme0 instance.
- machine_name: name of the virtual machine.
- key_file: absolute path where the key will be written on the machine.
- password: passphrase to protect the key.
- bits: RSA key size in bits (default: 4096).
- cipher: PEM encryption cipher (default: 'AES-256-CBC').
112def eval_self_signed_certificate(grade: Grade0, machine_name: str, 113 key_file: str, cert_file: str, 114 password: str, 115 step: int = 1) -> dict | None: 116 """Check that key_file and cert_file are a password-protected TLS key and 117 a matching self-signed certificate on machine_name. 118 119 Verifies: 120 - key_file is a valid PEM private key decryptable with password. 121 - cert_file is a valid PEM certificate whose Issuer equals its Subject 122 (self-signed). 123 - The public key in the certificate matches the private key. 124 125 Args: 126 grade: the Grade0 instance. 127 machine_name: name of the virtual machine to inspect. 128 key_file: absolute path to the private key file on the machine. 129 cert_file: absolute path to the certificate file on the machine. 130 password: passphrase protecting the private key. 131 step: step number passed to grade.test() (default: 1). 132 133 Returns: 134 A dict with certificate fields (subject, issuer, not_before, not_after, 135 serial, fingerprint) if all checks pass, None otherwise. 136 """ 137 # All grade.test() calls must be made unconditionally so they are registered 138 # in the first (registration) pass and carry real results in the second pass. 139 q_key_file = shlex.quote(key_file) 140 q_cert_file = shlex.quote(cert_file) 141 q_passin = shlex.quote(f'pass:{password}') 142 _, key_code = grade.test( 143 machine_name=machine_name, 144 command=f"openssl pkey -in {q_key_file} -passin {q_passin} -noout", 145 step=step, 146 allow_error=True, 147 ) 148 cert_text, cert_code = grade.test( 149 machine_name=machine_name, 150 command=f"openssl x509 -in {q_cert_file} -noout -text -fingerprint -sha256", 151 step=step, 152 allow_error=True, 153 ) 154 cert_pubkey, cert_pubkey_code = grade.test( 155 machine_name=machine_name, 156 command=f"openssl x509 -in {q_cert_file} -noout -pubkey", 157 step=step, 158 allow_error=True, 159 ) 160 key_pubkey, key_pubkey_code = grade.test( 161 machine_name=machine_name, 162 command=f"openssl pkey -in {q_key_file} -passin {q_passin} -pubout", 163 step=step, 164 allow_error=True, 165 ) 166 subject_out, _ = grade.test( 167 machine_name=machine_name, 168 command=f"openssl x509 -in {q_cert_file} -noout -subject", 169 step=step, 170 allow_error=True, 171 ) 172 issuer_out, _ = grade.test( 173 machine_name=machine_name, 174 command=f"openssl x509 -in {q_cert_file} -noout -issuer", 175 step=step, 176 allow_error=True, 177 ) 178 dates_out, _ = grade.test( 179 machine_name=machine_name, 180 command=f"openssl x509 -in {q_cert_file} -noout -dates", 181 step=step, 182 allow_error=True, 183 ) 184 serial_out, _ = grade.test( 185 machine_name=machine_name, 186 command=f"openssl x509 -in {q_cert_file} -noout -serial", 187 step=step, 188 allow_error=True, 189 ) 190 fingerprint_out, _ = grade.test( 191 machine_name=machine_name, 192 command=f"openssl x509 -in {q_cert_file} -noout -fingerprint -sha256", 193 step=step, 194 allow_error=True, 195 ) 196 197 if key_code != 0 or cert_code != 0 or cert_pubkey_code != 0 or key_pubkey_code != 0: 198 return None 199 200 if cert_pubkey.strip() != key_pubkey.strip(): 201 return None 202 203 # Check self-signed: Subject must equal Issuer 204 subject_m = re.search(r'Subject:\s*(.+)', cert_text) 205 issuer_m = re.search(r'Issuer:\s*(.+)', cert_text) 206 if not subject_m or not issuer_m: 207 return None 208 if subject_m.group(1).strip() != issuer_m.group(1).strip(): 209 return None 210 211 return _cert_dict(subject_out, issuer_out, dates_out, serial_out, fingerprint_out)
Check that key_file and cert_file are a password-protected TLS key and a matching self-signed certificate on machine_name.
Verifies:
- key_file is a valid PEM private key decryptable with password.
- cert_file is a valid PEM certificate whose Issuer equals its Subject (self-signed).
- The public key in the certificate matches the private key.
Arguments:
- grade: the Grade0 instance.
- machine_name: name of the virtual machine to inspect.
- key_file: absolute path to the private key file on the machine.
- cert_file: absolute path to the certificate file on the machine.
- password: passphrase protecting the private key.
- step: step number passed to grade.test() (default: 1).
Returns:
A dict with certificate fields (subject, issuer, not_before, not_after, serial, fingerprint) if all checks pass, None otherwise.
214def eval_certificate(grade: Grade0, machine_name: str, 215 key_file: str, cert_file: str, 216 step: int = 1) -> dict | None: 217 """Check that key_file and cert_file are a matching TLS key pair on machine_name. 218 219 Verifies: 220 - cert_file is a valid PEM certificate. 221 - The public key in the certificate matches the private key in key_file. 222 223 Args: 224 grade: the Grade0 instance. 225 machine_name: name of the virtual machine to inspect. 226 key_file: absolute path to the private key file on the machine. 227 cert_file: absolute path to the certificate file on the machine. 228 step: step number passed to grade.test() (default: 1). 229 230 Returns: 231 A dict with certificate fields (subject, issuer, common_name, 232 not_before, not_after, serial, fingerprint) if all checks pass, 233 None otherwise. 234 """ 235 # All grade.test() calls must be made unconditionally so they are registered 236 # in the first (registration) pass and carry real results in the second pass. 237 q_key_file = shlex.quote(key_file) 238 q_cert_file = shlex.quote(cert_file) 239 _, cert_code = grade.test( 240 machine_name=machine_name, 241 command=f"openssl x509 -in {q_cert_file} -noout -text -fingerprint -sha256", 242 step=step, 243 allow_error=True, 244 ) 245 cert_pubkey, cert_pubkey_code = grade.test( 246 machine_name=machine_name, 247 command=f"openssl x509 -in {q_cert_file} -noout -pubkey", 248 step=step, 249 allow_error=True, 250 ) 251 key_pubkey, key_pubkey_code = grade.test( 252 machine_name=machine_name, 253 command=f"openssl pkey -in {q_key_file} -pubout", 254 step=step, 255 allow_error=True, 256 ) 257 subject_out, _ = grade.test( 258 machine_name=machine_name, 259 command=f"openssl x509 -in {q_cert_file} -noout -subject", 260 step=step, 261 allow_error=True, 262 ) 263 issuer_out, _ = grade.test( 264 machine_name=machine_name, 265 command=f"openssl x509 -in {q_cert_file} -noout -issuer", 266 step=step, 267 allow_error=True, 268 ) 269 dates_out, _ = grade.test( 270 machine_name=machine_name, 271 command=f"openssl x509 -in {q_cert_file} -noout -dates", 272 step=step, 273 allow_error=True, 274 ) 275 serial_out, _ = grade.test( 276 machine_name=machine_name, 277 command=f"openssl x509 -in {q_cert_file} -noout -serial", 278 step=step, 279 allow_error=True, 280 ) 281 fingerprint_out, _ = grade.test( 282 machine_name=machine_name, 283 command=f"openssl x509 -in {q_cert_file} -noout -fingerprint -sha256", 284 step=step, 285 allow_error=True, 286 ) 287 288 if cert_code != 0 or cert_pubkey_code != 0 or key_pubkey_code != 0: 289 return None 290 291 if cert_pubkey.strip() != key_pubkey.strip(): 292 return None 293 294 return _cert_dict(subject_out, issuer_out, dates_out, serial_out, fingerprint_out)
Check that key_file and cert_file are a matching TLS key pair on machine_name.
Verifies:
- cert_file is a valid PEM certificate.
- The public key in the certificate matches the private key in key_file.
Arguments:
- grade: the Grade0 instance.
- machine_name: name of the virtual machine to inspect.
- key_file: absolute path to the private key file on the machine.
- cert_file: absolute path to the certificate file on the machine.
- step: step number passed to grade.test() (default: 1).
Returns:
A dict with certificate fields (subject, issuer, common_name, not_before, not_after, serial, fingerprint) if all checks pass, None otherwise.
297def eval_certificate_validity(grade: Grade0, machine_name: str, 298 cert_file: str, ca_cert_file: str, 299 step: int = 1) -> bool: 300 """Check that cert_file is signed by ca_cert_file on machine_name. 301 302 Args: 303 grade: the Grade0 instance. 304 machine_name: name of the virtual machine to inspect. 305 cert_file: absolute path to the certificate file on the machine. 306 ca_cert_file: absolute path to the CA certificate file on the machine. 307 step: step number passed to grade.test() (default: 1). 308 309 Returns: 310 True if cert_file is a valid certificate signed by ca_cert_file, 311 False otherwise. 312 """ 313 _, verify_code = grade.test( 314 machine_name=machine_name, 315 command=f"openssl verify -CAfile {shlex.quote(ca_cert_file)} {shlex.quote(cert_file)}", 316 step=step, 317 allow_error=True, 318 ) 319 return verify_code == 0
Check that cert_file is signed by ca_cert_file on machine_name.
Arguments:
- grade: the Grade0 instance.
- machine_name: name of the virtual machine to inspect.
- cert_file: absolute path to the certificate file on the machine.
- ca_cert_file: absolute path to the CA certificate file on the machine.
- step: step number passed to grade.test() (default: 1).
Returns:
True if cert_file is a valid certificate signed by ca_cert_file, False otherwise.
322def eval_https_server(grade: Grade0, machine_name: str, url: str, 323 server_ip: str, cert: str, 324 server_port: int = 443, step: int = 1) -> bool: 325 """Check that an HTTPS server at server_ip responds correctly and presents 326 the expected certificate. 327 328 Verifies: 329 - A GET request to url (routed to server_ip:server_port) succeeds (2xx). 330 - The certificate presented by the server matches cert. 331 332 Args: 333 grade: the Grade0 instance. 334 machine_name: name of the virtual machine from which to connect. 335 url: full URL to request (e.g. https://myserver/index.html). 336 server_ip: IP address of the HTTPS server to connect to. 337 cert: PEM certificate content to verify against the server. 338 server_port: HTTPS port (default: 443). 339 step: step number passed to grade.test() (default: 1). 340 341 Returns: 342 True if all checks pass, False otherwise. 343 """ 344 # All grade.test() calls must be made unconditionally so they are registered 345 # in the first (registration) pass and carry real results in the second pass. 346 _, http_code = grade.test( 347 machine_name=machine_name, 348 command=f"curl -k -L --fail --connect-to ::{server_ip}:{server_port}" 349 f" -s -o /dev/null {url}", 350 step=step, 351 allow_error=True, 352 ) 353 server_fp, server_fp_code = grade.test( 354 machine_name=machine_name, 355 command=f"openssl s_client -connect {server_ip}:{server_port}" 356 f" </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256", 357 step=step, 358 allow_error=True, 359 ) 360 361 if http_code != 0: 362 return False 363 if server_fp_code != 0: 364 return False 365 366 try: 367 cert_fp_val = _cert_fingerprint_sha256(cert) 368 except Exception: 369 return False 370 371 server_fp_val = _val(server_fp, "SHA256 Fingerprint") 372 return bool(server_fp_val) and server_fp_val == cert_fp_val
Check that an HTTPS server at server_ip responds correctly and presents the expected certificate.
Verifies:
- A GET request to url (routed to server_ip:server_port) succeeds (2xx).
- The certificate presented by the server matches cert.
Arguments:
- grade: the Grade0 instance.
- machine_name: name of the virtual machine from which to connect.
- url: full URL to request (e.g. https://myserver/index.html).
- server_ip: IP address of the HTTPS server to connect to.
- cert: PEM certificate content to verify against the server.
- server_port: HTTPS port (default: 443).
- step: step number passed to grade.test() (default: 1).
Returns:
True if all checks pass, False otherwise.