Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .azure-pipelines/hidi-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,17 @@ extends:
MaxRetryAttempts: '5'
PendingAnalysisWaitTimeoutMinutes: '5'
- pwsh: |
Copy-Item artifacts\hidi\signing\Microsoft.OpenApi.Hidi.dll src\Microsoft.OpenApi.Hidi\bin\$(buildConfiguration)\net8.0\Microsoft.OpenApi.Hidi.dll
$ErrorActionPreference = 'Stop'
Copy-Item artifacts\hidi\signing\Microsoft.OpenApi.Hidi.exe artifacts\hidi\win-x64\Microsoft.OpenApi.Hidi.exe
New-Item -ItemType Directory -Force '$(Build.ArtifactStagingDirectory)\hidi' | Out-Null
dotnet pack src\Microsoft.OpenApi.Hidi\Microsoft.OpenApi.Hidi.csproj -c $(buildConfiguration) --no-build --include-symbols --include-source /p:SymbolPackageFormat=snupkg -o '$(Build.ArtifactStagingDirectory)\hidi'
dotnet pack src\Microsoft.OpenApi.Hidi\Microsoft.OpenApi.Hidi.csproj -c $(buildConfiguration) --no-build --include-symbols --include-source /p:SymbolPackageFormat=snupkg '/p:HidiSignedAssemblyPath=$(Build.SourcesDirectory)\artifacts\hidi\signing\Microsoft.OpenApi.Hidi.dll' -o '$(Build.ArtifactStagingDirectory)\hidi'
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Compress-Archive -Path artifacts\hidi\win-x64\* -DestinationPath '$(Build.ArtifactStagingDirectory)\hidi\hidi-win-x64-$(HidiVersion).zip'
Copy-Item artifacts\hidi\win-x64\Microsoft.OpenApi.Hidi.exe '$(Build.ArtifactStagingDirectory)\hidi\Microsoft.OpenApi.Hidi.exe'
displayName: Pack signed hidi binaries
- pwsh: |
.\scripts\verify-hidi-package-assembly.ps1 -PackagePath '$(Build.ArtifactStagingDirectory)\hidi\Microsoft.OpenApi.Hidi.$(HidiVersion).nupkg' -SignedAssemblyPath 'artifacts\hidi\signing\Microsoft.OpenApi.Hidi.dll'
displayName: Verify signed Hidi DLL payload before NuGet signing
- task: EsrpCodeSigning@6
displayName: Sign hidi NuGet package
inputs:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ jobs:
- name: Data gatherer
id: data_gatherer
shell: pwsh
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
# Get default branch
$repo = 'microsoft/OpenAPI.NET.OData'
$defaultBranch = Invoke-RestMethod -Method GET -Uri https://api.github.com/repos/$repo | Select-Object -ExpandProperty default_branch
$defaultBranch = Invoke-RestMethod -Method GET -Uri https://api.github.com/repos/$repo -Headers @{ Authorization = "Bearer $env:GITHUB_TOKEN" } | Select-Object -ExpandProperty default_branch
Write-Output "default_branch=$(echo $defaultBranch) >> $GITHUB_OUTPUT"

