001/**
002 * Copyright 2005-2016 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 java.io.ByteArrayInputStream;
019import java.io.ByteArrayOutputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.security.GeneralSecurityException;
023import java.security.Signature;
024import java.security.cert.CertificateFactory;
025
026import org.apache.commons.codec.binary.Base64;
027import org.apache.commons.httpclient.Header;
028import org.apache.commons.httpclient.HttpClient;
029import org.apache.commons.httpclient.methods.PostMethod;
030import org.apache.commons.lang.StringUtils;
031import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
032import org.kuali.rice.ksb.security.HttpClientHeaderDigitalSigner;
033import org.kuali.rice.ksb.security.SignatureVerifyingInputStream;
034import org.kuali.rice.ksb.security.admin.service.JavaSecurityManagementService;
035import org.kuali.rice.ksb.security.service.DigitalSignatureService;
036import org.kuali.rice.ksb.util.KSBConstants;
037import org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor;
038import org.springframework.remoting.httpinvoker.HttpInvokerClientConfiguration;
039
040
041/**
042 * At HttpInvokerRequestExecutor which is capable of digitally signing and verifying messages.  It's capabilities
043 * to execute the signing and verification can be turned on or off via an application constant.
044 * 
045 * @author Kuali Rice Team (rice.collab@kuali.org)
046 */
047public class KSBHttpInvokerRequestExecutor extends CommonsHttpInvokerRequestExecutor {
048        
049        private Boolean secure = Boolean.TRUE;
050        
051        public KSBHttpInvokerRequestExecutor() {
052                super();
053        }
054        
055        public KSBHttpInvokerRequestExecutor(Boolean secure) {
056                super();
057                this.secure = secure;
058        }
059
060        public KSBHttpInvokerRequestExecutor(HttpClient httpClient) {
061                super(httpClient);
062        }
063        
064        /**
065         * Signs the outgoing request by generating a digital signature from the bytes in the ByteArrayOutputStream and attaching the
066         * signature and our alias to the headers of the PostMethod.
067         */
068        @Override
069        protected void setRequestBody(HttpInvokerClientConfiguration config, PostMethod postMethod, ByteArrayOutputStream baos) throws IOException {
070                if (isSecure()) {
071                        try {
072                                signRequest(postMethod, baos);  
073                        } catch (Exception e) {
074                                throw new RuntimeException("Failed to sign the outgoing message.", e);
075                        }
076                }
077                super.setRequestBody(config, postMethod, baos);
078        }
079        
080        /**
081         * Returns a wrapped InputStream which is responsible for verifying the digital signature on the response after all
082         * data has been read.
083         */
084        @Override
085        protected InputStream getResponseBody(HttpInvokerClientConfiguration config, PostMethod postMethod) throws IOException {
086                if (isSecure()) {
087                        // extract and validate the headers
088                        Header digitalSignatureHeader = postMethod.getResponseHeader(KSBConstants.DIGITAL_SIGNATURE_HEADER);
089                        Header keyStoreAliasHeader = postMethod.getResponseHeader(KSBConstants.KEYSTORE_ALIAS_HEADER);
090                        Header certificateHeader = postMethod.getResponseHeader(KSBConstants.KEYSTORE_CERTIFICATE_HEADER);
091                        if (digitalSignatureHeader == null || StringUtils.isEmpty(digitalSignatureHeader.getValue())) {
092                                throw new RuntimeException("A digital signature header was required on the response but none was found.");
093                        }
094                        boolean foundValidKeystoreAlias = (keyStoreAliasHeader != null && StringUtils.isNotBlank(keyStoreAliasHeader.getValue()));
095                        boolean foundValidCertificate = (certificateHeader != null && StringUtils.isNotBlank(certificateHeader.getValue()));
096                        if (!foundValidCertificate && !foundValidKeystoreAlias) {
097                throw new RuntimeException("Either a key store alias header or a certificate header was required on the response but neither were found.");
098                        }
099                        // decode the digital signature from the header into binary
100                        byte[] digitalSignature = Base64.decodeBase64(digitalSignatureHeader.getValue().getBytes("UTF-8"));
101                        String errorQualifier = "General Security Error";
102                        try {
103                            Signature signature = null;
104                            if (foundValidCertificate) {
105                    errorQualifier = "Error with given certificate";
106                        // get the Signature for verification based on the alias that was sent to us
107                                byte[] encodedCertificate = Base64.decodeBase64(certificateHeader.getValue().getBytes("UTF-8"));
108                            CertificateFactory cf = CertificateFactory.getInstance("X.509");
109                        signature = getDigitalSignatureService().getSignatureForVerification(cf.generateCertificate(new ByteArrayInputStream(encodedCertificate)));
110                            } else if (foundValidKeystoreAlias) {
111                        // get the Signature for verification based on the alias that was sent to us
112                                String keystoreAlias = keyStoreAliasHeader.getValue();
113                                errorQualifier = "Error with given alias " + keystoreAlias;
114                        signature = getDigitalSignatureService().getSignatureForVerification(keystoreAlias);
115                            }
116                            
117                                // wrap the InputStream in an input stream that will verify the signature
118                                return new SignatureVerifyingInputStream(digitalSignature, signature, super.getResponseBody(config, postMethod));
119                        } catch (GeneralSecurityException e) {
120                                throw new RuntimeException("Problem verifying signature: " + errorQualifier,e);
121                        }
122                }
123                return super.getResponseBody(config, postMethod);
124        }
125
126        
127        
128        @Override
129        protected void validateResponse(HttpInvokerClientConfiguration config, PostMethod postMethod) throws IOException {
130                if (postMethod.getStatusCode() >= 300) {
131                        throw new HttpException(postMethod.getStatusCode(), "Did not receive successful HTTP response: status code = " + postMethod.getStatusCode() +
132                                        ", status message = [" + postMethod.getStatusText() + "]");
133                }
134        }
135
136        /**
137         * Signs the request by adding headers to the PostMethod.
138         */
139        protected void signRequest(PostMethod postMethod, ByteArrayOutputStream baos) throws Exception {
140                Signature signature = getDigitalSignatureService().getSignatureForSigning();
141                HttpClientHeaderDigitalSigner signer = new HttpClientHeaderDigitalSigner(signature, postMethod, getJavaSecurityManagementService().getModuleKeyStoreAlias());
142                signer.getSignature().update(baos.toByteArray());
143                signer.sign();
144        }
145        
146        protected boolean isSecure() {
147                return getSecure();// && Utilities.getBooleanConstant(KewApiConstants.SECURITY_HTTP_INVOKER_SIGN_MESSAGES, false);
148        }
149
150        public Boolean getSecure() {
151                return this.secure;
152        }
153
154        public void setSecure(Boolean secure) {
155                this.secure = secure;
156        }
157        
158        protected DigitalSignatureService getDigitalSignatureService() {
159                return (DigitalSignatureService) GlobalResourceLoader.getService(KSBConstants.ServiceNames.DIGITAL_SIGNATURE_SERVICE);
160        }
161        
162        protected JavaSecurityManagementService getJavaSecurityManagementService() {
163                return (JavaSecurityManagementService)GlobalResourceLoader.getService(KSBConstants.ServiceNames.JAVA_SECURITY_MANAGEMENT_SERVICE);
164        }
165        
166}