/**
* Copyright (c) 2009 - 2012 Red Hat, Inc.
*
* This software is licensed to you under the GNU General Public License,
* version 2 (GPLv2). There is NO WARRANTY for this software, express or
* implied, including the implied warranties of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
* along with this software; if not, see
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
*
* Red Hat trademarks are not licensed under GPLv2. No permission is
* granted to use or replicate Red Hat trademarks that are incorporated
* in this software or its documentation.
*/
package org.candlepin.util;
import static org.bouncycastle.asn1.DERTags.*;
import static org.candlepin.util.DERUtil.*;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x509.TBSCertList.CRLEntry;
import org.bouncycastle.jce.provider.X509CRLEntryObject;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Reads an X509 CRL in a stream and returns the serial number for a revoked certificate
* with each call to the iterator's next() function.
*
* The schema for an X509 CRL is described in
* <a href="https://tools.ietf.org/html/rfc5280#section-5">section 5 of RFC 5280</a>
*
* It is reproduced here for quick reference
*
* <pre>
* {@code
* CertificateList ::= SEQUENCE {
* tbsCertList TBSCertList,
* signatureAlgorithm AlgorithmIdentifier,
* signatureValue BIT STRING }
*
* TBSCertList ::= SEQUENCE {
* version Version OPTIONAL,
* -- if present, MUST be v2
* signature AlgorithmIdentifier,
* issuer Name,
* thisUpdate Time,
* nextUpdate Time OPTIONAL,
* revokedCertificates SEQUENCE OF SEQUENCE {
* userCertificate CertificateSerialNumber,
* revocationDate Time,
* crlEntryExtensions Extensions OPTIONAL
* -- if present, version MUST be v2
* } OPTIONAL,
* crlExtensions [0] EXPLICIT Extensions OPTIONAL
* -- if present, version MUST be v2
* }
*
* Version, Time, CertificateSerialNumber, and Extensions
* are all defined in the ASN.1 in Section 4.1
*
* AlgorithmIdentifier is defined in Section 4.1.1.2
* }
* </pre>
*
* ASN1 is based around the TLV (tag, length, value) concept. Any piece of
* data begins with a tag defining the data type, has a series of bytes to
* indicate the data length, and then the data itself.
*
* <b>This class does not perform any signature checking</b>. Checking the signature is
* theoretically possible but we would be unable to check it until after the list of
* entries had been exhausted (since each entry would have to be sent through the hasher). At
* that point, the user already has all the serial numbers. The best approach would perhaps
* be to check the signature in hasNext() once revokedSeqBytes hits zero and throw a
* RuntimeException. We would also need a RSA public key as a parameter to the constructor.
*
* See https://en.wikipedia.org/wiki/X.690 and http://luca.ntop.org/Teaching/Appunti/asn1.html
* for reference on ASN1 and DER encoding.
*/
public class X509CRLEntryStream implements Closeable, Iterator<X509CRLEntryObject> {
private InputStream crlStream;
private int revokedSeqBytes;
private AtomicInteger count;
/**
* Construct a X509CRLStream. <b>The underlying data in the stream parameter must
* be in DER format</b>. PEM format will not work because we need to operate
* on the raw ASN1 of the DER. Use Apache Common's Base64InputStream with the X509
* header and footers stripped off if you need to use a PEM file.
*
* @param stream
* @throws IOException if we can't read the provided File
*/
public X509CRLEntryStream(InputStream stream) throws IOException {
crlStream = stream;
revokedSeqBytes = discardHeader(crlStream);
count = new AtomicInteger();
}
/**
* Construct a X509CRLStream. <b>The crlFile parameter must be in DER format</b>.
* PEM format will not work because we need to operate on the raw ASN1 of the DER.
*
* @param crlFile
* @throws IOException if we can't read the provided File
*/
public X509CRLEntryStream(File crlFile) throws IOException {
this(new BufferedInputStream(new FileInputStream(crlFile)));
}
@Override
public void remove() {
throw new UnsupportedOperationException("Remove is not implemented.");
}
/**
* Strip off the CRL meta-data and drill down to the sequence containing the
* revokedCertificates objects.
*
* @return the length in bytes of the revokedCertificates sequence
* @throws IOException
*/
protected int discardHeader(InputStream s) throws IOException {
// Strip the tag and length of the CertificateList sequence
int tag = readTag(s, count);
readTagNumber(s, tag, count);
readLength(s, count);
// At this point we are at the tag for the TBSCertList sequence and we need to
// strip off the tag and length
tag = readTag(s, count);
readTagNumber(s, tag, count);
readLength(s, count);
// Now we are actually at the values within the TBSCertList sequence.
// Read the CRL metadata and trash it. We get to the thisUpdate item
// and then break out.
int tagNo = NULL;
while (true) {
tag = readTag(s, count);
tagNo = readTagNumber(s, tag, count);
int length = readLength(s, count);
byte[] item = new byte[length];
readFullyAndTrack(s, item, count);
if (tagNo == GENERALIZED_TIME || tagNo == UTC_TIME) {
break;
}
}
tag = readTag(s, count);
tagNo = readTagNumber(s, tag, count);
// The nextUpdate item is optional. If it's there, we trash it.
if (tagNo == GENERALIZED_TIME || tagNo == UTC_TIME) {
int length = readLength(s, count);
byte[] item = new byte[length];
readFullyAndTrack(s, item, count);
tag = readTag(s, count);
tagNo = readTagNumber(s, tag, count);
}
if (tagNo != SEQUENCE) {
// If we aren't at a sequence, then the CRL is empty and we are either
// at the crlExtensions or if there are no extensions, at the
// signatureAlgorithm
readLength(s, count);
return 0;
}
// Return the length of the revokedCertificates sequence. We need to
// track the bytes we read and read no more than this length to prevent
// decoding errors.
return readLength(s, count);
}
public X509CRLEntryObject next() {
try {
// Strip the tag for the revokedCertificate entry
int tag = readTag(crlStream, count);
int tagNo = readTagNumber(crlStream, tag, count);
if (tagNo == OBJECT_IDENTIFIER) {
// If our tag is an OID, it means we're in an empty CRL with no
// extensions. We could potentially detect this by looking at the upcoming
// tag in hasNext(), but that screws up the stream for X509CRLStreamWriter because
// it leaves the stream in the middle of a TLV.
throw new IllegalStateException("v1 CRLs with zero entries are unsupported." +
" Please use a v2 CRL.");
}
int entryLength = readLength(crlStream, count);
byte[] entry = new byte[entryLength];
readFullyAndTrack(crlStream, entry, count);
ByteArrayOutputStream reconstructed = new ByteArrayOutputStream();
// An ASN1 SEQUENCE tag is 0x30
reconstructed.write(0x30);
writeLength(reconstructed, entryLength);
reconstructed.write(entry);
/* NB: This BouncyCastle method is a bit slow. If we just read the serial number
* alone out of the sequence, we can loop through 2 million entries in 500 ms.
* Using this method takes around 2300 ms. But we need the entire
* X509CRLEntryObject for the X509CRLStreamWriter, so we're kind of stuck
* with it.
*/
DERSequence obj = (DERSequence) DERSequence.fromByteArray(reconstructed.toByteArray());
reconstructed.close();
CRLEntry crlEntry = new CRLEntry(obj);
return new X509CRLEntryObject(crlEntry);
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
public boolean hasNext() {
return revokedSeqBytes > count.get();
}
@Override
public void close() throws IOException {
crlStream.close();
}
}