Aller au contenu

L'arbre source des politiques, fichier par fichier

Écrire et exploiter les politiques explique comment penser un bundle de politiques. Cette page est la référence : chaque fichier que lit le plan de contrôle, chaque champ qu’il accepte, chaque valeur qu’admet une énumération, et ce que fait obsign-control compile quand le champ est mauvais.

La règle qui gouverne toute cette organisation : tout ce qui décide d’une issue est signé, et tout ce qui est signé est revu sous forme de diff. C’est pourquoi il n’y a ici aucun champ qu’un serveur en fonctionnement puisse changer, et pourquoi une faute de frappe est une erreur de compilation en CI plutôt qu’une surprise sur une passerelle.

  • Obligatoire — la compilation échoue si le champ est absent.
  • Défaut — la valeur utilisée quand le champ est absent. Un champ doté d’une valeur par défaut peut être omis sans risque ; c’est cette valeur que la signature couvrira.
  • Refusé à la compilation signifie que obsign-control compile sort avec un code non nul et affiche le chemin du fichier. Rien n’est écrit, rien n’est publié.

Les champs inconnus sont ignorés, à une exception près. L’analyse JSON est tolérante partout sauf dans deployment/origin-keys.json, qui rejette purement et simplement les champs inconnus. Ainsi "destructve": true dans tools.json compile sans broncher et l’outil n’est pas destructeur. Revoyez les diffs de ces fichiers comme vous revoyez du code, et préférez recopier les exemples ci-dessous plutôt que d’écrire de mémoire.

