diff --git a/.github/workflows/windows-cert-store-test.yml b/.github/workflows/windows-cert-store-test.yml new file mode 100644 index 000000000..ea109a8bb --- /dev/null +++ b/.github/workflows/windows-cert-store-test.yml @@ -0,0 +1,1145 @@ +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: [ '*' ] + workflow_dispatch: + +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 + timeout-minutes: 30 + + 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: ${{ 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 the Windows cert store API (not in the repo user_settings.h). + # Inserted into wolfssh/ide/winvs/user_settings.h, which the VS + # projects put on the include path before wolfssl/IDE/WIN. The insert + # lands before the closing include-guard #endif so the defines stay + # inside the guard. + # RFC 6187 names only one RSA X.509 algorithm, x509v3-ssh-rsa, and it + # signs with SHA-1, so both SHA-1 gates have to come down for any RSA + # certificate to negotiate: + # WC_SIG_MIN_HASH_TYPE - wc_SignatureVerify otherwise rejects + # SHA-1 at its SHA-256 floor (see the + # same define in tpm-ssh.yml). + # WOLFSSH_NO_SHA1_SOFT_DISABLE - x509v3-ssh-rsa is otherwise absent + # from cannedKeyAlgoNames, so the server + # never lists it in server-sig-algs and + # the client's RSA certificate fails + # PrepareUserAuthRequestPublicKey() with + # WS_MATCH_KEY_ALGO_E. + # Both lists put their SHA-1 entries last, so the ECDSA entries in the + # matrix still negotiate the same SHA-2 algorithms as before. + sed -i '/#endif \/\* _WIN_USER_SETTINGS_H_ \*\//i\ + /* Inserted by windows-cert-store-test CI */\ + #define WOLFSSH_WINDOWS_CERT_STORE\ + #define WOLFSSH_NO_SHA1_SOFT_DISABLE\ + #define WC_SIG_MIN_HASH_TYPE WC_HASH_TYPE_SHA' ${{env.USER_SETTINGS_H_NEW}} + grep -q '^#define WOLFSSH_WINDOWS_CERT_STORE' ${{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}} + + # Fails the build if the defines never reach wolfsshd.c (same guard as the + # build-sys-ca-certs job). + - name: Guard that the defines reach wolfsshd.c + working-directory: ${{ github.workspace }}\wolfssh + shell: bash + run: | + printf '\n#if !defined(WOLFSSH_WINDOWS_CERT_STORE) || !defined(WOLFSSH_NO_SHA1_SOFT_DISABLE) || !defined(WOLFSSH_SSHD)\n#error "CI: expected defines did not reach wolfsshd.c"\n#endif\n' >> apps/wolfsshd/wolfsshd.c + + - 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}} + + # Run the unit and API tests here, where WOLFSSH_WINDOWS_CERT_STORE is + # defined; no other workflow defines it, so test_ParseCertStoreSpec and + # test_SetCertManager only ever execute in this job. Run from the wolfssh + # checkout root so ./keys/ paths resolve (as in windows-check.yml). The + # solution build writes to $(SolutionDir)$(Configuration)\$(Platform). + - name: Run api-test and unit-test + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + $dir = "ide\winvs\${{env.WOLFSSH_BUILD_CONFIGURATION}}\${{env.BUILD_PLATFORM}}" + $dll = Get-ChildItem -Path "${{ github.workspace }}\wolfssl" -Recurse -Filter "wolfssl.dll" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($dll) { Copy-Item $dll.FullName $dir -Force } + foreach ($t in @("api-test", "unit-test")) { + $exe = Join-Path $dir "$t.exe" + if (-not (Test-Path $exe)) { throw "$exe not found" } + & $exe + if ($LASTEXITCODE -ne 0) { throw "$t failed (exit $LASTEXITCODE)" } + } + + - 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/** + + # Compile-only check of the WOLFSSL_SYS_CA_CERTS paths in wolfsshd, which + # the functional matrix never defines and so never builds. + build-sys-ca-certs: + runs-on: windows-latest + timeout-minutes: 30 + + 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: user_settings.h with sshd, x509, cert store, and system CA certs + working-directory: ${{ github.workspace }} + shell: bash + run: | + sed -i 's/#if 0/#if 1/g' ${{env.USER_SETTINGS_H_NEW}} + # Insert before the closing include-guard #endif, not after it. + sed -i '/#endif \/\* _WIN_USER_SETTINGS_H_ \*\//i\ + /* Inserted by windows-cert-store-test CI */\ + #define WOLFSSH_WINDOWS_CERT_STORE\ + #define WOLFSSL_SYS_CA_CERTS' ${{env.USER_SETTINGS_H_NEW}} + grep -q '^#define WOLFSSH_WINDOWS_CERT_STORE' ${{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: Restore NuGet packages + working-directory: ${{ github.workspace }}\wolfssh\ide\winvs + run: nuget restore ${{env.SOLUTION_FILE_PATH}} + + # Fails the build if the defines never reach wolfsshd.c, which otherwise + # compiles its #else branch and silently degrades to a duplicate of build. + - name: Guard that the defines reach wolfsshd.c + working-directory: ${{ github.workspace }}\wolfssh + shell: bash + run: | + printf '\n#if !defined(WOLFSSL_SYS_CA_CERTS) || !defined(WOLFSSH_WINDOWS_CERT_STORE) || !defined(WOLFSSH_SSHD)\n#error "CI: expected defines did not reach wolfsshd.c"\n#endif\n' >> apps/wolfsshd/wolfsshd.c + + - name: Build wolfssh (compile check) + 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}} + + # Autotools coverage for --enable-windows-cert-store: the mingw link + # libraries and both error paths. Configure only, so no cross-built wolfSSL + # is needed; the wolfssl link test is satisfied from the autoconf cache. + configure-windows-cert-store: + runs-on: ubuntu-latest + timeout-minutes: 10 + + env: + WOLFSSL_CACHE: ac_cv_lib_wolfssl_wolfCrypt_Init=yes + + steps: + - uses: actions/checkout@v4 + + - name: Install mingw toolchain and autotools + run: | + sudo apt-get update + sudo apt-get install -y gcc-mingw-w64-x86-64 autoconf automake libtool + + - name: Generate configure + run: ./autogen.sh + + - name: mingw host links crypt32 and ncrypt + run: | + ./configure --host=x86_64-w64-mingw32 --enable-certs \ + --enable-windows-cert-store $WOLFSSL_CACHE + grep -q -- '-lcrypt32' Makefile + grep -q -- '-lncrypt' Makefile + + - name: Rejects a non-Windows host and a missing --enable-certs + # Each assertion exits explicitly: bash errexit exempts a command + # inverted with '!', and the step status comes from the last command. + # The output grep pins each failure to the intended configure.ac error, + # so an unrelated earlier configure failure cannot keep the check green. + run: | + if ./configure --enable-certs --enable-windows-cert-store \ + $WOLFSSL_CACHE > conf-host.log 2>&1; then + echo 'ERROR: configure should have failed on a non-Windows host' + exit 1 + fi + if ! grep -q 'only supported on _WIN32 Windows hosts' conf-host.log; then + cat conf-host.log + echo 'ERROR: configure failed, but not with the non-Windows host error' + exit 1 + fi + if ./configure --host=x86_64-w64-mingw32 \ + --enable-windows-cert-store $WOLFSSL_CACHE > conf-nocerts.log 2>&1; then + echo 'ERROR: configure should have failed without --enable-certs' + exit 1 + fi + if ! grep -q 'requires X.509 cert support' conf-nocerts.log; then + cat conf-nocerts.log + echo 'ERROR: configure failed, but not with the missing-certs error' + exit 1 + fi + + test: + needs: build + runs-on: windows-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + # user_ca_source: store replaces the file-based TrustedUserCAKeys + # with wolfSSH_TrustedUserCAStore, so the store is the only trust + # anchor for the client certificate. + - server_key_source: file + client_key_source: x509 + key_algorithm: rsa + user_ca_source: store + test_name: "Server-File-Client-X509-UserCAStore" + - server_key_source: store + client_key_source: x509 + key_algorithm: rsa + test_name: "Server-Store-Client-X509" + # key_algorithm is the server host key; client_key_algorithm is the + # testuser client certificate key. Both are stated explicitly on the + # store-client entries so neither depends on which key renewcerts.sh + # happens to copy. + - server_key_source: file + client_key_source: store + key_algorithm: rsa + client_key_algorithm: ecdsa + test_name: "Server-File-Client-Store" + - server_key_source: store + client_key_source: store + key_algorithm: rsa + client_key_algorithm: ecdsa + test_name: "Server-Store-Client-Store" + - server_key_source: store + client_key_source: x509 + key_algorithm: ecdsa + test_name: "Server-Store-Client-X509-ECDSA" + # RSA client certificate, covering the x509v3-ssh-rsa user-auth and + # client-side RSA cert store signing paths that the ECDSA entries + # above cannot reach. + - server_key_source: file + client_key_source: store + key_algorithm: rsa + client_key_algorithm: rsa + test_name: "Server-File-Client-Store-RSA" + + 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 + + # renewcerts.sh copies fred's key, which is EC prime256v1, so testuser + # comes out ECDSA. Re-issue it explicitly for whichever algorithm the + # matrix entry asks for, rather than inheriting whatever fred's key + # happens to be. + ALG="${{ matrix.client_key_algorithm }}" + if [ -n "$ALG" ]; then + touch index.txt + sed 's/fred/testuser/g' renewcerts.cnf > renewcerts-testuser.cnf + if [ "$ALG" = "rsa" ]; then + openssl genrsa -out testuser-key.pem 2048 + else + openssl ecparam -name prime256v1 -genkey -noout \ + -out testuser-key.pem + fi + openssl req -subj "/C=US/ST=WA/L=Seattle/O=wolfSSL Inc/OU=Development/CN=testuser/emailAddress=testuser@example.com" \ + -key testuser-key.pem -out testuser-cert.csr \ + -config renewcerts-testuser.cnf -new -nodes + openssl x509 -req -in testuser-cert.csr -days 3650 \ + -extfile renewcerts-testuser.cnf -extensions v3_testuser \ + -CA ca-cert-ecc.pem -CAkey ca-key-ecc.pem -out testuser-cert.pem \ + -set_serial 7 + openssl x509 -in testuser-cert.pem -outform DER -out testuser-cert.der + if [ "$ALG" = "rsa" ]; then + openssl rsa -in testuser-key.pem -outform DER -out testuser-key.der + else + openssl ec -in testuser-key.pem -outform DER -out testuser-key.der + fi + rm -f renewcerts-testuser.cnf testuser-cert.csr index.* + fi + 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 + + # Assert the key really is the algorithm this entry asked for, so a + # change to renewcerts.sh cannot silently turn an entry into a + # duplicate of another one. Unset means whatever renewcerts.sh gives, + # which is fred's EC key. + EXPECT="${{ matrix.client_key_algorithm }}" + [ -n "$EXPECT" ] || EXPECT=ecdsa + if openssl rsa -inform DER -in keys/testuser-key.der -noout 2>/dev/null + then + ACTUAL=rsa + elif openssl ec -inform DER -in keys/testuser-key.der -noout 2>/dev/null + then + ACTUAL=ecdsa + else + echo "ERROR: testuser-key.der is neither RSA nor EC" + exit 1 + fi + if [ "$ACTUAL" != "$EXPECT" ]; then + echo "ERROR: testuser client key is $ACTUAL, expected $EXPECT" + exit 1 + fi + echo "testuser client key algorithm: $ACTUAL" + + 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" + + # Export the (self-signed) server cert as DER so the client can use + # it as the trust anchor when negotiating an x509v3-* host key. + Export-Certificate -Cert $serverCert -FilePath "server-store-cert.der" | Out-Null + } + + # 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: Import test CA into a Windows store + if: matrix.user_ca_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + run: | + # LocalMachine so the wolfsshd service (LocalSystem) can read it. + # certutil creates the store if it does not already exist. + $caDer = (Resolve-Path "keys\ca-cert-ecc.der").Path + certutil -addstore -f wolfSSHTestCA $caDer + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: certutil failed to add the CA to wolfSSHTestCA" + exit 1 + } + $caInStore = Get-ChildItem -Path "Cert:\LocalMachine\wolfSSHTestCA" -ErrorAction SilentlyContinue + if (-not $caInStore) { + Write-Host "ERROR: no certificate present in LocalMachine\wolfSSHTestCA" + exit 1 + } + Write-Host "CA imported: $($caInStore[0].Subject)" + + # An existing but empty store for the negative startup test. Adding + # then removing the CA leaves the store itself in place, so the + # failure is "no usable CA" and not "store not found". + certutil -addstore -f wolfSSHEmptyCA $caDer + if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: certutil -addstore wolfSSHEmptyCA"; exit 1 } + Get-ChildItem -Path "Cert:\LocalMachine\wolfSSHEmptyCA" | Remove-Item -Force + if (Get-ChildItem -Path "Cert:\LocalMachine\wolfSSHEmptyCA" -ErrorAction SilentlyContinue) { + Write-Host "ERROR: wolfSSHEmptyCA is not empty" + exit 1 + } + + - 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 + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: icacls failed on $authKeysFile" + exit 1 + } + + # wolfsshd serves SFTP from the home directory while impersonating + # testuser; the SFTP tests assert this name appears in the listing. + "marker" | Out-File -FilePath "$homeDir\wolfssh_sftp_marker.txt" -Encoding ASCII + icacls $homeDir /grant "testuser:(OI)(CI)RX" /T /q + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: icacls failed on $homeDir" + exit 1 + } + + # 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. Either from a + # PEM file (as per apps/wolfsshd/test/create_sshd_config.sh) or from + # the Windows store the CA was imported into, never both, so a + # successful client auth pins down which one supplied the anchor. + if ("${{ matrix.user_ca_source }}" -eq "store") { + $configContent += @" + + wolfSSH_TrustedUserCAStore yes + wolfSSH_WinUserStores CERT_STORE_PROV_SYSTEM + wolfSSH_WinUserPvPara wolfSSHTestCA + wolfSSH_WinUserDwFlags LOCAL_MACHINE + "@ + } else { + $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. HostKey and + # HostCertificate alongside HostKeyStore are rejected at startup. + $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: | + # This job has no wolfssl checkout; the artifact unpacks at the + # workspace root, so search there rather than under wolfssl\. + $sshdDir = Split-Path -Parent $env:SSHD_PATH + $searchRoot = "${{ github.workspace }}" + + $wolfsslDll = Get-ChildItem -Path $searchRoot -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 $($wolfsslDll.FullName) to $sshdDir" + exit 0 + } + + $wolfsslLib = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfssl.lib" -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($wolfsslLib) { + Write-Host "Static build ($($wolfsslLib.FullName)); wolfssl.dll not required" + } else { + Write-Host "WARNING: neither wolfssl.dll nor wolfssl.lib found under $searchRoot" + } + + - 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 + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: icacls failed on $sshdDir" + exit 1 + } + + - 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") + + # echoserver serves SFTP from its working directory; the SFTP tests + # assert this name appears in the remote listing. + "marker" | Out-File -FilePath (Join-Path $exeDir "wolfssh_sftp_marker.txt") -Encoding ASCII + + $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 + $ready = $false + while ($elapsed -lt $timeout -and -not $ready) { + Start-Sleep -Seconds 1 + $elapsed++ + try { + $conn = New-Object System.Net.Sockets.TcpClient("127.0.0.1", $port) + if ($conn.Connected) { $conn.Close(); $ready = $true; continue } + } 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 (-not $ready) { + 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 + timeout-minutes: 3 + run: | + # The plain host key algorithm wins negotiation here, so this covers + # the plain key slot and user auth; the x509v3 slot is covered by the + # next step. + $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 + } + if ((Get-Content sftp_echo_output.txt -Raw) -notmatch "wolfssh_sftp_marker.txt") { + Write-Host "ERROR: remote listing did not contain the marker file" + exit 1 + } + Write-Host "SFTP against echoserver succeeded" + + - name: Test SFTP against echoserver with x509v3 host key + if: matrix.server_key_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + timeout-minutes: 3 + run: | + # Force the x509v3 host key algorithm so the cert store certificate + # itself is sent as K_S and verified by the client, exercising the + # X.509 host-key slot instead of the plain-key slot. The server cert + # is self-signed, so it is its own trust anchor (-A), which also means + # a fallback to a file-based host key could not pass this step. + $testPort = ${{env.TEST_PORT}} + $sftpPath = $env:SFTP_PATH + + @" + pwd + ls + quit + "@ | Out-File -FilePath sftp_x509_commands.txt -Encoding ASCII + + $sftpArgs = @("-u", "testuser", "-h", "localhost", "-p", "$testPort") + 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 + } + $sftpArgs += "-A", (Resolve-Path "server-store-cert.der").Path, "-X" + if ("${{ matrix.key_algorithm }}" -eq "ecdsa") { + $sftpArgs += "-k", "x509v3-ecdsa-sha2-nistp256" + } else { + $sftpArgs += "-k", "x509v3-ssh-rsa" + } + + Write-Host "Running: $sftpPath $($sftpArgs -join ' ')" + $process = Start-Process -FilePath $sftpPath ` + -ArgumentList $sftpArgs ` + -RedirectStandardInput "sftp_x509_commands.txt" ` + -RedirectStandardOutput "sftp_x509_output.txt" ` + -RedirectStandardError "sftp_x509_error.txt" ` + -Wait -NoNewWindow -PassThru + + Write-Host "SFTP (x509v3 host key) exit code: $($process.ExitCode)" + Write-Host "=== SFTP Output ===" + if (Test-Path sftp_x509_output.txt) { Get-Content sftp_x509_output.txt } + Write-Host "=== SFTP Error ===" + if (Test-Path sftp_x509_error.txt) { Get-Content sftp_x509_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 with x509v3 host key failed" + exit 1 + } + if ((Get-Content sftp_x509_output.txt -Raw) -notmatch "wolfssh_sftp_marker.txt") { + Write-Host "ERROR: remote listing did not contain the marker file" + exit 1 + } + Write-Host "SFTP with x509v3 host key 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 + } + # Also kill by name in case PID tracking missed it + Get-Process -Name "echoserver" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + # wolfSSH skips SO_REUSEADDR on Windows (wolfssh/test.h), so wolfsshd + # hard-fails if it binds before the port is released. Poll until the + # listener is gone rather than sleeping a fixed interval. + $port = ${{env.TEST_PORT}} + $timeout = 30 + $elapsed = 0 + while (Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue) { + if ($elapsed -ge $timeout) { + Write-Host "ERROR: port $port still listening ${timeout}s after stopping echoserver" + exit 1 + } + Start-Sleep -Seconds 1 + $elapsed++ + } + Write-Host "Port $port released" + # Clear the env var so cleanup step doesn't try again + Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PID=" + + - name: wolfSSHd refuses to start with an empty user CA store + if: matrix.user_ca_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + timeout-minutes: 3 + run: | + # Start wolfsshd for real (no -t test mode) so refusal is observable + # as the process exiting without a listener, not just as a log line. + # Windows main() always returns 0, so the exit code is not asserted. + (Get-Content sshd_config_test) -replace 'wolfSSHTestCA', 'wolfSSHEmptyCA' | + Out-File -FilePath sshd_config_empty_ca -Encoding ASCII + $configPathFull = (Resolve-Path "sshd_config_empty_ca").Path + $port = ${{env.TEST_PORT}} + + $proc = Start-Process -FilePath (Resolve-Path $env:SSHD_PATH).Path ` + -ArgumentList @("-D", "-d", "-f", $configPathFull, "-p", $port) ` + -RedirectStandardOutput "sshd_empty_ca_out.txt" ` + -RedirectStandardError "sshd_empty_ca_err.txt" ` + -NoNewWindow -PassThru + Start-Sleep -Seconds 5 + + $log = "" + foreach ($f in @("sshd_empty_ca_out.txt", "sshd_empty_ca_err.txt")) { + if (Test-Path $f) { $log += (Get-Content $f -Raw) } + } + Write-Host "=== wolfsshd output ===" + Write-Host $log + + $failed = $false + # Windows may prune the registry key once the last cert is removed, in + # which case the store fails to open instead of enumerating empty. + # Either way startup must not succeed. + if ($log -notmatch "No usable CA certificates found in store" -and + $log -notmatch "Unable to open user CA cert store") { + Write-Host "ERROR: wolfsshd did not reject the empty user CA store" + $failed = $true + } + if (-not $proc.HasExited) { + Write-Host "ERROR: wolfsshd is still running with an empty user CA store" + $failed = $true + } + if (Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue) { + Write-Host "ERROR: wolfsshd is listening on port $port with an empty user CA store" + $failed = $true + } + if (-not $proc.HasExited) { + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue + } + if ($failed) { exit 1 } + Write-Host "wolfsshd refused to start with the empty user CA store" + + - 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. + # Single-string binPath with embedded quotes: how pwsh renders it to + # sc.exe depends on $PSNativeCommandArgumentPassing. This works here + # only because CI workspace paths contain no spaces. + $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 + timeout-minutes: 3 + 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 + } + # ls discards errors and doCmds always returns success, so assert on + # the listing itself rather than on the exit code alone. + if ((Get-Content sftp_output.txt -Raw) -notmatch "wolfssh_sftp_marker.txt") { + Write-Host "ERROR: remote listing did not contain the marker file" + 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 + foreach ($s in @("wolfSSHTestCA", "wolfSSHEmptyCA")) { + Get-ChildItem -Path "Cert:\LocalMachine\$s" -ErrorAction SilentlyContinue | + Remove-Item -Force -ErrorAction SilentlyContinue + } + Write-Host "Cleaned up test certificates" diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 9c5909184..db44508d6 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -54,7 +54,9 @@ #include #include -#ifdef WOLFSSL_FPKI +#if defined(WOLFSSH_CERTS) && (defined(WOLFSSL_FPKI) || defined(_WIN32)) +/* Used to bind a client certificate to the requested user name: by UPN + * with FPKI, by subject CN on Windows builds without FPKI. */ #include #endif @@ -641,6 +643,75 @@ static int IsAbsoluteAuthKeysPath(const char* path) return ret; } +#if defined(WOLFSSH_CERTS) && (defined(WOLFSSL_FPKI) || defined(_WIN32)) +/* True when the AuthorizedKeysFile pattern is guaranteed to resolve to a + * different file for every account, which is what makes an entry in it an + * implicit user-to-credential binding. A relative pattern resolves under the + * account's home directory, and an absolute one qualifies only when it carries + * a %u or %h token. An absolute pattern with neither (e.g. + * "/etc/ssh/authorized_keys_all") is one shared file for every account and + * binds a credential to nothing. */ +static int IsPerUserAuthKeysPattern(const char* pattern) +{ + word32 i; + word32 patSz; + word32 seg; + + if (pattern == NULL || *pattern == '\0') { + /* the built-in ~/.ssh/authorized_keys default */ + return 1; + } + + /* a ".." component can escape the home directory and collapse to one + * shared file for every account, so it is never per-user */ + patSz = (word32)WSTRLEN(pattern); + seg = 0; + for (i = 0; i <= patSz; i++) { + if (i == patSz || pattern[i] == '/' || pattern[i] == '\\') { + if (i - seg == 2 && pattern[seg] == '.' && + pattern[seg + 1] == '.') { + return 0; + } + seg = i + 1; + } + } + + if (!IsAbsoluteAuthKeysPath(pattern)) { + return 1; + } + + for (i = 0; (i + 1) < patSz; i++) { + if (pattern[i] != '%') { + continue; + } + if (pattern[i + 1] == 'u' || pattern[i + 1] == 'h') { + return 1; + } + /* "%%" is a literal percent, step over both characters */ + if (pattern[i + 1] == '%') { + i++; + } + } + + return 0; +} +#endif /* WOLFSSH_CERTS && (WOLFSSL_FPKI || _WIN32) */ + +/* Exported predicate matching the runtime identity-check skip; on builds + * without that check it always returns 0. */ +int wolfSSHD_AuthKeysPatternIsPerUser(const char* pattern) +{ +#if defined(WOLFSSH_CERTS) && (defined(WOLFSSL_FPKI) || defined(_WIN32)) + if (pattern == NULL) { + return 0; + } + return IsPerUserAuthKeysPattern(pattern); +#else + (void)pattern; + return 0; +#endif +} + /* Resolve the authorized keys file path for a user. The pattern is passed in * explicitly so concurrent authentications cannot race on it, and its tokens * are expanded so each user resolves to a distinct path. */ @@ -1881,7 +1952,8 @@ static int CAKeysFileDiffers(const char* a, const char* b) /* Returns 1 when the certificate UPN @ in name[0..nameSz) * authorizes login as 'usr'. allowList is a whitespace/comma list of permitted * realms; NULL/empty matches the local part only, else domain must be listed. */ -#if defined(WOLFSSL_FPKI) || defined(WOLFSSHD_UNIT_TEST) +#if (defined(WOLFSSL_FPKI) && defined(WOLFSSH_CERTS)) || \ + defined(WOLFSSHD_UNIT_TEST) WOLFSSHD_STATIC int MatchUPNToUser(const char* usr, const char* name, int nameSz, const char* allowList) { @@ -1939,7 +2011,7 @@ WOLFSSHD_STATIC int MatchUPNToUser(const char* usr, const char* name, return ret; } -#endif /* WOLFSSL_FPKI || WOLFSSHD_UNIT_TEST */ +#endif /* (WOLFSSL_FPKI && WOLFSSH_CERTS) || WOLFSSHD_UNIT_TEST */ /* @@ -2083,11 +2155,22 @@ static int RequestAuthentication(WS_UserAuthData* authData, ret = WOLFSSH_USERAUTH_REJECTED; } - #ifdef WOLFSSL_FPKI + #if defined(WOLFSSH_CERTS) && (defined(WOLFSSL_FPKI) || defined(_WIN32)) if (ret == WOLFSSH_USERAUTH_SUCCESS && authData->type == WOLFSSH_USERAUTH_PUBLICKEY) { - /* compare user name to UPN in certificate */ - if (authData->sf.publicKey.isCert) { + /* Bind the certificate to the requested user name via UPN with FPKI or + * CN without FPKI. Skipped only when a per-user AuthorizedKeysFile is + * configured, because such an entry is itself an explicit user to cert + * binding and is checked below. A shared AuthorizedKeysFile (an + * absolute pattern with no %u or %h) resolves to one file for every + * account and binds the certificate to nothing, so the identity check + * still has to run. Never skipped when AuthorizedUPNDomains is set, so + * the configured realm allowlist is always enforced. */ + if (authData->sf.publicKey.isCert && + !(wolfSSHD_ConfigGetAuthKeysFileSet(usrConf) && + wolfSSHD_ConfigGetAuthorizedUPNDomains(usrConf) == NULL && + IsPerUserAuthKeysPattern( + wolfSSHD_ConfigGetAuthKeysFile(usrConf)))) { DecodedCert* dCert; #ifdef WOLFSSH_SMALL_STACK dCert = (DecodedCert*)WMALLOC(sizeof(DecodedCert), NULL, @@ -2111,6 +2194,7 @@ static int RequestAuthentication(WS_UserAuthData* authData, } else { int usrMatch = 0; + #ifdef WOLFSSL_FPKI int upnRealmUnchecked = 0; DNS_entry* current = dCert->altNames; const char* upnDomains = @@ -2133,16 +2217,38 @@ static int RequestAuthentication(WS_UserAuthData* authData, current = current->next; } - /* a UPN matched but no realm policy is set; warn per auth + /* a UPN matched but no realm policy is set; note per auth * attempt so the opt-in gap is visible, no shared state */ if (upnRealmUnchecked) { - wolfSSH_Log(WS_LOG_WARN, "[SSHD] AuthorizedUPNDomains " + wolfSSH_Log(WS_LOG_INFO, "[SSHD] AuthorizedUPNDomains " "not set; certificate UPN domain is not checked"); } + #else + /* Without FPKI compare subject CN with user name. Only + * reachable on Windows, where account names are + * case-insensitive, so match the CN the same way. + * + * This is a name match only. There is no analogue of + * AuthorizedUPNDomains here, so any CA in the trust store + * may assert any CN; the trusted user CA set is the whole + * of the issuer policy. */ + if (dCert->subjectCN != NULL && dCert->subjectCNLen > 0 && + (int)XSTRLEN(usr) == dCert->subjectCNLen && + WSTRNCASECMP(usr, dCert->subjectCN, + (size_t)dCert->subjectCNLen) == 0) { + usrMatch = 1; + /* note per auth attempt so the weaker binding is + * visible, no shared state */ + wolfSSH_Log(WS_LOG_INFO, "[SSHD] certificate bound to " + "user by subject CN only; no issuer constraint is " + "applied, keep the trusted user CA set narrow"); + } + #endif if (usrMatch == 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] incorrect user cert " - "sent"); + "sent; certificate identity does not match the " + "requested user (user=%s)", usr); ret = WOLFSSH_USERAUTH_INVALID_PUBLICKEY; } } @@ -2175,8 +2281,11 @@ static int RequestAuthentication(WS_UserAuthData* authData, ret = WOLFSSH_USERAUTH_REJECTED; } else { - #ifdef _WIN32 - /* Still need to get users token on Windows */ + #if defined(WOLFSSH_CERTS) && defined(_WIN32) + /* Bound to the requested user above by certificate UPN with + * FPKI, or by subject CN otherwise; which of the two is in + * force is fixed by the wolfSSL build, not by configuration. + * Still need to get the users token on Windows. */ wolfSSH_Log(WS_LOG_INFO, "[SSHD] Relying on CA for public key check"); rc = SetupUserTokenWin(usr, &authData->sf.publicKey, @@ -2190,7 +2299,7 @@ static int RequestAuthentication(WS_UserAuthData* authData, "[SSHD] Error getting users token."); ret = WOLFSSH_USERAUTH_FAILURE; } - #elif defined(WOLFSSL_FPKI) + #elif defined(WOLFSSH_CERTS) && defined(WOLFSSL_FPKI) /* The UPN-vs-username check above already bound the certificate * to the requested user, so the CA-verified chain is * sufficient. */ @@ -2198,10 +2307,11 @@ static int RequestAuthentication(WS_UserAuthData* authData, "[SSHD] Relying on CA for public key check"); ret = WOLFSSH_USERAUTH_SUCCESS; #else - /* Without FPKI the certificate UPN/principal cannot be read, so - * the requested user cannot be bound to the certificate. Fail - * closed: require AuthorizedKeysFile (per-user key/cert mapping) - * or a wolfSSL build with FPKI. */ + /* No binding ran above: either the certificate UPN/principal + * cannot be read without FPKI, or this build has no + * certificate support at all. Fail closed: require + * AuthorizedKeysFile (per-user key/cert mapping) or a wolfSSL + * build with FPKI. */ wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Certificate authentication cannot bind the requested " "user without FPKI or AuthorizedKeysFile; rejecting " diff --git a/apps/wolfsshd/auth.h b/apps/wolfsshd/auth.h index 6d67bfb61..64dfc7c19 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -78,6 +78,7 @@ int wolfSSHD_AuthReducePermissionsUser(WOLFSSHD_AUTH* auth, WUID_T uid, int wolfSSHD_AuthSetGroups(const WOLFSSHD_AUTH* auth, const char* usr, WGID_T gid); long wolfSSHD_AuthGetGraceTime(const WOLFSSHD_AUTH* auth); +int wolfSSHD_AuthKeysPatternIsPerUser(const char* pattern); #ifdef WOLFSSH_OSSH_CERTS void wolfSSHD_AuthSetPeerIp(WOLFSSHD_AUTH* auth, const char* ip); const char* wolfSSHD_AuthGetForcedCmd(const WOLFSSHD_AUTH* auth); diff --git a/apps/wolfsshd/configuration.c b/apps/wolfsshd/configuration.c index 61ce9b1c1..9ded08551 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -87,6 +87,11 @@ 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; @@ -94,6 +99,11 @@ struct WOLFSSHD_CONFIG { char* forceCmd; char* pidFile; char* authorizedUPNDomains; /* allowlist of UPN realms for cert auth */ +#ifdef WOLFSSH_WINDOWS_CERT_STORE + char* winUserStores; + char* winUserDwFlags; + char* winUserPvPara; +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ WOLFSSHD_CONFIG* next; /* next config in list */ long loginTimer; word16 port; @@ -104,6 +114,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 @@ -338,6 +350,46 @@ static WOLFSSHD_CONFIG* wolfSSHD_ConfigCopy(WOLFSSHD_CONFIG* conf) newConf->heap); } +#ifdef WOLFSSH_WINDOWS_CERT_STORE + if (ret == WS_SUCCESS && conf->hostKeyStore) { + ret = CreateString(&newConf->hostKeyStore, conf->hostKeyStore, + (int)WSTRLEN(conf->hostKeyStore), + newConf->heap); + } + + if (ret == WS_SUCCESS && conf->hostKeyStoreSubject) { + ret = CreateString(&newConf->hostKeyStoreSubject, + conf->hostKeyStoreSubject, + (int)WSTRLEN(conf->hostKeyStoreSubject), + newConf->heap); + } + + if (ret == WS_SUCCESS && conf->hostKeyStoreFlags) { + ret = CreateString(&newConf->hostKeyStoreFlags, + conf->hostKeyStoreFlags, + (int)WSTRLEN(conf->hostKeyStoreFlags), + newConf->heap); + } + + if (ret == WS_SUCCESS && conf->winUserStores) { + ret = CreateString(&newConf->winUserStores, conf->winUserStores, + (int)WSTRLEN(conf->winUserStores), + newConf->heap); + } + + if (ret == WS_SUCCESS && conf->winUserDwFlags) { + ret = CreateString(&newConf->winUserDwFlags, conf->winUserDwFlags, + (int)WSTRLEN(conf->winUserDwFlags), + newConf->heap); + } + + if (ret == WS_SUCCESS && conf->winUserPvPara) { + ret = CreateString(&newConf->winUserPvPara, conf->winUserPvPara, + (int)WSTRLEN(conf->winUserPvPara), + newConf->heap); + } +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + if (ret == WS_SUCCESS) { newConf->loginTimer = conf->loginTimer; newConf->port = conf->port; @@ -348,6 +400,8 @@ static WOLFSSHD_CONFIG* wolfSSHD_ConfigCopy(WOLFSSHD_CONFIG* conf) newConf->permitEmptyPasswords = conf->permitEmptyPasswords; newConf->authKeysFileSet = conf->authKeysFileSet; newConf->strictModes = conf->strictModes; + newConf->useSystemCA = conf->useSystemCA; + newConf->useUserCAStore = conf->useUserCAStore; } else { wolfSSHD_ConfigFree(newConf); @@ -384,6 +438,14 @@ void wolfSSHD_ConfigFree(WOLFSSHD_CONFIG* conf) FreeString(¤t->authorizedUPNDomains, 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); + FreeString(¤t->winUserStores, heap); + FreeString(¤t->winUserDwFlags, heap); + FreeString(¤t->winUserPvPara, heap); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ WFREE(current, heap, DYNTYPE_SSHD); current = next; @@ -410,6 +472,9 @@ enum { OPT_PROTOCOL = 9, OPT_LOGIN_GRACE_TIME = 10, OPT_HOST_KEY = 11, + OPT_HOST_KEY_STORE = 50, + OPT_HOST_KEY_STORE_SUBJECT = 51, + OPT_HOST_KEY_STORE_FLAGS = 52, OPT_PASSWORD_AUTH = 12, OPT_PORT = 13, OPT_PERMIT_ROOT = 14, @@ -424,13 +489,14 @@ enum { OPT_BANNER = 23, OPT_PUBKEY_AUTH = 24, OPT_STRICT_MODES = 25, - OPT_AUTHORIZED_UPN_DOMAINS = 26, + OPT_TRUSTED_SYSTEM_CA_KEYS = 26, + OPT_TRUSTED_USER_CA_STORE = 27, + OPT_WIN_USER_STORES = 28, + OPT_WIN_USER_DW_FLAGS = 29, + OPT_WIN_USER_PV_PARA = 30, + OPT_AUTHORIZED_UPN_DOMAINS = 31 }; -enum { - NUM_OPTIONS = 27 -}; - -static const CONFIG_OPTION options[NUM_OPTIONS] = { +static const CONFIG_OPTION options[] = { {OPT_AUTH_KEYS_FILE, "AuthorizedKeysFile"}, {OPT_PRIV_SEP, "UsePrivilegeSeparation"}, {OPT_PERMIT_EMPTY_PW, "PermitEmptyPasswords"}, @@ -442,6 +508,14 @@ 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. Kept unconditional so + * "HostKeyStore" never prefix-matches "HostKey" on non-store builds. */ + {OPT_HOST_KEY_STORE_SUBJECT, "HostKeyStoreSubject"}, + {OPT_HOST_KEY_STORE_FLAGS, "HostKeyStoreFlags"}, + {OPT_HOST_KEY_STORE, "HostKeyStore"}, {OPT_HOST_KEY, "HostKey"}, {OPT_PASSWORD_AUTH, "PasswordAuthentication"}, {OPT_PUBKEY_AUTH, "PubkeyAuthentication"}, @@ -457,8 +531,45 @@ 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"}, + {OPT_WIN_USER_STORES, "wolfSSH_WinUserStores"}, + {OPT_WIN_USER_DW_FLAGS, "wolfSSH_WinUserDwFlags"}, + {OPT_WIN_USER_PV_PARA, "wolfSSH_WinUserPvPara"}, {OPT_AUTHORIZED_UPN_DOMAINS, "AuthorizedUPNDomains"}, }; +#define NUM_OPTIONS ((int)(sizeof(options) / sizeof(*options))) + +#ifdef WOLFSSHD_UNIT_TEST +/* Test hook for the option-table ordering invariant: the parser matches with + * WSTRNCMP over the table in order, so an earlier name that is a strict + * prefix of a later one would shadow it. Returns 1 and sets earlier/later on + * a violation, 0 when the table is well ordered. */ +int wolfSSHD_ConfigOptionPrefixShadow(const char** earlier, const char** later) +{ + int i; + int j; + int len; + + for (i = 0; i < NUM_OPTIONS; i++) { + len = (int)WSTRLEN(options[i].name); + for (j = i + 1; j < NUM_OPTIONS; j++) { + if ((int)WSTRLEN(options[j].name) > len && + WSTRNCMP(options[i].name, options[j].name, len) == 0) { + if (earlier != NULL) { + *earlier = options[i].name; + } + if (later != NULL) { + *later = options[j].name; + } + return 1; + } + } + } + + return 0; +} +#endif /* WOLFSSHD_UNIT_TEST */ /* returns WS_SUCCESS on success */ static int HandlePrivSep(WOLFSSHD_CONFIG* conf, const char* value) @@ -1221,6 +1332,20 @@ static int SetListString(char** dst, const char* value, int valueSz, return ret; } +/* CA trust sources are loaded once at startup from the global config; a + * Match-scoped setting would be silently ignored at authentication time. + * Reject such options at parse time instead of failing open. Returns + * WS_SUCCESS when conf is the global config. */ +static int CheckNotInMatch(const WOLFSSHD_CONFIG* conf, const char* option) +{ + if (conf->usrAppliesTo != NULL || conf->groupAppliesTo != NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Option %s is not supported inside a Match block", option); + return WS_BAD_ARGUMENT; + } + return WS_SUCCESS; +} + /* returns WS_SUCCESS on success */ /* NOLINTNEXTLINE(misc-no-recursion): bounded by WOLFSSHD_MAX_INCLUDE_DEPTH */ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, @@ -1311,6 +1436,11 @@ 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 = CheckNotInMatch(*conf, "wolfSSH_TrustedSystemCAKeys"); + if (ret == WS_SUCCESS) + ret = wolfSSHD_ConfigSetSystemCA(*conf, value); + break; case OPT_PIDFILE: ret = SetFileString(&(*conf)->pidFile, value, (*conf)->heap); break; @@ -1320,10 +1450,72 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, case OPT_STRICT_MODES: ret = HandleStrictModes(*conf, value); break; + case OPT_TRUSTED_USER_CA_STORE: + ret = CheckNotInMatch(*conf, "wolfSSH_TrustedUserCAStore"); + if (ret == WS_SUCCESS) + ret = wolfSSHD_ConfigSetUserCAStore(*conf, value); + break; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + case OPT_WIN_USER_STORES: + ret = CheckNotInMatch(*conf, "wolfSSH_WinUserStores"); + if (ret == WS_SUCCESS) + ret = wolfSSHD_ConfigSetWinUserStores(*conf, value); + break; + case OPT_WIN_USER_DW_FLAGS: + ret = CheckNotInMatch(*conf, "wolfSSH_WinUserDwFlags"); + if (ret == WS_SUCCESS) + ret = wolfSSHD_ConfigSetWinUserDwFlags(*conf, value); + break; + case OPT_WIN_USER_PV_PARA: + ret = CheckNotInMatch(*conf, "wolfSSH_WinUserPvPara"); + if (ret == WS_SUCCESS) + ret = wolfSSHD_ConfigSetWinUserPvPara(*conf, value); + break; + #else + case OPT_WIN_USER_STORES: + case OPT_WIN_USER_DW_FLAGS: + case OPT_WIN_USER_PV_PARA: + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_WinUser* options require a " + "WOLFSSH_WINDOWS_CERT_STORE build"); + ret = WS_NOT_COMPILED; + break; + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ case OPT_AUTHORIZED_UPN_DOMAINS: ret = SetListString(&(*conf)->authorizedUPNDomains, full, fullSz, (*conf)->heap); break; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + case OPT_HOST_KEY_STORE: + ret = CheckNotInMatch(*conf, "HostKeyStore"); + if (ret == WS_SUCCESS) + ret = SetFileString(&(*conf)->hostKeyStore, value, + (*conf)->heap); + break; + case OPT_HOST_KEY_STORE_SUBJECT: + ret = CheckNotInMatch(*conf, "HostKeyStoreSubject"); + /* use the full line remainder so a CN containing spaces is + * kept instead of being cut at the first token */ + if (ret == WS_SUCCESS) + ret = SetListString(&(*conf)->hostKeyStoreSubject, full, + fullSz, (*conf)->heap); + break; + case OPT_HOST_KEY_STORE_FLAGS: + ret = CheckNotInMatch(*conf, "HostKeyStoreFlags"); + if (ret == WS_SUCCESS) + ret = SetFileString(&(*conf)->hostKeyStoreFlags, value, + (*conf)->heap); + break; + #else + case OPT_HOST_KEY_STORE: + case OPT_HOST_KEY_STORE_SUBJECT: + case OPT_HOST_KEY_STORE_FLAGS: + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] HostKeyStore* options require a " + "WOLFSSH_WINDOWS_CERT_STORE build"); + ret = WS_NOT_COMPILED; + break; + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ default: break; } @@ -1668,6 +1860,187 @@ 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 || value == NULL) { + ret = WS_BAD_ARGUMENT; + } + + if (ret == WS_SUCCESS) { + 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_ERROR, + "[SSHD] wolfSSH_TrustedSystemCAKeys: expected 'yes' or 'no', " + "got '%s'", value); + ret = WS_BAD_ARGUMENT; + } + } + + 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 || value == NULL) { + ret = WS_BAD_ARGUMENT; + } + + if (ret == WS_SUCCESS) { + 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_ERROR, + "[SSHD] wolfSSH_TrustedUserCAStore: expected 'yes' or 'no', " + "got '%s'", value); + ret = WS_BAD_ARGUMENT; + } + } + + return ret; +} + +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Returns the configured store provider, or NULL when not configured. The + * caller decides what an unset value means. */ +char* wolfSSHD_ConfigGetWinUserStores(const WOLFSSHD_CONFIG* conf) +{ + char* ret = NULL; + + if (conf != NULL) { + ret = conf->winUserStores; + } + + return ret; +} + +int wolfSSHD_ConfigSetWinUserStores(WOLFSSHD_CONFIG* conf, const char* value) +{ + int ret = WS_SUCCESS; + + if (conf == NULL || value == 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; +} + +/* Returns the configured store location, or NULL when not configured. */ +char* wolfSSHD_ConfigGetWinUserDwFlags(const WOLFSSHD_CONFIG* conf) +{ + char* ret = NULL; + + if (conf != NULL) { + ret = conf->winUserDwFlags; + } + + return ret; +} + +int wolfSSHD_ConfigSetWinUserDwFlags(WOLFSSHD_CONFIG* conf, const char* value) +{ + int ret = WS_SUCCESS; + + if (conf == NULL || value == 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; +} + +/* Returns the configured store name, or NULL when not configured. There is + * deliberately no default: this store is a trust anchor source for client + * certificate auth and must be picked by the administrator. */ +char* wolfSSHD_ConfigGetWinUserPvPara(const WOLFSSHD_CONFIG* conf) +{ + char* ret = NULL; + + if (conf != NULL) { + ret = conf->winUserPvPara; + } + + return ret; +} + +int wolfSSHD_ConfigSetWinUserPvPara(WOLFSSHD_CONFIG* conf, const char* value) +{ + int ret = WS_SUCCESS; + + if (conf == NULL || value == 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 /* WOLFSSH_WINDOWS_CERT_STORE */ + char* wolfSSHD_ConfigGetUserCAKeysFile(const WOLFSSHD_CONFIG* conf) { char* ret = NULL; @@ -1690,6 +2063,19 @@ char* wolfSSHD_ConfigGetAuthorizedUPNDomains(const WOLFSSHD_CONFIG* conf) return ret; } +/* returns the next config node in the list (the global config is the head, + * each Match block adds a node) or NULL at the end of the list */ +WOLFSSHD_CONFIG* wolfSSHD_ConfigGetNext(const WOLFSSHD_CONFIG* conf) +{ + WOLFSSHD_CONFIG* ret = NULL; + + if (conf != NULL) { + ret = conf->next; + } + + return ret; +} + static int SetFileString(char** dst, const char* src, void* heap) { int ret = WS_SUCCESS; @@ -1712,6 +2098,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 5792b4e89..cca930c63 100644 --- a/apps/wolfsshd/configuration.h +++ b/apps/wolfsshd/configuration.h @@ -62,8 +62,26 @@ char* wolfSSHD_ConfigGetHostKeyFile(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetHostCertFile(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetUserCAKeysFile(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetAuthorizedUPNDomains(const WOLFSSHD_CONFIG* conf); +WOLFSSHD_CONFIG* wolfSSHD_ConfigGetNext(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 WOLFSSH_WINDOWS_CERT_STORE +char* wolfSSHD_ConfigGetWinUserStores(const WOLFSSHD_CONFIG* conf); +int wolfSSHD_ConfigSetWinUserStores(WOLFSSHD_CONFIG* conf, const char* value); +char* wolfSSHD_ConfigGetWinUserDwFlags(const WOLFSSHD_CONFIG* conf); +int wolfSSHD_ConfigSetWinUserDwFlags(WOLFSSHD_CONFIG* conf, const char* value); +char* wolfSSHD_ConfigGetWinUserPvPara(const WOLFSSHD_CONFIG* conf); +int wolfSSHD_ConfigSetWinUserPvPara(WOLFSSHD_CONFIG* conf, const char* value); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ int wolfSSHD_ConfigSetUserCAKeysFile(WOLFSSHD_CONFIG* conf, const char* file); word16 wolfSSHD_ConfigGetPort(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetAuthKeysFile(const WOLFSSHD_CONFIG* conf); @@ -84,6 +102,8 @@ void wolfSSHD_ConfigSavePID(const WOLFSSHD_CONFIG* conf); #ifdef WOLFSSHD_UNIT_TEST int ParseConfigLine(WOLFSSHD_CONFIG** conf, const char* l, int lSz, int depth); +int wolfSSHD_ConfigOptionPrefixShadow(const char** earlier, + const char** later); #endif #endif /* WOLFSSHD_H */ diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index 75708d53a..0ad4970f3 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -2588,8 +2588,27 @@ static int test_PermitRootLoginModes(void) return ret; } -/* Parses an AuthorizedUPNDomains line and confirms the stored value is returned - * by the getter, locking in the new config option's plumbing. */ +/* The config parser matches option names with WSTRNCMP over the options table + * in order, so no entry may be a strict prefix of a later one (e.g. "HostKey" + * must come after the "HostKeyStore*" names). */ +static int test_ConfigOptionPrefixOrder(void) +{ + int ret = WS_SUCCESS; + const char* earlier = NULL; + const char* later = NULL; + + Log(" Testing scenario: option table prefix ordering."); + if (wolfSSHD_ConfigOptionPrefixShadow(&earlier, &later)) { + Log(" option '%s' shadows later option '%s'.", earlier, later); + ret = WS_FATAL_ERROR; + } + Log(ret == WS_SUCCESS ? " PASSED.\n" : " FAILED.\n"); + + return ret; +} + +/* Parses an AuthorizedUPNDomains line and confirms the stored value is + * returned by the getter, locking in the new config option's plumbing. */ static int test_ConfigParseAuthorizedUPNDomains(void) { int ret = WS_SUCCESS; @@ -4007,6 +4026,7 @@ const TEST_CASE testCases[] = { TEST_DECL(test_ConfigDefaults), TEST_DECL(test_PermitRootProhibitPassword), TEST_DECL(test_ParseConfigLine), + TEST_DECL(test_ConfigOptionPrefixOrder), TEST_DECL(test_ConfigCopy), TEST_DECL(test_GetUserConfMatchOverride), TEST_DECL(test_MatchUnsupportedSelector), diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index fb8111deb..c6cf6e769 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -37,6 +37,30 @@ #include #include #include +#ifdef WOLFSSH_CERTS + #include +#endif + +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include + #include + #include + #include + #include + #include + #ifndef CERT_SYSTEM_STORE_LOCATION_MASK + #define CERT_SYSTEM_STORE_LOCATION_MASK 0x00FF0000 + #endif + #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_SYSTEM_STORE_USERS + #define CERT_SYSTEM_STORE_USERS 0x00060000 + #endif +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ #define WOLFSSH_TEST_SERVER #include @@ -228,8 +252,11 @@ static void interruptCatch(int in) /* redirect logging to a specific file and add the PID value */ static void wolfSSHDLoggingCb(enum wolfSSH_LogLevel lvl, const char *const str) { - /* always log errors and optionally log other info/debug level messages */ - if (lvl == WS_LOG_ERROR || debugMode) { + /* Always log errors and warnings, and optionally log other info/debug + * level messages. Warnings carry the security relevant notices, e.g. that + * a certificate was bound to an account by subject CN alone, so they must + * not depend on -d. */ + if (lvl == WS_LOG_ERROR || lvl == WS_LOG_WARN || debugMode) { fprintf(logFile, "[PID %d]: %s\n", WGETPID(), str); /* flush so each line is visible immediately, e.g. to a consumer * reading the log file while the daemon is still running */ @@ -342,6 +369,365 @@ static void CleanupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, (void)conf; } +#if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE) +/* Returns 1 when val is one of the defined CERT_SYSTEM_STORE_* location + * codes. The mask alone is not enough: values such as 0x000A0000 sit inside + * it but name no store, and CertOpenStore would fail with an opaque error + * instead of the configuration reporting a bad value. The location id lives + * in the high half; 1, 2 and 4 through 9 are assigned, 3 is not. */ +static int IsCertStoreLocation(unsigned long val) +{ + unsigned long id; + + if (val == 0 || + (val & ~(unsigned long)CERT_SYSTEM_STORE_LOCATION_MASK) != 0) { + return 0; + } + id = val >> 16; + + return id == 1 || id == 2 || (id >= 4 && id <= 9); +} + +/* Parse a Windows system store location, given either as a CERT_SYSTEM_STORE_* + * name (long or short form) or as a number in strtoul() base 0 form, so both + * 65536 and 0x00010000 work. Only location bits are accepted; anything else is + * either not a location or a control flag (e.g. CERT_STORE_DELETE_FLAG) that + * would make CertOpenStore destructive. Returns WS_SUCCESS on success. */ +static int ParseCertStoreLocation(const char* in, word32* out) +{ + int ret = WS_SUCCESS; + unsigned long val; + char* end; + + if (in == NULL || out == NULL || *in == '\0') { + return WS_BAD_ARGUMENT; + } + + if (WSTRCMP(in, "CURRENT_USER") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_CURRENT_USER") == 0) { + *out = (word32)CERT_SYSTEM_STORE_CURRENT_USER; + } + else if (WSTRCMP(in, "LOCAL_MACHINE") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_LOCAL_MACHINE") == 0) { + *out = (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE; + } + else if (WSTRCMP(in, "USERS") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_USERS") == 0) { + *out = (word32)CERT_SYSTEM_STORE_USERS; + } + else if (WSTRCMP(in, "CURRENT_SERVICE") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_CURRENT_SERVICE") == 0) { + *out = (word32)CERT_SYSTEM_STORE_CURRENT_SERVICE; + } + else if (WSTRCMP(in, "SERVICES") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_SERVICES") == 0) { + *out = (word32)CERT_SYSTEM_STORE_SERVICES; + } + else if (WSTRCMP(in, "CURRENT_USER_GROUP_POLICY") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY") == 0) { + *out = (word32)CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY; + } + else if (WSTRCMP(in, "LOCAL_MACHINE_GROUP_POLICY") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY") == 0) { + *out = (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY; + } + else if (WSTRCMP(in, "LOCAL_MACHINE_ENTERPRISE") == 0 || + WSTRCMP(in, "CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE") == 0) { + *out = (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE; + } + else { + end = NULL; + errno = 0; + val = strtoul(in, &end, 0); + if (end == in || *end != '\0' || errno == ERANGE) { + ret = WS_BAD_ARGUMENT; + } + else if (!IsCertStoreLocation(val)) { + ret = WS_BAD_ARGUMENT; + } + else { + *out = (word32)val; + } + } + + return ret; +} + +/* Result of CertIsCA(). Kept distinct so the caller can tell a certificate + * that is genuinely not a CA from one that could not be examined at all. */ +enum { + CERT_CA_NO = 0, /* parsed, basicConstraints CA is not TRUE */ + CERT_CA_YES = 1, /* parsed, basicConstraints CA:TRUE */ + CERT_CA_UNKNOWN = -1 /* could not parse or could not allocate */ +}; + +/* Returns CERT_CA_YES when der holds an X.509 certificate with + * basicConstraints CA:TRUE, CERT_CA_NO when it parses but is not a CA, and + * CERT_CA_UNKNOWN when it could not be examined. */ +static int CertIsCA(const byte* der, word32 derSz, void* heap) +{ + DecodedCert* dCert; + int isCA = CERT_CA_UNKNOWN; +#ifndef WOLFSSH_SMALL_STACK + DecodedCert sdCert; +#endif + +#ifdef WOLFSSH_SMALL_STACK + dCert = (DecodedCert*)WMALLOC(sizeof(DecodedCert), heap, DYNTYPE_CERT); + if (dCert == NULL) { + return CERT_CA_UNKNOWN; + } +#else + dCert = &sdCert; +#endif + + wc_InitDecodedCert(dCert, der, derSz, heap); + if (wc_ParseCert(dCert, CERT_TYPE, NO_VERIFY, NULL) == 0) { + isCA = (dCert->isCA != 0) ? CERT_CA_YES : CERT_CA_NO; + } + wc_FreeDecodedCert(dCert); +#ifdef WOLFSSH_SMALL_STACK + WFREE(dCert, heap, DYNTYPE_CERT); +#endif + + return isCA; +} + +/* Returns 1 when name refers to a Windows store populated by the OS or the + * Microsoft Trusted Root Program rather than the administrator. Store names + * resolve to registry keys, which are case-insensitive, and may carry a + * '\' or '\' prefix, so only the final path component is + * compared, case-insensitively. */ +static int IsWinPublicTrustStoreName(const char* name) +{ + static const char* const deny[] = { + "Root", "AuthRoot", "CA", "Disallowed", "TrustedPublisher", "trust" + }; + const char* base; + const char* sep; + word32 len; + word32 i; + + base = name; + sep = WSTRCHR(base, '\\'); + while (sep != NULL) { + base = sep + 1; + sep = WSTRCHR(base, '\\'); + } + len = (word32)WSTRLEN(base); + for (i = 0; i < (word32)(sizeof(deny) / sizeof(*deny)); i++) { + if (len == (word32)WSTRLEN(deny[i]) && + WSTRNCASECMP(base, deny[i], len) == 0) { + return 1; + } + } + + return 0; +} + +/* Add every CA certificate in the configured Windows store (winUserPvPara + * name, winUserDwFlags location) as a trusted root CA. Returns WS_SUCCESS on + * success. */ +static int LoadUserCACertsFromStore(const WOLFSSHD_CONFIG* conf, + WOLFSSH_CTX* ctx, void* heap) +{ + int ret = WS_SUCCESS; + char* storeNameStr; + char* dwFlagsStr; + char* providerStr; + word32 dwFlags = 0; + wchar_t* wStoreName = NULL; + int wStoreNameLen; + HCERTSTORE hStore = NULL; + PCCERT_CONTEXT pCertContext = NULL; + word32 loaded = 0; + word32 skipped = 0; + word32 rejected = 0; + word32 notX509 = 0; + int isCA; + + storeNameStr = wolfSSHD_ConfigGetWinUserPvPara(conf); + dwFlagsStr = wolfSSHD_ConfigGetWinUserDwFlags(conf); + providerStr = wolfSSHD_ConfigGetWinUserStores(conf); + + /* Every certificate in this store becomes a trust anchor for client + * authentication, so the administrator must name it. There is no default: + * guessing one silently would pick a store the administrator never + * reviewed. Name a store created for this purpose, e.g. 'SSH_UserCA'. + * The Windows 'Root', 'AuthRoot' and 'CA' stores must not be used: they + * are populated by the Microsoft Trusted Root Program, so pointing at one + * makes every public commercial CA an SSH login authority. */ + if (storeNameStr == NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_TrustedUserCAStore is enabled but no store name " + "is configured. Set wolfSSH_WinUserPvPara to a store holding " + "nothing but the client CA certificates to trust, e.g. " + "'SSH_UserCA'. Do not use the public 'Root', 'AuthRoot' or 'CA' " + "stores."); + return WS_BAD_ARGUMENT; + } + + if (IsWinPublicTrustStoreName(storeNameStr)) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_WinUserPvPara='%s' names a Windows system trust " + "store. Every CA it holds would become an SSH login authority. " + "Use a store created for this purpose instead.", storeNameStr); + return WS_BAD_ARGUMENT; + } + + /* Only the system-store provider is supported here. NULL means the option + * was not given, which is that same provider. Fail rather than silently + * load trust anchors from a different provider than the one configured. */ + if (providerStr != NULL && + WSTRCMP(providerStr, "CERT_STORE_PROV_SYSTEM") != 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_WinUserStores='%s' is not supported; only " + "CERT_STORE_PROV_SYSTEM is supported", providerStr); + return WS_BAD_ARGUMENT; + } + + /* The location is mandatory for the same reason the store name is. The + * per-user hive is writable by the account the daemon runs as, without + * elevation, so silently defaulting to CURRENT_USER would let anything + * running as that account add a trust anchor. */ + if (dwFlagsStr == NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_TrustedUserCAStore is enabled but no store " + "location is configured. Set wolfSSH_WinUserDwFlags, normally to " + "LOCAL_MACHINE."); + return WS_BAD_ARGUMENT; + } + if (ParseCertStoreLocation(dwFlagsStr, &dwFlags) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unrecognized user CA store flags '%s'", dwFlagsStr); + return WS_BAD_ARGUMENT; + } + /* Every location other than the LOCAL_MACHINE hives (per-user, per-service + * and HKEY_USERS stores) can be written without elevation by the account + * it belongs to. */ + if (dwFlags != (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE && + dwFlags != (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY && + dwFlags != (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] wolfSSH_WinUserDwFlags selects a store hive that its own " + "account can write to without elevation. " + "LOCAL_MACHINE is the safer location for trust anchors."); + } + +#ifdef WOLFSSH_NO_FPKI + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] WARNING: built without FPKI profile checking, so peer " + "certificates are not required to carry a client authentication " + "EKU. A TLS server, S/MIME or code signing certificate with a " + "matching subject is accepted for login."); +#endif + + wStoreNameLen = MultiByteToWideChar(CP_UTF8, 0, storeNameStr, -1, NULL, 0); + if (wStoreNameLen == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to convert user CA store name to wide characters"); + return WS_BAD_ARGUMENT; + } + wStoreName = (wchar_t*)WMALLOC(wStoreNameLen * sizeof(wchar_t), heap, + DYNTYPE_SSHD); + if (wStoreName == NULL) { + return WS_MEMORY_E; + } + if (MultiByteToWideChar(CP_UTF8, 0, storeNameStr, -1, wStoreName, + wStoreNameLen) == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to convert user CA store name to wide characters"); + WFREE(wStoreName, heap, DYNTYPE_SSHD); + return WS_BAD_ARGUMENT; + } + + hStore = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, (HCRYPTPROV_LEGACY)0, + dwFlags | CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG, + wStoreName); + if (hStore == NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unable to open user CA cert store '%s', error %lu", + storeNameStr, (unsigned long)GetLastError()); + WFREE(wStoreName, heap, DYNTYPE_SSHD); + return WS_FATAL_ERROR; + } + + /* Passing the previous context frees it and advances the enumeration. */ + for (;;) { + pCertContext = CertEnumCertificatesInStore(hStore, pCertContext); + if (pCertContext == NULL) { + break; + } + if (pCertContext->pbCertEncoded == NULL || + pCertContext->cbCertEncoded == 0) { + notX509++; + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Skipping an entry in store '%s' with no encoded " + "certificate", storeNameStr); + continue; + } + if ((pCertContext->dwCertEncodingType & X509_ASN_ENCODING) == 0) { + notX509++; + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Skipping an entry in store '%s' that is not X.509 " + "DER encoded", storeNameStr); + continue; + } + /* wolfSSL does not enforce basicConstraints CA:TRUE for user-loaded + * trust anchors, so an end-entity certificate sitting in the store + * would become a login authority. Filter it out here. */ + isCA = CertIsCA(pCertContext->pbCertEncoded, + (word32)pCertContext->cbCertEncoded, heap); + if (isCA != CERT_CA_YES) { + skipped++; + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Skipping a cert in store '%s': %s", storeNameStr, + isCA == CERT_CA_NO ? "not a CA (no basicConstraints CA:TRUE)" + : "could not be parsed"); + continue; + } + if (wolfSSH_CTX_AddRootCert_buffer(ctx, + (const byte*)pCertContext->pbCertEncoded, + (word32)pCertContext->cbCertEncoded, + WOLFSSH_FORMAT_ASN1) != WS_SUCCESS) { + /* Skip certs wolfSSH cannot use as a trust anchor. */ + rejected++; + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Skipping a CA cert in store '%s' that could not be " + "loaded as a root CA", storeNameStr); + continue; + } + loaded++; + } + + CertCloseStore(hStore, 0); + WFREE(wStoreName, heap, DYNTYPE_SSHD); + + if (loaded == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] No usable CA certificates found in store '%s' (%u not a " + "CA, %u rejected as a root CA, %u not X.509)", storeNameStr, + skipped, rejected, notX509); + ret = WS_FATAL_ERROR; + } + else { + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Trusting %u CA certificate(s) from store '%s' " + "(location 0x%08lx) for client authentication (%u not a CA, %u " + "rejected as a root CA, %u not X.509)", loaded, storeNameStr, + (unsigned long)dwFlags, skipped, rejected, notX509); + if (rejected > 0) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] %u CA certificate(s) in store '%s' could not be " + "loaded; the trust anchor set is incomplete", rejected, + storeNameStr); + } + } + + return ret; +} +#endif /* WOLFSSH_CERTS && WOLFSSH_WINDOWS_CERT_STORE */ + /* Initializes and sets up the WOLFSSH_CTX struct based on the configure options * return WS_SUCCESS on success */ @@ -383,98 +769,221 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, /* Load in host private key */ if (ret == WS_SUCCESS) { +#if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE) + char* hostKeyStore = wolfSSHD_ConfigGetHostKeyStore(conf); + char* hostKeyStoreSubject = wolfSSHD_ConfigGetHostKeyStoreSubject(conf); + char* hostKeyStoreFlags = wolfSSHD_ConfigGetHostKeyStoreFlags(conf); - char* hostKey = wolfSSHD_ConfigGetHostKeyFile(conf); - - if (hostKey == NULL) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No host private key set"); + if (hostKeyStore != NULL && hostKeyStoreSubject == NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] HostKeyStore set but HostKeyStoreSubject is missing"); + ret = WS_BAD_ARGUMENT; + } + else if (hostKeyStore == NULL && + (hostKeyStoreSubject != NULL || hostKeyStoreFlags != NULL)) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] HostKeyStoreSubject/HostKeyStoreFlags set but " + "HostKeyStore is missing"); + ret = WS_BAD_ARGUMENT; + } + /* The store branch below wins over the file path, so a HostKey line + * left in place would be silently discarded. StartSSHD() already + * rejects the same conflict expressed with -h. */ + else if (hostKeyStore != NULL && + wolfSSHD_ConfigGetHostKeyFile(conf) != NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] HostKey conflicts with the configured HostKeyStore. " + "Use one or the other."); + ret = WS_BAD_ARGUMENT; + } + /* A store host key carries its own certificate. A HostCertificate + * file would land on the same x509v3 slot and leave it advertised + * with no signing material behind it. */ + else if (hostKeyStore != NULL && + wolfSSHD_ConfigGetHostCertFile(conf) != NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] HostCertificate conflicts with the configured " + "HostKeyStore, which supplies its own certificate. Use one or " + "the other."); 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. */ + if (ret == WS_SUCCESS && + 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 = 0; + int subjectNameLen = 0; + + /* An unset location keeps the CURRENT_USER default set above. */ + if (hostKeyStoreFlags != NULL && + ParseCertStoreLocation(hostKeyStoreFlags, &dwFlags) + != WS_SUCCESS) { wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Error reading host key file."); - ret = WS_BAD_FILE_E; + "[SSHD] Unrecognized host key store flags '%s'", + hostKeyStoreFlags); + ret = WS_BAD_ARGUMENT; + } + if (ret == WS_SUCCESS && + dwFlags == (word32)CERT_SYSTEM_STORE_CURRENT_USER) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] HostKeyStoreFlags selects the per-user store " + "hive, which the daemon's own account can write to " + "without elevation. LOCAL_MACHINE is the safer location " + "for the host key."); + } + + /* Convert to wide strings */ + if (ret == WS_SUCCESS) { + storeNameLen = MultiByteToWideChar(CP_UTF8, 0, hostKeyStore, + -1, NULL, 0); + subjectNameLen = MultiByteToWideChar(CP_UTF8, 0, + hostKeyStoreSubject, -1, NULL, 0); + if (storeNameLen == 0 || subjectNameLen == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to convert cert store strings to wchar"); + ret = WS_BAD_ARGUMENT; + } } if (ret == WS_SUCCESS) { - /* Host keys may be PEM or DER. Detect by content: a DER key is - * an ASN.1 SEQUENCE (leading 0x30); anything else is treated as - * PEM text and decoded with wc_KeyPemToDer(), which handles - * PKCS#1, SEC1 and PKCS#8 "PRIVATE KEY" bodies. - * - * The previous code used wc_PemToDer(..., PRIVATEKEY_TYPE, ...), - * which only recognizes the classic "RSA/EC PRIVATE KEY" PEM - * headers. On a PKCS#8 body (how ML-DSA host keys are emitted) - * it returns *success* but yields a malformed body (leading - * 0x04, not a 0x30 SEQUENCE), which - * wolfSSH_CTX_UsePrivateKey_buffer() then rejects with - * WS_BAD_FILETYPE_E, so ML-DSA PEM host keys could not load. */ - byte* keyDer = NULL; - - if (dataSz == 0) { - /* An empty (0-byte) file passes the NULL check above but - * carries no key material. Handle it explicitly as a file - * error instead of falling into the PEM path, where - * WMALLOC(0) is implementation-defined (may return NULL and - * be misreported as WS_MEMORY_E). */ - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Host key file is empty."); - ret = WS_BAD_FILE_E; + 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 if (data[0] == 0x30) { - privBuf = data; - privBufSz = dataSz; + else if (MultiByteToWideChar(CP_UTF8, 0, hostKeyStore, -1, + wStoreName, storeNameLen) == 0 || + MultiByteToWideChar(CP_UTF8, 0, hostKeyStoreSubject, + -1, wSubjectName, subjectNameLen) == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to convert cert store strings to wchar"); + ret = WS_BAD_ARGUMENT; } else { - keyDer = (byte*)WMALLOC(dataSz, heap, DYNTYPE_SSHD); - if (keyDer == NULL) { - ret = WS_MEMORY_E; + 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 (wStoreName != NULL) { + WFREE(wStoreName, heap, DYNTYPE_SSHD); + } + if (wSubjectName != NULL) { + WFREE(wSubjectName, heap, DYNTYPE_SSHD); + } + } + } + else if (ret == WS_SUCCESS) +#endif /* WOLFSSH_CERTS && WOLFSSH_WINDOWS_CERT_STORE */ + { + char* hostKey = wolfSSHD_ConfigGetHostKeyFile(conf); + + 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] Error reading host key file."); + ret = WS_BAD_FILE_E; + + } + + if (ret == WS_SUCCESS) { + /* Host keys may be PEM or DER. Detect by content: a DER key + * is an ASN.1 SEQUENCE (leading 0x30); anything else is + * treated as PEM text and decoded with wc_KeyPemToDer(), + * which handles PKCS#1, SEC1 and PKCS#8 "PRIVATE KEY" + * bodies. + * + * The previous code used wc_PemToDer(..., PRIVATEKEY_TYPE, + * ...), which only recognizes the classic "RSA/EC PRIVATE + * KEY" PEM headers. On a PKCS#8 body (how ML-DSA host keys + * are emitted) it returns *success* but yields a malformed + * body (leading 0x04, not a 0x30 SEQUENCE), which + * wolfSSH_CTX_UsePrivateKey_buffer() then rejects with + * WS_BAD_FILETYPE_E, so ML-DSA PEM host keys could not + * load. */ + byte* keyDer = NULL; + + if (dataSz == 0) { + /* An empty (0-byte) file passes the NULL check above + * but carries no key material. Handle it explicitly as + * a file error instead of falling into the PEM path, + * where WMALLOC(0) is implementation-defined (may + * return NULL and be misreported as WS_MEMORY_E). */ + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Host key file is empty."); + ret = WS_BAD_FILE_E; + } + else if (data[0] == 0x30) { + privBuf = data; + privBufSz = dataSz; } else { - int keyDerSz = wc_KeyPemToDer(data, dataSz, keyDer, - (int)dataSz, NULL); - if (keyDerSz <= 0) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Failed to convert " - "host private key from PEM."); - ret = WS_BAD_FILE_E; + keyDer = (byte*)WMALLOC(dataSz, heap, DYNTYPE_SSHD); + if (keyDer == NULL) { + ret = WS_MEMORY_E; } else { - privBuf = keyDer; - privBufSz = (word32)keyDerSz; + int keyDerSz = wc_KeyPemToDer(data, dataSz, keyDer, + (int)dataSz, NULL); + if (keyDerSz <= 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to convert host private key " + "from PEM."); + ret = WS_BAD_FILE_E; + } + else { + privBuf = keyDer; + privBufSz = (word32)keyDerSz; + } } } - } - if (ret == WS_SUCCESS - && 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; - } + if (ret == WS_SUCCESS + && 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; + } - if (keyDer != NULL) { - WS_FORCEZERO(keyDer, dataSz); - WFREE(keyDer, heap, DYNTYPE_SSHD); + if (keyDer != NULL) { + WS_FORCEZERO(keyDer, dataSz); + WFREE(keyDer, heap, DYNTYPE_SSHD); + } + /* data held the raw private key — the DER bytes, or the PEM + * text decoded into keyDer above. Zeroize before freeing so + * key material does not linger in the heap after use. */ + WS_FORCEZERO(data, dataSz); + freeBufferFromFile(data, heap); } - /* data held the raw private key — the DER bytes, or the PEM - * text decoded into keyDer above. Zeroize before freeing so key - * material does not linger in the heap after use. */ - WS_FORCEZERO(data, dataSz); - freeBufferFromFile(data, heap); } } } @@ -526,6 +1035,165 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, #endif /* WOLFSSH_OSSH_CERTS || WOLFSSH_CERTS */ #ifdef WOLFSSH_CERTS + /* Load system CA certs from the OS trust store via wolfSSL into a + * temporary WOLFSSL_CTX, then import its cert manager. That cert manager + * verifies *client* certificates during user authentication, so every CA + * in the OS trust store becomes a login authority for this daemon. On a + * public trust store that is every commercial root CA, and the only + * remaining binding to an account is the certificate subject. Intended for + * a store that holds nothing but the organization's own CA. */ + #ifdef WOLFSSL_SYS_CA_CERTS + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetSystemCA(conf)) { + WOLFSSL_CTX* sslCtx; + + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] WARNING: wolfSSH_TrustedSystemCAKeys makes every CA in " + "the OS trust store an SSH user authentication authority. Any " + "certificate issued by any of them whose subject matches a local " + "account name can log in as that account. Use this only when the " + "OS trust store holds solely your organization's CA."); + #ifdef WOLFSSH_NO_FPKI + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] WARNING: built without FPKI profile checking, so peer " + "certificates are not required to carry a client authentication " + "EKU. A TLS server, S/MIME or code signing certificate with a " + "matching subject is accepted for login."); + #endif + sslCtx = wolfSSL_CTX_new(wolfSSLv23_server_method()); + if (sslCtx == NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unable to create temporary CTX for the system CAs"); + ret = WS_FATAL_ERROR; + } + + if (ret == WS_SUCCESS) { + if (wolfSSL_CTX_load_system_CA_certs(sslCtx) != WOLFSSL_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Issue loading system CAs"); + ret = WS_FATAL_ERROR; + } + } + + if (ret == WS_SUCCESS) { + if (wolfSSH_SetCertManager(*ctx, + wolfSSL_CTX_GetCertManager(sslCtx)) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Issue copying over system CAs"); + ret = WS_FATAL_ERROR; + } + } + + if (sslCtx != NULL) { + wolfSSL_CTX_free(sslCtx); + } + } + #else + /* The system CA directive is parsed unconditionally. Fail startup if it + * was set but wolfSSL was built without WOLFSSL_SYS_CA_CERTS, rather than + * silently running without the configured trust anchors. */ + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetSystemCA(conf)) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_TrustedSystemCAKeys set but wolfSSL was built " + "without WOLFSSL_SYS_CA_CERTS."); + ret = WS_NOT_COMPILED; + } + #endif /* WOLFSSL_SYS_CA_CERTS */ + + /* Load user CA certs (trust anchors used to verify client X.509 certs) + * directly from a Windows certificate store into the cert manager. */ + #if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE) + /* Mirror the HostKeyStore* validation: the wolfSSH_WinUser* group only + * has an effect through LoadUserCACertsFromStore(), so accepting it + * without the store enabled would start the daemon with no client CA + * trust anchors and no indication why logins fail. */ + if (ret == WS_SUCCESS && !wolfSSHD_ConfigGetUserCAStore(conf) && + (wolfSSHD_ConfigGetWinUserPvPara(conf) != NULL || + wolfSSHD_ConfigGetWinUserDwFlags(conf) != NULL || + wolfSSHD_ConfigGetWinUserStores(conf) != NULL)) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_WinUserPvPara/wolfSSH_WinUserDwFlags/" + "wolfSSH_WinUserStores are set but wolfSSH_TrustedUserCAStore is " + "not enabled, so they would have no effect."); + ret = WS_BAD_ARGUMENT; + } + + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetUserCAStore(conf)) { + ret = LoadUserCACertsFromStore(conf, *ctx, heap); + } + #else + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetUserCAStore(conf)) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_TrustedUserCAStore set but " + "WOLFSSH_WINDOWS_CERT_STORE is not compiled in."); + ret = WS_NOT_COMPILED; + } + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ + + /* State the cert-to-user binding once at startup. Which one is in force is + * fixed by the wolfSSL build, not by configuration, so this cannot be + * derived from the config file. */ + #if defined(WOLFSSH_CERTS) && !defined(WOLFSSL_FPKI) && defined(_WIN32) + if (ret == WS_SUCCESS && + (wolfSSHD_ConfigGetUserCAKeysFile(conf) != NULL || + wolfSSHD_ConfigGetUserCAStore(conf) || + wolfSSHD_ConfigGetSystemCA(conf))) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] WARNING: client certificates are bound to an account by " + "subject CN only. Any CA in the trusted user CA set may assert " + "any CN, so keep that set narrow. Build wolfSSL with FPKI for " + "UPN binding and AuthorizedUPNDomains."); + } + #endif + + /* AuthorizedUPNDomains is only enforced by the FPKI UPN check. Fail + * startup rather than silently ignore a configured realm policy. Check + * every config node since the directive may sit in a Match block. */ + #ifndef WOLFSSL_FPKI + if (ret == WS_SUCCESS) { + const WOLFSSHD_CONFIG* cur; + + cur = conf; + while (cur != NULL) { + if (wolfSSHD_ConfigGetAuthorizedUPNDomains(cur) != NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] AuthorizedUPNDomains is set but wolfSSL was " + "built without WOLFSSL_FPKI, so the UPN realm allowlist " + "cannot be enforced."); + ret = WS_BAD_ARGUMENT; + break; + } + cur = wolfSSHD_ConfigGetNext(cur); + } + } + #endif /* !WOLFSSL_FPKI */ + + /* A per-user AuthorizedKeysFile is its own user-to-certificate binding, + * so the certificate identity check is skipped for those accounts unless + * AuthorizedUPNDomains is set. Warn once at startup, checking every + * config node since AuthorizedKeysFile may be set in a Match block. */ + #if defined(WOLFSSL_FPKI) || defined(_WIN32) + if (ret == WS_SUCCESS) { + const WOLFSSHD_CONFIG* cur; + + cur = conf; + while (cur != NULL) { + if (wolfSSHD_ConfigGetAuthKeysFileSet(cur) && + wolfSSHD_AuthKeysPatternIsPerUser( + wolfSSHD_ConfigGetAuthKeysFile(cur)) && + wolfSSHD_ConfigGetAuthorizedUPNDomains(cur) == NULL) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] The certificate identity check is skipped for " + "accounts with a per-user AuthorizedKeysFile; those " + "certificates are bound by their authorized_keys entry " + "instead."); + break; + } + cur = wolfSSHD_ConfigGetNext(cur); + } + } + #endif /* WOLFSSL_FPKI || _WIN32 */ + + /* load in CA certs from file set */ if (ret == WS_SUCCESS) { char* caCert = wolfSSHD_ConfigGetUserCAKeysFile(conf); if (caCert != NULL) { @@ -570,6 +1238,14 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } } } +#else + if (ret == WS_SUCCESS && (wolfSSHD_ConfigGetSystemCA(conf) + || wolfSSHD_ConfigGetUserCAStore(conf))) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_TrustedSystemCAKeys/wolfSSH_TrustedUserCAStore set " + "but wolfSSH was built without WOLFSSH_CERTS."); + ret = WS_NOT_COMPILED; + } #endif if (ret == WS_SUCCESS) { @@ -2815,6 +3491,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) { @@ -2829,8 +3523,20 @@ static int StartSSHD(int argc, char** argv) } /* check if host key file was passed in */ - if (hostKeyFile != NULL) { - wolfSSHD_ConfigSetHostKeyFile(conf, hostKeyFile); + if (ret == WS_SUCCESS && hostKeyFile != NULL) { + #if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE) + /* The store branch in SetupCTX() wins over the file path, so an + * accepted -h here would be silently discarded. */ + if (wolfSSHD_ConfigGetHostKeyStore(conf) != NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] -h host key file conflicts with the configured " + "HostKeyStore. Use one or the other."); + ret = WS_BAD_ARGUMENT; + } + #endif + if (ret == WS_SUCCESS) { + wolfSSHD_ConfigSetHostKeyFile(conf, hostKeyFile); + } } if (ret == WS_SUCCESS) { @@ -2846,10 +3552,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 a98a70212..254d2891d 100644 --- a/configure.ac +++ b/configure.ac @@ -220,6 +220,12 @@ AC_ARG_ENABLE([ossh-certs], [AS_HELP_STRING([--enable-ossh-certs],[Enable OpenSSH certificate user auth (default: disabled)])], [ENABLED_OSSH_CERTS=$enableval],[ENABLED_OSSH_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)])], @@ -288,6 +294,15 @@ AS_IF([test "x$ENABLED_CERTS" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_CERTS"]) AS_IF([test "x$ENABLED_OSSH_CERTS" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_OSSH_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" + dnl Only the mingw triplets define _WIN32, which wolfssh/internal.h + dnl requires. MSYS/Cygwin toolchains do not. + AS_CASE([$host], + [*mingw*],[LIBS="$LIBS -lcrypt32 -lncrypt"], + [AC_MSG_ERROR([--enable-windows-cert-store is only supported on _WIN32 Windows hosts (mingw)])])]) AS_IF([test "x$ENABLED_SMALLSTACK" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_SMALL_STACK"]) AS_IF([test "x$ENABLED_NONE_CIPHER" = "xyes"], @@ -331,7 +346,11 @@ AC_CONFIG_LINKS([keys/gretel-key-rsa.pub:keys/gretel-key-rsa.pub keys/server-key-rsa.der:keys/server-key-rsa.der keys/server-key-ecc.der:keys/server-key-ecc.der keys/server-key-ecc-521.der:keys/server-key-ecc-521.der - keys/server-key-ed25519.der:keys/server-key-ed25519.der]) + keys/server-key-ed25519.der:keys/server-key-ed25519.der + keys/ca-cert-ecc.der:keys/ca-cert-ecc.der + keys/ca-key-ecc.der:keys/ca-key-ecc.der + keys/fred-cert.der:keys/fred-cert.der + keys/fred-key.der:keys/fred-key.der]) # Set the automake conditionals. AM_CONDITIONAL([BUILD_EXAMPLE_SERVERS],[test "x$ENABLED_EXAMPLES" = "xyes"]) @@ -399,4 +418,5 @@ AS_ECHO([" * TPM 2.0 support: $ENABLED_TPM"]) AS_ECHO([" * TCP/IP Forwarding: $ENABLED_FWD"]) AS_ECHO([" * X.509 Certs: $ENABLED_CERTS"]) AS_ECHO([" * OpenSSH Certs: $ENABLED_OSSH_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 b9df3416e..65bb3d0a0 100644 --- a/examples/client/common.c +++ b/examples/client/common.c @@ -1201,3 +1201,117 @@ void ClientFreeBuffers(const char* pubKeyName, const char* privKeyName, wc_ForceZero(userPassword, sizeof(userPassword)); pubKeyLoaded = 0; } + +#ifdef WOLFSSH_WINDOWS_CERT_STORE +int ClientSetPrivateKeyFromStore(WOLFSSH_CTX* ctx, + const wchar_t* storeName, word32 dwFlags, const wchar_t* subjectName) +{ + int ret; + + 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, void* heap) +{ + const byte* keyType = NULL; + WOLFSSH_PVT_KEY* pvtKey = NULL; + byte* certCopy = NULL; + word32 i; + + if (ctx == NULL) + return WS_BAD_ARGUMENT; + + /* wolfSSH_CTX_UsePrivateKey_fromStore() registers a store key under its + * plain key type and, when the build has one, under the matching x509v3 + * type. Only the x509v3 slot can be offered for certificate user auth, + * and it only exists when that algorithm is compiled in, so select on it + * rather than trusting slot ordering. */ + for (i = 0; i < ctx->privateKeyCount && i < WOLFSSH_MAX_PVT_KEYS; i++) { + WOLFSSH_PVT_KEY* cur = &ctx->privateKey[i]; + + if (!cur->useCertStore || cur->cert == NULL || cur->certSz == 0) + continue; + + /* Map the internal key format to the x509v3 SSH type name. Resolve + * it before touching the globals so a failure leaves them alone. */ + switch (cur->publicKeyFmt) { + case ID_X509V3_SSH_RSA: + keyType = (const byte*)"x509v3-ssh-rsa"; + break; + case ID_X509V3_ECDSA_SHA2_NISTP256: + keyType = (const byte*)"x509v3-ecdsa-sha2-nistp256"; + break; + case ID_X509V3_ECDSA_SHA2_NISTP384: + keyType = (const byte*)"x509v3-ecdsa-sha2-nistp384"; + break; + case ID_X509V3_ECDSA_SHA2_NISTP521: + keyType = (const byte*)"x509v3-ecdsa-sha2-nistp521"; + break; + default: + /* the plain twin of the same store key, keep looking */ + continue; + } + pvtKey = cur; + break; + } + + if (pvtKey == NULL) { + fprintf(stderr, "No cert store key with an x509v3 algorithm found in " + "CTX. RSA cert store keys need SHA-1 enabled " + "(WOLFSSH_NO_SHA1_SOFT_DISABLE) on both ends.\n"); + return WS_BAD_ARGUMENT; + } + + /* Copy the DER certificate before touching the globals so a failure + * leaves them alone. ClientFreeBuffers() frees the copy. */ + certCopy = (byte*)WMALLOC(pvtKey->certSz, heap, DYNTYPE_PRIVKEY); + if (certCopy == NULL) { + return WS_MEMORY_E; + } + WMEMCPY(certCopy, pvtKey->cert, pvtKey->certSz); + + /* Drop anything an earlier file based load left behind, the cert + * store key replaces it. Freed with the same heap the loaders in this + * file allocate with. */ + if (userPublicKeyAlloc && userPublicKey != NULL) { + WFREE(userPublicKey, heap, DYNTYPE_PRIVKEY); + userPublicKey = userPublicKeyBuf; + userPublicKeyAlloc = 0; + } + if (userPrivateKeyAlloc && userPrivateKey != NULL) { + wc_ForceZero(userPrivateKey, userPrivateKeySz); + WFREE(userPrivateKey, heap, DYNTYPE_PRIVKEY); + userPrivateKeyAlloc = 0; + } + + userPublicKey = certCopy; + userPublicKeySz = pvtKey->certSz; + userPublicKeyAlloc = 1; + userPublicKeyType = keyType; + userPublicKeyTypeSz = (word32)WSTRLEN((const char*)keyType); + + /* No in-memory private key, signing goes through the cert store. */ + userPrivateKey = userPrivateKeyBuf; + userPrivateKeySz = 0; + + pubKeyLoaded = 1; + return WS_SUCCESS; +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ diff --git a/examples/client/common.h b/examples/client/common.h index 6ea330c2e..62618d166 100644 --- a/examples/client/common.h +++ b/examples/client/common.h @@ -20,6 +20,12 @@ #ifndef WOLFSSH_COMMON_H #define WOLFSSH_COMMON_H + +#include +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include +#endif + int ClientLoadCA(WOLFSSH_CTX* ctx, const char* caCert); int ClientUsePubKey(const char* pubKeyName, int userEcc, void* heap); int ClientSetPrivateKey(const char* privKeyName, int userEcc, @@ -35,6 +41,14 @@ 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); +/* Supersedes ClientUseCert()/ClientUsePubKey()/ClientSetPrivateKey(), any key + * they loaded is released. Copies the certificate out of ctx; call + * ClientFreeBuffers() to release the copy. */ +int ClientSetupCertStoreAuth(WOLFSSH_CTX* ctx, void* heap); +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif /* WOLFSSH_COMMON_H */ diff --git a/examples/echoserver/echoserver.c b/examples/echoserver/echoserver.c index 4bb178bcf..c15dafda1 100644 --- a/examples/echoserver/echoserver.c +++ b/examples/echoserver/echoserver.c @@ -41,6 +41,9 @@ #include #include #include +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include +#endif #include #include #include @@ -117,6 +120,8 @@ #define SOCKET_EWOULDBLOCK WSAEWOULDBLOCK #endif +/* Shared by echoserver_test() and the -W pre-scan in wolfSSH_Echoserver(). */ +#define ES_OPTLIST "?1a:d:efEp:R:Ni:j:I:J:K:P:k:b:x:m:c:s:G:HW:" #ifndef NO_WOLFSSH_SERVER @@ -2878,6 +2883,14 @@ 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"); + printf(" also read from the WOLFSSH_CERT_STORE environment " + "variable\n"); + printf(" with either set, file names are relative to the " + "current directory\n"); +#endif printf(" -b test user auth would block\n"); printf(" -H set test highwater callback\n"); } @@ -2992,6 +3005,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; @@ -3001,7 +3017,7 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) #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 = ES_OPTLIST; myoptind = 0; while ((ch = mygetopt(argc, argv, optlist)) != -1) { switch (ch) { @@ -3126,6 +3142,15 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) useCustomHighWaterCb = 1; break; + case 'W': + #ifdef WOLFSSH_WINDOWS_CERT_STORE + certStoreSpec = myoptarg; + #else + ES_ERROR("-W requires wolfSSH built with " + "WOLFSSH_WINDOWS_CERT_STORE\n"); + #endif + break; + default: ShowUsage(); serverArgs->return_code = MY_EX_USAGE; @@ -3134,6 +3159,20 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) } } myoptind = 0; /* reset for test cases */ + + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* -W takes priority over the environment; empty means unset. */ + if (certStoreSpec == NULL) { + certStoreSpec = getenv("WOLFSSH_CERT_STORE"); + if (certStoreSpec != NULL && certStoreSpec[0] == '\0') { + certStoreSpec = NULL; + } + if (certStoreSpec != NULL) { + printf("Taking the host key from the WOLFSSH_CERT_STORE " + "environment variable\n"); + } + } + #endif wc_InitMutex(&doneLock); #ifdef WOLFSSH_TEST_BLOCK @@ -3325,6 +3364,38 @@ 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, heap); + if (ret != WS_SUCCESS) { + #ifdef WOLFSSH_SMALL_STACK + wc_ForceZero(keyLoadBuf, EXAMPLE_KEYLOAD_BUFFER_SZ); + WFREE(keyLoadBuf, NULL, 0); + #endif + ES_ERROR("Invalid cert store spec. Use: store:subject:flags\n"); + } + + ret = wolfSSH_CTX_UsePrivateKey_fromStore(ctx, wStoreName, + dwFlags, wSubjectName); + wolfSSH_FreeCertStoreSpec(wStoreName, wSubjectName, heap); + if (ret != WS_SUCCESS) { + #ifdef WOLFSSH_SMALL_STACK + wc_ForceZero(keyLoadBuf, EXAMPLE_KEYLOAD_BUFFER_SZ); + WFREE(keyLoadBuf, NULL, 0); + #endif + 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) { @@ -3729,7 +3800,38 @@ 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 + const char* envStore; + + /* 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. + * An empty WOLFSSH_CERT_STORE means unset. */ + envStore = getenv("WOLFSSH_CERT_STORE"); + if (envStore != NULL && envStore[0] != '\0') { + useStore = 1; + } + else { + int ch; + + /* Parse rather than match on argv, an option value could + * start with "-W" too. */ + myoptind = 0; + while ((ch = mygetopt(argc, argv, ES_OPTLIST)) != -1) { + if (ch == 'W') { + useStore = 1; + break; + } + } + myoptind = 0; + } + #endif + if (!useStore) { + ChangeToWolfSshRoot(); + } + } #endif #ifndef NO_WOLFSSH_SERVER echoserver_test(&args); diff --git a/examples/scpclient/scpclient.c b/examples/scpclient/scpclient.c index 08ee73f10..f2ecde3b2 100644 --- a/examples/scpclient/scpclient.c +++ b/examples/scpclient/scpclient.c @@ -329,6 +329,7 @@ THREAD_RETURN WOLFSSH_THREAD scp_client(void* args) } WCLOSESOCKET(sockFd); wolfSSH_free(ssh); + ClientFreeBuffers(pubKeyName, privKeyName, NULL); wolfSSH_CTX_free(ctx); if (ret != WS_SUCCESS && ret != WS_SOCKET_ERROR_E && ret != WS_CHANNEL_CLOSED) { @@ -336,7 +337,6 @@ THREAD_RETURN WOLFSSH_THREAD scp_client(void* args) "Closing scp stream failed. Connection could have been closed by peer"); } - ClientFreeBuffers(pubKeyName, privKeyName, NULL); #if !defined(WOLFSSH_NO_ECC) && defined(FP_ECC) && defined(HAVE_THREAD_LS) wc_ecc_fp_free(); /* free per thread cache */ #endif diff --git a/examples/sftpclient/sftpclient.c b/examples/sftpclient/sftpclient.c index e892a71e1..6093f4cbd 100644 --- a/examples/sftpclient/sftpclient.c +++ b/examples/sftpclient/sftpclient.c @@ -33,6 +33,9 @@ #include #include #include +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include +#endif #include #include #include @@ -398,6 +401,14 @@ 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"); + printf(" -k set the comma separated list of server host key " + "algos to accept\n"); +#ifdef WOLFSSH_WINDOWS_CERT_STORE + printf(" -W Windows cert store: \"store:subject:flags\"\n"); + printf(" Example: -W \"My:CN=MyCert:CURRENT_USER\"\n"); + printf(" supplies both keys, can not be used with " + "-i, -j or -J\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"); @@ -1566,13 +1577,21 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) char* pubKeyName = NULL; char* certName = NULL; char* caCert = NULL; + const char* keyList = 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:k: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; @@ -1634,6 +1653,10 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) pubKeyName = myoptarg; break; + case 'k': + keyList = myoptarg; + break; + #ifdef WOLFSSH_CERTS case 'J': certName = myoptarg; @@ -1650,6 +1673,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); @@ -1664,6 +1693,14 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) if (username == NULL) err_sys("client requires a username parameter."); +#ifdef WOLFSSH_WINDOWS_CERT_STORE + if (certStoreSpec != NULL && (privKeyName != NULL || pubKeyName != NULL || + certName != NULL)) { + err_sys("-W provides both keys, it can not be used with -i, -j " + "or -J."); + } +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + if ((pubKeyName == NULL && certName == NULL) && privKeyName != NULL) { err_sys("If setting priv key, need pub key."); } @@ -1696,29 +1733,80 @@ 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, heap); + 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) { + wolfSSH_FreeCertStoreSpec(wStoreName, wSubjectName, heap); + err_sys("Couldn't create wolfSSH client context."); + } + + /* Set private key from cert store. The names are only needed for + * this call, so release them here and leave the error paths with + * nothing to clean up. */ + ret = ClientSetPrivateKeyFromStore(ctx, wStoreName, dwFlags, + wSubjectName); + wolfSSH_FreeCertStoreSpec(wStoreName, wSubjectName, heap); + wStoreName = NULL; + wSubjectName = NULL; + if (ret != WS_SUCCESS) { + wolfSSH_CTX_free(ctx); + 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, heap); + if (ret != WS_SUCCESS) { + wolfSSH_CTX_free(ctx); + err_sys("Error setting up cert store auth"); + } + } 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"); + } + + #ifdef WOLFSSH_CERTS + /* passed in certificate to use */ + if (certName) { + ret = ClientUseCert(certName, heap); + } + else + #endif + { + ret = ClientUsePubKey(pubKeyName, 0, heap); + } + if (ret != 0) { + err_sys("Error setting public key"); + } - ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, heap); + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, heap); + } if (ctx == NULL) err_sys("Couldn't create wolfSSH client context."); + if (keyList) { + if (wolfSSH_CTX_SetAlgoListKey(ctx, keyList) != WS_SUCCESS) { + err_sys("Error setting key list."); + } + } + if (((func_args*)args)->user_auth == NULL) wolfSSH_SetUserAuth(ctx, ClientUserAuth); else @@ -1776,8 +1864,11 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) ret = wolfSSH_SFTP_connect(ssh); else ret = NonBlockSSH_connect(); - if (ret != WS_SUCCESS) + if (ret != WS_SUCCESS) { + fprintf(stderr, "wolfSSH_SFTP_connect failed: %d, %s\n", ret, + wolfSSH_ErrorToName(ret)); err_sys("Couldn't connect SFTP"); + } { /* get current working directory */ @@ -1861,13 +1952,16 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) WCLOSESOCKET(sockFd); wolfSSH_free(ssh); + + /* Clear the auth globals before the CTX, with a cert store key they + * alias memory owned by the CTX. */ + ClientFreeBuffers(pubKeyName, privKeyName, heap); wolfSSH_CTX_free(ctx); if (ret != WS_SUCCESS) { printf("error %d encountered\n", ret); ((func_args*)args)->return_code = ret; } - ClientFreeBuffers(pubKeyName, privKeyName, heap); #if !defined(WOLFSSH_NO_ECC) && defined(FP_ECC) && defined(HAVE_THREAD_LS) wc_ecc_fp_free(); /* free per thread cache */ #endif diff --git a/ide/winvs/api-test/api-test.vcxproj b/ide/winvs/api-test/api-test.vcxproj index b0289307d..8b40665e9 100644 --- a/ide/winvs/api-test/api-test.vcxproj +++ b/ide/winvs/api-test/api-test.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) @@ -364,7 +364,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32FIPS) @@ -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) @@ -400,7 +400,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32FIPS) @@ -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) @@ -436,7 +436,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64FIPS) @@ -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) @@ -472,7 +472,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64FIPS) @@ -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) @@ -511,7 +511,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32FIPS) @@ -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) @@ -551,7 +551,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32FIPS) @@ -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) @@ -591,7 +591,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64FIPS) @@ -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) @@ -631,7 +631,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64FIPS) diff --git a/ide/winvs/client/client.vcxproj b/ide/winvs/client/client.vcxproj index ce9887b3b..1a2eed004 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) @@ -364,7 +364,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32FIPS) @@ -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) @@ -400,7 +400,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32FIPS) @@ -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) @@ -436,7 +436,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64FIPS) @@ -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) @@ -472,7 +472,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64FIPS) @@ -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) @@ -511,7 +511,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32FIPS) @@ -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) @@ -551,7 +551,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32FIPS) @@ -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) @@ -591,7 +591,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64FIPS) @@ -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) @@ -631,7 +631,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64FIPS) diff --git a/ide/winvs/echoserver/echoserver.vcxproj b/ide/winvs/echoserver/echoserver.vcxproj index e220247c7..b32603fee 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) @@ -363,7 +363,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32FIPS) @@ -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) @@ -399,7 +399,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32FIPS) @@ -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) @@ -435,7 +435,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64FIPS) @@ -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) @@ -471,7 +471,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64FIPS) @@ -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) @@ -510,7 +510,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32FIPS) @@ -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) @@ -550,7 +550,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32FIPS) @@ -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) @@ -590,7 +590,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64FIPS) @@ -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) @@ -630,7 +630,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64FIPS) diff --git a/ide/winvs/testsuite/testsuite.vcxproj b/ide/winvs/testsuite/testsuite.vcxproj index a97835917..7d13d5c7c 100644 --- a/ide/winvs/testsuite/testsuite.vcxproj +++ b/ide/winvs/testsuite/testsuite.vcxproj @@ -348,7 +348,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32) @@ -366,7 +366,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32FIPS) @@ -384,7 +384,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32) @@ -402,7 +402,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32FIPS) @@ -420,7 +420,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64) @@ -438,7 +438,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64FIPS) @@ -456,7 +456,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64) @@ -474,7 +474,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64FIPS) @@ -493,7 +493,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32) @@ -513,7 +513,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32FIPS) @@ -533,7 +533,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32) @@ -553,7 +553,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32FIPS) @@ -573,7 +573,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64) @@ -593,7 +593,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64FIPS) @@ -613,7 +613,7 @@ Console true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64) @@ -633,7 +633,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64FIPS) diff --git a/ide/winvs/unit-test/unit-test.vcxproj b/ide/winvs/unit-test/unit-test.vcxproj index 383de1ee9..f716a0433 100644 --- a/ide/winvs/unit-test/unit-test.vcxproj +++ b/ide/winvs/unit-test/unit-test.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) @@ -363,7 +363,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32FIPS) @@ -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) @@ -399,7 +399,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32FIPS) @@ -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) @@ -435,7 +435,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64FIPS) @@ -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) @@ -471,7 +471,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64FIPS) @@ -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) @@ -510,7 +510,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32FIPS) @@ -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) @@ -550,7 +550,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32FIPS) @@ -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) @@ -590,7 +590,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64FIPS) @@ -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) @@ -630,7 +630,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64FIPS) diff --git a/ide/winvs/user_settings.h b/ide/winvs/user_settings.h index d923629bf..b47ca41db 100644 --- a/ide/winvs/user_settings.h +++ b/ide/winvs/user_settings.h @@ -58,6 +58,22 @@ #define WOLFSSH_CERTS #endif +/* Host and user keys held in the MS Certificate Store. Needs WOLFSSH_CERTS + * above, and the projects link crypt32.lib and ncrypt.lib for it. Left + * commented rather than behind an "#if 0" so enabling the X.509 block above + * does not silently pull it in. + * + * #undef WOLFSSH_WINDOWS_CERT_STORE + * #define WOLFSSH_WINDOWS_CERT_STORE + * + * An RSA certificate store key is offered as "x509v3-ssh-rsa", which RFC 6187 + * signs with SHA-1, so that combination also needs + * #define WOLFSSH_NO_SHA1_SOFT_DISABLE + * here and, in wolfSSL's user_settings.h, + * #define WC_SIG_MIN_HASH_TYPE WC_HASH_TYPE_SHA + * ECDSA certificate store keys need neither. + */ + /* default SSHD options */ #if 0 diff --git a/ide/winvs/wolfsftp-client/wolfsftp-client.vcxproj b/ide/winvs/wolfsftp-client/wolfsftp-client.vcxproj index 8ed347f93..4c0e39dea 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) @@ -364,7 +364,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug32FIPS) @@ -382,7 +382,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug32FIPS) @@ -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) @@ -436,7 +436,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDllDebug64FIPS) @@ -454,7 +454,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) msvcrt.lib $(wolfCryptDebug64FIPS) @@ -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) @@ -511,7 +511,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease32FIPS) @@ -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) @@ -551,7 +551,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease32FIPS) @@ -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) @@ -591,7 +591,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptRelease64FIPS) @@ -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) @@ -631,7 +631,7 @@ Console true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) true true $(wolfCryptDllRelease64FIPS) diff --git a/ide/winvs/wolfssh/wolfssh.vcxproj b/ide/winvs/wolfssh/wolfssh.vcxproj index 0808a12e2..25b2d7764 100644 --- a/ide/winvs/wolfssh/wolfssh.vcxproj +++ b/ide/winvs/wolfssh/wolfssh.vcxproj @@ -365,7 +365,7 @@ Windows true $(wolfCryptDllDebug32) - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) @@ -382,7 +382,7 @@ Windows true $(wolfCryptDllDebug32FIPS) - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) @@ -428,7 +428,7 @@ Windows true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) $(wolfCryptDllDebug64) @@ -445,7 +445,7 @@ Windows true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) $(wolfCryptDllDebug64FIPS) @@ -502,7 +502,7 @@ true true $(wolfCryptDllRelease32) - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) @@ -522,7 +522,7 @@ true true $(wolfCryptDllRelease32FIPS) - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) @@ -577,7 +577,7 @@ true true true - wolfssl.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) $(wolfCryptDllRelease64) @@ -597,7 +597,7 @@ true true true - wolfssl-fips.lib;ws2_32.lib;%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;crypt32.lib;ncrypt.lib;%(AdditionalDependencies) $(wolfCryptDllRelease64FIPS) diff --git a/ide/winvs/wolfsshd/wolfsshd.vcxproj b/ide/winvs/wolfsshd/wolfsshd.vcxproj index 2b14feaa2..fede8c151 100644 --- a/ide/winvs/wolfsshd/wolfsshd.vcxproj +++ b/ide/winvs/wolfsshd/wolfsshd.vcxproj @@ -254,7 +254,7 @@ Console true ..\..\..\..\wolfssl\Debug\Win32;..\Debug\Win32 - 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) @@ -269,7 +269,7 @@ Console true ..\..\..\..\wolfssl\IDE\WIN10\Debug\Win32;..\Debug\Win32 - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) @@ -284,7 +284,7 @@ Console true ..\..\..\..\wolfssl\IDE\WIN10\Debug\Win32;..\Debug\Win32 - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) @@ -303,7 +303,7 @@ true true ..\..\..\..\wolfssl\Release\Win32;..\Release\Win32 - 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) @@ -322,7 +322,7 @@ true true ..\..\..\..\wolfssl\IDE\WIN10\Release\Win32;..\Release\Win32 - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) @@ -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) @@ -352,7 +352,7 @@ Console true ..\..\..\..\wolfssl\IDE\WIN10\Debug\x64;..\Debug\x64 - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) @@ -367,7 +367,7 @@ Console true ..\..\..\..\wolfssl\IDE\WIN10\Debug\x64;..\Debug\x64 - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.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 @@ -404,7 +404,7 @@ true true true - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) ..\..\..\..\wolfssl\IDE\WIN10\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 @@ -433,7 +433,7 @@ Level3 - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) $(wolfCryptDLLRelease64FIPS) true true @@ -442,7 +442,7 @@ - wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + wolfssl-fips.lib;ws2_32.lib;secur32.lib;userenv.lib;crypt32.lib;ncrypt.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) $(wolfCryptDLLRelease32FIPS) @@ -456,7 +456,7 @@ ..;..\..\..;$(wolfCryptDir);..\..\..\apps\wolfsshd\;%(AdditionalIncludeDirectories) - 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) $(wolfCryptDLLRelease32FIPS) diff --git a/src/certman.c b/src/certman.c index 2674ca2f7..fcc22f281 100644 --- a/src/certman.c +++ b/src/certman.c @@ -45,6 +45,27 @@ #include #include +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include + #include + #include + #include + #ifndef CERT_SYSTEM_STORE_LOCATION_MASK + #define CERT_SYSTEM_STORE_LOCATION_MASK 0x00FF0000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCATION_SHIFT + #define CERT_SYSTEM_STORE_LOCATION_SHIFT 16 + #endif + #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_SYSTEM_STORE_USERS + #define CERT_SYSTEM_STORE_USERS 0x00060000 + #endif +#endif #ifdef WOLFSSH_CERTS @@ -85,6 +106,54 @@ 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 LIBWOLFSSL_VERSION_HEX < WOLFSSL_V4_6_0 + WOLFSSH_UNUSED(ctx); + WOLFSSH_UNUSED(cm); + + WLOG(WS_LOG_CERTMAN, "Importing a cert manager needs wolfSSL 4.6.0"); + return WS_NOT_COMPILED; +#else + if (ctx == NULL || cm == NULL || ctx->certMan == NULL) { + return WS_BAD_ARGUMENT; + } + + /* importing the manager already in use is a no-op */ + if (ctx->certMan->cm == cm) { + return WS_SUCCESS; + } + + if (wolfSSL_CertManager_up_ref(cm) != WOLFSSL_SUCCESS) { + WLOG(WS_LOG_CERTMAN, "Failed to increment cert manager reference"); + return WS_FATAL_ERROR; + } + +#ifdef HAVE_OCSP + /* an imported manager gets the same policy _CertMan_init() applies, and + * is rejected if it can't, rather than silently skipping revocation */ + if (wolfSSL_CertManagerEnableOCSP(cm, WOLFSSL_OCSP_CHECKALL) + != WOLFSSL_SUCCESS) { + WLOG(WS_LOG_CERTMAN, "Couldn't enable OCSP on imported cert manager"); + wolfSSL_CertManagerFree(cm); + return WS_FATAL_ERROR; + } +#endif + + /* free up existing cm if present */ + if (ctx->certMan->cm != NULL) { + wolfSSL_CertManagerFree(ctx->certMan->cm); + } + ctx->certMan->cm = cm; + + return WS_SUCCESS; +#endif +} + + static WOLFSSH_CERTMAN* _CertMan_init(WOLFSSH_CERTMAN* cm, void* heap) { WOLFSSH_CERTMAN* ret = NULL; @@ -644,4 +713,198 @@ static int CheckProfile(DecodedCert* cert, int profile) } #endif /* WOLFSSH_NO_FPKI */ + +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Returns 1 when dwFlags is exactly one assigned CERT_SYSTEM_STORE_* + * location with no control flags set, 0 otherwise. Location ids 1, 2 and + * 4..9 are assigned in wincrypt.h; 3 and 10..255 are not, and CertOpenStore + * fails opaquely on them. */ +int wolfSSH_CertStoreLocationValid(word32 dwFlags) +{ + word32 id; + + if ((dwFlags & ~(word32)CERT_SYSTEM_STORE_LOCATION_MASK) != 0) { + return 0; + } + id = dwFlags >> CERT_SYSTEM_STORE_LOCATION_SHIFT; + return id == 1 || id == 2 || (id >= 4 && id <= 9); +} + + +/* Parse a cert store spec string "store:subject[:flags]" into wide-string + * components. The spec is split at the first ':' for the store name and at + * the next one for the flags, so neither the store name nor the subject may + * contain a ':'; a spec with a third ':' is rejected. "My:CN=host:65536" is + * therefore store "My", subject "CN=host", flags 65536, never a two-field + * spec with a ':' in the subject. Allocates wStoreName and wSubjectName; + * caller releases them with wolfSSH_FreeCertStoreSpec(). On success dwFlags + * is set to the parsed flags value, on failure it is left alone. + * 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; + char* flagsEnd = NULL; + unsigned long flagsVal; + unsigned long locationMask; + word32 flags; + int wStoreNameLen, wSubjectNameLen; + size_t specLen; + + if (spec == NULL || wStoreName == NULL || wSubjectName == NULL || + dwFlags == NULL) { + return WS_BAD_ARGUMENT; + } + + *wStoreName = NULL; + *wSubjectName = NULL; + flags = CERT_SYSTEM_STORE_CURRENT_USER; + locationMask = (unsigned long)CERT_SYSTEM_STORE_LOCATION_MASK; + + 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 (*flagsStr == '\0') { + WLOG(WS_LOG_CERTMAN, + "Cert store spec has an empty flags field; expected " + "store:subject[:flags]"); + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_BAD_ARGUMENT; + } + if (WSTRCHR(flagsStr, ':') != NULL) { + WLOG(WS_LOG_CERTMAN, + "Cert store spec has too many ':'-separated fields; " + "expected store:subject[:flags]"); + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_BAD_ARGUMENT; + } + /* Accept the same spellings as wolfsshd's HostKeyStoreFlags and + * wolfSSH_WinUserDwFlags so one name works everywhere. */ + if (WSTRCMP(flagsStr, "CURRENT_USER") == 0 + || WSTRCMP(flagsStr, + "CERT_SYSTEM_STORE_CURRENT_USER") == 0) { + flags = CERT_SYSTEM_STORE_CURRENT_USER; + } + else if (WSTRCMP(flagsStr, "LOCAL_MACHINE") == 0 + || WSTRCMP(flagsStr, + "CERT_SYSTEM_STORE_LOCAL_MACHINE") == 0) { + flags = CERT_SYSTEM_STORE_LOCAL_MACHINE; + } + else if (WSTRCMP(flagsStr, "USERS") == 0 + || WSTRCMP(flagsStr, "CERT_SYSTEM_STORE_USERS") == 0) { + flags = CERT_SYSTEM_STORE_USERS; + } + else { + /* Fall back to a raw numeric value, decimal or 0x hex, that + * has to be consumed whole. Only system-store location bits + * are accepted. Anything else is either not a location or a + * control flag (e.g. CERT_STORE_DELETE_FLAG) that would make + * CertOpenStore destructive. */ + errno = 0; + flagsVal = strtoul(flagsStr, &flagsEnd, 0); + if (flagsEnd == flagsStr || *flagsEnd != '\0' + || errno == ERANGE) { + WLOG(WS_LOG_CERTMAN, "Malformed cert store flags value " + "'%s'; expected store:subject[:flags] with a " + "CERT_SYSTEM_STORE_* name or number", flagsStr); + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_BAD_ARGUMENT; + } + if (!wolfSSH_CertStoreLocationValid((word32)flagsVal)) { + WLOG(WS_LOG_CERTMAN, + "Cert store flags are not an assigned store " + "location"); + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_BAD_ARGUMENT; + } + flags = (word32)flagsVal; + } + } + } + + if (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; + } + + if (MultiByteToWideChar(CP_UTF8, 0, storeName, -1, + *wStoreName, wStoreNameLen) == 0 || + MultiByteToWideChar(CP_UTF8, 0, subjectName, -1, + *wSubjectName, wSubjectNameLen) == 0) { + WLOG(WS_LOG_CERTMAN, "Cert store spec wide-string conversion failed"); + WFREE(*wStoreName, heap, DYNTYPE_TEMP); + WFREE(*wSubjectName, heap, DYNTYPE_TEMP); + *wStoreName = NULL; + *wSubjectName = NULL; + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_FATAL_ERROR; + } + + *dwFlags = flags; + + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_SUCCESS; +} + + +/* Releases the wide strings allocated by wolfSSH_ParseCertStoreSpec(). + * Either pointer may be NULL. The heap must match the parse call. */ +void wolfSSH_FreeCertStoreSpec(wchar_t* wStoreName, wchar_t* wSubjectName, + void* heap) +{ + if (wStoreName != NULL) { + WFREE(wStoreName, heap, DYNTYPE_TEMP); + } + if (wSubjectName != NULL) { + WFREE(wSubjectName, heap, DYNTYPE_TEMP); + } + WOLFSSH_UNUSED(heap); +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + + #endif /* WOLFSSH_CERTS */ diff --git a/src/internal.c b/src/internal.c index 72169832c..6e6e9be7b 100644 --- a/src/internal.c +++ b/src/internal.c @@ -65,6 +65,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 0xFFFFFFFF + #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 @@ -1057,6 +1078,12 @@ static const char cannedKeyAlgoNames[] = #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 "x509v3-ecdsa-sha2-nistp256," #endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP256 */ + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 + "x509v3-ecdsa-sha2-nistp384," + #endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP384 */ + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 + "x509v3-ecdsa-sha2-nistp521," + #endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP521 */ #ifdef WOLFSSH_NO_SHA1_SOFT_DISABLE "x509v3-ssh-rsa," #endif /* WOLFSSH_NO_SHA1_SOFT_DISABLE */ @@ -1136,6 +1163,12 @@ static const char cannedKeyAlgoNamesHostKey[] = #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 "x509v3-ecdsa-sha2-nistp256," #endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP256 */ + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 + "x509v3-ecdsa-sha2-nistp384," + #endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP384 */ + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 + "x509v3-ecdsa-sha2-nistp521," + #endif /* WOLFSSH_NO_ECDSA_SHA2_NISTP521 */ #ifdef WOLFSSH_NO_SHA1_SOFT_DISABLE "x509v3-ssh-rsa," #endif /* WOLFSSH_NO_SHA1_SOFT_DISABLE */ @@ -1248,6 +1281,46 @@ 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. + * The certificate DER copied out of the store is released as well, so a slot + * can never pair a store certificate with a key from another source. Only a + * slot that is cert-store backed is touched, so a file certificate installed + * on the slot is left alone. */ +static void ClearCertStoreKey(WOLFSSH_CTX* ctx, WOLFSSH_PVT_KEY* pvtKey) +{ + if (!pvtKey->useCertStore) { + return; + } + + if (pvtKey->certStoreContext != NULL) { + CertFreeCertificateContext((PCCERT_CONTEXT)pvtKey->certStoreContext); + pvtKey->certStoreContext = NULL; + } +#ifdef WOLFSSH_CERTS + if (pvtKey->cert != NULL) { + WFREE(pvtKey->cert, ctx->heap, DYNTYPE_CERT); + pvtKey->cert = NULL; + pvtKey->certSz = 0; + } +#endif + 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()"); @@ -1268,6 +1341,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; } @@ -2136,16 +2212,67 @@ static int IdentifyCert(const byte* in, word32 inSz, void* heap) return ret; } + + +/* Returns 1 when id names an x509v3 host key algorithm. */ +static int IsCertKeyId(byte id) +{ + int ret; + + switch (id) { + case ID_X509V3_SSH_RSA: + case ID_X509V3_ECDSA_SHA2_NISTP256: + case ID_X509V3_ECDSA_SHA2_NISTP384: + case ID_X509V3_ECDSA_SHA2_NISTP521: + #ifndef WOLFSSH_NO_MLDSA + case ID_X509V3_MLDSA44: + case ID_X509V3_MLDSA65: + case ID_X509V3_MLDSA87: + #endif + ret = 1; + break; + default: + ret = 0; + } + + return ret; +} #endif /* WOLFSSH_CERTS */ -static void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) +WOLFSSH_LOCAL void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) { WOLFSSH_PVT_KEY* key; byte* publicKeyAlgo = ctx->publicKeyAlgo; word32 keyCount = ctx->privateKeyCount, publicKeyAlgoCount = 0, idx; for (idx = 0, key = ctx->privateKey; idx < keyCount; idx++, key++) { + #ifdef WOLFSSH_CERTS + /* An x509v3 slot whose certificate was dropped cannot produce a K_S, + * so do not advertise it. */ + if (IsCertKeyId(key->publicKeyFmt) && key->cert == NULL) { + WLOG(WS_LOG_DEBUG, "RefreshPublicKeyAlgo: skipping %s, " + "no certificate", IdToName(key->publicKeyFmt)); + continue; + } + #endif + /* A slot with no signing source at all cannot answer a KEXDH_INIT. + * This happens when a file HostCertificate lands on a slot whose + * paired key is TPM or cert-store backed, leaving a certificate with + * no key behind it. Advertising it would abort the handshake instead + * of falling back to an algorithm that does work. */ + if (key->key == NULL + #ifdef WOLFSSH_TPM + && !key->isTpm + #endif + #ifdef WOLFSSH_WINDOWS_CERT_STORE + && !IsCertStoreKey(key) + #endif + ) { + WLOG(WS_LOG_DEBUG, "RefreshPublicKeyAlgo: skipping %s, " + "no signing source", IdToName(key->publicKeyFmt)); + continue; + } if (key->publicKeyFmt == ID_SSH_RSA) { #ifndef WOLFSSH_NO_RSA_SHA2_512 if (publicKeyAlgoCount < WOLFSSH_MAX_PUB_KEY_ALGO) { @@ -2179,13 +2306,17 @@ static void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) } } } + if (publicKeyAlgoCount == 0 && keyCount > 0) { + WLOG(WS_LOG_ERROR, "RefreshPublicKeyAlgo: No usable host key; every " + "loaded slot lacks a certificate or signing source"); + } ctx->publicKeyAlgoCount = publicKeyAlgoCount; } #ifdef WOLFSSH_CERTS -static INLINE byte CertTypeForId(byte id) +WOLFSSH_LOCAL byte CertTypeForId(byte id) { switch (id) { #ifndef WOLFSSH_NO_SSH_RSA_SHA1 @@ -2271,14 +2402,20 @@ static int UpdateHostCertificates(WOLFSSH_CTX* ctx, if (HINTISSET(keyHint) && HINTISSET(certHint)) { byte* key = NULL; word32 keySz; + int copyKey; #ifdef WOLFSSH_TPM int keyIsTpm = ctx->privateKey[keyHint].isTpm; #endif + /* A cert-store or TPM slot has no software key bytes to copy. */ + copyKey = ctx->privateKey[keyHint].key != NULL + && ctx->privateKey[keyHint].keySz > 0; + #ifdef WOLFSSH_TPM /* A TPM-backed key has no software bytes to copy; clear any stale * software key on the certificate slot and mark it TPM-backed. */ if (keyIsTpm) { + copyKey = 0; if (ctx->privateKey[certHint].key != NULL) { WS_FORCEZERO(ctx->privateKey[certHint].key, ctx->privateKey[certHint].keySz); @@ -2287,14 +2424,14 @@ static int UpdateHostCertificates(WOLFSSH_CTX* ctx, ctx->privateKey[certHint].key = NULL; ctx->privateKey[certHint].keySz = 0; } + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* A slot is never both TPM- and cert-store backed. */ + ClearCertStoreKey(ctx, &ctx->privateKey[certHint]); + #endif ctx->privateKey[certHint].isTpm = 1; } #endif - if (ret == WS_SUCCESS -#ifdef WOLFSSH_TPM - && !keyIsTpm -#endif - ) { + if (ret == WS_SUCCESS && copyKey) { keySz = ctx->privateKey[keyHint].keySz; key = (byte*)WMALLOC(keySz, ctx->heap, DYNTYPE_PRIVKEY); if (key == NULL) { @@ -2309,6 +2446,17 @@ static int UpdateHostCertificates(WOLFSSH_CTX* ctx, WFREE(ctx->privateKey[certHint].key, ctx->heap, DYNTYPE_PRIVKEY); } + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* The slot's key material and its cert-store state change + * together, so the store certificate is never sent as K_S + * with a signature made by this software key. */ + if (IsCertStoreKey(&ctx->privateKey[certHint])) { + WLOG(WS_LOG_ERROR, "UpdateHostCertificates: Dropping " + "the cert-store x509v3 host key; the loaded file " + "key replaces it"); + } + ClearCertStoreKey(ctx, &ctx->privateKey[certHint]); + #endif ctx->privateKey[certHint].key = key; ctx->privateKey[certHint].keySz = keySz; #ifdef WOLFSSH_TPM @@ -2348,12 +2496,41 @@ static int SetHostCertificate(WOLFSSH_CTX* ctx, } } + /* Replace the existing certificate slot when there is one, rather than + * appending a second slot with the same publicKeyFmt. */ + if (HINTISSET(certIdx)) { + destIdx = certIdx; + } + if (destIdx >= WOLFSSH_MAX_PVT_KEYS) { + /* der not taken on this path; free it to avoid a leak */ + WFREE(der, ctx->heap, dynamicType); ret = WS_CTX_KEY_COUNT_E; } + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* A file certificate cannot be paired with a cert-store host key: the + * store slot holds no software key to copy onto the certificate slot, + * and clearing the store state below would tear down the only signing + * source this algorithm has. Report the misconfiguration instead. */ + else if (IsCertStoreKey(ctx->privateKey + destIdx) + || (HINTISSET(keyIdx) && IsCertStoreKey(ctx->privateKey + keyIdx))) { + WLOG(WS_LOG_ERROR, "SetHostCertificate: The host key for this " + "algorithm comes from the certificate store, which supplies its " + "own certificate; do not also load a host certificate file"); + WFREE(der, ctx->heap, dynamicType); + ret = WS_BAD_ARGUMENT; + } + #endif else { WOLFSSH_PVT_KEY* pvtKey = ctx->privateKey + destIdx; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Defensive only: the else-if above already rejects a cert-store + * slot, so this can only clear a slot in a state no writer + * currently produces. */ + ClearCertStoreKey(ctx, pvtKey); + #endif + if (pvtKey->publicKeyFmt == certId) { if (pvtKey->cert != NULL) { WFREE(pvtKey->cert, ctx->heap, dynamicType); @@ -2419,6 +2596,18 @@ 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. */ + if (IsCertStoreKey(pvtKey)) { + WLOG(WS_LOG_ERROR, "SetHostPrivateKey: Replacing the " + "certificate store host key for this algorithm with a " + "file key"); + } + ClearCertStoreKey(ctx, pvtKey); + #endif + pvtKey->key = der; pvtKey->keySz = derSz; #ifdef WOLFSSH_TPM @@ -2475,6 +2664,11 @@ int wolfSSH_SetHostTpmKey(WOLFSSH_CTX* ctx, byte keyId) pvtKey->key = NULL; pvtKey->keySz = 0; pvtKey->isTpm = 1; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* The slot is now TPM backed; drop any cert-store state so signing + * and K_S do not use a stale certificate context. */ + ClearCertStoreKey(ctx, pvtKey); + #endif #ifdef WOLFSSH_CERTS /* Mark the matching certificate slot TPM-backed so certificate KEX @@ -2491,6 +2685,9 @@ int wolfSSH_SetHostTpmKey(WOLFSSH_CTX* ctx, byte keyId) ctx->privateKey[certIdx].keySz = 0; } ctx->privateKey[certIdx].isTpm = 1; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + ClearCertStoreKey(ctx, &ctx->privateKey[certIdx]); + #endif break; } } @@ -5844,8 +6041,30 @@ static int ParseECCPubKeyCert(WOLFSSH *ssh, #ifndef WOLFSSH_NO_ECDSA byte* der = NULL; word32 derSz, idx = 0; + int expectedCurve; int error; + switch (ssh->handshake->pubKeyId) { + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + case ID_X509V3_ECDSA_SHA2_NISTP256: + expectedCurve = ECC_SECP256R1; + break; + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 + case ID_X509V3_ECDSA_SHA2_NISTP384: + expectedCurve = ECC_SECP384R1; + break; + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 + case ID_X509V3_ECDSA_SHA2_NISTP521: + expectedCurve = ECC_SECP521R1; + break; + #endif + default: + expectedCurve = ECC_CURVE_INVALID; + break; + } + ret = ParsePubKeyCert(ssh, pubKey, pubKeySz, &der, &derSz); if (ret == WS_SUCCESS) { error = InitPubKey(sigKeyBlock_ptr, ssh); @@ -5856,6 +6075,15 @@ static int ParseECCPubKeyCert(WOLFSSH *ssh, if (error == 0) error = wc_EccPublicKeyDecode(der, &idx, &sigKeyBlock_ptr->sk.ecc.key, derSz); + /* Bind the certificate's key to the negotiated curve, as + * ParseECCPubKey does for plain keys. */ + if (error == 0 && + wc_ecc_get_curve_id(sigKeyBlock_ptr->sk.ecc.key.idx) != + expectedCurve) { + WLOG(WS_LOG_DEBUG, "ParseECCPubKeyCert: certificate key curve " + "does not match the negotiated algorithm"); + error = WS_INVALID_PRIME_CURVE; + } if (error == 0) { sigKeyBlock_ptr->keySz = (word32)sizeof(sigKeyBlock_ptr->sk.ecc.key); } @@ -12676,6 +12904,16 @@ int SendKexInit(WOLFSSH* ssh) ret = WS_BAD_ARGUMENT; } + /* Loaded slots can all lack a signing source (RefreshPublicKeyAlgo + * skips them); fail here rather than send an empty, RFC 4253 + * violating, server-host-key-algorithms list. */ + if (ret == WS_SUCCESS && ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER && + ssh->algoListKey == NULL && ssh->ctx->publicKeyAlgoCount == 0) { + WLOG(WS_LOG_ERROR, "No usable host key: every loaded slot lacks a " + "certificate or signing source"); + ret = WS_BAD_ARGUMENT; + } + if (ret == WS_SUCCESS) { /* Set self is keying flag since we started sending the KEX init msg */ ssh->isKeying |= WOLFSSH_SELF_IS_KEYING; @@ -12825,6 +13063,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 { @@ -13186,6 +13427,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 */ /* Dispatches on pubKeyFmtId to sync with SendKexDhReply's free chain. * ID_RSA_SHA2_256/512 already collapse to ID_SSH_RSA. */ @@ -13197,26 +13442,75 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, FALL_THROUGH; #endif case ID_SSH_RSA: - /* 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; + + 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 && 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_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 if (ret == 0) { + 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) { @@ -13332,6 +13626,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, @@ -14367,6 +14700,320 @@ 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 WS_SUCCESS on success, otherwise a WS_ error code. */ +static int ExtractPubKeyDerFromCert(const byte* certDer, word32 certDerSz, + byte** outDer, word32* outDerSz, void* heap) +{ + struct DecodedCert* dCert = NULL; +#ifndef WOLFSSH_SMALL_STACK + struct DecodedCert dCert_s; +#endif + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; + int ret = 0; + + if (certDer == NULL || certDerSz == 0 || outDer == NULL || + outDerSz == NULL) { + return WS_BAD_ARGUMENT; + } + +#ifndef WOLFSSH_SMALL_STACK + dCert = &dCert_s; +#else + dCert = (struct DecodedCert*)WMALLOC(sizeof(struct DecodedCert), + heap, DYNTYPE_CERT); + if (dCert == NULL) { + return WS_MEMORY_E; + } +#endif + + 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 == WC_NO_ERR_TRACE(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); +#ifdef WOLFSSH_SMALL_STACK + WFREE(dCert, heap, DYNTYPE_CERT); +#endif + + if (ret == 0) { + *outDer = pubKeyDer; + *outDerSz = pubKeyDerSz; + ret = WS_SUCCESS; + } + else { + if (pubKeyDer != NULL) + WFREE(pubKeyDer, heap, DYNTYPE_PUBKEY); + /* Keep wolfCrypt codes out of the wolfSSH error space. */ + if (ret != WS_MEMORY_E) + ret = WS_CRYPTO_FAILED; + } + + 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; +} + + +/* Resolve the cert-store slot to sign a client user-auth request with. The + * slot must match the key type of the public key algorithm keyId AND hold + * the exact certificate being offered, so a credential the application + * supplied itself is never silently signed with a store key, and several + * slots sharing a base key type do not shadow one another. Returns NULL + * when the request is not a cert-store request, in which case the caller + * falls back to the in-memory key. */ +static const WOLFSSH_PVT_KEY* FindCertStoreAuthKey(const WOLFSSH_CTX* ctx, + byte keyId, const byte* cert, word32 certSz) +{ + const WOLFSSH_PVT_KEY* pvtKey; + byte baseId; + word32 i; + + if (ctx == NULL || cert == NULL || certSz == 0) { + return NULL; + } + + baseId = CertStoreBaseKeyId(keyId); + for (i = 0; i < ctx->privateKeyCount && i < WOLFSSH_MAX_PVT_KEYS; i++) { + pvtKey = &ctx->privateKey[i]; + if (IsCertStoreKey(pvtKey) && + CertStoreBaseKeyId(pvtKey->publicKeyFmt) == baseId && + pvtKey->cert != NULL && pvtKey->certSz == certSz && + WMEMCMP(pvtKey->cert, cert, certSz) == 0) { + return pvtKey; + } + } + + return NULL; +} +#endif /* WOLFSSH_CERTS */ + + +#ifndef WOLFSSH_NO_ECDSA +/* Field size in bytes of the curve behind an ECDSA key id, 0 when the id + * is not an ECDSA type. */ +static word32 CertStoreCurveSzForId(byte id) +{ + switch (CertStoreBaseKeyId(id)) { + case ID_ECDSA_SHA2_NISTP256: + return 32; + case ID_ECDSA_SHA2_NISTP384: + return 48; + case ID_ECDSA_SHA2_NISTP521: + return 66; + } + return 0; +} + + +/* Convert an ECDSA signature from NCryptSignHash, which is raw r||s with + * each component exactly the curve field size (not DER), into separate + * minimal mpint components with leading zeros trimmed. curveSz is the + * expected field size; a blob of any other length (e.g. a DER SEQUENCE + * from a misbehaving KSP) is rejected rather than split blindly. 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, word32 curveSz, + 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 (curveSz == 0 || sigSz != curveSz * 2) { + WLOG(WS_LOG_DEBUG, "CertStoreEccSigToRs: Signature size does not " + "match the curve"); + ret = WS_ECC_E; + } + if (ret == WS_SUCCESS) { + halfSz = curveSz; + 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. */ + WOLFSSH_UNUSED(ssh); + WOLFSSH_UNUSED(hashId); + + if (!IsCertStoreKey(pvtKey)) { + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: Not a cert store key"); + return WS_BAD_ARGUMENT; + } + if (sig == NULL || sigSz == NULL || *sigSz == 0) { + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: Bad signature buffer"); + 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)) { + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: Failed to acquire NCRYPT " + "private key, error: %lu", (unsigned long)GetLastError()); + 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%08lx", (unsigned long)nCryptRet); + ret = WS_CRYPTO_FAILED; + } else { + *sigSz = dwSigLen; + ret = WS_SUCCESS; + } + } + } + + /* Free the key handle if we acquired it. Only NCRYPT keys are acquired + * above; the CSP release is kept for the flags changing. */ + if (fCallerFreeProv) { + if (dwKeySpec == CERT_NCRYPT_KEY_SPEC) { + NCryptFreeObject(hCryptProv); + } + else { + CryptReleaseContext(hCryptProv, 0); + } + } + + WLOG(WS_LOG_DEBUG, "Leaving SignWithCertStoreKey(), ret = %d", ret); + return ret; +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + + static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, struct wolfSSH_sigKeyBlockFull *sigKey) #ifndef WOLFSSH_NO_RSA @@ -14420,6 +15067,24 @@ static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, if (ret == WS_SUCCESS) { WLOG(WS_LOG_INFO, "Signing hash with %s.", IdToName(ssh->handshake->pubKeyId)); + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* A slot is never both cert-store and TPM backed; testing the + * cert-store first matches SendKexGetSigningKey()'s dispatch. */ + if (IsCertStoreKey(sigKey->pvtKey)) { + /* Use cert store signing abstraction */ + ret = SignWithCertStoreKey(ssh, sigKey->pvtKey, encSig, encSigSz, + hashId, sig, sigSz); + if (ret == WS_SUCCESS && *sigSz == 0) { + WLOG(WS_LOG_DEBUG, "SignHRsa: Cert store sign gave no " + "signature"); + ret = WS_RSA_E; + } + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "SignHRsa: Cert store sign failed"); + } + } + else + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #ifdef WOLFSSH_TPM if (ssh->handshake->useTpm && ssh->ctx->tpmDev != NULL && ssh->ctx->tpmKey != NULL) { @@ -14427,23 +15092,31 @@ 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 + /* The self-check below is skipped for the TPM, so a zero-length + * signature would otherwise be emitted in the KEXDH_REPLY. */ + if (ret == 0 && *sigSz > 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; - } - else { - *sigSz = (word32)ret; - ret = WS_SUCCESS; + { + /* 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; + } } } @@ -14452,6 +15125,9 @@ static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, && !ssh->handshake->useTpm #endif ) { + /* For a cert store key the RsaKey holds only the public half + * decoded from the certificate by SendKexGetSigningKey(), which + * is all the self-verify needs. */ ret = wolfSSH_RsaVerify(sig, *sigSz, encSig, encSigSz, &sigKey->sk.rsa.key, heap, "SignHRsa"); } @@ -14525,20 +15201,47 @@ 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, the server does not self-verify + * this signature; CertStoreEccSigToRs() below validates the + * blob length against the curve, and only the peer performs a + * cryptographic 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; + } } } @@ -14579,10 +15282,21 @@ 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 the curve + * size), NOT DER-encoded. Split directly. */ + if (IsCertStoreKey(sigKey->pvtKey)) { + ret = CertStoreEccSigToRs(sig, *sigSz, + CertStoreCurveSzForId(sigKey->pvtKey->publicKeyFmt), + 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; + } } } @@ -16820,6 +17534,14 @@ static int PrepareUserAuthRequestRsaCert(WOLFSSH* ssh, word32* payloadSz, if (ret == WS_SUCCESS) { word32 idx = 0; +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Note: already inside #ifdef WOLFSSH_CERTS */ + const WOLFSSH_PVT_KEY* pvtKey; + + pvtKey = FindCertStoreAuthKey(ssh->ctx, keySig->keyId, + authData->sf.publicKey.publicKey, + authData->sf.publicKey.publicKeySz); +#endif #ifdef WOLFSSH_AGENT if (ssh->agentEnabled) ret = wc_RsaPublicKeyDecode(authData->sf.publicKey.publicKey, @@ -16827,9 +17549,38 @@ static int PrepareUserAuthRequestRsaCert(WOLFSSH* ssh, word32* payloadSz, authData->sf.publicKey.publicKeySz); else #endif /* WOLFSSH_AGENT */ +#ifdef WOLFSSH_WINDOWS_CERT_STORE + if (pvtKey != NULL) { + /* Cert store: decode public key from the stored certificate */ + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; + + ret = ExtractPubKeyDerFromCert(pvtKey->cert, pvtKey->certSz, + &pubKeyDer, &pubKeyDerSz, ssh->ctx->heap); + if (ret == WS_SUCCESS) { + 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 */ + if (authData->sf.publicKey.privateKey == NULL || + authData->sf.publicKey.privateKeySz == 0) { + /* A cert-store-only client has no in-memory key; a decode of + * the empty buffer would report a misleading wolfCrypt ASN + * error. */ + WLOG(WS_LOG_DEBUG, "PrepareUserAuthRequestRsaCert: No private " + "key; the offered certificate matched no cert-store slot"); + ret = WS_BAD_ARGUMENT; + } + else { ret = wc_RsaPrivateKeyDecode(authData->sf.publicKey.privateKey, &idx, &keySig->ks.rsa.key, authData->sf.publicKey.privateKeySz); + } } if (ret == WS_SUCCESS) { @@ -16953,18 +17704,61 @@ static int BuildUserAuthRequestRsaCert(WOLFSSH* ssh, } if (ret == WS_SUCCESS) { int sigSz; +#ifdef WOLFSSH_WINDOWS_CERT_STORE + const WOLFSSH_PVT_KEY* pvtKey; +#endif + 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; +#ifdef WOLFSSH_WINDOWS_CERT_STORE + pvtKey = FindCertStoreAuthKey(ssh->ctx, keySig->keyId, + authData->sf.publicKey.publicKey, + authData->sf.publicKey.publicKeySz); + if (pvtKey != NULL) { + /* Cert store: sign with NCryptSignHash via + * SignWithCertStoreKey (pszAlgId=NULL, data is + * the already-encoded DigestInfo). */ + word32 outSigSz; + + 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 { - ret = wolfSSH_RsaVerify(output + begin, keySig->sigSz, - encDigest, encDigestSz, &keySig->ks.rsa.key, - ssh->ctx->heap, "SUAR"); + 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"); + } } } @@ -17289,29 +18083,70 @@ 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 */ + const WOLFSSH_PVT_KEY* pvtKey; + + pvtKey = FindCertStoreAuthKey(ssh->ctx, keySig->keyId, + authData->sf.publicKey.publicKey, + authData->sf.publicKey.publicKeySz); + /* Cert store: no in-memory private key, decode the public key from + * the DER certificate that UsePrivateKey_fromStore saved. */ + if (pvtKey != NULL) { + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; + + ret = ExtractPubKeyDerFromCert(pvtKey->cert, pvtKey->certSz, + &pubKeyDer, &pubKeyDerSz, ssh->ctx->heap); + if (ret == WS_SUCCESS) { + 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); + if (authData->sf.publicKey.privateKey == NULL || + authData->sf.publicKey.privateKeySz == 0) { + /* A cert-store-only client has no in-memory key; a + * decode of the empty buffer would report a misleading + * wolfCrypt ASN error. */ + WLOG(WS_LOG_DEBUG, "PrepareUserAuthRequestEccCert: No " + "private key; the offered certificate matched no " + "cert-store slot"); + ret = WS_BAD_ARGUMENT; + } + else { + ret = wc_EccPrivateKeyDecode( + authData->sf.publicKey.privateKey, + &idx, &keySig->ks.ecc.key, + authData->sf.publicKey.privateKeySz); + } + } } if (ret == WS_SUCCESS) { @@ -17405,6 +18240,13 @@ static int BuildUserAuthRequestEccCert(WOLFSSH* ssh, #endif #endif { +#ifdef WOLFSSH_WINDOWS_CERT_STORE + const WOLFSSH_PVT_KEY* pvtKey; + + pvtKey = FindCertStoreAuthKey(ssh->ctx, keySig->keyId, + authData->sf.publicKey.publicKey, + authData->sf.publicKey.publicKeySz); +#endif if (ret == WS_SUCCESS) { WLOG(WS_LOG_INFO, "Signing hash with ECDSA cert."); ret = wc_HashInit(&hash, hashId); @@ -17412,22 +18254,58 @@ static int BuildUserAuthRequestEccCert(WOLFSSH* ssh, 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); + wc_HashFree(&hash, hashId); + } + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "SUAR: Bad ECC Cert Hash"); + ret = WS_ECC_E; + } + } + +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Cert store signing: NCryptSignHash returns raw r||s */ + if (pvtKey != NULL) { + if (ret == WS_SUCCESS) { + ret = SignWithCertStoreKey(ssh, pvtKey, + digest, digestSz, hashId, sig, &sigSz); + if (ret == WS_SUCCESS) { + /* NCryptSignHash ECDSA output is raw r||s, each + * component is the curve field size. */ + rSz = sSz = (word32)sizeof(rs) / 2; + r = rs; + s = rs + rSz; + ret = CertStoreEccSigToRs(sig, sigSz, + CertStoreCurveSzForId(pvtKey->publicKeyFmt), + 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 +#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 44d17b816..301453820 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -35,6 +35,31 @@ #include #include +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include + #include + #include + #include + #include + /* Fallbacks for SDKs that predate these wincrypt.h definitions. The + * values must match wincrypt.h exactly. */ + #ifndef CERT_NCRYPT_KEY_SPEC + #define CERT_NCRYPT_KEY_SPEC 0xFFFFFFFF + #endif + #ifndef CERT_SYSTEM_STORE_LOCATION_MASK + #define CERT_SYSTEM_STORE_LOCATION_MASK 0x00FF0000 + #endif + #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_SYSTEM_STORE_USERS + #define CERT_SYSTEM_STORE_USERS 0x00060000 + #endif +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + #ifdef NO_INLINE #include #else @@ -2829,6 +2854,604 @@ int wolfSSH_CTX_AddRootCert_buffer(WOLFSSH_CTX* ctx, return ret; } +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Returns 1 when the certificate's private key can actually be used for + * signing. CERT_KEY_PROV_INFO_PROP_ID is not enough: it is also set for + * legacy CryptoAPI/CSP keys, which CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG + * rejects. Use the same acquisition the signing path performs so a + * candidate that cannot sign is not chosen. */ +static int CertKeyCanSign(PCCERT_CONTEXT pCertContext) +{ + HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hKey = 0; + DWORD dwKeySpec = 0; + BOOL fCallerFree = FALSE; + + if (!CryptAcquireCertificatePrivateKey(pCertContext, + CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG | CRYPT_ACQUIRE_SILENT_FLAG, + NULL, &hKey, &dwKeySpec, &fCallerFree)) { + return 0; + } + + if (fCallerFree) { + if (dwKeySpec == CERT_NCRYPT_KEY_SPEC) { + NCryptFreeObject(hKey); + } + else { + CryptReleaseContext(hKey, 0); + } + } + + return 1; +} + + +/* Find the certificate in hStore whose Common Name 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 in full so + * that a lookup for "server1" does not select "server1.example" or + * "myserver1". The compare is case insensitive, matching both the + * pre-filter and X.500 name semantics. Candidates are ranked by how + * usable they are: time-valid with a usable key first, then any candidate + * with a usable key. A candidate with no usable key is never selected -- + * the caller repeats the same key acquisition and would only fail with a + * misleading error -- so a public-only duplicate neither ends the search + * nor is returned. The selected certificate is stored in out, and is NULL + * when no usable match exists. The caller frees it with + * CertFreeCertificateContext. + * Returns WS_SUCCESS on success. */ +static int FindCertByExactCN(void* heap, HCERTSTORE hStore, + const wchar_t* subjectName, PCCERT_CONTEXT* out) +{ + PCCERT_CONTEXT pCertContext; + PCCERT_CONTEXT keyedMatch; + const wchar_t* cn; + wchar_t* certCn; + DWORD certCnSz; + int match; + int hasKey; + int timeValidity; + int keyedEarly; + int keylessSeen; + int ret; + + *out = NULL; + ret = WS_SUCCESS; + keyedEarly = 0; + keylessSeen = 0; + + /* Strip an optional "CN=" prefix from the requested name. */ + cn = subjectName; + if (wcslen(cn) >= 3 && _wcsnicmp(cn, L"CN=", 3) == 0) { + cn = cn + 3; + } + if (*cn == L'\0') { + WLOG(WS_LOG_ERROR, "FindCertByExactCN: Empty common name requested"); + return WS_BAD_ARGUMENT; + } + + pCertContext = NULL; + keyedMatch = 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, NULL, 0); + if (certCnSz <= 1) { + continue; + } + certCn = (wchar_t*)WMALLOC(certCnSz * sizeof(wchar_t), heap, + DYNTYPE_TEMP); + if (certCn == NULL) { + CertFreeCertificateContext(pCertContext); + pCertContext = NULL; + ret = WS_MEMORY_E; + break; + } + certCnSz = CertGetNameStringW(pCertContext, CERT_NAME_ATTR_TYPE, 0, + (void*)szOID_COMMON_NAME, certCn, certCnSz); + match = (certCnSz > 1 && _wcsicmp(certCn, cn) == 0); + WFREE(certCn, heap, DYNTYPE_TEMP); + if (!match) { + continue; + } + + /* A duplicate that cannot sign must not end the search. + * CertVerifyTimeValidity returns -1 before the validity period and + * +1 after it. */ + hasKey = CertKeyCanSign(pCertContext); + timeValidity = CertVerifyTimeValidity(NULL, pCertContext->pCertInfo); + if (hasKey && timeValidity == 0) { + break; + } + + if (hasKey) { + if (keyedMatch == NULL) { + keyedMatch = CertDuplicateCertificateContext(pCertContext); + keyedEarly = (timeValidity < 0); + if (keyedMatch == NULL) { + ret = WS_MEMORY_E; + } + } + } + else { + keylessSeen = 1; + } + + if (ret != WS_SUCCESS) { + CertFreeCertificateContext(pCertContext); + pCertContext = NULL; + break; + } + } + + /* An allocation failure is reported as such rather than falling back + * to a candidate the enumeration had already rejected. */ + if (ret == WS_SUCCESS && pCertContext == NULL) { + if (keyedMatch != NULL) { + WLOG(WS_LOG_WARN, "FindCertByExactCN: No time-valid match, using " + "a %s '%ls' that has a private key", + keyedEarly ? "not yet valid" : "expired", subjectName); + pCertContext = keyedMatch; + keyedMatch = NULL; + } + else if (keylessSeen) { + WLOG(WS_LOG_ERROR, "FindCertByExactCN: '%ls' matched only " + "certificates with no usable private key", subjectName); + } + } + if (keyedMatch != NULL) { + CertFreeCertificateContext(keyedMatch); + } + + if (ret != WS_SUCCESS) { + if (pCertContext != NULL) { + CertFreeCertificateContext(pCertContext); + } + WLOG(WS_LOG_ERROR, "FindCertByExactCN: Failed, ret = %d", ret); + } + else { + *out = pCertContext; + } + + return ret; +} + + +/* Resources for one cert-store backed private key slot. Everything is + * allocated before any slot is modified so that registering the plain key + * type and the matching X.509 type is all or nothing. */ +typedef struct CertStoreSlot { + PCCERT_CONTEXT context; + byte* cert; + word32 certSz; + word32 keyIdx; + byte keyId; +} CertStoreSlot; + + +/* Index of the slot holding keyId, WOLFSSH_MAX_PVT_KEYS when not found. */ +static word32 FindKeySlot(WOLFSSH_CTX* ctx, byte keyId) +{ + word32 i; + word32 keyIdx; + + keyIdx = WOLFSSH_MAX_PVT_KEYS; + for (i = 0; i < ctx->privateKeyCount && i < WOLFSSH_MAX_PVT_KEYS; i++) { + if (ctx->privateKey[i].publicKeyFmt == keyId) { + keyIdx = i; + break; + } + } + + return keyIdx; +} + + +/* Release resources of a slot that was prepared but never committed. */ +static void FreeCertStoreSlot(void* heap, CertStoreSlot* slot) +{ + if (slot->context != NULL) { + CertFreeCertificateContext(slot->context); + } + if (slot->cert != NULL) { + WFREE(slot->cert, heap, DYNTYPE_CERT); + } + WMEMSET(slot, 0, sizeof(*slot)); +} + + +/* Allocate the resources slot keyIdx needs, without modifying the + * context. The slot takes its own reference on pCertContext and its own + * copy of the certificate DER so that every slot can be freed + * independently by CtxResourceFree. + * Returns WS_SUCCESS on success. */ +static int PrepCertStoreSlot(void* heap, byte keyId, word32 keyIdx, + PCCERT_CONTEXT pCertContext, CertStoreSlot* slot) +{ + /* A zero-length certificate would be committed to the slot and later + * advertised as an x509v3 host key with an empty K_S. */ + if (pCertContext->pbCertEncoded == NULL + || pCertContext->cbCertEncoded == 0) { + WLOG(WS_LOG_ERROR, "PrepCertStoreSlot: Store certificate is empty"); + return WS_BAD_ARGUMENT; + } + + WMEMSET(slot, 0, sizeof(*slot)); + slot->keyId = keyId; + slot->keyIdx = keyIdx; + slot->certSz = pCertContext->cbCertEncoded; + + slot->cert = (byte*)WMALLOC(slot->certSz, heap, DYNTYPE_CERT); + slot->context = CertDuplicateCertificateContext(pCertContext); + if (slot->cert == NULL || slot->context == NULL) { + FreeCertStoreSlot(heap, slot); + WLOG(WS_LOG_ERROR, "PrepCertStoreSlot: Memory allocation failed"); + return WS_MEMORY_E; + } + WMEMCPY(slot->cert, pCertContext->pbCertEncoded, slot->certSz); + + return WS_SUCCESS; +} + + +/* Move the prepared resources into the context. The slot may previously + * have held either a cert-store key or a file-based key/cert, so clear + * both kinds of resources. Cannot fail. */ +static void CommitCertStoreSlot(WOLFSSH_CTX* ctx, CertStoreSlot* slot) +{ + WOLFSSH_PVT_KEY* pvtKey; + void* heap; + + heap = ctx->heap; + if (slot->keyIdx >= WOLFSSH_MAX_PVT_KEYS) { + /* Unreachable: the caller bounds checks before preparing a slot. */ + WLOG(WS_LOG_ERROR, "CommitCertStoreSlot: Slot index out of range"); + FreeCertStoreSlot(heap, slot); + return; + } + pvtKey = &ctx->privateKey[slot->keyIdx]; + + if (pvtKey->certStoreContext != NULL) { + CertFreeCertificateContext( + (PCCERT_CONTEXT)pvtKey->certStoreContext); + } + if (pvtKey->key != NULL) { + /* The mirror-image order (a file key loaded after a store key) is + * reported the same way from SetHostPrivateKey(). */ + WLOG(WS_LOG_ERROR, "CommitCertStoreSlot: Replacing the file-based " + "host key for this algorithm with the certificate store key"); + WS_FORCEZERO(pvtKey->key, pvtKey->keySz); + WFREE(pvtKey->key, heap, DYNTYPE_PRIVKEY); + pvtKey->key = NULL; + pvtKey->keySz = 0; + } + if (pvtKey->cert != NULL) { + WFREE(pvtKey->cert, heap, DYNTYPE_CERT); + } + + pvtKey->publicKeyFmt = slot->keyId; +#ifdef WOLFSSH_TPM + /* A stale TPM mark would route signing through the TPM. */ + pvtKey->isTpm = 0; +#endif + pvtKey->useCertStore = 1; + pvtKey->certStoreContext = (void*)slot->context; + pvtKey->cert = slot->cert; + pvtKey->certSz = slot->certSz; + + /* Ownership moved to the context. */ + WMEMSET(slot, 0, sizeof(*slot)); +} + + +#ifndef WOLFSSH_NO_ECDSA +/* DER-encoded named-curve OIDs as they appear in a certificate's + * SubjectPublicKeyInfo algorithm parameters. */ +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 +static const byte certStoreOidP256[] = { + 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07 +}; +#endif +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 +static const byte certStoreOidP384[] = { + 0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22 +}; +#endif +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 +static const byte certStoreOidP521[] = { + 0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x23 +}; +#endif +#endif /* WOLFSSH_NO_ECDSA */ + + +/* Load a private key from MS Certificate Store + * storeName: Certificate store name (e.g., L"My", L"Root") + * dwFlags: Certificate store location, and only a location (e.g. + * CERT_SYSTEM_STORE_CURRENT_USER). Control flags such as + * CERT_STORE_DELETE_FLAG would make CertOpenStore destructive and are + * rejected. The store is opened read-only. + * subjectName: Certificate subject Common Name for lookup, with or without + * a "CN=" prefix. The CN must match in full, case insensitively; + * thumbprint lookup is not currently implemented. + * The key is registered both as its plain key type and, mirroring the + * file-based HostKey plus HostCertificate pairing, as the matching + * RFC6187 x509v3-* type so the store certificate itself can be sent as + * the public host key to peers that negotiate certificate algorithms. + * The x509v3-* registration is skipped when the build has no such + * algorithm for the key type, e.g. RSA with SHA-1 disabled. + * 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; + byte keyId = ID_NONE; + byte certId = ID_NONE; + PCERT_PUBLIC_KEY_INFO pPubKeyInfo = NULL; + CertStoreSlot keySlot; + CertStoreSlot certSlot; + word32 keyIdx; + word32 certIdx; + word32 newCount; + byte haveCertSlot; + HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hKey = 0; + DWORD dwKeySpec = 0; + BOOL fCallerFree = FALSE; +#ifndef WOLFSSH_NO_ECDSA + const byte* params = NULL; + DWORD paramsSz = 0; +#endif + + 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; + } + + /* Only accept an assigned system-store location. Anything else is + * either not a location or a control flag (e.g. CERT_STORE_DELETE_FLAG) + * that would make CertOpenStore destructive. */ + if (!wolfSSH_CertStoreLocationValid(dwFlags)) { + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Store " + "flags are not a system store location"); + return WS_BAD_ARGUMENT; + } + + /* Open the certificate store. Read-only, both because nothing here + * writes to it and because a read/write open of a LOCAL_MACHINE store + * fails for a non-administrator service account. */ + hStore = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, (HCRYPTPROV_LEGACY)0, + (DWORD)dwFlags | CERT_STORE_OPEN_EXISTING_FLAG + | CERT_STORE_READONLY_FLAG, storeName); + if (hStore == NULL) { + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Failed to " + "open store, error: %lu", (unsigned long)GetLastError()); + return WS_BAD_FILE_E; + } + + /* Find the certificate by full Common Name match. */ + ret = FindCertByExactCN(ctx->heap, hStore, subjectName, &pCertContext); + if (ret != WS_SUCCESS) { + CertCloseStore(hStore, 0); + return ret; + } + 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. Only algorithms and + * curves compiled into this build are accepted; anything else leaves + * keyId as ID_NONE and is rejected below rather than registering a + * host key type that cannot be used for signing. */ + if (pPubKeyInfo->Algorithm.pszObjId != NULL) { + /* Compare OID strings (they are ASCII, not wide) */ + if (strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_RSA_RSA) == 0) { + /* An RSA slot is useless without an RSA signature algorithm to + * negotiate, so require one the way wolfSSH_CTX_UseTpmHostKey does + * rather than consuming a slot RefreshPublicKeyAlgo will not + * advertise. */ + #if !defined(WOLFSSH_NO_RSA) && \ + (!defined(WOLFSSH_NO_RSA_SHA2_256) || \ + !defined(WOLFSSH_NO_RSA_SHA2_512) || \ + (defined(WOLFSSH_NO_SHA1_SOFT_DISABLE) && \ + !defined(WOLFSSH_NO_SSH_RSA_SHA1))) + keyId = ID_SSH_RSA; + #else + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: " + "No usable RSA signature algorithm is compiled in"); + #endif + } + else if (strcmp(pPubKeyInfo->Algorithm.pszObjId, + szOID_ECC_PUBLIC_KEY) == 0) { + #ifndef WOLFSSH_NO_ECDSA + /* The algorithm parameters hold the DER-encoded named-curve + * OID; match its raw bytes to select the ECDSA key type. */ + params = pPubKeyInfo->Algorithm.Parameters.pbData; + paramsSz = pPubKeyInfo->Algorithm.Parameters.cbData; + + if (params == NULL) { + paramsSz = 0; + } + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP256 + if (paramsSz == sizeof(certStoreOidP256) && + WMEMCMP(params, certStoreOidP256, + sizeof(certStoreOidP256)) == 0) { + keyId = ID_ECDSA_SHA2_NISTP256; + } + else + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 + if (paramsSz == sizeof(certStoreOidP384) && + WMEMCMP(params, certStoreOidP384, + sizeof(certStoreOidP384)) == 0) { + keyId = ID_ECDSA_SHA2_NISTP384; + } + else + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 + if (paramsSz == sizeof(certStoreOidP521) && + WMEMCMP(params, certStoreOidP521, + sizeof(certStoreOidP521)) == 0) { + keyId = ID_ECDSA_SHA2_NISTP521; + } + else + #endif + { + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: " + "Unsupported ECC curve parameters"); + } + #else + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: " + "ECDSA is not compiled in"); + #endif /* WOLFSSH_NO_ECDSA */ + } + else { + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: " + "Unsupported key algorithm: %s", + pPubKeyInfo->Algorithm.pszObjId); + } + } + else { + WLOG(WS_LOG_ERROR, + "wolfSSH_CTX_UsePrivateKey_fromStore: No algorithm OID"); + } + + if (keyId == ID_NONE) { + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + return WS_BAD_ARGUMENT; + } + + /* Verify private key is accessible before registering the key. + * This catches permission issues early (e.g., LocalSystem service + * cannot access the private key) rather than failing later during + * SSH handshake signing. + * 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)) { + 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.", + (unsigned long)GetLastError()); + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + return WS_CRYPTO_FAILED; + } + /* Release the key handle since we just needed to verify access. + * CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG makes the CNG case the only + * reachable one; the CSP release is kept for the flags changing. */ + 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"); + + /* Register the key under its plain type so peers without RFC6187 + * support get a raw public key, and under the matching X.509 type so + * the store certificate can be sent as K_S when a peer negotiates an + * x509v3-* algorithm. Both slots are located and prepared before + * either is committed, so a failure leaves the context, including any + * host key already in these slots, exactly as it was. */ + WMEMSET(&keySlot, 0, sizeof(keySlot)); + WMEMSET(&certSlot, 0, sizeof(certSlot)); + newCount = ctx->privateKeyCount; + keyIdx = FindKeySlot(ctx, keyId); + if (keyIdx == WOLFSSH_MAX_PVT_KEYS) { + keyIdx = newCount++; + } + + /* CertTypeForId returns keyId unchanged when the build has no X509 + * equivalent; skip the X509 ID slot in that case. haveCertSlot rather + * than a certIdx sentinel, so the "skip" marker cannot be confused with + * an index a full table legitimately computes. */ + certId = CertTypeForId(keyId); + certIdx = 0; + haveCertSlot = 0; + if (certId != keyId) { + certIdx = FindKeySlot(ctx, certId); + if (certIdx == WOLFSSH_MAX_PVT_KEYS) { + certIdx = newCount++; + } + haveCertSlot = 1; + } + else { + WLOG(WS_LOG_INFO, "wolfSSH_CTX_UsePrivateKey_fromStore: No x509v3 " + "algorithm for key type %d in this build, registering the " + "plain host key only", keyId); + } + + if (newCount > WOLFSSH_MAX_PVT_KEYS) { + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Not enough " + "free key slots; a store key needs one for the plain type and " + "one for the x509v3 type"); + ret = WS_CTX_KEY_COUNT_E; + } + if (ret == WS_SUCCESS) { + ret = PrepCertStoreSlot(ctx->heap, keyId, keyIdx, pCertContext, + &keySlot); + } + if (ret == WS_SUCCESS && haveCertSlot) { + ret = PrepCertStoreSlot(ctx->heap, certId, certIdx, pCertContext, + &certSlot); + } + + if (ret == WS_SUCCESS) { + CommitCertStoreSlot(ctx, &keySlot); + if (haveCertSlot) { + CommitCertStoreSlot(ctx, &certSlot); + } + ctx->privateKeyCount = newCount; + } + else { + FreeCertStoreSlot(ctx->heap, &keySlot); + FreeCertStoreSlot(ctx->heap, &certSlot); + } + + /* Each registered slot holds its own reference on the certificate + * context for later signing operations, so release the lookup + * reference from CertFindCertificateInStore. Closing the store does + * not invalidate the slot contexts. + * Note: if the certificate is removed from the store while we hold + * these contexts, CryptAcquireCertificatePrivateKey may fail at + * signing time. */ + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + + if (ret == WS_SUCCESS) { + /* 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/tests/unit.c b/tests/unit.c index f4693ed57..93e59b63e 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -47,21 +47,56 @@ #include #include "unit.h" -/* Regression coverage for non-CA intermediate promotion. - * Needs WOLFSSH_TEST_INTERNAL (the test bodies are in that section), the cert - * manager, runtime cert generation to forge the attack cert, ECDSA (the test - * certs are ECC), and a filesystem to load the test certs. */ +/* Regression coverage for non-CA intermediate promotion. The test bodies use + * only public API, but keep WOLFSSH_TEST_INTERNAL: it limits them to the + * autotools test builds, which run with the ./keys certs the tests hard-load. + * Also needs the cert manager, runtime cert generation to forge the attack + * cert, ECDSA (the test certs are ECC), and a filesystem for the certs. */ #if defined(WOLFSSH_TEST_INTERNAL) && defined(WOLFSSH_CERTS) && \ defined(WOLFSSL_CERT_GEN) && !defined(WOLFSSH_NO_ECDSA) && \ !defined(NO_FILESYSTEM) #define WOLFSSH_TEST_CERTMAN_PROMOTE + #include + #include +#endif + +/* Cert manager tests that read a test cert off disk. A superset of the + * WOLFSSH_TEST_CERTMAN_PROMOTE conditions above. */ +#if defined(WOLFSSH_CERTS) && !defined(WOLFSSH_NO_ECDSA) && \ + !defined(NO_FILESYSTEM) + #define WOLFSSH_TEST_CERTMAN_ROOTCA /* The certman helpers use malloc/free and LONG_MAX; pull these in here so - * the tests build even when the SCP block below is not compiled. */ + * the tests build even when the SCP block below is not compiled. + * certman.h itself comes from the WOLFSSH_CERTS block below. */ #include #include - #include - #include +#endif + +/* wolfSSH_SetCertManager() is an unconditional WS_NOT_COMPILED stub before + * wolfSSL 4.6.0, which is where wolfSSL_CertManager_up_ref() landed. Skip the + * test there rather than failing on behaviour that is by design. */ +#if defined(WOLFSSH_CERTS) && (LIBWOLFSSL_VERSION_HEX >= WOLFSSL_V4_6_0) + #define WOLFSSH_TEST_SET_CERTMAN +#endif + +#ifdef WOLFSSH_CERTS #include + #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_SYSTEM_STORE_USERS + #define CERT_SYSTEM_STORE_USERS 0x00060000 + #endif #endif #ifdef WOLFSSH_SFTP @@ -9524,7 +9559,17 @@ static int test_IdentifyAsn1Key(void) return result; } -#ifdef WOLFSSH_TEST_CERTMAN_PROMOTE +#endif /* WOLFSSH_TEST_INTERNAL */ + +/* The cert manager tests below use only public API, so they are outside the + * WOLFSSH_TEST_INTERNAL section. Each carries its own feature guard; + * WOLFSSH_TEST_CERTMAN_PROMOTE still implies WOLFSSH_TEST_INTERNAL. */ + +/* Guard by the actual users -- the promote tests and the root-CA half of + * test_SetCertManager() -- to avoid -Wunused-function. */ +#if defined(WOLFSSH_TEST_CERTMAN_PROMOTE) || \ + (defined(WOLFSSH_TEST_SET_CERTMAN) && \ + defined(WOLFSSH_TEST_CERTMAN_ROOTCA)) /* Read a whole file into a freshly malloc'd buffer. Caller frees *buf. */ static int certmanLoadFile(const char* fn, byte** buf, word32* bufSz) @@ -9573,6 +9618,11 @@ static int certmanLoadFile(const char* fn, byte** buf, word32* bufSz) return 0; } +#endif /* WOLFSSH_TEST_CERTMAN_PROMOTE || + * (WOLFSSH_TEST_SET_CERTMAN && WOLFSSH_TEST_CERTMAN_ROOTCA) */ + +#ifdef WOLFSSH_TEST_CERTMAN_PROMOTE + /* Forge an end-entity cert whose issuer is the supplied cert and which is * signed with the supplied (non-CA) key. Fills der/derSz on success. */ static int certmanForgeChild(const byte* issuerCert, word32 issuerCertSz, @@ -9986,6 +10036,219 @@ static int test_CertMan_PromoteValidCaIntermediate(void) #endif /* WOLFSSH_TEST_CERTMAN_PROMOTE */ +#ifdef WOLFSSH_TEST_SET_CERTMAN +/* wolfSSH_SetCertManager imports a WOLFSSL_CERT_MANAGER by reference into + * the wolfSSH context. Test argument checking, importing the same manager + * twice, replacing an already-imported manager, and the reference counting + * on both sides of the import: the imported manager must outlive the + * reference it was created with, and must outlive the wolfSSH context. Each + * of those is checked by loading a root CA through the manager afterwards, + * so a missing up_ref shows up as a failure or as a use-after-free under + * the sanitizer builds. */ +static int test_SetCertManager(void) +{ + int result = 0; + WOLFSSH_CTX* ctx = NULL; + WOLFSSL_CERT_MANAGER* cm = NULL; + WOLFSSL_CERT_MANAGER* cm2 = NULL; +#ifdef WOLFSSH_TEST_CERTMAN_ROOTCA + byte* root = NULL; + word32 rootSz = 0; + /* Autotools builds link keys/ca-cert-ecc.der into the build tree, but + * other runners (e.g. the VS unit-test.exe) may not run from a tree with + * ./keys. The argument and reference-count checks below need no file, so + * treat a missing cert as a SKIP of the root-CA half, not a failure. */ + int haveRoot = (certmanLoadFile("./keys/ca-cert-ecc.der", &root, &rootSz) + == 0); + + if (!haveRoot) { + printf("SetCertManager: SKIP root cert checks, " + "./keys/ca-cert-ecc.der not readable\n"); + } +#endif + + if (result == 0) { + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + result = -2; + } + + if (result == 0) { + cm = wolfSSL_CertManagerNew(); + if (cm == NULL) + result = -3; + } + + /* bad arguments */ + if (result == 0 && wolfSSH_SetCertManager(NULL, cm) != WS_BAD_ARGUMENT) + result = -4; + if (result == 0 && wolfSSH_SetCertManager(ctx, NULL) != WS_BAD_ARGUMENT) + result = -5; + + /* import, then import the same manager again */ + if (result == 0 && wolfSSH_SetCertManager(ctx, cm) != WS_SUCCESS) + result = -6; + if (result == 0 && wolfSSH_SetCertManager(ctx, cm) != WS_SUCCESS) + result = -7; + + /* the context holds its own reference: dropping the creating reference + * must leave the imported manager usable */ + if (result == 0) { + wolfSSL_CertManagerFree(cm); + cm = NULL; + } +#ifdef WOLFSSH_TEST_CERTMAN_ROOTCA + if (result == 0 && haveRoot && + wolfSSH_CTX_AddRootCert_buffer(ctx, root, rootSz, + WOLFSSH_FORMAT_ASN1) != WS_SUCCESS) + result = -8; +#endif + + /* replacing the imported manager releases the reference on the old one */ + if (result == 0) { + cm2 = wolfSSL_CertManagerNew(); + if (cm2 == NULL) + result = -9; + else if (wolfSSH_SetCertManager(ctx, cm2) != WS_SUCCESS) + result = -10; + } +#ifdef WOLFSSH_TEST_CERTMAN_ROOTCA + if (result == 0 && haveRoot && + wolfSSH_CTX_AddRootCert_buffer(ctx, root, rootSz, + WOLFSSH_FORMAT_ASN1) != WS_SUCCESS) + result = -11; +#endif + + /* the caller's reference outlives the context that imported it */ + if (ctx != NULL) + wolfSSH_CTX_free(ctx); +#ifdef WOLFSSH_TEST_CERTMAN_ROOTCA + if (result == 0 && haveRoot && cm2 != NULL && + wolfSSL_CertManagerLoadCABuffer(cm2, root, rootSz, + WOLFSSL_FILETYPE_ASN1) != WOLFSSL_SUCCESS) + result = -12; +#endif + + if (cm != NULL) + wolfSSL_CertManagerFree(cm); + if (cm2 != NULL) + wolfSSL_CertManagerFree(cm2); +#ifdef WOLFSSH_TEST_CERTMAN_ROOTCA + if (root != NULL) + free(root); +#endif + + return result; +} +#endif /* WOLFSSH_TEST_SET_CERTMAN */ + +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Check one wolfSSH_ParseCertStoreSpec call against expected results. + * expRet is the expected return value; the name/flag expectations are only + * checked when expRet is WS_SUCCESS. */ +static int certStoreSpecCheck(const char* spec, int expRet, + const wchar_t* expStore, const wchar_t* expSubject, word32 expFlags) +{ + int ret; + int result = 0; + wchar_t* wStoreName = NULL; + wchar_t* wSubjectName = NULL; + word32 dwFlags = 0; + + ret = wolfSSH_ParseCertStoreSpec(spec, &wStoreName, &wSubjectName, + &dwFlags, NULL); + if (ret != expRet) { + printf("ParseCertStoreSpec(%s): ret %d, expected %d\n", + spec != NULL ? spec : "(null)", ret, expRet); + result = -1; + } + if (result == 0 && ret == WS_SUCCESS) { + if (wcscmp(wStoreName, expStore) != 0) + result = -2; + else if (wcscmp(wSubjectName, expSubject) != 0) + result = -3; + else if (dwFlags != expFlags) + result = -4; + } + /* on failure the parser must not hand back allocations */ + if (result == 0 && ret != WS_SUCCESS && + (wStoreName != NULL || wSubjectName != NULL)) { + result = -5; + } + + wolfSSH_FreeCertStoreSpec(wStoreName, wSubjectName, NULL); + + return result; +} + + +static int test_ParseCertStoreSpec(void) +{ + int result; + wchar_t* wStoreName = NULL; + wchar_t* wSubjectName = NULL; + word32 dwFlags = 0; + + /* bad arguments */ + result = certStoreSpecCheck(NULL, WS_BAD_ARGUMENT, NULL, NULL, 0); + if (result == 0 && wolfSSH_ParseCertStoreSpec("My:server", NULL, + &wSubjectName, &dwFlags, NULL) != WS_BAD_ARGUMENT) + result = -10; + if (result == 0 && wolfSSH_ParseCertStoreSpec("My:server", &wStoreName, + NULL, &dwFlags, NULL) != WS_BAD_ARGUMENT) + result = -11; + if (result == 0 && wolfSSH_ParseCertStoreSpec("My:server", &wStoreName, + &wSubjectName, NULL, NULL) != WS_BAD_ARGUMENT) + result = -12; + + /* full spec with named flag values */ + if (result == 0) + result = certStoreSpecCheck("My:server:LOCAL_MACHINE", WS_SUCCESS, + L"My", L"server", CERT_SYSTEM_STORE_LOCAL_MACHINE); + if (result == 0) + result = certStoreSpecCheck("My:server:CURRENT_USER", WS_SUCCESS, + L"My", L"server", CERT_SYSTEM_STORE_CURRENT_USER); + + /* flags default to CURRENT_USER when not given */ + if (result == 0) + result = certStoreSpecCheck("My:server", WS_SUCCESS, + L"My", L"server", CERT_SYSTEM_STORE_CURRENT_USER); + + /* numeric flags: only system-store location bits are accepted */ + if (result == 0) + result = certStoreSpecCheck("My:server:393216", WS_SUCCESS, + L"My", L"server", CERT_SYSTEM_STORE_USERS); + if (result == 0) + result = certStoreSpecCheck("My:server:12345", WS_BAD_ARGUMENT, + NULL, NULL, 0); + /* location plus a control flag (CERT_STORE_DELETE_FLAG) is rejected */ + if (result == 0) + result = certStoreSpecCheck("My:server:65552", WS_BAD_ARGUMENT, + NULL, NULL, 0); + /* an unassigned location id (0x00030000) is rejected */ + if (result == 0) + result = certStoreSpecCheck("My:server:0x00030000", WS_BAD_ARGUMENT, + NULL, NULL, 0); + + /* missing or empty fields are rejected */ + if (result == 0) + result = certStoreSpecCheck("My", WS_BAD_ARGUMENT, NULL, NULL, 0); + if (result == 0) + result = certStoreSpecCheck("My:", WS_BAD_ARGUMENT, NULL, NULL, 0); + if (result == 0) + result = certStoreSpecCheck(":server", WS_BAD_ARGUMENT, NULL, NULL, 0); + if (result == 0) + result = certStoreSpecCheck("My:server:", WS_BAD_ARGUMENT, NULL, NULL, + 0); + if (result == 0) + result = certStoreSpecCheck("", WS_BAD_ARGUMENT, NULL, NULL, 0); + + return result; +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + +#ifdef WOLFSSH_TEST_INTERNAL + /* Tests below install a custom allocator via wolfSSL_SetAllocators. The * wolfSSL_Malloc_cb / wolfSSL_Free_cb / wolfSSL_Realloc_cb typedefs gain * extra parameters when wolfSSL is built with WOLFSSL_STATIC_MEMORY or @@ -13360,6 +13623,19 @@ int wolfSSH_UnitTest(int argc, char** argv) #endif +#ifdef WOLFSSH_TEST_SET_CERTMAN + unitResult = test_SetCertManager(); + printf("SetCertManager: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif + +#ifdef WOLFSSH_WINDOWS_CERT_STORE + unitResult = test_ParseCertStoreSpec(); + printf("ParseCertStoreSpec: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif + #ifdef WOLFSSH_TEST_CERTMAN_PROMOTE unitResult = test_CertMan_NoPromoteNonCaIntermediate(); printf("CertMan_NoPromoteNonCaIntermediate: %s\n", diff --git a/wolfssh/certman.h b/wolfssh/certman.h index f80735550..2ca3b27bc 100644 --- a/wolfssh/certman.h +++ b/wolfssh/certman.h @@ -30,6 +30,13 @@ #include #include +#include /* included for WOLFSSL_CERT_MANAGER struct */ +#ifdef WOLFSSH_CERTS + #include /* included for WOLFSSH_CTX */ +#endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include +#endif #ifdef __cplusplus extern "C" { @@ -40,6 +47,19 @@ struct WOLFSSH_CERTMAN; typedef struct WOLFSSH_CERTMAN WOLFSSH_CERTMAN; +#ifdef WOLFSSH_CERTS +/* Replaces the CTX's cert manager with cm, taking a reference on it and + * applying wolfSSH's revocation policy. The caller retains ownership, but + * note the policy is applied to the shared object: in an HAVE_OCSP build + * this enables WOLFSSL_OCSP_CHECKALL on cm, so a caller that keeps using + * the same manager for TLS will find every chain requiring an OCSP + * response. Returns WS_NOT_COMPILED for any arguments when built against + * wolfSSL older than 4.6.0 (wolfSSL_CertManager_up_ref() is unavailable + * there). */ +WOLFSSH_API +int wolfSSH_SetCertManager(WOLFSSH_CTX* ctx, WOLFSSL_CERT_MANAGER* cm); +#endif /* WOLFSSH_CERTS */ + WOLFSSH_API WOLFSSH_CERTMAN* wolfSSH_CERTMAN_new(void* heap); @@ -55,6 +75,28 @@ int wolfSSH_CERTMAN_VerifyCerts_buffer(WOLFSSH_CERTMAN* cm, const unsigned char* cert, word32 certSz, word32 certCount); +#if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE) +/* Splits "store:subject[:flags]", where flags is CURRENT_USER, + * LOCAL_MACHINE, USERS, or a decimal or 0x hex CERT_SYSTEM_STORE_* location, + * and defaults to CURRENT_USER. The spec is split at the first two ':', so + * neither the store name nor the subject may contain one and a third ':' is + * rejected. Returns WS_SUCCESS and gives the caller ownership of the two + * allocated wide strings, which must be released with + * wolfSSH_FreeCertStoreSpec() using the same heap. On failure both + * out-pointers are set to NULL and dwFlags is untouched. */ +WOLFSSH_API +int wolfSSH_ParseCertStoreSpec(const char* spec, + wchar_t** wStoreName, wchar_t** wSubjectName, + word32* dwFlags, void* heap); + +/* Frees the strings returned by wolfSSH_ParseCertStoreSpec(). Either + * pointer may be NULL. */ +WOLFSSH_API +void wolfSSH_FreeCertStoreSpec(wchar_t* wStoreName, wchar_t* wSubjectName, + void* heap); +#endif /* WOLFSSH_CERTS && WOLFSSH_WINDOWS_CERT_STORE */ + + #ifdef __cplusplus } #endif diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 2967b05cc..b6bafe762 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -57,6 +57,15 @@ #include #endif /* WOLFSSH_CERTS */ +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #ifndef WOLFSSH_CERTS + #error "WOLFSSH_WINDOWS_CERT_STORE requires WOLFSSH_CERTS" + #endif + #ifndef _WIN32 + #error "WOLFSSH_WINDOWS_CERT_STORE requires a Windows (_WIN32) target" + #endif +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + #ifdef WOLFSSH_TPM #include #endif /* WOLFSSH_TPM */ @@ -106,6 +115,8 @@ extern "C" { #define WOLFSSH_NO_DH #endif +/* wolfSSL_CertManager_up_ref() was added in wolfSSL 4.6.0 */ +#define WOLFSSL_V4_6_0 0x04006000 #define WOLFSSL_V5_0_0 0x05000000 #define WOLFSSL_V5_7_0 0x05007000 #define WOLFSSL_V5_7_2 0x05007002 @@ -743,8 +754,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; + /* 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. */ +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ } WOLFSSH_PVT_KEY; +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Returns 1 when the value is exactly one assigned CERT_SYSTEM_STORE_* + * location with no control flags set. Defined in certman.c. */ +WOLFSSH_LOCAL int wolfSSH_CertStoreLocationValid(word32 dwFlags); +#endif + /* our wolfSSH Context */ struct WOLFSSH_CTX { @@ -1264,6 +1288,7 @@ WOLFSSH_LOCAL int ChannelPutData(WOLFSSH_CHANNEL* channel, byte* data, word32 dataSz); WOLFSSH_LOCAL int ChannelCreditWindow(WOLFSSH* ssh, WOLFSSH_CHANNEL* channel, word32 amount); +WOLFSSH_LOCAL void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx); WOLFSSH_LOCAL int wolfSSH_ProcessBuffer(WOLFSSH_CTX* ctx, const byte* in, word32 inSz, int format, int type); @@ -1435,6 +1460,9 @@ WOLFSSH_LOCAL int GenerateKey(byte hashId, byte keyId, byte* key, WOLFSSH_LOCAL int wcPrimeForId(byte id); #endif WOLFSSH_LOCAL enum wc_HashType HashForId(byte id); +#ifdef WOLFSSH_CERTS +WOLFSSH_LOCAL byte CertTypeForId(byte id); +#endif enum AcceptStates { diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 57e8f2452..e8b71bedc 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 @@ -494,6 +499,27 @@ 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 + /* Use the certificate with Common Name subjectName, and its private + * key, from the storeName system certificate store as the host key. + * subjectName may carry a "CN=" prefix and matches in full, case + * insensitively. dwFlags selects the store location and must hold + * only CERT_SYSTEM_STORE_* location bits, e.g. + * CERT_SYSTEM_STORE_CURRENT_USER; control flags such as + * CERT_STORE_DELETE_FLAG are rejected with WS_BAD_ARGUMENT. The store + * is opened read-only. When no time-valid certificate with a usable + * private key matches, an expired or not-yet-valid one with a usable + * key is still selected with only a logged warning, so a renewal whose + * key is not yet readable can fall back to the previous certificate. + * Returns WS_SUCCESS on success, WS_BAD_FILE_E when the store cannot + * be opened, WS_CRYPTO_FAILED when the private key is inaccessible, + * WS_CTX_KEY_COUNT_E when two key slots are not free, and + * WS_FATAL_ERROR when no certificate matches; on any failure the + * context is left unchanged. */ + 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 bfadb703d..a1dbf28e1 100644 --- a/wolfssh/test.h +++ b/wolfssh/test.h @@ -1148,6 +1148,10 @@ static INLINE void build_addr_ipv6(struct sockaddr_in6* addr, const char* peer, #ifdef WOLFSSH_TEST_HEX2BIN +/* Use the local fallback whenever wolfSSL will not supply Base16_Decode: + * coding.c compiles to nothing under NO_CODING even with WOLFSSL_BASE16. */ +#if !defined(WOLFSSL_BASE16) || defined(NO_CODING) + #define BAD 0xFF static const byte hexDecode[] = @@ -1220,6 +1224,11 @@ static int Base16_Decode(const byte* in, word32 inLen, return 0; } +#undef BAD + +#else + #include +#endif /* !WOLFSSL_BASE16 || NO_CODING */ static void FreeBins(byte* b1, byte* b2, byte* b3, byte* b4) {