Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-70000] Fix HTTPS user cert auth for (temporary) private CA #120

Merged
merged 17 commits into from
Jul 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3c5cb46
HttpRequestExecution: pass ignoreSslErrors to CertificateAuthentication
jimklimov Nov 2, 2022
b6d751d
HttpRequestExecution: (optionally) trace details of executeRequest()
jimklimov Nov 2, 2022
661fedb
CertificateAuthentication.java: extend with support for Certificate C…
jimklimov Nov 2, 2022
b5faa43
CertificateAuthentication.java: drop dev-test logging
jimklimov Nov 2, 2022
f15fd88
CertificateAuthentication: set SSLConnectionSocketFactory as well
jimklimov Nov 3, 2022
f4ab5b1
README.adoc: add notes on Certificate-based Authentication with a Jen…
jimklimov Nov 3, 2022
06dbf12
HttpRequestExecution.java: hush down a remaining bit of debug logging
jimklimov Nov 4, 2022
91e0ab6
Add test keystore for HttpRequestStepCredentialsTest (lift from crede…
jimklimov Nov 23, 2022
19e24da
Add test keystore with trusted CA for HttpRequestStepCredentialsTest …
jimklimov Nov 23, 2022
219bf03
Add README about test keystores for HttpRequestStepCredentialsTest
jimklimov Nov 23, 2022
41cfa89
Relocate test*.p12 to propoer location for test suite visibility
jimklimov Nov 23, 2022
6aaebf1
HttpRequestStepCredentialsTest: import and adapt tests from PR creden…
jimklimov Nov 23, 2022
3eaa1ce
Merge branch 'master' into fix-cert-auth
jimklimov Nov 23, 2022
c3d5be2
Merge branch 'master' into fix-cert-auth
jimklimov Jan 25, 2023
e1f3786
Merge remote-tracking branch 'upstream/master' into fix-cert-auth
jimklimov Jun 26, 2023
ec8b646
Merge branch 'master' into fix-cert-auth
MarkEWaite Jul 1, 2023
ec04185
Merge branch 'master' into fix-cert-auth
MarkEWaite Jul 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ Use Basic Authentication to ensure that only authorized users can access your pr
* Supports Form Authentication:
Form Authentication enables users to authenticate themselves by submitting a username and password through a form, ensuring that only authorized users can access your resources.

* Supports Certificate-based Authentication:
Use a certificate from a Jenkins stored credential to authenticate your HTTPS requests to a remote server.

* Specify a required string in the response:
Ensure that a specific string is present in the response by specifying it beforehand. If the string is not present, the build will fail, alerting you to the issue.

Expand Down Expand Up @@ -270,6 +273,22 @@ def response = httpRequest authenticate: 'my-jenkins-credential-id',
url: 'https://api.github.com/user/jenkinsci'
----

You can send an SSL request with authentication by user certificate;
for a private CA, make sure to first add the CA certificate is as
"Trusted", then add the user key along with certification chain up
to same CA certificate, into your PKCS12 keystore file which you
upload to Jenkins credentials, and you also must use a non-trivial
password for that keystore. Keep in mind that for systems under test
which create their own self-signed CA and HTTPS protection, you can
programmatically create and upload the credentials, into a domain
where the job has write access (its folder etc.)

[source,groovy]
----
def response = httpRequest authentication: 'user_with_cert_and_ca',
url: 'https://sut123.local.domain:8443/api/v1/status/debug'
----

A basic WebDAV upload can be built using ``MKCOL`` and ``PUT`` like so:

[source,groovy]
Expand All @@ -288,7 +307,7 @@ httpRequest authenticate: 'my-jenkins-credential-id',
uploadFile: './local/path/to/report.html'
----

For details on the Pipeline features, use the Pipeline snippet generator in the Pipeline job
For details on the Pipeline features, use the Pipeline snippet generator in the Pipeline job
configuration.

[WARNING]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ private HttpRequestExecution(
}
}
if (credential instanceof StandardCertificateCredentials) {
auth = new CertificateAuthentication((StandardCertificateCredentials) credential);
auth = new CertificateAuthentication((StandardCertificateCredentials) credential, this.ignoreSslErrors);
}
}
}
Expand Down Expand Up @@ -435,6 +435,15 @@ private ResponseContentSupplier executeRequest(
CloseableHttpClient httpclient, HttpClientUtil clientUtil, HttpRequestBase httpRequestBase,
HttpContext context) throws IOException {
ResponseContentSupplier responseContentSupplier;
/*
// TODO: pick interesting fields/getters from these classes:
logger().println("Sending HTTP request with" +
" CloseableHttpClient=" + httpclient.toString() +
" HttpClientUtil=" + clientUtil.toString() +
" HttpRequestBase=" + httpRequestBase.toString() +
" HttpContext=" + context.toString()
);
*/
try {
final HttpResponse response = clientUtil.execute(httpclient, context, httpRequestBase, logger());
// The HttpEntity is consumed by the ResponseContentSupplier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,36 @@

import java.io.IOException;
import java.io.PrintStream;
import java.security.KeyStore;

import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.TrustStrategy;

import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials;

import hudson.Util;

public class CertificateAuthentication implements Authenticator {

private final StandardCertificateCredentials credentials;

private final boolean ignoreSslErrors;

public CertificateAuthentication(StandardCertificateCredentials credentials) {
this.credentials = credentials;
this.ignoreSslErrors = false;
}

public CertificateAuthentication(StandardCertificateCredentials credentials, boolean ignoreSslErrors) {
this.credentials = credentials;
this.ignoreSslErrors = ignoreSslErrors;
}

@Override
Expand All @@ -30,12 +45,59 @@ public CloseableHttpClient authenticate(HttpClientBuilder clientBuilder,
HttpRequestBase requestBase,
PrintStream logger) throws IOException {
try {
clientBuilder.setSSLContext(
SSLContexts.custom().loadKeyMaterial(credentials.getKeyStore(),
credentials.getPassword().getPlainText().toCharArray()).build());
KeyStore keyStore = credentials.getKeyStore();
// Note: modeled after CertificateCredentialsImpl.toCharArray()
// which ignores both null and "" empty passwords, even though
// technically the byte stream reader there *can* decipher with
// "" as the password. The null value is explicitly ignored by
// ultimate sun.security.pkcs12.PKCS12KeyStore::engineLoad(),
// for more context see comments in its sources.
String keyStorePass = Util.fixEmpty(credentials.getPassword().getPlainText());
char[] keyStorePassChars = (keyStorePass == null ? null : keyStorePass.toCharArray());
SSLContextBuilder contextBuilder = SSLContexts.custom();

if (keyStorePassChars == null) {
logger.println("WARNING: Jenkins Certificate Credential '" +
credentials.getId() + "' was saved without a password, " +
"so any certificates (and chain of trust) in it would " +
"be ignored by Java PKCS12 support!");
}

try {
TrustStrategy trustStrategy = null;
if (ignoreSslErrors) {
// e.g. for user certificate issued by test CA so
// is not persisted in the system 'cacerts' file.
// Hopefully it is at least added/trusted in the
// generated keystore...
trustStrategy = new TrustAllStrategy();
//trustStrategy = new TrustSelfSignedStrategy();
}

contextBuilder = contextBuilder.loadTrustMaterial(keyStore, trustStrategy);
logger.println("Added Trust Material from provided KeyStore");
} catch (Exception e) {
logger.println("Failed to add Trust Material from provided KeyStore (so Key Material might end up untrusted): " + e.getMessage());
// Do no re-throw, maybe system trust would suffice?
// TODO: Can we identify lack of trust material in
// key store vs. inability to load what exists?..
// And do we really care about the difference?
}

contextBuilder = contextBuilder.loadKeyMaterial(keyStore, keyStorePassChars);
logger.println("Added Key Material from provided KeyStore");

clientBuilder = clientBuilder.setSSLContext(contextBuilder.build());
logger.println("Set SSL context for the HTTP client builder");

SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
contextBuilder.build());
clientBuilder = clientBuilder.setSSLSocketFactory(sslsf);
logger.println("Set SSL socket factory for the HTTP client builder");

return clientBuilder.build();
} catch (Exception e) {
logger.println("Failed to set SSL context: " + e.getMessage());
throw new IOException(e);
}
}
Expand Down
Loading