From 6675163a5da6a306a8fef243f75ef5e7931c9c2c Mon Sep 17 00:00:00 2001 From: Avgustin Marinov Date: Wed, 12 Feb 2025 13:53:08 +0200 Subject: [PATCH] Add certificate authentication support in SDK (#2269) Signed-off-by: Avgustin Marinov --- hawkbit-sdk/hawkbit-sdk-commons/pom.xml | 6 + .../org/eclipse/hawkbit/sdk/Certificate.java | 84 +++++++++++ .../org/eclipse/hawkbit/sdk/Controller.java | 2 + .../eclipse/hawkbit/sdk/HawkbitClient.java | 94 +++++++++--- .../java/org/eclipse/hawkbit/sdk/Tenant.java | 13 ++ .../java/org/eclipse/hawkbit/sdk/ca/CA.java | 136 ++++++++++++++++++ .../hawkbit/sdk/demo/device/DeviceApp.java | 12 +- .../sdk/demo/multidevice/MultiDeviceApp.java | 10 +- hawkbit-sdk/hawkbit-sdk-device/pom.xml | 1 - .../hawkbit/sdk/device/DdiController.java | 4 + .../eclipse/hawkbit/sdk/device/DdiTenant.java | 1 - ...pi.java => AuthenticationSetupHelper.java} | 77 ++++++---- hawkbit-sdk/pom.xml | 1 + 13 files changed, 380 insertions(+), 61 deletions(-) create mode 100644 hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Certificate.java create mode 100644 hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/ca/CA.java rename hawkbit-sdk/hawkbit-sdk-mgmt/src/main/java/org/eclipse/hawkbit/sdk/mgmt/{MgmtApi.java => AuthenticationSetupHelper.java} (62%) diff --git a/hawkbit-sdk/hawkbit-sdk-commons/pom.xml b/hawkbit-sdk/hawkbit-sdk-commons/pom.xml index 1d0c2c503..f08a6cb69 100644 --- a/hawkbit-sdk/hawkbit-sdk-commons/pom.xml +++ b/hawkbit-sdk/hawkbit-sdk-commons/pom.xml @@ -40,5 +40,11 @@ org.springframework.boot spring-boot-starter-hateoas + + + org.bouncycastle + bcpkix-jdk18on + ${bouncycastle.version} + diff --git a/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Certificate.java b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Certificate.java new file mode 100644 index 000000000..c3655c97d --- /dev/null +++ b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Certificate.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.hawkbit.sdk; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Objects; + +import lombok.Data; + +@Data +public class Certificate { + + private final KeyPair keyPair; + private final X509Certificate[] certificateChain; + + // create holder for the certificate - key pair and the certificate chain + public Certificate(final KeyPair keyPair, final X509Certificate[] certificateChain) { + Objects.requireNonNull(keyPair, "keyPair must not be null"); + Objects.requireNonNull(certificateChain, "certificateChain must not be null"); + if (certificateChain.length == 0) { + throw new IllegalArgumentException("certificateChain must not be empty"); + } + this.keyPair = keyPair; + this.certificateChain = certificateChain; + } + + public KeyStore toKeyStore(final String pass) throws KeyStoreException { + try { + final KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(null, null); // init + keyStore.setKeyEntry("alias", keyPair.getPrivate(), pass.toCharArray(), certificateChain); + return keyStore; + } catch (final NoSuchAlgorithmException | CertificateException | IOException e) { + throw new KeyStoreException(e); + } + } + + public void writeToOS(final String pass, final OutputStream os) throws KeyStoreException { + try { + toKeyStore(pass).store(os, pass.toCharArray()); + } catch (final NoSuchAlgorithmException | CertificateException | IOException e) { + throw new KeyStoreException(e); + } + } + + public StringBuilder printPem(final StringBuilder sb) throws CertificateException { + for (final X509Certificate certificate : certificateChain) { + sb.append("-----BEGIN CERTIFICATE-----\n"); + sb.append(toPem(certificate.getEncoded())); + sb.append("-----END CERTIFICATE-----\n"); + } + sb.append('\n'); + + sb.append("-----BEGIN PRIVATE KEY-----\n"); + sb.append(toPem(keyPair.getPrivate().getEncoded())); + sb.append("-----END PRIVATE KEY-----\n"); + + return sb; + } + + private static String toPem(final byte[] ba) { + final String base64 = Base64.getEncoder().encodeToString(ba); + final StringBuilder formatted = new StringBuilder(); + for (int off = 0, end; (end = Math.min(off + 64, base64.length())) >= 0; off = end) { + formatted.append(base64, off, end).append("\n"); + } + return formatted.toString(); + } +} \ No newline at end of file diff --git a/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Controller.java b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Controller.java index d63ef2095..50f23a029 100644 --- a/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Controller.java +++ b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Controller.java @@ -24,4 +24,6 @@ public class Controller { // (target) security token @Nullable private String securityToken; + @Nullable + private Certificate certificate; } diff --git a/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/HawkbitClient.java b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/HawkbitClient.java index 9c3b41dca..e883dbc31 100644 --- a/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/HawkbitClient.java +++ b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/HawkbitClient.java @@ -21,15 +21,28 @@ import java.lang.reflect.Proxy; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; import java.util.Base64; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Random; import java.util.UUID; import java.util.function.BiFunction; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; + import com.fasterxml.jackson.databind.ObjectMapper; import feign.Client; import feign.Contract; @@ -58,29 +71,22 @@ public class HawkbitClient { private static final String AUTHORIZATION = "Authorization"; public static final BiFunction DEFAULT_REQUEST_INTERCEPTOR_FN = - (tenant, controller) -> - controller == null - ? template -> - template.header( - AUTHORIZATION, - - "Basic " + - Base64.getEncoder() - .encodeToString( - (Objects.requireNonNull(tenant.getUsername(), "User is null!") + - ":" + - Objects.requireNonNull(tenant.getPassword(), - "Password is not available!")) - .getBytes(StandardCharsets.ISO_8859_1))) - : template -> { - if (ObjectUtils.isEmpty(tenant.getGatewayToken())) { - if (!ObjectUtils.isEmpty(controller.getSecurityToken())) { - template.header(AUTHORIZATION, "TargetToken " + controller.getSecurityToken()); - } // else do not send authentication - } else { - template.header(AUTHORIZATION, "GatewayToken " + tenant.getGatewayToken()); - } - }; + (tenant, controller) -> controller == null + ? template -> + template.header( + AUTHORIZATION, + "Basic " + Base64.getEncoder().encodeToString( + (Objects.requireNonNull(tenant.getUsername(), "User is null!") + + ":" + + Objects.requireNonNull(tenant.getPassword(),"Password is not available!")) + .getBytes(StandardCharsets.ISO_8859_1))) + : template -> { + if (!ObjectUtils.isEmpty(tenant.getGatewayToken())) { + template.header(AUTHORIZATION, "GatewayToken " + tenant.getGatewayToken()); + } else if (!ObjectUtils.isEmpty(controller.getSecurityToken())) { + template.header(AUTHORIZATION, "TargetToken " + controller.getSecurityToken()); + } // else do not send authentication, no auth or certificate based + }; private static final ErrorDecoder DEFAULT_ERROR_DECODER_0 = new ErrorDecoder.Default(); public static final ErrorDecoder DEFAULT_ERROR_DECODER = (methodKey, response) -> { final Exception e = DEFAULT_ERROR_DECODER_0.decode(methodKey, response); @@ -144,6 +150,19 @@ public class HawkbitClient { } private T service0(final Class serviceType, final Tenant tenant, final Controller controller) { + final Client client; + if (controller != null && controller.getCertificate() != null && hawkBitServer.getDdiUrl().startsWith("https://")) { + // mTLS could be requested + try { + client = mTlsClient(controller.getCertificate(), tenant); + } catch (final RuntimeException | Error e) { + throw e; + } catch (final Exception e) { + throw new IllegalStateException("Failed to create mTLS client", e); + } + } else { + client = this.client; + } return Feign.builder().client(client) .encoder(encoder) .decoder(decoder) @@ -312,4 +331,33 @@ public class HawkbitClient { } return null; } + + private static final String KEYSTORE_PASSWORD; + static { + final Random random = new SecureRandom(); + final byte[] bytes = new byte[16]; + random.nextBytes(bytes); + KEYSTORE_PASSWORD = Base64.getEncoder().encodeToString(bytes); + } + private static Client mTlsClient(final Certificate certificate, final Tenant tenant) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException, CertificateException, IOException, + KeyManagementException { + final KeyStore clientKeyStore = certificate.toKeyStore(KEYSTORE_PASSWORD); + final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(clientKeyStore, KEYSTORE_PASSWORD.toCharArray()); + // Truststore + final TrustManagerFactory trustManagerFactory; + if (tenant.getDdiCertificate() == null) { + trustManagerFactory = null; + } else { + final KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null, null); + trustStore.setEntry("alias", new KeyStore.TrustedCertificateEntry(tenant.getDdiCertificate()), null); + trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + } + final SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory == null ? null : trustManagerFactory.getTrustManagers(), null); + return new Client.Default(sslContext.getSocketFactory(), null); + } } \ No newline at end of file diff --git a/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Tenant.java b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Tenant.java index 6fd3542d1..550480a65 100644 --- a/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Tenant.java +++ b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/Tenant.java @@ -9,8 +9,11 @@ */ package org.eclipse.hawkbit.sdk; +import java.security.cert.Certificate; + import lombok.Data; import lombok.ToString; +import org.eclipse.hawkbit.sdk.ca.CA; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; @@ -33,6 +36,16 @@ public class Tenant { // gateway token @Nullable private String gatewayToken; + // gateway token + @Nullable + private String[] certificateFingerprints; + + // the tenant DDI server certificate - it shall be trusted by controllers connecting via HTTPS + @Nullable + private Certificate ddiCertificate; + // Certificate Authority for the tenant that is used to sign the target certificates. It shall be trusted by the DDI server + @Nullable + private CA ddiCA; // amqp settings (if DMF is used) @Nullable diff --git a/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/ca/CA.java b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/ca/CA.java new file mode 100644 index 000000000..57fadcc0a --- /dev/null +++ b/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/ca/CA.java @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.hawkbit.sdk.ca; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Objects; + +import javax.security.auth.x500.X500Principal; + +import lombok.Data; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.eclipse.hawkbit.sdk.Certificate; + +@Data +public class CA { + + public static final String DEFAULT_CA_DN = "CN=CA, O=hawkBit, L=Sofia, C=BG"; + public static final long DEFAULT_NOT_BEFORE_DAYS_OFFSET = 1; + public static final long DEFAULT_NOT_AFTER_DAYS_OFFSET = 30; + + private static final String SHA_256_WITH_RSA_ENCRYPTION = "SHA256WithRSAEncryption"; + + private final Certificate certificate; + private long nextSerial = System.currentTimeMillis(); + + // creates a self-signed CA with defaults + public CA() throws CertificateException { + this(null, null, null); + } + + // creates a self-signed CA + public CA(final String caDN, final Date notBefore, final Date notAfter) throws CertificateException { + this(selfSign(caDN, notBefore, notAfter)); + } + + // create a CA with a key and certificate chain + public CA(final Certificate certificate) { + this(certificate, 0); + } + + // create a CA with a key and certificate chain + public CA(final Certificate certificate, final long nextSerial) { + this.certificate = certificate; + this.nextSerial = nextSerial; + } + + // generate key and issue a certificate with defaults + public Certificate issue(final String subject) throws CertificateException { + return issue(subject, null, null); + } + + // generate key and issue a certificate + public Certificate issue(final String subject, final Date notBefore, final Date notAfter) throws CertificateException { + Objects.requireNonNull(subject); + try { + final KeyPair keyPair = genKey(); + final X509Certificate[] certificateChain = certificate.getCertificateChain(); + final ContentSigner signer = new JcaContentSignerBuilder(SHA_256_WITH_RSA_ENCRYPTION).build(certificate.getKeyPair().getPrivate()); + final X509v3CertificateBuilder caCertBuilder = new JcaX509v3CertificateBuilder( + certificateChain[0].getSubjectX500Principal(), + BigInteger.valueOf(nextSerial++), notBefore(notBefore), notAfter(notAfter), new X500Principal(subject), + keyPair.getPublic()); + final X509Certificate[] subjectCertificateChain = new X509Certificate[certificateChain.length + 1]; + certificateChain[0] = new JcaX509CertificateConverter().getCertificate(caCertBuilder.build(signer)); + System.arraycopy(certificateChain, 0, subjectCertificateChain, 1, certificateChain.length); + return new Certificate(keyPair, subjectCertificateChain); + } catch (final NoSuchAlgorithmException | OperatorCreationException e) { + throw new CertificateException(e); + } + } + + public String getFingerprint() { + try { + final X509Certificate[] certificateChain = certificate.getCertificateChain(); + return toHex(MessageDigest.getInstance("SHA-256").digest(certificateChain[certificateChain.length - 1].getEncoded())); + } catch (final NoSuchAlgorithmException | CertificateEncodingException e) { + throw new IllegalArgumentException(e); + } + } + + private static String toHex(final byte[] bytes) { + final StringBuilder sb = new StringBuilder(); + for (final byte b : bytes) { + sb.append(String.format("%02x", b)).append(':'); + } + sb.deleteCharAt(sb.length() - 1); + return sb.toString(); + } + + private static Certificate selfSign(final String caDN, final Date notBefore, final Date notAfter) throws CertificateException { + try { + final KeyPair keyPair = genKey(); + final X500Principal caPrincipal = new X500Principal(caDN == null ? DEFAULT_CA_DN : caDN); + final ContentSigner selfSigner = new JcaContentSignerBuilder(SHA_256_WITH_RSA_ENCRYPTION).build(keyPair.getPrivate()); + final X509v3CertificateBuilder caCertBuilder = new JcaX509v3CertificateBuilder( + caPrincipal, BigInteger.valueOf(0L), notBefore(notBefore), notAfter(notAfter), caPrincipal, keyPair.getPublic()); + return new Certificate(keyPair, new X509Certificate[] { new JcaX509CertificateConverter().getCertificate(caCertBuilder.build(selfSigner)) }); + } catch (final NoSuchAlgorithmException | OperatorCreationException e) { + throw new CertificateException(e); + } + } + + private static KeyPair genKey() throws NoSuchAlgorithmException { + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + return keyPairGenerator.generateKeyPair(); + } + + private static Date notBefore(final Date notBefore) { + return notBefore == null ? new Date(System.currentTimeMillis() - DEFAULT_NOT_BEFORE_DAYS_OFFSET * 24 * 3600_000L) : notBefore; + } + + private static Date notAfter(final Date notAfter) { + return notAfter == null ? new Date(System.currentTimeMillis() + DEFAULT_NOT_AFTER_DAYS_OFFSET * 24 * 3600_000L) : notAfter; + } +} \ No newline at end of file diff --git a/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/device/DeviceApp.java b/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/device/DeviceApp.java index 62bddd7b7..65f3d8b14 100644 --- a/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/device/DeviceApp.java +++ b/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/device/DeviceApp.java @@ -24,7 +24,7 @@ import org.eclipse.hawkbit.sdk.Tenant; import org.eclipse.hawkbit.sdk.device.DdiController; import org.eclipse.hawkbit.sdk.device.DdiTenant; import org.eclipse.hawkbit.sdk.device.UpdateHandler; -import org.eclipse.hawkbit.sdk.mgmt.MgmtApi; +import org.eclipse.hawkbit.sdk.mgmt.AuthenticationSetupHelper; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @@ -57,8 +57,8 @@ public class DeviceApp { } @Bean - MgmtApi mgmtApi(final Tenant tenant, final HawkbitClient hawkbitClient) { - return new MgmtApi(tenant, hawkbitClient); + AuthenticationSetupHelper mgmtApi(final Tenant tenant, final HawkbitClient hawkbitClient) { + return new AuthenticationSetupHelper(tenant, hawkbitClient); } @ShellComponent @@ -67,10 +67,10 @@ public class DeviceApp { private final DdiTenant ddiTenant; private final DdiController device; - private final MgmtApi mgmtApi; + private final AuthenticationSetupHelper mgmtApi; @SuppressWarnings("java:S3358") - Shell(final DdiTenant ddiTenant, final MgmtApi mgmtApi, final Optional updateHandler) { + Shell(final DdiTenant ddiTenant, final AuthenticationSetupHelper mgmtApi, final Optional updateHandler) { this.ddiTenant = ddiTenant; this.mgmtApi = mgmtApi; String controllerId = System.getProperty("demo.controller.id"); @@ -79,7 +79,7 @@ public class DeviceApp { this.device = this.ddiTenant.createController(Controller.builder() .controllerId(controllerId) .securityToken(ObjectUtils.isEmpty(securityToken) ? - (ObjectUtils.isEmpty(ddiTenant.getTenant().getGatewayToken()) ? MgmtApi.randomToken() : securityToken) : + (ObjectUtils.isEmpty(ddiTenant.getTenant().getGatewayToken()) ? AuthenticationSetupHelper.randomToken() : securityToken) : securityToken) .build(), updateHandler.orElse(null)).setOverridePollMillis(10_000); diff --git a/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/multidevice/MultiDeviceApp.java b/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/multidevice/MultiDeviceApp.java index d1a958595..b4d7567b8 100644 --- a/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/multidevice/MultiDeviceApp.java +++ b/hawkbit-sdk/hawkbit-sdk-demo/src/main/java/org/eclipse/hawkbit/sdk/demo/multidevice/MultiDeviceApp.java @@ -24,7 +24,7 @@ import org.eclipse.hawkbit.sdk.Tenant; import org.eclipse.hawkbit.sdk.device.DdiController; import org.eclipse.hawkbit.sdk.device.DdiTenant; import org.eclipse.hawkbit.sdk.device.UpdateHandler; -import org.eclipse.hawkbit.sdk.mgmt.MgmtApi; +import org.eclipse.hawkbit.sdk.mgmt.AuthenticationSetupHelper; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @@ -57,20 +57,20 @@ public class MultiDeviceApp { } @Bean - MgmtApi mgmtApi(final Tenant defaultTenant, final HawkbitClient hawkbitClient) { - return new MgmtApi(defaultTenant, hawkbitClient); + AuthenticationSetupHelper mgmtApi(final Tenant defaultTenant, final HawkbitClient hawkbitClient) { + return new AuthenticationSetupHelper(defaultTenant, hawkbitClient); } @ShellComponent public static class Shell { private final DdiTenant ddiTenant; - private final MgmtApi mgmtApi; + private final AuthenticationSetupHelper mgmtApi; private final UpdateHandler updateHandler; private boolean setup; - Shell(final DdiTenant ddiTenant, final MgmtApi mgmtApi, final Optional updateHandler) { + Shell(final DdiTenant ddiTenant, final AuthenticationSetupHelper mgmtApi, final Optional updateHandler) { this.ddiTenant = ddiTenant; this.mgmtApi = mgmtApi; this.updateHandler = updateHandler.orElse(null); diff --git a/hawkbit-sdk/hawkbit-sdk-device/pom.xml b/hawkbit-sdk/hawkbit-sdk-device/pom.xml index 8362ac3a5..5feceb1bb 100644 --- a/hawkbit-sdk/hawkbit-sdk-device/pom.xml +++ b/hawkbit-sdk/hawkbit-sdk-device/pom.xml @@ -36,6 +36,5 @@ hawkbit-ddi-api ${project.version} - \ No newline at end of file diff --git a/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiController.java b/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiController.java index 606f3fcc8..ea52dc17a 100644 --- a/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiController.java +++ b/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiController.java @@ -32,6 +32,7 @@ import org.eclipse.hawkbit.ddi.json.model.DdiDeployment; import org.eclipse.hawkbit.ddi.json.model.DdiDeploymentBase; import org.eclipse.hawkbit.ddi.json.model.DdiUpdateMode; import org.eclipse.hawkbit.ddi.rest.api.DdiRootControllerRestApi; +import org.eclipse.hawkbit.sdk.Certificate; import org.eclipse.hawkbit.sdk.Controller; import org.eclipse.hawkbit.sdk.HawkbitClient; import org.eclipse.hawkbit.sdk.Tenant; @@ -64,6 +65,8 @@ public class DdiController { private final boolean downloadAuthenticationEnabled; private final String gatewayToken; private final String targetSecurityToken; + private final Certificate certificate; + @Setter @Accessors(chain = true) private long overridePollMillis = -1; // -1 means disabled @@ -90,6 +93,7 @@ public class DdiController { downloadAuthenticationEnabled = tenant.isDownloadAuthenticationEnabled(); this.controllerId = controller.getControllerId(); this.targetSecurityToken = controller.getSecurityToken(); + this.certificate = controller.getCertificate(); this.updateHandler = updateHandler == null ? UpdateHandler.SKIP : updateHandler; ddiApi = hawkbitClient.ddiService(DdiRootControllerRestApi.class, tenant, controller); } diff --git a/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiTenant.java b/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiTenant.java index 28387b4fe..bbb89ab1e 100644 --- a/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiTenant.java +++ b/hawkbit-sdk/hawkbit-sdk-device/src/main/java/org/eclipse/hawkbit/sdk/device/DdiTenant.java @@ -50,5 +50,4 @@ public class DdiTenant { public Optional getController(final String controllerId) { return Optional.ofNullable(controllers.get(controllerId)); } - } \ No newline at end of file diff --git a/hawkbit-sdk/hawkbit-sdk-mgmt/src/main/java/org/eclipse/hawkbit/sdk/mgmt/MgmtApi.java b/hawkbit-sdk/hawkbit-sdk-mgmt/src/main/java/org/eclipse/hawkbit/sdk/mgmt/AuthenticationSetupHelper.java similarity index 62% rename from hawkbit-sdk/hawkbit-sdk-mgmt/src/main/java/org/eclipse/hawkbit/sdk/mgmt/MgmtApi.java rename to hawkbit-sdk/hawkbit-sdk-mgmt/src/main/java/org/eclipse/hawkbit/sdk/mgmt/AuthenticationSetupHelper.java index 4afaf7f6c..8be4252b0 100644 --- a/hawkbit-sdk/hawkbit-sdk-mgmt/src/main/java/org/eclipse/hawkbit/sdk/mgmt/MgmtApi.java +++ b/hawkbit-sdk/hawkbit-sdk-mgmt/src/main/java/org/eclipse/hawkbit/sdk/mgmt/AuthenticationSetupHelper.java @@ -26,19 +26,24 @@ import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetRestApi; import org.eclipse.hawkbit.mgmt.rest.api.MgmtTenantManagementRestApi; import org.eclipse.hawkbit.sdk.HawkbitClient; import org.eclipse.hawkbit.sdk.Tenant; +import org.eclipse.hawkbit.sdk.ca.CA; import org.springframework.util.ObjectUtils; /** - * Management Api Interface + * Helper for authentication setup */ @Slf4j @AllArgsConstructor -public class MgmtApi { +public class AuthenticationSetupHelper { private static final String AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY = "authentication.gatewaytoken.key"; private static final String AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED = "authentication.gatewaytoken.enabled"; private static final String AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED = "authentication.targettoken.enabled"; + private static final String AUTHENTICATION_MODE_HEADER_ENABLED = "authentication.header.enabled"; + private static final String AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME = "authentication.header.authority"; + private static final Random RND = new SecureRandom(); + @NonNull private final Tenant tenant; @NonNull @@ -50,42 +55,47 @@ public class MgmtApi { return Base64.getEncoder().encodeToString(rnd); } - // if gateway toke is configured then the gateway auth is enabled key is set + // if gateway token is configured then the gateway auth is enabled key is set // so all devices use gateway token authentication - // otherwise target token authentication is enabled. Then all devices shall be registerd + // otherwise target token authentication is enabled. Then all devices shall be registered // and the target token shall be set to the one from the DDI controller instance public void setupTargetAuthentication() { - final MgmtTenantManagementRestApi mgmtTenantManagementRestApi = - hawkbitClient.mgmtService(MgmtTenantManagementRestApi.class, tenant); + final MgmtTenantManagementRestApi mgmtTenantManagementRestApi = hawkbitClient.mgmtService(MgmtTenantManagementRestApi.class, tenant); final String gatewayToken = tenant.getGatewayToken(); if (ObjectUtils.isEmpty(gatewayToken)) { if (!(Boolean.TRUE.equals(Objects.requireNonNull(mgmtTenantManagementRestApi .getTenantConfigurationValue(AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED) .getBody()).getValue()))) { - mgmtTenantManagementRestApi.updateTenantConfiguration( - Map.of(AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED, true) - ); + mgmtTenantManagementRestApi.updateTenantConfiguration(Map.of(AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED, true)); } } else { if (!(Boolean.TRUE.equals(Objects.requireNonNull(mgmtTenantManagementRestApi .getTenantConfigurationValue(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED) .getBody()).getValue()))) { - mgmtTenantManagementRestApi.updateTenantConfiguration( - Map.of(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, true) - ); + mgmtTenantManagementRestApi.updateTenantConfiguration(Map.of(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, true)); } if (!gatewayToken.equals( Objects.requireNonNull(mgmtTenantManagementRestApi .getTenantConfigurationValue(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY) .getBody()).getValue())) { - mgmtTenantManagementRestApi.updateTenantConfiguration( - Map.of(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, gatewayToken) - ); + mgmtTenantManagementRestApi.updateTenantConfiguration(Map.of(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, gatewayToken)); } } } - // returns target token + // set gateway token authentication (generate and sets gateway token to tenant, if not set up) + // return the gateway token + public String setupGatewayToken() { + String gatewayToken = tenant.getGatewayToken(); + if (ObjectUtils.isEmpty(gatewayToken)) { + gatewayToken = randomToken(); + tenant.setGatewayToken(gatewayToken); + } + setupTargetAuthentication(); + return gatewayToken; + } + + // sets up a target token and returns it public String setupTargetToken(final String controllerId, String securityTargetToken) { if (ObjectUtils.isEmpty(tenant.getGatewayToken())) { final MgmtTargetRestApi mgmtTargetRestApi = hawkbitClient.mgmtService(MgmtTargetRestApi.class, tenant); @@ -96,15 +106,13 @@ public class MgmtApi { if (ObjectUtils.isEmpty(target.getSecurityToken())) { // generate random to set to tha existing target without configured security token securityTargetToken = randomToken(); - mgmtTargetRestApi.updateTarget(controllerId, - new MgmtTargetRequestBody().setSecurityToken(securityTargetToken)); + mgmtTargetRestApi.updateTarget(controllerId, new MgmtTargetRequestBody().setSecurityToken(securityTargetToken)); } else { securityTargetToken = target.getSecurityToken(); } } else if (!securityTargetToken.equals(target.getSecurityToken())) { // update target's with the security token (since it doesn't match) - mgmtTargetRestApi.updateTarget(controllerId, - new MgmtTargetRequestBody().setSecurityToken(securityTargetToken)); + mgmtTargetRestApi.updateTarget(controllerId, new MgmtTargetRequestBody().setSecurityToken(securityTargetToken)); } } catch (final FeignException.NotFound e) { if (ObjectUtils.isEmpty(securityTargetToken)) { @@ -112,16 +120,35 @@ public class MgmtApi { } // create target with the security token mgmtTargetRestApi.createTargets(List.of( - new MgmtTargetRequestBody() - .setControllerId(controllerId) - .setSecurityToken(securityTargetToken))); + new MgmtTargetRequestBody().setControllerId(controllerId).setSecurityToken(securityTargetToken))); } } return securityTargetToken; } - public void deleteController(final String controllerId) { - hawkbitClient.mgmtService(MgmtTargetRestApi.class, tenant).deleteTarget(controllerId); + // sets up a target token and returns it + public void setupCertificateFingerprint() { + final MgmtTenantManagementRestApi mgmtTenantManagementRestApi = hawkbitClient.mgmtService(MgmtTenantManagementRestApi.class, tenant); + final CA ddiCA = tenant.getDdiCA(); + final Object enabled = Objects.requireNonNull(mgmtTenantManagementRestApi + .getTenantConfigurationValue(AUTHENTICATION_MODE_HEADER_ENABLED) + .getBody()).getValue(); + if (ddiCA == null) { + if (Boolean.TRUE.equals(enabled)) { + mgmtTenantManagementRestApi.updateTenantConfiguration(Map.of(AUTHENTICATION_MODE_HEADER_ENABLED, false)); + } + } else { + if (!Boolean.TRUE.equals(enabled)) { + mgmtTenantManagementRestApi.updateTenantConfiguration(Map.of(AUTHENTICATION_MODE_HEADER_ENABLED, true)); + } + final String fingerprint = ddiCA.getFingerprint(); + if (!fingerprint.equals( + Objects.requireNonNull(mgmtTenantManagementRestApi + .getTenantConfigurationValue(AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME) + .getBody()).getValue())) { + mgmtTenantManagementRestApi.updateTenantConfiguration(Map.of(AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, fingerprint)); + } + } } } \ No newline at end of file diff --git a/hawkbit-sdk/pom.xml b/hawkbit-sdk/pom.xml index f9444a84f..ee2fbdeb4 100644 --- a/hawkbit-sdk/pom.xml +++ b/hawkbit-sdk/pom.xml @@ -26,6 +26,7 @@ 4.2.0 13.5 + 1.78.1