diff --git a/.gitignore b/.gitignore
index eb1e1b535..ab1f74c70 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,7 @@ wrapper
/mvnw.cmd
/orchestrator/target
/serviceregistry/target
+/certificate-authority/target
/.settings
/authorization/.settings
/core-common/.settings
diff --git a/README.md b/README.md
index c1a70b759..6aa500b7c 100644
--- a/README.md
+++ b/README.md
@@ -74,7 +74,14 @@ Please be aware, that 4.1.3 is __NOT__ backwards compatible with 4.1.2. If you h
* [Client](#gateway_endpoints_client)
* [Private](#gateway_endpoints_private)
* [Management](#gateway_endpoints_mgmt)
- 6. [QoS Monitor (Quality of Service Monitor)](#qos_monitor)
+ 7. [Certificate Authority](#ca)
+ * [System Design Description Overview](#ca_sdd)
+ * [Services and Use Cases](#ca_usecases)
+ * [Endpoints](#ca_endpoints)
+ * [Client](#ca_endpoints_client)
+ * [Private](#ca_endpoints_private)
+ * [Management](#ca_endpoints_mgmt)
+ 8. [QoS Monitor (Quality of Service Monitor)](#qos_monitor)
* [System Design Description Overview](#qos_monitor_sdd)
* [Services and Use Cases](#qos_monitor_usecases)
* [Endpoints](#qos_monitor_endpoints)
@@ -6273,6 +6280,106 @@ __ActiveSession__ is the output.
| `responseControlQueue` | control queue of response messaging through the the Relay |
| `sessionStartedAt` | Time stamp of session start |
+
+
+# Certificate Authority
+
+
+
+## System Design Description Overview
+
+The purpose of the Certificate Authority supporting core system is issuing signed certificates to be used in the local cloud.
+
+
+
+## Provided services
+
+The Certificate Authority provides the following services:
+* [Echo](#ca_endpoints_get_echo)
+* [Certificate signing](#ca_endpoints_sign)
+
+
+
+## Use cases
+
+The main use case of the Certificate Authority is to issue signed certificates to new consumers coming via the Onboarding Controller.
+
+
+
+## Endpoints
+
+
+
+### Client endpoint description
+
+| Function | URL subpath | Method | Input | Output |
+| -------- | ----------- | ------ | ----- | ------ |
+| [Echo](#ca_endpoints_get_echo) | /echo | GET | - | OK |
+
+
+
+### Private endpoint description
+
+| Function | URL subpath | Method | Input | Output |
+| -------- | ----------- | ------ | ----- | ------ |
+| [Sign CSR with the Cloud Certificate](#ca_endpoints_sign) | /sign | POST | CertificateSigningRequest | [CertificateSigningResponse](#ca_certificate_signing_response) |
+
+
+
+### Echo
+```
+GET /certificate-authority/echo
+```
+
+Returns a "Got it" message with the purpose of testing the core service availability.
+
+
+
+### Sign CSR with the Cloud Certificate
+
+```
+POST /certificate-authority/sign
+```
+
+Returns the whole certificate chain beginning with the newly generated leaf certificate and ending with the root certificate.
+
+Each certificate's issuer is the same as the subject of the following one. The issuer of the root certificate is the same as the subject.
+
+__Sign CSR with the Cloud Certificate__ inputs:
+
+`https://ca_ip:ca_port/certificate-authority/sign`
+
+| __Sign CSR with the Cloud Certificate__ query parameters |
+| ------------------------------------------------------- |
+
+| Parameter | Description | Necessity | Format/Limitations |
+| --------- | ----------- | --------- | ----------- |
+| `encodedCSR` | PKCS #10 Certificate Signing Request | mandatory | Base64 encoded CSR |
+
+
+
+__Sign CSR with the Cloud Certificate__ output :
+
+```json
+{
+ "certificateChain": [
+ "",
+ "",
+ ""
+ ]
+}
+```
+
+| Field | Description |
+| ----- | ----------- |
+| `certificateChain` | The whole certificate chain in an array of PEM encoded strings |
+
+
+
+### Management endpoint description
+
+The Certificate Authority does not have management endpoints.
+
# QOS MONITOR (QUALITY OF SERVICE MONITOR)
diff --git a/certificate-authority/.classpath b/certificate-authority/.classpath
new file mode 100644
index 000000000..6e0021feb
--- /dev/null
+++ b/certificate-authority/.classpath
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/certificate-authority/.project b/certificate-authority/.project
new file mode 100644
index 000000000..9b4a8b591
--- /dev/null
+++ b/certificate-authority/.project
@@ -0,0 +1,23 @@
+
+
+ arrowhead-certificate-authority
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/certificate-authority/pom.xml b/certificate-authority/pom.xml
new file mode 100644
index 000000000..257563a28
--- /dev/null
+++ b/certificate-authority/pom.xml
@@ -0,0 +1,153 @@
+
+ 4.0.0
+
+ eu.arrowhead
+ core
+ ${revision}
+
+
+ arrowhead-certificate-authority
+ jar
+ Arrowhead Certificate Authority
+ Certificate Authority core service
+
+
+
+ eu.arrowhead
+ arrowhead-core-common
+ ${revision}
+
+
+
+ org.bouncycastle
+ bcprov-jdk15on
+ ${bouncy.castle.version}
+
+
+
+ org.bouncycastle
+ bcpkix-jdk15on
+ ${bouncy.castle.version}
+
+
+ org.jetbrains
+ annotations
+ 18.0.0
+ compile
+
+
+
+
+
+
+ maven-resources-plugin
+ ${mvn.resources.version}
+
+
+ copy-resources
+
+ validate
+
+ copy-resources
+
+
+ ${basedir}/target
+
+
+ src/main/resources
+
+ application.properties
+ log4j2.xml
+
+
+
+
+
+
+
+
+
+ jdeb
+ org.vafer
+ 1.8
+
+
+ package
+
+ jdeb
+
+
+ true
+ [YYMMddHHmm].${git.commit.id.abbrev}
+ target/${project.artifactId}_${revision}.deb
+ true
+ ${basedir}/src/deb/control
+
+
+ file
+ ${project.build.directory}/${project.build.finalName}.jar
+
+ perm
+ /usr/share/arrowhead/certificate_authority
+ 755
+
+
+
+
+ link
+ /usr/share/arrowhead/certificate_authority/${project.artifactId}.jar
+ /usr/share/arrowhead/certificate_authority/${project.build.finalName}.jar
+
+
+
+ file
+ ${project.build.directory}/log4j2.xml
+
+ perm
+ /etc/arrowhead/systems/certificate_authority
+ 644
+
+
+
+
+ file
+ ${project.build.directory}/application.properties
+
+ perm
+ /etc/arrowhead/systems/certificate_authority
+ 644
+
+
+
+
+ link
+ /usr/share/arrowhead/certificate_authority/application.properties
+ /etc/arrowhead/systems/certificate_authority/application.properties
+
+
+
+ file
+ ${project.basedir}/src/deb/arrowhead-certificate-authority.service
+
+ perm
+ /etc/systemd/system
+ 644
+
+
+
+
+ file
+ ${project.basedir}/../scripts/certificate_authority_privileges.sql
+
+ perm
+ /usr/share/arrowhead/conf
+
+
+
+
+
+
+
+
+
+
diff --git a/certificate-authority/src/deb/arrowhead-certificate-authority.service b/certificate-authority/src/deb/arrowhead-certificate-authority.service
new file mode 100644
index 000000000..31d62af19
--- /dev/null
+++ b/certificate-authority/src/deb/arrowhead-certificate-authority.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=arrowhead-certificate-authority
+After=network.target mysql.target
+
+[Service]
+WorkingDirectory=/etc/arrowhead/systems/certificate_authority
+ExecStart=/usr/bin/java -Dlog4j.configurationFile=file:/etc/arrowhead/systems/certificate_authority/log4j2.xml -jar /usr/share/arrowhead/certificate_authority/arrowhead-certificate-authority.jar
+ExecStartPost=/bin/bash -c 'sleep 10'
+TimeoutStopSec=5
+Type=simple
+User=arrowhead
+Group=arrowhead
+
+[Install]
+WantedBy=default.target
diff --git a/certificate-authority/src/deb/control/config b/certificate-authority/src/deb/control/config
new file mode 100644
index 000000000..1c83ed2f4
--- /dev/null
+++ b/certificate-authority/src/deb/control/config
@@ -0,0 +1,20 @@
+#!/bin/sh -e
+
+. /usr/share/debconf/confmodule
+
+if logname 2>/dev/null; then
+ USER=$(logname)
+ HOME=$(eval echo "~$USER")
+else
+ USER="root"
+ HOME="/root"
+fi
+
+# Get installation type
+db_input high arrowhead-core-common/db_host || true
+db_input high arrowhead-core-common/mysql_password_ca || true
+db_input high arrowhead-core-common/sr_host || true
+
+# Run dialogs
+db_go || true
+
diff --git a/certificate-authority/src/deb/control/control b/certificate-authority/src/deb/control/control
new file mode 100644
index 000000000..1b7ccbf25
--- /dev/null
+++ b/certificate-authority/src/deb/control/control
@@ -0,0 +1,10 @@
+Package: arrowhead-certificate-authority
+Version: [[version]]
+Section: contrib/java
+Priority: optional
+Architecture: all
+Maintainer: Rajmund Bocsi
+Homepage: http://www.arrowhead.eu
+Description: Arrowhead Certificate Authority
+Distribution: development
+Depends: java-runtime-headless, openssl, virtual-mysql-server, arrowhead-core-common
diff --git a/certificate-authority/src/deb/control/postinst b/certificate-authority/src/deb/control/postinst
new file mode 100644
index 000000000..a47d9de5a
--- /dev/null
+++ b/certificate-authority/src/deb/control/postinst
@@ -0,0 +1,82 @@
+#!/bin/sh
+# postinst script for arrowhead-certificate-authority
+#
+# see: dh_installdeb(1)
+
+set -e
+
+. /usr/share/debconf/confmodule
+
+SYSTEM_NAME="certificate_authority"
+PKG_NAME="arrowhead-certificate-authority"
+
+# summary of how this script can be called:
+# * `configure'
+# * `abort-upgrade'
+# * `abort-remove' `in-favour'
+#
+# * `abort-remove'
+# * `abort-deconfigure' `in-favour'
+# `removing'
+#
+# for details, see https://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+case "$1" in
+ configure)
+ . /usr/share/arrowhead/conf/ahconf.sh
+ SYSTEM_DIR="${AH_SYSTEMS_DIR}/${SYSTEM_NAME}"
+
+ db_get arrowhead-core-common/mysql_password_ca; system_passwd=$RET || true
+ db_unregister arrowhead-core-common/mysql_password_ca
+
+ # Generate password (if required)
+ if [ -z "${system_passwd}" ]; then
+ system_passwd="$(openssl rand -base64 12)"
+ fi
+
+ echo "Configuring MySQL database..." >&2
+ ah_db_tables_and_user ${SYSTEM_NAME} ${SYSTEM_NAME}_privileges.sql ${system_passwd}
+
+ if [ ! -d "${SYSTEM_DIR}" ]; then
+ mkdir -p ${SYSTEM_DIR}
+ fi
+
+ ah_ca_keystore ${SYSTEM_NAME}
+
+ mv ${SYSTEM_DIR}/application.properties ${SYSTEM_DIR}/application.properties.orig
+ cat ${SYSTEM_DIR}/application.properties.orig | \
+ sed -r -e "\#^.*spring\.datasource\.url=#s#.*\$#spring.datasource.url=jdbc:mysql://"${db_host}":3306/arrowhead?serverTimezone=Europe/Budapest#" \
+ -e "\#^.*spring\.datasource\.password=#s#.*\$#spring.datasource.password=${system_passwd}#" \
+ -e "\#^.*server\.address=#s#.*\$#server.address=${OWN_IP}#" \
+ -e "\#^.*server\.ssl\.key-store=#s#.*\$#server.ssl.key-store=file:${SYSTEM_DIR}/${SYSTEM_NAME}.p12#" \
+ -e "\#^.*server\.ssl\.key-store-password=#s#.*\$#server.ssl.key-store-password=${AH_PASS_CERT}#" \
+ -e "\#^.*server\.ssl\.key-password=#s#.*\$#server.ssl.key-password=${AH_PASS_CERT}#" \
+ -e "\#^.*server\.ssl\.trust-store=#s#.*\$#server.ssl.trust-store=file:${AH_CONF_DIR}/truststore.p12#" \
+ -e "\#^.*server\.ssl\.trust-store-password=#s#.*\$#server.ssl.trust-store-password=${AH_PASS_CERT}#" > ${SYSTEM_DIR}/application.properties
+
+ rm ${SYSTEM_DIR}/application.properties.orig
+ chown arrowhead:arrowhead ${SYSTEM_DIR}/application.properties
+ chmod 640 ${SYSTEM_DIR}/application.properties
+
+ echo "Starting ${PKG_NAME}..." >&2
+ systemctl daemon-reload
+ systemctl enable ${PKG_NAME}.service
+ systemctl start ${PKG_NAME}.service
+ ;;
+
+ abort-upgrade|abort-remove|abort-deconfigure)
+ ;;
+
+ *)
+ echo "postinst called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/certificate-authority/src/deb/control/postrm b/certificate-authority/src/deb/control/postrm
new file mode 100644
index 000000000..5ec593529
--- /dev/null
+++ b/certificate-authority/src/deb/control/postrm
@@ -0,0 +1,54 @@
+#!/bin/sh
+# postrm script for arrowhead-certificate-authority
+#
+# see: dh_installdeb(1)
+
+set -e
+
+. /usr/share/debconf/confmodule
+
+SYSTEM_NAME="certificate_authority"
+
+# summary of how this script can be called:
+# * `remove'
+# * `purge'
+# * `upgrade'
+# * `failed-upgrade'
+# * `abort-install'
+# * `abort-install'
+# * `abort-upgrade'
+# * `disappear'
+#
+# for details, see https://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+ purge)
+ AH_CONF_DIR="/etc/arrowhead"
+ AH_SYSTEMS_DIR="${AH_CONF_DIR}/systems"
+ SYSTEM_DIR="${AH_SYSTEMS_DIR}/${SYSTEM_NAME}"
+
+ rm -f \
+ /var/log/arrowhead/${SYSTEM_NAME}*.log \
+ ${SYSTEM_DIR}/log4j2.xml \
+ ${SYSTEM_DIR}/application.properties \
+ ${SYSTEM_DIR}/${SYSTEM_NAME}.p12 \
+ rmdir ${SYSTEM_DIR} 2>/dev/null || true
+ db_purge
+ ;;
+ remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
+ ;;
+
+ *)
+ echo "postrm called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/certificate-authority/src/deb/control/preinst b/certificate-authority/src/deb/control/preinst
new file mode 100644
index 000000000..a0a30f7d9
--- /dev/null
+++ b/certificate-authority/src/deb/control/preinst
@@ -0,0 +1,35 @@
+#!/bin/sh
+# preinst script for arrowhead-certificate-authority
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+# * `install'
+# * `install'
+# * `upgrade'
+# * `abort-upgrade'
+# for details, see https://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+ install|upgrade)
+ ;;
+
+ abort-upgrade)
+ ;;
+
+ *)
+ echo "preinst called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/certificate-authority/src/deb/control/prerm b/certificate-authority/src/deb/control/prerm
new file mode 100644
index 000000000..4c2d565a7
--- /dev/null
+++ b/certificate-authority/src/deb/control/prerm
@@ -0,0 +1,45 @@
+#!/bin/sh
+# prerm script for arrowhead-certificate-authority
+#
+# see: dh_installdeb(1)
+
+set -e
+
+PKG_NAME="arrowhead-certificate-authority"
+
+# summary of how this script can be called:
+# * `remove'
+# * `upgrade'
+# * `failed-upgrade'
+# * `remove' `in-favour'
+# * `deconfigure' `in-favour'
+# `removing'
+#
+# for details, see https://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+ remove)
+ systemctl stop ${PKG_NAME}.service
+ systemctl disable ${PKG_NAME}.service
+ ;;
+
+ upgrade|deconfigure)
+ ;;
+
+ failed-upgrade)
+ ;;
+
+ *)
+ echo "prerm called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
diff --git a/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CAProperties.java b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CAProperties.java
new file mode 100644
index 000000000..a466f1fe3
--- /dev/null
+++ b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CAProperties.java
@@ -0,0 +1,24 @@
+package eu.arrowhead.core.certificate_authority;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import eu.arrowhead.common.CommonConstants;
+
+@Component
+public class CAProperties {
+
+ @Value(CommonConstants.$CA_CERT_VALIDITY_NEG_OFFSET_MILLIS)
+ private long certValidityNegativeOffsetMillis;
+
+ @Value(CommonConstants.$CA_CERT_VALIDITY_POS_OFFSET_MILLIS)
+ private long certValidityPositiveOffsetMillis;
+
+ public long getCertValidityNegativeOffsetMillis() {
+ return certValidityNegativeOffsetMillis;
+ }
+
+ public long getCertValidityPositiveOffsetMillis() {
+ return certValidityPositiveOffsetMillis;
+ }
+}
diff --git a/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityApplicationInitListener.java b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityApplicationInitListener.java
new file mode 100644
index 000000000..aa3e97061
--- /dev/null
+++ b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityApplicationInitListener.java
@@ -0,0 +1,20 @@
+package eu.arrowhead.core.certificate_authority;
+
+import java.security.Security;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.stereotype.Component;
+import eu.arrowhead.common.ApplicationInitListener;
+
+@Component
+public class CertificateAuthorityApplicationInitListener extends ApplicationInitListener {
+
+ @Override
+ protected void customInit(ContextRefreshedEvent event) {
+ logger.debug("customInit started...");
+
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+}
diff --git a/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityController.java b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityController.java
new file mode 100644
index 000000000..75608ddb3
--- /dev/null
+++ b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityController.java
@@ -0,0 +1,106 @@
+package eu.arrowhead.core.certificate_authority;
+
+import java.util.List;
+
+import javax.validation.Valid;
+
+import eu.arrowhead.common.CommonConstants;
+import eu.arrowhead.common.CoreCommonConstants;
+import eu.arrowhead.common.Defaults;
+import eu.arrowhead.common.dto.internal.CertificateSigningRequestDTO;
+import eu.arrowhead.common.dto.internal.CertificateSigningResponseDTO;
+import eu.arrowhead.common.exception.BadPayloadException;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import org.apache.http.HttpStatus;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.ObjectError;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@Api(tags = { CoreCommonConstants.SWAGGER_TAG_ALL })
+@CrossOrigin(maxAge = Defaults.CORS_MAX_AGE, allowCredentials = Defaults.CORS_ALLOW_CREDENTIALS,
+ allowedHeaders = { HttpHeaders.ORIGIN, HttpHeaders.CONTENT_TYPE, HttpHeaders.ACCEPT, HttpHeaders.AUTHORIZATION }
+)
+@RestController
+@RequestMapping(CommonConstants.CERTIFICATE_AUTHRORITY_URI)
+public class CertificateAuthorityController {
+
+ //=================================================================================================
+ // members
+ //
+ private static final String GET_CLOUD_COMMON_NAME_HTTP_200_MESSAGE = "Cloud Common Name returned";
+ private static final String SIGN_CERTIFICATE_HTTP_200_MESSAGE = "Successful certificate signing";
+ private static final String SIGN_CERTIFICATE_HTTP_400_MESSAGE = "Invalid Certificate Signing Request";
+
+ private final Logger logger = LogManager.getLogger(CertificateAuthorityController.class);
+
+ @Autowired
+ private CertificateAuthorityService certificateAuthorityService;
+
+ //=================================================================================================
+ // methods
+
+ //-------------------------------------------------------------------------------------------------
+ @ApiOperation(value = "Return an echo message with the purpose of testing the core service availability", response = String.class, tags = { CoreCommonConstants.SWAGGER_TAG_CLIENT })
+ @ApiResponses(value = {
+ @ApiResponse(code = HttpStatus.SC_OK, message = CoreCommonConstants.SWAGGER_HTTP_200_MESSAGE),
+ @ApiResponse(code = HttpStatus.SC_UNAUTHORIZED, message = CoreCommonConstants.SWAGGER_HTTP_401_MESSAGE),
+ @ApiResponse(code = HttpStatus.SC_INTERNAL_SERVER_ERROR, message = CoreCommonConstants.SWAGGER_HTTP_500_MESSAGE)
+ })
+ @GetMapping(path = CommonConstants.ECHO_URI)
+ public String echoService() {
+ return "Got it!";
+ }
+
+ //-------------------------------------------------------------------------------------------------
+ @ApiOperation(value = "Return the cloud's Common Name", response = String.class, tags = { CoreCommonConstants.SWAGGER_TAG_CLIENT })
+ @ApiResponses(value = {
+ @ApiResponse(code = HttpStatus.SC_OK, message = GET_CLOUD_COMMON_NAME_HTTP_200_MESSAGE),
+ @ApiResponse(code = HttpStatus.SC_UNAUTHORIZED, message = CoreCommonConstants.SWAGGER_HTTP_401_MESSAGE),
+ @ApiResponse(code = HttpStatus.SC_INTERNAL_SERVER_ERROR, message = CoreCommonConstants.SWAGGER_HTTP_500_MESSAGE)
+ })
+ @GetMapping(path = CommonConstants.OP_CA_CLOUD_COMMON_NAME_URI)
+ public String getCloudCommonName() {
+ logger.debug("getCloudCommonName started ...");
+
+ return certificateAuthorityService.getCloudCommonName();
+ }
+
+ //-------------------------------------------------------------------------------------------------
+ @ApiOperation(value = "Return a signed certificate", response = CertificateSigningResponseDTO.class, tags = { CoreCommonConstants.SWAGGER_TAG_CLIENT })
+ @ApiResponses(value = {
+ @ApiResponse(code = HttpStatus.SC_OK, message = SIGN_CERTIFICATE_HTTP_200_MESSAGE),
+ @ApiResponse(code = HttpStatus.SC_BAD_REQUEST, message = SIGN_CERTIFICATE_HTTP_400_MESSAGE),
+ @ApiResponse(code = HttpStatus.SC_UNAUTHORIZED, message = CoreCommonConstants.SWAGGER_HTTP_401_MESSAGE),
+ @ApiResponse(code = HttpStatus.SC_INTERNAL_SERVER_ERROR, message = CoreCommonConstants.SWAGGER_HTTP_500_MESSAGE)
+ })
+ @PostMapping(path = CommonConstants.OP_CA_SIGN_CERTIFICATE_URI, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseBody
+ public CertificateSigningResponseDTO signCertificate(@Valid @RequestBody final CertificateSigningRequestDTO request, BindingResult bindingResult) {
+ handleBindingResult(bindingResult);
+ return certificateAuthorityService.signCertificate(request);
+ }
+
+ private void handleBindingResult(BindingResult bindingResult) {
+ if (bindingResult.hasErrors()) {
+ final List allErrors = bindingResult.getAllErrors();
+ for (ObjectError error : allErrors) {
+ logger.debug(error.toString());
+ }
+ throw new BadPayloadException(allErrors.get(0).getDefaultMessage());
+ }
+ }
+}
diff --git a/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityMain.java b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityMain.java
new file mode 100644
index 000000000..a984109d5
--- /dev/null
+++ b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityMain.java
@@ -0,0 +1,28 @@
+package eu.arrowhead.core.certificate_authority;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+import eu.arrowhead.common.CommonConstants;
+import eu.arrowhead.common.CoreCommonConstants;
+import eu.arrowhead.common.database.repository.RefreshableRepositoryImpl;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+@SpringBootApplication
+@ComponentScan(CommonConstants.BASE_PACKAGE)
+@EntityScan(CoreCommonConstants.DATABASE_ENTITY_PACKAGE)
+@EnableJpaRepositories(basePackages = CoreCommonConstants.DATABASE_REPOSITORY_PACKAGE, repositoryBaseClass = RefreshableRepositoryImpl.class)
+@EnableSwagger2
+public class CertificateAuthorityMain {
+
+ //=================================================================================================
+ // methods
+
+ //-------------------------------------------------------------------------------------------------
+ public static void main(final String[] args) {
+ SpringApplication.run(CertificateAuthorityMain.class, args);
+ }
+}
\ No newline at end of file
diff --git a/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityService.java b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityService.java
new file mode 100644
index 000000000..55f6a2f9d
--- /dev/null
+++ b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityService.java
@@ -0,0 +1,128 @@
+package eu.arrowhead.core.certificate_authority;
+
+import eu.arrowhead.common.SSLProperties;
+import eu.arrowhead.common.Utilities;
+import eu.arrowhead.common.dto.internal.CertificateSigningRequestDTO;
+import eu.arrowhead.common.dto.internal.CertificateSigningResponseDTO;
+import eu.arrowhead.common.exception.InvalidParameterException;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.ServiceConfigurationError;
+
+@Service
+public class CertificateAuthorityService {
+
+ private static final Logger logger = LogManager.getLogger(CertificateAuthorityService.class);
+
+ @Autowired
+ private SSLProperties sslProperties;
+
+ @Autowired
+ private CAProperties caProperties;
+
+ private SecureRandom random;
+ private KeyStore keyStore;
+
+ private X509Certificate rootCertificate;
+ private X509Certificate cloudCertificate;
+ private String cloudCommonName;
+
+ @PostConstruct
+ private void init() {
+ random = new SecureRandom();
+ random.reseed();
+ keyStore = getKeyStore();
+
+ rootCertificate = Utilities.getRootCertFromKeyStore(keyStore);
+ cloudCertificate = Utilities.getCloudCertFromKeyStore(keyStore);
+ cloudCommonName = CertificateAuthorityUtils.getCloudCommonName(cloudCertificate);
+ }
+
+ public String getCloudCommonName() {
+ return cloudCommonName;
+ }
+
+ public CertificateSigningResponseDTO signCertificate(CertificateSigningRequestDTO request) {
+ verifyCertificateSigningRequest(request);
+
+ final JcaPKCS10CertificationRequest csr = CertificateAuthorityUtils.decodePKCS10CSR(request);
+ CertificateAuthorityUtils.checkCommonName(csr, cloudCommonName);
+ CertificateAuthorityUtils.checkCsrSignature(csr);
+
+ logger.info("Signing certificate for " + csr.getSubject().toString() + "...");
+
+ final PrivateKey cloudPrivateKey = Utilities.getPrivateKey(keyStore, cloudCommonName,
+ sslProperties.getKeyPassword());
+
+ final X509Certificate clientCertificate = buildCertificate(csr, cloudPrivateKey, cloudCertificate);
+ final List encodedCertificateChain = buildEncodedCertificateChain(clientCertificate);
+
+ return new CertificateSigningResponseDTO(encodedCertificateChain);
+ }
+
+ private KeyStore getKeyStore() {
+ try {
+ final KeyStore keystore = KeyStore.getInstance(sslProperties.getKeyStoreType());
+ keystore.load(sslProperties.getKeyStore().getInputStream(),
+ sslProperties.getKeyStorePassword().toCharArray());
+ return keystore;
+ } catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException e) {
+ throw new ServiceConfigurationError("Cannot open keystore: " + e.getMessage());
+ }
+ }
+
+ private static void verifyCertificateSigningRequest(CertificateSigningRequestDTO request) {
+ if (request == null) {
+ logger.error("CertificateSigningRequest cannot be null");
+ throw new InvalidParameterException("CertificateSigningRequest cannot be null");
+ }
+
+ final String encodedCSR = request.getEncodedCSR();
+
+ if (encodedCSR == null || encodedCSR.isEmpty()) {
+ logger.error("CertificateSigningRequest cannot be empty");
+ throw new InvalidParameterException("CertificateSigningRequest cannot be empty");
+ }
+ }
+
+ private List buildEncodedCertificateChain(X509Certificate clientCertificate) {
+ final ArrayList encodedCertificateChain = new ArrayList<>();
+ encodedCertificateChain.add(CertificateAuthorityUtils.encodeCertificate(clientCertificate));
+ encodedCertificateChain.add(CertificateAuthorityUtils.encodeCertificate(cloudCertificate));
+ encodedCertificateChain.add(CertificateAuthorityUtils.encodeCertificate(rootCertificate));
+ return encodedCertificateChain;
+ }
+
+ private X509Certificate buildCertificate(JcaPKCS10CertificationRequest csr, PrivateKey cloudPrivateKey,
+ X509Certificate cloudCertificate) {
+ final ZonedDateTime now = LocalDateTime.now().atZone(ZoneId.systemDefault());
+ final Date validFrom = Date.from(
+ now.minus(Duration.ofMillis(caProperties.getCertValidityPositiveOffsetMillis())).toInstant());
+ final Date validUntil = Date.from(
+ now.plus(Duration.ofMillis(caProperties.getCertValidityPositiveOffsetMillis())).toInstant());
+
+ return CertificateAuthorityUtils.buildCertificate(csr, cloudPrivateKey, cloudCertificate, validFrom, validUntil,
+ random);
+ }
+}
diff --git a/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityUtils.java b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityUtils.java
new file mode 100644
index 000000000..ac012b205
--- /dev/null
+++ b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityUtils.java
@@ -0,0 +1,215 @@
+package eu.arrowhead.core.certificate_authority;
+
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Date;
+import java.util.List;
+import java.util.ServiceConfigurationError;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.bouncycastle.asn1.pkcs.Attribute;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.x500.RDN;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x500.style.IETFUtils;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.Extensions;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.cert.CertIOException;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.ContentVerifierProvider;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
+import org.bouncycastle.pkcs.PKCSException;
+import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
+
+import eu.arrowhead.common.Utilities;
+import eu.arrowhead.common.dto.internal.CertificateSigningRequestDTO;
+import eu.arrowhead.common.exception.AuthException;
+import eu.arrowhead.common.exception.BadPayloadException;
+import eu.arrowhead.common.exception.DataNotFoundException;
+import eu.arrowhead.common.exception.InvalidParameterException;
+
+class CertificateAuthorityUtils {
+
+ private static final Logger logger = LogManager.getLogger(CertificateAuthorityService.class);
+
+ private static final String PROVIDER = "BC";
+ private static final String SIGNATURE_ALGORITHM = "SHA512withRSA";
+
+ static JcaPKCS10CertificationRequest decodePKCS10CSR(CertificateSigningRequestDTO csr) {
+ try {
+ final byte[] csrBytes = Base64.getDecoder().decode(csr.getEncodedCSR());
+ return new JcaPKCS10CertificationRequest(csrBytes);
+ } catch (Exception e) {
+ logger.error("Failed to parse request as a PKCS10 CSR, because: " + e.getMessage());
+ throw new BadPayloadException("Failed to parse request as a PKCS10 CSR", e);
+ }
+ }
+
+ static String encodeCertificate(X509Certificate certificate) {
+ try {
+ return Base64.getEncoder().encodeToString(certificate.getEncoded());
+ } catch (CertificateEncodingException | NullPointerException e) {
+ throw new AuthException("Certificate encoding failed! (" + e.getMessage() + ")", e);
+ }
+ }
+
+ static String getCloudCommonName(X509Certificate cloudCertificate) {
+ try {
+ final X500Name subject = new JcaX509CertificateHolder(cloudCertificate).getSubject();
+ final RDN cn = subject.getRDNs(BCStyle.CN)[0];
+ return IETFUtils.valueToString(cn.getFirst().getValue());
+ } catch (CertificateEncodingException | NullPointerException e) {
+ logger.error("Cannot get common name from cloud cert, because: " + e.getMessage());
+ throw new ServiceConfigurationError("Cannot get common name from cloud cert.", e);
+ }
+ }
+
+ static void checkCommonName(JcaPKCS10CertificationRequest csr, String cloudCN) {
+ if (csr == null) {
+ throw new BadPayloadException("CSR cannot be null");
+ }
+ if (cloudCN == null || cloudCN.isEmpty()) {
+ throw new BadPayloadException("CloudCN cannot be null");
+ }
+ final String clientCN = Utilities.getCertCNFromSubject(csr.getSubject().toString());
+ if (!Utilities.isKeyStoreCNArrowheadValid(clientCN, cloudCN)) {
+ throw new BadPayloadException(
+ "Certificate request does not have a valid common name! Valid common name: {systemName}."
+ + cloudCN);
+ }
+ }
+
+ static void checkCsrSignature(JcaPKCS10CertificationRequest csr) {
+ try {
+ final ContentVerifierProvider verifierProvider = new JcaContentVerifierProviderBuilder()
+ .setProvider(PROVIDER)
+ .build(csr.getSubjectPublicKeyInfo());
+ if (!csr.isSignatureValid(verifierProvider)) {
+ throw new BadPayloadException("Certificate request has invalid signature! (key pair does not match)");
+ }
+ } catch (OperatorCreationException | PKCSException | NullPointerException e) {
+ throw new BadPayloadException("Encapsulated " + e.getClass().getCanonicalName() + ": " + e.getMessage(), e);
+ }
+ }
+
+ static PublicKey getClientKey(JcaPKCS10CertificationRequest csr) {
+ try {
+ return csr.getPublicKey();
+ } catch (InvalidKeyException | NoSuchAlgorithmException | NullPointerException e) {
+ // This should not be possible after a successful signature verification
+ throw new DataNotFoundException("Extracting the public key from the CSR failed (" + e.getMessage() + ")",
+ e);
+ }
+ }
+
+ static X509Certificate buildCertificate(JcaPKCS10CertificationRequest csr, PrivateKey cloudPrivateKey,
+ X509Certificate cloudCertificate, Date validFrom, Date validUntil, SecureRandom random) {
+
+ final ZonedDateTime now = LocalDateTime.now().atZone(ZoneId.systemDefault());
+ final BigInteger serial = BigInteger.valueOf(now.toInstant().toEpochMilli())
+ .multiply(BigInteger.valueOf(random.nextLong()));
+
+ final PublicKey clientKey = getClientKey(csr);
+
+ X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(cloudCertificate, serial, validFrom,
+ validUntil, csr.getSubject(), clientKey);
+
+ addCertificateExtensions(builder, csr, clientKey, cloudCertificate);
+
+ JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(PROVIDER);
+ try {
+ return new JcaX509CertificateConverter().setProvider(PROVIDER)
+ .getCertificate(builder.build(signerBuilder.build(cloudPrivateKey)));
+ } catch (CertificateException e) {
+ throw new BadPayloadException("Certificate encoding failed! (" + e.getMessage() + ")", e);
+ } catch (OperatorCreationException e) {
+ throw new BadPayloadException("Certificate signing failed! (" + e.getMessage() + ")", e);
+ }
+ }
+
+ /**
+ * Adding the following extensions to the new certificate:
+ *
+ * - The subject alternative name makes possible to use the certificate for
+ * accessing a host via IP address or hostname
+ *
+ * - The subject key identifier provides a hashed value that should uniquely
+ * identify the public key
+ *
+ * - The authority key identifier provides a hashed value that should uniquely
+ * identify the issuer of the certificate
+ *
+ * - And this basic constraint is for forbidding issuing other certificates
+ * under this certificate
+ *
+ */
+ static void addCertificateExtensions(X509v3CertificateBuilder builder, JcaPKCS10CertificationRequest csr,
+ PublicKey clientKey, X509Certificate cloudCertificate) {
+ try {
+ JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
+
+ final List subjectAlternativeNames = getSubjectAlternativeNames(csr);
+ builder.addExtension(Extension.subjectAlternativeName, false,
+ new GeneralNames(subjectAlternativeNames.toArray(new GeneralName[] {})));
+ builder.addExtension(Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(clientKey));
+ builder.addExtension(Extension.authorityKeyIdentifier, false,
+ extUtils.createAuthorityKeyIdentifier(cloudCertificate));
+ builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false));
+ } catch (NoSuchAlgorithmException | CertIOException | CertificateEncodingException | NullPointerException e) {
+ throw new InvalidParameterException("Appending extensions to the certificate failed! (" + e.getMessage() + ")", e);
+ }
+ }
+
+ static List getSubjectAlternativeNames(JcaPKCS10CertificationRequest csr) {
+ if (csr == null) {
+ throw new InvalidParameterException("CSR cannot be null");
+ }
+
+ List alternativeNames = new ArrayList<>();
+ for (final Attribute attribute : csr.getAttributes()) {
+ if (attribute == null) {
+ continue;
+ }
+ if (attribute.getAttrType().equals(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest)) {
+ final Extensions extensions = Extensions.getInstance(attribute.getAttrValues().getObjectAt(0));
+ final GeneralNames generalNames = GeneralNames.fromExtensions(extensions,
+ Extension.subjectAlternativeName);
+ if (generalNames != null && generalNames.getNames() != null && generalNames.getNames().length > 0) {
+ for (final GeneralName name : generalNames.getNames()) {
+ if (name.getTagNo() == GeneralName.dNSName || name.getTagNo() == GeneralName.iPAddress) {
+ alternativeNames.add(name);
+ }
+ }
+ }
+ }
+ }
+
+ return alternativeNames;
+ }
+
+ private CertificateAuthorityUtils() {
+ }
+}
diff --git a/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/security/CAAccessControlFilter.java b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/security/CAAccessControlFilter.java
new file mode 100644
index 000000000..0af563874
--- /dev/null
+++ b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/security/CAAccessControlFilter.java
@@ -0,0 +1,34 @@
+package eu.arrowhead.core.certificate_authority.security;
+
+import java.util.Map;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import eu.arrowhead.common.CommonConstants;
+import eu.arrowhead.common.CoreCommonConstants;
+import eu.arrowhead.common.security.CoreSystemAccessControlFilter;
+
+@Component
+@ConditionalOnProperty(name = CommonConstants.SERVER_SSL_ENABLED, matchIfMissing = true)
+public class CAAccessControlFilter extends CoreSystemAccessControlFilter {
+
+ //=================================================================================================
+ // members
+
+ //=================================================================================================
+ // assistant methods
+
+ //-------------------------------------------------------------------------------------------------
+ @Override
+ protected void checkClientAuthorized(final String clientCN, final String method, final String requestTarget, final String requestJSON, final Map queryParams) {
+ super.checkClientAuthorized(clientCN, method, requestTarget, requestJSON, queryParams);
+
+ final String cloudCN = getServerCloudCN();
+
+ if (requestTarget.contains(CoreCommonConstants.MGMT_URI)) {
+ // Only the local System Operator can use these methods
+ checkIfLocalSystemOperator(clientCN, cloudCN, requestTarget);
+ }
+ }
+}
diff --git a/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/security/CASecurityConfig.java b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/security/CASecurityConfig.java
new file mode 100644
index 000000000..5f129c51d
--- /dev/null
+++ b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/security/CASecurityConfig.java
@@ -0,0 +1,12 @@
+package eu.arrowhead.core.certificate_authority.security;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+
+import eu.arrowhead.common.security.DefaultSecurityConfig;
+
+@Configuration
+@EnableWebSecurity
+public class CASecurityConfig extends DefaultSecurityConfig {
+
+}
\ No newline at end of file
diff --git a/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/swagger/CASwaggerConfig.java b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/swagger/CASwaggerConfig.java
new file mode 100644
index 000000000..ae0b4321e
--- /dev/null
+++ b/certificate-authority/src/main/java/eu/arrowhead/core/certificate_authority/swagger/CASwaggerConfig.java
@@ -0,0 +1,28 @@
+package eu.arrowhead.core.certificate_authority.swagger;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import eu.arrowhead.common.CommonConstants;
+import eu.arrowhead.common.swagger.DefaultSwaggerConfig;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+@EnableSwagger2
+@Configuration
+public class CASwaggerConfig extends DefaultSwaggerConfig {
+
+ //=================================================================================================
+ // methods
+
+ //-------------------------------------------------------------------------------------------------
+ public CASwaggerConfig() {
+ super(CommonConstants.CORE_SYSTEM_CERTIFICATE_AUTHORITY);
+ }
+
+ //-------------------------------------------------------------------------------------------------
+ @Bean
+ public Docket customizeSwagger() {
+ return configureSwaggerForCoreSystem(this.getClass().getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/certificate-authority/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/certificate-authority/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 000000000..af4455de4
--- /dev/null
+++ b/certificate-authority/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -0,0 +1,47 @@
+{"properties": [
+ {
+ "name": "ping_scheduled",
+ "type": "java.lang.Boolean",
+ "description": "A description for 'ping_scheduled'"
+ },
+ {
+ "name": "ping_timeout",
+ "type": "java.lang.Long",
+ "description": "A description for 'ping_timeout'"
+ },
+ {
+ "name": "ping_interval",
+ "type": "java.lang.Long",
+ "description": "A description for 'ping_interval'"
+ },
+ {
+ "name": "ttl_scheduled",
+ "type": "java.lang.Boolean",
+ "description": "A description for 'ttl_scheduled'"
+ },
+ {
+ "name": "ttl_interval",
+ "type": "java.lang.Long",
+ "description": "A description for 'ttl_interval'"
+ },
+ {
+ "name": "disable.hostname.verifier",
+ "type": "java.lang.Boolean",
+ "description": "A description for 'disable.hostname.verifier'"
+ },
+ {
+ "name": "log_all_request_and_response",
+ "type": "java.lang.Boolean",
+ "description": "A description for 'log_all_request_and_response'"
+ },
+ {
+ "name": "use_strict_service_intf_name_verifier",
+ "type": "java.lang.Boolean",
+ "description": "A description for 'use_strict_service_intf_name_verifier'"
+ },
+ {
+ "name": "core_system_name",
+ "type": "java.lang.String",
+ "description": "A description for 'core_system_name'"
+ }
+]}
\ No newline at end of file
diff --git a/certificate-authority/src/main/resources/application.properties b/certificate-authority/src/main/resources/application.properties
new file mode 100644
index 000000000..93f325bd4
--- /dev/null
+++ b/certificate-authority/src/main/resources/application.properties
@@ -0,0 +1,59 @@
+############################################
+### APPLICATION PARAMETERS ###
+############################################
+
+# Database connection (mandatory)
+# Change the server timezone if neccessary
+spring.datasource.url=jdbc:mysql://127.0.0.1:3306/arrowhead?serverTimezone=Europe/Budapest
+spring.datasource.username=certificate_authority
+spring.datasource.password=FsdG6Kgf9QpPfv2
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+
+spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
+# use true only for debugging
+spring.jpa.show-sql=false
+spring.jpa.properties.hibernate.format_sql=true
+spring.jpa.hibernate.ddl-auto=none
+
+# Certificate Authority web-server parameters
+server.address=127.0.0.1
+server.port=8448
+domain.name=127.0.0.1
+domain.port=8448
+
+############################################
+### CUSTOM PARAMETERS ###
+############################################
+
+# Name of the core system
+core_system_name=certificate_authority
+
+# Show all request/response in debug log
+log_all_request_and_response=false
+
+# Service Registry web-server parameters (to register the Certificate-Authority services)
+sr_address=127.0.0.1
+sr_port=8443
+
+############################################
+### SECURE MODE ###
+############################################
+
+# configure secure mode
+
+# Set this to false to disable https mode
+server.ssl.enabled=true
+
+server.ssl.key-store-type=PKCS12
+server.ssl.key-store=classpath:certificates/certificate_authority.p12
+server.ssl.key-store-password=123456
+server.ssl.key-alias=certificate_authority
+server.ssl.key-password=123456
+server.ssl.client-auth=need
+server.ssl.trust-store-type=PKCS12
+server.ssl.trust-store=classpath:certificates/truststore.p12
+server.ssl.trust-store-password=123456
+
+#If true, http client does not check whether the hostname is match one of the server's SAN in its certificate
+#Just for testing, DO NOT USE this feature in production environment
+disable.hostname.verifier=false
diff --git a/certificate-authority/src/main/resources/certificates/certificate_authority.p12 b/certificate-authority/src/main/resources/certificates/certificate_authority.p12
new file mode 100644
index 000000000..57fba65b4
Binary files /dev/null and b/certificate-authority/src/main/resources/certificates/certificate_authority.p12 differ
diff --git a/certificate-authority/src/main/resources/certificates/truststore.p12 b/certificate-authority/src/main/resources/certificates/truststore.p12
new file mode 100644
index 000000000..57c1c5f43
Binary files /dev/null and b/certificate-authority/src/main/resources/certificates/truststore.p12 differ
diff --git a/certificate-authority/src/main/resources/log4j2.xml b/certificate-authority/src/main/resources/log4j2.xml
new file mode 100644
index 000000000..fcce0981a
--- /dev/null
+++ b/certificate-authority/src/main/resources/log4j2.xml
@@ -0,0 +1,46 @@
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${hostName} --- [%15.15t] %-40.40c{1.} : %m%n%ex
+
+ INFO
+ INFO
+ .
+
+
+
+
+
+
+
+ ${LOG_PATTERN}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/certificate-authority/src/main/resources/swagger/swagger-ui.html b/certificate-authority/src/main/resources/swagger/swagger-ui.html
new file mode 100644
index 000000000..4dec726e9
--- /dev/null
+++ b/certificate-authority/src/main/resources/swagger/swagger-ui.html
@@ -0,0 +1,75 @@
+
+
+
+
+
+ Certificate Authority
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/certificate-authority/src/test/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityControllerTest.java b/certificate-authority/src/test/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityControllerTest.java
new file mode 100644
index 000000000..dda6e9b0b
--- /dev/null
+++ b/certificate-authority/src/test/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityControllerTest.java
@@ -0,0 +1,207 @@
+package eu.arrowhead.core.certificate_authority;
+
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.x509;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.head;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+
+import eu.arrowhead.common.dto.internal.CertificateSigningRequestDTO;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = CertificateAuthorityMain.class)
+@AutoConfigureMockMvc
+public class CertificateAuthorityControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ public void testEchoWithoutCert() throws Exception {
+ mockMvc.perform(get("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/certificate-authority/echo")
+ .secure(true))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ public void testEchoWithInvalidCert() throws Exception {
+ mockMvc.perform(get("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/certificate-authority/echo")
+ .secure(true)
+ .with(x509("certificates/notvalid.pem")))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ public void testEchoWithValidCert() throws Exception {
+ mockMvc.perform(get("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/certificate-authority/echo")
+ .secure(true)
+ .with(x509("certificates/valid.pem")))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testGetCloudCommonNameWithoutCert() throws Exception {
+ mockMvc.perform(get("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/certificate-authority/name")
+ .secure(true))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ public void testGetCloudCommonNameWithInvalidCert() throws Exception {
+ mockMvc.perform(get("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/certificate-authority/name")
+ .secure(true)
+ .with(x509("certificates/notvalid.pem")))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ public void testGetCloudCommonNameWithValidCert() throws Exception {
+ mockMvc.perform(get("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/certificate-authority/name")
+ .secure(true)
+ .with(x509("certificates/valid.pem")))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_PLAIN))
+ .andExpect(content().string("testcloud2.aitia.arrowhead.eu"));
+ }
+
+ @Test
+ public void testSignWithoutCert() throws Exception {
+ mockMvc.perform(post("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/certificate-authority/sign")
+ .secure(true))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ public void testSignWithInvalidCert() throws Exception {
+ mockMvc.perform(post("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/certificate-authority/sign")
+ .secure(true)
+ .with(x509("certificates/notvalid.pem")))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ public void testSignGet() throws Exception {
+ mockMvc.perform(get("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/certificate-authority/sign")
+ .secure(true)
+ .with(x509("certificates/valid.pem")))
+ .andExpect(status().isMethodNotAllowed());
+ }
+
+ @Test
+ public void testSignPut() throws Exception {
+ mockMvc.perform(put("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/certificate-authority/sign")
+ .secure(true)
+ .with(x509("certificates/valid.pem")))
+ .andExpect(status().isMethodNotAllowed());
+ }
+
+ @Test
+ public void testSignPatch() throws Exception {
+ mockMvc.perform(patch("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/certificate-authority/sign")
+ .secure(true)
+ .with(x509("certificates/valid.pem")))
+ .andExpect(status().isMethodNotAllowed());
+ }
+
+ @Test
+ public void testSignDelete() throws Exception {
+ mockMvc.perform(delete("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/certificate-authority/sign")
+ .secure(true)
+ .with(x509("certificates/valid.pem")))
+ .andExpect(status().isMethodNotAllowed());
+ }
+
+ @Test
+ public void testSignHead() throws Exception {
+ mockMvc.perform(head("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/certificate-authority/sign")
+ .secure(true)
+ .with(x509("certificates/valid.pem")))
+ .andExpect(status().isMethodNotAllowed());
+ }
+
+ @Test
+ public void testSignOptions() throws Exception {
+ mockMvc.perform(options("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/certificate-authority/sign")
+ .secure(true)
+ .with(x509("certificates/valid.pem")))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSignPostInvalidContentType() throws Exception {
+ mockMvc.perform(post("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/certificate-authority/sign")
+ .secure(true)
+ .with(x509("certificates/valid.pem")))
+ .andExpect(status().isUnsupportedMediaType());
+ }
+
+ @Test
+ public void testSignPostWithNullCSR() throws Exception {
+ mockMvc.perform(post("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/certificate-authority/sign")
+ .secure(true)
+ .with(x509("certificates/valid.pem"))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(asJsonString(new CertificateSigningRequestDTO(null))))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ public void testSignPostWithInvalidCSR() throws Exception {
+ mockMvc.perform(post("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/certificate-authority/sign")
+ .secure(true)
+ .with(x509("certificates/valid.pem"))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(asJsonString(new CertificateSigningRequestDTO("INVALID"))))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ public void testSignPostWithValidCSR() throws Exception {
+ mockMvc.perform(post("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/certificate-authority/sign")
+ .secure(true)
+ .with(x509("certificates/valid.pem"))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ .content(asJsonString(new CertificateSigningRequestDTO(getResourceContent("certificates/valid.csr")))))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.certificateChain").isArray());
+ }
+
+ private static String asJsonString(final Object obj) {
+ try {
+ return new ObjectMapper().writeValueAsString(obj);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static String getResourceContent(String resourcePath) throws IOException {
+ File resource = new ClassPathResource(resourcePath).getFile();
+ return new String(Files.readAllBytes(resource.toPath())).trim();
+ }
+}
diff --git a/certificate-authority/src/test/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityUtilsTest.java b/certificate-authority/src/test/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityUtilsTest.java
new file mode 100644
index 000000000..a11fe32a8
--- /dev/null
+++ b/certificate-authority/src/test/java/eu/arrowhead/core/certificate_authority/CertificateAuthorityUtilsTest.java
@@ -0,0 +1,94 @@
+package eu.arrowhead.core.certificate_authority;
+
+import static org.junit.Assert.assertNotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Base64;
+import java.util.ServiceConfigurationError;
+
+import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
+import org.hamcrest.core.IsInstanceOf;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import eu.arrowhead.common.dto.internal.CertificateSigningRequestDTO;
+import eu.arrowhead.common.exception.AuthException;
+import eu.arrowhead.common.exception.BadPayloadException;
+import eu.arrowhead.common.exception.DataNotFoundException;
+
+@RunWith(SpringRunner.class)
+public class CertificateAuthorityUtilsTest {
+
+ @Rule
+ public ExpectedException thrownException = ExpectedException.none();
+
+ @Test(expected = BadPayloadException.class)
+ public void testDecodePKCS10CSRNull() {
+ CertificateAuthorityUtils.decodePKCS10CSR(null);
+ }
+
+ @Test(expected = BadPayloadException.class)
+ public void testDecodePKCS10CSREmpty() {
+ CertificateAuthorityUtils.decodePKCS10CSR(new CertificateSigningRequestDTO());
+ }
+
+ @Test(expected = BadPayloadException.class)
+ public void testDecodePKCS10CSRInvalidString() {
+ CertificateAuthorityUtils.decodePKCS10CSR(new CertificateSigningRequestDTO("Invalid CSR String"));
+ }
+
+ @Test
+ public void testEncodeCertificateNull() {
+ thrownException.expect(IsInstanceOf.instanceOf(AuthException.class));
+ thrownException.expectCause(IsInstanceOf.instanceOf(NullPointerException.class));
+ CertificateAuthorityUtils.encodeCertificate(null);
+ }
+
+ @Test(expected = ServiceConfigurationError.class)
+ public void testGetCloudCommonNameNullCert() {
+ CertificateAuthorityUtils.getCloudCommonName(null);
+ }
+
+ @Test(expected = BadPayloadException.class)
+ public void testCheckCommonNameNullCSR() {
+ CertificateAuthorityUtils.checkCommonName(null, "cloudCN");
+ }
+
+ @Test(expected = BadPayloadException.class)
+ public void testCheckCommonNameNullCloudCN() throws NullPointerException, IOException {
+ final JcaPKCS10CertificationRequest csr = getResourceCSR("certificates/valid.csr");
+ assertNotNull(csr);
+ CertificateAuthorityUtils.checkCommonName(csr, null);
+ }
+
+ @Test(expected = BadPayloadException.class)
+ public void testCheckCsrSignatureNull() {
+ CertificateAuthorityUtils.checkCsrSignature(null);
+ }
+
+ @Test
+ public void testGetClientKeyNull() {
+ thrownException.expect(DataNotFoundException.class);
+ thrownException.expectCause(IsInstanceOf.instanceOf(NullPointerException.class));
+ CertificateAuthorityUtils.getClientKey(null);
+ }
+
+ private static String getResourceContentAsString(final String resourcePath) throws IOException {
+ final File resource = new ClassPathResource(resourcePath).getFile();
+ return new String(Files.readAllBytes(resource.toPath())).trim();
+ }
+
+ private static JcaPKCS10CertificationRequest getResourceCSR(
+ final String resourcePath)
+ throws IOException, NullPointerException {
+ final String resourceBytes = getResourceContentAsString(resourcePath);
+ final byte[] csrBytes = Base64.getDecoder().decode(resourceBytes);
+ return new JcaPKCS10CertificationRequest(csrBytes);
+ }
+}
diff --git a/certificate-authority/src/test/resources/banner.txt b/certificate-authority/src/test/resources/banner.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/certificate-authority/src/test/resources/certificates/notvalid.pem b/certificate-authority/src/test/resources/certificates/notvalid.pem
new file mode 100644
index 000000000..a70c46837
--- /dev/null
+++ b/certificate-authority/src/test/resources/certificates/notvalid.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDMzCCAhugAwIBAgIEXNlw3zANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxh
+cnJvd2hlYWQuZXUwHhcNMTkwNTEzMTMyNzU5WhcNMjkwNTEzMTMyNzU5WjAoMSYw
+JAYDVQQDDB10ZXN0Y2xvdWQxLmFpdGlhLmFycm93aGVhZC5ldTCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBANTIWrd0mcvxx6LfPG6RvviN5VY1n9uphoYm
+CoRA47scc0duc6kIWsIydEsKD9xEd2WdClbQXVazLSF+OWGocxBuyPaAjgu4DdVY
+pJLgAaeSzbcjqS+A6bB3B6i8A9RxCWwtDJ5EJsUuIOa9yqcfkmdXVOHpNlwycPeH
+FEOIUdUcd+RAL0Fw4h39a3HmTDf4nb2Px/3CWvZk89woxDXURJfyDl2fJyB3kLbp
+4HWBht9YZNw2ZuIT1fO5rfm2XGEaPiUseI4KEwDyhW36HWOxv3oP9lMWjCaeWFg1
+A4q6sbjm+fsEsZJavgPWANINCTxKyy/IclwmxKimS6aDBHHyW6UCAwEAAaN2MHQw
+DwYDVR0TBAgwBgEB/wIBAjBCBgNVHSMEOzA5gBSaipkeMUwesuYttprtxx4xyt07
+/qEbpBkwFzEVMBMGA1UEAwwMYXJyb3doZWFkLmV1ggRc1T6tMB0GA1UdDgQWBBT0
+jA9ICBgevPSr86oEvVJ8uDamJTANBgkqhkiG9w0BAQsFAAOCAQEAiy1OVJulFL2V
+HOYlERD2MJCgIaVKoN6GGxGXQ7mBAjGE+am+dKvbGZ9l5ADRKxKgd1T0hklfTZJU
+j7CHBp1JJ7cj7OkFyountQcdTJ3WVLDMxfRRictRwuEUaKMry4nmcU2WpDTJ+DSV
+H6qW3VlXxJKnGe1M3u14wUx5k0iXr4S2oYkq/WZ9TUkLWKmzVPDlU3x6iSsyZj/5
+1a5o2qNceezzpGhcvoiHwQ0MFm9OLDYrh61Zb4M7jhn2AocHV4f3Y0P0IfdLlXq5
+MIgOK1ds2BekemekJ3VnW/TDj8fY6sxWl8aWFYJ1RhmVWTODiN9xu9XPyzAO5ryk
+zG4AOQa5rQ==
+-----END CERTIFICATE-----
diff --git a/certificate-authority/src/test/resources/certificates/valid.csr b/certificate-authority/src/test/resources/certificates/valid.csr
new file mode 100644
index 000000000..d1d8fc393
--- /dev/null
+++ b/certificate-authority/src/test/resources/certificates/valid.csr
@@ -0,0 +1 @@
+MIIDKTCCAhECAQAwTDEPMA0GA1UECgwGZWlsYWJzMQswCQYDVQQGEwJodTEsMCoGA1UEAwwjc3lzb3AudGVzdGNsb3VkMi5haXRpYS5hcnJvd2hlYWQuZXUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgKgJZOMH5QiLxYLz6bKcDAOkLvh13EIVG3W6CdNJpbYcYljnuZlGZ+38A+HLsL4fJfLOPvsrpMKSv2NfEDKSuQaHJt0nchM26WiVkiYfZXPxYduZy9wqi1069V7E2i7vYsqJEytugjEhEk+1O1rtFuGZXIitNUSUBUNYWNkc/sr3wCGgZrcAOnTFuHSorUGm4Kc8qbrgNtjnbPCvnyZJp7IvsyAT51bBYfH2vhf9Yjv/apAiKjwNO/Hfo8cYq6u3CnFpXSj6BD5DVhwPWZ9IenfG1hACdldEsiNoN3ssZYwbuPcduq6SSNOHIqtQIp+spdAUi0FQnnw8kmryxPGgvAgMBAAGggZcwFAYJKoZIhvcNAQkHMQcMBTI0NTM0MBUGCSqGSIb3DQEJAjEIDAZlaWxhYnMwaAYJKoZIhvcNAQkOMVswWTAPBgNVHREECDAGhwR/AAABMCcGA1UdIwQgMB6AFAQwTQgjkz3eZj0WWqfRyV4jppdPoQCCBFzZYhkwHQYDVR0OBBYEFAVG6MVZFFyHQXW6qlQWA4EfXWSIMA0GCSqGSIb3DQEBCwUAA4IBAQAkRvc+/O/GWx3dg6CgnpWSCFZKnaaD1l+1Rls93qZZ3jP1tukK+7xh+1AcRNnB5h5tNW0Zaa7KrYIS/RX2wsqGRIil4iTmGaa2/MPFY3GAm65891JuF4t0dk/5GcOJjZOAEV603ils9qxZ1np3asQRGiFAXbvL1tr+2iaQR2olfsn00qSw+zYICKPtBAJhUcmxavMrlZPoiCiJzWqQZMNOlYzJitim8N8rMwKZnwmDdgqgjkVrIQXKVh3Q2xw2mGlqPIRToLSgnGtLQ5caNVhBeF1OnfBxziP8SVckazTSlJ8uVm8an6Jt2R6VQf1avuclAxWAwS8CyAn5TpJNzVjL
diff --git a/certificate-authority/src/test/resources/certificates/valid.pem b/certificate-authority/src/test/resources/certificates/valid.pem
new file mode 100644
index 000000000..56eae293d
--- /dev/null
+++ b/certificate-authority/src/test/resources/certificates/valid.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDLzCCAhegAwIBAgIEXPYrATANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQDDB10
+ZXN0Y2xvdWQyLmFpdGlhLmFycm93aGVhZC5ldTAeFw0xOTA2MDQwODI1MzdaFw0y
+MDA2MDMwODI1MzdaMC4xLDAqBgNVBAMMI3N5c29wLnRlc3RjbG91ZDIuYWl0aWEu
+YXJyb3doZWFkLmV1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoCoC
+WTjB+UIi8WC8+mynAwDpC74ddxCFRt1ugnTSaW2HGJY57mZRmft/APhy7C+HyXyz
+j77K6TCkr9jXxAykrkGhybdJ3ITNulolZImH2Vz8WHbmcvcKotdOvVexNou72LKi
+RMrboIxIRJPtTta7RbhmVyIrTVElAVDWFjZHP7K98AhoGa3ADp0xbh0qK1BpuCnP
+Km64DbY52zwr58mSaeyL7MgE+dWwWHx9r4X/WI7/2qQIio8DTvx36PHGKurtwpxa
+V0o+gQ+Q1YcD1mfSHp3xtYQAnZXRLIjaDd7LGWMG7j3HbqukkjThyKrUCKfrKXQF
+ItBUJ58PJJq8sTxoLwIDAQABo1swWTAPBgNVHREECDAGhwR/AAABMCcGA1UdIwQg
+MB6AFAQwTQgjkz3eZj0WWqfRyV4jppdPoQCCBFzZYhkwHQYDVR0OBBYEFAVG6MVZ
+FFyHQXW6qlQWA4EfXWSIMA0GCSqGSIb3DQEBCwUAA4IBAQBUEExzkf2bH4fF/tyP
+r7DxO94aqQOySGJq/25DzfZQOpuK5Ud3MPpXSlxYw+MB5rRDJu0NzGUX+ha4mG3T
+dXxd3X2QaDFR0SYUD4J0hTtmYp9TzgCXnU+J7t6sku/LYFJrCl5m4o124ShZXBgR
+Dj/gDVd4C6InTPhpIPHaD6D97wKyObbC3V1dGiVs5Z5/+wUJdUFUmnfAb1rbX0nV
+0TTKluWTtRT4cjBeIFs7MZiThMx8bgSE4mZIC1ry4PikjAap/oEIQVxbBTbl+5q8
+xhPbPn+JhZwQp39a1oPMEQog0wQhtKvZrYx//H1o3mzRo1ZFzL+R/LCKKElMPMXx
+UHKC
+-----END CERTIFICATE-----
diff --git a/certificate-authority/src/test/resources/log4j2.xml b/certificate-authority/src/test/resources/log4j2.xml
new file mode 100644
index 000000000..931cbfebb
--- /dev/null
+++ b/certificate-authority/src/test/resources/log4j2.xml
@@ -0,0 +1,49 @@
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${hostName} --- [%15.15t] %-40.40c{1.} : %m%n%ex
+
+ OFF
+ OFF
+
+
+
+
+
+
+
+ ${LOG_PATTERN}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core-common/src/deb/ahconf.sh b/core-common/src/deb/ahconf.sh
index 1242632b7..0b2ea2cb3 100644
--- a/core-common/src/deb/ahconf.sh
+++ b/core-common/src/deb/ahconf.sh
@@ -190,6 +190,23 @@ ah_ip_valid () {
fi
}
+ah_subject_alternative_names() {
+ host=${1}
+ ip=${2}
+
+ san="IP:127.0.0.1,DNS:localhost,DNS:${host},IP:${ip}"
+
+ if [ ! -z ${AH_DOMAIN_NAME} ]; then
+ if ah_ip_valid ${AH_DOMAIN_NAME}; then
+ san=${san},IP:${AH_DOMAIN_NAME}
+ else
+ san=${san},DNS:${AH_DOMAIN_NAME}
+ fi
+ fi
+
+ return san
+}
+
ah_cert_signed_system () {
name=${1}
passwd=${2}
@@ -218,15 +235,7 @@ ah_cert_signed_system () {
src_file="${AH_CLOUDS_DIR}/${AH_CLOUD_NAME}.p12"
if [ ! -f "${file}" ]; then
- san="IP:127.0.0.1,DNS:localhost,DNS:${host},IP:${ip}"
-
- if [ ! -z ${AH_DOMAIN_NAME} ]; then
- if ah_ip_valid ${AH_DOMAIN_NAME}; then
- san=${san},IP:${AH_DOMAIN_NAME}
- else
- san=${san},DNS:${AH_DOMAIN_NAME}
- fi
- fi
+ san=ah_subject_alternative_names ${host} ${ip}
ah_cert ${path} ${name} "${name}.${AH_CLOUD_NAME}.${AH_OPERATOR}.arrowhead.eu" ${passwd}
@@ -266,6 +275,94 @@ ah_cert_signed_system () {
fi
}
+# Create the keystore for the CA system
+# Apart from being a regular Arrowhead compliant system keystore,
+# it also has to contain the private key of the cloud
+ah_ca_keystore () {
+ ca_sys_name=${1} # name of the CA system
+ host=${2} # hostname of the CA system (default: `hostname`)
+ ip=${3} # IP address of the CA system (default: $OWN_IP)
+ cloud_keystore=${4} # path of the cloud keystore (default: "${AH_CLOUDS_DIR}/${AH_CLOUD_NAME}.p12")
+ ca_password=${5} # password used in the CA keystore (default: $AH_PASS_CERT)
+ cloud_password=${6} # password used in the cloud keystore (default: $AH_PASS_CERT)
+ cloud_alias_src=${7} # cloud cert alias in the cloud keystore (default: $AH_CLOUD_NAME)
+ cloud_alias_dst=${8} # cloud cert alias in the CA keystore (default: $AH_CLOUD_NAME)
+
+ if [ -z ${host} ]; then
+ host=`hostname`
+ fi
+
+ if [ -z ${ip} ]; then
+ ip=${OWN_IP}
+ fi
+
+ if [ -z ${cloud_keystore} ]; then
+ cloud_keystore="${AH_CLOUDS_DIR}/${AH_CLOUD_NAME}.p12"
+ fi
+
+ if [ -z ${ca_password} ]; then
+ ca_password=${AH_PASS_CERT}
+ fi
+
+ if [ -z ${cloud_password} ]; then
+ cloud_password=${ca_password}
+ fi
+
+ if [ -z ${cloud_alias_src} ]; then
+ cloud_alias_src=${AH_CLOUD_NAME}
+ fi
+
+ if [ -z ${cloud_alias_dst} ]; then
+ cloud_alias_dst=${cloud_alias_src}
+ fi
+
+ ca_sys_dir="${AH_SYSTEMS_DIR}/${ca_sys_name}"
+ ca_keystore="${ca_sys_dir}/${ca_sys_name}.p12"
+
+ if [ ! -f "${ca_keystore}" ]; then
+ san=ah_subject_alternative_names ${host} ${ip}
+
+ # generate CA system keypair
+ ah_cert ${ca_sys_dir} ${ca_sys_name} "${ca_sys_name}.${AH_CLOUD_NAME}.${AH_OPERATOR}.arrowhead.eu" ${ca_password}
+
+ # import cloud keypair
+ # this is necessary, because CA signs system certificates with the cloud cert
+ keytool -importkeystore \
+ -srckeypass ${cloud_password} \
+ -srcstorepass ${cloud_password} \
+ -destkeypass ${ca_password} \
+ -deststorepass ${ca_password} \
+ -srcalias ${cloud_alias_src} \
+ -destalias ${cloud_alias_dst} \
+ -srckeystore "${cloud_keystore}" \
+ -destkeystore "${ca_keystore}" \
+ -deststoretype PKCS12
+
+ # sign CA system cert with the cloud cert
+ keytool -certreq \
+ -alias ${ca_sys_name} \
+ -keypass ${ca_password} \
+ -keystore "${ca_keystore}" \
+ -storepass ${ca_password} \
+ | keytool -gencert \
+ -alias ${cloud_alias_dst} \
+ -keypass ${ca_password} \
+ -keystore "${ca_keystore}" \
+ -storepass ${ca_password} \
+ -validity 3650 \
+ -ext SubjectAlternativeName=${san} \
+ | keytool -importcert \
+ -alias ${ca_sys_name} \
+ -keypass ${ca_password} \
+ -keystore "${ca_keystore}" \
+ -storepass ${ca_password} \
+ -noprompt
+
+ # import root certificate
+ ah_cert_import "${AH_CONF_DIR}" "master" "${ca_sys_dir}" ${ca_sys_name} ${ca_password}
+ fi
+}
+
ah_cert_trust () {
dst_path=${1}
src_path=${2}
@@ -359,4 +456,4 @@ ah_transform_log_file () {
rm ${log_path}/log4j2.xml.orig
chown :arrowhead ${log_path}/log4j2.xml
chmod 640 ${log_path}/log4j2.xml
-}
\ No newline at end of file
+}
diff --git a/core-common/src/main/java/eu/arrowhead/common/ApplicationInitListener.java b/core-common/src/main/java/eu/arrowhead/common/ApplicationInitListener.java
index d1ecee8d7..24fabea64 100644
--- a/core-common/src/main/java/eu/arrowhead/common/ApplicationInitListener.java
+++ b/core-common/src/main/java/eu/arrowhead/common/ApplicationInitListener.java
@@ -187,12 +187,15 @@ private void obtainKeys(final KeyStore keyStore, final ApplicationContext appCon
logger.debug("obtainKeys started...");
@SuppressWarnings("unchecked")
final Map context = appContext.getBean(CommonConstants.ARROWHEAD_CONTEXT, Map.class);
-
- publicKey = Utilities.getFirstCertFromKeyStore(keyStore).getPublicKey();
+
+ final X509Certificate serverCertificate = Utilities.getFirstCertFromKeyStore(keyStore);
+ publicKey = serverCertificate.getPublicKey();
context.put(CommonConstants.SERVER_PUBLIC_KEY, publicKey);
-
+
final PrivateKey privateKey = Utilities.getPrivateKey(keyStore, sslProperties.getKeyPassword());
context.put(CommonConstants.SERVER_PRIVATE_KEY, privateKey);
+
+ context.put(CommonConstants.SERVER_CERTIFICATE, serverCertificate);
}
//-------------------------------------------------------------------------------------------------
diff --git a/core-common/src/main/java/eu/arrowhead/common/CommonConstants.java b/core-common/src/main/java/eu/arrowhead/common/CommonConstants.java
index b8c9d3783..db91e1a24 100644
--- a/core-common/src/main/java/eu/arrowhead/common/CommonConstants.java
+++ b/core-common/src/main/java/eu/arrowhead/common/CommonConstants.java
@@ -14,8 +14,9 @@ public class CommonConstants {
// members
public static final String BASE_PACKAGE = "eu.arrowhead";
-
+
public static final String CORE_SYSTEM_AUTHORIZATION = "Authorization";
+ public static final String CORE_SYSTEM_CERTIFICATE_AUTHORITY = "Certificate Authority";
public static final String CORE_SYSTEM_CHOREOGRAPHER = "Choreographer";
public static final String CORE_SYSTEM_EVENT_HANDLER = "Event Handler";
public static final String CORE_SYSTEM_GATEKEEPER = "Gatekeeper";
@@ -42,6 +43,8 @@ public class CommonConstants {
public static final String CORE_SERVICE_EVENT_HANDLER_SUBSCRIBE = "event-subscribe";
public static final String CORE_SERVICE_EVENT_HANDLER_UNSUBSCRIBE = "event-unsubscribe";
public static final String CORE_SERVICE_EVENT_HANDLER_PUBLISH_AUTH_UPDATE = "event-publish-auth-update";
+
+ public static final String CORE_SERVICE_CERTIFICATE_AUTHORITY_SIGN = "ca-sign";
public static final String CORE_SERVICE_QOS_MONITOR_PING_MEASUREMENT = "qos-monitor-ping-measurement";
@@ -51,6 +54,7 @@ public class CommonConstants {
public static final String SERVER_COMMON_NAME = "server.common.name";
public static final String SERVER_PUBLIC_KEY = "server.public.key";
public static final String SERVER_PRIVATE_KEY = "server.private.key";
+ public static final String SERVER_CERTIFICATE = "server.certificate";
public static final String HTTPS = "https";
public static final String HTTP = "http";
@@ -80,7 +84,11 @@ public class CommonConstants {
public static final String OP_AUTH_INTRA_CHECK_URI = "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/intracloud/check";
public static final String OP_AUTH_INTER_CHECK_URI = "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/intercloud/check";
public static final String OP_AUTH_SUBSCRIPTION_CHECK_URI = "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/subscription/check";
-
+
+ public static final String CERTIFICATE_AUTHRORITY_URI = "/certificate-authority";
+ public static final String OP_CA_CLOUD_COMMON_NAME_URI = "/name";
+ public static final String OP_CA_SIGN_CERTIFICATE_URI = "/sign";
+
public static final String ORCHESTRATOR_URI = "/orchestrator";
public static final String OP_ORCH_PROCESS = "/orchestration";
public static final String ORCHESTRATON_FLAG_MATCHMAKING = "matchmaking";
@@ -156,6 +164,11 @@ public class CommonConstants {
public static final String DISABLE_HOSTNAME_VERIFIER = "disable.hostname.verifier";
public static final String $DISABLE_HOSTNAME_VERIFIER_WD = "${" + DISABLE_HOSTNAME_VERIFIER + ":" + Defaults.DEFAULT_DISABLE_HOSTNAME_VERIFIER + "}";
+ public static final String CA_CERT_VALIDITY_NEG_OFFSET_MILLIS = "ca.validity.negative-offest-millis";
+ public static final String $CA_CERT_VALIDITY_NEG_OFFSET_MILLIS = "${" + CA_CERT_VALIDITY_NEG_OFFSET_MILLIS + ":" + Defaults.DEFAULT_CA_CERT_VALIDITY_NEG_OFFSET_MILLIS + "}";
+ public static final String CA_CERT_VALIDITY_POS_OFFSET_MILLIS = "ca.validity.positive-offest-millis";
+ public static final String $CA_CERT_VALIDITY_POS_OFFSET_MILLIS = "${" + CA_CERT_VALIDITY_POS_OFFSET_MILLIS + ":" + Defaults.DEFAULT_CA_CERT_VALIDITY_POS_OFFSET_MILLIS + "}";
+
public static final String JWT_CLAIM_CONSUMER_ID = "cid";
public static final String JWT_CLAIM_SERVICE_ID = "sid";
public static final String JWT_CLAIM_INTERFACE_ID = "iid";
diff --git a/core-common/src/main/java/eu/arrowhead/common/Defaults.java b/core-common/src/main/java/eu/arrowhead/common/Defaults.java
index 9099e58d1..f26508c58 100644
--- a/core-common/src/main/java/eu/arrowhead/common/Defaults.java
+++ b/core-common/src/main/java/eu/arrowhead/common/Defaults.java
@@ -19,7 +19,7 @@ public class Defaults {
public static final int DEFAULT_GATEWAY_PORT = 8453;
public static final int DEFAULT_EVENT_HANDLER_PORT = 8455;
public static final int DEFAULT_CHOREOGRAPHER_PORT = 8457;
- public static final int DEFAULT_CERTIFICATE_AUTHORITY_PORT = 8459;
+ public static final int DEFAULT_CERTIFICATE_AUTHORITY_PORT = 8448;
public static final int DEFAULT_QOS_MONITOR_PORT = 8451;
// HTTP client defaults
@@ -30,9 +30,12 @@ public class Defaults {
// CORS defaults
public static final long CORS_MAX_AGE = 600;
public static final String CORS_ALLOW_CREDENTIALS = "true";
-
+
+ // CA defaults
+ public static final long DEFAULT_CA_CERT_VALIDITY_NEG_OFFSET_MILLIS = 5000;
+ public static final long DEFAULT_CA_CERT_VALIDITY_POS_OFFSET_MILLIS = 1000L * 3600 * 24 * 365;
//-------------------------------------------------------------------------------------------------
private Defaults() {
throw new UnsupportedOperationException();
}
-}
\ No newline at end of file
+}
diff --git a/core-common/src/main/java/eu/arrowhead/common/Utilities.java b/core-common/src/main/java/eu/arrowhead/common/Utilities.java
index 790ea5832..7e8c1398a 100644
--- a/core-common/src/main/java/eu/arrowhead/common/Utilities.java
+++ b/core-common/src/main/java/eu/arrowhead/common/Utilities.java
@@ -376,6 +376,54 @@ public static X509Certificate getFirstCertFromKeyStore(final KeyStore keystore)
}
}
+ //-------------------------------------------------------------------------------------------------
+ public static X509Certificate getCloudCertFromKeyStore(final KeyStore keystore) {
+ Assert.notNull(keystore, "Key store is not defined.");
+
+ try {
+ final Enumeration enumeration = keystore.aliases();
+ while (enumeration.hasMoreElements()) {
+ final String alias = enumeration.nextElement();
+ final String[] aliasParts = alias.split("\\.");
+ if (aliasParts.length == 4 && aliasParts[2].equals(AH_MASTER_NAME) && aliasParts[3].equals(AH_MASTER_SUFFIX)) {
+ return (X509Certificate) keystore.getCertificate(alias);
+ }
+ }
+
+ final String errorMsg = "Getting the cloud cert from keystore failed. " +
+ "Cannot find alias in the following format: {cloudname}.{cloudoperator}." + AH_MASTER_NAME + "." + AH_MASTER_SUFFIX;
+ logger.error(errorMsg);
+ throw new ServiceConfigurationError(errorMsg);
+ } catch (final KeyStoreException | NoSuchElementException ex) {
+ logger.error("Getting the cloud cert from keystore failed...", ex);
+ throw new ServiceConfigurationError("Getting the cloud cert from keystore failed...", ex);
+ }
+ }
+
+ //-------------------------------------------------------------------------------------------------
+ public static X509Certificate getRootCertFromKeyStore(final KeyStore keystore) {
+ Assert.notNull(keystore, "Key store is not defined.");
+
+ try {
+ final Enumeration enumeration = keystore.aliases();
+ while (enumeration.hasMoreElements()) {
+ final String alias = enumeration.nextElement();
+ final String[] aliasParts = alias.split("\\.");
+ if (aliasParts.length == 2 && aliasParts[0].equals(AH_MASTER_NAME) && aliasParts[1].equals(AH_MASTER_SUFFIX)) {
+ return (X509Certificate) keystore.getCertificate(alias);
+ }
+ }
+
+ final String errorMsg = "Getting the root cert from keystore failed. " +
+ "Cannot find alias in the following format: " + AH_MASTER_NAME + "." + AH_MASTER_SUFFIX;
+ logger.error(errorMsg);
+ throw new ServiceConfigurationError(errorMsg);
+ } catch (final KeyStoreException | NoSuchElementException ex) {
+ logger.error("Getting the root cert from keystore failed...", ex);
+ throw new ServiceConfigurationError("Getting the root cert from keystore failed...", ex);
+ }
+ }
+
//-------------------------------------------------------------------------------------------------
public static PrivateKey getPrivateKey(final KeyStore keystore, final String keyPass) {
Assert.notNull(keystore, "Key store is not defined.");
@@ -404,6 +452,24 @@ public static PrivateKey getPrivateKey(final KeyStore keystore, final String key
return privateKey;
}
+ //-------------------------------------------------------------------------------------------------
+ public static PrivateKey getPrivateKey(final KeyStore keystore, final String alias, final String keyPass) {
+ Assert.notNull(keystore, "Key store is not defined.");
+ Assert.notNull(keyPass, "Password is not defined.");
+
+ try {
+ PrivateKey privateKey = (PrivateKey) keystore.getKey(alias, keyPass.toCharArray());
+ if (privateKey != null) {
+ return privateKey;
+ } else {
+ throw new ServiceConfigurationError("Getting the private key failed, key store aliases do not identify a key.");
+ }
+ } catch (final KeyStoreException | UnrecoverableKeyException | NoSuchAlgorithmException ex) {
+ logger.error("Getting the private key from key store failed...", ex);
+ throw new ServiceConfigurationError("Getting the private key from key store failed...", ex);
+ }
+ }
+
//-------------------------------------------------------------------------------------------------
public static PublicKey getPublicKeyFromBase64EncodedString(final String encodedKey) {
Assert.isTrue(!isEmpty(encodedKey), "Encoded key is null or blank");
@@ -502,4 +568,4 @@ private static PublicKey generatePublicKeyFromByteArray(final byte[] keyBytes) {
throw new AuthException("Public key decoding failed due wrong input key", ex);
}
}
-}
\ No newline at end of file
+}
diff --git a/core-common/src/main/java/eu/arrowhead/common/core/CoreSystem.java b/core-common/src/main/java/eu/arrowhead/common/core/CoreSystem.java
index 22fd1a918..ed661c62c 100644
--- a/core-common/src/main/java/eu/arrowhead/common/core/CoreSystem.java
+++ b/core-common/src/main/java/eu/arrowhead/common/core/CoreSystem.java
@@ -24,7 +24,7 @@ public enum CoreSystem {
GATEWAY(Defaults.DEFAULT_GATEWAY_PORT, List.of(CoreSystemService.GATEWAY_PUBLIC_KEY_SERVICE, CoreSystemService.GATEWAY_PROVIDER_SERVICE,CoreSystemService.GATEWAY_CONSUMER_SERVICE)),
CHOREOGRAPHER(Defaults.DEFAULT_CHOREOGRAPHER_PORT, List.of()), // TODO: add services
QOS_MONITOR(Defaults.DEFAULT_QOS_MONITOR_PORT, List.of(CoreSystemService.QOS_MONITOR_PING_MEASUREMENT_SERVICE)),
- CERTIFICATE_AUTHORITY(Defaults.DEFAULT_CERTIFICATE_AUTHORITY_PORT, List.of()); // TODO: add services
+ CERTIFICATE_AUTHORITY(Defaults.DEFAULT_CERTIFICATE_AUTHORITY_PORT, List.of(CoreSystemService.CERTIFICATE_AUTHORITY_SIGN_SERVICE));
//=================================================================================================
// members
@@ -48,4 +48,4 @@ private CoreSystem(final int defaultPort, final List services
this.services = services != null ? Collections.unmodifiableList(services) : List.of();
this.defaultPort = defaultPort;
}
-}
\ No newline at end of file
+}
diff --git a/core-common/src/main/java/eu/arrowhead/common/core/CoreSystemService.java b/core-common/src/main/java/eu/arrowhead/common/core/CoreSystemService.java
index a0008d51b..2cbe9f535 100644
--- a/core-common/src/main/java/eu/arrowhead/common/core/CoreSystemService.java
+++ b/core-common/src/main/java/eu/arrowhead/common/core/CoreSystemService.java
@@ -35,10 +35,13 @@ public enum CoreSystemService {
EVENT_UNSUBSCRIBE_SERVICE(CommonConstants.CORE_SERVICE_EVENT_HANDLER_UNSUBSCRIBE, CommonConstants.EVENT_HANDLER_URI + CommonConstants.OP_EVENT_HANDLER_UNSUBSCRIBE),
EVENT_PUBLISH_AUTH_UPDATE_SERVICE(CommonConstants.CORE_SERVICE_EVENT_HANDLER_PUBLISH_AUTH_UPDATE, CommonConstants.EVENT_HANDLER_URI + CommonConstants.OP_EVENT_HANDLER_PUBLISH_AUTH_UPDATE),
+ // CA services
+ CERTIFICATE_AUTHORITY_SIGN_SERVICE(CommonConstants.CORE_SERVICE_CERTIFICATE_AUTHORITY_SIGN, CommonConstants.CERTIFICATE_AUTHRORITY_URI + CommonConstants.OP_CA_SIGN_CERTIFICATE_URI),
+
// QoS Monitor services
QOS_MONITOR_PING_MEASUREMENT_SERVICE(CommonConstants.CORE_SERVICE_QOS_MONITOR_PING_MEASUREMENT, CommonConstants.QOS_MONITOR_URI + CommonConstants.OP_QOS_MONITOR_PING_MEASUREMENT +
CommonConstants.OP_QOS_MONITOR_PING_MEASUREMENT_SUFFIX);
-
+
//TODO: additional services
//=================================================================================================
@@ -66,4 +69,4 @@ private CoreSystemService(final String serviceDefinition, final String serviceUr
this.serviceDefinition = serviceDefinition;
this.serviceUri = serviceUri;
}
-}
\ No newline at end of file
+}
diff --git a/core-common/src/main/java/eu/arrowhead/common/dto/internal/CertificateSigningRequestDTO.java b/core-common/src/main/java/eu/arrowhead/common/dto/internal/CertificateSigningRequestDTO.java
new file mode 100644
index 000000000..780c30e81
--- /dev/null
+++ b/core-common/src/main/java/eu/arrowhead/common/dto/internal/CertificateSigningRequestDTO.java
@@ -0,0 +1,27 @@
+package eu.arrowhead.common.dto.internal;
+
+import java.io.Serializable;
+
+import javax.validation.constraints.NotBlank;
+
+public class CertificateSigningRequestDTO implements Serializable {
+
+ private static final long serialVersionUID = -6810780579000655432L;
+
+ @NotBlank(message = "The encodedCSR is mandatory")
+ private String encodedCSR;
+
+ public CertificateSigningRequestDTO() {}
+
+ public CertificateSigningRequestDTO(String encodedCSR) {
+ this.encodedCSR = encodedCSR;
+ }
+
+ public String getEncodedCSR() {
+ return encodedCSR;
+ }
+
+ public void setEncodedCSR(String encodedCSR) {
+ this.encodedCSR = encodedCSR;
+ }
+}
diff --git a/core-common/src/main/java/eu/arrowhead/common/dto/internal/CertificateSigningResponseDTO.java b/core-common/src/main/java/eu/arrowhead/common/dto/internal/CertificateSigningResponseDTO.java
new file mode 100644
index 000000000..cb8726244
--- /dev/null
+++ b/core-common/src/main/java/eu/arrowhead/common/dto/internal/CertificateSigningResponseDTO.java
@@ -0,0 +1,26 @@
+package eu.arrowhead.common.dto.internal;
+
+import java.io.Serializable;
+import java.util.List;
+
+public class CertificateSigningResponseDTO implements Serializable {
+
+ private static final long serialVersionUID = -6810780579000655432L;
+
+ private List certificateChain;
+
+ public CertificateSigningResponseDTO() {
+ }
+
+ public CertificateSigningResponseDTO(List certificateChain) {
+ this.certificateChain = certificateChain;
+ }
+
+ public List getCertificateChain() {
+ return certificateChain;
+ }
+
+ public void setCertificateChain(List certificateChain) {
+ this.certificateChain = certificateChain;
+ }
+}
diff --git a/docker-all/example/docker-compose.yml b/docker-all/example/docker-compose.yml
index ab10fdf71..3e193d153 100644
--- a/docker-all/example/docker-compose.yml
+++ b/docker-all/example/docker-compose.yml
@@ -27,6 +27,7 @@ services:
#- ./properties/eventhandler.properties:/opt/arrowhead-core/eventhandler/application.properties
- ./properties/gatekeeper.properties:/opt/arrowhead-core/gatekeeper/application.properties
- ./properties/gateway.properties:/opt/arrowhead-core/gateway/application.properties
+ - ./properties/certificateauthority.properties:/opt/arrowhead-core/certificate-authority/application.properties
ports:
- 8443:8443 # Service Registry
- 8445:8445 # Authorization
@@ -34,6 +35,7 @@ services:
#- 8455:8455 # Event Handler
- 8449:8449 # Gatekeeper
- 8453:8453 # Gateway
+ - 8448:8448 # Certificate Authority
volumes:
arrowhead_core_mysql:
diff --git a/docker-all/example/properties/certificateauthority.properties b/docker-all/example/properties/certificateauthority.properties
new file mode 100644
index 000000000..93f325bd4
--- /dev/null
+++ b/docker-all/example/properties/certificateauthority.properties
@@ -0,0 +1,59 @@
+############################################
+### APPLICATION PARAMETERS ###
+############################################
+
+# Database connection (mandatory)
+# Change the server timezone if neccessary
+spring.datasource.url=jdbc:mysql://127.0.0.1:3306/arrowhead?serverTimezone=Europe/Budapest
+spring.datasource.username=certificate_authority
+spring.datasource.password=FsdG6Kgf9QpPfv2
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+
+spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
+# use true only for debugging
+spring.jpa.show-sql=false
+spring.jpa.properties.hibernate.format_sql=true
+spring.jpa.hibernate.ddl-auto=none
+
+# Certificate Authority web-server parameters
+server.address=127.0.0.1
+server.port=8448
+domain.name=127.0.0.1
+domain.port=8448
+
+############################################
+### CUSTOM PARAMETERS ###
+############################################
+
+# Name of the core system
+core_system_name=certificate_authority
+
+# Show all request/response in debug log
+log_all_request_and_response=false
+
+# Service Registry web-server parameters (to register the Certificate-Authority services)
+sr_address=127.0.0.1
+sr_port=8443
+
+############################################
+### SECURE MODE ###
+############################################
+
+# configure secure mode
+
+# Set this to false to disable https mode
+server.ssl.enabled=true
+
+server.ssl.key-store-type=PKCS12
+server.ssl.key-store=classpath:certificates/certificate_authority.p12
+server.ssl.key-store-password=123456
+server.ssl.key-alias=certificate_authority
+server.ssl.key-password=123456
+server.ssl.client-auth=need
+server.ssl.trust-store-type=PKCS12
+server.ssl.trust-store=classpath:certificates/truststore.p12
+server.ssl.trust-store-password=123456
+
+#If true, http client does not check whether the hostname is match one of the server's SAN in its certificate
+#Just for testing, DO NOT USE this feature in production environment
+disable.hostname.verifier=false
diff --git a/docker-all/example/sql/setup.sql b/docker-all/example/sql/setup.sql
index 755b9a6e5..8fc8e0225 100644
--- a/docker-all/example/sql/setup.sql
+++ b/docker-all/example/sql/setup.sql
@@ -537,4 +537,13 @@ GRANT ALL PRIVILEGES ON `arrowhead`.`service_definition` TO 'gateway'@'%';
GRANT ALL PRIVILEGES ON `arrowhead`.`authorization_inter_cloud` TO 'gateway'@'%';
GRANT ALL PRIVILEGES ON `arrowhead`.`logs` TO 'gateway'@'%';
-FLUSH PRIVILEGES;
\ No newline at end of file
+-- Certificate Authority
+DROP USER IF EXISTS 'certificate_authority'@'localhost';
+CREATE USER IF NOT EXISTS 'certificate_authority'@'localhost' IDENTIFIED BY 'FsdG6Kgf9QpPfv2';
+GRANT ALL PRIVILEGES ON `arrowhead`.`logs` TO 'certificate_authority'@'localhost';
+
+DROP USER IF EXISTS 'certificate_authority'@'%';
+CREATE USER IF NOT EXISTS 'certificate_authority'@'%' IDENTIFIED BY 'FsdG6Kgf9QpPfv2';
+GRANT ALL PRIVILEGES ON `arrowhead`.`logs` TO 'certificate_authority'@'%';
+
+FLUSH PRIVILEGES;
diff --git a/docker/core_system_config/certificateauthority.properties b/docker/core_system_config/certificateauthority.properties
new file mode 100644
index 000000000..93f325bd4
--- /dev/null
+++ b/docker/core_system_config/certificateauthority.properties
@@ -0,0 +1,59 @@
+############################################
+### APPLICATION PARAMETERS ###
+############################################
+
+# Database connection (mandatory)
+# Change the server timezone if neccessary
+spring.datasource.url=jdbc:mysql://127.0.0.1:3306/arrowhead?serverTimezone=Europe/Budapest
+spring.datasource.username=certificate_authority
+spring.datasource.password=FsdG6Kgf9QpPfv2
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+
+spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
+# use true only for debugging
+spring.jpa.show-sql=false
+spring.jpa.properties.hibernate.format_sql=true
+spring.jpa.hibernate.ddl-auto=none
+
+# Certificate Authority web-server parameters
+server.address=127.0.0.1
+server.port=8448
+domain.name=127.0.0.1
+domain.port=8448
+
+############################################
+### CUSTOM PARAMETERS ###
+############################################
+
+# Name of the core system
+core_system_name=certificate_authority
+
+# Show all request/response in debug log
+log_all_request_and_response=false
+
+# Service Registry web-server parameters (to register the Certificate-Authority services)
+sr_address=127.0.0.1
+sr_port=8443
+
+############################################
+### SECURE MODE ###
+############################################
+
+# configure secure mode
+
+# Set this to false to disable https mode
+server.ssl.enabled=true
+
+server.ssl.key-store-type=PKCS12
+server.ssl.key-store=classpath:certificates/certificate_authority.p12
+server.ssl.key-store-password=123456
+server.ssl.key-alias=certificate_authority
+server.ssl.key-password=123456
+server.ssl.client-auth=need
+server.ssl.trust-store-type=PKCS12
+server.ssl.trust-store=classpath:certificates/truststore.p12
+server.ssl.trust-store-password=123456
+
+#If true, http client does not check whether the hostname is match one of the server's SAN in its certificate
+#Just for testing, DO NOT USE this feature in production environment
+disable.hostname.verifier=false
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 5748354fa..ebeb3c0c3 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -72,7 +72,17 @@ services:
volumes:
- ./core_system_config/gateway.properties:/gateway/application.properties
ports:
- - 8453:8453
+ - 8453:8453
+
+ certificateauthority:
+ container_name: certificateauthority
+ image: svetlint/certificateauthority:latest
+ depends_on:
+ - mysql
+ volumes:
+ - ./core_system_config/certificateauthority.properties:/certificateauthority/application.properties
+ ports:
+ - 8448:8448
volumes:
mysql:
diff --git a/documentation/certificate_authority/ReleaseNotes-4.1.3-CertificateAuthority.odt b/documentation/certificate_authority/ReleaseNotes-4.1.3-CertificateAuthority.odt
new file mode 100644
index 000000000..eb222ba70
Binary files /dev/null and b/documentation/certificate_authority/ReleaseNotes-4.1.3-CertificateAuthority.odt differ
diff --git a/pom.xml b/pom.xml
index 03a4757a1..7b57213ff 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@
${revision}
pom
core-java-spring
- Arrowhead Core Sevices using Spring Boot
+ Arrowhead Core Services using Spring Boot
https://github.com/arrowhead-f/core-java-spring
@@ -26,6 +26,7 @@
core-common
serviceregistry
authorization
+ certificate-authority
orchestrator
eventhandler
gatekeeper
@@ -55,6 +56,7 @@
3.9
2.0.1
5.15.9
+ 1.64
1.2
5.5.0
UTF-8
diff --git a/scripts/certificate_authority_privileges.sql b/scripts/certificate_authority_privileges.sql
new file mode 100644
index 000000000..e39d0c9f7
--- /dev/null
+++ b/scripts/certificate_authority_privileges.sql
@@ -0,0 +1,9 @@
+USE `arrowhead`;
+
+REVOKE ALL, GRANT OPTION FROM 'certificate_authority'@'localhost';
+GRANT ALL PRIVILEGES ON `arrowhead`.`logs` TO 'certificate_authority'@'localhost';
+
+REVOKE ALL, GRANT OPTION FROM 'certificate_authority'@'%';
+GRANT ALL PRIVILEGES ON `arrowhead`.`logs` TO 'certificate_authority'@'%';
+
+FLUSH PRIVILEGES;
diff --git a/scripts/create_empty_arrowhead_db.sql b/scripts/create_empty_arrowhead_db.sql
index f97fc6df8..481b0dca6 100644
--- a/scripts/create_empty_arrowhead_db.sql
+++ b/scripts/create_empty_arrowhead_db.sql
@@ -42,7 +42,12 @@ CREATE USER IF NOT EXISTS 'gateway'@'localhost' IDENTIFIED BY 'LfiSM9DpGfDEP5g';
CREATE USER IF NOT EXISTS 'gateway'@'%' IDENTIFIED BY 'LfiSM9DpGfDEP5g';
source gateway_privileges.sql
+-- Certificate Authority
+CREATE USER IF NOT EXISTS 'certificate_authority'@'localhost' IDENTIFIED BY 'FsdG6Kgf9QpPfv2';
+CREATE USER IF NOT EXISTS 'certificate_authority'@'%' IDENTIFIED BY 'FsdG6Kgf9QpPfv2';
+source certificate_authority_privileges.sql
+
-- QoS Monitor
CREATE USER IF NOT EXISTS 'qos_monitor'@'localhost' IDENTIFIED BY 'RLY3UEx6nx4kSXy';
CREATE USER IF NOT EXISTS 'qos_monitor'@'%' IDENTIFIED BY 'RLY3UEx6nx4kSXy';
-source qos_monitor_privileges.sql
\ No newline at end of file
+source qos_monitor_privileges.sql
diff --git a/scripts/initSQL.sh b/scripts/initSQL.sh
index 123f4d41e..4e84421d9 100644
--- a/scripts/initSQL.sh
+++ b/scripts/initSQL.sh
@@ -16,4 +16,5 @@ wget https://raw.githubusercontent.com/arrowhead-f/core-java-spring/master/scrip
wget https://raw.githubusercontent.com/arrowhead-f/core-java-spring/master/scripts/event_handler_privileges.sql
wget https://raw.githubusercontent.com/arrowhead-f/core-java-spring/master/scripts/choreographer_privileges.sql
wget https://raw.githubusercontent.com/arrowhead-f/core-java-spring/master/scripts/gatekeeper_privileges.sql
-wget https://raw.githubusercontent.com/arrowhead-f/core-java-spring/master/scripts/gateway_privileges.sql
\ No newline at end of file
+wget https://raw.githubusercontent.com/arrowhead-f/core-java-spring/master/scripts/gateway_privileges.sql
+wget https://raw.githubusercontent.com/arrowhead-f/core-java-spring/master/scripts/certificate_authority_privileges.sql
diff --git a/scripts/start_coresystems.bat b/scripts/start_coresystems.bat
index 3e96b0ad8..2f86c4be2 100644
--- a/scripts/start_coresystems.bat
+++ b/scripts/start_coresystems.bat
@@ -36,6 +36,10 @@ cd ..\..\choreographer\target
START "" /B "cmd /c javaw -jar arrowhead-choreographer-4.1.3.jar > sout_choreographer.log 2>&1"
echo Choreographer started
+cd ..\..\certificate-authority\target
+START "" /B "cmd /c javaw -jar arrowhead-certificate-authority-4.1.3.jar > sout_ca.log 2>&1"
+echo Certificate Authority started
+
cd %parent_path%
::Kill self
diff --git a/scripts/start_coresystems.sh b/scripts/start_coresystems.sh
index 4d3c7044b..24dbf6fde 100644
--- a/scripts/start_coresystems.sh
+++ b/scripts/start_coresystems.sh
@@ -35,3 +35,8 @@ echo Orchestrator started
cd ../../choreographer/target
nohup java -jar $(find . -maxdepth 1 -name arrowhead-choreographer-\*.jar | sort | tail -n1) &> sout_choreographer.log &
echo Choreographer started
+
+cd ../../certificate-authority/target
+nohup java -jar $(find . -maxdepth 1 -name arrowhead-certificate-authority-\*.jar | sort | tail -n1) &> sout_ca.log &
+echo Certificate Authority started
+
diff --git a/scripts/start_coresystems_local.bat b/scripts/start_coresystems_local.bat
index 179670d0b..e812085fa 100644
--- a/scripts/start_coresystems_local.bat
+++ b/scripts/start_coresystems_local.bat
@@ -26,6 +26,10 @@ cd ..\..\orchestrator\target
START "" /B "cmd /c javaw -jar arrowhead-orchestrator-4.1.3.jar > sout_orch.log 2>&1"
echo Orchestrator started
+cd ..\..\certificate-authority\target
+START "" /B "cmd /c javaw -jar arrowhead-certificate-authority-4.1.3.jar > sout_ca.log 2>&1"
+echo Certificate Authority started
+
cd %parent_path%
::Kill self
diff --git a/scripts/start_coresystems_local.sh b/scripts/start_coresystems_local.sh
index 884b5cb5d..1d0f3e9a1 100644
--- a/scripts/start_coresystems_local.sh
+++ b/scripts/start_coresystems_local.sh
@@ -26,3 +26,7 @@ echo Authorization started
cd ../../orchestrator/target
nohup java -jar $(find . -maxdepth 1 -name arrowhead-orchestrator-\*.jar | sort | tail -n1) &> sout_orch.log &
echo Orchestrator started
+
+cd ../../certificate-authority/target
+nohup java -jar $(find . -maxdepth 1 -name arrowhead-certificate-authority-\*.jar | sort | tail -n1) &> sout_ca.log &
+echo Certificate Authority started
diff --git a/scripts/stop_coresystems.bat b/scripts/stop_coresystems.bat
index dc6f3a234..31ab118f0 100644
--- a/scripts/stop_coresystems.bat
+++ b/scripts/stop_coresystems.bat
@@ -10,6 +10,7 @@ FOR /F "tokens=1" %%p in ('"jps -v | find "gatekeeper""') DO taskkill /pid %%p >
FOR /F "tokens=1" %%p in ('"jps -v | find "eventhandler""') DO taskkill /pid %%p > NUL 2>&1
FOR /F "tokens=1" %%p in ('"jps -v | find "gateway""') DO taskkill /pid %%p > NUL 2>&1
FOR /F "tokens=1" %%p in ('"jps -v | find "authorization""') DO taskkill /pid %%p > NUL 2>&1
+FOR /F "tokens=1" %%p in ('"jps -v | find "certificate""') DO taskkill /pid %%p > NUL 2>&1
timeout /t %time_to_sleep% /nobreak > NUL
FOR /F "tokens=1" %%p in ('"jps -v | find "serviceregistry""') DO taskkill /pid %%p > NUL 2>&1
@@ -27,6 +28,7 @@ IF "%STILL_THERE%"=="""" (
FOR /F "tokens=1" %%p in ('"jps -v | find "eventhandler""') DO taskkill /F /pid %%p
FOR /F "tokens=1" %%p in ('"jps -v | find "gateway""') DO taskkill /F /pid %%p
FOR /F "tokens=1" %%p in ('"jps -v | find "authorization""') DO taskkill /F /pid %%p
+ FOR /F "tokens=1" %%p in ('"jps -v | find "certificate""') DO taskkill /F /pid %%p
FOR /F "tokens=1" %%p in ('"jps -v | find "serviceregistry""') DO taskkill /F /pid %%p
echo Core systems forcefully killed
)
diff --git a/scripts/stop_coresystems.sh b/scripts/stop_coresystems.sh
index b19816282..1540cc284 100644
--- a/scripts/stop_coresystems.sh
+++ b/scripts/stop_coresystems.sh
@@ -7,6 +7,7 @@ pkill -f gatekeeper
pkill -f authorization
pkill -f eventhandler
pkill -f gateway
+pkill -f certificate-authority
sleep 5s
pkill -f serviceregistry
sleep 2s
@@ -19,8 +20,9 @@ then
kill -KILL $(ps aux | grep 'authorization' | awk '{print $2}')
kill -KILL $(ps aux | grep 'eventhandler' | awk '{print $2}')
kill -KILL $(ps aux | grep 'gateway' | awk '{print $2}')
+ kill -KILL $(ps aux | grep 'certificate-authority' | awk '{print $2}')
kill -KILL $(ps aux | grep 'serviceregistry' | awk '{print $2}')
echo Core systems forcefully killed
else
echo Core systems killed
-fi
\ No newline at end of file
+fi
diff --git a/serviceregistry/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/serviceregistry/src/main/resources/META-INF/additional-spring-configuration-metadata.json
index 8ccdfc698..2623b21e2 100644
--- a/serviceregistry/src/main/resources/META-INF/additional-spring-configuration-metadata.json
+++ b/serviceregistry/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -1,28 +1,13 @@
{"properties": [
{
- "name": "ping_scheduled",
- "type": "java.lang.Boolean",
- "description": "A description for 'ping_scheduled'"
- },
- {
- "name": "ping_timeout",
- "type": "java.lang.Long",
- "description": "A description for 'ping_timeout'"
- },
- {
- "name": "ping_interval",
+ "name": "ca.validity.negative-offest-millis",
"type": "java.lang.Long",
- "description": "A description for 'ping_interval'"
+ "description": "The negative offset in milliseconds from the current time for the beginning of the validity of the generated certificates"
},
{
- "name": "ttl_scheduled",
- "type": "java.lang.Boolean",
- "description": "A description for 'ttl_scheduled'"
- },
- {
- "name": "ttl_interval",
+ "name": "ca.validity.positive-offest-millis",
"type": "java.lang.Long",
- "description": "A description for 'ttl_interval'"
+ "description": "The offset in milliseconds from the current time for the end of the validity of the generated certificates"
},
{
"name": "disable.hostname.verifier",
@@ -34,11 +19,6 @@
"type": "java.lang.Boolean",
"description": "A description for 'log_all_request_and_response'"
},
- {
- "name": "use_strict_service_intf_name_verifier",
- "type": "java.lang.Boolean",
- "description": "A description for 'use_strict_service_intf_name_verifier'"
- },
{
"name": "core_system_name",
"type": "java.lang.String",
@@ -54,4 +34,4 @@
"type": "java.lang.Integer",
"description": "A description for 'domain.port'"
}
-]}
\ No newline at end of file
+]}