Skip to content

Commit

Permalink
Merge pull request #1010 from jmdesprez/JENKINS-74907
Browse files Browse the repository at this point in the history
[JENKINS-74907] Add validations when Jenkins is in FIPS mode
  • Loading branch information
fcojfernandez authored Jan 31, 2025
2 parents 828850f + 22bda8f commit f40220e
Show file tree
Hide file tree
Showing 24 changed files with 1,675 additions and 9 deletions.
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ THE SOFTWARE.
<properties>
<changelist>999999-SNAPSHOT</changelist>
<!-- https://www.jenkins.io/doc/developer/plugin-development/choosing-jenkins-baseline/ -->
<jenkins.baseline>2.452</jenkins.baseline>
<jenkins.version>${jenkins.baseline}.4</jenkins.version>
<jenkins.baseline>2.462</jenkins.baseline>
<jenkins.version>${jenkins.baseline}.3</jenkins.version>
<gitHubRepo>jenkinsci/${project.artifactId}-plugin</gitHubRepo>
<hpi.compatibleSinceVersion>1626</hpi.compatibleSinceVersion>
<spotless.check.skip>false</spotless.check.skip>
Expand All @@ -87,7 +87,7 @@ THE SOFTWARE.
<dependency>
<groupId>io.jenkins.tools.bom</groupId>
<artifactId>bom-${jenkins.baseline}.x</artifactId>
<version>3761.vd922730f0fd2</version>
<version>3722.vcc62e7311580</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Expand Down
22 changes: 20 additions & 2 deletions src/main/java/hudson/plugins/ec2/EC2Cloud.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
import hudson.model.PeriodicWork;
import hudson.model.TaskListener;
import hudson.plugins.ec2.util.AmazonEC2Factory;
import hudson.plugins.ec2.util.FIPS140Utils;
import hudson.security.ACL;
import hudson.slaves.Cloud;
import hudson.slaves.NodeProvisioner.PlannedNode;
Expand Down Expand Up @@ -300,7 +301,9 @@ public EC2PrivateKey resolvePrivateKey() {
LOGGER.fine(() -> "(resolvePrivateKey) Using jenkins ssh credential");
SSHUserPrivateKey privateKeyCredential = getSshCredential(sshKeysCredentialsId, Jenkins.get());
if (privateKeyCredential != null) {
return new EC2PrivateKey(privateKeyCredential.getPrivateKey());
String privateKey = privateKeyCredential.getPrivateKey();
FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey);
return new EC2PrivateKey(privateKey);
}
}
return null;
Expand Down Expand Up @@ -433,7 +436,9 @@ protected Object readResolve() {
}

if (this.sshKeysCredentialsId == null && this.privateKey != null) {
migratePrivateSshKeyToCredential(this.privateKey.getPrivateKey());
String privateKey = this.privateKey.getPrivateKey();
FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey);
migratePrivateSshKeyToCredential(privateKey);
}
this.privateKey =
null; // This enforces it not to be persisted and that CasC will never output privateKey on export
Expand Down Expand Up @@ -1483,6 +1488,12 @@ public FormValidation doCheckSshKeysCredentialsId(
}
}

try {
FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey);
} catch (IllegalArgumentException ex) {
validations.add(FormValidation.error(ex, ex.getLocalizedMessage()));
}

validations.add(FormValidation.ok("SSH key validation successful"));
return FormValidation.aggregate(validations);
}
Expand Down Expand Up @@ -1570,6 +1581,13 @@ public FormValidation doTestConnection(
validations.add(FormValidation.ok("Using private key file"));
}
}

try {
FIPS140Utils.ensurePrivateKeyInFipsMode(privateKey);
} catch (IllegalArgumentException ex) {
validations.add(FormValidation.error(ex, ex.getLocalizedMessage()));
}

Check warning on line 1589 in src/main/java/hudson/plugins/ec2/EC2Cloud.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 1492-1589 are not covered by tests

