# L'identité de l'exécutable d'un agent après le remplacement d'un lien symbolique

Un nom de fichier sert de repère, pas d'identité. Cette distinction peut sembler tatillonne jusqu'au jour où un agent obtient une approbation alors qu'il s'exécute depuis `~/bin/agent`, où quelqu'un remplace ce chemin par un autre programme et où la passerelle considère que l'appelant suivant est digne de confiance parce que le libellé semble toujours familier.

Les liens symboliques rendent l'erreur facile à reproduire, mais ils n'en sont pas la cause profonde. Toute indirection modifiable peut créer le même problème : un wrapper shell, une entrée de PATH, un binaire copié, une copie de travail de développement ou un lanceur qui résout un chemin à chaque requête. Une passerelle qui approuve un nom de chemin approuve un objet qu'un utilisateur non privilégié peut souvent remplacer.

Sur macOS, l'unité utile est le processus en cours et son identité de code. Les services de signature de code d'Apple peuvent décrire et valider le code associé à un processus. Un chemin peut apparaître dans l'enregistrement à des fins de diagnostic, mais il ne doit pas déterminer l'autorisation.

## Un lien symbolique est un service de noms, pas le programme

Un lien symbolique stocke une chaîne que le noyau résout lorsqu'un programme l'ouvre ou l'exécute. Le remplacement de cette chaîne modifie ce qu'un prochain appel à `exec` trouvera. Il ne réécrit pas l'image exécutable d'un processus déjà démarré.

Ce détail donne une fausse impression de sécurité. On teste le remplacement d'un lien symbolique, on voit le processus d'origine continuer à fonctionner et on en conclut que l'attaque est inoffensive. Le test n'a montré que le comportement normal d'un processus. Il n'a pas vérifié la décision importante : la passerelle reconnaît-elle un processus ultérieur lancé depuis l'alias comme étant celui qu'elle avait approuvé auparavant ?

Prenons un agent de programmation lancé depuis ce chemin :

```text
~/bin/build-agent -> ~/work/approved-agent
```

La passerelle reçoit la première requête, voit `~/bin/build-agent` et demande l'autorisation à l'opérateur. Un attaquant modifie ensuite le lien :

```text
~/bin/build-agent -> ~/Downloads/replacement-agent
```

Si le processus approuvé continue d'envoyer des requêtes, la passerelle peut légitimement poursuivre cette session jusqu'à la fin du processus. Ce n'est pas le problème. Le problème apparaît lorsqu'un autre processus démarre avec le même nom et que la passerelle dit : « J'ai déjà approuvé `~/bin/build-agent`. »

Le second processus est un nouveau sujet de sécurité. Il doit faire l'objet d'une nouvelle inspection et, sauf s'il correspond à une règle de mise à jour définie intentionnellement, d'une nouvelle approbation.

Le même bug apparaît sans lien symbolique. Une passerelle qui exécute `codesign` sur le chemin fourni par un client peut inspecter un fichier tout en acceptant une requête provenant d'un autre processus. Le client peut exploiter une condition de concurrence en modifiant le chemin après l'inspection, ou simplement indiquer à la passerelle un alias stable qui renvoie vers des cibles changeantes. Un chemin bien présenté sur une carte d'approbation ne corrige pas cette conception.

Apple traite les exigences de code comme des contraintes d'identité plutôt que comme des noms de fichiers. Sa documentation indique qu'une designated requirement contient les critères servant à déterminer si un code est le même que celui observé précédemment. Elle est normalement dérivée de l'autorité de signature et de l'identifiant intégré lorsque le développeur n'en a pas déclaré une. C'est la bonne direction, avec une réserve importante : une exigence de code décrit l'identité du code, pas une session de processus en direct.

## Le processus en cours est l'objet qui a obtenu l'approbation

Une passerelle doit prendre sa décision d'autorisation à partir d'une référence vers un processus en cours, obtenue auprès du système d'exploitation. Elle doit ensuite conserver cette décision liée à l'exécution du processus, et non à un chemin déclaré par le client, à un nom d'exécutable ou à une variable d'environnement réutilisable.

