/**
* 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 org.bouncycastle.crypto.Signer;
import org.bouncycastle.util.io.Streams;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Collection of utilities for working on DER encoded ASN1.
*/
public class DERUtil {
private DERUtil() {
// No instances allowed
}
/**
* When the length of an ASN1 encoded value changes then the number of bytes representing
* the length of the value can change as well. We need to calculate that change because
* those bytes will be accounted for in any container object.
*
* @param before
* @param after
* @return the number of bytes the length header changes
* @throws IOException if the streams cannot be written to.
*/
public static int findHeaderBytesDelta(int before, int after)
throws IOException {
ByteArrayOutputStream oldTbsLengthStream = new ByteArrayOutputStream();
ByteArrayOutputStream newTbsLengthStream = new ByteArrayOutputStream();
try {
writeLength(oldTbsLengthStream, before, null);
writeLength(newTbsLengthStream, after, null);
return newTbsLengthStream.toByteArray().length - oldTbsLengthStream.toByteArray().length;
}
finally {
oldTbsLengthStream.close();
newTbsLengthStream.close();
}
}
/**
* Read exactly the number of bytes equal to the length of the bytes parameter and
* increase the counter accordingly.
*
* @param s the stream to read from
* @param bytes the byte array to fill
* @param count the counter to modify. Can be null.
* @throws IOException if the stream cannot provide the number of required bytes
*/
public static void readFullyAndTrack(InputStream s, byte[] bytes,
AtomicInteger count) throws IOException {
if (Streams.readFully(s, bytes) != bytes.length) {
throw new EOFException("EOF encountered in middle of object");
}
if (count != null) {
count.addAndGet(bytes.length);
}
}
/**
* Read a single byte and increment the counter.
*
* @param s the stream to read from
* @param count the counter to increment. Can be null.
* @return an integer representing the byte read.
* @throws IOException if something goes wrong
*/
public static int readAndTrack(InputStream s, AtomicInteger count) throws IOException {
int i = s.read();
if (count != null) {
count.incrementAndGet();
}
return i;
}
/**
* The tag is the first byte of an ASN1 TLV group. The tag specifies the
* data type. A tag can span multiple bytes but we have to examine the first
* byte to determine if it does.
*
* This code is a slightly adapted version of the code in BouncyCastle's
* ASN1InputStream.
*
* @param s an InputStream to read
* @param count the counter to modify. Can be null.
* @return an integer representing the first byte of the tag
* @throws IOException if something goes wrong
*/
public static int readTag(InputStream s, AtomicInteger count) throws IOException {
int tag = readAndTrack(s, count);
if (tag <= 0) {
if (tag == 0) {
throw new IOException("unexpected end-of-contents marker");
}
throw new IOException("negative tag value");
}
return tag;
}
/**
* Read the tag value out of the tag byte and/or consume extra bytes
* if the tag spans multiple octets.
*
* A tag is a single byte with the first 2 bits representing
* the tag class (Universal, Application, etc) and the third bit
* representing if the tag is primitive or constructed (meaning it
* holds other tags within it). The last 5 bits determine the data
* type. If the tag value is greater than 30, it won't fit in 5 bits
* and the value 0b11111 is reserved to indicate that. The tag is then
* encoded in subsequent octets.
*
* See https://en.wikipedia.org/wiki/X.690#Identifier_octets
*
* This code is a slightly adapted version of the code in BouncyCastle's
* ASN1InputStream.
*
* @param s an InputStream to read
* @param tag the first byte of the tag
* @param count the counter to modify. Can be null.
* @return an integer representing the entire tag value
* @throws IOException if something goes wrong
*/
public static int readTagNumber(InputStream s, int tag, AtomicInteger count) throws IOException {
int tagNo = tag & 0x1f;
// with tagged object tag number is bottom 5 bits, or stored at the start of the content
if (tagNo == 0x1f) {
/* During CRL operations, we should never actually enter this block.
* A tagNo == 0x1f means that the tag is not a universal type and a CRL
* shouldn't include private, context-specific, or application-specific
* tags.
*/
tagNo = 0;
int b = readAndTrack(s, count);
// X.690-0207 8.1.2.4.2
// "c) bits 7 to 1 of the first subsequent octet shall not all be zero."
// Note: -1 will pass
if ((b & 0x7f) == 0) {
throw new IOException("corrupted stream - invalid high tag number found");
}
while ((b >= 0) && ((b & 0x80) != 0)) {
tagNo |= (b & 0x7f);
tagNo <<= 7;
b = readAndTrack(s, count);
}
if (b < 0) {
throw new EOFException("EOF found inside tag value.");
}
tagNo |= (b & 0x7f);
}
return tagNo;
}
/**
* Read the length header from an input stream and return the length read.
*
* This code is a slightly adapted version of the code in BouncyCastle's
* ASN1InputStream.
*
* @param s the InputStream to read from
* @param count the counter to modify. Can be null.
* @return the length of the value
* @throws IOException if something goes wrong
*/
public static int readLength(InputStream s, AtomicInteger count) throws IOException {
int length = readAndTrack(s, count);
if (length < 0) {
throw new EOFException("EOF found when length expected");
}
// indefinite-length encoding
if (length == 0x80) {
// We don't support this and shouldn't encounter any of these anyway
// since indefinite length formats are forbidden in DER.
throw new IOException("Indefinite length encoding detected." +
" Check that input is DER and not BER/CER.");
}
if (length > 127) {
int size = length & 0x7f;
// Note: The invalid long form "0xff" (see X.690 8.1.3.5c) will be caught here
if (size > 4) {
/* Long definite lengths can go up to 2^1008 - 1 but since we are storing
* length in an integer, we can only store lengths up to 2^31 - 1 (the maximum
* for a Java integer).
*
* ASN1 lengths use the last 7 bits of subsequent octets to encode the length value
* for long form lengths. Therefore, a 4 byte length translates to 2^(8*3) maximum
* or around 16 million bytes of data but a 5 byte length is 2^(8*4)
* and larger than what an int can handle.
*/
throw new IOException("DER length more than 4 bytes: " + size);
}
length = 0;
for (int i = 0; i < size; i++) {
int next = readAndTrack(s, count);
if (next < 0) {
throw new EOFException("EOF found reading length");
}
length = (length << 8) + next;
}
if (length < 0) {
throw new IOException("corrupted stream - negative length found");
}
}
return length;
}
public static void writeLength(OutputStream out, int length) throws IOException {
writeLength(out, length, null);
}
/**
* Write an integer as a DER encoded definite length.
*
* @param out
* @param length
* @throws IOException if something goes wrong
*/
public static void writeLength(OutputStream out, int length, Signer signer) throws IOException {
if (length > 127) {
int size = 1;
int val = length;
while ((val >>>= 8) != 0) {
size++;
}
byte b = (byte) (size | 0x80);
out.write(b);
if (signer != null) {
signer.update(b);
}
for (int i = (size - 1) * 8; i >= 0; i -= 8) {
b = (byte) (length >> i);
out.write(b);
if (signer != null) {
signer.update(b);
}
}
}
else {
byte b = (byte) length;
out.write(b);
if (signer != null) {
signer.update(b);
}
}
}
public static void writeTag(OutputStream out, int tag, int tagNo) throws IOException {
writeTag(out, tag, tagNo, null);
}
public static void writeTag(OutputStream out, int tag, int tagNo, Signer signer) throws IOException {
int rebuiltTag = rebuildTag(tag, tagNo);
out.write(rebuiltTag);
if (signer != null) {
signer.update((byte) rebuiltTag);
}
}
/* The writeValue() methods are just aliases to writeBytes, but there is a semantic
* difference in their usage. writeValue is meant to be used to write the value portion
* of a ASN1 tag-length-value. writeBytes is used to write arbitrary bytes and
* is mostly used to write an entire TLV.
*/
public static void writeValue(OutputStream out, byte[] value) throws IOException {
writeBytes(out, value, null);
}
public static void writeValue(OutputStream out, byte[] value, Signer signer) throws IOException {
writeBytes(out, value, signer);
}
public static int rebuildTag(int tag, int tagNo) {
// Universal tags have zeroes as the first two bits.
if ((tag >>> 6) != 0) {
throw new RuntimeException("This class only supports universal ASN1 tags.");
}
return tag;
}
public static void writeBytes(OutputStream out, byte[] value) throws IOException {
writeBytes(out, value, null);
}
public static void writeBytes(OutputStream out, byte[] value, Signer signer) throws IOException {
out.write(value);
if (signer != null) {
signer.update(value, 0, value.length);
}
}
}