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: + *
    + *
  1. The subject alternative name makes possible to use the certificate for + * accessing a host via IP address or hostname
  2. + * + *
  3. The subject key identifier provides a hashed value that should uniquely + * identify the public key
  4. + * + *
  5. The authority key identifier provides a hashed value that should uniquely + * identify the issuer of the certificate
  6. + * + *
  7. And this basic constraint is for forbidding issuing other certificates + * under this certificate
  8. + *
+ */ + 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 +]}