CheminObligatoireAnalysé commeCompilé dansAbsent signifie
policies/*.cedarouisource Cedarpolicy-bundle.jsonrefusé — aucun jeu de politiques du tout
tools.jsonouitableau d’objets outilpolicy-bundle.jsonrefusé — chaque outil serait refusé
fail-mode.jsonnonobjet de mode de défaillancepolicy-bundle.json{"default":"closed"}
identity/provider.jsonavec identity/objet fournisseuridentity-bundle.jsonpas de bundle d’identité : la passerelle démarre en mode déclaré et le dit
identity/jwks.jsonavec identity/JWKSidentity-bundle.jsonidem
deployment/origin-keys.jsonavec deployment/tableau d’entrées de clédeployment-bundle.jsonaucune passerelle enrôlée
deployment/attestation.jsonnontableau d’attestationsdeployment-bundle.jsonpas d’enrôlement TPM (v3 inutilisée)

identity/ et deployment/ fonctionnent en tout ou rien : créez le répertoire et son fichier obligatoire le devient aussi : une demi-configuration d’identité ne vérifie rien, et un deployment/ vide mérite un fichier explicite plutôt qu’un ensemble vide silencieux.

Tout autre fichier de l’arbre (un README.md, une configuration de CI, une fixture de test) n’est jamais lu. Seuls les sept chemins ci-dessus atteignent un bundle.

RègleDétail
Quels fichiers*.cedar, fichiers réguliers, directement dans policies/. Les sous-répertoires ne sont pas parcourus — policies/finance/10-x.cedar est silencieusement invisible.
OrdreTriés par nom de fichier, dans l’ordre lexicographique (ordre des octets), puis concaténés. 00-base.cedar avant 10-finance.cedar ; 9-x.cedar après 10-x.cedar — complétez vos préfixes par des zéros.
ProvenanceChaque fichier est préfixé dans le bundle par un commentaire // ─── <name> ───, si bien qu’une règle de l’artefact compilé peut être retracée jusqu’à son fichier.
MinimumAu moins un fichier .cedar. Un jeu de politiques vide refuse tout, ce qui mérite un fichier explicite qui le dise.
@idObligatoire sur chaque règle, et unique sur toute la concaténation. L’identifiant est le motif d’audit.

Il n’y a aucun fichier de schéma à écrire à la main : le modèle d’entités (User, Group, Tool, Resource, Prompt, Server, les six actions et les attributs de contexte) est fixé par la passerelle, documenté dans le modèle que voient vos règles, et généré depuis votre propre tools.json par obsign-control schema. compile vérifie le typage de chaque règle contre ce modèle et refuse de signer une règle qui lit un attribut que la passerelle n’expose pas.

Le niveau supérieur est un tableau JSON, pas un objet. Un outil qui en est absent est refusé avant que Cedar ne s’exécute.

[
{
"name": "delete_production_db",
"server": "mcp://db",
"destructive": true,
"required_scope": "db:admin"
}
]
ChampTypeObligatoireDéfautSignification
namechaîneouiLe nom de l’outil MCP, comparé exactement à celui de tools/call. Également la ressource Cedar : Tool::"delete_production_db". Doit être non vide et unique dans le fichier.
serverchaîneouiQuel serveur sous-jacent possède l’outil. Exposé à Cedar sous la forme resource.server. Libre ; la convention est d’utiliser la même URI que celle avec laquelle la passerelle est démarrée (--server-id mcp://db). Purement descriptif — aucun verdict n’est indexé dessus.
destructiveboolnonfalseVotre jugement : un effet qui ne peut pas être annulé (suppression, virement, envoi externe). Exposé sous la forme resource.destructive. Marquer un outil comme destructeur ne coûte rien et permet à une seule règle de protéger tous les outils dangereux, y compris ceux ajoutés plus tard.
required_scopechaînenonabsentScope de délégation dont l’outil a besoin. Exposé sous la forme resource.required_scope, comme la chaîne vide en son absence — Cedar n’a pas d’optionnel ergonomique, écrivez donc resource.required_scope != "" plutôt qu’un test de nullité. Le déclarer ne l’applique pas : il faut qu’une règle le lise (voir allow_scoped dans Écrire des règles).
policy_argstableaunon[]Les arguments que la politique peut lire. Toute liste non vide fait passer le bundle en obsign-policy/2 — voir l’avertissement sur l’ordre plus bas.

Refusé à la compilation : un name vide ; le même name deux fois (le moteur indexe par nom, un doublon masquerait donc une définition et laquelle l’emporte dépendrait de l’ordre des fichiers) ; du JSON malformé.

policy_args[] — la liste d’autorisation des arguments

Section intitulée « policy_args[] — la liste d’autorisation des arguments »

Cette liste est une frontière de confidentialité : un argument qui n’y est pas déclaré n’atteint jamais le moteur, et le journal d’audit conserve args_hash, jamais les valeurs.

{
"name": "send_message",
"server": "mcp://chat",
"policy_args": [
{ "name": "channel", "kind": "string" },
{ "name": "amount_cents", "kind": "long", "default": 0 },
{ "name": "labels", "kind": "string_set", "default": [] },
{ "name": "target", "kind": "string", "at": "/recipient/id" }
]
}
ChampTypeObligatoireDéfautSignification
namechaîneouiLa clé sous context.args. Non vide, unique au sein de l’outil.
kindénumérationouistring | long | bool | string_set. Voir la table de coercition.
atchaînenon/<name>Pointeur JSON RFC 6901 dans l’objet arguments de l’appel, pour une valeur imbriquée ou nommée autrement. Doit commencer par /. Lorsqu’il est dérivé de name, le nom est échappé (~~0, /~1), si bien qu’un argument littéralement nommé path/glob lit la clé path/glob et non arguments.path.glob.
defaultquelconquenonabsentInjecté quand l’appel omet l’argument. Doit lui-même se coercer vers kind. Un argument sans valeur par défaut est obligatoire : un appel qui l’omet est refusé avant que Cedar ne s’exécute, avec un message explicite.

Un null JSON explicite dans l’appel compte comme absent : les SDK clients MCP sérialisent couramment un champ optionnel omis sous la forme null, et le traiter comme présent refuserait un appel que l’omission complète de la clé aurait autorisé.

kindJSON acceptéType CedarRefusé
stringchaîne, ≤ 4096 octetsStringtout ce qui n’est pas une chaîne ; une chaîne plus longue (les arguments pertinents pour une politique sont des identifiants, pas des charges utiles)
longnombre entier, plage i64Longtout flottant — même 2.0 — et tout entier hors plage. Jamais arrondi : un contrôle de montant qui arrondit est un contrôle de montant troué. Les règles monétaires déclarent des unités mineures (centimes).
booltrue / falseBoolles chaînes "true" / "false", 0, 1
string_settableau de chaînes, ≤ 64 éléments, chacun ≤ 4096 octetsSet of Stringce qui n’est pas un tableau ; un tableau contenant autre chose que des chaînes
LimiteValeur
Arguments déclarés par outil16
Longueur de chaîne (et de chaque élément d’ensemble)4096 octets
Éléments d’un ensemble64

Refusé à la compilation : policy_args sur un bundle qui n’est pas obsign-policy/2 (impossible depuis un arbre source, le plan de contrôle choisit le format automatiquement) ; plus de 16 arguments ; un nom d’argument vide ou en double ; un at qui ne commence pas par / ; un default qui ne se coerce pas vers son kind.

Dès qu’un outil déclare policy_args, la compilation évalue en plus chaque règle une fois par outil contre une requête synthétique construite à partir de vos valeurs par défaut déclarées (ou de la valeur zéro : "", 0, false, []). Un context.args.chanel mal orthographié échoue donc en CI, en nommant la règle, au lieu de refaire surface des mois plus tard comme un événement de mode de défaillance sur une passerelle en production.

Les arbres qui ne déclarent aucun argument continuent de compiler sans ce contrôle : bloquer leur publication sur une entrée synthétique reviendrait à passer outre le choix de mode de défaillance qu’ils ont déjà fait.

Le plan de contrôle émet obsign-policy/2 dès qu’un outil déclare policy_args, et une passerelle non mise à niveau refuse un bundle /2 au démarrage plutôt que d’appliquer en silence moins que ce que le bundle prescrit. Mettez d’abord à niveau toutes les images de passerelle, publiez ensuite le bundle qui déclare des arguments.

Ce qui se passe quand le moteur ne peut pas décider, qu’il s’agisse d’un bundle qu’il ne sait pas lire ou d’une règle qui lève une erreur d’évaluation. Ce n’est pas le chemin du refus : une règle qui refuse est une décision, et le mode de défaillance ne s’y applique jamais.

{
"default": "closed",
"tools": {
"search_docs": "open",
"list_tickets": "open"
}
}
ChampTypeObligatoireDéfautSignification
defaulténumérationoui, si le fichier existeComportement pour tout ce qui n’a pas de surcharge.
toolsobjetnon{}Surcharges par outil : nom d’outil → comportement.

Omettre le fichier entier donne {"default": "closed", "tools": {}}. Mais un fichier contenant {} est refusé. default n’a pas de valeur par défaut à l’intérieur du fichier, et c’est délibéré : écrire le fichier, c’est déclarer une position.

ValeurComportementEnregistré comme
closedRefuser en cas de doute.Deny, motif evaluation failed, fail-closed: …
openLaisser passer l’appel.AllowFailOpen — jamais un Allow franc. La dégradation reste visible à la relecture du journal deux ans plus tard.

Par outil, parce qu’il n’existe pas de bonne réponse universelle : bloquer une recherche en lecture seule casse la production pour rien, laisser passer une suppression est indéfendable.

Refusé à la compilation : une surcharge nommant un outil qui n’est pas dans tools.json. Une faute de frappe n’échouerait pas là, elle appliquerait silencieusement la valeur par défaut à l’outil que l’auteur voulait surcharger : la pire issue possible pour une déclaration en défaillance ouverte.

Les surcharges ne valent que pour les outils du catalogue. Les actions de capacité (resource_read, prompt_get, sampling, elicitation, notify) sont indexées par nom d’action, et un nom d’action n’est pas un nom d’outil : la compilation le refuse donc comme outil inconnu. Ces actions suivent par conséquent toujours default. Gardez default: "closed" sauf si vous avez décidé autrement pour toute la surface.

Qui peut forger des identités, et où les lire à l’intérieur du jeton. Ce fichier plus jwks.json devient le bundle d’identité signé, signé parce que quiconque peut l’écrire peut se forger une identité, et parce que déplacer un chemin de claim change quels groupes sont attribués, donc quelles règles Cedar s’appliquent.

{
"issuer": "https://idp.example.com/realms/corp",
"audience": "obsign-gateway"
}
ChampTypeObligatoireDéfautSignification
issuerchaîneouiClaim iss attendu, comparé exactement. Doit être non vide.
audiencechaîneouiClaim aud attendu. Doit être non vide — une audience vide accepterait des jetons forgés pour n’importe quel autre service.
claimsobjetnonles valeurs par défaut ci-dessousTable de claims. Tout ou rien, voir l’avertissement.

Piège Keycloak. Les jetons d’accès Keycloak portent aud: "account" par défaut. Configurez un mapper d’audience dans le realm pour qu’il émette l’audience de votre passerelle. N’assouplissez jamais le contrôle de ce côté-ci.

Les valeurs par défaut couvrent Keycloak, Entra ID et Okta sans aucune configuration. Omettez complètement claims à moins que votre IdP ne place quelque chose ailleurs.

claims est en tout ou rien pour ses quatre premiers champs. subject, scopes, groups et client_id n’ont pas de valeur par défaut individuelle à l’intérieur de l’objet : dès l’instant où vous écrivez "claims": { … }, les quatre doivent être présents ou le fichier est refusé. Copiez le bloc par défaut complet ci-dessous et éditez-le, plutôt que d’écrire une surcharge partielle. labels et machine, eux, ont bien une valeur par défaut individuelle.

{
"issuer": "https://idp.example.com/realms/corp",
"audience": "obsign-gateway",
"claims": {
"subject": "/sub",
"scopes": ["/scope", "/scp"],
"groups": ["/groups", "/roles", "/realm_access/roles", "/resource_access/*/roles"],
"client_id": ["/client_id", "/azp"],
"labels": ["/preferred_username", "/email", "/name"],
"machine": {
"subject_is_client": true,
"equals": [{ "path": "/idtyp", "value": "app" }],
"prefixes": [{ "path": "/preferred_username", "value": "service-account-" }]
}
}
}
ChampTypeObligatoire dans claimsDéfautRésolution
subjectchaîne (un chemin)oui/subL’identifiant stable. Devient User::"<subject>" dans Cedar et l’identité dans chaque enregistrement d’audit.
scopestableau de cheminsoui["/scope", "/scp"]Le premier chemin qui répond l’emporte. Les scopes ne s’accumulent pas d’une représentation à l’autre : scope et scp sont deux orthographes d’une même chose, les fusionner produirait des doublons trompeurs. Parvient à Cedar sous la forme context.scopes.
groupstableau de cheminsoui["/groups", "/roles", "/realm_access/roles", "/resource_access/*/roles"]Union de tous les chemins, triée et dédoublonnée : un utilisateur peut légitimement porter à la fois des groupes d’annuaire et des rôles applicatifs. Chacun devient un parent Group::"<name>" du principal, si bien que principal in Group::"dba" fonctionne.
client_idtableau de cheminsoui["/client_id", "/azp"]Premier chemin qui répond. Utilisé par le marqueur machine subject_is_client.
labelstableau de cheminsnon["/preferred_username", "/email", "/name"]Nom d’affichage lisible par un humain, la première réponse non vide l’emporte. Enregistré à côté du sujet, jamais à sa place — un nom d’affichage peut être renommé, une piste d’audit a besoin de l’identifiant qui ne le peut pas. Le claim d’où il vient est enregistré avec lui.
machineobjetnonle bloc ci-dessusCe qui marque un jeton comme celui d’une machine. Voir plus bas.

