From 462e240f0a99d5821154b09bc1ac3a23b8195670 Mon Sep 17 00:00:00 2001 From: James Nord Date: Mon, 24 Jun 2024 17:50:03 +0100 Subject: [PATCH 1/9] [JENKINS-73335] add support for certificates that are not PKCS12 formatted Provides an alternative method for entering Certificate credentials, required when running in FIPS as we can not use PKCS12 --- pom.xml | 7 +- .../impl/CertificateCredentialsImpl.java | 358 ++++++++++++++---- .../PEMUploadedKeyStoreSource/config.jelly | 108 ++++++ .../credentials/impl/Messages.properties | 3 +- 4 files changed, 399 insertions(+), 77 deletions(-) create mode 100644 src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMUploadedKeyStoreSource/config.jelly diff --git a/pom.xml b/pom.xml index f60ed1b70..800de47f9 100644 --- a/pom.xml +++ b/pom.xml @@ -105,7 +105,12 @@ configuration-as-code true - + + org.jenkins-ci.plugins + bouncycastle-api + + 2.30.1.78.1-238.v991b_a_c571a_29 + org.mockito diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java index b87577de6..c63b2490f 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java @@ -30,6 +30,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; +import hudson.PluginManager; import hudson.Util; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; @@ -41,18 +42,26 @@ import java.io.ObjectStreamException; import java.io.Serializable; import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; import java.security.UnrecoverableEntryException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; import java.security.cert.CertificateException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Enumeration; +import java.util.List; import java.util.Objects; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; +import jenkins.bouncycastle.api.PEMEncodable; import jenkins.model.Jenkins; import jenkins.security.FIPS140; import net.jcip.annotations.GuardedBy; @@ -143,17 +152,18 @@ public synchronized KeyStore getKeyStore() { if (keyStore == null || keyStoreLastModified < lastModified) { KeyStore keyStore; try { - keyStore = KeyStore.getInstance("PKCS12"); - } catch (KeyStoreException e) { - throw new IllegalStateException("PKCS12 is a keystore type per the JLS spec", e); - } - try { - keyStore.load(new ByteArrayInputStream(keyStoreSource.getKeyStoreBytes()), toCharArray(password)); - } catch (CertificateException | NoSuchAlgorithmException | IOException e) { + keyStore = keyStoreSource.toKeyStore(toCharArray(password)); + } catch (GeneralSecurityException | IOException e) { LogRecord lr = new LogRecord(Level.WARNING, "Credentials ID {0}: Could not load keystore from {1}"); lr.setParameters(new Object[]{getId(), keyStoreSource}); lr.setThrown(e); LOGGER.log(lr); + // provide an empty KeyStore for consumers + try { + keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + } catch (KeyStoreException e2) { + throw new IllegalStateException("JVM can not create a KeyStore of the JVM Default Type ("+ KeyStore.getDefaultType() +")", e2); + } } this.keyStore = keyStore; this.keyStoreLastModified = lastModified; @@ -220,12 +230,15 @@ public String getIconClassName() { public static abstract class KeyStoreSource extends AbstractDescribableImpl { /** - * Returns the {@code byte[]} content of the {@link KeyStore}. - * - * @return the {@code byte[]} content of the {@link KeyStore}. + * @deprecated code should neither implement nor call this. + * This is an internal representation of a KeyStore and use of this internal representation would require knowledge of the keystore type. + * @throws IllegalStateException always */ @NonNull - public abstract byte[] getKeyStoreBytes(); + @Deprecated(forRemoval = true) + public byte[] getKeyStoreBytes() { + throw new IllegalStateException("Callers should use toKeyStore"); + } /** * Returns a {@link System#currentTimeMillis()} comparable timestamp of when the content was last modified. @@ -236,6 +249,16 @@ public static abstract class KeyStoreSource extends AbstractDescribableImpl { + protected static FormValidation validateCertificateKeystore(KeyStore keyStore, char[] passwordChars) + throws KeyStoreException, NoSuchAlgorithmException { + int size = keyStore.size(); + if (size == 0) { + return FormValidation.warning(Messages.CertificateCredentialsImpl_EmptyKeystore()); + } + StringBuilder buf = new StringBuilder(); + boolean first = true; + for (Enumeration enumeration = keyStore.aliases(); enumeration.hasMoreElements(); ) { + String alias = enumeration.nextElement(); + if (first) { + first = false; + } else { + buf.append(", "); + } + buf.append(alias); + if (keyStore.isCertificateEntry(alias)) { + keyStore.getCertificate(alias); + } else if (keyStore.isKeyEntry(alias)) { + if (passwordChars == null) { + return FormValidation.warning( + Messages.CertificateCredentialsImpl_LoadKeyFailedQueryEmptyPassword(alias)); + } + try { + keyStore.getKey(alias, passwordChars); + } catch (UnrecoverableEntryException e) { + return FormValidation.warning(e, + Messages.CertificateCredentialsImpl_LoadKeyFailed(alias)); + } + } + } + return FormValidation.ok(StringUtils + .defaultIfEmpty(StandardCertificateCredentials.NameProvider.getSubjectDN(keyStore), + buf.toString())); + } + /** * {@inheritDoc} */ @@ -268,67 +327,6 @@ protected KeyStoreSourceDescriptor(Class clazz) { super(clazz); } - /** - * Helper method that performs form validation on a {@link KeyStore}. - * - * @param type the type of keystore to instantiate, see {@link KeyStore#getInstance(String)}. - * @param keystoreBytes the {@code byte[]} content of the {@link KeyStore}. - * @param password the password to use when loading the {@link KeyStore} and recovering the key from the - * {@link KeyStore}. - * @return the validation results. - */ - @NonNull - protected static FormValidation validateCertificateKeystore(String type, byte[] keystoreBytes, - String password) { - - if (keystoreBytes == null || keystoreBytes.length == 0) { - return FormValidation.warning(Messages.CertificateCredentialsImpl_LoadKeystoreFailed()); - } - - char[] passwordChars = toCharArray(Secret.fromString(password)); - try { - KeyStore keyStore = KeyStore.getInstance(type); - keyStore.load(new ByteArrayInputStream(keystoreBytes), passwordChars); - int size = keyStore.size(); - if (size == 0) { - return FormValidation.warning(Messages.CertificateCredentialsImpl_EmptyKeystore()); - } - StringBuilder buf = new StringBuilder(); - boolean first = true; - for (Enumeration enumeration = keyStore.aliases(); enumeration.hasMoreElements(); ) { - String alias = enumeration.nextElement(); - if (first) { - first = false; - } else { - buf.append(", "); - } - buf.append(alias); - if (keyStore.isCertificateEntry(alias)) { - keyStore.getCertificate(alias); - } else if (keyStore.isKeyEntry(alias)) { - if (passwordChars == null) { - return FormValidation.warning( - Messages.CertificateCredentialsImpl_LoadKeyFailedQueryEmptyPassword(alias)); - } - try { - keyStore.getKey(alias, passwordChars); - } catch (UnrecoverableEntryException e) { - return FormValidation.warning(e, - Messages.CertificateCredentialsImpl_LoadKeyFailed(alias)); - } - } - } - return FormValidation.ok(StringUtils - .defaultIfEmpty(StandardCertificateCredentials.NameProvider.getSubjectDN(keyStore), - buf.toString())); - } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) { - return FormValidation.warning(e, Messages.CertificateCredentialsImpl_LoadKeystoreFailed()); - } finally { - if (passwordChars != null) { - Arrays.fill(passwordChars, ' '); - } - } - } } /** @@ -450,6 +448,21 @@ public boolean isSnapshotSource() { return true; } + @Override + public KeyStore toKeyStore(char[] password) throws NoSuchAlgorithmException, CertificateException, KeyStoreException, KeyStoreException, IOException { + if (FIPS140.useCompliantAlgorithms()) { + Class self = this.getClass(); + String className = self.getName(); + String pluginName = Jenkins.get().getPluginManager().whichPlugin(self).getShortName(); + throw new IllegalStateException(className + " is not FIPS compliant and can not be used when Jenkins is in FIPS mode. " + + "An issue should be filed against the plugin " + pluginName + " to ensure it is adapted to be able to work in this mode"); + } + // legacy behaviour that assumed all KeyStoreSources where in the non compliant PKCS12 format + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(new ByteArrayInputStream(getKeyStoreBytes()), password); + return keyStore; + } + /** * {@inheritDoc} */ @@ -479,7 +492,7 @@ public static class DescriptorImpl extends KeyStoreSourceDescriptor { */ @Restricted(NoExternalUse.class) @Extension - public static DescriptorImpl extension() { + public static KeyStoreSourceDescriptor extension() { return FIPS140.useCompliantAlgorithms() ? null : new DescriptorImpl(); } @@ -542,7 +555,7 @@ public FormValidation doCheckUploadedKeystore(@QueryParameter String value, // Priority for the file, to cover the (re-)upload cases if (StringUtils.isNotEmpty(uploadedCertFile)) { byte[] uploadedCertFileBytes = Base64.getDecoder().decode(uploadedCertFile.getBytes(StandardCharsets.UTF_8)); - return validateCertificateKeystore("PKCS12", uploadedCertFileBytes, password); + return validateCertificateKeystore(uploadedCertFileBytes, password); } if (StringUtils.isBlank(value)) { @@ -558,10 +571,205 @@ public FormValidation doCheckUploadedKeystore(@QueryParameter String value, if (keystoreBytes == null || keystoreBytes.length == 0) { return FormValidation.error(Messages.CertificateCredentialsImpl_LoadKeystoreFailed()); } + return validateCertificateKeystore(keystoreBytes, password); + } + + /** + * Helper method that performs form validation on a {@link KeyStore}. + * + * @param type the type of keystore to instantiate, see {@link KeyStore#getInstance(String)}. + * @param keystoreBytes the {@code byte[]} content of the {@link KeyStore}. + * @param password the password to use when loading the {@link KeyStore} and recovering the key from the + * {@link KeyStore}. + * @return the validation results. + */ + @NonNull + protected static FormValidation validateCertificateKeystore(byte[] keystoreBytes, + String password) { + + ensureNotRunningInFIPSMode(); + if (keystoreBytes == null || keystoreBytes.length == 0) { + return FormValidation.warning(Messages.CertificateCredentialsImpl_LoadKeystoreFailed()); + } + + char[] passwordChars = toCharArray(Secret.fromString(password)); + try { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(new ByteArrayInputStream(keystoreBytes), passwordChars); + return validateCertificateKeystore(keyStore, passwordChars); + } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) { + return FormValidation.warning(e, Messages.CertificateCredentialsImpl_LoadKeystoreFailed()); + } finally { + if (passwordChars != null) { + Arrays.fill(passwordChars, ' '); + } + } + } + + } + } + + /** + * A user uploaded file containing a set of PEM encoded certificates and a key. + */ + public static class PEMUploadedKeyStoreSource extends KeyStoreSource implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * The uploaded PEM certs and key. + */ + private final SecretBytes pemBytes; + + /** + * Constructor able to receive file directly + * + * @param uploadedCertFile the file containing PEM encoded certs and key. + * @param uploadedKeystore the PEM data, in case the file is not uploaded (e.g. update of the password / description) + */ + @SuppressWarnings("unused") // by stapler + @DataBoundConstructor + public PEMUploadedKeyStoreSource(FileItem uploadedPemFile, @CheckForNull SecretBytes pemBytes) { + if (uploadedPemFile != null) { + byte[] fileBytes = uploadedPemFile.get(); + if (fileBytes.length != 0) { + pemBytes = SecretBytes.fromBytes(fileBytes); + } + } + this.pemBytes = pemBytes; + } + + /** + * Returns the private key file name. + * + * @return the private key file name. + */ + public SecretBytes getPemBytes() { + return pemBytes; + } + + /** + * {@inheritDoc} + */ + @Override + public long getKeyStoreLastModified() { + return 0L; // our content is final so it will never change + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isSnapshotSource() { + return true; + } + + @Override + public KeyStore toKeyStore(char[] password) throws NoSuchAlgorithmException, CertificateException, KeyStoreException, KeyStoreException, UnrecoverableKeyException, IOException { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + // PEM (rfc7468) only defined the textual encoding + // the data is always encapsulated in base64 + // the pre-emble defined to be labelchar ( %x21-2C / %x2E-7E any printable character; except hyphen-minus) + // As far as text is concerned this is all just ascii, however this is the text representation not what may be stored on disk + // mostly but not always this would only affect us if we where dealing with some esoteric single byte encoding or multibyte encoding + // for most purposes this is just going to be ASCII but lets assume UTF-8 (to match the bouncycastle plugin read methods) + String pem = new String(pemBytes.getPlainData(), StandardCharsets.UTF_8); + + return toKeyStore(pem, password); + } + + protected static KeyStore toKeyStore(String pem, char[] password) throws NoSuchAlgorithmException, CertificateException, KeyStoreException, KeyStoreException, UnrecoverableKeyException, IOException { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + List pemEncodeables = PEMEncodable.decodeAll(pem, password); + + // add the certs first + int i = 0; + for (PEMEncodable pe : pemEncodeables) { + Certificate cert = pe.toCertificate(); + if (cert != null) { + keyStore.setCertificateEntry("cert-"+ i++, cert); + } + } + // then the private keys so we already have the cert entries + i = 0; + for (PEMEncodable pe : pemEncodeables) { + PrivateKey pk = pe.toPrivateKey(); + if (pk != null) { + keyStore.setKeyEntry("key-" + i++, pk, password, null); + } + } + // XXX if something else (like a public key) was provided we should error... + return keyStore; + } - return validateCertificateKeystore("PKCS12", keystoreBytes, password); + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "PEMUploadedKeyStoreSource{pemBytes=******}"; + } + + @Extension + public static class DescriptorImpl extends KeyStoreSourceDescriptor { + + public static final String DEFAULT_VALUE = UploadedKeyStoreSource.class.getName() + ".default-value"; + + @NonNull + @Override + public String getDisplayName() { + return Messages.CertificateCredentialsImpl_PEMUploadedKeyStoreSourceDisplayName(); } + /** + * Checks the keystore content. + * + * @param value the keystore content. + * @param password the password. + * @return the {@link FormValidation} results. + */ + @SuppressWarnings("unused") // stapler form validation + @Restricted(NoExternalUse.class) + @RequirePOST + public FormValidation doCheckUploadedPemFile(@QueryParameter String value, + @QueryParameter String uploadedPemFile, + @QueryParameter String password) { + // Priority for the file, to cover the (re-)upload cases + if (StringUtils.isNotEmpty(uploadedPemFile)) { + byte[] uploadedCertFileBytes = Base64.getDecoder().decode(uploadedPemFile.getBytes(StandardCharsets.UTF_8)); + return validateCertificateKeystore(uploadedCertFileBytes, password); + } + + if (StringUtils.isBlank(value)) { + return FormValidation.error(Messages.CertificateCredentialsImpl_NoCertificateUploaded()); + } + if (DEFAULT_VALUE.equals(value)) { + return FormValidation.ok(); + } + + // If no file, we rely on the previous value, stored as SecretBytes in an hidden input + SecretBytes secretBytes = SecretBytes.fromString(value); + byte[] keystoreBytes = secretBytes.getPlainData(); + if (keystoreBytes == null || keystoreBytes.length == 0) { + return FormValidation.error(Messages.CertificateCredentialsImpl_LoadKeystoreFailed()); + } + return validateCertificateKeystore(keystoreBytes, password); + } + + private FormValidation validateCertificateKeystore(byte[] keystoreBytes, String password) { + char[] passwordChars = toCharArray(Secret.fromString(password)); + String pem = new String(keystoreBytes, StandardCharsets.UTF_8); + try { + KeyStore ks = PEMUploadedKeyStoreSource.toKeyStore(pem, passwordChars); + return validateCertificateKeystore(ks, passwordChars); + } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | UnrecoverableKeyException | IOException e) { + return FormValidation.warning(e, Messages.CertificateCredentialsImpl_LoadKeystoreFailed()); + } finally { + if (passwordChars != null) { + Arrays.fill(passwordChars, ' '); + } + } + } } } diff --git a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMUploadedKeyStoreSource/config.jelly b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMUploadedKeyStoreSource/config.jelly new file mode 100644 index 000000000..60a12c903 --- /dev/null +++ b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMUploadedKeyStoreSource/config.jelly @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties b/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties index b517bdfb8..3bd7cc6ef 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties +++ b/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties @@ -28,5 +28,6 @@ CertificateCredentialsImpl.LoadKeyFailed=Could retrieve key "{0}" CertificateCredentialsImpl.LoadKeyFailedQueryEmptyPassword=Could retrieve key "{0}". You may need to provide a password CertificateCredentialsImpl.LoadKeystoreFailed=Could not load keystore CertificateCredentialsImpl.NoCertificateUploaded=No certificate uploaded -CertificateCredentialsImpl.UploadedKeyStoreSourceDisplayName=Upload PKCS#12 certificate +CertificateCredentialsImpl.UploadedKeyStoreSourceDisplayName=Upload PKCS#12 certificate and key +CertificateCredentialsImpl.PEMUploadedKeyStoreSourceDisplayName=Upload PEM encoded certificate and key From 44569cb06473ce0bf839eaa4e604e55a345884b9 Mon Sep 17 00:00:00 2001 From: James Nord Date: Fri, 5 Jul 2024 12:59:01 +0100 Subject: [PATCH 2/9] Switch from file upload to user entry As PEMs are not binary files but tet encoding it makes more sense to have users enter them. Additionally feedback is that user normally manage the certificate chain and keys separately --- .../impl/CertificateCredentialsImpl.java | 278 +++++++++++------- .../PEMEntryKeyStoreSource/config.jelly | 34 +++ .../help-certChain.html | 37 +++ .../help-privateKey.html | 32 ++ .../PEMUploadedKeyStoreSource/config.jelly | 108 ------- .../UploadedKeyStoreSource/config.jelly | 5 + .../credentials.jelly | 2 +- .../credentials/impl/Messages.properties | 6 +- .../impl/CertificateCredentialsImplTest.java | 82 +++++- .../plugins/credentials/impl/certs.pem | 39 +++ .../plugins/credentials/impl/key.pem | 18 ++ 11 files changed, 427 insertions(+), 214 deletions(-) create mode 100644 src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/config.jelly create mode 100644 src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/help-certChain.html create mode 100644 src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/help-privateKey.html delete mode 100644 src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMUploadedKeyStoreSource/config.jelly create mode 100644 src/test/resources/com/cloudbees/plugins/credentials/impl/certs.pem create mode 100644 src/test/resources/com/cloudbees/plugins/credentials/impl/key.pem diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java index c63b2490f..ab6a0714c 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java @@ -30,7 +30,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; -import hudson.PluginManager; +import hudson.RelativePath; import hudson.Util; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; @@ -43,7 +43,6 @@ import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; -import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -52,7 +51,11 @@ import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateException; -import java.util.ArrayList; +import java.security.cert.X509Certificate; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.RSAKey; +import java.security.interfaces.RSAPrivateKey; import java.util.Arrays; import java.util.Base64; import java.util.Enumeration; @@ -61,6 +64,9 @@ import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.crypto.interfaces.DHPrivateKey; +import javax.security.auth.DestroyFailedException; import jenkins.bouncycastle.api.PEMEncodable; import jenkins.model.Jenkins; import jenkins.security.FIPS140; @@ -73,6 +79,7 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.interceptor.RequirePOST; +import org.kohsuke.stapler.verb.POST; public class CertificateCredentialsImpl extends BaseStandardCredentials implements StandardCertificateCredentials { @@ -146,6 +153,7 @@ private static char[] toCharArray(@NonNull Secret password) { * * @return the {@link KeyStore} containing the certificate. */ + @Override @NonNull public synchronized KeyStore getKeyStore() { long lastModified = keyStoreSource.getKeyStoreLastModified(); @@ -158,7 +166,7 @@ public synchronized KeyStore getKeyStore() { lr.setParameters(new Object[]{getId(), keyStoreSource}); lr.setThrown(e); LOGGER.log(lr); - // provide an empty KeyStore for consumers + // provide an empty uninitialised KeyStore for consumers try { keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); } catch (KeyStoreException e2) { @@ -176,6 +184,7 @@ public synchronized KeyStore getKeyStore() { * * @return the password used to protect the certificate's private key in {@link #getKeyStore()}. */ + @Override @NonNull public Secret getPassword() { return password; @@ -222,6 +231,23 @@ public String getDisplayName() { public String getIconClassName() { return "icon-application-certificate"; } + + @Restricted(NoExternalUse.class) + @POST + public FormValidation doCheckPassword(@QueryParameter String value) { + Secret s = Secret.fromString(value); + String pw = s.getPlainText(); + if (FIPS140.useCompliantAlgorithms() && pw.length() < 14) { + return FormValidation.error("password is too short (< 14 characters)"); + } + if (pw.isEmpty()) { + return FormValidation.warning("password is empty"); + } + if (pw.length() < 14) { + return FormValidation.warning("password is short (< 14 characters)"); + } + return FormValidation.ok(); + } } /** @@ -232,6 +258,7 @@ public static abstract class KeyStoreSource extends AbstractDescribableImpl pemEncodeables = PEMEncodable.decodeAll(pem, password); - - // add the certs first - int i = 0; - for (PEMEncodable pe : pemEncodeables) { - Certificate cert = pe.toCertificate(); - if (cert != null) { - keyStore.setCertificateEntry("cert-"+ i++, cert); - } - } - // then the private keys so we already have the cert entries - i = 0; - for (PEMEncodable pe : pemEncodeables) { - PrivateKey pk = pe.toPrivateKey(); - if (pk != null) { - keyStore.setKeyEntry("key-" + i++, pk, password, null); - } + keyStore.load(null, password); // initialise the keystore + + List pemEncodeableCerts = PEMEncodable.decodeAll(pemEncodedCerts, password); + List certs = pemEncodeableCerts.stream().map(PEMEncodable::toCertificate).filter(Objects::nonNull).collect(Collectors.toList()); + + List pemEncodeableKeys = PEMEncodable.decodeAll(pemEncodedKey, password); + if (pemEncodeableKeys.size() != 1) { + throw new IOException("expected one key but got " + pemEncodeableKeys.size()); } - // XXX if something else (like a public key) was provided we should error... + + PrivateKey privateKey = pemEncodeableKeys.get(0).toPrivateKey(); + + keyStore.setKeyEntry("keychain", privateKey, password, certs.toArray(new Certificate[] {})); + return keyStore; } @@ -707,67 +722,132 @@ protected static KeyStore toKeyStore(String pem, char[] password) throws NoSuchA */ @Override public String toString() { - return "PEMUploadedKeyStoreSource{pemBytes=******}"; + return "PEMEntryKeyStoreSource{pemCertChain=******,pemKey=******}"; } @Extension public static class DescriptorImpl extends KeyStoreSourceDescriptor { - public static final String DEFAULT_VALUE = UploadedKeyStoreSource.class.getName() + ".default-value"; - @NonNull @Override public String getDisplayName() { - return Messages.CertificateCredentialsImpl_PEMUploadedKeyStoreSourceDisplayName(); + return Messages.CertificateCredentialsImpl_PEMEntryKeyStoreSourceDisplayName(); } - /** - * Checks the keystore content. - * - * @param value the keystore content. - * @param password the password. - * @return the {@link FormValidation} results. - */ - @SuppressWarnings("unused") // stapler form validation @Restricted(NoExternalUse.class) - @RequirePOST - public FormValidation doCheckUploadedPemFile(@QueryParameter String value, - @QueryParameter String uploadedPemFile, - @QueryParameter String password) { - // Priority for the file, to cover the (re-)upload cases - if (StringUtils.isNotEmpty(uploadedPemFile)) { - byte[] uploadedCertFileBytes = Base64.getDecoder().decode(uploadedPemFile.getBytes(StandardCharsets.UTF_8)); - return validateCertificateKeystore(uploadedCertFileBytes, password); - } - - if (StringUtils.isBlank(value)) { - return FormValidation.error(Messages.CertificateCredentialsImpl_NoCertificateUploaded()); - } - if (DEFAULT_VALUE.equals(value)) { + @POST + public FormValidation doCheckCertChain(@QueryParameter String value) { + String pemCerts = Secret.fromString(value).getPlainText(); + try { + List pemEncodables = PEMEncodable.decodeAll(pemCerts, null); + long count = pemEncodables.stream().map(PEMEncodable::toCertificate).filter(Objects::nonNull).count(); + if (count < 1) { + return FormValidation.error("No Certificates provided"); + } + // ensure only certs are provided. + if (pemEncodables.size() != count) { + return FormValidation.error("PEM contains non certificate entries"); + } + Certificate cert = pemEncodables.get(0).toCertificate(); + if (cert instanceof X509Certificate) { + X509Certificate x509 = (X509Certificate) cert; + return FormValidation.ok(x509.getSubjectDN().getName()); + } + // no details return FormValidation.ok(); + } catch (UnrecoverableKeyException | IOException e) { + String message = e.getMessage(); + if (message != null) { + return FormValidation.error(e, "Could not parse certificate chain: " + message); + } + return FormValidation.error(e, "Could not parse certificate chain"); } + } - // If no file, we rely on the previous value, stored as SecretBytes in an hidden input - SecretBytes secretBytes = SecretBytes.fromString(value); - byte[] keystoreBytes = secretBytes.getPlainData(); - if (keystoreBytes == null || keystoreBytes.length == 0) { - return FormValidation.error(Messages.CertificateCredentialsImpl_LoadKeystoreFailed()); + @Restricted(NoExternalUse.class) + @POST + public FormValidation doCheckPrivateKey(@QueryParameter String value, + @RelativePath("..") + @QueryParameter String password) { + String key = Secret.fromString(value).getPlainText(); + try { + List pemEncodables = PEMEncodable.decodeAll(key, toCharArray(Secret.fromString(password))); + long count = pemEncodables.stream().map(PEMEncodable::toPrivateKey).filter(Objects::nonNull).count(); + if (count == 0) { + return FormValidation.error("No Keys Provided"); + } + if (count > 1) { + return FormValidation.error("More than 1 key provided"); + } + // ensure only keys are provided. + if (pemEncodables.size() != 1) { + return FormValidation.error("PEM contains non key entries"); + } + PrivateKey pk = pemEncodables.get(0).toPrivateKey(); + String format; + String length; + if (pk instanceof RSAPrivateKey) { + format = "RSA"; + length = ((RSAKey)pk).getModulus().bitLength() + " bit"; + } else if (pk instanceof ECPrivateKey) { + format = "elliptic curve (EC)"; + length = ((ECPrivateKey)pk).getParams().getOrder().bitLength() + " bit"; + } else if (pk instanceof DSAPrivateKey) { + format = "DSA"; + length = ((DSAPrivateKey)pk).getParams().getP().bitLength() + " bit"; + } else if (pk instanceof DHPrivateKey) { + format = "Diffie-Hellman"; + length = ((DHPrivateKey)pk).getParams().getP().bitLength() + " bit"; + } else if (pk != null) { + // spotbugs things pk may be null, but we have already checked + // the size of pemEncodables is one and contains a private key + // so it can not be + format = "unknown format (" + pk.getClass() +")"; + length = "unknown length"; + } else { // pk == null can not happen + return FormValidation.error("there is a bug in the code, pk is null!"); + } + try { + pk.destroy(); + } catch (@SuppressWarnings("unused") DestroyFailedException ignored) { + // best effort + } + return FormValidation.ok(length + " " + format + " private key"); + } catch (UnrecoverableKeyException | IOException e) { + return FormValidation.error(e, "Could not parse key: " + e.getLocalizedMessage()); } - return validateCertificateKeystore(keystoreBytes, password); } - private FormValidation validateCertificateKeystore(byte[] keystoreBytes, String password) { - char[] passwordChars = toCharArray(Secret.fromString(password)); - String pem = new String(keystoreBytes, StandardCharsets.UTF_8); + /** + * Checks all the values. + * + * @param certChain pem encoded list of certificates (possibly a secret) + * @param privateKey pem encoded private key (possibly a secret) + * @param password for the key (may be null, and possibly a secret) + * @return the {@link FormValidation} results. + */ + public FormValidation doXXCheckCertChain(@QueryParameter String certChain, + @QueryParameter String privateKey, + @RelativePath("..") + @QueryParameter String password) { + String certs = Secret.fromString(certChain).getPlainText(); + String key = Secret.fromString(privateKey).getPlainText(); + String pass = Secret.fromString(password).getPlainText(); + if (StringUtils.isBlank(certs)) { + return FormValidation.error(Messages.CertificateCredentialsImpl_PEMNoCertificates()); + } + if (StringUtils.isBlank(key)) { + return FormValidation.error(Messages.CertificateCredentialsImpl_PEMNoKey()); + } + // pass will always be blank see JENKINS-65616 + if (StringUtils.isBlank(pass)) { + return FormValidation.error(Messages.CertificateCredentialsImpl_PEMNoPassword()); + } try { - KeyStore ks = PEMUploadedKeyStoreSource.toKeyStore(pem, passwordChars); - return validateCertificateKeystore(ks, passwordChars); - } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | UnrecoverableKeyException | IOException e) { + KeyStore ks = PEMEntryKeyStoreSource.toKeyStore(certs, key, pass.toCharArray()); + return validateCertificateKeystore(ks, pass.toCharArray()); + } catch (GeneralSecurityException | IOException e) { return FormValidation.warning(e, Messages.CertificateCredentialsImpl_LoadKeystoreFailed()); - } finally { - if (passwordChars != null) { - Arrays.fill(passwordChars, ' '); - } } } } diff --git a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/config.jelly b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/config.jelly new file mode 100644 index 000000000..861c61fb6 --- /dev/null +++ b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/config.jelly @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/help-certChain.html b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/help-certChain.html new file mode 100644 index 000000000..f6c88ec2b --- /dev/null +++ b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/help-certChain.html @@ -0,0 +1,37 @@ + + +
+ A certificate chain containing one or more PEM encoded certificates. + The certificates must be in order such that each one directly certifies the preceding one. + The certificate, for which the private key will be entered below must appear first. +

The entry should look something like the following:

+
+-----BEGIN CERTIFICATE----- 
+Base64 encoded contents 
+-----END CERTIFICATE----- 
+-----BEGIN CERTIFICATE----- 
+Base64 encoded contents  
+-----END CERTIFICATE----
+
\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/help-privateKey.html b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/help-privateKey.html new file mode 100644 index 000000000..49c7226fd --- /dev/null +++ b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/help-privateKey.html @@ -0,0 +1,32 @@ + + +
+ A single PEM encoded private key that is the key for the primary certificate entered above. +

The entry should look something like the following:

+
+-----BEGIN PRIVATE KEY----- 
+Base64 encoded contents 
+-----END PRIVATE KEY----- 
+
\ No newline at end of file diff --git a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMUploadedKeyStoreSource/config.jelly b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMUploadedKeyStoreSource/config.jelly deleted file mode 100644 index 60a12c903..000000000 --- a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMUploadedKeyStoreSource/config.jelly +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/UploadedKeyStoreSource/config.jelly b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/UploadedKeyStoreSource/config.jelly index feac2db35..f0f25d667 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/UploadedKeyStoreSource/config.jelly +++ b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/UploadedKeyStoreSource/config.jelly @@ -55,6 +55,11 @@ uploadedCertFileInput.onchange = fileOnChange.bind(uploadedCertFileInput); } function fileOnChange() { + // only trigger validation if the PKCS12 upload is selected + var e = document.getElementById("${fileId}"); + if (e.closest(".form-container").className.indexOf("-hidden") != -1) { + return + } try { // inspired by https://stackoverflow.com/a/754398 var uploadedCertFileInputFile = uploadedCertFileInput.files[0]; var reader = new FileReader(); diff --git a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/credentials.jelly b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/credentials.jelly index 5e86264ed..28a479022 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/credentials.jelly +++ b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/credentials.jelly @@ -37,7 +37,7 @@ - + diff --git a/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties b/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties index 3bd7cc6ef..1b017a973 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties +++ b/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties @@ -29,5 +29,7 @@ CertificateCredentialsImpl.LoadKeyFailedQueryEmptyPassword=Could retrieve key "{ CertificateCredentialsImpl.LoadKeystoreFailed=Could not load keystore CertificateCredentialsImpl.NoCertificateUploaded=No certificate uploaded CertificateCredentialsImpl.UploadedKeyStoreSourceDisplayName=Upload PKCS#12 certificate and key -CertificateCredentialsImpl.PEMUploadedKeyStoreSourceDisplayName=Upload PEM encoded certificate and key - +CertificateCredentialsImpl.PEMEntryKeyStoreSourceDisplayName=PEM encoded certificate and key +CertificateCredentialsImpl.PEMNoCertificates=No certificates where provided +CertificateCredentialsImpl.PEMNoKey=No key was provided +CertificateCredentialsImpl.PEMNoPassword=No password was provided \ No newline at end of file diff --git a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java index 2e8c02f0e..97ff01fd1 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java @@ -41,12 +41,14 @@ import org.htmlunit.WebRequest; import org.htmlunit.html.DomNode; import org.htmlunit.html.DomNodeList; +import org.htmlunit.html.HtmlButton; import org.htmlunit.html.HtmlElementUtil; import org.htmlunit.html.HtmlFileInput; import org.htmlunit.html.HtmlForm; import org.htmlunit.html.HtmlOption; import org.htmlunit.html.HtmlPage; import org.htmlunit.html.HtmlRadioButtonInput; + import hudson.FilePath; import hudson.Util; import hudson.cli.CLICommandInvoker; @@ -57,6 +59,7 @@ import hudson.util.Secret; import jenkins.model.Jenkins; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -71,6 +74,7 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.security.KeyStore; import java.util.Base64; import java.util.Collections; import java.util.List; @@ -91,10 +95,14 @@ public class CertificateCredentialsImplTest { private File p12; private File p12Invalid; + private String pemCert; + private String pemKey; private static final String VALID_PASSWORD = "password"; private static final String INVALID_PASSWORD = "blabla"; private static final String EXPECTED_DISPLAY_NAME = "EMAILADDRESS=me@myhost.mydomain, CN=pkcs12, O=Fort-Funston, L=SanFrancisco, ST=CA, C=US"; + // BC uses a different format even though the file was converted from the pkcs12 file + private static final String EXPECTED_DISPLAY_NAME_PEM = "C=US,ST=CA,L=SanFrancisco,O=Fort-Funston,CN=pkcs12,E=me@myhost.mydomain"; @Before public void setup() throws IOException { @@ -103,6 +111,9 @@ public void setup() throws IOException { p12Invalid = tmp.newFile("invalid.p12"); FileUtils.copyURLToFile(CertificateCredentialsImplTest.class.getResource("invalid.p12"), p12Invalid); + pemCert = IOUtils.toString(CertificateCredentialsImplTest.class.getResource("certs.pem"), StandardCharsets.UTF_8); + pemKey = IOUtils.toString(CertificateCredentialsImplTest.class.getResource("key.pem"), StandardCharsets.UTF_8); + r.jenkins.setCrumbIssuer(null); } @@ -212,7 +223,8 @@ public void doCheckUploadedKeystore_keyStoreInvalid() throws Exception { @Issue("JENKINS-63761") public void fullSubmitOfUploadedKeystore() throws Exception { String certificateDisplayName = r.jenkins.getDescriptor(CertificateCredentialsImpl.class).getDisplayName(); - + String KeyStoreSourceDisplayName = r.jenkins.getDescriptor(CertificateCredentialsImpl.UploadedKeyStoreSource.class).getDisplayName(); + JenkinsRule.WebClient wc = r.createWebClient(); HtmlPage htmlPage = wc.goTo("credentials/store/system/domain/_/newCredentials"); HtmlForm newCredentialsForm = htmlPage.getFormByName("newCredentials"); @@ -234,9 +246,11 @@ public void fullSubmitOfUploadedKeystore() throws Exception { return false; }); assertTrue("The Certificate option was not found in the credentials type select", optionFound); - - HtmlRadioButtonInput keyStoreRadio = htmlPage.getDocumentElement().querySelector("input[name$=keyStoreSource]"); - HtmlElementUtil.click(keyStoreRadio); + + List inputs = htmlPage.getDocumentElement(). + getByXPath("//input[contains(@name, 'keyStoreSource') and following-sibling::label[contains(.,'"+KeyStoreSourceDisplayName+"')]]"); + assertThat("query should return only a singular input", inputs, hasSize(1)); + HtmlElementUtil.click(inputs.get(0)); HtmlFileInput uploadedCertFileInput = htmlPage.getDocumentElement().querySelector("input[type=file][name=uploadedCertFile]"); uploadedCertFileInput.setFiles(p12); @@ -258,6 +272,66 @@ public void fullSubmitOfUploadedKeystore() throws Exception { assertEquals(EXPECTED_DISPLAY_NAME, displayName); } + @Test + @Issue("JENKINS-73335") + public void fullSubmitOfUploadedPEM() throws Exception { + String certificateDisplayName = r.jenkins.getDescriptor(CertificateCredentialsImpl.class).getDisplayName(); + String KeyStoreSourceDisplayName = r.jenkins.getDescriptor(CertificateCredentialsImpl.PEMEntryKeyStoreSource.class).getDisplayName(); + + JenkinsRule.WebClient wc = r.createWebClient(); + HtmlPage htmlPage = wc.goTo("credentials/store/system/domain/_/newCredentials"); + HtmlForm newCredentialsForm = htmlPage.getFormByName("newCredentials"); + + DomNodeList allOptions = htmlPage.getDocumentElement().querySelectorAll("select.dropdownList option"); + boolean optionFound = allOptions.stream().anyMatch(domNode -> { + if (domNode instanceof HtmlOption) { + HtmlOption option = (HtmlOption) domNode; + if (option.getVisibleText().equals(certificateDisplayName)) { + try { + HtmlElementUtil.click(option); + } catch (IOException e) { + throw new RuntimeException(e); + } + return true; + } + } + + return false; + }); + assertTrue("The Certificate option was not found in the credentials type select", optionFound); + + List inputs = htmlPage.getDocumentElement(). + getByXPath("//input[contains(@name, 'keyStoreSource') and following-sibling::label[contains(.,'"+KeyStoreSourceDisplayName+"')]]"); + assertThat("query should return only a singular input", inputs, hasSize(1)); + HtmlElementUtil.click(inputs.get(0)); + + // enable entry of the secret (HACK just click all the Add buttons) + List buttonsByName = htmlPage.getDocumentElement().getByXPath("//button[contains(.,'Add')]"); + assertThat("I need 2 buttons", buttonsByName, hasSize(2)); + for (HtmlButton b : buttonsByName) { + HtmlElementUtil.click(b); + } + + newCredentialsForm.getTextAreaByName("_.certChain").setTextContent(pemCert); + newCredentialsForm.getTextAreaByName("_.privateKey").setTextContent(pemKey); + + // for all the types of credentials + newCredentialsForm.getInputsByName("_.password").forEach(input -> input.setValue(VALID_PASSWORD)); + + List certificateCredentials = CredentialsProvider.lookupCredentialsInItemGroup(CertificateCredentials.class, (ItemGroup) null, ACL.SYSTEM2); + assertThat(certificateCredentials, hasSize(0)); + + r.submit(newCredentialsForm); + + certificateCredentials = CredentialsProvider.lookupCredentialsInItemGroup(CertificateCredentials.class, (ItemGroup) null, ACL.SYSTEM2); + assertThat(certificateCredentials, hasSize(1)); + + CertificateCredentials certificate = certificateCredentials.get(0); + KeyStore ks = certificate.getKeyStore(); + String displayName = StandardCertificateCredentials.NameProvider.getSubjectDN(certificate.getKeyStore()); + assertEquals(EXPECTED_DISPLAY_NAME_PEM, displayName); + } + private String getValidP12_base64() throws Exception { return Base64.getEncoder().encodeToString(Files.readAllBytes(p12.toPath())); } diff --git a/src/test/resources/com/cloudbees/plugins/credentials/impl/certs.pem b/src/test/resources/com/cloudbees/plugins/credentials/impl/certs.pem new file mode 100644 index 000000000..40c1ad895 --- /dev/null +++ b/src/test/resources/com/cloudbees/plugins/credentials/impl/certs.pem @@ -0,0 +1,39 @@ +-----BEGIN CERTIFICATE----- +MIIDRzCCArCgAwIBAgIBATANBgkqhkiG9w0BAQQFADBmMQswCQYDVQQGEwJLRzEL +MAkGA1UECBMCTkExEDAOBgNVBAcTB0JJU0hLRUsxFTATBgNVBAoTDE9wZW5WUE4t +VEVTVDEhMB8GCSqGSIb3DQEJARYSbWVAbXlob3N0Lm15ZG9tYWluMB4XDTA1MDgw +NDE4MTYyMFoXDTE1MDgwMjE4MTYyMFowfDELMAkGA1UEBhMCVVMxCzAJBgNVBAgT +AkNBMRUwEwYDVQQHEwxTYW5GcmFuY2lzY28xFTATBgNVBAoTDEZvcnQtRnVuc3Rv +bjEPMA0GA1UEAxMGcGtjczEyMSEwHwYJKoZIhvcNAQkBFhJtZUBteWhvc3QubXlk +b21haW4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMOT0PRbWiTEJTUjjiwW +yPC7hR2ruxshzWcgWZUuNg5RARnnsQfGpBK+kKp4QsJSunVCo2fmUFkU/UGYVVXK +nHMEcDtX2JqVY/bAPjxptn5k1bnvMFkKFnaAZl5Mi0K0s+D9U0ivpIaw1QXdQbw+ +w3STcv1kpy8rmyerH6KOXL1bAgMBAAGjge4wgeswCQYDVR0TBAIwADAsBglghkgB +hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE +FP9dcedV6TFtLIWOWXxIQ5h6JR45MIGQBgNVHSMEgYgwgYWAFImmYOO66j6v/GR/ +TL2M0kiN4MxGoWqkaDBmMQswCQYDVQQGEwJLRzELMAkGA1UECBMCTkExEDAOBgNV +BAcTB0JJU0hLRUsxFTATBgNVBAoTDE9wZW5WUE4tVEVTVDEhMB8GCSqGSIb3DQEJ +ARYSbWVAbXlob3N0Lm15ZG9tYWluggEAMA0GCSqGSIb3DQEBBAUAA4GBABP/5mXw +ttXKG6dqQl5kPisFs/c+0j64xytp5/cdB/zMpEWRWTBXtyL3T5T16xs52kJS0VfT +t+jezYbeu/dCdBL8Moz3RTYb1aY2/xymZ433kWjvgtrOzgGlaW3eKXcQpQEyK2v/ +J4q7+oDCElBRilZCm0mBcQsySKsZjGm8BMjh +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDBjCCAm+gAwIBAgIBADANBgkqhkiG9w0BAQQFADBmMQswCQYDVQQGEwJLRzEL +MAkGA1UECBMCTkExEDAOBgNVBAcTB0JJU0hLRUsxFTATBgNVBAoTDE9wZW5WUE4t +VEVTVDEhMB8GCSqGSIb3DQEJARYSbWVAbXlob3N0Lm15ZG9tYWluMB4XDTA0MTEy +NTE0NDA1NVoXDTE0MTEyMzE0NDA1NVowZjELMAkGA1UEBhMCS0cxCzAJBgNVBAgT +Ak5BMRAwDgYDVQQHEwdCSVNIS0VLMRUwEwYDVQQKEwxPcGVuVlBOLVRFU1QxITAf +BgkqhkiG9w0BCQEWEm1lQG15aG9zdC5teWRvbWFpbjCBnzANBgkqhkiG9w0BAQEF +AAOBjQAwgYkCgYEAqPjWJnesPu6bR/iec4FMz3opVaPdBHxg+ORKNmrnVZPh0t8/ +ZT34KXkYoI9B82scurp8UlZVXG8JdUsz+yai8ti9+g7vcuyKUtcCIjn0HLgmdPu5 +gFX25lB0pXw+XIU031dOfPvtROdG5YZN5yCErgCy7TE7zntLnkEDuRmyU6cCAwEA +AaOBwzCBwDAdBgNVHQ4EFgQUiaZg47rqPq/8ZH9MvYzSSI3gzEYwgZAGA1UdIwSB +iDCBhYAUiaZg47rqPq/8ZH9MvYzSSI3gzEahaqRoMGYxCzAJBgNVBAYTAktHMQsw +CQYDVQQIEwJOQTEQMA4GA1UEBxMHQklTSEtFSzEVMBMGA1UEChMMT3BlblZQTi1U +RVNUMSEwHwYJKoZIhvcNAQkBFhJtZUBteWhvc3QubXlkb21haW6CAQAwDAYDVR0T +BAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQBfJoiWYrYdjM0mKPEzUQk0nLYTovBP +I0es/2rfGrin1zbcFY+4dhVBd1E/StebnG+CP8r7QeEIwu7x8gYDdOLLsZn+2vBL +e4jNU1ClI6Q0L7jrzhhunQ5mAaZztVyYwFB15odYcdN2iO0tP7jtEsvrRqxICNy3 +8itzViPTf5W4sA== +-----END CERTIFICATE----- diff --git a/src/test/resources/com/cloudbees/plugins/credentials/impl/key.pem b/src/test/resources/com/cloudbees/plugins/credentials/impl/key.pem new file mode 100644 index 000000000..0a6376f27 --- /dev/null +++ b/src/test/resources/com/cloudbees/plugins/credentials/impl/key.pem @@ -0,0 +1,18 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIC1DBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIgQMvnL0ahAUCAggA +MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECJebW2HlBGc+BIICgGGVZNXyaNYJ +Uzwe7qEkgqBoJLQdQH5oTS6oC2ETwGb4IWf3Q76ag/vaWFYZp7iEwUzatycbKaLj +Uos74QRwmN2W+7jRVLvf0KXLseyR0CNGqj6+O0Ms+abERMSHECslqgGQAdkh3ivT +bDjtAjlP7VjAbXhn21htk4/54fo2o1+w1OIWBuf1g33/AjD+9wzrsESH29yssMOX +5hok885VbvIwyJLdSyKLDCv3xSdT8L9X2gYl4CAzSOwwJuFT1bSA4EjE8R/G+ANT +53sRQq+f/pt/NlNLkKd38Fa7ufk049oaPiIY+c/Kw/zibXrtwXvi57Quofm/qpXC +BBz71XoeksLwkCrn8PCtrZiSheGg01Z7TfumrG1PZdccqr7q8q0XjxzYCo53qnNq +lYhi9hNhmghLeWctdlY3IoN7cIig4z/ALwOMQmjaqYaLU7Maqm8lEqfgnPp9isjp +JtBr705AAU117f3F+MTWtOI6w+T4VABliLfk9JJqQz8SDkVjYOpLpGBnMJYlZTwS +FFfPEKFvOwh7q0Hy5hUovuc8Nd5X2G94h/E1vJumTBmmZepJ2nmDFiEjuejodwcA +wAXtmm7p+dSDXi2+0i9WRB24M7UNdEwxOfVjLNNSqK/GFNK4YHz0efrD/K2qg/+D +DHYgMPuzKtvb8wd0PiNxBY7EauB5Ge/jKTz0UIqqPvt6Hw6FuEiHKPsKXtaBc+II +LQWJyWd6J0d5ol+4FB/Eu5BWoRkTjukWGAkb9IscVC0cF2oHiKcCrboajIlUavnv +d3DpvVJ3xKwTQ0UdYa3jvMargu1LaXVccIlck9B6YHowi7sv9y+y64NHg1K9Bjdj +SpXz7h5osB4= +-----END ENCRYPTED PRIVATE KEY----- From a6e34579c7c694cc9c062ecf9bb40a532a8ed965 Mon Sep 17 00:00:00 2001 From: James Nord Date: Fri, 5 Jul 2024 16:53:12 +0100 Subject: [PATCH 3/9] switch to the exepcted SecretTextArea Now that formvalidation has been manually tested swap back to SecretTextArea The formValidation won't work pending JENKINS-73404 (and to some extent JENKINS-65616) but once they are fixed will magically start working --- .../credentials/impl/CertificateCredentialsImpl.java | 7 +++++++ .../PEMEntryKeyStoreSource/config.jelly | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java index ab6a0714c..1e74b05e6 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java @@ -134,6 +134,13 @@ public CertificateCredentialsImpl(@CheckForNull CredentialsScope scope, Objects.requireNonNull(keyStoreSource); this.password = Secret.fromString(password); this.keyStoreSource = keyStoreSource; + // ensure the keySore is valid + // we check here as otherwise it will lead to hard to diagnose errors when used + try { + keyStoreSource.toKeyStore(toCharArray(this.password)); + } catch (GeneralSecurityException | IOException e) { + throw new IllegalArgumentException("KeyStore is not valid.", e); + } } /** diff --git a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/config.jelly b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/config.jelly index 861c61fb6..b823363da 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/config.jelly +++ b/src/main/resources/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl/PEMEntryKeyStoreSource/config.jelly @@ -26,9 +26,9 @@ - + - + From a43b0b7ea77fe8545bed5d013eb2fadce4c78522 Mon Sep 17 00:00:00 2001 From: James Nord Date: Fri, 5 Jul 2024 16:58:49 +0100 Subject: [PATCH 4/9] pick up release of bouncycastle-api --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 800de47f9..cbb2f1971 100644 --- a/pom.xml +++ b/pom.xml @@ -88,6 +88,7 @@ io.jenkins.tools.bom bom-2.426.x + 2961.v1f472390972e import pom @@ -108,8 +109,7 @@ org.jenkins-ci.plugins bouncycastle-api - - 2.30.1.78.1-238.v991b_a_c571a_29 + 2.30.1.78.1-246.ve1089fe22055 From 9552a4007a3b473a4aad751eff6ea054f0ffae24 Mon Sep 17 00:00:00 2001 From: James Nord Date: Mon, 8 Jul 2024 11:42:22 +0100 Subject: [PATCH 5/9] Use a valid keystore --- .../plugins/credentials/casc/CredentialsCategoryTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/casc/CredentialsCategoryTest.java b/src/test/java/com/cloudbees/plugins/credentials/casc/CredentialsCategoryTest.java index 05f722992..e59ad06eb 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/casc/CredentialsCategoryTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/casc/CredentialsCategoryTest.java @@ -17,6 +17,8 @@ import static io.jenkins.plugins.casc.misc.Util.toYamlString; import io.jenkins.plugins.casc.model.CNode; import java.io.ByteArrayOutputStream; +import java.io.InputStream; + import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; import static org.hamcrest.CoreMatchers.equalTo; @@ -101,12 +103,13 @@ public void exportUsernamePasswordCredentialsImplConfiguration() throws Exceptio @Test public void exportCertificateCredentialsImplConfiguration() throws Exception { + byte[] p12Bytes = CertificateCredentialsImpl.class.getResourceAsStream("test.p12").readAllBytes(); CertificateCredentialsImpl certificateCredentials = new CertificateCredentialsImpl(CredentialsScope.GLOBAL, "credential-certificate", "Credential with certificate", "password", - new CertificateCredentialsImpl.UploadedKeyStoreSource(null, SecretBytes.fromBytes("Testing not real certificate".getBytes()))); + new CertificateCredentialsImpl.UploadedKeyStoreSource(null, SecretBytes.fromBytes(p12Bytes))); SystemCredentialsProvider.getInstance().getCredentials().add(certificateCredentials); ByteArrayOutputStream out = new ByteArrayOutputStream(); From edbec020f5a495080661b766e5f49ddfd6ed92fa Mon Sep 17 00:00:00 2001 From: James Nord Date: Mon, 8 Jul 2024 11:43:03 +0100 Subject: [PATCH 6/9] localize FormValidation messages --- .../impl/CertificateCredentialsImpl.java | 26 +++++++++---------- .../credentials/impl/Messages.properties | 13 +++++++++- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java index 1e74b05e6..4e9cdead6 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java @@ -245,13 +245,13 @@ public FormValidation doCheckPassword(@QueryParameter String value) { Secret s = Secret.fromString(value); String pw = s.getPlainText(); if (FIPS140.useCompliantAlgorithms() && pw.length() < 14) { - return FormValidation.error("password is too short (< 14 characters)"); + return FormValidation.error(Messages.CertificateCredentialsImpl_ShortPasswordFIPS()); } if (pw.isEmpty()) { - return FormValidation.warning("password is empty"); + return FormValidation.warning(Messages.CertificateCredentialsImpl_NoPassword()); } if (pw.length() < 14) { - return FormValidation.warning("password is short (< 14 characters)"); + return FormValidation.warning(Messages.CertificateCredentialsImpl_ShortPassword()); } return FormValidation.ok(); } @@ -749,11 +749,11 @@ public FormValidation doCheckCertChain(@QueryParameter String value) { List pemEncodables = PEMEncodable.decodeAll(pemCerts, null); long count = pemEncodables.stream().map(PEMEncodable::toCertificate).filter(Objects::nonNull).count(); if (count < 1) { - return FormValidation.error("No Certificates provided"); + return FormValidation.error(Messages.CertificateCredentialsImpl_PEMNoCertificates()); } // ensure only certs are provided. if (pemEncodables.size() != count) { - return FormValidation.error("PEM contains non certificate entries"); + return FormValidation.error(Messages.CertificateCredentialsImpl_PEMNoCertificates()); } Certificate cert = pemEncodables.get(0).toCertificate(); if (cert instanceof X509Certificate) { @@ -765,9 +765,9 @@ public FormValidation doCheckCertChain(@QueryParameter String value) { } catch (UnrecoverableKeyException | IOException e) { String message = e.getMessage(); if (message != null) { - return FormValidation.error(e, "Could not parse certificate chain: " + message); + return FormValidation.error(e, Messages.CertificateCredentialsImpl_PEMCertificateParsingError(message)); } - return FormValidation.error(e, "Could not parse certificate chain"); + return FormValidation.error(e, Messages.CertificateCredentialsImpl_PEMCertificateParsingError("unkown reason")); } } @@ -781,14 +781,14 @@ public FormValidation doCheckPrivateKey(@QueryParameter String value, List pemEncodables = PEMEncodable.decodeAll(key, toCharArray(Secret.fromString(password))); long count = pemEncodables.stream().map(PEMEncodable::toPrivateKey).filter(Objects::nonNull).count(); if (count == 0) { - return FormValidation.error("No Keys Provided"); + return FormValidation.error(Messages.CertificateCredentialsImpl_PEMNoKeys()); } if (count > 1) { - return FormValidation.error("More than 1 key provided"); + return FormValidation.error(Messages.CertificateCredentialsImpl_PEMMultipleKeys()); } // ensure only keys are provided. if (pemEncodables.size() != 1) { - return FormValidation.error("PEM contains non key entries"); + return FormValidation.error(Messages.CertificateCredentialsImpl_PEMNonKeys()); } PrivateKey pk = pemEncodables.get(0).toPrivateKey(); String format; @@ -810,7 +810,7 @@ public FormValidation doCheckPrivateKey(@QueryParameter String value, // the size of pemEncodables is one and contains a private key // so it can not be format = "unknown format (" + pk.getClass() +")"; - length = "unknown length"; + length = "unknown strength"; } else { // pk == null can not happen return FormValidation.error("there is a bug in the code, pk is null!"); } @@ -819,9 +819,9 @@ public FormValidation doCheckPrivateKey(@QueryParameter String value, } catch (@SuppressWarnings("unused") DestroyFailedException ignored) { // best effort } - return FormValidation.ok(length + " " + format + " private key"); + return FormValidation.ok(Messages.CertificateCredentialsImpl_PEMKeyInfo(length, format)); } catch (UnrecoverableKeyException | IOException e) { - return FormValidation.error(e, "Could not parse key: " + e.getLocalizedMessage()); + return FormValidation.error(e, Messages.CertificateCredentialsImpl_PEMKeyParseError(e.getLocalizedMessage())); } } diff --git a/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties b/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties index 1b017a973..608714530 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties +++ b/src/main/resources/com/cloudbees/plugins/credentials/impl/Messages.properties @@ -32,4 +32,15 @@ CertificateCredentialsImpl.UploadedKeyStoreSourceDisplayName=Upload PKCS#12 cert CertificateCredentialsImpl.PEMEntryKeyStoreSourceDisplayName=PEM encoded certificate and key CertificateCredentialsImpl.PEMNoCertificates=No certificates where provided CertificateCredentialsImpl.PEMNoKey=No key was provided -CertificateCredentialsImpl.PEMNoPassword=No password was provided \ No newline at end of file +CertificateCredentialsImpl.PEMNoPassword=No password was provided +CertificateCredentialsImpl.ShortPassword=Password is short (< 14 characters) +CertificateCredentialsImpl.ShortPasswordFIPS=Password is too short (< 14 characters) +CertificateCredentialsImpl.NoPassword=Password is empty +CertificateCredentialsImpl.PEMNoCertificate=No Certificates provided +CertificateCredentialsImpl.PEMNonCertificates=PEM contains non certificate entries +CertificateCredentialsImpl.PEMCertificateParsingError=Could not parse certificate chain: {0} +CertificateCredentialsImpl.PEMNoKeys=No Keys Provided +CertificateCredentialsImpl.PEMMultipleKeys=More than 1 key provided +CertificateCredentialsImpl.PEMNonKeys=PEM contains non key entries +CertificateCredentialsImpl.PEMKeyInfo={0} {1} private key +CertificateCredentialsImpl.PEMKeyParseError=Could not parse key: {0} From 1662ee17d5b091994f593db86458f318695d06a5 Mon Sep 17 00:00:00 2001 From: James Nord Date: Mon, 8 Jul 2024 11:52:59 +0100 Subject: [PATCH 7/9] remove unused validation --- .../impl/CertificateCredentialsImpl.java | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java index 4e9cdead6..18fe3c344 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java @@ -825,38 +825,6 @@ public FormValidation doCheckPrivateKey(@QueryParameter String value, } } - /** - * Checks all the values. - * - * @param certChain pem encoded list of certificates (possibly a secret) - * @param privateKey pem encoded private key (possibly a secret) - * @param password for the key (may be null, and possibly a secret) - * @return the {@link FormValidation} results. - */ - public FormValidation doXXCheckCertChain(@QueryParameter String certChain, - @QueryParameter String privateKey, - @RelativePath("..") - @QueryParameter String password) { - String certs = Secret.fromString(certChain).getPlainText(); - String key = Secret.fromString(privateKey).getPlainText(); - String pass = Secret.fromString(password).getPlainText(); - if (StringUtils.isBlank(certs)) { - return FormValidation.error(Messages.CertificateCredentialsImpl_PEMNoCertificates()); - } - if (StringUtils.isBlank(key)) { - return FormValidation.error(Messages.CertificateCredentialsImpl_PEMNoKey()); - } - // pass will always be blank see JENKINS-65616 - if (StringUtils.isBlank(pass)) { - return FormValidation.error(Messages.CertificateCredentialsImpl_PEMNoPassword()); - } - try { - KeyStore ks = PEMEntryKeyStoreSource.toKeyStore(certs, key, pass.toCharArray()); - return validateCertificateKeystore(ks, pass.toCharArray()); - } catch (GeneralSecurityException | IOException e) { - return FormValidation.warning(e, Messages.CertificateCredentialsImpl_LoadKeystoreFailed()); - } - } } } From 7e785a14173ee93f471872c7a7cf3e4654bf3c0d Mon Sep 17 00:00:00 2001 From: James Nord Date: Mon, 8 Jul 2024 12:09:42 +0100 Subject: [PATCH 8/9] Fixup invalid javadoc --- .../credentials/impl/CertificateCredentialsImpl.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java index 18fe3c344..00121cf79 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java @@ -347,16 +347,10 @@ protected static FormValidation validateCertificateKeystore(KeyStore keyStore, c buf.toString())); } - /** - * {@inheritDoc} - */ protected KeyStoreSourceDescriptor() { super(); } - /** - * {@inheritDoc} - */ protected KeyStoreSourceDescriptor(Class clazz) { super(clazz); } @@ -515,9 +509,6 @@ private static void ensureNotRunningInFIPSMode() { } } - /** - * {@inheritDoc} - */ public static class DescriptorImpl extends KeyStoreSourceDescriptor { public static final String DEFAULT_VALUE = UploadedKeyStoreSource.class.getName() + ".default-value"; @@ -611,7 +602,6 @@ public FormValidation doCheckUploadedKeystore(@QueryParameter String value, /** * Helper method that performs form validation on a {@link KeyStore}. * - * @param type the type of keystore to instantiate, see {@link KeyStore#getInstance(String)}. * @param keystoreBytes the {@code byte[]} content of the {@link KeyStore}. * @param password the password to use when loading the {@link KeyStore} and recovering the key from the * {@link KeyStore}. From e2d2a4e20e256037abdecb88d4a2229d483db854 Mon Sep 17 00:00:00 2001 From: James Nord Date: Mon, 8 Jul 2024 12:10:08 +0100 Subject: [PATCH 9/9] remove unused imports --- .../credentials/casc/CredentialsCategoryTest.java | 1 - .../impl/CertificateCredentialsImplTest.java | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/src/test/java/com/cloudbees/plugins/credentials/casc/CredentialsCategoryTest.java b/src/test/java/com/cloudbees/plugins/credentials/casc/CredentialsCategoryTest.java index e59ad06eb..236c02172 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/casc/CredentialsCategoryTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/casc/CredentialsCategoryTest.java @@ -17,7 +17,6 @@ import static io.jenkins.plugins.casc.misc.Util.toYamlString; import io.jenkins.plugins.casc.model.CNode; import java.io.ByteArrayOutputStream; -import java.io.InputStream; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; diff --git a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java index 97ff01fd1..36782545b 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java @@ -26,15 +26,12 @@ import com.cloudbees.hudson.plugins.folder.Folder; import com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider; -import com.cloudbees.plugins.credentials.Credentials; import com.cloudbees.plugins.credentials.CredentialsNameProvider; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.CredentialsStore; import com.cloudbees.plugins.credentials.SecretBytes; -import com.cloudbees.plugins.credentials.SystemCredentialsProvider; import com.cloudbees.plugins.credentials.common.CertificateCredentials; import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; -import com.cloudbees.plugins.credentials.domains.Domain; import org.htmlunit.FormEncodingType; import org.htmlunit.HttpMethod; import org.htmlunit.Page; @@ -49,15 +46,10 @@ import org.htmlunit.html.HtmlPage; import org.htmlunit.html.HtmlRadioButtonInput; -import hudson.FilePath; import hudson.Util; -import hudson.cli.CLICommandInvoker; -import hudson.cli.UpdateJobCommand; import hudson.model.ItemGroup; -import hudson.model.Job; import hudson.security.ACL; import hudson.util.Secret; -import jenkins.model.Jenkins; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.junit.Before; @@ -66,7 +58,6 @@ import org.junit.rules.TemporaryFolder; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.recipes.LocalData; import java.io.File; import java.io.IOException; @@ -76,11 +67,8 @@ import java.nio.file.Files; import java.security.KeyStore; import java.util.Base64; -import java.util.Collections; import java.util.List; -import static hudson.cli.CLICommandInvoker.Matcher.failedWith; -import static hudson.cli.CLICommandInvoker.Matcher.succeeded; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.*;