From bcb242377940135b9726b7b654197d94701ea004 Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Fri, 5 Apr 2024 16:33:38 +0200 Subject: [PATCH 1/4] Add coaps+tcp support for server and client based on java-coap. --- .../security/jsse/LwM2mX509TrustManager.java | 93 ++++++++++ .../JavaCoapsTcpClientEndpointsProvider.java | 135 +++++++++++++++ .../endpoint/SSLSocketClientTransport.java | 66 ++++++++ .../javacoap/SingleX509KeyManager.java | 78 +++++++++ .../identity/DefaultTlsIdentityHandler.java | 61 +++++++ .../identity/IdentityHandlerProvider.java | 33 ++++ .../identity/TlsTransportContextKeys.java | 26 +++ .../JavaCoapTcpServerEndpointsProvider.java | 2 +- .../JavaCoapsTcpServerEndpointsProvider.java | 159 ++++++++++++++++++ .../LwM2mTransportContextMatcher.java | 74 ++++++++ .../transport/CoapsTcpTransportResolver.java | 70 ++++++++ .../DefaultTransportContextMatcher.java | 7 +- .../transport/NettyCoapTcpTransport.java | 97 +++++++---- 13 files changed, 869 insertions(+), 32 deletions(-) create mode 100644 leshan-core/src/main/java/org/eclipse/leshan/core/security/jsse/LwM2mX509TrustManager.java create mode 100644 leshan-tl-javacoap-client-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/client/coaptcp/endpoint/JavaCoapsTcpClientEndpointsProvider.java create mode 100644 leshan-tl-javacoap-client-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/client/coaptcp/endpoint/SSLSocketClientTransport.java create mode 100644 leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/SingleX509KeyManager.java create mode 100644 leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/DefaultTlsIdentityHandler.java create mode 100644 leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/IdentityHandlerProvider.java create mode 100644 leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/TlsTransportContextKeys.java create mode 100644 leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/endpoint/JavaCoapsTcpServerEndpointsProvider.java create mode 100644 leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/endpoint/LwM2mTransportContextMatcher.java create mode 100644 leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/CoapsTcpTransportResolver.java diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/security/jsse/LwM2mX509TrustManager.java b/leshan-core/src/main/java/org/eclipse/leshan/core/security/jsse/LwM2mX509TrustManager.java new file mode 100644 index 0000000000..1c19bc1dba --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/security/jsse/LwM2mX509TrustManager.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 2024 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.core.security.jsse; + +import java.net.InetSocketAddress; +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; + +import org.eclipse.leshan.core.security.certificate.util.CertPathUtil; +import org.eclipse.leshan.core.security.certificate.verifier.X509CertificateVerifier; +import org.eclipse.leshan.core.security.certificate.verifier.X509CertificateVerifier.Role; + +public class LwM2mX509TrustManager extends X509ExtendedTrustManager { + + private final X509CertificateVerifier certificateVerifier; + + public LwM2mX509TrustManager(X509CertificateVerifier certificateVerifier) { + this.certificateVerifier = certificateVerifier; + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + // TODO not clear what this is about... + return new X509Certificate[0]; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + throw new UnsupportedOperationException(); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + throw new UnsupportedOperationException(); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + certificateVerifier.verifyCertificate(CertPathUtil.generateCertPath(Arrays.asList(chain)), + getPeerAddress(socket), Role.CLIENT); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + certificateVerifier.verifyCertificate(CertPathUtil.generateCertPath(Arrays.asList(chain)), + getPeerAddress(socket), Role.SERVER); + } + + protected InetSocketAddress getPeerAddress(Socket socket) { + return (InetSocketAddress) socket.getRemoteSocketAddress(); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + certificateVerifier.verifyCertificate(CertPathUtil.generateCertPath(Arrays.asList(chain)), + getPeerAddress(engine), Role.CLIENT); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + certificateVerifier.verifyCertificate(CertPathUtil.generateCertPath(Arrays.asList(chain)), + getPeerAddress(engine), Role.SERVER); + } + + protected InetSocketAddress getPeerAddress(SSLEngine engine) { + if (engine.getPeerHost() == null) + return null; + + return new InetSocketAddress(engine.getPeerHost(), engine.getPeerPort()); + } +} diff --git a/leshan-tl-javacoap-client-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/client/coaptcp/endpoint/JavaCoapsTcpClientEndpointsProvider.java b/leshan-tl-javacoap-client-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/client/coaptcp/endpoint/JavaCoapsTcpClientEndpointsProvider.java new file mode 100644 index 0000000000..e3461c713b --- /dev/null +++ b/leshan-tl-javacoap-client-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/client/coaptcp/endpoint/JavaCoapsTcpClientEndpointsProvider.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright (c) 2024 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.client.coaptcp.endpoint; + +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; + +import org.eclipse.leshan.client.security.CertificateVerifierFactory; +import org.eclipse.leshan.client.servers.ServerInfo; +import org.eclipse.leshan.core.endpoint.Protocol; +import org.eclipse.leshan.core.security.jsse.LwM2mX509TrustManager; +import org.eclipse.leshan.transport.javacoap.SingleX509KeyManager; +import org.eclipse.leshan.transport.javacoap.client.endpoint.AbstractJavaCoapClientEndpointsProvider; +import org.eclipse.leshan.transport.javacoap.identity.DefaultTlsIdentityHandler; +import org.eclipse.leshan.transport.javacoap.identity.TlsTransportContextKeys; + +import com.mbed.coap.packet.BlockSize; +import com.mbed.coap.packet.CoapPacket; +import com.mbed.coap.packet.CoapRequest; +import com.mbed.coap.packet.CoapResponse; +import com.mbed.coap.packet.Opaque; +import com.mbed.coap.server.CoapServer; +import com.mbed.coap.server.TcpCoapServer; +import com.mbed.coap.server.filter.TokenGeneratorFilter; +import com.mbed.coap.transport.TransportContext; +import com.mbed.coap.utils.Service; + +public class JavaCoapsTcpClientEndpointsProvider extends AbstractJavaCoapClientEndpointsProvider { + + private final CertificateVerifierFactory certificateVerifierFactory = new CertificateVerifierFactory(); + + public JavaCoapsTcpClientEndpointsProvider() { + super(Protocol.COAPS_TCP, "CoAP over TLS experimental endpoint based on java-coap library", + new DefaultTlsIdentityHandler()); + } + + @Override + protected CoapServer createCoapServer(ServerInfo serverInfo, Service router, + List trustStore) { + + // Create SSL Socket Factory using right credentials. + SSLContext tlsContext; + try { + // Create context + tlsContext = SSLContext.getInstance("TLSv1.2"); + + // Configure it + X509KeyManager keys = new SingleX509KeyManager(serverInfo.privateKey, + new X509Certificate[] { (X509Certificate) serverInfo.clientCertificate }); + + X509TrustManager trustManger = new LwM2mX509TrustManager( + certificateVerifierFactory.create(serverInfo, trustStore)); + + // Initialize it + tlsContext.init(new KeyManager[] { keys }, new TrustManager[] { trustManger }, null); + } catch (KeyManagementException | NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Unable to create tls endpoint point", e); + } + + return TcpCoapServer.builder() /// + .transport(new SSLSocketClientTransport(serverInfo.getAddress(), tlsContext.getSocketFactory(), true) { + + @Override + public CompletableFuture receive() { + // HACK to define transport context + return super.receive().thenApply(packet -> { + packet.setTransportContext(getTransportContext(getSslSocket())); + return packet; + }); + }; + })// + .blockSize(BlockSize.S_1024_BERT) // + .outboundFilter(TokenGeneratorFilter.RANDOM)// + .route(router) // + .build(); + } + + private TransportContext getTransportContext(SSLSocket sslSocket) { + SSLSession sslSession = sslSocket.getSession(); + + if (sslSession == null) { + throw new IllegalStateException("Missing Session"); + } + + // Get Principal + Principal principal; + try { + principal = sslSession.getPeerPrincipal(); + } catch (SSLPeerUnverifiedException e) { + throw new IllegalStateException("Unable to get Principal", e); + } + if (principal == null) { + throw new IllegalStateException("Missing Principal"); + } + + // Get Cipher Suite + String cipherSuite = sslSession.getCipherSuite(); + if (cipherSuite == null) { + throw new IllegalStateException("Missing Cipher Suite"); + + } + + return TransportContext.of(TlsTransportContextKeys.TLS_SESSION_ID, new Opaque(sslSession.getId()).toHex()) // + .with(TlsTransportContextKeys.PRINCIPAL, principal) // + .with(TlsTransportContextKeys.CIPHER_SUITE, cipherSuite); + + } +} diff --git a/leshan-tl-javacoap-client-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/client/coaptcp/endpoint/SSLSocketClientTransport.java b/leshan-tl-javacoap-client-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/client/coaptcp/endpoint/SSLSocketClientTransport.java new file mode 100644 index 0000000000..262934ad34 --- /dev/null +++ b/leshan-tl-javacoap-client-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/client/coaptcp/endpoint/SSLSocketClientTransport.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) 2024 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.client.coaptcp.endpoint; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mbed.coap.transport.javassl.SocketClientTransport; + +public class SSLSocketClientTransport extends SocketClientTransport { + private static final Logger LOGGER = LoggerFactory.getLogger(SSLSocketClientTransport.class); + + public SSLSocketClientTransport(InetSocketAddress destination, SSLSocketFactory socketFactory, + boolean autoReconnect) { + super(destination, socketFactory, autoReconnect); + } + + @Override + protected void connect() throws IOException { + SSLSocket sslSocket = (SSLSocket) socketFactory.createSocket(destination.getAddress(), destination.getPort()); + + sslSocket.addHandshakeCompletedListener(handshakeCompletedEvent -> { + try { + LOGGER.debug("Connected [{}, {}]", handshakeCompletedEvent.getSource(), + ((X509Certificate) sslSocket.getSession().getPeerCertificates()[0]).getSubjectX500Principal()); + } catch (SSLPeerUnverifiedException e) { + LOGGER.warn(e.getMessage(), e); + } + listener.onConnected((InetSocketAddress) socket.getRemoteSocketAddress()); + }); + + this.socket = sslSocket; + + synchronized (this) { + outputStream = new BufferedOutputStream(socket.getOutputStream()); + } + inputStream = new BufferedInputStream(socket.getInputStream(), 1024); + } + + public SSLSocket getSslSocket() { + return (SSLSocket) socket; + } +} diff --git a/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/SingleX509KeyManager.java b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/SingleX509KeyManager.java new file mode 100644 index 0000000000..33c4698e76 --- /dev/null +++ b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/SingleX509KeyManager.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2024 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap; + +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import javax.net.ssl.X509KeyManager; + +public class SingleX509KeyManager implements X509KeyManager { + + private final PrivateKey privateKey; + private final X509Certificate[] certChain; + + public SingleX509KeyManager(PrivateKey privateKey, X509Certificate[] certChain) { + this.privateKey = privateKey; + this.certChain = certChain; + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return null; + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + if (Arrays.asList(keyType).contains(privateKey.getAlgorithm())) { + return getAlias(certChain); + } + return null; + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return null; + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + if (privateKey.getAlgorithm().equals(keyType)) { + return getAlias(certChain); + } + return null; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return certChain; + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return privateKey; + } + + private String getAlias(X509Certificate[] certChain) { + X509Certificate x509Certificate = certChain[0]; + String issuerName = x509Certificate.getIssuerX500Principal().getName(); + String algorithm = x509Certificate.getPublicKey().getAlgorithm(); + return algorithm + "_Certificate_" + issuerName; + } +} diff --git a/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/DefaultTlsIdentityHandler.java b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/DefaultTlsIdentityHandler.java new file mode 100644 index 0000000000..a9d151f978 --- /dev/null +++ b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/DefaultTlsIdentityHandler.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 2024 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.identity; + +import java.security.Principal; + +import javax.security.auth.x500.X500Principal; + +import org.eclipse.leshan.core.peer.IpPeer; +import org.eclipse.leshan.core.peer.LwM2mPeer; +import org.eclipse.leshan.core.peer.X509Identity; +import org.eclipse.leshan.core.security.certificate.util.X509CertUtil; + +import com.mbed.coap.packet.CoapRequest; +import com.mbed.coap.transport.TransportContext; + +public class DefaultTlsIdentityHandler extends DefaultCoapIdentityHandler { + + @Override + protected LwM2mPeer getIdentity(CoapRequest receivedRequest) { + Principal principal = receivedRequest.getTransContext().get(TlsTransportContextKeys.PRINCIPAL); + if (principal != null) { + if (principal instanceof X500Principal) { + // Extract common name + String x509CommonName = X509CertUtil.extractCN(principal.getName()); + return new IpPeer(receivedRequest.getPeerAddress(), new X509Identity(x509CommonName)); + } + throw new IllegalStateException( + String.format("Unable to extract sender identity : unexpected type of Principal %s [%s]", + principal.getClass(), principal.toString())); + } else { + return new IpPeer(receivedRequest.getPeerAddress()); + } + } + + @Override + public TransportContext createTransportContext(LwM2mPeer client, boolean allowConnectionInitiation) { + Principal peerIdentity = null; + if (client.getIdentity() instanceof X509Identity) { + /* simplify distinguished name to CN= part */ + peerIdentity = new X500Principal("CN=" + ((X509Identity) client.getIdentity()).getX509CommonName()); + return TransportContext.of(TlsTransportContextKeys.PRINCIPAL, peerIdentity); + } else { + throw new IllegalStateException(String.format("Unsupported Identity : %s", client.getIdentity())); + } + } + +} diff --git a/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/IdentityHandlerProvider.java b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/IdentityHandlerProvider.java new file mode 100644 index 0000000000..450dfb8239 --- /dev/null +++ b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/IdentityHandlerProvider.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2022 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.identity; + +public class IdentityHandlerProvider { + +// private final HashMap identityHandlers = new HashMap<>(); +// +// public void addIdentityHandler(Endpoint endpoint, IdentityHandler identityHandler) { +// identityHandlers.put(endpoint, identityHandler); +// } +// +// public void clear() { +// identityHandlers.clear(); +// } +// +// public IdentityHandler getIdentityHandler(Endpoint endpoint) { +// return identityHandlers.get(endpoint); +// } +} diff --git a/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/TlsTransportContextKeys.java b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/TlsTransportContextKeys.java new file mode 100644 index 0000000000..a4e7515f34 --- /dev/null +++ b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/TlsTransportContextKeys.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2024 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.identity; + +import java.security.Principal; + +import com.mbed.coap.transport.TransportContext; + +public class TlsTransportContextKeys { + public static final TransportContext.Key TLS_SESSION_ID = new TransportContext.Key<>(null); + public static final TransportContext.Key CIPHER_SUITE = new TransportContext.Key<>(null); + public static final TransportContext.Key PRINCIPAL = new TransportContext.Key<>(null); +} diff --git a/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/endpoint/JavaCoapTcpServerEndpointsProvider.java b/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/endpoint/JavaCoapTcpServerEndpointsProvider.java index 218935d7e4..0e2422bf09 100644 --- a/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/endpoint/JavaCoapTcpServerEndpointsProvider.java +++ b/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/endpoint/JavaCoapTcpServerEndpointsProvider.java @@ -50,7 +50,7 @@ protected CoapServer createCoapServer(InetSocketAddress localAddress, ServerSecu NotificationsReceiver notificationReceiver, ObservationsStore observationsStore) { return createCoapServer() // .transport(new NettyCoapTcpTransport(localAddress, new CoapTcpTransportResolver(), - new DefaultTransportContextMatcher())) // + new DefaultTransportContextMatcher(), null)) // .blockSize(BlockSize.S_1024_BERT) // .maxIncomingBlockTransferSize(4000) // .maxMessageSize(2100) // diff --git a/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/endpoint/JavaCoapsTcpServerEndpointsProvider.java b/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/endpoint/JavaCoapsTcpServerEndpointsProvider.java new file mode 100644 index 0000000000..d757b37ce6 --- /dev/null +++ b/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/endpoint/JavaCoapsTcpServerEndpointsProvider.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * Copyright (c) 2023 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.server.coaptcp.endpoint; + +import java.net.InetSocketAddress; +import java.security.Principal; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import javax.net.ssl.SSLException; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; +import javax.security.auth.x500.X500Principal; + +import org.eclipse.leshan.core.endpoint.Protocol; +import org.eclipse.leshan.core.security.certificate.util.X509CertUtil; +import org.eclipse.leshan.core.security.certificate.verifier.DefaultCertificateVerifier; +import org.eclipse.leshan.core.security.jsse.LwM2mX509TrustManager; +import org.eclipse.leshan.server.security.EditableSecurityStore; +import org.eclipse.leshan.server.security.SecurityInfo; +import org.eclipse.leshan.server.security.SecurityStore; +import org.eclipse.leshan.server.security.SecurityStoreListener; +import org.eclipse.leshan.server.security.ServerSecurityInfo; +import org.eclipse.leshan.transport.javacoap.SingleX509KeyManager; +import org.eclipse.leshan.transport.javacoap.identity.DefaultTlsIdentityHandler; +import org.eclipse.leshan.transport.javacoap.identity.TlsTransportContextKeys; +import org.eclipse.leshan.transport.javacoap.server.coaptcp.transport.CoapsTcpTransportResolver; +import org.eclipse.leshan.transport.javacoap.server.coaptcp.transport.NettyCoapTcpTransport; +import org.eclipse.leshan.transport.javacoap.server.coaptcp.transport.TransportContextHandler; +import org.eclipse.leshan.transport.javacoap.server.endpoint.AbstractJavaCoapServerEndpointsProvider; + +import com.mbed.coap.packet.BlockSize; +import com.mbed.coap.packet.CoapRequest; +import com.mbed.coap.packet.CoapResponse; +import com.mbed.coap.server.CoapServer; +import com.mbed.coap.server.CoapServerBuilderForTcp; +import com.mbed.coap.server.TcpCoapServer; +import com.mbed.coap.server.filter.TokenGeneratorFilter; +import com.mbed.coap.server.observe.NotificationsReceiver; +import com.mbed.coap.server.observe.ObservationsStore; +import com.mbed.coap.transport.TransportContext; +import com.mbed.coap.utils.Service; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.util.Attribute; + +public class JavaCoapsTcpServerEndpointsProvider extends AbstractJavaCoapServerEndpointsProvider { + + public JavaCoapsTcpServerEndpointsProvider(InetSocketAddress localAddress) { + super(Protocol.COAPS_TCP, "CoAP over TLS experimental endpoint based on java-coap and netty libraries", + localAddress, new DefaultTlsIdentityHandler()); + } + + @Override + protected CoapServer createCoapServer(InetSocketAddress localAddress, ServerSecurityInfo serverSecurityInfo, + SecurityStore securityStore, Service resources, + NotificationsReceiver notificationReceiver, ObservationsStore observationsStore) { + + // Create SSL Handler with right Credentials + SslContext sslContext; + try { + // Create context + X509KeyManager keys = new SingleX509KeyManager(serverSecurityInfo.getPrivateKey(), + serverSecurityInfo.getCertificateChain()); + X509TrustManager trustManger = new LwM2mX509TrustManager(new DefaultCertificateVerifier( + Arrays.asList(X509CertUtil.asX509Certificates(serverSecurityInfo.getTrustedCertificates()))) { + @Override + protected void validateSubject(InetSocketAddress peerSocket, X509Certificate receivedServerCertificate) + throws CertificateException { + // Do not validate subject at server side. + } + }); + + sslContext = SslContextBuilder // + .forServer(keys) // + .startTls(false) // + .trustManager(trustManger) // + .protocols("TLSv1.2") // + .clientAuth(ClientAuth.REQUIRE) // + .build(); + + } catch (SSLException | CertificateException e) { + throw new IllegalStateException("Unable to create tls endpoint point", e); + } + + if (sslContext == null) { + throw new IllegalStateException("Unable to create tls endpoint point : sslcontext must not be null"); + } + + NettyCoapTcpTransport transport = new NettyCoapTcpTransport(localAddress, new CoapsTcpTransportResolver(), + new LwM2mTransportContextMatcher(), sslContext); + + createAndAttachConnectionCleaner(transport, securityStore); + + return createCoapServer() // + .transport(transport) // + .blockSize(BlockSize.S_1024_BERT) // + .maxIncomingBlockTransferSize(4000) // + .maxMessageSize(2100) // + .route(resources) // + .notificationsReceiver(notificationReceiver) // + .observationsStore(observationsStore) // + .build(); + } + + protected CoapServerBuilderForTcp createCoapServer() { + return TcpCoapServer.builder().outboundFilter(TokenGeneratorFilter.RANDOM); + } + + protected void createAndAttachConnectionCleaner(NettyCoapTcpTransport transport, SecurityStore securityStore) { + if (securityStore instanceof EditableSecurityStore) { + ((EditableSecurityStore) securityStore).addListener(new SecurityStoreListener() { + + @Override + public void securityInfoRemoved(boolean infosAreCompromised, SecurityInfo... infos) { + + transport.closeConnections(channel -> { + Attribute attr = channel.attr(TransportContextHandler.TRANSPORT_CONTEXT_ATTR); + if (attr != null) { + Principal principal = attr.get().get(TlsTransportContextKeys.PRINCIPAL); + if (principal != null) { + for (SecurityInfo info : infos) { + if (info != null) { + // x509 + if (info.useX509Cert() && principal instanceof X500Principal) { + // Extract common name + String x509CommonName = X509CertUtil.extractCN(principal.getName()); + if (x509CommonName.equals(info.getEndpoint())) { + return true; + } + } + } + } + } + } + + return false; + }); + } + }); + } + } +} diff --git a/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/endpoint/LwM2mTransportContextMatcher.java b/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/endpoint/LwM2mTransportContextMatcher.java new file mode 100644 index 0000000000..ea14139a41 --- /dev/null +++ b/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/endpoint/LwM2mTransportContextMatcher.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) 2024 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.server.coaptcp.endpoint; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.security.auth.x500.X500Principal; + +import org.eclipse.leshan.transport.javacoap.identity.TlsTransportContextKeys; +import org.eclipse.leshan.transport.javacoap.server.coaptcp.transport.DefaultTransportContextMatcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mbed.coap.transport.TransportContext.Key; + +public class LwM2mTransportContextMatcher extends DefaultTransportContextMatcher { + + private final Logger LOG = LoggerFactory.getLogger(LwM2mTransportContextMatcher.class); + + @Override + protected boolean matches(Key key, Object packetValue, Object channelValue) { + if (key.equals(TlsTransportContextKeys.PRINCIPAL)) { + if (packetValue instanceof X500Principal || channelValue instanceof X500Principal) { + try { + String requestedCommonName = extractCN(((X500Principal) packetValue).getName()); + String availableCommonName = extractCN(((X500Principal) channelValue).getName()); + return requestedCommonName.equals(availableCommonName); + } catch (IllegalStateException e) { + LOG.debug("Unable to extract CN from certificate {} or {}", packetValue, channelValue); + return false; + } + } else { + LOG.debug("Unsupported kind of principal {} or {}", packetValue.getClass().getSimpleName(), + channelValue.getClass().getSimpleName()); + return false; + } + } else { + return super.matches(key, packetValue, channelValue); + } + + } + + /** + * Extract "common name" from "distinguished name". + * + * @param dn The distinguished name. + * @return The extracted common name. + * @throws IllegalStateException if no CN is contained in DN. + */ + public static String extractCN(String dn) { + // Extract common name + Matcher endpointMatcher = Pattern.compile("CN=(.*?)(,|$)").matcher(dn); + if (endpointMatcher.find()) { + return endpointMatcher.group(1); + } else { + throw new IllegalStateException( + "Unable to extract sender identity : can not get common name in certificate"); + } + } +} diff --git a/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/CoapsTcpTransportResolver.java b/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/CoapsTcpTransportResolver.java new file mode 100644 index 0000000000..bbaa8bea57 --- /dev/null +++ b/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/CoapsTcpTransportResolver.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2024 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.html. + * + * Contributors: + * Sierra Wireless - initial API and implementation + *******************************************************************************/ +package org.eclipse.leshan.transport.javacoap.server.coaptcp.transport; + +import java.security.Principal; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; + +import org.eclipse.leshan.transport.javacoap.identity.TlsTransportContextKeys; + +import com.mbed.coap.packet.Opaque; +import com.mbed.coap.transport.TransportContext; + +import io.netty.channel.Channel; +import io.netty.handler.ssl.SslHandler; + +public class CoapsTcpTransportResolver extends CoapTcpTransportResolver { + + @Override + public TransportContext apply(Channel channel) { + // Get Session + SslHandler sslHandler = channel.pipeline().get(SslHandler.class); + if (sslHandler == null) { + throw new IllegalStateException("Missing SslHandler"); + } + SSLEngine sslEngine = sslHandler.engine(); + SSLSession sslSession = sslEngine.getSession(); + if (sslSession == null) { + throw new IllegalStateException("Missing Session"); + } + + // Get Principal + Principal principal; + try { + principal = sslSession.getPeerPrincipal(); + } catch (SSLPeerUnverifiedException e) { + throw new IllegalStateException("Unable to get Principal", e); + } + if (principal == null) { + throw new IllegalStateException("Missing Principal"); + } + + // Get Cipher Suite + String cipherSuite = sslSession.getCipherSuite(); + if (cipherSuite == null) { + throw new IllegalStateException("Missing Cipher Suite"); + + } + + return super.apply(channel) // + .with(TlsTransportContextKeys.TLS_SESSION_ID, new Opaque(sslSession.getId()).toHex()) // + .with(TlsTransportContextKeys.PRINCIPAL, principal) // + .with(TlsTransportContextKeys.CIPHER_SUITE, cipherSuite); + } +} diff --git a/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/DefaultTransportContextMatcher.java b/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/DefaultTransportContextMatcher.java index 7f5f72b1c3..34eb715e7b 100644 --- a/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/DefaultTransportContextMatcher.java +++ b/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/DefaultTransportContextMatcher.java @@ -17,6 +17,8 @@ import java.util.function.BiFunction; +import org.eclipse.leshan.transport.javacoap.identity.TlsTransportContextKeys; + import com.mbed.coap.transport.TransportContext; import com.mbed.coap.transport.TransportContext.Key; @@ -27,7 +29,10 @@ public class DefaultTransportContextMatcher implements BiFunction... knownKeys) { diff --git a/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/NettyCoapTcpTransport.java b/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/NettyCoapTcpTransport.java index 5703519ad7..ef79859f74 100644 --- a/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/NettyCoapTcpTransport.java +++ b/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/NettyCoapTcpTransport.java @@ -25,6 +25,12 @@ import java.util.concurrent.ConcurrentMap; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.function.Predicate; + +import javax.net.ssl.SSLHandshakeException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.mbed.coap.packet.CoapPacket; import com.mbed.coap.transport.CoapTcpListener; @@ -42,23 +48,31 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.ssl.SslHandshakeCompletionEvent; import io.netty.handler.timeout.IdleStateEvent; import io.netty.handler.timeout.IdleStateHandler; public class NettyCoapTcpTransport implements CoapTcpTransport { + private static final Logger LOGGER = LoggerFactory.getLogger(NettyCoapTcpTransport.class); + private final InetSocketAddress localAddress; private volatile Channel mainChannel; private final ConcurrentMap activeChannels = new ConcurrentHashMap<>(); private volatile CoapTcpListener listener; private CompletableFuture receivePromise = new CompletableFuture<>(); + private final SslContext sslContext; private final Function contextResolver; private final BiFunction contextMatcher; public NettyCoapTcpTransport(InetSocketAddress localadddress, // Function contextResolver, // - BiFunction contextMatcher) { + BiFunction contextMatcher, // + SslContext sslContext) { this.localAddress = localadddress; + this.sslContext = sslContext; this.contextResolver = contextResolver; this.contextMatcher = contextMatcher; } @@ -92,7 +106,25 @@ protected void initChannel(SocketChannel ch) throws Exception { // 3. Stream-to-message decoder // 4. Hand-off decoded messages to CoAP stack // 5. Close connections on errors. - + if (sslContext != null) { + ch.pipeline().addFirst(sslContext.newHandler(ch.alloc())); + ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + // Trigger channel active on TLS handshake Complete + if (evt instanceof SslHandshakeCompletionEvent) { + if (((SslHandshakeCompletionEvent) evt).isSuccess()) { + super.channelActive(ctx); + } + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + // block default channel active event. + } + }); + } ch.pipeline().addLast(new TransportContextHandler(contextResolver)); ch.pipeline().addLast(new ChannelTracker()); // Remove IdleStateHandler for now because, we could define expected behavoir @@ -107,31 +139,6 @@ protected void initChannel(SocketChannel ch) throws Exception { } } -// public static class TransportContextHandler extends ChannelInboundHandlerAdapter { -// -// public static final AttributeKey TRANSPORT_CONTEXT_ATTR = AttributeKey -// .newInstance("transport"); -// -// @Override -// public void channelActive(ChannelHandlerContext ctx) throws Exception { -// // create context -// TransportContext tansportContext = createTransportContext(); -// if (tansportContext == null) { -// throw new IllegalStateException("transport context must not be null"); -// } -// -// // add it to the channel -// TransportContext oldTansportContext = ctx.channel().attr(TRANSPORT_CONTEXT_ATTR) -// .setIfAbsent(tansportContext); -// if (oldTansportContext != null) { -// throw new IllegalStateException( -// String.format("Can not create new transport context %s as %s already exists.", tansportContext, -// oldTansportContext)); -// } -// super.channelActive(ctx); -// } -// } - class ChannelTracker extends ChannelInboundHandlerAdapter { @Override @@ -142,7 +149,13 @@ public void channelActive(ChannelHandlerContext ctx) throws Exception { @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { - activeChannels.remove(ctx.channel().remoteAddress()); + if (ctx.channel().remoteAddress() != null) { + activeChannels.remove(ctx.channel().remoteAddress()); + } else { + // it seems that sometime remoteAddress is null, I don't know why ... + LOGGER.warn("Channel Remote Address is null"); + activeChannels.values().remove(ctx.channel()); + } super.channelInactive(ctx); } } @@ -193,8 +206,18 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc private static class CloseOnErrorHandler extends ChannelInboundHandlerAdapter { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - cause.printStackTrace(); - ctx.close(); + if (!checkAndHandleIfExpectedError(ctx, cause)) { + LOGGER.warn("Unexpected Error :", cause); + ctx.close(); + } + } + + private boolean checkAndHandleIfExpectedError(ChannelHandlerContext ctx, Throwable error) { + if (error instanceof SSLHandshakeException || error.getCause() instanceof SSLHandshakeException) { + LOGGER.debug("Handshake Failed with {} peer:", ctx.channel().remoteAddress(), error); + return true; + } + return false; } } @@ -220,6 +243,20 @@ public CompletableFuture sendPacket(CoapPacket packet) { return toCompletableFuture(channelPromise).thenApply(__ -> true); } + public void closeConnections(Predicate filter) { + for (Channel channel : activeChannels.values()) { + if (filter.test(channel)) { + SslHandler sslHandler = channel.pipeline().get(SslHandler.class); + + // Invalidate TLS session to not allow to resume + sslHandler.engine().getSession().invalidate(); + + // Close TLS connection + sslHandler.closeOutbound(); + } + } + } + @Override public CompletableFuture receive() { receivePromise = new CompletableFuture<>(); From 9c97f5037df9d73b5672c6c818d57068b92742ae Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Thu, 18 Apr 2024 17:59:54 +0200 Subject: [PATCH 2/4] Add Integrations tests for coaps+tcp. --- .../eclipse/leshan/integration/tests/security/X509Test.java | 5 +++-- .../integration/tests/util/LeshanTestClientBuilder.java | 3 +++ .../integration/tests/util/LeshanTestServerBuilder.java | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/security/X509Test.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/security/X509Test.java index 2da222eab6..aa8ebf0d8b 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/security/X509Test.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/security/X509Test.java @@ -79,7 +79,8 @@ public class X509Test { static Stream transports() { return Stream.of(// // ProtocolUsed - Client Endpoint Provider - Server Endpoint Provider - arguments(Protocol.COAPS, "Californium", "Californium")); + arguments(Protocol.COAPS, "Californium", "Californium"), + arguments(Protocol.COAPS_TCP, "java-coap", "java-coap")); } /*---------------------------------/ @@ -145,7 +146,7 @@ public void registered_device_with_x509cert_to_server_with_x509cert_then_remove_ server.getSecurityStore().remove(client.getEndpointName(), true); // try to update - Thread.sleep(100); + Thread.sleep(200); if (givenProtocol.equals(Protocol.COAPS)) { // For DTLS, Client doesn't know that connection is removed at server side. // So request will first timeout. diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestClientBuilder.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestClientBuilder.java index 5437fc975a..cbcc3ae402 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestClientBuilder.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestClientBuilder.java @@ -82,6 +82,7 @@ import org.eclipse.leshan.server.bootstrap.endpoint.LwM2mBootstrapServerEndpoint; import org.eclipse.leshan.server.endpoint.LwM2mServerEndpoint; import org.eclipse.leshan.transport.javacoap.client.coaptcp.endpoint.JavaCoapTcpClientEndpointsProvider; +import org.eclipse.leshan.transport.javacoap.client.coaptcp.endpoint.JavaCoapsTcpClientEndpointsProvider; import org.eclipse.leshan.transport.javacoap.client.endpoint.JavaCoapClientEndpointsProvider; public class LeshanTestClientBuilder extends LeshanClientBuilder { @@ -301,6 +302,8 @@ protected LwM2mClientEndpointsProvider getJavaCoapProtocolProvider(Protocol prot return new JavaCoapClientEndpointsProvider(); } else if (protocolToUse.equals(Protocol.COAP_TCP)) { return new JavaCoapTcpClientEndpointsProvider(); + } else if (protocolToUse.equals(Protocol.COAPS_TCP)) { + return new JavaCoapsTcpClientEndpointsProvider(); } throw new IllegalStateException(String.format("No Californium Protocol Provider for protocol %s", protocol)); } diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServerBuilder.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServerBuilder.java index d53bad4f52..6d358b755d 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServerBuilder.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/LeshanTestServerBuilder.java @@ -59,6 +59,7 @@ import org.eclipse.leshan.server.security.SecurityStore; import org.eclipse.leshan.server.security.ServerSecurityInfo; import org.eclipse.leshan.transport.javacoap.server.coaptcp.endpoint.JavaCoapTcpServerEndpointsProvider; +import org.eclipse.leshan.transport.javacoap.server.coaptcp.endpoint.JavaCoapsTcpServerEndpointsProvider; import org.eclipse.leshan.transport.javacoap.server.endpoint.JavaCoapServerEndpointsProvider; public class LeshanTestServerBuilder extends LeshanServerBuilder { @@ -251,6 +252,8 @@ protected LwM2mServerEndpointsProvider getJavaCoapProtocolProvider(Protocol prot return new JavaCoapServerEndpointsProvider(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); } else if (protocolToUse.equals(Protocol.COAP_TCP)) { return new JavaCoapTcpServerEndpointsProvider(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); + } else if (protocolToUse.equals(Protocol.COAPS_TCP)) { + return new JavaCoapsTcpServerEndpointsProvider(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); } throw new IllegalStateException(String.format("No Californium Protocol Provider for protocol %s", protocol)); } From 8d208a04b8e4a92461c64a33766ca8b6d49e9878 Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Thu, 18 Apr 2024 17:50:03 +0200 Subject: [PATCH 3/4] Refactor URL validation in client-demo --- .../client/demo/cli/LeshanClientDemoCLI.java | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/LeshanClientDemoCLI.java b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/LeshanClientDemoCLI.java index 9857981e65..f41de1f1eb 100644 --- a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/LeshanClientDemoCLI.java +++ b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/LeshanClientDemoCLI.java @@ -18,8 +18,11 @@ import java.io.File; import java.net.InetAddress; import java.net.UnknownHostException; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.scandium.dtls.cipher.CipherSuite; @@ -31,6 +34,7 @@ import org.eclipse.leshan.core.demo.cli.converters.InetAddressConverter; import org.eclipse.leshan.core.demo.cli.converters.ResourcePathConverter; import org.eclipse.leshan.core.demo.cli.converters.StrictlyPositiveIntegerConverter; +import org.eclipse.leshan.core.endpoint.Protocol; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.util.StringUtils; @@ -316,24 +320,33 @@ public void run() { // extract scheme int indexOf = main.url.indexOf("://"); String scheme = main.url.substring(0, indexOf); - // we support only coap and coaps - if (!"coap".equals(scheme) && !"coaps".equals(scheme) && !"coap+tcp".equals(scheme)) { - throw new MultiParameterException(spec.commandLine(), String.format( - "Invalid URL %s : unknown scheme '%s', we support only 'coap' or 'coaps' or 'coap+tcp' for now", - main.url, scheme), "-u"); + // check URI scheme is supported + List supportedUnsecuredProtocol = Arrays.asList(Protocol.COAP, Protocol.COAP_TCP) // + .stream().map(Protocol::getUriScheme).collect(Collectors.toList()); + List supportedTlsBasedProtocol = Arrays.asList(Protocol.COAPS) // + .stream().map(Protocol::getUriScheme).collect(Collectors.toList()); + List allSupportedProtocol = Stream + .concat(supportedUnsecuredProtocol.stream(), supportedTlsBasedProtocol.stream()) + .collect(Collectors.toList()); + + if (!allSupportedProtocol.contains(scheme)) { + throw new MultiParameterException(spec.commandLine(), + String.format("Invalid URL %s : unknown scheme '%s', we support only %s for now", main.url, scheme, + String.join(" or ", allSupportedProtocol)), + "-u"); } // check scheme matches configuration if (identity.hasIdentity()) { - if (!scheme.equals("coaps")) { + if (!supportedTlsBasedProtocol.contains(scheme)) { throw new MultiParameterException(spec.commandLine(), String.format( - "Invalid URL %s : '%s' scheme must be used without PSK, RPK or x509 option. Do you mean 'coaps' ? ", - main.url, scheme), "-u"); + "Invalid URL %s : '%s' scheme must be used without PSK, RPK or x509 option. Do you mean %s ? ", + main.url, scheme, String.join(" or ", supportedTlsBasedProtocol)), "-u"); } } else { - if (!scheme.equals("coap") && !scheme.equals("coap+tcp")) { + if (!supportedUnsecuredProtocol.contains(scheme)) { throw new MultiParameterException(spec.commandLine(), String.format( - "Invalid URL %s : '%s' scheme must be used with PSK, RPK or x509 option. Do you mean 'coap' ? ", - main.url, scheme), "-u"); + "Invalid URL %s : '%s' scheme must be used with PSK, RPK or x509 option. Do you mean %s ? ", + main.url, scheme, String.join(" or ", supportedUnsecuredProtocol)), "-u"); } } } From 99b94e7d08dfb3c5355ac1a2ca864b3201c84a0a Mon Sep 17 00:00:00 2001 From: Simon Bernard Date: Thu, 18 Apr 2024 18:00:31 +0200 Subject: [PATCH 4/4] Add coaps+tcp support based on java-coap to server-demo and client-demo. --- .../leshan/client/demo/LeshanClientDemo.java | 2 ++ .../leshan/client/demo/cli/LeshanClientDemoCLI.java | 2 +- .../leshan/server/demo/LeshanServerDemo.java | 12 ++++++++++-- .../leshan/server/demo/cli/LeshanServerDemoCLI.java | 13 +++++++++++++ .../identity/DefaultTlsIdentityHandler.java | 10 +++++----- .../coaptcp/transport/NettyCoapTcpTransport.java | 10 ++++++++-- 6 files changed, 39 insertions(+), 10 deletions(-) diff --git a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java index 158114f91f..df61263223 100644 --- a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java +++ b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java @@ -78,6 +78,7 @@ import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.response.BootstrapWriteResponse; import org.eclipse.leshan.transport.javacoap.client.coaptcp.endpoint.JavaCoapTcpClientEndpointsProvider; +import org.eclipse.leshan.transport.javacoap.client.coaptcp.endpoint.JavaCoapsTcpClientEndpointsProvider; import org.eclipse.leshan.transport.javacoap.client.endpoint.JavaCoapClientEndpointsProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -319,6 +320,7 @@ protected DtlsConnectorConfig.Builder createRootDtlsConnectorConfigBuilder( endpointsProvider.add(new JavaCoapClientEndpointsProvider()); } endpointsProvider.add(new JavaCoapTcpClientEndpointsProvider()); + endpointsProvider.add(new JavaCoapsTcpClientEndpointsProvider()); // Create client LeshanClientBuilder builder = new LeshanClientBuilder(cli.main.endpoint); diff --git a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/LeshanClientDemoCLI.java b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/LeshanClientDemoCLI.java index f41de1f1eb..e1daad4628 100644 --- a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/LeshanClientDemoCLI.java +++ b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/cli/LeshanClientDemoCLI.java @@ -323,7 +323,7 @@ public void run() { // check URI scheme is supported List supportedUnsecuredProtocol = Arrays.asList(Protocol.COAP, Protocol.COAP_TCP) // .stream().map(Protocol::getUriScheme).collect(Collectors.toList()); - List supportedTlsBasedProtocol = Arrays.asList(Protocol.COAPS) // + List supportedTlsBasedProtocol = Arrays.asList(Protocol.COAPS, Protocol.COAPS_TCP) // .stream().map(Protocol::getUriScheme).collect(Collectors.toList()); List allSupportedProtocol = Stream .concat(supportedUnsecuredProtocol.stream(), supportedTlsBasedProtocol.stream()) diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java index b6d617a6f1..e31c823add 100644 --- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java +++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java @@ -63,6 +63,7 @@ import org.eclipse.leshan.server.security.EditableSecurityStore; import org.eclipse.leshan.server.security.FileSecurityStore; import org.eclipse.leshan.transport.javacoap.server.coaptcp.endpoint.JavaCoapTcpServerEndpointsProvider; +import org.eclipse.leshan.transport.javacoap.server.coaptcp.endpoint.JavaCoapsTcpServerEndpointsProvider; import org.eclipse.leshan.transport.javacoap.server.endpoint.JavaCoapServerEndpointsProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -265,9 +266,16 @@ public static LeshanServer createLeshanServer(LeshanServerDemoCLI cli) throws Ex JavaCoapTcpServerEndpointsProvider javacoapTcpEndpointsProvider = new JavaCoapTcpServerEndpointsProvider( coapTcpAddr); + // Create CoAP over TLS endpoint based on java-coap + int coapsTcpPort = cli.main.jTlsLocalPort; + InetSocketAddress coapsTcpAddr = cli.main.jTlsLocalAddress == null ? new InetSocketAddress(coapsTcpPort) + : new InetSocketAddress(cli.main.jTlsLocalAddress, coapTcpPort); + JavaCoapsTcpServerEndpointsProvider javacoapsTcpEndpointsProvider = new JavaCoapsTcpServerEndpointsProvider( + coapsTcpAddr); + // Create LWM2M server - builder.setEndpointsProviders(endpointsBuilder.build(), javacoapEndpointsProvider, - javacoapTcpEndpointsProvider); + builder.setEndpointsProviders(endpointsBuilder.build(), javacoapEndpointsProvider, javacoapTcpEndpointsProvider, + javacoapsTcpEndpointsProvider); return builder.build(); } diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/cli/LeshanServerDemoCLI.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/cli/LeshanServerDemoCLI.java index 7ed307efee..6ae9cf7260 100644 --- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/cli/LeshanServerDemoCLI.java +++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/cli/LeshanServerDemoCLI.java @@ -82,6 +82,19 @@ public static class ServerGeneralSection extends GeneralSection { converter = PortConverter.class) public Integer jTcpLocalPort = 5683; + @Option(names = { "-tsh", "--java-coaps-tcp-host" }, + description = { // + "Set the local CoAP over TLS address of endpoint based on java-coap library.", // + "Default: any local address." }) + public String jTlsLocalAddress; + + @Option(names = { "-tsp", "--java-coaps-tcp-port" }, + description = { // + "Set the local CoAP over TLS port of endpoint based on java-coap library.", // + "Default: ${DEFAULT-VALUE}" }, + converter = PortConverter.class) + public Integer jTlsLocalPort = 5684; + @Option(names = { "-r", "--redis" }, description = { // "Use redis to store registration and securityInfo.", // diff --git a/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/DefaultTlsIdentityHandler.java b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/DefaultTlsIdentityHandler.java index a9d151f978..781424b665 100644 --- a/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/DefaultTlsIdentityHandler.java +++ b/leshan-tl-javacoap-core/src/main/java/org/eclipse/leshan/transport/javacoap/identity/DefaultTlsIdentityHandler.java @@ -15,6 +15,7 @@ *******************************************************************************/ package org.eclipse.leshan.transport.javacoap.identity; +import java.net.InetSocketAddress; import java.security.Principal; import javax.security.auth.x500.X500Principal; @@ -24,25 +25,24 @@ import org.eclipse.leshan.core.peer.X509Identity; import org.eclipse.leshan.core.security.certificate.util.X509CertUtil; -import com.mbed.coap.packet.CoapRequest; import com.mbed.coap.transport.TransportContext; public class DefaultTlsIdentityHandler extends DefaultCoapIdentityHandler { @Override - protected LwM2mPeer getIdentity(CoapRequest receivedRequest) { - Principal principal = receivedRequest.getTransContext().get(TlsTransportContextKeys.PRINCIPAL); + protected LwM2mPeer getIdentity(InetSocketAddress address, TransportContext context) { + Principal principal = context.get(TlsTransportContextKeys.PRINCIPAL); if (principal != null) { if (principal instanceof X500Principal) { // Extract common name String x509CommonName = X509CertUtil.extractCN(principal.getName()); - return new IpPeer(receivedRequest.getPeerAddress(), new X509Identity(x509CommonName)); + return new IpPeer(address, new X509Identity(x509CommonName)); } throw new IllegalStateException( String.format("Unable to extract sender identity : unexpected type of Principal %s [%s]", principal.getClass(), principal.toString())); } else { - return new IpPeer(receivedRequest.getPeerAddress()); + return new IpPeer(address); } } diff --git a/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/NettyCoapTcpTransport.java b/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/NettyCoapTcpTransport.java index ef79859f74..67c584899f 100644 --- a/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/NettyCoapTcpTransport.java +++ b/leshan-tl-javacoap-server-coaptcp/src/main/java/org/eclipse/leshan/transport/javacoap/server/coaptcp/transport/NettyCoapTcpTransport.java @@ -175,7 +175,10 @@ public void channelActive(ChannelHandlerContext ctx) throws Exception { // if (tansportContext == null) // throw new IllegalStateException("transport context should not be null"); - if (listener != null) + if (listener != null + // Not clear what is the consequence but it seems that remote addresse can be null : + // https://github.com/netty/netty/issues/8501 + && ctx.channel().remoteAddress() != null) listener.onConnected((InetSocketAddress) ctx.channel().remoteAddress()); super.channelActive(ctx); @@ -187,7 +190,10 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception { // if (tansportContext == null) // throw new IllegalStateException("transport context should not be null"); - if (listener != null) + if (listener != null + // Not clear what is the consequence but it seems that remote addresse can be null : + // https://github.com/netty/netty/issues/8501 + && ctx.channel().remoteAddress() != null) listener.onDisconnected((InetSocketAddress) ctx.channel().remoteAddress()); super.channelInactive(ctx);