FYI: another fix for openssl-style cert
--- a/src/share/classes/sun/security/provider/X509Factory.java Sat Dec 01 00:00:00 2007 +0000
+++ b/src/share/classes/sun/security/provider/X509Factory.java Tue Feb 03 11:28:31 2009 +0800
@@ -62,10 +62,6 @@
public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
public static final String END_CERT = "-----END CERTIFICATE-----";
- private static final int defaultExpectedLineLength = 80;
-
- private static final char[] endBoundary = "-----END".toCharArray();
-
private static final int ENC_MAX_LENGTH = 4096 * 1024; // 4 MB MAX
private static final Cache certCache = Cache.newSoftMemoryCache(750);
@@ -92,13 +88,7 @@
throw new CertificateException("Missing input stream");
}
try {
- if (is.markSupported() == false) {
- // consume the entire input stream
- byte[] totalBytes;
- totalBytes = getTotalBytes(new BufferedInputStream(is));
- is = new ByteArrayInputStream(totalBytes);
- }
- byte[] encoding = readSequence(is);
+ byte[] encoding = readOneBlock(is);
if (encoding != null) {
X509CertImpl cert = (X509CertImpl)getFromCache(certCache, encoding);
if (cert != null) {
@@ -108,94 +98,30 @@
addToCache(certCache, cert.getEncodedInternal(), cert);
return cert;
} else {
- X509CertImpl cert;
- // determine if binary or Base64 encoding. If Base64 encoding,
- // the certificate must be bounded at the beginning by
- // "-----BEGIN".
- if (isBase64(is)) {
- // Base64
- byte[] data = base64_to_binary(is);
- cert = new X509CertImpl(data);
- } else {
- // binary
- cert = new X509CertImpl(new DerValue(is));
- }
- return intern(cert);
+ throw new IOException("Empty input");
}
} catch (IOException ioe) {
- throw (CertificateException)new CertificateException
+ throw (CertificateException)new CertificateParsingException
("Could not parse certificate: " + ioe.toString()).initCause(ioe);
}
}
/**
- * Read a DER SEQUENCE from an InputStream and return the encoding.
- * If data does not represent a SEQUENCE, it uses indefinite length
- * encoding, or is longer than ENC_MAX_LENGTH, the stream is reset
- * and this method returns null.
- */
- private static byte[] readSequence(InputStream in) throws IOException {
- in.mark(ENC_MAX_LENGTH);
- byte[] b = new byte[4];
- int i = readFully(in, b, 0, b.length);
- if ((i != b.length) || (b[0] != 0x30)) { // first byte must be SEQUENCE
- in.reset();
- return null;
- }
- i = b[1] & 0xff;
- int totalLength;
- if (i < 0x80) {
- int valueLength = i;
- totalLength = valueLength + 2;
- } else if (i == 0x81) {
- int valueLength = b[2] & 0xff;
- totalLength = valueLength + 3;
- } else if (i == 0x82) {
- int valueLength = ((b[2] & 0xff) << 8) | (b[3] & 0xff);
- totalLength = valueLength + 4;
- } else { // ignore longer length forms
- in.reset();
- return null;
- }
- if (totalLength > ENC_MAX_LENGTH) {
- in.reset();
- return null;
- }
- byte[] encoding = new byte[totalLength];
- if( totalLength < b.length ) {
- in.reset();
- i = readFully(in, encoding, 0, totalLength);
- if( i != totalLength ) {
- in.reset();
- return null;
- }
- } else {
- System.arraycopy(b, 0, encoding, 0, b.length);
- int n = totalLength - b.length;
- i = readFully(in, encoding, b.length, n);
- if (i != n) {
- in.reset();
- return null;
- }
- }
- return encoding;
- }
-
- /**
* Read from the stream until length bytes have been read or EOF has
* been reached. Return the number of bytes actually read.
*/
- private static int readFully(InputStream in, byte[] buffer, int offset,
+ private static int readFully(InputStream in, ByteArrayOutputStream bout,
int length) throws IOException {
int read = 0;
+ byte[] buffer = new byte[length];
while (length > 0) {
- int n = in.read(buffer, offset, length);
+ int n = in.read(buffer, 0, length);
if (n <= 0) {
break;
}
+ bout.write(buffer, 0, n);
read += n;
length -= n;
- offset += n;
}
return read;
}
@@ -309,24 +235,14 @@
throw new CertificateException("Missing input stream");
}
try {
- if (inStream.markSupported() == false) {
- // consume the entire input stream
- byte[] totalBytes;
- totalBytes = getTotalBytes(new BufferedInputStream(inStream));
- inStream = new ByteArrayInputStream(totalBytes);
- }
- // determine if binary or Base64 encoding. If Base64 encoding,
- // each certificate must be bounded at the beginning by
- // "-----BEGIN".
- if (isBase64(inStream)) {
- // Base64
- byte[] data = base64_to_binary(inStream);
- return new X509CertPath(new ByteArrayInputStream(data));
+ byte[] encoding = readOneBlock(inStream);
+ if (encoding != null) {
+ return new X509CertPath(new ByteArrayInputStream(encoding));
} else {
- return new X509CertPath(inStream);
+ throw new IOException("Empty input");
}
} catch (IOException ioe) {
- throw new CertificateException(ioe.getMessage());
+ throw new CertificateParsingException(ioe.getMessage());
}
}
@@ -350,24 +266,14 @@
throw new CertificateException("Missing input stream");
}
try {
- if (inStream.markSupported() == false) {
- // consume the entire input stream
- byte[] totalBytes;
- totalBytes = getTotalBytes(new BufferedInputStream(inStream));
- inStream = new ByteArrayInputStream(totalBytes);
- }
- // determine if binary or Base64 encoding. If Base64 encoding,
- // each certificate must be bounded at the beginning by
- // "-----BEGIN".
- if (isBase64(inStream)) {
- // Base64
- byte[] data = base64_to_binary(inStream);
+ byte[] data = readOneBlock(inStream);
+ if (data != null) {
return new X509CertPath(new ByteArrayInputStream(data), encoding);
} else {
- return(new X509CertPath(inStream, encoding));
+ throw new IOException("Empty input");
}
} catch (IOException ioe) {
- throw new CertificateException(ioe.getMessage());
+ throw new CertificateParsingException(ioe.getMessage());
}
}
@@ -426,14 +332,9 @@
throw new CertificateException("Missing input stream");
}
try {
- if (is.markSupported() == false) {
- // consume the entire input stream
- is = new ByteArrayInputStream
- (getTotalBytes(new BufferedInputStream(is)));
- }
return parseX509orPKCS7Cert(is);
} catch (IOException ioe) {
- throw new CertificateException(ioe);
+ throw new CertificateParsingException(ioe);
}
}
@@ -458,13 +359,7 @@
throw new CRLException("Missing input stream");
}
try {
- if (is.markSupported() == false) {
- // consume the entire input stream
- byte[] totalBytes;
- totalBytes = getTotalBytes(new BufferedInputStream(is));
- is = new ByteArrayInputStream(totalBytes);
- }
- byte[] encoding = readSequence(is);
+ byte[] encoding = readOneBlock(is);
if (encoding != null) {
X509CRLImpl crl = (X509CRLImpl)getFromCache(crlCache, encoding);
if (crl != null) {
@@ -474,19 +369,7 @@
addToCache(crlCache, crl.getEncodedInternal(), crl);
return crl;
} else {
- X509CRLImpl crl;
- // determine if binary or Base64 encoding. If Base64 encoding,
- // the CRL must be bounded at the beginning by
- // "-----BEGIN".
- if (isBase64(is)) {
- // Base64
- byte[] data = base64_to_binary(is);
- crl = new X509CRLImpl(data);
- } else {
- // binary
- crl = new X509CRLImpl(new DerValue(is));
- }
- return intern(crl);
+ throw new IOException("Empty input");
}
} catch (IOException ioe) {
throw new CRLException(ioe.getMessage());
@@ -512,11 +395,6 @@
throw new CRLException("Missing input stream");
}
try {
- if (is.markSupported() == false) {
- // consume the entire input stream
- is = new ByteArrayInputStream
- (getTotalBytes(new BufferedInputStream(is)));
- }
return parseX509orPKCS7CRL(is);
} catch (IOException ioe) {
throw new CRLException(ioe.getMessage());
@@ -533,42 +411,28 @@
throws CertificateException, IOException
{
Collection<X509CertImpl> coll = new ArrayList<X509CertImpl>();
- boolean first = true;
- while (is.available() != 0) {
- // determine if binary or Base64 encoding. If Base64 encoding,
- // each certificate must be bounded at the beginning by
- // "-----BEGIN".
- InputStream is2 = is;
- if (isBase64(is2)) {
- // Base64
- is2 = new ByteArrayInputStream(base64_to_binary(is2));
- }
- if (first)
- is2.mark(is2.available());
+ List<byte[]> data = readBlocks(is, 0);
+ if (data.isEmpty()) {
+ throw new IOException("Empty input");
+ } else if (data.size() == 1) {
try {
- // treat as X.509 cert
- coll.add(intern(new X509CertImpl(new DerValue(is2))));
+ coll.add(new X509CertImpl(data.get(0)));
} catch (CertificateException e) {
- Throwable cause = e.getCause();
- // only treat as PKCS#7 if this is the first cert parsed
- // and the root cause of the decoding failure is an IOException
- if (first && cause != null && (cause instanceof IOException)) {
- // treat as PKCS#7
- is2.reset();
- PKCS7 pkcs7 = new PKCS7(is2);
- X509Certificate[] certs = pkcs7.getCertificates();
- // certs are optional in PKCS #7
- if (certs != null) {
- return Arrays.asList(certs);
- } else {
- // no certs provided
- return new ArrayList<X509Certificate>(0);
- }
+ PKCS7 pkcs7 = new PKCS7(data.get(0));
+
+ X509Certificate[] certs = pkcs7.getCertificates();
+ // certs are optional in PKCS #7
+ if (certs != null) {
+ return Arrays.asList(certs);
} else {
- throw e;
+ // no certs provided
+ return new ArrayList<X509Certificate>(0);
}
}
- first = false;
+ } else {
+ for (byte[] d: data) {
+ coll.add(new X509CertImpl(d));
+ }
}
return coll;
}
@@ -583,168 +447,298 @@
throws CRLException, IOException
{
Collection<X509CRLImpl> coll = new ArrayList<X509CRLImpl>();
- boolean first = true;
- while (is.available() != 0) {
- // determine if binary or Base64 encoding. If Base64 encoding,
- // the CRL must be bounded at the beginning by
- // "-----BEGIN".
- InputStream is2 = is;
- if (isBase64(is)) {
- // Base64
- is2 = new ByteArrayInputStream(base64_to_binary(is2));
- }
- if (first)
- is2.mark(is2.available());
+ List<byte[]> data = readBlocks(is, 0);
+ if (data.isEmpty()) {
+ throw new IOException("Empty input");
+ } else if (data.size() == 1) {
try {
- // treat as X.509 CRL
- coll.add(new X509CRLImpl(is2));
+ coll.add(new X509CRLImpl(data.get(0)));
} catch (CRLException e) {
- // only treat as PKCS#7 if this is the first CRL parsed
- if (first) {
- is2.reset();
- PKCS7 pkcs7 = new PKCS7(is2);
- X509CRL[] crls = pkcs7.getCRLs();
- // CRLs are optional in PKCS #7
- if (crls != null) {
- return Arrays.asList(crls);
- } else {
- // no crls provided
- return new ArrayList<X509CRL>(0);
- }
+ PKCS7 pkcs7 = new PKCS7(data.get(0));
+ X509CRL[] crls = pkcs7.getCRLs();
+ // CRLs are optional in PKCS #7
+ if (crls != null) {
+ return Arrays.asList(crls);
+ } else {
+ // no crls provided
+ return new ArrayList<X509CRL>(0);
}
}
- first = false;
+ } else {
+ for (byte[] d: data) {
+ coll.add(new X509CRLImpl(d));
+ }
}
return coll;
}
- /*
- * Converts a Base64-encoded X.509 certificate or X.509 CRL or PKCS#7 data
- * to binary encoding.
- * In all cases, the data must be bounded at the beginning by
- * "-----BEGIN", and must be bounded at the end by "-----END".
+ /**
+ * Returns a list of binary BER/DER-encoded data from the input stream,
+ * any PEM (ASCII) data is decoded before the return.
+ *
+ * Rules:
+ *
+ * INPUT := BLANK (BLOCK BLANK)+
+ * BLANK := Zero-Or-More-Consecutive-Blanks
+ * BLOCK := BER|PEM
+ * BER := 0x30 ... (until a single BER encoding is found)
+ * PEM := HEAD DATA
+ * HEAD := Zero-or-More-Chars-Before-1st-Appearance-of-5-Consecutive-Hyphens-And-Not-Started-With-0x30-Or-Blank
+ * DATA := "-----" Char-not-NewLine NewLine (BASE64-CHAR|Blank)* "-" Shortest-Char-Block-With-9-Hyphen
+ *
+ * This implies:
+ * 1. If there's something between 2 BER, it MUST be either all blanks or
+ * includes a PEM inside
+ * 2. The first "-----" inside a PEM block SHOULD be the real start of the
+ * BASE64 data. (Not exactly, see Corner Case 1 below)
+ * 3. "-----END XXX-----" does NOT ended with a NewLine. If there is a
+ * NewLine after it, it's counted as a part of BLANK
+ *
+ * OpenSSL outputs obey these rules, where the first char is never 0x30
+ * and BASE64 data is always at the end. Even if users didn't cut the text
+ * properly, there should be only extra blanks (or no NewLine) after the
+ * BASE64 data.
+ *
+ * Corner Cases:
+ *
+ * 1. A certificate in which issuer or owner's DName includes "-----".
+ *
+ * Workaround: After this false sign, it should be very possible to meet
+ * a non BASE64-char (: or , etc) before the real BASE64 data begins.
+ * We can exit the loop immediately and restart from this char.
+ *
+ * ToDo:
+ *
+ * 1. Need to check the word after "-----BEGIN" ?
+ * 2. Need to check the "-----END XXX-----" line precisely? (Currently, I
+ * only count 10 "-"s)
+ *
+ * @param is the InputStream
+ * @param limit How many data BLOCK to read at most, 0 is unlimited.
+ * @returns the list of byte blocks
+ * @throws IOException If any parsing error
*/
- private byte[] base64_to_binary(InputStream is)
- throws IOException
- {
- long len = 0; // total length of base64 encoding, including boundaries
+ private static List<byte[]> readBlocks(InputStream is, int limit) throws IOException {
- is.mark(is.available());
+ // Performance
+ /*if (!(is instanceof BufferedInputStream)
+ && !(is instanceof ByteArrayInputStream)) {
+ is = new BufferedInputStream(is);
+ }*/
- BufferedInputStream bufin = new BufferedInputStream(is);
- BufferedReader br =
- new BufferedReader(new InputStreamReader(bufin, "ASCII"));
+ // The list to be returned
+ List<byte[]> result = new ArrayList<byte[]>();
- // First read all of the data that is found between
- // the "-----BEGIN" and "-----END" boundaries into a buffer.
- String temp;
- if ((temp=readLine(br))==null || !temp.startsWith("-----BEGIN")) {
- throw new IOException("Unsupported encoding");
- } else {
- len += temp.length();
+ // If the first character of a BLOCK is available. Normally it's
+ // false. The only case when it is true is for Corner Case 1,
+ // when it's detected that the current position is not inside
+ // a real DATA section.
+ boolean xAvailable = false;
+
+ // The first character of a BLOCK.
+ int x = 0;
+
+ // The count of BLOCKs read.
+ int count = 0;
+
+ big: while (true) {
+
+ if (limit > 0 && count >= limit) {
+ break;
+ }
+
+ if (!xAvailable) {
+ while (true) { // Go thru BLANK
+ int c = is.read();
+ if (c == -1) {
+ break big;
+ } else if (c > ' ') {
+ x = c;
+ break;
+ }
+ }
+ }
+ xAvailable = false;
+
+ if (x == -1) {
+ break;
+ }
+
+ if (x == 0x30) {
+ // BER
+ result.add(readBER(is, 0x30));
+ count++;
+ } else {
+ // PEM
+ int prefix = (x == '-')?1:0;
+
+ while (prefix != 5) {
+ int c = is.read();
+ if (c == -1) {
+ throw new IOException("Cannot find a PEM block");
+ }
+ if (c == '-') {
+ prefix++;
+ } else {
+ prefix = 0;
+ }
+ }
+
+ // Go thru the -----BEGIN line
+ while (true) {
+ int c = is.read();
+ if (c == -1) {
+ throw new IOException("Invalid PEM header");
+ } else if (c == '\r' || c == '\n') {
+ break;
+ }
+ }
+
+ BASE64Decoder decoder = new BASE64Decoder();
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+
+ while (true) { // reading BASE64 block
+ int c = is.read();
+ if (c == -1) {
+ throw new IOException("Incomplete PEM data");
+ }
+ if (c == '-') { // end of BASE64
+ break;
+ } else if (c <= ' ') { // blanks, dismissed
+ ;
+ } else if (c == '+' || c == '/' || c == '='
+ || (c >= 'A' && c <= 'Z')
+ || (c >= 'a' && c <= 'z')
+ || (c >= '0' && c <= '9')){
+ bout.write(c);
+ } else {
+ // Used to throw new IOException("Invalid BASE64 Char")
+ // Workaround for Corner Case 1:
+ x = c;
+ xAvailable = true;
+ //System.err.println(">>>>>>>>> UGLY WORKAROUND");
+ // Since x is already not 0x30, this will go into
+ // PEM read again
+ continue big;
+ }
+ }
+
+ // Go thru the -----END line
+ int hyphen = 1;
+ while (hyphen != 10) {
+ int c = is.read();
+ if (c == -1) {
+ throw new IOException("Invalid PEM footer");
+ }
+ if (c == '-') {
+ hyphen++; // Lazy, Should be 5 at each end
+ }
+ }
+
+ result.add(decoder.decodeBuffer(
+ new String(bout.toByteArray())));
+ count++;
+
+ }
}
- StringBuffer strBuf = new StringBuffer();
- while ((temp=readLine(br))!=null && !temp.startsWith("-----END")) {
- strBuf.append(temp);
- }
- if (temp == null) {
- throw new IOException("Unsupported encoding");
- } else {
- len += temp.length();
- }
-
- // consume only as much as was needed
- len += strBuf.length();
- is.reset();
- is.skip(len);
-
- // Now, that data is supposed to be a single X.509 certificate or
- // X.509 CRL or PKCS#7 formatted data... Base64 encoded.
- // Decode into binary and return the result.
- BASE64Decoder decoder = new BASE64Decoder();
- return decoder.decodeBuffer(strBuf.toString());
+ return result;
}
- /*
- * Reads the entire input stream into a byte array.
+ /**
+ * Simple way of calling readBlocks with limit=1.
+ * @param is Read from this stream
+ * @returns the 1st encoding, or null if empty
+ * @throws IOException If any parsing error
*/
- private byte[] getTotalBytes(InputStream is) throws IOException {
- byte[] buffer = new byte[8192];
- ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);
- int n;
- baos.reset();
- while ((n = is.read(buffer, 0, buffer.length)) != -1) {
- baos.write(buffer, 0, n);
- }
- return baos.toByteArray();
- }
-
- /*
- * Determines if input is binary or Base64 encoded.
- */
- private boolean isBase64(InputStream is) throws IOException {
- if (is.available() >= 10) {
- is.mark(10);
- int c1 = is.read();
- int c2 = is.read();
- int c3 = is.read();
- int c4 = is.read();
- int c5 = is.read();
- int c6 = is.read();
- int c7 = is.read();
- int c8 = is.read();
- int c9 = is.read();
- int c10 = is.read();
- is.reset();
- if (c1 == '-' && c2 == '-' && c3 == '-' && c4 == '-'
- && c5 == '-' && c6 == 'B' && c7 == 'E' && c8 == 'G'
- && c9 == 'I' && c10 == 'N') {
- return true;
- } else {
- return false;
- }
+ private static byte[] readOneBlock(InputStream is) throws IOException {
+ List<byte[]> blocks = readBlocks(is, 1);
+ if (blocks.size() > 0) {
+ return blocks.get(0);
} else {
- return false;
+ return null;
}
}
- /*
- * Read a line of text. A line is considered to be terminated by any one
- * of a line feed ('\n'), a carriage return ('\r'), a carriage return
- * followed immediately by a linefeed, or an end-of-certificate marker.
+ /**
+ * Read a block of BER data. If the tag of the BER is already read outside this
+ * method, specify it as the tag argument. Otherwise, provide -1.
*
- * @return A String containing the contents of the line, including
- * any line-termination characters, or null if the end of the
- * stream has been reached.
+ * @param is Read from this InputStream
+ * @param tag Tag already read (-1 mean not read)
+ * @returns The data bytes
*/
- private String readLine(BufferedReader br) throws IOException {
- int c;
- int i = 0;
- boolean isMatch = true;
- boolean matched = false;
- StringBuffer sb = new StringBuffer(defaultExpectedLineLength);
- do {
- c = br.read();
- if (isMatch && (i < endBoundary.length)) {
- isMatch = ((char)c != endBoundary[i++]) ? false : true;
+ private static byte[] readBER(InputStream is, int tag) throws IOException {
+ // We're goinf to store the BER bytes inside a ByteArrayOutputStream. If
+ // the tag is already read outside the method, we still put it inside.
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ if (tag != -1) {
+ bout.write(tag);
+ }
+ readBERInternal(is, bout, tag);
+ return bout.toByteArray();
+ }
+
+ /**
+ * Read one BER data block. This method knows about indefinite-length BER
+ * encoding and will read all of the sub-sections in a recursive way
+ *
+ * @param is Read from this InputStream
+ * @param bout Write into this OutputStream
+ * @param tag Tag already read (-1 mean not read)
+ * @returns The current tag, used to detect EOC for indefinite-length encoding
+ * @throws IOException Any parsing error
+ */
+ private static int readBERInternal(InputStream is, ByteArrayOutputStream bout, int tag)
+ throws IOException {
+
+ if (tag == -1) { // Not read before the call, read now
+ tag = is.read();
+ if (tag == -1) {
+ throw new IOException("BER/DER tag info absent");
}
- if (!matched)
- matched = (isMatch && (i == endBoundary.length));
- sb.append((char)c);
- } while ((c != -1) && (c != '\n') && (c != '\r'));
+ bout.write(tag);
+ }
- if (!matched && c == -1) {
- return null;
+ int n = is.read();
+ if (n == -1) {
+ throw new IOException("BER/DER length info ansent");
}
- if (c == '\r') {
- br.mark(1);
- int c2 = br.read();
- if (c2 == '\n') {
- sb.append((char)c);
- } else {
- br.reset();
+ bout.write(n);
+
+ int length;
+
+ if (n == 0x80) { // Indefinite-length encoding
+ while (true) {
+ int subTag = readBERInternal(is, bout, -1);
+ if (subTag == 0) { // EOC, end of indefinite-length section
+ break;
+ }
}
+ } else {
+ if (n < 0x80) {
+ length = n;
+ } else if (n == 0x81) {
+ length = is.read();
+ if (length == -1) {
+ throw new IOException("Incomplete BER/DER length info");
+ }
+ bout.write(length);
+ } else if (n == 0x82) {
+ int highByte = is.read();
+ int lowByte = is.read();
+ if (lowByte == -1) {
+ throw new IOException("Incomplete BER/DER length info");
+ }
+ bout.write(highByte);
+ bout.write(lowByte);
+ length = (highByte << 8) | lowByte;
+ } else { // ignore longer length forms
+ throw new IOException("Invalid BER/DER data (too huge?)");
+ }
+ if (readFully(is, bout, length) != length) {
+ throw new IOException("Incomplete BER/DER data");
+ };
}
- return sb.toString();
+ return tag;
}
}