diff --git a/README.md b/README.md index 56d9ebb..15c53b4 100644 --- a/README.md +++ b/README.md @@ -62,15 +62,14 @@ try(ApkFile apkFile = new ApkFile(new File(filePath))) { } ``` -##### 4. Get certificate and verify apk signature +##### 4. Get Apk Sign info + +To get apk signer certificate info and other messages, using: ```java try(ApkFile apkFile = new ApkFile(new File(filePath))) { - ApkSignStatus signStatus = apkFile.verifyApk(); - List certs = apkFile.getCertificateMetas(); - for (CertificateMeta certificateMeta : certs) { - System.out.println(certificateMeta.getSignAlgorithm()); - } + List signers = apkFile.getApkSingers(); // apk v1 signers + List v2signers = apkFile.getApkV2Singers(); // apk v2 signers } ``` diff --git a/pom.xml b/pom.xml index c86e825..547b154 100644 --- a/pom.xml +++ b/pom.xml @@ -61,11 +61,13 @@ org.bouncycastle bcprov-jdk15on 1.58 + true org.bouncycastle bcpkix-jdk15on 1.58 + true junit diff --git a/src/main/java/net/dongliu/apk/parser/AbstractApkFile.java b/src/main/java/net/dongliu/apk/parser/AbstractApkFile.java index 0e0f366..fdb7633 100644 --- a/src/main/java/net/dongliu/apk/parser/AbstractApkFile.java +++ b/src/main/java/net/dongliu/apk/parser/AbstractApkFile.java @@ -127,7 +127,7 @@ public List getApkSingers() throws IOException, CertificateException private void parseCertificates() throws IOException, CertificateException { this.apkSigners = new ArrayList<>(); for (CertificateFile file : getAllCertificateData()) { - CertificateParser parser = new CertificateParser(file.getData()); + CertificateParser parser = CertificateParser.getInstance(file.getData()); List certificateMetas = parser.parse(); apkSigners.add(new ApkSigner(file.getPath(), certificateMetas)); } @@ -339,9 +339,11 @@ private void parseResourceTable() throws IOException { } /** - * Check apk sign. - * TODO:Now only use jar-sign, apk-signing v2 not supported + * Check apk sign. This method only use apk v1 scheme verifier + * + * @deprecated using google official ApkVerifier of apksig lib instead. */ + @Deprecated public abstract ApkSignStatus verifyApk() throws IOException; @Override diff --git a/src/main/java/net/dongliu/apk/parser/ApkFile.java b/src/main/java/net/dongliu/apk/parser/ApkFile.java index 98a200f..32fe13d 100644 --- a/src/main/java/net/dongliu/apk/parser/ApkFile.java +++ b/src/main/java/net/dongliu/apk/parser/ApkFile.java @@ -71,7 +71,13 @@ protected ByteBuffer fileData() throws IOException { } + /** + * {@inheritDoc} + * + * @deprecated using google official ApkVerifier of apksig lib instead. + */ @Override + @Deprecated public ApkSignStatus verifyApk() throws IOException { ZipEntry entry = zf.getEntry("META-INF/MANIFEST.MF"); if (entry == null) { diff --git a/src/main/java/net/dongliu/apk/parser/ApkParsers.java b/src/main/java/net/dongliu/apk/parser/ApkParsers.java index 0a98ad5..e0cc1cf 100644 --- a/src/main/java/net/dongliu/apk/parser/ApkParsers.java +++ b/src/main/java/net/dongliu/apk/parser/ApkParsers.java @@ -13,6 +13,20 @@ */ public class ApkParsers { + private static boolean useBouncyCastle; + + public static boolean useBouncyCastle() { + return useBouncyCastle; + } + + /** + * Use BouncyCastle instead of JSSE to parse X509 certificate. + * If want to use BouncyCastle, you will also need to add bcprov and bcpkix lib to your project. + */ + public static void useBouncyCastle(boolean useBouncyCastle) { + ApkParsers.useBouncyCastle = useBouncyCastle; + } + /** * Get apk meta info for apk file * diff --git a/src/main/java/net/dongliu/apk/parser/Main.java b/src/main/java/net/dongliu/apk/parser/Main.java index 3c180c8..d10116b 100644 --- a/src/main/java/net/dongliu/apk/parser/Main.java +++ b/src/main/java/net/dongliu/apk/parser/Main.java @@ -11,7 +11,7 @@ public class Main { public static void main(String[] args) throws IOException, CertificateException { try (ApkFile apkFile = new ApkFile(args[0])) { - System.out.println(apkFile.getApkV2Singers().get(0).getCertificateMetas()); + System.out.println(apkFile.getApkSingers().get(0).getCertificateMetas()); } } } diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1BerParser.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1BerParser.java new file mode 100644 index 0000000..6388a32 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1BerParser.java @@ -0,0 +1,636 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +import net.dongliu.apk.parser.cert.asn1.ber.*; +import net.dongliu.apk.parser.utils.Buffers; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Parser of ASN.1 BER-encoded structures. + *

+ *

Structure is described to the parser by providing a class annotated with {@link Asn1Class}, + * containing fields annotated with {@link Asn1Field}. + */ +public final class Asn1BerParser { + private Asn1BerParser() { + } + + /** + * Returns the ASN.1 structure contained in the BER encoded input. + * + * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer + * is advanced to the first position following the end of the consumed structure. + * @param containerClass class describing the structure of the input. The class must meet the + * following requirements: + *

    + *
  • The class must be annotated with {@link Asn1Class}.
  • + *
  • The class must expose a public no-arg constructor.
  • + *
  • Member fields of the class which are populated with parsed input must be + * annotated with {@link Asn1Field} and be public and non-final.
  • + *
+ * @throws Asn1DecodingException if the input could not be decoded into the specified Java + * object + */ + public static T parse(ByteBuffer encoded, Class containerClass) + throws Asn1DecodingException { + BerDataValue containerDataValue; + try { + containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue(); + } catch (BerDataValueFormatException e) { + throw new Asn1DecodingException("Failed to decode top-level data value", e); + } + if (containerDataValue == null) { + throw new Asn1DecodingException("Empty input"); + } + return parse(containerDataValue, containerClass); + } + + /** + * Returns the implicit {@code SET OF} contained in the provided ASN.1 BER input. Implicit means + * that this method does not care whether the tag number of this data structure is + * {@code SET OF} and whether the tag class is {@code UNIVERSAL}. + *

+ *

Note: The returned type is {@link List} rather than {@link java.util.Set} because ASN.1 + * SET may contain duplicate elements. + * + * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer + * is advanced to the first position following the end of the consumed structure. + * @param elementClass class describing the structure of the values/elements contained in this + * container. The class must meet the following requirements: + *

    + *
  • The class must be annotated with {@link Asn1Class}.
  • + *
  • The class must expose a public no-arg constructor.
  • + *
  • Member fields of the class which are populated with parsed input must be + * annotated with {@link Asn1Field} and be public and non-final.
  • + *
+ * @throws Asn1DecodingException if the input could not be decoded into the specified Java + * object + */ + public static List parseImplicitSetOf(ByteBuffer encoded, Class elementClass) + throws Asn1DecodingException { + BerDataValue containerDataValue; + try { + containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue(); + } catch (BerDataValueFormatException e) { + throw new Asn1DecodingException("Failed to decode top-level data value", e); + } + if (containerDataValue == null) { + throw new Asn1DecodingException("Empty input"); + } + return parseSetOf(containerDataValue, elementClass); + } + + private static T parse(BerDataValue container, Class containerClass) + throws Asn1DecodingException { + if (container == null) { + throw new NullPointerException("container == null"); + } + if (containerClass == null) { + throw new NullPointerException("containerClass == null"); + } + + Asn1Type dataType = getContainerAsn1Type(containerClass); + switch (dataType) { + case CHOICE: + return parseChoice(container, containerClass); + + case SEQUENCE: { + int expectedTagClass = BerEncoding.TAG_CLASS_UNIVERSAL; + int expectedTagNumber = BerEncoding.getTagNumber(dataType); + if ((container.getTagClass() != expectedTagClass) + || (container.getTagNumber() != expectedTagNumber)) { + throw new Asn1UnexpectedTagException( + "Unexpected data value read as " + containerClass.getName() + + ". Expected " + BerEncoding.tagClassAndNumberToString( + expectedTagClass, expectedTagNumber) + + ", but read: " + BerEncoding.tagClassAndNumberToString( + container.getTagClass(), container.getTagNumber())); + } + return parseSequence(container, containerClass); + } + + default: + throw new Asn1DecodingException("Parsing container " + dataType + " not supported"); + } + } + + private static T parseChoice(BerDataValue dataValue, Class containerClass) + throws Asn1DecodingException { + List fields = getAnnotatedFields(containerClass); + if (fields.isEmpty()) { + throw new Asn1DecodingException( + "No fields annotated with " + Asn1Field.class.getName() + + " in CHOICE class " + containerClass.getName()); + } + + // Check that class + tagNumber don't clash between the choices + for (int i = 0; i < fields.size() - 1; i++) { + AnnotatedField f1 = fields.get(i); + int tagNumber1 = f1.getBerTagNumber(); + int tagClass1 = f1.getBerTagClass(); + for (int j = i + 1; j < fields.size(); j++) { + AnnotatedField f2 = fields.get(j); + int tagNumber2 = f2.getBerTagNumber(); + int tagClass2 = f2.getBerTagClass(); + if ((tagNumber1 == tagNumber2) && (tagClass1 == tagClass2)) { + throw new Asn1DecodingException( + "CHOICE fields are indistinguishable because they have the same tag" + + " class and number: " + containerClass.getName() + + "." + f1.getField().getName() + + " and ." + f2.getField().getName()); + } + } + } + + // Instantiate the container object / result + T obj; + try { + obj = containerClass.getConstructor().newInstance(); + } catch (IllegalArgumentException | ReflectiveOperationException e) { + throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e); + } + + // Set the matching field's value from the data value + for (AnnotatedField field : fields) { + try { + field.setValueFrom(dataValue, obj); + return obj; + } catch (Asn1UnexpectedTagException expected) { + // not a match + } + } + + throw new Asn1DecodingException( + "No options of CHOICE " + containerClass.getName() + " matched"); + } + + private static T parseSequence(BerDataValue container, Class containerClass) + throws Asn1DecodingException { + List fields = getAnnotatedFields(containerClass); + Collections.sort( + fields, new Comparator() { + @Override + public int compare(AnnotatedField f1, AnnotatedField f2) { + return f1.getAnnotation().index() - f2.getAnnotation().index(); + } + }); + // Check that there are no fields with the same index + if (fields.size() > 1) { + AnnotatedField lastField = null; + for (AnnotatedField field : fields) { + if ((lastField != null) + && (lastField.getAnnotation().index() == field.getAnnotation().index())) { + throw new Asn1DecodingException( + "Fields have the same index: " + containerClass.getName() + + "." + lastField.getField().getName() + + " and ." + field.getField().getName()); + } + lastField = field; + } + } + + // Instantiate the container object / result + T t; + try { + t = containerClass.getConstructor().newInstance(); + } catch (IllegalArgumentException | ReflectiveOperationException e) { + throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e); + } + + // Parse fields one by one. A complication is that there may be optional fields. + int nextUnreadFieldIndex = 0; + BerDataValueReader elementsReader = container.contentsReader(); + while (nextUnreadFieldIndex < fields.size()) { + BerDataValue dataValue; + try { + dataValue = elementsReader.readDataValue(); + } catch (BerDataValueFormatException e) { + throw new Asn1DecodingException("Malformed data value", e); + } + if (dataValue == null) { + break; + } + + for (int i = nextUnreadFieldIndex; i < fields.size(); i++) { + AnnotatedField field = fields.get(i); + try { + if (field.isOptional()) { + // Optional field -- might not be present and we may thus be trying to set + // it from the wrong tag. + try { + field.setValueFrom(dataValue, t); + nextUnreadFieldIndex = i + 1; + break; + } catch (Asn1UnexpectedTagException e) { + // This field is not present, attempt to use this data value for the + // next / iteration of the loop + continue; + } + } else { + // Mandatory field -- if we can't set its value from this data value, then + // it's an error + field.setValueFrom(dataValue, t); + nextUnreadFieldIndex = i + 1; + break; + } + } catch (Asn1DecodingException e) { + throw new Asn1DecodingException( + "Failed to parse " + containerClass.getName() + + "." + field.getField().getName(), + e); + } + } + } + + return t; + } + + // NOTE: This method returns List rather than Set because ASN.1 SET_OF does require uniqueness + // of elements -- it's an unordered collection. + @SuppressWarnings("unchecked") + private static List parseSetOf(BerDataValue container, Class elementClass) + throws Asn1DecodingException { + List result = new ArrayList<>(); + BerDataValueReader elementsReader = container.contentsReader(); + while (true) { + BerDataValue dataValue; + try { + dataValue = elementsReader.readDataValue(); + } catch (BerDataValueFormatException e) { + throw new Asn1DecodingException("Malformed data value", e); + } + if (dataValue == null) { + break; + } + T element; + if (ByteBuffer.class.equals(elementClass)) { + element = (T) dataValue.getEncodedContents(); + } else if (Asn1OpaqueObject.class.equals(elementClass)) { + element = (T) new Asn1OpaqueObject(dataValue.getEncoded()); + } else { + element = parse(dataValue, elementClass); + } + result.add(element); + } + return result; + } + + private static Asn1Type getContainerAsn1Type(Class containerClass) + throws Asn1DecodingException { + Asn1Class containerAnnotation = containerClass.getAnnotation(Asn1Class.class); + if (containerAnnotation == null) { + throw new Asn1DecodingException( + containerClass.getName() + " is not annotated with " + + Asn1Class.class.getName()); + } + + switch (containerAnnotation.type()) { + case CHOICE: + case SEQUENCE: + return containerAnnotation.type(); + default: + throw new Asn1DecodingException( + "Unsupported ASN.1 container annotation type: " + + containerAnnotation.type()); + } + } + + private static Class getElementType(Field field) + throws Asn1DecodingException, ClassNotFoundException { + String type = field.getGenericType().toString(); + int delimiterIndex = type.indexOf('<'); + if (delimiterIndex == -1) { + throw new Asn1DecodingException("Not a container type: " + field.getGenericType()); + } + int startIndex = delimiterIndex + 1; + int endIndex = type.indexOf('>', startIndex); + // TODO: handle comma? + if (endIndex == -1) { + throw new Asn1DecodingException("Not a container type: " + field.getGenericType()); + } + String elementClassName = type.substring(startIndex, endIndex); + return Class.forName(elementClassName); + } + + private static final class AnnotatedField { + private final Field mField; + private final Asn1Field mAnnotation; + private final Asn1Type mDataType; + private final Asn1TagClass mTagClass; + private final int mBerTagClass; + private final int mBerTagNumber; + private final Asn1Tagging mTagging; + private final boolean mOptional; + + public AnnotatedField(Field field, Asn1Field annotation) throws Asn1DecodingException { + mField = field; + mAnnotation = annotation; + mDataType = annotation.type(); + + Asn1TagClass tagClass = annotation.cls(); + if (tagClass == Asn1TagClass.AUTOMATIC) { + if (annotation.tagNumber() != -1) { + tagClass = Asn1TagClass.CONTEXT_SPECIFIC; + } else { + tagClass = Asn1TagClass.UNIVERSAL; + } + } + mTagClass = tagClass; + mBerTagClass = BerEncoding.getTagClass(mTagClass); + + int tagNumber; + if (annotation.tagNumber() != -1) { + tagNumber = annotation.tagNumber(); + } else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) { + tagNumber = -1; + } else { + tagNumber = BerEncoding.getTagNumber(mDataType); + } + mBerTagNumber = tagNumber; + + mTagging = annotation.tagging(); + if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT)) + && (annotation.tagNumber() == -1)) { + throw new Asn1DecodingException( + "Tag number must be specified when tagging mode is " + mTagging); + } + + mOptional = annotation.optional(); + } + + public Field getField() { + return mField; + } + + public Asn1Field getAnnotation() { + return mAnnotation; + } + + public boolean isOptional() { + return mOptional; + } + + public int getBerTagClass() { + return mBerTagClass; + } + + public int getBerTagNumber() { + return mBerTagNumber; + } + + public void setValueFrom(BerDataValue dataValue, Object obj) throws Asn1DecodingException { + int readTagClass = dataValue.getTagClass(); + if (mBerTagNumber != -1) { + int readTagNumber = dataValue.getTagNumber(); + if ((readTagClass != mBerTagClass) || (readTagNumber != mBerTagNumber)) { + throw new Asn1UnexpectedTagException( + "Tag mismatch. Expected: " + + BerEncoding.tagClassAndNumberToString(mBerTagClass, mBerTagNumber) + + ", but found " + + BerEncoding.tagClassAndNumberToString(readTagClass, readTagNumber)); + } + } else { + if (readTagClass != mBerTagClass) { + throw new Asn1UnexpectedTagException( + "Tag mismatch. Expected class: " + + BerEncoding.tagClassToString(mBerTagClass) + + ", but found " + + BerEncoding.tagClassToString(readTagClass)); + } + } + + if (mTagging == Asn1Tagging.EXPLICIT) { + try { + dataValue = dataValue.contentsReader().readDataValue(); + } catch (BerDataValueFormatException e) { + throw new Asn1DecodingException( + "Failed to read contents of EXPLICIT data value", e); + } + } + + BerToJavaConverter.setFieldValue(obj, mField, mDataType, dataValue); + } + } + + private static class Asn1UnexpectedTagException extends Asn1DecodingException { + private static final long serialVersionUID = 1L; + + public Asn1UnexpectedTagException(String message) { + super(message); + } + } + + private static String oidToString(ByteBuffer encodedOid) throws Asn1DecodingException { + if (!encodedOid.hasRemaining()) { + throw new Asn1DecodingException("Empty OBJECT IDENTIFIER"); + } + + // First component encodes the first two nodes, X.Y, as X * 40 + Y, with 0 <= X <= 2 + long firstComponent = decodeBase128UnsignedLong(encodedOid); + int firstNode = (int) Math.min(firstComponent / 40, 2); + long secondNode = firstComponent - firstNode * 40; + StringBuilder result = new StringBuilder(); + result.append(Long.toString(firstNode)).append('.') + .append(Long.toString(secondNode)); + + // Each consecutive node is encoded as a separate component + while (encodedOid.hasRemaining()) { + long node = decodeBase128UnsignedLong(encodedOid); + result.append('.').append(Long.toString(node)); + } + + return result.toString(); + } + + private static long decodeBase128UnsignedLong(ByteBuffer encoded) throws Asn1DecodingException { + if (!encoded.hasRemaining()) { + return 0; + } + long result = 0; + while (encoded.hasRemaining()) { + if (result > Long.MAX_VALUE >>> 7) { + throw new Asn1DecodingException("Base-128 number too large"); + } + int b = encoded.get() & 0xff; + result <<= 7; + result |= b & 0x7f; + if ((b & 0x80) == 0) { + return result; + } + } + throw new Asn1DecodingException( + "Truncated base-128 encoded input: missing terminating byte, with highest bit not" + + " set"); + } + + private static BigInteger integerToBigInteger(ByteBuffer encoded) { + if (!encoded.hasRemaining()) { + return BigInteger.ZERO; + } + return new BigInteger(Buffers.readBytes(encoded)); + } + + private static int integerToInt(ByteBuffer encoded) throws Asn1DecodingException { + BigInteger value = integerToBigInteger(encoded); + try { + return value.intValue(); + } catch (ArithmeticException e) { + throw new Asn1DecodingException( + String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value), e); + } + } + + private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException { + BigInteger value = integerToBigInteger(encoded); + try { + return value.intValue(); + } catch (ArithmeticException e) { + throw new Asn1DecodingException( + String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value), + e); + } + } + + private static List getAnnotatedFields(Class containerClass) + throws Asn1DecodingException { + Field[] declaredFields = containerClass.getDeclaredFields(); + List result = new ArrayList<>(declaredFields.length); + for (Field field : declaredFields) { + Asn1Field annotation = field.getAnnotation(Asn1Field.class); + if (annotation == null) { + continue; + } + if (Modifier.isStatic(field.getModifiers())) { + throw new Asn1DecodingException( + Asn1Field.class.getName() + " used on a static field: " + + containerClass.getName() + "." + field.getName()); + } + + AnnotatedField annotatedField; + try { + annotatedField = new AnnotatedField(field, annotation); + } catch (Asn1DecodingException e) { + throw new Asn1DecodingException( + "Invalid ASN.1 annotation on " + + containerClass.getName() + "." + field.getName(), + e); + } + result.add(annotatedField); + } + return result; + } + + private static final class BerToJavaConverter { + private BerToJavaConverter() { + } + + public static void setFieldValue( + Object obj, Field field, Asn1Type type, BerDataValue dataValue) + throws Asn1DecodingException { + try { + switch (type) { + case SET_OF: + case SEQUENCE_OF: + if (Asn1OpaqueObject.class.equals(field.getType())) { + field.set(obj, convert(type, dataValue, field.getType())); + } else { + field.set(obj, parseSetOf(dataValue, getElementType(field))); + } + return; + default: + field.set(obj, convert(type, dataValue, field.getType())); + break; + } + } catch (ReflectiveOperationException e) { + throw new Asn1DecodingException( + "Failed to set value of " + obj.getClass().getName() + + "." + field.getName(), + e); + } + } + + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + @SuppressWarnings("unchecked") + public static T convert( + Asn1Type sourceType, + BerDataValue dataValue, + Class targetType) throws Asn1DecodingException { + if (ByteBuffer.class.equals(targetType)) { + return (T) dataValue.getEncodedContents(); + } else if (byte[].class.equals(targetType)) { + ByteBuffer resultBuf = dataValue.getEncodedContents(); + if (!resultBuf.hasRemaining()) { + return (T) EMPTY_BYTE_ARRAY; + } + byte[] result = new byte[resultBuf.remaining()]; + resultBuf.get(result); + return (T) result; + } else if (Asn1OpaqueObject.class.equals(targetType)) { + return (T) new Asn1OpaqueObject(dataValue.getEncoded()); + } + + ByteBuffer encodedContents = dataValue.getEncodedContents(); + switch (sourceType) { + case INTEGER: + if ((int.class.equals(targetType)) || (Integer.class.equals(targetType))) { + return (T) Integer.valueOf(integerToInt(encodedContents)); + } else if ((long.class.equals(targetType)) || (Long.class.equals(targetType))) { + return (T) Long.valueOf(integerToLong(encodedContents)); + } else if (BigInteger.class.equals(targetType)) { + return (T) integerToBigInteger(encodedContents); + } + break; + case OBJECT_IDENTIFIER: + if (String.class.equals(targetType)) { + return (T) oidToString(encodedContents); + } + break; + case SEQUENCE: { + Asn1Class containerAnnotation = targetType.getAnnotation(Asn1Class.class); + if ((containerAnnotation != null) + && (containerAnnotation.type() == Asn1Type.SEQUENCE)) { + return parseSequence(dataValue, targetType); + } + break; + } + case CHOICE: { + Asn1Class containerAnnotation = targetType.getAnnotation(Asn1Class.class); + if ((containerAnnotation != null) + && (containerAnnotation.type() == Asn1Type.CHOICE)) { + return parseChoice(dataValue, targetType); + } + break; + } + default: + break; + } + + throw new Asn1DecodingException( + "Unsupported conversion: ASN.1 " + sourceType + " to " + targetType.getName()); + } + } +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Class.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Class.java new file mode 100644 index 0000000..bc888ff --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Class.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Asn1Class { + Asn1Type type(); +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1DecodingException.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1DecodingException.java new file mode 100644 index 0000000..bca7af2 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1DecodingException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +/** + * Indicates that input could not be decoded into intended ASN.1 structure. + */ +public class Asn1DecodingException extends Exception { + private static final long serialVersionUID = 1L; + + public Asn1DecodingException(String message) { + super(message); + } + + public Asn1DecodingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1DerEncoder.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1DerEncoder.java new file mode 100644 index 0000000..63ece85 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1DerEncoder.java @@ -0,0 +1,535 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +import net.dongliu.apk.parser.cert.asn1.ber.BerEncoding; + +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Encoder of ASN.1 structures into DER-encoded form. + *

+ *

Structure is described to the encoder by providing a class annotated with {@link Asn1Class}, + * containing fields annotated with {@link Asn1Field}. + */ +public final class Asn1DerEncoder { + private Asn1DerEncoder() { + } + + /** + * Returns the DER-encoded form of the provided ASN.1 structure. + * + * @param container container to be encoded. The container's class must meet the following + * requirements: + *

    + *
  • The class must be annotated with {@link Asn1Class}.
  • + *
  • Member fields of the class which are to be encoded must be annotated with + * {@link Asn1Field} and be public.
  • + *
+ * @throws Asn1EncodingException if the input could not be encoded + */ + public static byte[] encode(Object container) throws Asn1EncodingException { + Class containerClass = container.getClass(); + Asn1Class containerAnnotation = containerClass.getAnnotation(Asn1Class.class); + if (containerAnnotation == null) { + throw new Asn1EncodingException( + containerClass.getName() + " not annotated with " + Asn1Class.class.getName()); + } + + Asn1Type containerType = containerAnnotation.type(); + switch (containerType) { + case CHOICE: + return toChoice(container); + case SEQUENCE: + return toSequence(container); + default: + throw new Asn1EncodingException("Unsupported container type: " + containerType); + } + } + + private static byte[] toChoice(Object container) throws Asn1EncodingException { + Class containerClass = container.getClass(); + List fields = getAnnotatedFields(container); + if (fields.isEmpty()) { + throw new Asn1EncodingException( + "No fields annotated with " + Asn1Field.class.getName() + + " in CHOICE class " + containerClass.getName()); + } + + AnnotatedField resultField = null; + for (AnnotatedField field : fields) { + Object fieldValue = getMemberFieldValue(container, field.getField()); + if (fieldValue != null) { + if (resultField != null) { + throw new Asn1EncodingException( + "Multiple non-null fields in CHOICE class " + containerClass.getName() + + ": " + resultField.getField().getName() + + ", " + field.getField().getName()); + } + resultField = field; + } + } + + if (resultField == null) { + throw new Asn1EncodingException( + "No non-null fields in CHOICE class " + containerClass.getName()); + } + + return resultField.toDer(); + } + + private static byte[] toSequence(Object container) throws Asn1EncodingException { + Class containerClass = container.getClass(); + List fields = getAnnotatedFields(container); + Collections.sort( + fields, new Comparator() { + @Override + public int compare(AnnotatedField f1, AnnotatedField f2) { + return f1.getAnnotation().index() - f2.getAnnotation().index(); + } + }); + if (fields.size() > 1) { + AnnotatedField lastField = null; + for (AnnotatedField field : fields) { + if ((lastField != null) + && (lastField.getAnnotation().index() == field.getAnnotation().index())) { + throw new Asn1EncodingException( + "Fields have the same index: " + containerClass.getName() + + "." + lastField.getField().getName() + + " and ." + field.getField().getName()); + } + lastField = field; + } + } + + List serializedFields = new ArrayList<>(fields.size()); + for (AnnotatedField field : fields) { + byte[] serializedField; + try { + serializedField = field.toDer(); + } catch (Asn1EncodingException e) { + throw new Asn1EncodingException( + "Failed to encode " + containerClass.getName() + + "." + field.getField().getName(), + e); + } + if (serializedField != null) { + serializedFields.add(serializedField); + } + } + + return createTag( + BerEncoding.TAG_CLASS_UNIVERSAL, true, BerEncoding.TAG_NUMBER_SEQUENCE, + serializedFields.toArray(new byte[0][])); + } + + private static byte[] toSetOf(Collection values, Asn1Type elementType) + throws Asn1EncodingException { + List serializedValues = new ArrayList<>(values.size()); + for (Object value : values) { + serializedValues.add(JavaToDerConverter.toDer(value, elementType, null)); + } + if (serializedValues.size() > 1) { + Collections.sort(serializedValues, ByteArrayLexicographicComparator.INSTANCE); + } + return createTag( + BerEncoding.TAG_CLASS_UNIVERSAL, true, BerEncoding.TAG_NUMBER_SET, + serializedValues.toArray(new byte[0][])); + } + + /** + * Compares two bytes arrays based on their lexicographic order. Corresponding elements of the + * two arrays are compared in ascending order. Elements at out of range indices are assumed to + * be smaller than the smallest possible value for an element. + */ + private static class ByteArrayLexicographicComparator implements Comparator { + private static final ByteArrayLexicographicComparator INSTANCE = + new ByteArrayLexicographicComparator(); + + @Override + public int compare(byte[] arr1, byte[] arr2) { + int commonLength = Math.min(arr1.length, arr2.length); + for (int i = 0; i < commonLength; i++) { + int diff = (arr1[i] & 0xff) - (arr2[i] & 0xff); + if (diff != 0) { + return diff; + } + } + return arr1.length - arr2.length; + } + } + + private static List getAnnotatedFields(Object container) + throws Asn1EncodingException { + Class containerClass = container.getClass(); + Field[] declaredFields = containerClass.getDeclaredFields(); + List result = new ArrayList<>(declaredFields.length); + for (Field field : declaredFields) { + Asn1Field annotation = field.getAnnotation(Asn1Field.class); + if (annotation == null) { + continue; + } + if (Modifier.isStatic(field.getModifiers())) { + throw new Asn1EncodingException( + Asn1Field.class.getName() + " used on a static field: " + + containerClass.getName() + "." + field.getName()); + } + + AnnotatedField annotatedField; + try { + annotatedField = new AnnotatedField(container, field, annotation); + } catch (Asn1EncodingException e) { + throw new Asn1EncodingException( + "Invalid ASN.1 annotation on " + + containerClass.getName() + "." + field.getName(), + e); + } + result.add(annotatedField); + } + return result; + } + + private static byte[] toInteger(int value) { + return toInteger((long) value); + } + + private static byte[] toInteger(long value) { + return toInteger(BigInteger.valueOf(value)); + } + + private static byte[] toInteger(BigInteger value) { + return createTag( + BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_INTEGER, + value.toByteArray()); + } + + private static byte[] toOid(String oid) throws Asn1EncodingException { + ByteArrayOutputStream encodedValue = new ByteArrayOutputStream(); + String[] nodes = oid.split("\\."); + if (nodes.length < 2) { + throw new Asn1EncodingException( + "OBJECT IDENTIFIER must contain at least two nodes: " + oid); + } + int firstNode; + try { + firstNode = Integer.parseInt(nodes[0]); + } catch (NumberFormatException e) { + throw new Asn1EncodingException("Node #1 not numeric: " + nodes[0]); + } + if ((firstNode > 6) || (firstNode < 0)) { + throw new Asn1EncodingException("Invalid value for node #1: " + firstNode); + } + + int secondNode; + try { + secondNode = Integer.parseInt(nodes[1]); + } catch (NumberFormatException e) { + throw new Asn1EncodingException("Node #2 not numeric: " + nodes[1]); + } + if ((secondNode >= 40) || (secondNode < 0)) { + throw new Asn1EncodingException("Invalid value for node #2: " + secondNode); + } + int firstByte = firstNode * 40 + secondNode; + if (firstByte > 0xff) { + throw new Asn1EncodingException( + "First two nodes out of range: " + firstNode + "." + secondNode); + } + + encodedValue.write(firstByte); + for (int i = 2; i < nodes.length; i++) { + String nodeString = nodes[i]; + int node; + try { + node = Integer.parseInt(nodeString); + } catch (NumberFormatException e) { + throw new Asn1EncodingException("Node #" + (i + 1) + " not numeric: " + nodeString); + } + if (node < 0) { + throw new Asn1EncodingException("Invalid value for node #" + (i + 1) + ": " + node); + } + if (node <= 0x7f) { + encodedValue.write(node); + continue; + } + if (node < 1 << 14) { + encodedValue.write(0x80 | (node >> 7)); + encodedValue.write(node & 0x7f); + continue; + } + if (node < 1 << 21) { + encodedValue.write(0x80 | (node >> 14)); + encodedValue.write(0x80 | ((node >> 7) & 0x7f)); + encodedValue.write(node & 0x7f); + continue; + } + throw new Asn1EncodingException("Node #" + (i + 1) + " too large: " + node); + } + + return createTag( + BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_OBJECT_IDENTIFIER, + encodedValue.toByteArray()); + } + + private static Object getMemberFieldValue(Object obj, Field field) + throws Asn1EncodingException { + try { + return field.get(obj); + } catch (ReflectiveOperationException e) { + throw new Asn1EncodingException( + "Failed to read " + obj.getClass().getName() + "." + field.getName(), e); + } + } + + private static final class AnnotatedField { + private final Field mField; + private final Object mObject; + private final Asn1Field mAnnotation; + private final Asn1Type mDataType; + private final Asn1Type mElementDataType; + private final Asn1TagClass mTagClass; + private final int mDerTagClass; + private final int mDerTagNumber; + private final Asn1Tagging mTagging; + private final boolean mOptional; + + public AnnotatedField(Object obj, Field field, Asn1Field annotation) + throws Asn1EncodingException { + mObject = obj; + mField = field; + mAnnotation = annotation; + mDataType = annotation.type(); + mElementDataType = annotation.elementType(); + + Asn1TagClass tagClass = annotation.cls(); + if (tagClass == Asn1TagClass.AUTOMATIC) { + if (annotation.tagNumber() != -1) { + tagClass = Asn1TagClass.CONTEXT_SPECIFIC; + } else { + tagClass = Asn1TagClass.UNIVERSAL; + } + } + mTagClass = tagClass; + mDerTagClass = BerEncoding.getTagClass(mTagClass); + + int tagNumber; + if (annotation.tagNumber() != -1) { + tagNumber = annotation.tagNumber(); + } else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) { + tagNumber = -1; + } else { + tagNumber = BerEncoding.getTagNumber(mDataType); + } + mDerTagNumber = tagNumber; + + mTagging = annotation.tagging(); + if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT)) + && (annotation.tagNumber() == -1)) { + throw new Asn1EncodingException( + "Tag number must be specified when tagging mode is " + mTagging); + } + + mOptional = annotation.optional(); + } + + public Field getField() { + return mField; + } + + public Asn1Field getAnnotation() { + return mAnnotation; + } + + public byte[] toDer() throws Asn1EncodingException { + Object fieldValue = getMemberFieldValue(mObject, mField); + if (fieldValue == null) { + if (mOptional) { + return null; + } + throw new Asn1EncodingException("Required field not set"); + } + + byte[] encoded = JavaToDerConverter.toDer(fieldValue, mDataType, mElementDataType); + switch (mTagging) { + case NORMAL: + return encoded; + case EXPLICIT: + return createTag(mDerTagClass, true, mDerTagNumber, encoded); + case IMPLICIT: + int originalTagNumber = BerEncoding.getTagNumber(encoded[0]); + if (originalTagNumber == 0x1f) { + throw new Asn1EncodingException("High-tag-number form not supported"); + } + if (mDerTagNumber >= 0x1f) { + throw new Asn1EncodingException( + "Unsupported high tag number: " + mDerTagNumber); + } + encoded[0] = BerEncoding.setTagNumber(encoded[0], mDerTagNumber); + encoded[0] = BerEncoding.setTagClass(encoded[0], mDerTagClass); + return encoded; + default: + throw new RuntimeException("Unknown tagging mode: " + mTagging); + } + } + } + + private static byte[] createTag( + int tagClass, boolean constructed, int tagNumber, byte[]... contents) { + if (tagNumber >= 0x1f) { + throw new IllegalArgumentException("High tag numbers not supported: " + tagNumber); + } + // tag class & number fit into the first byte + byte firstIdentifierByte = + (byte) ((tagClass << 6) | (constructed ? 1 << 5 : 0) | tagNumber); + + int contentsLength = 0; + for (byte[] c : contents) { + contentsLength += c.length; + } + int contentsPosInResult; + byte[] result; + if (contentsLength < 0x80) { + // Length fits into one byte + contentsPosInResult = 2; + result = new byte[contentsPosInResult + contentsLength]; + result[0] = firstIdentifierByte; + result[1] = (byte) contentsLength; + } else { + // Length is represented as multiple bytes + // The low 7 bits of the first byte represent the number of length bytes (following the + // first byte) in which the length is in big-endian base-256 form + if (contentsLength <= 0xff) { + contentsPosInResult = 3; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x81; // 1 length byte + result[2] = (byte) contentsLength; + } else if (contentsLength <= 0xffff) { + contentsPosInResult = 4; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x82; // 2 length bytes + result[2] = (byte) (contentsLength >> 8); + result[3] = (byte) (contentsLength & 0xff); + } else if (contentsLength <= 0xffffff) { + contentsPosInResult = 5; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x83; // 3 length bytes + result[2] = (byte) (contentsLength >> 16); + result[3] = (byte) ((contentsLength >> 8) & 0xff); + result[4] = (byte) (contentsLength & 0xff); + } else { + contentsPosInResult = 6; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x84; // 4 length bytes + result[2] = (byte) (contentsLength >> 24); + result[3] = (byte) ((contentsLength >> 16) & 0xff); + result[4] = (byte) ((contentsLength >> 8) & 0xff); + result[5] = (byte) (contentsLength & 0xff); + } + result[0] = firstIdentifierByte; + } + for (byte[] c : contents) { + System.arraycopy(c, 0, result, contentsPosInResult, c.length); + contentsPosInResult += c.length; + } + return result; + } + + private static final class JavaToDerConverter { + private JavaToDerConverter() { + } + + public static byte[] toDer(Object source, Asn1Type targetType, Asn1Type targetElementType) + throws Asn1EncodingException { + Class sourceType = source.getClass(); + if (Asn1OpaqueObject.class.equals(sourceType)) { + ByteBuffer buf = ((Asn1OpaqueObject) source).getEncoded(); + byte[] result = new byte[buf.remaining()]; + buf.get(result); + return result; + } + + if ((targetType == null) || (targetType == Asn1Type.ANY)) { + return encode(source); + } + + switch (targetType) { + case OCTET_STRING: + byte[] value = null; + if (source instanceof ByteBuffer) { + ByteBuffer buf = (ByteBuffer) source; + value = new byte[buf.remaining()]; + buf.slice().get(value); + } else if (source instanceof byte[]) { + value = (byte[]) source; + } + if (value != null) { + return createTag( + BerEncoding.TAG_CLASS_UNIVERSAL, + false, + BerEncoding.TAG_NUMBER_OCTET_STRING, + value); + } + break; + case INTEGER: + if (source instanceof Integer) { + return toInteger((Integer) source); + } else if (source instanceof Long) { + return toInteger((Long) source); + } else if (source instanceof BigInteger) { + return toInteger((BigInteger) source); + } + break; + case OBJECT_IDENTIFIER: + if (source instanceof String) { + return toOid((String) source); + } + break; + case SEQUENCE: { + Asn1Class containerAnnotation = sourceType.getAnnotation(Asn1Class.class); + if ((containerAnnotation != null) + && (containerAnnotation.type() == Asn1Type.SEQUENCE)) { + return toSequence(source); + } + break; + } + case CHOICE: { + Asn1Class containerAnnotation = sourceType.getAnnotation(Asn1Class.class); + if ((containerAnnotation != null) + && (containerAnnotation.type() == Asn1Type.CHOICE)) { + return toChoice(source); + } + break; + } + case SET_OF: + return toSetOf((Collection) source, targetElementType); + default: + break; + } + + throw new Asn1EncodingException( + "Unsupported conversion: " + sourceType.getName() + " to ASN.1 " + targetType); + } + } +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1EncodingException.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1EncodingException.java new file mode 100644 index 0000000..98c4695 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1EncodingException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +/** + * Indicates that an ASN.1 structure could not be encoded. + */ +public class Asn1EncodingException extends Exception { + private static final long serialVersionUID = 1L; + + public Asn1EncodingException(String message) { + super(message); + } + + public Asn1EncodingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Field.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Field.java new file mode 100644 index 0000000..dbae46d --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Field.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Asn1Field { + /** Index used to order fields in a container. Required for fields of SEQUENCE containers. */ + public int index() default 0; + + public Asn1TagClass cls() default Asn1TagClass.AUTOMATIC; + + public Asn1Type type(); + + /** Tagging mode. Default: NORMAL. */ + public Asn1Tagging tagging() default Asn1Tagging.NORMAL; + + /** Tag number. Required when IMPLICIT and EXPLICIT tagging mode is used.*/ + public int tagNumber() default -1; + + /** {@code true} if this field is optional. Ignored for fields of CHOICE containers. */ + public boolean optional() default false; + + /** Type of elements. Used only for SET_OF or SEQUENCE_OF. */ + public Asn1Type elementType() default Asn1Type.ANY; +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1OpaqueObject.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1OpaqueObject.java new file mode 100644 index 0000000..15a9b6a --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1OpaqueObject.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +import java.nio.ByteBuffer; + +/** + * Opaque holder of encoded ASN.1 stuff. + */ +public class Asn1OpaqueObject { + private final ByteBuffer mEncoded; + + public Asn1OpaqueObject(ByteBuffer encoded) { + mEncoded = encoded.slice(); + } + + public Asn1OpaqueObject(byte[] encoded) { + mEncoded = ByteBuffer.wrap(encoded); + } + + public ByteBuffer getEncoded() { + return mEncoded.slice(); + } +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1TagClass.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1TagClass.java new file mode 100644 index 0000000..d8bb931 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1TagClass.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +public enum Asn1TagClass { + UNIVERSAL, + APPLICATION, + CONTEXT_SPECIFIC, + PRIVATE, + + /** + * Not really an actual tag class: decoder/encoder will attempt to deduce the correct tag class + * automatically. + */ + AUTOMATIC, +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Tagging.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Tagging.java new file mode 100644 index 0000000..506c625 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Tagging.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +public enum Asn1Tagging { + NORMAL, + EXPLICIT, + IMPLICIT, +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Type.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Type.java new file mode 100644 index 0000000..029c580 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/Asn1Type.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1; + +public enum Asn1Type { + ANY, + CHOICE, + INTEGER, + OBJECT_IDENTIFIER, + OCTET_STRING, + SEQUENCE, + SEQUENCE_OF, + SET_OF, +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValue.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValue.java new file mode 100644 index 0000000..4172c07 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValue.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1.ber; + +import java.nio.ByteBuffer; + +/** + * ASN.1 Basic Encoding Rules (BER) data value -- see {@code X.690}. + */ +public class BerDataValue { + private final ByteBuffer mEncoded; + private final ByteBuffer mEncodedContents; + private final int mTagClass; + private final boolean mConstructed; + private final int mTagNumber; + + BerDataValue( + ByteBuffer encoded, + ByteBuffer encodedContents, + int tagClass, + boolean constructed, + int tagNumber) { + mEncoded = encoded; + mEncodedContents = encodedContents; + mTagClass = tagClass; + mConstructed = constructed; + mTagNumber = tagNumber; + } + + /** + * Returns the tag class of this data value. See {@link BerEncoding} {@code TAG_CLASS} + * constants. + */ + public int getTagClass() { + return mTagClass; + } + + /** + * Returns {@code true} if the content octets of this data value are the complete BER encoding + * of one or more data values, {@code false} if the content octets of this data value directly + * represent the value. + */ + public boolean isConstructed() { + return mConstructed; + } + + /** + * Returns the tag number of this data value. See {@link BerEncoding} {@code TAG_NUMBER} + * constants. + */ + public int getTagNumber() { + return mTagNumber; + } + + /** + * Returns the encoded form of this data value. + */ + public ByteBuffer getEncoded() { + return mEncoded.slice(); + } + + /** + * Returns the encoded contents of this data value. + */ + public ByteBuffer getEncodedContents() { + return mEncodedContents.slice(); + } + + /** + * Returns a new reader of the contents of this data value. + */ + public BerDataValueReader contentsReader() { + return new ByteBufferBerDataValueReader(getEncodedContents()); + } + + /** + * Returns a new reader which returns just this data value. This may be useful for re-reading + * this value in different contexts. + */ + public BerDataValueReader dataValueReader() { + return new ParsedValueReader(this); + } + + private static final class ParsedValueReader implements BerDataValueReader { + private final BerDataValue mValue; + private boolean mValueOutput; + + public ParsedValueReader(BerDataValue value) { + mValue = value; + } + + @Override + public BerDataValue readDataValue() throws BerDataValueFormatException { + if (mValueOutput) { + return null; + } + mValueOutput = true; + return mValue; + } + } +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueFormatException.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueFormatException.java new file mode 100644 index 0000000..4258ac8 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueFormatException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1.ber; + +/** + * Indicates that an ASN.1 data value being read could not be decoded using + * Basic Encoding Rules (BER). + */ +public class BerDataValueFormatException extends Exception { + + private static final long serialVersionUID = 1L; + + public BerDataValueFormatException(String message) { + super(message); + } + + public BerDataValueFormatException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueReader.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueReader.java new file mode 100644 index 0000000..652cda4 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerDataValueReader.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1.ber; + +/** + * Reader of ASN.1 Basic Encoding Rules (BER) data values. + * + *

BER data value reader returns data values, one by one, from a source. The interpretation of + * data values (e.g., how to obtain a numeric value from an INTEGER data value, or how to extract + * the elements of a SEQUENCE value) is left to clients of the reader. + */ +public interface BerDataValueReader { + + /** + * Returns the next data value or {@code null} if end of input has been reached. + * + * @throws BerDataValueFormatException if the value being read is malformed. + */ + BerDataValue readDataValue() throws BerDataValueFormatException; +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerEncoding.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerEncoding.java new file mode 100644 index 0000000..7f4e9cf --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/BerEncoding.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1.ber; + +import net.dongliu.apk.parser.cert.asn1.Asn1TagClass; +import net.dongliu.apk.parser.cert.asn1.Asn1Type; + +/** + * ASN.1 Basic Encoding Rules (BER) constants and helper methods. See {@code X.690}. + */ +public abstract class BerEncoding { + private BerEncoding() {} + + /** + * Constructed vs primitive flag in the first identifier byte. + */ + public static final int ID_FLAG_CONSTRUCTED_ENCODING = 1 << 5; + + /** + * Tag class: UNIVERSAL + */ + public static final int TAG_CLASS_UNIVERSAL = 0; + + /** + * Tag class: APPLICATION + */ + public static final int TAG_CLASS_APPLICATION = 1; + + /** + * Tag class: CONTEXT SPECIFIC + */ + public static final int TAG_CLASS_CONTEXT_SPECIFIC = 2; + + /** + * Tag class: PRIVATE + */ + public static final int TAG_CLASS_PRIVATE = 3; + + /** + * Tag number: INTEGER + */ + public static final int TAG_NUMBER_INTEGER = 0x2; + + /** + * Tag number: OCTET STRING + */ + public static final int TAG_NUMBER_OCTET_STRING = 0x4; + + /** + * Tag number: NULL + */ + public static final int TAG_NUMBER_NULL = 0x05; + + /** + * Tag number: OBJECT IDENTIFIER + */ + public static final int TAG_NUMBER_OBJECT_IDENTIFIER = 0x6; + + /** + * Tag number: SEQUENCE + */ + public static final int TAG_NUMBER_SEQUENCE = 0x10; + + /** + * Tag number: SET + */ + public static final int TAG_NUMBER_SET = 0x11; + + public static int getTagNumber(Asn1Type dataType) { + switch (dataType) { + case INTEGER: + return TAG_NUMBER_INTEGER; + case OBJECT_IDENTIFIER: + return TAG_NUMBER_OBJECT_IDENTIFIER; + case OCTET_STRING: + return TAG_NUMBER_OCTET_STRING; + case SET_OF: + return TAG_NUMBER_SET; + case SEQUENCE: + case SEQUENCE_OF: + return TAG_NUMBER_SEQUENCE; + default: + throw new IllegalArgumentException("Unsupported data type: " + dataType); + } + } + + public static int getTagClass(Asn1TagClass tagClass) { + switch (tagClass) { + case APPLICATION: + return TAG_CLASS_APPLICATION; + case CONTEXT_SPECIFIC: + return TAG_CLASS_CONTEXT_SPECIFIC; + case PRIVATE: + return TAG_CLASS_PRIVATE; + case UNIVERSAL: + return TAG_CLASS_UNIVERSAL; + default: + throw new IllegalArgumentException("Unsupported tag class: " + tagClass); + } + } + + public static String tagClassToString(int typeClass) { + switch (typeClass) { + case TAG_CLASS_APPLICATION: + return "APPLICATION"; + case TAG_CLASS_CONTEXT_SPECIFIC: + return ""; + case TAG_CLASS_PRIVATE: + return "PRIVATE"; + case TAG_CLASS_UNIVERSAL: + return "UNIVERSAL"; + default: + throw new IllegalArgumentException("Unsupported type class: " + typeClass); + } + } + + public static String tagClassAndNumberToString(int tagClass, int tagNumber) { + String classString = tagClassToString(tagClass); + String numberString = tagNumberToString(tagNumber); + return classString.isEmpty() ? numberString : classString + " " + numberString; + } + + + public static String tagNumberToString(int tagNumber) { + switch (tagNumber) { + case TAG_NUMBER_INTEGER: + return "INTEGER"; + case TAG_NUMBER_OCTET_STRING: + return "OCTET STRING"; + case TAG_NUMBER_NULL: + return "NULL"; + case TAG_NUMBER_OBJECT_IDENTIFIER: + return "OBJECT IDENTIFIER"; + case TAG_NUMBER_SEQUENCE: + return "SEQUENCE"; + case TAG_NUMBER_SET: + return "SET"; + default: + return "0x" + Integer.toHexString(tagNumber); + } + } + + /** + * Returns {@code true} if the provided first identifier byte indicates that the data value uses + * constructed encoding for its contents, or {@code false} if the data value uses primitive + * encoding for its contents. + */ + public static boolean isConstructed(byte firstIdentifierByte) { + return (firstIdentifierByte & ID_FLAG_CONSTRUCTED_ENCODING) != 0; + } + + /** + * Returns the tag class encoded in the provided first identifier byte. See {@code TAG_CLASS} + * constants. + */ + public static int getTagClass(byte firstIdentifierByte) { + return (firstIdentifierByte & 0xff) >> 6; + } + + public static byte setTagClass(byte firstIdentifierByte, int tagClass) { + return (byte) ((firstIdentifierByte & 0x3f) | (tagClass << 6)); + } + + /** + * Returns the tag number encoded in the provided first identifier byte. See {@code TAG_NUMBER} + * constants. + */ + public static int getTagNumber(byte firstIdentifierByte) { + return firstIdentifierByte & 0x1f; + } + + public static byte setTagNumber(byte firstIdentifierByte, int tagNumber) { + return (byte) ((firstIdentifierByte & ~0x1f) | tagNumber); + } +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/ByteBufferBerDataValueReader.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/ByteBufferBerDataValueReader.java new file mode 100644 index 0000000..86d7978 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/ByteBufferBerDataValueReader.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1.ber; + +import java.nio.ByteBuffer; + +/** + * {@link BerDataValueReader} which reads from a {@link ByteBuffer} containing BER-encoded data + * values. See {@code X.690} for the encoding. + */ +public class ByteBufferBerDataValueReader implements BerDataValueReader { + private final ByteBuffer mBuf; + + public ByteBufferBerDataValueReader(ByteBuffer buf) { + if (buf == null) { + throw new NullPointerException("buf == null"); + } + mBuf = buf; + } + + @Override + public BerDataValue readDataValue() throws BerDataValueFormatException { + int startPosition = mBuf.position(); + if (!mBuf.hasRemaining()) { + return null; + } + byte firstIdentifierByte = mBuf.get(); + int tagNumber = readTagNumber(firstIdentifierByte); + boolean constructed = BerEncoding.isConstructed(firstIdentifierByte); + + if (!mBuf.hasRemaining()) { + throw new BerDataValueFormatException("Missing length"); + } + int firstLengthByte = mBuf.get() & 0xff; + int contentsLength; + int contentsOffsetInTag; + if ((firstLengthByte & 0x80) == 0) { + // short form length + contentsLength = readShortFormLength(firstLengthByte); + contentsOffsetInTag = mBuf.position() - startPosition; + skipDefiniteLengthContents(contentsLength); + } else if (firstLengthByte != 0x80) { + // long form length + contentsLength = readLongFormLength(firstLengthByte); + contentsOffsetInTag = mBuf.position() - startPosition; + skipDefiniteLengthContents(contentsLength); + } else { + // indefinite length -- value ends with 0x00 0x00 + contentsOffsetInTag = mBuf.position() - startPosition; + contentsLength = + constructed + ? skipConstructedIndefiniteLengthContents() + : skipPrimitiveIndefiniteLengthContents(); + } + + // Create the encoded data value ByteBuffer + int endPosition = mBuf.position(); + mBuf.position(startPosition); + int bufOriginalLimit = mBuf.limit(); + mBuf.limit(endPosition); + ByteBuffer encoded = mBuf.slice(); + mBuf.position(mBuf.limit()); + mBuf.limit(bufOriginalLimit); + + // Create the encoded contents ByteBuffer + encoded.position(contentsOffsetInTag); + encoded.limit(contentsOffsetInTag + contentsLength); + ByteBuffer encodedContents = encoded.slice(); + encoded.clear(); + + return new BerDataValue( + encoded, + encodedContents, + BerEncoding.getTagClass(firstIdentifierByte), + constructed, + tagNumber); + } + + private int readTagNumber(byte firstIdentifierByte) throws BerDataValueFormatException { + int tagNumber = BerEncoding.getTagNumber(firstIdentifierByte); + if (tagNumber == 0x1f) { + // high-tag-number form, where the tag number follows this byte in base-128 + // big-endian form, where each byte has the highest bit set, except for the last + // byte + return readHighTagNumber(); + } else { + // low-tag-number form + return tagNumber; + } + } + + private int readHighTagNumber() throws BerDataValueFormatException { + // Base-128 big-endian form, where each byte has the highest bit set, except for the last + // byte + int b; + int result = 0; + do { + if (!mBuf.hasRemaining()) { + throw new BerDataValueFormatException("Truncated tag number"); + } + b = mBuf.get(); + if (result > Integer.MAX_VALUE >>> 7) { + throw new BerDataValueFormatException("Tag number too large"); + } + result <<= 7; + result |= b & 0x7f; + } while ((b & 0x80) != 0); + return result; + } + + private int readShortFormLength(int firstLengthByte) { + return firstLengthByte & 0x7f; + } + + private int readLongFormLength(int firstLengthByte) throws BerDataValueFormatException { + // The low 7 bits of the first byte represent the number of bytes (following the first + // byte) in which the length is in big-endian base-256 form + int byteCount = firstLengthByte & 0x7f; + if (byteCount > 4) { + throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes"); + } + int result = 0; + for (int i = 0; i < byteCount; i++) { + if (!mBuf.hasRemaining()) { + throw new BerDataValueFormatException("Truncated length"); + } + int b = mBuf.get(); + if (result > Integer.MAX_VALUE >>> 8) { + throw new BerDataValueFormatException("Length too large"); + } + result <<= 8; + result |= b & 0xff; + } + return result; + } + + private void skipDefiniteLengthContents(int contentsLength) throws BerDataValueFormatException { + if (mBuf.remaining() < contentsLength) { + throw new BerDataValueFormatException( + "Truncated contents. Need: " + contentsLength + " bytes, available: " + + mBuf.remaining()); + } + mBuf.position(mBuf.position() + contentsLength); + } + + private int skipPrimitiveIndefiniteLengthContents() throws BerDataValueFormatException { + // Contents are terminated by 0x00 0x00 + boolean prevZeroByte = false; + int bytesRead = 0; + while (true) { + if (!mBuf.hasRemaining()) { + throw new BerDataValueFormatException( + "Truncated indefinite-length contents: " + bytesRead + " bytes read"); + + } + int b = mBuf.get(); + bytesRead++; + if (bytesRead < 0) { + throw new BerDataValueFormatException("Indefinite-length contents too long"); + } + if (b == 0) { + if (prevZeroByte) { + // End of contents reached -- we've read the value and its terminator 0x00 0x00 + return bytesRead - 2; + } + prevZeroByte = true; + } else { + prevZeroByte = false; + } + } + } + + private int skipConstructedIndefiniteLengthContents() throws BerDataValueFormatException { + // Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it + // can contain data values which are themselves indefinite length encoded. As a result, we + // must parse the direct children of this data value to correctly skip over the contents of + // this data value. + int startPos = mBuf.position(); + while (mBuf.hasRemaining()) { + // Check whether the 0x00 0x00 terminator is at current position + if ((mBuf.remaining() > 1) && (mBuf.getShort(mBuf.position()) == 0)) { + int contentsLength = mBuf.position() - startPos; + mBuf.position(mBuf.position() + 2); + return contentsLength; + } + // No luck. This must be a BER-encoded data value -- skip over it by parsing it + readDataValue(); + } + + throw new BerDataValueFormatException( + "Truncated indefinite-length contents: " + + (mBuf.position() - startPos) + " bytes read"); + } +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/InputStreamBerDataValueReader.java b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/InputStreamBerDataValueReader.java new file mode 100644 index 0000000..e9835f4 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/asn1/ber/InputStreamBerDataValueReader.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.asn1.ber; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * {@link BerDataValueReader} which reads from an {@link InputStream} returning BER-encoded data + * values. See {@code X.690} for the encoding. + */ +public class InputStreamBerDataValueReader implements BerDataValueReader { + private final InputStream mIn; + + public InputStreamBerDataValueReader(InputStream in) { + if (in == null) { + throw new NullPointerException("in == null"); + } + mIn = in; + } + + @Override + public BerDataValue readDataValue() throws BerDataValueFormatException { + return readDataValue(mIn); + } + + /** + * Returns the next data value or {@code null} if end of input has been reached. + * + * @throws BerDataValueFormatException if the value being read is malformed. + */ + @SuppressWarnings("resource") + private static BerDataValue readDataValue(InputStream input) + throws BerDataValueFormatException { + RecordingInputStream in = new RecordingInputStream(input); + + try { + int firstIdentifierByte = in.read(); + if (firstIdentifierByte == -1) { + // End of input + return null; + } + int tagNumber = readTagNumber(in, firstIdentifierByte); + + int firstLengthByte = in.read(); + if (firstLengthByte == -1) { + throw new BerDataValueFormatException("Missing length"); + } + + boolean constructed = BerEncoding.isConstructed((byte) firstIdentifierByte); + int contentsLength; + int contentsOffsetInDataValue; + if ((firstLengthByte & 0x80) == 0) { + // short form length + contentsLength = readShortFormLength(firstLengthByte); + contentsOffsetInDataValue = in.getReadByteCount(); + skipDefiniteLengthContents(in, contentsLength); + } else if ((firstLengthByte & 0xff) != 0x80) { + // long form length + contentsLength = readLongFormLength(in, firstLengthByte); + contentsOffsetInDataValue = in.getReadByteCount(); + skipDefiniteLengthContents(in, contentsLength); + } else { + // indefinite length + contentsOffsetInDataValue = in.getReadByteCount(); + contentsLength = + constructed + ? skipConstructedIndefiniteLengthContents(in) + : skipPrimitiveIndefiniteLengthContents(in); + } + + byte[] encoded = in.getReadBytes(); + ByteBuffer encodedContents = + ByteBuffer.wrap(encoded, contentsOffsetInDataValue, contentsLength); + return new BerDataValue( + ByteBuffer.wrap(encoded), + encodedContents, + BerEncoding.getTagClass((byte) firstIdentifierByte), + constructed, + tagNumber); + } catch (IOException e) { + throw new BerDataValueFormatException("Failed to read data value", e); + } + } + + private static int readTagNumber(InputStream in, int firstIdentifierByte) + throws IOException, BerDataValueFormatException { + int tagNumber = BerEncoding.getTagNumber((byte) firstIdentifierByte); + if (tagNumber == 0x1f) { + // high-tag-number form + return readHighTagNumber(in); + } else { + // low-tag-number form + return tagNumber; + } + } + + private static int readHighTagNumber(InputStream in) + throws IOException, BerDataValueFormatException { + // Base-128 big-endian form, where each byte has the highest bit set, except for the last + // byte where the highest bit is not set + int b; + int result = 0; + do { + b = in.read(); + if (b == -1) { + throw new BerDataValueFormatException("Truncated tag number"); + } + if (result > Integer.MAX_VALUE >>> 7) { + throw new BerDataValueFormatException("Tag number too large"); + } + result <<= 7; + result |= b & 0x7f; + } while ((b & 0x80) != 0); + return result; + } + + private static int readShortFormLength(int firstLengthByte) { + return firstLengthByte & 0x7f; + } + + private static int readLongFormLength(InputStream in, int firstLengthByte) + throws IOException, BerDataValueFormatException { + // The low 7 bits of the first byte represent the number of bytes (following the first + // byte) in which the length is in big-endian base-256 form + int byteCount = firstLengthByte & 0x7f; + if (byteCount > 4) { + throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes"); + } + int result = 0; + for (int i = 0; i < byteCount; i++) { + int b = in.read(); + if (b == -1) { + throw new BerDataValueFormatException("Truncated length"); + } + if (result > Integer.MAX_VALUE >>> 8) { + throw new BerDataValueFormatException("Length too large"); + } + result <<= 8; + result |= b & 0xff; + } + return result; + } + + private static void skipDefiniteLengthContents(InputStream in, int len) + throws IOException, BerDataValueFormatException { + long bytesRead = 0; + while (len > 0) { + int skipped = (int) in.skip(len); + if (skipped <= 0) { + throw new BerDataValueFormatException( + "Truncated definite-length contents: " + bytesRead + " bytes read" + + ", " + len + " missing"); + } + len -= skipped; + bytesRead += skipped; + } + } + + private static int skipPrimitiveIndefiniteLengthContents(InputStream in) + throws IOException, BerDataValueFormatException { + // Contents are terminated by 0x00 0x00 + boolean prevZeroByte = false; + int bytesRead = 0; + while (true) { + int b = in.read(); + if (b == -1) { + throw new BerDataValueFormatException( + "Truncated indefinite-length contents: " + bytesRead + " bytes read"); + } + bytesRead++; + if (bytesRead < 0) { + throw new BerDataValueFormatException("Indefinite-length contents too long"); + } + if (b == 0) { + if (prevZeroByte) { + // End of contents reached -- we've read the value and its terminator 0x00 0x00 + return bytesRead - 2; + } + prevZeroByte = true; + continue; + } else { + prevZeroByte = false; + } + } + } + + private static int skipConstructedIndefiniteLengthContents(RecordingInputStream in) + throws BerDataValueFormatException { + // Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it + // can contain data values which are indefinite length encoded as well. As a result, we + // must parse the direct children of this data value to correctly skip over the contents of + // this data value. + int readByteCountBefore = in.getReadByteCount(); + while (true) { + // We can't easily peek for the 0x00 0x00 terminator using the provided InputStream. + // Thus, we use the fact that 0x00 0x00 parses as a data value whose encoded form we + // then check below to see whether it's 0x00 0x00. + BerDataValue dataValue = readDataValue(in); + if (dataValue == null) { + throw new BerDataValueFormatException( + "Truncated indefinite-length contents: " + + (in.getReadByteCount() - readByteCountBefore) + " bytes read"); + } + if (in.getReadByteCount() <= 0) { + throw new BerDataValueFormatException("Indefinite-length contents too long"); + } + ByteBuffer encoded = dataValue.getEncoded(); + if ((encoded.remaining() == 2) && (encoded.get(0) == 0) && (encoded.get(1) == 0)) { + // 0x00 0x00 encountered + return in.getReadByteCount() - readByteCountBefore - 2; + } + } + } + + private static class RecordingInputStream extends InputStream { + private final InputStream mIn; + private final ByteArrayOutputStream mBuf; + + private RecordingInputStream(InputStream in) { + mIn = in; + mBuf = new ByteArrayOutputStream(); + } + + public byte[] getReadBytes() { + return mBuf.toByteArray(); + } + + public int getReadByteCount() { + return mBuf.size(); + } + + @Override + public int read() throws IOException { + int b = mIn.read(); + if (b != -1) { + mBuf.write(b); + } + return b; + } + + @Override + public int read(byte[] b) throws IOException { + int len = mIn.read(b); + if (len > 0) { + mBuf.write(b, 0, len); + } + return len; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + len = mIn.read(b, off, len); + if (len > 0) { + mBuf.write(b, off, len); + } + return len; + } + + @Override + public long skip(long n) throws IOException { + if (n <= 0) { + return mIn.skip(n); + } + + byte[] buf = new byte[4096]; + int len = mIn.read(buf, 0, (int) Math.min(buf.length, n)); + if (len > 0) { + mBuf.write(buf, 0, len); + } + return (len < 0) ? 0 : len; + } + + @Override + public int available() throws IOException { + return super.available(); + } + + @Override + public void close() throws IOException { + super.close(); + } + + @Override + public synchronized void mark(int readlimit) {} + + @Override + public synchronized void reset() throws IOException { + throw new IOException("mark/reset not supported"); + } + + @Override + public boolean markSupported() { + return false; + } + } +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/package-info.java b/src/main/java/net/dongliu/apk/parser/cert/package-info.java new file mode 100644 index 0000000..2f2e1d6 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/package-info.java @@ -0,0 +1,5 @@ +/** + * Code in ths package copied from Android apksig source. + * Only for Internal Use. + */ +package net.dongliu.apk.parser.cert; \ No newline at end of file diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/AlgorithmIdentifier.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/AlgorithmIdentifier.java new file mode 100644 index 0000000..0715581 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/AlgorithmIdentifier.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + + +import net.dongliu.apk.parser.cert.asn1.Asn1Class; +import net.dongliu.apk.parser.cert.asn1.Asn1Field; +import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject; +import net.dongliu.apk.parser.cert.asn1.Asn1Type; + +/** + * PKCS #7 {@code AlgorithmIdentifier} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class AlgorithmIdentifier { + + @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER) + public String algorithm; + + @Asn1Field(index = 1, type = Asn1Type.ANY, optional = true) + public Asn1OpaqueObject parameters; + + public AlgorithmIdentifier() {} + + public AlgorithmIdentifier(String algorithmOid, Asn1OpaqueObject parameters) { + this.algorithm = algorithmOid; + this.parameters = parameters; + } +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Attribute.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Attribute.java new file mode 100644 index 0000000..a56dcc6 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Attribute.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +import net.dongliu.apk.parser.cert.asn1.Asn1Class; +import net.dongliu.apk.parser.cert.asn1.Asn1Field; +import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject; +import net.dongliu.apk.parser.cert.asn1.Asn1Type; + +import java.util.List; + +/** + * PKCS #7 {@code Attribute} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class Attribute { + + @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER) + public String attrType; + + @Asn1Field(index = 1, type = Asn1Type.SET_OF) + public List attrValues; +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/ContentInfo.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/ContentInfo.java new file mode 100644 index 0000000..16c5263 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/ContentInfo.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + + +import net.dongliu.apk.parser.cert.asn1.*; + +/** + * PKCS #7 {@code ContentInfo} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class ContentInfo { + + @Asn1Field(index = 1, type = Asn1Type.OBJECT_IDENTIFIER) + public String contentType; + + @Asn1Field(index = 2, type = Asn1Type.ANY, tagging = Asn1Tagging.EXPLICIT, tagNumber = 0) + public Asn1OpaqueObject content; +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/EncapsulatedContentInfo.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/EncapsulatedContentInfo.java new file mode 100644 index 0000000..113f330 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/EncapsulatedContentInfo.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +import net.dongliu.apk.parser.cert.asn1.Asn1Class; +import net.dongliu.apk.parser.cert.asn1.Asn1Field; +import net.dongliu.apk.parser.cert.asn1.Asn1Tagging; +import net.dongliu.apk.parser.cert.asn1.Asn1Type; + +import java.nio.ByteBuffer; + +/** + * PKCS #7 {@code EncapsulatedContentInfo} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class EncapsulatedContentInfo { + + @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER) + public String contentType; + + @Asn1Field( + index = 1, + type = Asn1Type.OCTET_STRING, + tagging = Asn1Tagging.EXPLICIT, tagNumber = 0, + optional = true) + public ByteBuffer content; + + public EncapsulatedContentInfo() {} + + public EncapsulatedContentInfo(String contentTypeOid) { + contentType = contentTypeOid; + } +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/IssuerAndSerialNumber.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/IssuerAndSerialNumber.java new file mode 100644 index 0000000..e244428 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/IssuerAndSerialNumber.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +import net.dongliu.apk.parser.cert.asn1.Asn1Class; +import net.dongliu.apk.parser.cert.asn1.Asn1Field; +import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject; +import net.dongliu.apk.parser.cert.asn1.Asn1Type; + +import java.math.BigInteger; + +/** + * PKCS #7 {@code IssuerAndSerialNumber} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class IssuerAndSerialNumber { + + @Asn1Field(index = 0, type = Asn1Type.ANY) + public Asn1OpaqueObject issuer; + + @Asn1Field(index = 1, type = Asn1Type.INTEGER) + public BigInteger certificateSerialNumber; + + public IssuerAndSerialNumber() {} + + public IssuerAndSerialNumber(Asn1OpaqueObject issuer, BigInteger certificateSerialNumber) { + this.issuer = issuer; + this.certificateSerialNumber = certificateSerialNumber; + } +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Pkcs7Constants.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Pkcs7Constants.java new file mode 100644 index 0000000..16b46ea --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/Pkcs7Constants.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +/** + * Assorted PKCS #7 constants from RFC 5652. + */ +public abstract class Pkcs7Constants { + private Pkcs7Constants() {} + + public static final String OID_DATA = "1.2.840.113549.1.7.1"; + public static final String OID_SIGNED_DATA = "1.2.840.113549.1.7.2"; + public static final String OID_CONTENT_TYPE = "1.2.840.113549.1.9.3"; + public static final String OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4"; +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignedData.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignedData.java new file mode 100644 index 0000000..b1dc720 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignedData.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +import net.dongliu.apk.parser.cert.asn1.*; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * PKCS #7 {@code SignedData} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class SignedData { + + @Asn1Field(index = 0, type = Asn1Type.INTEGER) + public int version; + + @Asn1Field(index = 1, type = Asn1Type.SET_OF) + public List digestAlgorithms; + + @Asn1Field(index = 2, type = Asn1Type.SEQUENCE) + public EncapsulatedContentInfo encapContentInfo; + + @Asn1Field( + index = 3, + type = Asn1Type.SET_OF, + tagging = Asn1Tagging.IMPLICIT, tagNumber = 0, + optional = true) + public List certificates; + + @Asn1Field( + index = 4, + type = Asn1Type.SET_OF, + tagging = Asn1Tagging.IMPLICIT, tagNumber = 1, + optional = true) + public List crls; + + @Asn1Field(index = 5, type = Asn1Type.SET_OF) + public List signerInfos; +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerIdentifier.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerIdentifier.java new file mode 100644 index 0000000..b6f9ef9 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerIdentifier.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +import net.dongliu.apk.parser.cert.asn1.Asn1Class; +import net.dongliu.apk.parser.cert.asn1.Asn1Field; +import net.dongliu.apk.parser.cert.asn1.Asn1Tagging; +import net.dongliu.apk.parser.cert.asn1.Asn1Type; + +import java.nio.ByteBuffer; + +/** + * PKCS #7 {@code SignerIdentifier} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.CHOICE) +public class SignerIdentifier { + + @Asn1Field(type = Asn1Type.SEQUENCE) + public IssuerAndSerialNumber issuerAndSerialNumber; + + @Asn1Field(type = Asn1Type.OCTET_STRING, tagging = Asn1Tagging.IMPLICIT, tagNumber = 0) + public ByteBuffer subjectKeyIdentifier; + + public SignerIdentifier() {} + + public SignerIdentifier(IssuerAndSerialNumber issuerAndSerialNumber) { + this.issuerAndSerialNumber = issuerAndSerialNumber; + } +} diff --git a/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerInfo.java b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerInfo.java new file mode 100644 index 0000000..770e556 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/cert/pkcs7/SignerInfo.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dongliu.apk.parser.cert.pkcs7; + +import net.dongliu.apk.parser.cert.asn1.*; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * PKCS #7 {@code SignerInfo} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class SignerInfo { + + @Asn1Field(index = 0, type = Asn1Type.INTEGER) + public int version; + + @Asn1Field(index = 1, type = Asn1Type.CHOICE) + public SignerIdentifier sid; + + @Asn1Field(index = 2, type = Asn1Type.SEQUENCE) + public AlgorithmIdentifier digestAlgorithm; + + @Asn1Field( + index = 3, + type = Asn1Type.SET_OF, + tagging = Asn1Tagging.IMPLICIT, tagNumber = 0, + optional = true) + public Asn1OpaqueObject signedAttrs; + + @Asn1Field(index = 4, type = Asn1Type.SEQUENCE) + public AlgorithmIdentifier signatureAlgorithm; + + @Asn1Field(index = 5, type = Asn1Type.OCTET_STRING) + public ByteBuffer signature; + + @Asn1Field( + index = 6, + type = Asn1Type.SET_OF, + tagging = Asn1Tagging.IMPLICIT, tagNumber = 1, + optional = true) + public List unsignedAttrs; +} diff --git a/src/main/java/net/dongliu/apk/parser/parser/BCCertificateParser.java b/src/main/java/net/dongliu/apk/parser/parser/BCCertificateParser.java new file mode 100644 index 0000000..b3e855c --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/parser/BCCertificateParser.java @@ -0,0 +1,56 @@ +package net.dongliu.apk.parser.parser; + +import net.dongliu.apk.parser.bean.CertificateMeta; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.SignerInformationStore; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.util.Store; + +import java.security.Provider; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Parser certificate info using BouncyCastle. + * + * @author dongliu + */ +class BCCertificateParser extends CertificateParser { + + private static final Provider provider = new BouncyCastleProvider(); + + public BCCertificateParser(byte[] data) { + super(data); + } + + /** + * get certificate info + */ + public List parse() throws CertificateException { + CMSSignedData cmsSignedData; + try { + cmsSignedData = new CMSSignedData(data); + } catch (CMSException e) { + throw new CertificateException(e); + } + Store certStore = cmsSignedData.getCertificates(); + SignerInformationStore signerInfos = cmsSignedData.getSignerInfos(); + Collection signers = signerInfos.getSigners(); + List certificates = new ArrayList<>(); + for (SignerInformation signer : signers) { + Collection matches = certStore.getMatches(signer.getSID()); + for (X509CertificateHolder holder : matches) { + certificates.add(new JcaX509CertificateConverter().setProvider(provider).getCertificate(holder)); + } + } + return CertificateMetas.from(certificates); + } + +} diff --git a/src/main/java/net/dongliu/apk/parser/parser/CertificateMetas.java b/src/main/java/net/dongliu/apk/parser/parser/CertificateMetas.java index a37b2c3..80c033c 100644 --- a/src/main/java/net/dongliu/apk/parser/parser/CertificateMetas.java +++ b/src/main/java/net/dongliu/apk/parser/parser/CertificateMetas.java @@ -33,7 +33,7 @@ public static CertificateMeta from(X509Certificate certificate) throws Certifica certificateMeta.setCertMd5(certMd5); certificateMeta.setStartDate(certificate.getNotBefore()); certificateMeta.setEndDate(certificate.getNotAfter()); - certificateMeta.setSignAlgorithm(certificate.getSigAlgName()); + certificateMeta.setSignAlgorithm(certificate.getSigAlgName().toUpperCase()); certificateMeta.setSignAlgorithmOID(certificate.getSigAlgOID()); return certificateMeta; } diff --git a/src/main/java/net/dongliu/apk/parser/parser/CertificateParser.java b/src/main/java/net/dongliu/apk/parser/parser/CertificateParser.java index a8e4dbf..78564d8 100644 --- a/src/main/java/net/dongliu/apk/parser/parser/CertificateParser.java +++ b/src/main/java/net/dongliu/apk/parser/parser/CertificateParser.java @@ -1,20 +1,9 @@ package net.dongliu.apk.parser.parser; +import net.dongliu.apk.parser.ApkParsers; import net.dongliu.apk.parser.bean.CertificateMeta; -import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import org.bouncycastle.cms.CMSException; -import org.bouncycastle.cms.CMSSignedData; -import org.bouncycastle.cms.SignerInformation; -import org.bouncycastle.cms.SignerInformationStore; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.util.Store; - -import java.security.Security; + import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; /** @@ -23,41 +12,24 @@ * * @author dongliu */ -public class CertificateParser { - - private final byte[] data; +public abstract class CertificateParser { - static { - Security.addProvider(new BouncyCastleProvider()); - } + protected final byte[] data; public CertificateParser(byte[] data) { this.data = data; } + public static CertificateParser getInstance(byte[] data) { + if (ApkParsers.useBouncyCastle()) { + return new BCCertificateParser(data); + } + return new JSSECertificateParser(data); + } + /** * get certificate info */ - public List parse() throws CertificateException { - - - CMSSignedData cmsSignedData; - try { - cmsSignedData = new CMSSignedData(data); - } catch (CMSException e) { - throw new CertificateException(e); - } - Store certStore = cmsSignedData.getCertificates(); - SignerInformationStore signerInfos = cmsSignedData.getSignerInfos(); - Collection signers = signerInfos.getSigners(); - List certificates = new ArrayList<>(); - for (SignerInformation signer : signers) { - Collection matches = certStore.getMatches(signer.getSID()); - for (X509CertificateHolder holder : matches) { - certificates.add(new JcaX509CertificateConverter().setProvider("BC").getCertificate(holder)); - } - } - return CertificateMetas.from(certificates); - } + public abstract List parse() throws CertificateException; } diff --git a/src/main/java/net/dongliu/apk/parser/parser/JSSECertificateParser.java b/src/main/java/net/dongliu/apk/parser/parser/JSSECertificateParser.java new file mode 100644 index 0000000..fe05fe2 --- /dev/null +++ b/src/main/java/net/dongliu/apk/parser/parser/JSSECertificateParser.java @@ -0,0 +1,59 @@ +package net.dongliu.apk.parser.parser; + +import net.dongliu.apk.parser.bean.CertificateMeta; +import net.dongliu.apk.parser.cert.asn1.Asn1BerParser; +import net.dongliu.apk.parser.cert.asn1.Asn1DecodingException; +import net.dongliu.apk.parser.cert.asn1.Asn1OpaqueObject; +import net.dongliu.apk.parser.cert.pkcs7.ContentInfo; +import net.dongliu.apk.parser.cert.pkcs7.Pkcs7Constants; +import net.dongliu.apk.parser.cert.pkcs7.SignedData; +import net.dongliu.apk.parser.utils.Buffers; + +import java.io.ByteArrayInputStream; +import java.nio.ByteBuffer; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +/** + * Parser certificate info using jsse. + * + * @author dongliu + */ +class JSSECertificateParser extends CertificateParser { + public JSSECertificateParser(byte[] data) { + super(data); + } + + public List parse() throws CertificateException { + ContentInfo contentInfo; + try { + contentInfo = Asn1BerParser.parse(ByteBuffer.wrap(data), ContentInfo.class); + } catch (Asn1DecodingException e) { + throw new CertificateException(e); + } + if (!Pkcs7Constants.OID_SIGNED_DATA.equals(contentInfo.contentType)) { + throw new CertificateException("Unsupported ContentInfo.contentType: " + contentInfo.contentType); + } + SignedData signedData; + try { + signedData = Asn1BerParser.parse(contentInfo.content.getEncoded(), SignedData.class); + } catch (Asn1DecodingException e) { + throw new CertificateException(e); + } + List encodedCertificates = signedData.certificates; + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + List result = new ArrayList<>(encodedCertificates.size()); + for (int i = 0; i < encodedCertificates.size(); i++) { + Asn1OpaqueObject encodedCertificate = encodedCertificates.get(i); + byte[] encodedForm = Buffers.readBytes(encodedCertificate.getEncoded()); + Certificate certificate = certFactory.generateCertificate(new ByteArrayInputStream(encodedForm)); + result.add((X509Certificate) certificate); + } + return CertificateMetas.from(result); + } + +} diff --git a/src/test/java/net/dongliu/apk/parser/parser/BCCertificateParserTest.java b/src/test/java/net/dongliu/apk/parser/parser/BCCertificateParserTest.java new file mode 100644 index 0000000..2a52f92 --- /dev/null +++ b/src/test/java/net/dongliu/apk/parser/parser/BCCertificateParserTest.java @@ -0,0 +1,31 @@ +package net.dongliu.apk.parser.parser; + +import net.dongliu.apk.parser.bean.CertificateMeta; +import net.dongliu.apk.parser.utils.Inputs; +import org.junit.Test; + +import java.io.IOException; +import java.security.cert.CertificateException; +import java.util.List; + +import static org.junit.Assert.*; + +public class BCCertificateParserTest { + + @Test + public void parse() throws IOException, CertificateException { + byte[] data = Inputs.readAll(getClass().getResourceAsStream("/sign/63_CERT.RSA")); + CertificateParser parser = new BCCertificateParser(data); + List certificateMetas = parser.parse(); + assertEquals("SHA1WITHRSA", certificateMetas.get(0).getSignAlgorithm()); + + + data = Inputs.readAll(getClass().getResourceAsStream("/sign/gmail_CERT.RSA")); + parser = new BCCertificateParser(data); + certificateMetas = parser.parse(); + assertEquals(1, certificateMetas.size()); + CertificateMeta certificateMeta = certificateMetas.get(0); + assertEquals("MD5WITHRSA", certificateMeta.getSignAlgorithm()); + assertEquals("9decc0608f773ad1f4a017c02598d80c", certificateMeta.getCertBase64Md5()); + } +} \ No newline at end of file diff --git a/src/test/java/net/dongliu/apk/parser/parser/JSSECertificateParserTest.java b/src/test/java/net/dongliu/apk/parser/parser/JSSECertificateParserTest.java new file mode 100644 index 0000000..6df58e7 --- /dev/null +++ b/src/test/java/net/dongliu/apk/parser/parser/JSSECertificateParserTest.java @@ -0,0 +1,38 @@ +package net.dongliu.apk.parser.parser; + +import net.dongliu.apk.parser.bean.CertificateMeta; +import net.dongliu.apk.parser.cert.asn1.Asn1DecodingException; +import net.dongliu.apk.parser.utils.Inputs; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.IOException; +import java.security.cert.CertificateException; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class JSSECertificateParserTest { + + + @Test + @Ignore + // issue 63 + public void parseJDKFailed() throws IOException, CertificateException { + byte[] data = Inputs.readAll(getClass().getResourceAsStream("/sign/63_CERT.RSA")); + CertificateParser parser = new JSSECertificateParser(data); + List certificateMetas = parser.parse(); + assertEquals("SHA1WITHRSA", certificateMetas.get(0).getSignAlgorithm()); + } + + @Test + public void parseJDK() throws IOException, CertificateException { + byte[] data = Inputs.readAll(getClass().getResourceAsStream("/sign/gmail_CERT.RSA")); + CertificateParser parser = new JSSECertificateParser(data); + List certificateMetas = parser.parse(); + assertEquals(1, certificateMetas.size()); + CertificateMeta certificateMeta = certificateMetas.get(0); + assertEquals("MD5WITHRSA", certificateMeta.getSignAlgorithm()); + assertEquals("9decc0608f773ad1f4a017c02598d80c", certificateMeta.getCertBase64Md5()); + } +} \ No newline at end of file diff --git a/src/test/resources/sign/63_CERT.RSA b/src/test/resources/sign/63_CERT.RSA new file mode 100644 index 0000000..3944d95 Binary files /dev/null and b/src/test/resources/sign/63_CERT.RSA differ diff --git a/src/test/resources/sign/gmail_CERT.RSA b/src/test/resources/sign/gmail_CERT.RSA new file mode 100644 index 0000000..ec21317 Binary files /dev/null and b/src/test/resources/sign/gmail_CERT.RSA differ diff --git a/src/test/resources/sign/gmail_sign_block b/src/test/resources/sign/gmail_sign_block new file mode 100644 index 0000000..e82ce58 Binary files /dev/null and b/src/test/resources/sign/gmail_sign_block differ