Werkplaats / De werkplaats
In jouw eigen tempo
← AI-security & red teaming
MODULE 03 · LES 10

Bind goedkeuring aan de precieze aanroep

Vergelijk de volledige callvingerafdruk met een afzonderlijke goedkeuring en toets actor, audience en actuele rol- en scopevoorwaarden.

105 min2 checkpoints
DIT GA JE ONTDEKKEN
  • Je berekent vaste bytes en een vingerafdruk voor server, tool en alle argumenten.
  • Je weigert een veranderde aanroep of ontoereikende actuele rechten zonder de gewone taak te blokkeren.

Je hebt toestemming gekregen om kaart-a te lezen. Vlak voor uitvoering verandert het doel ongemerkt in kaart-b, terwijl de applicatie nog dezelfde goedkeuring gebruikt. Een vinkje bij de toolnaam vertelt dan te weinig over het verzoek dat werkelijk wordt uitgevoerd.

Bewaar daarom eerst de precieze aanroep. Vergelijk bij uitvoering zowel dat vastgelegde verzoek als de huidige bevoegdheden. Deze oefening gebruikt eigen records; de vertrouwde context en goedkeuring zijn afzonderlijk aangeleverd, na authenticatie die buiten onze functies plaatsvindt.

1. Geef dezelfde aanroep dezelfde bytes

Een aanroep bevat server, tool en arguments. In ons kleine formaat zijn argumentnamen en waarden ids, zoals target en kaart-a. Alle velden horen bij de toestemming, ook als twee verzoeken dezelfde tool gebruiken.

Dicts kunnen dezelfde inhoud in een andere sleutelvolgorde bevatten. Spreek daarom één representatie af: gesorteerde keys, geen overbodige spaties en UTF-8 als bytecodering. Zo blijven betekenisvolle veranderingen zichtbaar zonder dat een andere invoegvolgorde een nieuw verzoek lijkt.

Noem de aanroep c, de canonieke JSON-weergave J en de omzetting naar UTF-8-bytes U. H is de vingerafdrukfunctie; SHA256 is de gebruikte standaard hashfunctie. Zij geeft hier een reeks van 64 kleine hextekens terug.

H(c)=SHA256(U(J(c)))

Dit ene verzoek heeft één argument. De afgesproken weergave telt 67 bytes; alle tekens in dit voorbeeld zijn ASCII en tellen elk als één byte. De code controleert de lengte en toont alleen de eerste acht tekens van de volledige vingerafdruk.

call = dict(server="kaarten",
    tool="read",
    arguments={"target":"kaart-a"})
raw = json.dumps(call, sort_keys=True,
    ensure_ascii=False,
    separators=(",", ":"))
print(len(raw.encode("utf-8")))
print(call_fingerprint(call)[:8])
# 67
# 8f84da43
01
CHECKPOINTBind alle aanroepvelden aan één vingerafdruk

Een hash is geen toestemming en verbergt de argumenten niet. Iemand kan zelf een hash van een ongewenst verzoek berekenen. De referentie waarmee je vergelijkt moet daarom uit het afzonderlijk vertrouwde goedkeuringsrecord komen.

2. Controleer ook de huidige bevoegdheden

Onze context noemt actor, rol, audience en scopes. Audience is de server waarvoor deze oefencontext bedoeld is. Het applicatiebeleid noemt per volledig server/toolpaar toegestane rollen en vereiste scopes; de toolbeschrijving stelt dit beleid niet vast.

Je kunt ontbrekende scopes met twee kleine verzamelingen volgen. Het beleid vraagt read en audit, terwijl de context alleen read heeft. Er ontbreekt dus precies één scope, ook als het verzoek en de goedkeuring verder overeenkomen.

required = {"read", "audit"}
granted = {"read"}
missing = sorted(required - granted)
print(missing)
print(len(missing))
# ['audit']
# 1

De beslissing A vereist zeven booleaanse controles. p betekent dat het policypaar bestaat, a dat audience klopt, i dat de actor bij de goedkeuring hoort en s dat haar server klopt. h betekent gelijke callhash, r een toegestane huidige rol en q dat alle vereiste scopes aanwezig zijn.

A=p∧a∧i∧s∧h∧r∧q

