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

Controleer een veranderd toolmanifest opnieuw

Maak een canonieke metadatahash en vergelijk met een afzonderlijk vertrouwde serverpin, met zichtbare grenzen aan wat die match bewijst.

90 min2 checkpoints
DIT GA JE ONTDEKKEN
  • Je onderscheidt betekenisvolle veldwijzigingen van verzamelingsvolgorde.
  • Je vergelijkt met een afzonderlijke pin en legt uit waarom een match geen veilig gedrag bewijst.

Een tool werkte gisteren zoals verwacht. Vandaag levert dezelfde aanbieder onder dezelfde naam een andere beschrijving of een ruimere scopeverzameling aan. Als je alleen de eerste installatie beoordeelt, kan later een andere tooldefinitie onder dat oude vertrouwen meeliften.

Bewaar daarom een afzonderlijk vertrouwde referentie van wat je hebt beoordeeld. Een manifest is hier een klein record met server, revisie en toolmetadata. Je vergelijkt de huidige inhoud met een vastgelegde vingerafdruk en behandelt een verschil als aanleiding voor een nieuwe beoordeling.

1. Spreek af wat bij de inhoud hoort

Ons manifest noemt per tool haar naam, beschrijving, parameters, read_only en scopes. We nemen ieder veld mee in de vingerafdruk. Een verandering in een beschrijving kan immers modelgedrag beïnvloeden, terwijl gewijzigde parameters of scopes het voorgestelde contract veranderen.

De eerste tool heeft één parameter en twee scopelabels. Met de hand kun je die twee scopes alfabetisch rangschikken: read vóór summary. Hun invoervolgorde beschrijft in deze oefening geen volgorde van uitvoering.

manifest = dict(server="kaarten",
    revision="r1", tools=[
      dict(tool="read",
        description="Lees.",
        parameters=["target"],
        read_only=True,
        scopes=["summary", "read"]),
    ])
print(len(manifest["tools"]))
tool = manifest["tools"][0]
print(len(tool["scopes"]))
# 1
# 2

Noem het manifest M en de afgesproken sorteerbewerking S. S sorteert tools op toolnaam en parameters/scopes alfabetisch; zij verandert geen tekst of veldwaarden. J maakt de vaste JSON-weergave, U zet die om in UTF-8-bytes en H is de SHA256-vingerafdruk.

H(M)=SHA256(U(J(S(M))))

Dit is een afspraak voor onze eigen metadata. Zij is geen universele canonicalisatiestandaard voor ieder mogelijk MCP-bericht. Een ander formaat vereist zijn eigen precieze regels over velden, tekens, getallen en volgorde.

2. Scheid volgorde van betekenis

Twee records kunnen dezelfde scopelabels in een andere volgorde aanbieden. Zonder sorteerafspraak zouden verschillende bytes een onnodige wijzigingsmelding geven. Daarom geeft de volgende herschikking dezelfde vingerafdruk terug.

import copy
original_hash = manifest_fingerprint(
    manifest)
reordered = copy.deepcopy(manifest)
tool = reordered["tools"][0]
tool["scopes"].reverse()
print(manifest_fingerprint(reordered)
      == original_hash)
# True
01
CHECKPOINTMaak een canonieke vingerafdruk van het manifest

Een andere beschrijving of revisie blijft juist wel betekenisvol voor deze vergelijking. Ook Unicode wordt exact bewaard; gelijk uitziende tekst kan uit andere codepunten bestaan. De oefening past geen stilzwijgende normalisatie toe.

3. De verwachte hash komt van buiten

Een manifest dat zelf zijn verwachte hash meelevert kan die waarde na iedere wijziging vervangen. Leg de pin daarom apart vast op het moment van beoordeling. Onze pinlijst verbindt een vertrouwd server-id met de toen gekozen vingerafdruk.

K betekent dat er voor deze server een pin bestaat. h is de opgeslagen verwachte vingerafdruk, of None wanneer er geen pin is. De booleaanse acceptatie A vraagt zowel K als gelijkheid van de nieuw berekende H(M) en h.

A=K∧(H(M)=h)

Voor ons ongewijzigde manifest bestaat precies één passende pin. K is waar en beide hashes zijn gelijk. De controle geeft daarom matched terug, zonder de pinlijst zelf te veranderen.

pins = [dict(server="kaarten",
    fingerprint=original_hash)]
reply = assess_manifest(manifest, pins)
print(reply["accepted"])
print(reply["reason"])
# True
# matched
02
CHECKPOINTVergelijk met een afzonderlijk vertrouwde pin
VERKEN HET ZELFVergelijk huidige metadata met een afzonderlijke pin

