Spécification

Attestation de preuves de conformité, v1.

https://attestkeep.com/attestation/compliance-evidence/v1 est le predicateType de chaque attestation qu'Attestkeep signe sur un rapport de conformité. Si un document vous a mené à cette URL, cette page en est la définition : l'affirmation, chaque champ, l'enveloppe, l'encodage signé et un vrai exemple à vérifier.

L'affirmation

En une phrase : ce rapport de conformité a été produit par cette installation et n'a pas été modifié depuis. C'est toute l'affirmation, et tout ce qui suit en découle.

L'affirmation est transportée dans le format que le monde de la supply chain utilise déjà pour cela (un statement in-toto dans une enveloppe DSSE, signée en Ed25519) plutôt que dans un schéma de signature de notre cru. Ce choix est délibéré : un format que nous seuls pourrions vérifier ferait de nous un goulot d'étranglement, et la valeur du document tient justement à ce qu'un auditeur puisse le vérifier sans nous. La clé de signature est générée dans l'installation et ne la quitte jamais ; on ne demande à personne de faire confiance à autre chose que sa clé publique.

Le statement et son subject

Le payload de l'enveloppe est un statement in-toto, version 1. Ses champs extérieurs :

ChampTypeSens
_typestringToujours https://in-toto.io/Statement/v1.
subjectarrayExactement un resource descriptor désignant le rapport concerné : name est l'identifiant du rapport et digest.sha256 le SHA-256 des octets exacts générés, en 64 caractères hexadécimaux minuscules sans préfixe sha256:.
predicateTypestringToujours https://attestkeep.com/attestation/compliance-evidence/v1, c'est-à-dire l'adresse de cette page.
predicateobjectL'affirmation elle-même ; chaque champ est défini dans la section suivante.

Le statement porte sur un document, donc le document est le subject. Le rapport lui-même n'est pas transporté dans l'attestation, et c'est voulu : le rapport est volumineux, et un lecteur peut l'avoir sous forme de PDF ou de CSV. Ce qui doit être prouvable, c'est que la copie qu'il a en main est celle qui a été signée ; un digest répond à cette question pour n'importe quelle copie des octets, alors qu'une copie intégrée n'y répondrait que pour elle-même.

Le framework et la période d'audit figurent dans la signature, à côté du subject. La même période évaluée selon deux frameworks donne deux documents différents, et un lecteur ne doit pas pouvoir faire passer l'un pour l'autre. Aucun élément du statement (subject, digest, période, framework, horodatages) ne peut être modifié après signature sans que la vérification échoue.

Champs du predicate

Les champs facultatifs sont omis du JSON quand ils sont vides, jamais remplis d'une valeur de substitution. Un horodatage que l'installation n'a pas est écrit comme une chaîne vide, jamais comme une date inventée.

ChampTypeSens
report_idstringL'identité du document, le même identifiant que celui imprimé sur le rapport, pour rapprocher d'un coup d'œil l'attestation et le rapport que vous avez en main. Il reprend le name du subject.
frameworkstringLe mapping de conformité selon lequel le rapport a été assemblé, par exemple soc2. Dans la signature, pour que le rapport d'un framework ne puisse pas être présenté comme celui d'un autre.
periodobjectLa fenêtre d'audit couverte par le rapport.
period.start, period.endstringHorodatages RFC 3339, normalisés en UTC. Transmis comme chaînes, parce que ce sont les octets exacts qui sont signés.
installationobjectIdentifie le signataire sans décrire le cluster.
installation.fingerprintstringL'identifiant opaque du cluster auquel la licence est liée : un hash sans topologie, sans noms, sans rien qu'un client refuserait de voir dans un document qui sort de chez lui.
installation.license_idstring, facultatifPermet à l'émetteur de confirmer que l'installation était sous licence à ce moment-là : c'est le lien entre « ce fichier est intact » et « il vient d'une vraie installation ».
installation.binding_idstring, facultatifLa liaison de licence sous laquelle cette installation s'est activée ; elle sert à la même confirmation.
generated_atstringLe moment où l'installation a produit le rapport, d'après sa propre horloge, en RFC 3339 UTC. C'est une assertion du signataire, pas une preuve : rien ne lie le document à une horloge de confiance, et un lecteur qui en a besoin doit l'obtenir ailleurs.
generated_bystring, facultatifLe compte qui a demandé le rapport, sous la forme user:<account id>. Depuis la 1.2.0, ce n'est jamais un nom ni une adresse e-mail ; sur demande, l'opérateur retrouve l'identifiant dans le journal d'audit de sa console.
generatorobjectLe logiciel qui a produit le rapport.
generator.name, generator.versionstringLa release dont les mappings de conformité étaient en vigueur. Les mappings évoluent à mesure que les frameworks sont lus plus attentivement ; une preuve qui ne dit pas sur quelle lecture elle repose ne peut pas être réexaminée plus tard.

