Resolve 'redundant length bytes' error when parsing BER encoded certificate
am: 1b063ad846
Change-Id: I0523d0d5743f3815edf241acff7822d4987c09d2
diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
index aeb02cd..c91d219 100644
--- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
+++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java
@@ -22,6 +22,7 @@
import com.android.apksig.SigningCertificateLineage.SignerCapabilities;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.MinSdkVersionException;
+import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
@@ -1347,7 +1348,7 @@
// Load certificates
Collection<? extends Certificate> certs;
try (FileInputStream in = new FileInputStream(certFile)) {
- certs = CertificateFactory.getInstance("X.509").generateCertificates(in);
+ certs = X509CertificateUtils.generateCertificates(in);
}
List<X509Certificate> certList = new ArrayList<>(certs.size());
for (Certificate cert : certs) {
diff --git a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
index a828bcc..2fd7808 100644
--- a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java
@@ -37,6 +37,7 @@
import com.android.apksig.internal.pkcs7.SignerInfo;
import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.internal.util.InclusiveIntRange;
import com.android.apksig.internal.zip.CentralDirectoryRecord;
@@ -44,7 +45,7 @@
import com.android.apksig.util.DataSinks;
import com.android.apksig.util.DataSource;
import com.android.apksig.zip.ZipFormatException;
-import java.io.ByteArrayInputStream;
+
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
@@ -56,7 +57,6 @@
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
@@ -72,6 +72,7 @@
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
+
import javax.security.auth.x500.X500Principal;
/**
@@ -748,22 +749,13 @@
return Collections.emptyList();
}
- CertificateFactory certFactory;
- try {
- certFactory = CertificateFactory.getInstance("X.509");
- } catch (CertificateException e) {
- throw new RuntimeException("Failed to create X.509 CertificateFactory", e);
- }
-
List<X509Certificate> result = new ArrayList<>(encodedCertificates.size());
for (int i = 0; i < encodedCertificates.size(); i++) {
Asn1OpaqueObject encodedCertificate = encodedCertificates.get(i);
X509Certificate certificate;
byte[] encodedForm = ByteBufferUtils.toByteArray(encodedCertificate.getEncoded());
try {
- certificate =
- (X509Certificate) certFactory.generateCertificate(
- new ByteArrayInputStream(encodedForm));
+ certificate = X509CertificateUtils.generateCertificate(encodedForm);
} catch (CertificateException e) {
throw new CertificateException("Failed to parse certificate #" + (i + 1), e);
}
@@ -848,6 +840,8 @@
private static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3";
private static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1";
static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2";
+ static final String OID_SIG_SHA384_WITH_DSA = "2.16.840.1.101.3.4.3.3";
+ static final String OID_SIG_SHA512_WITH_DSA = "2.16.840.1.101.3.4.3.4";
static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
private static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1";
@@ -1215,6 +1209,8 @@
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_DSA, "SHA-1 with DSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_DSA, "SHA-224 with DSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_DSA, "SHA-256 with DSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_DSA, "SHA-384 with DSA");
+ OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_DSA, "SHA-512 with DSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_EC_PUBLIC_KEY, "ECDSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_ECDSA, "SHA-1 with ECDSA");
diff --git a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
index a7c5d8f..bbef027 100644
--- a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java
@@ -24,6 +24,7 @@
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.util.DataSource;
@@ -345,10 +346,7 @@
byte[] encodedCert = ApkSigningBlockUtils.readLengthPrefixedByteArray(certificates);
X509Certificate certificate;
try {
- certificate =
- (X509Certificate)
- certFactory.generateCertificate(
- new ByteArrayInputStream(encodedCert));
+ certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
} catch (CertificateException e) {
result.addError(
Issue.V2_SIG_MALFORMED_CERTIFICATE,
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
index 453f12c..9a2932b 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java
@@ -30,10 +30,10 @@
import com.android.apksig.internal.apk.SignatureInfo;
import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.util.ByteBufferUtils;
+import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.util.DataSource;
-import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
@@ -293,7 +293,7 @@
int parsedMaxSdkVersion = signerBlock.getInt();
result.minSdkVersion = parsedMinSdkVersion;
result.maxSdkVersion = parsedMaxSdkVersion;
- if (parsedMinSdkVersion < 1 || parsedMinSdkVersion > parsedMaxSdkVersion) {
+ if (parsedMinSdkVersion < 0 || parsedMinSdkVersion > parsedMaxSdkVersion) {
result.addError(
Issue.V3_SIG_INVALID_SDK_VERSIONS, parsedMinSdkVersion, parsedMaxSdkVersion);
}
@@ -407,10 +407,7 @@
byte[] encodedCert = readLengthPrefixedByteArray(certificates);
X509Certificate certificate;
try {
- certificate =
- (X509Certificate)
- certFactory.generateCertificate(
- new ByteArrayInputStream(encodedCert));
+ certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
} catch (CertificateException e) {
result.addError(
Issue.V3_SIG_MALFORMED_CERTIFICATE,
diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java
index 946b03a..e1e01a9 100644
--- a/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java
+++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java
@@ -25,8 +25,8 @@
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.X509CertificateUtils;
-import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
@@ -39,7 +39,6 @@
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
@@ -93,12 +92,6 @@
// * uint32: signature algorithm id (used by to sign next cert in lineage)
// * length-prefixed bytes: signature over above signed data
- CertificateFactory certFactory;
- try {
- certFactory = CertificateFactory.getInstance("X.509");
- } catch (CertificateException e) {
- throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
- }
X509Certificate lastCert = null;
int lastSigAlgorithmId = 0;
@@ -146,8 +139,7 @@
throw new SecurityException("Signing algorithm ID mismatch for certificate #"
+ nodeBytes + " when verifying V3SigningCertificateLineage object");
}
- lastCert = (X509Certificate)
- certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
+ lastCert = X509CertificateUtils.generateCertificate(encodedCert);
lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
if (certHistorySet.contains(lastCert)) {
throw new SecurityException("Encountered duplicate entries in "
diff --git a/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java b/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
index 8b1b59b..232bba3 100644
--- a/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
+++ b/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java
@@ -129,14 +129,15 @@
|| (container.getTagNumber() != expectedTagNumber)) {
throw new Asn1UnexpectedTagException(
"Unexpected data value read as " + containerClass.getName()
- + ". Expected " + BerEncoding.tagClassAndNumberToString(
+ + ". Expected " + BerEncoding.tagClassAndNumberToString(
expectedTagClass, expectedTagNumber)
- + ", but read: " + BerEncoding.tagClassAndNumberToString(
+ + ", but read: " + BerEncoding.tagClassAndNumberToString(
container.getTagClass(), container.getTagNumber()));
}
return parseSequence(container, containerClass);
}
-
+ case UNENCODED_CONTAINER:
+ return parseSequence(container, containerClass, true);
default:
throw new Asn1DecodingException("Parsing container " + dataType + " not supported");
}
@@ -177,7 +178,6 @@
} 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 {
@@ -194,6 +194,11 @@
private static <T> T parseSequence(BerDataValue container, Class<T> containerClass)
throws Asn1DecodingException {
+ return parseSequence(container, containerClass, false);
+ }
+
+ private static <T> T parseSequence(BerDataValue container, Class<T> containerClass,
+ boolean isUnencodedContainer) throws Asn1DecodingException {
List<AnnotatedField> fields = getAnnotatedFields(containerClass);
Collections.sort(
fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index());
@@ -226,7 +231,13 @@
while (nextUnreadFieldIndex < fields.size()) {
BerDataValue dataValue;
try {
- dataValue = elementsReader.readDataValue();
+ // if this is the first field of an unencoded container then the entire contents of
+ // the container should be used when assigning to this field.
+ if (isUnencodedContainer && nextUnreadFieldIndex == 0) {
+ dataValue = container;
+ } else {
+ dataValue = elementsReader.readDataValue();
+ }
} catch (BerDataValueFormatException e) {
throw new Asn1DecodingException("Malformed data value", e);
}
@@ -310,6 +321,7 @@
switch (containerAnnotation.type()) {
case CHOICE:
case SEQUENCE:
+ case UNENCODED_CONTAINER:
return containerAnnotation.type();
default:
throw new Asn1DecodingException(
@@ -591,7 +603,6 @@
} else if (Asn1OpaqueObject.class.equals(targetType)) {
return (T) new Asn1OpaqueObject(dataValue.getEncoded());
}
-
ByteBuffer encodedContents = dataValue.getEncodedContents();
switch (sourceType) {
case INTEGER:
@@ -608,6 +619,29 @@
return (T) oidToString(encodedContents);
}
break;
+ case UTC_TIME:
+ case GENERALIZED_TIME:
+ if (String.class.equals(targetType)) {
+ return (T) new String(ByteBufferUtils.toByteArray(encodedContents));
+ }
+ case BOOLEAN:
+ // A boolean should be encoded in a single byte with a value of 0 for false and
+ // any non-zero value for true.
+ if (boolean.class.equals(targetType)) {
+ if (encodedContents.remaining() != 1) {
+ throw new Asn1DecodingException(
+ "Incorrect encoded size of boolean value: "
+ + encodedContents.remaining());
+ }
+ boolean result;
+ if (encodedContents.get() == 0) {
+ result = false;
+ } else {
+ result = true;
+ }
+ return (T) new Boolean(result);
+
+ }
case SEQUENCE:
{
Asn1Class containerAnnotation =
diff --git a/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java b/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java
index a913d6d..da87368 100644
--- a/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java
+++ b/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java
@@ -17,6 +17,7 @@
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;
@@ -64,6 +65,8 @@
return toChoice(container);
case SEQUENCE:
return toSequence(container);
+ case UNENCODED_CONTAINER:
+ return toSequence(container, true);
default:
throw new Asn1EncodingException("Unsupported container type: " + containerType);
}
@@ -101,6 +104,11 @@
}
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(
@@ -120,6 +128,7 @@
}
List<byte[]> serializedFields = new ArrayList<>(fields.size());
+ int contentLen = 0;
for (AnnotatedField field : fields) {
byte[] serializedField;
try {
@@ -132,25 +141,50 @@
}
if (serializedField != null) {
serializedFields.add(serializedField);
+ contentLen += serializedField.length;
}
}
- return createTag(
- BerEncoding.TAG_CLASS_UNIVERSAL, true, BerEncoding.TAG_NUMBER_SEQUENCE,
- serializedFields.toArray(new byte[0][]));
+ 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)
+ 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));
}
- if (serializedValues.size() > 1) {
- Collections.sort(serializedValues, ByteArrayLexicographicComparator.INSTANCE);
+ 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, BerEncoding.TAG_NUMBER_SET,
+ BerEncoding.TAG_CLASS_UNIVERSAL, true, tagNumber,
serializedValues.toArray(new byte[0][]));
}
@@ -220,6 +254,18 @@
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("\\.");
@@ -469,6 +515,7 @@
switch (targetType) {
case OCTET_STRING:
+ case BIT_STRING:
byte[] value = null;
if (source instanceof ByteBuffer) {
ByteBuffer buf = (ByteBuffer) source;
@@ -481,7 +528,7 @@
return createTag(
BerEncoding.TAG_CLASS_UNIVERSAL,
false,
- BerEncoding.TAG_NUMBER_OCTET_STRING,
+ BerEncoding.getTagNumber(targetType),
value);
}
break;
@@ -494,6 +541,17 @@
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);
@@ -521,6 +579,8 @@
}
case SET_OF:
return toSetOf((Collection<?>) source, targetElementType);
+ case SEQUENCE_OF:
+ return toSequenceOf((Collection<?>) source, targetElementType);
default:
break;
}
diff --git a/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java b/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java
index 710df71..7300622 100644
--- a/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java
+++ b/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java
@@ -25,4 +25,11 @@
SEQUENCE,
SEQUENCE_OF,
SET_OF,
+ BIT_STRING,
+ UTC_TIME,
+ GENERALIZED_TIME,
+ BOOLEAN,
+ // This type can be used to annotate classes that encapsulate ASN.1 structures that are not
+ // classified as a SEQUENCE or SET.
+ UNENCODED_CONTAINER
}
diff --git a/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java b/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java
index 5fd0b52..d32330c 100644
--- a/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java
+++ b/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java
@@ -51,11 +51,21 @@
public static final int TAG_CLASS_PRIVATE = 3;
/**
+ * Tag number: BOOLEAN
+ */
+ public static final int TAG_NUMBER_BOOLEAN = 0x1;
+
+ /**
* Tag number: INTEGER
*/
public static final int TAG_NUMBER_INTEGER = 0x2;
/**
+ * Tag number: BIT STRING
+ */
+ public static final int TAG_NUMBER_BIT_STRING = 0x3;
+
+ /**
* Tag number: OCTET STRING
*/
public static final int TAG_NUMBER_OCTET_STRING = 0x4;
@@ -80,6 +90,16 @@
*/
public static final int TAG_NUMBER_SET = 0x11;
+ /**
+ * Tag number: UTC_TIME
+ */
+ public final static int TAG_NUMBER_UTC_TIME = 0x17;
+
+ /**
+ * Tag number: GENERALIZED_TIME
+ */
+ public final static int TAG_NUMBER_GENERALIZED_TIME = 0x18;
+
public static int getTagNumber(Asn1Type dataType) {
switch (dataType) {
case INTEGER:
@@ -88,11 +108,19 @@
return TAG_NUMBER_OBJECT_IDENTIFIER;
case OCTET_STRING:
return TAG_NUMBER_OCTET_STRING;
+ case BIT_STRING:
+ return TAG_NUMBER_BIT_STRING;
case SET_OF:
return TAG_NUMBER_SET;
case SEQUENCE:
case SEQUENCE_OF:
return TAG_NUMBER_SEQUENCE;
+ case UTC_TIME:
+ return TAG_NUMBER_UTC_TIME;
+ case GENERALIZED_TIME:
+ return TAG_NUMBER_GENERALIZED_TIME;
+ case BOOLEAN:
+ return TAG_NUMBER_BOOLEAN;
default:
throw new IllegalArgumentException("Unsupported data type: " + dataType);
}
@@ -141,6 +169,8 @@
return "INTEGER";
case TAG_NUMBER_OCTET_STRING:
return "OCTET STRING";
+ case TAG_NUMBER_BIT_STRING:
+ return "BIT STRING";
case TAG_NUMBER_NULL:
return "NULL";
case TAG_NUMBER_OBJECT_IDENTIFIER:
@@ -149,6 +179,12 @@
return "SEQUENCE";
case TAG_NUMBER_SET:
return "SET";
+ case TAG_NUMBER_BOOLEAN:
+ return "BOOLEAN";
+ case TAG_NUMBER_GENERALIZED_TIME:
+ return "GENERALIZED TIME";
+ case TAG_NUMBER_UTC_TIME:
+ return "UTC TIME";
default:
return "0x" + Integer.toHexString(tagNumber);
}
diff --git a/src/test/java/com/android/apksig/internal/util/ByteStreams.java b/src/main/java/com/android/apksig/internal/util/ByteStreams.java
similarity index 100%
rename from src/test/java/com/android/apksig/internal/util/ByteStreams.java
rename to src/main/java/com/android/apksig/internal/util/ByteStreams.java
diff --git a/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java b/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java
new file mode 100644
index 0000000..9a266f2
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2018 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.util;
+
+import com.android.apksig.internal.asn1.Asn1BerParser;
+import com.android.apksig.internal.asn1.Asn1DecodingException;
+import com.android.apksig.internal.asn1.Asn1DerEncoder;
+import com.android.apksig.internal.asn1.Asn1EncodingException;
+import com.android.apksig.internal.x509.Certificate;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collection;
+
+/**
+ * Provides methods to generate {@code X509Certificate}s from their encoded form. These methods
+ * can be used to generate certificates that would be rejected by the Java {@code
+ * CertificateFactory}.
+ */
+public class X509CertificateUtils {
+
+ private static CertificateFactory sCertFactory = null;
+
+ // The PEM certificate header and footer as specified in RFC 7468:
+ // There is exactly one space character (SP) separating the "BEGIN" or
+ // "END" from the label. There are exactly five hyphen-minus (also
+ // known as dash) characters ("-") on both ends of the encapsulation
+ // boundaries, no more, no less.
+ public static final byte[] BEGIN_CERT_HEADER = "-----BEGIN CERTIFICATE-----".getBytes();
+ public static final byte[] END_CERT_FOOTER = "-----END CERTIFICATE-----".getBytes();
+
+ private static void buildCertFactory() {
+ if (sCertFactory != null) {
+ return;
+ }
+ try {
+ sCertFactory = CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new RuntimeException("Failed to create X.509 CertificateFactory", e);
+ }
+ }
+
+ /**
+ * Generates an {@code X509Certificate} from the {@code InputStream}.
+ *
+ * @throws CertificateException if the {@code InputStream} cannot be decoded to a valid
+ * certificate.
+ */
+ public static X509Certificate generateCertificate(InputStream in) throws CertificateException {
+ byte[] encodedForm;
+ try {
+ encodedForm = ByteStreams.toByteArray(in);
+ } catch (IOException e) {
+ throw new CertificateException("Failed to parse certificate", e);
+ }
+ return generateCertificate(encodedForm);
+ }
+
+ /**
+ * Generates an {@code X509Certificate} from the encoded form.
+ *
+ * @throws CertificateException if the encodedForm cannot be decoded to a valid certificate.
+ */
+ public static X509Certificate generateCertificate(byte[] encodedForm)
+ throws CertificateException {
+ if (sCertFactory == null) {
+ buildCertFactory();
+ }
+ return generateCertificate(encodedForm, sCertFactory);
+ }
+
+ /**
+ * Generates an {@code X509Certificate} from the encoded form using the provided
+ * {@code CertificateFactory}.
+ *
+ * @throws CertificateException if the encodedForm cannot be decoded to a valid certificate.
+ */
+ public static X509Certificate generateCertificate(byte[] encodedForm,
+ CertificateFactory certFactory) throws CertificateException {
+ X509Certificate certificate;
+ try {
+ certificate = (X509Certificate) certFactory.generateCertificate(
+ new ByteArrayInputStream(encodedForm));
+ return certificate;
+ } catch (CertificateException e) {
+ // This could be expected if the certificate is encoded using a BER encoding that does
+ // not use the minimum number of bytes to represent the length of the contents; attempt
+ // to decode the certificate using the BER parser and re-encode using the DER encoder
+ // below.
+ }
+ try {
+ // Some apps were previously signed with a BER encoded certificate that now results
+ // in exceptions from the CertificateFactory generateCertificate(s) methods. Since
+ // the original BER encoding of the certificate is used as the signature for these
+ // apps that original encoding must be maintained when signing updated versions of
+ // these apps and any new apps that may require capabilities guarded by the
+ // signature. To maintain the same signature the BER parser can be used to parse
+ // the certificate, then it can be re-encoded to its DER equivalent which is
+ // accepted by the generateCertificate method. The positions in the ByteBuffer can
+ // then be used with the GuaranteedEncodedFormX509Certificate object to ensure the
+ // getEncoded method returns the original signature of the app.
+ ByteBuffer encodedCertBuffer = getNextDEREncodedCertificateBlock(
+ ByteBuffer.wrap(encodedForm));
+ int startingPos = encodedCertBuffer.position();
+ Certificate reencodedCert = Asn1BerParser.parse(encodedCertBuffer, Certificate.class);
+ byte[] reencodedForm = Asn1DerEncoder.encode(reencodedCert);
+ certificate = (X509Certificate) certFactory.generateCertificate(
+ new ByteArrayInputStream(reencodedForm));
+ // If the reencodedForm is successfully accepted by the CertificateFactory then copy the
+ // original encoding from the ByteBuffer and use that encoding in the Guaranteed object.
+ byte[] originalEncoding = new byte[encodedCertBuffer.position() - startingPos];
+ encodedCertBuffer.position(startingPos);
+ encodedCertBuffer.get(originalEncoding);
+ GuaranteedEncodedFormX509Certificate guaranteedEncodedCert =
+ new GuaranteedEncodedFormX509Certificate(certificate, originalEncoding);
+ return guaranteedEncodedCert;
+ } catch (Asn1DecodingException | Asn1EncodingException | CertificateException e) {
+ throw new CertificateException("Failed to parse certificate", e);
+ }
+ }
+
+ /**
+ * Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code
+ * InputStream}.
+ *
+ * @throws CertificateException if the InputStream cannot be decoded to zero or more valid
+ * {@code Certificate} objects.
+ */
+ public static Collection<? extends java.security.cert.Certificate> generateCertificates(
+ InputStream in) throws CertificateException {
+ if (sCertFactory == null) {
+ buildCertFactory();
+ }
+ return generateCertificates(in, sCertFactory);
+ }
+
+ /**
+ * Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code
+ * InputStream} using the provided {@code CertificateFactory}.
+ *
+ * @throws CertificateException if the InputStream cannot be decoded to zero or more valid
+ * {@code Certificates} objects.
+ */
+ public static Collection<? extends java.security.cert.Certificate> generateCertificates(
+ InputStream in, CertificateFactory certFactory) throws CertificateException {
+ // Since the InputStream is not guaranteed to support mark / reset operations first read it
+ // into a byte array to allow using the BER parser / DER encoder if it cannot be read by
+ // the CertificateFactory.
+ byte[] encodedCerts;
+ try {
+ encodedCerts = ByteStreams.toByteArray(in);
+ } catch (IOException e) {
+ throw new CertificateException("Failed to read the input stream", e);
+ }
+ try {
+ return certFactory.generateCertificates(new ByteArrayInputStream(encodedCerts));
+ } catch (CertificateException e) {
+ // This could be expected if the certificates are encoded using a BER encoding that does
+ // not use the minimum number of bytes to represent the length of the contents; attempt
+ // to decode the certificates using the BER parser and re-encode using the DER encoder
+ // below.
+ }
+ try {
+ Collection<X509Certificate> certificates = new ArrayList<>(1);
+ ByteBuffer encodedCertsBuffer = ByteBuffer.wrap(encodedCerts);
+ while (encodedCertsBuffer.hasRemaining()) {
+ ByteBuffer certBuffer = getNextDEREncodedCertificateBlock(encodedCertsBuffer);
+ int startingPos = certBuffer.position();
+ Certificate reencodedCert = Asn1BerParser.parse(certBuffer, Certificate.class);
+ byte[] reencodedForm = Asn1DerEncoder.encode(reencodedCert);
+ X509Certificate certificate = (X509Certificate) certFactory.generateCertificate(
+ new ByteArrayInputStream(reencodedForm));
+ byte[] originalEncoding = new byte[certBuffer.position() - startingPos];
+ certBuffer.position(startingPos);
+ certBuffer.get(originalEncoding);
+ GuaranteedEncodedFormX509Certificate guaranteedEncodedCert =
+ new GuaranteedEncodedFormX509Certificate(certificate, originalEncoding);
+ certificates.add(guaranteedEncodedCert);
+ }
+ return certificates;
+ } catch (Asn1DecodingException | Asn1EncodingException e) {
+ throw new CertificateException("Failed to parse certificates", e);
+ }
+ }
+
+ /**
+ * Parses the provided ByteBuffer to obtain the next certificate in DER encoding. If the buffer
+ * does not begin with the PEM certificate header then it is returned with the assumption that
+ * it is already DER encoded. If the buffer does begin with the PEM certificate header then the
+ * certificate data is read from the buffer until the PEM certificate footer is reached; this
+ * data is then base64 decoded and returned in a new ByteBuffer.
+ *
+ * If the buffer is in PEM format then the position of the buffer is moved to the end of the
+ * current certificate; if the buffer is already DER encoded then the position of the buffer is
+ * not modified.
+ *
+ * @throws CertificateException if the buffer contains the PEM certificate header but does not
+ * contain the expected footer.
+ */
+ private static ByteBuffer getNextDEREncodedCertificateBlock(ByteBuffer certificateBuffer)
+ throws CertificateException {
+ if (certificateBuffer == null) {
+ throw new NullPointerException("The certificateBuffer cannot be null");
+ }
+ // if the buffer does not contain enough data for the PEM cert header then just return the
+ // provided buffer.
+ if (certificateBuffer.remaining() < BEGIN_CERT_HEADER.length) {
+ return certificateBuffer;
+ }
+ certificateBuffer.mark();
+ for (int i = 0; i < BEGIN_CERT_HEADER.length; i++) {
+ if (certificateBuffer.get() != BEGIN_CERT_HEADER[i]) {
+ certificateBuffer.reset();
+ return certificateBuffer;
+ }
+ }
+ StringBuilder pemEncoding = new StringBuilder();
+ while (certificateBuffer.hasRemaining()) {
+ char encodedChar = (char) certificateBuffer.get();
+ // if the current character is a '-' then the beginning of the footer has been reached
+ if (encodedChar == '-') {
+ break;
+ } else if (Character.isWhitespace(encodedChar)) {
+ continue;
+ } else {
+ pemEncoding.append(encodedChar);
+ }
+ }
+ // start from the second index in the certificate footer since the first '-' should have
+ // been consumed above.
+ for (int i = 1; i < END_CERT_FOOTER.length; i++) {
+ if (!certificateBuffer.hasRemaining()) {
+ throw new CertificateException(
+ "The provided input contains the PEM certificate header but does not "
+ + "contain sufficient data for the footer");
+ }
+ if (certificateBuffer.get() != END_CERT_FOOTER[i]) {
+ throw new CertificateException(
+ "The provided input contains the PEM certificate header without a "
+ + "valid certificate footer");
+ }
+ }
+ byte[] derEncoding = Base64.getDecoder().decode(pemEncoding.toString());
+ // consume any trailing whitespace in the byte buffer
+ int nextEncodedChar = certificateBuffer.position();
+ while (certificateBuffer.hasRemaining()) {
+ char trailingChar = (char) certificateBuffer.get();
+ if (Character.isWhitespace(trailingChar)) {
+ nextEncodedChar++;
+ } else {
+ break;
+ }
+ }
+ certificateBuffer.position(nextEncodedChar);
+ return ByteBuffer.wrap(derEncoding);
+ }
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java b/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java
new file mode 100644
index 0000000..077db23
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1OpaqueObject;
+import com.android.apksig.internal.asn1.Asn1Type;
+
+/**
+ * {@code AttributeTypeAndValue} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class AttributeTypeAndValue {
+
+ @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
+ public String attrType;
+
+ @Asn1Field(index = 1, type = Asn1Type.ANY)
+ public Asn1OpaqueObject attrValue;
+}
\ No newline at end of file
diff --git a/src/main/java/com/android/apksig/internal/x509/Certificate.java b/src/main/java/com/android/apksig/internal/x509/Certificate.java
new file mode 100644
index 0000000..abb3c15
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/Certificate.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
+
+import java.nio.ByteBuffer;
+
+/**
+ * X509 {@code Certificate} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class Certificate {
+ @Asn1Field(index = 0, type = Asn1Type.SEQUENCE)
+ public TBSCertificate certificate;
+
+ @Asn1Field(index = 1, type = Asn1Type.SEQUENCE)
+ public AlgorithmIdentifier signatureAlgorithm;
+
+ @Asn1Field(index = 2, type = Asn1Type.BIT_STRING)
+ public ByteBuffer signature;
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/Extension.java b/src/main/java/com/android/apksig/internal/x509/Extension.java
new file mode 100644
index 0000000..bf37c1e
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/Extension.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+
+import java.nio.ByteBuffer;
+
+/**
+ * X509 {@code Extension} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class Extension {
+ @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
+ public String extensionID;
+
+ @Asn1Field(index = 1, type = Asn1Type.BOOLEAN, optional = true)
+ public boolean isCritial = false;
+
+ @Asn1Field(index = 2, type = Asn1Type.OCTET_STRING)
+ public ByteBuffer extensionValue;
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/Name.java b/src/main/java/com/android/apksig/internal/x509/Name.java
new file mode 100644
index 0000000..08400d6
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/Name.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+
+import java.util.List;
+
+/**
+ * X501 {@code Name} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.CHOICE)
+public class Name {
+
+ // This field is the RDNSequence specified in RFC 5280.
+ @Asn1Field(index = 0, type = Asn1Type.SEQUENCE_OF)
+ public List<RelativeDistinguishedName> relativeDistinguishedNames;
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java b/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java
new file mode 100644
index 0000000..bb89e8d
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+
+import java.util.List;
+
+/**
+ * {@code RelativeDistinguishedName} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.UNENCODED_CONTAINER)
+public class RelativeDistinguishedName {
+
+ @Asn1Field(index = 0, type = Asn1Type.SET_OF)
+ public List<AttributeTypeAndValue> attributes;
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java b/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java
new file mode 100644
index 0000000..8215237
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
+
+import java.nio.ByteBuffer;
+
+/**
+ * {@code SubjectPublicKeyInfo} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class SubjectPublicKeyInfo {
+ @Asn1Field(index = 0, type = Asn1Type.SEQUENCE)
+ public AlgorithmIdentifier algorithmIdentifier;
+
+ @Asn1Field(index = 1, type = Asn1Type.BIT_STRING)
+ public ByteBuffer subjectPublicKey;
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java b/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java
new file mode 100644
index 0000000..922f52c
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+import com.android.apksig.internal.asn1.Asn1Tagging;
+import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+/**
+ * To Be Signed Certificate as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class TBSCertificate {
+
+ @Asn1Field(
+ index = 0,
+ type = Asn1Type.INTEGER,
+ tagging = Asn1Tagging.EXPLICIT, tagNumber = 0)
+ public int version;
+
+ @Asn1Field(index = 1, type = Asn1Type.INTEGER)
+ public BigInteger serialNumber;
+
+ @Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
+ public AlgorithmIdentifier signatureAlgorithm;
+
+ @Asn1Field(index = 3, type = Asn1Type.CHOICE)
+ public Name issuer;
+
+ @Asn1Field(index = 4, type = Asn1Type.SEQUENCE)
+ public Validity validity;
+
+ @Asn1Field(index = 5, type = Asn1Type.CHOICE)
+ public Name subject;
+
+ @Asn1Field(index = 6, type = Asn1Type.SEQUENCE)
+ public SubjectPublicKeyInfo subjectPublicKeyInfo;
+
+ @Asn1Field(index = 7,
+ type = Asn1Type.BIT_STRING,
+ tagging = Asn1Tagging.IMPLICIT,
+ optional = true,
+ tagNumber = 1)
+ public ByteBuffer issuerUniqueID;
+
+ @Asn1Field(index = 8,
+ type = Asn1Type.BIT_STRING,
+ tagging = Asn1Tagging.IMPLICIT,
+ optional = true,
+ tagNumber = 2)
+ public ByteBuffer subjectUniqueID;
+
+ @Asn1Field(index = 9,
+ type = Asn1Type.SEQUENCE_OF,
+ tagging = Asn1Tagging.EXPLICIT,
+ optional = true,
+ tagNumber = 3)
+ public List<Extension> extensions;
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/Time.java b/src/main/java/com/android/apksig/internal/x509/Time.java
new file mode 100644
index 0000000..def2ee8
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/Time.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+
+/**
+ * {@code Time} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.CHOICE)
+public class Time {
+
+ @Asn1Field(type = Asn1Type.UTC_TIME)
+ public String utcTime;
+
+ @Asn1Field(type = Asn1Type.GENERALIZED_TIME)
+ public String generalizedTime;
+}
diff --git a/src/main/java/com/android/apksig/internal/x509/Validity.java b/src/main/java/com/android/apksig/internal/x509/Validity.java
new file mode 100644
index 0000000..df9acb3
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/x509/Validity.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 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.x509;
+
+import com.android.apksig.internal.asn1.Asn1Class;
+import com.android.apksig.internal.asn1.Asn1Field;
+import com.android.apksig.internal.asn1.Asn1Type;
+
+/**
+ * {@code Validity} as specified in RFC 5280.
+ */
+@Asn1Class(type = Asn1Type.SEQUENCE)
+public class Validity {
+
+ @Asn1Field(index = 0, type = Asn1Type.CHOICE)
+ public Time notBefore;
+
+ @Asn1Field(index = 1, type = Asn1Type.CHOICE)
+ public Time notAfter;
+}
diff --git a/src/test/java/com/android/apksig/internal/asn1/Asn1BerParserTest.java b/src/test/java/com/android/apksig/internal/asn1/Asn1BerParserTest.java
index 93bd3b4..1816204 100644
--- a/src/test/java/com/android/apksig/internal/asn1/Asn1BerParserTest.java
+++ b/src/test/java/com/android/apksig/internal/asn1/Asn1BerParserTest.java
@@ -61,6 +61,39 @@
}
@Test
+ public void testBitString() throws Exception {
+ assertEquals(
+ "123456",
+ HexEncoding.encode(parse("30050303123456", SequenceWithBitString.class).buf));
+ assertEquals(
+ "", HexEncoding.encode(parse("30020300", SequenceWithBitString.class).buf));
+ }
+
+ @Test
+ public void testBoolean() throws Exception {
+ assertEquals(false, parse("3003010100", SequenceWithBoolean.class).value);
+ assertEquals(true, parse("3003010101", SequenceWithBoolean.class).value);
+ assertEquals(true, parse("30030101FF", SequenceWithBoolean.class).value);
+ }
+
+ @Test
+ public void testUTCTime() throws Exception {
+ assertEquals("1212211221Z",
+ parse("300d170b313231323231313232315a", SequenceWithUTCTime.class).value);
+ assertEquals("9912312359Z",
+ parse("300d170b393931323331323335395a", SequenceWithUTCTime.class).value);
+ }
+
+ @Test
+ public void testGeneralizedTime() throws Exception {
+ assertEquals("201212211220.999-07", parse("301518133230313231323231313232302e3939392d3037",
+ SequenceWithGeneralizedTime.class).value);
+ assertEquals("20380119031407.000+00",
+ parse("3017181532303338303131393033313430372e3030302b3030",
+ SequenceWithGeneralizedTime.class).value);
+ }
+
+ @Test
public void testInteger() throws Exception {
// Various Java types decoded from INTEGER
// Empty SEQUENCE (0x3000) followed by garbage (0x12345678)
@@ -101,6 +134,15 @@
}
@Test
+ public void testUnencodedContainer() throws Exception {
+ SequenceWithSequenceOfUnencodedContainers seq = parse("300C300A31023000310430003000",
+ SequenceWithSequenceOfUnencodedContainers.class);
+ assertEquals(2, seq.containers.size());
+ assertEquals(1, seq.containers.get(0).values.size());
+ assertEquals(2, seq.containers.get(1).values.size());
+ }
+
+ @Test
public void testImplicitOptionalField() throws Exception {
// Optional field f2 missing in the input
SequenceWithImplicitOptionalField seq =
@@ -361,6 +403,12 @@
}
@Asn1Class(type = Asn1Type.SEQUENCE)
+ public static class SequenceWithBitString {
+ @Asn1Field(index = 0, type = Asn1Type.BIT_STRING)
+ public ByteBuffer buf;
+ }
+
+ @Asn1Class(type = Asn1Type.SEQUENCE)
public static class SequenceWithSequenceOf {
@Asn1Field(index = 0, type = Asn1Type.SEQUENCE_OF)
public List<EmptySequence> values;
@@ -377,4 +425,34 @@
@Asn1Field(type = Asn1Type.ANY)
public Asn1OpaqueObject obj;
}
+
+ @Asn1Class(type = Asn1Type.SEQUENCE)
+ public static class SequenceWithSequenceOfUnencodedContainers {
+ @Asn1Field(type = Asn1Type.SEQUENCE_OF)
+ public List<UnencodedContainerWithSetOf> containers;
+ }
+
+ @Asn1Class(type = Asn1Type.UNENCODED_CONTAINER)
+ public static class UnencodedContainerWithSetOf {
+ @Asn1Field(type = Asn1Type.SET_OF)
+ public List<EmptySequence> values;
+ }
+
+ @Asn1Class(type = Asn1Type.SEQUENCE)
+ public static class SequenceWithBoolean {
+ @Asn1Field(type = Asn1Type.BOOLEAN)
+ public boolean value;
+ }
+
+ @Asn1Class(type = Asn1Type.SEQUENCE)
+ public static class SequenceWithUTCTime {
+ @Asn1Field(type = Asn1Type.UTC_TIME)
+ public String value;
+ }
+
+ @Asn1Class(type = Asn1Type.SEQUENCE)
+ public static class SequenceWithGeneralizedTime {
+ @Asn1Field(type = Asn1Type.GENERALIZED_TIME)
+ public String value;
+ }
}
diff --git a/src/test/java/com/android/apksig/internal/asn1/Asn1DerEncoderTest.java b/src/test/java/com/android/apksig/internal/asn1/Asn1DerEncoderTest.java
index ad4ec82..02edb23 100644
--- a/src/test/java/com/android/apksig/internal/asn1/Asn1DerEncoderTest.java
+++ b/src/test/java/com/android/apksig/internal/asn1/Asn1DerEncoderTest.java
@@ -17,6 +17,7 @@
package com.android.apksig.internal.asn1;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.fail;
import com.android.apksig.internal.util.HexEncoding;
@@ -63,6 +64,26 @@
}
@Test
+ public void testBitString() throws Exception {
+ assertEquals(
+ "30050303010203",
+ encodeToHex(
+ new SequenceWithByteBufferBitString(
+ ByteBuffer.wrap(new byte[] {1, 2, 3}))));
+ assertEquals(
+ "30030301ff",
+ encodeToHex(
+ new SequenceWithByteBufferBitString(
+ ByteBuffer.wrap(new byte[] {(byte) 0xff}))));
+
+ assertEquals(
+ "30020300",
+ encodeToHex(
+ new SequenceWithByteBufferBitString(ByteBuffer.wrap(new byte[0]))));
+ }
+
+
+ @Test
public void testOid() throws Exception {
assertEquals("3003060100", encodeToHex(new SequenceWithOid("0.0")));
assertEquals(
@@ -138,6 +159,57 @@
new Asn1OpaqueObject(new byte[] {0x06, 0x01, 0x00}))));
}
+ @Test
+ public void testBoolean() throws Exception {
+ assertEquals("3003010100", encodeToHex(new SequenceWithBoolean(false)));
+ String value = encodeToHex(new SequenceWithBoolean(true));
+ // The encoding of a true value can be any non-zero value so verify the static portion of
+ // the encoding of a sequeuence with a boolean, then verify the last byte is non-zero
+ assertEquals("The encoding of a sequence with a boolean is not the expected length.", 10,
+ value.length());
+ assertEquals(
+ "The prefix of the encoding of a sequence with a boolean is not the expected "
+ + "value.",
+ "30030101", value.substring(0, 8));
+ assertNotEquals("The encoding of true should be non-zero.", "00", value.substring(8));
+ }
+
+ @Test
+ public void testUTCTime() throws Exception {
+ assertEquals("300d170b313231323231313232315a",
+ encodeToHex(new SequenceWithUTCTime("1212211221Z")));
+ assertEquals("300d170b393931323331323335395a",
+ encodeToHex(new SequenceWithUTCTime("9912312359Z")));
+ }
+
+ @Test
+ public void testGeneralizedTime() throws Exception {
+ assertEquals("301518133230313231323231313232302e3939392d3037",
+ encodeToHex(new SequenceWithGeneralizedTime("201212211220.999-07")));
+ assertEquals("3017181532303338303131393033313430372e3030302b3030",
+ encodeToHex(new SequenceWithGeneralizedTime("20380119031407.000+00")));
+ }
+
+ @Test
+ public void testUnencodedContainer() throws Exception {
+ assertEquals("30233021310b30030201003004020200ff310830060204800000003108300602047fffffff",
+ encodeToHex(
+ new SequenceWithSequenceOfUnencodedContainers(
+ Arrays.asList(
+ new UnencodedContainerWithSetOfIntegers(
+ Arrays.asList(
+ new SequenceWithInteger(0),
+ new SequenceWithInteger(255))),
+ new UnencodedContainerWithSetOfIntegers(
+ Arrays.asList(
+ new SequenceWithInteger(
+ Integer.MIN_VALUE))),
+ new UnencodedContainerWithSetOfIntegers(
+ Arrays.asList(
+ new SequenceWithInteger(
+ Integer.MAX_VALUE)))))));
+ }
+
private static byte[] encode(Object obj) throws Asn1EncodingException {
return Asn1DerEncoder.encode(obj);
}
@@ -180,6 +252,17 @@
}
}
+ @Asn1Class(type = Asn1Type.SEQUENCE)
+ public static class SequenceWithByteBufferBitString {
+
+ @Asn1Field(index = 1, type = Asn1Type.BIT_STRING)
+ public ByteBuffer data;
+
+ public SequenceWithByteBufferBitString(ByteBuffer data) {
+ this.data = data;
+ }
+ }
+
@Asn1Class(type = Asn1Type.CHOICE)
public static class Choice {
@@ -252,4 +335,58 @@
this.obj = obj;
}
}
+
+ @Asn1Class(type = Asn1Type.SEQUENCE)
+ public static class SequenceWithBoolean {
+
+ @Asn1Field(index = 1, type = Asn1Type.BOOLEAN)
+ public boolean value;
+
+ public SequenceWithBoolean(boolean value) {
+ this.value = value;
+ }
+ }
+
+ @Asn1Class(type = Asn1Type.SEQUENCE)
+ public static class SequenceWithUTCTime {
+
+ @Asn1Field(index = 1, type = Asn1Type.UTC_TIME)
+ public String utcTime;
+
+ public SequenceWithUTCTime(String utcTime) {
+ this.utcTime = utcTime;
+ }
+ }
+
+ @Asn1Class(type = Asn1Type.SEQUENCE)
+ public static class SequenceWithGeneralizedTime {
+
+ @Asn1Field(index = 1, type = Asn1Type.GENERALIZED_TIME)
+ public String generalizedTime;
+
+ public SequenceWithGeneralizedTime(String generalizedTime) {
+ this.generalizedTime = generalizedTime;
+ }
+ }
+
+ @Asn1Class(type = Asn1Type.SEQUENCE)
+ public static class SequenceWithSequenceOfUnencodedContainers {
+ @Asn1Field(index = 1, type = Asn1Type.SEQUENCE_OF)
+ public List<UnencodedContainerWithSetOfIntegers> containers;
+
+ public SequenceWithSequenceOfUnencodedContainers(
+ List<UnencodedContainerWithSetOfIntegers> containers) {
+ this.containers = containers;
+ }
+ }
+
+ @Asn1Class(type = Asn1Type.UNENCODED_CONTAINER)
+ public static class UnencodedContainerWithSetOfIntegers {
+ @Asn1Field(index = 1, type = Asn1Type.SET_OF)
+ public List<SequenceWithInteger> values;
+
+ public UnencodedContainerWithSetOfIntegers(List<SequenceWithInteger> values) {
+ this.values = values;
+ }
+ }
}
diff --git a/src/test/java/com/android/apksig/internal/util/AllTests.java b/src/test/java/com/android/apksig/internal/util/AllTests.java
index b78acde..78a6aa4 100644
--- a/src/test/java/com/android/apksig/internal/util/AllTests.java
+++ b/src/test/java/com/android/apksig/internal/util/AllTests.java
@@ -25,5 +25,6 @@
ChainedDataSourceTest.class,
DirectByteBufferSinkTest.class,
VerityTreeBuilderTest.class,
+ X509CertificateUtilsTest.class,
})
public class AllTests {}
diff --git a/src/test/java/com/android/apksig/internal/util/Resources.java b/src/test/java/com/android/apksig/internal/util/Resources.java
index 09c206d..82bf76f 100644
--- a/src/test/java/com/android/apksig/internal/util/Resources.java
+++ b/src/test/java/com/android/apksig/internal/util/Resources.java
@@ -28,7 +28,6 @@
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
@@ -52,14 +51,21 @@
}
}
+ public static InputStream toInputStream(Class<?> cls, String resourceName) throws IOException {
+ InputStream in = cls.getResourceAsStream(resourceName);
+ if (in == null) {
+ throw new IllegalArgumentException("Resource not found: " + resourceName);
+ }
+ return in;
+ }
+
public static X509Certificate toCertificate(
Class <?> cls, String resourceName) throws IOException, CertificateException {
try (InputStream in = cls.getResourceAsStream(resourceName)) {
if (in == null) {
throw new IllegalArgumentException("Resource not found: " + resourceName);
}
- return (X509Certificate)
- CertificateFactory.getInstance("X.509").generateCertificate(in);
+ return X509CertificateUtils.generateCertificate(in);
}
}
@@ -70,7 +76,7 @@
if (in == null) {
throw new IllegalArgumentException("Resource not found: " + resourceName);
}
- certs = CertificateFactory.getInstance("X.509").generateCertificates(in);
+ certs = X509CertificateUtils.generateCertificates(in);
}
List<X509Certificate> result = new ArrayList<>(certs.size());
for (Certificate cert : certs) {
diff --git a/src/test/java/com/android/apksig/internal/util/X509CertificateUtilsTest.java b/src/test/java/com/android/apksig/internal/util/X509CertificateUtilsTest.java
new file mode 100644
index 0000000..daf6739
--- /dev/null
+++ b/src/test/java/com/android/apksig/internal/util/X509CertificateUtilsTest.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2018 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.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public class X509CertificateUtilsTest {
+ // The PEM and DER encodings of a certificate without redundant length bytes; since the
+ // certificates are the same they have the same hex encoding of their digest.
+ public static final String RSA_2048_VALID_PEM_ENCODING = "rsa-2048.x509.pem";
+ public static final String RSA_2048_VALID_DER_ENCODING = "rsa-2048.x509.der";
+ public static final String RSA_2048_VALID_DIGEST_HEX_ENCODING =
+ "fb5dbd3c669af9fc236c6991e6387b7f11ff0590997f22d0f5c74ff40e04fca8";
+
+ // The PEM and DER encodings of a certificate with redundant length bytes; valid DER encodings
+ // require that the length of contents within the encoding be specified with the minimum number
+ // of bytes, but BER encodings allow redundant '00' bytes when specifying length.
+ public static final String RSA_2048_REDUNDANT_LEN_BYTES_PEM_ENCODING =
+ "rsa-2048-redun-len.x509.pem";
+ public static final String RSA_2048_REDUNDANT_LEN_BYTES_DER_ENCODING =
+ "rsa-2048-redun-len.x509.der";
+ public static final String RSA_2048_REDUNDANT_LEN_DIGEST_HEX_ENCODING =
+ "38481f124f8af6c36017abdfbefe375157ac304fb90adaa641ecba71b08dcd0f";
+
+ // The PEM and DER encodings of both the valid and redundant length byte certificates above.
+ public static final String RSA_2048_TWO_CERTS_PEM_ENCODING = "rsa-2048-2-certs.x509.pem";
+ public static final String RSA_2048_TWO_CERTS_DER_ENCODING = "rsa-2048-2-certs.x509.der";
+
+ @Test
+ public void testGenerateCertificateWithValidPEMEncoding() throws Exception {
+ // The generateCertificate method should support both PEM and DER encodings; since the PEM
+ // format is just the DER encoding base64'd with a header and a footer this test verifies
+ // that a valid DER encoding in PEM format is successfully parsed and returns the expected
+ // encoding.
+ assertEquals(RSA_2048_VALID_DIGEST_HEX_ENCODING,
+ getHexEncodedDigestOfCertFromResources(RSA_2048_VALID_PEM_ENCODING));
+ }
+
+ @Test
+ public void testGenerateCertificateWithRedundantLengthBytesInPEMEncoding() throws Exception {
+ // This test verifies that a BER encoding of a certificate with redundant length bytes
+ // can still be successfully parsed and returns the expected unmodified encoding.
+ assertEquals(RSA_2048_REDUNDANT_LEN_DIGEST_HEX_ENCODING,
+ getHexEncodedDigestOfCertFromResources(RSA_2048_REDUNDANT_LEN_BYTES_PEM_ENCODING));
+ }
+
+ @Test
+ public void testGenerateCertificateWithValidDEREncoding() throws Exception {
+ // This test verifies the generateCertificate method successfully parses and returns the
+ // expected encoding of a certificate with a valid DER encoding.
+ assertEquals(RSA_2048_VALID_DIGEST_HEX_ENCODING,
+ getHexEncodedDigestOfCertFromResources(RSA_2048_VALID_DER_ENCODING));
+ }
+
+ @Test
+ public void testGenerateCertificateWithRedundantLengthBytesInDERENcoding() throws Exception {
+ // This test verifies the generateCertificate method successfully parses and returns the
+ // original encoding of a certificate with redundant length bytes in the encoding.
+ assertEquals(RSA_2048_REDUNDANT_LEN_DIGEST_HEX_ENCODING,
+ getHexEncodedDigestOfCertFromResources(RSA_2048_REDUNDANT_LEN_BYTES_DER_ENCODING));
+ }
+
+ @Test
+ public void testGenerateCertificatesWithTwoPEMEncodedCerts() throws Exception {
+ // The generateCertificates method accepts an InputStream which could contain zero or more
+ // certificates in PEM or DER encoding; this test verifies both certificates in PEM format
+ // are returned with the expected encodings.
+ List<String> encodedCerts = getHexEncodedDigestsOfCertsFromResources(
+ RSA_2048_TWO_CERTS_PEM_ENCODING);
+ Set<String> expectedEncodings = createSetOfValues(RSA_2048_VALID_DIGEST_HEX_ENCODING,
+ RSA_2048_REDUNDANT_LEN_DIGEST_HEX_ENCODING);
+ assertEncodingsMatchExpectedValues(encodedCerts, expectedEncodings);
+ }
+
+ @Test
+ public void testGenerateCertificatesWithTwoDEREncodedCerts() throws Exception {
+ // This test verifies the generateCertificates method returns the expected encodings for
+ // an InputStream with both DER encoded certificates.
+ List<String> encodedCerts = getHexEncodedDigestsOfCertsFromResources(
+ RSA_2048_TWO_CERTS_DER_ENCODING);
+ Set<String> expectedEncodings = createSetOfValues(RSA_2048_VALID_DIGEST_HEX_ENCODING,
+ RSA_2048_REDUNDANT_LEN_DIGEST_HEX_ENCODING);
+ assertEncodingsMatchExpectedValues(encodedCerts, expectedEncodings);
+ }
+
+ @Test
+ public void testGenerateCertificateAndGenerateCertificatesReturnSameValues() throws Exception {
+ // The generateCertificates method is intended to read multiple certificates in the provided
+ // InputStream, but it can also read a single certificate. Verify that both
+ // generateCertificate and generateCertificates return the same encodings for the same
+ // certificates.
+ List<String> certResources = Arrays.asList(RSA_2048_VALID_PEM_ENCODING,
+ RSA_2048_VALID_DER_ENCODING, RSA_2048_REDUNDANT_LEN_BYTES_PEM_ENCODING,
+ RSA_2048_REDUNDANT_LEN_BYTES_DER_ENCODING);
+ for (String certResource : certResources) {
+ String genCertValue = getHexEncodedDigestOfCertFromResources(certResource);
+ List<String> genCertsValues = getHexEncodedDigestsOfCertsFromResources(certResource);
+ assertEquals(
+ "The generateCertificates method should have returned a single certificate", 1,
+ genCertsValues.size());
+ assertEquals(
+ "The hex encoded digest of the certificate from generateCertificate does not "
+ + "match the value from generateCertificates",
+ genCertValue, genCertsValues.get(0));
+ }
+ }
+
+ @Test
+ public void testGenerateCertificatesWithEmptyInput() throws Exception {
+ // This test verifies the generateCertificates method returns an empty Collection of
+ // Certificates when provided an empty InputStream.
+ assertEquals(
+ "Zero certificates should be returned when passing an empty InputStream to "
+ + "generateCertificates",
+ 0, X509CertificateUtils.generateCertificates(
+ new ByteArrayInputStream(new byte[0])).size());
+ }
+
+ private static Set<String> createSetOfValues(String... values) {
+ Set<String> result = new HashSet<>();
+ for (String value : values) {
+ result.add(value);
+ }
+ return result;
+ }
+
+ /**
+ * Returns a hex encoding of the digest of the specified certificate from the resources.
+ */
+ private static String getHexEncodedDigestOfCertFromResources(String resourceName)
+ throws Exception {
+ byte[] encodedForm = Resources.toByteArray(X509CertificateUtilsTest.class, resourceName);
+ X509Certificate cert = X509CertificateUtils.generateCertificate(encodedForm);
+ return getHexEncodedDigestOfBytes(cert.getEncoded());
+ }
+
+ /**
+ * Returns a list of hex encodings of the digests of the certificates in the specified resource.
+ */
+ private static List<String> getHexEncodedDigestsOfCertsFromResources(String resourceName)
+ throws Exception {
+ InputStream in = Resources.toInputStream(X509CertificateUtilsTest.class, resourceName);
+ Collection<? extends Certificate> certs = X509CertificateUtils.generateCertificates(in);
+ List<String> encodedCerts = new ArrayList<>(certs.size());
+ for (Certificate cert : certs) {
+ encodedCerts.add(getHexEncodedDigestOfBytes(cert.getEncoded()));
+ }
+ return encodedCerts;
+ }
+
+ /**
+ * Returns the hex encoding of the digest of the specified bytes.
+ */
+ private static String getHexEncodedDigestOfBytes(byte[] bytes)
+ throws NoSuchAlgorithmException {
+ return HexEncoding.encode(MessageDigest.getInstance("SHA-256").digest(bytes));
+ }
+
+ /**
+ * Asserts that the encoding of the provided certificates match the expected values.
+ */
+ private static void assertEncodingsMatchExpectedValues(List<String> encodedCerts,
+ Set<String> expectedValues) {
+ assertEquals(
+ "The number of encoded certificates does not match the expected number of values",
+ expectedValues.size(), encodedCerts.size());
+ for (String encodedCert : encodedCerts) {
+ // if the current encoding is found in the expected Set then remove it to ensure that
+ // duplicate values do not cause the test to pass if they are not expected.
+ if (expectedValues.contains(encodedCert)) {
+ expectedValues.remove(encodedCert);
+ } else {
+ fail("An unexpected certificate with the following encoding was returned: "
+ + encodedCert);
+ }
+ }
+ }
+}
diff --git a/src/test/resources/com/android/apksig/internal/util/rsa-2048-2-certs.x509.der b/src/test/resources/com/android/apksig/internal/util/rsa-2048-2-certs.x509.der
new file mode 100644
index 0000000..6464e3a
--- /dev/null
+++ b/src/test/resources/com/android/apksig/internal/util/rsa-2048-2-certs.x509.der
Binary files differ
diff --git a/src/test/resources/com/android/apksig/internal/util/rsa-2048-2-certs.x509.pem b/src/test/resources/com/android/apksig/internal/util/rsa-2048-2-certs.x509.pem
new file mode 100644
index 0000000..0659079
--- /dev/null
+++ b/src/test/resources/com/android/apksig/internal/util/rsa-2048-2-certs.x509.pem
@@ -0,0 +1,37 @@
+-----BEGIN CERTIFICATE-----
+MIIC+TCCAeGgAwIBAgIJAI41MGzdARX3MA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
+BAMMCHJzYS0yMDQ4MB4XDTE2MDMzMTE0NTc0OVoXDTQzMDgxNzE0NTc0OVowEzER
+MA8GA1UEAwwIcnNhLTIwNDgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQDQ4JI1EJ239V4wss0jpVlZMudh2/kARCVdoBgsRQuvc2RNnO23Eyynlt9UN+Dc
+NRdQIhbCpVTjdEl/bePECHlqg9NE3frAj5GebiUdWL6A/idKsZA1nAKyIgxxjcnu
++38OcrlO6XOm36euxGfd/ULrghZGXzMVFq4uLiIv3DqFkUcIlE0BvUiUoNwpopV4
+MKj1GQgoaEObJG5xkMBKO6vg36VfJ3s3V3r48uJxYGhhBZEB0EpoXLd4i0piAB8S
+MLb0Ek6wA/HZ8A2rdnStk1wl/83OM1jO0uB3hyfJpqIijlvNGnrloYyyOIqS0LGH
+nxSJD7goASH2Ef0h4yxbsOvHAgMBAAGjUDBOMB0GA1UdDgQWBBQXAi1zEH84mzkS
+62ohswGGWSwdbzAfBgNVHSMEGDAWgBQXAi1zEH84mzkS62ohswGGWSwdbzAMBgNV
+HRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAB92T5toLkF6dLl65/boH5Qvub
+5wfIk0AD12T3t3kYWQFOH0YDCHNL3SfmrjYM/CwNJAd1KuCL5AZcn0km/n0SFXt5
+8Ps/MBcb0eK1fYezeEehKUyt5IBgDTKeQOel6So8rGuQRrDf/WV8rt6fugkIODFx
+sB3oj4ESaGXbvmvWD6q4a3koq/nV26kALchnAr7/FTNq3HEIQ1BDr9pldVh1gEV/
+ohHKcQP4M22Es7lredzpIcb5K6Ko/UtwsSRtHnoOjwmb+L/FsgAJsekmcJG5TK1X
+ciIsrrNFDCYzf/d9O1PD/V95kB7460qMzrGWZpc3mLe+OnmVMq6c4omOtIKl
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDAzCCAeugAwIBAgIJAI41MGzdARX3MA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
+BAMMCHJzYS0yMDQ4MB4XDTE2MDMzMTE0NTc0OVoXDTQzMDgxNzE0NTc0OVowEzER
+MA8GA1UEAwwIcnNhLTIwNDgwggEjMA0GCSqGSIb3DQEBAQUAA4MAAQ8AMIIBCgKC
+AQEA0OCSNRCdt/VeMLLNI6VZWTLnYdv5AEQlXaAYLEULr3NkTZzttxMsp5bfVDfg
+3DUXUCIWwqVU43RJf23jxAh5aoPTRN36wI+Rnm4lHVi+gP4nSrGQNZwCsiIMcY3J
+7vt/DnK5Tulzpt+nrsRn3f1C64IWRl8zFRauLi4iL9w6hZFHCJRNAb1IlKDcKaKV
+eDCo9RkIKGhDmyRucZDASjur4N+lXyd7N1d6+PLicWBoYQWRAdBKaFy3eItKYgAf
+EjC29BJOsAPx2fANq3Z0rZNcJf/NzjNYztLgd4cnyaaiIo5bzRp65aGMsjiKktCx
+h58UiQ+4KAEh9hH9IeMsW7DrxwIDAQABo1kwVzAgBgNVHQ4BAQAEFgQUFwItcxB/
+OJs5EutqIbMBhlksHW8wIgYDVR0jAQEABBgwFoAUFwItcxB/OJs5EutqIbMBhlks
+HW8wDwYDVR0TAQEABAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAfdk+baC5Ben
+S5euf26B+UL7m+cHyJNAA9dk97d5GFkBTh9GAwhzS90n5q42DPwsDSQHdSrgi+QG
+XJ9JJv59EhV7efD7PzAXG9HitX2Hs3hHoSlMreSAYA0ynkDnpekqPKxrkEaw3/1l
+fK7en7oJCDgxcbAd6I+BEmhl275r1g+quGt5KKv51dupAC3IZwK+/xUzatxxCENQ
+Q6/aZXVYdYBFf6IRynED+DNthLO5a3nc6SHG+SuiqP1LcLEkbR56Do8Jm/i/xbIA
+CbHpJnCRuUytV3IiLK6zRQwmM3/3fTtTw/1feZAe+OtKjM6xlmaXN5i3vjp5lTKu
+nOKJjrSCpQ==
+-----END CERTIFICATE-----
diff --git a/src/test/resources/com/android/apksig/internal/util/rsa-2048-redun-len.x509.der b/src/test/resources/com/android/apksig/internal/util/rsa-2048-redun-len.x509.der
new file mode 100644
index 0000000..19b3f89
--- /dev/null
+++ b/src/test/resources/com/android/apksig/internal/util/rsa-2048-redun-len.x509.der
Binary files differ
diff --git a/src/test/resources/com/android/apksig/internal/util/rsa-2048-redun-len.x509.pem b/src/test/resources/com/android/apksig/internal/util/rsa-2048-redun-len.x509.pem
new file mode 100644
index 0000000..a1ddac7
--- /dev/null
+++ b/src/test/resources/com/android/apksig/internal/util/rsa-2048-redun-len.x509.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDAzCCAeugAwIBAgIJAI41MGzdARX3MA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
+BAMMCHJzYS0yMDQ4MB4XDTE2MDMzMTE0NTc0OVoXDTQzMDgxNzE0NTc0OVowEzER
+MA8GA1UEAwwIcnNhLTIwNDgwggEjMA0GCSqGSIb3DQEBAQUAA4MAAQ8AMIIBCgKC
+AQEA0OCSNRCdt/VeMLLNI6VZWTLnYdv5AEQlXaAYLEULr3NkTZzttxMsp5bfVDfg
+3DUXUCIWwqVU43RJf23jxAh5aoPTRN36wI+Rnm4lHVi+gP4nSrGQNZwCsiIMcY3J
+7vt/DnK5Tulzpt+nrsRn3f1C64IWRl8zFRauLi4iL9w6hZFHCJRNAb1IlKDcKaKV
+eDCo9RkIKGhDmyRucZDASjur4N+lXyd7N1d6+PLicWBoYQWRAdBKaFy3eItKYgAf
+EjC29BJOsAPx2fANq3Z0rZNcJf/NzjNYztLgd4cnyaaiIo5bzRp65aGMsjiKktCx
+h58UiQ+4KAEh9hH9IeMsW7DrxwIDAQABo1kwVzAgBgNVHQ4BAQAEFgQUFwItcxB/
+OJs5EutqIbMBhlksHW8wIgYDVR0jAQEABBgwFoAUFwItcxB/OJs5EutqIbMBhlks
+HW8wDwYDVR0TAQEABAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAfdk+baC5Ben
+S5euf26B+UL7m+cHyJNAA9dk97d5GFkBTh9GAwhzS90n5q42DPwsDSQHdSrgi+QG
+XJ9JJv59EhV7efD7PzAXG9HitX2Hs3hHoSlMreSAYA0ynkDnpekqPKxrkEaw3/1l
+fK7en7oJCDgxcbAd6I+BEmhl275r1g+quGt5KKv51dupAC3IZwK+/xUzatxxCENQ
+Q6/aZXVYdYBFf6IRynED+DNthLO5a3nc6SHG+SuiqP1LcLEkbR56Do8Jm/i/xbIA
+CbHpJnCRuUytV3IiLK6zRQwmM3/3fTtTw/1feZAe+OtKjM6xlmaXN5i3vjp5lTKu
+nOKJjrSCpQ==
+-----END CERTIFICATE-----
diff --git a/src/test/resources/com/android/apksig/internal/util/rsa-2048.x509.der b/src/test/resources/com/android/apksig/internal/util/rsa-2048.x509.der
new file mode 100644
index 0000000..d61baef
--- /dev/null
+++ b/src/test/resources/com/android/apksig/internal/util/rsa-2048.x509.der
Binary files differ
diff --git a/src/test/resources/com/android/apksig/internal/util/rsa-2048.x509.pem b/src/test/resources/com/android/apksig/internal/util/rsa-2048.x509.pem
new file mode 100644
index 0000000..0e7b38e
--- /dev/null
+++ b/src/test/resources/com/android/apksig/internal/util/rsa-2048.x509.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC+TCCAeGgAwIBAgIJAI41MGzdARX3MA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
+BAMMCHJzYS0yMDQ4MB4XDTE2MDMzMTE0NTc0OVoXDTQzMDgxNzE0NTc0OVowEzER
+MA8GA1UEAwwIcnNhLTIwNDgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQDQ4JI1EJ239V4wss0jpVlZMudh2/kARCVdoBgsRQuvc2RNnO23Eyynlt9UN+Dc
+NRdQIhbCpVTjdEl/bePECHlqg9NE3frAj5GebiUdWL6A/idKsZA1nAKyIgxxjcnu
++38OcrlO6XOm36euxGfd/ULrghZGXzMVFq4uLiIv3DqFkUcIlE0BvUiUoNwpopV4
+MKj1GQgoaEObJG5xkMBKO6vg36VfJ3s3V3r48uJxYGhhBZEB0EpoXLd4i0piAB8S
+MLb0Ek6wA/HZ8A2rdnStk1wl/83OM1jO0uB3hyfJpqIijlvNGnrloYyyOIqS0LGH
+nxSJD7goASH2Ef0h4yxbsOvHAgMBAAGjUDBOMB0GA1UdDgQWBBQXAi1zEH84mzkS
+62ohswGGWSwdbzAfBgNVHSMEGDAWgBQXAi1zEH84mzkS62ohswGGWSwdbzAMBgNV
+HRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAB92T5toLkF6dLl65/boH5Qvub
+5wfIk0AD12T3t3kYWQFOH0YDCHNL3SfmrjYM/CwNJAd1KuCL5AZcn0km/n0SFXt5
+8Ps/MBcb0eK1fYezeEehKUyt5IBgDTKeQOel6So8rGuQRrDf/WV8rt6fugkIODFx
+sB3oj4ESaGXbvmvWD6q4a3koq/nV26kALchnAr7/FTNq3HEIQ1BDr9pldVh1gEV/
+ohHKcQP4M22Es7lredzpIcb5K6Ko/UtwsSRtHnoOjwmb+L/FsgAJsekmcJG5TK1X
+ciIsrrNFDCYzf/d9O1PD/V95kB7460qMzrGWZpc3mLe+OnmVMq6c4omOtIKl
+-----END CERTIFICATE-----