fix(core): derive only the signing key for ECC Sign without AEAD

Part 6 6.8.1 excludes the encrypting key and IV from L for ECC policies
without AEAD in SecurityMode Sign. We derived the full length, so Sign
with nistP256/384 and brainpool failed with other stacks. Adds a test.
This commit is contained in:
Andreas Ebner
2026-10-07 14:56:43 +02:00
committed by Andreas Ebner
parent bf8d24e0cf
commit 1e4eff47c8
2 changed files with 87 additions and 8 deletions
+23 -8
View File
@@ -92,6 +92,21 @@ captureIKMSlot(UA_SecureChannel *channel, const UA_ByteString *combined) {
return UA_ByteString_copy(&ikmSlot, &channel->currentIKM);
}
/* OPC UA Part 6, 6.8.1 Step 1: "When not using AuthenticatedEncryption with
* Sign only, the EncryptionKeyLength and InitializationVectorLength are set to
* 0 in the calculation of L." L is part of the HKDF salt and the length of the
* derived key material, so only the signing key is derived for the ECC
* policies without AEAD in the SecurityMode Sign. The encrypting key and IV
* are not used in this mode and stay zeroed. */
static size_t
derivedKeyMaterialLength(const UA_SecureChannel *channel, size_t signKL,
size_t totalLength) {
if(channel->securityPolicy->policyType == UA_SECURITYPOLICYTYPE_ECC &&
channel->securityMode == UA_MESSAGESECURITYMODE_SIGN)
return signKL;
return totalLength;
}
UA_StatusCode
UA_SecureChannel_generateLocalKeys(UA_SecureChannel *channel) {
const UA_SecurityPolicy *sp = channel->securityPolicy;
@@ -117,9 +132,9 @@ UA_SecureChannel_generateLocalKeys(UA_SecureChannel *channel) {
UA_ByteString localSigningKey = {signKL, buf.data};
UA_ByteString localEncryptingKey = {encrKL, &buf.data[signKL]};
UA_ByteString localIv = {ivLen, &buf.data[signKL + encrKL]};
/* TODO: Signal that no ECC salt is generated. Find a clean solution for this. */
buf.data[0] = 0x00;
memset(buf.data, 0, buf.length);
UA_ByteString derived = {
derivedKeyMaterialLength(channel, signKL, buf.length), buf.data};
/* Build the IKM-prefixed `secret` input (the helper detects
* the prepend via the length mismatch against the local
@@ -141,7 +156,7 @@ UA_SecureChannel_generateLocalKeys(UA_SecureChannel *channel) {
* both derive against the same previous accumulator, so the
* advance to IKM_n happens exactly once, in generateRemoteKeys
* (the last of the two passes). */
res = sp->generateKey(sp, cc, &secretInput, &seedInput, &buf);
res = sp->generateKey(sp, cc, &secretInput, &seedInput, &derived);
UA_CHECK_STATUS(res, goto error);
/* Set the channel context */
@@ -185,9 +200,9 @@ generateRemoteKeys(UA_SecureChannel *channel) {
UA_ByteString remoteSigningKey = {signKL, buf.data};
UA_ByteString remoteEncryptingKey = {encrKL, &buf.data[signKL]};
UA_ByteString remoteIv = {ivLen, &buf.data[signKL + encrKL]};
/* TODO: Signal that no ECC salt is generated. Find a clean solution for this. */
buf.data[0] = 0x00;
memset(buf.data, 0, buf.length);
UA_ByteString derived = {
derivedKeyMaterialLength(channel, signKL, buf.length), buf.data};
/* Build the IKM-prefixed `secret` input. Both the local- and
* remote-keys passes of one OPN derive against the *same* previous
@@ -204,7 +219,7 @@ generateRemoteKeys(UA_SecureChannel *channel) {
/* Generate key. The policy's wrapper XORs the previous accumulator
* with the new shared secret and writes the result (IKM_n) back
* into secretCombined. */
res = sp->generateKey(sp, cc, &secretInput, &seedInput, &buf);
res = sp->generateKey(sp, cc, &secretInput, &seedInput, &derived);
UA_CHECK_STATUS(res, goto error);
/* This is the last derivation of the OPN: promote the just-updated
+64
View File
@@ -92,6 +92,62 @@ teardown_key_sizes(void) {
memset(&keySizes, 0, sizeof(struct key_sizes));
}
/* OPC UA Part 6, 6.8.1: for ECC policies without AuthenticatedEncryption in
* the SecurityMode Sign, the encrypting key and IV are not part of L (the
* length of the derived key material, also part of the HKDF salt). The
* generateKey call of the SecurityPolicy gets L as the output length. */
static size_t derivedLength;
static UA_StatusCode
(*testingGenerateKey)(const UA_SecurityPolicy *policy, void *channelContext,
const UA_ByteString *secret, const UA_ByteString *seed,
UA_ByteString *out);
static UA_StatusCode
recordGenerateKey(const UA_SecurityPolicy *policy, void *channelContext,
const UA_ByteString *secret, const UA_ByteString *seed,
UA_ByteString *out) {
derivedLength = out->length;
return testingGenerateKey(policy, channelContext, secret, seed, out);
}
static const struct {
UA_SecurityPolicyType policyType;
UA_MessageSecurityMode securityMode;
size_t derivedLength;
} keyLengthCases[] = {
/* Only the signing key */
{UA_SECURITYPOLICYTYPE_ECC, UA_MESSAGESECURITYMODE_SIGN,
DEFAULT_SYM_SIGNING_KEY_LENGTH},
/* Signing key, encrypting key and IV (the block size) */
{UA_SECURITYPOLICYTYPE_ECC, UA_MESSAGESECURITYMODE_SIGNANDENCRYPT,
DEFAULT_SYM_SIGNING_KEY_LENGTH + DEFAULT_SYM_ENCRYPTION_KEY_LENGTH +
DEFAULT_SYM_ENCRYPTION_BLOCK_SIZE},
{UA_SECURITYPOLICYTYPE_ECC_AEAD, UA_MESSAGESECURITYMODE_SIGN,
DEFAULT_SYM_SIGNING_KEY_LENGTH + DEFAULT_SYM_ENCRYPTION_KEY_LENGTH +
DEFAULT_SYM_ENCRYPTION_BLOCK_SIZE},
{UA_SECURITYPOLICYTYPE_RSA, UA_MESSAGESECURITYMODE_SIGN,
DEFAULT_SYM_SIGNING_KEY_LENGTH + DEFAULT_SYM_ENCRYPTION_KEY_LENGTH +
DEFAULT_SYM_ENCRYPTION_BLOCK_SIZE}
};
START_TEST(SecureChannel_derivedKeyLength) {
dummyPolicy.policyType = keyLengthCases[_i].policyType;
testChannel.securityMode = keyLengthCases[_i].securityMode;
testingGenerateKey = dummyPolicy.generateKey;
dummyPolicy.generateKey = recordGenerateKey;
derivedLength = 0;
ck_assert_uint_eq(UA_SecureChannel_generateLocalKeys(&testChannel),
UA_STATUSCODE_GOOD);
ck_assert_uint_eq(derivedLength, keyLengthCases[_i].derivedLength);
derivedLength = 0;
ck_assert_uint_eq(generateRemoteKeys(&testChannel), UA_STATUSCODE_GOOD);
ck_assert_uint_eq(derivedLength, keyLengthCases[_i].derivedLength);
dummyPolicy.generateKey = testingGenerateKey;
} END_TEST
START_TEST(SecureChannel_initAndDelete) {
TestingPolicy(&dummyPolicy, dummyCertificate, &fCalled, &keySizes);
UA_StatusCode retval;
@@ -1032,6 +1088,14 @@ testSuite_SecureChannel(void) {
tcase_add_test(tc_initAndDelete, SecureChannel_initAndDelete);
suite_add_tcase(s, tc_initAndDelete);
TCase *tc_keys = tcase_create("Derived key length");
tcase_add_checked_fixture(tc_keys, setup_funcs_called, teardown_funcs_called);
tcase_add_checked_fixture(tc_keys, setup_key_sizes, teardown_key_sizes);
tcase_add_checked_fixture(tc_keys, setup_secureChannel, teardown_secureChannel);
tcase_add_loop_test(tc_keys, SecureChannel_derivedKeyLength, 0,
sizeof(keyLengthCases) / sizeof(keyLengthCases[0]));
suite_add_tcase(s, tc_keys);
TCase *tc_open = tcase_create("OpenSecureChannel security modes");
tcase_add_checked_fixture(tc_open, setup_funcs_called, teardown_funcs_called);
tcase_add_checked_fixture(tc_open, setup_key_sizes, teardown_key_sizes);