Les valeurs sont lues avec souplesse à la feuille : une chaîne séparée par des espaces ("scope": "a b c") et un tableau ("scp": ["a","b"]) se ramènent tous deux à la même liste, récursivement.

Pas tout à fait RFC 6901 : des segments séparés par /, plus * signifiant chaque enfant d’un objet ou d’un tableau.

CheminLit
/suble sub de premier niveau
/realm_access/rolesimbriqué — les rôles de realm Keycloak
/resource_access/*/rolesles roles de chaque client — les rôles client de Keycloak, dont le segment intermédiaire dépend du client et ne peut pas être écrit en dur
/ext/*/loginn’importe quelle imbrication d’un niveau

Il n’y a aucun échappement ~0/~1 ici (contrairement à policy_args.at) : un claim dont le nom contient littéralement / n’est pas adressable. Un chemin qui ne résout rien n’est pas une erreur : il ne répond simplement pas, et le chemin suivant est essayé.

Ce sont eux qui décident du PrincipalKind, donc des règles Cedar qui s’appliquent, et c’est pourquoi ils voyagent à l’intérieur du bundle signé plutôt que comme une simple option de fichier. Chaque marqueur ne fait qu’ajouter un verdict Machine, si bien qu’élargir ce bloc ne peut jamais rétrograder un vrai humain en robot, seulement l’inverse, qui est le sens sûr.