- name: Conditionals handler
Expand Down
72 changes: 39 additions & 33 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,59 +60,65 @@ jobs:
- name: Install PowerShell test dependency
shell: pwsh
run: Install-Module Pester -RequiredVersion 5.7.1 -Scope CurrentUser -Force
- name: Test Hidi NuGet helper with measured coverage
- name: Test Hidi NuGet helpers with measured coverage
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
Import-Module Pester -RequiredVersion 5.7.1 -ErrorAction Stop
$helperPath = 'scripts/check-nuget-package-published.ps1'
$helper = (Resolve-Path $helperPath).Path
$helperPaths = @('scripts/check-nuget-package-published.ps1', 'scripts/verify-hidi-package-assembly.ps1')
$helpers = @($helperPaths | ForEach-Object { (Resolve-Path $_).Path })
$outputRoot = Join-Path $PWD 'artifacts\hidi\powershell-coverage'
New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null
$configuration = New-PesterConfiguration
$configuration.Run.Path = 'test\Microsoft.OpenApi.Hidi.Tests\check-nuget-package-published.Tests.ps1'
$configuration.Run.Path = @('test\Microsoft.OpenApi.Hidi.Tests\check-nuget-package-published.Tests.ps1', 'test\Microsoft.OpenApi.Hidi.Tests\verify-hidi-package-assembly.Tests.ps1')
$configuration.Run.PassThru = $true
$configuration.CodeCoverage.Enabled = $true
$configuration.CodeCoverage.Path = $helper
$configuration.CodeCoverage.Path = $helpers
$configuration.CodeCoverage.OutputFormat = 'JaCoCo'
$configuration.CodeCoverage.OutputPath = Join-Path $outputRoot 'pester-coverage.xml'
$configuration.CodeCoverage.CoveragePercentTarget = 80
$result = Invoke-Pester -Configuration $configuration
if ($result.FailedCount -gt 0 -or $result.PassedCount -lt 24) {
throw "Hidi NuGet helper tests failed or did not execute all 24 cases."
if ($result.FailedCount -gt 0 -or $result.PassedCount -lt 45) {
throw "Hidi NuGet helper tests failed or did not execute all 45 cases."
}
$report = [xml](Get-Content $configuration.CodeCoverage.OutputPath.Value -Raw)
$sourceFiles = @($report.SelectNodes('//sourcefile'))
if ($sourceFiles.Count -ne 1 -or $sourceFiles[0].name -ne [IO.Path]::GetFileName($helper)) {
throw 'Expected measured coverage of only the Hidi NuGet helper.'
}
$lines = @($sourceFiles[0].SelectNodes('line'))
$sourceLineCount = @(Get-Content $helper).Count
$lineNumbers = @($lines | ForEach-Object { [int]$_.nr })
if ($lines.Count -eq 0 -or @($lineNumbers | Select-Object -Unique).Count -ne $lines.Count -or
@($lines | Where-Object {
[int]$_.nr -le 0 -or [int]$_.nr -gt $sourceLineCount -or
-not $_.HasAttribute('ci') -or -not $_.HasAttribute('mi') -or
[int]$_.ci -lt 0 -or [int]$_.mi -lt 0 -or ([int]$_.ci + [int]$_.mi) -le 0
}).Count -gt 0) {
throw 'Missing or invalid measured helper coverage lines.'
}
$covered = @($lines | Where-Object { [int]$_.ci -gt 0 }).Count
if ($covered / $lines.Count -lt 0.8) {
throw "Insufficient measured Hidi helper coverage: $covered/$($lines.Count) lines."
if ($sourceFiles.Count -ne $helpers.Count) {
throw 'Expected measured coverage of both Hidi NuGet helpers.'
}
$coverage = [xml]'<coverage version="1"/>'
$file = $coverage.CreateElement('file')
$file.SetAttribute('path', $helperPath)
[void]$coverage.DocumentElement.AppendChild($file)
foreach ($line in $lines) {
$entry = $coverage.CreateElement('lineToCover')
$entry.SetAttribute('lineNumber', $line.nr)
$entry.SetAttribute('covered', ([int]$line.ci -gt 0).ToString().ToLowerInvariant())
[void]$file.AppendChild($entry)
foreach ($helperPath in $helperPaths) {
$helper = (Resolve-Path $helperPath).Path
$sourceFile = @($sourceFiles | Where-Object { $_.name -eq [IO.Path]::GetFileName($helper) })
if ($sourceFile.Count -ne 1) { throw "Missing or ambiguous helper coverage: $helperPath" }
$lines = @($sourceFile[0].SelectNodes('line'))
$sourceLineCount = @(Get-Content $helper).Count
$lineNumbers = @($lines | ForEach-Object { [int]$_.nr })
if ($lines.Count -eq 0 -or @($lineNumbers | Select-Object -Unique).Count -ne $lines.Count -or
@($lines | Where-Object {
[int]$_.nr -le 0 -or [int]$_.nr -gt $sourceLineCount -or
-not $_.HasAttribute('ci') -or -not $_.HasAttribute('mi') -or
[int]$_.ci -lt 0 -or [int]$_.mi -lt 0 -or ([int]$_.ci + [int]$_.mi) -le 0
}).Count -gt 0) {
throw "Missing or invalid measured helper coverage lines: $helperPath"
}
$covered = @($lines | Where-Object { [int]$_.ci -gt 0 }).Count
if ($covered / $lines.Count -lt 0.8) {
throw "Insufficient measured Hidi helper coverage: $helperPath $covered/$($lines.Count) lines."
}
$file = $coverage.CreateElement('file')
$file.SetAttribute('path', $helperPath)
[void]$coverage.DocumentElement.AppendChild($file)
foreach ($line in $lines) {
$entry = $coverage.CreateElement('lineToCover')
$entry.SetAttribute('lineNumber', $line.nr)
$entry.SetAttribute('covered', ([int]$line.ci -gt 0).ToString().ToLowerInvariant())
[void]$file.AppendChild($entry)
}
Write-Host "Measured Hidi helper coverage: $helperPath $covered/$($lines.Count) lines."
}
$coverage.Save((Join-Path $outputRoot 'sonar-coverage.xml'))
Write-Host "Measured Hidi NuGet helper coverage: $covered/$($lines.Count) lines; Sonar generic report generated."
Write-Host 'Sonar generic report generated for both Hidi NuGet helpers.'
- name: Build and analyze
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
Expand Down
59 changes: 59 additions & 0 deletions scripts/verify-hidi-package-assembly.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

<#
.SYNOPSIS
Verifies the Hidi tool package contains the exact Microsoft-signed staging DLL.
.DESCRIPTION
Run after tool packing and before NuGet signing. A signed NuGet container does
not prove that its assembly payload retained the ESRP signature.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$PackagePath,
[Parameter(Mandatory = $true)]
[string]$SignedAssemblyPath
)

$ErrorActionPreference = 'Stop'

foreach ($path in @($PackagePath, $SignedAssemblyPath)) {
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
throw "Missing Hidi signing verification input: $path"
}
}

$temporaryDirectory = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid().ToString())
New-Item -ItemType Directory -Path $temporaryDirectory | Out-Null
$packagedAssembly = Join-Path $temporaryDirectory 'Microsoft.OpenApi.Hidi.dll'
$archive = $null
try {
$archive = [IO.Compression.ZipFile]::OpenRead((Resolve-Path -LiteralPath $PackagePath).Path)
$assemblies = @($archive.Entries | Where-Object { $_.Name -ieq 'Microsoft.OpenApi.Hidi.dll' })
if ($assemblies.Count -ne 1 -or $assemblies[0].FullName -cne 'tools/net8.0/any/Microsoft.OpenApi.Hidi.dll') {
throw 'Expected exactly one Hidi DLL at tools/net8.0/any/Microsoft.OpenApi.Hidi.dll.'
}
[IO.Compression.ZipFileExtensions]::ExtractToFile($assemblies[0], $packagedAssembly)

$stagingHash = (Get-FileHash -LiteralPath $SignedAssemblyPath -Algorithm SHA256).Hash
$packageHash = (Get-FileHash -LiteralPath $packagedAssembly -Algorithm SHA256).Hash
if ($packageHash -cne $stagingHash) {
throw "Packaged Hidi DLL differs from the signed staging DLL: $packageHash != $stagingHash"
}
foreach ($assembly in @($SignedAssemblyPath, $packagedAssembly)) {
$signature = Get-AuthenticodeSignature -LiteralPath $assembly
if ($signature.Status -ne 'Valid' -or
$signature.SignerCertificate.Subject -notmatch '(^|,\s*)O=Microsoft Corporation(,|$)') {
throw "Hidi DLL must have a valid Microsoft Corporation Authenticode signature: $assembly ($($signature.Status))"
}
}
Write-Host "Verified packaged Hidi DLL: valid Microsoft Authenticode signature; signed staging SHA256=$stagingHash"
}
finally {
if ($null -ne $archive) { $archive.Dispose() }
if (Test-Path -LiteralPath $packagedAssembly) {
Remove-Item -LiteralPath $packagedAssembly -Force
}
Remove-Item -LiteralPath $temporaryDirectory -Force
}
20 changes: 20 additions & 0 deletions src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,24 @@
<None Include="./readme.md" Pack="true" PackagePath="" />
</ItemGroup>

