メインコンテンツまでスキップ
適用対象:
DuoKey Cockpit v2SignTool · jarsigner · Git · cosignRemote signing API

Overview​

A code signing application binds one non-exportable vault key, optionally a code signing certificate chain, and optionally an approval policy. Build machines and developers send a digest or a small payload to the signing API; the vault signs it; the signature goes back to the standard tool, which embeds it. The key never leaves the vault, every signature can require M-of-N approval, and every signature is written to the application's journal.

What is supported​

UseToolHow it signs
Authenticode (EXE, DLL, MSI, CAB, …)Microsoft SignToolsigntool sign /dg writes the digest, the DuoKey client has the vault sign it, signtool sign /di embeds the signature
Java archives (JAR)JDK JAR signing engine, through the DuoKey JAR signer; verify with jarsigner -verifyThe JDK builds the JAR signature block; its signature operation is performed by DuoKey
Git commits and tagsGit SSH signing (gpg.format=ssh) with the DuoKey client as gpg.ssh.programOpenSSH signature made with the application key
Container imagescosign generate / cosign attach signature / cosign verify --keyThe signature payload is signed through the DuoKey client
Any detached signatureAny tool that can sign a digest or a small file over HTTPSPOST /api/apps/{id}/code-sign/sign
Kernel modules, Office VBA macros—Application types exist and use the same signing API; no tool integration is provided

Not supported: Notation (Notary Project) signing, and local PKCS#11 or Windows CNG providers for code signing. Tools integrate through the remote signing API and the DuoKey reference clients: dkesign.py (Python 3.8+, standard library only) and DuoKeyJarSigner.java (JDK 17+, no dependency), available from DuoKey support.

Security model​

  • The key stays in the vault. The bound key must be RSA 2048/4096 or EC P-256/P-384, have the sign capability and have been created non-exportable. Exportability is one-way (a non-exportable key can never be made exportable), and every key-export path refuses it.
  • No way around the application. The generic key-signing API refuses a key bound to a code signing application, and one key backs at most one application per tenant, so a second application without approval cannot be pointed at the same key.
  • Approval. An application can require every signature to be approved under a quorum policy that enforces dual control (threshold of 2 or more, or the requester barred from approving). The approval is bound to the application, the digest, the algorithm and the artefact name, usable once and by the requester only. Switching approval off, moving it to another policy or deleting the application itself needs an approval.
  • Journal. Each signature is journaled before the vault is called and completed after, so no signature leaves without its record. Signatures, application changes and certificate requests are also written to the activity log.
  • Permissions. Signing and verification need Operations.Apps.CodeSignAccess; reading the configuration and journal needs Operations.Apps.Read. Lookups are tenant- and organizational-unit-scoped. Crypto policies apply to every signature.
  • Credentials. Clients authenticate with Authorization: Bearer <token>: a machine API key (stored only as a hash) or a session token. The key is never part of a URL and never logged.

Set up an application​

1

Create the key

Create an RSA or EC key in a vault with exportable off and the Sign operation enabled. For Authenticode, choose a vault that signs precomputed digests (see Vault support).

2

Create the application

In the application wizard choose SignTool, JAR signing, Git commit signing or container signing, and select the key.

3

Add a certificate (Authenticode and JAR)

Generate a certificate request signed by the vault key (CSR on the application page), have a CA issue a code signing certificate, and store the chain (leaf first). The chain is refused if the leaf does not certify the key, has expired, or has an extended key usage without code signing. Git and container signatures do not need a certificate.

4

Require approval (optional)

Set requires approval and a dual-control quorum policy on the application page.

5

Create a machine key for CI

Create an API key for a service user that holds only Operations.Apps.CodeSignAccess and Operations.Apps.Read.

Publicly trusted code signing certificates

The CA/Browser Forum code signing requirements require publicly trusted code signing keys to be held in hardware. Use an HSM-backed vault and follow your CA's key-attestation process; Cockpit does not produce key attestation statements for code signing keys.

Vault support​

Signing a payload (data_b64) works on every vault that signs. Signing a digest computed by the caller (digest_b64) — which SignTool's /dg flow requires — is available on the Software Vault, Azure Key Vault / Managed HSM and Google Cloud KMS; other vaults answer 400 and ask for the payload instead. Authenticode therefore needs one of these three vaults. EC P-256 keys sign with SHA-256, EC P-384 keys with SHA-384.

Recipes​

Common environment for the DuoKey client (in CI, map the secret explicitly and never echo it):

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 with 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

Use the same /fd as the client's digest algorithm. Produce the leaf in DER with openssl x509 -in signing/chain.pem -outform DER -out signing/leaf.cer. Use a signing certificate with a normal validity period: SignTool does not select certificates with an extreme expiry date.

JAR signing​

java DuoKeyJarSigner.java app.jar app-signed.jar SHA256
jarsigner -verify -verbose -certs app-signed.jar

The JDK's own JAR signing engine builds the signature; the signer name in META-INF is DUOKEY. Timestamping is not wired in the reference JAR signer.

Git commit and tag signing​

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 "$@"

Verify with an allowed-signers file (git config gpg.ssh.allowedSignersFile allowed_signers, then git verify-commit HEAD). Register the same public key as a signing key of the committer on your Git hosting service to have commits shown as verified.

Container images with 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>

Sign images by digest, not by tag. --insecure-ignore-tlog=true is needed because the signature is not uploaded to a transparency log; verification relies on the application's public key.

CI pipelines​

The machine API key is a secret variable mapped into the step's environment. The client sends it in the Authorization header only: it never appears in a URL, a command line or a log. With approval required, DKE_APPROVAL_WAIT makes the client re-send the request every 15 seconds until the approvers decide (or the wait ends, exit status 3). The approval is bound to the digest, so the artefact signed is exactly the one the approvers saw.

The examples below show the pattern; adapt paths and build steps to your 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"

Signing API​

All routes are under /api and authenticated with Authorization: Bearer.

RoutePurpose
POST /apps/code-sign, GET /apps/code-sign, GET / PUT / DELETE /apps/code-sign/{id}Create, list, read, update (name, certificate chain, approval), delete. Weakening approval or deleting answers 202 until approved
POST /apps/{id}/code-sign/enable, /disableA disabled application refuses to sign
GET /apps/{id}/code-sign/configPublic key (PEM and OpenSSH), certificate chain, digest algorithms. Nothing secret
POST /apps/{id}/code-sign/signSign digest_b64 (caller-hashed) or data_b64 (at most 1 MiB), with digest_algorithm (sha256, sha384, sha512), rsa_padding (pkcs1 or pss) and artifact_name. Answers 202 with a request_id while approval is pending
POST /apps/{id}/code-sign/verifyVerify a signature with the application's public key
POST /apps/{id}/code-sign/csrPKCS#10 request for the key, signed in the vault
GET /apps/{id}/code-sign/healthEnabled, key non-exportable and usable, vault reachable, certificate valid, approval policy still dual control
GET /apps/{id}/code-sign/operationsSigning journal

When a signature is pending approval, approvers decide under Quorum requests; the requester then sends the same request again and receives the signature. Repeating the request while it is pending returns the same request_id.

Limits​

  • Notation, local PKCS#11 / CNG providers for code signing, and key attestation statements are not provided.
  • Authenticode needs a vault that signs precomputed digests (Software Vault, Azure Key Vault / Managed HSM, Google Cloud KMS).
  • The reference JAR signer does not add an RFC 3161 timestamp; use SignTool's timestamp command for Authenticode.
  • Kernel-module and Office-macro signing are available through the API only.