Skip to content
← Tous les articles
· Delta1 Labs Nebula.NETObfuscation.NETGuide

Resigner un assembly .NET après obfuscation : noms forts et Authenticode

Le renommage réécrit votre assembly, ce qui invalide le nom fort et la signature Authenticode avec lesquels il a été compilé — une build protégée livrée non signée, ou pire avec une signature désormais cassée, ne se chargera pas ou échouera à la vérification. Nebula.NET resigne pour vous comme dernière étape du pipeline : une nouvelle signature de nom fort sur l'image obfusquée, les jetons de clé publique inter-assemblys réécrits pour que les assemblys interdépendants se résolvent encore, puis une signature Authenticode par-dessus. Voici l'ordre exact, comment fournir la clé à un serveur de build sans la committer, et ce que casse InternalsVisibleTo si vous l'oubliez.

La signature et l’obfuscation ont un problème d’ordre qui mord chaque équipe la première fois qu’elle active la protection dans un pipeline de publication. Un nom fort comme une signature Authenticode sont des signatures sur les octets de votre assembly. L’obfuscation réécrit ces octets : renommer un type change les métadonnées, chiffrer une chaîne réécrit l’IL, aplatir le flux de contrôle remplace les corps de méthode. Un assembly que vous avez signé puis obfusqué porte donc une signature qui ne correspond plus à son propre contenu : le CLR rejette le nom fort comme altéré, et toute signature Authenticode se vérifie comme cassée. La build semble bonne sur votre machine et ne se charge pas sur une machine verrouillée.

La règle est simple — signer en dernier — mais le faire à la main à travers un ensemble interdépendant d’assemblys, dans le bon ordre, sans fuiter la clé dans le dépôt, est délicat. Nebula.NET replie tout cela à la fin du pipeline. Cet article explique exactement ce qu’il fait, dans quel ordre, et les deux choses (clés CI et InternalsVisibleTo) qui requièrent encore votre attention.

Pourquoi le renommage invalide le nom fort

Un nom fort est une signature RSA sur un hachage du contenu de l’assembly, stockée dans l’assembly lui-même, la clé publique correspondante devenant partie de l’identité de l’assembly. Le CLR recalcule ce hachage au chargement (pour un assembly entièrement signé dans un contexte qui le vérifie) et compare. Changez un octet de métadonnées ou d’IL après signature et les hachages divergent :

> sn -vf MyApp.dll
Failed to verify assembly -- Strong name validation failed.

C’est le résultat attendu d’une obfuscation d’un assembly présigné. Le .snk d’origine reste la bonne clé ; ce qui ne va pas, c’est que la signature a été calculée sur les octets d’avant l’obfuscation. Vous ne pouvez pas « préserver » un nom fort à travers l’obfuscation ; vous ne pouvez que le réappliquer après, sur l’image finale. C’est ce que fait strongNameKeyFile :

{
  "inputs": ["bin/Release/net8.0/MyApp.dll"],
  "outputDirectory": "bin/Release/net8.0/obf",
  "controlFlowObfuscation": true,
  "encryptStrings": true,
  "strongNameKeyFile": "keys/MyApp.snk"
}

Nebula obfusque, puis signe la sortie avec cette clé comme étape finale de métadonnées. La clé publique de la sortie — et donc son identité de nom fort — est celle que porte le .snk. Signer et resigner après l’obfuscation est une fonctionnalité Pro, au même titre que le reste du durcissement au-delà du renommage.

Gardez la clé hors du disque du serveur de build

Committer un .snk dans le dépôt annule l’intérêt d’en avoir un. Utilisez strongNameKeyEnvVar : il nomme une variable d’environnement contenant le base64 du .snk, que le coffre de secrets de votre CI injecte au moment du build. Il prime sur strongNameKeyFile, donc la même configuration produit une vraie build signée sur le serveur et une build non signée (ou à signature différée) sur une copie de développeur sans secret.

{
  "inputs": ["bin/Release/net8.0/MyApp.dll"],
  "outputDirectory": "bin/Release/net8.0/obf",
  "strongNameKeyEnvVar": "MYAPP_SNK_BASE64",
  "delaySign": false
}
# In CI, from the secret store — the key never touches the repo or the config:
export MYAPP_SNK_BASE64="$(cat /secrets/MyApp.snk | base64 -w0)"
nebula --config nebula.config.json

delaySign: true embarque uniquement la clé publique et laisse de la place pour une signature appliquée plus tard avec sn -R — utile quand la clé privée vit dans un HSM ou un service de signature contrôlé et n’atteint jamais le build.

L’ordre qu’il ne faut pas inverser

Quand vous signez aussi avec Authenticode, l’ordre n’est pas une préférence : il est imposé par ce que couvre chaque signature. La signature de nom fort vit dans les métadonnées de l’assembly, donc la calculer change le fichier. La signature Authenticode est calculée sur (presque) tout le fichier et y est embarquée. Par conséquent :

