/*************************************************************************
* *
* CESeCore: CE Security Core *
* *
* This software is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public *
* License as published by the Free Software Foundation; either *
* version 2.1 of the License, or any later version. *
* *
* See terms of license at gnu.org. *
* *
*************************************************************************/
package org.cesecore.authentication.tokens;
import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
import javax.security.auth.x500.X500Principal;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.cesecore.authorization.user.AccessUserAspect;
import org.cesecore.authorization.user.matchvalues.X500PrincipalAccessMatchValue;
import org.cesecore.certificates.util.DNFieldExtractor;
import org.cesecore.util.CertTools;
/**
* This is an implementation of the AuthenticationToken concept, based on using an {@link X509Certificate} as it's single credential, and that
* certificate's {@link X500Principal} as its principle, but as the X500Principle is contained in the X509Certificate, this remains little more than a
* formality. This AuthenticationToken is the default used in EJBCA.
*
* The implementation of the matches(...)
method is based on AdminEntity.java 10832 2010-12-13 13:54:25Z anatom
from EJBCA.
*
*
* @version $Id: X509CertificateAuthenticationToken.java 31393 2019-02-05 11:07:23Z samuellb $
*
*/
public class X509CertificateAuthenticationToken extends NestableAuthenticationToken {
public static final X509CertificateAuthenticationTokenMetaData metaData = new X509CertificateAuthenticationTokenMetaData();
private static final Logger log = Logger.getLogger(X509CertificateAuthenticationToken.class);
private static final long serialVersionUID = 1097165653913865515L;
private static final Pattern serialPattern = Pattern.compile("\\bSERIALNUMBER=", Pattern.CASE_INSENSITIVE);
private final X509Certificate certificate;
// get the subjectDN from the certificate and keep it for caching (speed optimization)
private transient String adminSubjectDN;
private final int adminCaId;
private final DNFieldExtractor dnExtractor;
private final DNFieldExtractor anExtractor;
/**
* Standard constructor for X509CertificateAuthenticationToken
*
* @param principals
* A set of X500Principals. Should contain one and only one value.
* @param credentials
* A set of X509Certificates. As with the principals, this set should contain one and only one value, anything else will result in a
* {@link InvalidAuthenticationTokenException} being thrown.
*/
public X509CertificateAuthenticationToken(final Set principals, final Set credentials) {
super(principals, credentials);
/*
* In order to save having to verify the credentials set every time the matches(...)
method is called, it's checked here, and the
* resulting credential is stored locally.
*/
final X509Certificate[] certificateArray = getCredentials().toArray(new X509Certificate[0]);
if (certificateArray.length != 1) {
throw new InvalidAuthenticationTokenException("X509CertificateAuthenticationToken was containing " + certificateArray.length
+ " credentials instead of 1.");
} else {
// Speed optimization, make it into a BC class, since we will want that many times later on
final String clazz = certificateArray[0].getClass().getName();
if (clazz.contains("org.bouncycastle")) {
certificate = certificateArray[0];
} else {
final CertificateFactory cf = CertTools.getCertificateFactory();
X509Certificate cert;
try {
cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificateArray[0].getEncoded()));
} catch (CertificateException e) {
log.warn("Error encoding/decoding client TLS certificate in BC, just passing instead of optimizing: ", e);
cert = certificateArray[0];
}
certificate = cert;
}
}
String certstring = CertTools.getSubjectDN(certificate).toString();
adminCaId = CertTools.getIssuerDN(certificate).hashCode();
adminSubjectDN = CertTools.getSubjectDN(certificate);
certstring = serialPattern.matcher(certstring).replaceAll("SN=");
final String altNameString = CertTools.getSubjectAlternativeName(certificate);
dnExtractor = new DNFieldExtractor(certstring, DNFieldExtractor.TYPE_SUBJECTDN);
anExtractor = new DNFieldExtractor(altNameString, DNFieldExtractor.TYPE_SUBJECTALTNAME);
}
/**
* Standard simplified constructor for X509CertificateAuthenticationToken
*
* @param certificate A X509Certificate that will be used as principal and credential.
* @throws NullPointerException if the provided certificate is null
*/
public X509CertificateAuthenticationToken(final X509Certificate certificate) {
this(new HashSet<>(Arrays.asList(certificate.getSubjectX500Principal())), new HashSet<>(Arrays.asList(certificate)));
}
@Override
public boolean matches(AccessUserAspect accessUser) {
// Protect against spoofing by checking if this token was created locally
if (!super.isCreatedInThisJvm()) {
return false;
}
boolean returnvalue = false;
int parameter;
int size = 0;
String[] clientstrings = null;
if (StringUtils.equals(getMetaData().getTokenType(), accessUser.getTokenType())) {
// First check that issuers match.
if (accessUser.getCaId() == adminCaId) {
// Check if we actually have some value to match against, null is not an allowed match value
if (accessUser.getMatchValue() != null) {
// Determine part of certificate to match with.
DNFieldExtractor usedExtractor = dnExtractor;
X500PrincipalAccessMatchValue matchValue = (X500PrincipalAccessMatchValue) getMatchValueFromDatabaseValue(accessUser.getMatchWith());
if (matchValue == X500PrincipalAccessMatchValue.WITH_SERIALNUMBER) {
try {
BigInteger matchValueAsBigInteger = new BigInteger(accessUser.getMatchValue(), 16);
switch (accessUser.getMatchTypeAsType()) {
case TYPE_EQUALCASE:
case TYPE_EQUALCASEINS:
returnvalue = matchValueAsBigInteger.equals(certificate.getSerialNumber());
break;
case TYPE_NOT_EQUALCASE:
case TYPE_NOT_EQUALCASEINS:
returnvalue = !matchValueAsBigInteger.equals(certificate.getSerialNumber());
break;
default:
}
} catch (NumberFormatException nfe) {
log.info("Invalid matchValue for accessUser when expecting a hex serialNumber: "+accessUser.getMatchValue());
}
} else if (matchValue == X500PrincipalAccessMatchValue.WITH_FULLDN) {
String value = accessUser.getMatchValue();
switch (accessUser.getMatchTypeAsType()) {
case TYPE_EQUALCASE:
returnvalue = value.equals(CertTools.getSubjectDN(certificate));
break;
case TYPE_EQUALCASEINS:
returnvalue = value.equalsIgnoreCase(CertTools.getSubjectDN(certificate));
break;
case TYPE_NOT_EQUALCASE:
returnvalue = !value.equals(CertTools.getSubjectDN(certificate));
case TYPE_NOT_EQUALCASEINS:
returnvalue = !value.equalsIgnoreCase(CertTools.getSubjectDN(certificate));
break;
default:
}
} else {
parameter = DNFieldExtractor.CN;
switch (matchValue) {
case WITH_COUNTRY:
parameter = DNFieldExtractor.C;
break;
case WITH_DOMAINCOMPONENT:
parameter = DNFieldExtractor.DC;
break;
case WITH_STATEORPROVINCE:
parameter = DNFieldExtractor.ST;
break;
case WITH_LOCALITY:
parameter = DNFieldExtractor.L;
break;
case WITH_ORGANIZATION:
parameter = DNFieldExtractor.O;
break;
case WITH_ORGANIZATIONALUNIT:
parameter = DNFieldExtractor.OU;
break;
case WITH_TITLE:
parameter = DNFieldExtractor.T;
break;
case WITH_DNSERIALNUMBER:
parameter = DNFieldExtractor.SN;
break;
case WITH_COMMONNAME:
parameter = DNFieldExtractor.CN;
break;
case WITH_UID:
parameter = DNFieldExtractor.UID;
break;
case WITH_DNEMAILADDRESS:
parameter = DNFieldExtractor.E;
break;
case WITH_RFC822NAME:
parameter = DNFieldExtractor.RFC822NAME;
usedExtractor = anExtractor;
break;
case WITH_UPN:
parameter = DNFieldExtractor.UPN;
usedExtractor = anExtractor;
break;
default:
}
size = usedExtractor.getNumberOfFields(parameter);
clientstrings = new String[size];
for (int i = 0; i < size; i++) {
clientstrings[i] = usedExtractor.getField(parameter, i);
}
// Determine how to match.
if (clientstrings != null) {
switch (accessUser.getMatchTypeAsType()) {
case TYPE_EQUALCASE:
String accessUserMatchValue = accessUser.getMatchValue();
for (int i = 0; i < size; i++) {
returnvalue = clientstrings[i].equals(accessUserMatchValue);
if (returnvalue) {
break;
}
}
break;
case TYPE_EQUALCASEINS:
for (int i = 0; i < size; i++) {
returnvalue = clientstrings[i].equalsIgnoreCase(accessUser.getMatchValue());
if (returnvalue) {
break;
}
}
break;
case TYPE_NOT_EQUALCASE:
for (int i = 0; i < size; i++) {
returnvalue = !clientstrings[i].equals(accessUser.getMatchValue());
if (returnvalue) {
break;
}
}
break;
case TYPE_NOT_EQUALCASEINS:
for (int i = 0; i < size; i++) {
returnvalue = !clientstrings[i].equalsIgnoreCase(accessUser.getMatchValue());
if (returnvalue) {
break;
}
}
break;
default:
}
}
}
} else {
if (log.isTraceEnabled()) {
log.trace("Match value is null and could not be matched. A value is required.");
}
}
} else {
if (log.isTraceEnabled()) {
log.trace("Caid does not match. Required="+adminCaId+", actual was "+accessUser.getCaId());
}
}
} else {
if (log.isTraceEnabled()) {
log.trace("Token type does not match. Required="+getMetaData().getTokenType()+", actual was "+accessUser.getTokenType());
}
}
return returnvalue;
}
@Override
public int getPreferredMatchKey() {
return X500PrincipalAccessMatchValue.WITH_SERIALNUMBER.getNumericValue();
}
/** Returns the serial number as a decimal string */
@Override
public String getPreferredMatchValue() {
return CertTools.getSerialNumberAsString(certificate);
}
/** Returns user information of the user this authentication token belongs to. */
@Override
public String toString() {
return super.toString();
}
/** Override the default X500Principal.getName() when doing toString on this object. */
@Override
protected String toStringOverride() {
// Return cached value to optimize, because this can be called multiple times during the tokens lifetime
if (adminSubjectDN == null) {
adminSubjectDN = CertTools.getSubjectDN(certificate);
}
return adminSubjectDN;
}
@Override
public int hashCode() {
final int prime = 4711;
int result = 1;
result = prime * result + ((certificate == null) ? 0 : certificate.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
X509CertificateAuthenticationToken other = (X509CertificateAuthenticationToken) obj;
if (certificate == null) {
if (other.certificate != null) {
return false;
}
} else if (!certificate.equals(other.certificate)) {
return false;
}
return true;
}
/**
* @return the certificate
*/
public X509Certificate getCertificate() {
return certificate;
}
@Override
protected String generateUniqueId() {
byte[] encodedCertificate = null;
try {
encodedCertificate = certificate.getEncoded();
} catch (CertificateEncodingException e) {
throw new IllegalStateException(e);
}
return generateUniqueId(super.isCreatedInThisJvm(), encodedCertificate) + ";" + super.generateUniqueId();
}
@Override
public X509CertificateAuthenticationTokenMetaData getMetaData() {
return metaData;
}
}