Sur macOS, une bonne inspection a deux objectifs :

1. Obtenir les informations de signature du véritable processus appelant.
2. Valider ce processus avant d'utiliser les informations d'identité comme preuve d'autorisation.

Ces deux tâches sont distinctes. `SecCodeCopyDesignatedRequirement` peut renvoyer une designated requirement pour du code signé, mais Apple précise explicitement que cet appel ne valide pas la signature. Du code modifié après la signature ou mal signé peut tout de même produire des informations incomplètes ou trompeuses. Une passerelle doit également effectuer un contrôle de validité.

L'enregistrement du processus doit contenir suffisamment d'éléments pour expliquer la décision a posteriori :

- l'identifiant du processus ou, mieux encore, un identifiant de processus fourni par le système d'exploitation et résistant à la réutilisation des PID
- le chemin de l'exécutable observé au moment de l'inspection, conservé uniquement comme contexte
- l'identifiant de signature et l'identifiant d'équipe lorsqu'ils sont disponibles
- l'autorité de signature et la chaîne de certificats le cas échéant
- la designated requirement
- le cdhash ou l'ensemble des valeurs cdhash prises en charge
- le résultat de la validation de la signature et l'heure de l'inspection

Ne prenez pas cette liste pour un langage de politique. La passerelle doit répondre à une question précise : quel processus en cours fait la demande, et un humain a-t-il approuvé cette exécution ? Quelques éléments d'identité suffisent. Une accumulation de règles de chemin ne suffit pas.

La gestion des mises à jour est la partie délicate. Une designated requirement reste souvent stable entre des versions légitimes. C'est pourquoi macOS l'utilise pour assurer la continuité lorsqu'un utilisateur autorise une application à accéder à un service protégé. La note technique Apple TN3127 donne l'exemple courant d'une application mise à jour qui souhaite de nouveau utiliser le microphone : macOS compare la nouvelle version à la designated requirement enregistrée.

Ce comportement convient à une autorisation durable du système d'exploitation. Il est trop large si vous réutilisez silencieusement une ancienne approbation humaine pour une nouvelle exécution autonome d'agent. Une passerelle peut afficher l'autorité de signature pour aider l'opérateur à reconnaître le programme, tout en redemandant une approbation lorsqu'un nouveau processus démarre. Le signataire établit la provenance. La limite du processus détermine la durée du consentement.

## Les contrôles statiques de chemin laissent une condition de concurrence impossible à justifier

Un contrôle statique répond à une question statique : « Quelle signature ce fichier possède-t-il actuellement à ce chemin ? » Il ne peut pas, à lui seul, répondre à la question : « Quel code a émis cette requête ? »

Cette lacune compte même lorsque le contrôle statique est techniquement correct. Supposons qu'une passerelle reçoive une requête indiquant que son chemin d'exécutable est `/Users/dev/bin/agent`. Elle exécute :

```sh
codesign --verify --strict --verbose=2 /Users/dev/bin/agent
codesign -d -r- /Users/dev/bin/agent
```

Les deux commandes peuvent signaler une signature valide et une designated requirement. La passerelle enregistre alors le chemin comme identité approuvée. Entre le contrôle et une requête ultérieure, un attaquant peut faire pointer `agent` vers un autre fichier. Lors de la requête suivante, la passerelle peut exécuter à nouveau ces commandes et obtenir des informations sur le remplacement. Aucune des deux commandes ne prouve un lien avec le processus qui a émis la requête.

Il existe une seconde condition de concurrence que les équipes oublient. Un composant auxiliaire peut inspecter un chemin avant de lancer un agent, puis recevoir plus tard une connexion d'un processus enfant. Le contrôle du chemin décrit le fichier parent au moment du lancement. La connexion décrit un processus au moment de la requête. Si le composant auxiliaire ne relie pas ces deux événements à l'aide d'un identifiant fourni par le système d'exploitation, il a créé une supposition et l'a appelée provenance.