validations.add(FormValidation.ok(Messages.EC2Cloud_Success()));
return FormValidation.aggregate(validations);
} catch (AmazonClientException e) {
Expand Down
72 changes: 69 additions & 3 deletions src/main/java/hudson/plugins/ec2/WindowsData.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

import hudson.Extension;
import hudson.model.Descriptor;
import hudson.plugins.ec2.util.FIPS140Utils;
import hudson.util.FormValidation;
import hudson.util.Secret;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.verb.POST;

public class WindowsData extends AMITypeData {

Expand All @@ -22,7 +27,13 @@ public WindowsData(
boolean useHTTPS,
String bootDelay,
boolean specifyPassword,
boolean allowSelfSignedCertificate) {
boolean allowSelfSignedCertificate)
throws Descriptor.FormException {
try {
FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate);
} catch (IllegalArgumentException e) {
throw new Descriptor.FormException(e, "allowSelfSignedCertificate");
}
this.password = Secret.fromString(password);
this.useHTTPS = useHTTPS;
this.bootDelay = bootDelay;
Expand All @@ -32,15 +43,25 @@ public WindowsData(
}
this.specifyPassword = specifyPassword;

try {
if (specifyPassword) {
FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password);
FIPS140Utils.ensurePasswordLength(password);
}
} catch (IllegalArgumentException e) {
throw new Descriptor.FormException(e, "password");
}

this.allowSelfSignedCertificate = allowSelfSignedCertificate;
}

