Skip to content

Commit e24ccbb

Browse files
committed
base
1 parent 79fc4b6 commit e24ccbb

11 files changed

Lines changed: 1056 additions & 1 deletion

File tree

.gitignore

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Local .terraform directories
22
.terraform/
3+
*src/.env
4+
app-logs.zip
5+
LogFiles
6+
deploy.log
7+
__pycache__
8+
*.log
9+
*agents_state.json
10+
.env.example
11+
.env_automation
312

413
# .tfstate files
514
*.tfstate
@@ -13,7 +22,6 @@ crash.*.log
1322
# password, private keys, and other secrets. These should not be part of version
1423
# control as they are data points which are potentially sensitive and subject
1524
# to change depending on the environment.
16-
*.tfvars
1725
*.tfvars.json
1826

1927
# Ignore override files as they are usually used to override resources locally and so
@@ -35,3 +43,11 @@ override.tf.json
3543
# Ignore CLI configuration files
3644
.terraformrc
3745
terraform.rc
46+
47+
# .NET build output
48+
**/bin/
49+
**/obj/
50+
51+
# Visual Studio / VS Code
52+
.vs/
53+
.vscode/

README.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Azure Artifact Signing (Azure DevOps + Terraform)
2+
3+
This repo is a minimal, demo-friendly setup for:
4+
- Provisioning Azure Artifact Signing (Trusted Signing) resources with Terraform.
5+
- Building a small Windows .NET executable.
6+
- Signing it in Azure DevOps using **SignTool + Artifact Signing dlib** (private key stays in Microsoft-managed HSMs).
7+
8+
## What Terraform creates
9+
10+
- Resource group
11+
- Artifact Signing account (`Microsoft.CodeSigning/codeSigningAccounts`)
12+
- (Optional, Terraform-deployed) Certificate profile (`.../certificateProfiles`) once you provide an **Identity validation Id**
13+
- (Optional, Terraform-deployed) Microsoft Entra app registration + service principal for an Azure DevOps **Workload Identity Federation** service connection
14+
- (Optional, Terraform-deployed) Azure DevOps resources (when `ado_enabled = true`):
15+
- Project
16+
- Git repo
17+
- YAML pipeline
18+
- AzureRM service connection (Workload Identity Federation)
19+
- Pipeline authorizations for the service connection + variable group
20+
- (Optional, Terraform-deployed) RBAC assignment: `Artifact Signing Certificate Profile Signer` at the certificate profile scope
21+
22+
Notes:
23+
- **Identity validation** itself is **portal-only** (service requirement). Terraform can’t complete that workflow; you paste the resulting `identity_validation_id` into `terraform.tfvars`.
24+
- If Terraform creates the Azure DevOps service connection (`ado_enabled = true`), it can also read the generated WIF **Issuer** and **Subject** and create the Entra **federated credential** automatically (no copy/paste).
25+
26+
## Prereqs
27+
28+
- Azure CLI installed and logged in (`az login`)
29+
- Terraform installed
30+
- Permission to register resource providers + create resources in your subscription
31+
32+
One-time provider registration (per subscription):
33+
34+
```pwsh
35+
az provider register --namespace Microsoft.CodeSigning
36+
```
37+
38+
## Deploy with Terraform
39+
40+
Terraform files live in `terraform-infrastructure/`.
41+
42+
1) Edit `terraform-infrastructure/terraform.tfvars` and set a globally-unique account name.
43+
44+
2) Run Terraform:
45+
46+
```pwsh
47+
cd terraform-infrastructure
48+
terraform init
49+
terraform validate
50+
terraform apply -auto-approve
51+
```
52+
53+
3) In Azure portal, open the Artifact Signing account and complete **Identity validation** (portal-only).
54+
55+
4) Copy the **Identity validation Id** from the portal and add it to `terraform-infrastructure/terraform.tfvars`:
56+
57+
```hcl
58+
identity_validation_id = "00000000-0000-0000-0000-000000000000"
59+
certificate_profile_name = "demo-code-signing"
60+
certificate_profile_type = "PublicTrustTest"
61+
```
62+
63+
5) Apply again to create the certificate profile:
64+
65+
```pwsh
66+
cd terraform-infrastructure
67+
terraform validate
68+
terraform apply -auto-approve
69+
```
70+
71+
## Azure DevOps (Terraform-managed)
72+
73+
This repo can also create the Azure DevOps project/repo/pipeline/service-connection using the Terraform `azuredevops` provider.
74+
75+
1) Create a PAT in Azure DevOps with permissions to manage projects/repos/pipelines/service connections.
76+
77+
2) Set env vars (PowerShell):
78+
79+
```pwsh
80+
$env:AZDO_ORG_SERVICE_URL = "https://dev.azure.com/<your-org>"
81+
$env:AZDO_PERSONAL_ACCESS_TOKEN = "<your-pat>"
82+
```
83+
84+
3) In `terraform-infrastructure/terraform.tfvars`, set:
85+
86+
```hcl
87+
ado_enabled = true
88+
ado_org_service_url = "https://dev.azure.com/<your-org>"
89+
```
90+
91+
4) Apply. Terraform will:
92+
- create the Azure DevOps project + repo + YAML pipeline
93+
- create the AzureRM service connection using Workload Identity Federation (WIF)
94+
- read the generated WIF Issuer/Subject and create the Entra federated credential
95+
96+
Notes:
97+
- The `WorkloadIdentityFederation` auth scheme requires your org feature to be enabled. If your org can’t use it yet, set `ado_service_endpoint_authentication_scheme = "ServicePrincipal"` and also set `TF_VAR_ado_service_principal_client_secret` for the service principal secret.
98+
- Terraform creates an empty repo. You still need to push this repo’s code into the Azure DevOps repo (Terraform will output the clone URL).
99+
100+
## Pipeline
101+
102+
- [azure-pipelines.yml](azure-pipelines.yml) builds `SigningDemo.exe`, installs the required signing components via NuGet extraction, then signs using the official SignTool + `/dlib` flow.
103+
- Configure pipeline variables:
104+
- `artifactSigningEndpoint` (Terraform output `artifact_signing_endpoint`)
105+
- `artifactSigningAccountName` (Terraform output `artifact_signing_account_name`)
106+
- `artifactSigningCertificateProfileName` (your profile name)
107+
- Service connection name in YAML: update `azureSubscription` if you didn't name it `sc-artifact-signing`.
108+
109+
### Optional: use Azure Key Vault for pipeline variables
110+
111+
If you enable Key Vault (`keyvault_enabled=true`) Terraform will:
112+
- create an RBAC-enabled Key Vault
113+
- grant your current identity **Key Vault Secrets Officer** (so you can set secrets)
114+
- grant the Azure DevOps service principal **Key Vault Secrets User** (so the pipeline can read secrets)
115+
116+
Then create secrets in Key Vault with these names:
117+
- `artifactSigningEndpoint`
118+
- `artifactSigningAccountName`
119+
- `artifactSigningCertificateProfileName`
120+
121+
The pipeline will automatically load them (it runs `AzureKeyVault@2` when `keyVaultName` is non-empty).
122+
123+
If signing fails with 403, validate:
124+
- Endpoint matches region
125+
- Service connection identity has `Artifact Signing Certificate Profile Signer` at the certificate profile scope
126+
127+
## Azure Portal link
128+
129+
After `terraform apply`, open the resource group:
130+
https://portal.azure.com/#view/HubsExtension/BrowseResourceGroups

