diff options
Diffstat (limited to 'base/util/src/netscape/security/util/DerOutputStream.java')
-rw-r--r-- | base/util/src/netscape/security/util/DerOutputStream.java | 729 |
1 files changed, 729 insertions, 0 deletions
diff --git a/base/util/src/netscape/security/util/DerOutputStream.java b/base/util/src/netscape/security/util/DerOutputStream.java new file mode 100644 index 000000000..62290d604 --- /dev/null +++ b/base/util/src/netscape/security/util/DerOutputStream.java @@ -0,0 +1,729 @@ +// --- BEGIN COPYRIGHT BLOCK --- +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; version 2 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// +// (C) 2007 Red Hat, Inc. +// All rights reserved. +// --- END COPYRIGHT BLOCK --- +package netscape.security.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetEncoder; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Comparator; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * Output stream marshaling DER-encoded data. This is eventually provided + * in the form of a byte array; there is no advance limit on the size of + * that byte array. + * + * <P> + * At this time, this class supports only a subset of the types of DER data encodings which are defined. That subset is + * sufficient for generating most X.509 certificates. + * + * @version 1.32 + * + * @author David Brownell + * @author Amit Kapoor + * @author Hemma Prafullchandra + */ +public class DerOutputStream + extends ByteArrayOutputStream implements DerEncoder { + /** + * Construct an DER output stream. + * + * @param size how large a buffer to preallocate. + */ + public DerOutputStream(int size) { + super(size); + } + + /** + * Construct an DER output stream. + */ + public DerOutputStream() { + } + + /** + * Writes tagged, pre-marshaled data. This calcuates and encodes + * the length, so that the output data is the standard triple of + * { tag, length, data } used by all DER values. + * + * @param tag the DER value tag for the data, such as <em>DerValue.tag_Sequence</em> + * @param buf buffered data, which must be DER-encoded + */ + public void write(byte tag, byte[] buf) throws IOException { + write(tag); + putLength(buf.length); + write(buf, 0, buf.length); + } + + /** + * Writes tagged data using buffer-to-buffer copy. As above, + * this writes a standard DER record. This is often used when + * efficiently encapsulating values in sequences. + * + * @param tag the DER value tag for the data, such as <em>DerValue.tag_Sequence</em> + * @param out buffered data + */ + public void write(byte tag, DerOutputStream out) throws IOException { + write(tag); + putLength(out.count); + write(out.buf, 0, out.count); + } + + /** + * Writes implicitly tagged data using buffer-to-buffer copy. As above, + * this writes a standard DER record. This is often used when + * efficiently encapsulating implicitly tagged values. + * + * @param tag the DER value of the context-specific tag that replaces + * original tag of the value in the output , such as in + * + * <pre> + * <em> <field> [N] IMPLICIT <type></em> + * </pre> + * + * For example, <em>FooLength [1] IMPLICIT INTEGER</em>, with value=4; + * would be encoded as "81 01 04" whereas in explicit + * tagging it would be encoded as "A1 03 02 01 04". + * Notice that the tag is A1 and not 81, this is because with + * explicit tagging the form is always constructed. + * @param value original value being implicitly tagged + */ + public void writeImplicit(byte tag, DerOutputStream value) + throws IOException { + write(tag); + write(value.buf, 1, value.count - 1); + } + + /** + * Marshals pre-encoded DER value onto the output stream. + */ + public void putDerValue(DerValue val) throws IOException { + val.encode(this); + } + + /* + * PRIMITIVES -- these are "universal" ASN.1 simple types. + * + * BOOLEAN, INTEGER, BIT STRING, OCTET STRING, NULL + * OBJECT IDENTIFIER, SEQUENCE(OF), SET(OF) + * PrintableString, T61String, IA5String, UTCTime + */ + + /** + * Marshals a DER boolean on the output stream. + */ + public void putBoolean(boolean val) throws IOException { + write(DerValue.tag_Boolean); + putLength(1); + if (val) { + write(0xff); + } else { + write(0); + } + } + + /** + * Marshals a DER unsigned integer on the output stream. + */ + public void putInteger(BigInt i) throws IOException { + putUnsignedInteger(i.toByteArray()); + } + + /** + * Marshals a DER unsigned integer on the output stream. + */ + public void putUnsignedInteger(byte[] integerBytes) throws IOException { + + write(DerValue.tag_Integer); + if ((integerBytes[0] & 0x080) != 0) { + /* + * prepend zero so it's not read as a negative number + */ + putLength(integerBytes.length + 1); + write(0); + } else + putLength(integerBytes.length); + write(integerBytes, 0, integerBytes.length); + } + + /** + * Marshals a DER enumerated value on the output stream. + */ + public void putEnumerated(int i) throws IOException { + write(DerValue.tag_Enumerated); + + int bytemask = 0xff000000; + int signmask = 0x80000000; + int length; + if ((i & 0x80000000) != 0) { + // negative case + for (length = 4; length > 1; --length) { + if ((i & bytemask) != bytemask) + break; + bytemask = bytemask >>> 8; + signmask = signmask >>> 8; + } + if ((i & signmask) == 0) { + // ensure negative case + putLength(length + 1); + write(0xff); + } else { + putLength(length); + } + // unrolled loop + switch (length) { + case 4: + write((byte) (i >>> 24)); + case 3: + write((byte) (i >>> 16)); + case 2: + write((byte) (i >>> 8)); + case 1: + write((byte) i); + } + } else { + // positive case + for (length = 4; length > 0; --length) { + if ((i & bytemask) != 0) + break; + bytemask = bytemask >>> 8; + signmask = signmask >>> 8; + } + if ((i & signmask) != 0) { + // ensure posititive case + putLength(length + 1); + write(0x00); + } else { + putLength(length); + } + // unrolled loop + switch (length) { + case 4: + write((byte) (i >>> 24)); + case 3: + write((byte) (i >>> 16)); + case 2: + write((byte) (i >>> 8)); + case 1: + write((byte) i); + } + } + } + + /** + * Marshals a DER bit string on the output stream. The bit + * string must be byte-aligned. + * + * @param bits the bit string, MSB first + */ + public void putBitString(byte[] bits) throws IOException { + write(DerValue.tag_BitString); + putLength(bits.length + 1); + write(0); // all of last octet is used + write(bits); + } + + /** + * Converts a boolean array to a BitArray. Trims trailing 0 bits + * in accordance with DER encoding standard. We assume the input is not + * null. + */ + private static BitArray toBitArray(boolean[] bitString) { + if (bitString.length == 0) { + return new BitArray(bitString); + } + + // find index of last 1 bit. -1 if there aren't any + int i; + for (i = bitString.length - 1; i >= 0; i--) { + if (bitString[i]) { + break; + } + } + int length = i + 1; + + // if length changed, copy to new appropriately-sized array + if (length != bitString.length) { + boolean[] newBitString = new boolean[length]; + System.arraycopy(bitString, 0, newBitString, 0, length); + bitString = newBitString; + } + + return new BitArray(bitString); + } + + /** + * Converts bit string to a BitArray, stripping off trailing 0 bits. + * We assume that the bit string is not null. + */ + private static BitArray toBitArray(byte[] bitString) { + // compute length in bits of bit string + int length, i; + int maxIndex = 0; + + if (bitString.length == 0) { + return new BitArray(0, bitString); + } + + // find the index of the last byte with a 1 bit + for (i = 0; i < bitString.length; i++) { + if (bitString[i] != 0) { + maxIndex = i; + } + } + byte lastByte = bitString[maxIndex]; + length = (maxIndex + 1) * 8; // maximum, might reduce in next step + + // now find the last 1 bit in this last byte + for (i = 1; i <= 0x80; i <<= 1) { + if ((lastByte & i) == 0) { + length--; + } else { + break; + } + } + return new BitArray(length, bitString); + } + + /** + * Marshals a DER bit string on the output stream. + * The bit strings need not be byte-aligned. + * + * @param bits the bit string, MSB first + */ + public void putUnalignedBitString(BitArray ba) throws IOException { + byte[] bits = ba.toByteArray(); + + write(DerValue.tag_BitString); + putLength(bits.length + 1); + write(bits.length * 8 - ba.length()); // excess bits in last octet + write(bits); + } + + /** + * Marshals a DER bit string on the output stream. + * All trailing 0 bits will be stripped off in accordance with DER + * encoding. + * + * @param bits the bit string, MSB first + */ + public void putUnalignedBitString(byte[] bitString) throws IOException { + putUnalignedBitString(toBitArray(bitString)); + } + + /** + * Marshals a DER bit string on the output stream. + * All trailing 0 bits will be stripped off in accordance with DER + * encoding. + * + * @param bits the bit string as an array of booleans. + */ + public void putUnalignedBitString(boolean[] bitString) throws IOException { + putUnalignedBitString(toBitArray(bitString)); + } + + /** + * DER-encodes an ASN.1 OCTET STRING value on the output stream. + * + * @param octets the octet string + */ + public void putOctetString(byte[] octets) throws IOException { + write(DerValue.tag_OctetString, octets); + } + + /** + * Marshals a DER "null" value on the output stream. These are + * often used to indicate optional values which have been omitted. + */ + public void putNull() throws IOException { + write(DerValue.tag_Null); + putLength(0); + } + + /** + * Marshals an object identifier (OID) on the output stream. + * Corresponds to the ASN.1 "OBJECT IDENTIFIER" construct. + */ + public void putOID(ObjectIdentifier oid) throws IOException { + oid.encode(this); + } + + /** + * Marshals a sequence on the output stream. This supports both + * the ASN.1 "SEQUENCE" (zero to N values) and "SEQUENCE OF" + * (one to N values) constructs. + */ + public void putSequence(DerValue[] seq) throws IOException { + DerOutputStream bytes = new DerOutputStream(); + int i; + + for (i = 0; i < seq.length; i++) + seq[i].encode(bytes); + + write(DerValue.tag_Sequence, bytes); + } + + /** + * Marshals the contents of a set on the output stream without + * ordering the elements. Ok for BER encoding, but not for DER + * encoding. + * + * For DER encoding, use orderedPutSet() or orderedPutSetOf(). + */ + public void putSet(DerValue[] set) throws IOException { + DerOutputStream bytes = new DerOutputStream(); + int i; + + for (i = 0; i < set.length; i++) + set[i].encode(bytes); + + write(DerValue.tag_Set, bytes); + } + + /** + * NSCP : + * Like putOrderSetOf, except not sorted. + * This may defy DER encoding but is needed for compatibility + * with communicator. + */ + public void putSet(byte tag, DerEncoder[] set) throws IOException { + putOrderedSet(tag, set, null); + } + + /** + * Marshals the contents of a set on the output stream. Sets + * are semantically unordered, but DER requires that encodings of + * set elements be sorted into ascending lexicographical order + * before being output. Hence sets with the same tags and + * elements have the same DER encoding. + * + * This method supports the ASN.1 "SET OF" construct, but not + * "SET", which uses a different order. + */ + public void putOrderedSetOf(byte tag, DerEncoder[] set) throws IOException { + putOrderedSet(tag, set, lexOrder); + } + + /** + * Marshals the contents of a set on the output stream. Sets + * are semantically unordered, but DER requires that encodings of + * set elements be sorted into ascending tag order + * before being output. Hence sets with the same tags and + * elements have the same DER encoding. + * + * This method supports the ASN.1 "SET" construct, but not + * "SET OF", which uses a different order. + */ + public void putOrderedSet(byte tag, DerEncoder[] set) throws IOException { + putOrderedSet(tag, set, tagOrder); + } + + /** + * Lexicographical order comparison on byte arrays, for ordering + * elements of a SET OF objects in DER encoding. + */ + private static ByteArrayLexOrder lexOrder = new ByteArrayLexOrder(); + + /** + * Tag order comparison on byte arrays, for ordering elements of + * SET objects in DER encoding. + */ + private static ByteArrayTagOrder tagOrder = new ByteArrayTagOrder(); + + /** + * Marshals a the contents of a set on the output stream with the + * encodings of its sorted in increasing order. + * + * @param order the order to use when sorting encodings of components. + */ + private void putOrderedSet(byte tag, DerEncoder[] set, + Comparator<byte[]> order) throws IOException { + DerOutputStream[] streams = new DerOutputStream[set.length]; + + for (int i = 0; i < set.length; i++) { + streams[i] = new DerOutputStream(); + set[i].derEncode(streams[i]); + } + + // order the element encodings + byte[][] bufs = new byte[streams.length][]; + for (int i = 0; i < streams.length; i++) { + bufs[i] = streams[i].toByteArray(); + } + if (order != null) { + Arrays.sort(bufs, order); + } + + DerOutputStream bytes = new DerOutputStream(); + for (int i = 0; i < streams.length; i++) { + bytes.write(bufs[i]); + } + write(tag, bytes); + + } + + /** + * Converts string to printable and writes to der output stream. + */ + public void putPrintableString(String s) throws IOException { + putStringType(DerValue.tag_PrintableString, s); + } + + public void putVisibleString(String s) throws IOException { + putStringType(DerValue.tag_VisibleString, s); + } + + /** + * Marshals a string which is consists of BMP (unicode) characters + */ + public void putBMPString(String s) throws IOException { + putStringType(DerValue.tag_BMPString, s); + } + + public void putGeneralString(String s) throws IOException { + putStringType(DerValue.tag_GeneralString, s); + } + + // /* + // * T61 is an 8 bit extension to ASCII, escapes e.g. to Japanese + // */ + // void putT61String(String s) throws IOException + // { + // // XXX IMPLEMENT ME + // + // throw new IOException("DerOutputStream.putT61String() NYI"); + // } + + // /* + // * Universal String. + // */ + // void putUniversalString(String s) throws IOException + // { + // // XXX IMPLEMENT ME + // + // throw new IOException("DerOutputStream.putUniversalString() NYI"); + // } + + /** + * Marshals a string which is consists of IA5(ASCII) characters + */ + public void putIA5String(String s) throws IOException { + putStringType(DerValue.tag_IA5String, s); + } + + public void putUTF8String(String s) throws IOException { + putStringType(DerValue.tag_UTF8String, s); + } + + public void putStringType(byte tag, String s) throws IOException { + try { + CharsetEncoder encoder = ASN1CharStrConvMap.getDefault().getEncoder(tag); + if (encoder == null) + throw new IOException("No encoder for tag"); + + CharBuffer charBuffer = CharBuffer.wrap(s.toCharArray()); + ByteBuffer byteBuffer = encoder.encode(charBuffer); + + write(tag); + putLength(byteBuffer.limit()); + write(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.limit()); + + } catch (CharacterCodingException e) { + throw new IOException("Not a valid string type " + tag, e); + } + } + + private void put2DateBytes(byte[] buffer, int value, int offset) { + int upper = value / 10; + int lower = value % 10; + buffer[offset] = (byte) ((byte) upper + (byte) '0'); + buffer[offset + 1] = (byte) ((byte) lower + (byte) '0'); + } + + private static Calendar GMTGregorianCalendar = null; + + private Calendar getGMTGregorianCalendar() { + if (GMTGregorianCalendar == null) { + TimeZone tz = TimeZone.getTimeZone("GMT"); + GMTGregorianCalendar = new GregorianCalendar(tz); + } + return (Calendar) GMTGregorianCalendar.clone(); + } + + public byte[] getDateBytes(Date d, boolean UTC) { + + byte[] datebytes; + + if (UTC) { + datebytes = new byte[13]; + } else { // generalized time has 4 digits for yr + datebytes = new byte[15]; + } + + Calendar cal = getGMTGregorianCalendar(); + cal.setTime(d); + + int i = 0; + if (!UTC) { + put2DateBytes(datebytes, cal.get(Calendar.YEAR) / 100, i); + i += 2; + } + put2DateBytes(datebytes, cal.get(Calendar.YEAR) % 100, i); + // Calendar's MONTH is zero-based + i += 2; + put2DateBytes(datebytes, cal.get(Calendar.MONTH) + 1, i); + i += 2; + put2DateBytes(datebytes, cal.get(Calendar.DAY_OF_MONTH), i); + i += 2; + put2DateBytes(datebytes, cal.get(Calendar.HOUR_OF_DAY), i); + i += 2; + put2DateBytes(datebytes, cal.get(Calendar.MINUTE), i); + i += 2; + put2DateBytes(datebytes, cal.get(Calendar.SECOND), i); + i += 2; + // datebytes[i] = 'Z'; + datebytes[i] = (byte) 'Z'; + + return datebytes; + } + + /** + * Marshals a DER UTC time/date value. + * + * <P> + * YYMMDDhhmmss{Z|+hhmm|-hhmm} ... emits only using Zulu time and with seconds (even if seconds=0) as per IETF-PKIX + * partI. + */ + public void putUTCTime(Date d) throws IOException { + /* + * Format the date. + */ + + // This was the old code. Way too slow to be usable (stevep) + + // String pattern = "yyMMddHHmmss'Z'"; + // SimpleDateFormat sdf = new SimpleDateFormat(pattern); + // TimeZone tz = TimeZone.getTimeZone("GMT"); + // sdf.setTimeZone(tz); + // byte[] utc = (sdf.format(d)).getBytes(); + + byte[] datebytes = getDateBytes(d, true); // UTC = true + + /* + * Write the formatted date. + */ + write(DerValue.tag_UtcTime); + putLength(datebytes.length); + write(datebytes); + } + + /** + * Marshals a DER Generalized Time/date value. + * + * <P> + * YYYYMMDDhhmmss{Z|+hhmm|-hhmm} ... emits only using Zulu time and with seconds (even if seconds=0) as per + * IETF-PKIX partI. + */ + public void putGeneralizedTime(Date d) throws IOException { + /* + * Format the date. + */ + TimeZone tz = TimeZone.getTimeZone("GMT"); + + // This is way too slow to be usable (stevep) + String pattern = "yyyyMMddHHmmss'Z'"; + SimpleDateFormat sdf = new SimpleDateFormat(pattern); + sdf.setTimeZone(tz); + byte[] gt = (sdf.format(d)).getBytes(); + + /* + * Write the formatted date. + */ + write(DerValue.tag_GeneralizedTime); + putLength(gt.length); + write(gt); + } + + /** + * Put the encoding of the length in the stream. + * + * @param len the length of the attribute. + * @exception IOException on writing errors. + */ + public void putLength(int len) throws IOException { + if (len < 128) { + write((byte) len); + + } else if (len < (1 << 8)) { + write((byte) 0x081); + write((byte) len); + + } else if (len < (1 << 16)) { + write((byte) 0x082); + write((byte) (len >> 8)); + write((byte) len); + + } else if (len < (1 << 24)) { + write((byte) 0x083); + write((byte) (len >> 16)); + write((byte) (len >> 8)); + write((byte) len); + + } else { + write((byte) 0x084); + write((byte) (len >> 24)); + write((byte) (len >> 16)); + write((byte) (len >> 8)); + write((byte) len); + } + } + + /** + * Put the tag of the attribute in the stream. + * + * @param class the tag class type, one of UNIVERSAL, CONTEXT, + * APPLICATION or PRIVATE + * @param form if true, the value is constructed, otherwise it is + * primitive. + * @param val the tag value + */ + public void putTag(byte tagClass, boolean form, byte val) { + byte tag = (byte) (tagClass | val); + if (form) { + tag |= (byte) 0x20; + } + write(tag); + } + + /** + * Write the current contents of this <code>DerOutputStream</code> to an <code>OutputStream</code>. + * + * @exception IOException on output error. + */ + public void derEncode(OutputStream out) throws IOException { + out.write(toByteArray()); + } +} |