package org.dcache.gplazma.plugins;
import eu.emi.security.authn.x509.X509CertChainValidatorExt;
import org.italiangrid.voms.VOMSAttribute;
import org.italiangrid.voms.VOMSValidators;
import org.italiangrid.voms.ac.VOMSACValidator;
import org.italiangrid.voms.ac.VOMSValidationResult;
import org.italiangrid.voms.error.VOMSValidationErrorMessage;
import org.italiangrid.voms.store.VOMSTrustStore;
import org.italiangrid.voms.store.VOMSTrustStores;
import org.italiangrid.voms.util.CertificateValidatorBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.security.Principal;
import java.security.cert.CRLException;
import java.security.cert.CertPath;
import java.security.cert.CertificateException;
import java.util.Base64;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import org.dcache.auth.FQANPrincipal;
import org.dcache.gplazma.AuthenticationException;
import org.dcache.gplazma.util.CertPaths;
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Arrays.asList;
import static org.dcache.gplazma.util.Preconditions.checkAuthentication;
/**
* Validates and extracts FQANs from any X509Certificate certificate chain in
* the public credentials.
*/
public class VomsPlugin implements GPlazmaAuthenticationPlugin
{
private static final Logger LOG = LoggerFactory.getLogger(VomsPlugin.class);
private static final String CADIR = "gplazma.vomsdir.ca";
private static final String VOMSDIR = "gplazma.vomsdir.dir";
private final String caDir;
private final String vomsDir;
private VOMSACValidator validator;
private final Random random = new Random();
public VomsPlugin(Properties properties)
throws CertificateException, CRLException, IOException
{
caDir = properties.getProperty(CADIR);
vomsDir = properties.getProperty(VOMSDIR);
checkArgument(caDir != null, "Undefined property: " + CADIR);
checkArgument(vomsDir != null, "Undefined property: " + VOMSDIR);
}
@Override
public void start()
{
VOMSTrustStore vomsTrustStore = VOMSTrustStores.newTrustStore(asList(vomsDir));
X509CertChainValidatorExt certChainValidator = new CertificateValidatorBuilder().trustAnchorsDir(caDir).build();
validator = VOMSValidators.newValidator(vomsTrustStore, certChainValidator);
}
@Override
public void stop()
{
validator.shutdown();
}
@Override
public void authenticate(Set<Object> publicCredentials,
Set<Object> privateCredentials,
Set<Principal> identifiedPrincipals)
throws AuthenticationException
{
boolean primary = true;
boolean hasX509 = false;
boolean hasFQANs = false;
String ids = null;
boolean multipleIds = false;
for (Object credential : publicCredentials) {
if (CertPaths.isX509CertPath(credential)) {
hasX509 = true;
List<VOMSValidationResult> results = validator.validateWithResult(CertPaths.getX509Certificates((CertPath) credential));
for (VOMSValidationResult result : results) {
if (result.isValid()) {
VOMSAttribute attr = result.getAttributes();
for (String fqan : attr.getFQANs()) {
hasFQANs = true;
identifiedPrincipals.add(new FQANPrincipal(fqan, primary));
primary = false;
}
} else {
byte[] rawId = new byte[3]; // a Base64 char represents 6 bits; 4 chars represent 3 bytes.
random.nextBytes(rawId);
String id = Base64.getEncoder().withoutPadding().encodeToString(rawId);
String message = buildErrorMessage(result.getValidationErrors());
LOG.warn("Validation failure {}: {}", id, message);
if (ids == null) {
ids = id;
} else {
ids = ids + ", " + id;
multipleIds = true;
}
}
}
}
}
if (ids != null && !hasFQANs) {
String failure = multipleIds ? "failures" : "failure";
throw new AuthenticationException("validation " + failure + ": " + ids);
}
checkAuthentication(hasX509, "no X509 certificate chain");
checkAuthentication(hasFQANs, "no FQANs");
}
private String buildErrorMessage(List<VOMSValidationErrorMessage> errors)
{
return errors.isEmpty() ? "(unknown)" : errors.stream().
map(VOMSValidationErrorMessage::toString).
collect(Collectors.joining(", ", "[", "]"));
}
}