/*
* 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;
// CS-SUPPRESS-SINGLE: RegexpSingleline Extension is used to refer to certificate extensions, keeping this rule disable for the whole file
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);
}
}
// CS-ENFORCE-SINGLE