Skip to content

Commit 7b53c2a

Browse files
use X509 with test
1 parent 5d7059a commit 7b53c2a

1 file changed

Lines changed: 252 additions & 47 deletions

File tree

.github/workflows/windows-cert-store-test.yml

Lines changed: 252 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ jobs:
172172
- server_key_source: file
173173
client_key_source: file
174174
test_name: "Server-File-Client-File"
175+
- server_key_source: file
176+
client_key_source: x509
177+
test_name: "Server-File-Client-X509"
178+
- server_key_source: store
179+
client_key_source: x509
180+
test_name: "Server-Store-Client-X509"
175181

176182
steps:
177183
- uses: actions/checkout@v4
@@ -203,32 +209,121 @@ jobs:
203209
# using certutil or other tools that can handle private key import
204210
205211
# Create server certificate in cert store with exportable key
206-
$serverCert = New-SelfSignedCertificate `
207-
-Subject "CN=wolfSSH-Test-Server" `
208-
-KeyAlgorithm RSA `
209-
-KeyLength 2048 `
210-
-CertStoreLocation "Cert:\CurrentUser\My" `
211-
-KeyExportPolicy Exportable `
212-
-NotAfter (Get-Date).AddYears(1) `
213-
-KeyUsage DigitalSignature, KeyEncipherment
214-
215-
Write-Host "Server cert created: $($serverCert.Subject)"
212+
# For server: use LocalMachine so service (LocalSystem) can access it
213+
# For client: use CurrentUser (accessed by testuser)
214+
if ("${{ matrix.server_key_source }}" -eq "store") {
215+
$serverCert = New-SelfSignedCertificate `
216+
-Subject "CN=wolfSSH-Test-Server" `
217+
-KeyAlgorithm RSA `
218+
-KeyLength 2048 `
219+
-CertStoreLocation "Cert:\LocalMachine\My" `
220+
-KeyExportPolicy Exportable `
221+
-NotAfter (Get-Date).AddYears(1) `
222+
-KeyUsage DigitalSignature, KeyEncipherment
223+
Write-Host "Server cert created in LocalMachine: $($serverCert.Subject)"
224+
} else {
225+
# Still create it for consistency, but in CurrentUser (not used for server)
226+
$serverCert = New-SelfSignedCertificate `
227+
-Subject "CN=wolfSSH-Test-Server" `
228+
-KeyAlgorithm RSA `
229+
-KeyLength 2048 `
230+
-CertStoreLocation "Cert:\CurrentUser\My" `
231+
-KeyExportPolicy Exportable `
232+
-NotAfter (Get-Date).AddYears(1) `
233+
-KeyUsage DigitalSignature, KeyEncipherment
234+
Write-Host "Server cert created in CurrentUser: $($serverCert.Subject)"
235+
}
216236
Write-Host "Server cert thumbprint: $($serverCert.Thumbprint)"
217237
Add-Content -Path $env:GITHUB_ENV -Value "SERVER_CERT_SUBJECT=$($serverCert.Subject)"
238+
Add-Content -Path $env:GITHUB_ENV -Value "SERVER_CERT_STORE=${{ matrix.server_key_source }}"
218239
219-
# Create client certificate in cert store
220-
$clientCert = New-SelfSignedCertificate `
221-
-Subject "CN=wolfSSH-Test-Client" `
222-
-KeyAlgorithm RSA `
223-
-KeyLength 2048 `
224-
-CertStoreLocation "Cert:\CurrentUser\My" `
225-
-KeyExportPolicy Exportable `
226-
-NotAfter (Get-Date).AddYears(1) `
227-
-KeyUsage DigitalSignature, KeyEncipherment
228-
229-
Write-Host "Client cert created: $($clientCert.Subject)"
230-
Write-Host "Client cert thumbprint: $($clientCert.Thumbprint)"
231-
Add-Content -Path $env:GITHUB_ENV -Value "CLIENT_CERT_SUBJECT=$($clientCert.Subject)"
240+
# Create/import client certificate based on client_key_source
241+
if ("${{ matrix.client_key_source }}" -eq "store") {
242+
# For cert store: import existing fred-cert.der (signed by CA) into cert store
243+
# This ensures the cert is signed by the CA that the server trusts
244+
$fredCertPath = Join-Path "${{ github.workspace }}" "wolfssh\keys\fred-cert.der"
245+
$fredKeyPath = Join-Path "${{ github.workspace }}" "wolfssh\keys\fred-key.der"
246+
247+
if (-not (Test-Path $fredCertPath) -or -not (Test-Path $fredKeyPath)) {
248+
Write-Host "ERROR: fred-cert.der or fred-key.der not found"
249+
exit 1
250+
}
251+
252+
# Convert DER cert+key to PFX for import into cert store using openssl
253+
$pfxPath = Join-Path $env:TEMP "fred-client.pfx"
254+
$pfxPassword = "TempP@ss123"
255+
256+
# Check if openssl is available
257+
$opensslPath = Get-Command openssl -ErrorAction SilentlyContinue
258+
if ($opensslPath) {
259+
Write-Host "Converting fred-cert.der + fred-key.der to PFX using openssl..."
260+
# Convert DER to PEM first, then to PFX
261+
$fredCertPem = Join-Path $env:TEMP "fred-cert.pem"
262+
$fredKeyPem = Join-Path $env:TEMP "fred-key.pem"
263+
264+
& openssl x509 -inform DER -in $fredCertPath -out $fredCertPem
265+
& openssl rsa -inform DER -in $fredKeyPath -out $fredKeyPem
266+
267+
# Create PFX
268+
& openssl pkcs12 -export -out $pfxPath -inkey $fredKeyPem -in $fredCertPem -password "pass:$pfxPassword" -nodes
269+
270+
if (Test-Path $pfxPath) {
271+
Write-Host "PFX created, importing into cert store..."
272+
# Import PFX into cert store
273+
Import-PfxCertificate -FilePath $pfxPath -CertStoreLocation "Cert:\CurrentUser\My" -Password (ConvertTo-SecureString -String $pfxPassword -Force -AsPlainText) | Out-Null
274+
275+
# Get the imported cert
276+
$importedCert = Get-ChildItem -Path "Cert:\CurrentUser\My" | Where-Object { $_.Subject -match "fred" } | Select-Object -First 1
277+
278+
# Cleanup temp files
279+
Remove-Item -Path $pfxPath, $fredCertPem, $fredKeyPem -ErrorAction SilentlyContinue
280+
281+
if ($importedCert) {
282+
Write-Host "Successfully imported fred cert: $($importedCert.Subject)"
283+
} else {
284+
Write-Host "WARNING: Cert imported but not found by subject search"
285+
# Get the most recently added cert
286+
$importedCert = Get-ChildItem -Path "Cert:\CurrentUser\My" | Sort-Object NotBefore -Descending | Select-Object -First 1
287+
}
288+
} else {
289+
Write-Host "WARNING: Failed to create PFX, falling back to self-signed cert"
290+
$importedCert = $null
291+
}
292+
} else {
293+
Write-Host "WARNING: openssl not found, cannot import fred-cert. Creating self-signed cert (may not work with CA verification)"
294+
$importedCert = $null
295+
}
296+
297+
if (-not $importedCert) {
298+
# Fallback: create self-signed cert (won't work with CA verification, but allows test to proceed)
299+
Write-Host "Creating self-signed client cert as fallback..."
300+
$clientCert = New-SelfSignedCertificate `
301+
-Subject "CN=wolfSSH-Test-Client" `
302+
-KeyAlgorithm RSA `
303+
-KeyLength 2048 `
304+
-CertStoreLocation "Cert:\CurrentUser\My" `
305+
-KeyExportPolicy Exportable `
306+
-NotAfter (Get-Date).AddYears(1) `
307+
-KeyUsage DigitalSignature, KeyEncipherment
308+
$importedCert = $clientCert
309+
}
310+
311+
Write-Host "Client cert in store: $($importedCert.Subject)"
312+
Write-Host "Client cert thumbprint: $($importedCert.Thumbprint)"
313+
Add-Content -Path $env:GITHUB_ENV -Value "CLIENT_CERT_SUBJECT=$($importedCert.Subject)"
314+
} else {
315+
# For file/x509: create a placeholder cert (not used, but keeps env var consistent)
316+
$clientCert = New-SelfSignedCertificate `
317+
-Subject "CN=wolfSSH-Test-Client" `
318+
-KeyAlgorithm RSA `
319+
-KeyLength 2048 `
320+
-CertStoreLocation "Cert:\CurrentUser\My" `
321+
-KeyExportPolicy Exportable `
322+
-NotAfter (Get-Date).AddYears(1) `
323+
-KeyUsage DigitalSignature, KeyEncipherment
324+
Write-Host "Client cert created (not used for file/x509): $($clientCert.Subject)"
325+
Add-Content -Path $env:GITHUB_ENV -Value "CLIENT_CERT_SUBJECT=$($clientCert.Subject)"
326+
}
232327
233328
- name: Create Windows user testuser and authorized_keys
234329
shell: pwsh
@@ -258,19 +353,39 @@ jobs:
258353
} else {
259354
Write-Host "Created user testuser"
260355
}
356+
Add-Content -Path $env:GITHUB_ENV -Value "TESTUSER_PASSWORD=$pw"
357+
358+
# authorized_keys: add keys based on client_key_source
359+
# For X509 (both x509 file and store): no authorized_keys needed (server verifies cert against CA)
360+
$authKeysContent = @()
261361
262-
# authorized_keys: hansel public key with comment 'testuser'
263-
$pubKey = Get-Content "${{ github.workspace }}\wolfssh\keys\hansel-key-ecc.pub" -Raw
264-
if (-not $pubKey) {
265-
Write-Host "ERROR: hansel-key-ecc.pub not found"
266-
exit 1
362+
if ("${{ matrix.client_key_source }}" -eq "x509" -or "${{ matrix.client_key_source }}" -eq "store") {
363+
Write-Host "X509 certificate auth (source: ${{ matrix.client_key_source }}): authorized_keys not needed (server uses CA verification)"
364+
# Create empty file - X509 doesn't use authorized_keys, but file should exist
365+
"" | Out-File -FilePath $authKeysFile -Encoding ASCII -NoNewline
366+
} else {
367+
# For file: add hansel public key (public key authentication, not X509)
368+
$pubKey = Get-Content "${{ github.workspace }}\wolfssh\keys\hansel-key-ecc.pub" -Raw
369+
if (-not $pubKey) {
370+
Write-Host "ERROR: hansel-key-ecc.pub not found"
371+
exit 1
372+
}
373+
$pubKey = ($pubKey -replace '\s+hansel\s*$', ' testuser').TrimEnd()
374+
$authKeysContent += $pubKey
375+
}
376+
377+
# Write authorized_keys (empty for X509, populated for pubkey auth)
378+
if ("${{ matrix.client_key_source }}" -eq "file") {
379+
$authKeysContent -join "`n" | Out-File -FilePath $authKeysFile -Encoding ASCII -NoNewline
380+
# ensure testuser can read (wolfsshd impersonates testuser when checking authorized_keys)
381+
icacls $authKeysFile /grant "testuser:R" /q
382+
Write-Host "Created $authKeysFile with $($authKeysContent.Count) key(s)"
383+
Get-Content $authKeysFile
384+
} else {
385+
# X509 (x509 or store): file already created empty above
386+
icacls $authKeysFile /grant "testuser:R" /q
387+
Write-Host "Created $authKeysFile (empty - X509 uses CA verification)"
267388
}
268-
$pubKey = ($pubKey -replace '\s+hansel\s*$', ' testuser').TrimEnd()
269-
$pubKey | Out-File -FilePath $authKeysFile -Encoding ASCII -NoNewline
270-
# ensure testuser can read (wolfsshd impersonates testuser when checking authorized_keys)
271-
icacls $authKeysFile /grant "testuser:R" /q
272-
Write-Host "Created $authKeysFile"
273-
Get-Content $authKeysFile
274389
275390
# Set ProfileImagePath so SHGetKnownFolderPath(FOLDERID_Profile) returns $homeDir
276391
# for testuser (GetHomeDirectory in wolfsshd uses that; otherwise it can fail for new users).
@@ -290,19 +405,35 @@ jobs:
290405
PermitRootLogin yes
291406
"@
292407
408+
# For X509 client auth (both x509 file and store): configure TrustedUserCAKeys (server verifies client cert against CA)
409+
if ("${{ matrix.client_key_source }}" -eq "x509" -or "${{ matrix.client_key_source }}" -eq "store") {
410+
$caCertPath = Join-Path "${{ github.workspace }}" "wolfssh\keys\ca-cert-ecc.der"
411+
$caCertPathFull = (Resolve-Path $caCertPath -ErrorAction SilentlyContinue)
412+
if (-not $caCertPathFull) {
413+
Write-Host "ERROR: CA cert not found at: $caCertPath"
414+
exit 1
415+
}
416+
Write-Host "Using CA cert for X509 verification (client source: ${{ matrix.client_key_source }}): $($caCertPathFull.Path)"
417+
$configContent += @"
418+
419+
TrustedUserCAKeys $($caCertPathFull.Path)
420+
"@
421+
}
422+
293423
if ("${{ matrix.server_key_source }}" -eq "store") {
294424
# Get server cert subject from environment
295-
$serverSubject = (Get-Content env:SERVER_CERT_SUBJECT)
425+
$serverSubject = $env:SERVER_CERT_SUBJECT
296426
if ([string]::IsNullOrEmpty($serverSubject)) {
297427
Write-Host "ERROR: SERVER_CERT_SUBJECT not set"
298428
exit 1
299429
}
300430
Write-Host "Using cert store host key with subject: $serverSubject"
431+
# Server cert is in LocalMachine (service runs as LocalSystem)
301432
$configContent += @"
302433
303434
HostKeyStore My
304435
HostKeyStoreSubject $serverSubject
305-
HostKeyStoreFlags CURRENT_USER
436+
HostKeyStoreFlags LOCAL_MACHINE
306437
"@
307438
} else {
308439
# Use absolute path for file-based key
@@ -466,14 +597,33 @@ jobs:
466597
Write-Host "ERROR: HostKeyStoreSubject is empty"
467598
exit 1
468599
}
469-
# Verify cert exists in store
470-
$cert = Get-ChildItem -Path "Cert:\CurrentUser\My" | Where-Object { $_.Subject -eq $subject } | Select-Object -First 1
600+
# Verify cert exists in store (check both LocalMachine and CurrentUser based on flags)
601+
$storeFlags = ""
602+
if ($configContent -match "HostKeyStoreFlags\s+([^\r\n]+)") {
603+
$storeFlags = $matches[1].Trim()
604+
}
605+
$storePath = "Cert:\CurrentUser\My"
606+
if ($storeFlags -eq "LOCAL_MACHINE") {
607+
$storePath = "Cert:\LocalMachine\My"
608+
}
609+
Write-Host "Checking cert store: $storePath"
610+
$cert = Get-ChildItem -Path $storePath -ErrorAction SilentlyContinue | Where-Object { $_.Subject -eq $subject } | Select-Object -First 1
471611
if ($cert) {
472612
Write-Host "Certificate found in store: OK (Thumbprint: $($cert.Thumbprint))"
613+
# Verify cert has private key accessible
614+
try {
615+
$hasPrivateKey = $cert.HasPrivateKey
616+
Write-Host "Cert has private key: $hasPrivateKey"
617+
if (-not $hasPrivateKey) {
618+
Write-Host "WARNING: Certificate does not have a private key accessible"
619+
}
620+
} catch {
621+
Write-Host "WARNING: Could not verify private key access: $_"
622+
}
473623
} else {
474624
Write-Host "ERROR: Certificate not found in store with subject: $subject"
475-
Write-Host "Available certificates in Cert:\CurrentUser\My:"
476-
Get-ChildItem -Path "Cert:\CurrentUser\My" | Select-Object Subject, Thumbprint | Format-Table
625+
Write-Host "Available certificates in $storePath :"
626+
Get-ChildItem -Path $storePath -ErrorAction SilentlyContinue | Select-Object Subject, Thumbprint | Format-Table
477627
exit 1
478628
}
479629
} else {
@@ -482,6 +632,26 @@ jobs:
482632
}
483633
}
484634
635+
# For X509 (both x509 file and store): TrustedUserCAKeys is required instead of authorized_keys
636+
if ("${{ matrix.client_key_source }}" -eq "x509" -or "${{ matrix.client_key_source }}" -eq "store") {
637+
if ($configContent -match "TrustedUserCAKeys\s+") {
638+
Write-Host "Found TrustedUserCAKeys directive (X509 CA verification, client source: ${{ matrix.client_key_source }})"
639+
if ($configContent -match "TrustedUserCAKeys\s+([^\r\n]+)") {
640+
$caPath = $matches[1].Trim()
641+
Write-Host "CA cert path: $caPath"
642+
if (Test-Path $caPath) {
643+
Write-Host "CA cert file exists: OK"
644+
} else {
645+
Write-Host "ERROR: CA cert file not found: $caPath"
646+
exit 1
647+
}
648+
}
649+
} else {
650+
Write-Host "ERROR: TrustedUserCAKeys not found (required for X509, client source: ${{ matrix.client_key_source }})"
651+
exit 1
652+
}
653+
}
654+
485655
if (-not $hasHostKey) {
486656
Write-Host "ERROR: No host key configuration found in config file!"
487657
exit 1
@@ -735,18 +905,38 @@ jobs:
735905
)
736906
737907
if ("${{ matrix.client_key_source }}" -eq "store") {
738-
# Get client cert subject from environment
739-
$clientSubject = (Get-Content env:CLIENT_CERT_SUBJECT)
908+
$clientSubject = $env:CLIENT_CERT_SUBJECT
909+
if ([string]::IsNullOrEmpty($clientSubject)) {
910+
Write-Host "ERROR: CLIENT_CERT_SUBJECT not set"
911+
exit 1
912+
}
740913
$sftpArgs += "-W", "My:$clientSubject:CURRENT_USER"
914+
} elseif ("${{ matrix.client_key_source }}" -eq "x509") {
915+
# X509 certificate authentication: use certificate + private key
916+
$certPath = "keys\fred-cert.der"
917+
$keyPath = "keys\fred-key.der"
918+
$caCertPath = "keys\ca-cert-ecc.der"
919+
if (-not (Test-Path $certPath)) {
920+
Write-Host "ERROR: Client cert not found: $certPath"
921+
exit 1
922+
}
923+
if (-not (Test-Path $keyPath)) {
924+
Write-Host "ERROR: Client key not found: $keyPath"
925+
exit 1
926+
}
927+
$sftpArgs += "-J", $certPath
928+
$sftpArgs += "-i", $keyPath
929+
# Optional: CA cert for host verification
930+
if (Test-Path $caCertPath) {
931+
$sftpArgs += "-A", $caCertPath
932+
}
741933
} else {
742934
# Use file-based key for client
743935
$sftpArgs += "-i", "keys\hansel-key-ecc.der"
744936
$sftpArgs += "-j", "keys\hansel-key-ecc.pub"
745937
}
746938
747-
# For public key auth, we don't need password
748-
# The test will verify key exchange works (even if auth fails)
749-
# In a real scenario, you'd set up authorized_keys
939+
# X509 certificate auth for store/x509, public key auth for file - no password fallback
750940
751941
Write-Host "Running: $sftpPath $($sftpArgs -join ' ')"
752942
Write-Host "Test matrix: server=${{ matrix.server_key_source }}, client=${{ matrix.client_key_source }}"
@@ -821,11 +1011,26 @@ jobs:
8211011
"localhost"
8221012
)
8231013
824-
# Set environment variable for cert store if using store key
1014+
# Set authentication method based on client_key_source
8251015
if ("${{ matrix.client_key_source }}" -eq "store") {
826-
$clientSubject = (Get-Content env:CLIENT_CERT_SUBJECT)
1016+
$clientSubject = $env:CLIENT_CERT_SUBJECT
8271017
$env:WOLFSSH_CERT_STORE = "My:$clientSubject:CURRENT_USER"
8281018
Write-Host "Using cert store key via WOLFSSH_CERT_STORE: $env:WOLFSSH_CERT_STORE" -ForegroundColor Yellow
1019+
} elseif ("${{ matrix.client_key_source }}" -eq "x509") {
1020+
# X509 certificate authentication: use certificate + private key
1021+
$certPath = "keys\fred-cert.der"
1022+
$keyPath = "keys\fred-key.der"
1023+
$caCertPath = "keys\ca-cert-ecc.der"
1024+
if (-not (Test-Path $certPath) -or -not (Test-Path $keyPath)) {
1025+
Write-Host "WARNING: X509 cert/key not found, skipping SSH client test"
1026+
exit 0
1027+
}
1028+
$sshArgs += "-J", $certPath
1029+
$sshArgs += "-i", $keyPath
1030+
if (Test-Path $caCertPath) {
1031+
$sshArgs += "-A", $caCertPath
1032+
}
1033+
Write-Host "Using X509 certificate authentication" -ForegroundColor Yellow
8291034
} else {
8301035
# Use file-based key - set via environment variable for default key location
8311036
# Or we can use the existing keyFile mechanism

0 commit comments

Comments
 (0)