/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @author Alexander Y. Kleymenov * @version $Revision$ */ package org.apache.harmony.security.provider.cert; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charsets; import java.security.cert.CRL; import java.security.cert.CRLException; import java.security.cert.CertPath; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactorySpi; import java.security.cert.X509CRL; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import libcore.io.Base64; import libcore.io.Streams; import org.apache.harmony.security.asn1.ASN1Constants; import org.apache.harmony.security.asn1.BerInputStream; import org.apache.harmony.security.pkcs7.ContentInfo; import org.apache.harmony.security.pkcs7.SignedData; import org.apache.harmony.security.x509.CertificateList; /** * X509 Certificate Factory Service Provider Interface Implementation. * It supports CRLs and Certificates in (PEM) ASN.1 DER encoded form, * and Certification Paths in PkiPath and PKCS7 formats. * For Certificates and CRLs factory maintains the caching * mechanisms allowing to speed up repeated Certificate/CRL * generation. * @see Cache */ public class X509CertFactoryImpl extends CertificateFactorySpi { // number of leading/trailing bytes used for cert hash computation private static final int CERT_CACHE_SEED_LENGTH = 28; // certificate cache private static final Cache CERT_CACHE = new Cache(CERT_CACHE_SEED_LENGTH); // number of leading/trailing bytes used for crl hash computation private static final int CRL_CACHE_SEED_LENGTH = 24; // crl cache private static final Cache CRL_CACHE = new Cache(CRL_CACHE_SEED_LENGTH); /** * Default constructor. * Creates the instance of Certificate Factory SPI ready for use. */ public X509CertFactoryImpl() { } /** * Generates the X.509 certificate from the data in the stream. * The data in the stream can be either in ASN.1 DER encoded X.509 * certificate, or PEM (Base64 encoding bounded by * <code>"-----BEGIN CERTIFICATE-----"</code> at the beginning and * <code>"-----END CERTIFICATE-----"</code> at the end) representation * of the former encoded form. * * Before the generation the encoded form is looked up in * the cache. If the cache contains the certificate with requested encoded * form it is returned from it, otherwise it is generated by ASN.1 * decoder. * * @see java.security.cert.CertificateFactorySpi#engineGenerateCertificate(InputStream) * method documentation for more info */ public Certificate engineGenerateCertificate(InputStream inStream) throws CertificateException { if (inStream == null) { throw new CertificateException("inStream == null"); } try { if (!inStream.markSupported()) { // create the mark supporting wrapper inStream = new RestoringInputStream(inStream); } // mark is needed to recognize the format of the provided encoding // (ASN.1 or PEM) inStream.mark(1); // check whether the provided certificate is in PEM encoded form if (inStream.read() == '-') { // decode PEM, retrieve CRL return getCertificate(decodePEM(inStream, CERT_BOUND_SUFFIX)); } else { inStream.reset(); // retrieve CRL return getCertificate(inStream); } } catch (IOException e) { throw new CertificateException(e); } } /** * Generates the collection of the certificates on the base of provided * via input stream encodings. * @see java.security.cert.CertificateFactorySpi#engineGenerateCertificates(InputStream) * method documentation for more info */ public Collection<? extends Certificate> engineGenerateCertificates(InputStream inStream) throws CertificateException { if (inStream == null) { throw new CertificateException("inStream == null"); } ArrayList<Certificate> result = new ArrayList<Certificate>(); try { if (!inStream.markSupported()) { // create the mark supporting wrapper inStream = new RestoringInputStream(inStream); } // if it is PEM encoded form this array will contain the encoding // so ((it is PEM) <-> (encoding != null)) byte[] encoding = null; // The following by SEQUENCE ASN.1 tag, used for // recognizing the data format // (is it PKCS7 ContentInfo structure, X.509 Certificate, or // unsupported encoding) int second_asn1_tag = -1; inStream.mark(1); int ch; while ((ch = inStream.read()) != -1) { // check if it is PEM encoded form if (ch == '-') { // beginning of PEM encoding ('-' char) // decode PEM chunk and store its content (ASN.1 encoding) encoding = decodePEM(inStream, FREE_BOUND_SUFFIX); } else if (ch == 0x30) { // beginning of ASN.1 sequence (0x30) encoding = null; inStream.reset(); // prepare for data format determination inStream.mark(CERT_CACHE_SEED_LENGTH); } else { // unsupported data if (result.size() == 0) { throw new CertificateException("Unsupported encoding"); } else { // it can be trailing user data, // so keep it in the stream inStream.reset(); return result; } } // Check the data format BerInputStream in = (encoding == null) ? new BerInputStream(inStream) : new BerInputStream(encoding); // read the next ASN.1 tag second_asn1_tag = in.next(); // inStream position changed if (encoding == null) { // keep whole structure in the stream inStream.reset(); } // check if it is a TBSCertificate structure if (second_asn1_tag != ASN1Constants.TAG_C_SEQUENCE) { if (result.size() == 0) { // there were not read X.509 Certificates, so // break the cycle and check // whether it is PKCS7 structure break; } else { // it can be trailing user data, // so return what we already read return result; } } else { if (encoding == null) { result.add(getCertificate(inStream)); } else { result.add(getCertificate(encoding)); } } // mark for the next iteration inStream.mark(1); } if (result.size() != 0) { // some Certificates have been read return result; } else if (ch == -1) { throw new CertificateException("There is no data in the stream"); } // else: check if it is PKCS7 if (second_asn1_tag == ASN1Constants.TAG_OID) { // it is PKCS7 ContentInfo structure, so decode it ContentInfo info = (ContentInfo) ((encoding != null) ? ContentInfo.ASN1.decode(encoding) : ContentInfo.ASN1.decode(inStream)); // retrieve SignedData SignedData data = info.getSignedData(); if (data == null) { throw new CertificateException("Invalid PKCS7 data provided"); } List<org.apache.harmony.security.x509.Certificate> certs = data.getCertificates(); if (certs != null) { for (org.apache.harmony.security.x509.Certificate cert : certs) { result.add(new X509CertImpl(cert)); } } return result; } // else: Unknown data format throw new CertificateException("Unsupported encoding"); } catch (IOException e) { throw new CertificateException(e); } } /** * @see java.security.cert.CertificateFactorySpi#engineGenerateCRL(InputStream) * method documentation for more info */ public CRL engineGenerateCRL(InputStream inStream) throws CRLException { if (inStream == null) { throw new CRLException("inStream == null"); } try { if (!inStream.markSupported()) { // Create the mark supporting wrapper // Mark is needed to recognize the format // of provided encoding form (ASN.1 or PEM) inStream = new RestoringInputStream(inStream); } inStream.mark(1); // check whether the provided crl is in PEM encoded form if (inStream.read() == '-') { // decode PEM, retrieve CRL return getCRL(decodePEM(inStream, FREE_BOUND_SUFFIX)); } else { inStream.reset(); // retrieve CRL return getCRL(inStream); } } catch (IOException e) { throw new CRLException(e); } } /** * @see java.security.cert.CertificateFactorySpi#engineGenerateCRLs(InputStream) * method documentation for more info */ public Collection<? extends CRL> engineGenerateCRLs(InputStream inStream) throws CRLException { if (inStream == null) { throw new CRLException("inStream == null"); } ArrayList<CRL> result = new ArrayList<CRL>(); try { if (!inStream.markSupported()) { inStream = new RestoringInputStream(inStream); } // if it is PEM encoded form this array will contain the encoding // so ((it is PEM) <-> (encoding != null)) byte[] encoding = null; // The following by SEQUENCE ASN.1 tag, used for // recognizing the data format // (is it PKCS7 ContentInfo structure, X.509 CRL, or // unsupported encoding) int second_asn1_tag = -1; inStream.mark(1); int ch; while ((ch = inStream.read()) != -1) { // check if it is PEM encoded form if (ch == '-') { // beginning of PEM encoding ('-' char) // decode PEM chunk and store its content (ASN.1 encoding) encoding = decodePEM(inStream, FREE_BOUND_SUFFIX); } else if (ch == 0x30) { // beginning of ASN.1 sequence (0x30) encoding = null; inStream.reset(); // prepare for data format determination inStream.mark(CRL_CACHE_SEED_LENGTH); } else { // unsupported data if (result.size() == 0) { throw new CRLException("Unsupported encoding"); } else { // it can be trailing user data, // so keep it in the stream inStream.reset(); return result; } } // Check the data format BerInputStream in = (encoding == null) ? new BerInputStream(inStream) : new BerInputStream(encoding); // read the next ASN.1 tag second_asn1_tag = in.next(); if (encoding == null) { // keep whole structure in the stream inStream.reset(); } // check if it is a TBSCertList structure if (second_asn1_tag != ASN1Constants.TAG_C_SEQUENCE) { if (result.size() == 0) { // there were not read X.509 CRLs, so // break the cycle and check // whether it is PKCS7 structure break; } else { // it can be trailing user data, // so return what we already read return result; } } else { if (encoding == null) { result.add(getCRL(inStream)); } else { result.add(getCRL(encoding)); } } inStream.mark(1); } if (result.size() != 0) { // the stream was read out return result; } else if (ch == -1) { throw new CRLException("There is no data in the stream"); } // else: check if it is PKCS7 if (second_asn1_tag == ASN1Constants.TAG_OID) { // it is PKCS7 ContentInfo structure, so decode it ContentInfo info = (ContentInfo) ((encoding != null) ? ContentInfo.ASN1.decode(encoding) : ContentInfo.ASN1.decode(inStream)); // retrieve SignedData SignedData data = info.getSignedData(); if (data == null) { throw new CRLException("Invalid PKCS7 data provided"); } List<CertificateList> crls = data.getCRLs(); if (crls != null) { for (CertificateList crl : crls) { result.add(new X509CRLImpl(crl)); } } return result; } // else: Unknown data format throw new CRLException("Unsupported encoding"); } catch (IOException e) { throw new CRLException(e); } } /** * @see java.security.cert.CertificateFactorySpi#engineGenerateCertPath(InputStream) * method documentation for more info */ public CertPath engineGenerateCertPath(InputStream inStream) throws CertificateException { if (inStream == null) { throw new CertificateException("inStream == null"); } return engineGenerateCertPath(inStream, "PkiPath"); } /** * @see java.security.cert.CertificateFactorySpi#engineGenerateCertPath(InputStream,String) * method documentation for more info */ public CertPath engineGenerateCertPath( InputStream inStream, String encoding) throws CertificateException { if (inStream == null) { throw new CertificateException("inStream == null"); } if (!inStream.markSupported()) { inStream = new RestoringInputStream(inStream); } try { inStream.mark(1); int ch; // check if it is PEM encoded form if ((ch = inStream.read()) == '-') { // decode PEM chunk into ASN.1 form and decode CertPath object return X509CertPathImpl.getInstance( decodePEM(inStream, FREE_BOUND_SUFFIX), encoding); } else if (ch == 0x30) { // ASN.1 Sequence inStream.reset(); // decode ASN.1 form return X509CertPathImpl.getInstance(inStream, encoding); } else { throw new CertificateException("Unsupported encoding"); } } catch (IOException e) { throw new CertificateException(e); } } /** * @see java.security.cert.CertificateFactorySpi#engineGenerateCertPath(List) * method documentation for more info */ public CertPath engineGenerateCertPath(List certificates) throws CertificateException { return new X509CertPathImpl(certificates); } /** * @see java.security.cert.CertificateFactorySpi#engineGetCertPathEncodings() * method documentation for more info */ public Iterator<String> engineGetCertPathEncodings() { return X509CertPathImpl.encodings.iterator(); } // --------------------------------------------------------------------- // ------------------------ Staff methods ------------------------------ // --------------------------------------------------------------------- private static final byte[] PEM_BEGIN = "-----BEGIN".getBytes(Charsets.UTF_8); private static final byte[] PEM_END = "-----END".getBytes(Charsets.UTF_8); /** * Code describing free format for PEM boundary suffix: * "^-----BEGIN.*\n" at the beginning, and<br> * "\n-----END.*(EOF|\n)$" at the end. */ private static final byte[] FREE_BOUND_SUFFIX = null; /** * Code describing PEM boundary suffix for X.509 certificate: * "^-----BEGIN CERTIFICATE-----\n" at the beginning, and<br> * "\n-----END CERTIFICATE-----" at the end. */ private static final byte[] CERT_BOUND_SUFFIX = " CERTIFICATE-----".getBytes(Charsets.UTF_8); /** * Method retrieves the PEM encoded data from the stream * and returns its decoded representation. * Method checks correctness of PEM boundaries. It supposes that * the first '-' of the opening boundary has already been read from * the stream. So first of all it checks that the leading bytes * are equal to "-----BEGIN" boundary prefix. Than if boundary_suffix * is not null, it checks that next bytes equal to boundary_suffix * + new line char[s] ([CR]LF). * If boundary_suffix parameter is null, method supposes free suffix * format and skips any bytes until the new line.<br> * After the opening boundary has been read and checked, the method * read Base64 encoded data until closing PEM boundary is not reached.<br> * Than it checks closing boundary - it should start with new line + * "-----END" + boundary_suffix. If boundary_suffix is null, * any characters are skipped until the new line.<br> * After this any trailing new line characters are skipped from the stream, * Base64 encoding is decoded and returned. * @param inStream the stream containing the PEM encoding. * @param boundary_suffix the suffix of expected PEM multipart * boundary delimiter.<br> * If it is null, that any character sequences are accepted. * @throws IOException If PEM boundary delimiter does not comply * with expected or some I/O or decoding problems occur. */ private byte[] decodePEM(InputStream inStream, byte[] boundary_suffix) throws IOException { int ch; // the char to be read // check and skip opening boundary delimiter // (first '-' is supposed as already read) for (int i = 1; i < PEM_BEGIN.length; ++i) { if (PEM_BEGIN[i] != (ch = inStream.read())) { throw new IOException( "Incorrect PEM encoding: '-----BEGIN" + ((boundary_suffix == null) ? "" : new String(boundary_suffix)) + "' is expected as opening delimiter boundary."); } } if (boundary_suffix == null) { // read (skip) the trailing characters of // the beginning PEM boundary delimiter while ((ch = inStream.read()) != '\n') { if (ch == -1) { throw new IOException("Incorrect PEM encoding: EOF before content"); } } } else { for (int i=0; i<boundary_suffix.length; i++) { if (boundary_suffix[i] != inStream.read()) { throw new IOException("Incorrect PEM encoding: '-----BEGIN" + new String(boundary_suffix) + "' is expected as opening delimiter boundary."); } } // read new line characters if ((ch = inStream.read()) == '\r') { // CR has been read, now read LF character ch = inStream.read(); } if (ch != '\n') { throw new IOException("Incorrect PEM encoding: newline expected after " + "opening delimiter boundary"); } } int size = 1024; // the size of the buffer containing Base64 data byte[] buff = new byte[size]; int index = 0; // read bytes while ending boundary delimiter is not reached while ((ch = inStream.read()) != '-') { if (ch == -1) { throw new IOException("Incorrect Base64 encoding: EOF without closing delimiter"); } buff[index++] = (byte) ch; if (index == size) { // enlarge the buffer byte[] newbuff = new byte[size+1024]; System.arraycopy(buff, 0, newbuff, 0, size); buff = newbuff; size += 1024; } } if (buff[index-1] != '\n') { throw new IOException("Incorrect Base64 encoding: newline expected before " + "closing boundary delimiter"); } // check and skip closing boundary delimiter prefix // (first '-' was read) for (int i = 1; i < PEM_END.length; ++i) { if (PEM_END[i] != inStream.read()) { throw badEnd(boundary_suffix); } } if (boundary_suffix == null) { // read (skip) the trailing characters of // the closing PEM boundary delimiter while (((ch = inStream.read()) != -1) && (ch != '\n') && (ch != '\r')) { } } else { for (int i=0; i<boundary_suffix.length; i++) { if (boundary_suffix[i] != inStream.read()) { throw badEnd(boundary_suffix); } } } // skip trailing line breaks inStream.mark(1); while (((ch = inStream.read()) != -1) && (ch == '\n' || ch == '\r')) { inStream.mark(1); } inStream.reset(); buff = Base64.decode(buff, index); if (buff == null) { throw new IOException("Incorrect Base64 encoding"); } return buff; } private IOException badEnd(byte[] boundary_suffix) throws IOException { String s = (boundary_suffix == null) ? "" : new String(boundary_suffix); throw new IOException("Incorrect PEM encoding: '-----END" + s + "' is expected as closing delimiter boundary."); } /** * Reads the data of specified length from source * and returns it as an array. * @return the byte array contained read data or * null if the stream contains not enough data * @throws IOException if some I/O error has been occurred. */ private static byte[] readBytes(InputStream source, int length) throws IOException { byte[] result = new byte[length]; for (int i=0; i<length; i++) { int bytik = source.read(); if (bytik == -1) { return null; } result[i] = (byte) bytik; } return result; } /** * Returns the Certificate object corresponding to the provided encoding. * Resulting object is retrieved from the cache * if it contains such correspondence * and is constructed on the base of encoding * and stored in the cache otherwise. * @throws IOException if some decoding errors occur * (in the case of cache miss). */ private static Certificate getCertificate(byte[] encoding) throws CertificateException, IOException { if (encoding.length < CERT_CACHE_SEED_LENGTH) { throw new CertificateException("encoding.length < CERT_CACHE_SEED_LENGTH"); } synchronized (CERT_CACHE) { long hash = CERT_CACHE.getHash(encoding); if (CERT_CACHE.contains(hash)) { Certificate res = (Certificate) CERT_CACHE.get(hash, encoding); if (res != null) { return res; } } Certificate res = new X509CertImpl(encoding); CERT_CACHE.put(hash, encoding, res); return res; } } /** * Returns the Certificate object corresponding to the encoding provided * by the stream. * Resulting object is retrieved from the cache * if it contains such correspondence * and is constructed on the base of encoding * and stored in the cache otherwise. * @throws IOException if some decoding errors occur * (in the case of cache miss). */ private static Certificate getCertificate(InputStream inStream) throws CertificateException, IOException { synchronized (CERT_CACHE) { inStream.mark(CERT_CACHE_SEED_LENGTH); // read the prefix of the encoding byte[] buff = readBytes(inStream, CERT_CACHE_SEED_LENGTH); inStream.reset(); if (buff == null) { throw new CertificateException("InputStream doesn't contain enough data"); } long hash = CERT_CACHE.getHash(buff); if (CERT_CACHE.contains(hash)) { byte[] encoding = new byte[BerInputStream.getLength(buff)]; if (encoding.length < CERT_CACHE_SEED_LENGTH) { throw new CertificateException("Bad Certificate encoding"); } Streams.readFully(inStream, encoding); Certificate res = (Certificate) CERT_CACHE.get(hash, encoding); if (res != null) { return res; } res = new X509CertImpl(encoding); CERT_CACHE.put(hash, encoding, res); return res; } else { inStream.reset(); Certificate res = new X509CertImpl(inStream); CERT_CACHE.put(hash, res.getEncoded(), res); return res; } } } /** * Returns the CRL object corresponding to the provided encoding. * Resulting object is retrieved from the cache * if it contains such correspondence * and is constructed on the base of encoding * and stored in the cache otherwise. * @throws IOException if some decoding errors occur * (in the case of cache miss). */ private static CRL getCRL(byte[] encoding) throws CRLException, IOException { if (encoding.length < CRL_CACHE_SEED_LENGTH) { throw new CRLException("encoding.length < CRL_CACHE_SEED_LENGTH"); } synchronized (CRL_CACHE) { long hash = CRL_CACHE.getHash(encoding); if (CRL_CACHE.contains(hash)) { X509CRL res = (X509CRL) CRL_CACHE.get(hash, encoding); if (res != null) { return res; } } X509CRL res = new X509CRLImpl(encoding); CRL_CACHE.put(hash, encoding, res); return res; } } /** * Returns the CRL object corresponding to the encoding provided * by the stream. * Resulting object is retrieved from the cache * if it contains such correspondence * and is constructed on the base of encoding * and stored in the cache otherwise. * @throws IOException if some decoding errors occur * (in the case of cache miss). */ private static CRL getCRL(InputStream inStream) throws CRLException, IOException { synchronized (CRL_CACHE) { inStream.mark(CRL_CACHE_SEED_LENGTH); byte[] buff = readBytes(inStream, CRL_CACHE_SEED_LENGTH); // read the prefix of the encoding inStream.reset(); if (buff == null) { throw new CRLException("InputStream doesn't contain enough data"); } long hash = CRL_CACHE.getHash(buff); if (CRL_CACHE.contains(hash)) { byte[] encoding = new byte[BerInputStream.getLength(buff)]; if (encoding.length < CRL_CACHE_SEED_LENGTH) { throw new CRLException("Bad CRL encoding"); } Streams.readFully(inStream, encoding); CRL res = (CRL) CRL_CACHE.get(hash, encoding); if (res != null) { return res; } res = new X509CRLImpl(encoding); CRL_CACHE.put(hash, encoding, res); return res; } else { X509CRL res = new X509CRLImpl(inStream); CRL_CACHE.put(hash, res.getEncoded(), res); return res; } } } /* * This class extends any existing input stream with * mark functionality. It acts as a wrapper over the * stream and supports reset to the * marked state with readlimit no more than BUFF_SIZE. */ private static class RestoringInputStream extends InputStream { // wrapped input stream private final InputStream inStream; // specifies how much of the read data is buffered // after the mark has been set up private static final int BUFF_SIZE = 32; // buffer to keep the bytes read after the mark has been set up private final int[] buff = new int[BUFF_SIZE*2]; // position of the next byte to read, // the value of -1 indicates that the buffer is not used // (mark was not set up or was invalidated, or reset to the marked // position has been done and all the buffered data was read out) private int pos = -1; // position of the last buffered byte private int bar = 0; // position in the buffer where the mark becomes invalidated private int end = 0; /** * Creates the mark supporting wrapper over the stream. */ public RestoringInputStream(InputStream inStream) { this.inStream = inStream; } @Override public int available() throws IOException { return (bar - pos) + inStream.available(); } @Override public void close() throws IOException { inStream.close(); } @Override public void mark(int readlimit) { if (pos < 0) { pos = 0; bar = 0; end = BUFF_SIZE - 1; } else { end = (pos + BUFF_SIZE - 1) % BUFF_SIZE; } } @Override public boolean markSupported() { return true; } /** * Reads the byte from the stream. If mark has been set up * and was not invalidated byte is read from the underlying * stream and saved into the buffer. If the current read position * has been reset to the marked position and there are remaining * bytes in the buffer, the byte is taken from it. In the other cases * (if mark has been invalidated, or there are no buffered bytes) * the byte is taken directly from the underlying stream and it is * returned without saving to the buffer. * * @see java.io.InputStream#read() * method documentation for more info */ public int read() throws IOException { // if buffer is currently used if (pos >= 0) { // current position in the buffer int cur = pos % BUFF_SIZE; // check whether the buffer contains the data to be read if (cur < bar) { // return the data from the buffer pos++; return buff[cur]; } // check whether buffer has free space if (cur != end) { // it has, so read the data from the wrapped stream // and place it in the buffer buff[cur] = inStream.read(); bar = cur+1; pos++; return buff[cur]; } else { // buffer if full and can not operate // any more, so invalidate the mark position // and turn off the using of buffer pos = -1; } } // buffer is not used, so return the data from the wrapped stream return inStream.read(); } @Override public int read(byte[] b, int off, int len) throws IOException { int read_b; int i; for (i=0; i<len; i++) { if ((read_b = read()) == -1) { return (i == 0) ? -1 : i; } b[off+i] = (byte) read_b; } return i; } @Override public void reset() throws IOException { if (pos >= 0) { pos = (end + 1) % BUFF_SIZE; } else { throw new IOException("Could not reset the stream: " + "position became invalid or stream has not been marked"); } } } }