sun.security.util.DerValue Maven / Gradle / Ivy
/*
* Copyright (c) 1996, 2021, 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 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.*;
/**
* 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.
*
* All DER-encoded data are triples {type, length, data}. This
* class represents such tagged values as they have been read (or constructed),
* and provides structured access to the encoded data.
*
*
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_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 "GenerallString" 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 an 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
* @returns 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);
}
/**
* 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.
*/
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
*/
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) throws IOException {
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;
};
}
/**
* 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");
}
int numOfPadBits = buffer[start];
if ((numOfPadBits < 0) || (numOfPadBits > 7)) {
throw new IOException("Invalid number of padding bits");
}
// minus the first byte which indicates the number of padding bits
byte[] retval = Arrays.copyOfRange(buffer, start + 1, end);
if (numOfPadBits != 0) {
// get rid of the padding bits
retval[end - start - 2] &= (0xff << numOfPadBits);
}
data.pos = data.end; // Compatibility. Reach end.
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];
if ((numOfPadBits < 0) || (numOfPadBits > 7)) {
throw new IOException("Invalid number of padding bits");
}
if (end == start + 1) {
return new BitArray(0);
} else {
return new BitArray(((end - start - 1) << 3) - numOfPadBits,
Arrays.copyOfRange(buffer, start + 1, end));
}
}
/**
* Helper routine to return all the bytes contained in the
* DerInputStream associated with this object.
*/
public byte[] getDataBytes() throws IOException {
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());
}
/**
* 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 * 60) + 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 * 60) + 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';
}
/**
* 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)) {
return false;
}
DerValue other = (DerValue) o;
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, 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() throws IOException {
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);
}
/**
* Returns a hashcode for this DerValue.
*
* @return a hashcode for this DerValue.
*/
@Override
public int hashCode() {
int result = tag;
for (int i = start; i < end; i++) {
result = 31 * result + buffer[i];
}
return result;
}
/**
* 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
*/
DerValue[] subs(byte expectedTag, int startLen) throws IOException {
if (expectedTag != 0 && expectedTag != tag) {
throw new IOException("Not the correct tag");
}
List 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);
}
}