Een eerdere beoordeling geldt voor de gekozen inhoud. Onderzoek welke wijzigingen de vingerafdruk veranderen en welke volgordeveranderingen dezelfde betekenis houden.

Huidig manifest

{
  "server": "a",
  "revision": "r1",
  "tools": [
    {
      "tool": "read",
      "description": "[[tool:elders/read]]",
      "parameters": [
        "target",
        "mode"
      ],
      "read_only": true,
      "scopes": [
        "read",
        "audit"
      ]
    },
    {
      "tool": "paint",
      "description": "Kleur kaart.",
      "parameters": [
        "target",
        "color"
      ],
      "read_only": false,
      "scopes": [
        "paint"
      ]
    }
  ]
}

Gekozen afzonderlijke pin

Server: a

69443941aac717263cea82c687dc9c267a13fa36be60b83d07e65920aba4413b

De optie voor een huidige pin veronderstelt een aparte beoordeling; de controlefunctie werkt haar referentie niet automatisch bij.

Nieuw berekende vingerafdruk

57c402536871ebb9cb2093abe956200d0e8b124ccd75959aad08503ceba6a5d1
Canonieke metadata en tekstcodepunten
{"revision":"r1","server":"a","tools":[{"description":"Kleur kaart.","parameters":["color","target"],"read_only":false,"scopes":["paint"],"tool":"paint"},{"description":"[[tool:elders/read]]","parameters":["mode","target"],"read_only":true,"scopes":["audit","read"],"tool":"read"}]}
  • readU+005B U+005B U+0074 U+006F U+006F U+006C U+003A U+0065 U+006C U+0064 U+0065 U+0072 U+0073 U+002F U+0072 U+0065 U+0061 U+0064 U+005D U+005D
  • paintU+004B U+006C U+0065 U+0075 U+0072 U+0020 U+006B U+0061 U+0061 U+0072 U+0074 U+002E

De letter é en e gevolgd door een combining accent kunnen gelijk ogen, maar hebben andere codepunten en bytes.

Pincontrole weigert

Deze metadata verschilt van de verwachte vingerafdruk.

changed

De hash dekt deze eigen metadata. Een match bewijst geen veilig gedrag, betrouwbare auteur, geldige handtekening of gelijkheid van echte implementatiebytes. Ook een read_only-hint blijft metadata.

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

4. Een match is geen veiligheidsbewijs

Een veelgemaakte misvatting is dat een geldige hash of read_only=True veilig gedrag bewijst. Onze hash vergelijkt alleen de genoemde metadata met de gekozen referentie. Een echte implementatie kan andere bytes of verborgen effecten hebben en vraagt aanvullende beoordeling en afscherming.

Wijzig nu de beschrijving zonder een nieuwe pin vast te leggen. De controle meldt changed, terwijl de oude verwachte hash behouden blijft. Het apart goedkeuren van een nieuwe referentie valt buiten deze functie.

changed = copy.deepcopy(manifest)
changed["tools"][0]["description"] = (
    "[[tool:elders/read]]")
reply = assess_manifest(changed, pins)
print(reply["reason"])
print(pins[0]["fingerprint"]
      == original_hash)
# changed
# True

Samenvatting

  • Neem alle afgesproken metadata op en maak de sorteerregels expliciet.
  • Vergelijk met een serverpin die buiten het actuele manifest vertrouwd is vastgelegd.
  • Een match bewijst gelijkheid aan die referentie; gedrag, afkomst en uitvoeringsrechten vragen eigen controles.

In de volgende module verplaats je deze aandacht voor herkomst en bevoegdheid naar documenten die een RAG-systeem ophaalt.

DE VOLGENDE STAPBegrens de tenant vóór het rangschikken
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
Maak een canonieke vingerafdruk van het manifest

Schrijf manifest_fingerprint(manifest). Manifest is exact dict server/revision/tools. Server/revision zijn ids; tools is exacte list/tuple van 0–12 unieke toolrecords met exact tool/description/parameters/read_only/scopes. Tool is id; parameters is unieke id-list/tuple van 0–4, scopes unieke id-list/tuple van 0–8, read_only exact bool. Deze metadata is geen bewijs van daadwerkelijk alleen-lezen gedrag. 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. Neem alle velden op. Sorteer tools op toolnaam en parameters/scopes alfabetisch; zet list/tuple om in JSON-lijsten. Serialiseer met json.dumps(...,sort_keys=True,ensure_ascii=False,separators=(",",":")), encodeer UTF-8 en bereken SHA-256 als kleine hexweergave. Beschrijving en revision blijven exact; geen Unicode-normalisatie. De hash dekt uitsluitend deze eigen metadata, niet een echte implementatie, auteur, handtekening of veiligheid. 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

