44 lines
3.7 KiB
Python
44 lines
3.7 KiB
Python
from http_helpers import *
|
|
import base64,hashlib,struct,pathlib
|
|
# Reset only the disposable plugin fixture.
|
|
for p in (pathlib.Path(FIXTURE)/'bl-content/databases/plugins/bludit-secure').glob('secure.*.php'):p.unlink()
|
|
PHP_CMD=[PHP,'-c',INI]
|
|
b64=lambda x:base64.urlsafe_b64encode(x).decode().rstrip('=')
|
|
def cbor(x):
|
|
def head(major,n):
|
|
return bytes([major*32+n]) if n<24 else bytes([major*32+24,n]) if n<256 else bytes([major*32+25])+struct.pack('>H',n)
|
|
if isinstance(x,int):return head(0,x) if x>=0 else head(1,-x-1)
|
|
if isinstance(x,bytes):return head(2,len(x))+x
|
|
if isinstance(x,str):return head(3,len(x.encode()))+x.encode()
|
|
if isinstance(x,dict):return head(5,len(x))+b''.join(cbor(k)+cbor(v) for k,v in x.items())
|
|
keyfile=str(pathlib.Path(FIXTURE)/'test-key.pem')
|
|
raw=subprocess.check_output(PHP_CMD+['-r',f'$k=openssl_pkey_new(["private_key_type"=>OPENSSL_KEYTYPE_EC,"curve_name"=>"prime256v1"]);openssl_pkey_export($k,$pem);file_put_contents("{keyfile}",$pem);$d=openssl_pkey_get_details($k);echo json_encode([base64_encode($d["ec"]["x"]),base64_encode($d["ec"]["y"])]);'])
|
|
x,y=map(base64.b64decode,json.loads(raw))
|
|
s,b,h=req();token=csrf(b)
|
|
s,b,h=req('/admin/login?bs=password',{'secureCsrf':token,'username':'admin','password':'TestPassword123'});s,b,h=req('/admin/plugin/pluginbluditsecure');token=csrf(b)
|
|
def post(action,**fields):return req('/admin/login?bs='+action,{'secureCsrf':token,**fields})
|
|
s,b,h=post('key-create-options');options=json.loads(b);check(s==200,'registration options')
|
|
cred=b'bludit-test-key-1';rp=hashlib.sha256(b'127.0.0.1').digest()
|
|
auth=rp+b'\x45'+struct.pack('>I',0)+b'\0'*16+struct.pack('>H',len(cred))+cred+cbor({1:2,3:-7,-1:1,-2:x,-3:y})
|
|
client=json.dumps({'type':'webauthn.create','challenge':options['publicKey']['challenge'],'origin':'https://127.0.0.1:18765'}).encode()
|
|
s,b,h=post('key-create-finish',clientDataJSON=b64(client),attestationObject=b64(cbor({'fmt':'none','attStmt':{},'authData':auth})),label='Test passkey');check(s==200,'signed registration accepted')
|
|
s,b,h=req('/admin/plugin/pluginbluditsecure');check('Test passkey' in b,'registered passkey listed');token=csrf(b)
|
|
post('logout');s,b,h=req();token=csrf(b);post('password',username='admin',password='TestPassword123');s,b,h=req('/admin/');token=csrf(b);check('Two-step verification' in b and 'login-card' in b,'MFA uses native login layout')
|
|
check('data-auto-passkey="true"' in b,'passkey prompt starts automatically')
|
|
check(re.search(r'assets/secure\.js\?v=[0-9a-f]{16}', b), 'login script URL has a content hash to invalidate stale caches')
|
|
check('name="password"' not in b and 'assets/secure.css' not in b,'native password form replaced only during second phase')
|
|
def assertion(origin,valid=True):
|
|
s,b,h=post('key-get-options');check(s==200,'assertion options');opts=json.loads(b)
|
|
client=json.dumps({'type':'webauthn.get','challenge':opts['publicKey']['challenge'],'origin':origin}).encode()
|
|
auth=rp+b'\x05'+struct.pack('>I',1)
|
|
msg=auth+hashlib.sha256(client).digest()
|
|
signature=subprocess.check_output(['openssl','dgst','-sha256','-sign',keyfile],input=msg)
|
|
return post('key-get-finish',id=b64(cred),clientDataJSON=b64(client),authenticatorData=b64(auth),signature=b64(signature if valid else b'bad'))
|
|
s,b,h=assertion('https://evil.example');check(s==400,'wrong origin rejected')
|
|
s,b,h=assertion('https://127.0.0.1:18765',False);check(s==400,'bad signature rejected')
|
|
s,b,h=assertion('https://127.0.0.1:18765');check(s==200 and 'redirect' in json.loads(b),'valid signed assertion completes MFA')
|
|
s,b,h=req('/admin/plugin/pluginbluditsecure');check('Security' in b,'WebAuthn authenticated session')
|
|
# A finish request cannot reuse a consumed challenge.
|
|
s,b,h=post('key-get-finish',id=b64(cred));check(s==400,'consumed challenge rejected')
|
|
print('WebAuthn integration checks complete')
|