| /* | |
| * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. Oracle designates this | |
| * particular file as subject to the "Classpath" exception as provided | |
| * by Oracle in the LICENSE file that accompanied this code. | |
| * | |
| * This code 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 | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| package sun.security.util; | |
| import jdk.internal.util.ArraysSupport; | |
| import sun.nio.cs.UTF_32BE; | |
| import sun.util.calendar.CalendarDate; | |
| import sun.util.calendar.CalendarSystem; | |
| import java.io.*; | |
| import java.math.BigInteger; | |
| import java.nio.charset.Charset; | |
| import java.util.*; | |
| import static java.nio.charset.StandardCharsets.ISO_8859_1; | |
| import static java.nio.charset.StandardCharsets.US_ASCII; | |
| import static java.nio.charset.StandardCharsets.UTF_16BE; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| /** | |
| * Represents a single DER-encoded value. DER encoding rules are a subset | |
| * of the "Basic" Encoding Rules (BER), but they only support a single way | |
| * ("Definite" encoding) to encode any given value. | |
| * | |
| * <P>All DER-encoded data are triples <em>{type, length, data}</em>. This | |
| * class represents such tagged values as they have been read (or constructed), | |
| * and provides structured access to the encoded data. | |
| * | |
| * <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 parsing | |
| * most X.509 certificates, and working with selected additional formats | |
| * (such as PKCS #10 certificate requests, and some kinds of PKCS #7 data). | |
| * | |
| * A note with respect to T61/Teletex strings: From RFC 1617, section 4.1.3 | |
| * and RFC 5280, section 8, we assume that this kind of string will contain | |
| * ISO-8859-1 characters only. | |
| * | |
| * | |
| * @author David Brownell | |
| * @author Amit Kapoor | |
| * @author Hemma Prafullchandra | |
| */ | |
| public class DerValue { | |
| /** The tag class types */ | |
| public static final byte TAG_UNIVERSAL = (byte)0x000; | |
| public static final byte TAG_CONSTRUCT = (byte)0x020; | |
| public static final byte TAG_APPLICATION = (byte)0x040; | |
| public static final byte TAG_CONTEXT = (byte)0x080; | |
| public static final byte TAG_PRIVATE = (byte)0x0c0; | |
| /* | |
| * The type starts at the first byte of the encoding, and | |
| * is one of these tag_* values. That may be all the type | |
| * data that is needed. | |
| */ | |
| /* | |
| * These tags are the "universal" tags ... they mean the same | |
| * in all contexts. (Mask with 0x1f -- five bits.) | |
| */ | |
| /** Tag value indicating an ASN.1 "BOOLEAN" value. */ | |
| public static final byte tag_Boolean = 0x01; | |
| /** Tag value indicating an ASN.1 "INTEGER" value. */ | |
| public static final byte tag_Integer = 0x02; | |
| /** Tag value indicating an ASN.1 "BIT STRING" value. */ | |
| public static final byte tag_BitString = 0x03; | |
| /** Tag value indicating an ASN.1 "OCTET STRING" value. */ | |
| public static final byte tag_OctetString = 0x04; | |
| /** Tag value indicating an ASN.1 "NULL" value. */ | |
| public static final byte tag_Null = 0x05; | |
| /** Tag value indicating an ASN.1 "OBJECT IDENTIFIER" value. */ | |
| public static final byte tag_ObjectId = 0x06; | |
| /** Tag value including an ASN.1 "ENUMERATED" value */ | |
| public static final byte tag_Enumerated = 0x0A; | |
| /** Tag value indicating an ASN.1 "UTF8String" value. */ | |
| public static final byte tag_UTF8String = 0x0C; | |
| /** Tag value including a "printable" string */ | |
| public static final byte tag_PrintableString = 0x13; | |
| /** Tag value including a "teletype" string */ | |
| public static final byte tag_T61String = 0x14; | |
| /** Tag value including an ASCII string */ | |
| public static final byte tag_IA5String = 0x16; | |
| /** Tag value indicating an ASN.1 "UTCTime" value. */ | |
| public static final byte tag_UtcTime = 0x17; | |
| /** Tag value indicating an ASN.1 "GeneralizedTime" value. */ | |
| public static final byte tag_GeneralizedTime = 0x18; | |
| /** Tag value indicating an ASN.1 "GeneralString" value. */ | |
| public static final byte tag_GeneralString = 0x1B; | |
| /** Tag value indicating an ASN.1 "UniversalString" value. */ | |
| public static final byte tag_UniversalString = 0x1C; | |
| /** Tag value indicating an ASN.1 "BMPString" value. */ | |
| public static final byte tag_BMPString = 0x1E; | |
| // CONSTRUCTED seq/set | |
| /** | |
| * Tag value indicating an ASN.1 | |
| * "SEQUENCE" (zero to N elements, order is significant). | |
| */ | |
| public static final byte tag_Sequence = 0x30; | |
| /** | |
| * Tag value indicating an ASN.1 | |
| * "SEQUENCE OF" (one to N elements, order is significant). | |
| */ | |
| public static final byte tag_SequenceOf = 0x30; | |
| /** | |
| * Tag value indicating an ASN.1 | |
| * "SET" (zero to N members, order does not matter). | |
| */ | |
| public static final byte tag_Set = 0x31; | |
| /** | |
| * Tag value indicating an ASN.1 | |
| * "SET OF" (one to N members, order does not matter). | |
| */ | |
| public static final byte tag_SetOf = 0x31; | |
| // This class is mostly immutable except that: | |
| // | |
| // 1. resetTag() modifies the tag | |
| // 2. the data field is mutable | |
| // | |
| // For compatibility, data, getData() and resetTag() are preserved. | |
| // A modern caller should call withTag() or data() instead. | |
| // | |
| // Also, some constructors have not cloned buffer, so the data could | |
| // be modified externally. | |
| public /*final*/ byte tag; | |
| final byte[] buffer; | |
| private final int start; | |
| final int end; | |
| private final boolean allowBER; | |
| // Unsafe. Legacy. Never null. | |
| public final DerInputStream data; | |
| /* | |
| * These values are the high order bits for the other kinds of tags. | |
| */ | |
| /** | |
| * Returns true if the tag class is UNIVERSAL. | |
| */ | |
| public boolean isUniversal() { return ((tag & 0x0c0) == 0x000); } | |
| /** | |
| * Returns true if the tag class is APPLICATION. | |
| */ | |
| public boolean isApplication() { return ((tag & 0x0c0) == 0x040); } | |
| /** | |
| * Returns true iff the CONTEXT SPECIFIC bit is set in the type tag. | |
| * This is associated with the ASN.1 "DEFINED BY" syntax. | |
| */ | |
| public boolean isContextSpecific() { return ((tag & 0x0c0) == 0x080); } | |
| /** | |
| * Returns true iff the CONTEXT SPECIFIC TAG matches the passed tag. | |
| */ | |
| public boolean isContextSpecific(byte cntxtTag) { | |
| if (!isContextSpecific()) { | |
| return false; | |
| } | |
| return ((tag & 0x01f) == cntxtTag); | |
| } | |
| boolean isPrivate() { return ((tag & 0x0c0) == 0x0c0); } | |
| /** Returns true iff the CONSTRUCTED bit is set in the type tag. */ | |
| public boolean isConstructed() { return ((tag & 0x020) == 0x020); } | |
| /** | |
| * Returns true iff the CONSTRUCTED TAG matches the passed tag. | |
| */ | |
| public boolean isConstructed(byte constructedTag) { | |
| if (!isConstructed()) { | |
| return false; | |
| } | |
| return ((tag & 0x01f) == constructedTag); | |
| } | |
| /** | |
| * Creates a new DerValue by specifying all its fields. | |
| */ | |
| DerValue(byte tag, byte[] buffer, int start, int end, boolean allowBER) { | |
| if ((tag & 0x1f) == 0x1f) { | |
| throw new IllegalArgumentException("Tag number over 30 is not supported"); | |
| } | |
| this.tag = tag; | |
| this.buffer = buffer; | |
| this.start = start; | |
| this.end = end; | |
| this.allowBER = allowBER; | |
| this.data = data(); | |
| } | |
| /** | |
| * Creates a PrintableString or UTF8string DER value from a string. | |
| */ | |
| public DerValue(String value) { | |
| this(isPrintableString(value) ? tag_PrintableString : tag_UTF8String, | |
| value); | |
| } | |
| private static boolean isPrintableString(String value) { | |
| for (int i = 0; i < value.length(); i++) { | |
| if (!isPrintableStringChar(value.charAt(i))) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| /** | |
| * Creates a string type DER value from a String object | |
| * @param stringTag the tag for the DER value to create | |
| * @param value the String object to use for the DER value | |
| */ | |
| public DerValue(byte stringTag, String value) { | |
| this(stringTag, string2bytes(stringTag, value), false); | |
| } | |
| private static byte[] string2bytes(byte stringTag, String value) { | |
| Charset charset = switch (stringTag) { | |
| case tag_PrintableString, tag_IA5String, tag_GeneralString -> US_ASCII; | |
| case tag_T61String -> ISO_8859_1; | |
| case tag_BMPString -> UTF_16BE; | |
| case tag_UTF8String -> UTF_8; | |
| case tag_UniversalString -> Charset.forName("UTF_32BE"); | |
| default -> throw new IllegalArgumentException("Unsupported DER string type"); | |
| }; | |
| return value.getBytes(charset); | |
| } | |
| DerValue(byte tag, byte[] buffer, boolean allowBER) { | |
| this(tag, buffer, 0, buffer.length, allowBER); | |
| } | |
| /** | |
| * Creates a DerValue from a tag and some DER-encoded data. | |
| * | |
| * This is a public constructor. | |
| * | |
| * @param tag the DER type tag | |
| * @param buffer the DER-encoded data | |
| */ | |
| public DerValue(byte tag, byte[] buffer) { | |
| this(tag, buffer.clone(), true); | |
| } | |
| /** | |
| * Wraps a DerOutputStream. All bytes currently written | |
| * into the stream will become the content of the newly | |
| * created DerValue. | |
| * | |
| * Attention: do not reset the DerOutputStream after this call. | |
| * No array copying is made. | |
| * | |
| * @param tag the tag | |
| * @param out the DerOutputStream | |
| * @return a new DerValue using out as its content | |
| */ | |
| public static DerValue wrap(byte tag, DerOutputStream out) { | |
| return new DerValue(tag, out.buf(), 0, out.size(), false); | |
| } | |
| /** | |
| * Wraps a byte array as a single DerValue. | |
| * | |
| * Attention: no cloning is made. | |
| * | |
| * @param buf the byte array containing the DER-encoded datum | |
| * @return a new DerValue | |
| */ | |
| public static DerValue wrap(byte[] buf) | |
| throws IOException { | |
| return wrap(buf, 0, buf.length); | |
| } | |
| /** | |
| * Wraps a byte array as a single DerValue. | |
| * | |
| * Attention: no cloning is made. | |
| * | |
| * @param buf the byte array containing the DER-encoded datum | |
| * @param offset where the encoded datum starts inside {@code buf} | |
| * @param len length of bytes to parse inside {@code buf} | |
| * @return a new DerValue | |
| */ | |
| public static DerValue wrap(byte[] buf, int offset, int len) | |
| throws IOException { | |
| return new DerValue(buf, offset, len, true, false); | |
| } | |
| /** | |
| * Parse an ASN.1/BER encoded datum. The entire encoding must hold exactly | |
| * one datum, including its tag and length. | |
| * | |
| * This is a public constructor. | |
| */ | |
| @SuppressWarnings("this-escape") | |
| public DerValue(byte[] encoding) throws IOException { | |
| this(encoding.clone(), 0, encoding.length, true, false); | |
| } | |
| /** | |
| * Parse an ASN.1 encoded datum from a byte array. | |
| * | |
| * @param buf the byte array containing the DER-encoded datum | |
| * @param offset where the encoded datum starts inside {@code buf} | |
| * @param len length of bytes to parse inside {@code buf} | |
| * @param allowBER whether BER is allowed | |
| * @param allowMore whether extra bytes are allowed after the encoded datum. | |
| * If true, {@code len} can be bigger than the length of | |
| * the encoded datum. | |
| * | |
| * @throws IOException if it's an invalid encoding or there are extra bytes | |
| * after the encoded datum and {@code allowMore} is false. | |
| */ | |
| DerValue(byte[] buf, int offset, int len, boolean allowBER, boolean allowMore) | |
| throws IOException { | |
| if (len < 2) { | |
| throw new IOException("Too short"); | |
| } | |
| int pos = offset; | |
| tag = buf[pos++]; | |
| if ((tag & 0x1f) == 0x1f) { | |
| throw new IOException("Tag number over 30 at " + offset + " is not supported"); | |
| } | |
| int lenByte = buf[pos++]; | |
| int length; | |
| if (lenByte == (byte) 0x80) { // indefinite length | |
| if (!allowBER) { | |
| throw new IOException("Indefinite length encoding " + | |
| "not supported with DER"); | |
| } | |
| if (!isConstructed()) { | |
| throw new IOException("Indefinite length encoding " + | |
| "not supported with non-constructed data"); | |
| } | |
| // Reconstruct data source | |
| buf = DerIndefLenConverter.convertStream( | |
| new ByteArrayInputStream(buf, pos, len - (pos - offset)), tag); | |
| offset = 0; | |
| len = buf.length; | |
| pos = 2; | |
| if (tag != buf[0]) { | |
| throw new IOException("Indefinite length encoding not supported"); | |
| } | |
| lenByte = buf[1]; | |
| if (lenByte == (byte) 0x80) { | |
| throw new IOException("Indefinite len conversion failed"); | |
| } | |
| } | |
| if ((lenByte & 0x080) == 0x00) { // short form, 1 byte datum | |
| length = lenByte; | |
| } else { // long form | |
| lenByte &= 0x07f; | |
| if (lenByte > 4) { | |
| throw new IOException("Invalid lenByte"); | |
| } | |
| if (len < 2 + lenByte) { | |
| throw new IOException("Not enough length bytes"); | |
| } | |
| length = 0x0ff & buf[pos++]; | |
| lenByte--; | |
| if (length == 0 && !allowBER) { | |
| // DER requires length value be encoded in minimum number of bytes | |
| throw new IOException("Redundant length bytes found"); | |
| } | |
| while (lenByte-- > 0) { | |
| length <<= 8; | |
| length += 0x0ff & buf[pos++]; | |
| } | |
| if (length < 0) { | |
| throw new IOException("Invalid length bytes"); | |
| } else if (length <= 127 && !allowBER) { | |
| throw new IOException("Should use short form for length"); | |
| } | |
| } | |
| // pos is now at the beginning of the content | |
| if (len - length < pos - offset) { | |
| throw new EOFException("not enough content"); | |
| } | |
| if (len - length > pos - offset && !allowMore) { | |
| throw new IOException("extra data at the end"); | |
| } | |
| this.buffer = buf; | |
| this.start = pos; | |
| this.end = pos + length; | |
| this.allowBER = allowBER; | |
| this.data = data(); | |
| } | |
| // Get an ASN1/DER encoded datum from an input stream w/ additional | |
| // arg to control whether DER checks are enforced. | |
| DerValue(InputStream in, boolean allowBER) throws IOException { | |
| this.tag = (byte)in.read(); | |
| if ((tag & 0x1f) == 0x1f) { | |
| throw new IOException("Tag number over 30 is not supported"); | |
| } | |
| int length = DerInputStream.getLength(in); | |
| if (length == -1) { // indefinite length encoding found | |
| if (!allowBER) { | |
| throw new IOException("Indefinite length encoding " + | |
| "not supported with DER"); | |
| } | |
| if (!isConstructed()) { | |
| throw new IOException("Indefinite length encoding " + | |
| "not supported with non-constructed data"); | |
| } | |
| this.buffer = DerIndefLenConverter.convertStream(in, tag); | |
| ByteArrayInputStream bin = new ByteArrayInputStream(this.buffer); | |
| if (tag != bin.read()) { | |
| throw new IOException | |
| ("Indefinite length encoding not supported"); | |
| } | |
| length = DerInputStream.getDefiniteLength(bin); | |
| this.start = this.buffer.length - bin.available(); | |
| this.end = this.start + length; | |
| // position of in is undetermined. Precisely, it might be n-bytes | |
| // after DerValue, and these n bytes are at the end of this.buffer | |
| // after this.end. | |
| } else { | |
| this.buffer = IOUtils.readExactlyNBytes(in, length); | |
| this.start = 0; | |
| this.end = length; | |
| // position of in is right after the DerValue | |
| } | |
| this.allowBER = allowBER; | |
| this.data = data(); | |
| } | |
| /** | |
| * Get an ASN1/DER encoded datum from an input stream. The | |
| * stream may have additional data following the encoded datum. | |
| * In case of indefinite length encoded datum, the input stream | |
| * must hold only one datum, i.e. all bytes in the stream might | |
| * be consumed. Otherwise, only one DerValue will be consumed. | |
| * | |
| * @param in the input stream holding a single DER datum, | |
| * which may be followed by additional data | |
| */ | |
| @SuppressWarnings("this-escape") | |
| public DerValue(InputStream in) throws IOException { | |
| this(in, true); | |
| } | |
| /** | |
| * Encode an ASN1/DER encoded datum onto a DER output stream. | |
| */ | |
| public void encode(DerOutputStream out) { | |
| out.write(tag); | |
| out.putLength(end - start); | |
| out.write(buffer, start, end - start); | |
| data.pos = data.end; // Compatibility. Reach end. | |
| } | |
| /** | |
| * Returns a new DerInputStream pointing at the start of this | |
| * DerValue's content. | |
| * | |
| * @return the new DerInputStream value | |
| */ | |
| public final DerInputStream data() { | |
| return new DerInputStream(buffer, start, end - start, allowBER); | |
| } | |
| /** | |
| * Returns the data field inside this class directly. | |
| * | |
| * Both this method and the {@link #data} field should be avoided. | |
| * Consider using {@link #data()} instead. | |
| */ | |
| public final DerInputStream getData() { | |
| return data; | |
| } | |
| public final byte getTag() { | |
| return tag; | |
| } | |
| /** | |
| * Returns an ASN.1 BOOLEAN | |
| * | |
| * @return the boolean held in this DER value | |
| */ | |
| public boolean getBoolean() throws IOException { | |
| if (tag != tag_Boolean) { | |
| throw new IOException("DerValue.getBoolean, not a BOOLEAN " + tag); | |
| } | |
| if (end - start != 1) { | |
| throw new IOException("DerValue.getBoolean, invalid length " | |
| + (end - start)); | |
| } | |
| data.pos = data.end; // Compatibility. Reach end. | |
| return buffer[start] != 0; | |
| } | |
| /** | |
| * Returns an ASN.1 OBJECT IDENTIFIER. | |
| * | |
| * @return the OID held in this DER value | |
| */ | |
| public ObjectIdentifier getOID() throws IOException { | |
| if (tag != tag_ObjectId) { | |
| throw new IOException("DerValue.getOID, not an OID " + tag); | |
| } | |
| data.pos = data.end; // Compatibility. Reach end. | |
| return new ObjectIdentifier(Arrays.copyOfRange(buffer, start, end)); | |
| } | |
| /** | |
| * Returns an ASN.1 OCTET STRING | |
| * | |
| * @return the octet string held in this DER value | |
| */ | |
| public byte[] getOctetString() throws IOException { | |
| if (tag != tag_OctetString && !isConstructed(tag_OctetString)) { | |
| throw new IOException( | |
| "DerValue.getOctetString, not an Octet String: " + tag); | |
| } | |
| // Note: do not attempt to call buffer.read(bytes) at all. There's a | |
| // known bug that it returns -1 instead of 0. | |
| if (end - start == 0) { | |
| return new byte[0]; | |
| } | |
| data.pos = data.end; // Compatibility. Reach end. | |
| if (!isConstructed()) { | |
| return Arrays.copyOfRange(buffer, start, end); | |
| } else { | |
| ByteArrayOutputStream bout = new ByteArrayOutputStream(); | |
| DerInputStream dis = data(); | |
| while (dis.available() > 0) { | |
| bout.write(dis.getDerValue().getOctetString()); | |
| } | |
| return bout.toByteArray(); | |
| } | |
| } | |
| /** | |
| * Returns an ASN.1 INTEGER value as an integer. | |
| * | |
| * @return the integer held in this DER value. | |
| */ | |
| public int getInteger() throws IOException { | |
| return getIntegerInternal(tag_Integer); | |
| } | |
| private int getIntegerInternal(byte expectedTag) throws IOException { | |
| BigInteger result = getBigIntegerInternal(expectedTag, false); | |
| if (result.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0) { | |
| throw new IOException("Integer below minimum valid value"); | |
| } | |
| if (result.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) { | |
| throw new IOException("Integer exceeds maximum valid value"); | |
| } | |
| return result.intValue(); | |
| } | |
| /** | |
| * Returns an ASN.1 INTEGER value as a BigInteger. | |
| * | |
| * @return the integer held in this DER value as a BigInteger. | |
| */ | |
| public BigInteger getBigInteger() throws IOException { | |
| return getBigIntegerInternal(tag_Integer, false); | |
| } | |
| /** | |
| * Returns an ASN.1 INTEGER value as a positive BigInteger. | |
| * This is just to deal with implementations that incorrectly encode | |
| * some values as negative. | |
| * | |
| * @return the integer held in this DER value as a BigInteger. | |
| */ | |
| public BigInteger getPositiveBigInteger() throws IOException { | |
| return getBigIntegerInternal(tag_Integer, true); | |
| } | |
| /** | |
| * Returns a BigInteger value | |
| * | |
| * @param makePositive whether to always return a positive value, | |
| * irrespective of actual encoding | |
| * @return the integer as a BigInteger. | |
| */ | |
| private BigInteger getBigIntegerInternal(byte expectedTag, boolean makePositive) throws IOException { | |
| if (tag != expectedTag) { | |
| throw new IOException("DerValue.getBigIntegerInternal, not expected " + tag); | |
| } | |
| if (end == start) { | |
| throw new IOException("Invalid encoding: zero length Int value"); | |
| } | |
| data.pos = data.end; // Compatibility. Reach end. | |
| if (!allowBER && (end - start >= 2 && (buffer[start] == 0) && (buffer[start + 1] >= 0))) { | |
| throw new IOException("Invalid encoding: redundant leading 0s"); | |
| } | |
| return makePositive | |
| ? new BigInteger(1, buffer, start, end - start) | |
| : new BigInteger(buffer, start, end - start); | |
| } | |
| /** | |
| * Returns an ASN.1 ENUMERATED value. | |
| * | |
| * @return the integer held in this DER value. | |
| */ | |
| public int getEnumerated() throws IOException { | |
| return getIntegerInternal(tag_Enumerated); | |
| } | |
| /** | |
| * Returns an ASN.1 BIT STRING value. The bit string must be byte-aligned. | |
| * | |
| * @return the bit string held in this value | |
| */ | |
| public byte[] getBitString() throws IOException { | |
| return getBitString(false); | |
| } | |
| /** | |
| * Returns an ASN.1 BIT STRING value that need not be byte-aligned. | |
| * | |
| * @return a BitArray representing the bit string held in this value | |
| */ | |
| public BitArray getUnalignedBitString() throws IOException { | |
| return getUnalignedBitString(false); | |
| } | |
| /** | |
| * Returns the name component as a Java string, regardless of its | |
| * encoding restrictions (ASCII, T61, Printable, IA5, BMP, UTF8). | |
| */ | |
| // TBD: Need encoder for UniversalString before it can be handled. | |
| public String getAsString() throws IOException { | |
| return switch (tag) { | |
| case tag_UTF8String -> getUTF8String(); | |
| case tag_PrintableString -> getPrintableString(); | |
| case tag_T61String -> getT61String(); | |
| case tag_IA5String -> getIA5String(); | |
| case tag_UniversalString -> getUniversalString(); | |
| case tag_BMPString -> getBMPString(); | |
| case tag_GeneralString -> getGeneralString(); | |
| default -> null; | |
| }; | |
| } | |
| // check the number of pad bits, validate the pad bits in the bytes | |
| // if enforcing DER (i.e. allowBER == false), and return the number of | |
| // bits of the resulting BitString | |
| private static int checkPaddedBits(int numOfPadBits, byte[] data, | |
| int start, int end, boolean allowBER) throws IOException { | |
| // number of pad bits should be from 0(min) to 7(max). | |
| if ((numOfPadBits < 0) || (numOfPadBits > 7)) { | |
| throw new IOException("Invalid number of padding bits"); | |
| } | |
| int lenInBits = ((end - start) << 3) - numOfPadBits; | |
| if (lenInBits < 0) { | |
| throw new IOException("Not enough bytes in BitString"); | |
| } | |
| // padding bits should be all zeros for DER | |
| if (!allowBER && numOfPadBits != 0 && | |
| (data[end - 1] & (0xff >>> (8 - numOfPadBits))) != 0) { | |
| throw new IOException("Invalid value of padding bits"); | |
| } | |
| return lenInBits; | |
| } | |
| /** | |
| * Returns an ASN.1 BIT STRING value, with the tag assumed implicit | |
| * based on the parameter. The bit string must be byte-aligned. | |
| * | |
| * @param tagImplicit if true, the tag is assumed implicit. | |
| * @return the bit string held in this value | |
| */ | |
| public byte[] getBitString(boolean tagImplicit) throws IOException { | |
| if (!tagImplicit) { | |
| if (tag != tag_BitString) { | |
| throw new IOException("DerValue.getBitString, not a bit string " | |
| + tag); | |
| } | |
| } | |
| if (end == start) { | |
| throw new IOException("Invalid encoding: zero length bit string"); | |
| } | |
| data.pos = data.end; // Compatibility. Reach end. | |
| int numOfPadBits = buffer[start]; | |
| checkPaddedBits(numOfPadBits, buffer, start + 1, end, allowBER); | |
| byte[] retval = Arrays.copyOfRange(buffer, start + 1, end); | |
| if (allowBER && numOfPadBits != 0) { | |
| // fix the potential non-zero padding bits | |
| retval[retval.length - 1] &= (byte)((0xff << numOfPadBits)); | |
| } | |
| return retval; | |
| } | |
| /** | |
| * Returns an ASN.1 BIT STRING value, with the tag assumed implicit | |
| * based on the parameter. The bit string need not be byte-aligned. | |
| * | |
| * @param tagImplicit if true, the tag is assumed implicit. | |
| * @return the bit string held in this value | |
| */ | |
| public BitArray getUnalignedBitString(boolean tagImplicit) | |
| throws IOException { | |
| if (!tagImplicit) { | |
| if (tag != tag_BitString) { | |
| throw new IOException("DerValue.getBitString, not a bit string " | |
| + tag); | |
| } | |
| } | |
| if (end == start) { | |
| throw new IOException("Invalid encoding: zero length bit string"); | |
| } | |
| data.pos = data.end; // Compatibility. Reach end. | |
| int numOfPadBits = buffer[start]; | |
| int len = checkPaddedBits(numOfPadBits, buffer, start + 1, end, | |
| allowBER); | |
| return new BitArray(len, buffer, start + 1); | |
| } | |
| /** | |
| * Helper routine to return all the bytes contained in the | |
| * DerInputStream associated with this object. | |
| */ | |
| public byte[] getDataBytes() { | |
| data.pos = data.end; // Compatibility. Reach end. | |
| return Arrays.copyOfRange(buffer, start, end); | |
| } | |
| private String readStringInternal(byte expectedTag, Charset cs) throws IOException { | |
| if (tag != expectedTag) { | |
| throw new IOException("Incorrect string type " + tag + " is not " + expectedTag); | |
| } | |
| data.pos = data.end; // Compatibility. Reach end. | |
| return new String(buffer, start, end - start, cs); | |
| } | |
| /** | |
| * Returns an ASN.1 STRING value | |
| * | |
| * @return the printable string held in this value | |
| */ | |
| public String getPrintableString() throws IOException { | |
| return readStringInternal(tag_PrintableString, US_ASCII); | |
| } | |
| /** | |
| * Returns an ASN.1 T61 (Teletype) STRING value | |
| * | |
| * @return the teletype string held in this value | |
| */ | |
| public String getT61String() throws IOException { | |
| return readStringInternal(tag_T61String, ISO_8859_1); | |
| } | |
| /** | |
| * Returns an ASN.1 IA5 (ASCII) STRING value | |
| * | |
| * @return the ASCII string held in this value | |
| */ | |
| public String getIA5String() throws IOException { | |
| return readStringInternal(tag_IA5String, US_ASCII); | |
| } | |
| /** | |
| * Returns the ASN.1 BMP (Unicode) STRING value as a Java string. | |
| * | |
| * @return a string corresponding to the encoded BMPString held in | |
| * this value | |
| */ | |
| public String getBMPString() throws IOException { | |
| // BMPString is the same as Unicode in big endian, unmarked format. | |
| return readStringInternal(tag_BMPString, UTF_16BE); | |
| } | |
| /** | |
| * Returns the ASN.1 UTF-8 STRING value as a Java String. | |
| * | |
| * @return a string corresponding to the encoded UTF8String held in | |
| * this value | |
| */ | |
| public String getUTF8String() throws IOException { | |
| return readStringInternal(tag_UTF8String, UTF_8); | |
| } | |
| /** | |
| * Returns the ASN.1 GENERAL STRING value as a Java String. | |
| * | |
| * @return a string corresponding to the encoded GeneralString held in | |
| * this value | |
| */ | |
| public String getGeneralString() throws IOException { | |
| return readStringInternal(tag_GeneralString, US_ASCII); | |
| } | |
| /** | |
| * Returns the ASN.1 UNIVERSAL (UTF-32) STRING value as a Java String. | |
| * | |
| * @return a string corresponding to the encoded UniversalString held in | |
| * this value | |
| */ | |
| public String getUniversalString() throws IOException { | |
| return readStringInternal(tag_UniversalString, new UTF_32BE()); | |
| } | |
| /** | |
| * Checks that the BMPString does not contain any surrogate characters, | |
| * which are outside the Basic Multilingual Plane. | |
| * | |
| * @throws IOException if illegal characters are detected | |
| */ | |
| public void validateBMPString() throws IOException { | |
| String bmpString = getBMPString(); | |
| for (int i = 0; i < bmpString.length(); i++) { | |
| if (Character.isSurrogate(bmpString.charAt(i))) { | |
| throw new IOException( | |
| "Illegal character in BMPString, index: " + i); | |
| } | |
| } | |
| } | |
| /** | |
| * Reads the ASN.1 NULL value | |
| */ | |
| public void getNull() throws IOException { | |
| if (tag != tag_Null) { | |
| throw new IOException("DerValue.getNull, not NULL: " + tag); | |
| } | |
| if (end != start) { | |
| throw new IOException("NULL should contain no data"); | |
| } | |
| } | |
| /** | |
| * Private helper routine to extract time from the der value. | |
| * @param generalized true if Generalized Time is to be read, false | |
| * if UTC Time is to be read. | |
| */ | |
| private Date getTimeInternal(boolean generalized) throws IOException { | |
| /* | |
| * UTC time encoded as ASCII chars: | |
| * YYMMDDhhmmZ | |
| * YYMMDDhhmmssZ | |
| * YYMMDDhhmm+hhmm | |
| * YYMMDDhhmm-hhmm | |
| * YYMMDDhhmmss+hhmm | |
| * YYMMDDhhmmss-hhmm | |
| * UTC Time is broken in storing only two digits of year. | |
| * If YY < 50, we assume 20YY; | |
| * if YY >= 50, we assume 19YY, as per RFC 5280. | |
| * | |
| * Generalized time has a four-digit year and allows any | |
| * precision specified in ISO 8601. However, for our purposes, | |
| * we will only allow the same format as UTC time, except that | |
| * fractional seconds (millisecond precision) are supported. | |
| */ | |
| int year, month, day, hour, minute, second, millis; | |
| String type; | |
| int pos = start; | |
| int len = end - start; | |
| if (generalized) { | |
| type = "Generalized"; | |
| year = 1000 * toDigit(buffer[pos++], type); | |
| year += 100 * toDigit(buffer[pos++], type); | |
| year += 10 * toDigit(buffer[pos++], type); | |
| year += toDigit(buffer[pos++], type); | |
| len -= 2; // For the two extra YY | |
| } else { | |
| type = "UTC"; | |
| year = 10 * toDigit(buffer[pos++], type); | |
| year += toDigit(buffer[pos++], type); | |
| if (year < 50) { // origin 2000 | |
| year += 2000; | |
| } else { | |
| year += 1900; // origin 1900 | |
| } | |
| } | |
| month = 10 * toDigit(buffer[pos++], type); | |
| month += toDigit(buffer[pos++], type); | |
| day = 10 * toDigit(buffer[pos++], type); | |
| day += toDigit(buffer[pos++], type); | |
| hour = 10 * toDigit(buffer[pos++], type); | |
| hour += toDigit(buffer[pos++], type); | |
| minute = 10 * toDigit(buffer[pos++], type); | |
| minute += toDigit(buffer[pos++], type); | |
| len -= 10; // YYMMDDhhmm | |
| /* | |
| * We allow for non-encoded seconds, even though the | |
| * IETF-PKIX specification says that the seconds should | |
| * always be encoded even if it is zero. | |
| */ | |
| millis = 0; | |
| if (len > 2) { | |
| second = 10 * toDigit(buffer[pos++], type); | |
| second += toDigit(buffer[pos++], type); | |
| len -= 2; | |
| // handle fractional seconds (if present) | |
| if (generalized && (buffer[pos] == '.' || buffer[pos] == ',')) { | |
| len --; | |
| if (len == 0) { | |
| throw new IOException("Parse " + type + | |
| " time, empty fractional part"); | |
| } | |
| pos++; | |
| int precision = 0; | |
| while (buffer[pos] != 'Z' && | |
| buffer[pos] != '+' && | |
| buffer[pos] != '-') { | |
| // Validate all digits in the fractional part but | |
| // store millisecond precision only | |
| int thisDigit = toDigit(buffer[pos], type); | |
| precision++; | |
| len--; | |
| if (len == 0) { | |
| throw new IOException("Parse " + type + | |
| " time, invalid fractional part"); | |
| } | |
| pos++; | |
| switch (precision) { | |
| case 1 -> millis += 100 * thisDigit; | |
| case 2 -> millis += 10 * thisDigit; | |
| case 3 -> millis += thisDigit; | |
| } | |
| } | |
| if (precision == 0) { | |
| throw new IOException("Parse " + type + | |
| " time, empty fractional part"); | |
| } | |
| } | |
| } else | |
| second = 0; | |
| if (month == 0 || day == 0 | |
| || month > 12 || day > 31 | |
| || hour >= 24 || minute >= 60 || second >= 60) { | |
| throw new IOException("Parse " + type + " time, invalid format"); | |
| } | |
| /* | |
| * Generalized time can theoretically allow any precision, | |
| * but we're not supporting that. | |
| */ | |
| CalendarSystem gcal = CalendarSystem.getGregorianCalendar(); | |
| CalendarDate date = gcal.newCalendarDate(null); // no time zone | |
| date.setDate(year, month, day); | |
| date.setTimeOfDay(hour, minute, second, millis); | |
| long time = gcal.getTime(date); | |
| /* | |
| * Finally, "Z" or "+hhmm" or "-hhmm" ... offsets change hhmm | |
| */ | |
| if (! (len == 1 || len == 5)) { | |
| throw new IOException("Parse " + type + " time, invalid offset"); | |
| } | |
| int hr, min; | |
| switch (buffer[pos++]) { | |
| case '+': | |
| if (len != 5) { | |
| throw new IOException("Parse " + type + " time, invalid offset"); | |
| } | |
| hr = 10 * toDigit(buffer[pos++], type); | |
| hr += toDigit(buffer[pos++], type); | |
| min = 10 * toDigit(buffer[pos++], type); | |
| min += toDigit(buffer[pos++], type); | |
| if (hr >= 24 || min >= 60) { | |
| throw new IOException("Parse " + type + " time, +hhmm"); | |
| } | |
| time -= ((hr * 60L) + min) * 60 * 1000; | |
| break; | |
| case '-': | |
| if (len != 5) { | |
| throw new IOException("Parse " + type + " time, invalid offset"); | |
| } | |
| hr = 10 * toDigit(buffer[pos++], type); | |
| hr += toDigit(buffer[pos++], type); | |
| min = 10 * toDigit(buffer[pos++], type); | |
| min += toDigit(buffer[pos++], type); | |
| if (hr >= 24 || min >= 60) { | |
| throw new IOException("Parse " + type + " time, -hhmm"); | |
| } | |
| time += ((hr * 60L) + min) * 60 * 1000; | |
| break; | |
| case 'Z': | |
| if (len != 1) { | |
| throw new IOException("Parse " + type + " time, invalid format"); | |
| } | |
| break; | |
| default: | |
| throw new IOException("Parse " + type + " time, garbage offset"); | |
| } | |
| return new Date(time); | |
| } | |
| /** | |
| * Converts byte (represented as a char) to int. | |
| * @throws IOException if integer is not a valid digit in the specified | |
| * radix (10) | |
| */ | |
| private static int toDigit(byte b, String type) throws IOException { | |
| if (b < '0' || b > '9') { | |
| throw new IOException("Parse " + type + " time, invalid format"); | |
| } | |
| return b - '0'; | |
| } | |
| /** | |
| * Determines whether Date was encoded as UTC or Generalized time and | |
| * calls getUTCTime or getGeneralizedTime accordingly | |
| */ | |
| public Date getTime() throws IOException { | |
| return (tag == tag_UtcTime) ? getUTCTime() : getGeneralizedTime(); | |
| } | |
| /** | |
| * Returns a Date if the DerValue is UtcTime. | |
| * | |
| * @return the Date held in this DER value | |
| */ | |
| public Date getUTCTime() throws IOException { | |
| if (tag != tag_UtcTime) { | |
| throw new IOException("DerValue.getUTCTime, not a UtcTime: " + tag); | |
| } | |
| if (end - start < 11 || end - start > 17) | |
| throw new IOException("DER UTC Time length error"); | |
| data.pos = data.end; // Compatibility. Reach end. | |
| return getTimeInternal(false); | |
| } | |
| /** | |
| * Returns a Date if the DerValue is GeneralizedTime. | |
| * | |
| * @return the Date held in this DER value | |
| */ | |
| public Date getGeneralizedTime() throws IOException { | |
| if (tag != tag_GeneralizedTime) { | |
| throw new IOException( | |
| "DerValue.getGeneralizedTime, not a GeneralizedTime: " + tag); | |
| } | |
| if (end - start < 13) | |
| throw new IOException("DER Generalized Time length error"); | |
| data.pos = data.end; // Compatibility. Reach end. | |
| return getTimeInternal(true); | |
| } | |
| /** | |
| * Bitwise equality comparison. DER encoded values have a single | |
| * encoding, so that bitwise equality of the encoded values is an | |
| * efficient way to establish equivalence of the unencoded values. | |
| * | |
| * @param o the object being compared with this one | |
| */ | |
| @Override | |
| public boolean equals(Object o) { | |
| if (this == o) { | |
| return true; | |
| } | |
| if (!(o instanceof DerValue other)) { | |
| return false; | |
| } | |
| if (tag != other.tag) { | |
| return false; | |
| } | |
| if (buffer == other.buffer && start == other.start && end == other.end) { | |
| return true; | |
| } | |
| return Arrays.equals(buffer, start, end, other.buffer, other.start, other.end); | |
| } | |
| /** | |
| * Returns a printable representation of the value. | |
| * | |
| * @return printable representation of the value | |
| */ | |
| @Override | |
| public String toString() { | |
| return String.format("DerValue(%02x, %s, %d, %d)", | |
| 0xff & tag, HexFormat.of().withUpperCase().formatHex(buffer), | |
| start, end); | |
| } | |
| /** | |
| * Returns a DER-encoded value, such that if it's passed to the | |
| * DerValue constructor, a value equivalent to "this" is returned. | |
| * | |
| * @return DER-encoded value, including tag and length. | |
| */ | |
| public byte[] toByteArray() { | |
| data.pos = data.start; // Compatibility. At head. | |
| // Minimize content duplication by writing out tag and length only | |
| DerOutputStream out = new DerOutputStream(); | |
| out.write(tag); | |
| out.putLength(end - start); | |
| int headLen = out.size(); | |
| byte[] result = Arrays.copyOf(out.buf(), end - start + headLen); | |
| System.arraycopy(buffer, start, result, headLen, end - start); | |
| return result; | |
| } | |
| /** | |
| * For "set" and "sequence" types, this function may be used | |
| * to return a DER stream of the members of the set or sequence. | |
| * This operation is not supported for primitive types such as | |
| * integers or bit strings. | |
| */ | |
| public DerInputStream toDerInputStream() throws IOException { | |
| if (tag == tag_Sequence || tag == tag_Set) | |
| return data; | |
| throw new IOException("toDerInputStream rejects tag type " + tag); | |
| } | |
| /** | |
| * Get the length of the encoded value. | |
| */ | |
| public int length() { | |
| return end - start; | |
| } | |
| /** | |
| * Determine if a character is one of the permissible characters for | |
| * PrintableString: | |
| * A-Z, a-z, 0-9, space, apostrophe (39), left and right parentheses, | |
| * plus sign, comma, hyphen, period, slash, colon, equals sign, | |
| * and question mark. | |
| * | |
| * Characters that are *not* allowed in PrintableString include | |
| * exclamation point, quotation mark, number sign, dollar sign, | |
| * percent sign, ampersand, asterisk, semicolon, less than sign, | |
| * greater than sign, at sign, left and right square brackets, | |
| * backslash, circumflex (94), underscore, back quote (96), | |
| * left and right curly brackets, vertical line, tilde, | |
| * and the control codes (0-31 and 127). | |
| * | |
| * This list is based on X.680 (the ASN.1 spec). | |
| */ | |
| public static boolean isPrintableStringChar(char ch) { | |
| if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || | |
| (ch >= '0' && ch <= '9')) { | |
| return true; | |
| } else { | |
| switch (ch) { | |
| case ' ': /* space */ | |
| case '\'': /* apostrophe */ | |
| case '(': /* left paren */ | |
| case ')': /* right paren */ | |
| case '+': /* plus */ | |
| case ',': /* comma */ | |
| case '-': /* hyphen */ | |
| case '.': /* period */ | |
| case '/': /* slash */ | |
| case ':': /* colon */ | |
| case '=': /* equals */ | |
| case '?': /* question mark */ | |
| return true; | |
| default: | |
| return false; | |
| } | |
| } | |
| } | |
| /** | |
| * Create the tag of the attribute. | |
| * | |
| * @param tagClass 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 static byte createTag(byte tagClass, boolean form, byte val) { | |
| if (val < 0 || val > 30) { | |
| throw new IllegalArgumentException("Tag number over 30 is not supported"); | |
| } | |
| byte tag = (byte)(tagClass | val); | |
| if (form) { | |
| tag |= (byte)0x20; | |
| } | |
| return (tag); | |
| } | |
| /** | |
| * Set the tag of the attribute. Commonly used to reset the | |
| * tag value used for IMPLICIT encodings. | |
| * | |
| * This method should be avoided, consider using withTag() instead. | |
| * | |
| * @param tag the tag value | |
| */ | |
| public void resetTag(byte tag) { | |
| this.tag = tag; | |
| } | |
| /** | |
| * Returns a new DerValue with a different tag. This method is used | |
| * to convert a DerValue decoded from an IMPLICIT encoding to its real | |
| * tag. The content is not checked against the tag in this method. | |
| * | |
| * @param newTag the new tag | |
| * @return a new DerValue | |
| */ | |
| public DerValue withTag(byte newTag) { | |
| return new DerValue(newTag, buffer, start, end, allowBER); | |
| } | |
| /** | |
| * {@return a hashcode for this DerValue} | |
| */ | |
| @Override | |
| public int hashCode() { | |
| return ArraysSupport.hashCode(buffer, start, end - start, tag); | |
| } | |
| /** | |
| * Reads the sub-values in a constructed DerValue. | |
| * | |
| * @param expectedTag the expected tag, or zero if we don't check. | |
| * This is useful when this DerValue is IMPLICIT. | |
| * @param startLen estimated number of sub-values | |
| * @return the sub-values in an array | |
| */ | |
| public DerValue[] subs(byte expectedTag, int startLen) throws IOException { | |
| if (expectedTag != 0 && expectedTag != tag) { | |
| throw new IOException("Not the correct tag"); | |
| } | |
| List<DerValue> result = new ArrayList<>(startLen); | |
| DerInputStream dis = data(); | |
| while (dis.available() > 0) { | |
| result.add(dis.getDerValue()); | |
| } | |
| return result.toArray(new DerValue[0]); | |
| } | |
| public void clear() { | |
| Arrays.fill(buffer, start, end, (byte)0); | |
| } | |
| } |