diff --git a/.github/workflows/windows-cert-store-test.yml b/.github/workflows/windows-cert-store-test.yml new file mode 100644 index 000000000..bbc0f981d --- /dev/null +++ b/.github/workflows/windows-cert-store-test.yml @@ -0,0 +1,2195 @@ +name: Windows Certificate Store Test + +# This workflow tests MS Certificate Store integration for WolfSSH +# It tests 4 combinations: +# 1. Server using cert store key, Client using cert store key +# 2. Server using cert store key, Client using file-based key (interop) +# 3. Server using file-based key, Client using cert store key (interop) +# 4. Server using file-based key, Client using file-based key (baseline) + +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + +env: + WOLFSSL_SOLUTION_FILE_PATH: wolfssl64.sln + SOLUTION_FILE_PATH: wolfssh.sln + USER_SETTINGS_H_NEW: wolfssh/ide/winvs/user_settings.h + USER_SETTINGS_H: wolfssl/IDE/WIN/user_settings.h + INCLUDE_DIR: wolfssh + WOLFSSL_BUILD_CONFIGURATION: Release + WOLFSSH_BUILD_CONFIGURATION: Release + BUILD_PLATFORM: x64 + TARGET_PLATFORM: 10 + TEST_PORT: 22222 + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + with: + repository: wolfssl/wolfssl + path: wolfssl + + - uses: actions/checkout@v4 + with: + path: wolfssh + + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v1 + + - name: Restore wolfSSL NuGet packages + working-directory: ${{ github.workspace }}\wolfssl + run: nuget restore ${{env.WOLFSSL_SOLUTION_FILE_PATH}} + + - name: updated user_settings.h for sshd and x509 + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: | + # Enable SSHD, SFTP, and X509 support (including WOLFSSH_NO_FPKI) + sed -i 's/#if 0/#if 1/g' ${{env.USER_SETTINGS_H_NEW}} + cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}} + # Verify WOLFSSH_NO_FPKI will be defined + if grep -q "WOLFSSH_NO_FPKI" ${{env.USER_SETTINGS_H}}; then + echo "WOLFSSH_NO_FPKI found in user_settings.h" + else + echo "WARNING: WOLFSSH_NO_FPKI not found in user_settings.h" + fi + + - name: Build wolfssl library + working-directory: ${{ github.workspace }}\wolfssl + run: msbuild /m /p:PlatformToolset=v142 /p:Platform=${{env.BUILD_PLATFORM}} /p:Configuration=${{env.WOLFSSL_BUILD_CONFIGURATION}} /t:wolfssl ${{env.WOLFSSL_SOLUTION_FILE_PATH}} + + - name: Upload wolfSSL build artifacts + uses: actions/upload-artifact@v4 + with: + name: wolfssl-windows-build + if-no-files-found: warn + retention-days: 1 + path: | + wolfssl/IDE/WIN/${{env.WOLFSSL_BUILD_CONFIGURATION}}/${{env.BUILD_PLATFORM}}/** + wolfssl/IDE/WIN/${{env.WOLFSSL_BUILD_CONFIGURATION}}/** + wolfssl/${{env.WOLFSSL_BUILD_CONFIGURATION}}/${{env.BUILD_PLATFORM}}/** + wolfssl/${{env.WOLFSSL_BUILD_CONFIGURATION}}/** + + - name: Restore NuGet packages + working-directory: ${{ github.workspace }}\wolfssh\ide\winvs + run: nuget restore ${{env.SOLUTION_FILE_PATH}} + + - name: Build wolfssh + working-directory: ${{ github.workspace }}\wolfssh\ide\winvs + run: msbuild /m /p:PlatformToolset=v142 /p:Platform=${{env.BUILD_PLATFORM}} /p:WindowsTargetPlatformVersion=${{env.TARGET_PLATFORM}} /p:Configuration=${{env.WOLFSSH_BUILD_CONFIGURATION}} ${{env.SOLUTION_FILE_PATH}} + + - name: Upload wolfSSH build artifacts + uses: actions/upload-artifact@v4 + with: + name: wolfssh-windows-build + if-no-files-found: error + path: | + wolfssh/ide/winvs/**/Release/** + + - name: Create PowerShell script to import cert to store + working-directory: ${{ github.workspace }}\wolfssh + run: | + @" + # Import certificate and key to Windows Certificate Store + param( + [string]$CertPath, + [string]$KeyPath, + [string]$StoreName = "My", + [string]$SubjectName, + [string]$StoreLocation = "CurrentUser" + ) + + `$ErrorActionPreference = "Stop" + + # Convert DER to Base64 for import + `$certBytes = [System.IO.File]::ReadAllBytes(`$CertPath) + `$certBase64 = [System.Convert]::ToBase64String(`$certBytes) + + # Create certificate object + `$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + `$cert.Import([System.Convert]::FromBase64String(`$certBase64)) + + # If subject name not provided, use CN from certificate + if ([string]::IsNullOrEmpty(`$SubjectName)) { + `$SubjectName = `$cert.Subject + } + + # Determine store location + `$storeLocationEnum = [System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser + if (`$StoreLocation -eq "LocalMachine") { + `$storeLocationEnum = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine + } + + # Open the certificate store + `$store = New-Object System.Security.Cryptography.X509Certificates.X509Store(`$StoreName, `$storeLocationEnum) + `$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + + try { + # Remove existing certificate with same subject if present + `$existingCerts = `$store.Certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindBySubjectName, `$SubjectName, `$false) + foreach (`$existingCert in `$existingCerts) { + `$store.Remove(`$existingCert) + } + + # Import the certificate + # Note: For private key import, we need to use certutil or other methods + # This is a simplified version - in practice, you may need to use + # certutil -importPFX or other methods to import with private key + `$store.Add(`$cert) + Write-Host "Certificate imported successfully to `$StoreName store" + Write-Host "Subject: `$SubjectName" + Write-Host "Thumbprint: `$(`$cert.Thumbprint)" + } + finally { + `$store.Close() + } + + return `$cert.Thumbprint + "@ | Out-File -FilePath import-cert.ps1 -Encoding UTF8 + + - name: Build import script + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # Note: This step is informational - actual import happens in test job + Write-Host "Keys will be imported to cert store in test job" + + test: + needs: build + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + include: + - server_key_source: file + client_key_source: x509 + test_name: "Server-File-Client-X509" + - server_key_source: store + client_key_source: x509 + test_name: "Server-Store-Client-X509" + - server_key_source: file + client_key_source: store + test_name: "Server-File-Client-Store" + - server_key_source: store + client_key_source: store + test_name: "Server-Store-Client-Store" + + steps: + - uses: actions/checkout@v4 + with: + path: wolfssh + + - name: Download wolfSSH build artifacts + uses: actions/download-artifact@v4 + with: + name: wolfssh-windows-build + path: . + + - name: Download wolfSSL build artifacts + uses: actions/download-artifact@v4 + with: + name: wolfssl-windows-build + path: . + + - name: Set up test environment - ${{ matrix.test_name }} + working-directory: ${{ github.workspace }}\wolfssh + shell: bash + env: + # Disable MSYS path conversion - Git Bash converts /C=US/... to C:/Program Files/Git/C=US/... + MSYS_NO_PATHCONV: 1 + MSYS2_ARG_CONV_EXCL: "*" + run: | + echo "=== Test Configuration ===" + echo "Server key source: ${{ matrix.server_key_source }}" + echo "Client key source: ${{ matrix.client_key_source }}" + echo "=========================" + + # Create X509 certificate for testuser using renewcerts.sh (like sshd_x509_test.sh does) + if [[ "${{ matrix.client_key_source }}" == "x509" || "${{ matrix.client_key_source }}" == "store" ]]; then + echo "Creating X509 certificate for testuser using renewcerts.sh..." + + # Check required files exist + if [[ ! -f "keys/renewcerts.sh" ]]; then + echo "ERROR: renewcerts.sh not found at keys/renewcerts.sh" + exit 1 + fi + if [[ ! -f "keys/fred-key.pem" ]]; then + echo "ERROR: fred-key.pem not found at keys/fred-key.pem" + exit 1 + fi + if [[ ! -f "keys/ca-key-ecc.pem" ]]; then + echo "ERROR: ca-key-ecc.pem not found at keys/ca-key-ecc.pem" + exit 1 + fi + + # Run renewcerts.sh with testuser argument (this creates testuser-cert.der and testuser-key.der) + cd keys + bash renewcerts.sh testuser + cd .. + + # Verify certificates were created + if [[ -f "keys/testuser-cert.der" && -f "keys/testuser-key.der" ]]; then + echo "Created testuser-cert.der and testuser-key.der" + + # Verify the certificate has the correct CN + certText=$(openssl x509 -in keys/testuser-cert.der -inform DER -text -noout 2>&1) + if echo "$certText" | grep -q "CN.*=.*testuser"; then + echo "Certificate CN verified: testuser" + else + echo "WARNING: Certificate CN may not match testuser" + echo "Certificate subject:" + echo "$certText" | grep "Subject:" + fi + + echo "CLIENT_CERT_FILE=keys/testuser-cert.der" >> $GITHUB_ENV + echo "CLIENT_KEY_FILE=keys/testuser-key.der" >> $GITHUB_ENV + else + echo "ERROR: Failed to create certificate files" + echo "Expected: keys/testuser-cert.der and keys/testuser-key.der" + ls -la keys/ + exit 1 + fi + fi + + - name: Set up cert store certificates + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # For testing, we'll create self-signed certificates in the cert store + # In a production scenario, you would import existing DER keys/certs + # using certutil or other tools that can handle private key import + + # Create server certificate in cert store with exportable key + # For server: use LocalMachine so service (LocalSystem) can access it + # For client: use CurrentUser (accessed by testuser) + if ("${{ matrix.server_key_source }}" -eq "store") { + $serverCert = New-SelfSignedCertificate ` + -Subject "CN=wolfSSH-Test-Server" ` + -KeyAlgorithm RSA ` + -KeyLength 2048 ` + -CertStoreLocation "Cert:\LocalMachine\My" ` + -KeyExportPolicy Exportable ` + -NotAfter (Get-Date).AddYears(1) ` + -KeyUsage DigitalSignature, KeyEncipherment + Write-Host "Server cert created in LocalMachine: $($serverCert.Subject)" + + # Grant LocalSystem (NT AUTHORITY\SYSTEM) access to the private key. + # This is required for the wolfsshd service to access the key when running + # as LocalSystem. Without this, CryptAcquireCertificatePrivateKey fails. + Write-Host "=== Granting LocalSystem access to private key ===" + Write-Host "Certificate thumbprint: $($serverCert.Thumbprint)" + Write-Host "Certificate has private key: $($serverCert.HasPrivateKey)" + + # Try multiple methods to get the private key info + $keyFound = $false + + # Method 1: Try RSACertificateExtensions + try { + Write-Host "Trying RSACertificateExtensions.GetRSAPrivateKey..." + $rsaKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($serverCert) + if ($rsaKey) { + Write-Host " Got RSA key object: $($rsaKey.GetType().FullName)" + # Try to get the key name from CngKey + if ($rsaKey.Key) { + $keyName = $rsaKey.Key.UniqueName + Write-Host " Key unique name: $keyName" + } elseif ($rsaKey -is [System.Security.Cryptography.RSACng]) { + $cngKey = $rsaKey.Key + if ($cngKey) { + $keyName = $cngKey.UniqueName + Write-Host " CNG Key unique name: $keyName" + } + } + } + } catch { + Write-Host " RSACertificateExtensions method failed: $_" + } + + # Method 2: Try PrivateKey property (older API) + if (-not $keyName) { + try { + Write-Host "Trying PrivateKey property..." + if ($serverCert.PrivateKey) { + $pk = $serverCert.PrivateKey + Write-Host " Got private key: $($pk.GetType().FullName)" + if ($pk.CspKeyContainerInfo) { + $keyName = $pk.CspKeyContainerInfo.UniqueKeyContainerName + Write-Host " CSP Key container name: $keyName" + } + } + } catch { + Write-Host " PrivateKey property method failed: $_" + } + } + + # Search for key file in known locations + if ($keyName) { + $keyPaths = @( + "$env:ProgramData\Microsoft\Crypto\Keys\$keyName", + "$env:ProgramData\Microsoft\Crypto\RSA\MachineKeys\$keyName", + "$env:ProgramData\Microsoft\Crypto\SystemKeys\$keyName" + ) + foreach ($keyPath in $keyPaths) { + if (Test-Path $keyPath) { + Write-Host "Found key file at: $keyPath" + # Show current ACL + $acl = Get-Acl $keyPath + Write-Host "Current ACL:" + $acl.Access | ForEach-Object { Write-Host " $($_.IdentityReference): $($_.FileSystemRights)" } + + # Add SYSTEM access + $permission = "NT AUTHORITY\SYSTEM", "FullControl", "Allow" + $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $permission + $acl.SetAccessRule($accessRule) + Set-Acl $keyPath $acl + Write-Host "Granted SYSTEM FullControl to: $keyPath" + + # Verify new ACL + $newAcl = Get-Acl $keyPath + Write-Host "New ACL:" + $newAcl.Access | ForEach-Object { Write-Host " $($_.IdentityReference): $($_.FileSystemRights)" } + + $keyFound = $true + break + } + } + if (-not $keyFound) { + Write-Host "WARNING: Key file not found in any expected location" + Write-Host "Searching for key files..." + Get-ChildItem "$env:ProgramData\Microsoft\Crypto" -Recurse -File 2>$null | ForEach-Object { + if ($_.Name -like "*$($keyName.Substring(0, [Math]::Min(8, $keyName.Length)))*") { + Write-Host " Possible match: $($_.FullName)" + } + } + } + } else { + Write-Host "WARNING: Could not determine private key name" + } + + if (-not $keyFound) { + Write-Host "WARNING: Could not grant SYSTEM access to private key - service may fail" + } + } else { + # Still create it for consistency, but in CurrentUser (not used for server) + $serverCert = New-SelfSignedCertificate ` + -Subject "CN=wolfSSH-Test-Server" ` + -KeyAlgorithm RSA ` + -KeyLength 2048 ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -KeyExportPolicy Exportable ` + -NotAfter (Get-Date).AddYears(1) ` + -KeyUsage DigitalSignature, KeyEncipherment + Write-Host "Server cert created in CurrentUser: $($serverCert.Subject)" + } + Write-Host "Server cert thumbprint: $($serverCert.Thumbprint)" + Write-Host "Server cert full subject: $($serverCert.Subject)" + # Extract just the CN value without "CN=" prefix for CertFindCertificateInStore + # CertFindCertificateInStore with CERT_FIND_SUBJECT_STR searches the formatted name + # which may not include the "CN=" prefix + $subjectForSearch = $serverCert.Subject + if ($subjectForSearch -match "^CN=(.+)$") { + $subjectForSearch = $matches[1] + Write-Host "Using CN value for search: $subjectForSearch" + } + Add-Content -Path $env:GITHUB_ENV -Value "SERVER_CERT_SUBJECT=$subjectForSearch" + Add-Content -Path $env:GITHUB_ENV -Value "SERVER_CERT_STORE=${{ matrix.server_key_source }}" + + # Create/import client certificate based on client_key_source + if ("${{ matrix.client_key_source }}" -eq "store") { + # For cert store: import testuser certificate (signed by CA) into cert store + # This ensures the cert is signed by the CA that the server trusts and has CN=testuser + $userCertPath = $env:CLIENT_CERT_FILE + $userKeyPath = $env:CLIENT_KEY_FILE + if ([string]::IsNullOrEmpty($userCertPath)) { + $userCertPath = Join-Path "${{ github.workspace }}" "wolfssh\keys\testuser-cert.der" + } + if ([string]::IsNullOrEmpty($userKeyPath)) { + $userKeyPath = Join-Path "${{ github.workspace }}" "wolfssh\keys\testuser-key.der" + } + # Fallback to fred if testuser certs don't exist + if (-not (Test-Path $userCertPath)) { + Write-Host "WARNING: testuser-cert.der not found, trying fred-cert.der" + $userCertPath = Join-Path "${{ github.workspace }}" "wolfssh\keys\fred-cert.der" + } + if (-not (Test-Path $userKeyPath)) { + Write-Host "WARNING: testuser-key.der not found, trying fred-key.der" + $userKeyPath = Join-Path "${{ github.workspace }}" "wolfssh\keys\fred-key.der" + } + + if (-not (Test-Path $userCertPath) -or -not (Test-Path $userKeyPath)) { + Write-Host "ERROR: Client cert or key not found: $userCertPath or $userKeyPath" + exit 1 + } + + # Convert DER cert+key to PFX for import into cert store using openssl + $pfxPath = Join-Path $env:TEMP "testuser-client.pfx" + $pfxPassword = "TempP@ss123" + + # Check if openssl is available + $opensslPath = Get-Command openssl -ErrorAction SilentlyContinue + if ($opensslPath) { + Write-Host "Converting testuser-cert.der + testuser-key.der to PFX using openssl..." + # Convert DER to PEM first, then to PFX + $userCertPem = Join-Path $env:TEMP "testuser-cert.pem" + $userKeyPem = Join-Path $env:TEMP "testuser-key.pem" + + # Convert certificate DER to PEM + $certConvert = & openssl x509 -inform DER -in $userCertPath -out $userCertPem 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Failed to convert cert DER to PEM: $certConvert" + $importedCert = $null + } else { + # Try to convert key - first try as RSA, then as ECC + $keyConvert = & openssl rsa -inform DER -in $userKeyPath -out $userKeyPem 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "Key is not RSA, trying ECC..." + $keyConvert = & openssl ec -inform DER -in $userKeyPath -out $userKeyPem 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Failed to convert key DER to PEM (tried RSA and ECC): $keyConvert" + $importedCert = $null + } else { + Write-Host "Successfully converted ECC key to PEM" + } + } else { + Write-Host "Successfully converted RSA key to PEM" + } + + if ($importedCert -eq $null -and (Test-Path $userKeyPem)) { + # Create PFX + $pfxConvert = & openssl pkcs12 -export -out $pfxPath -inkey $userKeyPem -in $userCertPem -password "pass:$pfxPassword" -nodes 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Failed to create PFX: $pfxConvert" + $importedCert = $null + } elseif (Test-Path $pfxPath) { + Write-Host "PFX created, importing into cert store..." + # Import PFX into cert store + try { + Import-PfxCertificate -FilePath $pfxPath -CertStoreLocation "Cert:\CurrentUser\My" -Password (ConvertTo-SecureString -String $pfxPassword -Force -AsPlainText) -ErrorAction Stop | Out-Null + + # Get the imported cert - look for "testuser" in subject or check most recent + $importedCert = Get-ChildItem -Path "Cert:\CurrentUser\My" | Where-Object { $_.Subject -match "testuser" } | Select-Object -First 1 + if (-not $importedCert) { + Write-Host "WARNING: Cert imported but not found by subject search, checking most recent..." + # Get the most recently added cert + $importedCert = Get-ChildItem -Path "Cert:\CurrentUser\My" | Sort-Object NotBefore -Descending | Select-Object -First 1 + } + + if ($importedCert) { + Write-Host "Successfully imported testuser cert: $($importedCert.Subject)" + } + } catch { + Write-Host "ERROR: Failed to import PFX: $_" + $importedCert = $null + } + + # Cleanup temp files + Remove-Item -Path $pfxPath, $userCertPem, $userKeyPem -ErrorAction SilentlyContinue + } else { + Write-Host "WARNING: PFX file was not created" + $importedCert = $null + } + } + } + } else { + Write-Host "WARNING: openssl not found, cannot import testuser cert. Creating self-signed cert (may not work with CA verification)" + $importedCert = $null + } + + if (-not $importedCert) { + # Fallback: create self-signed cert (won't work with CA verification, but allows test to proceed) + Write-Host "Creating self-signed client cert as fallback..." + $clientCert = New-SelfSignedCertificate ` + -Subject "CN=wolfSSH-Test-Client" ` + -KeyAlgorithm RSA ` + -KeyLength 2048 ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -KeyExportPolicy Exportable ` + -NotAfter (Get-Date).AddYears(1) ` + -KeyUsage DigitalSignature, KeyEncipherment + $importedCert = $clientCert + } + + Write-Host "Client cert in store: $($importedCert.Subject)" + Write-Host "Client cert thumbprint: $($importedCert.Thumbprint)" + # Extract CN from the subject for use as a cert store search string. + # The full X.500 DN contains commas which break command-line argument + # parsing, but CertFindCertificateInStore does substring matching so + # the CN alone is sufficient. + $cn = $importedCert.Subject + if ($cn -match 'CN=([^,]+)') { + $cn = $matches[1].Trim() + } + Write-Host "Client cert CN for store lookup: '$cn'" + Add-Content -Path $env:GITHUB_ENV -Value "CLIENT_CERT_SUBJECT=$cn" + } else { + # For file/x509: create a placeholder cert (not used, but keeps env var consistent) + $clientCert = New-SelfSignedCertificate ` + -Subject "CN=wolfSSH-Test-Client" ` + -KeyAlgorithm RSA ` + -KeyLength 2048 ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -KeyExportPolicy Exportable ` + -NotAfter (Get-Date).AddYears(1) ` + -KeyUsage DigitalSignature, KeyEncipherment + Write-Host "Client cert created (placeholder for non-store tests): $($clientCert.Subject)" + Add-Content -Path $env:GITHUB_ENV -Value "CLIENT_CERT_SUBJECT=$($clientCert.Subject)" + } + + - name: Create Windows user testuser and authorized_keys + shell: pwsh + run: | + $homeDir = "C:\Users\testuser" + $sshDir = "$homeDir\.ssh" + $authKeysFile = "$sshDir\authorized_keys" + # Password: <=14 chars to avoid net user "Windows 2000" prompt; mixed case, number, special. + # This is a test user and not a sensitive password. + $pw = 'T3stP@ss!xY9' + + # Create home dir and .ssh for testuser (default: .ssh/authorized_keys) + New-Item -ItemType Directory -Path $homeDir -Force | Out-Null + New-Item -ItemType Directory -Path $sshDir -Force | Out-Null + Write-Host "Created $homeDir and $sshDir" + + # Create local user testuser (net user avoids New-LocalUser password policy issues in CI) + $o = net user testuser $pw /add /homedir:$homeDir 2>&1 + if ($LASTEXITCODE -ne 0) { + if ($o -match "already exists") { + Write-Host "User testuser already exists" + net user testuser /homedir:$homeDir 2>$null + } else { + Write-Host "net user failed: $o" + exit 1 + } + } else { + Write-Host "Created user testuser" + } + Add-Content -Path $env:GITHUB_ENV -Value "TESTUSER_PASSWORD=$pw" + + # For X509: no authorized_keys needed (server verifies cert against CA) + Write-Host "X509 certificate auth (source: ${{ matrix.client_key_source }}): authorized_keys not needed (server uses CA verification)" + # Create empty file - X509 doesn't use authorized_keys, but file should exist + "" | Out-File -FilePath $authKeysFile -Encoding ASCII -NoNewline + icacls $authKeysFile /grant "testuser:R" /q + Write-Host "Created $authKeysFile (empty - X509 uses CA verification)" + + # Set ProfileImagePath so SHGetKnownFolderPath(FOLDERID_Profile) returns $homeDir + # for testuser (GetHomeDirectory in wolfsshd uses that; otherwise it can fail for new users). + $sid = (New-Object System.Security.Principal.NTAccount("testuser")).Translate([System.Security.Principal.SecurityIdentifier]).Value + $profKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$sid" + if (-not (Test-Path $profKey)) { New-Item -Path $profKey -Force | Out-Null } + Set-ItemProperty -Path $profKey -Name "ProfileImagePath" -Value $homeDir -Force + Write-Host "Set ProfileImagePath for testuser to $homeDir" + + - name: Create wolfSSHd config file + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $configContent = @" + Port ${{env.TEST_PORT}} + PasswordAuthentication yes + PermitRootLogin yes + "@ + + # For X509 client auth: configure TrustedUserCAKeys and HostCertificate (server verifies client cert against CA) + # Use PEM format as per apps/wolfsshd/test/create_sshd_config.sh + $caCertPathPem = Join-Path "${{ github.workspace }}" "wolfssh\keys\ca-cert-ecc.pem" + $caCertPathFull = (Resolve-Path $caCertPathPem -ErrorAction SilentlyContinue) + if (-not $caCertPathFull) { + Write-Host "ERROR: CA cert not found at: $caCertPathPem" + exit 1 + } + Write-Host "Using CA cert (PEM format) for X509 verification: $($caCertPathFull.Path)" + + $configContent += @" + + TrustedUserCAKeys $($caCertPathFull.Path) + "@ + + if ("${{ matrix.server_key_source }}" -eq "store") { + # Get server cert subject from environment + $serverSubject = $env:SERVER_CERT_SUBJECT + if ([string]::IsNullOrEmpty($serverSubject)) { + Write-Host "ERROR: SERVER_CERT_SUBJECT not set" + exit 1 + } + Write-Host "Using cert store host key with subject: $serverSubject" + # Server cert is in LocalMachine (service runs as LocalSystem) + # Note: When using cert store, the certificate is part of the store entry + # Do NOT specify HostCertificate separately - it would conflict with the store cert + $configContent += @" + + HostKeyStore My + HostKeyStoreSubject $serverSubject + HostKeyStoreFlags LOCAL_MACHINE + "@ + } else { + # Use PEM format as per apps/wolfsshd/test/create_sshd_config.sh + $keyPath = Join-Path "${{ github.workspace }}" "wolfssh\keys\server-key.pem" + $keyPathFull = (Resolve-Path $keyPath -ErrorAction SilentlyContinue) + if (-not $keyPathFull) { + Write-Host "ERROR: Host key file not found at: $keyPath" + Write-Host "Checking for key files..." + Get-ChildItem -Path "${{ github.workspace }}\wolfssh\keys" -Filter "server-key*" | Select-Object FullName + exit 1 + } + Write-Host "Using file-based host key: $($keyPathFull.Path)" + + # Add HostCertificate only for file-based keys (not cert store) + $serverCertPathPem = Join-Path "${{ github.workspace }}" "wolfssh\keys\server-cert.pem" + $serverCertPathFull = (Resolve-Path $serverCertPathPem -ErrorAction SilentlyContinue) + if (-not $serverCertPathFull) { + Write-Host "ERROR: server-cert.pem not found at: $serverCertPathPem (required for X509)" + Write-Host "Checking for server cert files..." + Get-ChildItem -Path "${{ github.workspace }}\wolfssh\keys" -Filter "server-cert*" | Select-Object FullName + exit 1 + } + Write-Host "Using server certificate: $($serverCertPathFull.Path)" + + $configContent += @" + + HostKey $($keyPathFull.Path) + HostCertificate $($serverCertPathFull.Path) + "@ + } + + $configContent | Out-File -FilePath sshd_config_test -Encoding ASCII + Write-Host "=== wolfSSHd Config ===" + Get-Content sshd_config_test + Write-Host "=== End Config ===" + + - name: Find wolfSSH executables + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $searchRoot = "${{ github.workspace }}" + Write-Host "Searching for built executables under: $searchRoot" + + # Find wolfsshd.exe + $sshdExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfsshd.exe" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*Release*" -or $_.FullName -like "*Debug*" } | + Select-Object -First 1 + if ($sshdExe) { + Write-Host "Found wolfsshd.exe at: $($sshdExe.FullName)" + Add-Content -Path $env:GITHUB_ENV -Value "SSHD_PATH=$($sshdExe.FullName)" + } else { + Write-Host "ERROR: wolfsshd.exe not found" + Get-ChildItem -Path $searchRoot -Recurse -Filter "*.exe" -ErrorAction SilentlyContinue | Select-Object FullName + exit 1 + } + + # Find wolfsftp client exe (project name is often wolfsftp-client) + $sftpExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfsftp.exe" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*Release*" -or $_.FullName -like "*Debug*" } | + Select-Object -First 1 + if (-not $sftpExe) { + $sftpExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfsftp-client.exe" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*Release*" -or $_.FullName -like "*Debug*" } | + Select-Object -First 1 + } + if ($sftpExe) { + Write-Host "Found SFTP client exe at: $($sftpExe.FullName)" + Add-Content -Path $env:GITHUB_ENV -Value "SFTP_PATH=$($sftpExe.FullName)" + } else { + Write-Host "ERROR: SFTP client exe not found (wolfsftp.exe or wolfsftp-client.exe)" + Get-ChildItem -Path $searchRoot -Recurse -Filter "*.exe" -ErrorAction SilentlyContinue | Select-Object FullName + exit 1 + } + + # Find wolfssh.exe (SSH client) (optional) + $sshExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfssh.exe" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*Release*" -or $_.FullName -like "*Debug*" } | + Select-Object -First 1 + if ($sshExe) { + Write-Host "Found wolfssh.exe at: $($sshExe.FullName)" + Add-Content -Path $env:GITHUB_ENV -Value "SSH_PATH=$($sshExe.FullName)" + } else { + Write-Host "WARNING: wolfssh.exe not found (SSH client test will be skipped)" + } + + # Find echoserver.exe (used for cert store server test instead of wolfsshd for better debug logs) + $echoserverExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "echoserver.exe" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*Release*" -or $_.FullName -like "*Debug*" } | + Select-Object -First 1 + if ($echoserverExe) { + Write-Host "Found echoserver.exe at: $($echoserverExe.FullName)" + Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PATH=$($echoserverExe.FullName)" + } else { + Write-Host "WARNING: echoserver.exe not found (cert store server test will need it)" + } + + - name: Copy wolfSSL DLL to executable directory (if dynamic build) + working-directory: ${{ github.workspace }} + shell: pwsh + run: | + $sshdPath = (Get-Content env:SSHD_PATH) + if (-not (Test-Path $sshdPath)) { + Write-Host "ERROR: wolfsshd.exe path not found in environment" + exit 1 + } + + $sshdDir = Split-Path -Parent $sshdPath + Write-Host "wolfsshd.exe directory: $sshdDir" + + # If wolfssl.lib is already next to wolfsshd.exe, it's a static build - no DLL needed + $libInSshdDir = Join-Path $sshdDir "wolfssl.lib" + if (Test-Path $libInSshdDir) { + Write-Host "wolfssl.lib present beside wolfsshd.exe - static build; wolfssl.dll not required" + exit 0 + } + + # Dynamic build: find and copy wolfssl.dll + $wolfsslRoot = "${{ github.workspace }}\wolfssl" + $buildConfig = "${{env.WOLFSSL_BUILD_CONFIGURATION}}" + $buildPlatform = "${{env.BUILD_PLATFORM}}" + + $commonPaths = @( + "$wolfsslRoot\IDE\WIN\$buildConfig\$buildPlatform\wolfssl.dll", + "$wolfsslRoot\IDE\WIN\$buildConfig\wolfssl.dll", + "$wolfsslRoot\$buildConfig\$buildPlatform\wolfssl.dll", + "$wolfsslRoot\$buildConfig\wolfssl.dll" + ) + + $wolfsslDll = $null + foreach ($path in $commonPaths) { + if (Test-Path $path) { $wolfsslDll = Get-Item $path; break } + } + if (-not $wolfsslDll) { + $wolfsslDll = Get-ChildItem -Path $wolfsslRoot -Recurse -Filter "wolfssl.dll" -ErrorAction SilentlyContinue | Select-Object -First 1 + } + + if ($wolfsslDll) { + $targetDll = Join-Path $sshdDir "wolfssl.dll" + Copy-Item -Path $wolfsslDll.FullName -Destination $targetDll -Force + Write-Host "Copied wolfssl.dll to $targetDll" + } else { + Write-Host "wolfssl.dll not found; if build is static (wolfssl.lib in output), this is OK" + } + + - name: Verify host key configuration + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + Write-Host "=== Verifying Host Key Configuration ===" + $configPath = "sshd_config_test" + if (-not (Test-Path $configPath)) { + Write-Host "ERROR: Config file not found: $configPath" + exit 1 + } + + $configContent = Get-Content $configPath -Raw + Write-Host "Config file content:" + Write-Host $configContent + + # Check if host key is configured + $hasHostKey = $false + if ($configContent -match "HostKey\s+") { + Write-Host "Found HostKey directive (file-based)" + $hasHostKey = $true + # Verify the key file exists + if ($configContent -match "HostKey\s+([^\r\n]+)") { + $keyPath = $matches[1].Trim() + Write-Host "Host key path: $keyPath" + if (Test-Path $keyPath) { + Write-Host "Host key file exists: OK" + } else { + Write-Host "ERROR: Host key file not found: $keyPath" + exit 1 + } + } + } + if ($configContent -match "HostKeyStore\s+") { + Write-Host "Found HostKeyStore directive (cert store-based)" + $hasHostKey = $true + # Verify cert store subject is set + if ($configContent -match "HostKeyStoreSubject\s+([^\r\n]+)") { + $subject = $matches[1].Trim() + Write-Host "Host key store subject: $subject" + if ([string]::IsNullOrEmpty($subject)) { + Write-Host "ERROR: HostKeyStoreSubject is empty" + exit 1 + } + # Verify cert exists in store (check both LocalMachine and CurrentUser based on flags) + $storeFlags = "" + if ($configContent -match "HostKeyStoreFlags\s+([^\r\n]+)") { + $storeFlags = $matches[1].Trim() + } + $storePath = "Cert:\CurrentUser\My" + if ($storeFlags -eq "LOCAL_MACHINE") { + $storePath = "Cert:\LocalMachine\My" + } + Write-Host "Checking cert store: $storePath" + # Use -like with wildcards for substring match (same as CertFindCertificateInStore) + # The subject might be "wolfSSH-Test-Server" but cert has "CN=wolfSSH-Test-Server" + $cert = Get-ChildItem -Path $storePath -ErrorAction SilentlyContinue | Where-Object { $_.Subject -like "*$subject*" } | Select-Object -First 1 + if ($cert) { + Write-Host "Certificate found in store: OK (Thumbprint: $($cert.Thumbprint), Subject: $($cert.Subject))" + # Verify cert has private key accessible + try { + $hasPrivateKey = $cert.HasPrivateKey + Write-Host "Cert has private key: $hasPrivateKey" + if (-not $hasPrivateKey) { + Write-Host "WARNING: Certificate does not have a private key accessible" + } + } catch { + Write-Host "WARNING: Could not verify private key access: $_" + } + } else { + Write-Host "ERROR: Certificate not found in store with subject: $subject" + Write-Host "Available certificates in $storePath :" + Get-ChildItem -Path $storePath -ErrorAction SilentlyContinue | Select-Object Subject, Thumbprint | Format-Table + exit 1 + } + } else { + Write-Host "ERROR: HostKeyStoreSubject not found in config" + exit 1 + } + } + + # For X509 (both x509 file and store): TrustedUserCAKeys is required instead of authorized_keys + if ("${{ matrix.client_key_source }}" -eq "x509" -or "${{ matrix.client_key_source }}" -eq "store") { + if ($configContent -match "TrustedUserCAKeys\s+") { + Write-Host "Found TrustedUserCAKeys directive (X509 CA verification, client source: ${{ matrix.client_key_source }})" + if ($configContent -match "TrustedUserCAKeys\s+([^\r\n]+)") { + $caPath = $matches[1].Trim() + Write-Host "CA cert path: $caPath" + if (Test-Path $caPath) { + Write-Host "CA cert file exists: OK" + $caFileInfo = Get-Item $caPath + Write-Host "CA cert file size: $($caFileInfo.Length) bytes" + } else { + Write-Host "ERROR: CA cert file not found: $caPath" + Write-Host "Current directory: $(Get-Location)" + Write-Host "Files in keys directory:" + if (Test-Path "keys") { + Get-ChildItem "keys" -Filter "*ca-cert*" | Format-Table Name, Length + } + exit 1 + } + } + } else { + Write-Host "ERROR: TrustedUserCAKeys not found (required for X509, client source: ${{ matrix.client_key_source }})" + exit 1 + } + + # For X509: AuthorizedKeysFile should NOT be set (server uses CA verification only) + if ($configContent -match "AuthorizedKeysFile\s+") { + Write-Host "WARNING: AuthorizedKeysFile is set for X509 auth - this may prevent CA verification" + Write-Host "Server will only use CA verification if AuthorizedKeysFile is NOT set" + } else { + Write-Host "AuthorizedKeysFile not set: OK (server will use CA verification)" + } + + # Verify client cert files exist (for x509 file case) + if ("${{ matrix.client_key_source }}" -eq "x509") { + $userCertPath = $env:CLIENT_CERT_FILE + $userKeyPath = $env:CLIENT_KEY_FILE + if ([string]::IsNullOrEmpty($userCertPath)) { + $userCertPath = "keys\testuser-cert.der" + } + if ([string]::IsNullOrEmpty($userKeyPath)) { + $userKeyPath = "keys\testuser-key.der" + } + # Fallback to fred if testuser certs don't exist + if (-not (Test-Path $userCertPath)) { + Write-Host "WARNING: testuser-cert.der not found, trying fred-cert.der" + $userCertPath = "keys\fred-cert.der" + } + if (-not (Test-Path $userKeyPath)) { + Write-Host "WARNING: testuser-key.der not found, trying fred-key.der" + $userKeyPath = "keys\fred-key.der" + } + Write-Host "Verifying client cert files for X509 authentication..." + if (-not (Test-Path $userCertPath)) { + Write-Host "ERROR: Client cert not found: $userCertPath" + exit 1 + } + if (-not (Test-Path $userKeyPath)) { + Write-Host "ERROR: Client key not found: $userKeyPath" + exit 1 + } + Write-Host "Client cert files found: OK" + $certInfo = Get-Item $userCertPath + $keyInfo = Get-Item $userKeyPath + Write-Host " $($certInfo.Name): $($certInfo.Length) bytes" + Write-Host " $($keyInfo.Name): $($keyInfo.Length) bytes" + } + } + + if (-not $hasHostKey) { + Write-Host "ERROR: No host key configuration found in config file!" + exit 1 + } + + Write-Host "Host key configuration verified: OK" + + - name: Verify dependencies and environment + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $sshdPath = (Get-Content env:SSHD_PATH) + $sshdDir = Split-Path -Parent $sshdPath + + Write-Host "=== Verifying wolfsshd.exe environment ===" + Write-Host "Executable: $sshdPath" + Write-Host "Directory: $sshdDir" + + # Check if DLL is present + $dllPath = Join-Path $sshdDir "wolfssl.dll" + if (Test-Path $dllPath) { + Write-Host "✓ wolfssl.dll found" + $dllInfo = Get-Item $dllPath + Write-Host " Size: $($dllInfo.Length) bytes" + Write-Host " Modified: $($dllInfo.LastWriteTime)" + } else { + Write-Host "✗ wolfssl.dll NOT FOUND in $sshdDir" + Write-Host "Files in directory:" + Get-ChildItem -Path $sshdDir | Select-Object Name, Length | Format-Table + } + + # Check config file + $configPath = "sshd_config_test" + if (Test-Path $configPath) { + Write-Host "✓ Config file found: $configPath" + } else { + Write-Host "✗ Config file NOT FOUND: $configPath" + } + + # Note: Direct execution will fail with "StartServiceCtrlDispatcher failed" + # This is expected - the executable is built as a service and must run via SCM + Write-Host "" + Write-Host "Note: wolfsshd.exe is built as a Windows service." + Write-Host "Direct execution will fail (this is expected)." + Write-Host "It must be started via Service Control Manager (sc.exe)." + + - name: Grant service (LocalSystem) access to config, keys, and executable + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # wolfsshd runs as LocalSystem; it must be able to read config, key files, and run the exe. + # Grant NT AUTHORITY\SYSTEM read+execute on the entire wolfssh tree so the service can: + # - run wolfsshd.exe (and load wolfssl.dll if dynamic) + # - read sshd_config_test and all files under keys/ + # /T = apply to existing files and subdirs; (OI)(CI) = inherit to new objects + $wolfsshRoot = (Get-Location).Path + Write-Host "Granting SYSTEM (RX) on entire wolfssh tree: $wolfsshRoot" + icacls $wolfsshRoot /grant "NT AUTHORITY\SYSTEM:(OI)(CI)RX" /T /q + if ($LASTEXITCODE -ne 0) { + Write-Host "WARNING: icacls on wolfssh root failed, trying config and keys only" + $configPathFull = (Resolve-Path "sshd_config_test" -ErrorAction Stop).Path + $keysDir = (Resolve-Path "keys" -ErrorAction Stop).Path + icacls $configPathFull /grant "NT AUTHORITY\SYSTEM:R" /q + icacls $keysDir /grant "NT AUTHORITY\SYSTEM:(OI)(CI)R" /T /q + $sshdPath = $env:SSHD_PATH + if ($sshdPath -and (Test-Path $sshdPath)) { + $sshdDir = (Resolve-Path (Split-Path -Parent $sshdPath)).Path + icacls $sshdDir /grant "NT AUTHORITY\SYSTEM:(OI)(CI)RX" /T /q + } + } + Write-Host "Done." + + - name: Test cert store access as LocalSystem + if: matrix.server_key_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # Run the standalone cert store test AS LocalSystem using a scheduled task. + # This verifies that the service account can actually access the private key. + Write-Host "=== Testing cert store access as LocalSystem ===" + + $testExe = $env:WINCERTSTORE_TEST_PATH + if (-not $testExe -or -not (Test-Path $testExe)) { + Write-Host "Skipping LocalSystem test (win-cert-store-test.exe not found)" + exit 0 + } + + $store = "My" + $subject = "wolfSSH-Test-Server" + $location = "LOCAL_MACHINE" + $outputFile = "$env:TEMP\localsystem-cert-test-output.txt" + + # Create a batch script to run the test and capture output + $batchScript = @" + "$testExe" $store $subject $location > "$outputFile" 2>&1 + echo EXIT_CODE=%ERRORLEVEL% >> "$outputFile" + "@ + $batchPath = "$env:TEMP\run-cert-test.cmd" + $batchScript | Out-File -FilePath $batchPath -Encoding ASCII + + # Create a scheduled task that runs as SYSTEM + $taskName = "WolfSSH-CertStoreTest" + $action = New-ScheduledTaskAction -Execute "cmd.exe" -Argument "/c `"$batchPath`"" + $principal = New-ScheduledTaskPrincipal -UserId "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries + + # Remove existing task if present + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + + # Register and run the task + Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Settings $settings | Out-Null + Start-ScheduledTask -TaskName $taskName + + # Wait for task to complete (max 30 seconds) + $timeout = 30 + $elapsed = 0 + while ($elapsed -lt $timeout) { + Start-Sleep -Seconds 1 + $elapsed++ + $task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue + if ($task.State -eq "Ready") { + break + } + } + + # Get task result + $taskInfo = Get-ScheduledTaskInfo -TaskName $taskName -ErrorAction SilentlyContinue + Write-Host "Task last run result: $($taskInfo.LastTaskResult)" + + # Display output + if (Test-Path $outputFile) { + Write-Host "=== LocalSystem cert store test output ===" + Get-Content $outputFile + Write-Host "=== End output ===" + + # Check for success + $content = Get-Content $outputFile -Raw + if ($content -match "EXIT_CODE=0") { + Write-Host "SUCCESS: LocalSystem can access the certificate private key" + } else { + Write-Host "FAILURE: LocalSystem cannot access the certificate private key" + Write-Host "This explains why the wolfsshd service fails to start." + # Don't exit with error - let the service test provide the final verdict + } + } else { + Write-Host "WARNING: No output file generated" + } + + # Cleanup + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + Remove-Item $batchPath -ErrorAction SilentlyContinue + Remove-Item $outputFile -ErrorAction SilentlyContinue + + - name: Test wolfsshd config loading as LocalSystem + if: matrix.server_key_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # Try to get more info by testing if wolfsshd can at least parse the config + # We'll create a small test that verifies LocalSystem can read all config-referenced files + Write-Host "=== Testing config file access as LocalSystem ===" + + $configPath = (Resolve-Path "sshd_config_test").Path + $outputFile = "$env:TEMP\localsystem-config-test-output.txt" + + # Create a PowerShell script to test file access as SYSTEM + $testScript = @' + $ErrorActionPreference = "Continue" + $configPath = $args[0] + $outputPath = $args[1] + + $results = @() + $results += "Testing config access as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)" + $results += "" + + # Test config file + $results += "Config file: $configPath" + if (Test-Path $configPath) { + $results += " Exists: YES" + try { + $content = Get-Content $configPath -Raw + $results += " Readable: YES" + $results += " Content:" + $content -split "`n" | ForEach-Object { $results += " $_" } + $results += "" + + # Extract and test TrustedUserCAKeys + if ($content -match "TrustedUserCAKeys\s+([^\r\n]+)") { + $caPath = $matches[1].Trim() + $results += "TrustedUserCAKeys: $caPath" + if (Test-Path $caPath) { + $results += " Exists: YES" + try { + $caContent = Get-Content $caPath -Raw + $results += " Readable: YES ($($caContent.Length) bytes)" + } catch { + $results += " Readable: NO - $_" + } + } else { + $results += " Exists: NO" + } + } + } catch { + $results += " Readable: NO - $_" + } + } else { + $results += " Exists: NO" + } + + $results | Out-File -FilePath $outputPath -Encoding UTF8 + '@ + + $scriptPath = "$env:TEMP\test-config-access.ps1" + $testScript | Out-File -FilePath $scriptPath -Encoding UTF8 + + # Create scheduled task to run as SYSTEM + $taskName = "WolfSSH-ConfigAccessTest" + $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -File `"$scriptPath`" `"$configPath`" `"$outputFile`"" + $principal = New-ScheduledTaskPrincipal -UserId "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries + + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Settings $settings | Out-Null + Start-ScheduledTask -TaskName $taskName + + # Wait for completion + $timeout = 30 + $elapsed = 0 + while ($elapsed -lt $timeout) { + Start-Sleep -Seconds 1 + $elapsed++ + $task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue + if ($task.State -eq "Ready") { break } + } + + # Show results + if (Test-Path $outputFile) { + Write-Host "=== LocalSystem config access test results ===" + Get-Content $outputFile + Write-Host "=== End results ===" + } else { + Write-Host "WARNING: No output file generated" + } + + # Cleanup + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + Remove-Item $scriptPath -ErrorAction SilentlyContinue + Remove-Item $outputFile -ErrorAction SilentlyContinue + + - name: Dry-run wolfsshd as LocalSystem (cert store diagnostics) + if: matrix.server_key_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # Run wolfsshd in non-daemon test mode AS LOCALSYSTEM via scheduled + # task. This captures the exact error that the service would hit. + $sshdPath = (Get-Content env:SSHD_PATH -ErrorAction SilentlyContinue) + if (-not $sshdPath -or -not (Test-Path $sshdPath)) { + Write-Host "Skipping: wolfsshd.exe not found" + exit 0 + } + $sshdPathFull = (Resolve-Path $sshdPath).Path + $configPathFull = (Resolve-Path "sshd_config_test").Path + $port = ${{env.TEST_PORT}} + + # Output goes to a temp file that LocalSystem can write to + $outFile = "$env:TEMP\wolfsshd-localsystem-dryrun.txt" + + # We wrap the call in cmd /c so stdout+stderr go to the file + $cmdLine = "`"$sshdPathFull`" -D -t -d -f `"$configPathFull`" -p $port" + Write-Host "Will run as SYSTEM: $cmdLine" + + # Create scheduled task to run as SYSTEM + $taskName = "WolfSSH-DryRunLocalSystem" + $action = New-ScheduledTaskAction ` + -Execute "cmd.exe" ` + -Argument "/c $cmdLine > `"$outFile`" 2>&1" + $principal = New-ScheduledTaskPrincipal ` + -UserId "NT AUTHORITY\SYSTEM" ` + -LogonType ServiceAccount -RunLevel Highest + $settings = New-ScheduledTaskSettingsSet ` + -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries + + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Settings $settings | Out-Null + Start-ScheduledTask -TaskName $taskName + + # Wait for completion (wolfsshd -t exits quickly) + $timeout = 30 + $elapsed = 0 + while ($elapsed -lt $timeout) { + Start-Sleep -Seconds 1 + $elapsed++ + $task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue + if ($task.State -eq "Ready") { break } + } + + # Get exit code + $taskInfo = Get-ScheduledTaskInfo -TaskName $taskName -ErrorAction SilentlyContinue + if ($taskInfo) { + Write-Host "Scheduled task last result: $($taskInfo.LastTaskResult)" + } + + # Show output + Write-Host "=== wolfsshd dry-run as LocalSystem ===" + if (Test-Path $outFile) { + Get-Content $outFile + } else { + Write-Host "(no output file generated)" + } + Write-Host "=== end LocalSystem dry-run ===" + + # Cleanup + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + Remove-Item $outFile -ErrorAction SilentlyContinue + + - name: Start echoserver with cert store (cert store matrix – more debug logs) + if: matrix.server_key_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # For cert store we use echoserver instead of wolfsshd for now (better debug logs). + # Start echoserver detached (via cmd start /B) so it survives after this step ends; + # otherwise the runner may kill the process when the step completes. + $echoserverPath = $env:ECHOSERVER_PATH + if (-not $echoserverPath -or -not (Test-Path $echoserverPath)) { + Write-Host "ERROR: echoserver.exe not found (ECHOSERVER_PATH not set or missing)" + Get-ChildItem -Path "${{ github.workspace }}" -Recurse -Filter "echoserver.exe" -ErrorAction SilentlyContinue | Select-Object FullName + exit 1 + } + $exeDir = Split-Path -Parent $echoserverPath + $exeName = Split-Path -Leaf $echoserverPath + $store = "My" + $subject = "wolfSSH-Test-Server" + $location = "LOCAL_MACHINE" + $port = ${{env.TEST_PORT}} + $spec = "${store}:${subject}:${location}" + + # Build echoserver arguments. Besides the cert-store host key (-W) + # and port, the echoserver needs: + # -a so it can verify client X.509 certs + # -K testuser: so the user-auth callback recognises testuser + $echoArgs = @("-W", $spec, "-p", $port) + $wolfsshRoot = "${{ github.workspace }}\wolfssh" + + # CA certificate for client-cert verification + $caCertPem = Join-Path $wolfsshRoot "keys\ca-cert-ecc.pem" + if (Test-Path $caCertPem) { + $echoArgs += "-a", $caCertPem + Write-Host "CA cert for client verification: $caCertPem" + } else { + Write-Host "WARNING: CA cert not found at $caCertPem (client cert auth will fail)" + } + + # Register testuser with their certificate so the auth callback accepts them + $clientCert = $env:CLIENT_CERT_FILE + if ([string]::IsNullOrEmpty($clientCert)) { + $clientCert = Join-Path $wolfsshRoot "keys\testuser-cert.der" + } + # Resolve to absolute path (CLIENT_CERT_FILE may be relative) + if (Test-Path $clientCert) { + $clientCert = (Resolve-Path $clientCert).Path + $echoArgs += "-K", "testuser:$clientCert" + Write-Host "Registered testuser cert: $clientCert" + } else { + Write-Host "WARNING: Client cert not found at $clientCert (testuser auth will fail)" + } + + Write-Host "=== Starting echoserver with cert store (detached, debug logs) ===" + $argStr = ($echoArgs | ForEach-Object { $_ }) -join " " + Write-Host "Command: $echoserverPath $argStr" + # Redirect stdout+stderr to a log file so we can inspect debug output later. + # Use cmd /c with output redirection to run the echoserver detached. + $echoLogFile = Join-Path $wolfsshRoot "echoserver_debug.log" + Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_LOG=$echoLogFile" + Write-Host "Debug log: $echoLogFile" + $cmdLine = "`"$echoserverPath`" $argStr > `"$echoLogFile`" 2>&1" + Start-Process -FilePath "cmd.exe" ` + -ArgumentList "/c", "start", "/B", "cmd", "/c", $cmdLine ` + -WorkingDirectory $exeDir -NoNewWindow -Wait:$false + Start-Sleep -Seconds 2 + $proc = Get-Process -Name "echoserver" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($proc) { + Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PID=$($proc.Id)" + Write-Host "echoserver started with PID $($proc.Id)" + } else { + Write-Host "WARNING: echoserver process not found by name after start" + } + # Wait for port to be listening (max 15 seconds) + $timeout = 15 + $elapsed = 0 + while ($elapsed -lt $timeout) { + Start-Sleep -Seconds 1 + $elapsed++ + try { + $conn = New-Object System.Net.Sockets.TcpClient("127.0.0.1", $port) + if ($conn.Connected) { $conn.Close(); break } + } catch {} + $stillRunning = Get-Process -Name "echoserver" -ErrorAction SilentlyContinue + if ($stillRunning) { continue } + Write-Host "ERROR: echoserver exited before port was ready" + exit 1 + } + if ($elapsed -ge $timeout) { + Write-Host "WARNING: Port $port not listening after ${timeout}s (echoserver may still be starting)" + } else { + Write-Host "echoserver is listening on port $port" + } + + - name: Test SFTP against echoserver (cert store server – debug logs) + if: matrix.server_key_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + Write-Host "=== SFTP test against echoserver (cert store host key) ===" + $testPort = ${{env.TEST_PORT}} + $sftpPath = (Get-Content env:SFTP_PATH) + if (-not (Test-Path $sftpPath)) { + Write-Host "ERROR: wolfsftp.exe not found at $sftpPath" + exit 1 + } + + # Test commands + $testCommands = @" + pwd + ls + quit + "@ + $testCommands | Out-File -FilePath sftp_echo_commands.txt -Encoding ASCII + + # Build SFTP args (same logic as main SFTP test step) + $sftpArgs = @( + "-u", "testuser", + "-h", "localhost", + "-p", "$testPort" + ) + + if ("${{ matrix.client_key_source }}" -eq "store") { + $clientSubject = $env:CLIENT_CERT_SUBJECT + if ([string]::IsNullOrEmpty($clientSubject)) { + Write-Host "ERROR: CLIENT_CERT_SUBJECT not set"; exit 1 + } + $certStoreSpec = "My:${clientSubject}:CURRENT_USER" + $sftpArgs += "-W", $certStoreSpec + $caCertPath = "keys\ca-cert-ecc.der" + if (Test-Path $caCertPath) { + $sftpArgs += "-A", (Resolve-Path $caCertPath).Path + } + $sftpArgs += "-X" + } elseif ("${{ matrix.client_key_source }}" -eq "x509") { + $certPath = $env:CLIENT_CERT_FILE + $keyPath = $env:CLIENT_KEY_FILE + if ([string]::IsNullOrEmpty($certPath)) { $certPath = "keys\testuser-cert.der" } + if ([string]::IsNullOrEmpty($keyPath)) { $keyPath = "keys\testuser-key.der" } + if (-not (Test-Path $certPath)) { $certPath = "keys\fred-cert.der" } + if (-not (Test-Path $keyPath)) { $keyPath = "keys\fred-key.der" } + $sftpArgs += "-J", (Resolve-Path $certPath).Path + $sftpArgs += "-i", (Resolve-Path $keyPath).Path + $caCertPath = "keys\ca-cert-ecc.der" + if (Test-Path $caCertPath) { + $sftpArgs += "-A", (Resolve-Path $caCertPath).Path + } + $sftpArgs += "-X" + } + + Write-Host "Running: $sftpPath $($sftpArgs -join ' ')" + $process = Start-Process -FilePath $sftpPath ` + -ArgumentList $sftpArgs ` + -RedirectStandardInput "sftp_echo_commands.txt" ` + -RedirectStandardOutput "sftp_echo_output.txt" ` + -RedirectStandardError "sftp_echo_error.txt" ` + -Wait -NoNewWindow -PassThru + + Write-Host "SFTP (echoserver) exit code: $($process.ExitCode)" + Write-Host "=== SFTP Output (echoserver) ===" + if (Test-Path sftp_echo_output.txt) { Get-Content sftp_echo_output.txt } + Write-Host "=== SFTP Error (echoserver) ===" + if (Test-Path sftp_echo_error.txt) { Get-Content sftp_echo_error.txt } + + # Dump echoserver debug log + $echoLog = $env:ECHOSERVER_LOG + if (-not [string]::IsNullOrEmpty($echoLog) -and (Test-Path $echoLog)) { + Write-Host "=== Echoserver Debug Log ===" + Get-Content $echoLog + Write-Host "=== End Echoserver Debug Log ===" + } + + if ($process.ExitCode -ne 0) { + Write-Host "WARNING: SFTP against echoserver failed (exit $($process.ExitCode)) – will continue to wolfsshd test" + } else { + Write-Host "SFTP against echoserver succeeded" + } + + - name: Stop echoserver before wolfsshd test + if: matrix.server_key_source == 'store' + shell: pwsh + run: | + $echoserverPid = $env:ECHOSERVER_PID + if (-not [string]::IsNullOrEmpty($echoserverPid)) { + Write-Host "Stopping echoserver (PID $echoserverPid) before starting wolfsshd" + Stop-Process -Id $echoserverPid -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + } + # Also kill by name in case PID tracking missed it + Get-Process -Name "echoserver" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + # Clear the env var so cleanup step doesn't try again + Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PID=" + + - name: Validate wolfsshd config (non-daemon dry run) + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # Run wolfsshd in non-daemon test mode (-D -t -d) to validate + # config loading and cert store access. This gives us visible + # stdout/stderr output, unlike the service which logs to + # OutputDebugString. + $sshdPath = (Get-Content env:SSHD_PATH) + if (-not (Test-Path $sshdPath)) { + Write-Host "Skipping dry-run: wolfsshd.exe not found" + exit 0 + } + $configPathFull = (Resolve-Path "sshd_config_test").Path + $port = ${{env.TEST_PORT}} + + Write-Host "=== wolfsshd dry-run: $sshdPath -D -t -d -f $configPathFull -p $port ===" + $proc = Start-Process -FilePath $sshdPath ` + -ArgumentList "-D", "-t", "-d", "-f", $configPathFull, "-p", $port ` + -RedirectStandardOutput "wolfsshd_dryrun_out.txt" ` + -RedirectStandardError "wolfsshd_dryrun_err.txt" ` + -Wait -NoNewWindow -PassThru + + Write-Host "Exit code: $($proc.ExitCode)" + Write-Host "=== stdout ===" + if (Test-Path wolfsshd_dryrun_out.txt) { Get-Content wolfsshd_dryrun_out.txt } + Write-Host "=== stderr ===" + if (Test-Path wolfsshd_dryrun_err.txt) { Get-Content wolfsshd_dryrun_err.txt } + Write-Host "=== end dry-run ===" + + if ($proc.ExitCode -ne 0) { + Write-Host "WARNING: wolfsshd dry-run failed (exit $($proc.ExitCode))" + Write-Host "The service will likely also fail to start." + } else { + Write-Host "wolfsshd dry-run succeeded - config is valid" + } + + - name: Start wolfSSHd as Windows service + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $sshdPath = (Get-Content env:SSHD_PATH) + if (-not (Test-Path $sshdPath)) { + Write-Host "ERROR: wolfsshd.exe not found at $sshdPath" + exit 1 + } + + # Get absolute path for service + $sshdPathFull = (Resolve-Path $sshdPath).Path + $configPathFull = (Resolve-Path "sshd_config_test").Path + + # Service name + $serviceName = "wolfsshd" + + # Remove service if it already exists + $existingService = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + if ($existingService) { + Write-Host "Removing existing $serviceName service" + if ($existingService.Status -eq 'Running') { + Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + } + sc.exe delete $serviceName | Out-Null + Start-Sleep -Seconds 2 + } + + # Show config file content for debugging + Write-Host "=== Config file content ===" + Get-Content $configPathFull + Write-Host "=== End config ===" + + # Verify all files referenced in config are accessible + Write-Host "=== Verifying config file references ===" + $configContent = Get-Content $configPathFull -Raw + + # Check TrustedUserCAKeys + if ($configContent -match "TrustedUserCAKeys\s+([^\r\n]+)") { + $caPath = $matches[1].Trim() + Write-Host "TrustedUserCAKeys: $caPath" + if (Test-Path $caPath) { + Write-Host " File exists: YES" + $acl = Get-Acl $caPath + $systemAccess = $acl.Access | Where-Object { $_.IdentityReference -like "*SYSTEM*" } + if ($systemAccess) { + Write-Host " SYSTEM access: $($systemAccess.FileSystemRights)" + } else { + Write-Host " WARNING: No explicit SYSTEM access (may inherit)" + } + } else { + Write-Host " ERROR: File not found!" + } + } + + # Check HostKeyStoreSubject + if ($configContent -match "HostKeyStoreSubject\s+([^\r\n]+)") { + $subject = $matches[1].Trim() + Write-Host "HostKeyStoreSubject: '$subject'" + Write-Host " Length: $($subject.Length) chars" + # Show hex dump for debugging + $bytes = [System.Text.Encoding]::UTF8.GetBytes($subject) + $hex = ($bytes | ForEach-Object { '{0:X2}' -f $_ }) -join ' ' + Write-Host " UTF-8 hex: $hex" + } + + # Pre-service checks + $sshdDir = Split-Path -Parent $sshdPathFull + $dllPath = Join-Path $sshdDir "wolfssl.dll" + $libPath = Join-Path $sshdDir "wolfssl.lib" + Write-Host "=== Pre-service checks ===" + Write-Host "Executable: $sshdPathFull" + Write-Host "Config: $configPathFull" + Write-Host "Working directory (sshd dir): $sshdDir" + + # wolfSSL: either DLL (dynamic) or static (.lib linked into exe) + if (Test-Path $dllPath) { + Write-Host "✓ wolfssl.dll found (dynamic build)" + $dllInfo = Get-Item $dllPath + Write-Host " Size: $($dllInfo.Length) bytes" + } elseif (Test-Path $libPath) { + Write-Host "✓ wolfssl.lib present, no wolfssl.dll - static build (no DLL required)" + } else { + Write-Host "WARNING: Neither wolfssl.dll nor wolfssl.lib in $sshdDir" + Write-Host "Files present:" + Get-ChildItem -Path $sshdDir | Select-Object Name, Length | Format-Table + # Continue anyway; service may still start if wolfssl is linked another way + } + + if (-not (Test-Path $configPathFull)) { + Write-Host "ERROR: Config file not found: $configPathFull" + exit 1 + } else { + Write-Host "✓ Config file found" + # Verify config file is readable + try { + $configContent = Get-Content $configPathFull -Raw + Write-Host " Size: $($configContent.Length) bytes" + } catch { + Write-Host "ERROR: Cannot read config file: $_" + exit 1 + } + } + + # Create the service with proper binpath + # Note: sc.exe requires the binPath to have the executable path and arguments + # The entire command line goes in binPath, with the exe path in quotes + # We do NOT include -E here because LocalSystem only has RX on + # the wolfssh directory and cannot create a log file. Debug output + # from the service goes to OutputDebugString; for visible diagnostics + # we rely on the "Validate wolfsshd config" dry-run step. + $binPath = "`"$sshdPathFull`" -f `"$configPathFull`" -p ${{env.TEST_PORT}}" + Write-Host "Creating service with binpath: $binPath" + + $createResult = sc.exe create $serviceName binPath= $binPath + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Failed to create service" + Write-Host $createResult + exit 1 + } + Write-Host "Service created: $createResult" + + # Set service to auto-start on failure (for debugging) + # This won't help if it exits cleanly, but might help with crashes + sc.exe failure $serviceName reset= 86400 actions= restart/5000/restart/5000/restart/5000 | Out-Null + + # Start the service + Write-Host "Starting $serviceName service" + $startResult = sc.exe start $serviceName + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Failed to start service" + Write-Host $startResult + # Try to get service status for debugging + sc.exe query $serviceName + exit 1 + } + Write-Host "Service started: $startResult" + + # Wait a bit for service to start + Start-Sleep -Seconds 5 + + # Check service status + $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + if (-not $service) { + Write-Host "ERROR: Service $serviceName not found after creation" + exit 1 + } + + if ($service.Status -ne 'Running') { + Write-Host "ERROR: Service is not running. Status: $($service.Status)" + + # Get detailed service information + Write-Host "=== Service Query ===" + sc.exe query $serviceName + + # Get service configuration to see the actual command + Write-Host "=== Service Configuration ===" + sc.exe qc $serviceName + + # Check service error code and details + Write-Host "=== Service Error Code ===" + $serviceInfo = Get-CimInstance -ClassName Win32_Service -Filter "Name='$serviceName'" -ErrorAction SilentlyContinue + if ($serviceInfo) { + Write-Host "ExitCode: $($serviceInfo.ExitCode)" + Write-Host "State: $($serviceInfo.State)" + Write-Host "Status: $($serviceInfo.Status)" + Write-Host "PathName: $($serviceInfo.PathName)" + Write-Host "StartMode: $($serviceInfo.StartMode)" + } + + # Try to get process exit code if it ran briefly + Write-Host "=== Checking for recent process exit ===" + $recentProcesses = Get-WinEvent -FilterHashtable @{LogName='System'; ID=7034,7035,7036} -MaxEvents 50 -ErrorAction SilentlyContinue | + Where-Object { $_.Message -like "*wolfsshd*" } | + Select-Object -First 5 + if ($recentProcesses) { + Write-Host "Recent service events:" + $recentProcesses | ForEach-Object { Write-Host " $($_.TimeCreated): $($_.Message)" } + } + + # Check event logs for errors + Write-Host "=== System Event Log (Service Control Manager) ===" + Get-EventLog -LogName System -Source "Service Control Manager" -Newest 20 -ErrorAction SilentlyContinue | + Where-Object { $_.Message -like "*wolfsshd*" -or $_.Message -like "*$serviceName*" } | + Select-Object TimeGenerated, EntryType, Message | Format-List + + Write-Host "=== Application Event Log ===" + Get-EventLog -LogName Application -Newest 30 -ErrorAction SilentlyContinue | + Where-Object { $_.Source -like "*wolf*" -or $_.Message -like "*wolf*" } | + Select-Object TimeGenerated, Source, EntryType, Message | Format-List + + # Check wolfSSL: DLL (dynamic) or .lib (static) + Write-Host "=== Checking wolfSSL (DLL or static) ===" + $sshdDir = Split-Path -Parent $sshdPathFull + $dllPath = Join-Path $sshdDir "wolfssl.dll" + $libPath = Join-Path $sshdDir "wolfssl.lib" + if (Test-Path $dllPath) { + Write-Host "wolfssl.dll: YES (dynamic build)" + } elseif (Test-Path $libPath) { + Write-Host "wolfssl.dll: NO; wolfssl.lib: YES (static build - OK)" + } else { + Write-Host "wolfssl.dll: NO; wolfssl.lib: NO" + Write-Host "Files in $sshdDir :" + Get-ChildItem -Path $sshdDir | Select-Object Name, Length | Format-Table + } + + # Check if process is running + Write-Host "=== Checking if process is running ===" + $processes = Get-Process | Where-Object { $_.ProcessName -like "*wolfsshd*" } + if ($processes) { + Write-Host "Found processes:" + $processes | Format-Table Id, ProcessName, StartTime, Path + } else { + Write-Host "No wolfsshd processes found" + } + + exit 1 + } + + Write-Host "wolfSSHd service is running (Status: $($service.Status))" + Add-Content -Path $env:GITHUB_ENV -Value "SSHD_SERVICE_NAME=$serviceName" + + - name: Test SFTP connection against wolfsshd + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # First, verify server is running and accessible + Write-Host "=== Pre-flight checks (wolfsshd) ===" + $serviceName = $env:SSHD_SERVICE_NAME + $testPort = ${{env.TEST_PORT}} + + if ($serviceName) { + $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + if ($service) { + Write-Host "Service status: $($service.Status)" + if ($service.Status -ne "Running") { + Write-Host "ERROR: Service is not running!" + exit 1 + } + } else { + Write-Host "WARNING: Service $serviceName not found" + } + } + + # Test TCP connectivity to the server + Write-Host "Testing TCP connection to localhost:$testPort..." + try { + $tcpClient = New-Object System.Net.Sockets.TcpClient + $connect = $tcpClient.BeginConnect("localhost", $testPort, $null, $null) + $wait = $connect.AsyncWaitHandle.WaitOne(3000, $false) + if ($wait) { + $tcpClient.EndConnect($connect) + Write-Host "TCP connection successful: OK" + $tcpClient.Close() + } else { + Write-Host "ERROR: TCP connection timeout - server may not be listening on port $testPort" + exit 1 + } + } catch { + Write-Host "ERROR: TCP connection failed: $_" + Write-Host "Checking if port $testPort is in use..." + $listener = Get-NetTCPConnection -LocalPort $testPort -ErrorAction SilentlyContinue + if ($listener) { + Write-Host "Port $testPort is in use by:" + $listener | Format-Table LocalAddress, LocalPort, State, OwningProcess + } else { + Write-Host "Port $testPort is not in use" + } + exit 1 + } + + # Verify server config file exists and is readable + if (Test-Path "sshd_config_test") { + Write-Host "Server config file exists: OK" + $configContent = Get-Content "sshd_config_test" -Raw + Write-Host "=== Server Config Content ===" + Write-Host $configContent + Write-Host "=== End Server Config ===" + + if ($configContent -match "TrustedUserCAKeys\s+([^\r\n]+)") { + $caPath = $matches[1].Trim() + Write-Host "TrustedUserCAKeys found: $caPath" + if (Test-Path $caPath) { + Write-Host " CA cert file exists: OK" + } else { + Write-Host " ERROR: CA cert file not found: $caPath" + } + } else { + Write-Host "ERROR: TrustedUserCAKeys not found in config" + } + + if ($configContent -match "HostCertificate\s+([^\r\n]+)") { + $hostCertPath = $matches[1].Trim() + Write-Host "HostCertificate found: $hostCertPath" + if (Test-Path $hostCertPath) { + Write-Host " Host cert file exists: OK" + } else { + Write-Host " ERROR: Host cert file not found: $hostCertPath" + } + } elseif ($configContent -match "HostKeyStore\s+") { + Write-Host "HostKeyStore configured (cert store) – no HostCertificate file needed" + } else { + Write-Host "ERROR: Neither HostCertificate nor HostKeyStore found in config" + } + + if ($configContent -match "HostKey\s+([^\r\n]+)") { + $hostKeyPath = $matches[1].Trim() + Write-Host "HostKey found: $hostKeyPath" + if (Test-Path $hostKeyPath) { + Write-Host " Host key file exists: OK" + } else { + Write-Host " ERROR: Host key file not found: $hostKeyPath" + } + } elseif ($configContent -match "HostKeyStoreSubject\s+([^\r\n]+)") { + Write-Host "HostKeyStoreSubject found: $($matches[1].Trim()) (cert store)" + } + } else { + Write-Host "ERROR: sshd_config_test not found" + exit 1 + } + + # Verify service is using the correct config + $serviceName = $env:SSHD_SERVICE_NAME + if ($serviceName) { + Write-Host "=== Verifying Service Configuration ===" + $serviceConfig = sc.exe qc $serviceName + Write-Host $serviceConfig + if ($serviceConfig -match "BINARY_PATH_NAME\s*:\s*(.+)") { + $binPath = $matches[1].Trim() + Write-Host "Service binary path: $binPath" + if ($binPath -match "sshd_config_test") { + Write-Host " Config file in service path: OK" + } else { + Write-Host " WARNING: Config file path not found in service binary path" + } + } + } + + # First test with SSH client (like sshd_x509_test.sh) to verify basic X509 connection + $sshPath = (Get-Content env:SSH_PATH -ErrorAction SilentlyContinue) + if ($sshPath -and (Test-Path $sshPath)) { + Write-Host "=== Testing X509 connection with SSH client (like sshd_x509_test.sh) ===" + + if ("${{ matrix.client_key_source }}" -eq "x509") { + $certPath = "keys\fred-cert.der" + $keyPath = "keys\fred-key.der" + $caCertPath = "keys\ca-cert-ecc.der" + + if ((Test-Path $certPath) -and (Test-Path $keyPath) -and (Test-Path $caCertPath)) { + $certPathFull = (Resolve-Path $certPath).Path + $keyPathFull = (Resolve-Path $keyPath).Path + $caCertPathFull = (Resolve-Path $caCertPath).Path + $sshTestArgs = @( + "-u", "testuser", + "-h", "localhost", + "-p", "${{env.TEST_PORT}}", + "-i", $keyPathFull, + "-J", $certPathFull, + "-A", $caCertPathFull, + "-X", + "-c", "pwd" + ) + Write-Host "Running SSH client test: $sshPath $($sshTestArgs -join ' ')" + $sshProcess = Start-Process -FilePath $sshPath ` + -ArgumentList $sshTestArgs ` + -RedirectStandardOutput "ssh_test_output.txt" ` + -RedirectStandardError "ssh_test_error.txt" ` + -Wait -NoNewWindow -PassThru + + Write-Host "SSH client exit code: $($sshProcess.ExitCode)" + if (Test-Path ssh_test_output.txt) { + Write-Host "=== SSH Output ===" + Get-Content ssh_test_output.txt + } + if (Test-Path ssh_test_error.txt) { + Write-Host "=== SSH Error ===" + Get-Content ssh_test_error.txt + } + + if ($sshProcess.ExitCode -eq 0) { + Write-Host "✓ SSH client X509 connection successful" + } else { + Write-Host "✗ SSH client X509 connection failed (exit code: $($sshProcess.ExitCode))" + Write-Host "This suggests the X509 authentication itself may be failing" + } + } else { + Write-Host "WARNING: Required cert files not found for SSH client test" + } + } + } else { + Write-Host "WARNING: SSH client not found (SSH_PATH not set or file missing)" + Write-Host "SSH client test skipped - proceeding with SFTP test only" + } + + $sftpPath = (Get-Content env:SFTP_PATH) + if (-not (Test-Path $sftpPath)) { + Write-Host "ERROR: wolfsftp.exe not found at $sftpPath" + exit 1 + } + + # Create test commands file + $testCommands = @" + pwd + ls + quit + "@ + $testCommands | Out-File -FilePath sftp_commands.txt -Encoding ASCII + + # Build SFTP command arguments + $sftpArgs = @( + "-u", "testuser", + "-h", "localhost", + "-p", "${{env.TEST_PORT}}" + ) + + if ("${{ matrix.client_key_source }}" -eq "store") { + $clientSubject = $env:CLIENT_CERT_SUBJECT + Write-Host "CLIENT_CERT_SUBJECT = '$clientSubject'" + if ([string]::IsNullOrEmpty($clientSubject)) { + Write-Host "ERROR: CLIENT_CERT_SUBJECT not set" + exit 1 + } + $certStoreSpec = "My:${clientSubject}:CURRENT_USER" + Write-Host "Cert store spec: $certStoreSpec" + $sftpArgs += "-W", $certStoreSpec + # CA cert for host verification (use DER format) + $caCertPath = "keys\ca-cert-ecc.der" + if (Test-Path $caCertPath) { + $caCertPathFull = (Resolve-Path $caCertPath).Path + $sftpArgs += "-A", $caCertPathFull + Write-Host "CA cert: $caCertPathFull" + } else { + Write-Host "WARNING: CA cert not found: $caCertPath (host verification may fail)" + } + # Add -X flag to ignore IP checks on peer vs peer certificate + $sftpArgs += "-X" + } elseif ("${{ matrix.client_key_source }}" -eq "x509") { + # X509 certificate authentication: use certificate + private key + # Use testuser certificate (created by renewcerts.sh) to match the username + $certPath = $env:CLIENT_CERT_FILE + $keyPath = $env:CLIENT_KEY_FILE + if ([string]::IsNullOrEmpty($certPath)) { + $certPath = "keys\testuser-cert.der" + } + if ([string]::IsNullOrEmpty($keyPath)) { + $keyPath = "keys\testuser-key.der" + } + # Fallback to fred if testuser certs don't exist + if (-not (Test-Path $certPath)) { + Write-Host "WARNING: $certPath not found, trying fred-cert.der" + $certPath = "keys\fred-cert.der" + } + if (-not (Test-Path $keyPath)) { + Write-Host "WARNING: $keyPath not found, trying fred-key.der" + $keyPath = "keys\fred-key.der" + } + $caCertPath = "keys\ca-cert-ecc.der" + + Write-Host "Verifying X509 certificate files..." + Write-Host "Current directory: $(Get-Location)" + + if (-not (Test-Path $certPath)) { + Write-Host "ERROR: Client cert not found: $certPath" + Write-Host "Files in keys directory:" + if (Test-Path "keys") { + Get-ChildItem "keys" -Filter "*fred*" | Format-Table Name, Length + } + exit 1 + } + if (-not (Test-Path $keyPath)) { + Write-Host "ERROR: Client key not found: $keyPath" + exit 1 + } + + # Verify file sizes (should not be empty) + $certInfo = Get-Item $certPath + $keyInfo = Get-Item $keyPath + Write-Host "Client cert: $($certInfo.FullName) ($($certInfo.Length) bytes)" + Write-Host "Client key: $($keyInfo.FullName) ($($keyInfo.Length) bytes)" + + # Use absolute paths to avoid any path issues + $certPathFull = (Resolve-Path $certPath).Path + $keyPathFull = (Resolve-Path $keyPath).Path + + $sftpArgs += "-J", $certPathFull + $sftpArgs += "-i", $keyPathFull + # CA cert for host verification (use DER format as per test script) + if (Test-Path $caCertPath) { + $caCertPathFull = (Resolve-Path $caCertPath).Path + $sftpArgs += "-A", $caCertPathFull + Write-Host "CA cert: $caCertPathFull" + } else { + Write-Host "WARNING: CA cert not found: $caCertPath (host verification may fail)" + } + # Add -X flag to ignore IP checks on peer vs peer certificate (as per test script) + $sftpArgs += "-X" + } + + # X509 certificate auth only - no password fallback + + Write-Host "Running: $sftpPath $($sftpArgs -join ' ')" + Write-Host "Test matrix: server=${{ matrix.server_key_source }}, client=${{ matrix.client_key_source }}" + + # Run SFTP with commands + # Note: This may fail on auth, but we're testing that key exchange works + $process = Start-Process -FilePath $sftpPath ` + -ArgumentList $sftpArgs ` + -RedirectStandardInput "sftp_commands.txt" ` + -RedirectStandardOutput "sftp_output.txt" ` + -RedirectStandardError "sftp_error.txt" ` + -Wait -NoNewWindow -PassThru + + Write-Host "SFTP exit code: $($process.ExitCode)" + Write-Host "=== SFTP Output ===" + if (Test-Path sftp_output.txt) { + Get-Content sftp_output.txt + } + Write-Host "=== SFTP Error ===" + if (Test-Path sftp_error.txt) { + Get-Content sftp_error.txt + } + + # Dump echoserver debug log (if running echoserver instead of wolfsshd) + $echoLog = $env:ECHOSERVER_LOG + if (-not [string]::IsNullOrEmpty($echoLog) -and (Test-Path $echoLog)) { + Write-Host "=== Echoserver Debug Log ===" + Get-Content $echoLog + Write-Host "=== End Echoserver Debug Log ===" + } + + # For X509 tests: check server logs for certificate verification errors + if ("${{ matrix.client_key_source }}" -eq "x509" -or "${{ matrix.client_key_source }}" -eq "store") { + Write-Host "=== Checking server logs for X509 certificate verification ===" + $serviceName = $env:SSHD_SERVICE_NAME + if ($serviceName) { + # Check Application event log for wolfSSH errors + Get-EventLog -LogName Application -Newest 50 -ErrorAction SilentlyContinue | + Where-Object { $_.Source -like "*wolf*" -or $_.Message -like "*wolf*" -or $_.Message -like "*cert*" -or $_.Message -like "*CA*" } | + Select-Object TimeGenerated, Source, EntryType, Message | Format-List + } + } + + # Check if we got past key exchange (connection established) + $output = "" + $errOut = "" + if (Test-Path sftp_output.txt) { + $output = Get-Content sftp_output.txt -Raw + } + if (Test-Path sftp_error.txt) { + $errOut = Get-Content sftp_error.txt -Raw + } + + # Failure indicators + if ($output -match "connection.*refused" -or $errOut -match "connection.*refused") { + Write-Host "ERROR: Connection refused - server may not be running" + exit 1 + } + if ($output -match "key.*exchange.*fail" -or $errOut -match "key.*exchange.*fail") { + Write-Host "ERROR: Key exchange failed - cert store key may not be working" + exit 1 + } + if ($output -match "Couldn't connect" -or $errOut -match "Couldn't connect") { + Write-Host "ERROR: SFTP could not connect" + if ("${{ matrix.client_key_source }}" -eq "x509" -or "${{ matrix.client_key_source }}" -eq "store") { + Write-Host "For X509 auth, check:" + Write-Host " 1. Server has TrustedUserCAKeys configured correctly" + Write-Host " 2. Client cert (testuser-cert.der) is signed by CA (ca-cert-ecc.pem/der) and has CN=testuser" + Write-Host " 3. Server can read the CA cert file" + } else { + Write-Host "Check authorized_keys, user, or server configuration" + } + exit 1 + } + if ($process.ExitCode -ne 0) { + Write-Host "ERROR: SFTP client exited with code $($process.ExitCode)" + exit 1 + } + + Write-Host "Test completed - key exchange and SFTP connection succeeded" + + - name: Test SSH client connection + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $sshPath = (Get-Content env:SSH_PATH -ErrorAction SilentlyContinue) + if (-not $sshPath -or -not (Test-Path $sshPath)) { + Write-Host "WARNING: wolfssh.exe not found, skipping SSH client test" + Write-Host "This is OK if the SSH client wasn't built" + exit 0 + } + + Write-Host "Found wolfssh.exe at: $sshPath" -ForegroundColor Green + + # Build SSH client command arguments + $sshArgs = @( + "-l", "testuser", + "-p", "${{env.TEST_PORT}}", + "localhost" + ) + + # Set authentication method based on client_key_source (X509 only) + if ("${{ matrix.client_key_source }}" -eq "store") { + $clientSubject = $env:CLIENT_CERT_SUBJECT + $env:WOLFSSH_CERT_STORE = "My:$clientSubject:CURRENT_USER" + Write-Host "Using cert store key via WOLFSSH_CERT_STORE: $env:WOLFSSH_CERT_STORE" -ForegroundColor Yellow + # Add -X flag to ignore IP checks + $sshArgs += "-X" + } elseif ("${{ matrix.client_key_source }}" -eq "x509") { + # X509 certificate authentication: use certificate + private key + # Use testuser certificate (created by renewcerts.sh) to match the username + $certPath = $env:CLIENT_CERT_FILE + $keyPath = $env:CLIENT_KEY_FILE + if ([string]::IsNullOrEmpty($certPath)) { + $certPath = "keys\testuser-cert.der" + } + if ([string]::IsNullOrEmpty($keyPath)) { + $keyPath = "keys\testuser-key.der" + } + # Fallback to fred if testuser certs don't exist + if (-not (Test-Path $certPath)) { + Write-Host "WARNING: $certPath not found, trying fred-cert.der" + $certPath = "keys\fred-cert.der" + } + if (-not (Test-Path $keyPath)) { + Write-Host "WARNING: $keyPath not found, trying fred-key.der" + $keyPath = "keys\fred-key.der" + } + $caCertPath = "keys\ca-cert-ecc.der" + if (-not (Test-Path $certPath) -or -not (Test-Path $keyPath)) { + Write-Host "WARNING: X509 cert/key not found, skipping SSH client test" + exit 0 + } + $sshArgs += "-J", $certPath + $sshArgs += "-i", $keyPath + if (Test-Path $caCertPath) { + $sshArgs += "-A", $caCertPath + } + # Add -X flag to ignore IP checks (as per test script) + $sshArgs += "-X" + Write-Host "Using X509 certificate authentication" -ForegroundColor Yellow + } + + Write-Host "Running: $sshPath $($sshArgs -join ' ')" -ForegroundColor Yellow + Write-Host "Test matrix: server=${{ matrix.server_key_source }}, client=${{ matrix.client_key_source }}" -ForegroundColor Cyan + + # Run SSH client with a simple command (non-interactive) + # Use -N for no command, or -c for a command + $sshArgs += "-N" # No command, just test connection + + $process = Start-Process -FilePath $sshPath ` + -ArgumentList $sshArgs ` + -RedirectStandardOutput "ssh_output.txt" ` + -RedirectStandardError "ssh_error.txt" ` + -Wait -NoNewWindow -PassThru + + Write-Host "SSH client exit code: $($process.ExitCode)" -ForegroundColor $(if ($process.ExitCode -eq 0) { "Green" } else { "Yellow" }) + Write-Host "=== SSH Client Output ===" -ForegroundColor Cyan + if (Test-Path ssh_output.txt) { + Get-Content ssh_output.txt + } + Write-Host "=== SSH Client Error ===" -ForegroundColor Cyan + if (Test-Path ssh_error.txt) { + Get-Content ssh_error.txt + } + + # Check if we got past key exchange (connection established) + # Auth failure is OK - we're testing cert store key loading + $output = "" + $errOut = "" + if (Test-Path ssh_output.txt) { + $output = Get-Content ssh_output.txt -Raw + } + if (Test-Path ssh_error.txt) { + $errOut = Get-Content ssh_error.txt -Raw + } + + # Success indicators: connection established, key exchange completed + # Failure indicators: connection refused, key exchange failed + if ($output -match "connection.*refused" -or $errOut -match "connection.*refused") { + Write-Host "ERROR: Connection refused - server may not be running" -ForegroundColor Red + exit 1 + } + if ($output -match "key.*exchange.*fail" -or $errOut -match "key.*exchange.*fail") { + Write-Host "ERROR: Key exchange failed - cert store key may not be working" -ForegroundColor Red + exit 1 + } + + Write-Host "SSH client test completed - key exchange appears to have worked" -ForegroundColor Green + + - name: Cleanup + if: always() + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # Stop echoserver if we started it (cert store matrix) + $echoserverPid = $env:ECHOSERVER_PID + if (-not [string]::IsNullOrEmpty($echoserverPid)) { + Write-Host "Stopping echoserver (PID $echoserverPid)" + Stop-Process -Id $echoserverPid -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 1 + } + + # Stop and remove wolfSSHd service + $serviceName = $env:SSHD_SERVICE_NAME + if ([string]::IsNullOrEmpty($serviceName)) { + $serviceName = "wolfsshd" + } + + $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + if ($service) { + if ($service.Status -eq 'Running') { + Write-Host "Stopping $serviceName service" + Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + } + + Write-Host "Deleting $serviceName service" + sc.exe delete $serviceName | Out-Null + Start-Sleep -Seconds 1 + } + + # Remove test certificates from store + Get-ChildItem -Path "Cert:\CurrentUser\My" | Where-Object { + $_.Subject -like "*wolfSSH-Test*" + } | Remove-Item -Force + Write-Host "Cleaned up test certificates" diff --git a/apps/wolfsshd/configuration.c b/apps/wolfsshd/configuration.c index 694825e50..b7bed7d46 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -74,12 +74,24 @@ struct WOLFSSHD_CONFIG { char* hostKeyFile; char* hostCertFile; char* userCAKeysFile; +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + char* hostKeyStore; + char* hostKeyStoreSubject; + char* hostKeyStoreFlags; +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ char* hostKeyAlgos; char* kekAlgos; char* listenAddress; char* authKeysFile; char* forceCmd; char* pidFile; +#ifdef USE_WINDOWS_API + char* winUserStores; + char* winUserDwFlags; + char* winUserPvPara; +#endif /* USE_WINDOWS_API */ WOLFSSHD_CONFIG* next; /* next config in list */ long loginTimer; word16 port; @@ -89,6 +101,8 @@ struct WOLFSSHD_CONFIG { byte permitRootLogin:1; byte permitEmptyPasswords:1; byte authKeysFileSet:1; /* if not set then no explicit authorized keys */ + byte useSystemCA:1; + byte useUserCAStore:1; }; int CountWhitespace(const char* in, int inSz, byte inv); @@ -311,7 +325,19 @@ void wolfSSHD_ConfigFree(WOLFSSHD_CONFIG* conf) FreeString(¤t->authKeysFile, heap); FreeString(¤t->hostKeyFile, heap); FreeString(¤t->hostCertFile, heap); +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + FreeString(¤t->hostKeyStore, heap); + FreeString(¤t->hostKeyStoreSubject, heap); + FreeString(¤t->hostKeyStoreFlags, heap); +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ FreeString(¤t->pidFile, heap); +#ifdef USE_WINDOWS_API + FreeString(¤t->winUserStores, heap); + FreeString(¤t->winUserDwFlags, heap); + FreeString(¤t->winUserPvPara, heap); +#endif /* USE_WINDOWS_API */ WFREE(current, heap, DYNTYPE_SSHD); current = next; @@ -338,6 +364,13 @@ enum { OPT_PROTOCOL = 9, OPT_LOGIN_GRACE_TIME = 10, OPT_HOST_KEY = 11, +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + OPT_HOST_KEY_STORE = 50, + OPT_HOST_KEY_STORE_SUBJECT = 51, + OPT_HOST_KEY_STORE_FLAGS = 52, +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ OPT_PASSWORD_AUTH = 12, OPT_PORT = 13, OPT_PERMIT_ROOT = 14, @@ -350,9 +383,22 @@ enum { OPT_TRUSTED_USER_CA_KEYS = 21, OPT_PIDFILE = 22, OPT_BANNER = 23, + OPT_TRUSTED_SYSTEM_CA_KEYS = 24, + OPT_TRUSTED_USER_CA_STORE = 25, +#ifdef USE_WINDOWS_API + OPT_WIN_USER_STORES = 26, + OPT_WIN_USER_DW_FLAGS = 27, + OPT_WIN_USER_PV_PARA = 28, +#endif /* USE_WINDOWS_API */ }; enum { - NUM_OPTIONS = 24 + NUM_OPTIONS = 26 +#ifdef USE_WINDOWS_API + + 3 +#ifdef WOLFSSH_CERTS + + 3 +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ }; static const CONFIG_OPTION options[NUM_OPTIONS] = { @@ -367,6 +413,17 @@ static const CONFIG_OPTION options[NUM_OPTIONS] = { {OPT_ACCEPT_ENV, "AcceptEnv"}, {OPT_PROTOCOL, "Protocol"}, {OPT_LOGIN_GRACE_TIME, "LoginGraceTime"}, + /* The config parser uses strncmp with the option-name length, so longer + * option names that share a common prefix MUST appear before the shorter + * one. HostKeyStoreSubject/HostKeyStoreFlags before HostKeyStore, + * and all HostKeyStore* before HostKey. */ +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + {OPT_HOST_KEY_STORE_SUBJECT, "HostKeyStoreSubject"}, + {OPT_HOST_KEY_STORE_FLAGS, "HostKeyStoreFlags"}, + {OPT_HOST_KEY_STORE, "HostKeyStore"}, +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ {OPT_HOST_KEY, "HostKey"}, {OPT_PASSWORD_AUTH, "PasswordAuthentication"}, {OPT_PORT, "Port"}, @@ -380,6 +437,13 @@ static const CONFIG_OPTION options[NUM_OPTIONS] = { {OPT_TRUSTED_USER_CA_KEYS, "TrustedUserCAKeys"}, {OPT_PIDFILE, "PidFile"}, {OPT_BANNER, "Banner"}, + {OPT_TRUSTED_SYSTEM_CA_KEYS, "wolfSSH_TrustedSystemCAKeys"}, + {OPT_TRUSTED_USER_CA_STORE, "wolfSSH_TrustedUserCaStore"}, +#ifdef USE_WINDOWS_API + {OPT_WIN_USER_STORES, "wolfSSH_WinUserStores"}, + {OPT_WIN_USER_DW_FLAGS, "wolfSSH_WinUserDwFlags"}, + {OPT_WIN_USER_PV_PARA, "wolfSSH_WinUserPvPara"}, +#endif /* USE_WINDOWS_API */ }; /* returns WS_SUCCESS on success */ @@ -1021,12 +1085,50 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, /* TODO: Add logic to check if file exists? */ ret = wolfSSHD_ConfigSetUserCAKeysFile(*conf, value); break; + case OPT_TRUSTED_SYSTEM_CA_KEYS: + ret = wolfSSHD_ConfigSetSystemCA(*conf, value); + break; case OPT_PIDFILE: ret = SetFileString(&(*conf)->pidFile, value, (*conf)->heap); break; case OPT_BANNER: ret = SetFileString(&(*conf)->banner, value, (*conf)->heap); break; + case OPT_TRUSTED_USER_CA_STORE: + ret = wolfSSHD_ConfigSetUserCAStore(*conf, value); + break; + #ifdef USE_WINDOWS_API + case OPT_WIN_USER_STORES: + ret = wolfSSHD_ConfigSetWinUserStores(*conf, value); + break; + case OPT_WIN_USER_DW_FLAGS: + ret = wolfSSHD_ConfigSetWinUserDwFlags(*conf, value); + break; + case OPT_WIN_USER_PV_PARA: + ret = wolfSSHD_ConfigSetWinUserPvPara(*conf, value); + break; + #endif /* USE_WINDOWS_API */ + #ifdef USE_WINDOWS_API + #ifdef WOLFSSH_CERTS + case OPT_HOST_KEY_STORE: + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Parsed HostKeyStore = '%s'", value); + ret = SetFileString(&(*conf)->hostKeyStore, value, (*conf)->heap); + break; + case OPT_HOST_KEY_STORE_SUBJECT: + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Parsed HostKeyStoreSubject = '%s'", value); + ret = SetFileString(&(*conf)->hostKeyStoreSubject, value, + (*conf)->heap); + break; + case OPT_HOST_KEY_STORE_FLAGS: + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Parsed HostKeyStoreFlags = '%s'", value); + ret = SetFileString(&(*conf)->hostKeyStoreFlags, value, + (*conf)->heap); + break; + #endif /* WOLFSSH_CERTS */ + #endif /* USE_WINDOWS_API */ default: break; } @@ -1309,6 +1411,178 @@ char* wolfSSHD_ConfigGetHostCertFile(const WOLFSSHD_CONFIG* conf) return ret; } + +/* getter function for if using system CAs + * return 1 if true and 0 if false */ +int wolfSSHD_ConfigGetSystemCA(const WOLFSSHD_CONFIG* conf) +{ + if (conf != NULL) { + return conf->useSystemCA; + } + return 0; +} + + +/* setter function for if using system CAs + * 'yes' if true and 'no' if false + * returns WS_SUCCESS on success */ +int wolfSSHD_ConfigSetSystemCA(WOLFSSHD_CONFIG* conf, const char* value) +{ + int ret = WS_SUCCESS; + + if (conf != NULL) { + if (WSTRCMP(value, "yes") == 0) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] System CAs enabled"); + conf->useSystemCA = 1; + } + else if (WSTRCMP(value, "no") == 0) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] System CAs disabled"); + conf->useSystemCA = 0; + } + else { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] System CAs unexpected flag"); + ret = WS_FATAL_ERROR; + } + } + + return ret; +} + +/* getter function for if using user CA store + * return 1 if true and 0 if false */ +int wolfSSHD_ConfigGetUserCAStore(const WOLFSSHD_CONFIG* conf) +{ + if (conf != NULL) { + return conf->useUserCAStore; + } + return 0; +} + + +/* setter function for if using user CA store + * 'yes' if true and 'no' if false + * returns WS_SUCCESS on success */ +int wolfSSHD_ConfigSetUserCAStore(WOLFSSHD_CONFIG* conf, const char* value) +{ + int ret = WS_SUCCESS; + + if (conf != NULL) { + if (WSTRCMP(value, "yes") == 0) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] User CA store enabled. Note this " + "is currently only supported on Windows."); + conf->useUserCAStore = 1; + } + else if (WSTRCMP(value, "no") == 0) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] User CA store disabled"); + conf->useUserCAStore = 0; + } + else { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] User CA store unexpected flag"); + ret = WS_FATAL_ERROR; + } + } + + return ret; +} + +#ifdef USE_WINDOWS_API +char* wolfSSHD_ConfigGetWinUserStores(WOLFSSHD_CONFIG* conf) +{ + if (conf != NULL) { + if (conf->winUserStores == NULL) { + /* If no value was specified, default to CERT_STORE_PROV_SYSTEM */ + CreateString(&conf->winUserStores, "CERT_STORE_PROV_SYSTEM", + (int)WSTRLEN("CERT_STORE_PROV_SYSTEM"), conf->heap); + } + + return conf->winUserStores; + } + + return NULL; +} + +int wolfSSHD_ConfigSetWinUserStores(WOLFSSHD_CONFIG* conf, const char* value) +{ + int ret = WS_SUCCESS; + + if (conf == NULL) { + ret = WS_BAD_ARGUMENT; + } + + if (ret == WS_SUCCESS) { + ret = CreateString(&conf->winUserStores, value, + (int)WSTRLEN(value), conf->heap); + } + + return ret; +} + +char* wolfSSHD_ConfigGetWinUserDwFlags(WOLFSSHD_CONFIG* conf) +{ + if (conf != NULL) { + if (conf->winUserDwFlags == NULL) { + /* If no value was specified, default to + * CERT_SYSTEM_STORE_CURRENT_USER */ + CreateString(&conf->winUserDwFlags, + "CERT_SYSTEM_STORE_CURRENT_USER", + (int)WSTRLEN("CERT_SYSTEM_STORE_CURRENT_USER"), + conf->heap); + } + + return conf->winUserDwFlags; + } + + return NULL; +} + +int wolfSSHD_ConfigSetWinUserDwFlags(WOLFSSHD_CONFIG* conf, const char* value) +{ + int ret = WS_SUCCESS; + + if (conf == NULL) { + ret = WS_BAD_ARGUMENT; + } + + if (ret == WS_SUCCESS) { + ret = CreateString(&conf->winUserDwFlags, value, + (int)WSTRLEN(value), conf->heap); + } + + return ret; +} + +char* wolfSSHD_ConfigGetWinUserPvPara(WOLFSSHD_CONFIG* conf) +{ + if (conf != NULL) { + if (conf->winUserPvPara == NULL) { + /* If no value was specified, default to MY */ + CreateString(&conf->winUserPvPara, "MY", + (int)WSTRLEN("MY"), conf->heap); + } + + return conf->winUserPvPara; + } + + return NULL; +} + +int wolfSSHD_ConfigSetWinUserPvPara(WOLFSSHD_CONFIG* conf, const char* value) +{ + int ret = WS_SUCCESS; + + if (conf == NULL) { + ret = WS_BAD_ARGUMENT; + } + + if (ret == WS_SUCCESS) { + ret = CreateString(&conf->winUserPvPara, value, + (int)WSTRLEN(value), conf->heap); + } + + return ret; +} +#endif /* USE_WINDOWS_API */ + char* wolfSSHD_ConfigGetUserCAKeysFile(const WOLFSSHD_CONFIG* conf) { char* ret = NULL; @@ -1342,6 +1616,45 @@ int SetFileString(char** dst, const char* src, void* heap) return ret; } +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS +char* wolfSSHD_ConfigGetHostKeyStore(const WOLFSSHD_CONFIG* conf) +{ + char* ret = NULL; + + if (conf != NULL) { + ret = conf->hostKeyStore; + } + + return ret; +} + + +char* wolfSSHD_ConfigGetHostKeyStoreSubject(const WOLFSSHD_CONFIG* conf) +{ + char* ret = NULL; + + if (conf != NULL) { + ret = conf->hostKeyStoreSubject; + } + + return ret; +} + + +char* wolfSSHD_ConfigGetHostKeyStoreFlags(const WOLFSSHD_CONFIG* conf) +{ + char* ret = NULL; + + if (conf != NULL) { + ret = conf->hostKeyStoreFlags; + } + + return ret; +} +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ + int wolfSSHD_ConfigSetHostKeyFile(WOLFSSHD_CONFIG* conf, const char* file) { int ret = WS_SUCCESS; diff --git a/apps/wolfsshd/configuration.h b/apps/wolfsshd/configuration.h index 064db6bbb..657009c34 100644 --- a/apps/wolfsshd/configuration.h +++ b/apps/wolfsshd/configuration.h @@ -42,6 +42,25 @@ char* wolfSSHD_ConfigGetHostCertFile(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetUserCAKeysFile(const WOLFSSHD_CONFIG* conf); int wolfSSHD_ConfigSetHostKeyFile(WOLFSSHD_CONFIG* conf, const char* file); int wolfSSHD_ConfigSetHostCertFile(WOLFSSHD_CONFIG* conf, const char* file); +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS +char* wolfSSHD_ConfigGetHostKeyStore(const WOLFSSHD_CONFIG* conf); +char* wolfSSHD_ConfigGetHostKeyStoreSubject(const WOLFSSHD_CONFIG* conf); +char* wolfSSHD_ConfigGetHostKeyStoreFlags(const WOLFSSHD_CONFIG* conf); +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ +int wolfSSHD_ConfigSetSystemCA(WOLFSSHD_CONFIG* conf, const char* value); +int wolfSSHD_ConfigGetSystemCA(const WOLFSSHD_CONFIG* conf); +int wolfSSHD_ConfigSetUserCAStore(WOLFSSHD_CONFIG* conf, const char* value); +int wolfSSHD_ConfigGetUserCAStore(const WOLFSSHD_CONFIG* conf); +#ifdef USE_WINDOWS_API +char* wolfSSHD_ConfigGetWinUserStores(WOLFSSHD_CONFIG* conf); +int wolfSSHD_ConfigSetWinUserStores(WOLFSSHD_CONFIG* conf, const char* value); +char* wolfSSHD_ConfigGetWinUserDwFlags(WOLFSSHD_CONFIG* conf); +int wolfSSHD_ConfigSetWinUserDwFlags(WOLFSSHD_CONFIG* conf, const char* value); +char* wolfSSHD_ConfigGetWinUserPvPara(WOLFSSHD_CONFIG* conf); +int wolfSSHD_ConfigSetWinUserPvPara(WOLFSSHD_CONFIG* conf, const char* value); +#endif /* USE_WINDOWS_API */ int wolfSSHD_ConfigSetUserCAKeysFile(WOLFSSHD_CONFIG* conf, const char* file); word16 wolfSSHD_ConfigGetPort(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetAuthKeysFile(const WOLFSSHD_CONFIG* conf); diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 2c0682d50..a2a3985d1 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -38,6 +38,20 @@ #include #include +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + #include + #include + #include + #ifndef CERT_SYSTEM_STORE_CURRENT_USER + #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE + #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 + #endif +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ + #define WOLFSSH_TEST_SERVER #include @@ -340,14 +354,80 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, /* Load in host private key */ if (ret == WS_SUCCESS) { +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + char* hostKeyStore = wolfSSHD_ConfigGetHostKeyStore(conf); + char* hostKeyStoreSubject = wolfSSHD_ConfigGetHostKeyStoreSubject(conf); + char* hostKeyStoreFlags = wolfSSHD_ConfigGetHostKeyStoreFlags(conf); + + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Cert store code compiled in. " + "hostKeyStore=%s, hostKeyStoreSubject=%s, hostKeyStoreFlags=%s", + hostKeyStore ? hostKeyStore : "(null)", + hostKeyStoreSubject ? hostKeyStoreSubject : "(null)", + hostKeyStoreFlags ? hostKeyStoreFlags : "(null)"); + + if (hostKeyStore != NULL && hostKeyStoreSubject != NULL) { + /* Use cert store host key */ + wchar_t* wStoreName = NULL; + wchar_t* wSubjectName = NULL; + DWORD dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; + int storeNameLen, subjectNameLen; + + /* Parse flags if provided */ + if (hostKeyStoreFlags != NULL) { + if (WSTRCMP(hostKeyStoreFlags, "CURRENT_USER") == 0) { + dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; + } else if (WSTRCMP(hostKeyStoreFlags, "LOCAL_MACHINE") == 0) { + dwFlags = CERT_SYSTEM_STORE_LOCAL_MACHINE; + } else { + dwFlags = (DWORD)atoi(hostKeyStoreFlags); + } + } + + /* Convert to wide strings */ + storeNameLen = MultiByteToWideChar(CP_UTF8, 0, hostKeyStore, -1, NULL, 0); + subjectNameLen = MultiByteToWideChar(CP_UTF8, 0, hostKeyStoreSubject, -1, NULL, 0); + + wStoreName = (wchar_t*)WMALLOC(storeNameLen * sizeof(wchar_t), heap, DYNTYPE_SSHD); + wSubjectName = (wchar_t*)WMALLOC(subjectNameLen * sizeof(wchar_t), heap, DYNTYPE_SSHD); + + if (wStoreName == NULL || wSubjectName == NULL) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Memory allocation failed for cert store strings"); + ret = WS_MEMORY_E; + } else { + MultiByteToWideChar(CP_UTF8, 0, hostKeyStore, -1, wStoreName, storeNameLen); + MultiByteToWideChar(CP_UTF8, 0, hostKeyStoreSubject, -1, wSubjectName, subjectNameLen); + + ret = wolfSSH_CTX_UsePrivateKey_fromStore(*ctx, wStoreName, dwFlags, wSubjectName); + if (ret != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Failed to load host key from certificate store"); + } + + WFREE(wStoreName, heap, DYNTYPE_SSHD); + WFREE(wSubjectName, heap, DYNTYPE_SSHD); + } + } else +#else + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] WOLFSSH_CERTS not defined - cert store support disabled"); +#endif /* WOLFSSH_CERTS */ +#else + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] USE_WINDOWS_API not defined - cert store support disabled"); +#endif /* USE_WINDOWS_API */ + { + char* hostKey = wolfSSHD_ConfigGetHostKeyFile(conf); - char* hostKey = wolfSSHD_ConfigGetHostKeyFile(conf); + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] File-based host key path entered. hostKey=%s", + hostKey ? hostKey : "(null)"); - if (hostKey == NULL) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No host private key set"); - ret = WS_BAD_ARGUMENT; - } - else { + if (hostKey == NULL) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No host private key set"); + ret = WS_BAD_ARGUMENT; + } + else { byte* data; word32 dataSz = 0; @@ -384,6 +464,7 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, wc_FreeDer(&der); } } + } } #if defined(WOLFSSH_OSSH_CERTS) || defined(WOLFSSH_CERTS) @@ -433,6 +514,62 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, #endif /* WOLFSSH_OSSH_CERTS || WOLFSSH_CERTS */ #ifdef WOLFSSH_CERTS + /* check if loading in system and/or user CA certs */ + #ifdef WOLFSSL_SYS_CA_CERTS + if (ret == WS_SUCCESS && (wolfSSHD_ConfigGetSystemCA(conf) + || wolfSSHD_ConfigGetUserCAStore(conf))) { + WOLFSSL_CTX* sslCtx; + + wolfSSH_Log(WS_LOG_INFO, "[SSHD] Using system CAs"); + sslCtx = wolfSSL_CTX_new(wolfSSLv23_server_method()); + if (sslCtx == NULL) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] Unable to create temporary CTX"); + ret = WS_FATAL_ERROR; + } + + if (ret == WS_SUCCESS) { + if (wolfSSHD_ConfigGetSystemCA(conf)) { + if (wolfSSL_CTX_load_system_CA_certs(sslCtx) != WOLFSSL_SUCCESS) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] Issue loading system CAs"); + ret = WS_FATAL_ERROR; + } + } + } + + if (ret == WS_SUCCESS) { + if (wolfSSHD_ConfigGetUserCAStore(conf)) { +#ifdef USE_WINDOWS_API + if (wolfSSL_CTX_load_windows_user_CA_certs(sslCtx, + wolfSSHD_ConfigGetWinUserStores(conf), + wolfSSHD_ConfigGetWinUserDwFlags(conf), + wolfSSHD_ConfigGetWinUserPvPara(conf)) != WOLFSSL_SUCCESS) { + wolfSSH_Log(WS_LOG_INFO, "[SSHD] Issue loading user CAs"); + ret = WS_FATAL_ERROR; + } +#else + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] User CA store is only supported on Windows"); + ret = WS_BAD_ARGUMENT; +#endif /* USE_WINDOWS_API */ + } + } + + if (ret == WS_SUCCESS) { + if (wolfSSH_SetCertManager(*ctx, + wolfSSL_CTX_GetCertManager(sslCtx)) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Issue copying over system CAs"); + ret = WS_FATAL_ERROR; + } + } + + if (sslCtx != NULL) { + wolfSSL_CTX_free(sslCtx); + } + } + #endif + + /* load in CA certs from file set */ if (ret == WS_SUCCESS) { char* caCert = wolfSSHD_ConfigGetUserCAKeysFile(conf); if (caCert != NULL) { @@ -2401,6 +2538,24 @@ static int StartSSHD(int argc, char** argv) } } + if (logFile == NULL) { + logFile = stderr; + } +#ifdef _WIN32 + /* The early -D detection (wide-string comparison of cmdArgs before + * conversion) may have set ServiceDebugCb even when -D was supplied. + * Now that mygetopt has been processed, restore the file-based + * callback in any case where output should go to logFile: + * - isDaemon==0 → running interactively, logs to logFile (stderr) + * - isDaemon==1 but -E was used → logs to the specified file + * This must happen BEFORE config/SetupCTX so their log messages are + * captured in the file (or stderr) rather than lost to + * OutputDebugString. */ + if (!isDaemon || logFile != stderr) { + wolfSSH_SetLoggingCb(wolfSSHDLoggingCb); + } +#endif + if (ret == WS_SUCCESS) { ret = wolfSSHD_ConfigLoad(conf, configFile); if (ret != WS_SUCCESS) { @@ -2432,10 +2587,6 @@ static int StartSSHD(int argc, char** argv) } } - if (logFile == NULL) { - logFile = stderr; - } - /* run as a daemon or service */ #ifndef WIN32 if (ret == WS_SUCCESS && isDaemon) { diff --git a/examples/client/common.c b/examples/client/common.c index 2a40b8e87..29bc781da 100644 --- a/examples/client/common.c +++ b/examples/client/common.c @@ -47,6 +47,11 @@ #ifdef WOLFSSH_CERTS #include + #ifdef USE_WINDOWS_API + #include + #include + #include + #endif /* USE_WINDOWS_API */ #endif static byte userPublicKeyBuf[512]; @@ -1138,3 +1143,85 @@ void ClientFreeBuffers(const char* pubKeyName, const char* privKeyName, WFREE(keyboardResponseLengths, NULL, 0); #endif } + +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS +int ClientSetPrivateKeyFromStore(WOLFSSH_CTX* ctx, + const wchar_t* storeName, DWORD dwFlags, const wchar_t* subjectName) +{ + int ret = WS_SUCCESS; + + if (ctx == NULL || storeName == NULL || subjectName == NULL) { + return WS_BAD_ARGUMENT; + } + + ret = wolfSSH_CTX_UsePrivateKey_fromStore(ctx, storeName, dwFlags, subjectName); + if (ret != WS_SUCCESS) { + fprintf(stderr, "Error loading private key from certificate store: %d\n", ret); + } + + return ret; +} + + +/* After loading a cert store key, populate the global auth callback variables + * (userPublicKeyType, userPublicKey, etc.) so that ClientUserAuth can present + * the certificate for public key authentication. + * For x509 cert auth the "public key" is the DER certificate, and the type + * is the x509v3 name that matches the key algorithm. */ +int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx) +{ + word32 i; + + if (ctx == NULL) + return WS_BAD_ARGUMENT; + + for (i = 0; i < ctx->privateKeyCount && i < WOLFSSH_MAX_PVT_KEYS; i++) { + WOLFSSH_PVT_KEY* pvtKey = &ctx->privateKey[i]; + if (!pvtKey->useCertStore) + continue; + + /* Point userPublicKey at the DER certificate stored in the CTX. + * This is safe because the CTX outlives the auth callback. */ + userPublicKey = pvtKey->cert; + userPublicKeySz = pvtKey->certSz; + + /* Map the internal key format to the x509v3 SSH type name. */ + switch (pvtKey->publicKeyFmt) { + case ID_SSH_RSA: + case ID_RSA_SHA2_256: + case ID_RSA_SHA2_512: + userPublicKeyType = (const byte*)"x509v3-ssh-rsa"; + break; + case ID_ECDSA_SHA2_NISTP256: + case ID_X509V3_ECDSA_SHA2_NISTP256: + userPublicKeyType = (const byte*)"x509v3-ecdsa-sha2-nistp256"; + break; + case ID_ECDSA_SHA2_NISTP384: + case ID_X509V3_ECDSA_SHA2_NISTP384: + userPublicKeyType = (const byte*)"x509v3-ecdsa-sha2-nistp384"; + break; + case ID_ECDSA_SHA2_NISTP521: + case ID_X509V3_ECDSA_SHA2_NISTP521: + userPublicKeyType = (const byte*)"x509v3-ecdsa-sha2-nistp521"; + break; + default: + fprintf(stderr, "Unsupported cert store key type: %d\n", + pvtKey->publicKeyFmt); + return WS_BAD_ARGUMENT; + } + userPublicKeyTypeSz = (word32)WSTRLEN((const char*)userPublicKeyType); + + /* No in-memory private key — signing goes through the cert store. */ + userPrivateKey = NULL; + userPrivateKeySz = 0; + + pubKeyLoaded = 1; + return WS_SUCCESS; + } + + fprintf(stderr, "No cert store key found in CTX\n"); + return WS_BAD_ARGUMENT; +} +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ diff --git a/examples/client/common.h b/examples/client/common.h index 6ea330c2e..a9cc7309b 100644 --- a/examples/client/common.h +++ b/examples/client/common.h @@ -35,6 +35,13 @@ void ClientFreeBuffers(const char* pubKeyName, const char* privKeyName, #ifdef WOLFSSH_TPM int ClientSetTpm(WOLFSSH* ssh); #endif +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS +int ClientSetPrivateKeyFromStore(WOLFSSH_CTX* ctx, + const wchar_t* storeName, DWORD dwFlags, const wchar_t* subjectName); +int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx); +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ #endif /* WOLFSSH_COMMON_H */ diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index a6a83140f..4c6fee108 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -41,12 +41,14 @@ #include #include #include +#include #include #include #include #include #include "examples/echoserver/echoserver.h" +#include "examples/client/common.h" #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) #include @@ -113,6 +115,16 @@ #define SOCKET_EWOULDBLOCK WSAEWOULDBLOCK #endif +#if defined(USE_WINDOWS_API) && defined(WOLFSSH_CERTS) + #include + #include + #ifndef CERT_SYSTEM_STORE_CURRENT_USER + #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE + #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 + #endif +#endif #ifndef NO_WOLFSSH_SERVER @@ -2528,6 +2540,9 @@ static void ShowUsage(void) printf(" -x set the comma separated list of key exchange algos " "to use\n"); printf(" -m set the comma separated list of mac algos to use\n"); +#if defined(USE_WINDOWS_API) && defined(WOLFSSH_CERTS) + printf(" -W Windows cert store: \"store:subject:flags\" (e.g. My:CN=Server:CURRENT_USER)\n"); +#endif printf(" -b test user auth would block\n"); printf(" -H set test highwater callback\n"); } @@ -2626,6 +2641,9 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #ifdef WOLFSSH_CERTS char* caCert = NULL; #endif + #if defined(USE_WINDOWS_API) && defined(WOLFSSH_CERTS) + const char* certStoreSpec = NULL; + #endif int argc = serverArgs->argc; char** argv = serverArgs->argv; @@ -2634,8 +2652,11 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) kbAuthData.promptCount = 0; #endif + #if defined(USE_WINDOWS_API) && defined(WOLFSSH_CERTS) + certStoreSpec = getenv("WOLFSSH_CERT_STORE"); + #endif if (argc > 0) { - const char* optlist = "?1a:d:efEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:H"; + const char* optlist = "?1a:d:efEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:HW:"; myoptind = 0; while ((ch = mygetopt(argc, argv, optlist)) != -1) { switch (ch) { @@ -2751,6 +2772,12 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) useCustomHighWaterCb = 1; break; + #if defined(USE_WINDOWS_API) && defined(WOLFSSH_CERTS) + case 'W': + certStoreSpec = myoptarg; + break; + #endif + default: ShowUsage(); serverArgs->return_code = MY_EX_USAGE; @@ -2931,28 +2958,53 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #endif bufSz = EXAMPLE_KEYLOAD_BUFFER_SZ; - bufSz = load_key(peerEcc, keyLoadBuf, bufSz); - if (bufSz == 0) { - ES_ERROR("Couldn't load first key file.\n"); - } - if (wolfSSH_CTX_UsePrivateKey_buffer(ctx, keyLoadBuf, bufSz, - WOLFSSH_FORMAT_ASN1) < 0) { - ES_ERROR("Couldn't use first key buffer.\n"); - } +#if defined(USE_WINDOWS_API) && defined(WOLFSSH_CERTS) + if (certStoreSpec != NULL) { + /* Load host key from Windows certificate store */ + wchar_t* wStoreName = NULL; + wchar_t* wSubjectName = NULL; + DWORD dwFlags = 0; + int ret; - #if !defined(WOLFSSH_NO_RSA) && !defined(WOLFSSH_NO_ECC) - peerEcc = !peerEcc; - bufSz = EXAMPLE_KEYLOAD_BUFFER_SZ; + ret = wolfSSH_ParseCertStoreSpec(certStoreSpec, &wStoreName, + &wSubjectName, &dwFlags, NULL); + if (ret != WS_SUCCESS) { + ES_ERROR("Invalid cert store spec. Use: store:subject:flags\n"); + } - bufSz = load_key(peerEcc, keyLoadBuf, bufSz); - if (bufSz == 0) { - ES_ERROR("Couldn't load second key file.\n"); - } - if (wolfSSH_CTX_UsePrivateKey_buffer(ctx, keyLoadBuf, bufSz, - WOLFSSH_FORMAT_ASN1) < 0) { - ES_ERROR("Couldn't use second key buffer.\n"); + ret = wolfSSH_CTX_UsePrivateKey_fromStore(ctx, wStoreName, + dwFlags, wSubjectName); + WFREE(wStoreName, NULL, DYNTYPE_TEMP); + WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + if (ret != WS_SUCCESS) { + ES_ERROR("Couldn't load host key from certificate store.\n"); + } + } else +#endif + { + bufSz = load_key(peerEcc, keyLoadBuf, bufSz); + if (bufSz == 0) { + ES_ERROR("Couldn't load first key file.\n"); + } + if (wolfSSH_CTX_UsePrivateKey_buffer(ctx, keyLoadBuf, bufSz, + WOLFSSH_FORMAT_ASN1) < 0) { + ES_ERROR("Couldn't use first key buffer.\n"); + } + + #if !defined(WOLFSSH_NO_RSA) && !defined(WOLFSSH_NO_ECC) + peerEcc = !peerEcc; + bufSz = EXAMPLE_KEYLOAD_BUFFER_SZ; + + bufSz = load_key(peerEcc, keyLoadBuf, bufSz); + if (bufSz == 0) { + ES_ERROR("Couldn't load second key file.\n"); + } + if (wolfSSH_CTX_UsePrivateKey_buffer(ctx, keyLoadBuf, bufSz, + WOLFSSH_FORMAT_ASN1) < 0) { + ES_ERROR("Couldn't use second key buffer.\n"); + } + #endif } - #endif #ifndef NO_FILESYSTEM if (userPubKey) { @@ -3249,7 +3301,29 @@ int wolfSSH_Echoserver(int argc, char** argv) #endif #if !defined(WOLFSSL_NUCLEUS) && !defined(INTEGRITY) && !defined(__INTEGRITY) - ChangeToWolfSshRoot(); + { + int useStore = 0; + #if defined(USE_WINDOWS_API) && defined(WOLFSSH_CERTS) + /* When using the Windows certificate store for host keys, the + * echoserver does not need file-based keys, so skip the root + * directory search that looks for ./keys/server-key-rsa.pem. */ + if (getenv("WOLFSSH_CERT_STORE") != NULL) { + useStore = 1; + } + else { + int i; + for (i = 1; i < argc; i++) { + if (WSTRCMP(argv[i], "-W") == 0) { + useStore = 1; + break; + } + } + } + #endif + if (!useStore) { + ChangeToWolfSshRoot(); + } + } #endif #ifndef NO_WOLFSSH_SERVER echoserver_test(&args); diff --git a/examples/sftpclient/sftpclient.c b/examples/sftpclient/sftpclient.c index be4a3687b..d225363bb 100644 --- a/examples/sftpclient/sftpclient.c +++ b/examples/sftpclient/sftpclient.c @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,17 @@ #ifdef WOLFSSH_CERTS #include + #ifdef USE_WINDOWS_API + #include + #include + #include + #ifndef CERT_SYSTEM_STORE_CURRENT_USER + #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE + #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 + #endif + #endif /* USE_WINDOWS_API */ #endif #if defined(WOLFSSH_SFTP) && !defined(NO_WOLFSSH_CLIENT) @@ -388,6 +400,12 @@ static void ShowUsage(void) printf(" -g put local filename as remote filename\n"); printf(" -G get remote filename as local filename\n"); printf(" -i filename for the user's private key\n"); +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + printf(" -W Windows cert store: \"store:subject:flags\"\n"); + printf(" Example: -W \"My:CN=MyCert:CURRENT_USER\"\n"); +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ #ifdef WOLFSSH_CERTS printf(" -J filename for DER certificate to use\n"); printf(" Certificate example : client -u orange \\\n"); @@ -1254,13 +1272,24 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) char* pubKeyName = NULL; char* certName = NULL; char* caCert = NULL; +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + const char* certStoreSpec = NULL; /* Format: "store:subject:flags" */ +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ SFTPC_HEAP_HINT* heap = NULL; int argc = ((func_args*)args)->argc; char** argv = ((func_args*)args)->argv; ((func_args*)args)->return_code = 0; - while ((ch = mygetopt(argc, argv, "?d:gh:i:j:l:p:r:u:EGNP:J:A:X")) != -1) { + while ((ch = mygetopt(argc, argv, "?d:gh:i:j:l:p:r:u:EGNP:J:A:X" +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + "W:" +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ + )) != -1) { switch (ch) { case 'd': defaultSftpPath = myoptarg; @@ -1338,6 +1367,14 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) #endif #endif +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + case 'W': + certStoreSpec = myoptarg; + break; +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ + case '?': ShowUsage(); exit(EXIT_SUCCESS); @@ -1384,26 +1421,68 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) } #endif /* WOLFSSH_STATIC_MEMORY */ - ret = ClientSetPrivateKey(privKeyName, userEcc, heap, NULL); - if (ret != 0) { - err_sys("Error setting private key"); - } +#ifdef USE_WINDOWS_API #ifdef WOLFSSH_CERTS - /* passed in certificate to use */ - if (certName) { - ret = ClientUseCert(certName, heap); - } - else -#endif + if (certStoreSpec != NULL) { + wchar_t* wStoreName = NULL; + wchar_t* wSubjectName = NULL; + DWORD dwFlags = 0; + + ret = wolfSSH_ParseCertStoreSpec(certStoreSpec, &wStoreName, + &wSubjectName, &dwFlags, NULL); + if (ret != WS_SUCCESS) { + err_sys("Invalid cert store spec. Use: store:subject:flags"); + } + + /* Create context first */ + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, heap); + if (ctx == NULL) { + err_sys("Couldn't create wolfSSH client context."); + } + + /* Set private key from cert store */ + ret = ClientSetPrivateKeyFromStore(ctx, wStoreName, dwFlags, + wSubjectName); + if (ret != WS_SUCCESS) { + err_sys("Error setting private key from certificate store"); + } + + /* Set up auth callback globals (public key type, cert DER) so + * that ClientUserAuth presents the certificate for public key + * authentication. */ + ret = ClientSetupCertStoreAuth(ctx); + if (ret != WS_SUCCESS) { + err_sys("Error setting up cert store auth"); + } + + WFREE(wStoreName, NULL, DYNTYPE_TEMP); + WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + } else +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ { - ret = ClientUsePubKey(pubKeyName, 0, heap); - } - if (ret != 0) { - err_sys("Error setting public key"); - } + ret = ClientSetPrivateKey(privKeyName, userEcc, heap, NULL); + if (ret != 0) { + err_sys("Error setting private key"); + } + + #ifdef WOLFSSH_CERTS + /* passed in certificate to use */ + if (certName) { + ret = ClientUseCert(certName, heap); + } + else + #endif + { + ret = ClientUsePubKey(pubKeyName, 0, heap); + } + if (ret != 0) { + err_sys("Error setting public key"); + } - ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, heap); + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, heap); + } if (ctx == NULL) err_sys("Couldn't create wolfSSH client context."); diff --git a/ide/winvs/api-test/api-test.vcxproj b/ide/winvs/api-test/api-test.vcxproj index b0289307d..2524860b7 100644 --- a/ide/winvs/api-test/api-test.vcxproj +++ b/ide/winvs/api-test/api-test.vcxproj @@ -1,4 +1,4 @@ - + @@ -346,7 +346,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32) @@ -382,7 +382,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32) @@ -418,7 +418,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64) @@ -454,7 +454,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64) @@ -491,7 +491,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32) @@ -531,7 +531,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32) @@ -571,7 +571,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64) @@ -611,7 +611,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64) diff --git a/ide/winvs/client/client.vcxproj b/ide/winvs/client/client.vcxproj index ce9887b3b..d8d0d838c 100644 --- a/ide/winvs/client/client.vcxproj +++ b/ide/winvs/client/client.vcxproj @@ -346,7 +346,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32) @@ -382,7 +382,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32) @@ -418,7 +418,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64) @@ -454,7 +454,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64) @@ -491,7 +491,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32) @@ -531,7 +531,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32) @@ -571,7 +571,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64) @@ -611,7 +611,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64) diff --git a/ide/winvs/echoserver/echoserver.vcxproj b/ide/winvs/echoserver/echoserver.vcxproj index e220247c7..c5715bc14 100644 --- a/ide/winvs/echoserver/echoserver.vcxproj +++ b/ide/winvs/echoserver/echoserver.vcxproj @@ -345,7 +345,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32) @@ -381,7 +381,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32) @@ -417,7 +417,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64) @@ -453,7 +453,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64) @@ -490,7 +490,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32) @@ -530,7 +530,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32) @@ -570,7 +570,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64) @@ -610,7 +610,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64) diff --git a/ide/winvs/unit-test/unit-test.vcxproj b/ide/winvs/unit-test/unit-test.vcxproj index 383de1ee9..cf1e70a18 100644 --- a/ide/winvs/unit-test/unit-test.vcxproj +++ b/ide/winvs/unit-test/unit-test.vcxproj @@ -1,4 +1,4 @@ - + @@ -345,7 +345,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32) @@ -381,7 +381,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32) @@ -417,7 +417,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64) @@ -453,7 +453,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64) @@ -490,7 +490,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32) @@ -530,7 +530,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32) @@ -570,7 +570,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64) @@ -610,7 +610,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64) diff --git a/ide/winvs/wolfsftp-client/wolfsftp-client.vcxproj b/ide/winvs/wolfsftp-client/wolfsftp-client.vcxproj index 2a4a0f3b1..94e8cad8c 100644 --- a/ide/winvs/wolfsftp-client/wolfsftp-client.vcxproj +++ b/ide/winvs/wolfsftp-client/wolfsftp-client.vcxproj @@ -347,7 +347,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32) @@ -401,7 +401,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32) @@ -419,7 +419,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64) @@ -473,7 +473,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64) @@ -492,7 +492,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32) @@ -532,7 +532,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32) @@ -572,7 +572,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64) @@ -612,7 +612,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64) diff --git a/ide/winvs/wolfssh/wolfssh.vcxproj b/ide/winvs/wolfssh/wolfssh.vcxproj index 8bff98e6d..631bd3a6d 100644 --- a/ide/winvs/wolfssh/wolfssh.vcxproj +++ b/ide/winvs/wolfssh/wolfssh.vcxproj @@ -364,7 +364,7 @@ Windows true $(wolfCryptDllDebug32) - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) @@ -427,7 +427,7 @@ Windows true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) $(wolfCryptDllDebug64) @@ -501,7 +501,7 @@ true true $(wolfCryptDllRelease32) - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) @@ -576,7 +576,7 @@ true true true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) $(wolfCryptDllRelease64) diff --git a/ide/winvs/wolfsshd/wolfsshd.vcxproj b/ide/winvs/wolfsshd/wolfsshd.vcxproj index 2b14feaa2..ea006b8c0 100644 --- a/ide/winvs/wolfsshd/wolfsshd.vcxproj +++ b/ide/winvs/wolfsshd/wolfsshd.vcxproj @@ -337,7 +337,7 @@ Console true ..\..\..\..\wolfssl\Debug\x64;..\Debug\x64 - wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) @@ -385,7 +385,7 @@ true true true - wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) ..\..\..\..\wolfssl\Release\x64;..\Release\x64 @@ -417,7 +417,7 @@ Level3 - wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) $(wolfCryptDLLRelease64) true true diff --git a/src/certman.c b/src/certman.c index 89a6f8265..7f3f75619 100644 --- a/src/certman.c +++ b/src/certman.c @@ -36,7 +36,6 @@ #endif -#include #include #include #include @@ -44,6 +43,16 @@ #include #include +#if defined(USE_WINDOWS_API) && defined(WOLFSSH_CERTS) + #include + #include + #ifndef CERT_SYSTEM_STORE_CURRENT_USER + #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE + #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 + #endif +#endif #ifdef WOLFSSH_CERTS @@ -84,6 +93,26 @@ struct WOLFSSH_CERTMAN { }; +/* used to import an external cert manager, frees and replaces existing manager + * returns WS_SUCCESS on success + */ +int wolfSSH_SetCertManager(WOLFSSH_CTX* ctx, WOLFSSL_CERT_MANAGER* cm) +{ + if (ctx == NULL || cm == NULL || ctx->certMan == NULL) { + return WS_BAD_ARGUMENT; + } + + /* free up existing cm if present */ + if (ctx->certMan->cm != NULL) { + wolfSSL_CertManagerFree(ctx->certMan->cm); + } + wolfSSL_CertManager_up_ref(cm); + ctx->certMan->cm = cm; + + return WS_SUCCESS; +} + + static WOLFSSH_CERTMAN* _CertMan_init(WOLFSSH_CERTMAN* cm, void* heap) { WOLFSSH_CERTMAN* ret = NULL; @@ -546,4 +575,96 @@ static int CheckProfile(DecodedCert* cert, int profile) } #endif /* WOLFSSH_NO_FPKI */ + +#if defined(USE_WINDOWS_API) +/* Parse a cert store spec string "store:subject:flags" into wide-string + * components. Allocates wStoreName and wSubjectName via WMALLOC; caller + * must WFREE them. dwFlags is set to the parsed flags value. + * Returns WS_SUCCESS on success. */ +int wolfSSH_ParseCertStoreSpec(const char* spec, + wchar_t** wStoreName, wchar_t** wSubjectName, + DWORD* dwFlags, void* heap) +{ + char* specCopy = NULL; + char* storeName = NULL; + char* subjectName = NULL; + char* flagsStr = NULL; + int wStoreNameLen, wSubjectNameLen; + size_t specLen; + + if (spec == NULL || wStoreName == NULL || wSubjectName == NULL || + dwFlags == NULL) { + return WS_BAD_ARGUMENT; + } + + *wStoreName = NULL; + *wSubjectName = NULL; + *dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; + + specLen = WSTRLEN(spec) + 1; + specCopy = (char*)WMALLOC(specLen, heap, DYNTYPE_TEMP); + if (specCopy == NULL) + return WS_MEMORY_E; + WSTRNCPY(specCopy, spec, specLen); + + /* Parse "store:subject:flags" */ + storeName = specCopy; + subjectName = WSTRCHR(storeName, ':'); + if (subjectName != NULL) { + *subjectName++ = '\0'; + flagsStr = WSTRCHR(subjectName, ':'); + if (flagsStr != NULL) { + *flagsStr++ = '\0'; + if (WSTRCMP(flagsStr, "CURRENT_USER") == 0) { + *dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; + } + else if (WSTRCMP(flagsStr, "LOCAL_MACHINE") == 0) { + *dwFlags = CERT_SYSTEM_STORE_LOCAL_MACHINE; + } + else { + *dwFlags = (DWORD)atoi(flagsStr); + } + } + } + + if (storeName == NULL || subjectName == NULL || *storeName == '\0' || + *subjectName == '\0') { + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_BAD_ARGUMENT; + } + + /* Convert to wide strings */ + wStoreNameLen = MultiByteToWideChar(CP_UTF8, 0, storeName, -1, NULL, 0); + wSubjectNameLen = MultiByteToWideChar(CP_UTF8, 0, subjectName, -1, + NULL, 0); + + *wStoreName = (wchar_t*)WMALLOC(wStoreNameLen * sizeof(wchar_t), + heap, DYNTYPE_TEMP); + *wSubjectName = (wchar_t*)WMALLOC(wSubjectNameLen * sizeof(wchar_t), + heap, DYNTYPE_TEMP); + + if (*wStoreName == NULL || *wSubjectName == NULL) { + if (*wStoreName != NULL) { + WFREE(*wStoreName, heap, DYNTYPE_TEMP); + *wStoreName = NULL; + } + if (*wSubjectName != NULL) { + WFREE(*wSubjectName, heap, DYNTYPE_TEMP); + *wSubjectName = NULL; + } + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_MEMORY_E; + } + + MultiByteToWideChar(CP_UTF8, 0, storeName, -1, + *wStoreName, wStoreNameLen); + MultiByteToWideChar(CP_UTF8, 0, subjectName, -1, + *wSubjectName, wSubjectNameLen); + + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_SUCCESS; +} +#endif /* USE_WINDOWS_API */ + + #endif /* WOLFSSH_CERTS */ diff --git a/src/internal.c b/src/internal.c index 77f165dbb..6891e1953 100644 --- a/src/internal.c +++ b/src/internal.c @@ -61,6 +61,43 @@ #include #endif +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + #include + #include + #include + #ifndef CERT_SYSTEM_STORE_CURRENT_USER + #define CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE + #define CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 + #endif + #ifndef CERT_NCRYPT_KEY_SPEC + #define CERT_NCRYPT_KEY_SPEC 0x00000003 + #endif + #ifndef BCRYPT_SHA1_ALGORITHM + #define BCRYPT_SHA1_ALGORITHM L"SHA1" + #endif + #ifndef BCRYPT_SHA256_ALGORITHM + #define BCRYPT_SHA256_ALGORITHM L"SHA256" + #endif + #ifndef BCRYPT_SHA384_ALGORITHM + #define BCRYPT_SHA384_ALGORITHM L"SHA384" + #endif + #ifndef BCRYPT_SHA512_ALGORITHM + #define BCRYPT_SHA512_ALGORITHM L"SHA512" + #endif + #ifndef BCRYPT_PAD_PKCS1 + #define BCRYPT_PAD_PKCS1 0x00000002 + #endif +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ + +#if defined(USE_WINDOWS_API) && defined(WOLFSSH_CERTS) +static int ExtractPubKeyDerFromCert(const byte* certDer, word32 certDerSz, + byte** outDer, word32* outDerSz, void* heap); +#endif + #ifdef NO_INLINE #include #else @@ -1083,6 +1120,23 @@ void CtxResourceFree(WOLFSSH_CTX* ctx) ctx->privateKey[i].cert = NULL; ctx->privateKey[i].certSz = 0; } +#ifdef USE_WINDOWS_API + if (ctx->privateKey[i].useCertStore) { + if (ctx->privateKey[i].certStoreContext != NULL) { + CertFreeCertificateContext((PCCERT_CONTEXT)ctx->privateKey[i].certStoreContext); + ctx->privateKey[i].certStoreContext = NULL; + } + if (ctx->privateKey[i].storeName != NULL) { + WFREE((void*)ctx->privateKey[i].storeName, ctx->heap, DYNTYPE_STRING); + ctx->privateKey[i].storeName = NULL; + } + if (ctx->privateKey[i].subjectName != NULL) { + WFREE((void*)ctx->privateKey[i].subjectName, ctx->heap, DYNTYPE_STRING); + ctx->privateKey[i].subjectName = NULL; + } + ctx->privateKey[i].useCertStore = 0; + } +#endif /* USE_WINDOWS_API */ #endif ctx->privateKey[i].publicKeyFmt = ID_NONE; } @@ -2091,7 +2145,7 @@ static int IdentifyCert(const byte* in, word32 inSz, void* heap) #endif /* WOLFSSH_CERTS */ -static void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) +void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) { WOLFSSH_PVT_KEY* key; byte* publicKeyAlgo = ctx->publicKeyAlgo; @@ -10891,6 +10945,11 @@ struct wolfSSH_sigKeyBlockFull { word32 pubKeyNameSz; const char *pubKeyFmtName; word32 pubKeyFmtNameSz; +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + const WOLFSSH_PVT_KEY* pvtKey; /* Pointer to private key for cert store support */ +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ union { #ifndef WOLFSSH_NO_RSA struct { @@ -11125,6 +11184,13 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, heap = ssh->ctx->heap; +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + /* Set pointer to private key for cert store support */ + sigKeyBlock_ptr->pvtKey = &ssh->ctx->privateKey[keyIdx]; +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ + switch (sigKeyBlock_ptr->pubKeyId) { #ifndef WOLFSSH_NO_RSA #ifdef WOLFSSH_CERTS @@ -11135,16 +11201,69 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, case ID_SSH_RSA: case ID_RSA_SHA2_256: case ID_RSA_SHA2_512: - /* Decode the user-configured RSA private key. */ - sigKeyBlock_ptr->sk.rsa.eSz = - (word32)sizeof(sigKeyBlock_ptr->sk.rsa.e); - sigKeyBlock_ptr->sk.rsa.nSz = - (word32)sizeof(sigKeyBlock_ptr->sk.rsa.n); - ret = wc_InitRsaKey(&sigKeyBlock_ptr->sk.rsa.key, heap); - if (ret == 0) - ret = wc_RsaPrivateKeyDecode(ssh->ctx->privateKey[keyIdx].key, - &scratch, &sigKeyBlock_ptr->sk.rsa.key, - (int)ssh->ctx->privateKey[keyIdx].keySz); +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + /* Check if this is a cert store key */ + if (ssh->ctx->privateKey[keyIdx].useCertStore) { + /* For cert store keys, extract the RSA public key from the + * DER certificate so that wc_RsaFlattenPublicKey (below) + * can produce the correct e/n for the key-exchange hash, + * and so that wolfSSH_RsaVerify can self-check the + * signature. Signing will still use the cert store. */ + const byte* certDer = + ssh->ctx->privateKey[keyIdx].cert; + word32 certDerSz = + ssh->ctx->privateKey[keyIdx].certSz; + + if (certDer != NULL && certDerSz > 0) { + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; + + ret = ExtractPubKeyDerFromCert(certDer, certDerSz, + &pubKeyDer, &pubKeyDerSz, heap); + if (ret == 0) { + word32 idx2 = 0; + sigKeyBlock_ptr->sk.rsa.eSz = + (word32)sizeof(sigKeyBlock_ptr->sk.rsa.e); + sigKeyBlock_ptr->sk.rsa.nSz = + (word32)sizeof(sigKeyBlock_ptr->sk.rsa.n); + ret = wc_InitRsaKey( + &sigKeyBlock_ptr->sk.rsa.key, heap); + if (ret == 0) + ret = wc_RsaPublicKeyDecode(pubKeyDer, + &idx2, + &sigKeyBlock_ptr->sk.rsa.key, + pubKeyDerSz); + } + if (pubKeyDer != NULL) + WFREE(pubKeyDer, heap, DYNTYPE_PUBKEY); + + if (ret != 0) { + WLOG(WS_LOG_DEBUG, + "SendKexDhReply: cert store RSA pubkey " + "decode failed %d", ret); + ret = WS_CRYPTO_FAILED; + } + } else { + WLOG(WS_LOG_DEBUG, + "SendKexDhReply: cert store key has no cert DER"); + ret = WS_BAD_ARGUMENT; + } + } else +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ + { + /* Decode the user-configured RSA private key. */ + sigKeyBlock_ptr->sk.rsa.eSz = + (word32)sizeof(sigKeyBlock_ptr->sk.rsa.e); + sigKeyBlock_ptr->sk.rsa.nSz = + (word32)sizeof(sigKeyBlock_ptr->sk.rsa.n); + ret = wc_InitRsaKey(&sigKeyBlock_ptr->sk.rsa.key, heap); + if (ret == 0) + ret = wc_RsaPrivateKeyDecode(ssh->ctx->privateKey[keyIdx].key, + &scratch, &sigKeyBlock_ptr->sk.rsa.key, + (int)ssh->ctx->privateKey[keyIdx].keySz); + } /* hash in usual public key if not RFC6187 style cert use */ if (!isCert) { @@ -12112,6 +12231,215 @@ static int KeyAgreeEcdhMlKem_server(WOLFSSH* ssh, byte hashId, #endif /* ML-KEM variants */ +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS +/* Extract DER-encoded public key from a DER certificate. + * Caller must WFREE(*outDer, heap, DYNTYPE_PUBKEY) on success. + * Returns 0 on success. */ +static int ExtractPubKeyDerFromCert(const byte* certDer, word32 certDerSz, + byte** outDer, word32* outDerSz, void* heap) +{ + struct DecodedCert dCert; + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; + int ret; + + if (certDer == NULL || certDerSz == 0 || outDer == NULL || + outDerSz == NULL) { + return WS_BAD_ARGUMENT; + } + + wc_InitDecodedCert(&dCert, certDer, certDerSz, heap); + ret = wc_ParseCert(&dCert, CERT_TYPE, 0, NULL); + if (ret == 0) { + ret = wc_GetPubKeyDerFromCert(&dCert, NULL, &pubKeyDerSz); + if (ret == LENGTH_ONLY_E) { + ret = 0; + pubKeyDer = (byte*)WMALLOC(pubKeyDerSz, heap, DYNTYPE_PUBKEY); + if (pubKeyDer == NULL) + ret = WS_MEMORY_E; + } + } + if (ret == 0) + ret = wc_GetPubKeyDerFromCert(&dCert, pubKeyDer, &pubKeyDerSz); + wc_FreeDecodedCert(&dCert); + + if (ret == 0) { + *outDer = pubKeyDer; + *outDerSz = pubKeyDerSz; + } + else { + if (pubKeyDer != NULL) + WFREE(pubKeyDer, heap, DYNTYPE_PUBKEY); + } + + return ret; +} + + +/* Signing abstraction for MS Certificate Store support + * This function provides a clean abstraction for signing that can use + * either traditional keys or keys from the MS Certificate Store. + * For RSA, expects encoded signature (digest + OID) in digest parameter. + * For ECDSA, expects raw hash in digest parameter. + */ +static int SignWithCertStoreKey(WOLFSSH* ssh, + const WOLFSSH_PVT_KEY* pvtKey, + const byte* data, word32 dataSz, + enum wc_HashType hashId, + byte* sig, word32* sigSz) +{ + int ret = WS_SUCCESS; + PCCERT_CONTEXT pCertContext = NULL; + HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hCryptProv = 0; + DWORD dwKeySpec = 0; + BOOL fCallerFreeProv = FALSE; + DWORD dwSigLen = 0; + SECURITY_STATUS nCryptRet = 0; + + WLOG(WS_LOG_DEBUG, "Entering SignWithCertStoreKey()"); + + if (pvtKey == NULL || !pvtKey->useCertStore || + pvtKey->certStoreContext == NULL) { + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: Not a cert store key"); + return WS_BAD_ARGUMENT; + } + + pCertContext = (PCCERT_CONTEXT)pvtKey->certStoreContext; + + /* Get the private key handle from the certificate */ + if (!CryptAcquireCertificatePrivateKey(pCertContext, + CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG | CRYPT_ACQUIRE_SILENT_FLAG, + NULL, &hCryptProv, &dwKeySpec, &fCallerFreeProv)) { + DWORD dwErr = GetLastError(); + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: Failed to acquire private key, error: %lu", dwErr); + return WS_CRYPTO_FAILED; + } + + /* Sign using CNG (Next Generation Crypto API) */ + if (dwKeySpec == CERT_NCRYPT_KEY_SPEC) { + DWORD cbSignature = *sigSz; + + /* Determine padding and algorithm based on key type */ + if (pvtKey->publicKeyFmt == ID_SSH_RSA || + pvtKey->publicKeyFmt == ID_RSA_SHA2_256 || + pvtKey->publicKeyFmt == ID_RSA_SHA2_512 || + pvtKey->publicKeyFmt == ID_X509V3_SSH_RSA) { + /* RSA PKCS1 padding. + * The caller (SignHRsa) passes a DER-encoded DigestInfo + * (OID + hash) via wc_EncodeSignature(). Setting pszAlgId + * to NULL tells NCryptSignHash that the data is already a + * complete DigestInfo and should be placed directly into + * the PKCS#1 v1.5 block without further wrapping. + * If pszAlgId were non-NULL, NCryptSignHash would expect + * a raw hash and would construct DigestInfo internally, + * causing NTE_INVALID_PARAMETER (0x80090027). */ + BCRYPT_PKCS1_PADDING_INFO paddingInfo; + + WMEMSET(&paddingInfo, 0, sizeof(paddingInfo)); + paddingInfo.pszAlgId = NULL; + + nCryptRet = NCryptSignHash(hCryptProv, &paddingInfo, + (PBYTE)data, dataSz, sig, cbSignature, &dwSigLen, + BCRYPT_PAD_PKCS1); + } else if (pvtKey->publicKeyFmt == ID_ECDSA_SHA2_NISTP256 || + pvtKey->publicKeyFmt == ID_ECDSA_SHA2_NISTP384 || + pvtKey->publicKeyFmt == ID_ECDSA_SHA2_NISTP521 || + pvtKey->publicKeyFmt == ID_X509V3_ECDSA_SHA2_NISTP256 || + pvtKey->publicKeyFmt == ID_X509V3_ECDSA_SHA2_NISTP384 || + pvtKey->publicKeyFmt == ID_X509V3_ECDSA_SHA2_NISTP521) { + /* ECDSA - no padding */ + nCryptRet = NCryptSignHash(hCryptProv, NULL, + (PBYTE)data, dataSz, sig, cbSignature, &dwSigLen, 0); + } else { + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: Unsupported key type"); + ret = WS_BAD_ARGUMENT; + } + + if (ret == WS_SUCCESS) { + if (nCryptRet != 0) { + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: NCryptSignHash failed, error: 0x%08x", nCryptRet); + ret = WS_CRYPTO_FAILED; + } else { + *sigSz = dwSigLen; + ret = WS_SUCCESS; + } + } + } else { + /* Use legacy CryptoAPI for signing - not recommended but supported */ + HCRYPTHASH hHash = 0; + ALG_ID algId = 0; + + /* Determine the algorithm ID based on hash type */ + switch (hashId) { + case WC_HASH_TYPE_SHA: + algId = CALG_SHA1; + break; + case WC_HASH_TYPE_SHA256: + algId = CALG_SHA_256; + break; + case WC_HASH_TYPE_SHA384: + algId = CALG_SHA_384; + break; + case WC_HASH_TYPE_SHA512: + algId = CALG_SHA_512; + break; + default: + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: Unsupported hash type"); + ret = WS_BAD_ARGUMENT; + break; + } + + if (ret == WS_SUCCESS) { + if (!CryptCreateHash(hCryptProv, algId, 0, 0, &hHash)) { + DWORD dwErr = GetLastError(); + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: CryptCreateHash failed, error: %lu", dwErr); + ret = WS_CRYPTO_FAILED; + } else { + if (!CryptSetHashParam(hHash, HP_HASHVAL, (BYTE*)data, 0)) { + DWORD dwErr = GetLastError(); + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: CryptSetHashParam failed, error: %lu", dwErr); + ret = WS_CRYPTO_FAILED; + } else { + dwSigLen = *sigSz; + if (!CryptSignHash(hHash, dwKeySpec, NULL, 0, sig, &dwSigLen)) { + DWORD dwErr = GetLastError(); + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: CryptSignHash failed, error: %lu", dwErr); + ret = WS_CRYPTO_FAILED; + } else { + /* CryptSignHash outputs in little-endian byte order. + * SSH requires big-endian. Reverse the signature. */ + DWORD ii; + for (ii = 0; ii < dwSigLen / 2; ii++) { + byte tmp = sig[ii]; + sig[ii] = sig[dwSigLen - 1 - ii]; + sig[dwSigLen - 1 - ii] = tmp; + } + *sigSz = dwSigLen; + ret = WS_SUCCESS; + } + } + CryptDestroyHash(hHash); + } + } + } + + /* Free the key handle if we acquired it */ + if (fCallerFreeProv) { + if (dwKeySpec == CERT_NCRYPT_KEY_SPEC) { + NCryptFreeObject(hCryptProv); + } else { + CryptReleaseContext(hCryptProv, 0); + } + } + + WLOG(WS_LOG_DEBUG, "Leaving SignWithCertStoreKey(), ret = %d", ret); + return ret; +} +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ + + static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, struct wolfSSH_sigKeyBlockFull *sigKey) #ifndef WOLFSSH_NO_RSA @@ -12165,30 +12493,63 @@ static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, if (ret == WS_SUCCESS) { WLOG(WS_LOG_INFO, "Signing hash with %s.", IdToName(ssh->handshake->pubKeyId)); - #ifdef WOLFSSH_TPM - if (ssh->ctx->tpmDev && ssh->ctx->tpmKey) { - ret = wolfTPM2_SignHashScheme(ssh->ctx->tpmDev, - ssh->ctx->tpmKey, encSig, encSigSz, sig, (int*)sigSz, - TPM_ALG_RSASSA, TPM2_GetTpmHashType(hashId)); - } - else - #endif /* WOLFSSH_TPM */ - ret = wc_RsaSSL_Sign(encSig, encSigSz, sig, - KEX_SIG_SIZE, &sigKey->sk.rsa.key, - ssh->rng); - if (ret <= 0) { - WLOG(WS_LOG_DEBUG, "SignHRsa: Bad RSA Sign"); - ret = WS_RSA_E; - } - else { - *sigSz = (word32)ret; - ret = WS_SUCCESS; + #ifdef WOLFSSH_TPM + if (ssh->ctx->tpmDev && ssh->ctx->tpmKey) { + ret = wolfTPM2_SignHashScheme(ssh->ctx->tpmDev, + ssh->ctx->tpmKey, encSig, encSigSz, sig, (int*)sigSz, + TPM_ALG_RSASSA, TPM2_GetTpmHashType(hashId)); + } + else + #endif + #ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + /* Check if this is a cert store key */ + if (sigKey->pvtKey != NULL && sigKey->pvtKey->useCertStore) { + /* Use cert store signing abstraction */ + ret = SignWithCertStoreKey(ssh, sigKey->pvtKey, encSig, encSigSz, + hashId, sig, sigSz); + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "SignHRsa: Cert store sign failed"); + } + } else +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ + { + /* Use traditional key signing */ + ret = wc_RsaSSL_Sign(encSig, encSigSz, sig, + KEX_SIG_SIZE, &sigKey->sk.rsa.key, + ssh->rng); + if (ret <= 0) { + WLOG(WS_LOG_DEBUG, "SignHRsa: Bad RSA Sign"); + ret = WS_RSA_E; + } + else { + *sigSz = (word32)ret; + ret = WS_SUCCESS; + } } } if (ret == WS_SUCCESS) { - ret = wolfSSH_RsaVerify(sig, *sigSz, encSig, encSigSz, - &sigKey->sk.rsa.key, heap, "SignHRsa"); +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + /* For cert store keys the private key lives in the Windows cert + * store and the in-memory RsaKey may only contain the public + * half extracted from the certificate. The self-verify step + * still works because the public key was decoded from the cert + * in SendKexDhReply. */ + if (sigKey->pvtKey != NULL && sigKey->pvtKey->useCertStore) { + /* Verify using the public-key-only RsaKey decoded from + * the cert store certificate. */ + ret = wolfSSH_RsaVerify(sig, *sigSz, encSig, encSigSz, + &sigKey->sk.rsa.key, heap, "SignHRsa(certStore)"); + } else +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ + { + ret = wolfSSH_RsaVerify(sig, *sigSz, encSig, encSigSz, + &sigKey->sk.rsa.key, heap, "SignHRsa"); + } } #ifdef WOLFSSH_SMALL_STACK @@ -12242,14 +12603,35 @@ static int SignHEcdsa(WOLFSSH* ssh, byte* sig, word32* sigSz, if (ret == WS_SUCCESS) { WLOG(WS_LOG_INFO, "Signing hash with %s.", IdToName(ssh->handshake->pubKeyId)); - ret = wc_ecc_sign_hash(digest, digestSz, sig, sigSz, ssh->rng, - &sigKey->sk.ecc.key); - if (ret != MP_OKAY) { - WLOG(WS_LOG_DEBUG, "SignHEcdsa: Bad ECDSA Sign"); - ret = WS_ECC_E; - } - else { - ret = WS_SUCCESS; +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + /* Check if this is a cert store key */ + if (sigKey->pvtKey != NULL && sigKey->pvtKey->useCertStore) { + /* Use cert store signing abstraction - ECDSA uses raw hash. + * Note: unlike the RSA path, ECDSA does not self-verify here + * because NCryptSignHash returns raw r||s (not DER), and + * converting back for wc_ecc_verify_hash would add complexity. + * The key exchange hash comparison by the peer serves as + * the primary verification. */ + ret = SignWithCertStoreKey(ssh, sigKey->pvtKey, digest, digestSz, + hashId, sig, sigSz); + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "SignHEcdsa: Cert store sign failed"); + } + } else +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ + { + /* Use traditional key signing */ + ret = wc_ecc_sign_hash(digest, digestSz, sig, sigSz, ssh->rng, + &sigKey->sk.ecc.key); + if (ret != MP_OKAY) { + WLOG(WS_LOG_DEBUG, "SignHEcdsa: Bad ECDSA Sign"); + ret = WS_ECC_E; + } + else { + ret = WS_SUCCESS; + } } } @@ -12268,9 +12650,35 @@ static int SignHEcdsa(WOLFSSH* ssh, byte* sig, word32* sigSz, } if (ret == WS_SUCCESS) { - ret = wc_ecc_sig_to_rs(sig, *sigSz, r, &rSz, s, &sSz); - if (ret != 0) { - ret = WS_ECC_E; +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + /* NCryptSignHash for ECDSA returns raw r||s (each half of sigSz), + * NOT DER-encoded. Split directly. */ + if (sigKey->pvtKey != NULL && sigKey->pvtKey->useCertStore) { + word32 halfSz = *sigSz / 2; + word32 rOff = 0, sOff = 0; + WMEMCPY(r, sig, halfSz); + WMEMCPY(s, sig + halfSz, halfSz); + /* Trim leading zeroes (use offset to preserve base pointer + * so WFREE works in SMALL_STACK builds). */ + while (rOff < halfSz - 1 && r[rOff] == 0) + rOff++; + while (sOff < halfSz - 1 && s[sOff] == 0) + sOff++; + if (rOff > 0) + WMEMMOVE(r, r + rOff, halfSz - rOff); + rSz = halfSz - rOff; + if (sOff > 0) + WMEMMOVE(s, s + sOff, halfSz - sOff); + sSz = halfSz - sOff; + } else +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ + { + ret = wc_ecc_sig_to_rs(sig, *sigSz, r, &rSz, s, &sSz); + if (ret != 0) { + ret = WS_ECC_E; + } } } @@ -14234,6 +14642,34 @@ static int PrepareUserAuthRequestRsaCert(WOLFSSH* ssh, word32* payloadSz, authData->sf.publicKey.publicKeySz); else #endif /* WOLFSSH_AGENT */ +#ifdef USE_WINDOWS_API + /* Note: already inside #ifdef WOLFSSH_CERTS */ + if (authData->sf.publicKey.privateKey == NULL) { + /* Cert store: decode public key from the stored certificate */ + word32 ki; + ret = WS_BAD_ARGUMENT; + for (ki = 0; ki < ssh->ctx->privateKeyCount; ki++) { + if (ssh->ctx->privateKey[ki].useCertStore && + ssh->ctx->privateKey[ki].cert != NULL) { + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; + + ret = ExtractPubKeyDerFromCert( + ssh->ctx->privateKey[ki].cert, + ssh->ctx->privateKey[ki].certSz, + &pubKeyDer, &pubKeyDerSz, ssh->ctx->heap); + if (ret == 0) { + idx = 0; + ret = wc_RsaPublicKeyDecode(pubKeyDer, &idx, + &keySig->ks.rsa.key, pubKeyDerSz); + } + if (pubKeyDer != NULL) + WFREE(pubKeyDer, ssh->ctx->heap, DYNTYPE_PUBKEY); + break; + } + } + } else +#endif /* USE_WINDOWS_API */ ret = wc_RsaPrivateKeyDecode(authData->sf.publicKey.privateKey, &idx, &keySig->ks.rsa.key, authData->sf.publicKey.privateKeySz); @@ -14361,17 +14797,56 @@ static int BuildUserAuthRequestRsaCert(WOLFSSH* ssh, if (ret == WS_SUCCESS) { int sigSz; WLOG(WS_LOG_INFO, "Signing hash with RSA."); - sigSz = wc_RsaSSL_Sign(encDigest, encDigestSz, - output + begin, keySig->sigSz, - &keySig->ks.rsa.key, ssh->rng); - if (sigSz <= 0 || (word32)sigSz != keySig->sigSz) { - WLOG(WS_LOG_DEBUG, "SUAR: Bad RSA Sign"); - ret = WS_RSA_E; - } - else { - ret = wolfSSH_RsaVerify(output + begin, keySig->sigSz, - encDigest, encDigestSz, &keySig->ks.rsa.key, - ssh->ctx->heap, "SUAR"); +#ifdef USE_WINDOWS_API + if (authData->sf.publicKey.privateKey == NULL) { + /* Cert store: sign with NCryptSignHash via + * SignWithCertStoreKey (pszAlgId=NULL, data is + * the already-encoded DigestInfo). */ + word32 ki; + const WOLFSSH_PVT_KEY* pvtKey = NULL; + for (ki = 0; ki < ssh->ctx->privateKeyCount; ki++) { + if (ssh->ctx->privateKey[ki].useCertStore) { + pvtKey = &ssh->ctx->privateKey[ki]; + break; + } + } + if (pvtKey != NULL) { + word32 outSigSz = keySig->sigSz; + ret = SignWithCertStoreKey(ssh, pvtKey, + encDigest, encDigestSz, hashId, + output + begin, &outSigSz); + if (ret == WS_SUCCESS) { + sigSz = (int)outSigSz; + ret = wolfSSH_RsaVerify(output + begin, + outSigSz, encDigest, encDigestSz, + &keySig->ks.rsa.key, ssh->ctx->heap, + "SUAR(certStore)"); + } else { + WLOG(WS_LOG_DEBUG, + "SUAR: Cert store RSA sign failed"); + ret = WS_RSA_E; + } + } else { + WLOG(WS_LOG_DEBUG, + "SUAR: Cert store key not found for RSA"); + ret = WS_BAD_ARGUMENT; + } + } else +#endif /* USE_WINDOWS_API */ + { + sigSz = wc_RsaSSL_Sign(encDigest, encDigestSz, + output + begin, keySig->sigSz, + &keySig->ks.rsa.key, ssh->rng); + if (sigSz <= 0 || (word32)sigSz != keySig->sigSz) { + WLOG(WS_LOG_DEBUG, "SUAR: Bad RSA Sign"); + ret = WS_RSA_E; + } + else { + ret = wolfSSH_RsaVerify(output + begin, + keySig->sigSz, encDigest, encDigestSz, + &keySig->ks.rsa.key, ssh->ctx->heap, + "SUAR"); + } } } @@ -14683,29 +15158,62 @@ static int PrepareUserAuthRequestEccCert(WOLFSSH* ssh, word32* payloadSz, if (ret == WS_SUCCESS) { word32 idx = 0; +#ifdef USE_WINDOWS_API + /* Note: already inside #ifdef WOLFSSH_CERTS. + * Cert store: no in-memory private key — decode public key from + * the DER certificate that UsePrivateKey_fromStore saved. */ + if (authData->sf.publicKey.privateKey == NULL) { + word32 ki; + ret = WS_BAD_ARGUMENT; + for (ki = 0; ki < ssh->ctx->privateKeyCount; ki++) { + if (ssh->ctx->privateKey[ki].useCertStore && + ssh->ctx->privateKey[ki].cert != NULL) { + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; + + ret = ExtractPubKeyDerFromCert( + ssh->ctx->privateKey[ki].cert, + ssh->ctx->privateKey[ki].certSz, + &pubKeyDer, &pubKeyDerSz, ssh->ctx->heap); + if (ret == 0) { + idx = 0; + ret = wc_EccPublicKeyDecode(pubKeyDer, &idx, + &keySig->ks.ecc.key, pubKeyDerSz); + } + if (pubKeyDer != NULL) + WFREE(pubKeyDer, ssh->ctx->heap, DYNTYPE_PUBKEY); + break; + } + } + } else +#endif /* USE_WINDOWS_API */ + { #if 0 #ifdef WOLFSSH_AGENT - if (ssh->agentEnabled) { - word32 sz; - const byte* c = (const byte*)authData->sf.publicKey.publicKey; - - ato32(c + idx, &sz); - idx += LENGTH_SZ + sz; - ato32(c + idx, &sz); - idx += LENGTH_SZ + sz; - ato32(c + idx, &sz); - idx += LENGTH_SZ; - c += idx; - idx = 0; + if (ssh->agentEnabled) { + word32 sz; + const byte* c = + (const byte*)authData->sf.publicKey.publicKey; + + ato32(c + idx, &sz); + idx += LENGTH_SZ + sz; + ato32(c + idx, &sz); + idx += LENGTH_SZ + sz; + ato32(c + idx, &sz); + idx += LENGTH_SZ; + c += idx; + idx = 0; - ret = wc_ecc_import_x963(c, sz, &keySig->ks.ecc.key); - } - else + ret = wc_ecc_import_x963(c, sz, &keySig->ks.ecc.key); + } + else #endif #endif - ret = wc_EccPrivateKeyDecode(authData->sf.publicKey.privateKey, - &idx, &keySig->ks.ecc.key, - authData->sf.publicKey.privateKeySz); + ret = wc_EccPrivateKeyDecode( + authData->sf.publicKey.privateKey, + &idx, &keySig->ks.ecc.key, + authData->sf.publicKey.privateKeySz); + } } if (ret == WS_SUCCESS) { @@ -14804,24 +15312,74 @@ static int BuildUserAuthRequestEccCert(WOLFSSH* ssh, ret = wc_HashInit(&hash, hashId); if (ret == WS_SUCCESS) { ret = HashUpdate(&hash, hashId, checkData, checkDataSz); - if (ret == WS_SUCCESS) - ret = wc_HashFinal(&hash, hashId, digest); - if (ret == WS_SUCCESS) - ret = wc_ecc_sign_hash(digest, digestSz, sig, &sigSz, - ssh->rng, &keySig->ks.ecc.key); + } + if (ret == WS_SUCCESS) + ret = wc_HashFinal(&hash, hashId, digest); + wc_HashFree(&hash, hashId); + } + +#ifdef USE_WINDOWS_API + /* Cert store signing: NCryptSignHash returns raw r||s */ + if (ret == WS_SUCCESS && + authData->sf.publicKey.privateKey == NULL) { + word32 ki; + const WOLFSSH_PVT_KEY* pvtKey = NULL; + for (ki = 0; ki < ssh->ctx->privateKeyCount; ki++) { + if (ssh->ctx->privateKey[ki].useCertStore) { + pvtKey = &ssh->ctx->privateKey[ki]; + break; + } + } + if (pvtKey != NULL) { + ret = SignWithCertStoreKey(ssh, pvtKey, + digest, digestSz, hashId, sig, &sigSz); + if (ret == WS_SUCCESS) { + /* NCryptSignHash ECDSA output is raw r||s, each + * component is half the total signature size. */ + word32 halfSz = sigSz / 2; + word32 rOff = 0, sOff = 0; + r = rs; + s = rs + halfSz; + WMEMCPY(r, sig, halfSz); + WMEMCPY(s, sig + halfSz, halfSz); + /* Trim leading zeroes */ + while (rOff < halfSz - 1 && r[rOff] == 0) rOff++; + while (sOff < halfSz - 1 && s[sOff] == 0) sOff++; + if (rOff > 0) { + WMEMMOVE(r, r + rOff, halfSz - rOff); + } + rSz = halfSz - rOff; + if (sOff > 0) { + WMEMMOVE(s, s + sOff, halfSz - sOff); + } + sSz = halfSz - sOff; + } else { + WLOG(WS_LOG_DEBUG, "SUAR: Cert store ECC sign failed"); + ret = WS_ECC_E; + } + } else { + WLOG(WS_LOG_DEBUG, + "SUAR: Cert store key not found for ECC"); + ret = WS_BAD_ARGUMENT; + } + } else +#endif /* USE_WINDOWS_API */ + { + if (ret == WS_SUCCESS) { + ret = wc_ecc_sign_hash(digest, digestSz, sig, &sigSz, + ssh->rng, &keySig->ks.ecc.key); if (ret != WS_SUCCESS) { WLOG(WS_LOG_DEBUG, "SUAR: Bad ECC Cert Sign"); ret = WS_ECC_E; } - wc_HashFree(&hash, hashId); } - } - if (ret == WS_SUCCESS) { - rSz = sSz = (word32)sizeof(rs) / 2; - r = rs; - s = rs + rSz; - ret = wc_ecc_sig_to_rs(sig, sigSz, r, &rSz, s, &sSz); + if (ret == WS_SUCCESS) { + rSz = sSz = (word32)sizeof(rs) / 2; + r = rs; + s = rs + rSz; + ret = wc_ecc_sig_to_rs(sig, sigSz, r, &rSz, s, &sSz); + } } if (ret == WS_SUCCESS) { diff --git a/src/ssh.c b/src/ssh.c index cf686a7dc..9965df3ab 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -35,6 +35,18 @@ #include #include +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + #include + #include + #include + #include + #ifndef CERT_NCRYPT_KEY_SPEC + #define CERT_NCRYPT_KEY_SPEC 0x00000003 + #endif +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ + #ifdef NO_INLINE #include #else @@ -2473,6 +2485,261 @@ int wolfSSH_CTX_AddRootCert_buffer(WOLFSSH_CTX* ctx, return ret; } +#ifdef USE_WINDOWS_API +/* Load a private key from MS Certificate Store + * storeName: Certificate store name (e.g., L"My", L"Root") + * dwFlags: Certificate store flags (e.g., CERT_SYSTEM_STORE_CURRENT_USER) + * subjectName: Certificate subject name or thumbprint for lookup + * returns WS_SUCCESS on success + */ +int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, + const wchar_t* storeName, DWORD dwFlags, + const wchar_t* subjectName) +{ + int ret = WS_SUCCESS; + HCERTSTORE hStore = NULL; + PCCERT_CONTEXT pCertContext = NULL; + word32 keyIdx = 0; + byte keyId = ID_NONE; + void* heap = NULL; + + WLOG(WS_LOG_DEBUG, "Entering wolfSSH_CTX_UsePrivateKey_fromStore()"); + + if (ctx == NULL || storeName == NULL || subjectName == NULL) { + WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Bad argument"); + return WS_BAD_ARGUMENT; + } + + heap = ctx->heap; + + /* Open the certificate store */ + hStore = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, (HCRYPTPROV_LEGACY)0, + dwFlags | CERT_STORE_OPEN_EXISTING_FLAG, storeName); + if (hStore == NULL) { + DWORD dwErr = GetLastError(); + WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Failed to open store, error: %lu", dwErr); + return WS_FATAL_ERROR; + } + + /* Find the certificate by subject name. + * CERT_FIND_SUBJECT_STR_W does a substring search on the formatted subject + * name. CertNameToStr with CERT_SIMPLE_NAME_STR format typically returns + * just the CN value without "CN=" prefix. So we try multiple searches: + * 1. The provided subject name as-is + * 2. If it starts with "CN=", try without the prefix + */ + pCertContext = CertFindCertificateInStore(hStore, + X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + 0, CERT_FIND_SUBJECT_STR_W, subjectName, NULL); + + /* If not found and subject starts with "CN=", try without the prefix */ + if (pCertContext == NULL && wcslen(subjectName) > 3 && + (wcsncmp(subjectName, L"CN=", 3) == 0 || + wcsncmp(subjectName, L"cn=", 3) == 0)) { + WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Retrying " + "search without CN= prefix"); + pCertContext = CertFindCertificateInStore(hStore, + X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + 0, CERT_FIND_SUBJECT_STR_W, subjectName + 3, NULL); + } + + if (pCertContext == NULL) { + /* Try finding by thumbprint if subject name didn't work */ + /* Note: subjectName could be a thumbprint in format "XX XX XX ..." */ + CertCloseStore(hStore, 0); + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Certificate " + "not found with subject '%ls'", subjectName); + return WS_FATAL_ERROR; + } + + /* Determine key type from certificate */ + /* Get the public key info to determine algorithm */ + PCERT_PUBLIC_KEY_INFO pPubKeyInfo = &pCertContext->pCertInfo->SubjectPublicKeyInfo; + + /* Check algorithm OID to determine key type */ + if (pPubKeyInfo->Algorithm.pszObjId != NULL) { + /* Compare OID strings (they are ASCII, not wide) */ + if (strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_RSA_RSA) == 0 || + strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_RSA_ENCRYPT) == 0) { + keyId = ID_SSH_RSA; + } else if (strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_ECC_PUBLIC_KEY) == 0) { + /* Decode the curve OID from the algorithm parameters to select + * the correct ECDSA key type. The Parameters field contains + * a DER-encoded OID identifying the named curve. */ + char* curveOid = NULL; + DWORD curveOidSz = 0; + + if (pPubKeyInfo->Algorithm.Parameters.cbData > 0 && + CryptDecodeObjectEx(X509_ASN_ENCODING, + X509_OBJECT_IDENTIFIER, + pPubKeyInfo->Algorithm.Parameters.pbData, + pPubKeyInfo->Algorithm.Parameters.cbData, + CRYPT_DECODE_ALLOC_FLAG, NULL, + &curveOid, &curveOidSz)) { + /* Compare against well-known curve OIDs */ + if (strcmp(curveOid, "1.2.840.10045.3.1.7") == 0) { + keyId = ID_ECDSA_SHA2_NISTP256; + } else if (strcmp(curveOid, "1.3.132.0.34") == 0) { + keyId = ID_ECDSA_SHA2_NISTP384; + } else if (strcmp(curveOid, "1.3.132.0.35") == 0) { + keyId = ID_ECDSA_SHA2_NISTP521; + } else { + WLOG(WS_LOG_DEBUG, + "wolfSSH_CTX_UsePrivateKey_fromStore: " + "Unrecognized ECC curve OID: %s, " + "defaulting to P-256", curveOid); + keyId = ID_ECDSA_SHA2_NISTP256; + } + LocalFree(curveOid); + } else { + WLOG(WS_LOG_DEBUG, + "wolfSSH_CTX_UsePrivateKey_fromStore: " + "Failed to decode ECC curve parameters, " + "defaulting to P-256"); + keyId = ID_ECDSA_SHA2_NISTP256; + } + } else { + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Unsupported key algorithm: %s", pPubKeyInfo->Algorithm.pszObjId); + return WS_BAD_ARGUMENT; + } + } else { + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: No algorithm OID"); + return WS_BAD_ARGUMENT; + } + + /* Find an available slot or existing key of same type */ + keyIdx = WOLFSSH_MAX_PVT_KEYS; + { + word32 i; + for (i = 0; i < ctx->privateKeyCount && i < WOLFSSH_MAX_PVT_KEYS; i++) { + if (ctx->privateKey[i].publicKeyFmt == keyId) { + keyIdx = i; + break; + } + } + if (keyIdx == WOLFSSH_MAX_PVT_KEYS && ctx->privateKeyCount < WOLFSSH_MAX_PVT_KEYS) { + keyIdx = ctx->privateKeyCount; + ctx->privateKeyCount++; + } + } + + if (keyIdx >= WOLFSSH_MAX_PVT_KEYS) { + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: No available key slot"); + return WS_MEMORY_E; + } + + /* Set up the private key structure */ + ctx->privateKey[keyIdx].publicKeyFmt = keyId; + ctx->privateKey[keyIdx].useCertStore = 1; + ctx->privateKey[keyIdx].certStoreContext = (void*)pCertContext; + + /* Store the store name and subject name */ + { + size_t storeNameLen = wcslen(storeName) + 1; + size_t subjectNameLen = wcslen(subjectName) + 1; + wchar_t* storeNameCopy = (wchar_t*)WMALLOC(storeNameLen * sizeof(wchar_t), heap, DYNTYPE_STRING); + wchar_t* subjectNameCopy = (wchar_t*)WMALLOC(subjectNameLen * sizeof(wchar_t), heap, DYNTYPE_STRING); + + if (storeNameCopy == NULL || subjectNameCopy == NULL) { + if (storeNameCopy != NULL) WFREE(storeNameCopy, heap, DYNTYPE_STRING); + if (subjectNameCopy != NULL) WFREE(subjectNameCopy, heap, DYNTYPE_STRING); + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Memory allocation failed"); + return WS_MEMORY_E; + } + + WMEMCPY(storeNameCopy, storeName, storeNameLen * sizeof(wchar_t)); + WMEMCPY(subjectNameCopy, subjectName, subjectNameLen * sizeof(wchar_t)); + ctx->privateKey[keyIdx].storeName = storeNameCopy; + ctx->privateKey[keyIdx].subjectName = subjectNameCopy; + ctx->privateKey[keyIdx].dwFlags = dwFlags; + } + + /* Extract certificate for public key operations */ + { + DWORD certSz = pCertContext->cbCertEncoded; + byte* certBuf = (byte*)WMALLOC(certSz, heap, DYNTYPE_CERT); + if (certBuf == NULL) { + /* Cleanup */ + WFREE((void*)ctx->privateKey[keyIdx].storeName, heap, DYNTYPE_STRING); + WFREE((void*)ctx->privateKey[keyIdx].subjectName, heap, DYNTYPE_STRING); + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Certificate buffer allocation failed"); + return WS_MEMORY_E; + } + WMEMCPY(certBuf, pCertContext->pbCertEncoded, certSz); + ctx->privateKey[keyIdx].cert = certBuf; + ctx->privateKey[keyIdx].certSz = certSz; + } + + /* Verify private key is accessible before returning success. + * This catches permission issues early (e.g., LocalSystem service + * cannot access the private key) rather than failing later during + * SSH handshake signing. */ + { + HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hKey = 0; + DWORD dwKeySpec = 0; + BOOL fCallerFree = FALSE; + + if (!CryptAcquireCertificatePrivateKey(pCertContext, + CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG | CRYPT_ACQUIRE_SILENT_FLAG, + NULL, &hKey, &dwKeySpec, &fCallerFree)) { + DWORD dwErr = GetLastError(); + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Cannot " + "access private key, error: %lu. Check that the current user " + "or service account has permission to access the key.", dwErr); + /* Cleanup already stored data */ + WFREE((void*)ctx->privateKey[keyIdx].storeName, heap, DYNTYPE_STRING); + WFREE((void*)ctx->privateKey[keyIdx].subjectName, heap, DYNTYPE_STRING); + WFREE(ctx->privateKey[keyIdx].cert, heap, DYNTYPE_CERT); + ctx->privateKey[keyIdx].useCertStore = 0; + CertFreeCertificateContext(pCertContext); + ctx->privateKey[keyIdx].certStoreContext = NULL; + ctx->privateKey[keyIdx].storeName = NULL; + ctx->privateKey[keyIdx].subjectName = NULL; + ctx->privateKey[keyIdx].cert = NULL; + ctx->privateKey[keyIdx].certSz = 0; + ctx->privateKeyCount--; + CertCloseStore(hStore, 0); + return WS_CRYPTO_FAILED; + } + /* Release the key handle since we just needed to verify access */ + if (fCallerFree) { + if (dwKeySpec == CERT_NCRYPT_KEY_SPEC) { + NCryptFreeObject(hKey); + } else { + CryptReleaseContext(hKey, 0); + } + } + WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Private key " + "access verified successfully"); + } + + /* The cert context (pCertContext) is retained in + * privateKey[].certStoreContext for later signing operations. + * CertFindCertificateInStore incremented its reference count, so + * closing the store does not invalidate it. It will be freed in + * CtxResourceFree via CertFreeCertificateContext. + * Note: if the certificate is removed from the store while we hold + * this context, CryptAcquireCertificatePrivateKey may fail at + * signing time. */ + CertCloseStore(hStore, 0); + + /* Refresh public key algorithm list */ + RefreshPublicKeyAlgo(ctx); + + WLOG(WS_LOG_DEBUG, "Leaving wolfSSH_CTX_UsePrivateKey_fromStore(), ret = %d", ret); + return ret; +} +#endif /* USE_WINDOWS_API */ #endif /* WOLFSSH_CERTS */ diff --git a/wolfssh/certman.h b/wolfssh/certman.h index f80735550..bbb4f5e20 100644 --- a/wolfssh/certman.h +++ b/wolfssh/certman.h @@ -30,6 +30,7 @@ #include #include +#include /* included for WOLFSSL_CERT_MANAGER struct */ #ifdef __cplusplus extern "C" { @@ -40,6 +41,9 @@ struct WOLFSSH_CERTMAN; typedef struct WOLFSSH_CERTMAN WOLFSSH_CERTMAN; +WOLFSSH_API +int wolfSSH_SetCertManager(WOLFSSH_CTX* ctx, WOLFSSL_CERT_MANAGER* cm); + WOLFSSH_API WOLFSSH_CERTMAN* wolfSSH_CERTMAN_new(void* heap); @@ -55,6 +59,14 @@ int wolfSSH_CERTMAN_VerifyCerts_buffer(WOLFSSH_CERTMAN* cm, const unsigned char* cert, word32 certSz, word32 certCount); +#if defined(USE_WINDOWS_API) +WOLFSSH_API +int wolfSSH_ParseCertStoreSpec(const char* spec, + wchar_t** wStoreName, wchar_t** wSubjectName, + DWORD* dwFlags, void* heap); +#endif /* USE_WINDOWS_API */ + + #ifdef __cplusplus } #endif diff --git a/wolfssh/internal.h b/wolfssh/internal.h index ef59304b5..9acae1536 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -539,6 +539,21 @@ typedef struct WOLFSSH_PVT_KEY { byte publicKeyFmt; /* Public key format for the private key. Note, some public key * formats are used with multiple public key signing algorithms. */ +#ifdef USE_WINDOWS_API +#ifdef WOLFSSH_CERTS + byte useCertStore:1; + /* Flag indicating if this key is from MS Certificate Store. */ + void* certStoreContext; + /* Windows certificate context (PCCERT_CONTEXT) for MS Certificate Store. + * Owned by CTX, must be freed with CertFreeCertificateContext. */ + const wchar_t* storeName; + /* Certificate store name (e.g., "My", "Root"). Owned by CTX. */ + const wchar_t* subjectName; + /* Certificate subject name or thumbprint for lookup. Owned by CTX. */ + DWORD dwFlags; + /* Certificate store flags (e.g., CERT_SYSTEM_STORE_CURRENT_USER). */ +#endif /* WOLFSSH_CERTS */ +#endif /* USE_WINDOWS_API */ } WOLFSSH_PVT_KEY; @@ -670,7 +685,8 @@ typedef struct HandshakeInfo { #ifndef WOLFSSH_NO_ECDH ecc_key ecc; #endif -#ifndef WOLFSSH_NO_CURVE25519_SHA256 +#if !defined(WOLFSSH_NO_CURVE25519_SHA256) || \ + !defined(WOLFSSH_NO_CURVE25519_MLKEM768_SHA256) curve25519_key curve25519; #endif } privKey; @@ -994,6 +1010,7 @@ WOLFSSH_LOCAL void ChannelDelete(WOLFSSH_CHANNEL*, void*); WOLFSSH_LOCAL WOLFSSH_CHANNEL* ChannelFind(WOLFSSH*, word32, byte); WOLFSSH_LOCAL int ChannelRemove(WOLFSSH*, word32, byte); WOLFSSH_LOCAL int ChannelPutData(WOLFSSH_CHANNEL*, byte*, word32); +WOLFSSH_LOCAL void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx); WOLFSSH_LOCAL int wolfSSH_ProcessBuffer(WOLFSSH_CTX*, const byte*, word32, int, int); @@ -1437,6 +1454,7 @@ WOLFSSH_LOCAL int wolfSSH_RsaVerify( const byte* encDigest, word32 encDigestSz, RsaKey* key, void* heap, const char* loc); #endif + WOLFSSH_LOCAL void DumpOctetString(const byte*, word32); WOLFSSH_LOCAL int wolfSSH_oct2dec(WOLFSSH* ssh, byte* oct, word32 octSz); WOLFSSH_LOCAL void AddAssign64(word32*, word32); diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index b5b04eb59..01855774a 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -399,6 +399,11 @@ WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_buffer(WOLFSSH_CTX*, const byte* cert, word32 certSz, int format); WOLFSSH_API int wolfSSH_CTX_AddRootCert_buffer(WOLFSSH_CTX* ctx, const byte* cert, word32 certSz, int format); +#ifdef USE_WINDOWS_API + WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, + const wchar_t* storeName, DWORD dwFlags, + const wchar_t* subjectName); +#endif /* USE_WINDOWS_API */ #endif /* WOLFSSH_CERTS */ WOLFSSH_API int wolfSSH_CTX_SetWindowPacketSize(WOLFSSH_CTX*, word32, word32); diff --git a/wolfssh/test.h b/wolfssh/test.h index 0f22fd64c..d3434dca1 100644 --- a/wolfssh/test.h +++ b/wolfssh/test.h @@ -1109,6 +1109,7 @@ static INLINE void build_addr_ipv6(struct sockaddr_in6* addr, const char* peer, #define BAD 0xFF +#ifndef WOLFSSL_BASE16 static const byte hexDecode[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, @@ -1178,7 +1179,9 @@ static int Base16_Decode(const byte* in, word32 inLen, *outLen = outIdx; return 0; } - +#else + #include +#endif /* !WOLFSSL_BASE16 */ static void FreeBins(byte* b1, byte* b2, byte* b3, byte* b4) {