GUACAMOLE-839: Use BouncyCastle for retrieval of certificate details.

Java's build-in support for reading X.509 certificates does not deal
well with PIV certificates containing the username as a "serialNumber"
attribute. Rather than exposing the string value of that attribute, the
Java implementation exposes a byte array that does not fully match the
string value shown by a tool like OpenSSL.

BouncyCastle, on the other hand, _does_ match the output of OpenSSL, and
provides a predictable means of decoding the certificate.
This commit is contained in:
Michael Jumper
2023-03-07 16:34:38 -08:00
parent b9958fa331
commit d0574f8d82
5 changed files with 54 additions and 18 deletions

View File

@@ -119,6 +119,13 @@
<artifactId>jsr311-api</artifactId>
</dependency>
<!-- Use FIPS variant of Bouncy Castle crypto library -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-fips</artifactId>
<version>1.0.7</version>
</dependency>
</dependencies>
</project>

View File

@@ -19,17 +19,14 @@
package org.apache.guacamole.auth.ssl;
import com.google.inject.Inject;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.naming.InvalidNameException;
@@ -46,9 +43,14 @@ import javax.ws.rs.core.UriBuilder;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleResourceNotFoundException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.ssl.conf.ConfigurationService;
import org.apache.guacamole.auth.sso.NonceService;
import org.apache.guacamole.auth.sso.SSOResource;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.RFC4519Style;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.openssl.PEMParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -247,27 +249,25 @@ public class SSLClientAuthenticationResource extends SSOResource {
public String getUsername(byte[] certificate) throws GuacamoleException {
// Parse and re-verify certificate is valid with respect to timestamps
X509Certificate cert;
try (InputStream input = new ByteArrayInputStream(certificate)) {
X509CertificateHolder cert;
try (Reader reader = new StringReader(new String(certificate, StandardCharsets.UTF_8))) {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
cert = (X509Certificate) certFactory.generateCertificate(input);
PEMParser parser = new PEMParser(reader);
cert = (X509CertificateHolder) parser.readObject();
// Verify certificate is valid (it should be given pre-validation
// from SSL termination, but it's worth rechecking for sanity)
cert.checkValidity();
if (!cert.isValidOn(new Date()))
throw new GuacamoleClientException("Certificate has expired.");
}
catch (CertificateException e) {
throw new GuacamoleClientException("Certificate is not valid: " + e.getMessage(), e);
}
catch (IOException e) {
throw new GuacamoleClientException("Certificate could not be read: " + e.getMessage(), e);
throw new GuacamoleServerException("Certificate could not be read: " + e.getMessage(), e);
}
// Extract user's DN from their X.509 certificate
Principal principal = cert.getSubjectX500Principal();
return getUsername(principal.getName());
// Extract user's DN from their X.509 certificate in LDAP (RFC 4919) format
X500Name subject = X500Name.getInstance(RFC4519Style.INSTANCE, cert.getSubject());
return getUsername(subject.toString());
}