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