<Target Name="UseHidiSignedAssemblyForPublish"
AfterTargets="ComputeFilesToPublish"
Condition="'$(PackAsTool)' == 'true' and '$(HidiSignedAssemblyPath)' != ''">
<Error Condition="!Exists('$(HidiSignedAssemblyPath)')"
Text="The signed Hidi assembly does not exist: $(HidiSignedAssemblyPath)" />
<ItemGroup>
<_HidiAssemblyToReplace Include="@(ResolvedFileToPublish)"
Condition="'%(ResolvedFileToPublish.RelativePath)' == '$(TargetFileName)'" />
</ItemGroup>
<Error Condition="'@(_HidiAssemblyToReplace->Count())' != '1'"
Text="Expected exactly one Hidi assembly in ResolvedFileToPublish." />
<ItemGroup>
<!-- Tool packing publishes the intermediate assembly, not the bin copy. -->
<ResolvedFileToPublish Remove="@(_HidiAssemblyToReplace)" />
<ResolvedFileToPublish Include="$(HidiSignedAssemblyPath)"
RelativePath="$(TargetFileName)"
CopyToPublishDirectory="Always" />
</ItemGroup>
</Target>

</Project>
15 changes: 15 additions & 0 deletions src/Microsoft.OpenApi.Hidi/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ the signed Hidi `.nupkg`, a matching `.snupkg` and the private-feed version-chec
script in the `Hidi` artifact. The `nuget-org` release job consumes that artifact
without a repository checkout or the Docker context.