In het gewone geval hieronder zijn alle zeven voorwaarden waar. De goedkeuring bewaart de actor, server en volledige vingerafdruk. De regel vraagt hier alleen read, zodat de normale taak door kan gaan.

context = dict(actor="alice",
    role="reader", audience="kaarten",
    scopes=["read"])
policy = [dict(server="kaarten",
    tool="read", roles=["reader"],
    scopes=["read"])]
approval = dict(actor="alice",
    server="kaarten",
    fingerprint=call_fingerprint(call))
reply = authorize_bound_call(
    call, context, policy, approval)
print(reply["allowed"])
print(reply["reason"])
# True
# allowed
02
CHECKPOINTToets goedkeuring en actuele bevoegdheden samen
VERKEN HET ZELFHoort de goedkeuring nog bij deze aanroep?

Verander de call of de huidige bevoegdheden. Een oude goedkeuring past alleen bij hetzelfde vastgelegde verzoek en voldoende actuele rechten.

Voorgestelde aanroep

{
  "server": "a",
  "tool": "read",
  "arguments": {
    "target": "kaart-b",
    "mode": "kort"
  }
}

Vingerafdruk van deze call

32d39ee5227bad01b890f5e5d24bc191309813c8eb4539483cf8f2ff54bcfe27

Huidige vertrouwde context

Actor
alice
Rol
reader
Audience
a
Scopes
read

Deze context is apart vertrouwd na authenticatie buiten de oefening.

Afzonderlijke goedkeuring

Actor
alice
Server
a

Vastgelegde vingerafdruk

6de0afc38bff7c90a607216ef5b276d328c5c0509fa8acafbf8632970abc5c53

Huidig applicatiebeleid

a/read
Toegestane rollen
reader
Vereiste scopes
read

Dit beleid wordt niet uit de toolbeschrijving afgeleid.

Alleen de callhash vergeleken: verschillend.

Canonieke JSON van deze aanroep
{"arguments":{"mode":"kort","target":"kaart-b"},"server":"a","tool":"read"}
Geweigerd

De goedkeuring hoort bij andere aanroepbytes.

Ontbrekende scopes bij deze reden: []

De eerste weigering bepaalt de reden. Alleen bij reden scope worden ontbrekende scopes genoemd. Dit eigen id-mapformaat vervangt geen volledig toolschema, authenticatie of eenmalige goedkeuringsadministratie.

Alle beschrijvingen, aanroepen, bevoegdheden en pins zijn eigen oefengegevens. Deze vergelijking roept geen echte tool of server aan.

3. Veranderingen vragen een nieuwe beoordeling

Een veelgemaakte fout is de goedgekeurde hash behandelen als blijvende bevoegdheid. Een gewijzigd argument moet de vergelijking laten mislukken; gewijzigde rollen of scopes moeten opnieuw door het huidige beleid worden getoetst. De functie rapporteert de eerste weigering volgens de afgesproken prioriteit.

changed = dict(server="kaarten",
    tool="read",
    arguments={"target":"kaart-b"})
reply = authorize_bound_call(
    changed, context, policy, approval)
print(reply["allowed"])
print(reply["reason"])
# False
# fingerprint

Wij bouwen hiermee geen JWT-verificatie, tokenuitgifte, verval of eenmalige consumptie. Een herhaalde identieke aanroep kan onder hetzelfde huidige beleid weer slagen. De functies laten precies zien welke binding onze eigen beslissing controleert.

Samenvatting

  • Leg server, tool en alle argumenten samen vast.
  • Vergelijk met een apart vertrouwde goedkeuring en toets actuele bevoegdheden.
  • Een gelijke vingerafdruk vervangt geen actor-, audience-, rol- of scopecontrole.

Hierna onderzoek je wat er gebeurt wanneer de tooldefinitie zelf verandert nadat je haar eerder hebt beoordeeld.

DE VOLGENDE STAPControleer een veranderd toolmanifest opnieuw
JOUW EXPERIMENT

Probeer het zelf

Python
main.py

Python start zodra jij code uitvoert.

UITVOER

Je uitvoer verschijnt hier.
Alles rekent in je eigen browser.

Jouw checkpoints0 / 2
01
Bind alle aanroepvelden aan één vingerafdruk

