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