ChampTypeObligatoireDéfautSignification
subject_is_clientboolnontruesub == client_id : la forme client_credentials de manuel. Mettez false si votre IdP réutilise azp == sub pour des connexions humaines de première partie.
equalstableau de {path, value}non[{"path":"/idtyp","value":"app"}]Un claim égale exactement une valeur. La valeur par défaut est le marqueur « application seule » d’Entra ID.
prefixestableau de {path, value}non[{"path":"/preferred_username","value":"service-account-"}]Un claim commence par un préfixe. La valeur par défaut est le préfixe de connexion réservé aux comptes de service de Keycloak, qu’aucun humain ne porte.

path utilise la même syntaxe que plus haut, joker compris. value est comparé comme une simple chaîne.

Pourquoi trois marqueurs et pas seulement le premier : un vrai jeton client_credentials Keycloak ou Entra a un sub réglé sur l’identifiant propre du principal de service, distinct de l’identifiant de client. À lui seul, sub == client_id laisserait un robot sans clé se classer comme Human et satisfaire une règle « exige un humain ».

La classification obtenue :

context.principal_kindQuand
machineun marqueur quelconque se déclenche
delegated_humanaucun marqueur, et une chaîne act est présente (échange de jetons RFC 8693)
humanaucun marqueur, aucune chaîne act

