diff --git a/nucleus/admin/server-mgmt/pom.xml b/nucleus/admin/server-mgmt/pom.xml index dd9fd866823..dacddee54db 100644 --- a/nucleus/admin/server-mgmt/pom.xml +++ b/nucleus/admin/server-mgmt/pom.xml @@ -51,7 +51,7 @@ 4.0.0 server-mgmt glassfish-jar - + admin-server-management Server Management @@ -77,7 +77,7 @@ **/*.xsd - + org.codehaus.mojo @@ -129,7 +129,7 @@ - + org.glassfish.main.common @@ -162,5 +162,13 @@ org.glassfish.annotations logging-annotation-processor + + org.bouncycastle + bcprov-jdk15on + + + org.bouncycastle + bcpkix-jdk15on + diff --git a/nucleus/admin/server-mgmt/src/main/java/fish/payara/admin/servermgmt/cli/PrintCertificateCommand.java b/nucleus/admin/server-mgmt/src/main/java/fish/payara/admin/servermgmt/cli/PrintCertificateCommand.java new file mode 100644 index 00000000000..e166ddcb842 --- /dev/null +++ b/nucleus/admin/server-mgmt/src/main/java/fish/payara/admin/servermgmt/cli/PrintCertificateCommand.java @@ -0,0 +1,254 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2019 Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package fish.payara.admin.servermgmt.cli; + +import com.sun.enterprise.admin.cli.CLICommand; +import com.sun.enterprise.admin.servermgmt.KeystoreManager; +import com.sun.enterprise.security.auth.realm.certificate.CertificateRealm; + +import java.io.File; +import java.security.KeyStore; +import java.security.Provider; +import java.security.PublicKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.security.interfaces.DSAKey; +import java.security.interfaces.RSAKey; +import java.util.Collection; +import java.util.concurrent.Callable; + +import javax.security.auth.x500.X500Principal; + +import org.glassfish.api.Param; +import org.glassfish.api.admin.CommandException; +import org.glassfish.api.admin.CommandValidationException; +import org.glassfish.hk2.api.PerLookup; +import org.jvnet.hk2.annotations.Service; + +/** + * Prints information about a certificate given in a file. + *

+ * Uses RFC-2253 and the {@link CertificateRealm#OID_MAP} for the principal distinguished name. + * The Subject from the output can be directly used for a role mapping when using client + * certificate authentication, f.e.
+ * <principal-name>CN=My Client,OU=Payara,O=Payara Foundation,L=Great Malvern,ST=Worcestershire,C=UK</principal-name> + *

+ * WARNING: JKS and JCEKS may be removed from some JDKs, then this command may fail. + * + * @author David Matejcek + */ +@Service(name = "print-certificate") +@PerLookup +public class PrintCertificateCommand extends CLICommand { + + @Param(name = "file", primary = true) + String file; + + @Param(name = "certificatealias", optional = true, defaultValue = "") + String certificateAlias; + + @Param(name = "providerclass", optional = true) + String providerClass; + + private Provider provider; + + private File derFile; + + private File keystoreFile; + + private char[] keystorePassword; + + @Override + protected void validate() throws CommandException, CommandValidationException { + super.validate(); + if (!ok(this.file)) { + throw new CommandValidationException("The file with the certificate must be specified."); + } + final File sourceFile = new File(this.file); + if (!sourceFile.canRead()) { + throw new CommandValidationException( + "The file '" + this.file + "' with the certificate must exist and must be readable."); + } + if (ok(this.providerClass)) { + try { + this.provider = (Provider) Class.forName(this.providerClass).newInstance(); + } catch (final ReflectiveOperationException e) { + throw new CommandValidationException("The provider class was not found on classpath.", e); + } + } + + if (isDerEncodedFile()) { + this.derFile = sourceFile; + } else if (isKeystoreFile()) { + this.keystoreFile = sourceFile; + if (!ok(this.certificateAlias)) { + throw new CommandValidationException("The certificate alias is mandatory for the keystore type."); + } + this.keystorePassword = getPassword("keystorePassword", "Keystore Password", null, false); + } else { + throw new CommandValidationException("The file type is not supported by this command."); + } + } + + + @Override + protected int executeCommand() throws CommandException { + if (this.provider != null) { + Security.insertProviderAt(this.provider, 1); + } + final X509Certificate certificate = getCertificate(); + System.out.println("Found Certificate:\n" + toPayaraFormattedString(certificate)); + return 0; + } + + + private String toPayaraFormattedString(final X509Certificate certificate) { + final StringBuilder output = new StringBuilder(1024); + output.append("Subject: ").append(toString(certificate.getSubjectX500Principal())); + output.append("\nValidity: ").append(certificate.getNotBefore()).append(" - ").append(certificate.getNotAfter()); + output.append("\nS/N: ").append(certificate.getSerialNumber()); + output.append("\nVersion: ").append(certificate.getVersion()); + output.append("\nIssuer: ").append(toString(certificate.getIssuerX500Principal())); + output.append("\nPublic Key: ").append(toString(certificate.getPublicKey())); + output.append("\nSign. Alg.: ").append(certificate.getSigAlgName()).append(" (OID: ").append(certificate.getSigAlgOID()).append(')'); + + return output.toString(); + } + + + private String toString(final X500Principal principal) { + return principal.getName(X500Principal.RFC2253, CertificateRealm.OID_MAP); + } + + + private String toString(final PublicKey key) { + if (key instanceof RSAKey) { + final RSAKey rsaKey = (RSAKey) key; + return key.getAlgorithm() + ", " + rsaKey.getModulus().bitLength() + " bits"; + } + if (key instanceof DSAKey) { + final DSAKey dsaKey = (DSAKey) key; + if (dsaKey.getParams() != null) { + return key.getAlgorithm() + ", " + dsaKey.getParams().getP().bitLength() + " bits"; + } + } + return key.getAlgorithm() + ", unresolved bit length."; + } + + + private boolean isDerEncodedFile() { + return this.file.trim().matches(".*\\.(cer|cert|crt|der|pem)"); + } + + + private boolean isKeystoreFile() { + // PKCS #12: p12, pfx, pkcs12 + // Other Java Keystores: jks, jceks (both Oracle proprietary) + return this.file.trim().matches(".*\\.(jks|jceks|pkcs12|pfx|p12)"); + } + + + private String getKeystoreType() { + final String ksFilename = this.keystoreFile.getName().toLowerCase(); + if (ksFilename.endsWith("jks")) { + return "JKS"; + } + if (ksFilename.endsWith("jceks")) { + return "JCEKS"; + } + if (ksFilename.endsWith("p12") || ksFilename.endsWith("pfx") || ksFilename.endsWith("pkcs12")) { + return "PKCS12"; + } + throw new IllegalStateException("Reached unreachable code, validation is incomplete!"); + } + + + private X509Certificate getCertificate() throws CommandException { + if (this.derFile != null) { + return getCertificateFromDerFile(); + } + if (this.keystoreFile != null) { + return getCertificateFromKeystore(); + } + throw new CommandException("Could not read the certificate from the provided file."); + } + + + private X509Certificate getCertificateFromDerFile() throws CommandException { + try { + final Callable supplier = () -> { + final KeystoreManager manager = new KeystoreManager(); + final Collection chain = manager.readPemCertificateChain(this.derFile); + if (chain.isEmpty()) { + return null; + } + return chain.iterator().next(); + }; + return getX509Certificate(supplier); + } catch (final Exception e) { + throw new CommandException("Could not read the certificate from the provided file.", e); + } + } + + + private X509Certificate getCertificateFromKeystore() throws CommandException { + try { + final Callable supplier = () -> { + final KeystoreManager manager = new KeystoreManager(); + final String ksType = getKeystoreType(); + final KeyStore keystore = manager.openKeyStore(this.keystoreFile, ksType, this.keystorePassword); + return keystore.getCertificate(this.certificateAlias); + }; + return getX509Certificate(supplier); + } catch (final Exception e) { + throw new CommandException("Could not read the certificate from the provided keystore.", e); + } + } + + + private static X509Certificate getX509Certificate(final Callable supplier) throws Exception { + final Certificate certificate = supplier.call(); + if (certificate instanceof X509Certificate) { + return (X509Certificate) certificate; + } + throw new IllegalStateException("The certificate was found but it is not supported X509 certificate."); + } +} diff --git a/nucleus/admin/server-mgmt/src/test/java/fish/payara/admin/servermgmt/cli/PrintCertificateCommandTest.java b/nucleus/admin/server-mgmt/src/test/java/fish/payara/admin/servermgmt/cli/PrintCertificateCommandTest.java new file mode 100644 index 00000000000..4758ad1d099 --- /dev/null +++ b/nucleus/admin/server-mgmt/src/test/java/fish/payara/admin/servermgmt/cli/PrintCertificateCommandTest.java @@ -0,0 +1,287 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2019 Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package fish.payara.admin.servermgmt.cli; + +import static com.sun.enterprise.util.SystemPropertyConstants.INSTALL_ROOT_PROPERTY; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.X500NameBuilder; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.glassfish.api.admin.CommandValidationException; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Tests the {@link PrintCertificateCommand} with default and BouncyCastle provider. + * Verifies work with several file formats. + * + * @author David Matejcek + */ +@RunWith(Parameterized.class) +public class PrintCertificateCommandTest { + + private static final String ALIAS = "test"; + + private static final String KEYSTORE_PASSWORD = "changeit"; + + private static final File FILE_JKS = new File("target/pcct.jks"); + private static final File FILE_PKCS12 = new File("target/pcct.p12"); + private static final File FILE_JCEKS = new File("target/pcct.jceks"); + private static final File FILE_DER = new File("target/pcct.der"); + private static final File FILE_PEM = new File("target/pcct.pem"); + + private static final PrintStream ORIGINAL_STDOUT = System.out; + private static final String ORIGINAL_INSTALL_ROOT = System.getProperty(INSTALL_ROOT_PROPERTY); + + private ByteArrayOutputStream stdout; + private int providerCount; + + private final PrintCertificateCommandMock command; + + + @BeforeClass + public static void initEnvironment() throws Exception { + // prevents NPE in CLICommand + if (ORIGINAL_INSTALL_ROOT == null) { + System.setProperty(INSTALL_ROOT_PROPERTY, "."); + } + + final KeyPair keyPair = createKeyPair(); + final X509Certificate certificate = createSelfSignedCertificate(keyPair); + + saveKeyStore(keyPair.getPrivate(), certificate, FILE_PKCS12, "PKCS12"); + saveKeyStore(keyPair.getPrivate(), certificate, FILE_JKS, "JKS"); + saveKeyStore(keyPair.getPrivate(), certificate, FILE_JCEKS, "JCEKS"); + saveDer(certificate); + savePem(certificate); + } + + + @AfterClass + public static void resetEnvironment() { + if (ORIGINAL_INSTALL_ROOT == null) { + System.clearProperty(INSTALL_ROOT_PROPERTY); + } else { + System.setProperty(INSTALL_ROOT_PROPERTY, ORIGINAL_INSTALL_ROOT); + } + System.setOut(ORIGINAL_STDOUT); + } + + + @Parameters(name = "{index}: {0}") + public static Collection data() { + return Arrays.asList(new Object[][] { // + {FILE_DER, null}, {FILE_PEM, null}, {FILE_PKCS12, ALIAS}, {FILE_JKS, ALIAS}, {FILE_JCEKS, ALIAS} // + }); + } + + + public PrintCertificateCommandTest(final File file, final String alias) { + this.command = new PrintCertificateCommandMock(); + this.command.certificateAlias = alias; + this.command.file = file.getAbsolutePath(); + } + + + @Before + public void init() { + this.stdout = new ByteArrayOutputStream(); + System.setOut(new PrintStream(this.stdout)); + this.providerCount = Security.getProviders().length; + } + + + @After + public void cleanup() { + // if the count of providers changed, we were successful with the registration of one more. + if (Security.getProviders().length != this.providerCount) { + Security.removeProvider(Security.getProviders()[0].getName()); + } + } + + + @Test + public void testDefaultProvider() throws Exception { + this.command.validate(); + this.command.executeCommand(); + final String[] output = this.stdout.toString().split("\n"); + + final String dn = "UID=LDAP-Test,EMAILADDRESS=nobody@nowhere.space,CN=PrintCertificateCommandTest," + + "OU=Test Test\\, Test,O=Payara Foundation,L=Pilsen,C=CZ"; + assertEquals("Found Certificate:", output[0]); + assertEquals("Subject: " + dn, output[1]); + assertThat(output[2], startsWith("Validity: ")); + assertEquals("S/N: 1", output[3]); + assertEquals("Version: 3", output[4]); + assertEquals("Issuer: " + dn, output[5]); + assertEquals("Public Key: RSA, 2048 bits", output[6]); + assertEquals("Sign. Alg.: SHA256withRSA (OID: 1.2.840.113549.1.1.11)", output[7]); + } + + + @Test + public void testBCProvider() throws Exception { + this.command.providerClass = BouncyCastleProvider.class.getName(); + this.command.validate(); + this.command.executeCommand(); + final String[] output = this.stdout.toString().split("\n"); + + final String dn = "UID=LDAP-Test,EMAILADDRESS=nobody@nowhere.space,CN=PrintCertificateCommandTest," + + "OU=Test Test\\, Test,O=Payara Foundation,L=Pilsen,C=CZ"; + assertEquals("Found Certificate:", output[0]); + assertEquals("Subject: " + dn, output[1]); + assertThat(output[2], startsWith("Validity: ")); + assertEquals("S/N: 1", output[3]); + assertEquals("Version: 3", output[4]); + assertEquals("Issuer: " + dn, output[5]); + assertEquals("Public Key: RSA, 2048 bits", output[6]); + assertEquals("Sign. Alg.: SHA256WITHRSA (OID: 1.2.840.113549.1.1.11)", output[7]); + } + + + private static void saveKeyStore(final PrivateKey key, final X509Certificate certificate, // + final File keystoreFile, final String keystoreType) throws Exception { + + final KeyStore keystore = KeyStore.getInstance(keystoreType); + keystore.load(null, null); + keystore.setKeyEntry(ALIAS, key, "changeit".toCharArray(), new Certificate[] {certificate}); + try (final OutputStream os = new FileOutputStream(keystoreFile)) { + keystore.store(os, "changeit".toCharArray()); + } + System.out.println("File has been created: " + keystoreFile.getAbsolutePath()); + } + + + private static void saveDer(final X509Certificate certificate) throws Exception { + try (OutputStream os = new FileOutputStream(FILE_DER)) { + os.write(certificate.getEncoded()); + } + } + + + private static void savePem(final X509Certificate certificate) throws IOException { + try (JcaPEMWriter pw = new JcaPEMWriter( + new OutputStreamWriter(new FileOutputStream(FILE_PEM), StandardCharsets.US_ASCII))) { + pw.writeObject(certificate); + } + } + + + private static KeyPair createKeyPair() throws NoSuchAlgorithmException { + final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + return kpg.generateKeyPair(); + } + + + private static X509Certificate createSelfSignedCertificate(KeyPair keyPair) + throws OperatorCreationException, CertificateException { + + final Instant now = LocalDate.of(2019, 8, 1).atStartOfDay(ZoneId.of("UTC")).toInstant(); + final X500Name dn = new X500NameBuilder() // + .addRDN(BCStyle.C, "CZ") // + .addRDN(BCStyle.L, "Pilsen") // + .addRDN(BCStyle.O, "Payara Foundation") // + .addRDN(BCStyle.OU, "Test Test, Test") // + .addRDN(BCStyle.CN, PrintCertificateCommandTest.class.getSimpleName()) // + .addRDN(BCStyle.EmailAddress, "nobody@nowhere.space") // + .addRDN(BCStyle.UID, "LDAP-Test") // + .build(); + + final ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate()); + final JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(dn, BigInteger.ONE, + Date.from(now), Date.from(now.plus(Duration.ofDays(1))), dn, keyPair.getPublic()); + return new JcaX509CertificateConverter().getCertificate(certBuilder.build(contentSigner)); + } + + /** + * Mocks the getPassword method to avoid user interaction. + */ + private class PrintCertificateCommandMock extends PrintCertificateCommand { + + @Override + protected char[] getPassword(String paramname, String localizedPrompt, String localizedPromptConfirm, + boolean create) throws CommandValidationException { + return KEYSTORE_PASSWORD.toCharArray(); + } + } +} diff --git a/nucleus/pom.xml b/nucleus/pom.xml index d4276b5b503..0e630c5140f 100644 --- a/nucleus/pom.xml +++ b/nucleus/pom.xml @@ -1417,6 +1417,19 @@ Parent is ${project.parent} jaxb-osgi ${jaxb-impl.version} + + + org.bouncycastle + bcprov-jdk15on + 1.62 + test + + + org.bouncycastle + bcpkix-jdk15on + 1.62 + test +