View Javadoc

1   /*
2    * Copyright [2007] [University Corporation for Advanced Internet Development, Inc.]
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.opensaml.saml2.binding.encoding;
18  
19  import java.io.UnsupportedEncodingException;
20  
21  import org.apache.velocity.VelocityContext;
22  import org.apache.velocity.app.VelocityEngine;
23  import org.opensaml.common.binding.SAMLMessageContext;
24  import org.opensaml.common.xml.SAMLConstants;
25  import org.opensaml.ws.message.encoder.MessageEncodingException;
26  import org.opensaml.ws.transport.http.HTTPTransportUtils;
27  import org.opensaml.xml.Configuration;
28  import org.opensaml.xml.io.Marshaller;
29  import org.opensaml.xml.io.MarshallingException;
30  import org.opensaml.xml.security.SecurityConfiguration;
31  import org.opensaml.xml.security.SecurityException;
32  import org.opensaml.xml.security.SecurityHelper;
33  import org.opensaml.xml.security.SigningUtil;
34  import org.opensaml.xml.security.credential.Credential;
35  import org.opensaml.xml.security.keyinfo.KeyInfoGenerator;
36  import org.opensaml.xml.signature.KeyInfo;
37  import org.opensaml.xml.util.Base64;
38  import org.opensaml.xml.util.DatatypeHelper;
39  import org.opensaml.xml.util.XMLHelper;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  /**
44   * SAML 2.0 HTTP-POST-SimpleSign binding message encoder.
45   * 
46   * <p>
47   * The spec does not preclude the SAML 2 protocol message from being signed using the XML Signature method, in addition
48   * to the SimpleSign method specified by this binding. Signing via XML Signature over the SAML request and response
49   * payload may be toggled by the <code>signXMLProtocolMessage</code> parameter to the constructor
50   * {@link HTTPPostSimpleSignEncoder#HTTPPostSimpleSignEncoder(VelocityEngine, String, boolean)}. If this constructor
51   * variant is not used, the flag defaults to <code>false</code>.
52   * </p>
53   */
54  public class HTTPPostSimpleSignEncoder extends HTTPPostEncoder {
55  
56      /** Class logger. */
57      private final Logger log = LoggerFactory.getLogger(HTTPPostSimpleSignEncoder.class);
58  
59      /**
60       * Flag to indicate whether the SAML 2 protocol message should additionally be signed using the XML Signature, in
61       * addition to SimpleSign.
62       */
63      private boolean signProtocolMessageWithXMLDSIG;
64  
65      /**
66       * Constructor.
67       * 
68       * @param engine Velocity engine instance used to create POST body
69       * @param templateId ID of the template used to create POST body
70       */
71      public HTTPPostSimpleSignEncoder(VelocityEngine engine, String templateId) {
72          super(engine, templateId);
73          signProtocolMessageWithXMLDSIG = false;
74      }
75  
76      /**
77       * Constructor.
78       * 
79       * @param engine Velocity engine instance used to create POST body
80       * @param templateId ID of the template used to create POST body
81       * @param signXMLProtocolMessage if true, the protocol message will be signed according to the XML Signature
82       *            specification, in addition to the HTTP-POST-SimpleSign binding specification
83       */
84      public HTTPPostSimpleSignEncoder(VelocityEngine engine, String templateId, boolean signXMLProtocolMessage) {
85          super(engine, templateId);
86          signProtocolMessageWithXMLDSIG = signXMLProtocolMessage;
87      }
88  
89      /** {@inheritDoc} */
90      public String getBindingURI() {
91          return SAMLConstants.SAML2_POST_SIMPLE_SIGN_BINDING_URI;
92      }
93  
94      /** {@inheritDoc} */
95      protected void signMessage(SAMLMessageContext messageContext) throws MessageEncodingException {
96          if (signProtocolMessageWithXMLDSIG) {
97              super.signMessage(messageContext);
98          }
99      }
100 
101     /** {@inheritDoc} */
102     protected void populateVelocityContext(VelocityContext velocityContext, SAMLMessageContext messageContext,
103             String endpointURL) throws MessageEncodingException {
104 
105         super.populateVelocityContext(velocityContext, messageContext, endpointURL);
106 
107         Credential signingCredential = messageContext.getOuboundSAMLMessageSigningCredential();
108         if (signingCredential == null) {
109             log.debug("No signing credential was supplied, skipping HTTP-Post simple signing");
110             return;
111         }
112 
113         // TODO pull SecurityConfiguration from SAMLMessageContext? needs to be added
114         // TODO pull binding-specific keyInfoGenName from encoder setting, etc?
115         String sigAlgURI = getSignatureAlgorithmURI(signingCredential, null);
116         velocityContext.put("SigAlg", sigAlgURI);
117 
118         String formControlData = buildFormDataToSign(velocityContext, sigAlgURI);
119         velocityContext.put("Signature", generateSignature(signingCredential, sigAlgURI, formControlData));
120 
121         KeyInfoGenerator kiGenerator = SecurityHelper.getKeyInfoGenerator(signingCredential, null, null);
122         if (kiGenerator != null) {
123             String kiBase64 = buildKeyInfo(signingCredential, kiGenerator);
124             if (!DatatypeHelper.isEmpty(kiBase64)) {
125                 velocityContext.put("KeyInfo", kiBase64);
126             }
127         }
128     }
129 
130     /**
131      * Build the {@link KeyInfo} from the signing credential.
132      * 
133      * @param signingCredential the credential used for signing
134      * @param kiGenerator the generator for the KeyInfo
135      * @throws MessageEncodingException thrown if there is an error generating or marshalling the KeyInfo
136      * @return the marshalled, serialized and base64-encoded KeyInfo, or null if none was generated
137      */
138     protected String buildKeyInfo(Credential signingCredential, KeyInfoGenerator kiGenerator)
139             throws MessageEncodingException {
140 
141         try {
142             KeyInfo keyInfo = kiGenerator.generate(signingCredential);
143             if (keyInfo != null) {
144                 Marshaller marshaller = Configuration.getMarshallerFactory().getMarshaller(keyInfo);
145                 if (marshaller == null) {
146                     log.error("No KeyInfo marshaller available from configuration");
147                     throw new MessageEncodingException("No KeyInfo marshaller was configured");
148                 }
149                 String kiXML = XMLHelper.nodeToString(marshaller.marshall(keyInfo));
150                 String kiBase64 = Base64.encodeBytes(kiXML.getBytes(), Base64.DONT_BREAK_LINES);
151                 return kiBase64;
152             } else {
153                 return null;
154             }
155         } catch (SecurityException e) {
156             log.error("Error generating KeyInfo from signing credential", e);
157             throw new MessageEncodingException("Error generating KeyInfo from signing credential", e);
158         } catch (MarshallingException e) {
159             log.error("Error marshalling KeyInfo based on signing credential", e);
160             throw new MessageEncodingException("Error marshalling KeyInfo based on signing credential", e);
161         }
162     }
163 
164     /**
165      * Build the form control data string over which the signature is computed.
166      * 
167      * @param velocityContext the Velocity context which is already populated with the values for SAML message and relay
168      *            state
169      * @param sigAlgURI the signature algorithm URI
170      * 
171      * @return the form control data string for signature computation
172      */
173     protected String buildFormDataToSign(VelocityContext velocityContext, String sigAlgURI) {
174         StringBuilder builder = new StringBuilder();
175 
176         boolean isRequest = false;
177         if (velocityContext.get("SAMLRequest") != null) {
178             isRequest = true;
179         }
180 
181         String msgB64;
182         if (isRequest) {
183             msgB64 = (String) velocityContext.get("SAMLRequest");
184         } else {
185             msgB64 = (String) velocityContext.get("SAMLResponse");
186         }
187 
188         String msg = null;
189         try {
190             msg = new String(Base64.decode(msgB64), "UTF-8");
191         } catch (UnsupportedEncodingException e) {
192             // All JVM's required to support UTF-8
193         }
194 
195         if (isRequest) {
196             builder.append("SAMLRequest=" + msg);
197         } else {
198             builder.append("SAMLResponse=" + msg);
199         }
200 
201         if (velocityContext.get("RelayState") != null) {
202             builder.append("&RelayState=" + HTTPTransportUtils.urlDecode((String) velocityContext.get("RelayState")));
203         }
204 
205         builder.append("&SigAlg=" + sigAlgURI);
206 
207         return builder.toString();
208     }
209 
210     /**
211      * Gets the signature algorithm URI to use with the given signing credential.
212      * 
213      * @param credential the credential that will be used to sign the message
214      * @param config the SecurityConfiguration to use (may be null)
215      * 
216      * @return signature algorithm to use with the given signing credential
217      * 
218      * @throws MessageEncodingException thrown if the algorithm URI could not be derived from the supplied credential
219      */
220     protected String getSignatureAlgorithmURI(Credential credential, SecurityConfiguration config)
221             throws MessageEncodingException {
222 
223         SecurityConfiguration secConfig;
224         if (config != null) {
225             secConfig = config;
226         } else {
227             secConfig = Configuration.getGlobalSecurityConfiguration();
228         }
229 
230         String signAlgo = secConfig.getSignatureAlgorithmURI(credential);
231 
232         if (signAlgo == null) {
233             throw new MessageEncodingException("The signing credential's algorithm URI could not be derived");
234         }
235 
236         return signAlgo;
237     }
238 
239     /**
240      * Generates the signature over the string of concatenated form control data as indicated by the SimpleSign spec.
241      * 
242      * @param signingCredential credential that will be used to sign
243      * @param algorithmURI algorithm URI of the signing credential
244      * @param formData form control data to be signed
245      * 
246      * @return base64 encoded signature of form control data
247      * 
248      * @throws MessageEncodingException there is an error computing the signature
249      */
250     protected String generateSignature(Credential signingCredential, String algorithmURI, String formData)
251             throws MessageEncodingException {
252 
253         log.debug(String.format(
254                 "Generating signature with key type '%s', algorithm URI '%s' over form control string '%s'",
255                 SecurityHelper.extractSigningKey(signingCredential).getAlgorithm(), algorithmURI, formData));
256 
257         String b64Signature = null;
258         try {
259             byte[] rawSignature = SigningUtil.signWithURI(signingCredential, algorithmURI, formData.getBytes("UTF-8"));
260             b64Signature = Base64.encodeBytes(rawSignature, Base64.DONT_BREAK_LINES);
261             log.debug("Generated digital signature value (base64-encoded) {}", b64Signature);
262         } catch (SecurityException e) {
263             log.error("Error during URL signing process", e);
264             throw new MessageEncodingException("Unable to sign form control string", e);
265         } catch (UnsupportedEncodingException e) {
266             // UTF-8 encoding is required to be supported by all JVMs
267         }
268 
269         return b64Signature;
270     }
271 
272 }