Files
UA-.NETStandard/samples/Reference/ConsoleReferenceClient/generate_user_certificate.ps1
Copilot d525bc3f28 Bound reconnect cleanup and organize samples by domain (#4276)
# Description

This PR fixes bounded ManagedSession reconnect cleanup and reorganizes
the sample applications into domain-focused folders.

## Reconnect cleanup

- Bounds reconnect integration cleanup with independent 30-second close
and dispose limits.
- Sends `CloseSession` through the awaited transport request path during
`Session.DisposeAsync`.
- Preserves explicit `Session.CloseAsync` behavior that waits for or
cancels outstanding publish requests.
- Covers the disposal request path with a regression test.

## Sample organization

- `samples/OpenUsd`: `GeneratorServer`, `SiteCompositionServer`
- `samples/Redundancy`: `RedundantClient`, `RedundantServer`,
`RedundantPubSub`
- `samples/Isa95`: `MinimalIsa95Server`
- `samples/MinimalApi`: `MinimalBoilerServer`, `MinimalCalcServer`,
`MinimalClient`
- `samples/DI`: `PumpDeviceIntegrationServer`
- `samples/Lds`: `ConsoleLdsServer`
- `samples/Reference`: `ConsoleReferenceClient`,
`ConsoleReferenceServer`
- `samples/PubSub`: `ConsoleReferencePubSubClient`

`UA.slnx` now exposes the same requested domain groups. Project
references, tests, runtime sample probes, CI workflows, Dockerfiles,
Compose files, helper scripts, and documentation were updated to the new
paths. `samples/Robotics/MinimalRobotServer` remains under `Robotics`.

## Validation

- `dotnet build UA.slnx`
- 3 Redundancy short-haul sample tests
- 70 DI sample tests
- 914 OpenUSD tests
- 1 WotCon model-materialization test
- 137 AOT tests
- `docker compose config` for all 6 moved Redundancy Compose files

## Related Issues

- Fixes #4273

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce new build or
analyzer warnings.
- [x] I built the complete `UA.slnx` solution locally.
- [ ] I ran all tests locally against at least .NET Framework and .NET
10.
- [ ] I fixed all failing and flaky tests in the CI pipelines and all
CodeQL warnings.
- [x] I have addressed all PR feedback received.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
Co-authored-by: Marc Schier <mschier@microsoft.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
Copilot-Session: a2c2838f-74ae-4997-a1d4-50574d9e0636
2026-08-21 04:35:47 +00:00

160 lines
6.0 KiB
PowerShell

# 1. Ensure directories exist
$certDir = "./bin/pki/trustedUser/certs"
$privateDir = "./bin/pki/trustedUser/private"
$curves = @(
'nistP256',
'nistP384',
'brainpoolP256r1',
'brainpoolP384r1'
)
foreach ($d in @($certDir, $privateDir)) {
if (-not (Test-Path $d)) {
New-Item -ItemType Directory -Path $d -Force | Out-Null
}
}
function New-CertificateWithAKI {
param(
[System.Security.Cryptography.X509Certificates.X509Certificate2] $SourceCert,
[string] $HashAlgorithmName = 'SHA256'
)
$hashAlg = [System.Security.Cryptography.HashAlgorithmName]::new($HashAlgorithmName)
# Determine key type and create CertificateRequest
$ecdsa = [System.Security.Cryptography.X509Certificates.ECDsaCertificateExtensions]::GetECDsaPrivateKey($SourceCert)
$rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($SourceCert)
if ($ecdsa) {
$req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new(
$SourceCert.SubjectName, $ecdsa, $hashAlg)
}
elseif ($rsa) {
$req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new(
$SourceCert.SubjectName, $rsa, $hashAlg,
[System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
}
else {
throw "Unsupported key type"
}
# Copy all extensions from source cert
foreach ($ext in $SourceCert.Extensions) {
$req.CertificateExtensions.Add($ext)
}
# Build Authority Key Identifier from Subject Key Identifier
$skiExt = $SourceCert.Extensions | Where-Object {
$_ -is [System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension]
}
if ($skiExt) {
$hex = $skiExt.SubjectKeyIdentifier
$keyIdBytes = [byte[]]::new($hex.Length / 2)
for ($i = 0; $i -lt $keyIdBytes.Length; $i++) {
$keyIdBytes[$i] = [System.Convert]::ToByte($hex.Substring($i * 2, 2), 16)
}
# AKI ASN.1: SEQUENCE { [0] IMPLICIT keyIdentifier }
# 30 <seqLen> 80 <keyIdLen> <keyIdBytes>
[byte[]] $akiInner = @(0x80, $keyIdBytes.Length) + $keyIdBytes
[byte[]] $akiValue = @(0x30, $akiInner.Length) + $akiInner
$akiOid = [System.Security.Cryptography.Oid]::new('2.5.29.35', 'Authority Key Identifier')
$akiExt = [System.Security.Cryptography.X509Certificates.X509Extension]::new(
$akiOid, $akiValue, $false)
$req.CertificateExtensions.Add($akiExt)
}
$notBefore = [DateTimeOffset]::UtcNow.AddMonths(-1)
$notAfter = [DateTimeOffset]::UtcNow.AddYears(1)
return $req.CreateSelfSigned($notBefore, $notAfter)
}
# 2. Create a self-signed ECC certificate (NIST P-256)
# $cert = New-SelfSignedCertificate `
# -Subject "CN=iama.tester@example.com" `
# -CertStoreLocation "Cert:\CurrentUser\My" `
# -KeyExportPolicy Exportable `
# -KeySpec Signature `
# -KeyAlgorithm ECDSA_nistP256 `
# -Curve 'CurveName' `
# -HashAlgorithm SHA256 `
# -NotAfter (Get-Date).AddYears(1)
foreach ($curve in $curves) {
Write-Host "Generating certificate for curve: $curve"
$signatureAlgorithm = if ($curve -match 'P384') { 'SHA384' } else { 'SHA256' }
# OIDs for the extensions
# 2.5.29.14 = Subject Key Identifier
# 2.5.29.35 = Authority Key Identifier
# 2.5.29.19 = Basic Constraints (Subject Type=End Entity or CA)
$name = 'CN=iama.tester@example.com,O=' + $curve;
$params = @{
Type = 'Custom'
Subject = $name
TextExtension = @(
'2.5.29.37={text}1.3.6.1.5.5.7.3.2', # Enhanced Key Usage (Client Auth)
'2.5.29.17={text}upn=iama.tester@example.com', # SAN
'2.5.29.19={text}ca=0' # Basic Constraints: Not a CA
)
KeyUsage = @('DigitalSignature', 'NonRepudiation') # Added KeyCertSign for self-signed logic
KeyAlgorithm = "ECDSA_$curve"
CurveExport = 'CurveName'
HashAlgorithm = $signatureAlgorithm
CertStoreLocation = 'Cert:\CurrentUser\My'
NotBefore = (Get-Date).AddMonths(-1)
NotAfter = (Get-Date).AddYears(1)
}
# 1. Create cert and rebuild with Authority Key Identifier
$tempCert = New-SelfSignedCertificate @params
$cert = New-CertificateWithAKI -SourceCert $tempCert -HashAlgorithmName $signatureAlgorithm
Remove-Item "Cert:\CurrentUser\My\$($tempCert.Thumbprint)" -Force
Write-Host "Certificate generated with Thumbprint: $($cert.Thumbprint)"
# 2. Export DER
$derPath = Join-Path $certDir "iama.tester.$curve.der"
[System.IO.File]::WriteAllBytes($derPath, $cert.RawData)
# 3. Export PFX with password
$pfxPath = Join-Path $privateDir "iama.tester.$curve.pfx"
$pfxBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, 'password')
[System.IO.File]::WriteAllBytes($pfxPath, $pfxBytes)
Write-Host "Finished: $curve`n"
}
Write-Host "`n=== Generating RSA-2048 ==="
$rsaParams = @{
Type = 'Custom'
Subject = 'CN=iama.tester@example.com'
TextExtension = @(
'2.5.29.37={text}1.3.6.1.5.5.7.3.2'
'2.5.29.17={text}upn=iama.tester@example.com'
)
KeyUsage = @('DigitalSignature','DataEncipherment','NonRepudiation','KeyEncipherment', 'CertSign')
KeyAlgorithm = 'RSA'
KeyLength = 2048
CertStoreLocation = 'Cert:\CurrentUser\My'
NotBefore = (Get-Date).AddMonths(-1)
NotAfter = (Get-Date).AddYears(1)
}
$tempRsaCert = New-SelfSignedCertificate @rsaParams
$rsaCert = New-CertificateWithAKI -SourceCert $tempRsaCert -HashAlgorithmName 'SHA256'
Remove-Item "Cert:\CurrentUser\My\$($tempRsaCert.Thumbprint)" -Force
$derPath = Join-Path $certDir "iama.tester.rsa.der"
[System.IO.File]::WriteAllBytes($derPath, $rsaCert.RawData)
$pfxPath = Join-Path $privateDir "iama.tester.rsa.pfx"
$pfxBytes = $rsaCert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, 'password')
[System.IO.File]::WriteAllBytes($pfxPath, $pfxBytes)