View Javadoc
1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements. See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership. The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License. You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied. See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.wss4j.dom.transform;
20  
21  import org.apache.jcp.xml.dsig.internal.dom.ApacheOctetStreamData;
22  import org.apache.wss4j.common.ext.Attachment;
23  import org.apache.wss4j.common.ext.AttachmentRequestCallback;
24  import org.apache.wss4j.common.ext.AttachmentResultCallback;
25  import org.apache.wss4j.common.ext.WSSecurityException;
26  import org.apache.wss4j.common.util.AttachmentUtils;
27  import org.apache.wss4j.common.util.CRLFOutputStream;
28  import org.apache.wss4j.dom.WSConstants;
29  import org.apache.xml.security.c14n.CanonicalizationException;
30  import org.apache.xml.security.c14n.Canonicalizer;
31  import org.apache.xml.security.c14n.InvalidCanonicalizerException;
32  import org.apache.xml.security.parser.XMLParserException;
33  import org.apache.xml.security.signature.XMLSignatureInput;
34  import org.apache.xml.security.signature.XMLSignatureStreamInput;
35  
36  import javax.security.auth.callback.Callback;
37  import javax.security.auth.callback.CallbackHandler;
38  import javax.xml.crypto.Data;
39  import javax.xml.crypto.MarshalException;
40  import javax.xml.crypto.OctetStreamData;
41  import javax.xml.crypto.XMLCryptoContext;
42  import javax.xml.crypto.XMLStructure;
43  import javax.xml.crypto.dsig.TransformException;
44  import javax.xml.crypto.dsig.TransformService;
45  import javax.xml.crypto.dsig.spec.TransformParameterSpec;
46  
47  import java.io.BufferedInputStream;
48  import java.io.ByteArrayInputStream;
49  import java.io.ByteArrayOutputStream;
50  import java.io.FilterInputStream;
51  import java.io.IOException;
52  import java.io.InputStream;
53  import java.io.OutputStream;
54  import java.security.InvalidAlgorithmParameterException;
55  import java.security.spec.AlgorithmParameterSpec;
56  import java.util.List;
57  
58  public class AttachmentContentSignatureTransform extends TransformService {
59  
60      public static final String TRANSFORM_URI = WSConstants.SWA_ATTACHMENT_CONTENT_SIG_TRANS;
61      public static final String ATTACHMENT_CALLBACKHANDLER = "AttachmentContentTransform.attachmentCallbackHandler";
62  
63      private AttachmentTransformParameterSpec attachmentTransformParameterSpec;
64  
65      @Override
66      public void init(TransformParameterSpec params) throws InvalidAlgorithmParameterException {
67          if (!(params instanceof AttachmentTransformParameterSpec)) {
68              throw new InvalidAlgorithmParameterException("Expected AttachmentTransformParameterSpec");
69          }
70          this.attachmentTransformParameterSpec = (AttachmentTransformParameterSpec) params;
71      }
72  
73      protected AttachmentTransformParameterSpec getAttachmentTransformParameterSpec() {
74          return attachmentTransformParameterSpec;
75      }
76  
77      @Override
78      public void init(XMLStructure parent, XMLCryptoContext context) throws InvalidAlgorithmParameterException {
79      }
80  
81      @Override
82      public void marshalParams(XMLStructure parent, XMLCryptoContext context) throws MarshalException {
83      }
84  
85      @Override
86      public AlgorithmParameterSpec getParameterSpec() {
87          return attachmentTransformParameterSpec;
88      }
89  
90      @Override
91      public Data transform(Data data, XMLCryptoContext context) throws TransformException {
92          return transform(data, context, null);
93      }
94  
95      /*
96       * http://docs.oasis-open.org/wss-m/wss/v1.1.1/os/wss-SwAProfile-v1.1.1-os.html
97       * 5.2 Referencing Attachments
98       * This profile assumes, since it is not defined in RFC 2396 Section 4.2, that
99       * all cid: references are not same-document references and that therefore, under
100      * XMLDSIG, dereferencing a cid: URI always yields an octet stream as input to the
101      * transform chain [RFC2396], [XMLDSIG].
102      */
103     @Override
104     public Data transform(Data data, XMLCryptoContext context, OutputStream os) throws TransformException {
105 
106         String attachmentUri = ((ApacheOctetStreamData) data).getURI();
107         String attachmentId = null;
108         try {
109             attachmentId = AttachmentUtils.getAttachmentId(attachmentUri);
110         } catch (WSSecurityException e) {
111             throw new TransformException(e);
112         }
113 
114         Attachment attachment;
115         if (attachmentTransformParameterSpec != null) {
116             attachment = attachmentTransformParameterSpec.getAttachment();
117             context.setProperty(ATTACHMENT_CALLBACKHANDLER,
118                                 attachmentTransformParameterSpec.getAttachmentCallbackHandler());
119         } else {
120             attachment = attachmentRequestCallback(context, attachmentId);
121         }
122         return processAttachment(context, os, attachmentUri, attachment);
123     }
124 
125     protected Attachment attachmentRequestCallback(XMLCryptoContext context, String attachmentId)
126         throws TransformException {
127         CallbackHandler attachmentCallbackHandler =
128             (CallbackHandler) context.getProperty(ATTACHMENT_CALLBACKHANDLER);
129         if (attachmentCallbackHandler == null) {
130             throw new TransformException("No attachment callbackhandler supplied");
131         }
132         AttachmentRequestCallback attachmentRequestCallback = new AttachmentRequestCallback();
133         attachmentRequestCallback.setAttachmentId(attachmentId);
134         try {
135             attachmentCallbackHandler.handle(new Callback[]{attachmentRequestCallback});
136         } catch (Exception e) {
137             throw new TransformException(e);
138         }
139         List<Attachment> attachments = attachmentRequestCallback.getAttachments();
140         if (attachments == null || attachments.isEmpty() || !attachmentId.equals(attachments.get(0).getId())) {
141             throw new TransformException("Attachment not found");
142         }
143         return attachments.get(0);
144     }
145 
146     protected void attachmentResultCallback(XMLCryptoContext context, Attachment attachment)
147         throws TransformException {
148         CallbackHandler attachmentCallbackHandler =
149             (CallbackHandler) context.getProperty(ATTACHMENT_CALLBACKHANDLER);
150         if (attachmentCallbackHandler == null) {
151             throw new TransformException("No attachment callbackhandler supplied");
152         }
153         AttachmentResultCallback attachmentResultCallback = new AttachmentResultCallback();
154         attachmentResultCallback.setAttachmentId(attachment.getId());
155         attachmentResultCallback.setAttachment(attachment);
156         try {
157             attachmentCallbackHandler.handle(new Callback[]{attachmentResultCallback});
158         } catch (Exception e) {
159             throw new TransformException(e);
160         }
161     }
162 
163     @SuppressWarnings("resource")
164     protected Data processAttachment(XMLCryptoContext context, OutputStream os, String attachmentUri,
165                                      Attachment attachment) throws TransformException {
166         try {
167             //try to reuse the inputStream in the hope that the provided inputStream is backed by a disk storage
168             InputStream inputStream = attachment.getSourceStream(); //NOPMD
169             if (!inputStream.markSupported()) {
170                 inputStream = new BufferedInputStream(inputStream);
171             }
172             inputStream.mark(Integer.MAX_VALUE); //we can process at maximum 2G with the standard jdk streams
173             inputStream = new FilterInputStream(inputStream) {
174                 @Override
175                 public void close() throws IOException {
176                     //I hate stuff which are closing _my_ streams!
177                 }
178             };
179 
180             OutputStream outputStream = os;
181             if (outputStream == null) {
182                 outputStream = new ByteArrayOutputStream(); //NOPMD
183             }
184 
185             String mimeType = attachment.getMimeType();
186 
187             if (mimeType != null
188                 && (mimeType.matches("(?i)(text/xml).*")
189                     || mimeType.matches("(?i)(application/xml).*")
190                     || mimeType.matches("(?i)(application|image)/.*\\+xml.*"))) {
191                 /* 5.4.2:
192                  * Content of an XML Content-Type MUST be XML canonicalized using
193                  * Exclusive XML Canonicalization without comments, as specified by
194                  * the URI http://www.w3.org/2001/10/xml-exc-c14n# [Excl-Canon].
195                  * The reason for requiring Exclusive Canonicalization is that many
196                  * implementations will support Exclusive Canonicalization for other
197                  * XML Signature purposes, since this form of canonicalization
198                  * supports context changes. The InclusiveNamespace PrefixList
199                  * attribute SHOULD be empty or not present.
200                  */
201                 Canonicalizer canon = Canonicalizer.getInstance(WSConstants.C14N_EXCL_OMIT_COMMENTS);
202 
203                 XMLSignatureInput xmlSignatureInput = new XMLSignatureStreamInput(inputStream); //NOPMD
204                 canon.canonicalizeXPathNodeSet(xmlSignatureInput.getNodeSet(), outputStream);
205 
206             } else if (mimeType != null && mimeType.matches("(?i)(text/).*")) {
207                 CRLFOutputStream crlfOutputStream = new CRLFOutputStream(outputStream); //NOPMD
208                 int numBytes;
209                 byte[] buf = new byte[8192];
210                 while ((numBytes = inputStream.read(buf)) != -1) {
211                     crlfOutputStream.write(buf, 0, numBytes);
212                 }
213 
214             } else {
215                 int numBytes;
216                 byte[] buf = new byte[8192];
217                 while ((numBytes = inputStream.read(buf)) != -1) {
218                     outputStream.write(buf, 0, numBytes);
219                 }
220             }
221 
222             //reset the inputStream to be able to reuse it
223             inputStream.reset();
224 
225             //create a new attachment and do the result callback
226             final Attachment resultAttachment = new Attachment();
227             resultAttachment.setId(attachment.getId());
228             resultAttachment.setMimeType(mimeType);
229             resultAttachment.addHeaders(attachment.getHeaders());
230             resultAttachment.setSourceStream(inputStream);
231             attachmentResultCallback(context, resultAttachment);
232 
233             if (os == null) {
234                 return new OctetStreamData(
235                         new ByteArrayInputStream(
236                                 ((ByteArrayOutputStream)outputStream).toByteArray()
237                         ),
238                         attachmentUri, mimeType);
239             }
240             return null;
241         } catch (IOException | InvalidCanonicalizerException | CanonicalizationException
242             | XMLParserException e) {
243             throw new TransformException(e);
244         }
245     }
246 
247     @Override
248     public boolean isFeatureSupported(String feature) {
249         if (feature == null) {
250             throw new NullPointerException();
251         } else {
252             return false;
253         }
254     }
255 }