package org.apereo.cas.adaptors.x509.authentication.principal; import org.apache.commons.lang3.builder.ToStringBuilder; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1OctetString; import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.ASN1String; import org.bouncycastle.asn1.ASN1TaggedObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.List; /** * Credential to principal resolver that extracts Subject Alternative Name UPN extension * from the provided certificate if available as a resolved principal id. * * @author Dmitriy Kopylenko * @since 4.1.0 */ public class X509SubjectAlternativeNameUPNPrincipalResolver extends AbstractX509PrincipalResolver { /** * ObjectID for upn altName for windows smart card logon. */ public static final String UPN_OBJECTID = "1.3.6.1.4.1.311.20.2.3"; private static final Logger LOGGER = LoggerFactory.getLogger(X509SubjectAlternativeNameUPNPrincipalResolver.class); /** * Retrieves Subject Alternative Name UPN extension as a principal id String. * * @param certificate X.509 certificate credential. * @return Resolved principal ID or null if no SAN UPN extension is available in provided certificate. * @see AbstractX509PrincipalResolver#resolvePrincipalInternal(java.security.cert.X509Certificate) * @see java.security.cert.X509Certificate#getSubjectAlternativeNames() */ @Override protected String resolvePrincipalInternal(final X509Certificate certificate) { LOGGER.debug("Resolving principal from Subject Alternative Name UPN for [{}]", certificate); try { final Collection<List<?>> subjectAltNames = certificate.getSubjectAlternativeNames(); if (subjectAltNames != null) { for (final List<?> sanItem : subjectAltNames) { final ASN1Sequence seq = getAltnameSequence(sanItem); final String upnString = getUPNStringFromSequence(seq); if (upnString != null) { return upnString; } } } } catch (final CertificateParsingException e) { LOGGER.error("Error is encountered while trying to retrieve subject alternative names collection from certificate", e); LOGGER.debug("Returning null principal..."); return null; } LOGGER.debug("Returning null principal id..."); return null; } /** * Get UPN String. * * @param seq ASN1Sequence abstraction representing subject alternative name. * First element is the object identifier, second is the object itself. * @return UPN string or null */ private static String getUPNStringFromSequence(final ASN1Sequence seq) { if (seq != null) { // First in sequence is the object identifier, that we must check final ASN1ObjectIdentifier id = ASN1ObjectIdentifier.getInstance(seq.getObjectAt(0)); if (id != null && UPN_OBJECTID.equals(id.getId())) { final ASN1TaggedObject obj = (ASN1TaggedObject) seq.getObjectAt(1); ASN1Primitive prim = obj.getObject(); // Due to bug in java cert.getSubjectAltName, it can be tagged an extra time if (prim instanceof ASN1TaggedObject) { prim = ASN1TaggedObject.getInstance(prim).getObject(); } if (prim instanceof ASN1OctetString) { return new String(((ASN1OctetString) prim).getOctets(), StandardCharsets.UTF_8); } if (prim instanceof ASN1String) { return ((ASN1String) prim).getString(); } return null; } } return null; } /** * Get alt name seq. * * @param sanItem subject alternative name value encoded as a two elements List with elem(0) representing object id and elem(1) * representing object (subject alternative name) itself. * @return ASN1Sequence abstraction representing subject alternative name or null if the passed in * List doesn't contain at least to elements * as expected to be returned by implementation of {@code X509Certificate.html#getSubjectAlternativeNames} * @see <a href="http://docs.oracle.com/javase/7/docs/api/java/security/cert/X509Certificate.html#getSubjectAlternativeNames()"> * X509Certificate#getSubjectAlternativeNames</a> */ private static ASN1Sequence getAltnameSequence(final List sanItem) { //Should not be the case, but still, a extra "safety" check if (sanItem.size() < 2) { LOGGER.error("Subject Alternative Name List does not contain at least two required elements. Returning null principal id..."); } final Integer itemType = (Integer) sanItem.get(0); if (itemType == 0) { final byte[] altName = (byte[]) sanItem.get(1); return getAltnameSequence(altName); } return null; } /** * Get alt name seq. * * @param sanValue subject alternative name value encoded as byte[] * @return ASN1Sequence abstraction representing subject alternative name * @see <a href="http://docs.oracle.com/javase/7/docs/api/java/security/cert/X509Certificate.html#getSubjectAlternativeNames()"> * X509Certificate#getSubjectAlternativeNames</a> */ private static ASN1Sequence getAltnameSequence(final byte[] sanValue) { ASN1Primitive oct = null; try (ByteArrayInputStream bInput = new ByteArrayInputStream(sanValue)) { try (ASN1InputStream input = new ASN1InputStream(bInput)) { oct = input.readObject(); } catch (final IOException e) { LOGGER.error("Error on getting Alt Name as a DERSEquence: [{}]", e.getMessage(), e); } return ASN1Sequence.getInstance(oct); } catch (final IOException e) { LOGGER.error("An error has occurred while reading the subject alternative name value", e); } return null; } @Override public String toString() { return new ToStringBuilder(this) .appendSuper(super.toString()) .toString(); } }