L’imbrication des act est suivie jusqu’à une profondeur de 8 et parvient à Cedar sous les formes context.actor_chain et context.delegation_depth. Voir Identité & délégation.

compile émet toujours le format courant, obsign-identity/3, dont la signature couvre les chemins de libellés et les marqueurs machine. Les bundles plus anciens (/1, /2) se vérifient toujours, mais chacun refuse exactement ce que sa propre signature ne couvre pas : un bundle /1 ou /2 portant des labels personnalisés, ou un bundle /1 portant des marqueurs machine personnalisés, est rejeté au chargement. Resigner au format courant est à un obsign-control compile de distance.

Les clés publiques de l’IdP, chargées depuis un fichier, jamais depuis le réseau : la passerelle est sur le chemin critique et ne fait aucun appel sortant. Récupérez-le depuis le jwks_uri de votre fournisseur et committez-le tel quel, comme une règle :

Fenêtre de terminal
curl -s https://idp.example.com/realms/corp/protocol/openid-connect/certs \
> identity/jwks.json
{
"keys": [
{
"kty": "RSA",
"kid": "rMx8k…",
"alg": "RS256",
"n": "0vx7agoebGcQSuu…",
"e": "AQAB"
}
]
}
ChampTypeObligatoireDéfautSignification
keystableauouiLe jeu de clés. Doit se résoudre en au moins une clé utilisable.
keys[].ktyénumérationouiRSA | EC | OKP. Tout le reste est refusé.
keys[].kidchaîneouiComparé à l’en-tête kid du jeton. Obligatoire dans le jeton aussi : sans lui, il faudrait essayer chaque clé, ce qui masque une rotation ratée et brouille le diagnostic.
keys[].algchaînenonselon kty, ci-dessousAlgorithme de signature.
keys[].crvchaînenonTransportée et signée, sans autre interprétation ; la courbe est impliquée par alg pour EC et par kty pour OKP.
keys[].n, keys[].ebase64urlpour RSAModule et exposant.
keys[].xbase64urlpour EC et OKPCoordonnée X (EC) ou la clé publique brute (OKP/Ed25519).
keys[].ybase64urlpour ECCoordonnée Y. Ignorée pour OKP.
ktyalg par défautAcceptés
RSARS256RS256, RS384, RS512, PS256, PS384, PS512
ECES256ES256, ES384
OKPEdDSAEdDSA (le champ n’est pas consulté — une clé Ed25519 signe en EdDSA)

