key-cape/scripts/test_openbao_negative_probe.py
tegwick bae9f85160 KEY-WP-0039: probe registration packet and attended negative-probe script
T01 done: railiance-platform agreed the case list. The packet adds an
otherwise-valid expiry client and pins audience on the claim probes so each
token fails only the claim under test. The script prints one refused boolean
and refusal class per case, revokes any accepted token, and never counts a
transport failure as a refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 779409@bnt-lap001
Assistant-Session: c658ca06-19b5-468d-be64-2985dc20bfd3
2026-10-10 13:18:24 +02:00

99 lines
4.1 KiB
Python

import base64
import json
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(__file__))
import openbao_negative_probe as probe # noqa: E402
TOKEN_URL = 'https://kc.example/token'
BAO = 'https://bao.example'
SECRET = 'not-a-real-secret'
SECRETS = {cid: SECRET for _, cid, _ in probe.CLAIM_PROBES + [probe.EXPIRY_PROBE, probe.CODING_AGENT]}
def fake_jwt(client_id, exp=1000):
payload = base64.urlsafe_b64encode(json.dumps({'exp': exp, 'cid': client_id}).encode()).decode().rstrip('=')
return f'h.{payload}.s'
class FakeBao:
"""KeyCape and OpenBao in one: refuses every token unless told to accept it."""
def __init__(self, accept=(), login_status=None):
self.accept = set(accept)
self.login_status = login_status
self.revoked = 0
def __call__(self, url, form=None, body=None, headers=None, timeout=15):
if url == TOKEN_URL:
cid = base64.b64decode(headers['Authorization'].split()[1]).decode().split(':')[0]
return 200, json.dumps({'access_token': fake_jwt(cid)}).encode()
if url.endswith('/revoke-self'):
self.revoked += 1
return 204, b''
cid = json.loads(base64.urlsafe_b64decode(body['jwt'].split('.')[1] + '==')).get('cid', 'human')
if self.login_status:
return self.login_status, b''
if cid in self.accept:
return 200, json.dumps({'auth': {'client_token': 'bao-token'}}).encode()
reason = {'openbao-probe-expiry': 'token is expired',
'human': 'claim "principal_type" does not match'}.get(cid, 'claim "sub" does not match any bound value')
return 400, json.dumps({'errors': [reason]}).encode()
def run(fake, **kw):
kw.setdefault('human_token', fake_jwt('human'))
return probe.run(fake, SECRETS, TOKEN_URL, BAO, 'keycape-services', 'secrets-engine-openbao',
sleep=lambda s: None, now=lambda: 0, **kw)
class ProbeTest(unittest.TestCase):
def test_all_refused_with_classes(self):
receipt = run(FakeBao())
self.assertTrue(receipt['all_refused'])
classes = {c['case']: c['class'] for c in receipt['cases']}
self.assertEqual(classes['expired'], 'expired')
self.assertEqual(classes['principal_type'], 'not_service_principal')
self.assertEqual(classes['sub'], 'claim_mismatch')
self.assertEqual(len(receipt['cases']), 8)
def test_accepted_token_fails_and_is_revoked(self):
fake = FakeBao(accept={'openbao-probe-tenant'})
receipt = run(fake)
self.assertFalse(receipt['all_refused'])
tenant = next(c for c in receipt['cases'] if c['case'] == 'tenant')
self.assertEqual((tenant['refused'], tenant['class'], tenant['revoked']), (False, 'accepted', True))
self.assertEqual(fake.revoked, 1)
def test_transport_failure_is_never_a_refusal(self):
receipt = run(FakeBao(login_status=503))
self.assertFalse(receipt['all_refused'])
self.assertTrue(all(c['class'] == 'inconclusive' for c in receipt['cases']))
def test_missing_inputs_are_inconclusive(self):
receipt = run(FakeBao(), human_token=None)
pt = next(c for c in receipt['cases'] if c['case'] == 'principal_type')
self.assertEqual(pt['class'], 'inconclusive')
self.assertFalse(receipt['all_refused'])
def test_receipt_carries_no_values(self):
text = json.dumps(run(FakeBao(accept={'openbao-probe-aud'})))
for value in (SECRET, 'bao-token', 'does not match', 'h.'):
self.assertNotIn(value, text)
def test_expiry_waits_past_leeway(self):
waits = []
probe.run(FakeBao(), SECRETS, TOKEN_URL, BAO, 'm', 'r', human_token=fake_jwt('human'),
leeway=150, sleep=waits.append, now=lambda: 900)
self.assertEqual(waits, [1000 + 150 + 10 - 900])
def test_classify(self):
self.assertEqual(probe.classify('error validating token: token is expired'), 'expired')
self.assertEqual(probe.classify('aud claim does not match any bound audience'), 'claim_mismatch')
self.assertEqual(probe.classify('permission denied'), 'refused_other')
if __name__ == '__main__':
unittest.main()