Schrijf call_fingerprint(call). Call is exact dict server/tool/arguments. Server/tool zijn ids. Arguments is exact dict met 0–4 id-keys en id-waarden, een eigen beperkt oefenformaat. Een id is exact str met ASCII [a-z][a-z0-9-]{0,31}. Beschrijvingen zijn exacte strs van 0–256 Python-codepunten die geldig UTF-8 encoderen. Een digest is exact str met 64 kleine hextekens. Canonieke callbytes zijn UTF-8 van json.dumps(validated_call, sort_keys=True, ensure_ascii=False, separators=(",", ":")). Alle server/tool/argumentwaarden tellen mee; dictkeyvolgorde niet. Bereken de SHA-256 met hashlib en geef de kleine hexweergave. Geen gegeven helper doet deze hashing. Gebruik exacte Python-builtins en gewone str-keys; geen typeconversie of normalisatie. Valideer alle invoer, ook latere records wanneer een eerdere weigering al vaststaat. Ongeldige invoer geeft ValueError. Behoud inhoud, sleutelvolgorde, readonlyflags en Python-/NumPy-RNG, ook bij fouten. Alle mutabele resultaten zijn nieuw, zonder interne aliases of gedeelde containers met invoer of herhaalde resultaten. Gegeven helpers doen validatie en kopiëren; jij schrijft de beslissing of hashberekening. Iedere functie werkt als alle andere vijf leerlingfuncties nog TODO zijn. Geen echte tools, servers, tokens, bestanden of netwerkacties gebruiken.

Een hint

Bouw eerst de exacte canonieke callbytes; hash daarna die bytes.

02
Toets goedkeuring en actuele bevoegdheden samen

Schrijf authorize_bound_call(call,context,policy,approval). Call is exact dict server/tool/arguments. Server/tool zijn ids. Arguments is exact dict met 0–4 id-keys en id-waarden, een eigen beperkt oefenformaat. Een id is exact str met ASCII [a-z][a-z0-9-]{0,31}. Beschrijvingen zijn exacte strs van 0–256 Python-codepunten die geldig UTF-8 encoderen. Een digest is exact str met 64 kleine hextekens. Canonieke callbytes zijn UTF-8 van json.dumps(validated_call, sort_keys=True, ensure_ascii=False, separators=(",", ":")). Alle server/tool/argumentwaarden tellen mee; dictkeyvolgorde niet. Bereken de SHA-256 met hashlib als kleine hexweergave voor de interne vergelijking; retourneer uitsluitend het hieronder genoemde beslisrecord. Geen gegeven helper doet deze hashing. Context is apart vertrouwd exact dict actor/role/audience/scopes; de eerste drie zijn ids, scopes unieke id-list/tuple 0–8. Het is oefencontext NA externe authenticatie, geen door ons gevalideerde JWT. Policy is list/tuple 0–12 unieke server/toolregels met exact server/tool/roles/scopes; rollen en scopes zijn unieke id-list/tuple 0–8. Approval is apart vertrouwd exact dict actor/server/fingerprint met geldige ids/digest. Redenprioriteit: onbekend policypaar unknown-tool; verkeerde contextaudience audience; contextactor verschilt van approvalactor actor; approvalserver verschilt van callserver approval-server; hash verschilt fingerprint; contextrol niet in roles role; ontbrekende vereiste scopes scope; anders allowed. Geef exact dict allowed/reason/missing_scopes: allowed is exacte bool, alleen bij allowed True. Missing_scopes is alleen bij reden scope een nieuwe gesorteerde lijst van ontbrekende scopes, anders []. Bereken de hash zelfstandig; roep call_fingerprint niet als afhankelijke leerlingfunctie aan. Huidig beleid geldt bij iedere aanroep. Dit bouwt geen eenmalige consumptie, verval of OAuth-protocol. Gebruik exacte Python-builtins en gewone str-keys; geen typeconversie of normalisatie. Valideer alle invoer, ook latere records wanneer een eerdere weigering al vaststaat. Ongeldige invoer geeft ValueError. Behoud inhoud, sleutelvolgorde, readonlyflags en Python-/NumPy-RNG, ook bij fouten. Alle mutabele resultaten zijn nieuw, zonder interne aliases of gedeelde containers met invoer of herhaalde resultaten. Gegeven helpers doen validatie en kopiëren; jij schrijft de beslissing of hashberekening. Iedere functie werkt als alle andere vijf leerlingfuncties nog TODO zijn. Geen echte tools, servers, tokens, bestanden of netwerkacties gebruiken.

