Preparar uma PKI descartável
Guarda o código abaixo como run.py e executa python3 run.py --output evidence.json. Define DR_OPENSSL_BIN se OpenSSL estiver noutro caminho. O ensaio registado usa Python 3.13.1 e OpenSSL 3.6.1 em macOS. Cria duas CAs temporárias e dois certificados leaf emitidos pela CA principal, com o mesmo SAN api.fund.test. Um leaf permanece válido no índice e o outro é revogado. As chaves e a base da CA são eliminadas no fim. O desenho é didático, offline e direto, sem serviço de responder ou alterações à confiança do sistema.
Prever antes de executar
Antes de ler o JSON, desenha uma matriz com leaf bom e leaf revogado nas linhas. Nas colunas coloca CRL atual com verificação ativa, CRL carregada sem verificação e ausência de CRL com verificação obrigatória. Prevê aceitação e rejeição para cada célula. No ensaio, -CRLfile disponibiliza o objeto, enquanto -crl_check ativa a verificação do leaf. A distinção ajuda num caso fictício em que uma equipa entrega ficheiros a todas as instâncias, mas o controlo continua inativo. Uma mudança só deve fechar quando a configuração e os probes demonstrarem o efeito pretendido.
Interpretar as recusas
Com a CRL atual e a política ativa, o leaf não revogado passa e o outro é rejeitado com certificate revoked. Quando falta uma CRL aplicável, o erro é unable to get certificate CRL. Estas observações não têm o mesmo significado. A primeira identifica o estado negativo comunicado; a segunda mostra que falta informação exigida para decidir. Num incidente fictício, regista o emissor, o serial, a política, o artefacto consultado e o erro. Não transformes ausência de informação em confirmação positiva nem declares todos os certificados revogados por falha de distribuição.
Separar os relógios dos artefactos
O código gera uma CRL expirada e outra futura através de datas relativas. A primeira tem nextUpdate no passado; a segunda tem lastUpdate no futuro. O relógio do sistema permanece intacto. Os dois casos são rejeitados, embora o leaf continue dentro da sua validade. Para um batch fictício interrompido às duas da manhã, compara primeiro o relógio efetivo e os tempos do objeto. Se a fonte não foi atualizada, recupera o job ou o caminho alternativo aprovado. Alterar o relógio para esconder o erro pode afetar outros serviços e não recupera a distribuição.
Observar snapshots diferentes
before.pem foi emitida antes de registar a revogação; current.pem foi emitida depois. Ambas são temporalmente aceitáveis no instante do ensaio, mas só a segunda contém o evento. O leaf revogado passa com a primeira e falha com a segunda. A observação não prova que o estado mais recente chega instantaneamente a todos os consumidores. Para uma mudança real, define publicação, atualização, propagação e critérios de alarme. A janela de exposição depende desses mecanismos e da política aplicável. Acrescenta consumidores noturnos ao inventário, para que o piloto interativo não oculte cópias antigas ainda utilizadas.
Manter integridade e âmbito
O grupo de integridade altera um byte da assinatura da CRL em DER e converte o objeto novamente para PEM. A estrutura continua legível, mas a verificação rejeita a assinatura. Ler um objeto não equivale a confiar nele. O laboratório também não demonstra revogação de todos os intermédios, CRLs indiretas ou delta, nem comportamento de endpoints TLS. A raiz emite diretamente os leaves e a verificação selecionada cobre o leaf. No handover, explica estas fronteiras e pede provas próprias para a cadeia real. Recuperação após compromisso de chave deve excluir o par exposto do rollback aprovado.
"""Original DR offline CRL/OCSP experiment. Disposable PKI, no network or TLS endpoint."""
import argparse
import datetime as dt
import hashlib
import json
import os
from pathlib import Path
import platform
import re
import subprocess
import tempfile
OPENSSL = os.environ.get('DR_OPENSSL_BIN', '/opt/homebrew/bin/openssl')
evidence = {'executedAt': dt.datetime.now(dt.timezone.utc).isoformat(),
'pythonVersion': platform.python_version(), 'platform': platform.platform(),
'commands': [], 'checks': {}, 'observations': {}}
def execute(*args, ok=False):
result = subprocess.run([OPENSSL, *args], cwd=base, capture_output=True, timeout=15)
text = (result.stdout + result.stderr).decode('utf-8', errors='replace')
evidence['commands'].append({'args': list(args), 'exitCode': result.returncode})
if ok and result.returncode:
raise RuntimeError('Command failed: ' + ' '.join(args) + '\n' + text)
return result.returncode, text
def write(name, value):
(base/name).write_text(value)
(base/name).chmod(0o600)
def observe(name, result):
code, text = result
evidence['observations'][name] = {'exitCode': code, 'output': text}
return code, text
def check(name, condition, **facts):
evidence['checks'][name] = {'passed': bool(condition), **facts}
def crl(name, start, end):
execute('ca', '-batch', '-config', 'ca.cnf', '-gencrl', '-crl_lastupdate', stamp(start),
'-crl_nextupdate', stamp(end), '-out', name, ok=True)
def verify(cert, *args):
return execute('verify', '-CAfile', 'ca.pem', '-no-CApath', '-no-CAstore',
'-purpose', 'sslserver', '-verify_hostname', 'api.fund.test', *args, cert)
def request(name, *args):
execute('ocsp', '-issuer', 'ca.pem', *args, '-reqout', name, ok=True)
def response(req, name):
execute('ocsp', '-index', 'index.txt', '-rsigner', 'ca.pem', '-rkey', 'ca.key',
'-CA', 'ca.pem', '-reqin', req, '-respout', name, '-ndays', '1', ok=True)
def inspect(req, resp, trust='ca.pem'):
return execute('ocsp', '-reqin', req, '-respin', resp, '-CAfile', trust,
'-no-CApath', '-no-CAstore')
parser = argparse.ArgumentParser()
parser.add_argument('--output', default='evidence.json')
output = Path(parser.parse_args().output).resolve()
with tempfile.TemporaryDirectory(prefix='dr-tls-status-') as directory:
base = Path(directory)
base.chmod(0o700)
evidence['opensslCLI'] = execute('version', ok=True)[1].strip()
now = dt.datetime.now(dt.timezone.utc).replace(microsecond=0)
stamp = lambda seconds: (now + dt.timedelta(seconds=seconds)).strftime('%Y%m%d%H%M%SZ')
write('req.cnf', '[req]\ndistinguished_name=dn\n[dn]\n')
for name in ['ca', 'wrong-ca', 'good', 'revoked']:
execute('genpkey', '-algorithm', 'EC', '-pkeyopt', 'ec_paramgen_curve:P-256', '-out', name+'.key', ok=True)
(base/(name+'.key')).chmod(0o600)
for name in ['ca', 'wrong-ca']:
execute('req', '-new', '-x509', '-config', 'req.cnf', '-key', name+'.key',
'-out', name+'.pem', '-subj', '/CN=DR synthetic '+name, '-days', '3',
'-addext', 'basicConstraints=critical,CA:TRUE',
'-addext', 'keyUsage=critical,keyCertSign,cRLSign',
'-addext', 'subjectKeyIdentifier=hash', ok=True)
write('index.txt', '')
write('serial', '1000\n')
write('crlnumber', '01\n')
(base/'newcerts').mkdir()
write('ca.cnf', '''[ca]
default_ca=fixture
[fixture]
database=index.txt
new_certs_dir=newcerts
certificate=ca.pem
private_key=ca.key
serial=serial
crlnumber=crlnumber
default_md=sha256
default_days=2
default_crl_days=1
policy=policy
unique_subject=no
x509_extensions=leaf
crl_extensions=crl_ext
[policy]
commonName=supplied
[leaf]
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature
extendedKeyUsage=serverAuth
subjectAltName=DNS:api.fund.test
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid
[crl_ext]
authorityKeyIdentifier=keyid:always
''')
for name in ['good', 'revoked']:
execute('req', '-new', '-config', 'req.cnf', '-key', name+'.key', '-out', name+'.csr',
'-subj', '/CN=DR synthetic '+name, ok=True)
execute('ca', '-batch', '-config', 'ca.cnf', '-in', name+'.csr', '-out', name+'.pem',
'-notext', ok=True)
crl('before.pem', -60, 3600)
execute('ca', '-batch', '-config', 'ca.cnf', '-revoke', 'revoked.pem', '-crl_reason', 'keyCompromise', ok=True)
crl('current.pem', -30, 3600)
crl('expired.pem', -7200, -3600)
crl('future.pem', 3600, 7200)
code, text = observe('no-crl-policy', verify('revoked.pem', '-CRLfile', 'current.pem'))
check('loading-crl-does-not-enable-revocation-check', code == 0, exitCode=code, crlLoaded=True, checkEnabled=False)
code, text = observe('good-with-current-crl', verify('good.pem', '-crl_check', '-CRLfile', 'current.pem'))
check('unrevoked-leaf-passes-explicit-crl-check', code == 0, exitCode=code)
code, text = observe('revoked-with-current-crl', verify('revoked.pem', '-crl_check', '-CRLfile', 'current.pem'))
check('revoked-leaf-fails-explicit-crl-check', code != 0 and 'certificate revoked' in text, exitCode=code, revokedReported='certificate revoked' in text)
code, text = observe('missing-crl', verify('good.pem', '-crl_check'))
check('missing-crl-is-not-a-good-status', code != 0 and 'unable to get certificate CRL' in text, exitCode=code, missingReported='unable to get certificate CRL' in text)
code, text = observe('expired-crl', verify('good.pem', '-crl_check', '-CRLfile', 'expired.pem'))
check('expired-crl-is-rejected', code != 0 and 'CRL has expired' in text, exitCode=code, expiredReported='CRL has expired' in text)
code, text = observe('future-crl', verify('good.pem', '-crl_check', '-CRLfile', 'future.pem'))
check('future-crl-is-rejected', code != 0 and 'CRL is not yet valid' in text, exitCode=code, futureReported='CRL is not yet valid' in text)
code, text = observe('older-still-valid-crl', verify('revoked.pem', '-crl_check', '-CRLfile', 'before.pem'))
check('older-valid-crl-does-not-contain-later-revocation', code == 0, exitCode=code, olderSnapshot=True, newestStateKnown=False)
execute('crl', '-in', 'current.pem', '-outform', 'DER', '-out', 'current.der', ok=True)
data = bytearray((base/'current.der').read_bytes()); data[-1] ^= 1
(base/'tampered.der').write_bytes(data)
execute('crl', '-inform', 'DER', '-in', 'tampered.der', '-out', 'tampered.pem', ok=True)
code, text = observe('tampered-crl', verify('good.pem', '-crl_check', '-CRLfile', 'tampered.pem'))
check('altered-crl-signature-is-rejected', code != 0 and 'CRL signature failure' in text, exitCode=code, signatureFailure='CRL signature failure' in text)
request('batch.req', '-cert', 'good.pem', '-cert', 'revoked.pem', '-serial', '9999')
response('batch.req', 'batch.resp')
code, text = observe('verified-ocsp-batch', inspect('batch.req', 'batch.resp'))
verified = code == 0 and 'Response verify OK' in text
# Report statuses for named CertIDs separately; nonce binding was checked above.
code, text = observe('ocsp-certificate-statuses', execute('ocsp', '-issuer', 'ca.pem',
'-cert', 'good.pem', '-cert', 'revoked.pem', '-serial', '9999', '-respin', 'batch.resp',
'-CAfile', 'ca.pem', '-no-CApath', '-no-CAstore', '-no_nonce'))
verified = verified and 'Response verify OK' in text
check('ocsp-good-is-a-certificate-status', verified and 'good.pem: good' in text, responseVerified=verified, goodReported='good.pem: good' in text)
check('verified-ocsp-response-can-report-revoked', verified and 'revoked.pem: revoked' in text, responseVerified=verified, revokedReported='revoked.pem: revoked' in text, exitCode=code)
check('verified-ocsp-response-can-report-unknown', verified and '9999: unknown' in text, responseVerified=verified, unknownReported='9999: unknown' in text, exitCode=code)
code, text = observe('wrong-ocsp-trust', inspect('batch.req', 'batch.resp', 'wrong-ca.pem'))
check('ocsp-response-signature-needs-approved-trust', code != 0 and 'Response Verify Failure' in text, exitCode=code, verificationFailed='Response Verify Failure' in text)
request('other-nonce.req', '-cert', 'good.pem', '-cert', 'revoked.pem', '-serial', '9999')
code, text = observe('mismatched-nonce', inspect('other-nonce.req', 'batch.resp'))
check('different-request-nonce-is-rejected', code != 0 and 'Nonce Verify error' in text, exitCode=code, nonceMismatch='Nonce Verify error' in text)
request('good-only.req', '-cert', 'good.pem')
response('good-only.req', 'good-only.resp')
# Suppress nonce comparison only for this deliberate CertID mismatch probe.
# Signature verification stays enabled. This is not a production policy.
code, text = observe('missing-certid', execute('ocsp', '-issuer', 'ca.pem', '-cert', 'revoked.pem',
'-respin', 'good-only.resp', '-CAfile', 'ca.pem', '-no-CApath', '-no-CAstore', '-no_nonce'))
check('another-certificate-response-is-not-applicable', 'Response verify OK' in text and 'No Status found' in text,
responseVerified='Response verify OK' in text, statusMissing='No Status found' in text, exitCode=code)
evidence['publicArtifactHashes'] = {name: hashlib.sha256((base/name).read_bytes()).hexdigest()
for name in ['before.pem', 'current.pem', 'expired.pem', 'future.pem', 'batch.req', 'batch.resp']}
evidence['temporaryMaterialDeleted'] = not base.exists()
evidence['systemClockChanged'] = False
evidence['networkUsed'] = False
evidence['scriptSha256'] = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
evidence['passed'] = sum(c['passed'] for c in evidence['checks'].values())
evidence['failed'] = len(evidence['checks']) - evidence['passed']
evidence['scope'] = ('Actual offline OpenSSL CRL generation/verification and OCSP request/signed-response processing. '
'Disposable direct CA-issued leaves, issuer-signed responses and local files only. '
'No responder service, HTTP cache, TLS stapling, delegated responder, full-chain CRL coverage, '
'delta/indirect CRLs, session resumption, existing-connection revocation, production integration or human workshop. '
'CRL timestamps are generated around the current instant; the system clock is never changed. '
'No private keys are exported into evidence.')
output.write_text(json.dumps(evidence, indent=2)+'\n')
print(json.dumps({'passed': evidence['passed'], 'failed': evidence['failed'],
'failedChecks': [n for n,c in evidence['checks'].items() if not c['passed']],
'temporaryMaterialDeleted': evidence['temporaryMaterialDeleted']}))
raise SystemExit(bool(evidence['failed']))
Exercício: a CRL chegou às três instâncias, mas uma continua sem verificação ativa e outra conserva before.pem. Preenche a matriz de probes e define o critério de fecho por instância.
Armadilhas comuns
Confundir ficheiro com controlo ativo, CRL ausente com revogação, validade temporal com estado mais recente ou leitura PEM com assinatura válida.
Tópicos relacionados: Identidade, nome e tempo · Revogação e transição de confiança · Ativação, contextos e ligações existentes
A decisão depende de política ativa e informação autêntica, aplicável e temporalmente aceitável; a distribuição precisa de evidência por consumidor.
Referência: OpenSSL verify: explicit certificate checks · DR TLS/certificates 2026-09; selected TLS 1.2/1.3, RFC 9525 identity and OpenSSL 3.5 diagnostics