SDK 2 clients cannot log in to SDK 1.5 OtOpcUa servers with API keys (65-char key sent as RsaEncryptedSecret -> BadIdentityTokenInvalid): rollback/QA-compat risk #880
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found by the #800 Windows service gate on windev, 2026-11-12. Both client and server ran on the same box, so clock skew is ruled out (#873 does not apply).
Rollback/QA-compat risk.
Symptom: an SDK 2.0.1 client cannot log in to an SDK 1.5.378.182 OtOpcUa server (known-good master
efb9197a) with a production-format API key. Each attempt getsBadIdentityTokenInvalidat ActivateSession, in both Sign and SignAndEncrypt (Basic256Sha256), on a channel the server trusts. This was seen with both our #800 probe (SDK 2.0.1) and the actual SDK 2 Client CLI at master438484fd.Controls (same server, same boot, same trusted client certificate):
otua_<16 hex>_<43>, theApiKeySecret.Generateform)BadIdentityTokenInvalidefb9197a)BadIdentityTokenRejected(the server's real key check)BadIdentityTokenInvalid(rejected on format before the key check)438484fd)Cause (as #873 describes): an SDK 2 client encrypts any UserName password over 64 bytes as
RsaEncryptedSecret. The 1.5.378 server does not accept that format, so the token is invalid before any key lookup. Every OtOpcUa API key is 65 characters.Impact:
b8b4c18f, SDK 1.5.378.106) and any known-good build a node is rolled back to. That covers:Repro:
scripts/validation/opcua-windows-service/(#800). Install the baseline, then run-Phase Checks -BaselineServer. The 65-char cases are recorded as expected denials, andcli-sdk2-encrypt-key-65appears under findings.Options:
ApiKeySecret; existing keys keep working.Root cause (from the decompiled SDK assemblies, confirmed against the upstream
2.0.1tag)This is not a 1.5 server bug. An SDK 2.0.1 client picks a password format that a pre-1.05.04 server cannot decode, and nothing negotiates it.
UserNameIdentityTokenHandler.EncryptAsync.RsaEncryptedSecret. The threshold isprivate const int RsaEncryptedSecretPasswordThreshold = 64, which has no setting.RsaEncryptedSecretis a hybrid ExtensionObject: an RSA-wrapped AES key and HMAC key, an AES-CBC payload, and a signing time. It is sent withEncryptionAlgorithm = null.RSA-OAEP(len ‖ password ‖ serverNonce)under the server certificate, withEncryptionAlgorithm = …xmlenc#rsa-oaep(…rsa-oaep-sha2-256for RsaPss).UserTokenPolicyadvertises only aSecurityPolicyUri, never a format, so the client has nothing to negotiate from.UserNameIdentityToken.Decrypthandles only the legacy format for any non-ECC policy. The 1.5EncryptedSecretis ECC-only.SecurityPolicies.DecryptwithAlgorithm == nullthrows0x80200000BadIdentityTokenInvalidwith the message "Unexpected encryption algorithm". That happens before the authenticator runs, which is why a wrong 65-char key also showedBadIdentityTokenInvalidrather thanRejected.RsaEncryptedSecretwhen it is null.Fix (client side):
be28aa44LegacyRsaUserNameIdentityTokenHandlerlives inDriver.OpcUaClient.Sessionand plugs in through the SDK's publicUserIdentity(IUserIdentityTokenHandler)constructor.None, the ECC policies and a missing certificate go to the SDK handler unchanged.RsaEncryptedSecret's padding of the secret's length.OpcUaUserIdentityFactory.UserName(...)is now the one UserName identity used by the OpcUaClient driver, the AdminUI browse and Client.Shared (the CLI and UI). Client.Shared now references.Session.cli-sdk2-encrypt-key-65now gates against-BaselineServer. The probe stays as the SDK-default negative control. A rerun on windev needs a CLI built from this commit or later.Proof
Cross-version matrix (
scripts/validation/opcua-sdk-compatibility, #782; harness2ad72130; evidencee5b26f37,evidence/2026-11-12-880/). Baseline83ed880e(SDK 1.5.378.182).otua_matrix_+ 43 characters, 55 in all, so it stayed under the threshold and hid this bug. It now uses the productionApiKeySecret.Generateshape: 65 characters, random per run, never recorded.production-length-API-key-wrong-secret-reaches-key-checkmust be refused withBadIdentityTokenRejected.Invalidwould mean the server could not decrypt the token.sdk2-default-RsaEncryptedSecret-control(SDK 2 clients only) is the negative control: the SDK's own handler with the valid key must be refused withBadIdentityTokenInvalidby a 1.x server and admitted by SDK 2.Rejected, SDK-default control getsInvalidBadIdentityTokenInvalid, wrong key getsInvalid(notRejected), control getsInvalidBadIdentityTokenInvalidRejectedIn-repo tests:
LegacyRsaUserNameIdentityTokenHandlerTestscovers the three RSA policies. Each must carry the algorithm URI that SDK 1.5 requires and must round-trip through the unmodified SDK 2 server decrypt. Other tests check that the sessionCopy()keeps the type, that policy None is unchanged, and an SDK control that pins the 64/65-byte switch.Sdk2TrustedChannelTests.A_production_length_API_key_is_admitted_in_either_password_format(SDK 2 to SDK 2, Sign and SignAndEncrypt) checks that both formats are admitted and that a wrong 65-char key reaches the authenticator withBadIdentityTokenRejected.Remaining constraint
SDK 2 tooling that we do not build (generic SDK 2 clients using the default handler) is still refused by SDK 1.5 servers with 65-char keys. ScadaBridge is unaffected because it is an SDK 1.5 client. Cross-ref #782 (SDK upgrade, matrix) and #801 (SDK 2 live gate: consumer compatibility and paired-cluster rollout). QA vq03/vq04 and any rollback target now accept post-fix SDK 2 clients.