001/**
002 * Copyright 2005-2014 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.ksb.messaging;
017
018import org.apache.commons.codec.binary.Base64;
019import org.apache.commons.lang.StringUtils;
020import org.apache.http.Header;
021import org.apache.http.HttpResponse;
022import org.apache.http.HttpStatus;
023import org.apache.http.client.HttpClient;
024import org.apache.http.client.methods.HttpPost;
025import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
026import org.kuali.rice.ksb.security.HttpClientHeaderDigitalSigner;
027import org.kuali.rice.ksb.security.SignatureVerifyingInputStream;
028import org.kuali.rice.ksb.security.admin.service.JavaSecurityManagementService;
029import org.kuali.rice.ksb.security.service.DigitalSignatureService;
030import org.kuali.rice.ksb.util.KSBConstants;
031import org.springframework.remoting.httpinvoker.HttpComponentsHttpInvokerRequestExecutor;
032import org.springframework.remoting.httpinvoker.HttpInvokerClientConfiguration;
033
034import java.io.ByteArrayInputStream;
035import java.io.ByteArrayOutputStream;
036import java.io.IOException;
037import java.io.InputStream;
038import java.security.GeneralSecurityException;
039import java.security.Signature;
040import java.security.cert.CertificateFactory;
041
042
043/**
044 * At HttpInvokerRequestExecutor which is capable of digitally signing and verifying messages.  It's capabilities
045 * to execute the signing and verification can be turned on or off via an application constant.
046 *
047 * @author Kuali Rice Team (rice.collab@kuali.org)
048 */
049public class KSBHttpInvokerRequestExecutor extends HttpComponentsHttpInvokerRequestExecutor {
050
051    private Boolean secure = Boolean.TRUE;
052
053    public KSBHttpInvokerRequestExecutor() {
054        super();
055    }
056
057    public KSBHttpInvokerRequestExecutor(Boolean secure) {
058        super();
059        this.secure = secure;
060    }
061
062    public KSBHttpInvokerRequestExecutor(HttpClient httpClient) {
063        super(httpClient);
064    }
065
066    /**
067     * Signs the outgoing request by generating a digital signature from the bytes in the ByteArrayOutputStream and attaching the
068     * signature and our alias to the headers of the PostMethod.
069     */
070    @Override
071    protected void setRequestBody(HttpInvokerClientConfiguration config, HttpPost httpPost, ByteArrayOutputStream baos) throws IOException {
072        if (isSecure()) {
073            try {
074                signRequest(httpPost, baos);
075            } catch (Exception e) {
076                throw new RuntimeException("Failed to sign the outgoing message.", e);
077            }
078        }
079        super.setRequestBody(config, httpPost, baos);
080    }
081
082    /**
083     * Returns a wrapped InputStream which is responsible for verifying the digital signature on the response after all
084     * data has been read.
085     */
086    @Override
087    protected InputStream getResponseBody(HttpInvokerClientConfiguration config, HttpResponse postMethod) throws IOException {
088        if (isSecure()) {
089            // extract and validate the headers
090            Header digitalSignatureHeader = postMethod.getFirstHeader(KSBConstants.DIGITAL_SIGNATURE_HEADER);
091            Header keyStoreAliasHeader = postMethod.getFirstHeader(KSBConstants.KEYSTORE_ALIAS_HEADER);
092            Header certificateHeader = postMethod.getFirstHeader(KSBConstants.KEYSTORE_CERTIFICATE_HEADER);
093
094            if (digitalSignatureHeader == null || StringUtils.isEmpty(digitalSignatureHeader.getValue())) {
095                throw new RuntimeException("A digital signature header was required on the response but none was found.");
096            }
097
098            boolean foundValidKeystoreAlias = (keyStoreAliasHeader != null && StringUtils.isNotBlank(keyStoreAliasHeader.getValue()));
099            boolean foundValidCertificate = (certificateHeader != null && StringUtils.isNotBlank(certificateHeader.getValue()));
100
101            if (!foundValidCertificate && !foundValidKeystoreAlias) {
102                throw new RuntimeException("Either a key store alias header or a certificate header was required on the response but neither were found.");
103            }
104
105            // decode the digital signature from the header into binary
106            byte[] digitalSignature = Base64.decodeBase64(digitalSignatureHeader.getValue().getBytes("UTF-8"));
107            String errorQualifier = "General Security Error";
108
109            try {
110                Signature signature = null;
111
112                if (foundValidCertificate) {
113                    errorQualifier = "Error with given certificate";
114                    // get the Signature for verification based on the alias that was sent to us
115                    byte[] encodedCertificate = Base64.decodeBase64(certificateHeader.getValue().getBytes("UTF-8"));
116                    CertificateFactory cf = CertificateFactory.getInstance("X.509");
117                    signature = getDigitalSignatureService().getSignatureForVerification(cf.generateCertificate(new ByteArrayInputStream(encodedCertificate)));
118                } else if (foundValidKeystoreAlias) {
119                    // get the Signature for verification based on the alias that was sent to us
120                    String keystoreAlias = keyStoreAliasHeader.getValue();
121                    errorQualifier = "Error with given alias " + keystoreAlias;
122                    signature = getDigitalSignatureService().getSignatureForVerification(keystoreAlias);
123                }
124
125                // wrap the InputStream in an input stream that will verify the signature
126                return new SignatureVerifyingInputStream(digitalSignature, signature, super.getResponseBody(config, postMethod));
127            } catch (GeneralSecurityException e) {
128                throw new RuntimeException("Problem verifying signature: " + errorQualifier,e);
129            }
130        }
131
132        return super.getResponseBody(config, postMethod);
133    }
134
135
136
137    @Override
138    protected void validateResponse(HttpInvokerClientConfiguration config, HttpResponse response) throws HttpException {
139        int statusCode = response.getStatusLine().getStatusCode();
140
141        // HTTP status codes in the 200-299 range indicate success
142        if (statusCode >= HttpStatus.SC_MULTIPLE_CHOICES /* 300 */) {
143            throw new HttpException(statusCode, "Did not receive successful HTTP response: status code = " + statusCode +
144                    ", status message = [" + response.getStatusLine().getReasonPhrase() + "]");
145        }
146    }
147
148    /**
149     * Signs the request by adding headers to the PostMethod.
150     */
151    protected void signRequest(HttpPost postMethod, ByteArrayOutputStream baos) throws Exception {
152        Signature signature = getDigitalSignatureService().getSignatureForSigning();
153        HttpClientHeaderDigitalSigner signer =
154                new HttpClientHeaderDigitalSigner(signature, postMethod, getJavaSecurityManagementService().getModuleKeyStoreAlias());
155        signer.getSignature().update(baos.toByteArray());
156        signer.sign();
157    }
158
159    protected boolean isSecure() {
160        return getSecure();// && Utilities.getBooleanConstant(KewApiConstants.SECURITY_HTTP_INVOKER_SIGN_MESSAGES, false);
161    }
162
163    public Boolean getSecure() {
164        return this.secure;
165    }
166
167    public void setSecure(Boolean secure) {
168        this.secure = secure;
169    }
170
171    protected DigitalSignatureService getDigitalSignatureService() {
172        return (DigitalSignatureService) GlobalResourceLoader.getService(KSBConstants.ServiceNames.DIGITAL_SIGNATURE_SERVICE);
173    }
174
175    protected JavaSecurityManagementService getJavaSecurityManagementService() {
176        return (JavaSecurityManagementService)GlobalResourceLoader.getService(KSBConstants.ServiceNames.JAVA_SECURITY_MANAGEMENT_SERVICE);
177    }
178
179}