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}