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

Vertrouw herkomst buiten het document

Vergelijk volledige recordbytes met afzonderlijke receipts en zoek daarna alleen in toegelaten kaarten.

90 min2 checkpoints
DIT GA JE ONTDEKKEN
  • Je bindt inhoud, metadata en vector aan dezelfde canonieke recordhash.
  • Je vergelijkt met een apart beoordeelde receipt zonder een pinmatch als waarheid te behandelen.

Je zoekmachine vindt een overtuigend kaartje met het verkeerde kleurantwoord. Onderaan staat dezelfde bronnaam als op je vertrouwde handleiding. Als de schrijver dat bronlabel zelf mocht invullen, vertelt het label nog niet wie de inhoud heeft beoordeeld.

De invoer van het corpus is daarom een eigen vertrouwensgrens. We vergelijken een aangeboden record met een afzonderlijk bewaarde referentie. Zo blijft een gewijzigde kaart niet ongemerkt onder een eerdere beoordeling vallen.

1. Leg vast welke inhoud je bedoelt

Een record bestaat hier uit id, tenant, source, revision, text en vector. De eerste vijf bepalen identiteit, herkomst en inhoud; de vector beïnvloedt de rangschikking. Alleen de zichtbare tekst controleren zou een gewijzigde zoekpositie missen.

Onze kaart vermeldt color=blue. Dat zijn tien tekens; color=red zou negen tekens bevatten. Het verschil is met de hand zichtbaar, ook al blijven id en source gelijk.

record = dict(id="kaart", tenant="a",
    source="manual", revision="r1",
    text="color=blue", vector=[2, 0])
print(len(record["text"]))
print(len("color=red"))
print(len(record_fingerprint(record)))
# 10
# 9
# 64

B(r) is onze vaste UTF-8-weergave van het volledige record r. We sorteren objectkeys alfabetisch en gebruiken JSON zonder extra scheidingsspaties. H(r) is de SHA256-vingerafdruk van die bytes, weergegeven met 64 kleine hextekens.

H(r)=SHA256(B(r))

De keyvolgorde van een dict verandert die afspraak niet. De volgorde van vectorcoördinaten is juist wel betekenisvol. Tekst blijft exact bewaard: dezelfde zichtbare letter kan uit andere Unicode-codepunten bestaan en daardoor andere bytes geven.

01
CHECKPOINTBind alle recordvelden aan een vingerafdruk

2. De referentie komt van buiten

Een document dat zijn eigen verwachte hash mag kiezen, kan na een verandering ook die verwachting vervangen. Bewaar de ontvangstlijst daarom apart, vanuit een beoordelingsproces buiten de aangeboden inhoud. Onze receipt verbindt een record-id met één vastgelegde volledige vingerafdruk.

receipts = [dict(id="kaart",
    fingerprint=record_fingerprint(
        record))]
reply = admit_pinned([record], receipts)
print([r["id"] for r in
       reply["accepted"]])
# ['kaart']

Het ongewijzigde record blijft bruikbaar voor de gewone zoektaak. De toelatingsfunctie levert nieuwe recorddicts en nieuwe vectorlijsten, zodat latere aanpassingen aan het resultaat de oorspronkelijke invoer niet veranderen. Ook de receiptlijst blijft intact.

3. Een corpuswijziging vraagt aandacht

Noem K de vraag of voor dit id een receipt bestaat. h is de apart vastgelegde hash; A betekent dat de kaart wordt toegelaten. Beide voorwaarden moeten gelden.

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

Wijzig nu alleen de kleurtekst. Een naïeve corpusroute zou beide schemageldige kaarten opnemen; onze controle vergelijkt de veranderde inhoud met de oorspronkelijke receipt. De bronnaam manual kan die vergelijking niet overschrijven.

changed = dict(record, text="color=red")
reply = admit_pinned(
    [changed], receipts)
print(reply["rejected"][0]["reason"])
print(len(reply["accepted"]))
# changed
# 0

Zonder receipt is de reden unregistered; bij een afwijkende hash is zij changed. De functie voegt geen referenties toe en verandert geen bestaande hashes. Volledige invoervalidatie blijft gelden, ook wanneer de aangeboden lijst geen passende id bevat.

02
CHECKPOINTVergelijk corpusinvoer met apart bewaarde receipts
VERKEN HET ZELFWelke kaart hoort bij de oude beoordeling?

Verander de aangeboden inhoud en vergelijk haar volledige hash met een afzonderlijke ontvangstlijst. Daarna zoekt de vaste tenant a alleen in toegelaten records.

Aangeboden record

{
  "id": "doc-a",
  "tenant": "a",
  "source": "manual",
  "revision": "r1",
  "text": "color=red\nlabel=café",
  "vector": [
    2,
    0
  ]
}

Volledige recordhash

be2ad09c18b4599cce220e4336e0808ea64ff15001e8a0b09965c8c0c1397532

Afzonderlijk gekozen receipts

Record-id: doc-a

35d1be292446bff0bce9c01fea6d8f222f1ec088c500279a2be348d9a713c8fd

Een huidige receipt kiezen veronderstelt een afzonderlijke beoordeling. De toelatingsfunctie werkt de lijst niet zelf bij.

Canonieke bytes en tekstcodepunten bekijken
{"id":"doc-a","revision":"r1","source":"manual","tenant":"a","text":"color=red\nlabel=café","vector":[2,0]}
U+0063 U+006F U+006C U+006F U+0072 U+003D U+0072 U+0065 U+0064 U+000A U+006C U+0061 U+0062 U+0065 U+006C U+003D U+0063 U+0061 U+0066 U+00E9
Record geweigerd