Een hint

Een overeenkomende vingerafdruk bewaart het verzoek; controleer daarnaast identiteit, audience en actuele rol/scopes.

Vastgelopen? Bekijk de voorbeeldoplossing
import hashlib
import json
import re
_ID = re.compile('[a-z][a-z0-9-]{0,31}', re.ASCII)
_DIGEST = re.compile('[0-9a-f]{64}', re.ASCII)

# Jouw functies

def call_fingerprint(call):
    data = _call(call)
    raw = json.dumps(data, sort_keys=True, ensure_ascii=False, separators=(',', ':')).encode('utf-8')
    return hashlib.sha256(raw).hexdigest()

def authorize_bound_call(call, context, policy, approval):
    data = _call(call)
    trusted = _context(context)
    rules = _policy(policy)
    approved = _approval(approval)
    raw = json.dumps(data, sort_keys=True, ensure_ascii=False, separators=(',', ':')).encode('utf-8')
    fingerprint = hashlib.sha256(raw).hexdigest()
    pair = (data['server'], data['tool'])
    missing = []
    if pair not in rules:
        reason = 'unknown-tool'
    elif trusted['audience'] != data['server']:
        reason = 'audience'
    elif trusted['actor'] != approved['actor']:
        reason = 'actor'
    elif approved['server'] != data['server']:
        reason = 'approval-server'
    elif approved['fingerprint'] != fingerprint:
        reason = 'fingerprint'
    elif trusted['role'] not in rules[pair]['roles']:
        reason = 'role'
    else:
        missing = sorted(set(rules[pair]['scopes']) - set(trusted['scopes']))
        reason = 'scope' if missing else 'allowed'
    return {'allowed': reason == 'allowed', 'reason': reason, 'missing_scopes': missing}

# Gegeven validatiehelpers

def _record(value, keys):
    if type(value) is not dict or any((type(key) is not str for key in value)) or set(value) != set(keys):
        raise ValueError('record')
    return value

def _id(value):
    if type(value) is not str or _ID.fullmatch(value) is None:
        raise ValueError('id')
    return value

def _digest(value):
    if type(value) is not str or _DIGEST.fullmatch(value) is None:
        raise ValueError('digest')
    return value

def _ids(value):
    if type(value) not in (list, tuple) or len(value) > 8:
        raise ValueError('ids')
    result = [_id(item) for item in value]
    if len(set(result)) != len(result):
        raise ValueError('duplicate id')
    return result

def _call(value):
    _record(value, ('server', 'tool', 'arguments'))
    server, tool = (_id(value['server']), _id(value['tool']))
    arguments = value['arguments']
    if type(arguments) is not dict or len(arguments) > 4:
        raise ValueError('arguments')
    copied = {_id(key): _id(item) for key, item in arguments.items()}
    return {'server': server, 'tool': tool, 'arguments': copied}

def _context(value):
    _record(value, ('actor', 'role', 'audience', 'scopes'))
    return {'actor': _id(value['actor']), 'role': _id(value['role']), 'audience': _id(value['audience']), 'scopes': _ids(value['scopes'])}

def _policy(value):
    if type(value) not in (list, tuple) or len(value) > 12:
        raise ValueError('policy')
    result = {}
    for row in value:
        _record(row, ('server', 'tool', 'roles', 'scopes'))
        pair = (_id(row['server']), _id(row['tool']))
        roles, scopes = (_ids(row['roles']), _ids(row['scopes']))
        if pair in result:
            raise ValueError('duplicate rule')
        result[pair] = {'roles': roles, 'scopes': scopes}
    return result

def _approval(value):
    _record(value, ('actor', 'server', 'fingerprint'))
    return {'actor': _id(value['actor']), 'server': _id(value['server']), 'fingerprint': _digest(value['fingerprint'])}

Voer de code uit en verklaar voor jezelf waarom elke stap werkt.