Changeset b1d5f3f in Klonkt for src/routes/activitypub.js


Ignore:
Timestamp:
10/24/2026 11:48:41 AM (24 hours ago)
Author:
Bart <bart@…>
Branches:
main
Children:
5d0d3f35
Parents:
61d52ed
Message:

Identiteitsbewijzen (FEP-c390): een sleutel van elders zegt dat dit account van hem is

Een Ed25519-sleutel die niet op deze server staat -- in Shaer op papier, alleen
ingetypt als hij nodig is -- ondertekent een VerifiableIdentityStatement:
"did:key:... is ook deze actor". Klonkt ondertekent niets. Hij controleert de
verklaring en zet hem onder attachment op de actor, waar iedereen hem zelf
kan nalopen.

Wat dat bewijst: dat dezelfde entiteit achter sleutel en account zit. Verhuist
het account, dan draagt het nieuwe dezelfde did:key, en dat bewijst de
continuiteit ook als de oude server weg of vijandig is -- een Move kan dat niet,
want die moet door de OUDE server getekend worden.

eddsa-jcs-2022, omdat Mitra en streams dat controleren. IdentityProof.js is
puur: JCS (RFC 8785, in JavaScript precies een gesorteerde JSON.stringify),
base58btc, did:key en de proof zelf. De controle volgt c390 regel voor regel,
met een eigen reden per weigering; verificationMethod moet PRECIES de subject
zijn, zonder het #-fragment dat de W3C-voorbeelden gebruiken.

Getoetst tegen de W3C-testvectoren (vc-di-eddsa B.3): dezelfde did:key, dezelfde
canonieke vorm, dezelfde hashes en byte voor byte dezelfde handtekening. Een
implementatie die het alleen met zichzelf eens is, bewijst niets.

POST /ap/users/:slug/identityProofs (alleen de eigenaar) controleert en
publiceert, één per did:key, hoogstens drie; DELETE ?subject= haalt hem weg.
Beide sturen een Update van de actor naar de volgers. De actor noemt het
eindpunt als endpoints.shaer:identityProofs. Elke verklaring draagt haar eigen
@context, zodat de actor er geen bij hoeft -- did/v1 en data-integrity/v1 hebben
beschermde termen die op actorniveau met onze inline context konden botsen.

shaer-jpxj
Co-Authored-By: Claude Opus 5.5 <claude@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    r61d52ed rb1d5f3f  
    2121import * as Guardianship from '../services/guardianship/index.js';
    2222import * as ConversationKey from '../services/conversation-key.js';
     23import * as IdentityProof from '../services/IdentityProof.js';
    2324import * as Migration from '../services/MigrationService.js';
    2425import { getPrimarySite } from '../middleware/site.js';
    … …  
    10291030  },
    10301031});
     1032// ── Identiteitsbewijzen (FEP-c390, shaer-jpxj) ────────────────────
     1033//
     1034// De eigenaar biedt een verklaring aan die met een sleutel van ELDERS is
     1035// ondertekend: "did:key:... is ook deze actor". Deze server ondertekent niets;
     1036// hij controleert (IdentityProof.checkStatement) en publiceert onder
     1037// `attachment` op de actor, waar iedereen hem zelf kan nalopen.
     1038//
     1039// Eén per did:key: een nieuwe verklaring met dezelfde sleutel vervangt de
     1040// vorige, want een verhuizing of een vernieuwde datum is dezelfde bewering.
     1041// Een handvol is genoeg; meer is geen identiteit meer maar een verzameling.
     1042const MAX_IDENTITY_PROOFS = 3;
     1043function readProofs(site) {
     1044  try { const a = JSON.parse(site.identity_proofs || '[]'); return Array.isArray(a) ? a : []; } catch { return []; }
     1045}
     1046function writeProofs(site, list) {
     1047  db.prepare('UPDATE sites SET identity_proofs = ? WHERE id = ?').run(list.length ? JSON.stringify(list) : null, site.id);
     1048  // De volgers horen dat de actor veranderde; anders blijft hun kopie de oude.
     1049  const vers = db.prepare('SELECT * FROM sites WHERE id = ?').get(site.id);
     1050  if (vers) AP.deliverActorUpdate(vers).catch((e) => console.warn('[c390] actor-update:', e && e.message));
     1051}
     1052router.post('/ap/users/:slug/identityProofs', express.json({ limit: '16kb', type: ['application/json', 'application/activity+json', 'application/ld+json'] }), (req, res) => {
     1053  const auth = OAuth.verifyBearer(req.headers.authorization);
     1054  if (!auth || auth.site.slug !== req.params.slug) return res.status(403).end();
     1055  const base = baseUrl(req);
     1056  const me = AP.actorId(base, auth.site.slug);
     1057  const stmt = req.body;
     1058  const uitslag = IdentityProof.checkStatement(stmt, me);
     1059  if (!uitslag.ok) return res.status(422).json({ error: uitslag.error });
     1060  const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(auth.site.id);
     1061  const rest = readProofs(site).filter((b) => b && b.subject !== uitslag.subject);
     1062  if (rest.length >= MAX_IDENTITY_PROOFS) return res.status(409).json({ error: 'too_many_identity_proofs', max: MAX_IDENTITY_PROOFS });
     1063  writeProofs(site, [...rest, stmt]);
     1064  res.status(201).json({ subject: uitslag.subject, alsoKnownAs: me });
     1065});
     1066router.delete('/ap/users/:slug/identityProofs', (req, res) => {
     1067  const auth = OAuth.verifyBearer(req.headers.authorization);
     1068  if (!auth || auth.site.slug !== req.params.slug) return res.status(403).end();
     1069  const subject = String(req.query.subject || '');
     1070  if (!IdentityProof.publicKeyFromDidKey(subject)) return res.status(400).json({ error: 'subject must be an Ed25519 did:key' });
     1071  const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(auth.site.id);
     1072  const voor = readProofs(site);
     1073  const na = voor.filter((b) => b && b.subject !== subject);
     1074  if (na.length === voor.length) return res.status(404).json({ error: 'no_such_identity_proof' });
     1075  writeProofs(site, na);
     1076  res.status(204).end();
     1077});
     1078
    10311079router.post('/ap/users/:slug/uploadMedia', (req, res) => {
    10321080  const auth = OAuth.verifyBearer(req.headers.authorization);
Note: See TracChangeset for help on using the changeset viewer.