| /* |
| * 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 com.android.apksig.internal.asn1; |
| |
| import com.android.apksig.internal.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. |
| * |
| * <p>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: |
| * <ul> |
| * <li>The class must be annotated with {@link Asn1Class}.</li> |
| * <li>Member fields of the class which are to be encoded must be annotated with |
| * {@link Asn1Field} and be public.</li> |
| * </ul> |
| * |
| * @throws Asn1EncodingException if the input could not be encoded |
| */ |
| public static byte[] encode(Object container) throws Asn1EncodingException { |
| Class<?> containerClass = container.getClass(); |
| Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(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); |
| case UNENCODED_CONTAINER: |
| return toSequence(container, true); |
| default: |
| throw new Asn1EncodingException("Unsupported container type: " + containerType); |
| } |
| } |
| |
| private static byte[] toChoice(Object container) throws Asn1EncodingException { |
| Class<?> containerClass = container.getClass(); |
| List<AnnotatedField> 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 { |
| return toSequence(container, false); |
| } |
| |
| private static byte[] toSequence(Object container, boolean omitTag) |
| throws Asn1EncodingException { |
| Class<?> containerClass = container.getClass(); |
| List<AnnotatedField> fields = getAnnotatedFields(container); |
| Collections.sort( |
| fields, (f1, f2) -> 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<byte[]> serializedFields = new ArrayList<>(fields.size()); |
| int contentLen = 0; |
| 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); |
| contentLen += serializedField.length; |
| } |
| } |
| |
| if (omitTag) { |
| byte[] unencodedResult = new byte[contentLen]; |
| int index = 0; |
| for (byte[] serializedField : serializedFields) { |
| System.arraycopy(serializedField, 0, unencodedResult, index, serializedField.length); |
| index += serializedField.length; |
| } |
| return unencodedResult; |
| } else { |
| 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 { |
| return toSequenceOrSetOf(values, elementType, true); |
| } |
| |
| private static byte[] toSequenceOf(Collection<?> values, Asn1Type elementType) throws Asn1EncodingException { |
| return toSequenceOrSetOf(values, elementType, false); |
| } |
| |
| private static byte[] toSequenceOrSetOf(Collection<?> values, Asn1Type elementType, boolean toSet) |
| throws Asn1EncodingException { |
| List<byte[]> serializedValues = new ArrayList<>(values.size()); |
| for (Object value : values) { |
| serializedValues.add(JavaToDerConverter.toDer(value, elementType, null)); |
| } |
| int tagNumber; |
| if (toSet) { |
| if (serializedValues.size() > 1) { |
| Collections.sort(serializedValues, ByteArrayLexicographicComparator.INSTANCE); |
| } |
| tagNumber = BerEncoding.TAG_NUMBER_SET; |
| } else { |
| tagNumber = BerEncoding.TAG_NUMBER_SEQUENCE; |
| } |
| return createTag( |
| BerEncoding.TAG_CLASS_UNIVERSAL, true, tagNumber, |
| 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<byte[]> { |
| 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<AnnotatedField> getAnnotatedFields(Object container) |
| throws Asn1EncodingException { |
| Class<?> containerClass = container.getClass(); |
| Field[] declaredFields = containerClass.getDeclaredFields(); |
| List<AnnotatedField> result = new ArrayList<>(declaredFields.length); |
| for (Field field : declaredFields) { |
| Asn1Field annotation = field.getDeclaredAnnotation(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[] toBoolean(boolean value) { |
| // A boolean should be encoded in a single byte with a value of 0 for false and any non-zero |
| // value for true. |
| byte[] result = new byte[1]; |
| if (value == false) { |
| result[0] = 0; |
| } else { |
| result[0] = 1; |
| } |
| return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_BOOLEAN, result); |
| } |
| |
| 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: |
| case BIT_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.getTagNumber(targetType), |
| 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 BOOLEAN: |
| if (source instanceof Boolean) { |
| return toBoolean((Boolean) (source)); |
| } |
| break; |
| case UTC_TIME: |
| case GENERALIZED_TIME: |
| if (source instanceof String) { |
| return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false, |
| BerEncoding.getTagNumber(targetType), ((String) source).getBytes()); |
| } |
| case OBJECT_IDENTIFIER: |
| if (source instanceof String) { |
| return toOid((String) source); |
| } |
| break; |
| case SEQUENCE: |
| { |
| Asn1Class containerAnnotation = |
| sourceType.getDeclaredAnnotation(Asn1Class.class); |
| if ((containerAnnotation != null) |
| && (containerAnnotation.type() == Asn1Type.SEQUENCE)) { |
| return toSequence(source); |
| } |
| break; |
| } |
| case CHOICE: |
| { |
| Asn1Class containerAnnotation = |
| sourceType.getDeclaredAnnotation(Asn1Class.class); |
| if ((containerAnnotation != null) |
| && (containerAnnotation.type() == Asn1Type.CHOICE)) { |
| return toChoice(source); |
| } |
| break; |
| } |
| case SET_OF: |
| return toSetOf((Collection<?>) source, targetElementType); |
| case SEQUENCE_OF: |
| return toSequenceOf((Collection<?>) source, targetElementType); |
| default: |
| break; |
| } |
| |
| throw new Asn1EncodingException( |
| "Unsupported conversion: " + sourceType.getName() + " to ASN.1 " + targetType); |
| } |
| } |
| } |