1 · obfusquerécrire image renommée2 · nom fortembarquer en métadonnées3 · Authenticodesigner le fichier en dernierinverser 2↔3et le fichierchange sousla signature

Si vous signiez Authenticode avant de réappliquer le nom fort, embarquer la signature de nom fort réécrirait des octets sous la signature Authenticode et la casserait. Nebula fait toujours le nom fort d’abord, puis Authenticode ; vous fournissez simplement le certificat :

{
  "inputs": ["bin/Release/net8.0/MyApp.dll"],
  "outputDirectory": "bin/Release/net8.0/obf",
  "strongNameKeyEnvVar": "MYAPP_SNK_BASE64",
  "authenticodeCertThumbprint": "9F2C…",            // a cert already in the Windows store
  "authenticodeTimestampUrl": "http://timestamp.digicert.com"
}

Préférez authenticodeCertThumbprint (un certificat dans le magasin de la machine) ou un .pfx dont le mot de passe vient de authenticodeCertPassword fourni via une variable d’environnement plutôt qu’une chaîne en clair. Définissez toujours authenticodeTimestampUrl : un horodatage RFC-3161 permet à la signature de continuer à se vérifier après l’expiration du certificat de signature. Authenticode nécessite signtool du SDK Windows sur l’agent de build.

Multi-assembly : une clé pour tout l’ensemble

Si votre produit livre des assemblys interdépendants, resignez-les dans la même exécution de Nebula. Le renommage change l’identité de chaque assembly, et une référence d’assembly porte le jeton de clé publique de l’assembly référencé ; Nebula réécrit la référence de chaque frère vers le nouveau jeton pour que l’ensemble reste mutuellement chargeable :

{
  "inputs": ["bin/Release/net8.0/MyApp.dll", "bin/Release/net8.0/MyApp.Core.dll"],
  "outputDirectory": "bin/Release/net8.0/obf",
  "crossAssemblyRename": true,
  "strongNameKeyEnvVar": "MYAPP_SNK_BASE64"
}

Signez les deux assemblys séparément, avec des clés différentes ou dans des exécutions différentes, et la référence de l’app à MyApp.Core nommera un jeton de clé publique que le MyApp.Core resigné n’a plus — un FileLoadException au premier appel vers lui.

Le piège d’InternalsVisibleTo

La seule chose que resigner casse silencieusement est une relation d’assembly ami. InternalsVisibleTo nomme l’ami par sa clé publique, car sinon n’importe qui pourrait usurper votre assembly de test pour atteindre vos internes. Si vous resignez avec un .snk différent de celui avec lequel l’assembly a été construit à l’origine, la clé publique de la sortie change, et l’attribut nomme toujours l’ancienne :

// In MyApp.csproj's AssemblyInfo — this names the OLD public key:
[assembly: InternalsVisibleTo("MyApp.Tests, PublicKey=0024000004800000…")]

À l’exécution, le CLR voit que MyApp.Tests n’a plus la clé nommée et refuse l’accès ami — un MethodAccessException ou une erreur de chargement « friend assembly reference is invalid ». Deux corrections honnêtes :

  • À privilégier : ne livrez pas l’ami. InternalsVisibleTo existe presque toujours pour un assembly de test que vous ne distribuez pas. Signez vos assemblys livrés avec votre clé de publication et laissez l’assembly de test entièrement hors de l’exécution d’obfuscation ; l’attribut ne compte que quand l’assembly nommé est réellement chargé contre la build protégée.
  • Si l’ami est livré aussi : mettez à jour le PublicKey= de l’attribut vers la clé publique du .snk de publication (lisez-la avec sn -tp release.snk) et obfusquez les deux assemblys ensemble avec cette unique clé.

Vérifiez avant de livrer

Resigner est l’étape qui mérite le plus un contrôle rapide en CI, car une signature cassée n’apparaît que sur une machine qui l’impose :

sn -vf obf/MyApp.dll                       # strong name verifies
signtool verify /pa /v obf/MyApp.dll        # Authenticode chains + timestamp present

Puis exécutez l’app depuis le dossier de sortie — le vrai test que chaque référence inter-assemblys se résout encore contre les frères resignés.

À retenir

L’obfuscation invalide toute signature calculée avant elle, donc la signature doit avoir lieu après, sur l’image finale, dans un ordre fixe : obfusquer, embarquer le nom fort, signer Authenticode en dernier. Nebula fait les trois comme queue du pipeline — alimentez-le avec la clé de nom fort via strongNameKeyEnvVar pour qu’elle ne touche jamais le dépôt, signez un ensemble interdépendant en une seule exécution pour que les jetons de clé publique restent cohérents, et définissez une URL d’horodatage pour qu’Authenticode survive au certificat. Les deux seules choses qui vous restent sont de garder la clé hors du contrôle de version et de vous souvenir qu’InternalsVisibleTo nomme une clé qui vient de changer. Vérifiez avec sn -vf et signtool verify en CI et l’échec « marche ici, ne charge pas là » n’atteint jamais un client.

Essayez Nebula.NET

Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.