Connect P04 audited recovery to fresh-MFA platform browser flow
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
parent
317d897b85
commit
cb51584f58
5 changed files with 256 additions and 2 deletions
|
|
@ -101,6 +101,49 @@ def run():
|
|||
check('platform_recovery_disabled_factor',applied['status']=='recovered' and store.snapshot('alice',serial)['active'] is False)
|
||||
check('platform_recovery_audit_durable',store.receipt(request['reference'])['complete'] is True)
|
||||
check('platform_recovery_replay_no_mutation',recover(store,approved)['replayed'] is True)
|
||||
phase='authenticated_recovery_service'
|
||||
from recovery_service import RecoveryService,verify_token
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from werkzeug.test import Client
|
||||
from werkzeug.wrappers import Response
|
||||
import jwt
|
||||
signing=rsa.generate_private_key(public_exponent=65537,key_size=2048)
|
||||
now=int(time.time())
|
||||
operator_claims=dict(iss='https://fixture.invalid',aud='fixture-portal',sub='fixture-operator',
|
||||
iat=now,exp=now+300,principal_type='human',roles=['platform-operator'],assurance=dict(level='aal2',mfa=True,at=now))
|
||||
def signed(**changes):return jwt.encode(dict(operator_claims,**changes),signing,algorithm='RS256')
|
||||
with app.app_context():
|
||||
service=RecoveryService(store,lambda token:verify_token(token,signing.public_key(),'https://fixture.invalid','fixture-portal'),b'fixture-ticket-key')
|
||||
service_client=Client(service,Response)
|
||||
from privacyidea.lib.token import enable_token
|
||||
enable_token(serial,enable=True)
|
||||
def operation(body,token=None):
|
||||
r=service_client.post('/recover',json=body,headers={'Authorization':'Bearer '+(token or signed())})
|
||||
return r.status_code,r.json
|
||||
preview_body=dict(action='preview',user='alice',reference='fixture-browser-recovery')
|
||||
for name,token in [('tenant_admin',signed(roles=['tenant-admin'])),('wrong_audience',signed(aud='other')),('stale_mfa',signed(assurance=dict(level='aal2',mfa=True,at=now-301))),('unsigned','invalid')]:
|
||||
status,_=operation(preview_body,token)
|
||||
check('service_denies_'+name,status==403 and store.snapshot('alice',serial)['active'])
|
||||
status,preview=operation(preview_body)
|
||||
check('service_preview_owned_factor',status==200 and len(preview['factors'])==1)
|
||||
confirmation=preview['factors'][0]['confirmation']
|
||||
apply_body=dict(action='apply',confirmation=confirmation,identity_verified=True)
|
||||
status,_=operation(dict(apply_body,confirmation=confirmation+'x'))
|
||||
check('service_rejects_changed_confirmation',status==409 and store.snapshot('alice',serial)['active'])
|
||||
status,receipt=operation(apply_body)
|
||||
check('service_audited_recovery',status==200 and receipt['changes_applied'] and store.receipt('fixture-browser-recovery')['complete'])
|
||||
status,receipt=operation(apply_body)
|
||||
check('service_safe_retry',status==200 and receipt['replayed'])
|
||||
phase='replacement_possession'
|
||||
detail=enroll();serial=detail['serial']
|
||||
seed=urllib.parse.parse_qs(urllib.parse.urlsplit(detail['googleurl']['value']).query)['secret'][0]
|
||||
key=base64.b32decode(seed+'='*((-len(seed))%8))
|
||||
code,body=req('POST','/token/init',{'serial':serial,'type':'totp','verify':otp()},user)
|
||||
check('replacement_possession_confirmed',code==200 and body['result']['value'] is True)
|
||||
digest=hmac.new(key,struct.pack('>Q',int(time.time())//30+1),hashlib.sha1).digest();offset=digest[-1]&15
|
||||
next_code=str((struct.unpack('>I',digest[offset:offset+4])[0]&0x7fffffff)%1000000).zfill(6)
|
||||
code,body=req('POST','/validate/check',{'user':'alice','realm':'fixture','pass':next_code},reader)
|
||||
check('replacement_authentication_succeeds',code==200 and body['result']['value'] is True and body['detail']['serial']==serial)
|
||||
phase='finished';result['success']=True
|
||||
with contextlib.redirect_stdout(io.StringIO()),contextlib.redirect_stderr(io.StringIO()):
|
||||
try:run()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue