1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.apksig.internal.util; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.fail; 21 22 import org.junit.Test; 23 import org.junit.runner.RunWith; 24 import org.junit.runners.JUnit4; 25 26 import java.io.ByteArrayInputStream; 27 import java.io.InputStream; 28 import java.security.MessageDigest; 29 import java.security.NoSuchAlgorithmException; 30 import java.security.cert.Certificate; 31 import java.security.cert.X509Certificate; 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.Collection; 35 import java.util.HashSet; 36 import java.util.List; 37 import java.util.Set; 38 39 @RunWith(JUnit4.class) 40 public class X509CertificateUtilsTest { 41 // The PEM and DER encodings of a certificate without redundant length bytes; since the 42 // certificates are the same they have the same hex encoding of their digest. 43 public static final String RSA_2048_VALID_PEM_ENCODING = "rsa-2048.x509.pem"; 44 public static final String RSA_2048_VALID_DER_ENCODING = "rsa-2048.x509.der"; 45 public static final String RSA_2048_VALID_DIGEST_HEX_ENCODING = 46 "fb5dbd3c669af9fc236c6991e6387b7f11ff0590997f22d0f5c74ff40e04fca8"; 47 48 // The PEM and DER encodings of a certificate with redundant length bytes; valid DER encodings 49 // require that the length of contents within the encoding be specified with the minimum number 50 // of bytes, but BER encodings allow redundant '00' bytes when specifying length. 51 public static final String RSA_2048_REDUNDANT_LEN_BYTES_PEM_ENCODING = 52 "rsa-2048-redun-len.x509.pem"; 53 public static final String RSA_2048_REDUNDANT_LEN_BYTES_DER_ENCODING = 54 "rsa-2048-redun-len.x509.der"; 55 public static final String RSA_2048_REDUNDANT_LEN_DIGEST_HEX_ENCODING = 56 "38481f124f8af6c36017abdfbefe375157ac304fb90adaa641ecba71b08dcd0f"; 57 58 // The PEM and DER encodings of both the valid and redundant length byte certificates above. 59 public static final String RSA_2048_TWO_CERTS_PEM_ENCODING = "rsa-2048-2-certs.x509.pem"; 60 public static final String RSA_2048_TWO_CERTS_DER_ENCODING = "rsa-2048-2-certs.x509.der"; 61 62 @Test testGenerateCertificateWithValidPEMEncoding()63 public void testGenerateCertificateWithValidPEMEncoding() throws Exception { 64 // The generateCertificate method should support both PEM and DER encodings; since the PEM 65 // format is just the DER encoding base64'd with a header and a footer this test verifies 66 // that a valid DER encoding in PEM format is successfully parsed and returns the expected 67 // encoding. 68 assertEquals(RSA_2048_VALID_DIGEST_HEX_ENCODING, 69 getHexEncodedDigestOfCertFromResources(RSA_2048_VALID_PEM_ENCODING)); 70 } 71 72 @Test testGenerateCertificateWithRedundantLengthBytesInPEMEncoding()73 public void testGenerateCertificateWithRedundantLengthBytesInPEMEncoding() throws Exception { 74 // This test verifies that a BER encoding of a certificate with redundant length bytes 75 // can still be successfully parsed and returns the expected unmodified encoding. 76 assertEquals(RSA_2048_REDUNDANT_LEN_DIGEST_HEX_ENCODING, 77 getHexEncodedDigestOfCertFromResources(RSA_2048_REDUNDANT_LEN_BYTES_PEM_ENCODING)); 78 } 79 80 @Test testGenerateCertificateWithValidDEREncoding()81 public void testGenerateCertificateWithValidDEREncoding() throws Exception { 82 // This test verifies the generateCertificate method successfully parses and returns the 83 // expected encoding of a certificate with a valid DER encoding. 84 assertEquals(RSA_2048_VALID_DIGEST_HEX_ENCODING, 85 getHexEncodedDigestOfCertFromResources(RSA_2048_VALID_DER_ENCODING)); 86 } 87 88 @Test testGenerateCertificateWithRedundantLengthBytesInDERENcoding()89 public void testGenerateCertificateWithRedundantLengthBytesInDERENcoding() throws Exception { 90 // This test verifies the generateCertificate method successfully parses and returns the 91 // original encoding of a certificate with redundant length bytes in the encoding. 92 assertEquals(RSA_2048_REDUNDANT_LEN_DIGEST_HEX_ENCODING, 93 getHexEncodedDigestOfCertFromResources(RSA_2048_REDUNDANT_LEN_BYTES_DER_ENCODING)); 94 } 95 96 @Test testGenerateCertificatesWithTwoPEMEncodedCerts()97 public void testGenerateCertificatesWithTwoPEMEncodedCerts() throws Exception { 98 // The generateCertificates method accepts an InputStream which could contain zero or more 99 // certificates in PEM or DER encoding; this test verifies both certificates in PEM format 100 // are returned with the expected encodings. 101 List<String> encodedCerts = getHexEncodedDigestsOfCertsFromResources( 102 RSA_2048_TWO_CERTS_PEM_ENCODING); 103 Set<String> expectedEncodings = createSetOfValues(RSA_2048_VALID_DIGEST_HEX_ENCODING, 104 RSA_2048_REDUNDANT_LEN_DIGEST_HEX_ENCODING); 105 assertEncodingsMatchExpectedValues(encodedCerts, expectedEncodings); 106 } 107 108 @Test testGenerateCertificatesWithTwoDEREncodedCerts()109 public void testGenerateCertificatesWithTwoDEREncodedCerts() throws Exception { 110 // This test verifies the generateCertificates method returns the expected encodings for 111 // an InputStream with both DER encoded certificates. 112 List<String> encodedCerts = getHexEncodedDigestsOfCertsFromResources( 113 RSA_2048_TWO_CERTS_DER_ENCODING); 114 Set<String> expectedEncodings = createSetOfValues(RSA_2048_VALID_DIGEST_HEX_ENCODING, 115 RSA_2048_REDUNDANT_LEN_DIGEST_HEX_ENCODING); 116 assertEncodingsMatchExpectedValues(encodedCerts, expectedEncodings); 117 } 118 119 @Test testGenerateCertificateAndGenerateCertificatesReturnSameValues()120 public void testGenerateCertificateAndGenerateCertificatesReturnSameValues() throws Exception { 121 // The generateCertificates method is intended to read multiple certificates in the provided 122 // InputStream, but it can also read a single certificate. Verify that both 123 // generateCertificate and generateCertificates return the same encodings for the same 124 // certificates. 125 List<String> certResources = Arrays.asList(RSA_2048_VALID_PEM_ENCODING, 126 RSA_2048_VALID_DER_ENCODING, RSA_2048_REDUNDANT_LEN_BYTES_PEM_ENCODING, 127 RSA_2048_REDUNDANT_LEN_BYTES_DER_ENCODING); 128 for (String certResource : certResources) { 129 String genCertValue = getHexEncodedDigestOfCertFromResources(certResource); 130 List<String> genCertsValues = getHexEncodedDigestsOfCertsFromResources(certResource); 131 assertEquals( 132 "The generateCertificates method should have returned a single certificate", 1, 133 genCertsValues.size()); 134 assertEquals( 135 "The hex encoded digest of the certificate from generateCertificate does not " 136 + "match the value from generateCertificates", 137 genCertValue, genCertsValues.get(0)); 138 } 139 } 140 141 @Test testGenerateCertificatesWithEmptyInput()142 public void testGenerateCertificatesWithEmptyInput() throws Exception { 143 // This test verifies the generateCertificates method returns an empty Collection of 144 // Certificates when provided an empty InputStream. 145 assertEquals( 146 "Zero certificates should be returned when passing an empty InputStream to " 147 + "generateCertificates", 148 0, X509CertificateUtils.generateCertificates( 149 new ByteArrayInputStream(new byte[0])).size()); 150 } 151 createSetOfValues(String... values)152 private static Set<String> createSetOfValues(String... values) { 153 Set<String> result = new HashSet<>(); 154 for (String value : values) { 155 result.add(value); 156 } 157 return result; 158 } 159 160 /** 161 * Returns a hex encoding of the digest of the specified certificate from the resources. 162 */ getHexEncodedDigestOfCertFromResources(String resourceName)163 private static String getHexEncodedDigestOfCertFromResources(String resourceName) 164 throws Exception { 165 byte[] encodedForm = Resources.toByteArray(X509CertificateUtilsTest.class, resourceName); 166 X509Certificate cert = X509CertificateUtils.generateCertificate(encodedForm); 167 return getHexEncodedDigestOfBytes(cert.getEncoded()); 168 } 169 170 /** 171 * Returns a list of hex encodings of the digests of the certificates in the specified resource. 172 */ getHexEncodedDigestsOfCertsFromResources(String resourceName)173 private static List<String> getHexEncodedDigestsOfCertsFromResources(String resourceName) 174 throws Exception { 175 InputStream in = Resources.toInputStream(X509CertificateUtilsTest.class, resourceName); 176 Collection<? extends Certificate> certs = X509CertificateUtils.generateCertificates(in); 177 List<String> encodedCerts = new ArrayList<>(certs.size()); 178 for (Certificate cert : certs) { 179 encodedCerts.add(getHexEncodedDigestOfBytes(cert.getEncoded())); 180 } 181 return encodedCerts; 182 } 183 184 /** 185 * Returns the hex encoding of the digest of the specified bytes. 186 */ getHexEncodedDigestOfBytes(byte[] bytes)187 private static String getHexEncodedDigestOfBytes(byte[] bytes) 188 throws NoSuchAlgorithmException { 189 return HexEncoding.encode(MessageDigest.getInstance("SHA-256").digest(bytes)); 190 } 191 192 /** 193 * Asserts that the encoding of the provided certificates match the expected values. 194 */ assertEncodingsMatchExpectedValues(List<String> encodedCerts, Set<String> expectedValues)195 private static void assertEncodingsMatchExpectedValues(List<String> encodedCerts, 196 Set<String> expectedValues) { 197 assertEquals( 198 "The number of encoded certificates does not match the expected number of values", 199 expectedValues.size(), encodedCerts.size()); 200 for (String encodedCert : encodedCerts) { 201 // if the current encoding is found in the expected Set then remove it to ensure that 202 // duplicate values do not cause the test to pass if they are not expected. 203 if (expectedValues.contains(encodedCert)) { 204 expectedValues.remove(encodedCert); 205 } else { 206 fail("An unexpected certificate with the following encoding was returned: " 207 + encodedCert); 208 } 209 } 210 } 211 } 212