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 }