Tool packing republishes the SDK's intermediate assembly, so replacing the `bin`
DLL does not preserve signing. The official no-build pack supplies
`HidiSignedAssemblyPath` to replace only the Hidi `ResolvedFileToPublish` item
with the ESRP-signed staging DLL. Missing or ambiguous inputs stop packing.
Before NuGet signing, `scripts/verify-hidi-package-assembly.ps1` requires exactly
one Hidi DLL at the expected tool path, byte-for-byte SHA256 equality with staging,
and valid Microsoft Corporation Authenticode signatures on both DLLs.
A signed NuGet container alone is not proof of a signed assembly payload.
Local packing without this property retains the normal SDK behavior.

Before ESRP publication, the job requires the exact `hidi-v3.*` release package
and symbols with a version newer than 3.10.2. It checks
`GraphDeveloperExperiences_Public` using `System.AccessToken` through
Expand All @@ -94,6 +104,11 @@ execute the cases. The test resides physically inside the Hidi test project so
SonarScanner for .NET classifies it as test code, rather than root source code.
The Sonar workflow also measures helper line coverage and
imports a generic coverage report alongside the existing C# OpenCover reports.
The payload verifier has 21 isolated Pester cases in
`test\Microsoft.OpenApi.Hidi.Tests\verify-hidi-package-assembly.Tests.ps1`,
included in that measured coverage. These tests mock signature verification;
they do not establish real ESRP signing. Final readiness requires a
publish-disabled official build and verification of the downloaded DLL payload.

### Windows executable

Expand Down
Loading
Loading