1 /*
   2  * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import java.io.IOException;
  25 import java.net.InetSocketAddress;
  26 import java.util.Collections;
  27 import java.util.List;
  28 import javax.xml.crypto.dsig.CanonicalizationMethod;
  29 import javax.xml.crypto.dsig.DigestMethod;
  30 import javax.xml.crypto.dsig.SignatureMethod;
  31 import javax.xml.crypto.dsig.SignedInfo;
  32 import javax.xml.crypto.dsig.XMLSignature;
  33 import javax.xml.crypto.dsig.XMLSignatureFactory;
  34 import javax.xml.crypto.dsig.dom.DOMSignContext;
  35 import javax.xml.crypto.dsig.dom.DOMValidateContext;
  36 import javax.xml.crypto.dsig.keyinfo.KeyInfo;
  37 import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
  38 import javax.xml.crypto.dsig.keyinfo.KeyValue;
  39 import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
  40 import javax.xml.crypto.dsig.spec.TransformParameterSpec;
  41 import javax.xml.parsers.DocumentBuilderFactory;
  42 import javax.xml.transform.Transformer;
  43 import javax.xml.transform.TransformerFactory;
  44 import javax.xml.transform.dom.DOMSource;
  45 import javax.xml.transform.stream.StreamResult;
  46 import org.w3c.dom.Document;
  47 import org.w3c.dom.Element;
  48 import org.w3c.dom.Node;
  49 import org.w3c.dom.NodeList;
  50 import com.sun.net.httpserver.HttpExchange;
  51 import com.sun.net.httpserver.HttpHandler;
  52 import com.sun.net.httpserver.HttpServer;
  53 import java.io.ByteArrayInputStream;
  54 import java.io.StringWriter;
  55 import java.security.InvalidAlgorithmParameterException;
  56 import java.security.Key;
  57 import java.security.KeyException;
  58 import java.security.KeyPair;
  59 import java.security.KeyPairGenerator;
  60 import java.security.NoSuchAlgorithmException;
  61 import java.security.PublicKey;
  62 import java.security.SecureRandom;
  63 import java.util.ArrayList;
  64 import java.util.Arrays;
  65 import javax.crypto.KeyGenerator;
  66 import javax.xml.crypto.MarshalException;
  67 import javax.xml.crypto.dom.DOMStructure;
  68 import javax.xml.crypto.dsig.Transform;
  69 import javax.xml.crypto.dsig.XMLSignatureException;
  70 import javax.xml.crypto.dsig.spec.XPathFilter2ParameterSpec;
  71 import javax.xml.crypto.dsig.spec.XPathFilterParameterSpec;
  72 import javax.xml.crypto.dsig.spec.XPathType;
  73 import javax.xml.crypto.dsig.spec.XSLTTransformParameterSpec;
  74 import javax.xml.parsers.ParserConfigurationException;
  75 import javax.xml.transform.TransformerConfigurationException;
  76 import javax.xml.transform.TransformerException;
  77 import org.xml.sax.SAXException;
  78 
  79 /**
  80  * @test
  81  * @bug 8074784
  82  * @summary Tests for generating detached XML Signatures
  83  * @modules jdk.httpserver/com.sun.net.httpserver
  84  */
  85 public class Detached {
  86 
  87     private static final String BOGUS = "bogus";
  88 
  89     private static final String[] canonicalizationMethods = new String[] {
  90         CanonicalizationMethod.EXCLUSIVE,
  91         CanonicalizationMethod.EXCLUSIVE_WITH_COMMENTS,
  92         CanonicalizationMethod.INCLUSIVE,
  93         CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS
  94     };
  95 
  96     private static final String[] xml_transforms = new String[] {
  97         Transform.XSLT,
  98         Transform.XPATH,
  99         Transform.XPATH2,
 100         CanonicalizationMethod.EXCLUSIVE,
 101         CanonicalizationMethod.EXCLUSIVE_WITH_COMMENTS,
 102         CanonicalizationMethod.INCLUSIVE,
 103         CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS,
 104     };
 105 
 106     private static final String[] non_xml_transforms = new String[] {
 107         null, Transform.BASE64
 108     };
 109 
 110     private static final String[] signatureMethods = new String[] {
 111         SignatureMethod.DSA_SHA1,
 112         SignatureMethod.RSA_SHA1,
 113         SignatureMethod.HMAC_SHA1
 114     };
 115 
 116     private static final String XSLT = ""
 117           + "<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform'\n"
 118           + "            xmlns='http://www.w3.org/TR/xhtml1/strict' \n"
 119           + "            exclude-result-prefixes='foo' \n"
 120           + "            version='1.0'>\n"
 121           + "  <xsl:output encoding='UTF-8' \n"
 122           + "           indent='no' \n"
 123           + "           method='xml' />\n"
 124           + "  <xsl:template match='/'>\n"
 125           + "    <html>Test</html>\n"
 126           + "  </xsl:template>\n"
 127           + "</xsl:stylesheet>\n";
 128 
 129     private static enum KeyInfoType {
 130         KeyValue, x509data, KeyName
 131     }
 132 
 133     private static enum Content {
 134         Xml, Text, Base64, NotExisitng
 135     }
 136 
 137     private static class Test {
 138 
 139         final String digestMethod = DigestMethod.SHA1;
 140         final String canonicalizationMethod;
 141         final String signatureMethod;
 142         final String transform;
 143         final KeyInfoType keyInfo;
 144         final Class expectedException;
 145         final boolean expectedFailure;
 146         final Content contentType;
 147 
 148         // The test stores a signed document here for further validation
 149         String signature;
 150 
 151         // The test stores a public key here fot further validation
 152         Key validationKey;
 153 
 154         Test(String canonicalizationMethod,  String signatueMethod,
 155                 String transform, KeyInfoType keyInfo, Content contentType,
 156                 boolean expectedFailure, Class expectedException) {
 157             this.canonicalizationMethod = canonicalizationMethod;
 158             this.signatureMethod = signatueMethod;
 159             this.transform = transform;
 160             this.keyInfo = keyInfo;
 161             this.expectedException = expectedException;
 162             this.expectedFailure = expectedFailure;
 163             this.contentType = contentType;
 164         }
 165 
 166         void print() {
 167             System.out.println("Test case:");
 168             System.out.println("    Canonicalization method: "
 169                     + canonicalizationMethod);
 170             System.out.println("    Signature method: "
 171                     + signatureMethod);
 172             System.out.println("    Transform: " + transform);
 173             System.out.println("    Digest method: " + digestMethod);
 174             System.out.println("    KeyInfoType: " + keyInfo);
 175             System.out.println("    Content type: " + contentType);
 176             System.out.println("    Expected failure: "
 177                     + (expectedFailure ? "yes" : "no"));
 178             System.out.println("    Expected exception: "
 179                     + (expectedException == null ?
 180                             "no" : expectedException.getName()));
 181         }
 182     }
 183 
 184     private static class PositiveTest extends Test {
 185 
 186         PositiveTest(String canonicalizationMethod,  String signatueMethod,
 187                 String transform, KeyInfoType keyInfo, Content contentType) {
 188             super(canonicalizationMethod, signatueMethod, transform, keyInfo,
 189                     contentType, false, null);
 190         }
 191     }
 192 
 193     private static class Http implements HttpHandler, AutoCloseable {
 194 
 195         private final HttpServer server;
 196 
 197         private Http(HttpServer server) {
 198             this.server = server;
 199         }
 200 
 201         public static Http createHttpServer() throws IOException {
 202             HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
 203             return new Http(server);
 204         }
 205 
 206         public void start() {
 207             server.createContext("/", this);
 208             server.start();
 209         }
 210 
 211         public void stop() {
 212             server.stop(0);
 213         }
 214 
 215         public int getPort() {
 216             return server.getAddress().getPort();
 217         }
 218 
 219         @Override
 220         public void handle(HttpExchange t) throws IOException {
 221             try {
 222                 String type;
 223                 String path = t.getRequestURI().getPath();
 224                 if (path.startsWith("/")) {
 225                     type = path.substring(1);
 226                 } else {
 227                     type = path;
 228                 }
 229 
 230                 String contentTypeHeader = "";
 231                 byte[] output = new byte[] {};
 232                 int code = 200;
 233                 Content testContentType = Content.valueOf(type);
 234                 switch (testContentType) {
 235                     case Base64:
 236                         contentTypeHeader = "application/octet-stream";
 237                         output = "VGVzdA==".getBytes();
 238                         break;
 239                     case Text:
 240                         contentTypeHeader = "text/plain";
 241                         output = "Text".getBytes();
 242                         break;
 243                     case Xml:
 244                         contentTypeHeader = "application/xml";
 245                         output = "<tag>test</tag>".getBytes();
 246                         break;
 247                     case NotExisitng:
 248                         code = 404;
 249                         break;
 250                     default:
 251                         throw new IOException("Unknown test content type");
 252                 }
 253 
 254                 t.getResponseHeaders().set("Content-Type", contentTypeHeader);
 255                 t.sendResponseHeaders(code, output.length);
 256                 t.getResponseBody().write(output);
 257             } catch (IOException e) {
 258                 System.out.println("Exception: " + e);
 259                 t.sendResponseHeaders(500, 0);
 260             }
 261             t.close();
 262         }
 263 
 264         @Override
 265         public void close() {
 266             stop();
 267         }
 268     }
 269 
 270     public static void main(String[] args) throws IOException {
 271         try (Http server = Http.createHttpServer()) {
 272             server.start();
 273 
 274             List<Test> tests = new ArrayList<>();
 275 
 276             // Tests for XML documents
 277             Arrays.stream(canonicalizationMethods).forEach(c ->
 278                 Arrays.stream(signatureMethods).forEach(s ->
 279                     Arrays.stream(xml_transforms).forEach(t ->
 280                         Arrays.stream(KeyInfoType.values()).forEach(k -> {
 281                             tests.add(new PositiveTest(c, s, t, k,
 282                                     Content.Xml));
 283                         }))));
 284 
 285             // Tests for text data with no transform
 286             Arrays.stream(canonicalizationMethods).forEach(c ->
 287                 Arrays.stream(signatureMethods).forEach(s ->
 288                     Arrays.stream(KeyInfoType.values()).forEach(k -> {
 289                         tests.add(new PositiveTest(c, s, null, k,
 290                                 Content.Text));
 291                     })));
 292 
 293             // Tests for base64 data
 294             Arrays.stream(canonicalizationMethods).forEach(c ->
 295                 Arrays.stream(signatureMethods).forEach(s ->
 296                     Arrays.stream(non_xml_transforms).forEach(t ->
 297                         Arrays.stream(KeyInfoType.values()).forEach(k -> {
 298                             tests.add(new PositiveTest(c, s, t, k,
 299                                     Content.Base64));
 300                         }))));
 301 
 302             // Negative tests
 303 
 304             // unknown CanonicalizationMethod
 305             tests.add(new Test(CanonicalizationMethod.EXCLUSIVE + BOGUS,
 306                     SignatureMethod.DSA_SHA1,
 307                     CanonicalizationMethod.INCLUSIVE,
 308                     KeyInfoType.KeyName, Content.Xml,
 309                     true, NoSuchAlgorithmException.class));
 310 
 311             // unknown SignatureMethod
 312             tests.add(new Test(CanonicalizationMethod.EXCLUSIVE,
 313                     SignatureMethod.DSA_SHA1 + BOGUS,
 314                     CanonicalizationMethod.INCLUSIVE,
 315                     KeyInfoType.KeyName, Content.Xml,
 316                     true, NoSuchAlgorithmException.class));
 317 
 318             // unknown Transform
 319             tests.add(new Test(CanonicalizationMethod.EXCLUSIVE,
 320                     SignatureMethod.DSA_SHA1,
 321                     CanonicalizationMethod.INCLUSIVE + BOGUS,
 322                     KeyInfoType.KeyName, Content.Xml,
 323                     true, NoSuchAlgorithmException.class));
 324 
 325             // no source document
 326             tests.add(new Test(CanonicalizationMethod.EXCLUSIVE,
 327                     SignatureMethod.DSA_SHA1,
 328                     CanonicalizationMethod.INCLUSIVE,
 329                     KeyInfoType.KeyName, Content.NotExisitng,
 330                     true, XMLSignatureException.class));
 331 
 332             // wrong transfrom for text data
 333             tests.add(new Test(CanonicalizationMethod.EXCLUSIVE,
 334                     SignatureMethod.DSA_SHA1,
 335                     CanonicalizationMethod.INCLUSIVE,
 336                     KeyInfoType.KeyName, Content.Text,
 337                     true, XMLSignatureException.class));
 338 
 339             boolean success = tests.stream().allMatch(
 340                     (test) -> runTest(test, server.getPort()));
 341 
 342             if (!success) {
 343                 throw new RuntimeException("Some test cases failed");
 344             }
 345 
 346             System.out.println("Test passed");
 347         }
 348 
 349     }
 350 
 351     static KeyPair getKeyPair(SignatureMethod sm)
 352             throws NoSuchAlgorithmException {
 353         KeyPairGenerator keygen;
 354         switch (sm.getAlgorithm()) {
 355             case SignatureMethod.DSA_SHA1:
 356                 keygen = KeyPairGenerator.getInstance("DSA");
 357                 break;
 358             case SignatureMethod.RSA_SHA1:
 359                 keygen = KeyPairGenerator.getInstance("RSA");
 360                 break;
 361             default:
 362                 throw new RuntimeException("Unsupported signature algorithm");
 363         }
 364 
 365         SecureRandom random = new SecureRandom();
 366         keygen.initialize(1024, random);
 367         return keygen.generateKeyPair();
 368     }
 369 
 370     static void createSignature(Test test, int port) throws IOException,
 371             NoSuchAlgorithmException, InvalidAlgorithmParameterException,
 372             KeyException, ParserConfigurationException, MarshalException,
 373             XMLSignatureException, TransformerConfigurationException,
 374             TransformerException, SAXException {
 375 
 376         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
 377         dbf.setNamespaceAware(true);
 378 
 379         XMLSignatureFactory fac = XMLSignatureFactory.getInstance();
 380 
 381         // Create SignedInfo
 382         DigestMethod dm = fac.newDigestMethod(test.digestMethod, null);
 383 
 384         List transformList = null;
 385         if (test.transform != null) {
 386             TransformParameterSpec params = null;
 387             switch (test.transform) {
 388                 case Transform.XPATH:
 389                     params = new XPathFilterParameterSpec("//.");
 390                     break;
 391                 case Transform.XPATH2:
 392                     params = new XPathFilter2ParameterSpec(
 393                             Collections.singletonList(new XPathType("//.",
 394                                     XPathType.Filter.INTERSECT)));
 395                     break;
 396                 case Transform.XSLT:
 397                     Element element = dbf.newDocumentBuilder()
 398                             .parse(new ByteArrayInputStream(XSLT.getBytes()))
 399                             .getDocumentElement();
 400                     DOMStructure stylesheet = new DOMStructure(element);
 401                     params = new XSLTTransformParameterSpec(stylesheet);
 402                     break;
 403             }
 404             transformList = Collections.singletonList(fac.newTransform(
 405                     test.transform, params));
 406         }
 407 
 408         String url = String.format("http://localhost:%d/%s", port,
 409                 test.contentType);
 410         List refs = Collections.singletonList(fac.newReference(url, dm,
 411                 transformList, null, null));
 412 
 413         CanonicalizationMethod cm = fac.newCanonicalizationMethod(
 414                 test.canonicalizationMethod, (C14NMethodParameterSpec) null);
 415 
 416         SignatureMethod sm = fac.newSignatureMethod(test.signatureMethod, null);
 417 
 418         // Generate keys and save a key for validaition in Test instance,
 419         // it will be used for signature validation
 420         Key signingKey;
 421         switch (test.signatureMethod) {
 422             case SignatureMethod.DSA_SHA1:
 423             case SignatureMethod.RSA_SHA1:
 424                 KeyPair kp = getKeyPair(sm);
 425                 test.validationKey = kp.getPublic();
 426                 signingKey = kp.getPrivate();
 427                 break;
 428             case SignatureMethod.HMAC_SHA1:
 429                 KeyGenerator kg = KeyGenerator.getInstance("HmacSHA1");
 430                 signingKey = kg.generateKey();
 431                 test.validationKey = signingKey;
 432                 break;
 433             default:
 434                 throw new RuntimeException("Unsupported signature algorithm");
 435         }
 436 
 437 
 438         SignedInfo si = fac.newSignedInfo(cm, sm, refs, null);
 439 
 440         // Create KeyInfo
 441         KeyInfoFactory kif = fac.getKeyInfoFactory();
 442         List list = null;
 443         if (test.keyInfo == KeyInfoType.KeyValue) {
 444             if (test.validationKey instanceof PublicKey) {
 445                 KeyValue kv = kif.newKeyValue((PublicKey) test.validationKey);
 446                 list = Collections.singletonList(kv);
 447             }
 448         } else if (test.keyInfo == KeyInfoType.x509data) {
 449             list = Collections.singletonList(
 450                     kif.newX509Data(Collections.singletonList("cn=Test")));
 451         } else if (test.keyInfo == KeyInfoType.KeyName) {
 452             list = Collections.singletonList(kif.newKeyName("Test"));
 453         } else {
 454             throw new RuntimeException("Unexpected KeyInfo: " + test.keyInfo);
 455         }
 456         KeyInfo ki = list != null ? kif.newKeyInfo(list) : null;
 457 
 458         // Create an empty doc for detached signature
 459         Document doc = dbf.newDocumentBuilder().newDocument();
 460         DOMSignContext xsc = new DOMSignContext(signingKey, doc);
 461 
 462         // Generate signature
 463         XMLSignature signature = fac.newXMLSignature(si, ki);
 464         signature.sign(xsc);
 465 
 466         // Save signature
 467         try (StringWriter writer = new StringWriter()) {
 468             TransformerFactory tf = TransformerFactory.newInstance();
 469             Transformer trans = tf.newTransformer();
 470             Node parent = xsc.getParent();
 471             trans.transform(new DOMSource(parent), new StreamResult(writer));
 472             test.signature = writer.toString();
 473         }
 474     }
 475 
 476     static boolean validateDsig(Test test) throws ParserConfigurationException,
 477             SAXException, IOException, MarshalException, XMLSignatureException {
 478 
 479         XMLSignatureFactory fac = XMLSignatureFactory.getInstance();
 480 
 481         // Load signature
 482         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
 483         dbf.setNamespaceAware(true);
 484         dbf.setValidating(false);
 485 
 486         Document doc;
 487         try (ByteArrayInputStream bis = new ByteArrayInputStream(
 488                 test.signature.getBytes())) {
 489             doc = dbf.newDocumentBuilder().parse(bis);
 490         }
 491 
 492         NodeList nodeLst = doc.getElementsByTagName("Signature");
 493         Node node = nodeLst.item(0);
 494         if (node == null) {
 495             throw new RuntimeException("Couldn't find Signature element");
 496         }
 497         if (!(node instanceof Element)) {
 498             throw new RuntimeException("Unexpected node type");
 499         }
 500         Element sig = (Element) node;
 501 
 502         // Validate signature
 503         DOMValidateContext vc = new DOMValidateContext(test.validationKey, sig);
 504         vc.setProperty("org.jcp.xml.dsig.secureValidation", Boolean.FALSE);
 505         XMLSignature signature = fac.unmarshalXMLSignature(vc);
 506 
 507         boolean success = signature.validate(vc);
 508         if (!success) {
 509             System.out.println("Core signature validation failed");
 510             return false;
 511         }
 512 
 513         success = signature.getSignatureValue().validate(vc);
 514         if (!success) {
 515             System.out.println("Cryptographic validation of signature failed");
 516             return false;
 517         }
 518 
 519         return true;
 520     }
 521 
 522     static boolean runTest(Test test, int port) {
 523         test.print();
 524         try {
 525             System.out.print("Sign ... ");
 526             createSignature(test, port);
 527             System.out.println("Done");
 528 
 529             System.out.print("Validate ... ");
 530             boolean success = validateDsig(test);
 531 
 532             if (success && test.expectedFailure) {
 533                 System.out.println("Signature validation unexpectedly passed");
 534                 return false;
 535             }
 536 
 537             if (!success && !test.expectedFailure) {
 538                 System.out.println("Signature validation unexpectedly failed");
 539                 return false;
 540             }
 541 
 542             System.out.println("Done");
 543 
 544             if (test.expectedException != null) {
 545                 System.out.println("Expected " + test.expectedException
 546                         + " not thrown");
 547                 return false;
 548             }
 549         } catch (Exception e) {
 550             if (test.expectedException == null
 551                     || !e.getClass().isAssignableFrom(test.expectedException)) {
 552                 System.out.println("Unexpected exception: " + e);
 553                 e.printStackTrace(System.out);
 554                 return false;
 555             }
 556 
 557             System.out.println("Expected exception: " + e);
 558         }
 559 
 560         System.out.println("Test case passed");
 561         return true;
 562     }
 563 
 564 }