SigningDemo/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System;
2+
3+
Console.WriteLine("Hello from SigningDemo at " + DateTimeOffset.UtcNow.ToString("u"));

SigningDemo/SigningDemo.csproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
</Project>

azure-pipelines.yml

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
trigger:
2+
- main
3+
4+
pool:
5+
vmImage: 'windows-latest'
6+
7+
variables:
8+
buildConfiguration: 'Release'
9+
runtimeIdentifier: 'win-x64'
10+
11+
# Update if your service connection name differs.
12+
azureServiceConnection: 'sc-artifact-signing'
13+
14+
# Set these as pipeline variables or in a variable group.
15+
# If you set keyVaultName (or Terraform provides it via variable group),
16+
# the AzureKeyVault@2 task below will populate the artifactSigning* variables.
17+
keyVaultName: ''
18+
artifactSigningEndpoint: ''
19+
artifactSigningAccountName: ''
20+
artifactSigningCertificateProfileName: ''
21+
22+
steps:
23+
- task: DotNetCoreCLI@2
24+
displayName: Publish (unsigned)
25+
inputs:
26+
command: publish
27+
projects: '**/SigningDemo.csproj'
28+
arguments: '-c $(buildConfiguration) -r $(runtimeIdentifier) --self-contained false'
29+
30+
- task: AzureKeyVault@2
31+
displayName: Load signing variables from Key Vault
32+
condition: and(succeeded(), ne(variables['keyVaultName'], ''))
33+
inputs:
34+
azureSubscription: '$(azureServiceConnection)'
35+
KeyVaultName: '$(keyVaultName)'
36+
SecretsFilter: 'artifactSigningEndpoint,artifactSigningAccountName,artifactSigningCertificateProfileName'
37+
RunAsPreJob: false
38+
39+
- task: AzureCLI@2
40+
displayName: Sign with Azure Artifact Signing (SignTool + dlib)
41+
inputs:
42+
azureSubscription: '$(azureServiceConnection)'
43+
scriptType: ps
44+
scriptLocation: inlineScript
45+
inlineScript: |
46+
$ErrorActionPreference = 'Stop'
47+
48+
if ([string]::IsNullOrWhiteSpace("$(artifactSigningEndpoint)")) { throw "Missing variable: artifactSigningEndpoint" }
49+
if ([string]::IsNullOrWhiteSpace("$(artifactSigningAccountName)")) { throw "Missing variable: artifactSigningAccountName" }
50+
if ([string]::IsNullOrWhiteSpace("$(artifactSigningCertificateProfileName)")) { throw "Missing variable: artifactSigningCertificateProfileName" }
51+
52+
az account show --output table
53+
54+
$tempRoot = Join-Path "$(Agent.TempDirectory)" "artifact-signing"
55+
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
56+
57+
$nugetExe = Join-Path $tempRoot "nuget.exe"
58+
Invoke-WebRequest -Uri https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -OutFile $nugetExe
59+
60+
$sdkOut = Join-Path $tempRoot "sdk"
61+
& $nugetExe install Microsoft.Windows.SDK.BuildTools -x -OutputDirectory $sdkOut | Out-Host
62+
63+
$signtool = Get-ChildItem -Path $sdkOut -Recurse -Filter signtool.exe | Where-Object { $_.FullName -match "\\x64\\signtool\\.exe$" } | Select-Object -First 1
64+
if (-not $signtool) {
65+
$signtool = Get-ChildItem -Path $sdkOut -Recurse -Filter signtool.exe | Where-Object { $_.FullName -match "\\x64\\signtool\\.exe" } | Select-Object -First 1
66+
}
67+
if (-not $signtool) { throw "signtool.exe not found after extracting Microsoft.Windows.SDK.BuildTools" }
68+
69+
$dlibOut = Join-Path $tempRoot "dlib"
70+
& $nugetExe install Microsoft.ArtifactSigning.Client -x -OutputDirectory $dlibOut | Out-Host
71+
72+
$dlib = Get-ChildItem -Path $dlibOut -Recurse -Filter Azure.CodeSigning.Dlib.dll | Where-Object { $_.FullName -match "\\x64\\Azure\\.CodeSigning\\.Dlib\\.dll$" } | Select-Object -First 1
73+
if (-not $dlib) {
74+
$dlib = Get-ChildItem -Path $dlibOut -Recurse -Filter Azure.CodeSigning.Dlib.dll | Select-Object -First 1
75+
}
76+
if (-not $dlib) { throw "Azure.CodeSigning.Dlib.dll not found after extracting Microsoft.ArtifactSigning.Client" }
77+
78+
$metadataPath = Join-Path $tempRoot "metadata.json"
79+
$metadata = @{
80+
Endpoint = "$(artifactSigningEndpoint)"
81+
CodeSigningAccountName = "$(artifactSigningAccountName)"
82+
CertificateProfileName = "$(artifactSigningCertificateProfileName)"
83+
CorrelationId = "build-$(Build.BuildId)"
84+
} | ConvertTo-Json -Depth 4
85+
86+
$metadata | Out-File -FilePath $metadataPath -Encoding utf8
87+
88+
$unsigned = "$(Build.SourcesDirectory)\SigningDemo\bin\$(buildConfiguration)\net8.0\$(runtimeIdentifier)\publish\SigningDemo.exe"
89+
if (-not (Test-Path $unsigned)) { throw "Unsigned binary not found at $unsigned" }
90+
91+
$signedDir = "$(Build.ArtifactStagingDirectory)"
92+
New-Item -ItemType Directory -Force -Path $signedDir | Out-Null
93+
$signed = Join-Path $signedDir "SigningDemo.signed.exe"
94+
Copy-Item $unsigned $signed -Force
95+
96+
Write-Host "Using signtool: $($signtool.FullName)"
97+
Write-Host "Using dlib: $($dlib.FullName)"
98+
99+
& $signtool.FullName sign /v /debug /fd SHA256 /tr "http://timestamp.acs.microsoft.com" /td SHA256 /dlib "$($dlib.FullName)" /dmdf "$metadataPath" "$signed"
100+
101+
- powershell: |
102+
$ErrorActionPreference = 'Stop'
103+
$signed = "$(Build.ArtifactStagingDirectory)\SigningDemo.signed.exe"
104+
Get-AuthenticodeSignature $signed | Format-List
105+
displayName: Verify signature
106+
107+
- task: PublishBuildArtifacts@1
108+
displayName: Publish signed artifact
109+
inputs:
110+
pathToPublish: '$(Build.ArtifactStagingDirectory)'
111+
artifactName: 'signed-drop'

terraform-infrastructure/.terraform.lock.hcl

Lines changed: 102 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)