1 module hunt.security.x509.X509Factory; 2 3 import hunt.security.x509.X509CertificatePair; 4 import hunt.security.x509.X509CertImpl; 5 import hunt.security.x509.X509CRLImpl; 6 7 8 import hunt.security.cert.CertificateFactorySpi; 9 import hunt.security.cert.Certificate; 10 import hunt.security.cert.CertPath; 11 import hunt.security.cert.CRL; 12 import hunt.security.cert.X509CRL; 13 import hunt.security.cert.X509Certificate; 14 15 import hunt.security.util.Cache; 16 17 import hunt.collection; 18 import hunt.stream.Common; 19 20 import hunt.Exceptions; 21 22 23 /** 24 * This class defines a certificate factory for X.509 v3 certificates & 25 * certification paths, and X.509 v2 certificate revocation lists (CRLs). 26 * 27 * @author Jan Luehe 28 * @author Hemma Prafullchandra 29 * @author Sean Mullan 30 * 31 * 32 * @see java.security.cert.CertificateFactorySpi 33 * @see java.security.cert.Certificate 34 * @see java.security.cert.CertPath 35 * @see java.security.cert.CRL 36 * @see java.security.cert.X509Certificate 37 * @see java.security.cert.X509CRL 38 * @see sun.security.x509.X509CertImpl 39 * @see sun.security.x509.X509CRLImpl 40 */ 41 42 class X509Factory : CertificateFactorySpi { 43 44 enum string BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; 45 enum string END_CERT = "-----END CERTIFICATE-----"; 46 47 private enum int ENC_MAX_LENGTH = 4096 * 1024; // 4 MB MAX 48 49 private static Cache!(Object, X509CertImpl) certCache; 50 private static Cache!(Object, X509CRLImpl) crlCache; 51 52 static this() 53 { 54 certCache = newSoftMemoryCache!(Object, X509CertImpl)(750); 55 crlCache = newSoftMemoryCache!(Object, X509CRLImpl)(750); 56 } 57 58 /** 59 * Generates an X.509 certificate object and initializes it with 60 * the data read from the input stream <code>inputStream</code>. 61 * 62 * @param inputStream an input stream with the certificate data. 63 * 64 * @return an X.509 certificate object initialized with the data 65 * from the input stream. 66 * 67 * @exception CertificateException on parsing errors. 68 */ 69 override 70 Certificate engineGenerateCertificate(InputStream inputStream) 71 { 72 if (inputStream is null) { 73 // clear the caches (for debugging) 74 certCache.clear(); 75 X509CertificatePair.clearCache(); 76 throw new CertificateException("Missing input stream"); 77 } 78 try { 79 byte[] encoding = readOneBlock(inputStream); 80 if (encoding !is null) { 81 X509CertImpl cert = getFromCache(certCache, encoding); 82 if (cert !is null) { 83 return cert; 84 } 85 cert = new X509CertImpl(encoding); 86 addToCache(certCache, cert.getEncodedInternal(), cert); 87 return cert; 88 } else { 89 throw new IOException("Empty input"); 90 } 91 } catch (IOException ioe) { 92 throw new CertificateException("Could not parse certificate: " ~ 93 ioe.toString(), ioe); 94 } 95 } 96 97 // /** 98 // * Read from the stream until length bytes have been read or EOF has 99 // * been reached. Return the number of bytes actually read. 100 // */ 101 // private static int readFully(InputStream inputStream, ByteArrayOutputStream bout, 102 // int length) { 103 // int read = 0; 104 // byte[] buffer = new byte[2048]; 105 // while (length > 0) { 106 // int n = inputStream.read(buffer, 0, length<2048?length:2048); 107 // if (n <= 0) { 108 // break; 109 // } 110 // bout.write(buffer, 0, n); 111 // read += n; 112 // length -= n; 113 // } 114 // return read; 115 // } 116 117 /** 118 * Return an interned X509CertImpl for the given certificate. 119 * If the given X509Certificate or X509CertImpl is already present 120 * in the cert cache, the cached object is returned. Otherwise, 121 * if it is a X509Certificate, it is first converted to a X509CertImpl. 122 * Then the X509CertImpl is added to the cache and returned. 123 * 124 * Note that all certificates created via generateCertificate(InputStream) 125 * are already interned and this method does not need to be called. 126 * It is useful for certificates that cannot be created via 127 * generateCertificate() and for converting other X509Certificate 128 * implementations to an X509CertImpl. 129 * 130 * @param c The source X509Certificate 131 * @return An X509CertImpl object that is either a cached certificate or a 132 * newly built X509CertImpl from the provided X509Certificate 133 * @ if failures occur while obtaining the DER 134 * encoding for certificate data. 135 */ 136 static X509CertImpl intern(X509Certificate c) { 137 implementationMissing(); 138 return null; 139 // if (c is null) { 140 // return null; 141 // } 142 // bool isImpl = c instanceof X509CertImpl; 143 // byte[] encoding; 144 // if (isImpl) { 145 // encoding = ((X509CertImpl)c).getEncodedInternal(); 146 // } else { 147 // encoding = c.getEncoded(); 148 // } 149 // X509CertImpl newC = getFromCache(certCache, encoding); 150 // if (newC !is null) { 151 // return newC; 152 // } 153 // if (isImpl) { 154 // newC = (X509CertImpl)c; 155 // } else { 156 // newC = new X509CertImpl(encoding); 157 // encoding = newC.getEncodedInternal(); 158 // } 159 // addToCache(certCache, encoding, newC); 160 // return newC; 161 } 162 163 /** 164 * Return an interned X509CRLImpl for the given certificate. 165 * For more information, see intern(X509Certificate). 166 * 167 * @param c The source X509CRL 168 * @return An X509CRLImpl object that is either a cached CRL or a 169 * newly built X509CRLImpl from the provided X509CRL 170 * @throws CRLException if failures occur while obtaining the DER 171 * encoding for CRL data. 172 */ 173 static X509CRLImpl intern(X509CRL c) { 174 if (c is null) { 175 return null; 176 } 177 implementationMissing(); 178 return null; 179 180 // X509CRLImpl cc = cast(X509CRLImpl)c; 181 // bool isImpl = cc !is null; 182 // byte[] encoding; 183 // if (isImpl) { 184 // encoding = cc.getEncodedInternal(); 185 // } else { 186 // encoding = c.getEncoded(); 187 // } 188 189 // X509CRLImpl newC = getFromCache(crlCache, encoding); 190 // if (newC !is null) { 191 // return newC; 192 // } 193 // if (isImpl) { 194 // newC = cc; 195 // } else { 196 // newC = new X509CRLImpl(encoding); 197 // encoding = newC.getEncodedInternal(); 198 // } 199 // addToCache(crlCache, encoding, newC); 200 // return newC; 201 } 202 203 /** 204 * Get the X509CertImpl or X509CRLImpl from the cache. 205 */ 206 private static V getFromCache(K,V)(Cache!(K, V) cache, 207 byte[] encoding) { 208 Object key = new EqualByteArray(encoding); 209 return cache.get(key); 210 } 211 212 /** 213 * Add the X509CertImpl or X509CRLImpl to the cache. 214 */ 215 private static void addToCache(V)(Cache!(Object, V) cache, 216 byte[] encoding, V value) { 217 if (encoding.length > ENC_MAX_LENGTH) { 218 return; 219 } 220 Object key = new EqualByteArray(encoding); 221 cache.put(key, value); 222 } 223 224 // /** 225 // * Generates a <code>CertPath</code> object and initializes it with 226 // * the data read from the <code>InputStream</code> inStream. The data 227 // * is assumed to be in the default encoding. 228 // * 229 // * @param inStream an <code>InputStream</code> containing the data 230 // * @return a <code>CertPath</code> initialized with the data from the 231 // * <code>InputStream</code> 232 // * @exception CertificateException if an exception occurs while decoding 233 // * @since 1.4 234 // */ 235 // override 236 // CertPath engineGenerateCertPath(InputStream inStream) 237 238 // { 239 // if (inStream is null) { 240 // throw new CertificateException("Missing input stream"); 241 // } 242 // try { 243 // byte[] encoding = readOneBlock(inStream); 244 // if (encoding !is null) { 245 // return new X509CertPath(new ByteArrayInputStream(encoding)); 246 // } else { 247 // throw new IOException("Empty input"); 248 // } 249 // } catch (IOException ioe) { 250 // throw new CertificateException(ioe.msg); 251 // } 252 // } 253 254 // /** 255 // * Generates a <code>CertPath</code> object and initializes it with 256 // * the data read from the <code>InputStream</code> inStream. The data 257 // * is assumed to be in the specified encoding. 258 // * 259 // * @param inStream an <code>InputStream</code> containing the data 260 // * @param encoding the encoding used for the data 261 // * @return a <code>CertPath</code> initialized with the data from the 262 // * <code>InputStream</code> 263 // * @exception CertificateException if an exception occurs while decoding or 264 // * the encoding requested is not supported 265 // * @since 1.4 266 // */ 267 // override 268 // CertPath engineGenerateCertPath(InputStream inStream, 269 // string encoding) 270 // { 271 // if (inStream is null) { 272 // throw new CertificateException("Missing input stream"); 273 // } 274 // try { 275 // byte[] data = readOneBlock(inStream); 276 // if (data !is null) { 277 // return new X509CertPath(new ByteArrayInputStream(data), encoding); 278 // } else { 279 // throw new IOException("Empty input"); 280 // } 281 // } catch (IOException ioe) { 282 // throw new CertificateException(ioe.msg); 283 // } 284 // } 285 286 // /** 287 // * Generates a <code>CertPath</code> object and initializes it with 288 // * a <code>List</code> of <code>Certificate</code>s. 289 // * <p> 290 // * The certificates supplied must be of a type supported by the 291 // * <code>CertificateFactory</code>. They will be copied out of the supplied 292 // * <code>List</code> object. 293 // * 294 // * @param certificates a <code>List</code> of <code>Certificate</code>s 295 // * @return a <code>CertPath</code> initialized with the supplied list of 296 // * certificates 297 // * @exception CertificateException if an exception occurs 298 // * @since 1.4 299 // */ 300 // override 301 // CertPath 302 // engineGenerateCertPath(List<? : Certificate> certificates) 303 304 // { 305 // return(new X509CertPath(certificates)); 306 // } 307 308 // /** 309 // * Returns an iteration of the <code>CertPath</code> encodings supported 310 // * by this certificate factory, with the default encoding first. 311 // * <p> 312 // * Attempts to modify the returned <code>Iterator</code> via its 313 // * <code>remove</code> method result in an 314 // * <code>UnsupportedOperationException</code>. 315 // * 316 // * @return an <code>Iterator</code> over the names of the supported 317 // * <code>CertPath</code> encodings (as <code>string</code>s) 318 // * @since 1.4 319 // */ 320 // override 321 // Iterator!string engineGetCertPathEncodings() { 322 // return(X509CertPath.getEncodingsStatic()); 323 // } 324 325 /** 326 * Returns a (possibly empty) collection view of X.509 certificates read 327 * from the given input stream <code>is</code>. 328 * 329 * @param stream the input stream with the certificates. 330 * 331 * @return a (possibly empty) collection view of X.509 certificate objects 332 * initialized with the data from the input stream. 333 * 334 * @exception CertificateException on parsing errors. 335 */ 336 override 337 Collection!(Certificate) engineGenerateCertificates(InputStream stream) { 338 if (stream is null) { 339 throw new CertificateException("Missing input stream"); 340 } 341 try { 342 return parseX509orPKCS7Cert(stream); 343 } catch (IOException ioe) { 344 throw new CertificateException("", ioe); 345 } 346 } 347 348 /** 349 * Generates an X.509 certificate revocation list (CRL) object and 350 * initializes it with the data read from the given input stream 351 * <code>is</code>. 352 * 353 * @param is an input stream with the CRL data. 354 * 355 * @return an X.509 CRL object initialized with the data 356 * from the input stream. 357 * 358 * @exception CRLException on parsing errors. 359 */ 360 override CRL engineGenerateCRL(InputStream stream) 361 { 362 if (stream is null) { 363 // clear the cache (for debugging) 364 crlCache.clear(); 365 throw new CRLException("Missing input stream"); 366 } 367 try { 368 // byte[] encoding = readOneBlock(stream); 369 // if (encoding !is null) { 370 // X509CRLImpl crl = getFromCache(crlCache, encoding); 371 // if (crl !is null) { 372 // return crl; 373 // } 374 // crl = new X509CRLImpl(encoding); 375 // addToCache(crlCache, crl.getEncodedInternal(), crl); 376 // return crl; 377 // } else { 378 // throw new IOException("Empty input"); 379 // } 380 implementationMissing(); 381 return null; 382 } catch (IOException ioe) { 383 throw new CRLException(ioe.msg); 384 } 385 } 386 387 /** 388 * Returns a (possibly empty) collection view of X.509 CRLs read 389 * from the given input stream <code>is</code>. 390 * 391 * @param stream the input stream with the CRLs. 392 * 393 * @return a (possibly empty) collection view of X.509 CRL objects 394 * initialized with the data from the input stream. 395 * 396 * @exception CRLException on parsing errors. 397 */ 398 override Collection!(CRL) engineGenerateCRLs(InputStream stream) { 399 if (stream is null) { 400 throw new CRLException("Missing input stream"); 401 } 402 try { 403 // return parseX509orPKCS7CRL(stream); 404 implementationMissing(); 405 return null; 406 } catch (IOException ioe) { 407 throw new CRLException(ioe.msg); 408 } 409 } 410 411 /* 412 * Parses the data in the given input stream as a sequence of DER 413 * encoded X.509 certificates (in binary or base 64 encoded format) OR 414 * as a single PKCS#7 encoded blob (in binary or base64 encoded format). 415 */ 416 private Collection!(Certificate) parseX509orPKCS7Cert(InputStream stream) { 417 int peekByte; 418 byte[] data; 419 // PushbackInputStream pbis = new PushbackInputStream(stream); 420 Collection!X509CertImpl coll = new ArrayList!X509CertImpl(); 421 422 // // Test the InputStream for end-of-stream. If the stream's 423 // // initial state is already at end-of-stream then return 424 // // an empty collection. Otherwise, push the byte back into the 425 // // stream and let readOneBlock look for the first certificate. 426 // peekByte = pbis.read(); 427 // if (peekByte == -1) { 428 // return new ArrayList!Certificate(0); 429 // } else { 430 // pbis.unread(peekByte); 431 // data = readOneBlock(pbis); 432 // } 433 434 // // If we end up with a null value after reading the first block 435 // // then we know the end-of-stream has been reached and no certificate 436 // // data has been found. 437 // if (data is null) { 438 // throw new CertificateException("No certificate data found"); 439 // } 440 441 // try { 442 // PKCS7 pkcs7 = new PKCS7(data); 443 // X509Certificate[] certs = pkcs7.getCertificates(); 444 // // certs are optional in PKCS #7 445 // if (certs !is null) { 446 // return Arrays.asList(certs); 447 // } else { 448 // // no certificates provided 449 // return new ArrayList<>(0); 450 // } 451 // } catch (ParsingException e) { 452 // while (data !is null) { 453 // coll.add(new X509CertImpl(data)); 454 // data = readOneBlock(pbis); 455 // } 456 // } 457 // return coll; 458 implementationMissing(); 459 return null; 460 } 461 462 // /* 463 // * Parses the data in the given input stream as a sequence of DER encoded 464 // * X.509 CRLs (in binary or base 64 encoded format) OR as a single PKCS#7 465 // * encoded blob (in binary or base 64 encoded format). 466 // */ 467 // private Collection<? : java.security.cert.CRL> 468 // parseX509orPKCS7CRL(InputStream is) 469 // , IOException 470 // { 471 // int peekByte; 472 // byte[] data; 473 // PushbackInputStream pbis = new PushbackInputStream(is); 474 // Collection<X509CRLImpl> coll = new ArrayList<>(); 475 476 // // Test the InputStream for end-of-stream. If the stream's 477 // // initial state is already at end-of-stream then return 478 // // an empty collection. Otherwise, push the byte back into the 479 // // stream and let readOneBlock look for the first CRL. 480 // peekByte = pbis.read(); 481 // if (peekByte == -1) { 482 // return new ArrayList<>(0); 483 // } else { 484 // pbis.unread(peekByte); 485 // data = readOneBlock(pbis); 486 // } 487 488 // // If we end up with a null value after reading the first block 489 // // then we know the end-of-stream has been reached and no CRL 490 // // data has been found. 491 // if (data is null) { 492 // throw new CRLException("No CRL data found"); 493 // } 494 495 // try { 496 // PKCS7 pkcs7 = new PKCS7(data); 497 // X509CRL[] crls = pkcs7.getCRLs(); 498 // // CRLs are optional in PKCS #7 499 // if (crls !is null) { 500 // return Arrays.asList(crls); 501 // } else { 502 // // no crls provided 503 // return new ArrayList<>(0); 504 // } 505 // } catch (ParsingException e) { 506 // while (data !is null) { 507 // coll.add(new X509CRLImpl(data)); 508 // data = readOneBlock(pbis); 509 // } 510 // } 511 // return coll; 512 // } 513 514 /** 515 * Returns an ASN.1 SEQUENCE from a stream, which might be a BER-encoded 516 * binary block or a PEM-style BASE64-encoded ASCII data. In the latter 517 * case, it's de-BASE64'ed before return. 518 * 519 * After the reading, the input stream pointer is after the BER block, or 520 * after the newline character after the -----END SOMETHING----- line. 521 * 522 * @param is the InputStream 523 * @returns byte block or null if end of stream 524 * @If any parsing error 525 */ 526 private static byte[] readOneBlock(InputStream stream) { 527 implementationMissing(false); 528 return null; 529 // The first character of a BLOCK. 530 // int c = stream.read(); 531 // if (c == -1) { 532 // return null; 533 // } 534 // if (c == DerValue.tag_Sequence) { 535 // ByteArrayOutputStream bout = new ByteArrayOutputStream(2048); 536 // bout.write(c); 537 // readBERInternal(stream, bout, c); 538 // return bout.toByteArray(); 539 // } else { 540 // // Read BASE64 encoded data, might skip info at the beginning 541 // char[] data = new char[2048]; 542 // int pos = 0; 543 544 // // Step 1: Read until header is found 545 // int hyphen = (c=='-') ? 1: 0; // count of consequent hyphens 546 // int last = (c=='-') ? -1: c; // the char before hyphen 547 // while (true) { 548 // int next = stream.read(); 549 // if (next == -1) { 550 // // We accept useless data after the last block, 551 // // say, empty lines. 552 // return null; 553 // } 554 // if (next == '-') { 555 // hyphen++; 556 // } else { 557 // hyphen = 0; 558 // last = next; 559 // } 560 // if (hyphen == 5 && (last == -1 || last == '\r' || last == '\n')) { 561 // break; 562 // } 563 // } 564 565 // // Step 2: Read the rest of header, determine the line end 566 // int end; 567 // StringBuilder header = new StringBuilder("-----"); 568 // while (true) { 569 // int next = stream.read(); 570 // if (next == -1) { 571 // throw new IOException("Incomplete data"); 572 // } 573 // if (next == '\n') { 574 // end = '\n'; 575 // break; 576 // } 577 // if (next == '\r') { 578 // next = stream.read(); 579 // if (next == -1) { 580 // throw new IOException("Incomplete data"); 581 // } 582 // if (next == '\n') { 583 // end = '\n'; 584 // } else { 585 // end = '\r'; 586 // data[pos++] = (char)next; 587 // } 588 // break; 589 // } 590 // header.append((char)next); 591 // } 592 593 // // Step 3: Read the data 594 // while (true) { 595 // int next = stream.read(); 596 // if (next == -1) { 597 // throw new IOException("Incomplete data"); 598 // } 599 // if (next != '-') { 600 // data[pos++] = (char)next; 601 // if (pos >= data.length) { 602 // data = Arrays.copyOf(data, data.length+1024); 603 // } 604 // } else { 605 // break; 606 // } 607 // } 608 609 // // Step 4: Consume the footer 610 // StringBuilder footer = new StringBuilder("-"); 611 // while (true) { 612 // int next = stream.read(); 613 // // Add next == '\n' for maximum safety, in case endline 614 // // is not consistent. 615 // if (next == -1 || next == end || next == '\n') { 616 // break; 617 // } 618 // if (next != '\r') footer.append((char)next); 619 // } 620 621 // checkHeaderFooter(header.toString(), footer.toString()); 622 623 // return Pem.decode(new string(data, 0, pos)); 624 // } 625 } 626 627 // private static void checkHeaderFooter(string header, 628 // string footer) { 629 // if (header.length() < 16 || !header.startsWith("-----BEGIN ") || 630 // !header.endsWith("-----")) { 631 // throw new IOException("Illegal header: " ~ header); 632 // } 633 // if (footer.length() < 14 || !footer.startsWith("-----END ") || 634 // !footer.endsWith("-----")) { 635 // throw new IOException("Illegal footer: " ~ footer); 636 // } 637 // string headerType = header.substring(11, header.length()-5); 638 // string footerType = footer.substring(9, footer.length()-5); 639 // if (!headerType.equals(footerType)) { 640 // throw new IOException("Header and footer do not match: " ~ 641 // header ~ " " ~ footer); 642 // } 643 // } 644 645 // /** 646 // * Read one BER data block. This method is aware of indefinite-length BER 647 // * encoding and will read all of the sub-sections in a recursive way 648 // * 649 // * @param stream Read from this InputStream 650 // * @param bout Write into this OutputStream 651 // * @param tag Tag already read (-1 mean not read) 652 // * @returns The current tag, used to check EOC in indefinite-length BER 653 // * @Any parsing error 654 // */ 655 // private static int readBERInternal(InputStream stream, 656 // ByteArrayOutputStream bout, int tag) { 657 658 // if (tag == -1) { // Not read before the call, read now 659 // tag = stream.read(); 660 // if (tag == -1) { 661 // throw new IOException("BER/DER tag info absent"); 662 // } 663 // if ((tag & 0x1f) == 0x1f) { 664 // throw new IOException("Multi octets tag not supported"); 665 // } 666 // bout.write(tag); 667 // } 668 669 // int n = stream.read(); 670 // if (n == -1) { 671 // throw new IOException("BER/DER length info absent"); 672 // } 673 // bout.write(n); 674 675 // int length; 676 677 // if (n == 0x80) { // Indefinite-length encoding 678 // if ((tag & 0x20) != 0x20) { 679 // throw new IOException( 680 // "Non constructed encoding must have definite length"); 681 // } 682 // while (true) { 683 // int subTag = readBERInternal(stream, bout, -1); 684 // if (subTag == 0) { // EOC, end of indefinite-length section 685 // break; 686 // } 687 // } 688 // } else { 689 // if (n < 0x80) { 690 // length = n; 691 // } else if (n == 0x81) { 692 // length = stream.read(); 693 // if (length == -1) { 694 // throw new IOException("Incomplete BER/DER length info"); 695 // } 696 // bout.write(length); 697 // } else if (n == 0x82) { 698 // int highByte = stream.read(); 699 // int lowByte = stream.read(); 700 // if (lowByte == -1) { 701 // throw new IOException("Incomplete BER/DER length info"); 702 // } 703 // bout.write(highByte); 704 // bout.write(lowByte); 705 // length = (highByte << 8) | lowByte; 706 // } else if (n == 0x83) { 707 // int highByte = stream.read(); 708 // int midByte = stream.read(); 709 // int lowByte = stream.read(); 710 // if (lowByte == -1) { 711 // throw new IOException("Incomplete BER/DER length info"); 712 // } 713 // bout.write(highByte); 714 // bout.write(midByte); 715 // bout.write(lowByte); 716 // length = (highByte << 16) | (midByte << 8) | lowByte; 717 // } else if (n == 0x84) { 718 // int highByte = stream.read(); 719 // int nextByte = stream.read(); 720 // int midByte = stream.read(); 721 // int lowByte = stream.read(); 722 // if (lowByte == -1) { 723 // throw new IOException("Incomplete BER/DER length info"); 724 // } 725 // if (highByte > 127) { 726 // throw new IOException("Invalid BER/DER data (a little huge?)"); 727 // } 728 // bout.write(highByte); 729 // bout.write(nextByte); 730 // bout.write(midByte); 731 // bout.write(lowByte); 732 // length = (highByte << 24 ) | (nextByte << 16) | 733 // (midByte << 8) | lowByte; 734 // } else { // ignore longer length forms 735 // throw new IOException("Invalid BER/DER data (too huge?)"); 736 // } 737 // if (readFully(stream, bout, length) != length) { 738 // throw new IOException("Incomplete BER/DER data"); 739 // } 740 // } 741 // return tag; 742 // } 743 }