001 /**
002 * Copyright 2005-2012 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 */
016 package org.kuali.rice.ksb.messaging;
017
018 import java.io.ByteArrayInputStream;
019 import java.io.ByteArrayOutputStream;
020 import java.io.IOException;
021 import java.io.InputStream;
022 import java.security.GeneralSecurityException;
023 import java.security.Signature;
024 import java.security.cert.CertificateFactory;
025
026 import org.apache.commons.codec.binary.Base64;
027 import org.apache.commons.httpclient.Header;
028 import org.apache.commons.httpclient.HttpClient;
029 import org.apache.commons.httpclient.methods.PostMethod;
030 import org.apache.commons.lang.StringUtils;
031 import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
032 import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
033 import org.kuali.rice.ksb.security.HttpClientHeaderDigitalSigner;
034 import org.kuali.rice.ksb.security.SignatureVerifyingInputStream;
035 import org.kuali.rice.ksb.security.admin.service.JavaSecurityManagementService;
036 import org.kuali.rice.ksb.security.service.DigitalSignatureService;
037 import org.kuali.rice.ksb.util.KSBConstants;
038 import org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor;
039 import 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 */
048 public 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 }