L'enveloppe DSSE

Le statement est placé dans une enveloppe DSSE de cette forme :

{
  "payload":     "<standard base64 of the statement JSON>",
  "payloadType": "application/vnd.in-toto+json",
  "signatures": [
    { "keyid": "ak-…", "sig": "<base64 of an Ed25519 signature>" }
  ]
}
  • payload est le base64 standard, avec padding, des octets exacts du statement sérialisé. Le statement est sérialisé une seule fois : les octets signés sont les octets transmis.
  • payloadType fait partie de ce qui est signé (voir l'encodage ci-dessous) ; une signature sur un statement in-toto ne peut donc jamais être présentée comme une signature sur les mêmes octets lus comme autre chose.
  • sig est une signature Ed25519 (RFC 8032) sur PAE(payloadType, payload), c'est-à-dire sur le pre-authentication encoding, et non sur la chaîne base64.
  • keyid est dérivé de la clé publique : "ak-" suivi des 16 premiers caractères hexadécimaux du SHA-256 de la clé de 32 octets. Puisqu'il est dérivé, il ne peut pas contredire la clé qu'il nomme ; et ce n'est qu'un indice : un vérificateur doit s'appuyer sur une clé en laquelle il a déjà confiance, jamais sur une clé que l'enveloppe lui désigne.
  • Une enveloppe peut porter plusieurs signatures. Un vérificateur teste chaque entrée avec sa clé de confiance et accepte l'enveloppe si l'une d'elles se vérifie.

L'encodage PAE

Ce qui est signé, c'est le pre-authentication encoding du payload avec son type :

PAE(type, body) = "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body

où SP est une espace unique et LEN la longueur en octets en ASCII décimal. Les longueurs comptent des octets, jamais des caractères. Par exemple, PAE("http://example.com/HelloWorld", "hello world") est la suite d'octets DSSEv1 29 http://example.com/HelloWorld 11 hello world.

Toute la sécurité de DSSE tient dans cette idée. Signer le payload seul permettrait de présenter une signature faite sur un statement in-toto comme une signature sur autre chose qui aurait les mêmes octets sous un autre payloadType. PAE lie les deux, avec préfixe de longueur, pour qu'aucun ne puisse passer pour une partie de l'autre.

La chaîne de certificats

La signature de l'enveloppe prouve que le rapport est inchangé et nomme une clé. Elle ne peut pas, à elle seule, prouver à qui appartient cette clé : un vérificateur qui prend la clé publique des mains de celui qui apporte le document ne prouve que la cohérence interne. Le certificat de licence comble cette lacune. C'est un document qu'Attestkeep signe pour chaque installation sous licence, et il porte la clé de signature de preuves de l'installation dans la signature : un lecteur qui fait confiance à une seule clé Attestkeep peut donc vérifier un rapport de n'importe quelle installation :

Clé racine Attestkeep
  └─ signe → certificat de licence (JWT compact)
                └─ cnf.jwk désigne → clé de preuves de l'installation
                                        └─ signe → enveloppe DSSE
                                                      └─ le digest du subject épingle → rapport

La clé racine. Cette valeur, obtenue sur cette page via TLS, est le seul point de confiance de la chaîne :

kid:     ak1
ed25519: e287aabe4712197cfa5b6fdf9ea8f2d7f0333ce16ca69aac7e4a3f1e84c04179

Le certificat est un compact JWT (RFC 7515) : base64url(header) "." base64url(payload) "." base64url(signature), en base64url sans padding (RFC 4648, section 5, sans =). Le header est {"alg":"EdDSA","typ":"JWT","kid":"ak1"} ; un vérificateur doit refuser tout alg autre que EdDSA, ce qui exclut par construction les attaques JWT classiques par rétrogradation (alg=none, confusion d'algorithme). La signature est en Ed25519 sur les octets ASCII de header_b64 "." payload_b64.

Champs du payload lus par un vérificateur de chaîne (tout champ inconnu doit être ignoré : l'émetteur peut ajouter des claims) :

ChampTypeSens
substringL'id de la licence pour laquelle le certificat a été émis.
fprstringL'empreinte du cluster auquel la licence est liée : c'est la valeur que doit avoir installation.fingerprint dans le predicate.
planstringLe code du plan, informatif pour un vérificateur de chaîne.
iat, expnumberÉmission et expiration, en secondes unix. Un exp à 0 signifie que le certificat n'expire pas.
jtistringLe numéro de série du certificat.
cnf.jwkstringLe claim de confirmation de la RFC 7800 : la clé publique Ed25519 de preuves de l'installation, en exactement 64 caractères hexadécimaux. Pas un objet JWK imbriqué, mais les 32 octets bruts encodés en hexadécimal. Un certificat sans ce champ ne garantit aucune clé et ne peut ancrer aucune chaîne.

Pour vérifier un rapport par la chaîne :

  1. Découpez le certificat sur . en exactement trois parties ; décodez le header et refusez-le si alg n'est pas EdDSA.
  2. Vérifiez la signature Ed25519 sur les octets ASCII de header_b64 "." payload_b64 avec la clé racine ci-dessus.
  3. Décodez le payload et lisez cnf.jwk, la clé de preuves de l'installation.
  4. Vérifiez l'enveloppe DSSE avec cette clé, exactement comme le décrivent les sections ci-dessus.
  5. Contre-vérification : le predicate.installation.fingerprint du statement doit être égal au fpr du certificat. Sans cette étape, on pourrait présenter un certificat authentique à côté d'une enveloppe authentique venant d'une autre installation. Si installation.license_id est présent et diffère de sub, avertissez le lecteur.

L'expiration est une information, pas un motif de rejet. Les certificats changent tous les trente jours, et un rapport du trimestre dernier est garanti par le certificat en vigueur à l'époque : refuser les certificats expirés rendrait invérifiable tout ancien rapport. Un vérificateur doit indiquer la date d'un certificat expiré et laisser le jugement au lecteur.

Un exemple complet

Cette enveloppe a été produite par le signataire de référence. Elle est signée avec une clé de test délibérément publique (la seed Ed25519 est faite des octets 0x01 à 0x20) : elle illustre donc le format, pas la provenance d'un vrai rapport. Les vraies attestations sont signées avec une clé générée dans chaque installation, qui ne la quitte jamais.

{
  "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicnB0LTlmMmM3YTQxZDBiODNlNTYiLCJkaWdlc3QiOnsic2hhMjU2IjoiNzQ3YjgzZWNkMTY2OTEyNjk2NmVlNjAyZTM3MjI0NTE2Y2M5YmMyZGQ5NWFhZWJlNDIwZWQxY2IzNGVkZWMwYyJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2F0dGVzdGtlZXAuY29tL2F0dGVzdGF0aW9uL2NvbXBsaWFuY2UtZXZpZGVuY2UvdjEiLCJwcmVkaWNhdGUiOnsicmVwb3J0X2lkIjoicnB0LTlmMmM3YTQxZDBiODNlNTYiLCJmcmFtZXdvcmsiOiJzb2MyIiwicGVyaW9kIjp7InN0YXJ0IjoiMjAyNi0wNy0wMVQwMDowMDowMFoiLCJlbmQiOiIyMDI2LTA3LTMxVDIzOjU5OjU5WiJ9LCJpbnN0YWxsYXRpb24iOnsiZmluZ2VycHJpbnQiOiIzZjdiMWE5ZTVjMmQ4ZjA0YTZiM2UxYzdkOTA1MmU4YTFiNGY2YzNkN2U5YTBiMmM0ZDZlOGYwYTFiM2M1ZDdlIiwibGljZW5zZV9pZCI6ImxpYy03YzMxZjBkMiIsImJpbmRpbmdfaWQiOiJiaW5kLWE5NGUyYzE3In0sImdlbmVyYXRlZF9hdCI6IjIwMjYtMDgtMDFUMDY6MDA6MDBaIiwiZ2VuZXJhdGVkX2J5IjoidXNlcjp1c3ItN2QyYzFhOWU0YjNmNWM2ZDhlMGYxYTJiIiwiZ2VuZXJhdG9yIjp7Im5hbWUiOiJhdHRlc3RrZWVwIiwidmVyc2lvbiI6IjAuMS4wIn19fQ==",
  "payloadType": "application/vnd.in-toto+json",
  "signatures": [
    {
      "keyid": "ak-65b60673d6ed884b",
      "sig": "DUTZfcrSxs8IuyoPBcBOVa10sgg5Yy2/QQkCF8t0RFa2U+sjp5NGfVwHprbZ30dNtCogfZeexpLR8EYKsEWKCQ=="
    }
  ]
}

Le payload se décode en ce statement :

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "rpt-9f2c7a41d0b83e56",
      "digest": {
        "sha256": "747b83ecd1669126966ee602e37224516cc9bc2dd95aaebe420ed1cb34edec0c"
      }
    }
  ],
  "predicateType": "https://attestkeep.com/attestation/compliance-evidence/v1",
  "predicate": {
    "report_id": "rpt-9f2c7a41d0b83e56",
    "framework": "soc2",
    "period": {
      "start": "2026-07-01T00:00:00Z",
      "end": "2026-07-31T23:59:59Z"
    },
    "installation": {
      "fingerprint": "3f7b1a9e5c2d8f04a6b3e1c7d9052e8a1b4f6c3d7e9a0b2c4d6e8f0a1b3c5d7e",
      "license_id": "lic-7c31f0d2",
      "binding_id": "bind-a94e2c17"
    },
    "generated_at": "2026-08-01T06:00:00Z",
    "generated_by": "user:usr-7d2c1a9e4b3f5c6d8e0f1a2b",
    "generator": {
      "name": "attestkeep",
      "version": "0.1.0"
    }
  }
}

