org.dcache.gsi.ServerGsiEngine.java Source code

Java tutorial

Introduction

Here is the source code for org.dcache.gsi.ServerGsiEngine.java

Source

/* 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);
            }
        }
    }
}