Les API de signature de code d'Apple exposent pour cette raison des catégories d'informations distinctes. `kSecCSSigningInformation` demande les données de certificat et CMS, `kSecCSRequirementInformation` demande les exigences et `kSecCSDynamicInformation` demande les informations de validité dynamique du code en cours d'exécution. L'API ne transforme pas ces éléments en conception complète de passerelle, mais elle rend la distinction claire : l'inspection statique d'une signature et l'état du code en cours sont des entrées différentes.

Un nom de fichier a toujours sa place dans l'affichage de l'approbation. Les opérateurs doivent voir qu'une requête provient d'une copie de travail dans `~/work/demo` plutôt que d'un outil installé dans `/Applications`. Considérez cela comme du contexte d'interface. L'enregistrement d'autorisation doit rester lié à l'appelant identifié par le système d'exploitation.

## Reproduire le remplacement sans toucher à un véritable agent

Vous pouvez démontrer le problème des chemins avec deux petits binaires dans un répertoire temporaire. Ce test ne nécessite ni jeton de production, ni clé SSH, ni bundle d'application modifié.

Créez un espace de travail et compilez deux programmes qui affichent des marqueurs différents. Le code reste volontairement banal, car le comportement testé est le remplacement, pas la logique du programme.

```sh
work="$(mktemp -d /tmp/agent-identity.XXXXXX)"
cd "$work"

cat > approved.c <<'EOF'
#include <cstdio.h>
#include <unistd.h>
int main(void) {
  printf("approved process pid=%d\n", getpid());
  fflush(stdout);
  sleep(60);
  return 0;
}
EOF

cat > replacement.c <<'EOF'
#include <cstdio.h>
#include <unistd.h>
int main(void) {
  printf("replacement process pid=%d\n", getpid());
  fflush(stdout);
  sleep(60);
  return 0;
}
EOF

clang approved.c -o approved-agent
clang replacement.c -o replacement-agent
codesign --force --sign - --identifier com.example.approved approved-agent
codesign --force --sign - --identifier com.example.replacement replacement-agent
ln -s "$work/approved-agent" agent
```

Une signature ad hoc suffit pour tester les mécanismes localement, mais elle ne possède aucune chaîne de certificats. Apple précise qu'un code signé ad hoc ne contient aucun certificat et que ses données CMS sont vides. N'utilisez pas une signature ad hoc pour simuler un signataire de distribution ou pour décider ce que votre passerelle de production accepte.

Notez la cible vers laquelle l'alias est résolu, inspectez sa signature et démarrez le premier processus :

```sh
printf 'alias before: %s\n' "$(readlink agent)"
codesign -d -r- agent 2>&1 | sed -n '1,8p'
./agent &
first_pid=$!
printf 'first pid: %s\n' "$first_pid"
ps -p "$first_pid" -o pid=,comm=,args=
```

La forme de la sortie devrait ressembler à ceci :

```text
alias before: /tmp/agent-identity.xxxxxx/approved-agent
Executable=/tmp/agent-identity.xxxxxx/agent
designated => identifier "com.example.approved"
approved process pid=48291
first pid: 48291
48291 ... ./agent
```

Remplacez maintenant le lien pendant que le premier processus dort :

```sh
rm agent
ln -s "$work/replacement-agent" agent
printf 'alias after: %s\n' "$(readlink agent)"
codesign -d -r- agent 2>&1 | sed -n '1,8p'
ps -p "$first_pid" -o pid=,comm=,args=
```

Vous devriez maintenant voir `com.example.replacement` en inspectant `agent`, tandis que `first_pid` reste actif. Lancez une seconde fois l'alias :

```sh
./agent &
second_pid=$!
printf 'second pid: %s\n' "$second_pid"
wait "$first_pid" "$second_pid"
```

Le second processus affiche `replacement process`. Les deux PID ont exécuté des images de programme différentes, même s'ils ont tous deux été lancés avec `./agent`.

