/* dCache - http://www.dcache.org/ * * Copyright (C) 2014-2015 Deutsches Elektronen-Synchrotron * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.dcache.gsi; import com.google.common.io.ByteSource; import eu.emi.security.authn.x509.X509Credential; import eu.emi.security.authn.x509.impl.CertificateUtils; import eu.emi.security.authn.x509.impl.KeyAndCertCredential; import eu.emi.security.authn.x509.proxy.ProxyCSRGenerator; import eu.emi.security.authn.x509.proxy.ProxyCertificateOptions; import org.bouncycastle.asn1.ASN1InputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLException; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.util.concurrent.TimeUnit; import static com.google.common.base.Preconditions.checkArgument; /** * Wrapper for SSLEngine that implements GSI delegation. Only the server side of GSI is * supported. The code is partly taken from org.globus.gsi.gssapi.GlobusGSSContextImpl. */ public class ServerGsiEngine extends InterceptingSSLEngine { private static final Logger LOGGER = LoggerFactory.getLogger(ServerGsiEngine.class); public static final String X509_CREDENTIAL = "org.dcache.credential"; /** The character sent on the wire to request delegation */ public static final char DELEGATION_CHAR = 'D'; private KeyPairCache keyPairCache = new KeyPairCache(30, TimeUnit.SECONDS); private final CertificateFactory cf; private boolean isUsingLegacyClose; private boolean isOutboundClosed; private KeyPair keyPair; public ServerGsiEngine(SSLEngine delegate, CertificateFactory cf) { super(delegate); this.cf = cf; receive(new GotDelegationCharacter()); } @Override public void closeOutbound() { isOutboundClosed = true; super.closeOutbound(); } public boolean isUsingLegacyClose() { return isUsingLegacyClose; } /** * Our SRM client (or rather JGlobus) doesn't like a proper SSL shutdown. If legacy close is * enabled any closure handshake messages are suppressed. */ public void setUsingLegacyClose(boolean usingLegacyClose) { this.isUsingLegacyClose = usingLegacyClose; } public void setKeyPairCache(KeyPairCache cache) { keyPairCache = cache; } @Override public void setUseClientMode(boolean isClientMode) { checkArgument(!isClientMode, "Only the server side of GSI is supported by this engine."); super.setUseClientMode(isClientMode); } @Override public boolean isInboundDone() { return (isUsingLegacyClose && isOutboundClosed) || super.isInboundDone(); } @Override public boolean isOutboundDone() { return (isUsingLegacyClose && isOutboundClosed) || super.isOutboundDone(); } @Override public SSLEngineResult.HandshakeStatus getHandshakeStatus() { if (isUsingLegacyClose && isOutboundClosed) { return SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; } return super.getHandshakeStatus(); } @Override public SSLEngineResult unwrap(ByteBuffer src, ByteBuffer[] dsts, int offset, int length) throws SSLException { if (isUsingLegacyClose && isOutboundClosed) { return new SSLEngineResult(SSLEngineResult.Status.CLOSED, SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING, 0,0); } return super.unwrap(src, dsts, offset, length); } @Override public SSLEngineResult wrap(ByteBuffer[] srcs, int offset, int length, ByteBuffer dst) throws SSLException { if (isUsingLegacyClose && isOutboundClosed) { return new SSLEngineResult(SSLEngineResult.Status.CLOSED, SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING, 0,0); } return super.wrap(srcs, offset, length, dst); } private ByteBuffer getCertRequest() throws IOException, GeneralSecurityException { X509Certificate[] chain = CertificateUtils.convertToX509Chain(getSession().getPeerCertificates()); int bits = ((RSAPublicKey) chain[0].getPublicKey()).getModulus().bitLength(); keyPair = keyPairCache.getKeyPair(bits); ProxyCertificateOptions options = new ProxyCertificateOptions(chain); options.setPublicKey(keyPair.getPublic()); options.setLimited(true); byte[] req = ProxyCSRGenerator.generate(options, keyPair.getPrivate()).getCSR().getEncoded(); return ByteBuffer.wrap(req, 0, req.length); } protected void verifyDelegatedCert(X509Certificate certificate) throws GeneralSecurityException { RSAPublicKey pubKey = (RSAPublicKey) certificate.getPublicKey(); RSAPrivateKey privKey = (RSAPrivateKey) keyPair.getPrivate(); if (!pubKey.getModulus().equals(privKey.getModulus())) { throw new GeneralSecurityException("Client delegated credentials do not match certificate request."); } } private void readDelegatedCredentials(ByteSource source) throws GeneralSecurityException, IOException { SSLSession session = getSession(); /* Parse the delegated certificate. */ X509Certificate certificate; try (InputStream in = source.openStream()) { certificate = (X509Certificate) cf.generateCertificate(in); } LOGGER.trace("Received delegated cert: {}", certificate); /* Verify that it matches our certificate request. */ verifyDelegatedCert(certificate); /* Build a certificate chain for the delegated certificate. */ Certificate[] chain = session.getPeerCertificates(); int chainLen = chain.length; X509Certificate[] newChain = new X509Certificate[chainLen + 1]; newChain[0] = certificate; for (int i = 0; i < chainLen; i++) { newChain[i + 1] = (X509Certificate) chain[i]; } /* Store GSI credentials in the SSL session. Use GsiRequestCustomizer to copy these * to the Request objects. */ X509Credential proxy = new KeyAndCertCredential(keyPair.getPrivate(), newChain); session.putValue(X509_CREDENTIAL, proxy); } private class GotDelegationCharacter implements Callback { @Override public void call(ByteBuffer buffer) throws SSLException { if (buffer.get(0) == DELEGATION_CHAR) { try { sendThenReceive(getCertRequest(), new GotDelegatedCredentials()); } catch (IOException | GeneralSecurityException e) { throw new SSLException("GSI delegation failed: " + e.toString(), e); } } } } private class GotDelegatedCredentials implements Callback { private int len; private ByteSource data; @Override public void call(ByteBuffer buffer) throws SSLException { checkArgument(buffer.hasArray(), "Buffer must have backing array"); len += buffer.position(); ByteSource chunk = ByteSource.wrap(buffer.array()).slice(buffer.arrayOffset(), buffer.position()); ByteSource source = (data == null) ? chunk : ByteSource.concat(data, chunk); try { readDelegatedCredentials(source); } catch (GeneralSecurityException | IOException e) { /* Check if we got the entire BER encoded object. We rely on the fact that the delegated * credential is transferred in its own SSL frames - i.e. buffer doesn't contain any * application data. * * Relying on an EofException isn't the most elegant solution, but the alternative would * be to implement a custom BER parser (REVISIT: check sun.security.provider.X509Factory * for a possibly cheaper way to read the entire certificate - we would have to copy the code * to get access to the relevant bits). */ try { try (ASN1InputStream in = new ASN1InputStream(source.openStream(), len, true)) { in.readObject(); } catch (EOFException f) { /* Incomplete - read another frame. */ ByteSource copy = ByteSource.wrap(chunk.read()); data = (data == null) ? copy : ByteSource.concat(data, copy); receive(this); return; } } catch (IOException f) { e.addSuppressed(f); } throw new SSLException("GSI delegation failed: " + e.toString(), e); } } } }