De volledige recordhash verschilt van de gekozen receipt.

changed
Werkelijk toegelaten en geweigerde records
{
  "accepted": [],
  "rejected": [
    {
      "id": "doc-a",
      "reason": "changed"
    }
  ]
}

De zoekcontext blijft tenant a, vraag [1, 0] en k=1.

Zoeken ná toelating, binnen tenant a

Gescoorde ids

[]

Resultaten op volgorde

Geen hits

Een pinmatch bewijst gelijkheid aan de gekozen referentie. Een onjuist kaartfeit kan ook apart zijn vastgelegd. Hashes bewijzen hier geen waarheid, handtekening, veilige betekenis of uitvoeringsrecht.

Alle kaarten, tenants, receipts, meetrijen en claims zijn eigen oefengegevens. Deze vergelijking doet geen echte zoek- of netwerkopdracht.

4. Een match bewijst geen waarheid

Een veelgemaakte fout is gelijkheid verwarren met juistheid. Wie een verkeerde kaart afzonderlijk goedkeurt, krijgt vervolgens terecht een hashmatch met die verkeerde kaart. Dat maakt het kaartfeit nog niet waar.

current = [dict(id="kaart",
    fingerprint=record_fingerprint(
        changed))]
reply = admit_pinned([changed], current)
print(reply["accepted"][0]["text"])
# color=red

Deze laatste invoer veronderstelt een andere, afzonderlijke beoordeling; de functie heeft haar oude receipt niet bijgewerkt. Onze demonstratie gebruikt geen handtekeningverificatie of echte documentaanbieder. Zij bewijst alleen de afgesproken inhoudsbinding binnen deze eigen records.

Samenvatting

  • Neem tekst, identiteit, relevante metadata en vector mee in de vastgelegde inhoud.
  • Haal de verwachte hash uit een afzonderlijk vertrouwde ontvangstlijst.
  • Behoud gewone records, signaleer veranderingen en verwissel referentiegelijkheid niet met waarheid.

In de volgende les meet je de gevolgen voor antwoorden en controleer je de koppeling tussen claim en bewijs.

DE VOLGENDE STAPMeet de aanval en houd bewijs bij de claim
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 recordvelden aan een vingerafdruk

Maak record_fingerprint(record). De gegeven helper _canonical valideert het record en levert de beschreven UTF-8-JSON-bytes. Geef daarvan de volledige SHA-256 als hextekst terug; verander of normaliseer de inhoud niet.

Een hint

Gebruik de gegeven canonieke bytes en geef de SHA256-hexweergave terug.

02
Vergelijk corpusinvoer met apart bewaarde receipts

Maak admit_pinned(records, receipts). Gebruik de gegeven validators en bereken de vingerafdruk binnen deze functie. Geef passende records terug in accepted; zet de overige ids in rejected met reden unregistered of changed. Behoud de recordvolgorde en verander de oorspronkelijke invoer niet.

Een hint

Zoek de receipt buiten het aangeboden record en vergelijk daarna de volledige hash; behoud ook gewone goedgekeurde records.

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 record_fingerprint(record):
    return hashlib.sha256(_canonical(record)).hexdigest()

def admit_pinned(records, receipts):
    data, expected = (_records(records), _receipts(receipts))
    accepted, rejected = ([], [])
    for row in data:
        if row['id'] not in expected:
            reason = 'unregistered'
        elif hashlib.sha256(_canonical(row)).hexdigest() != expected[row['id']]:
            reason = 'changed'
        else:
            accepted.append(row)
            continue
        rejected.append({'id': row['id'], 'reason': reason})
    return {'accepted': accepted, 'rejected': rejected}

# 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('text')
    try:
        value.encode('utf-8')
    except UnicodeEncodeError:
        raise ValueError('utf8') from None
    return value

def _vector(value):
    if type(value) not in (list, tuple) or len(value) != 2:
        raise ValueError('vector')
    if any((type(item) is not int or not -8 <= item <= 8 for item in value)):
        raise ValueError('coordinate')
    return list(value)

def _document(value):
    _record(value, ('id', 'tenant', 'source', 'revision', 'text', 'vector'))
    return {'id': _id(value['id']), 'tenant': _id(value['tenant']), 'source': _id(value['source']), 'revision': _id(value['revision']), 'text': _text(value['text']), 'vector': _vector(value['vector'])}

def _records(value):
    if type(value) not in (list, tuple) or len(value) > 12:
        raise ValueError('records')
    result = [_document(row) for row in value]
    if len({row['id'] for row in result}) != len(result):
        raise ValueError('duplicate record')
    return result

def _canonical(value):
    return json.dumps(_document(value), sort_keys=True, ensure_ascii=False, separators=(',', ':')).encode('utf-8')

def _receipts(value):
    if type(value) not in (list, tuple) or len(value) > 12:
        raise ValueError('receipts')
    result = {}
    for row in value:
        _record(row, ('id', 'fingerprint'))
        key, digest = (_id(row['id']), row['fingerprint'])
        if type(digest) is not str or _DIGEST.fullmatch(digest) is None:
            raise ValueError('fingerprint')
        if key in result:
            raise ValueError('duplicate receipt')
        result[key] = digest
    return result

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