diff --git a/build.gradle b/build.gradle index 726dd06d6f..8adff99244 100644 --- a/build.gradle +++ b/build.gradle @@ -298,7 +298,7 @@ dependencies { implementation 'com.google.guava:guava:30.0-jre' implementation 'org.greenrobot:eventbus:3.2.0' implementation 'commons-cli:commons-cli:1.3.1' - implementation 'org.bouncycastle:bcprov-jdk15on:1.67' + implementation "org.bouncycastle:bcprov-jdk15on:${versions.bouncycastle}" implementation 'org.ldaptive:ldaptive:1.2.3' implementation 'org.apache.httpcomponents:httpclient-cache:4.5.13' implementation 'io.jsonwebtoken:jjwt-api:0.10.8' @@ -328,6 +328,7 @@ dependencies { testImplementation 'org.apache.camel:camel-xmlsecurity:3.14.2' + implementation 'net.shibboleth.utilities:java-support:7.5.1' implementation 'org.opensaml:opensaml-core:3.4.5' implementation 'org.opensaml:opensaml-security-impl:3.4.5' @@ -427,6 +428,8 @@ dependencies { integrationTestImplementation 'org.apache.logging.log4j:log4j-jul:2.17.1' integrationTestImplementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.17.1' integrationTestImplementation 'org.hamcrest:hamcrest:2.2' + integrationTestImplementation "org.bouncycastle:bcpkix-jdk15on:${versions.bouncycastle}" + integrationTestImplementation "org.bouncycastle:bcutil-jdk15on:${versions.bouncycastle}" } jar { diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/AlgorithmKit.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/AlgorithmKit.java new file mode 100644 index 0000000000..270a2bad6f --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/AlgorithmKit.java @@ -0,0 +1,146 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +/* +* Copyright 2021 floragunn GmbH +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ + +package org.opensearch.test.framework.certificate; + +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.spec.ECGenParameterSpec; +import java.util.function.Supplier; + +import com.google.common.base.Strings; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import static java.util.Objects.requireNonNull; + +/** +* The class determines cryptographic algorithms used for certificate creation. To create certificate it is necessary to generate public +* and private key, so-called key pair. The class encapsulates the process of key pairs creation ({@link #generateKeyPair()}), +* thus determines algorithm used for key pair creation. Additionally, class defines also algorithms used to digitally sign a certificate. +* Please see {@link #getSignatureAlgorithmName()} +*/ +class AlgorithmKit { + + private static final Logger log = LogManager.getLogger(AlgorithmKit.class); + public static final String SIGNATURE_ALGORITHM_SHA_256_WITH_RSA = "SHA256withRSA"; + public static final String SIGNATURE_ALGORITHM_SHA_256_WITH_ECDSA = "SHA256withECDSA"; + + private final String signatureAlgorithmName; + private final Supplier keyPairSupplier; + + private AlgorithmKit(String signatureAlgorithmName, Supplier keyPairSupplier) { + notEmptyAlgorithmName(signatureAlgorithmName); + this.signatureAlgorithmName = signatureAlgorithmName; + this.keyPairSupplier = requireNonNull(keyPairSupplier, "Key pair supplier is required."); + } + + private static void notEmptyAlgorithmName(String signatureAlgorithmName) { + if(Strings.isNullOrEmpty(signatureAlgorithmName)){ + throw new RuntimeException("Algorithm name is required."); + } + } + + /** + * Static factory method. ECDSA algorithm used for key pair creation. Signature algorithm is defined by field + * {@link #SIGNATURE_ALGORITHM_SHA_256_WITH_ECDSA} + * + * @param securityProvider determines cryptographic algorithm implementation + * @param ellipticCurve + * @return new instance of class {@link AlgorithmKit} + */ + public static AlgorithmKit ecdsaSha256withEcdsa(Provider securityProvider, String ellipticCurve) { + notEmptyAlgorithmName(ellipticCurve); + Supplier supplier = ecdsaKeyPairSupplier(requireNonNull(securityProvider, "Security provider is required"), ellipticCurve); + return new AlgorithmKit(SIGNATURE_ALGORITHM_SHA_256_WITH_ECDSA, supplier); + } + + /** + * Static factory method. It creates object of {@link AlgorithmKit} which enforces usage of RSA algorithm for key pair generation. + * Signature algorithm is defined by {@link #SIGNATURE_ALGORITHM_SHA_256_WITH_RSA} + * + * @param securityProvider determines cryptographic algorithm implementation + * @param keySize defines key size for RSA algorithm + * @return new instance of class {@link AlgorithmKit} + */ + public static AlgorithmKit rsaSha256withRsa(Provider securityProvider, int keySize) { + positiveKeySize(keySize); + Supplier supplier = rsaKeyPairSupplier(securityProvider, keySize); + return new AlgorithmKit(SIGNATURE_ALGORITHM_SHA_256_WITH_RSA, supplier); + } + + private static void positiveKeySize(int keySize) { + if(keySize <= 0) { + throw new RuntimeException("Key size must be a positive integer value, provided: " + keySize); + } + } + + /** + * It determines algorithm used for digital signature + * @return algorithm name + */ + public String getSignatureAlgorithmName(){ + return signatureAlgorithmName; + } + + /** + * It creates new private and public key pair + * @return new pair of keys + */ + public KeyPair generateKeyPair(){ + return keyPairSupplier.get(); + } + private static Supplier rsaKeyPairSupplier(Provider securityProvider, int keySize) { + try { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", securityProvider); + log.info("Initialize key pair generator with keySize: {}", keySize); + generator.initialize(keySize); + return generator::generateKeyPair; + } catch (NoSuchAlgorithmException e) { + String message = "Error while initializing RSA asymmetric key generator."; + log.error(message, e); + throw new RuntimeException(message, e); + } + } + + private static Supplier ecdsaKeyPairSupplier(Provider securityProvider, String ellipticCurve) { + try { + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC", securityProvider); + log.info("Initialize key pair generator with elliptic curve: {}", ellipticCurve); + ECGenParameterSpec ecsp = new ECGenParameterSpec(ellipticCurve); + generator.initialize(ecsp); + return generator::generateKeyPair; + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { + String message = "Error while initializing ECDSA asymmetric key generator."; + log.error(message, e); + throw new RuntimeException(message, e); + } + } + +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificateData.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificateData.java new file mode 100644 index 0000000000..52aab926dc --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificateData.java @@ -0,0 +1,73 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +/* +* Copyright 2021 floragunn GmbH +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ + +package org.opensearch.test.framework.certificate; + +import java.security.KeyPair; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509CertificateHolder; + +/** +* The class contains all data related to Certificate including private key which is considered to be a secret. +*/ +class CertificateData { + + private final X509CertificateHolder certificate; + private final KeyPair keyPair; + + public CertificateData(X509CertificateHolder certificate, KeyPair keyPair) { + this.certificate = certificate; + this.keyPair = keyPair; + } + + /** + * The method returns X.509 certificate encoded in PEM format. PEM format is defined by + * RFC 1421. + * @return Certificate in PEM format + */ + public String certificateInPemFormat() { + return PemConverter.toPem(certificate); + } + + /** + * It returns the private key associated with certificate encoded in PEM format. PEM format is defined by + * RFC 1421. + * @param privateKeyPassword password used for private key encryption. null for unencrypted key. + * @return private key encoded in PEM format + */ + public String privateKeyInPemFormat(String privateKeyPassword) { + return PemConverter.toPem(keyPair.getPrivate(), privateKeyPassword); + } + + X500Name getCertificateSubject() { + return certificate.getSubject(); + } + + KeyPair getKeyPair() { + return keyPair; + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificateMetadata.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificateMetadata.java new file mode 100644 index 0000000000..75f933e0d3 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificateMetadata.java @@ -0,0 +1,220 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.test.framework.certificate; + + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import com.google.common.base.Strings; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static java.util.Objects.requireNonNull; + +/** +*

+* The class represents metadata which should be embedded in certificate to describe a certificate subject (person, company, web server, +* IoT device). The class contains some basic metadata and metadata which should be placed in certificate extensions. +*

+* +*

+* The class is immutable. +*

+* +*/ +class CertificateMetadata { + /** + * Certification subject (person, company, web server, IoT device). The subject of certificate is an owner of the certificate + * (simplification). The format of this field must adhere to RFC 4514. + * @see RFC 4514 + */ + private final String subject; + + /** + * It describes certificate expiration date + */ + private final int validityDays; + + /** + * Optionally used by Open Search to indicate that the certificate can be used by Open Search node to confirm the node identity. The + * value becomes a part of + * SAN (Subject Alternative Name) extension + * + * @see #dnsNames + * @see SAN (Subject Alternative Name) extension + */ + private final String nodeOid; + + /** + * The certificate contains only one {@link #subject}. This is a common limitation when a certificate is used by a web server which is + * associated with a few domains. To overcome this limitation SAN (Subject Alternative Name) extension was introduced. + * The field contains additional subject names which enables creation of so called multi-domain certificates. The extension is defined + * in section 4.2.1.6 of RFC 5280 + * + * @see RFC 5280 + */ + private final List dnsNames; + + /** + * Similar to {@link #dnsNames} but contains IP addresses instead of domains. + */ + private final List ipAddresses; + + /** + * If a private key associated with certificate is used to sign other certificate then this field has to be true. + */ + private final boolean basicConstrainIsCa; + + /** + * Allowed usages for public key associated with certificate + */ + private final Set keyUsages; + + + private CertificateMetadata(String subject, + int validityDays, + String nodeOid, + List dnsNames, + List ipAddresses, + boolean basicConstrainIsCa, + Set keyUsages) { + this.subject = subject; + this.validityDays = validityDays; + this.nodeOid = nodeOid; + this.dnsNames = requireNonNull(dnsNames, "List of dns names must not be null."); + this.ipAddresses = requireNonNull(ipAddresses, "List of IP addresses must not be null"); + this.basicConstrainIsCa = basicConstrainIsCa; + this.keyUsages = requireNonNull(keyUsages, "Key usage set must not be null."); + } + + /** + * Static factory method. It creates metadata which contains only basic information. + * @param subjectName please see {@link #subject} + * @param validityDays please see {@link #validityDays} + * @return new instance of {@link CertificateMetadata} + */ + public static CertificateMetadata basicMetadata(String subjectName, int validityDays) { + return new CertificateMetadata(subjectName, validityDays, null, emptyList(), emptyList(), false, emptySet()); + } + + /** + * It is related to private key associated with certificate. It specifies metadata related to allowed private key usage. + * @param basicConstrainIsCa {@link #basicConstrainIsCa} + * @param keyUsages {@link #keyUsages} + * @return returns newly created instance of {@link CertificateData} + */ + public CertificateMetadata withKeyUsage(boolean basicConstrainIsCa, PublicKeyUsage...keyUsages){ + Set usages = arrayToEnumSet(keyUsages); + return new CertificateMetadata(subject, validityDays, nodeOid, dnsNames, ipAddresses, basicConstrainIsCa, usages); + } + + private > Set arrayToEnumSet(T[] enumArray) { + if((enumArray == null) || (enumArray.length == 0)){ + return Collections.emptySet(); + } + return EnumSet.copyOf(asList(enumArray)); + } + + /** + * The method defines metadata related to SAN (Subject Alternative Name) extension. + * @param nodeOid {@link #nodeOid} + * @param dnsNames {@link #dnsNames} + * @param ipAddresses {@link #ipAddresses} + * @return new instance of {@link CertificateMetadata} + * @see SAN (Subject Alternative Name) extension + */ + public CertificateMetadata withSubjectAlternativeName(String nodeOid, List dnsNames, String...ipAddresses) { + return new CertificateMetadata(subject, validityDays, nodeOid, dnsNames, asList(ipAddresses), basicConstrainIsCa, keyUsages); + } + + /** + * {@link #subject} + * @return Subject name + */ + public String getSubject() { + return subject; + } + + /** + * {@link #validityDays} + * @return determines certificate expiration date + */ + public int getValidityDays() { + return validityDays; + } + + /** + * {@link #basicConstrainIsCa} + * @return Determines if another certificate can be derived from certificate. + */ + public boolean isBasicConstrainIsCa() { + return basicConstrainIsCa; + } + + KeyUsage asKeyUsage() { + Integer keyUsageBitMask = keyUsages + .stream() + .filter(PublicKeyUsage::isNotExtendedUsage) + .map(PublicKeyUsage::asInt) + .reduce(0, (accumulator, currentValue) -> accumulator | currentValue); + return new KeyUsage(keyUsageBitMask); + } + + boolean hasSubjectAlternativeNameExtension() { + return ((ipAddresses.size() + dnsNames.size()) > 0) || (Strings.isNullOrEmpty(nodeOid) == false); + } + + DERSequence createSubjectAlternativeNames() { + List subjectAlternativeNameList = new ArrayList<>(); + if (!Strings.isNullOrEmpty(nodeOid)) { + subjectAlternativeNameList.add(new GeneralName(GeneralName.registeredID, nodeOid)); + } + if (isNotEmpty(dnsNames)) { + for (String dnsName : dnsNames) { + subjectAlternativeNameList.add(new GeneralName(GeneralName.dNSName, dnsName)); + } + } + if (isNotEmpty(ipAddresses)) { + for (String ip : ipAddresses) { + subjectAlternativeNameList.add(new GeneralName(GeneralName.iPAddress, ip)); + } + } + return new DERSequence(subjectAlternativeNameList.toArray(ASN1Encodable[]::new)); + } + + private static boolean isNotEmpty(Collection collection) { + return (collection != null) && (!collection.isEmpty()); + } + + boolean hasExtendedKeyUsage() { + return keyUsages.stream().anyMatch(PublicKeyUsage::isNotExtendedUsage); + } + + ExtendedKeyUsage getExtendedKeyUsage() { + KeyPurposeId[] usages = keyUsages + .stream() + .filter(PublicKeyUsage::isExtendedUsage) + .map(PublicKeyUsage::getKeyPurposeId) + .toArray(KeyPurposeId[]::new); + return new ExtendedKeyUsage(usages); + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/Certificates.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/Certificates.java deleted file mode 100644 index 9895fc1484..0000000000 --- a/src/integrationTest/java/org/opensearch/test/framework/certificate/Certificates.java +++ /dev/null @@ -1,165 +0,0 @@ -/* -* SPDX-License-Identifier: Apache-2.0 -* -* The OpenSearch Contributors require contributions made to -* this file be licensed under the Apache-2.0 license or a -* compatible open source license. -* -* Modifications Copyright OpenSearch Contributors. See -* GitHub history for details. -*/ - -package org.opensearch.test.framework.certificate; - -/** -* Contains static certificates for the test cluster. -* Note: This is WIP and will be replaced by classes -* that can generate certificates on the fly. This -* class will be removed after that. -*/ -public class Certificates { - - final static String ROOT_CA_CERTIFICATE = "-----BEGIN CERTIFICATE-----\n" - + "MIID/jCCAuagAwIBAgIBATANBgkqhkiG9w0BAQsFADCBjzETMBEGCgmSJomT8ixk\n" - + "ARkWA2NvbTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1w\n" - + "bGUgQ29tIEluYy4xITAfBgNVBAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEh\n" - + "MB8GA1UEAwwYRXhhbXBsZSBDb20gSW5jLiBSb290IENBMB4XDTE4MDQyMjAzNDM0\n" - + "NloXDTI4MDQxOTAzNDM0NlowgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJ\n" - + "kiaJk/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEw\n" - + "HwYDVQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1w\n" - + "bGUgQ29tIEluYy4gUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\n" - + "ggEBAK/u+GARP5innhpXK0c0q7s1Su1VTEaIgmZr8VWI6S8amf5cU3ktV7WT9SuV\n" - + "TsAm2i2A5P+Ctw7iZkfnHWlsC3HhPUcd6mvzGZ4moxnamM7r+a9otRp3owYoGStX\n" - + "ylVTQusAjbq9do8CMV4hcBTepCd+0w0v4h6UlXU8xjhj1xeUIz4DKbRgf36q0rv4\n" - + "VIX46X72rMJSETKOSxuwLkov1ZOVbfSlPaygXIxqsHVlj1iMkYRbQmaTib6XWHKf\n" - + "MibDaqDejOhukkCjzpptGZOPFQ8002UtTTNv1TiaKxkjMQJNwz6jfZ53ws3fh1I0\n" - + "RWT6WfM4oeFRFnyFRmc4uYTUgAkCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAf\n" - + "BgNVHSMEGDAWgBSSNQzgDx4rRfZNOfN7X6LmEpdAczAdBgNVHQ4EFgQUkjUM4A8e\n" - + "K0X2TTnze1+i5hKXQHMwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IB\n" - + "AQBoQHvwsR34hGO2m8qVR9nQ5Klo5HYPyd6ySKNcT36OZ4AQfaCGsk+SecTi35QF\n" - + "RHL3g2qffED4tKR0RBNGQSgiLavmHGCh3YpDupKq2xhhEeS9oBmQzxanFwWFod4T\n" - + "nnsG2cCejyR9WXoRzHisw0KJWeuNlwjUdJY0xnn16srm1zL/M/f0PvCyh9HU1mF1\n" - + "ivnOSqbDD2Z7JSGyckgKad1Omsg/rr5XYtCeyJeXUPcmpeX6erWJJNTUh6yWC/hY\n" - + "G/dFC4xrJhfXwz6Z0ytUygJO32bJG4Np2iGAwvvgI9EfxzEv/KP+FGrJOvQJAq4/\n" - + "BU36ZAa80W/8TBnqZTkNnqZV\n" - + "-----END CERTIFICATE-----\n" - + ""; - - final static String NODE_CERTIFICATE = "-----BEGIN CERTIFICATE-----\n" - + "MIIEyTCCA7GgAwIBAgIGAWLrc1O2MA0GCSqGSIb3DQEBCwUAMIGPMRMwEQYKCZIm\n" - + "iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ\n" - + "RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290\n" - + "IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwHhcNMTgwNDIy\n" - + "MDM0MzQ3WhcNMjgwNDE5MDM0MzQ3WjBeMRIwEAYKCZImiZPyLGQBGRYCZGUxDTAL\n" - + "BgNVBAcMBHRlc3QxDTALBgNVBAoMBG5vZGUxDTALBgNVBAsMBG5vZGUxGzAZBgNV\n" - + "BAMMEm5vZGUtMC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\n" - + "AQoCggEBAJa+f476vLB+AwK53biYByUwN+40D8jMIovGXm6wgT8+9Sbs899dDXgt\n" - + "9CE1Beo65oP1+JUz4c7UHMrCY3ePiDt4cidHVzEQ2g0YoVrQWv0RedS/yx/DKhs8\n" - + "Pw1O715oftP53p/2ijD5DifFv1eKfkhFH+lwny/vMSNxellpl6NxJTiJVnQ9HYOL\n" - + "gf2t971ITJHnAuuxUF48HcuNovW4rhtkXef8kaAN7cE3LU+A9T474ULNCKkEFPIl\n" - + "ZAKN3iJNFdVsxrTU+CUBHzk73Do1cCkEvJZ0ZFjp0Z3y8wLY/gqWGfGVyA9l2CUq\n" - + "eIZNf55PNPtGzOrvvONiui48vBKH1LsCAwEAAaOCAVkwggFVMIG8BgNVHSMEgbQw\n" - + "gbGAFJI1DOAPHitF9k0583tfouYSl0BzoYGVpIGSMIGPMRMwEQYKCZImiZPyLGQB\n" - + "GRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQRXhhbXBs\n" - + "ZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290IENBMSEw\n" - + "HwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0GCAQEwHQYDVR0OBBYEFKyv\n" - + "78ZmFjVKM9g7pMConYH7FVBHMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgXg\n" - + "MCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjA1BgNVHREELjAsiAUq\n" - + "AwQFBYISbm9kZS0wLmV4YW1wbGUuY29tgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZI\n" - + "hvcNAQELBQADggEBAIOKuyXsFfGv1hI/Lkpd/73QNqjqJdxQclX57GOMWNbOM5H0\n" - + "5/9AOIZ5JQsWULNKN77aHjLRr4owq2jGbpc/Z6kAd+eiatkcpnbtbGrhKpOtoEZy\n" - + "8KuslwkeixpzLDNISSbkeLpXz4xJI1ETMN/VG8ZZP1bjzlHziHHDu0JNZ6TnNzKr\n" - + "XzCGMCohFfem8vnKNnKUneMQMvXd3rzUaAgvtf7Hc2LTBlf4fZzZF1EkwdSXhaMA\n" - + "1lkfHiqOBxtgeDLxCHESZ2fqgVqsWX+t3qHQfivcPW6txtDyrFPRdJOGhiMGzT/t\n" - + "e/9kkAtQRgpTb3skYdIOOUOV0WGQ60kJlFhAzIs=\n" - + "-----END CERTIFICATE-----\n" - + ""; - - final static String NODE_KEY = "-----BEGIN PRIVATE KEY-----\n" - + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCWvn+O+rywfgMC\n" - + "ud24mAclMDfuNA/IzCKLxl5usIE/PvUm7PPfXQ14LfQhNQXqOuaD9fiVM+HO1BzK\n" - + "wmN3j4g7eHInR1cxENoNGKFa0Fr9EXnUv8sfwyobPD8NTu9eaH7T+d6f9oow+Q4n\n" - + "xb9Xin5IRR/pcJ8v7zEjcXpZaZejcSU4iVZ0PR2Di4H9rfe9SEyR5wLrsVBePB3L\n" - + "jaL1uK4bZF3n/JGgDe3BNy1PgPU+O+FCzQipBBTyJWQCjd4iTRXVbMa01PglAR85\n" - + "O9w6NXApBLyWdGRY6dGd8vMC2P4KlhnxlcgPZdglKniGTX+eTzT7Rszq77zjYrou\n" - + "PLwSh9S7AgMBAAECggEABwiohxFoEIwws8XcdKqTWsbfNTw0qFfuHLuK2Htf7IWR\n" - + "htlzn66F3F+4jnwc5IsPCoVFriCXnsEC/usHHSMTZkL+gJqxlNaGdin6DXS/aiOQ\n" - + "nb69SaQfqNmsz4ApZyxVDqsQGkK0vAhDAtQVU45gyhp/nLLmmqP8lPzMirOEodmp\n" - + "U9bA8t/ttrzng7SVAER42f6IVpW0iTKTLyFii0WZbq+ObViyqib9hVFrI6NJuQS+\n" - + "IelcZB0KsSi6rqIjXg1XXyMiIUcSlhq+GfEa18AYgmsbPwMbExate7/8Ci7ZtCbh\n" - + "lx9bves2+eeqq5EMm3sMHyhdcg61yzd5UYXeZhwJkQKBgQDS9YqrAtztvLY2gMgv\n" - + "d+wOjb9awWxYbQTBjx33kf66W+pJ+2j8bI/XX2CpZ98w/oq8VhMqbr9j5b8MfsrF\n" - + "EoQvedA4joUo8sXd4j1mR2qKF4/KLmkgy6YYusNP2UrVSw7sh77bzce+YaVVoO/e\n" - + "0wIVTHuD/QZ6fG6MasOqcbl6hwKBgQC27cQruaHFEXR/16LrMVAX+HyEEv44KOCZ\n" - + "ij5OE4P7F0twb+okngG26+OJV3BtqXf0ULlXJ+YGwXCRf6zUZkld3NMy3bbKPgH6\n" - + "H/nf3BxqS2tudj7+DV52jKtisBghdvtlKs56oc9AAuwOs37DvhptBKUPdzDDqfys\n" - + "Qchv5JQdLQKBgERev+pcqy2Bk6xmYHrB6wdseS/4sByYeIoi0BuEfYH4eB4yFPx6\n" - + "UsQCbVl6CKPgWyZe3ydJbU37D8gE78KfFagtWoZ56j4zMF2RDUUwsB7BNCDamce/\n" - + "OL2bCeG/Erm98cBG3lxufOX+z47I8fTNfkdY2k8UmhzoZwurLm73HJ3RAoGBAKsp\n" - + "6yamuXF2FbYRhUXgjHsBbTD/vJO72/yO2CGiLRpi/5mjfkjo99269trp0C8sJSub\n" - + "5PBiSuADXFsoRgUv+HI1UAEGaCTwxFTQWrRWdtgW3d0sE2EQDVWL5kmfT9TwSeat\n" - + "mSoyAYR5t3tCBNkPJhbgA7pm4mASzHQ50VyxWs25AoGBAKPFx9X2oKhYQa+mW541\n" - + "bbqRuGFMoXIIcr/aeM3LayfLETi48o5NDr2NDP11j4yYuz26YLH0Dj8aKpWuehuH\n" - + "uB27n6j6qu0SVhQi6mMJBe1JrKbzhqMKQjYOoy8VsC2gdj5pCUP/kLQPW7zm9diX\n" - + "CiKTtKgPIeYdigor7V3AHcVT\n" - + "-----END PRIVATE KEY-----\n" - + ""; - - final static String ADMIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----\n" - + "MIIEdzCCA1+gAwIBAgIGAWLrc1O4MA0GCSqGSIb3DQEBCwUAMIGPMRMwEQYKCZIm\n" - + "iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ\n" - + "RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290\n" - + "IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwHhcNMTgwNDIy\n" - + "MDM0MzQ3WhcNMjgwNDE5MDM0MzQ3WjBNMQswCQYDVQQGEwJkZTENMAsGA1UEBwwE\n" - + "dGVzdDEPMA0GA1UECgwGY2xpZW50MQ8wDQYDVQQLDAZjbGllbnQxDTALBgNVBAMM\n" - + "BGtpcmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCwgBOoO88uMM8\n" - + "dREJsk58Yt4Jn0zwQ2wUThbvy3ICDiEWhiAhUbg6dTggpS5vWWJto9bvaaqgMVoh\n" - + "ElfYHdTDncX3UQNBEP8tqzHON6BFEFSGgJRGLd6f5dri6rK32nCotYS61CFXBFxf\n" - + "WumXjSukjyrcTsdkR3C5QDo2oN7F883MOQqRENPzAtZi9s3jNX48u+/e3yvJzXsB\n" - + "GS9Qmsye6C71enbIujM4CVwDT/7a5jHuaUp6OuNCFbdRPnu/wLYwOS2/yOtzAqk7\n" - + "/PFnPCe7YOa10ShnV/jx2sAHhp7ZQBJgFkkgnIERz9Ws74Au+EbptWnsWuB+LqRL\n" - + "x5G02IzpAgMBAAGjggEYMIIBFDCBvAYDVR0jBIG0MIGxgBSSNQzgDx4rRfZNOfN7\n" - + "X6LmEpdAc6GBlaSBkjCBjzETMBEGCgmSJomT8ixkARkWA2NvbTEXMBUGCgmSJomT\n" - + "8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1wbGUgQ29tIEluYy4xITAfBgNV\n" - + "BAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEhMB8GA1UEAwwYRXhhbXBsZSBD\n" - + "b20gSW5jLiBSb290IENBggEBMB0GA1UdDgQWBBRsdhuHn3MGDvZxOe22+1wliCJB\n" - + "mDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIF4DAWBgNVHSUBAf8EDDAKBggr\n" - + "BgEFBQcDAjANBgkqhkiG9w0BAQsFAAOCAQEAkPrUTKKn+/6g0CjhTPBFeX8mKXhG\n" - + "zw5z9Oq+xnwefZwxV82E/tgFsPcwXcJIBg0f43BaVSygPiV7bXqWhxASwn73i24z\n" - + "lveIR4+z56bKIhP6c3twb8WWR9yDcLu2Iroin7dYEm3dfVUrhz/A90WHr6ddwmLL\n" - + "3gcFF2kBu3S3xqM5OmN/tqRXFmo+EvwrdJRiTh4Fsf0tX1ZT07rrGvBFYktK7Kma\n" - + "lqDl4UDCF1UWkiiFubc0Xw+DR6vNAa99E0oaphzvCmITU1wITNnYZTKzVzQ7vUCq\n" - + "kLmXOFLTcxTQpptxSo5xDD3aTpzWGCvjExCKpXQtsITUOYtZc02AGjjPOQ==\n" - + "-----END CERTIFICATE-----\n" - + ""; - - final static String ADMIN_KEY = "-----BEGIN PRIVATE KEY-----\n" - + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCwgBOoO88uMM8\n" - + "dREJsk58Yt4Jn0zwQ2wUThbvy3ICDiEWhiAhUbg6dTggpS5vWWJto9bvaaqgMVoh\n" - + "ElfYHdTDncX3UQNBEP8tqzHON6BFEFSGgJRGLd6f5dri6rK32nCotYS61CFXBFxf\n" - + "WumXjSukjyrcTsdkR3C5QDo2oN7F883MOQqRENPzAtZi9s3jNX48u+/e3yvJzXsB\n" - + "GS9Qmsye6C71enbIujM4CVwDT/7a5jHuaUp6OuNCFbdRPnu/wLYwOS2/yOtzAqk7\n" - + "/PFnPCe7YOa10ShnV/jx2sAHhp7ZQBJgFkkgnIERz9Ws74Au+EbptWnsWuB+LqRL\n" - + "x5G02IzpAgMBAAECggEAEzwnMkeBbqqDgyRqFbO/PgMNvD7i0b/28V0dCtCPEVY6\n" - + "klzrg3RCERP5V9AN8VVkppYjPkCzZ2A4b0JpMUu7ncOmr7HCnoSCj2IfEyePSVg+\n" - + "4OHbbcBOAoDTHiI2myM/M9++8izNS34qGV4t6pfjaDyeQQ/5cBVWNBWnKjS34S5H\n" - + "rJWpAcDgxYk5/ah2Xs2aULZlXDMxbSikjrv+n4JIYTKFQo8ydzL8HQDBRmXAFLjC\n" - + "gNOSHf+5u1JdpY3uPIxK1ugVf8zPZ4/OEB23j56uu7c8+sZ+kZwfRWAQmMhFVG/y\n" - + "OXxoT5mOruBsAw29m2Ijtxg252/YzSTxiDqFziB/eQKBgQDjeVAdi55GW/bvhuqn\n" - + "xME/An8E3hI/FyaaITrMQJUBjiCUaStTEqUgQ6A7ZfY/VX6qafOX7sli1svihrXC\n" - + "uelmKrdve/CFEEqzX9JWWRiPiQ0VZD+EQRsJvX85Tw2UGvVUh6dO3UGPS0BhplMD\n" - + "jeVpyXgZ7Gy5we+DWjfwhYrCmwKBgQDbLmQhRy+IdVljObZmv3QtJ0cyxxZETWzU\n" - + "MKmgBFvcRw+KvNwO+Iy0CHEbDu06Uj63kzI2bK3QdINaSrjgr8iftXIQpBmcgMF+\n" - + "a1l5HtHlCp6RWd55nWQOEvn36IGN3cAaQkXuh4UYM7QfEJaAbzJhyJ+wXA3jWqUd\n" - + "8bDTIAZ0ywKBgFuZ44gyTAc7S2JDa0Up90O/ZpT4NFLRqMrSbNIJg7d/m2EIRNkM\n" - + "HhCzCthAg/wXGo3XYq+hCdnSc4ICCzmiEfoBY6LyPvXmjJ5VDOeWs0xBvVIK74T7\n" - + "jr7KX2wdiHNGs9pZUidw89CXVhK8nptEzcheyA1wZowbK68yamph7HHXAoGBAK3x\n" - + "7D9Iyl1mnDEWPT7f1Gh9UpDm1TIRrDvd/tBihTCVKK13YsFy2d+LD5Bk0TpGyUVR\n" - + "STlOGMdloFUJFh4jA3pUOpkgUr8Uo/sbYN+x6Ov3+I3sH5aupRhSURVA7YhUIz/z\n" - + "tqIt5R+m8Nzygi6dkQNvf+Qruk3jw0S3ahizwsvvAoGAL7do6dTLp832wFVxkEf4\n" - + "gg1M6DswfkgML5V/7GQ3MkIX/Hrmiu+qSuHhDGrp9inZdCDDYg5+uy1+2+RBMRZ3\n" - + "vDUUacvc4Fep05zp7NcjgU5y+/HWpuKVvLIlZAO1MBY4Xinqqii6RdxukIhxw7eT\n" - + "C6TPL5KAcV1R/XAihDhI18Y=\n" - + "-----END PRIVATE KEY-----\n" - + ""; -} diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificatesIssuer.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificatesIssuer.java new file mode 100644 index 0000000000..887e197369 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificatesIssuer.java @@ -0,0 +1,224 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +/* +* Copyright 2021 floragunn GmbH +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ + +package org.opensearch.test.framework.certificate; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.util.Calendar; +import java.util.Date; +import java.util.concurrent.atomic.AtomicLong; + +import com.google.common.base.Strings; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.RFC4519Style; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +import static java.util.Objects.requireNonNull; + +/** +*

+* The class is used to generate public key certificate. The class hides low level details related to certificate creation and +* usage of underlying Bouncy Castle library. +*

+*

+* The public key certificate according to its name contains a public key and some metadata. The metadata describes an entity (human, +* company, web server, IoT device, etc.) which is an owner of private key associated with the certificate (private key is not included +* into certificate and is a kind of secret). The responsibility of the class is to issue a certificate. To issue a certificate it is +* necessary to provide metadata which is embedded in the certificates. The metadata is represented by the class +* {@link CertificateMetadata}. Furthermore, the class needs a public key which also must be embedded in the certificate. To obtain public +* and private key pair the class uses {@link AlgorithmKit}. The result of creating certificate is data structure {@link CertificateData}. +* The class {@link CertificateData} contains entire information which is necessary to use the certificate by its owner, that is: +* certificate and private key. +*

+* +*

+* The class is able to create self-signed certificates or certificates signed by some entity. To create a self signed certificate +* the method {@link #issueSignedCertificate(CertificateMetadata, CertificateData)} is used, whereas to create signed certificates +* the method {@link #issueSignedCertificate(CertificateMetadata, CertificateData)} is employed. +*

+*

+* The instance of the class can be obtained by invocation of static method defined in class {@link CertificatesIssuerFactory}. +*

+*/ +class CertificatesIssuer { + + private static final Logger log = LogManager.getLogger(CertificatesIssuer.class); + + private static final AtomicLong ID_COUNTER = new AtomicLong(System.currentTimeMillis()); + + private final Provider securityProvider; + private final AlgorithmKit algorithmKit; + private final JcaX509ExtensionUtils extUtils; + + CertificatesIssuer(Provider securityProvider, AlgorithmKit algorithmKit) { + this.securityProvider = securityProvider; + this.algorithmKit = algorithmKit; + this.extUtils = getExtUtils(); + } + + /** + * The method creates a certificate with provided metadata and public key obtained from {@link #algorithmKit}. The result of invocation + * contains required data to use a certificate by its owner. + * + * @param certificateMetadata metadata which should be embedded into created certificate + * @return {@link CertificateData} which contain certificate and private key associated with the certificate. + */ + public CertificateData issueSelfSignedCertificate(CertificateMetadata certificateMetadata) { + try { + KeyPair publicAndPrivateKey = algorithmKit.generateKeyPair(); + X500Name issuerName = stringToX500Name(requireNonNull(certificateMetadata.getSubject(), "Certificate metadata are required.")); + X509CertificateHolder x509CertificateHolder = buildCertificateHolder( + certificateMetadata, + issuerName, + publicAndPrivateKey.getPublic(), + publicAndPrivateKey); + return new CertificateData(x509CertificateHolder, publicAndPrivateKey); + } catch (OperatorCreationException | CertIOException e) { + log.error("Error while generating certificate", e); + throw new RuntimeException("Error while generating self signed certificate", e); + } + } + + /** + * The method is similar to {@link #issueSignedCertificate(CertificateMetadata, CertificateData)} but additionally it signs created + * certificate using data from parentCertificateData. + * + * @param metadata metadata which should be embedded into created certificate + * @param parentCertificateData data required to signe a newly issued certificate (private key among others things). + * @return {@link CertificateData} which contain certificate and private key associated with the certificate. + */ + public CertificateData issueSignedCertificate(CertificateMetadata metadata, CertificateData parentCertificateData) { + try { + KeyPair publicAndPrivateKey = algorithmKit.generateKeyPair(); + KeyPair parentKeyPair = requireNonNull(parentCertificateData, "Issuer certificate data are required") + .getKeyPair(); + X500Name issuerName = parentCertificateData.getCertificateSubject(); + var x509CertificateHolder = buildCertificateHolder(requireNonNull(metadata, "Certificate metadata are required"), + issuerName, + publicAndPrivateKey.getPublic(), + parentKeyPair); + return new CertificateData(x509CertificateHolder, publicAndPrivateKey); + } catch (OperatorCreationException | CertIOException e) { + log.error("Error while generating signed certificate", e); + throw new RuntimeException("Error while generating signed certificate", e); + } + } + + private X509CertificateHolder buildCertificateHolder(CertificateMetadata certificateMetadata, + X500Name issuerName, + PublicKey certificatePublicKey, + KeyPair parentKeyPair) throws CertIOException, OperatorCreationException { + X509v3CertificateBuilder builder = builderWithBasicExtensions(certificateMetadata, issuerName, certificatePublicKey, parentKeyPair.getPublic()); + addSubjectAlternativeNameExtension(builder, certificateMetadata); + addExtendedKeyUsageExtension(builder, certificateMetadata); + return builder.build(createContentSigner(parentKeyPair.getPrivate())); + } + + private ContentSigner createContentSigner(PrivateKey privateKey) throws OperatorCreationException { + return new JcaContentSignerBuilder(algorithmKit.getSignatureAlgorithmName()) + .setProvider(securityProvider) + .build(privateKey); + } + + private void addExtendedKeyUsageExtension(X509v3CertificateBuilder builder, CertificateMetadata certificateMetadata) throws CertIOException { + if(certificateMetadata.hasExtendedKeyUsage()) { + builder.addExtension(Extension.extendedKeyUsage, true, certificateMetadata.getExtendedKeyUsage()); + } + } + + private X509v3CertificateBuilder builderWithBasicExtensions(CertificateMetadata certificateMetadata, + X500Name issuerName, + PublicKey certificatePublicKey, + PublicKey parentPublicKey) throws CertIOException { + X500Name subjectName = stringToX500Name(certificateMetadata.getSubject()); + Date validityStartDate = new Date(System.currentTimeMillis() - (24 * 3600 * 1000)); + Date validityEndDate = getEndDate(validityStartDate, certificateMetadata.getValidityDays()); + + BigInteger certificateSerialNumber = generateNextCertificateSerialNumber(); + return new X509v3CertificateBuilder(issuerName, certificateSerialNumber, validityStartDate, + validityEndDate, subjectName, SubjectPublicKeyInfo.getInstance(certificatePublicKey.getEncoded())) + .addExtension(Extension.basicConstraints, true, new BasicConstraints(certificateMetadata.isBasicConstrainIsCa())) + .addExtension(Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(parentPublicKey)) + .addExtension(Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(certificatePublicKey)) + .addExtension(Extension.keyUsage, true, certificateMetadata.asKeyUsage()); + } + + private void addSubjectAlternativeNameExtension(X509v3CertificateBuilder builder, CertificateMetadata metadata) throws CertIOException { + if(metadata.hasSubjectAlternativeNameExtension()){ + DERSequence subjectAlternativeNames = metadata.createSubjectAlternativeNames(); + builder.addExtension(Extension.subjectAlternativeName, false, subjectAlternativeNames); + } + } + + private Date getEndDate(Date startDate, int validityDays) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(startDate); + calendar.add(Calendar.DATE, validityDays); + return calendar.getTime(); + } + + private static JcaX509ExtensionUtils getExtUtils() { + try { + return new JcaX509ExtensionUtils(); + } catch (NoSuchAlgorithmException e) { + log.error("Getting certificate extension utils failed", e); + throw new RuntimeException("Getting certificate extension utils failed", e); + } + } + + private X500Name stringToX500Name(String distinguishedName) { + if (Strings.isNullOrEmpty(distinguishedName)) { + throw new RuntimeException("No DN (distinguished name) must not be null or empty"); + } + try { + return new X500Name(RFC4519Style.INSTANCE, distinguishedName); + } catch (IllegalArgumentException e) { + String message = String.format("Invalid DN (distinguished name) specified for %s certificate.", distinguishedName); + throw new RuntimeException(message, e); + } + } + + private BigInteger generateNextCertificateSerialNumber() { + return BigInteger.valueOf(ID_COUNTER.incrementAndGet()); + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificatesIssuerFactory.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificatesIssuerFactory.java new file mode 100644 index 0000000000..823a0c04ca --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificatesIssuerFactory.java @@ -0,0 +1,68 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.test.framework.certificate; + +import java.security.Provider; +import java.util.Optional; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import static org.opensearch.test.framework.certificate.AlgorithmKit.ecdsaSha256withEcdsa; +import static org.opensearch.test.framework.certificate.AlgorithmKit.rsaSha256withRsa; + +/** +* The class defines static factory method for class {@link CertificatesIssuer}. Object of class {@link CertificatesIssuer} created by +* various factory methods differs in terms of cryptographic algorithms used for certificates creation. +* +*/ +class CertificatesIssuerFactory { + + private static final int KEY_SIZE = 2048; + + private CertificatesIssuerFactory() { + + } + + private static final Provider DEFAULT_SECURITY_PROVIDER = new BouncyCastleProvider(); + + /** + * @see {@link #rsaBaseCertificateIssuer(Provider)} + */ + public static CertificatesIssuer rsaBaseCertificateIssuer() { + return rsaBaseCertificateIssuer(null); + } + + /** + * The method creates {@link CertificatesIssuer} which uses RSA algorithm for certificate creation. + * @param securityProvider determines cryptographic algorithm implementation, can be null. + * @return new instance of {@link CertificatesIssuer} + */ + public static CertificatesIssuer rsaBaseCertificateIssuer(Provider securityProvider) { + Provider provider = Optional.ofNullable(securityProvider).orElse(DEFAULT_SECURITY_PROVIDER); + return new CertificatesIssuer(provider, rsaSha256withRsa(provider, KEY_SIZE)); + } + + /** + * {@link #rsaBaseCertificateIssuer(Provider)} + */ + public static CertificatesIssuer ecdsaBaseCertificatesIssuer() { + return ecdsaBaseCertificatesIssuer(null); + } + + /** + * It creates {@link CertificatesIssuer} which uses asymmetric cryptography algorithm which relays on elliptic curves. + * @param securityProvider determines cryptographic algorithm implementation, can be null. + * @return new instance of {@link CertificatesIssuer} + */ + public static CertificatesIssuer ecdsaBaseCertificatesIssuer(Provider securityProvider) { + Provider provider = Optional.ofNullable(securityProvider).orElse(DEFAULT_SECURITY_PROVIDER); + return new CertificatesIssuer(provider, ecdsaSha256withEcdsa(securityProvider, "P-384")); + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/PemConverter.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/PemConverter.java new file mode 100644 index 0000000000..a92e2036d7 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/PemConverter.java @@ -0,0 +1,120 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +/* +* Copyright 2021 floragunn GmbH +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +*/ + +package org.opensearch.test.framework.certificate; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.security.PrivateKey; +import java.security.SecureRandom; + +import com.google.common.base.Strings; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.openssl.PKCS8Generator; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.OutputEncryptor; +import org.bouncycastle.util.io.pem.PemGenerationException; +import org.bouncycastle.util.io.pem.PemObject; + +import static java.util.Objects.requireNonNull; + +/** +* The class provides a method useful for converting certificate and private key into PEM format +* @see RFC 1421 +*/ +class PemConverter { + + private PemConverter() { + } + + private static final Logger log = LogManager.getLogger(PemConverter.class); + private static final SecureRandom secureRandom = new SecureRandom(); + + /** + * It converts certificate represented by {@link X509CertificateHolder} object to PEM format + * @param certificate is a certificate to convert + * @return {@link String} which contains PEM encoded certificate + */ + public static String toPem(X509CertificateHolder certificate) { + StringWriter stringWriter = new StringWriter(); + try (JcaPEMWriter writer = new JcaPEMWriter(stringWriter)) { + writer.writeObject(requireNonNull(certificate, "Certificate is required.")); + } catch (Exception e) { + throw new RuntimeException("Cannot write certificate in PEM format", e); + } + return stringWriter.toString(); + } + + /** + * It converts private key represented by class {@link PrivateKey} to PEM format. + * @param privateKey is a private key, cannot be null + * @param privateKeyPassword is a password used to encode private key, null for unencrypted private key + * @return {@link String} which contains PEM encoded private key + */ + public static String toPem(PrivateKey privateKey, String privateKeyPassword) { + try(StringWriter stringWriter = new StringWriter()){ + savePrivateKey(stringWriter, requireNonNull(privateKey, "Private key is required."), privateKeyPassword); + return stringWriter.toString(); + } catch (IOException e) { + throw new RuntimeException("Cannot convert private key into PEM format.", e); + } + } + + private static void savePrivateKey(Writer out, PrivateKey privateKey, String privateKeyPassword) { + try (JcaPEMWriter writer = new JcaPEMWriter(out)) { + writer.writeObject(createPkcs8PrivateKeyPem(privateKey, privateKeyPassword)); + } catch (Exception e) { + log.error("Error while writing private key.", e); + throw new RuntimeException("Error while writing private key ", e); + } + } + + private static PemObject createPkcs8PrivateKeyPem(PrivateKey privateKey, String password) { + try { + OutputEncryptor outputEncryptor = password == null ? null : getPasswordEncryptor(password); + return new PKCS8Generator(PrivateKeyInfo.getInstance(privateKey.getEncoded()), outputEncryptor).generate(); + } catch (PemGenerationException | OperatorCreationException e) { + log.error("Creating PKCS8 private key failed", e); + throw new RuntimeException("Creating PKCS8 private key failed", e); + } + } + + private static OutputEncryptor getPasswordEncryptor(String password) throws OperatorCreationException { + if (!Strings.isNullOrEmpty(password)) { + JceOpenSSLPKCS8EncryptorBuilder encryptorBuilder = new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.PBE_SHA1_3DES); + encryptorBuilder.setRandom(secureRandom); + encryptorBuilder.setPassword(password.toCharArray()); + return encryptorBuilder.build(); + } + return null; + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/PublicKeyUsage.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/PublicKeyUsage.java new file mode 100644 index 0000000000..8e9aba68e5 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/PublicKeyUsage.java @@ -0,0 +1,72 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.test.framework.certificate; + +import java.util.Objects; + +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; + +/** +* The class is associated with certificate extensions related to key usages. These extensions are defined by +* RFC 5280 and describes allowed usage of public kay which is embedded in +* certificate. The class is related to the following extensions: +*
    +*
  1. Key Usage, defined in section 4.2.1.3
  2. +*
  3. Extended Key Usage, defined in section 4.2.1.12
  4. +*
+* +* @see RFC 5280 +*/ +enum PublicKeyUsage { + DIGITAL_SIGNATURE(KeyUsage.digitalSignature), + KEY_CERT_SIGN(KeyUsage.keyCertSign), + CRL_SIGN(KeyUsage.cRLSign), + NON_REPUDIATION(KeyUsage.nonRepudiation), + KEY_ENCIPHERMENT(KeyUsage.keyEncipherment), + + SERVER_AUTH(KeyPurposeId.id_kp_serverAuth), + + CLIENT_AUTH(KeyPurposeId.id_kp_clientAuth); + + private final int keyUsage; + private final KeyPurposeId id; + + PublicKeyUsage(int keyUsage) { + this.keyUsage = keyUsage; + this.id = null; + } + + PublicKeyUsage(KeyPurposeId id) { + this.id = Objects.requireNonNull(id, "Key purpose id is required."); + this.keyUsage = 0; + } + + boolean isExtendedUsage() { + return this.id != null; + } + + boolean isNotExtendedUsage() { + return this.id == null; + } + + int asInt(){ + if(isExtendedUsage()) { + throw new RuntimeException("Integer value is not available for extended key usage"); + } + return keyUsage; + } + KeyPurposeId getKeyPurposeId() { + if(isExtendedUsage() == false){ + throw new RuntimeException("Key purpose id is not available."); + } + return id; + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java index d810f0b32f..df9bcfa41d 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java @@ -32,37 +32,137 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.opensearch.test.framework.certificate.PublicKeyUsage.CLIENT_AUTH; +import static org.opensearch.test.framework.certificate.PublicKeyUsage.CRL_SIGN; +import static org.opensearch.test.framework.certificate.PublicKeyUsage.DIGITAL_SIGNATURE; +import static org.opensearch.test.framework.certificate.PublicKeyUsage.KEY_CERT_SIGN; +import static org.opensearch.test.framework.certificate.PublicKeyUsage.KEY_ENCIPHERMENT; +import static org.opensearch.test.framework.certificate.PublicKeyUsage.NON_REPUDIATION; +import static org.opensearch.test.framework.certificate.PublicKeyUsage.SERVER_AUTH; /** -* Provides TLS certificates required in test cases. -* WIP At the moment the certificates are hard coded. -* This will be replaced by classes -* that can generate certificates on the fly. +* It provides TLS certificates required in test cases. The certificates are generated during process of creation objects of the class. +* The class exposes method which can be used to write certificates and private keys in temporally files. */ public class TestCertificates { + public static final Integer MAX_NUMBER_OF_NODE_CERTIFICATES = 3; + + private static final String CA_SUBJECT = "DC=com,DC=example,O=Example Com Inc.,OU=Example Com Inc. Root CA,CN=Example Com Inc. Root CA"; + private static final String ADMIN_DN = "CN=kirk,OU=client,O=client,L=test,C=de"; + private static final int CERTIFICATE_VALIDITY_DAYS = 365; + private static final String CERTIFICATE_FILE_EXTENSION = ".cert"; + private static final String KEY_FILE_EXTENSION = ".key"; + private final CertificateData caCertificate; + + private final CertificateData adminCertificate; + private final List nodeCertificates; + + public TestCertificates() { + this.caCertificate = createCaCertificate(); + this.nodeCertificates = IntStream.range(0, MAX_NUMBER_OF_NODE_CERTIFICATES) + .mapToObj(this::createNodeCertificate) + .collect(Collectors.toList()); + this.adminCertificate = createAdminCertificate(); + } + + + private CertificateData createCaCertificate() { + CertificateMetadata metadata = CertificateMetadata.basicMetadata(CA_SUBJECT, CERTIFICATE_VALIDITY_DAYS) + .withKeyUsage(true, DIGITAL_SIGNATURE, KEY_CERT_SIGN, CRL_SIGN); + return CertificatesIssuerFactory + .rsaBaseCertificateIssuer() + .issueSelfSignedCertificate(metadata); + } + + private CertificateData createAdminCertificate() { + CertificateMetadata metadata = CertificateMetadata.basicMetadata(ADMIN_DN, CERTIFICATE_VALIDITY_DAYS) + .withKeyUsage(false, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, CLIENT_AUTH); + return CertificatesIssuerFactory + .rsaBaseCertificateIssuer() + .issueSelfSignedCertificate(metadata); + } + + /** + * It returns the most trusted certificate. Certificates for nodes and users are derived from this certificate. + * @return file which contains certificate in PEM format, defined by RFC 1421 + */ public File getRootCertificate() { - return createTempFile("root", ".cert", Certificates.ROOT_CA_CERTIFICATE); + return createTempFile("root", CERTIFICATE_FILE_EXTENSION, caCertificate.certificateInPemFormat()); } + /** + * Certificate for Open Search node. The certificate is derived from root certificate, returned by method {@link #getRootCertificate()} + * @param node is a node index. It has to be less than {@link #MAX_NUMBER_OF_NODE_CERTIFICATES} + * @return file which contains certificate in PEM format, defined by RFC 1421 + */ public File getNodeCertificate(int node) { - return createTempFile("node-" + node, ".cert", Certificates.NODE_CERTIFICATE); + isCorrectNodeNumber(node); + CertificateData certificateData = nodeCertificates.get(node); + return createTempFile("node-" + node, CERTIFICATE_FILE_EXTENSION, certificateData.certificateInPemFormat()); + } + + private void isCorrectNodeNumber(int node) { + if (node >= MAX_NUMBER_OF_NODE_CERTIFICATES) { + String message = String.format("Cannot get certificate for node %d, number of created certificates for nodes is %d", node, + MAX_NUMBER_OF_NODE_CERTIFICATES); + throw new RuntimeException(message); + } + } + + private CertificateData createNodeCertificate(Integer node) { + String subject = String.format("DC=de,L=test,O=node,OU=node,CN=node-%d.example.com", node); + String domain = String.format("node-%d.example.com", node); + CertificateMetadata metadata = CertificateMetadata.basicMetadata(subject, CERTIFICATE_VALIDITY_DAYS) + .withKeyUsage(false, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, CLIENT_AUTH, SERVER_AUTH) + .withSubjectAlternativeName("1.2.3.4.5.5", List.of(domain, "localhost"), "127.0.0.1"); + return CertificatesIssuerFactory + .rsaBaseCertificateIssuer() + .issueSignedCertificate(metadata, caCertificate); } - public File getNodeKey(int node) { - return createTempFile("node-" + node, ".key", Certificates.NODE_KEY); + /** + * It returns private key associated with node certificate returned by method {@link #getNodeCertificate(int)} + * + * @param node is a node index. It has to be less than {@link #MAX_NUMBER_OF_NODE_CERTIFICATES} + * @param privateKeyPassword is a password used to encode private key, can be null to retrieve unencrypted key. + * @return file which contains private key encoded in PEM format, defined + * by RFC 1421 + * @throws IOException + */ + public File getNodeKey(int node, String privateKeyPassword) { + CertificateData certificateData = nodeCertificates.get(node); + return createTempFile("node-" + node, KEY_FILE_EXTENSION, certificateData.privateKeyInPemFormat(privateKeyPassword)); } + /** + * Certificate which proofs admin user identity. Certificate is derived from root certificate returned by + * method {@link #getRootCertificate()} + * @return file which contains certificate in PEM format, defined by RFC 1421 + * @throws IOException + */ public File getAdminCertificate() { - return createTempFile("admin", ".cert", Certificates.ADMIN_CERTIFICATE); + return createTempFile("admin", CERTIFICATE_FILE_EXTENSION, adminCertificate.certificateInPemFormat()); } - public File getAdminKey() { - return createTempFile("admin", ".key", Certificates.ADMIN_KEY); + /** + * It returns private key associated with admin certificate returned by {@link #getAdminCertificate()}. + * + * @param privateKeyPassword is a password used to encode private key, can be null to retrieve unencrypted key. + * @return file which contains private key encoded in PEM format, defined + * by RFC 1421 + * @throws IOException + */ + public File getAdminKey(String privateKeyPassword) throws IOException { + return createTempFile("admin", KEY_FILE_EXTENSION, adminCertificate.privateKeyInPemFormat(privateKeyPassword)); } public String[] getAdminDNs() { - return new String[] {"CN=kirk,OU=client,O=client,L=test,C=de"}; + return new String[] {ADMIN_DN}; } private File createTempFile(String name, String suffix, String contents) { @@ -70,8 +170,8 @@ private File createTempFile(String name, String suffix, String contents) { Path path = Files.createTempFile(name, suffix); Files.writeString(path, contents); return path.toFile(); - } catch (IOException e) { - throw new RuntimeException(String.format("Error while creating a temp file: %s%s", name, suffix), e); + } catch (IOException ex) { + throw new RuntimeException("Cannot create temp file with name " + name + " and suffix " + suffix); } } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java index 37cf69b266..b2660c87e9 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java @@ -33,6 +33,9 @@ public class MinimumSecuritySettingsSupplierFactory { + private final String PRIVATE_KEY_HTTP_PASSWORD = "aWVV63OJ4qzZyPrBwl2MFny4ZV8lQRZchjL"; + private final String PRIVATE_KEY_TRANSPORT_PASSWORD = "iWbUv9w79sbd5tcxvSJNfHXS9GhcPCvdw9x"; + private TestCertificates testCertificates; public MinimumSecuritySettingsSupplierFactory(TestCertificates testCertificates) { @@ -57,12 +60,14 @@ private Settings.Builder minimumOpenSearchSettingsBuilder(int node, boolean sslO builder.put("plugins.security.ssl.transport.pemtrustedcas_filepath", testCertificates.getRootCertificate().getAbsolutePath()); builder.put("plugins.security.ssl.transport.pemcert_filepath", testCertificates.getNodeCertificate(node).getAbsolutePath()); - builder.put("plugins.security.ssl.transport.pemkey_filepath", testCertificates.getNodeKey(node).getAbsolutePath()); + builder.put("plugins.security.ssl.transport.pemkey_filepath", testCertificates.getNodeKey(node, PRIVATE_KEY_TRANSPORT_PASSWORD).getAbsolutePath()); + builder.put("plugins.security.ssl.transport.pemkey_password", PRIVATE_KEY_TRANSPORT_PASSWORD); builder.put("plugins.security.ssl.http.enabled", true); builder.put("plugins.security.ssl.http.pemtrustedcas_filepath", testCertificates.getRootCertificate().getAbsolutePath()); builder.put("plugins.security.ssl.http.pemcert_filepath", testCertificates.getNodeCertificate(node).getAbsolutePath()); - builder.put("plugins.security.ssl.http.pemkey_filepath", testCertificates.getNodeKey(node).getAbsolutePath()); + builder.put("plugins.security.ssl.http.pemkey_filepath", testCertificates.getNodeKey(node, PRIVATE_KEY_HTTP_PASSWORD).getAbsolutePath()); + builder.put("plugins.security.ssl.http.pemkey_password", PRIVATE_KEY_HTTP_PASSWORD); builder.putList("plugins.security.authcz.admin_dn", testCertificates.getAdminDNs());