| package com.android.javacard.keymaster; |
| |
| import com.android.javacard.seprovider.KMException; |
| import javacard.framework.JCSystem; |
| import javacard.framework.Util; |
| |
| /** |
| * This is a utility class that helps in parsing the PKCS8 encoded RSA and EC keys, certificate |
| * subject, subjectPublicKey info and ECDSA signatures. |
| */ |
| public class KMAsn1Parser { |
| |
| // Below are the ASN.1 tag types |
| public static final byte ASN1_OCTET_STRING = 0x04; |
| public static final byte ASN1_SEQUENCE = 0x30; |
| public static final byte ASN1_SET = 0x31; |
| public static final byte ASN1_INTEGER = 0x02; |
| public static final byte OBJECT_IDENTIFIER = 0x06; |
| public static final byte ASN1_A0_TAG = (byte) 0xA0; |
| public static final byte ASN1_A1_TAG = (byte) 0xA1; |
| public static final byte ASN1_BIT_STRING = 0x03; |
| |
| public static final byte ASN1_UTF8_STRING = 0x0C; |
| public static final byte ASN1_TELETEX_STRING = 0x14; |
| public static final byte ASN1_PRINTABLE_STRING = 0x13; |
| public static final byte ASN1_UNIVERSAL_STRING = 0x1C; |
| public static final byte ASN1_BMP_STRING = 0x1E; |
| public static final byte IA5_STRING = 0x16; |
| // OID of the EC P256 curve 1.2.840.10045.3.1.7 |
| public static final byte[] EC_CURVE = { |
| 0x06, 0x08, 0x2a, (byte) 0x86, 0x48, (byte) 0xce, 0x3d, 0x03, 0x01, 0x07 |
| }; |
| // Constant for rsaEncryption pkcs#1 (1.2.840.113549.1.1.1) and NULL |
| public static final byte[] RSA_ALGORITHM = { |
| 0x06, |
| 0x09, |
| 0x2A, |
| (byte) 0x86, |
| 0x48, |
| (byte) 0x86, |
| (byte) 0xF7, |
| 0x0D, |
| 0x01, |
| 0x01, |
| 0x01, |
| 0x05, |
| 0x00 |
| }; |
| // Constant for ecPublicKey (1.2.840.10045.2.1) and prime256v1 (1.2.840.10045.3.1.7) |
| public static final byte[] EC_ALGORITHM = { |
| 0x06, |
| 0x07, |
| 0x2a, |
| (byte) 0x86, |
| 0x48, |
| (byte) 0xce, |
| 0x3d, |
| 0x02, |
| 0x01, |
| 0x06, |
| 0x08, |
| 0x2a, |
| (byte) 0x86, |
| 0x48, |
| (byte) 0xce, |
| 0x3d, |
| 0x03, |
| 0x01, |
| 0x07 |
| }; |
| // The maximum length of email id attribute. |
| public static final short MAX_EMAIL_ADD_LEN = 255; |
| // Datatable offsets. |
| private static final byte DATA_START_OFFSET = 0; |
| private static final byte DATA_LENGTH_OFFSET = 1; |
| private static final byte DATA_CURSOR_OFFSET = 2; |
| // This array contains the last byte of OID for each oid type. |
| // The first 4 bytes are common as shown above in COMMON_OID |
| private static final byte[] attributeOIds = { |
| 0x03, /* commonName COMMON_OID.3 */ 0x04, /* surName COMMON_OID.4*/ |
| 0x05, /* serialNumber COMMON_OID.5 */ 0x06, /* countryName COMMON_OID.6 */ |
| 0x07, /* locality COMMON_OID.7 */ 0x08, /* stateOrProviince COMMON_OID.8 */ |
| 0x0A, /* organizationName COMMON_OID.10 */ 0x0B, /* organizationalUnitName COMMON_OID.11 */ |
| 0x0C, /* title COMMON_OID.10 */ 0x29, /* name COMMON_OID.41 */ |
| 0x2A, /* givenName COMMON_OID.42 */ 0x2B, /* initials COMMON_OID.43 */ |
| 0x2C, /* generationQualifier COMMON_OID.44 */ 0x2E, /* dnQualifer COMMON_OID.46 */ |
| 0x41, /* pseudonym COMMON_OID.65 */ |
| }; |
| // https://datatracker.ietf.org/doc/html/rfc5280, RFC 5280, Page 124 |
| // TODO Specification does not mention about the DN_QUALIFIER_OID max length. |
| // So the max limit is set at 64. |
| // For name the RFC 5280 supports up to 32768, as Javacard doesn't support |
| // that much length, the max limit for name is set to 128. |
| private static final byte[] attributeValueMaxLen = { |
| 0x40, /* 1-64 commonName */ |
| 0x28, /* 1-40 surname */ |
| 0x40, /* 1-64 serial */ |
| 0x02, /* 1-2 country */ |
| (byte) 0x80, /* 1-128 locality */ |
| (byte) 0x80, /* 1-128 state */ |
| 0x40, /* 1-64 organization */ |
| 0x40, /* 1-64 organization unit*/ |
| 0x40, /* 1-64 title */ |
| 0x29, /* 1-128 name */ |
| 0x10, /* 1-16 givenName */ |
| 0x05, /* 1-5 initials */ |
| 0x03, /* 1-3 gen qualifier */ |
| 0x40, /* 1-64 dn-qualifier */ |
| (byte) 0x80 /* 1-128 pseudonym */ |
| }; |
| private static KMAsn1Parser inst; |
| // https://datatracker.ietf.org/doc/html/rfc5280, RFC 5280, Page 21 |
| // 2.5.4 |
| public byte[] COMMON_OID = new byte[] {0x06, 0x03, 0x55, 0x04}; |
| public byte[] EMAIL_ADDRESS_OID = |
| new byte[] { |
| 0x06, 0x09, 0x2A, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xF7, 0x0D, 0x01, 0x09, 0x01 |
| }; |
| private byte[] data; |
| private short[] dataInfo; |
| |
| private KMAsn1Parser() { |
| dataInfo = JCSystem.makeTransientShortArray((short) 3, JCSystem.CLEAR_ON_RESET); |
| dataInfo[DATA_START_OFFSET] = 0; |
| dataInfo[DATA_LENGTH_OFFSET] = 0; |
| dataInfo[DATA_CURSOR_OFFSET] = 0; |
| } |
| |
| public static KMAsn1Parser instance() { |
| if (inst == null) { |
| inst = new KMAsn1Parser(); |
| } |
| return inst; |
| } |
| |
| public short decodeRsa(short blob) { |
| init(blob); |
| decodeCommon((short) 0, RSA_ALGORITHM); |
| return decodeRsaPrivateKey((short) 0); |
| } |
| |
| public short decodeEc(short blob) { |
| init(blob); |
| decodeCommon((short) 0, EC_ALGORITHM); |
| return decodeEcPrivateKey((short) 1); |
| } |
| |
| /* |
| Name ::= CHOICE { -- only one possibility for now -- |
| rdnSequence RDNSequence } |
| RDNSequence ::= SEQUENCE OF RelativeDistinguishedName |
| RelativeDistinguishedName ::= |
| SET SIZE (1..MAX) OF AttributeTypeAndValue |
| AttributeTypeAndValue ::= SEQUENCE { |
| type AttributeType, |
| value AttributeValue } |
| AttributeType ::= OBJECT IDENTIFIER |
| AttributeValue ::= ANY -- DEFINED BY AttributeType |
| */ |
| public void validateDerSubject(short blob) { |
| init(blob); |
| header(ASN1_SEQUENCE); |
| while (dataInfo[DATA_CURSOR_OFFSET] |
| < ((short) (dataInfo[DATA_START_OFFSET] + dataInfo[DATA_LENGTH_OFFSET]))) { |
| header(ASN1_SET); |
| header(ASN1_SEQUENCE); |
| // Parse and validate OBJECT-IDENTIFIER and Value fields |
| // Cursor is incremented in validateAttributeTypeAndValue. |
| validateAttributeTypeAndValue(); |
| } |
| } |
| |
| public short decodeEcSubjectPublicKeyInfo(short blob) { |
| init(blob); |
| header(ASN1_SEQUENCE); |
| short len = header(ASN1_SEQUENCE); |
| short ecPublicInfo = KMByteBlob.instance(len); |
| getBytes(ecPublicInfo); |
| if (Util.arrayCompare( |
| KMByteBlob.cast(ecPublicInfo).getBuffer(), |
| KMByteBlob.cast(ecPublicInfo).getStartOff(), |
| EC_ALGORITHM, |
| (short) 0, |
| KMByteBlob.cast(ecPublicInfo).length()) |
| != 0) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| len = header(ASN1_BIT_STRING); |
| if (len < 1) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| // TODO need to handle if unused bits are not zero |
| byte unusedBits = getByte(); |
| if (unusedBits != 0) { |
| KMException.throwIt(KMError.UNIMPLEMENTED); |
| } |
| short pubKey = KMByteBlob.instance((short) (len - 1)); |
| getBytes(pubKey); |
| return pubKey; |
| } |
| |
| // Seq[Int,Int,Int,Int,<ignore rest>] |
| public short decodeRsaPrivateKey(short version) { |
| short resp = KMArray.instance((short) 3); |
| header(ASN1_OCTET_STRING); |
| header(ASN1_SEQUENCE); |
| short len = header(ASN1_INTEGER); |
| if (len != 1) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| short ver = getByte(); |
| if (ver != version) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| len = header(ASN1_INTEGER); |
| short modulus = KMByteBlob.instance(len); |
| getBytes(modulus); |
| updateRsaKeyBuffer(modulus); |
| len = header(ASN1_INTEGER); |
| short pubKey = KMByteBlob.instance(len); |
| getBytes(pubKey); |
| len = header(ASN1_INTEGER); |
| short privKey = KMByteBlob.instance(len); |
| getBytes(privKey); |
| updateRsaKeyBuffer(privKey); |
| KMArray.cast(resp).add((short) 0, modulus); |
| KMArray.cast(resp).add((short) 1, pubKey); |
| KMArray.cast(resp).add((short) 2, privKey); |
| return resp; |
| } |
| |
| private void updateRsaKeyBuffer(short blob) { |
| byte[] buffer = KMByteBlob.cast(blob).getBuffer(); |
| short startOff = KMByteBlob.cast(blob).getStartOff(); |
| short len = KMByteBlob.cast(blob).length(); |
| if (0 == buffer[startOff] && len > 256) { |
| KMByteBlob.cast(blob).setStartOff(++startOff); |
| KMByteBlob.cast(blob).setLength(--len); |
| } |
| } |
| |
| private short readEcdsa256SigIntegerHeader() { |
| short len = header(ASN1_INTEGER); |
| if (len == 33) { |
| if (0 != getByte()) { |
| KMException.throwIt(KMError.INVALID_DATA); |
| } |
| len--; |
| } else if (len > 33) { |
| KMException.throwIt(KMError.INVALID_DATA); |
| } |
| return len; |
| } |
| |
| // Seq [Int, Int] |
| public short decodeEcdsa256Signature(short blob, byte[] scratchPad, short scratchPadOff) { |
| init(blob); |
| short len = header(ASN1_SEQUENCE); |
| len = readEcdsa256SigIntegerHeader(); |
| // concatenate r and s in the buffer (r||s) |
| Util.arrayFillNonAtomic(scratchPad, scratchPadOff, (short) 64, (byte) 0); |
| // read r |
| getBytes(scratchPad, (short) (scratchPadOff + 32 - len), len); |
| len = readEcdsa256SigIntegerHeader(); |
| // read s |
| getBytes(scratchPad, (short) (scratchPadOff + 64 - len), len); |
| return (short) 64; |
| } |
| |
| // Seq [Int, Blob] |
| public void decodeCommon(short version, byte[] alg) { |
| short len = header(ASN1_SEQUENCE); |
| len = header(ASN1_INTEGER); |
| if (len != 1) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| short ver = getByte(); |
| if (ver != version) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| len = header(ASN1_SEQUENCE); |
| short blob = KMByteBlob.instance(len); |
| getBytes(blob); |
| if (Util.arrayCompare( |
| KMByteBlob.cast(blob).getBuffer(), |
| KMByteBlob.cast(blob).getStartOff(), |
| alg, |
| (short) 0, |
| KMByteBlob.cast(blob).length()) |
| != 0) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| } |
| |
| // Seq[Int,blob,blob] |
| public short decodeEcPrivateKey(short version) { |
| short resp = KMArray.instance((short) 2); |
| header(ASN1_OCTET_STRING); |
| header(ASN1_SEQUENCE); |
| short len = header(ASN1_INTEGER); |
| if (len != 1) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| short ver = getByte(); |
| if (ver != version) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| len = header(ASN1_OCTET_STRING); |
| short privKey = KMByteBlob.instance(len); |
| getBytes(privKey); |
| validateTag0IfPresent(); |
| header(ASN1_A1_TAG); |
| len = header(ASN1_BIT_STRING); |
| if (len < 1) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| // TODO need to handle if unused bits are not zero |
| byte unusedBits = getByte(); |
| if (unusedBits != 0) { |
| KMException.throwIt(KMError.UNIMPLEMENTED); |
| } |
| short pubKey = KMByteBlob.instance((short) (len - 1)); |
| getBytes(pubKey); |
| KMArray.cast(resp).add((short) 0, pubKey); |
| KMArray.cast(resp).add((short) 1, privKey); |
| return resp; |
| } |
| |
| private void validateTag0IfPresent() { |
| if (data[dataInfo[DATA_CURSOR_OFFSET]] != ASN1_A0_TAG) { |
| return; |
| } |
| short len = header(ASN1_A0_TAG); |
| if (len != EC_CURVE.length) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| if (Util.arrayCompare(data, dataInfo[DATA_CURSOR_OFFSET], EC_CURVE, (short) 0, len) != 0) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| incrementCursor(len); |
| } |
| |
| private void validateAttributeTypeAndValue() { |
| // First byte should be OBJECT_IDENTIFIER, otherwise it is not well-formed DER Subject. |
| if (data[dataInfo[DATA_CURSOR_OFFSET]] != OBJECT_IDENTIFIER) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| // Check if the OID matches the email address |
| if ((Util.arrayCompare( |
| data, |
| dataInfo[DATA_CURSOR_OFFSET], |
| EMAIL_ADDRESS_OID, |
| (short) 0, |
| (short) EMAIL_ADDRESS_OID.length) |
| == 0)) { |
| incrementCursor((short) EMAIL_ADDRESS_OID.length); |
| // Validate the length of the attribute value. |
| if (getByte() != IA5_STRING) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| short emailLength = getLength(); |
| if (emailLength <= 0 && emailLength > MAX_EMAIL_ADD_LEN) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| incrementCursor(emailLength); |
| return; |
| } |
| // Check other OIDs. |
| for (short i = 0; i < (short) attributeOIds.length; i++) { |
| if ((Util.arrayCompare( |
| data, |
| dataInfo[DATA_CURSOR_OFFSET], |
| COMMON_OID, |
| (short) 0, |
| (short) COMMON_OID.length) |
| == 0) |
| && (attributeOIds[i] |
| == data[(short) (dataInfo[DATA_CURSOR_OFFSET] + COMMON_OID.length)])) { |
| incrementCursor((short) (COMMON_OID.length + 1)); |
| // Validate the length of the attribute value. |
| short tag = getByte(); |
| if (tag != ASN1_UTF8_STRING |
| && tag != ASN1_TELETEX_STRING |
| && tag != ASN1_PRINTABLE_STRING |
| && tag != ASN1_UNIVERSAL_STRING |
| && tag != ASN1_BMP_STRING) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| short attrValueLength = getLength(); |
| if (attrValueLength <= 0 && attrValueLength > attributeValueMaxLen[i]) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| incrementCursor(attrValueLength); |
| return; |
| } |
| } |
| // If no match is found above then move the cursor to next element. |
| getByte(); // Move Cursor by one byte (OID) |
| incrementCursor(getLength()); // Move cursor to AtrributeTag |
| getByte(); // Move cursor to AttributeValue |
| incrementCursor(getLength()); // Move cursor to next SET element |
| } |
| |
| private short header(short tag) { |
| short t = getByte(); |
| if (t != tag) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| return getLength(); |
| } |
| |
| private byte getByte() { |
| byte d = data[dataInfo[DATA_CURSOR_OFFSET]]; |
| incrementCursor((short) 1); |
| return d; |
| } |
| |
| private short getShort() { |
| short d = Util.getShort(data, dataInfo[DATA_CURSOR_OFFSET]); |
| incrementCursor((short) 2); |
| return d; |
| } |
| |
| private void getBytes(short blob) { |
| short len = KMByteBlob.cast(blob).length(); |
| Util.arrayCopyNonAtomic( |
| data, |
| dataInfo[DATA_CURSOR_OFFSET], |
| KMByteBlob.cast(blob).getBuffer(), |
| KMByteBlob.cast(blob).getStartOff(), |
| len); |
| incrementCursor(len); |
| } |
| |
| private void getBytes(byte[] buffer, short offset, short len) { |
| Util.arrayCopyNonAtomic(data, dataInfo[DATA_CURSOR_OFFSET], buffer, offset, len); |
| incrementCursor(len); |
| } |
| |
| private short getLength() { |
| byte len = getByte(); |
| if (len >= 0) { |
| return len; |
| } |
| len = (byte) (len & 0x7F); |
| if (len == 1) { |
| return (short) (getByte() & 0xFF); |
| } else if (len == 2) { |
| return getShort(); |
| } else { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| return KMType.INVALID_VALUE; // should not come here |
| } |
| |
| public void init(short blob) { |
| data = KMByteBlob.cast(blob).getBuffer(); |
| dataInfo[DATA_START_OFFSET] = KMByteBlob.cast(blob).getStartOff(); |
| dataInfo[DATA_LENGTH_OFFSET] = KMByteBlob.cast(blob).length(); |
| dataInfo[DATA_CURSOR_OFFSET] = dataInfo[DATA_START_OFFSET]; |
| } |
| |
| public void incrementCursor(short n) { |
| dataInfo[DATA_CURSOR_OFFSET] += n; |
| if (dataInfo[DATA_CURSOR_OFFSET] |
| > ((short) (dataInfo[DATA_START_OFFSET] + dataInfo[DATA_LENGTH_OFFSET]))) { |
| KMException.throwIt(KMError.UNKNOWN_ERROR); |
| } |
| } |
| } |