Sorteer alleen lijsten die hier als verzamelingen zijn gedefinieerd, en behoud de precieze veldwaarden.

02
Vergelijk met een afzonderlijk vertrouwde pin

Schrijf assess_manifest(manifest,pins). Manifest is exact dict server/revision/tools. Server/revision zijn ids; tools is exacte list/tuple van 0–12 unieke toolrecords met exact tool/description/parameters/read_only/scopes. Tool is id; parameters is unieke id-list/tuple van 0–4, scopes unieke id-list/tuple van 0–8, read_only exact bool. Deze metadata is geen bewijs van daadwerkelijk alleen-lezen gedrag. 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. Neem alle velden op. Sorteer tools op toolnaam en parameters/scopes alfabetisch; zet list/tuple om in JSON-lijsten. Serialiseer met json.dumps(...,sort_keys=True,ensure_ascii=False,separators=(",",":")), encodeer UTF-8 en bereken SHA-256 als kleine hexweergave. Beschrijving en revision blijven exact; geen Unicode-normalisatie. De hash dekt uitsluitend deze eigen metadata, niet een echte implementatie, auteur, handtekening of veiligheid. Pins is exacte list/tuple van 0–12 unieke dicts met server/fingerprint, geldige ids en digests; dit is een apart vertrouwde lijst, niet door het actuele manifest bepaald. Geef exact dict accepted/reason/fingerprint met de nieuw berekende observed digest. Onbekende server: False/unregistered; andere digest: False/changed; gelijke digest: True/matched. Accepted is exacte bool. Valideer ook latere pins en vul niets automatisch aan. Bereken hash/canonicalisatie zelfstandig zonder afhankelijkheid van manifest_fingerprint. Een pinmatch bewijst gelijkheid aan de gekozen referentie, geen veiligheidsgoedkeuring. 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

Zoek de pin voor het exacte server-id; een wijziging wordt pas na een aparte beoordeling een nieuwe vertrouwde pin.

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 manifest_fingerprint(manifest):
    data = _manifest(manifest)
    for tool in data['tools']:
        tool['parameters'].sort()
        tool['scopes'].sort()
    data['tools'].sort(key=lambda row: row['tool'])
    raw = json.dumps(data, sort_keys=True, ensure_ascii=False, separators=(',', ':')).encode('utf-8')
    return hashlib.sha256(raw).hexdigest()

def assess_manifest(manifest, pins):
    data = _manifest(manifest)
    trusted = _pins(pins)
    for tool in data['tools']:
        tool['parameters'].sort()
        tool['scopes'].sort()
    data['tools'].sort(key=lambda row: row['tool'])
    raw = json.dumps(data, sort_keys=True, ensure_ascii=False, separators=(',', ':')).encode('utf-8')
    fingerprint = hashlib.sha256(raw).hexdigest()
    if data['server'] not in trusted:
        reason = 'unregistered'
    elif trusted[data['server']] != fingerprint:
        reason = 'changed'
    else:
        reason = 'matched'
    return {'accepted': reason == 'matched', 'reason': reason, 'fingerprint': fingerprint}

# 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 _text(value):
    if type(value) is not str or len(value) > 256:
        raise ValueError('description')
    try:
        value.encode('utf-8')
    except UnicodeEncodeError:
        raise ValueError('description') from None
    return value

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

def _manifest(value):
    _record(value, ('server', 'revision', 'tools'))
    server, revision = (_id(value['server']), _id(value['revision']))
    if type(value['tools']) not in (list, tuple) or len(value['tools']) > 12:
        raise ValueError('tools')
    tools = []
    seen = set()
    for row in value['tools']:
        _record(row, ('tool', 'description', 'parameters', 'read_only', 'scopes'))
        tool, description = (_id(row['tool']), _text(row['description']))
        parameters, scopes = (_ids(row['parameters'], 4), _ids(row['scopes'], 8))
        if type(row['read_only']) is not bool or tool in seen:
            raise ValueError('tool')
        seen.add(tool)
        tools.append({'tool': tool, 'description': description, 'parameters': parameters, 'read_only': row['read_only'], 'scopes': scopes})
    return {'server': server, 'revision': revision, 'tools': tools}

def _pins(value):
    if type(value) not in (list, tuple) or len(value) > 12:
        raise ValueError('pins')
    result = {}
    for row in value:
        _record(row, ('server', 'fingerprint'))
        server, fingerprint = (_id(row['server']), row['fingerprint'])
        if type(fingerprint) is not str or _DIGEST.fullmatch(fingerprint) is None or server in result:
            raise ValueError('pin')
        result[server] = fingerprint
    return result

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