Les algorithmes HMAC (HS256 …) et none sont délibérément absents. Une clé symétrique dans un JWKS publié est une vulnérabilité.

Refusé à la compilation — les mêmes contrôles que ceux que fait la passerelle, déplacés en CI : un jeu de clés vide ou entièrement inutilisable ; un kid en double (deux clés pour un même identifiant, et on ne peut plus dire laquelle a signé) ; un composant manquant pour le kty déclaré ; un kty ou un alg non pris en charge.

La rotation n’exige pas de redémarrage. Le bundle d’identité est relu à l’exécution, et chaque rechargement, appliqué ou rejeté, entre dans le journal sous la forme d’un enregistrement config_reload. C’est l’inverse du bundle de politiques. Publiez le nouveau JWKS avant que l’IdP ne commence à signer avec la nouvelle clé, et gardez l’ancienne clé dans le fichier jusqu’à l’expiration de tous les jetons émis.

Quelles passerelles ont le droit d’écrire dans le journal. Enrôler une passerelle, c’est committer son entrée publique ici ; la révoquer, c’est supprimer l’entrée et republier.

[
{
"key_id": "origin-3f19c2a8b4d70e51",
"algo": "ed25519",
"public_key": "3f19c2a8b4d70e51…",
"role": "origin"
}
]
ChampTypeObligatoireDéfautSignification
key_idchaîneouiDoit correspondre octet pour octet à l’identifiant avec lequel la passerelle signe. Les passerelles le dérivent sous la forme origin-<16 premiers caractères hexadécimaux de la clé publique>, donc n’en inventez pas un.
algochaîneouied25519 — la seule valeur acceptée aujourd’hui.
public_keychaîneouiLa clé publique brute de 32 octets, en hexadécimal, sans 0x.
roleénumérationen pratique ouisealorigin | seal. Doit valoir origin ici, et la valeur par défaut est seal : l’omettre est donc refusé.

Ce fichier rejette les champs inconnus. Une clé mal orthographiée est une erreur d’analyse franche. C’est le seul endroit de l’arbre où cette protection existe, parce que ces entrées décident qui peut écrire dans le journal.

Pourquoi role n’est pas optionnel en pratique : les clés d’origine authentifient l’auteur de l’écriture (la passerelle signe chaque enregistrement au moment où elle l’écrit), les clés de scellement certifient le journal (le ledger signe des points de contrôle dessus). Une clé de scellement qui certifierait son propre auteur d’écriture est exactement la cohabitation que les deux rôles empêchent, si bien que le bundle de déploiement ne porte que des clés d’origine.

La passerelle affiche sa propre entrée publique au démarrage, sur stderr. Recopiez l’objet JSON tel quel :

[obsign] origin key origin-3f19c2a8b4d70e51 — every record signed directly — public entry: {"key_id":"origin-…","algo":"ed25519","public_key":"…","role":"origin"}

Les déploiements à deux niveaux (--identity-key, ou --identity-hsm-module en production) affichent la même ligne pour la clé d’identité : [obsign] identity key … — certifies a session key per chain — public entry: {…}. Enrôlez cette entrée-là : la clé d’identité est ce à quoi le bundle doit faire confiance, et les clés par session qu’elle certifie en sont dérivées et vérifiées.

Un fichier absent alors que deployment/ est présent est refusé ; un tableau vide [] est accepté et signifie « aucune passerelle de confiance pour l’instant », légitime avant le premier enrôlement, et honnête plutôt que silencieux. Sous l’exigence par défaut, le ledger refusera alors de sceller quoi que ce soit, ce qui est correct : pas d’auteur d’écriture de confiance, pas de preuve.

