Add certificate authentication support in SDK (#2269)
Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -40,5 +40,11 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-hateoas</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk18on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,6 @@ public class Controller {
|
||||
// (target) security token
|
||||
@Nullable
|
||||
private String securityToken;
|
||||
@Nullable
|
||||
private Certificate certificate;
|
||||
}
|
||||
|
||||
@@ -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<Tenant, Controller, RequestInterceptor> 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> T service0(final Class<T> 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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> updateHandler) {
|
||||
Shell(final DdiTenant ddiTenant, final AuthenticationSetupHelper mgmtApi, final Optional<UpdateHandler> 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);
|
||||
|
||||
@@ -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> updateHandler) {
|
||||
Shell(final DdiTenant ddiTenant, final AuthenticationSetupHelper mgmtApi, final Optional<UpdateHandler> updateHandler) {
|
||||
this.ddiTenant = ddiTenant;
|
||||
this.mgmtApi = mgmtApi;
|
||||
this.updateHandler = updateHandler.orElse(null);
|
||||
|
||||
@@ -36,6 +36,5 @@
|
||||
<artifactId>hawkbit-ddi-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -50,5 +50,4 @@ public class DdiTenant {
|
||||
public Optional<DdiController> getController(final String controllerId) {
|
||||
return Optional.ofNullable(controllers.get(controllerId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
<properties>
|
||||
<spring-cloud-starter-openfeign.version>4.2.0</spring-cloud-starter-openfeign.version>
|
||||
<openfeign-hc5.version>13.5</openfeign-hc5.version>
|
||||
<bouncycastle.version>1.78.1</bouncycastle.version>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
|
||||
Reference in New Issue
Block a user