diff --git a/.github/workflows/windows-cert-store-test.yml b/.github/workflows/windows-cert-store-test.yml new file mode 100644 index 000000000..12973095c --- /dev/null +++ b/.github/workflows/windows-cert-store-test.yml @@ -0,0 +1,688 @@ +name: Windows Certificate Store Test + +# Tests MS Certificate Store integration for wolfSSH. The matrix covers +# server host keys and client user keys coming from the cert store, from +# X.509 cert/key files, or both, plus an ECDSA cert store host key. +# +# Test flow per matrix entry: +# 1. Create testuser client cert (renewcerts.sh) and, for store cases, +# import/create certificates in the Windows certificate store. +# 2. If the server key comes from the store: run echoserver with -W and +# connect with the SFTP client. +# 3. Run wolfsshd as a Windows service and connect with the SFTP client. + +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}} + # Enable Windows cert store API (not in repo user_settings.h). + # Must be appended to wolfssh/ide/winvs/user_settings.h: VS projects put ide/winvs on the + printf '\n/* Appended by windows-cert-store-test CI */\n#define WOLFSSH_WINDOWS_CERT_STORE\n' >> ${{env.USER_SETTINGS_H_NEW}} + cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}} + + - 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/** + + test: + needs: build + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + include: + - server_key_source: file + client_key_source: x509 + key_algorithm: rsa + test_name: "Server-File-Client-X509" + - server_key_source: store + client_key_source: x509 + key_algorithm: rsa + test_name: "Server-Store-Client-X509" + - server_key_source: file + client_key_source: store + key_algorithm: rsa + test_name: "Server-File-Client-Store" + - server_key_source: store + client_key_source: store + key_algorithm: rsa + test_name: "Server-Store-Client-Store" + - server_key_source: store + client_key_source: x509 + key_algorithm: ecdsa + test_name: "Server-Store-Client-X509-ECDSA" + + 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: Create testuser client certificate - ${{ 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: | + # Create an X509 certificate for testuser, signed by the test CA, + # using renewcerts.sh (like sshd_x509_test.sh does). Used directly + # for x509 clients and imported into the store for store clients. + cd keys + bash renewcerts.sh testuser + cd .. + + if [[ ! -f "keys/testuser-cert.der" || ! -f "keys/testuser-key.der" ]]; then + echo "ERROR: renewcerts.sh did not create testuser-cert.der/testuser-key.der" + ls -la keys/ + exit 1 + fi + echo "CLIENT_CERT_FILE=keys/testuser-cert.der" >> $GITHUB_ENV + echo "CLIENT_KEY_FILE=keys/testuser-key.der" >> $GITHUB_ENV + + - name: Set up cert store certificates + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # Server host key: self-signed cert in LocalMachine\My so the + # wolfsshd service (LocalSystem) can access it. + if ("${{ matrix.server_key_source }}" -eq "store") { + if ("${{ matrix.key_algorithm }}" -eq "ecdsa") { + $serverCert = New-SelfSignedCertificate ` + -Subject "CN=wolfSSH-Test-Server" ` + -KeyAlgorithm ECDSA_nistP256 ` + -CertStoreLocation "Cert:\LocalMachine\My" ` + -KeyExportPolicy Exportable ` + -NotAfter (Get-Date).AddYears(1) ` + -KeyUsage DigitalSignature + } else { + $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: $($serverCert.Subject) ($($serverCert.Thumbprint))" + + # Grant LocalSystem access to the private key file. Required for + # the wolfsshd service running as LocalSystem; without this, + # CryptAcquireCertificatePrivateKey fails. + if ("${{ matrix.key_algorithm }}" -eq "ecdsa") { + $privKey = [System.Security.Cryptography.X509Certificates.ECDsaCertificateExtensions]::GetECDsaPrivateKey($serverCert) + } else { + $privKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($serverCert) + } + $keyName = $privKey.Key.UniqueName + $keyFile = @( + "$env:ProgramData\Microsoft\Crypto\Keys\$keyName", + "$env:ProgramData\Microsoft\Crypto\RSA\MachineKeys\$keyName", + "$env:ProgramData\Microsoft\Crypto\SystemKeys\$keyName" + ) | Where-Object { Test-Path $_ } | Select-Object -First 1 + if (-not $keyFile) { + Write-Host "ERROR: Private key file not found for $keyName" + exit 1 + } + $acl = Get-Acl $keyFile + $rule = New-Object System.Security.AccessControl.FileSystemAccessRule ` + "NT AUTHORITY\SYSTEM", "FullControl", "Allow" + $acl.SetAccessRule($rule) + Set-Acl $keyFile $acl + Write-Host "Granted SYSTEM FullControl on private key: $keyFile" + + # Export the CN (without "CN=") for HostKeyStoreSubject + $subject = $serverCert.Subject + if ($subject -match "^CN=(.+)$") { $subject = $matches[1] } + Add-Content -Path $env:GITHUB_ENV -Value "SERVER_CERT_SUBJECT=$subject" + } + + # Client user key: import the CA-signed testuser cert+key into + # CurrentUser\My (via PFX; openssl converts the DER files). + if ("${{ matrix.client_key_source }}" -eq "store") { + $userCertPath = (Resolve-Path $env:CLIENT_CERT_FILE).Path + $userKeyPath = (Resolve-Path $env:CLIENT_KEY_FILE).Path + $userCertPem = Join-Path $env:TEMP "testuser-cert.pem" + $userKeyPem = Join-Path $env:TEMP "testuser-key.pem" + $pfxPath = Join-Path $env:TEMP "testuser-client.pfx" + $pfxPassword = "TempP@ss123" + + & openssl x509 -inform DER -in $userCertPath -out $userCertPem + if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cert DER to PEM failed"; exit 1 } + & openssl rsa -inform DER -in $userKeyPath -out $userKeyPem 2>$null + if ($LASTEXITCODE -ne 0) { + & openssl ec -inform DER -in $userKeyPath -out $userKeyPem + if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: key DER to PEM failed (tried RSA and ECC)"; exit 1 } + } + & openssl pkcs12 -export -out $pfxPath -inkey $userKeyPem -in $userCertPem -password "pass:$pfxPassword" -nodes + if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: PFX creation failed"; exit 1 } + + Import-PfxCertificate -FilePath $pfxPath -CertStoreLocation "Cert:\CurrentUser\My" ` + -Password (ConvertTo-SecureString -String $pfxPassword -Force -AsPlainText) | Out-Null + Remove-Item -Path $pfxPath, $userCertPem, $userKeyPem -ErrorAction SilentlyContinue + + $importedCert = Get-ChildItem -Path "Cert:\CurrentUser\My" | + Where-Object { $_.Subject -match "testuser" } | Select-Object -First 1 + if (-not $importedCert) { + Write-Host "ERROR: imported testuser cert not found in CurrentUser\My" + exit 1 + } + Write-Host "Client cert imported: $($importedCert.Subject) ($($importedCert.Thumbprint))" + + # Export the CN for the client cert store lookup. The full X.500 + # DN contains commas which break command-line argument parsing. + $cn = $importedCert.Subject + if ($cn -match 'CN=([^,]+)') { $cn = $matches[1].Trim() } + Add-Content -Path $env:GITHUB_ENV -Value "CLIENT_CERT_SUBJECT=$cn" + } + + - name: Create Windows user testuser + 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' + + New-Item -ItemType Directory -Path $homeDir -Force | Out-Null + New-Item -ItemType Directory -Path $sshDir -Force | Out-Null + + # 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") { + net user testuser /homedir:$homeDir 2>$null + } else { + Write-Host "net user failed: $o" + exit 1 + } + } + + # X509 auth verifies the client cert against the CA; authorized_keys + # is not used but the file should exist. + "" | Out-File -FilePath $authKeysFile -Encoding ASCII -NoNewline + icacls $authKeysFile /grant "testuser:R" /q + + # 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 + + - name: Create wolfSSHd config file + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $configContent = @" + Port ${{env.TEST_PORT}} + PasswordAuthentication yes + PermitRootLogin yes + "@ + + # Server verifies client X509 certs against the test CA (PEM format, + # as per apps/wolfsshd/test/create_sshd_config.sh) + $caCertPath = (Resolve-Path "keys\ca-cert-ecc.pem").Path + $configContent += @" + + TrustedUserCAKeys $caCertPath + "@ + + if ("${{ matrix.server_key_source }}" -eq "store") { + # The certificate is part of the store entry; do NOT specify + # HostCertificate separately. + $configContent += @" + + HostKeyStore My + HostKeyStoreSubject $env:SERVER_CERT_SUBJECT + HostKeyStoreFlags LOCAL_MACHINE + "@ + } else { + $keyPath = (Resolve-Path "keys\server-key.pem").Path + $certPath = (Resolve-Path "keys\server-cert.pem").Path + $configContent += @" + + HostKey $keyPath + HostCertificate $certPath + "@ + } + + $configContent | Out-File -FilePath sshd_config_test -Encoding ASCII + Write-Host "=== wolfSSHd Config ===" + Get-Content sshd_config_test + + - name: Find wolfSSH executables + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $searchRoot = "${{ github.workspace }}" + + $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 (-not $sshdExe) { + Write-Host "ERROR: wolfsshd.exe not found" + Get-ChildItem -Path $searchRoot -Recurse -Filter "*.exe" -ErrorAction SilentlyContinue | Select-Object FullName + exit 1 + } + Write-Host "wolfsshd.exe: $($sshdExe.FullName)" + Add-Content -Path $env:GITHUB_ENV -Value "SSHD_PATH=$($sshdExe.FullName)" + + # SFTP client (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 (-not $sftpExe) { + 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 + } + Write-Host "SFTP client: $($sftpExe.FullName)" + Add-Content -Path $env:GITHUB_ENV -Value "SFTP_PATH=$($sftpExe.FullName)" + + # echoserver (used for the cert store host key test) + $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 "echoserver.exe: $($echoserverExe.FullName)" + Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PATH=$($echoserverExe.FullName)" + } elseif ("${{ matrix.server_key_source }}" -eq "store") { + Write-Host "ERROR: echoserver.exe not found (required for cert store server test)" + exit 1 + } + + - name: Copy wolfSSL DLL to executable directory (if dynamic build) + working-directory: ${{ github.workspace }} + shell: pwsh + run: | + $sshdDir = Split-Path -Parent $env:SSHD_PATH + + # If wolfssl.lib is next to wolfsshd.exe, it's a static build - no DLL needed + if (Test-Path (Join-Path $sshdDir "wolfssl.lib")) { + Write-Host "wolfssl.lib present beside wolfsshd.exe - static build; wolfssl.dll not required" + exit 0 + } + + $wolfsslDll = Get-ChildItem -Path "${{ github.workspace }}\wolfssl" -Recurse -Filter "wolfssl.dll" -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($wolfsslDll) { + Copy-Item -Path $wolfsslDll.FullName -Destination (Join-Path $sshdDir "wolfssl.dll") -Force + Write-Host "Copied wolfssl.dll to $sshdDir" + } else { + Write-Host "wolfssl.dll not found; if build is static (wolfssl.lib in output), this is OK" + } + + - 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 the config + # and key files and run the exe (and load wolfssl.dll if dynamic). + # /T = apply to existing files and subdirs; (OI)(CI) = inherit to new objects + $wolfsshRoot = (Get-Location).Path + icacls $wolfsshRoot /grant "NT AUTHORITY\SYSTEM:(OI)(CI)RX" /T /q + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: icacls failed on $wolfsshRoot" + exit 1 + } + $sshdDir = (Resolve-Path (Split-Path -Parent $env:SSHD_PATH)).Path + icacls $sshdDir /grant "NT AUTHORITY\SYSTEM:(OI)(CI)RX" /T /q + + - name: Start echoserver with cert store host key + if: matrix.server_key_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # Exercise the cert store host key (-W Store:Subject:Location) in the + # echoserver before the wolfsshd service test. Start it detached (via + # cmd start /B) so it survives after this step ends. + $echoserverPath = $env:ECHOSERVER_PATH + $exeDir = Split-Path -Parent $echoserverPath + $port = ${{env.TEST_PORT}} + $spec = "My:wolfSSH-Test-Server:LOCAL_MACHINE" + $wolfsshRoot = "${{ github.workspace }}\wolfssh" + + # -a : verify client X.509 certs + # -K testuser:: register testuser with the auth callback + $caCertPem = Join-Path $wolfsshRoot "keys\ca-cert-ecc.pem" + $clientCert = (Resolve-Path (Join-Path $wolfsshRoot $env:CLIENT_CERT_FILE)).Path + $echoArgs = @("-W", $spec, "-p", $port, "-a", $caCertPem, "-K", "testuser:$clientCert") + + $argStr = $echoArgs -join " " + $echoLogFile = Join-Path $wolfsshRoot "echoserver_debug.log" + Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_LOG=$echoLogFile" + Write-Host "Command: $echoserverPath $argStr" + $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)" + } + + # Wait for the port to be listening + $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 {} + if (-not (Get-Process -Name "echoserver" -ErrorAction SilentlyContinue)) { + Write-Host "ERROR: echoserver exited before port was ready" + if (Test-Path $echoLogFile) { Get-Content $echoLogFile } + exit 1 + } + } + if ($elapsed -ge $timeout) { + Write-Host "ERROR: Port $port not listening after ${timeout}s" + if (Test-Path $echoLogFile) { Get-Content $echoLogFile } + exit 1 + } + Write-Host "echoserver is listening on port $port" + + - name: Test SFTP against echoserver (cert store host key) + if: matrix.server_key_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $testPort = ${{env.TEST_PORT}} + $sftpPath = $env:SFTP_PATH + + @" + pwd + ls + quit + "@ | Out-File -FilePath sftp_echo_commands.txt -Encoding ASCII + + $sftpArgs = @("-u", "testuser", "-h", "localhost", "-p", "$testPort") + $caCertDer = (Resolve-Path "keys\ca-cert-ecc.der").Path + if ("${{ matrix.client_key_source }}" -eq "store") { + $sftpArgs += "-W", "My:$($env:CLIENT_CERT_SUBJECT):CURRENT_USER" + } else { + $sftpArgs += "-J", (Resolve-Path $env:CLIENT_CERT_FILE).Path + $sftpArgs += "-i", (Resolve-Path $env:CLIENT_KEY_FILE).Path + } + # -A: CA cert for host verification; -X: ignore peer IP vs cert checks + $sftpArgs += "-A", $caCertDer, "-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 ===" + if (Test-Path sftp_echo_output.txt) { Get-Content sftp_echo_output.txt } + Write-Host "=== SFTP Error ===" + if (Test-Path sftp_echo_error.txt) { Get-Content sftp_echo_error.txt } + + if ($process.ExitCode -ne 0) { + $echoLog = $env:ECHOSERVER_LOG + if (-not [string]::IsNullOrEmpty($echoLog) -and (Test-Path $echoLog)) { + Write-Host "=== Echoserver Log ===" + Get-Content $echoLog + } + Write-Host "ERROR: SFTP against echoserver failed" + exit 1 + } + 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)) { + 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: Start wolfSSHd as Windows service + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $sshdPathFull = (Resolve-Path $env:SSHD_PATH).Path + $configPathFull = (Resolve-Path "sshd_config_test").Path + $serviceName = "wolfsshd" + + # Remove service if it already exists + $existingService = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + if ($existingService) { + 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 + } + + # 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. + $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 + } + + $startResult = sc.exe start $serviceName + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Failed to start service" + Write-Host $startResult + sc.exe query $serviceName + exit 1 + } + + Start-Sleep -Seconds 5 + $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + if (-not $service -or $service.Status -ne 'Running') { + Write-Host "ERROR: Service is not running. Status: $($service.Status)" + sc.exe query $serviceName + Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Service Control Manager'} -MaxEvents 20 -ErrorAction SilentlyContinue | + Where-Object { $_.Message -like "*$serviceName*" } | + Select-Object TimeCreated, LevelDisplayName, Message | Format-List + exit 1 + } + + Write-Host "wolfSSHd service is running" + 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: | + $testPort = ${{env.TEST_PORT}} + $sftpPath = $env:SFTP_PATH + + # Verify the server is listening before running the client + 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) + $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: $_" + exit 1 + } + + @" + pwd + ls + quit + "@ | Out-File -FilePath sftp_commands.txt -Encoding ASCII + + $sftpArgs = @("-u", "testuser", "-h", "localhost", "-p", "$testPort") + $caCertDer = (Resolve-Path "keys\ca-cert-ecc.der").Path + if ("${{ matrix.client_key_source }}" -eq "store") { + $sftpArgs += "-W", "My:$($env:CLIENT_CERT_SUBJECT):CURRENT_USER" + } else { + $sftpArgs += "-J", (Resolve-Path $env:CLIENT_CERT_FILE).Path + $sftpArgs += "-i", (Resolve-Path $env:CLIENT_KEY_FILE).Path + } + # -A: CA cert for host verification; -X: ignore peer IP vs cert checks + $sftpArgs += "-A", $caCertDer, "-X" + + Write-Host "Running: $sftpPath $($sftpArgs -join ' ')" + Write-Host "Test matrix: server=${{ matrix.server_key_source }}, client=${{ matrix.client_key_source }}" + $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 } + + 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: Cleanup + if: always() + shell: pwsh + run: | + # Stop echoserver if it is still running + $echoserverPid = $env:ECHOSERVER_PID + if (-not [string]::IsNullOrEmpty($echoserverPid)) { + Stop-Process -Id $echoserverPid -Force -ErrorAction SilentlyContinue + } + Get-Process -Name "echoserver" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + + # 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') { + Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + } + sc.exe delete $serviceName | Out-Null + } + + # Remove test certificates from the stores + Get-ChildItem -Path "Cert:\CurrentUser\My" | Where-Object { + $_.Subject -like "*wolfSSH-Test*" -or $_.Subject -like "*testuser*" + } | Remove-Item -Force -ErrorAction SilentlyContinue + Get-ChildItem -Path "Cert:\LocalMachine\My" | Where-Object { + $_.Subject -like "*wolfSSH-Test*" + } | Remove-Item -Force -ErrorAction SilentlyContinue + Write-Host "Cleaned up test certificates" diff --git a/apps/wolfsshd/configuration.c b/apps/wolfsshd/configuration.c index c16c63b16..fd47db5ff 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -71,12 +71,22 @@ struct WOLFSSHD_CONFIG { char* hostKeyFile; char* hostCertFile; char* userCAKeysFile; +#ifdef WOLFSSH_WINDOWS_CERT_STORE + char* hostKeyStore; + char* hostKeyStoreSubject; + char* hostKeyStoreFlags; +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ 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; @@ -87,6 +97,8 @@ struct WOLFSSHD_CONFIG { byte permitEmptyPasswords:1; byte authKeysFileSet:1; /* if not set then no explicit authorized keys */ byte strictModes:1; /* enforce file permission/ownership checks */ + byte useSystemCA:1; + byte useUserCAStore:1; }; /* Maximum depth of nested Include directives. Bounds the recursion @@ -359,6 +371,16 @@ void wolfSSHD_ConfigFree(WOLFSSHD_CONFIG* conf) FreeString(¤t->forceCmd, heap); FreeString(¤t->usrAppliesTo, heap); FreeString(¤t->groupAppliesTo, heap); +#ifdef WOLFSSH_WINDOWS_CERT_STORE + FreeString(¤t->hostKeyStore, heap); + FreeString(¤t->hostKeyStoreSubject, heap); + FreeString(¤t->hostKeyStoreFlags, heap); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ +#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; @@ -385,6 +407,11 @@ enum { OPT_PROTOCOL = 9, OPT_LOGIN_GRACE_TIME = 10, OPT_HOST_KEY = 11, +#ifdef WOLFSSH_WINDOWS_CERT_STORE + OPT_HOST_KEY_STORE = 50, + OPT_HOST_KEY_STORE_SUBJECT = 51, + OPT_HOST_KEY_STORE_FLAGS = 52, +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ OPT_PASSWORD_AUTH = 12, OPT_PORT = 13, OPT_PERMIT_ROOT = 14, @@ -399,9 +426,22 @@ enum { OPT_BANNER = 23, OPT_PUBKEY_AUTH = 24, OPT_STRICT_MODES = 25, + OPT_TRUSTED_SYSTEM_CA_KEYS = 26, + OPT_TRUSTED_USER_CA_STORE = 27, +#ifdef USE_WINDOWS_API + OPT_WIN_USER_STORES = 28, + OPT_WIN_USER_DW_FLAGS = 29, + OPT_WIN_USER_PV_PARA = 30 +#endif /* USE_WINDOWS_API */ }; enum { - NUM_OPTIONS = 26 + NUM_OPTIONS = 28 +#ifdef USE_WINDOWS_API + + 3 +#endif /* USE_WINDOWS_API */ +#ifdef WOLFSSH_WINDOWS_CERT_STORE + + 3 +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ }; static const CONFIG_OPTION options[NUM_OPTIONS] = { @@ -416,6 +456,15 @@ 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 WOLFSSH_WINDOWS_CERT_STORE + {OPT_HOST_KEY_STORE_SUBJECT, "HostKeyStoreSubject"}, + {OPT_HOST_KEY_STORE_FLAGS, "HostKeyStoreFlags"}, + {OPT_HOST_KEY_STORE, "HostKeyStore"}, +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ {OPT_HOST_KEY, "HostKey"}, {OPT_PASSWORD_AUTH, "PasswordAuthentication"}, {OPT_PUBKEY_AUTH, "PubkeyAuthentication"}, @@ -431,6 +480,13 @@ static const CONFIG_OPTION options[NUM_OPTIONS] = { {OPT_PIDFILE, "PidFile"}, {OPT_BANNER, "Banner"}, {OPT_STRICT_MODES, "StrictModes"}, + {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 */ @@ -1265,6 +1321,9 @@ 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; @@ -1274,6 +1333,39 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, case OPT_STRICT_MODES: ret = HandleStrictModes(*conf, value); 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 WOLFSSH_WINDOWS_CERT_STORE + 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_WINDOWS_CERT_STORE */ default: break; } @@ -1618,6 +1710,197 @@ 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 */ + if (CreateString(&conf->winUserStores, "CERT_STORE_PROV_SYSTEM", + (int)WSTRLEN("CERT_STORE_PROV_SYSTEM"), conf->heap) + != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unable to create default winUserStores"); + return NULL; + } + } + + 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) { + /* free any previously set value before replacing it */ + FreeString(&conf->winUserStores, conf->heap); + 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 */ + if (CreateString(&conf->winUserDwFlags, + "CERT_SYSTEM_STORE_CURRENT_USER", + (int)WSTRLEN("CERT_SYSTEM_STORE_CURRENT_USER"), + conf->heap) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unable to create default winUserDwFlags"); + return NULL; + } + } + + 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) { + /* free any previously set value before replacing it */ + FreeString(&conf->winUserDwFlags, conf->heap); + 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 */ + if (CreateString(&conf->winUserPvPara, "MY", + (int)WSTRLEN("MY"), conf->heap) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unable to create default winUserPvPara"); + return NULL; + } + } + + 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) { + /* free any previously set value before replacing it */ + FreeString(&conf->winUserPvPara, conf->heap); + 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; @@ -1651,6 +1934,43 @@ static int SetFileString(char** dst, const char* src, void* heap) return ret; } +#ifdef WOLFSSH_WINDOWS_CERT_STORE +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_WINDOWS_CERT_STORE */ + 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 40b541a27..a424cb9d2 100644 --- a/apps/wolfsshd/configuration.h +++ b/apps/wolfsshd/configuration.h @@ -51,6 +51,23 @@ 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 WOLFSSH_WINDOWS_CERT_STORE +char* wolfSSHD_ConfigGetHostKeyStore(const WOLFSSHD_CONFIG* conf); +char* wolfSSHD_ConfigGetHostKeyStoreSubject(const WOLFSSHD_CONFIG* conf); +char* wolfSSHD_ConfigGetHostKeyStoreFlags(const WOLFSSHD_CONFIG* conf); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ +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 b25a3f2fb..c827a1647 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -38,6 +38,18 @@ #include #include +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #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_WINDOWS_CERT_STORE */ + #define WOLFSSH_TEST_SERVER #include @@ -372,56 +384,131 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, /* Load in host private key */ if (ret == WS_SUCCESS) { +#ifdef WOLFSSH_WINDOWS_CERT_STORE + char* hostKeyStore = wolfSSHD_ConfigGetHostKeyStore(conf); + char* hostKeyStoreSubject = wolfSSHD_ConfigGetHostKeyStoreSubject(conf); + char* hostKeyStoreFlags = wolfSSHD_ConfigGetHostKeyStoreFlags(conf); - char* hostKey = wolfSSHD_ConfigGetHostKeyFile(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; + word32 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 = (word32)atoi(hostKeyStoreFlags); + } + } - if (hostKey == NULL) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No host private key set"); - ret = WS_BAD_ARGUMENT; - } - else { - byte* data; - word32 dataSz = 0; + /* Convert to wide strings */ + storeNameLen = MultiByteToWideChar(CP_UTF8, 0, hostKeyStore, -1, NULL, 0); + subjectNameLen = MultiByteToWideChar(CP_UTF8, 0, hostKeyStoreSubject, -1, NULL, 0); - /* The host private key is a secret trust anchor: refuse a symlink, - * an unsafe owner or path, or a group/world readable/writable - * file. */ - data = getBufferFromFile(hostKey, &dataSz, heap, - WOLFSSHD_LOAD_SECRET); - if (data == NULL) { - /* NULL means the secure gate rejected the file (bad owner, - * symlink, group/world writable/readable; reason already - * logged) or the read failed, so report a file error rather - * than a memory error. */ + if (storeNameLen == 0 || subjectNameLen == 0) { wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Error reading host key file."); - ret = WS_BAD_FILE_E; + "[SSHD] Failed to convert cert store strings to wide characters"); + ret = WS_BAD_ARGUMENT; + } else { + 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"); + } + } - if (ret == WS_SUCCESS) { - if (wc_PemToDer(data, dataSz, PRIVATEKEY_TYPE, &der, NULL, - NULL, NULL) != 0) { - wolfSSH_Log(WS_LOG_DEBUG, "[SSHD] Failed to convert host " - "private key from PEM. Assuming key in DER " - "format."); - privBuf = data; - privBufSz = dataSz; + if (wStoreName != NULL) { + WFREE(wStoreName, heap, DYNTYPE_SSHD); } - else { - privBuf = der->buffer; - privBufSz = der->length; + if (wSubjectName != NULL) { + WFREE(wSubjectName, heap, DYNTYPE_SSHD); } + } + } else +#elif defined(WOLFSSH_CERTS) + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] WOLFSSH_WINDOWS_CERT_STORE not defined - cert store support disabled"); +#else + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] WOLFSSH_CERTS not defined - cert store support disabled"); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + { + char* hostKey = wolfSSHD_ConfigGetHostKeyFile(conf); + + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] File-based host key path entered. hostKey=%s", + hostKey ? hostKey : "(null)"); - if (wolfSSH_CTX_UsePrivateKey_buffer(*ctx, privBuf, privBufSz, - WOLFSSH_FORMAT_ASN1) < 0) { + if (hostKey == NULL) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No host private key set"); + ret = WS_BAD_ARGUMENT; + } + else { + byte* data; + word32 dataSz = 0; + + /* The host private key is a secret trust anchor: refuse a symlink, + * an unsafe owner or path, or a group/world readable/writable + * file. */ + data = getBufferFromFile(hostKey, &dataSz, heap, + WOLFSSHD_LOAD_SECRET); + if (data == NULL) { + /* NULL means the secure gate rejected the file (bad owner, + * symlink, group/world writable/readable; reason already + * logged) or the read failed, so report a file error rather + * than a memory error. */ wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Failed to use host private key."); - ret = WS_BAD_ARGUMENT; + "[SSHD] Error reading host key file."); + ret = WS_BAD_FILE_E; + } - freeBufferFromFile(data, heap); - wc_FreeDer(&der); + if (ret == WS_SUCCESS) { + if (wc_PemToDer(data, dataSz, PRIVATEKEY_TYPE, &der, NULL, + NULL, NULL) != 0) { + wolfSSH_Log(WS_LOG_DEBUG, "[SSHD] Failed to convert host " + "private key from PEM. Assuming key in DER " + "format."); + privBuf = data; + privBufSz = dataSz; + } + else { + privBuf = der->buffer; + privBufSz = der->length; + } + + if (wolfSSH_CTX_UsePrivateKey_buffer(*ctx, privBuf, privBufSz, + WOLFSSH_FORMAT_ASN1) < 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to use host private key."); + ret = WS_BAD_ARGUMENT; + } + + freeBufferFromFile(data, heap); + wc_FreeDer(&der); + } } } } @@ -475,6 +562,72 @@ 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); + } + } + #else + /* The system/user CA-store directives are parsed unconditionally, so warn + * if they were set but wolfSSL was built without WOLFSSL_SYS_CA_CERTS, + * rather than silently ignoring them. */ + if (ret == WS_SUCCESS && (wolfSSHD_ConfigGetSystemCA(conf) + || wolfSSHD_ConfigGetUserCAStore(conf))) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_TrustedSystemCAKeys/wolfSSH_TrustedUserCaStore set " + "but wolfSSL was built without WOLFSSL_SYS_CA_CERTS; ignoring."); + } + #endif + + /* load in CA certs from file set */ if (ret == WS_SUCCESS) { char* caCert = wolfSSHD_ConfigGetUserCAKeysFile(conf); if (caCert != NULL) { @@ -2563,6 +2716,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) { @@ -2594,10 +2765,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/configure.ac b/configure.ac index 07731f0b3..68326f056 100644 --- a/configure.ac +++ b/configure.ac @@ -215,6 +215,12 @@ AC_ARG_ENABLE([certs], [AS_HELP_STRING([--enable-certs],[Enable X.509 cert support (default: disabled)])], [ENABLED_CERTS=$enableval],[ENABLED_CERTS=no]) +# Windows certificate store (host/client keys) +AC_ARG_ENABLE([windows-cert-store], + [AS_HELP_STRING([--enable-windows-cert-store],[Enable Windows certificate store integration for keys (default: disabled)])], + [ENABLED_WINDOWS_CERT_STORE=$enableval], + [ENABLED_WINDOWS_CERT_STORE=no]) + # TPM 2.0 Support AC_ARG_ENABLE([tpm], [AS_HELP_STRING([--enable-tpm],[Enable TPM 2.0 support (default: disabled)])], @@ -276,6 +282,12 @@ AS_IF([test "x$ENABLED_AGENT" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_AGENT"]) AS_IF([test "x$ENABLED_CERTS" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_CERTS"]) +AS_IF([test "x$ENABLED_WINDOWS_CERT_STORE" = "xyes"], + [AS_IF([test "x$ENABLED_CERTS" != "xyes"], + [AC_MSG_ERROR([--enable-windows-cert-store requires X.509 cert support (--enable-certs)])]) + AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_WINDOWS_CERT_STORE" + AS_CASE([$host], + [*mingw*|*msys*|*cygwin*],[LIBS="$LIBS -lcrypt32 -lncrypt"])]) AS_IF([test "x$ENABLED_SMALLSTACK" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_SMALL_STACK"]) AS_IF([test "x$ENABLED_SSHCLIENT" = "xyes"], @@ -380,4 +392,5 @@ AS_ECHO([" * agent: $ENABLED_AGENT"]) AS_ECHO([" * TPM 2.0 support: $ENABLED_TPM"]) AS_ECHO([" * TCP/IP Forwarding: $ENABLED_FWD"]) AS_ECHO([" * X.509 Certs: $ENABLED_CERTS"]) +AS_ECHO([" * Windows cert store: $ENABLED_WINDOWS_CERT_STORE"]) AS_ECHO([" * Examples: $ENABLED_EXAMPLES"]) diff --git a/examples/client/common.c b/examples/client/common.c index 30c513832..f0c5b4c41 100644 --- a/examples/client/common.c +++ b/examples/client/common.c @@ -48,6 +48,11 @@ #ifdef WOLFSSH_CERTS #include + #ifdef WOLFSSH_WINDOWS_CERT_STORE + #include + #include + #include + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif static byte userPublicKeyBuf[512]; @@ -1162,3 +1167,83 @@ void ClientFreeBuffers(const char* pubKeyName, const char* privKeyName, userPrivateKeySz = 0; wc_ForceZero(userPassword, sizeof(userPassword)); } + +#ifdef WOLFSSH_WINDOWS_CERT_STORE +int ClientSetPrivateKeyFromStore(WOLFSSH_CTX* ctx, + const wchar_t* storeName, word32 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_WINDOWS_CERT_STORE */ diff --git a/examples/client/common.h b/examples/client/common.h index 6ea330c2e..ffb97638f 100644 --- a/examples/client/common.h +++ b/examples/client/common.h @@ -35,6 +35,11 @@ void ClientFreeBuffers(const char* pubKeyName, const char* privKeyName, #ifdef WOLFSSH_TPM int ClientSetTpm(WOLFSSH* ssh); #endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE +int ClientSetPrivateKeyFromStore(WOLFSSH_CTX* ctx, + const wchar_t* storeName, word32 dwFlags, const wchar_t* subjectName); +int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif /* WOLFSSH_COMMON_H */ diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index f86fbc75f..dc0047812 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -51,6 +52,7 @@ #endif #include "examples/echoserver/echoserver.h" +#include "examples/client/common.h" #if defined(WOLFSSL_PTHREADS) && defined(WOLFSSL_TEST_GLOBAL_REQ) #include @@ -117,6 +119,16 @@ #define SOCKET_EWOULDBLOCK WSAEWOULDBLOCK #endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #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 @@ -2827,6 +2839,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"); +#ifdef WOLFSSH_WINDOWS_CERT_STORE + 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"); } @@ -2935,6 +2950,9 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #ifdef WOLFSSH_CERTS char* caCert = NULL; #endif + #ifdef WOLFSSH_WINDOWS_CERT_STORE + const char* certStoreSpec = NULL; + #endif int argc = serverArgs->argc; char** argv = serverArgs->argv; @@ -2943,8 +2961,11 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) kbAuthData.promptCount = 0; #endif + #ifdef WOLFSSH_WINDOWS_CERT_STORE + 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:G:H"; + const char* optlist = "?1a:d:efEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:HW:"; myoptind = 0; while ((ch = mygetopt(argc, argv, optlist)) != -1) { switch (ch) { @@ -3069,6 +3090,12 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) useCustomHighWaterCb = 1; break; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + case 'W': + certStoreSpec = myoptarg; + break; + #endif + default: ShowUsage(); serverArgs->return_code = MY_EX_USAGE; @@ -3261,6 +3288,31 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) } #endif + #ifdef WOLFSSH_WINDOWS_CERT_STORE + if (certStoreSpec != NULL) { + /* Load host key from Windows certificate store */ + wchar_t* wStoreName = NULL; + wchar_t* wSubjectName = NULL; + word32 dwFlags = 0; + int ret; + + ret = wolfSSH_ParseCertStoreSpec(certStoreSpec, &wStoreName, + &wSubjectName, &dwFlags, NULL); + if (ret != WS_SUCCESS) { + ES_ERROR("Invalid cert store spec. Use: store:subject:flags\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"); + } + loadDefaultHostKeys = 0; + } + #endif + if (loadDefaultHostKeys) { bufSz = load_key(peerEcc, keyLoadBuf, bufSz); if (bufSz == 0) { @@ -3618,7 +3670,29 @@ int wolfSSH_Echoserver(int argc, char** argv) #endif #if !defined(WOLFSSL_NUCLEUS) && !defined(INTEGRITY) && !defined(__INTEGRITY) - ChangeToWolfSshRoot(); + { + int useStore = 0; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* 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 b7e7c332d..5155ace99 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 WOLFSSH_WINDOWS_CERT_STORE + #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_WINDOWS_CERT_STORE */ #endif #if defined(WOLFSSH_SFTP) && !defined(NO_WOLFSSH_CLIENT) @@ -391,6 +403,10 @@ 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 WOLFSSH_WINDOWS_CERT_STORE + printf(" -W Windows cert store: \"store:subject:flags\"\n"); + printf(" Example: -W \"My:CN=MyCert:CURRENT_USER\"\n"); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ #ifdef WOLFSSH_CERTS printf(" -J filename for DER certificate to use\n"); printf(" Certificate example : client -u orange \\\n"); @@ -1546,13 +1562,20 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) char* pubKeyName = NULL; char* certName = NULL; char* caCert = NULL; +#ifdef WOLFSSH_WINDOWS_CERT_STORE + const char* certStoreSpec = NULL; /* Format: "store:subject:flags" */ +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ 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 WOLFSSH_WINDOWS_CERT_STORE + "W:" +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + )) != -1) { switch (ch) { case 'd': defaultSftpPath = myoptarg; @@ -1630,6 +1653,12 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) #endif #endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE + case 'W': + certStoreSpec = myoptarg; + break; +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + case '?': ShowUsage(); exit(EXIT_SUCCESS); @@ -1676,26 +1705,66 @@ 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 WOLFSSH_CERTS - /* passed in certificate to use */ - if (certName) { - ret = ClientUseCert(certName, heap); - } - else -#endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE + if (certStoreSpec != NULL) { + wchar_t* wStoreName = NULL; + wchar_t* wSubjectName = NULL; + word32 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_WINDOWS_CERT_STORE */ { - 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"); + } - ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, heap); + #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); + } 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 8ed347f93..26125b088 100644 --- a/ide/winvs/wolfsftp-client/wolfsftp-client.vcxproj +++ b/ide/winvs/wolfsftp-client/wolfsftp-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) @@ -400,7 +400,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) @@ -472,7 +472,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/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 661d5c5d9..d0ba0db6c 100644 --- a/src/certman.c +++ b/src/certman.c @@ -36,7 +36,6 @@ #endif -#include #include #include #include @@ -45,6 +44,16 @@ #include #include +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #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 @@ -85,6 +94,29 @@ 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 == cm) { + return WS_SUCCESS; + } + wolfSSL_CertManager_up_ref(cm); + if (ctx->certMan->cm != NULL) { + wolfSSL_CertManagerFree(ctx->certMan->cm); + } + ctx->certMan->cm = cm; + + return WS_SUCCESS; +} + + static WOLFSSH_CERTMAN* _CertMan_init(WOLFSSH_CERTMAN* cm, void* heap) { WOLFSSH_CERTMAN* ret = NULL; @@ -589,4 +621,101 @@ static int CheckProfile(DecodedCert* cert, int profile) } #endif /* WOLFSSH_NO_FPKI */ + +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* 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, + word32* 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 = (word32)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); + + if (wStoreNameLen == 0 || wSubjectNameLen == 0) { + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_FATAL_ERROR; + } + + *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 /* WOLFSSH_WINDOWS_CERT_STORE */ + + #endif /* WOLFSSH_CERTS */ diff --git a/src/internal.c b/src/internal.c index 64db49dce..12b34fa6c 100644 --- a/src/internal.c +++ b/src/internal.c @@ -64,6 +64,27 @@ #include #endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #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_PAD_PKCS1 + #define BCRYPT_PAD_PKCS1 0x00000002 + #endif + +static int ExtractPubKeyDerFromCert(const byte* certDer, word32 certDerSz, + byte** outDer, word32* outDerSz, void* heap); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + #ifdef NO_INLINE #include #else @@ -1149,6 +1170,40 @@ WOLFSSH_CTX* CtxInit(WOLFSSH_CTX* ctx, byte side, void* heap) } +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Release any MS Certificate Store state held by a private key slot and reset + * the cert-store fields so the slot is no longer treated as cert-store backed. + * Safe to call on a slot that never held cert-store state. */ +static void ClearCertStoreKey(WOLFSSH_CTX* ctx, WOLFSSH_PVT_KEY* pvtKey) +{ + if (pvtKey->certStoreContext != NULL) { + CertFreeCertificateContext((PCCERT_CONTEXT)pvtKey->certStoreContext); + pvtKey->certStoreContext = NULL; + } + if (pvtKey->storeName != NULL) { + WFREE(pvtKey->storeName, ctx->heap, DYNTYPE_STRING); + pvtKey->storeName = NULL; + } + if (pvtKey->subjectName != NULL) { + WFREE(pvtKey->subjectName, ctx->heap, DYNTYPE_STRING); + pvtKey->subjectName = NULL; + } + pvtKey->useCertStore = 0; +} + + +/* Returns 1 if the slot is genuinely backed by the MS Certificate Store. + * Requires a live cert context and no in-memory private key, so a slot that + * was later overwritten by a file-based key (which clears these) is not + * mistaken for a cert-store key. */ +static INLINE int IsCertStoreKey(const WOLFSSH_PVT_KEY* pvtKey) +{ + return pvtKey != NULL && pvtKey->useCertStore + && pvtKey->certStoreContext != NULL && pvtKey->key == NULL; +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + + void CtxResourceFree(WOLFSSH_CTX* ctx) { WLOG(WS_LOG_DEBUG, "Entering CtxResourceFree()"); @@ -1169,6 +1224,9 @@ void CtxResourceFree(WOLFSSH_CTX* ctx) ctx->privateKey[i].cert = NULL; ctx->privateKey[i].certSz = 0; } +#ifdef WOLFSSH_WINDOWS_CERT_STORE + ClearCertStoreKey(ctx, &ctx->privateKey[i]); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif ctx->privateKey[i].publicKeyFmt = ID_NONE; } @@ -2385,7 +2443,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; @@ -2611,6 +2669,12 @@ static int SetHostCertificate(WOLFSSH_CTX* ctx, pvtKey->publicKeyFmt = certId; } + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* A file-based certificate is replacing this slot's contents; drop + * any cert-store state so it is not mistaken for a cert-store key. */ + ClearCertStoreKey(ctx, pvtKey); + #endif + pvtKey->cert = der; pvtKey->certSz = derSz; @@ -2662,6 +2726,13 @@ static int SetHostPrivateKey(WOLFSSH_CTX* ctx, pvtKey->publicKeyFmt = keyId; } + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* This slot is now backed by an in-memory key; drop any cert-store + * state it may have carried so signing/K_S do not use a stale + * certificate context. */ + ClearCertStoreKey(ctx, pvtKey); + #endif + pvtKey->key = der; pvtKey->keySz = derSz; #ifdef WOLFSSH_TPM @@ -12407,6 +12478,9 @@ struct wolfSSH_sigKeyBlockFull { word32 pubKeyNameSz; const char *pubKeyFmtName; word32 pubKeyFmtNameSz; +#ifdef WOLFSSH_WINDOWS_CERT_STORE + const WOLFSSH_PVT_KEY* pvtKey; /* Pointer to private key for cert store support */ +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ union { #ifndef WOLFSSH_NO_RSA struct { @@ -12677,6 +12751,10 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, #ifdef WOLFSSH_TPM ssh->handshake->useTpm = ssh->ctx->privateKey[keyIdx].isTpm; #endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Set pointer to private key for cert store support */ + sigKeyBlock_ptr->pvtKey = &ssh->ctx->privateKey[keyIdx]; +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ switch (sigKeyBlock_ptr->pubKeyId) { #ifndef WOLFSSH_NO_RSA @@ -12688,26 +12766,77 @@ 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); - #ifdef WOLFSSH_TPM - if (ret == 0 && ssh->ctx->privateKey[keyIdx].isTpm) { - /* No private key in RAM; take the public key from the TPM. */ - ret = wolfTPM2_RsaKey_TpmToWolf(ssh->ctx->tpmDev, - ssh->ctx->tpmKey, &sigKeyBlock_ptr->sk.rsa.key); - if (ret != 0) - ret = WS_RSA_E; +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Check if this is a cert store key */ + if (IsCertStoreKey(&ssh->ctx->privateKey[keyIdx])) { + /* 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_WINDOWS_CERT_STORE */ + { + /* 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); + #ifdef WOLFSSH_TPM + if (ret == 0 && ssh->ctx->privateKey[keyIdx].isTpm) { + /* No private key in RAM; take the public key from the TPM. */ + ret = wolfTPM2_RsaKey_TpmToWolf(ssh->ctx->tpmDev, + ssh->ctx->tpmKey, &sigKeyBlock_ptr->sk.rsa.key); + if (ret != 0) + ret = WS_RSA_E; + } + else + #endif /* WOLFSSH_TPM */ + if (ret == 0) + ret = wc_RsaPrivateKeyDecode(ssh->ctx->privateKey[keyIdx].key, + &scratch, &sigKeyBlock_ptr->sk.rsa.key, + (int)ssh->ctx->privateKey[keyIdx].keySz); } - else - #endif /* WOLFSSH_TPM */ - 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) { @@ -12823,6 +12952,45 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, } else #endif /* WOLFSSH_TPM */ +#ifdef WOLFSSH_WINDOWS_CERT_STORE + if (ret == 0 && IsCertStoreKey(&ssh->ctx->privateKey[keyIdx])) { + /* For cert store keys, extract the ECC public key from the + * DER certificate. Signing uses the cert store handle via + * SignHEcdsa's cert-store branch. */ + 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; + ret = wc_EccPublicKeyDecode(pubKeyDer, &idx2, + &sigKeyBlock_ptr->sk.ecc.key, pubKeyDerSz); + } + if (pubKeyDer != NULL) + WFREE(pubKeyDer, heap, DYNTYPE_PUBKEY); + + if (ret != 0) { + WLOG(WS_LOG_DEBUG, + "SendKexDhReply: cert store ECC 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_WINDOWS_CERT_STORE */ if (ret == 0) ret = wc_EccPrivateKeyDecode(ssh->ctx->privateKey[keyIdx].key, &scratch, &sigKeyBlock_ptr->sk.ecc.key, @@ -13810,6 +13978,260 @@ static int KeyAgreeEcdhMlKem_server(WOLFSSH* ssh, byte hashId, #endif /* ML-KEM variants */ +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* 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; +} + + +#ifdef WOLFSSH_CERTS +/* Map a public key algorithm ID to the base key format ID stored in a + * private key slot's publicKeyFmt. The RSA signature variants and the + * X509 form collapse to ID_SSH_RSA, and the X509 ECDSA forms collapse to + * the matching plain curve ID. */ +static byte CertStoreBaseKeyId(byte id) +{ + byte baseId; + + baseId = id; + switch (id) { + case ID_RSA_SHA2_256: + case ID_RSA_SHA2_512: + case ID_X509V3_SSH_RSA: + baseId = ID_SSH_RSA; + break; + case ID_X509V3_ECDSA_SHA2_NISTP256: + baseId = ID_ECDSA_SHA2_NISTP256; + break; + case ID_X509V3_ECDSA_SHA2_NISTP384: + baseId = ID_ECDSA_SHA2_NISTP384; + break; + case ID_X509V3_ECDSA_SHA2_NISTP521: + baseId = ID_ECDSA_SHA2_NISTP521; + break; + } + + return baseId; +} + + +/* Find the cert-store-backed private key slot whose key type matches the + * public key algorithm keyId being used, so that a config holding both an + * RSA and an ECC cert-store key selects the correct slot. Returns NULL + * when no cert-store slot matches. */ +static const WOLFSSH_PVT_KEY* FindCertStoreKey(const WOLFSSH_CTX* ctx, + byte keyId) +{ + const WOLFSSH_PVT_KEY* pvtKey; + byte baseId; + word32 i; + + baseId = CertStoreBaseKeyId(keyId); + for (i = 0; i < ctx->privateKeyCount; i++) { + pvtKey = &ctx->privateKey[i]; + if (IsCertStoreKey(pvtKey) && + CertStoreBaseKeyId(pvtKey->publicKeyFmt) == baseId) { + return pvtKey; + } + } + + return NULL; +} +#endif /* WOLFSSH_CERTS */ + + +#ifndef WOLFSSH_NO_ECDSA +/* Convert an ECDSA signature from NCryptSignHash, which is raw r||s with + * each component exactly half of sigSz (not DER), into separate minimal + * mpint components with leading zeros trimmed. On input rSz and sSz hold + * the capacities of r and s; on output they hold the trimmed sizes. */ +static int CertStoreEccSigToRs(const byte* sig, word32 sigSz, + byte* r, word32* rSz, byte* s, word32* sSz) +{ + word32 halfSz; + word32 rOff, sOff; + int ret; + + halfSz = 0; + rOff = 0; + sOff = 0; + ret = WS_SUCCESS; + + if (sigSz < 2 || (sigSz & 1) != 0) { + WLOG(WS_LOG_DEBUG, "CertStoreEccSigToRs: Invalid signature size"); + ret = WS_ECC_E; + } + if (ret == WS_SUCCESS) { + halfSz = sigSz / 2; + if (halfSz > *rSz || halfSz > *sSz) { + WLOG(WS_LOG_DEBUG, "CertStoreEccSigToRs: Signature too large"); + ret = WS_ECC_E; + } + } + if (ret == WS_SUCCESS) { + /* Trim leading zeros so r and s are minimal mpints. */ + while (rOff < halfSz - 1 && sig[rOff] == 0) + rOff++; + while (sOff < halfSz - 1 && sig[halfSz + sOff] == 0) + sOff++; + WMEMCPY(r, sig + rOff, halfSz - rOff); + *rSz = halfSz - rOff; + WMEMCPY(s, sig + halfSz + sOff, halfSz - sOff); + *sSz = halfSz - sOff; + } + + return ret; +} +#endif /* !WOLFSSH_NO_ECDSA */ + + +/* 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()"); + + /* hashId is no longer needed now that only the NCRYPT signing path + * (which derives the algorithm from the key/DigestInfo) is used. */ + (void)hashId; + + 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. Only CNG/NCRYPT keys + * are supported (targets are Windows 10 and newer); legacy CryptoAPI/CSP + * keys are rejected here. */ + 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 NCRYPT private key, error: %lu", dwErr); + return WS_CRYPTO_FAILED; + } + + /* Sign using CNG (Next Generation Crypto API). Only NCRYPT keys are + * acquired above, so dwKeySpec is always CERT_NCRYPT_KEY_SPEC here. */ + { + 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; + } + } + } + + /* Free the key handle if we acquired it */ + if (fCallerFreeProv) { + NCryptFreeObject(hCryptProv); + } + + WLOG(WS_LOG_DEBUG, "Leaving SignWithCertStoreKey(), ret = %d", ret); + return ret; +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + + static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, struct wolfSSH_sigKeyBlockFull *sigKey) #ifndef WOLFSSH_NO_RSA @@ -13870,23 +14292,41 @@ static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, ret = wolfTPM2_SignHashScheme(ssh->ctx->tpmDev, ssh->ctx->tpmKey, digest, (int)digestSz, sig, (int*)sigSz, TPM_ALG_RSASSA, TPM2_GetTpmHashType(hashId)); - if (ret == 0) - ret = (int)*sigSz; - else + if (ret == 0) { + ret = WS_SUCCESS; + } + else { + WLOG(WS_LOG_DEBUG, "SignHRsa: Bad TPM Sign"); ret = WS_RSA_E; + } } 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; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Check if this is a cert store key */ + if (IsCertStoreKey(sigKey->pvtKey)) { + /* 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 { - *sigSz = (word32)ret; - ret = WS_SUCCESS; + else + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ + { + /* 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; + } } } @@ -13895,8 +14335,23 @@ static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, && !ssh->handshake->useTpm #endif ) { - ret = wolfSSH_RsaVerify(sig, *sigSz, encSig, encSigSz, - &sigKey->sk.rsa.key, heap, "SignHRsa"); +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* 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 (IsCertStoreKey(sigKey->pvtKey)) { + /* 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_WINDOWS_CERT_STORE */ + { + ret = wolfSSH_RsaVerify(sig, *sigSz, encSig, encSigSz, + &sigKey->sk.rsa.key, heap, "SignHRsa"); + } } #ifdef WOLFSSH_SMALL_STACK @@ -13963,20 +14418,48 @@ static int SignHEcdsa(WOLFSSH* ssh, byte* sig, word32* sigSz, WLOG(WS_LOG_INFO, "Signing hash with %s.", IdToName(ssh->handshake->pubKeyId)); #ifdef WOLFSSH_TPM - if (useTpm) + if (useTpm) { ret = wolfTPM2_SignHashScheme(ssh->ctx->tpmDev, ssh->ctx->tpmKey, digest, (int)digestSz, rawSig, (int*)&rawSigSz, TPM_ALG_ECDSA, TPM2_GetTpmHashType(hashId)); + if (ret != 0) { + WLOG(WS_LOG_DEBUG, "SignHEcdsa: Bad TPM Sign"); + ret = WS_ECC_E; + } + else { + ret = WS_SUCCESS; + } + } else #endif /* WOLFSSH_TPM */ - ret = wc_ecc_sign_hash(digest, digestSz, sig, sigSz, ssh->rng, - &sigKey->sk.ecc.key); - if (ret != 0) { - WLOG(WS_LOG_DEBUG, "SignHEcdsa: Bad ECDSA Sign"); - ret = WS_ECC_E; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Check if this is a cert store key */ + if (IsCertStoreKey(sigKey->pvtKey)) { + /* 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 { - ret = WS_SUCCESS; + else + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ + { + /* 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; + } } } @@ -14017,10 +14500,18 @@ static int SignHEcdsa(WOLFSSH* ssh, byte* sig, word32* sigSz, } else #endif /* WOLFSSH_TPM */ - ret = wc_ecc_sig_to_rs(sig, *sigSz, r, &rSz, s, &sSz); - - if (ret != 0) { - ret = WS_ECC_E; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* NCryptSignHash for ECDSA returns raw r||s (each half of sigSz), + * NOT DER-encoded. Split directly. */ + if (IsCertStoreKey(sigKey->pvtKey)) { + ret = CertStoreEccSigToRs(sig, *sigSz, r, &rSz, s, &sSz); + } else +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + { + ret = wc_ecc_sig_to_rs(sig, *sigSz, r, &rSz, s, &sSz); + if (ret != 0) { + ret = WS_ECC_E; + } } } @@ -16155,6 +16646,32 @@ static int PrepareUserAuthRequestRsaCert(WOLFSSH* ssh, word32* payloadSz, authData->sf.publicKey.publicKeySz); else #endif /* WOLFSSH_AGENT */ +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Note: already inside #ifdef WOLFSSH_CERTS */ + if (authData->sf.publicKey.privateKey == NULL) { + /* Cert store: decode public key from the stored certificate */ + const WOLFSSH_PVT_KEY* pvtKey; + + pvtKey = FindCertStoreKey(ssh->ctx, keySig->keyId); + if (pvtKey == NULL || pvtKey->cert == NULL) { + ret = WS_BAD_ARGUMENT; + } + else { + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; + + ret = ExtractPubKeyDerFromCert(pvtKey->cert, pvtKey->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); + } + } else +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ ret = wc_RsaPrivateKeyDecode(authData->sf.publicKey.privateKey, &idx, &keySig->ks.rsa.key, authData->sf.publicKey.privateKeySz); @@ -16282,17 +16799,59 @@ 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 WOLFSSH_WINDOWS_CERT_STORE + if (authData->sf.publicKey.privateKey == NULL) { + /* Cert store: sign with NCryptSignHash via + * SignWithCertStoreKey (pszAlgId=NULL, data is + * the already-encoded DigestInfo). */ + const WOLFSSH_PVT_KEY* pvtKey; + + pvtKey = FindCertStoreKey(ssh->ctx, keySig->keyId); + if (pvtKey != NULL) { + word32 outSigSz = keySig->sigSz; + ret = SignWithCertStoreKey(ssh, pvtKey, + encDigest, encDigestSz, hashId, + output + begin, &outSigSz); + if (ret == WS_SUCCESS) { + sigSz = (int)outSigSz; + if (sigSz <= 0 || + (word32)sigSz != keySig->sigSz) { + WLOG(WS_LOG_DEBUG, + "SUAR: Cert store RSA sig length mismatch"); + ret = WS_RSA_E; + } + else { + 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 /* WOLFSSH_WINDOWS_CERT_STORE */ + { + 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"); + } } } @@ -16604,29 +17163,60 @@ static int PrepareUserAuthRequestEccCert(WOLFSSH* ssh, word32* payloadSz, if (ret == WS_SUCCESS) { word32 idx = 0; +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* 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) { + const WOLFSSH_PVT_KEY* pvtKey; + + pvtKey = FindCertStoreKey(ssh->ctx, keySig->keyId); + if (pvtKey == NULL || pvtKey->cert == NULL) { + ret = WS_BAD_ARGUMENT; + } + else { + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; + + ret = ExtractPubKeyDerFromCert(pvtKey->cert, pvtKey->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); + } + } else +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + { #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) { @@ -16725,24 +17315,60 @@ 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 WOLFSSH_WINDOWS_CERT_STORE + /* Cert store signing: NCryptSignHash returns raw r||s */ + if (ret == WS_SUCCESS && + authData->sf.publicKey.privateKey == NULL) { + const WOLFSSH_PVT_KEY* pvtKey; + + pvtKey = FindCertStoreKey(ssh->ctx, keySig->keyId); + 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. */ + rSz = sSz = (word32)sizeof(rs) / 2; + r = rs; + s = rs + rSz; + ret = CertStoreEccSigToRs(sig, sigSz, r, &rSz, s, &sSz); + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, + "SUAR: Bad cert store ECC signature"); + } + } 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 /* WOLFSSH_WINDOWS_CERT_STORE */ + { + 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 f73754e92..d0424e859 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -35,6 +35,17 @@ #include #include +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include + #include + #include + #include + #include + #ifndef CERT_NCRYPT_KEY_SPEC + #define CERT_NCRYPT_KEY_SPEC 0x00000003 + #endif +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + #ifdef NO_INLINE #include #else @@ -2676,6 +2687,334 @@ int wolfSSH_CTX_AddRootCert_buffer(WOLFSSH_CTX* ctx, return ret; } +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Find the certificate in hStore whose Common Name exactly matches + * subjectName. subjectName may include a leading "CN=" prefix. + * CERT_FIND_SUBJECT_STR_W is only used as a substring pre-filter to + * enumerate candidates; each candidate's CN is then compared exactly so + * that a lookup for "server1" does not select "server1.example" or + * "myserver1". Returns the certificate context (caller must free with + * CertFreeCertificateContext) or NULL when no exact match exists. */ +static PCCERT_CONTEXT FindCertByExactCN(HCERTSTORE hStore, + const wchar_t* subjectName) +{ + PCCERT_CONTEXT pCertContext; + const wchar_t* cn; + wchar_t certCn[256]; + DWORD certCnSz; + + /* Strip an optional "CN=" prefix from the requested name. */ + cn = subjectName; + if (wcslen(cn) > 3 && + (wcsncmp(cn, L"CN=", 3) == 0 || wcsncmp(cn, L"cn=", 3) == 0)) { + cn = cn + 3; + } + + pCertContext = NULL; + for (;;) { + /* Passing the previous context frees it and continues the search. */ + pCertContext = CertFindCertificateInStore(hStore, + X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + 0, CERT_FIND_SUBJECT_STR_W, cn, pCertContext); + if (pCertContext == NULL) { + break; + } + certCnSz = CertGetNameStringW(pCertContext, CERT_NAME_ATTR_TYPE, 0, + (void*)szOID_COMMON_NAME, certCn, + (DWORD)(sizeof(certCn) / sizeof(certCn[0]))); + if (certCnSz > 1 && wcscmp(certCn, cn) == 0) { + break; + } + } + + return pCertContext; +} + + +/* 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 Common Name for lookup, with or without + * a "CN=" prefix. The CN must match exactly; thumbprint lookup is not + * currently implemented. + * returns WS_SUCCESS on success + */ +int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, + const wchar_t* storeName, word32 dwFlags, + const wchar_t* subjectName) +{ + int ret = WS_SUCCESS; + HCERTSTORE hStore = NULL; + PCCERT_CONTEXT pCertContext = NULL; + word32 keyIdx = 0; + byte keyId = ID_NONE; + byte addedNewSlot = 0; + void* heap = NULL; + PCERT_PUBLIC_KEY_INFO pPubKeyInfo = 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, + (DWORD)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 exact Common Name match. */ + pCertContext = FindCertByExactCN(hStore, subjectName); + + if (pCertContext == NULL) { + 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 */ + 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++; + addedNewSlot = 1; + } + } + + 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; + } + + /* Free existing resources if replacing an existing slot. The slot may + * previously have held either a cert-store key or a file-based + * key/cert, so clear both kinds of resources. */ + if (ctx->privateKey[keyIdx].useCertStore) { + if (ctx->privateKey[keyIdx].certStoreContext != NULL) { + CertFreeCertificateContext( + (PCCERT_CONTEXT)ctx->privateKey[keyIdx].certStoreContext); + ctx->privateKey[keyIdx].certStoreContext = NULL; + } + if (ctx->privateKey[keyIdx].storeName != NULL) { + WFREE(ctx->privateKey[keyIdx].storeName, heap, DYNTYPE_STRING); + ctx->privateKey[keyIdx].storeName = NULL; + } + if (ctx->privateKey[keyIdx].subjectName != NULL) { + WFREE(ctx->privateKey[keyIdx].subjectName, heap, DYNTYPE_STRING); + ctx->privateKey[keyIdx].subjectName = NULL; + } + } + if (ctx->privateKey[keyIdx].key != NULL) { + ForceZero(ctx->privateKey[keyIdx].key, ctx->privateKey[keyIdx].keySz); + WFREE(ctx->privateKey[keyIdx].key, heap, DYNTYPE_PRIVKEY); + ctx->privateKey[keyIdx].key = NULL; + ctx->privateKey[keyIdx].keySz = 0; + } + if (ctx->privateKey[keyIdx].cert != NULL) { + WFREE(ctx->privateKey[keyIdx].cert, heap, DYNTYPE_CERT); + ctx->privateKey[keyIdx].cert = NULL; + ctx->privateKey[keyIdx].certSz = 0; + } + + /* 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); + /* Clear the slot so CtxResourceFree cannot double-free the + * context that we are about to release here. */ + ctx->privateKey[keyIdx].useCertStore = 0; + CertFreeCertificateContext(pCertContext); + ctx->privateKey[keyIdx].certStoreContext = NULL; + if (addedNewSlot) + ctx->privateKeyCount--; + 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 already stored data so CtxResourceFree cannot + * double-free the just-freed context/strings. */ + WFREE(ctx->privateKey[keyIdx].storeName, heap, DYNTYPE_STRING); + WFREE(ctx->privateKey[keyIdx].subjectName, heap, DYNTYPE_STRING); + ctx->privateKey[keyIdx].useCertStore = 0; + CertFreeCertificateContext(pCertContext); + ctx->privateKey[keyIdx].certStoreContext = NULL; + ctx->privateKey[keyIdx].storeName = NULL; + ctx->privateKey[keyIdx].subjectName = NULL; + if (addedNewSlot) + ctx->privateKeyCount--; + 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; + + /* Require a CNG/NCRYPT key. Legacy CryptoAPI/CSP keys are not + * supported; targets are Windows 10 and newer. */ + 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(ctx->privateKey[keyIdx].storeName, heap, DYNTYPE_STRING); + WFREE(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; + if (addedNewSlot) + 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 /* WOLFSSH_WINDOWS_CERT_STORE */ #endif /* WOLFSSH_CERTS */ diff --git a/wolfssh/certman.h b/wolfssh/certman.h index f80735550..fe68aeaf5 100644 --- a/wolfssh/certman.h +++ b/wolfssh/certman.h @@ -30,6 +30,8 @@ #include #include +#include /* included for WOLFSSH_CTX */ +#include /* included for WOLFSSL_CERT_MANAGER struct */ #ifdef __cplusplus extern "C" { @@ -40,6 +42,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 +60,14 @@ int wolfSSH_CERTMAN_VerifyCerts_buffer(WOLFSSH_CERTMAN* cm, const unsigned char* cert, word32 certSz, word32 certCount); +#ifdef WOLFSSH_WINDOWS_CERT_STORE +WOLFSSH_API +int wolfSSH_ParseCertStoreSpec(const char* spec, + wchar_t** wStoreName, wchar_t** wSubjectName, + word32* dwFlags, void* heap); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + + #ifdef __cplusplus } #endif diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 95c74cc93..6122ccb96 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -610,6 +610,21 @@ typedef struct WOLFSSH_PVT_KEY { /* When set, the host key material lives in the TPM and key/keySz are * unused; signing and the public K_S come from ctx->tpmKey. */ #endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE + 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. */ + wchar_t* storeName; + /* Certificate store name (e.g., "My", "Root"). Owned by CTX. */ + wchar_t* subjectName; + /* Certificate subject name for lookup. Owned by CTX. */ + word32 dwFlags; + /* Certificate store flags (e.g., CERT_SYSTEM_STORE_CURRENT_USER). + * Kept as word32 so this header does not depend on Windows + * typedefs; converted to DWORD at the CertOpenStore call. */ +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ } WOLFSSH_PVT_KEY; @@ -1090,6 +1105,7 @@ WOLFSSH_LOCAL WOLFSSH_CHANNEL* ChannelFind(WOLFSSH* ssh, word32 channel, WOLFSSH_LOCAL int ChannelRemove(WOLFSSH* ssh, word32 channel, byte peer); WOLFSSH_LOCAL int ChannelPutData(WOLFSSH_CHANNEL* channel, byte* data, word32 dataSz); +WOLFSSH_LOCAL void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx); WOLFSSH_LOCAL int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, const byte* in, word32 inSz, int format, int type); diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 6ce19aa9d..74087cff1 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -43,6 +43,11 @@ #include #endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* The Windows certificate store API below uses wchar_t strings. */ +#include +#endif + #ifdef __cplusplus extern "C" { #endif @@ -445,6 +450,11 @@ WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_buffer(WOLFSSH_CTX* 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 WOLFSSH_WINDOWS_CERT_STORE + WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, + const wchar_t* storeName, word32 dwFlags, + const wchar_t* subjectName); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif /* WOLFSSH_CERTS */ WOLFSSH_API int wolfSSH_CTX_SetWindowPacketSize(WOLFSSH_CTX* ctx, word32 windowSz, word32 maxPacketSz); 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) {