Ir al contenido

Proyecto independiente de I+D · Colonia

Cómo funciona la verificación

Esta página usa el pasaporte de demostración en vivo, no una maqueta. La primera parte son cinco pasos en lenguaje llano: qué hace cada uno y qué no establece. La segunda parte es la misma comprobación en forma de comandos, para quien prefiera ejecutarla a leer sobre ella.

Los cinco pasos

  1. Descargue la credencial

    Cada página de pasaporte ofrece su credencial como un token firmado, una sola línea de texto. Descárguelo. Todo lo que sigue opera sobre ese archivo, nunca sobre la página que lo muestra.

    Esto establece que usted tiene el mismo artefacto que publicó el emisor. No establece que nada de lo que contiene sea cierto.

  2. Verifique la firma del emisor

    El token lleva una firma sobre su contenido. Un verificador rehace la comprobación con la clave pública del emisor; cambie un carácter del contenido y la comprobación falla.

    Esto establece que el contenido no ha cambiado desde que el titular de esa clave lo firmó. No establece quién tiene la clave: ese es el paso siguiente.

  3. Compruebe la identidad del emisor

    El emisor se nombra como did:web:anydpp.eu, que resuelve a un documento público de identificación en anydpp.eu. La clave usada en el paso dos debe figurar allí, bajo el propio nombre y dominio del emisor.

    Esto establece que la clave pertenece a quien controla ese dominio. No establece que el dueño del dominio sea, en el mundo real, quien dice ser.

  4. Compruebe la lista de estado

    La credencial nombra una lista de estado, publicada y firmada por el mismo emisor, y su propia posición en ella. Lea el bit en esa posición: 0 significa no revocada, 1 significa revocada.

    Esto establece si el emisor ha retirado la credencial desde que la emitió. No establece por qué, y no vuelve a comprobar nada de lo que la credencial dice.

  5. Inspeccione las referencias de evidencia

    La credencial lista los documentos de origen a partir de los que se construyó, cada uno por identificador y por un resumen del registro, y nombra el documento de origen detrás de cada atributo y su nivel de confianza. No contiene los documentos en sí.

    Esto establece qué documentos afirma haber leído el emisor, y permite a quien tenga uno probar que es el mismo. No establece que esos documentos sean auténticos ni que su contenido sea cierto.

Abrir el pasaporte de demostración

Cómo funciona la verificación

Del documento de origen a una firma que un tercero puede comprobar. El método, al nivel en que lo publicamos.

Esta película: producción asistida por IA, contenido verificado por personas. Locución sintética en inglés, subtítulos en cinco idiomas. Accesibilidad y transparencia de IA

Qué ha cambiado

Las primeras credenciales de demostración, emitidas en julio de 2026, llevaban una prueba etiquetada Ed25519Signature2020 cuyos bytes firmados no eran la forma canónica de esa suite. Una firma real, pero que ningún verificador basado en estándares aceptaba, de modo que la única comprobación disponible era la de la propia AnyLAI. Ese formato de prueba está obsoleto: los cuatro pasaportes que lo llevan siguen legibles como evidencia histórica, marcados como tal, sin botón de verificación.

Las credenciales de demostración actuales se emiten como un token firmado en la forma que el estándar W3C (World Wide Web Consortium) de Verifiable Credentials define para las pruebas envolventes, con la clave del emisor publicada en did:web:anydpp.eu y una lista de estado firmada para la revocación. Los pasos de arriba y los comandos de abajo usan solo ese token, esa clave y esa lista.

Qué no establece la verificación

Una verificación que supera todos los pasos anteriores responde a una pregunta: si esta es la credencial que el emisor publicó y si el emisor sigue respaldándola. No responde a estas:

Tres cosas distintas se leen fácilmente como una sola: la autenticidad criptográfica, que los pasos anteriores establecen; la verdad de las fuentes, que solo los documentos y las personas detrás de ellos pueden establecer; y la suficiencia regulatoria, que corresponde al operador que comercializa las mercancías y a la autoridad competente.

  • La verdad de cada afirmación de un documento de origen. Un resumen prueba que un documento es el que se leyó, no que lo que dice sea exacto.
  • El cumplimiento legal. La credencial registra evidencia y no emite veredicto sobre si las mercancías pueden comercializarse en ningún mercado.
  • La completitud de la diligencia debida. Lista los documentos que se leyeron, no los documentos que las propias obligaciones de un operador aún puedan exigir.
  • Una certificación de ningún tipo. Nadie ha auditado al productor, la parcela ni el envío sobre la base de esta credencial.
  • La aprobación de un regulador. Ninguna autoridad competente ha revisado ni respaldado la credencial, el emisor ni el método.