Ce résultat doit modifier votre plan de test de la passerelle. Un test qui vérifie seulement si l'ancien PID a continué à fonctionner n'a presque rien démontré. Le test utile consiste à vérifier si la passerelle traite le second PID comme un nouvel appelant et affiche son identité observée avant de lui permettre d'utiliser une action approuvée.

## Un test d'approbation utile comporte trois exécutions distinctes

Ne vous contentez pas d'une approbation nominale et d'une commande de remplacement de lien symbolique. Vous avez besoin de trois exécutions, car chacune vérifie une propriété différente.

Commencez par lancer la cible approuvée via l'alias et effectuez une demande d'action. La passerelle doit créer un enregistrement de session et afficher une approbation qui identifie le véritable appelant avec des informations qu'un opérateur peut évaluer. Enregistrez l'identifiant de session, le PID, le chemin observé et les éléments de signature de code.

Ensuite, modifiez l'alias en laissant le premier processus actif. Faites effectuer une autre requête au premier processus. Si votre modèle de session approuve une exécution jusqu'à sa fin, la passerelle peut autoriser cette requête. L'image exécutable n'a pas changé. Ne qualifiez pas à tort ce résultat attendu de vulnérabilité.

Enfin, lancez un nouveau processus via l'alias remplacé et effectuez la même requête. La passerelle ne doit pas hériter de l'approbation du premier processus simplement parce que le nom de l'exécutable, la ligne de commande, le répertoire de travail ou l'action demandée correspondent. Elle doit créer une session distincte et exiger le parcours d'autorisation habituel.

Utilisez un tableau de résultats pendant le test :

| Exécution | Cible de l'alias au lancement | Résultat attendu |
| --- | --- | --- |
| Premier processus | `approved-agent` | Nouvelle approbation, puis autorisation pour cette exécution |
| Premier processus après le remplacement | `replacement-agent` sur le disque, mais l'ancienne image reste en mémoire | Le comportement de la session existante continue jusqu'à la fin |
| Second processus | `replacement-agent` | Nouvelle approbation ou refus, jamais l'approbation héritée |

Un autre cas mérite d'être testé. Remettez l'alias sur le binaire d'origine, puis démarrez un troisième processus. La passerelle doit tout de même créer une nouvelle session. Une même identité de code ne signifie pas un même processus. Si votre produit propose délibérément une relation de confiance persistante pour un éditeur signé, faites-en une décision distincte et explicite de l'opérateur. Ne l'introduisez pas discrètement dans une fonction d'approbation par processus.

L'autorisation par session de Sallyport est activée par défaut, et sa carte d'approbation met en avant l'autorité de signature du processus plutôt qu'un nom de fichier modifiable. L'opérateur dispose ainsi d'un élément plus solide pour évaluer la demande d'action d'un nouvel agent.

## Le signataire, la designated requirement et le cdhash répondent à des questions différentes

Les équipes regroupent souvent trois concepts distincts sous l'expression « signé par la même application ». Ce raccourci produit soit une lassitude face aux approbations, soit des permissions qui durent trop longtemps.

Une autorité de signature répond à la question : qui a signé le code ? Pour un logiciel distribué, elle peut inclure une chaîne de certificats et un identifiant d'équipe. Elle aide l'opérateur à distinguer un éditeur connu d'un exécutable quelconque. Elle ne désigne pas un build précis.

Une designated requirement répond à la question : macOS doit-il considérer ce code comme la même identité au fil des mises à jour ? Apple indique que tout code signé possède une designated requirement, explicite ou synthétisée. Par défaut, celle-ci intègre normalement l'autorité de signature et l'identifiant intégré. Elle convient donc à la continuité, mais peut accepter des versions ultérieures que vous n'avez pas inspectées personnellement.