@Deprecated
public WindowsData(String password, boolean useHTTPS, String bootDelay, boolean specifyPassword) {
public WindowsData(String password, boolean useHTTPS, String bootDelay, boolean specifyPassword)
throws Descriptor.FormException {
this(password, useHTTPS, bootDelay, specifyPassword, true);
}

public WindowsData(String password, boolean useHTTPS, String bootDelay) {
public WindowsData(String password, boolean useHTTPS, String bootDelay) throws Descriptor.FormException {
this(password, useHTTPS, bootDelay, false);
}

Expand Down Expand Up @@ -95,6 +116,51 @@ public static class DescriptorImpl extends Descriptor<AMITypeData> {
public String getDisplayName() {
return "windows";
}

@POST
@SuppressWarnings("unused")
public FormValidation doCheckPassword(@QueryParameter String password) {
if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {

Check warning on line 123 in src/main/java/hudson/plugins/ec2/WindowsData.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 123 is only partially covered, one branch is missing
// for security reasons, do not perform any check if the user is not an admin
return FormValidation.ok();

Check warning on line 125 in src/main/java/hudson/plugins/ec2/WindowsData.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 125 is not covered by tests
}
try {
FIPS140Utils.ensurePasswordLength(password);
} catch (IllegalArgumentException ex) {
return FormValidation.error(ex, ex.getLocalizedMessage());
}
return FormValidation.ok();
}

@POST
@SuppressWarnings("unused")
public FormValidation doCheckUseHTTPS(@QueryParameter boolean useHTTPS, @QueryParameter String password) {
if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {

Check warning on line 138 in src/main/java/hudson/plugins/ec2/WindowsData.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 138 is only partially covered, one branch is missing
// for security reasons, do not perform any check if the user is not an admin
return FormValidation.ok();

Check warning on line 140 in src/main/java/hudson/plugins/ec2/WindowsData.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 140 is not covered by tests
}
try {
FIPS140Utils.ensureNoPasswordLeak(useHTTPS, password);
} catch (IllegalArgumentException ex) {
return FormValidation.error(ex, ex.getLocalizedMessage());
}
return FormValidation.ok();
}

@POST
@SuppressWarnings("unused")
public FormValidation doCheckAllowSelfSignedCertificate(@QueryParameter boolean allowSelfSignedCertificate) {
if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {

Check warning on line 153 in src/main/java/hudson/plugins/ec2/WindowsData.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 153 is only partially covered, one branch is missing
// for security reasons, do not perform any check if the user is not an admin
return FormValidation.ok();

Check warning on line 155 in src/main/java/hudson/plugins/ec2/WindowsData.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 155 is not covered by tests
}
try {
FIPS140Utils.ensureNoSelfSignedCertificate(allowSelfSignedCertificate);
} catch (IllegalArgumentException ex) {
return FormValidation.error(ex, ex.getLocalizedMessage());
}
return FormValidation.ok();
}
}

@Override
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/hudson/plugins/ec2/ssh/verifiers/HostKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
package hudson.plugins.ec2.ssh.verifiers;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.plugins.ec2.util.FIPS140Utils;
import java.io.Serializable;
import java.util.Arrays;
import org.apache.sshd.common.digest.BuiltinDigests;
Expand All @@ -48,6 +49,8 @@ public final class HostKey implements Serializable {

public HostKey(@NonNull String algorithm, @NonNull byte[] key) {
super();
FIPS140Utils.ensurePublicKeyInFipsMode(algorithm, key);

this.algorithm = algorithm;
this.key = key.clone();
}
Expand Down
177 changes: 177 additions & 0 deletions src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package hudson.plugins.ec2.util;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.plugins.ec2.Messages;
import java.io.IOException;
import java.net.URL;
import java.security.Key;
import java.security.UnrecoverableKeyException;
import java.security.interfaces.DSAKey;
import java.security.interfaces.ECKey;
import java.security.interfaces.RSAKey;
import jenkins.bouncycastle.api.PEMEncodable;
import jenkins.security.FIPS140;
import org.apache.commons.lang.StringUtils;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.DSAPublicKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.RSAKeyParameters;
import org.bouncycastle.crypto.util.OpenSSHPublicKeyUtil;

/**
* FIPS related utility methods (check Private and Public keys, ...)
*/
public class FIPS140Utils {

Check warning on line 24 in src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 24 is not covered by tests

/**
* Checks if the key is allowed when FIPS mode is requested.
* Allowed key with the following algorithms and sizes:
* <ul>
* <li>DSA with key size >= 2048</li>
* <li>RSA with key size >= 2048</li>
* <li>Elliptic curve (ED25519) with field size >= 224</li>
* </ul>
* If the key is valid and allowed or not in FIPS mode method will just exit.
* If not it will throw an {@link IllegalArgumentException}.
* @param key The key to check.
*/
public static void ensureKeyInFipsMode(Key key) {
if (!FIPS140.useCompliantAlgorithms()) {
return;
}
if (key instanceof RSAKey) {
if (((RSAKey) key).getModulus().bitLength() < 2048) {
throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode());
}
} else if (key instanceof DSAKey) {
if (((DSAKey) key).getParams().getP().bitLength() < 2048) {
throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode());
}
} else if (key instanceof ECKey) {
if (((ECKey) key).getParams().getCurve().getField().getFieldSize() < 224) {
throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeECInFIPSMode());
}
} else {
throw new IllegalArgumentException(Messages.EC2Cloud_keyIsNotApprovedInFIPSMode(key.getAlgorithm()));
}
}

/**
* Password leak prevention when FIPS mode is requested. If FIPS mode is not requested, this method does nothing.
* Otherwise, ensure that no password can be leaked
* @param url the requested URL
* @param password the password used
* @throws IllegalArgumentException if there is a risk that the password will leak
*/
public static void ensureNoPasswordLeak(URL url, String password) {
ensureNoPasswordLeak("https".equals(url.getProtocol()), password);
}

/**
* Password leak prevention when FIPS mode is requested. If FIPS mode is not requested, this method does nothing.
* Otherwise, ensure that no password can be leaked.
* @param useHTTPS is TLS used or not
* @param password the password used
* @throws IllegalArgumentException if there is a risk that the password will leak
*/
public static void ensureNoPasswordLeak(boolean useHTTPS, String password) {
ensureNoPasswordLeak(useHTTPS, !StringUtils.isEmpty(password));
}

/**
* Password leak prevention when FIPS mode is requested. If FIPS mode is not requested, this method does nothing.
* Otherwise, ensure that no password can be leaked.
* @param useHTTPS is TLS used or not
* @param usePassword is a password used
* @throws IllegalArgumentException if there is a risk that the password will leak
*/
public static void ensureNoPasswordLeak(boolean useHTTPS, boolean usePassword) {
if (FIPS140.useCompliantAlgorithms()) {
if (!useHTTPS && usePassword) {
throw new IllegalArgumentException(Messages.EC2Cloud_tlsIsRequiredInFIPSMode());
}
}
}

/**
* Password length check chen FIPS mode is requested. If FIPS mode is not requested, this method does nothing.
* Otherwise, ensure that the password length is at least 14 char long.
* @param password the password to check
* @throws IllegalArgumentException if FIPS mode is requested and the password is too short
*/
public static void ensurePasswordLength(String password) {
if (FIPS140.useCompliantAlgorithms()) {
if (StringUtils.isBlank(password) || password.length() < 14) {

Check warning on line 104 in src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 104 is only partially covered, one branch is missing
throw new IllegalArgumentException(Messages.EC2Cloud_passwordLengthInFIPSMode());
}
}
}

/**
* Password leak prevention when FIPS mode is requested. If FIPS mode is not requested, this method does nothing.
* Otherwise, ensure that no password can be leaked.
* @param allowSelfSignedCertificate is self-signed certificate allowed
* @throws IllegalArgumentException if FIPS mode is requested and a self-signed certificate is allowed
*/
public static void ensureNoSelfSignedCertificate(boolean allowSelfSignedCertificate) {
if (FIPS140.useCompliantAlgorithms()) {
if (allowSelfSignedCertificate) {
throw new IllegalArgumentException(Messages.EC2Cloud_selfSignedCertificateNotAllowedInFIPSMode());
}
}
}

/**
* Checks if the private key is allowed when FIPS mode is requested.
* Allowed private key with the following algorithms and sizes:
* <ul>
* <li>DSA with key size >= 2048</li>
* <li>RSA with key size >= 2048</li>
* <li>Elliptic curve (ED25519) with field size >= 224</li>
* </ul>
* If the private key is valid and allowed or not in FIPS mode method will just exit.
* If not it will throw an {@link IllegalArgumentException}.
* @param privateKeyString String containing the private key PEM.
*/
public static void ensurePrivateKeyInFipsMode(String privateKeyString) {
if (!FIPS140.useCompliantAlgorithms()) {
return;
}
if (StringUtils.isBlank(privateKeyString)) {

Check warning on line 140 in src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 140 is only partially covered, one branch is missing
throw new IllegalArgumentException(Messages.EC2Cloud_keyIsMandatoryInFIPSMode());

Check warning on line 141 in src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 141 is not covered by tests
}
try {
Key privateKey = PEMEncodable.decode(privateKeyString).toPrivateKey();
ensureKeyInFipsMode(privateKey);
} catch (RuntimeException | UnrecoverableKeyException | IOException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
}

public static void ensurePublicKeyInFipsMode(@NonNull String algorithm, @NonNull byte[] key) {
if (!FIPS140.useCompliantAlgorithms()) {
return;
}

AsymmetricKeyParameter asymmetricKeyParameter = OpenSSHPublicKeyUtil.parsePublicKey(key);

if (asymmetricKeyParameter instanceof RSAKeyParameters) {
RSAKeyParameters rsaKeyParameters = (RSAKeyParameters) asymmetricKeyParameter;
if (rsaKeyParameters.getModulus().bitLength() < 2048) {
throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode());
}
} else if (asymmetricKeyParameter instanceof DSAPublicKeyParameters) {
DSAPublicKeyParameters dsaPublicKeyParameters = (DSAPublicKeyParameters) asymmetricKeyParameter;
if (dsaPublicKeyParameters.getParameters().getP().bitLength() < 2048) {

Check warning on line 165 in src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 165 is only partially covered, one branch is missing
throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeInFIPSMode());
}
} else if (asymmetricKeyParameter instanceof ECPublicKeyParameters) {

Check warning on line 168 in src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 168 is only partially covered, one branch is missing
ECPublicKeyParameters ecPublicKeyParameters = (ECPublicKeyParameters) asymmetricKeyParameter;
if (ecPublicKeyParameters.getParameters().getCurve().getFieldSize() < 224) {

Check warning on line 170 in src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 170 is only partially covered, one branch is missing
throw new IllegalArgumentException(Messages.EC2Cloud_invalidKeySizeECInFIPSMode());

Check warning on line 171 in src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 171 is not covered by tests
}
} else {
throw new IllegalArgumentException(Messages.EC2Cloud_keyIsNotApprovedInFIPSMode(algorithm));

Check warning on line 174 in src/main/java/hudson/plugins/ec2/util/FIPS140Utils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 174 is not covered by tests
}
}
}
Loading

0 comments on commit f40220e

Please sign in to comment.