From 516541b5299c7060c279268927d17fc256c39f3e Mon Sep 17 00:00:00 2001 From: David Kohout Date: Wed, 8 Apr 2026 16:16:33 +0200 Subject: [PATCH 1/7] Fix KDF ceil issue --- development/src/main/java/gurux/dlms/secure/GXSecure.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/development/src/main/java/gurux/dlms/secure/GXSecure.java b/development/src/main/java/gurux/dlms/secure/GXSecure.java index 100a407..63377ff 100644 --- a/development/src/main/java/gurux/dlms/secure/GXSecure.java +++ b/development/src/main/java/gurux/dlms/secure/GXSecure.java @@ -408,11 +408,15 @@ public static byte[] generateKDF(final String hashAlg, final byte[] z, final int */ private static byte[] generateKDF(final String hashAlg, final byte[] z, final int keyDataLen, final byte[] otherInfo) { - byte[] key = new byte[keyDataLen / 8]; + byte[] key = new byte[keyDataLen / 8]; // AES-256 needs 64bytes => 256 / 8 = 64 try { MessageDigest md = MessageDigest.getInstance(hashAlg); int hashLen = md.getDigestLength(); - int cnt = key.length / hashLen; + int cnt = (key.length + hashLen - 1) / hashLen; // It needs to ceil the number, not basic round. + + //Alternative with ceil: + //int cnt = (int) Math.ceil((double) key.length / hashLen); + byte[] v = new byte[4]; for (int pos = 1; pos <= cnt; pos++) { md.reset(); From 5da73fa7f26d3a39f802fd3d3916db7fb4e92226 Mon Sep 17 00:00:00 2001 From: David Kohout Date: Wed, 8 Apr 2026 16:23:01 +0200 Subject: [PATCH 2/7] Fix variable length of Ephemeral signature --- .../main/java/gurux/dlms/secure/GXSecure.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/development/src/main/java/gurux/dlms/secure/GXSecure.java b/development/src/main/java/gurux/dlms/secure/GXSecure.java index 63377ff..6b05204 100644 --- a/development/src/main/java/gurux/dlms/secure/GXSecure.java +++ b/development/src/main/java/gurux/dlms/secure/GXSecure.java @@ -45,6 +45,7 @@ import java.security.Signature; import java.security.SignatureException; import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; import java.security.spec.ECParameterSpec; import java.util.ArrayList; import java.util.List; @@ -489,6 +490,7 @@ public static byte[] getEphemeralPublicKeySignature(final int keyId, final Publi ECParameterSpec params = ecKey.getParams(); int fieldSize = params.getCurve().getField().getFieldSize(); + int keySize = fieldSize / 8; byte[] epk = getEphemeralPublicKeyData(keyId, ephemeralKey); @@ -506,7 +508,7 @@ public static byte[] getEphemeralPublicKeySignature(final int keyId, final Publi byte[] sign = instance.sign(); GXAsn1Sequence tmp2 = (GXAsn1Sequence) GXAsn1Converter.fromByteArray(sign); LOGGER.log(Level.FINEST, "{0}", GXCommon.toHex(sign)); - GXByteBuffer data = new GXByteBuffer(64); + GXByteBuffer data = new GXByteBuffer(2 * keySize); // Truncate to 64 bytes. Remove zeros from the begin. byte[] arr = ((GXAsn1Integer) tmp2.get(0)).getByteArray(); if (arr.length < 32) { @@ -515,9 +517,9 @@ public static byte[] getEphemeralPublicKeySignature(final int keyId, final Publi bb.set(arr); arr = bb.array(); } - data.set(arr, arr.length - 32, 32); + data.set(arr, arr.length - keySize, keySize); arr = ((GXAsn1Integer) tmp2.get(1)).getByteArray(); - data.set(arr, arr.length - 32, 32); + data.set(arr, arr.length - keySize, keySize); return data.array(); } @@ -530,14 +532,28 @@ public static byte[] getEphemeralPublicKeySignature(final int keyId, final Publi */ public static boolean validateEphemeralPublicKeySignature(final byte[] data, final byte[] sign, final PublicKey publicSigningKey) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + ECPublicKey ecKey = (ECPublicKey) publicSigningKey; + ECParameterSpec params = ecKey.getParams(); + + int fieldSize = params.getCurve().getField().getFieldSize(); + int keySize = fieldSize / 8; - GXAsn1Integer a = new GXAsn1Integer(sign, 0, 32); - GXAsn1Integer b = new GXAsn1Integer(sign, 32, 32); + GXAsn1Integer a = new GXAsn1Integer(sign, 0, keySize); + GXAsn1Integer b = new GXAsn1Integer(sign, keySize, keySize); GXAsn1Sequence s = new GXAsn1Sequence(); s.add(a); s.add(b); byte[] tmp = GXAsn1Converter.toByteArray(s); - Signature instance = Signature.getInstance("SHA256withECDSA"); + + Signature instance; + if (fieldSize == 256) { + instance = Signature.getInstance("SHA256withECDSA"); + } else if (fieldSize == 384) { + instance = Signature.getInstance("SHA384withECDSA"); + } else { + throw new IllegalArgumentException("Not an ECDSA key"); + } + instance.initVerify(publicSigningKey); instance.update(data); boolean v = instance.verify(tmp); From 3c3dab4496941b02894213877151359e93610817 Mon Sep 17 00:00:00 2001 From: David Kohout Date: Wed, 8 Apr 2026 16:26:45 +0200 Subject: [PATCH 3/7] Fix keyagreement size signature in security setup --- .../java/gurux/dlms/objects/GXDLMSSecuritySetup.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java b/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java index 3172f1c..35c5c37 100644 --- a/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java +++ b/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java @@ -41,6 +41,8 @@ import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.SignatureException; +import java.security.interfaces.ECPrivateKey; +import java.security.spec.ECParameterSpec; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -535,7 +537,12 @@ public final byte[][] keyAgreement(final GXDLMSSecureClient client, final Global GXByteBuffer bb = new GXByteBuffer(); byte[] data = GXSecure.getEphemeralPublicKeyData(type.ordinal(), client.getCiphering().getEphemeralKeyPair().getPublic()); - bb.set(data, 1, 64); + + ECPrivateKey ecKey = (ECPrivateKey) client.getCiphering().getSigningKeyPair().getPrivate(); + ECParameterSpec params = ecKey.getParams(); + int fieldSize = params.getCurve().getField().getFieldSize(); + + bb.set(data, 1, fieldSize / 4); Logger.getLogger(GXDLMSSecuritySetup.class.getName()).log(Level.INFO, "Signin public key: {0}", client.getCiphering().getSigningKeyPair().getPublic()); From 75d73992e843e93e53b13635b00daf1f4312ca28 Mon Sep 17 00:00:00 2001 From: David Kohout Date: Wed, 8 Apr 2026 16:44:46 +0200 Subject: [PATCH 4/7] Add KDF to setup parameters from security suite --- .../main/java/gurux/dlms/secure/GXSecure.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/development/src/main/java/gurux/dlms/secure/GXSecure.java b/development/src/main/java/gurux/dlms/secure/GXSecure.java index 6b05204..e81bbdd 100644 --- a/development/src/main/java/gurux/dlms/secure/GXSecure.java +++ b/development/src/main/java/gurux/dlms/secure/GXSecure.java @@ -371,6 +371,44 @@ public static byte[] generateChallenge(final Authentication authentication, fina return result; } + /** + * Use key derivation function to get the final key + * @param suite Security Suite used to set correct KDF parameters + * @param z Shared secret + * @param partyUInfo Client system title bytes + * @param partyVInfo Server system title bytes + * @param suppPubInfo Not used in DLMS. + * @param suppPrivInfo Not used in DLMS. + * @return Generated KDF. + * @throws IllegalArgumentException If invalid security suite is given. + */ + public static byte[] generateKDF(SecuritySuite suite, final byte[] z, + final byte[] partyUInfo, final byte[] partyVInfo, + final byte[] suppPubInfo, final byte[] suppPrivInfo) throws IllegalArgumentException { + byte[] algorithmID; + String hashAlg; + int keyDataLen; + + switch (suite) { + case SUITE_1: + algorithmID = GXCommon.hexToBytes("60857405080300"); // AES-GCM-128 + hashAlg = "SHA-256"; + keyDataLen = 128; + break; + + case SUITE_2: + algorithmID = GXCommon.hexToBytes("60857405080301"); // AES-GCM-256 + hashAlg = "SHA-384"; + keyDataLen = 256; + break; + + default: + throw new IllegalArgumentException("Invalid security suite."); + } + + return generateKDF(hashAlg, z, keyDataLen, algorithmID, partyUInfo, partyVInfo, suppPubInfo, suppPrivInfo); + } + /* * Generate KDF. * @param hashAlg Hash Algorithm. (SHA-256 or SHA-384 ) From f9b2874979644ee506593eb19f014035c215214c Mon Sep 17 00:00:00 2001 From: David Kohout Date: Thu, 9 Apr 2026 11:45:09 +0200 Subject: [PATCH 5/7] Add key agreement finalizeMethod --- .../dlms/objects/GXDLMSSecuritySetup.java | 147 ++++++++++++++---- 1 file changed, 118 insertions(+), 29 deletions(-) diff --git a/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java b/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java index 35c5c37..17a7c34 100644 --- a/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java +++ b/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java @@ -43,10 +43,7 @@ import java.security.SignatureException; import java.security.interfaces.ECPrivateKey; import java.security.spec.ECParameterSpec; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; @@ -56,12 +53,7 @@ import javax.crypto.NoSuchPaddingException; import javax.xml.stream.XMLStreamException; -import gurux.dlms.GXByteBuffer; -import gurux.dlms.GXDLMSClient; -import gurux.dlms.GXDLMSSettings; -import gurux.dlms.GXDLMSTranslator; -import gurux.dlms.GXSimpleEntry; -import gurux.dlms.ValueEventArgs; +import gurux.dlms.*; import gurux.dlms.asn.GXAsn1Converter; import gurux.dlms.asn.GXAsn1Sequence; import gurux.dlms.asn.GXPkcs10; @@ -541,7 +533,7 @@ public final byte[][] keyAgreement(final GXDLMSSecureClient client, final Global ECPrivateKey ecKey = (ECPrivateKey) client.getCiphering().getSigningKeyPair().getPrivate(); ECParameterSpec params = ecKey.getParams(); int fieldSize = params.getCurve().getField().getFieldSize(); - + bb.set(data, 1, fieldSize / 4); Logger.getLogger(GXDLMSSecuritySetup.class.getName()).log(Level.INFO, "Signin public key: {0}", client.getCiphering().getSigningKeyPair().getPublic()); @@ -1071,9 +1063,120 @@ private void generateKeyPair(final GXDLMSSettings settings, final ValueEventArgs } } + /** + * Client side method, to finalize the key agreement method. This will generate the keys, that should be used on the client side. + * Currently supports only 1 key agreement at the same time, but can be extended to support multiple key agreements if needed. + * @param client + * @param replyData + * @return List of agreed keys + * @throws InvalidKeyException + * @throws NoSuchAlgorithmException + * @throws SignatureException + */ + public List> finalizeKeyAgreement(GXDLMSSecureClient client, GXReplyData replyData) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException { + if(client.getCiphering().getSigningKeyPair() == null || client.getCiphering().getSigningKeyPair().getPublic() == null) { + throw new IllegalArgumentException("Invalid signing key."); + } + + if(clientSystemTitle == null || clientSystemTitle.length != 8 || + serverSystemTitle == null || serverSystemTitle.length != 8) { + throw new IllegalArgumentException("Invalid system title."); + } + + GXByteBuffer data = new GXByteBuffer(replyData.getData()); + + if(data.getUInt8() != DataType.ARRAY.getValue()) { + throw new IllegalArgumentException("Invalid tag."); + } + + int size = GXCommon.getObjectCount(data); + + if(size > 1) { + // TODO: Handle multiple ephemeral key pairs to allow support for multiple keys. + throw new IllegalArgumentException("Only one key change is currently supported by key agreement method."); + } + + List> list = new ArrayList<>(); + + for (int i = 0; i < size; i++) { + if (data.getUInt8() != DataType.STRUCTURE.getValue()) { + throw new IllegalArgumentException("Invalid tag."); + } + + if (data.getUInt8() != 2) + { + throw new IllegalArgumentException("Invalid length."); + } + + if (data.getUInt8() != DataType.ENUM.getValue()) + { + throw new IllegalArgumentException("Invalid key id data type."); + } + + int keyId = data.getUInt8(); + + if (keyId > 4) + { + throw new IllegalArgumentException("Invalid key type."); + } + + if (data.getUInt8() != DataType.OCTET_STRING.getValue()) + { + throw new IllegalArgumentException("Invalid tag."); + } + + int keySize = GXCommon.getObjectCount(data) / 4; + GXByteBuffer keyData = new GXByteBuffer(data.remaining()); + + PublicKey publicKey = GXAsn1Converter.getPublicKey(keyData.subArray(0, keySize * 2)); + byte[] signature = keyData.subArray(keySize * 2, keySize * 2); + + if(!GXSecure.validateEphemeralPublicKeySignature(GXSecure.getEphemeralPublicKeyData(keyId, publicKey), signature, client.getCiphering().getSigningKeyPair().getPublic())) { + throw new IllegalArgumentException("Invalid signature of received KeyAgreement data."); + } + + // Generate shared secret. + KeyAgreement ka = KeyAgreement.getInstance("ECDH"); + ka.init(client.getCiphering().getEphemeralKeyPair().getPrivate()); + ka.doPhase(publicKey, true); + byte[] sharedSecret = ka.generateSecret(); + + GXByteBuffer kdf = new GXByteBuffer(); + kdf.set(GXSecure.generateKDF(securitySuite, sharedSecret, + clientSystemTitle, + serverSystemTitle, + null, null)); + list.add(new AbstractMap.SimpleEntry<>(GlobalKeyType.values()[keyId], kdf.array())); + } + + updateKeys(list); + + return list; + } + + // Function to update keys in this security setup object + private void updateKeys(List> listOfKeys) { + for(Map.Entry entry : listOfKeys) { + switch (entry.getKey()) { + case BROADCAST_ENCRYPTION: + gbek = entry.getValue(); + break; + case UNICAST_ENCRYPTION: + guek = entry.getValue(); + break; + case AUTHENTICATION: + gak = entry.getValue(); + break; + case KEK: + kek = entry.getValue(); + break; + } + } + } + private byte[] invokeKeyAgreement(final GXDLMSSettings settings, final ValueEventArgs e) { try { - List tmp = (List) ((List) e.getParameters()).get(0); + List tmp = (List) ((List) e.getParameters()).get(0); //It currently allows for only 1 keyAgreement short keyId = ((Number) tmp.get(0)).shortValue(); if (keyId != 0) { e.setError(ErrorCode.READ_WRITE_DENIED); @@ -1130,23 +1233,9 @@ private byte[] invokeKeyAgreement(final GXDLMSSettings settings, final ValueEven settings.getCipher().getSystemTitle(), null, null), 0, 16); Logger.getLogger(GXDLMSSecuritySetup.class.getName()).log(Level.INFO, "GUEK: {0}", kdf); settings.getCipher().setSigning(Signing.EPHEMERAL_UNIFIED_MODEL); - switch (GlobalKeyType.values()[keyId]) { - case BROADCAST_ENCRYPTION: - gbek = kdf.array(); - break; - case UNICAST_ENCRYPTION: - guek = kdf.array(); - break; - case AUTHENTICATION: - gak = kdf.array(); - break; - case KEK: - kek = kdf.array(); - break; - default: - e.setError(ErrorCode.INCONSISTENT_CLASS); - break; - } + + updateKeys(List.of(new AbstractMap.SimpleEntry<>(GlobalKeyType.values()[keyId], kdf.array()))); + return bb.array(); } } From 9128ebeb2456dcff181cf0f7f25b6dcd517fba94 Mon Sep 17 00:00:00 2001 From: David Kohout Date: Thu, 9 Apr 2026 12:01:29 +0200 Subject: [PATCH 6/7] add support for multiple key agreements in one on client --- .../dlms/objects/GXDLMSSecuritySetup.java | 139 ++++++++++-------- 1 file changed, 80 insertions(+), 59 deletions(-) diff --git a/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java b/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java index 17a7c34..304058e 100644 --- a/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java +++ b/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java @@ -35,12 +35,7 @@ package gurux.dlms.objects; import java.math.BigInteger; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.SignatureException; +import java.security.*; import java.security.interfaces.ECPrivateKey; import java.security.spec.ECParameterSpec; import java.util.*; @@ -118,6 +113,11 @@ public class GXDLMSSecuritySetup extends GXDLMSObject implements IGXDLMSBase { */ public KeyPair tls; + /** + * Ephemeral key pairs for key agreement method + */ + private Map ephemeralKeyPairs; + /** * Available certificates of the server. */ @@ -460,11 +460,13 @@ public final byte[][] globalKeyTransfer(final GXDLMSClient client, final byte[] /** * Agree on one or more symmetric keys using the key agreement algorithm. - * + * You need to have setup signing key pair: client.getCiphering().getSigningKeyPair() + * Ephemeral keys are generated in the method itself + * * @param client * DLMS client that is used to generate action. - * @param list - * List of keys. + * @param generateKeys + * Global key types. * @return Generated action. * @throws NoSuchPaddingException * No such padding exception. @@ -481,23 +483,49 @@ public final byte[][] globalKeyTransfer(final GXDLMSClient client, final byte[] * @throws SignatureException * Signature exception. */ - public final byte[][] keyAgreement(final GXDLMSClient client, final List> list) - throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, - InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, SignatureException { - if (list == null || list.isEmpty()) { - throw new IllegalArgumentException("Invalid list. It is empty."); - } + public final byte[][] keyAgreement(final GXDLMSSecureClient client, final GlobalKeyType... generateKeys) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, NoSuchPaddingException, + InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - GXByteBuffer bb = new GXByteBuffer(); - bb.setUInt8(DataType.ARRAY.getValue()); - bb.setUInt8((byte) list.size()); - for (GXSimpleEntry it : list) { - bb.setUInt8(DataType.STRUCTURE.getValue()); - bb.setUInt8(2); - GXCommon.setData(null, bb, DataType.ENUM, it.getKey().ordinal()); - GXCommon.setData(null, bb, DataType.OCTET_STRING, it.getValue()); + List> list = new ArrayList>(); + + ephemeralKeyPairs = new HashMap(); + + for(GlobalKeyType keyType : generateKeys) { + GXByteBuffer bb = new GXByteBuffer(); + + //Generate EphemeralKey + if (securitySuite == SecuritySuite.SUITE_1) { + ephemeralKeyPairs.put(keyType, GXEcdsa.generateKeyPair(Ecc.P256)); + } else if (securitySuite == SecuritySuite.SUITE_2) { + ephemeralKeyPairs.put(keyType, GXEcdsa.generateKeyPair(Ecc.P384)); + } else { + throw new IllegalArgumentException("Invalid security suite."); + } + + byte[] data = GXSecure.getEphemeralPublicKeyData(keyType.ordinal(), + ephemeralKeyPairs.get(keyType).getPublic()); + + ECPrivateKey ecKey = (ECPrivateKey) client.getCiphering().getSigningKeyPair().getPrivate(); + ECParameterSpec params = ecKey.getParams(); + int fieldSize = params.getCurve().getField().getFieldSize(); + + bb.set(data, 1, fieldSize / 4); + Logger.getLogger(GXDLMSSecuritySetup.class.getName()).log(Level.INFO, "Signin public key: {0}", + client.getCiphering().getSigningKeyPair().getPublic()); + + // Add signature. + byte[] sign = GXSecure.getEphemeralPublicKeySignature(keyType.ordinal(), + ephemeralKeyPairs.get(keyType).getPublic(), + client.getCiphering().getSigningKeyPair().getPrivate()); + bb.set(sign); + Logger.getLogger(GXDLMSSecuritySetup.class.getName()).log(Level.FINEST, "Data: {0}", GXCommon.toHex(data)); + Logger.getLogger(GXDLMSSecuritySetup.class.getName()).log(Level.FINEST, "Sign: {0}", GXCommon.toHex(sign)); + + + list.add(new GXSimpleEntry<>(keyType, bb.array())); } - return client.method(this, 3, bb.array(), DataType.ARRAY); + return keyAgreement(client, list); } /** @@ -505,8 +533,8 @@ public final byte[][] keyAgreement(final GXDLMSClient client, final List> list) + throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, + InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, SignatureException { + if (list == null || list.isEmpty()) { + throw new IllegalArgumentException("Invalid list. It is empty."); + } - List> list = new ArrayList>(); - list.add(new GXSimpleEntry(type, bb.array())); - return keyAgreement(client, list); + GXByteBuffer bb = new GXByteBuffer(); + bb.setUInt8(DataType.ARRAY.getValue()); + bb.setUInt8((byte) list.size()); + for (GXSimpleEntry it : list) { + bb.setUInt8(DataType.STRUCTURE.getValue()); + bb.setUInt8(2); + GXCommon.setData(null, bb, DataType.ENUM, it.getKey().ordinal()); + GXCommon.setData(null, bb, DataType.OCTET_STRING, it.getValue()); + } + return client.method(this, 3, bb.array(), DataType.ARRAY); } /** @@ -1064,8 +1083,7 @@ private void generateKeyPair(final GXDLMSSettings settings, final ValueEventArgs } /** - * Client side method, to finalize the key agreement method. This will generate the keys, that should be used on the client side. - * Currently supports only 1 key agreement at the same time, but can be extended to support multiple key agreements if needed. + * Client side method, to finalize the key agreement method. This will generate the keys from server response. * @param client * @param replyData * @return List of agreed keys @@ -1091,11 +1109,6 @@ public List> finalizeKeyAgreement(GXDLMSSecureC int size = GXCommon.getObjectCount(data); - if(size > 1) { - // TODO: Handle multiple ephemeral key pairs to allow support for multiple keys. - throw new IllegalArgumentException("Only one key change is currently supported by key agreement method."); - } - List> list = new ArrayList<>(); for (int i = 0; i < size; i++) { @@ -1135,9 +1148,14 @@ public List> finalizeKeyAgreement(GXDLMSSecureC throw new IllegalArgumentException("Invalid signature of received KeyAgreement data."); } + //Verify ephemeral private key exists + if(ephemeralKeyPairs == null || !ephemeralKeyPairs.containsKey(GlobalKeyType.values()[keyId])) { + throw new IllegalArgumentException("Invalid ephemeral key pair."); + } + // Generate shared secret. KeyAgreement ka = KeyAgreement.getInstance("ECDH"); - ka.init(client.getCiphering().getEphemeralKeyPair().getPrivate()); + ka.init(ephemeralKeyPairs.remove(GlobalKeyType.values()[keyId]).getPrivate()); //Take ephemeral key from saved map ka.doPhase(publicKey, true); byte[] sharedSecret = ka.generateSecret(); @@ -1151,6 +1169,9 @@ public List> finalizeKeyAgreement(GXDLMSSecureC updateKeys(list); + //Clear key pair map + ephemeralKeyPairs = null; + return list; } From 58f127395722ebd073cf7e4dcd00a8d846fe61f2 Mon Sep 17 00:00:00 2001 From: David Kohout Date: Thu, 9 Apr 2026 13:43:36 +0200 Subject: [PATCH 7/7] add support for multiple keyagreements for server method invoke --- .../dlms/objects/GXDLMSSecuritySetup.java | 158 +++++++++++------- 1 file changed, 93 insertions(+), 65 deletions(-) diff --git a/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java b/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java index 304058e..03fb76a 100644 --- a/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java +++ b/development/src/main/java/gurux/dlms/objects/GXDLMSSecuritySetup.java @@ -62,7 +62,6 @@ import gurux.dlms.enums.ErrorCode; import gurux.dlms.enums.ObjectType; import gurux.dlms.enums.Security; -import gurux.dlms.enums.Signing; import gurux.dlms.internal.GXCommon; import gurux.dlms.internal.GXDataInfo; import gurux.dlms.objects.enums.CertificateEntity; @@ -489,16 +488,18 @@ public final byte[][] keyAgreement(final GXDLMSSecureClient client, final Global List> list = new ArrayList>(); - ephemeralKeyPairs = new HashMap(); + if(ephemeralKeyPairs == null) { + ephemeralKeyPairs = new HashMap(); + } for(GlobalKeyType keyType : generateKeys) { GXByteBuffer bb = new GXByteBuffer(); - //Generate EphemeralKey + //Generate EphemeralKey if not in ephemeral map if (securitySuite == SecuritySuite.SUITE_1) { - ephemeralKeyPairs.put(keyType, GXEcdsa.generateKeyPair(Ecc.P256)); + ephemeralKeyPairs.putIfAbsent(keyType, GXEcdsa.generateKeyPair(Ecc.P256)); } else if (securitySuite == SecuritySuite.SUITE_2) { - ephemeralKeyPairs.put(keyType, GXEcdsa.generateKeyPair(Ecc.P384)); + ephemeralKeyPairs.putIfAbsent(keyType, GXEcdsa.generateKeyPair(Ecc.P384)); } else { throw new IllegalArgumentException("Invalid security suite."); } @@ -1196,70 +1197,97 @@ private void updateKeys(List> listOfKeys) { } private byte[] invokeKeyAgreement(final GXDLMSSettings settings, final ValueEventArgs e) { + List> keysToUpdate = new ArrayList<>(); + + GXByteBuffer response = new GXByteBuffer(); + try { - List tmp = (List) ((List) e.getParameters()).get(0); //It currently allows for only 1 keyAgreement - short keyId = ((Number) tmp.get(0)).shortValue(); - if (keyId != 0) { - e.setError(ErrorCode.READ_WRITE_DENIED); - } else { - byte[] data = (byte[]) tmp.get(1); - // ephemeral public key - GXByteBuffer data2 = new GXByteBuffer(65); - data2.setUInt8(keyId); - data2.set(data, 0, 64); - GXByteBuffer sign = new GXByteBuffer(); - sign.set(data, 64, 64); - PublicKey pk = settings.getCipher().getKeyAgreementKeyPair().getPublic(); - if (pk == null || !GXSecure.validateEphemeralPublicKeySignature(data2.array(), sign.array(), pk)) { - e.setError(ErrorCode.READ_WRITE_DENIED); - settings.setTargetEphemeralKey(null); + // Array, size + List> array = (List>) e.getParameters(); + + // Structure, size + for(List structure : array) { + // Enum, key id + short keyId = ((Number) structure.get(0)).shortValue(); + GlobalKeyType keyType = GlobalKeyType.values()[keyId]; + + if (keyId != 0) { + e.setError(ErrorCode.INCONSISTENT_CLASS); + return null; + } + + // Octet string, ephemeral public key + signature + GXByteBuffer keyData = new GXByteBuffer((byte[]) structure.get(1)); + int keySize = keyData.size() / 4; + + PublicKey publicKey = GXAsn1Converter.getPublicKey(keyData.subArray(0, keySize * 2)); + byte[] signature = keyData.subArray(keySize * 2, keySize * 2); + + // Verify signature of key data, using client public key (client signed it by signing certificate) + if(!GXSecure.validateEphemeralPublicKeySignature(GXSecure.getEphemeralPublicKeyData(keyId, publicKey), signature, + settings.getServerPublicKeyCertificate().getPublicKey())) { + e.setError(ErrorCode.OTHER_REASON); + return null; + } + e.setByteArray(true); //Not sure, what was it for. + + KeyPair serverEphemeralKeyPair; + + //Generate EphemeralKey if not in ephemeral map + if (securitySuite == SecuritySuite.SUITE_1) { + serverEphemeralKeyPair = GXEcdsa.generateKeyPair(Ecc.P256); + } else if (securitySuite == SecuritySuite.SUITE_2) { + serverEphemeralKeyPair = GXEcdsa.generateKeyPair(Ecc.P384); } else { - e.setByteArray(true); - settings.setTargetEphemeralKey(GXAsn1Converter.getPublicKey(data2.subArray(1, 64))); - // Generate ephemeral keys. - KeyPair eKpS = settings.getCipher().getEphemeralKeyPair(); - eKpS = GXEcdsa.generateKeyPair(Ecc.P256); - settings.getCipher().setEphemeralKeyPair(eKpS); - // Generate shared secret. - KeyAgreement ka = KeyAgreement.getInstance("ECDH"); - ka.init(eKpS.getPrivate()); - ka.doPhase(settings.getTargetEphemeralKey(), true); - byte[] sharedSecret = ka.generateSecret(); - // settings.getCipher().setSharedSecret(sharedSecret); - Logger.getLogger(GXDLMSSecuritySetup.class.getName()).log(Level.FINEST, "Server shared secret: {0}", - GXCommon.toHex(sharedSecret)); - GXByteBuffer bb = new GXByteBuffer(); - bb.setUInt8(DataType.ARRAY); - bb.setUInt8(1); - bb.setUInt8(DataType.STRUCTURE); - bb.setUInt8(2); - // Add key ID. - bb.setUInt8(0x16); - bb.setUInt8(0); - bb.setUInt8(DataType.OCTET_STRING); - GXCommon.setObjectCount(128, bb); - data = GXSecure.getEphemeralPublicKeyData(keyId, eKpS.getPublic()); - bb.set(data, 1, 64); - // Add signature. - byte[] tmp2 = GXSecure.getEphemeralPublicKeySignature(keyId, eKpS.getPublic(), - settings.getCipher().getSigningKeyPair().getPrivate()); - bb.set(tmp2); - Logger.getLogger(GXDLMSSecuritySetup.class.getName()).log(Level.FINEST, "Data: {0}", - GXCommon.toHex(data)); - Logger.getLogger(GXDLMSSecuritySetup.class.getName()).log(Level.FINEST, "Sign: {0}", - GXCommon.toHex(tmp2)); - byte[] algID = GXCommon.hexToBytes("60857405080300"); // AES-GCM-128 - GXByteBuffer kdf = new GXByteBuffer(); - kdf.set(GXSecure.generateKDF("SHA-256", sharedSecret, 256, algID, settings.getSourceSystemTitle(), - settings.getCipher().getSystemTitle(), null, null), 0, 16); - Logger.getLogger(GXDLMSSecuritySetup.class.getName()).log(Level.INFO, "GUEK: {0}", kdf); - settings.getCipher().setSigning(Signing.EPHEMERAL_UNIFIED_MODEL); - - updateKeys(List.of(new AbstractMap.SimpleEntry<>(GlobalKeyType.values()[keyId], kdf.array()))); - - return bb.array(); + e.setError(ErrorCode.OTHER_REASON); + throw new IllegalArgumentException("Invalid security suite."); } + + // Generate shared secret. + KeyAgreement ka = KeyAgreement.getInstance("ECDH"); + ka.init(serverEphemeralKeyPair.getPrivate()); + ka.doPhase(settings.getTargetEphemeralKey(), true); + byte[] sharedSecret = ka.generateSecret(); + + Logger.getLogger(GXDLMSSecuritySetup.class.getName()).log(Level.FINEST, keyType + ": Server shared secret: {0}", GXCommon.toHex(sharedSecret)); + + //Generate key from share secret + GXByteBuffer kdf = new GXByteBuffer(); + kdf.set(GXSecure.generateKDF(securitySuite, sharedSecret, + settings.getSourceSystemTitle(), settings.getCipher().getSystemTitle(), + null, null)); + + Logger.getLogger(GXDLMSSecuritySetup.class.getName()).log(Level.INFO, keyType + ": {0}", kdf); + keysToUpdate.add(new AbstractMap.SimpleEntry<>(keyType, kdf.array())); + + //Prepare key_data for client + GXByteBuffer keyDataForClient = new GXByteBuffer(); + byte[] data = GXSecure.getEphemeralPublicKeyData(keyType.ordinal(), + serverEphemeralKeyPair.getPublic()); + + ECPrivateKey signingKey = (ECPrivateKey) settings.getCipher().getSigningKeyPair().getPrivate(); + ECParameterSpec params = signingKey.getParams(); + int fieldSize = params.getCurve().getField().getFieldSize(); + + keyDataForClient.set(data, 0, fieldSize / 4); + + // Add signature. + byte[] sign = GXSecure.getEphemeralPublicKeySignature(keyType.ordinal(), + serverEphemeralKeyPair.getPublic(), + signingKey); + keyDataForClient.set(sign); + + response.setUInt8(DataType.STRUCTURE.getValue()); response.setUInt8(DataType.STRUCTURE.getValue()); + response.setUInt8(2); + GXCommon.setData(null, response, DataType.ENUM, keyType.ordinal()); + GXCommon.setData(null, response, DataType.OCTET_STRING, keyDataForClient.array()); } + + // Update keys in this security setup object + // TODO: add logic, to update keys for the server (if this security setup is for current association) + updateKeys(keysToUpdate); + + return response.array(); } catch (Exception ex) { e.setError(ErrorCode.INCONSISTENT_CLASS); }