La clé publique qui la vérifie, sous la forme qu'attend le vérificateur :

79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664

Vérifiez-la avec le script de la documentation, qui n'utilise que la bibliothèque standard, ou avec n'importe quelle implémentation DSSE : elle passe, et changer un seul caractère du payload la fait échouer. Le vérificateur dans le navigateur prend le paquet livré plutôt qu'une enveloppe et une clé isolées : il est conçu pour un auditeur qui détient une vraie archive, pas pour lire une spécification.

Vérifier une attestation

Trois façons, par ordre croissant d'indépendance :

  • la page de vérification de ce site, qui tourne entièrement dans votre navigateur et n'envoie rien nulle part ;
  • tout outil qui comprend DSSE et in-toto, car le format ne contient rien de propriétaire ;
  • une douzaine de lignes de votre cru, dans n'importe quel langage : vérifier le certificat de licence avec la clé racine, prendre la clé de l'installation dans son cnf.jwk, décoder le payload en base64, construire le PAE, vérifier la signature Ed25519 avec cette clé, et seulement ensuite lire le statement.

Quelle que soit la voie choisie, le point de confiance est le même : la clé racine d'Attestkeep, qui garantit la clé de l'installation par le certificat de licence, comme décrit plus haut. Jamais une clé prise dans le document lui-même, dont le keyid est une étiquette et non une preuve d'identité, et jamais une clé acceptée sur la foi de celui qui a livré le rapport. Une clé remise en main propre vérifie quand même une enveloppe, mais ne prouve que la cohérence interne, pas l'origine. Les limites sont les mêmes elles aussi : une chaîne valide prouve que le document est inchangé et quelle installation l'a signé. Ce n'est pas une signature électronique au sens juridique, et elle ne prouve pas quand la signature a eu lieu : generated_at et la période relèvent de la déclaration de l'installation elle-même. Ces deux limites sont détaillées sur la page de vérification.

Mis à jour : 29 août 2026