fix(opcua-client): send UserName passwords in the legacy RSA format so 65-char API keys log in to SDK 1.5 servers (#880)
SDK 2.0.1's UserNameIdentityTokenHandler encrypts a password longer than 64 bytes
(a private RsaEncryptedSecretPasswordThreshold, no setting) as an RsaEncryptedSecret
with EncryptionAlgorithm=null. SDK 1.5.378 (.106 and .182) decrypts only the legacy
format under RSA policies; its SecurityPolicies.Decrypt answers a null algorithm with
BadIdentityTokenInvalid ("Unexpected encryption algorithm") before any key check.
Every production API key is 65 chars, so no SDK 2 OtOpcUa client could use one
against QA (b8b4c18f), a rollback target or an older third-party server.
LegacyRsaUserNameIdentityTokenHandler (Driver.OpcUaClient.Session, plugged in through
the public UserIdentity(IUserIdentityTokenHandler) ctor) always uses the legacy
format, RSA-OAEP(len|password|serverNonce) under the server certificate, under an RSA
policy. That is what SDK 2 sends for passwords of 64 bytes or less and what SDK 1.5
clients always send. The legacy format has no size limit (the plaintext is
length-framed over as many RSA blocks as it needs), and an SDK 2 server reads both
formats. Policy None, the ECC policies and a missing certificate go to the SDK
handler unchanged.
OpcUaUserIdentityFactory.UserName is now the one UserName identity the driver, the
AdminUI browse and Client.Shared (CLI/UI) present. Client.Shared references .Session
for it.
Tests: handler unit tests (all three RSA policies, the 1.5-required algorithm URI,
SDK 2 server-side decrypt, the session Copy() keeps the type, None unchanged, and an
SDK control pinning the 64/65-byte switch); a Client.Shared identity test; and an
SDK2<->SDK2 integration test where a 65-char key is admitted in both formats and a
wrong 65-char key reaches the key check (BadIdentityTokenRejected). The #800 Windows
gate's SDK 2 CLI 65-char case now gates against the 1.5 server too.
Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
@@ -39,8 +39,9 @@ param(
|
||||
[int]$OpcUaPort = 14841,
|
||||
[int]$AkkaPort = 18801,
|
||||
[switch]$AllowNotReady,
|
||||
# The server under test is the SDK 1.5 known-good build. An SDK 2 client sends a password over 64 bytes (every
|
||||
# production 65-character API key) as RsaEncryptedSecret, which that server rejects; see README.md.
|
||||
# The server under test is the SDK 1.5 known-good build. The SDK 2 probe's own UserName handler sends a password over
|
||||
# 64 bytes (every production 65-character API key) as RsaEncryptedSecret, which that server rejects; the production
|
||||
# SDK 2 CLI forces the legacy format since lmxopcua#880 and must be admitted. See README.md.
|
||||
[switch]$BaselineServer,
|
||||
[switch]$PatchedIsolation
|
||||
)
|
||||
@@ -393,15 +394,14 @@ switch ($Phase) {
|
||||
Invoke-Cli 'cli-sdk1-encrypt-key-65' $LegacyCliExe $s.ApiKey 'encrypt'
|
||||
Invoke-Cli 'cli-sdk2-encrypt-key-54' $CliExe $s.ShortKey 'encrypt'
|
||||
)
|
||||
$cli65 = Invoke-Cli 'cli-sdk2-encrypt-key-65' $CliExe $s.ApiKey 'encrypt'
|
||||
if (-not $BaselineServer) { $clis += $cli65 }
|
||||
# Findings recorded but not gating here (pre-existing; see README.md): the advertised endpoint set, a session
|
||||
# over the None endpoint, and the SDK 2 CLI's 65-character key against the SDK 1.5 server.
|
||||
# lmxopcua#880: gating against both servers. A CLI built before the fix fails this against -BaselineServer.
|
||||
$clis += Invoke-Cli 'cli-sdk2-encrypt-key-65' $CliExe $s.ApiKey 'encrypt'
|
||||
# Findings recorded but not gating here (pre-existing; see README.md): the advertised endpoint set and a session
|
||||
# over the None endpoint.
|
||||
$findings = @(
|
||||
Invoke-Probe 'endpoints-advertised' @('endpoints', $client, $Url) ''
|
||||
Invoke-Probe 'none-endpoint-valid-key-54' @('probe', $client, $Url, 'none', 'allow') $s.ShortKey
|
||||
)
|
||||
if ($BaselineServer) { $findings += $cli65 }
|
||||
$failed = @($cases | Where-Object { $_.exit -ne 0 }) + @($clis | Where-Object { $_.exit -ne 0 -or -not $_.good })
|
||||
$leaks = @(@($cases) + @($clis) + @($findings) | Where-Object { $_.secretOccurrencesInOutput -ne 0 })
|
||||
Write-Evidence "checks-$Label" ([ordered]@{ label = $Label; baselineServer = [bool]$BaselineServer; utc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
|
||||
@@ -58,9 +58,12 @@ task secret, and the evidence keeps only the redacted copies and the occurrence
|
||||
server the client does not trust must be refused with `BadCertificateUntrusted`.
|
||||
- **Credentials.** These are gating cases. The 65-character production-format key and the 54-character key must both be
|
||||
admitted. A wrong secret, an unknown key id, a disabled key and anonymous must all be refused.
|
||||
- **Against the SDK 1.5 server** (`-BaselineServer`), the SDK 2 client's 65-character key is expected to be refused
|
||||
(lmxopcua#880). An SDK 2 client sends any password over 64 bytes as `RsaEncryptedSecret`, which 1.5.378 rejects with
|
||||
`BadIdentityTokenInvalid`. The 54-character key and the SDK 1.5 CLI with the 65-character key are the positive controls.
|
||||
- **Against the SDK 1.5 server** (`-BaselineServer`), the probe's 65-character key is expected to be refused. The probe
|
||||
uses the SDK's own UserName handler, and SDK 2.0.1 sends any password over 64 bytes as `RsaEncryptedSecret`, which
|
||||
1.5.378 rejects with `BadIdentityTokenInvalid` (lmxopcua#880). That makes the probe the SDK-default negative control.
|
||||
The production SDK 2 CLI forces the legacy format since #880, so `cli-sdk2-encrypt-key-65` gates against both servers.
|
||||
A CLI built before the fix fails it against the baseline; the 2026-11-12 evidence predates the fix. The 54-character
|
||||
key and the SDK 1.5 CLI with the 65-character key remain the positive controls.
|
||||
- **CLIs.** The CLI that matches each SDK line reads `ServerStatus` over SignAndEncrypt with an API key. The CLIs auto-accept
|
||||
the server certificate by design, so these reads do not count as strict-peer coverage.
|
||||
- **ScriptWorker isolation.** `IsolationInstall`/`IsolationUninstall` run `Install-ScriptWorkerIsolation.ps1`. The script
|
||||
|
||||
Reference in New Issue
Block a user