diff --git a/.gitignore b/.gitignore index 4361f0f47f..895b29c7b7 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ bin/ # log Files # **/src/main/**/logback*.xml **/src/test/**/logback*.xml + +# Credentials files # +**/*.der \ No newline at end of file diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Security.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Security.java index d3afb81926..1ecb353df6 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Security.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Security.java @@ -82,6 +82,15 @@ public static Security pskBootstrap(String serverUri, byte[] pskIdentity, byte[] privateKey.clone(), 0); } + /** + * Returns a new security instance (RPK) for a bootstrap server. + */ + public static Security rpkBootstrap(String serverUri, byte[] clientPublicKey, byte[] clientPrivateKey, + byte[] serverPublicKey) { + return new Security(serverUri, true, SecurityMode.RPK.code, clientPublicKey.clone(), serverPublicKey.clone(), + clientPrivateKey.clone(), 0); + } + /** * Returns a new security instance (NoSec) for a device management server. */ 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 df27150b65..5945b69724 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 @@ -24,6 +24,10 @@ import java.io.File; import java.net.InetAddress; import java.net.UnknownHostException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; +import java.util.Arrays; import java.util.List; import java.util.Scanner; @@ -44,6 +48,7 @@ import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.request.BindingMode; import org.eclipse.leshan.util.Hex; +import org.eclipse.leshan.util.SecurityUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,7 +60,7 @@ public class LeshanClientDemo { private static final int OBJECT_ID_TEMPERATURE_SENSOR = 3303; private final static String DEFAULT_ENDPOINT = "LeshanClientDemo"; - private final static String USAGE = "java -jar leshan-client-demo.jar [OPTION]"; + private final static String USAGE = "java -jar leshan-client-demo.jar [OPTION]\n\n"; private static MyLocation locationInstance; @@ -64,6 +69,24 @@ public static void main(final String[] args) { // Define options for command line tools Options options = new Options(); + final StringBuilder PSKChapter = new StringBuilder(); + PSKChapter.append("\n ."); + PSKChapter.append("\n ."); + PSKChapter.append("\n ================================[ PSK ]================================="); + PSKChapter.append("\n | By default Leshan demo use non secure connection. |"); + PSKChapter.append("\n | To use PSK, -i and -p options should be used together. |"); + PSKChapter.append("\n ------------------------------------------------------------------------"); + + final StringBuilder RPKChapter = new StringBuilder(); + RPKChapter.append("\n ."); + RPKChapter.append("\n ."); + RPKChapter.append("\n ================================[ RPK ]================================="); + RPKChapter.append("\n | By default Leshan demo use non secure connection. |"); + RPKChapter.append("\n | To use RPK, -cpubk -cpribk -spubk options should be used together. |"); + RPKChapter.append("\n | To get helps about files format and how to generate it, see : |"); + RPKChapter.append("\n | See https://github.com/eclipse/leshan/wiki/Credential-files-format |"); + RPKChapter.append("\n ------------------------------------------------------------------------"); + options.addOption("h", "help", false, "Display help information."); options.addOption("n", true, String.format( "Set the endpoint name of the Client.\nDefault: the local hostname or '%s' if any.", DEFAULT_ENDPOINT)); @@ -73,14 +96,20 @@ public static void main(final String[] args) { "Set the local CoAP port of the Client.\n Default: A valid port value is between 0 and 65535."); options.addOption("u", true, String.format("Set the LWM2M or Bootstrap server URL.\nDefault: localhost:%d.", LwM2m.DEFAULT_COAP_PORT)); - options.addOption("i", true, - "Set the LWM2M or Bootstrap server PSK identity in ascii.\nUse none secure mode if not set."); - options.addOption("p", true, - "Set the LWM2M or Bootstrap server Pre-Shared-Key in hexa.\nUse none secure mode if not set."); options.addOption("pos", true, - "Set the initial location (latitude, longitude) of the device to be reported by the Location object. Format: lat_float:long_float"); - options.addOption("sf", true, "Scale factor to apply when shifting position. Default is 1.0."); + "Set the initial location (latitude, longitude) of the device to be reported by the Location object.\n Format: lat_float:long_float"); + options.addOption("sf", true, "Scale factor to apply when shifting position.\n Default is 1.0." + PSKChapter); + options.addOption("i", true, "Set the LWM2M or Bootstrap server PSK identity in ascii."); + options.addOption("p", true, "Set the LWM2M or Bootstrap server Pre-Shared-Key in hexa." + RPKChapter); + options.addOption("cpubk", true, + "The path to your client public key file.\n The public Key should be in SubjectPublicKeyInfo format (DER encoding)."); + options.addOption("cprik", true, + "The path to your client private key file.\nThe private key should be in PKCS#8 format (DER encoding)."); + options.addOption("spubk", true, + "The path to your server public key file.\n The public Key should be in SubjectPublicKeyInfo format (DER encoding)."); + HelpFormatter formatter = new HelpFormatter(); + formatter.setWidth(90); formatter.setOptionComparator(null); // Parse arguments @@ -106,13 +135,23 @@ public static void main(final String[] args) { return; } - // Abort if we have not identity and key for psk. + // Abort if PSK config is not complete if ((cl.hasOption("i") && !cl.hasOption("p")) || !cl.hasOption("i") && cl.hasOption("p")) { - System.err.println("You should precise identity and Pre-Shared-Key if you want to connect in PSK"); + System.err + .println("You should precise identity (-i) and Pre-Shared-Key (-p) if you want to connect in PSK"); formatter.printHelp(USAGE, options); return; } + // Abort if all RPK config is not complete + if (cl.hasOption("cpubk") || cl.hasOption("cprik") || cl.hasOption("spubk")) { + if (!cl.hasOption("cpubk") || !cl.hasOption("cprik") || !cl.hasOption("spubk")) { + System.err.println("cpubk, cprik and spubk should be used together to connect using RPK"); + formatter.printHelp(USAGE, options); + return; + } + } + // Get endpoint name String endpoint; if (cl.hasOption("n")) { @@ -128,25 +167,42 @@ public static void main(final String[] args) { // Get server URI String serverURI; if (cl.hasOption("u")) { - if (cl.hasOption("i")) + if (cl.hasOption("i") || cl.hasOption("cpubk")) serverURI = "coaps://" + cl.getOptionValue("u"); else serverURI = "coap://" + cl.getOptionValue("u"); } else { - if (cl.hasOption("i")) + if (cl.hasOption("i") || cl.hasOption("cpubk")) serverURI = "coaps://localhost:" + LwM2m.DEFAULT_COAP_SECURE_PORT; else serverURI = "coap://localhost:" + LwM2m.DEFAULT_COAP_PORT; } - // get security info + // get PSK info byte[] pskIdentity = null; byte[] pskKey = null; - if (cl.hasOption("i") && cl.hasOption("p")) { + if (cl.hasOption("i")) { pskIdentity = cl.getOptionValue("i").getBytes(); pskKey = Hex.decodeHex(cl.getOptionValue("p").toCharArray()); } + // get RPK info + PublicKey clientPublicKey = null; + PrivateKey clientPrivateKey = null; + PublicKey serverPublicKey = null; + if (cl.hasOption("cpubk")) { + try { + clientPrivateKey = SecurityUtil.extractPrivateKey(cl.getOptionValue("cprik")); + clientPublicKey = SecurityUtil.extractPublicKey(cl.getOptionValue("cpubk")); + serverPublicKey = SecurityUtil.extractPublicKey(cl.getOptionValue("spubk")); + } catch (Exception e) { + System.err.println("Unable to load RPK files : " + e.getMessage()); + e.printStackTrace(); + formatter.printHelp(USAGE, options); + return; + } + } + // get local address String localAddress = null; int localPort = 0; @@ -189,11 +245,12 @@ public static void main(final String[] args) { } createAndStartClient(endpoint, localAddress, localPort, cl.hasOption("b"), serverURI, pskIdentity, pskKey, - latitude, longitude, scaleFactor); + clientPublicKey, clientPrivateKey, serverPublicKey, latitude, longitude, scaleFactor); } public static void createAndStartClient(String endpoint, String localAddress, int localPort, boolean needBootstrap, - String serverURI, byte[] pskIdentity, byte[] pskKey, Float latitude, Float longitude, float scaleFactor) { + String serverURI, byte[] pskIdentity, byte[] pskKey, PublicKey clientPublicKey, PrivateKey clientPrivateKey, + PublicKey serverPublicKey, Float latitude, Float longitude, float scaleFactor) { locationInstance = new MyLocation(latitude, longitude, scaleFactor); @@ -204,19 +261,27 @@ public static void createAndStartClient(String endpoint, String localAddress, in // Initialize object list ObjectsInitializer initializer = new ObjectsInitializer(new LwM2mModel(models)); if (needBootstrap) { - if (pskIdentity == null) { - initializer.setInstancesForObject(SECURITY, noSecBootstap(serverURI)); + if (pskIdentity != null) { + initializer.setInstancesForObject(SECURITY, pskBootstrap(serverURI, pskIdentity, pskKey)); + initializer.setClassForObject(SERVER, Server.class); + } else if (clientPublicKey != null) { + initializer.setInstancesForObject(SECURITY, rpkBootstrap(serverURI, clientPublicKey.getEncoded(), + clientPrivateKey.getEncoded(), serverPublicKey.getEncoded())); initializer.setClassForObject(SERVER, Server.class); } else { - initializer.setInstancesForObject(SECURITY, pskBootstrap(serverURI, pskIdentity, pskKey)); + initializer.setInstancesForObject(SECURITY, noSecBootstap(serverURI)); initializer.setClassForObject(SERVER, Server.class); } } else { - if (pskIdentity == null) { - initializer.setInstancesForObject(SECURITY, noSec(serverURI, 123)); + if (pskIdentity != null) { + initializer.setInstancesForObject(SECURITY, psk(serverURI, 123, pskIdentity, pskKey)); + initializer.setInstancesForObject(SERVER, new Server(123, 30, BindingMode.U, false)); + } else if (clientPublicKey != null) { + initializer.setInstancesForObject(SECURITY, rpk(serverURI, 123, clientPublicKey.getEncoded(), + clientPrivateKey.getEncoded(), serverPublicKey.getEncoded())); initializer.setInstancesForObject(SERVER, new Server(123, 30, BindingMode.U, false)); } else { - initializer.setInstancesForObject(SECURITY, psk(serverURI, 123, pskIdentity, pskKey)); + initializer.setInstancesForObject(SECURITY, noSec(serverURI, 123)); initializer.setInstancesForObject(SERVER, new Server(123, 30, BindingMode.U, false)); } } @@ -244,6 +309,32 @@ public static void createAndStartClient(String endpoint, String localAddress, in builder.setCoapConfig(coapConfig); final LeshanClient client = builder.build(); + // Display client public key to easily add it in Leshan Server Demo + if (clientPublicKey != null) { + PublicKey rawPublicKey = clientPublicKey; + if (rawPublicKey instanceof ECPublicKey) { + ECPublicKey ecPublicKey = (ECPublicKey) rawPublicKey; + // Get x coordinate + byte[] x = ecPublicKey.getW().getAffineX().toByteArray(); + if (x[0] == 0) + x = Arrays.copyOfRange(x, 1, x.length); + + // Get Y coordinate + byte[] y = ecPublicKey.getW().getAffineY().toByteArray(); + if (y[0] == 0) + y = Arrays.copyOfRange(y, 1, y.length); + + // Get Curves params + String params = ecPublicKey.getParams().toString(); + + LOG.info( + "Client public Key is : \n Elliptic Curve parameters : {} \n Public x coord : {} \n Public y coord : {}", + params, Hex.encodeHexString(x), Hex.encodeHexString(y)); + } else { + throw new IllegalStateException("Unsupported Public Key Format (only ECPublicKey supported)."); + } + } + LOG.info("Press 'w','a','s','d' to change reported Location ({},{}).", locationInstance.getLatitude(), locationInstance.getLongitude()); diff --git a/leshan-core/src/main/java/org/eclipse/leshan/util/SecurityUtil.java b/leshan-core/src/main/java/org/eclipse/leshan/util/SecurityUtil.java new file mode 100644 index 0000000000..8906170d03 --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/util/SecurityUtil.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2018 Sierra Wireless and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.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.util; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +public class SecurityUtil { + + /** + * Extract Elliptic Curve private key in PKCS8 format from file (DER encoded). + */ + public static PrivateKey extractPrivateKey(String fileName) throws Exception { + byte[] keyBytes = Files.readAllBytes(Paths.get(fileName)); + + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance("EC"); + return kf.generatePrivate(spec); + } + + /** + * Extract Elliptic Curve public key in SubjectPublicKeyInfo format from file (DER encoded). + */ + public static PublicKey extractPublicKey(String fileName) throws Exception { + byte[] keyBytes = Files.readAllBytes(Paths.get(fileName)); + + X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance("EC"); + return kf.generatePublic(spec); + } +} 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 b456dfc4a3..3fcbc681e0 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 @@ -25,6 +25,9 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import java.security.AlgorithmParameters; import java.security.Key; import java.security.KeyFactory; @@ -337,6 +340,7 @@ public static void createAndStartServer(String webAddress, int webPort, String l // Get keys publicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); + Files.write(Paths.get("server_pub.der"), publicKey.getEncoded(), StandardOpenOption.CREATE); PrivateKey privateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); builder.setPublicKey(publicKey); builder.setPrivateKey(privateKey);