Para ingenieros: la misma comprobación, en comandos

Todo lo que sigue se ejecuta sin repositorio y sin cuenta: tres descargas, un verificador de unas treinta líneas en Node o Python, una prueba de manipulación y los valores publicados para comparar. Los comandos no se traducen; un comando traducido deja de funcionar.

Copy and paste

  1. 1. Download the three artefacts

    # 1. the credential, as a compact JWS (application/vc+jwt)
    curl -s https://anylai.eu/api/corridor/passports/DEMO26-000001/credential.jwt -o DEMO26-000001.jwt
    
    # 2. the issuer's DID document (the public key lives HERE, not in the credential)
    curl -s https://anydpp.eu/.well-known/did.json -o did.json
    
    # 3. the status list credential (an EnvelopedVerifiableCredential around its own JWS)
    curl -s https://anylai.eu/api/corridor/status-list/1 -o status-list-1.json
    
    # what you downloaded
    sha256sum DEMO26-000001.jwt

    The credential is one line of text: header, payload and signature, base64url-encoded and dot-separated. The DID document is fetched from the issuer's own domain, never taken from inside the credential: a forged credential must not be allowed to vouch for itself.

  2. 2. Verify it in Node (one file, one dependency)

    // verify.mjs  --  npm i jose  --  node verify.mjs
    import { compactVerify, importJWK } from "jose";
    import { gunzipSync } from "node:zlib";
    
    const b64 = (s) => Buffer.from(s, "base64url");
    const jws = (await (await fetch("https://anylai.eu/api/corridor/passports/DEMO26-000001/credential.jwt")).text()).trim();
    const header = JSON.parse(b64(jws.split(".")[0]));
    if (header.typ !== "vc+jwt" || header.cty !== "vc") throw new Error("not a vc+jwt");
    
    const did = header.kid.split("#")[0];
    const didDoc = await (await fetch(`https://${did.replace("did:web:", "")}/.well-known/did.json`)).json();
    const method = didDoc.verificationMethod.find((m) => m.id === header.kid);
    if (!method || !didDoc.assertionMethod.includes(header.kid)) throw new Error("kid not under assertionMethod");
    
    const key = await importJWK(method.publicKeyJwk, header.alg);
    const { payload } = await compactVerify(jws, key); // throws JWSSignatureVerificationFailed on a bad signature
    const vc = JSON.parse(Buffer.from(payload).toString("utf8"));
    if (vc.issuer.id !== method.controller) throw new Error("key controller is not the issuer");
    console.log("signature: valid | issuer:", vc.issuer.id, "| kid:", header.kid);
    
    const wrapper = await (await fetch(vc.credentialStatus.statusListCredential)).json();
    const listJws = wrapper.id.replace("data:application/vc+jwt,", "");
    const listHeader = JSON.parse(b64(listJws.split(".")[0]));
    const listMethod = didDoc.verificationMethod.find((m) => m.id === listHeader.kid);
    const listKey = await importJWK(listMethod.publicKeyJwk, listHeader.alg);
    const list = JSON.parse(Buffer.from((await compactVerify(listJws, listKey)).payload).toString("utf8"));
    const encoded = list.credentialSubject.encodedList;
    if (!encoded.startsWith("u")) throw new Error("encodedList is not multibase base64url");
    const bits = gunzipSync(b64(encoded.slice(1)));
    const index = Number(vc.credentialStatus.statusListIndex);
    const revoked = (bits[index >> 3] >> (7 - (index & 7))) & 1;
    console.log("status:", revoked ? "revoked" : "active", "| status list signature: valid");

    Steps two to four of the page above, as code. It throws on a bad signature, on a key that is not listed under the issuer's assertionMethod, on a controller that is not the issuer, and on a status list that is not signed by the same key.

  3. 3. Or in Python

    # verify.py  --  pip install pyjwt cryptography requests  --  python verify.py
    import base64, gzip, json, jwt, requests
    
    jws = requests.get("https://anylai.eu/api/corridor/passports/DEMO26-000001/credential.jwt").text.strip()
    header = jwt.get_unverified_header(jws)
    assert header["typ"] == "vc+jwt" and header["cty"] == "vc"
    
    did = header["kid"].split("#")[0]
    did_doc = requests.get(f"https://{did.removeprefix('did:web:')}/.well-known/did.json").json()
    method = next(m for m in did_doc["verificationMethod"] if m["id"] == header["kid"])
    assert header["kid"] in did_doc["assertionMethod"]
    
    key = jwt.algorithms.OKPAlgorithm.from_jwk(json.dumps(method["publicKeyJwk"]))
    vc = jwt.decode(jws, key=key, algorithms=["EdDSA"])  # raises InvalidSignatureError on a bad signature
    assert vc["issuer"]["id"] == method["controller"], "key controller is not the issuer"
    print("signature: valid | issuer:", vc["issuer"]["id"], "| kid:", header["kid"])
    
    wrapper = requests.get(vc["credentialStatus"]["statusListCredential"]).json()
    list_jws = wrapper["id"].removeprefix("data:application/vc+jwt,")
    list_header = jwt.get_unverified_header(list_jws)
    list_method = next(m for m in did_doc["verificationMethod"] if m["id"] == list_header["kid"])
    list_key = jwt.algorithms.OKPAlgorithm.from_jwk(json.dumps(list_method["publicKeyJwk"]))
    status_list = jwt.decode(list_jws, key=list_key, algorithms=["EdDSA"])
    encoded = status_list["credentialSubject"]["encodedList"]
    assert encoded.startswith("u"), "encodedList is not multibase base64url"
    body = encoded[1:]
    bits = gzip.decompress(base64.urlsafe_b64decode(body + "=" * (-len(body) % 4)))
    index = int(vc["credentialStatus"]["statusListIndex"])
    print("status:", "revoked" if bits[index // 8] >> (7 - index % 8) & 1 else "active", "| status list signature: valid")

    The same check with PyJWT and cryptography. Both verifiers are the reader's own code calling an open-source library; neither calls anything of AnyLAI's except the three public downloads.

  4. 4. Prove the check has teeth

    // tamper.mjs -- change ONE character of the payload segment and the signature check must fail
    import { compactVerify, importJWK } from "jose";
    const jws = (await (await fetch("https://anylai.eu/api/corridor/passports/DEMO26-000001/credential.jwt")).text()).trim();
    const [h, p, s] = jws.split(".");
    const flipped = p.slice(0, 10) + (p[10] === "A" ? "B" : "A") + p.slice(11);
    const didDoc = await (await fetch("https://anydpp.eu/.well-known/did.json")).json();
    const key = await importJWK(didDoc.verificationMethod[0].publicKeyJwk, "EdDSA");
    await compactVerify(`${h}.${flipped}.${s}`, key)
      .then(() => { throw new Error("TAMPERED CREDENTIAL VERIFIED: stop trusting this check"); })
      .catch((e) => console.log("tampered credential rejected:", e.code ?? e.message));

    Feed a credential with one character of its payload changed to the same verifier. If it still passes, the verification is not doing anything and you should stop trusting it. That is the test worth running first.

Published values

Published values for the demonstration credential
Credential (compact JWS)https://anylai.eu/api/corridor/passports/DEMO26-000001/credential.jwt
Credential (JSON-LD envelope)https://anylai.eu/api/corridor/passports/DEMO26-000001/credential.jsonld
DID documenthttps://anydpp.eu/.well-known/did.json
Status listhttps://anylai.eu/api/corridor/status-list/1
kiddid:web:anydpp.eu#key-1
alg / typ / ctyEdDSA / vc+jwt / vc
Expected outcomesignature valid; kid under assertionMethod; controller = issuer; status active
SHA-256 of DEMO26-000001.jwtpublished in the release record

The digest is read from the live passport API when this page is viewed, so it names the artefact actually being served; compare it with the output of sha256sum above.

El radar regulatorio, una vez al mes

Qué cambió realmente en las normas europeas de deforestación y de pasaporte de producto, con fuente para cada punto y nada más en el sobre.

Doble confirmación: usted confirma pulsando un enlace en su bandeja, y puede salir con un clic desde cualquier número.

Hágalo usted mismo

El pasaporte de demostración es público. Su credencial puede comprobarse contra la clave del emisor publicada en did:web:anydpp.eu con los comandos de esta página, sin cuenta.