package org.dcache.gplazma.plugins;
import com.google.common.net.InetAddresses;
import eu.emi.security.authn.x509.impl.OpensslNameUtils;
import eu.emi.security.authn.x509.proxy.ProxyChainInfo;
import eu.emi.security.authn.x509.proxy.ProxyUtils;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERObjectIdentifier;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x509.PolicyInformation;
import org.globus.gsi.gssapi.jaas.GlobusPrincipal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.auth.x500.X500Principal;
import java.io.IOException;
import java.net.InetAddress;
import java.security.Principal;
import java.security.cert.CertPath;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import org.dcache.auth.EntityDefinitionPrincipal;
import org.dcache.auth.LoAPrincipal;
import org.dcache.auth.Origin;
import org.dcache.gplazma.AuthenticationException;
import org.dcache.gplazma.util.CertPaths;
import org.dcache.gplazma.util.IGTFInfoDirectory;
import static eu.emi.security.authn.x509.helpers.CertificateHelpers.getExtensionBytes;
import static org.dcache.auth.EntityDefinition.*;
import static org.dcache.auth.LoA.*;
import static org.dcache.gplazma.util.CertPaths.isX509CertPath;
import static org.dcache.gplazma.util.Preconditions.checkAuthentication;
/**
* Extracts GlobusPrincipals from any X509Certificate certificate
* chain in the public credentials.
*
* The certificate is not validated (ie no CRL checks and no CA
* signature check). It is assumed that the door did this check
* already.
*/
public class X509Plugin implements GPlazmaAuthenticationPlugin
{
private static final Logger LOG = LoggerFactory.getLogger(X509Plugin.class);
private static final String OID_CERTIFICATE_POLICIES = "2.5.29.32";
private static final String OID_ANY_POLICY = "2.5.29.32";
private static final DERSequence ANY_POLICY = new DERSequence(new ASN1ObjectIdentifier(OID_ANY_POLICY));
private final IGTFInfoDirectory infoDirectory;
public X509Plugin(Properties properties)
{
String path = properties.getProperty("gplazma.x509.igtf-info.path");
infoDirectory = path == null ? null : new IGTFInfoDirectory(path);
}
@Override
public void authenticate(Set<Object> publicCredentials,
Set<Object> privateCredentials,
Set<Principal> identifiedPrincipals)
throws AuthenticationException
{
String message = "no X.509 certificate chain";
Optional<Origin> origin = identifiedPrincipals.stream()
.filter(Origin.class::isInstance)
.map(Origin.class::cast)
.findFirst();
boolean found = false;
for (Object credential : publicCredentials) {
try {
if (isX509CertPath(credential)) {
X509Certificate[] chain = CertPaths.getX509Certificates((CertPath) credential);
if (origin.isPresent() && ProxyUtils.isProxy(chain)) {
ProxyChainInfo info = new ProxyChainInfo(chain);
InetAddress address = origin.get().getAddress();
if (!info.isHostAllowedAsSource(address.getAddress())) {
message = "forbidden client address: " + InetAddresses.toAddrString(address);
continue;
}
}
X509Certificate eec = ProxyUtils.getEndUserCertificate(chain);
if (eec == null) {
message = "X.509 chain contains no End-Entity Certificate";
continue;
}
identifiedPrincipals.add(asGlobusPrincipal(eec.getSubjectX500Principal()));
listPolicies(eec).stream()
.map(PolicyInformation::getInstance)
.map(PolicyInformation::getPolicyIdentifier)
.map(DERObjectIdentifier::getId)
.map(X509Plugin::asPrincipal)
.filter(Objects::nonNull)
.forEach(identifiedPrincipals::add);
if (infoDirectory != null) {
// REVISIT: this assumes that issuer of EEC is the CA. This
// is currently true for all IGTF CAs.
GlobusPrincipal issuer = asGlobusPrincipal(eec.getIssuerX500Principal());
identifiedPrincipals.addAll(infoDirectory.getPrincipals(issuer));
}
found = true;
}
} catch (IOException | CertificateException e) {
message = "broken certificate: " + e.getMessage();
}
}
checkAuthentication(found, message);
}
private GlobusPrincipal asGlobusPrincipal(X500Principal p)
{
return new GlobusPrincipal(OpensslNameUtils.convertFromRfc2253(p.getName(), true));
}
private List<ASN1Encodable> listPolicies(X509Certificate eec)
throws AuthenticationException
{
byte[] encoded;
try {
encoded = getExtensionBytes(eec, OID_CERTIFICATE_POLICIES);
} catch (IOException e) {
LOG.warn("Malformed policy extension {}: {}",
eec.getIssuerX500Principal().getName(), e.getMessage());
return Collections.emptyList();
}
if (encoded == null) { // has no Certificate Policies extension.
return Collections.emptyList();
}
Enumeration<ASN1Encodable> policySource = ASN1Sequence.getInstance(encoded).getObjects();
List<ASN1Encodable> policies = new ArrayList<>();
while (policySource.hasMoreElements()) {
ASN1Encodable policy = policySource.nextElement();
if (!policy.equals(ANY_POLICY)) {
policies.add(policy);
}
}
return policies;
}
private static Principal asPrincipal(String oid)
{
switch (oid) {
/*
* IGTF LoA. This encapsulates the LoA corresponding to existing
* Authentication Policies (APs) described below.
*/
case "1.2.840.113612.5.2.5.1":
return new LoAPrincipal(IGTF_LOA_ASPEN);
case "1.2.840.113612.5.2.5.2":
return new LoAPrincipal(IGTF_LOA_BIRCH);
case "1.2.840.113612.5.2.5.3":
return new LoAPrincipal(IGTF_LOA_CEDER);
case "1.2.840.113612.5.2.5.4":
return new LoAPrincipal(IGTF_LOA_DOGWOOD);
/*
* IGTF Authentication-Policy. Amongst other things, this defines
* how the user was identified to the Certificate Authority, which
* includes an element of LoA.
*/
case "1.2.840.113612.5.2.2.1":
return new LoAPrincipal(IGTF_AP_CLASSIC);
case "1.2.840.113612.5.2.2.2":
return new LoAPrincipal(IGTF_AP_SGCS);
case "1.2.840.113612.5.2.2.3":
return new LoAPrincipal(IGTF_AP_SLCS);
case "1.2.840.113612.5.2.2.4":
return new LoAPrincipal(IGTF_AP_EXPERIMENTAL);
case "1.2.840.113612.5.2.2.5":
return new LoAPrincipal(IGTF_AP_MICS);
case "1.2.840.113612.5.2.2.6":
return new LoAPrincipal(IGTF_AP_IOTA);
/*
* IGTF Entity-Definition. Currently (2015-10-11), this mostly
* identifies robot certificates.
*/
case "1.2.840.113612.5.2.3.3.1":
return new EntityDefinitionPrincipal(ROBOT);
case "1.2.840.113612.5.2.3.3.2":
return new EntityDefinitionPrincipal(HOST);
case "1.2.840.113612.5.2.3.3.3":
return new EntityDefinitionPrincipal(PERSON);
}
return null;
}
}