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>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-hateoas</artifactId>
|
<artifactId>spring-boot-starter-hateoas</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcpkix-jdk18on</artifactId>
|
||||||
|
<version>${bouncycastle.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</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
|
// (target) security token
|
||||||
@Nullable
|
@Nullable
|
||||||
private String securityToken;
|
private String securityToken;
|
||||||
|
@Nullable
|
||||||
|
private Certificate certificate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,15 +21,28 @@ import java.lang.reflect.Proxy;
|
|||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
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.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Random;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
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 com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import feign.Client;
|
import feign.Client;
|
||||||
import feign.Contract;
|
import feign.Contract;
|
||||||
@@ -58,29 +71,22 @@ public class HawkbitClient {
|
|||||||
|
|
||||||
private static final String AUTHORIZATION = "Authorization";
|
private static final String AUTHORIZATION = "Authorization";
|
||||||
public static final BiFunction<Tenant, Controller, RequestInterceptor> DEFAULT_REQUEST_INTERCEPTOR_FN =
|
public static final BiFunction<Tenant, Controller, RequestInterceptor> DEFAULT_REQUEST_INTERCEPTOR_FN =
|
||||||
(tenant, controller) ->
|
(tenant, controller) -> controller == null
|
||||||
controller == null
|
? template ->
|
||||||
? template ->
|
template.header(
|
||||||
template.header(
|
AUTHORIZATION,
|
||||||
AUTHORIZATION,
|
"Basic " + Base64.getEncoder().encodeToString(
|
||||||
|
(Objects.requireNonNull(tenant.getUsername(), "User is null!") +
|
||||||
"Basic " +
|
":" +
|
||||||
Base64.getEncoder()
|
Objects.requireNonNull(tenant.getPassword(),"Password is not available!"))
|
||||||
.encodeToString(
|
.getBytes(StandardCharsets.ISO_8859_1)))
|
||||||
(Objects.requireNonNull(tenant.getUsername(), "User is null!") +
|
: template -> {
|
||||||
":" +
|
if (!ObjectUtils.isEmpty(tenant.getGatewayToken())) {
|
||||||
Objects.requireNonNull(tenant.getPassword(),
|
template.header(AUTHORIZATION, "GatewayToken " + tenant.getGatewayToken());
|
||||||
"Password is not available!"))
|
} else if (!ObjectUtils.isEmpty(controller.getSecurityToken())) {
|
||||||
.getBytes(StandardCharsets.ISO_8859_1)))
|
template.header(AUTHORIZATION, "TargetToken " + controller.getSecurityToken());
|
||||||
: template -> {
|
} // else do not send authentication, no auth or certificate based
|
||||||
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());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final ErrorDecoder DEFAULT_ERROR_DECODER_0 = new ErrorDecoder.Default();
|
private static final ErrorDecoder DEFAULT_ERROR_DECODER_0 = new ErrorDecoder.Default();
|
||||||
public static final ErrorDecoder DEFAULT_ERROR_DECODER = (methodKey, response) -> {
|
public static final ErrorDecoder DEFAULT_ERROR_DECODER = (methodKey, response) -> {
|
||||||
final Exception e = DEFAULT_ERROR_DECODER_0.decode(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) {
|
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)
|
return Feign.builder().client(client)
|
||||||
.encoder(encoder)
|
.encoder(encoder)
|
||||||
.decoder(decoder)
|
.decoder(decoder)
|
||||||
@@ -312,4 +331,33 @@ public class HawkbitClient {
|
|||||||
}
|
}
|
||||||
return null;
|
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;
|
package org.eclipse.hawkbit.sdk;
|
||||||
|
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
import org.eclipse.hawkbit.sdk.ca.CA;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
@@ -33,6 +36,16 @@ public class Tenant {
|
|||||||
// gateway token
|
// gateway token
|
||||||
@Nullable
|
@Nullable
|
||||||
private String gatewayToken;
|
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)
|
// amqp settings (if DMF is used)
|
||||||
@Nullable
|
@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.DdiController;
|
||||||
import org.eclipse.hawkbit.sdk.device.DdiTenant;
|
import org.eclipse.hawkbit.sdk.device.DdiTenant;
|
||||||
import org.eclipse.hawkbit.sdk.device.UpdateHandler;
|
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.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@@ -57,8 +57,8 @@ public class DeviceApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
MgmtApi mgmtApi(final Tenant tenant, final HawkbitClient hawkbitClient) {
|
AuthenticationSetupHelper mgmtApi(final Tenant tenant, final HawkbitClient hawkbitClient) {
|
||||||
return new MgmtApi(tenant, hawkbitClient);
|
return new AuthenticationSetupHelper(tenant, hawkbitClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ShellComponent
|
@ShellComponent
|
||||||
@@ -67,10 +67,10 @@ public class DeviceApp {
|
|||||||
private final DdiTenant ddiTenant;
|
private final DdiTenant ddiTenant;
|
||||||
|
|
||||||
private final DdiController device;
|
private final DdiController device;
|
||||||
private final MgmtApi mgmtApi;
|
private final AuthenticationSetupHelper mgmtApi;
|
||||||
|
|
||||||
@SuppressWarnings("java:S3358")
|
@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.ddiTenant = ddiTenant;
|
||||||
this.mgmtApi = mgmtApi;
|
this.mgmtApi = mgmtApi;
|
||||||
String controllerId = System.getProperty("demo.controller.id");
|
String controllerId = System.getProperty("demo.controller.id");
|
||||||
@@ -79,7 +79,7 @@ public class DeviceApp {
|
|||||||
this.device = this.ddiTenant.createController(Controller.builder()
|
this.device = this.ddiTenant.createController(Controller.builder()
|
||||||
.controllerId(controllerId)
|
.controllerId(controllerId)
|
||||||
.securityToken(ObjectUtils.isEmpty(securityToken) ?
|
.securityToken(ObjectUtils.isEmpty(securityToken) ?
|
||||||
(ObjectUtils.isEmpty(ddiTenant.getTenant().getGatewayToken()) ? MgmtApi.randomToken() : securityToken) :
|
(ObjectUtils.isEmpty(ddiTenant.getTenant().getGatewayToken()) ? AuthenticationSetupHelper.randomToken() : securityToken) :
|
||||||
securityToken)
|
securityToken)
|
||||||
.build(),
|
.build(),
|
||||||
updateHandler.orElse(null)).setOverridePollMillis(10_000);
|
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.DdiController;
|
||||||
import org.eclipse.hawkbit.sdk.device.DdiTenant;
|
import org.eclipse.hawkbit.sdk.device.DdiTenant;
|
||||||
import org.eclipse.hawkbit.sdk.device.UpdateHandler;
|
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.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@@ -57,20 +57,20 @@ public class MultiDeviceApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
MgmtApi mgmtApi(final Tenant defaultTenant, final HawkbitClient hawkbitClient) {
|
AuthenticationSetupHelper mgmtApi(final Tenant defaultTenant, final HawkbitClient hawkbitClient) {
|
||||||
return new MgmtApi(defaultTenant, hawkbitClient);
|
return new AuthenticationSetupHelper(defaultTenant, hawkbitClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ShellComponent
|
@ShellComponent
|
||||||
public static class Shell {
|
public static class Shell {
|
||||||
|
|
||||||
private final DdiTenant ddiTenant;
|
private final DdiTenant ddiTenant;
|
||||||
private final MgmtApi mgmtApi;
|
private final AuthenticationSetupHelper mgmtApi;
|
||||||
private final UpdateHandler updateHandler;
|
private final UpdateHandler updateHandler;
|
||||||
|
|
||||||
private boolean setup;
|
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.ddiTenant = ddiTenant;
|
||||||
this.mgmtApi = mgmtApi;
|
this.mgmtApi = mgmtApi;
|
||||||
this.updateHandler = updateHandler.orElse(null);
|
this.updateHandler = updateHandler.orElse(null);
|
||||||
|
|||||||
@@ -36,6 +36,5 @@
|
|||||||
<artifactId>hawkbit-ddi-api</artifactId>
|
<artifactId>hawkbit-ddi-api</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</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.DdiDeploymentBase;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiUpdateMode;
|
import org.eclipse.hawkbit.ddi.json.model.DdiUpdateMode;
|
||||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRootControllerRestApi;
|
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.Controller;
|
||||||
import org.eclipse.hawkbit.sdk.HawkbitClient;
|
import org.eclipse.hawkbit.sdk.HawkbitClient;
|
||||||
import org.eclipse.hawkbit.sdk.Tenant;
|
import org.eclipse.hawkbit.sdk.Tenant;
|
||||||
@@ -64,6 +65,8 @@ public class DdiController {
|
|||||||
private final boolean downloadAuthenticationEnabled;
|
private final boolean downloadAuthenticationEnabled;
|
||||||
private final String gatewayToken;
|
private final String gatewayToken;
|
||||||
private final String targetSecurityToken;
|
private final String targetSecurityToken;
|
||||||
|
private final Certificate certificate;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
private long overridePollMillis = -1; // -1 means disabled
|
private long overridePollMillis = -1; // -1 means disabled
|
||||||
@@ -90,6 +93,7 @@ public class DdiController {
|
|||||||
downloadAuthenticationEnabled = tenant.isDownloadAuthenticationEnabled();
|
downloadAuthenticationEnabled = tenant.isDownloadAuthenticationEnabled();
|
||||||
this.controllerId = controller.getControllerId();
|
this.controllerId = controller.getControllerId();
|
||||||
this.targetSecurityToken = controller.getSecurityToken();
|
this.targetSecurityToken = controller.getSecurityToken();
|
||||||
|
this.certificate = controller.getCertificate();
|
||||||
this.updateHandler = updateHandler == null ? UpdateHandler.SKIP : updateHandler;
|
this.updateHandler = updateHandler == null ? UpdateHandler.SKIP : updateHandler;
|
||||||
ddiApi = hawkbitClient.ddiService(DdiRootControllerRestApi.class, tenant, controller);
|
ddiApi = hawkbitClient.ddiService(DdiRootControllerRestApi.class, tenant, controller);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,5 +50,4 @@ public class DdiTenant {
|
|||||||
public Optional<DdiController> getController(final String controllerId) {
|
public Optional<DdiController> getController(final String controllerId) {
|
||||||
return Optional.ofNullable(controllers.get(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.mgmt.rest.api.MgmtTenantManagementRestApi;
|
||||||
import org.eclipse.hawkbit.sdk.HawkbitClient;
|
import org.eclipse.hawkbit.sdk.HawkbitClient;
|
||||||
import org.eclipse.hawkbit.sdk.Tenant;
|
import org.eclipse.hawkbit.sdk.Tenant;
|
||||||
|
import org.eclipse.hawkbit.sdk.ca.CA;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Management Api Interface
|
* Helper for authentication setup
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@AllArgsConstructor
|
@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_KEY = "authentication.gatewaytoken.key";
|
||||||
private static final String AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED = "authentication.gatewaytoken.enabled";
|
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_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();
|
private static final Random RND = new SecureRandom();
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private final Tenant tenant;
|
private final Tenant tenant;
|
||||||
@NonNull
|
@NonNull
|
||||||
@@ -50,42 +55,47 @@ public class MgmtApi {
|
|||||||
return Base64.getEncoder().encodeToString(rnd);
|
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
|
// 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
|
// and the target token shall be set to the one from the DDI controller instance
|
||||||
public void setupTargetAuthentication() {
|
public void setupTargetAuthentication() {
|
||||||
final MgmtTenantManagementRestApi mgmtTenantManagementRestApi =
|
final MgmtTenantManagementRestApi mgmtTenantManagementRestApi = hawkbitClient.mgmtService(MgmtTenantManagementRestApi.class, tenant);
|
||||||
hawkbitClient.mgmtService(MgmtTenantManagementRestApi.class, tenant);
|
|
||||||
final String gatewayToken = tenant.getGatewayToken();
|
final String gatewayToken = tenant.getGatewayToken();
|
||||||
if (ObjectUtils.isEmpty(gatewayToken)) {
|
if (ObjectUtils.isEmpty(gatewayToken)) {
|
||||||
if (!(Boolean.TRUE.equals(Objects.requireNonNull(mgmtTenantManagementRestApi
|
if (!(Boolean.TRUE.equals(Objects.requireNonNull(mgmtTenantManagementRestApi
|
||||||
.getTenantConfigurationValue(AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED)
|
.getTenantConfigurationValue(AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED)
|
||||||
.getBody()).getValue()))) {
|
.getBody()).getValue()))) {
|
||||||
mgmtTenantManagementRestApi.updateTenantConfiguration(
|
mgmtTenantManagementRestApi.updateTenantConfiguration(Map.of(AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED, true));
|
||||||
Map.of(AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED, true)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!(Boolean.TRUE.equals(Objects.requireNonNull(mgmtTenantManagementRestApi
|
if (!(Boolean.TRUE.equals(Objects.requireNonNull(mgmtTenantManagementRestApi
|
||||||
.getTenantConfigurationValue(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED)
|
.getTenantConfigurationValue(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED)
|
||||||
.getBody()).getValue()))) {
|
.getBody()).getValue()))) {
|
||||||
mgmtTenantManagementRestApi.updateTenantConfiguration(
|
mgmtTenantManagementRestApi.updateTenantConfiguration(Map.of(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, true));
|
||||||
Map.of(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, true)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (!gatewayToken.equals(
|
if (!gatewayToken.equals(
|
||||||
Objects.requireNonNull(mgmtTenantManagementRestApi
|
Objects.requireNonNull(mgmtTenantManagementRestApi
|
||||||
.getTenantConfigurationValue(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY)
|
.getTenantConfigurationValue(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY)
|
||||||
.getBody()).getValue())) {
|
.getBody()).getValue())) {
|
||||||
mgmtTenantManagementRestApi.updateTenantConfiguration(
|
mgmtTenantManagementRestApi.updateTenantConfiguration(Map.of(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, gatewayToken));
|
||||||
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) {
|
public String setupTargetToken(final String controllerId, String securityTargetToken) {
|
||||||
if (ObjectUtils.isEmpty(tenant.getGatewayToken())) {
|
if (ObjectUtils.isEmpty(tenant.getGatewayToken())) {
|
||||||
final MgmtTargetRestApi mgmtTargetRestApi = hawkbitClient.mgmtService(MgmtTargetRestApi.class, tenant);
|
final MgmtTargetRestApi mgmtTargetRestApi = hawkbitClient.mgmtService(MgmtTargetRestApi.class, tenant);
|
||||||
@@ -96,15 +106,13 @@ public class MgmtApi {
|
|||||||
if (ObjectUtils.isEmpty(target.getSecurityToken())) {
|
if (ObjectUtils.isEmpty(target.getSecurityToken())) {
|
||||||
// generate random to set to tha existing target without configured security token
|
// generate random to set to tha existing target without configured security token
|
||||||
securityTargetToken = randomToken();
|
securityTargetToken = randomToken();
|
||||||
mgmtTargetRestApi.updateTarget(controllerId,
|
mgmtTargetRestApi.updateTarget(controllerId, new MgmtTargetRequestBody().setSecurityToken(securityTargetToken));
|
||||||
new MgmtTargetRequestBody().setSecurityToken(securityTargetToken));
|
|
||||||
} else {
|
} else {
|
||||||
securityTargetToken = target.getSecurityToken();
|
securityTargetToken = target.getSecurityToken();
|
||||||
}
|
}
|
||||||
} else if (!securityTargetToken.equals(target.getSecurityToken())) {
|
} else if (!securityTargetToken.equals(target.getSecurityToken())) {
|
||||||
// update target's with the security token (since it doesn't match)
|
// update target's with the security token (since it doesn't match)
|
||||||
mgmtTargetRestApi.updateTarget(controllerId,
|
mgmtTargetRestApi.updateTarget(controllerId, new MgmtTargetRequestBody().setSecurityToken(securityTargetToken));
|
||||||
new MgmtTargetRequestBody().setSecurityToken(securityTargetToken));
|
|
||||||
}
|
}
|
||||||
} catch (final FeignException.NotFound e) {
|
} catch (final FeignException.NotFound e) {
|
||||||
if (ObjectUtils.isEmpty(securityTargetToken)) {
|
if (ObjectUtils.isEmpty(securityTargetToken)) {
|
||||||
@@ -112,16 +120,35 @@ public class MgmtApi {
|
|||||||
}
|
}
|
||||||
// create target with the security token
|
// create target with the security token
|
||||||
mgmtTargetRestApi.createTargets(List.of(
|
mgmtTargetRestApi.createTargets(List.of(
|
||||||
new MgmtTargetRequestBody()
|
new MgmtTargetRequestBody().setControllerId(controllerId).setSecurityToken(securityTargetToken)));
|
||||||
.setControllerId(controllerId)
|
|
||||||
.setSecurityToken(securityTargetToken)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return securityTargetToken;
|
return securityTargetToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteController(final String controllerId) {
|
// sets up a target token and returns it
|
||||||
hawkbitClient.mgmtService(MgmtTargetRestApi.class, tenant).deleteTarget(controllerId);
|
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>
|
<properties>
|
||||||
<spring-cloud-starter-openfeign.version>4.2.0</spring-cloud-starter-openfeign.version>
|
<spring-cloud-starter-openfeign.version>4.2.0</spring-cloud-starter-openfeign.version>
|
||||||
<openfeign-hc5.version>13.5</openfeign-hc5.version>
|
<openfeign-hc5.version>13.5</openfeign-hc5.version>
|
||||||
|
<bouncycastle.version>1.78.1</bouncycastle.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
|
|||||||
Reference in New Issue
Block a user