Les enrôlements TPM : la preuve que la clé d’identité d’une passerelle réside dans du matériel réel exécutant un logiciel mesuré. Optionnel, et n’a de sens qu’avec un TPM 2.0 ; voir HSM (PKCS#11) et TPM.

Le niveau supérieur est un tableau, une entrée par clé enrôlée.

ChampTypeObligatoireDéfautSignification
key_idchaîneouiQuelle clé d’origine enrôlée cette attestation lie. Doit nommer une entrée de origin-keys.json.
ak_pubhexouiLa clé d’attestation TPM qui a signé le quote et le certify. 32 octets pour une AK Ed25519 ; 65 octets (04 ‖ x ‖ y, point non compressé) pour une AK ECDSA-P256 — le repli pour les nombreux TPM qui n’implémentent aucun EdDSA. L’algorithme est lu sur le matériel de clé, jamais sur un champ déclaré.
ek_certhex (DER)ouiLe certificat de clé d’endossement. Transporté de façon opaque : il chaîne jusqu’à la racine du fabricant du TPM, qui est validée hors bande, jamais ici. Peut être une chaîne vide — le rapport de vérification le signale alors comme invérifiable plutôt que de faire semblant.
certifyhexouiSortie de TPM2_Certify : un TPMS_ATTEST marshalé suivi de la signature de 64 octets de l’AK. Lie la clé d’identité à l’AK.
quotehexouiSortie de TPM2_Quote, de même forme. Rapporte les valeurs des PCR.
expected_pcrstableauouiLes valeurs de PCR que le quote doit rapporter — c’est cela, la politique, et c’est sous la signature ops.
expected_pcrs[].indexentierouiIndex de PCR. La mesure du binaire de la passerelle va par convention dans le 16.
expected_pcrs[].digesthexouiValeur attendue de ce PCR.
identity_pubhexnonabsentLe TPMT_PUBLIC marshalé de la clé d’identité. Présent dans tout ce qu’émet un vrai TPM : le vérificateur recalcule le Name auquel le certify doit correspondre (alg ‖ H(ces octets)) et en extrait la clé publique brute, qui doit être égale à l’entrée enrôlée — si bien que entrée → zone publique → Name → signature de l’AK se referme sans trou. Absent, on retombe sur l’ancienne liaison synthétique, conservée pour que les attestations antérieures au matériel continuent de se vérifier.

Tous les champs hexadécimaux sont en minuscules, sans préfixe 0x.

Fenêtre de terminal
obsign-tpm-enroll \
--tpm /dev/tpmrm0 \
--key-id origin-3f19c2a8b4d70e51 \
--binary-hash "$(sha256sum ./obsign-proxy | cut -d' ' -f1)" \
--pcr 16 \
--ek-cert-file ./ek.der \
--out ./one-attestation.json

Deux choses à savoir :

  1. --out écrit un objet nu ; le fichier doit être un tableau. Enveloppez-le (jq -s '.' one-attestation.json > deployment/attestation.json) ou collez l’objet dans le tableau existant.
  2. Si l’outil vous avertit que votre TPM a produit une clé d’identité ecdsa-p256, ne collez pas son identity_entry dans origin-keys.json. Les bundles de déploiement n’acceptent aujourd’hui que des clés d’origine Ed25519, et une entrée P-256 fait tomber tout le bundle en deployment_bundle_invalid. L’attestation elle-même est valable ; l’entrée attend la prise en charge des clés d’origine P-256.

Refusé à la compilation : une attestation dont le key_id n’est pas une clé d’origine enrôlée, un dérapage de copier-coller qu’il vaut mieux attraper en revue qu’au vérificateur.

my-policies/
├── policies/
│ ├── 00-base.cedar
│ └── 10-finance.cedar
├── tools.json
├── fail-mode.json
├── identity/
│ ├── provider.json
│ └── jwks.json
└── deployment/
└── origin-keys.json

policies/00-base.cedar

@id("forbid_destructive_prod")
forbid (principal, action == Action::"tool_call", resource)
when { resource.destructive && context.env == "prod" };
@id("forbid_robot_destructive")
forbid (principal, action == Action::"tool_call", resource)
when { resource.destructive && !context.has_human_delegation };
@id("allow_scoped")
permit (principal, action == Action::"tool_call", resource)
when {
resource.required_scope != "" &&
context.scopes.contains(resource.required_scope)
};

policies/10-finance.cedar

@id("forbid_large_transfers")
forbid (principal, action == Action::"tool_call", resource == Tool::"transfer_funds")
when { context.args.amount_cents > 100000 };
@id("allow_finance_transfers")
permit (principal in Group::"finance", action == Action::"tool_call", resource == Tool::"transfer_funds");

tools.json

[
{
"name": "search_docs",
"server": "mcp://docs"
},
{
"name": "transfer_funds",
"server": "mcp://ledger",
"destructive": true,
"required_scope": "finance:write",
"policy_args": [
{ "name": "amount_cents", "kind": "long" },
{ "name": "account", "kind": "string", "at": "/destination/iban" }
]
}
]

fail-mode.json

{ "default": "closed", "tools": { "search_docs": "open" } }

identity/provider.json

{
"issuer": "https://idp.example.com/realms/corp",
"audience": "obsign-gateway"
}

identity/jwks.json — committé tel quel depuis le jwks_uri de l’IdP.

deployment/origin-keys.json — l’objet public entry: recopié depuis la ligne de démarrage de chaque passerelle.

Puis :

Fenêtre de terminal
obsign-control compile --source . \
--key ./ops-key.hex --key-id ops-2026 --out ./out
MessageFichierCause
no policies/ directoryle répertoire obligatoire est absent
no .cedar file in policies/policies/un jeu de politiques vide refuse tout ; dites-le explicitement
rule "…" has no @id annotationun .cedarrègle anonyme — l’identifiant est le motif d’audit
conflicting or invalid @id valuesun .cedardeux règles partagent un identifiant, tous fichiers confondus
missing tools.jsonle catalogue fait foi ; sans lui, chaque outil est refusé
a tool with an empty nametools.json
tool "x" is declared twicetools.jsonlaquelle des définitions l’emporte dépendrait de l’ordre des fichiers
policy_args: N declared args, maximum is 16tools.json
policy_args: duplicate arg "x" / an arg has an empty nametools.json
arg "x": "…" is not a JSON pointer (must start with '/')tools.jsonat malformé
arg "x": default: expected an integer (i64 range, floats refused)tools.jsonun default qui ne correspond pas à son kind
attribute … not foundun .cedarune règle lit quelque chose que le modèle n’expose pas — le contrôle de typage refuse de la signer
tool "x": smoke evaluation: …un .cedartypiquement une faute de frappe dans context.args.<name>
missing field \default“fail-mode.jsonle fichier existe mais ne déclare aucune position
fail-mode override for "x", which is not in the cataloguefail-mode.jsonune faute de frappe appliquerait silencieusement la valeur par défaut
identity/ exists but …/provider.json is missingidentity/une demi-configuration d’identité ne vérifie rien
issuer and audience must both be setprovider.jsonune audience vide accepte des jetons forgés pour n’importe quel autre service
missing field \subject`(ouscopes, groups, client_id`)provider.jsonune surcharge claims partielle — fournissez les quatre
unsupported key type: octjwks.jsonune clé HMAC dans un JWKS — refusée par conception
duplicate kid / empty JWKS / malformed JWK / unsupported algorithmjwks.jsonles contrôles du magasin de clés de la passerelle, exécutés à la compilation
deployment/ exists but …/origin-keys.json is missingdeployment/un répertoire de déploiement vide mérite un fichier explicite
key "x" has role "seal"origin-keys.jsonrole omis ou faux — le bundle ne porte que des clés d’origine
origin key id "x" is declared twiceorigin-keys.json
key "x" unusable: …origin-keys.jsonalgo n’est pas ed25519, ou public_key n’est pas 32 octets hexadécimaux valides
unknown field \…“origin-keys.jsonle seul fichier qui rejette les champs inconnus
attestation for "x", which is not an enrolled origin keyattestation.jsondérapage de copier-coller
… is not inside a git repository — pass --labella compilation estampille un sha de commit, ou un libellé explicite
refus d’estampiller un arbre modifiéune citation policies@<sha> doit désigner les octets que ce commit contient

Voir aussi Modes de défaillance à connaître pour les défaillances qui surviennent à l’exécution plutôt qu’à la compilation.