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