Service de signature de code
Signez binaires, archives, commits et images de conteneurs avec une clé non exportable détenue dans votre vault ou HSM, sous approbation et avec un journal complet.
Vue d'ensemble
Une application de signature de code associe une clé de vault non exportable, éventuellement une chaîne de certificats de signature de code et éventuellement une politique d'approbation. Les machines de build et les développeurs envoient un condensat ou une petite charge utile à l'API de signature ; le vault signe ; la signature revient à l'outil standard, qui l'intègre. La clé ne quitte jamais le vault, chaque signature peut exiger une approbation M-of-N, et chaque signature est inscrite au journal de l'application.
Ce qui est pris en charge
| Usage | Outil | Fonctionnement |
|---|---|---|
| Authenticode (EXE, DLL, MSI, CAB…) | Microsoft SignTool | signtool sign /dg écrit le condensat, le client DuoKey le fait signer par le vault, signtool sign /di intègre la signature |
| Archives Java (JAR) | Moteur de signature JAR du JDK, via le signataire JAR DuoKey ; vérification avec jarsigner -verify | Le JDK construit le bloc de signature JAR ; l'opération de signature est réalisée par DuoKey |
| Commits et tags Git | Signature SSH de Git (gpg.format=ssh) avec le client DuoKey comme gpg.ssh.program | Signature OpenSSH réalisée avec la clé de l'application |
| Images de conteneurs | cosign generate / cosign attach signature / cosign verify --key | La charge utile de signature est signée via le client DuoKey |
| Toute signature détachée | Tout outil capable de faire signer un condensat ou un petit fichier en HTTPS | POST /api/apps/{id}/code-sign/sign |
| Modules noyau, macros VBA Office | — | Les types d'application existent et utilisent la même API de signature ; aucune intégration d'outil n'est fournie |
Non pris en charge : la signature Notation (Notary Project) et les fournisseurs PKCS#11 ou Windows CNG locaux pour la signature de code. Les outils s'intègrent via l'API de signature à distance et les clients de référence DuoKey : dkesign.py (Python 3.8+, bibliothèque standard uniquement) et DuoKeyJarSigner.java (JDK 17+, sans dépendance), disponibles auprès du support DuoKey.
Modèle de sécurité
- La clé reste dans le vault. La clé associée doit être RSA 2048/4096 ou EC P-256/P-384, avoir la capacité de signature et avoir été créée non exportable. L'exportabilité est à sens unique (une clé non exportable ne peut jamais devenir exportable), et tous les chemins d'export de clé la refusent.
- Pas de contournement de l'application. L'API générique de signature par clé refuse une clé associée à une application de signature de code, et une clé ne sert qu'à une application par tenant : une seconde application sans approbation ne peut pas utiliser la même clé.
- Approbation. Une application peut exiger que chaque signature soit approuvée selon une politique de quorum qui impose le double contrôle (seuil de 2 ou plus, ou demandeur exclu des approbateurs). L'approbation est liée à l'application, au condensat, à l'algorithme et au nom de l'artefact, utilisable une seule fois et par le seul demandeur. Désactiver l'approbation, la déplacer vers une autre politique ou supprimer l'application nécessite elle-même une approbation.
- Journal. Chaque signature est journalisée avant l'appel au vault puis complétée, si bien qu'aucune signature ne sort sans son enregistrement. Les signatures, modifications d'application et demandes de certificat sont aussi inscrites au journal d'activité.
- Permissions. Signer et vérifier nécessitent
Operations.Apps.CodeSignAccess; lire la configuration et le journal nécessiteOperations.Apps.Read. Les accès sont limités au tenant et à l'unité organisationnelle. Les politiques cryptographiques s'appliquent à chaque signature. - Identifiants. Les clients s'authentifient avec
Authorization: Bearer <token>: une clé d'API machine (stockée uniquement sous forme de hachage) ou un jeton de session. La clé ne figure jamais dans une URL et n'est jamais journalisée.
Mettre en place une application
Créer la clé
Créez une clé RSA ou EC dans un vault avec l'option exportable désactivée et l'opération Sign activée. Pour Authenticode, choisissez un vault qui signe des condensats précalculés (voir Prise en charge des vaults).
Créer l'application
Dans l'assistant d'application, choisissez SignTool, signature JAR, signature de commits Git ou signature de conteneurs, puis sélectionnez la clé.
Ajouter un certificat (Authenticode et JAR)
Générez une demande de certificat signée par la clé du vault (CSR sur la page de l'application), faites émettre un certificat de signature de code par une CA et enregistrez la chaîne (certificat final en premier). La chaîne est refusée si le certificat final ne certifie pas la clé, a expiré ou porte une utilisation étendue sans signature de code. Les signatures Git et de conteneurs ne nécessitent pas de certificat.
Exiger une approbation (optionnel)
Activez approbation requise et choisissez une politique de quorum à double contrôle sur la page de l'application.
Créer une clé machine pour la CI
Créez une clé d'API pour un utilisateur de service qui ne détient que Operations.Apps.CodeSignAccess et Operations.Apps.Read.
Les exigences du CA/Browser Forum pour la signature de code imposent que les clés de signature de code publiquement reconnues soient détenues dans du matériel. Utilisez un vault adossé à un HSM et suivez la procédure d'attestation de clé de votre CA ; Cockpit ne produit pas de déclaration d'attestation pour les clés de signature de code.
Prise en charge des vaults
La signature d'une charge utile (data_b64) fonctionne sur tout vault qui signe. La signature d'un condensat calculé par l'appelant (digest_b64) — exigée par le flux /dg de SignTool — est disponible sur le Software Vault, Azure Key Vault / Managed HSM et Google Cloud KMS ; les autres vaults répondent 400 et demandent la charge utile. Authenticode nécessite donc l'un de ces trois vaults. Les clés EC P-256 signent en SHA-256, les clés EC P-384 en SHA-384.
Recettes
Environnement commun du client DuoKey (en CI, mappez explicitement le secret et ne l'affichez jamais) :
export DKE_URL=https://cockpit.example.com
export DKE_CODESIGN_APP=<application uuid>
export DKE_API_KEY=<machine API key> # secret
export DKE_APPROVAL_WAIT=0 # seconds to wait for approvers; 0 = exit 3 if pending
python3 dkesign.py config --out-dir signing # public key, OpenSSH key and certificate chain
Authenticode avec SignTool
rem 1. digest of the file's to-be-signed content
signtool sign /dg out /fd SHA256 /f signing\leaf.cer app.exe
rem 2. vault signature of that digest
python dkesign.py sign-digest --digest-file out\app.exe.dig --out out\app.exe.dig.signed --artifact app.exe
rem 3. embed it
signtool sign /di out app.exe
rem 4. optional RFC 3161 timestamp from your timestamping authority
signtool timestamp /tr https://tsa.example.com /td SHA256 app.exe
signtool verify /pa /v app.exe
Utilisez le même /fd que l'algorithme de condensat du client. Produisez le certificat final en DER avec openssl x509 -in signing/chain.pem -outform DER -out signing/leaf.cer. Utilisez un certificat de signature avec une durée de validité normale : SignTool ne sélectionne pas les certificats dont la date d'expiration est extrême.
Signature JAR
java DuoKeyJarSigner.java app.jar app-signed.jar SHA256
jarsigner -verify -verbose -certs app-signed.jar
Le moteur de signature JAR du JDK construit la signature ; le nom du signataire dans META-INF est DUOKEY. L'horodatage n'est pas intégré au signataire JAR de référence.
Signature de commits et tags Git
python3 dkesign.py config --out-dir ~/.config/duokey-signing
git config --global gpg.format ssh
git config --global user.signingkey "key::$(cat ~/.config/duokey-signing/signing-key.pub)"
git config --global gpg.ssh.program /usr/local/bin/dkesign-ssh
git config --global commit.gpgsign true # and/or tag.gpgSign true
/usr/local/bin/dkesign-ssh :
#!/bin/sh
exec python3 /opt/duokey/dkesign.py ssh-keygen "$@"
Vérifiez avec un fichier de signataires autorisés (git config gpg.ssh.allowedSignersFile allowed_signers, puis git verify-commit HEAD). Enregistrez la même clé publique comme clé de signature de l'auteur sur votre service d'hébergement Git pour que les commits apparaissent comme vérifiés.
Images de conteneurs avec cosign
cosign generate registry.example.com/app@sha256:<digest> > payload.json
python3 dkesign.py sign-file --in payload.json --out payload.sig \
--artifact registry.example.com/app@sha256:<digest>
cosign attach signature --payload payload.json --signature payload.sig \
registry.example.com/app@sha256:<digest>
cosign verify --key signing/public-key.pem --insecure-ignore-tlog=true \
registry.example.com/app@sha256:<digest>
Signez les images par condensat, pas par tag. --insecure-ignore-tlog=true est nécessaire car la signature n'est pas publiée dans un journal de transparence ; la vérification repose sur la clé publique de l'application.
Pipelines CI
La clé d'API machine est une variable secrète mappée dans l'environnement de l'étape. Le client ne l'envoie que dans l'en-tête Authorization : elle n'apparaît jamais dans une URL, une ligne de commande ou un journal. Lorsque l'approbation est requise, DKE_APPROVAL_WAIT fait renvoyer la demande toutes les 15 secondes jusqu'à la décision des approbateurs (ou la fin de l'attente, code de sortie 3). L'approbation est liée au condensat : l'artefact signé est exactement celui que les approbateurs ont vu.
Les exemples ci-dessous montrent le principe ; adaptez les chemins et les étapes de build à votre pipeline.
Azure Pipelines
# Variable group "duokey-signing": DKE_URL, DKE_CODESIGN_APP (plain),
# DKE_API_KEY (secret, lock icon).
variables:
- group: duokey-signing
jobs:
- job: sign_windows
pool: { vmImage: windows-latest }
steps:
- script: dotnet publish -c Release -o out
displayName: Build
- pwsh: |
python tools/dkesign.py config --out-dir signing
openssl x509 -in signing/chain.pem -outform DER -out signing/leaf.cer
signtool sign /dg dig /fd SHA256 /f signing/leaf.cer out/app.exe
python tools/dkesign.py sign-digest --digest-file dig/app.exe.dig --out dig/app.exe.dig.signed --artifact "app.exe $(Build.BuildNumber)"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
signtool sign /di dig out/app.exe
signtool verify /pa out/app.exe
displayName: Sign with DuoKey
env:
DKE_URL: $(DKE_URL)
DKE_CODESIGN_APP: $(DKE_CODESIGN_APP)
DKE_API_KEY: $(DKE_API_KEY) # secret variables are only visible when mapped
DKE_APPROVAL_WAIT: "1800" # wait up to 30 min for approvers
GitHub Actions
on: { push: { tags: ["v*"] } }
permissions: { contents: read }
jobs:
sign-jar:
runs-on: ubuntu-latest
environment: release # protect the secret with environment reviewers if wanted
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: "21" }
- run: ./gradlew jar
- name: Sign with DuoKey
env:
DKE_URL: ${{ vars.DKE_URL }}
DKE_CODESIGN_APP: ${{ vars.DKE_CODESIGN_APP }}
DKE_API_KEY: ${{ secrets.DKE_API_KEY }}
DKE_APPROVAL_WAIT: "1800"
run: |
java tools/DuoKeyJarSigner.java build/libs/app.jar build/libs/app-signed.jar SHA256
jarsigner -verify build/libs/app-signed.jar
sign-image:
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}" }
- id: build
uses: docker/build-push-action@v6
with: { push: true, tags: "ghcr.io/${{ github.repository }}:${{ github.ref_name }}" }
- name: Sign image
env:
DKE_URL: ${{ vars.DKE_URL }}
DKE_CODESIGN_APP: ${{ vars.DKE_CODESIGN_APP }}
DKE_API_KEY: ${{ secrets.DKE_API_KEY }}
IMAGE: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
run: |
cosign generate "$IMAGE" > payload.json
python3 tools/dkesign.py sign-file --in payload.json --out payload.sig --artifact "$IMAGE"
cosign attach signature --payload payload.json --signature payload.sig "$IMAGE"
API de signature
Toutes les routes sont sous /api et authentifiées avec Authorization: Bearer.
| Route | Rôle |
|---|---|
POST /apps/code-sign, GET /apps/code-sign, GET / PUT / DELETE /apps/code-sign/{id} | Créer, lister, lire, modifier (nom, chaîne de certificats, approbation), supprimer. Affaiblir l'approbation ou supprimer répond 202 jusqu'à approbation |
POST /apps/{id}/code-sign/enable, /disable | Une application désactivée refuse de signer |
GET /apps/{id}/code-sign/config | Clé publique (PEM et OpenSSH), chaîne de certificats, algorithmes de condensat. Rien de secret |
POST /apps/{id}/code-sign/sign | Signer digest_b64 (condensat calculé par l'appelant) ou data_b64 (1 Mio au plus), avec digest_algorithm (sha256, sha384, sha512), rsa_padding (pkcs1 ou pss) et artifact_name. Répond 202 avec un request_id tant que l'approbation est en attente |
POST /apps/{id}/code-sign/verify | Vérifier une signature avec la clé publique de l'application |
POST /apps/{id}/code-sign/csr | Demande PKCS#10 pour la clé, signée dans le vault |
GET /apps/{id}/code-sign/health | Activée, clé non exportable et utilisable, vault joignable, certificat valide, politique d'approbation toujours à double contrôle |
GET /apps/{id}/code-sign/operations | Journal de signature |
Lorsqu'une signature attend une approbation, les approbateurs décident dans Demandes de quorum ; le demandeur renvoie ensuite la même demande et reçoit la signature. Répéter la demande pendant l'attente renvoie le même request_id.
Limites
- Notation, les fournisseurs PKCS#11 / CNG locaux pour la signature de code et les déclarations d'attestation de clé ne sont pas fournis.
- Authenticode nécessite un vault qui signe des condensats précalculés (Software Vault, Azure Key Vault / Managed HSM, Google Cloud KMS).
- Le signataire JAR de référence n'ajoute pas d'horodatage RFC 3161 ; utilisez la commande
timestampde SignTool pour Authenticode. - La signature de modules noyau et de macros Office n'est disponible que par l'API.