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 }