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    }