Un cdhash identifie un CodeDirectory signé précis. Pour l'autorisation, il s'apparente à l'empreinte d'un build. C'est une excellente preuve d'audit, car elle permet à un enquêteur de distinguer deux versions partageant le même signataire et le même identifiant. C'est généralement une mauvaise règle d'approbation permanente pour les outils de développement, car les mises à jour courantes le modifieront.

Utilisez les preuves selon la décision à prendre :

- Utilisez l'identifiant du processus en cours pour lier une session à un appelant.
- Affichez l'autorité de signature et l'identifiant dans l'interface d'approbation afin que l'opérateur puisse reconnaître la source.
- Enregistrez la designated requirement et le cdhash afin que les investigations ultérieures puissent distinguer la continuité de l'éditeur du build exact.
- Demandez une nouvelle approbation pour chaque nouveau processus d'agent, même lorsque le signataire et la designated requirement correspondent.

Ce dernier point est volontairement plus strict que les permissions macOS. Une passerelle d'agent peut envoyer des requêtes HTTP ou des commandes SSH avec des identifiants qui n'entrent jamais dans le processus de l'agent. Il s'agit d'une limite d'action, pas d'une demande ponctuelle d'accès au microphone. Réutiliser une approbation passée pour un processus ultérieur sans lien affaiblit le contrôle humain qui justifiait la passerelle.

## Ne transformez pas un observateur de fichiers en mécanisme d'autorisation

Une réaction tentante consiste à surveiller le chemin de l'agent et à révoquer l'autorisation lorsque le fichier change. L'idée séduit parce qu'elle semble simple : enregistrer l'inode d'origine, s'abonner aux événements du système de fichiers et invalider l'approbation après une écriture ou un renommage.

C'est une mauvaise fondation.

Les événements du système de fichiers sont une télémétrie utile, pas une preuve de l'identité de l'appelant. Les chemins peuvent avoir plusieurs noms. Un binaire peut être copié. Un processus peut être lancé depuis un fichier désolidarisé du système de fichiers. La livraison des événements peut être retardée ou regroupée. Un attaquant n'a pas besoin de gagner une course spectaculaire si votre conception autorise déjà le chemin au lieu du processus.

Une comparaison d'inodes ne corrige pas non plus le modèle. Elle peut aider à détecter un remplacement dans un répertoire donné, mais elle n'apprend rien d'utile sur un processus lancé depuis un autre lien physique ou une autre copie. Elle crée aussi un comportement fragile dans les flux de développement ordinaires, où les outils reconstruisent, renomment et remplacent constamment les fichiers.

Considérez les observations de fichiers comme un contexte d'audit supplémentaire. Si le chemin actuel pointe vers une autre cible qu'au moment de l'approbation, consignez-le. Si votre passerelle voit un nouveau processus appelant, inspectez cet appelant et appliquez le parcours d'autorisation habituel. La limite du processus prend en charge la décision de sécurité sans demander à l'observateur de se comporter comme un moniteur de référence.

Cette règle évite également une erreur plus subtile : autoriser un wrapper parce que sa signature semble familière tout en ignorant ce qu'il lance. Un wrapper signé peut exécuter un enfant non signé, charger un script local ou sélectionner une cible selon son environnement. La passerelle doit identifier le processus qui se connecte réellement et demande l'action privilégiée. Si le wrapper est l'appelant, c'est lui qu'il faut approuver. Si son enfant est l'appelant, inspectez l'enfant.

## Lier l'approbation à un identifiant système et refuser en cas d'ambiguïté

Une implémentation pratique a besoin d'une référence fournie par le système d'exploitation pour le demandeur. Sur macOS, cela signifie généralement obtenir un objet de code invité pour le processus appelant via Code Signing Services, en utilisant les attributs du processus fournis par le contexte de connexion fiable plutôt que des valeurs copiées depuis l'entrée de l'agent.

La séquence sûre ressemble à ceci :

