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}