1. Accepter une connexion et obtenir l'identité système de l'appelant depuis le mécanisme de connexion.
2. Relier cette identité à un objet de code en cours d'exécution.
3. Effectuer le contrôle de validité du code avant de lire les données d'identité à des fins d'autorisation.
4. Lire l'autorité de signature, l'identifiant, la designated requirement et le cdhash depuis cet objet de code en cours.
5. Créer un enregistrement de session lié à l'identifiant de l'appelant et demander une approbation si aucune session approuvée n'existe.
6. Vérifier de nouveau avant chaque action que l'identifiant de l'appelant fait toujours référence au même processus actif, puis détruire la session lorsque le processus se termine ou que l'opérateur la révoque.

Les détails de la première étape dépendent du transport. Un socket Unix local, une connexion XPC et un tube reliant des processus enfants exposent des identifiants différents. Le repli dangereux est le même dans tous les cas : accepter un PID, un chemin, un identifiant de bundle ou un résumé de signature que l'agent envoie dans le contenu de sa requête. L'agent contrôle ce contenu. Il ne constitue donc aucune preuve.

La réutilisation des PID mérite une attention particulière. Un PID seul n'est unique que pendant l'existence du processus. Après sa fin, le système d'exploitation peut attribuer le même numéro à un autre processus. Conservez un identifiant plus riche si l'API en fournit un. Si votre transport ne peut pas fournir une liaison durable avec l'appelant, limitez la session à la durée de la connexion et réautorisez après une reconnexion. Refuser en cas d'incertitude est moins pratique qu'accepter un appelant ambigu, mais c'est précisément dans cette ambiguïté que vivent les bugs de remplacement de chemin.

Les demandes d'approbation doivent rester honnêtes. Si la passerelle voit une signature ad hoc, dites-le. Si aucune signature valide n'existe, dites-le. Si le programme possède un nom d'affichage familier mais une nouvelle autorité de signature, ne cachez pas cette autorité derrière un volet secondaire. La première ligne doit indiquer à l'opérateur qui a signé le processus et quelle action il souhaite effectuer.

## La piste d'audit doit conserver la décision, pas seulement la requête

Une entrée de journal indiquant que `POST /deploy` a réussi ne peut pas expliquer un incident de remplacement de lien symbolique. Elle indique ce qui s'est passé, mais pas pourquoi la passerelle a autorisé cet appelant à le faire.

Pour chaque session approuvée, conservez un instantané des éléments d'identité observés au moment de l'approbation. Pour chaque action, enregistrez la référence de session et le résultat. L'enregistrement d'action n'a pas besoin de recopier les données de certificat à chaque fois, mais il doit renvoyer précisément à l'enregistrement d'approbation qui les contenait.

Un enregistrement compact peut ressembler à ceci :

```json
{
  "session": "6F2A...",
  "caller": {
    "process": "OS-issued caller credential",
    "path_observed": "/private/tmp/demo/agent",
    "signing_identifier": "com.example.approved",
    "designated_requirement": "identifier com.example.approved",
    "cdhash": "<observed digest>",
    "validity": "valid"
  },
  "approval": "granted",
  "action": "HTTP POST /deploy",
  "result": "success"
}
```

Le chemin reste utile. Il peut indiquer qu'un processus a été lancé depuis un répertoire temporaire ou un alias shell. Il ne doit pas suffire à rattacher une requête ultérieure à cette session.

Sallyport conserve à la fois un journal des sessions pour les exécutions d'agents et un journal d'activité pour les appels individuels, tous deux produits à partir d'un journal d'audit chiffré et chaîné par hachage. Sa commande hors ligne `sp audit verify` vérifie la chaîne sans exiger la clé du coffre-fort. Cette conception facilite l'examen d'un test de remplacement de lien symbolique, car l'événement d'approbation et l'action ultérieure sont deux faits distincts plutôt qu'un vague message de réussite.

Effectuez l'exercice du lien symbolique avant d'écrire une règle d'approbation que vous ne pourrez pas expliquer plus tard. Si un remplacement nouvellement lancé peut utiliser l'approbation du premier processus, cessez de considérer le nom de l'exécutable comme un simple champ pratique. Il s'est introduit dans la décision de confiance.
