1/*
2 * Copyright (C) 2017 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
17package com.android.server.locksettings.recoverablekeystore.certificate;
18
19import static com.google.common.truth.Truth.assertThat;
20
21import static java.nio.charset.StandardCharsets.UTF_8;
22
23import static org.testng.Assert.assertThrows;
24import static org.testng.Assert.expectThrows;
25
26import android.support.test.filters.SmallTest;
27import android.support.test.runner.AndroidJUnit4;
28
29import java.io.InputStream;
30import java.security.KeyPairGenerator;
31import java.security.PublicKey;
32import java.security.cert.CertPath;
33import java.security.cert.CertificateFactory;
34import java.security.cert.X509Certificate;
35import java.security.spec.ECGenParameterSpec;
36import java.util.ArrayList;
37import java.util.Arrays;
38import java.util.Base64;
39import java.util.Collections;
40import java.util.List;
41
42import org.junit.Test;
43import org.junit.runner.RunWith;
44import org.w3c.dom.Element;
45
46@SmallTest
47@RunWith(AndroidJUnit4.class)
48public final class CertUtilsTest {
49
50    private static final String XML_STR = ""
51            + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
52            + "<!-- comment 1 -->"
53            + "<root>\n\n\n\r\r\r"
54            + "  <node1>\r\n\r\n"
55            + "    node1-1</node1>"
56            + "  <!-- comment 2 -->"
57            + "  <node1>node1-2"
58            + "    \n\r\n\r</node1>"
59            + "  <node2>"
60            + "    <node1>   node2-node1-1</node1>"
61            + "    <node1>node2-node1-2   </node1>"
62            + "    <!-- comment 3 -->"
63            + "    <node1>   node2-node1-3   </node1>"
64            + "  </node2>"
65            + "</root>";
66
67    private static final String SIGNED_STR = "abcdefg\n";
68    private static final String SIGNATURE_BASE64 = ""
69            + "KxBt9B3pwL3/59SrjTJTpuhc9JRxLOUNwNr3J4EEdXj5BqkYOUeXIOjyBGp8XaOnmuW8WmBxhko3"
70            + "yTR3/M9x0/pJuKDgqQSSFG+I56O/IAri7DmMBfY8QqcgiF8RaR86G7mWXUIdu8ixEtpKa//T4bN7"
71            + "c8Txvt96ApAcW0wJDihfCqDEXyi56pFCp+qEZuL4fS8iZtZTUkvxim1tb2/IsZ9OyDd9BWxp+JTs"
72            + "zihzH6xqnUCa1aELSUZnU8OzWGeuKpVDQDbDMtQpcxJ9o+6L6wO5vmQutZAulgw5gRPGhYWVs8+0"
73            + "ATdNEbv8TSomkXkZ3/lMYnmPXKmaHxcP4330DA==";
74    private static final PublicKey SIGNER_PUBLIC_KEY = TestData.INTERMEDIATE_CA_2.getPublicKey();
75
76    @Test
77    public void decodeCert_readPemFile_succeeds_singleBlock() throws Exception {
78        InputStream f = TestData.openTestFile("pem/valid-cert.pem");
79        X509Certificate cert = CertUtils.decodeCert(f);
80        assertThat(cert).isEqualTo(TestData.ROOT_CA_TRUSTED);
81    }
82
83    @Test
84    public void decodeCert_readPemFile_succeeds_multipleBlocks() throws Exception {
85        InputStream in = TestData.openTestFile("pem/valid-cert-multiple-blocks.pem");
86        X509Certificate cert = CertUtils.decodeCert(in);
87        assertThat(cert).isEqualTo(TestData.ROOT_CA_TRUSTED);
88    }
89
90    @Test
91    public void decodeCert_readPemFile_throwsIfNoBeginEndLines() throws Exception {
92        InputStream in = TestData.openTestFile("pem/invalid-cert-1-no-begin-end.pem");
93        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(in));
94    }
95
96    @Test
97    public void decodeCert_readPemFile_throwsIfEmptyBlock() throws Exception {
98        InputStream in = TestData.openTestFile("pem/invalid-cert-2-empty-block.pem");
99        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(in));
100    }
101
102    @Test
103    public void decodeCert_readPemFile_throwsIfInvalidCert() throws Exception {
104        InputStream in = TestData.openTestFile("pem/invalid-cert-3-invalid-key.pem");
105        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(in));
106    }
107
108    @Test
109    public void decodeCert_readBytes_succeeds() throws Exception {
110        X509Certificate cert = CertUtils.decodeCert(TestData.INTERMEDIATE_CA_2.getEncoded());
111        assertThat(cert.getIssuerX500Principal().getName())
112                .isEqualTo("CN=Google CryptAuthVault Intermediate");
113    }
114
115    @Test
116    public void decodeCert_readBytes_throwsIfInvalidCert() throws Exception {
117        byte[] modifiedCertBytes = TestData.INTERMEDIATE_CA_1.getEncoded();
118        modifiedCertBytes[0] ^= (byte) 1;
119        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(modifiedCertBytes));
120    }
121
122    @Test
123    public void decodeBase64_succeeds() throws Exception {
124        assertThat(CertUtils.decodeBase64("VEVTVA==")).isEqualTo("TEST".getBytes(UTF_8));
125    }
126
127    @Test
128    public void decodeBase64_succeedsIfEmptyInput() throws Exception {
129        assertThat(CertUtils.decodeBase64("")).hasLength(0);
130    }
131
132    @Test
133    public void decodeBase64_throwsIfInvalidInput() throws Exception {
134        assertThrows(CertParsingException.class, () -> CertUtils.decodeBase64("EVTVA=="));
135    }
136
137    @Test
138    public void getXmlRootNode_succeeds() throws Exception {
139        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
140        assertThat(root.getTagName()).isEqualTo("root");
141    }
142
143    @Test
144    public void getXmlRootNode_throwsIfEmptyInput() throws Exception {
145        assertThrows(CertParsingException.class, () -> CertUtils.getXmlRootNode(new byte[0]));
146    }
147
148    @Test
149    public void getXmlNodeContents_singleLevel_succeeds() throws Exception {
150        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
151        assertThat(CertUtils.getXmlNodeContents(CertUtils.MUST_EXIST_UNENFORCED, root, "node1"))
152                .containsExactly("node1-1", "node1-2");
153    }
154
155    @Test
156    public void getXmlNodeContents_multipleLevels_succeeds() throws Exception {
157        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
158        assertThat(CertUtils.getXmlNodeContents(CertUtils.MUST_EXIST_UNENFORCED, root, "node2", "node1"))
159                .containsExactly("node2-node1-1", "node2-node1-2", "node2-node1-3");
160    }
161
162    @Test
163    public void getXmlNodeContents_mustExistFalse_succeedsIfNotExist() throws Exception {
164        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
165        assertThat(
166                CertUtils.getXmlNodeContents(
167                        CertUtils.MUST_EXIST_UNENFORCED, root, "node2", "node-not-exist"))
168                .isEmpty();
169    }
170
171    @Test
172    public void getXmlNodeContents_mustExistAtLeastOne_throwsIfNotExist() throws Exception {
173        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
174        CertParsingException expected =
175                expectThrows(
176                        CertParsingException.class,
177                        () ->
178                                CertUtils.getXmlNodeContents(
179                                        CertUtils.MUST_EXIST_AT_LEAST_ONE, root, "node2",
180                                        "node-not-exist"));
181        assertThat(expected.getMessage()).contains("must contain at least one");
182    }
183
184    @Test
185    public void getXmlNodeContents_mustExistExactlyOne_throwsIfNotExist() throws Exception {
186        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
187        CertParsingException expected =
188                expectThrows(
189                        CertParsingException.class,
190                        () ->
191                                CertUtils.getXmlNodeContents(
192                                        CertUtils.MUST_EXIST_EXACTLY_ONE, root, "node-not-exist",
193                                        "node1"));
194        assertThat(expected.getMessage()).contains("must contain exactly one");
195    }
196
197    @Test
198    public void getXmlNodeContents_mustExistExactlyOne_throwsIfMultipleExist() throws Exception {
199        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
200        CertParsingException expected =
201                expectThrows(
202                        CertParsingException.class,
203                        () ->
204                                CertUtils.getXmlNodeContents(
205                                        CertUtils.MUST_EXIST_EXACTLY_ONE, root, "node2", "node1"));
206        assertThat(expected.getMessage()).contains("must contain exactly one");
207    }
208
209    @Test
210    public void verifyRsaSha256Signature_succeeds() throws Exception {
211        CertUtils.verifyRsaSha256Signature(
212                SIGNER_PUBLIC_KEY,
213                Base64.getDecoder().decode(SIGNATURE_BASE64),
214                SIGNED_STR.getBytes(UTF_8));
215    }
216
217    @Test
218    public void verifyRsaSha256Signature_throwsIfMismatchSignature() throws Exception {
219        byte[] modifiedBytes = SIGNED_STR.getBytes(UTF_8);
220        modifiedBytes[0] ^= (byte) 1;
221        assertThrows(
222                CertValidationException.class,
223                () ->
224                        CertUtils.verifyRsaSha256Signature(
225                                SIGNER_PUBLIC_KEY, Base64.getDecoder().decode(SIGNATURE_BASE64),
226                                modifiedBytes));
227    }
228
229    @Test
230    public void verifyRsaSha256Signature_throwsIfWrongKeyType() throws Exception {
231        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
232        keyPairGenerator.initialize(new ECGenParameterSpec("secp256r1"));
233        PublicKey publicKey = keyPairGenerator.generateKeyPair().getPublic();
234        assertThrows(
235                CertValidationException.class,
236                () ->
237                        CertUtils.verifyRsaSha256Signature(
238                                publicKey,
239                                Base64.getDecoder().decode(SIGNATURE_BASE64),
240                                SIGNED_STR.getBytes(UTF_8)));
241    }
242
243    @Test
244    public void buildCertPath_succeedsWithoutIntermediates() throws Exception {
245        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
246        X509Certificate leafCert = TestData.INTERMEDIATE_CA_1;
247        CertPath certPath = CertUtils.buildCertPath(
248                CertUtils.buildPkixParams(
249                        TestData.DATE_ALL_CERTS_VALID, rootCert, Collections.emptyList(),
250                        leafCert));
251        assertThat(certPath.getCertificates()).containsExactly(
252                TestData.INTERMEDIATE_CA_1).inOrder();
253    }
254
255    @Test
256    public void buildCertPath_succeedsWithIntermediates() throws Exception {
257        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
258        List<X509Certificate> intermediateCerts =
259                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
260        X509Certificate leafCert = TestData.LEAF_CERT_2;
261        CertPath certPath =
262                CertUtils.buildCertPath(
263                        CertUtils.buildPkixParams(
264                                TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
265                                leafCert));
266        assertThat(certPath.getCertificates())
267                .containsExactly(
268                        TestData.LEAF_CERT_2, TestData.INTERMEDIATE_CA_2,
269                        TestData.INTERMEDIATE_CA_1)
270                .inOrder();
271    }
272
273    @Test
274    public void buildCertPath_succeedsWithIntermediates_ignoreUnrelatedIntermedateCert()
275            throws Exception {
276        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
277        List<X509Certificate> intermediateCerts =
278                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
279        X509Certificate leafCert = TestData.LEAF_CERT_1;
280        CertPath certPath =
281                CertUtils.buildCertPath(
282                        CertUtils.buildPkixParams(
283                                TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
284                                leafCert));
285        assertThat(certPath.getCertificates())
286                .containsExactly(TestData.LEAF_CERT_1, TestData.INTERMEDIATE_CA_1)
287                .inOrder();
288    }
289
290    @Test
291    public void buildCertPath_throwsIfWrongRootCommonName() throws Exception {
292        X509Certificate rootCert = TestData.ROOT_CA_DIFFERENT_COMMON_NAME;
293        List<X509Certificate> intermediateCerts =
294                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
295        X509Certificate leafCert = TestData.LEAF_CERT_1;
296
297        assertThrows(
298                CertValidationException.class,
299                () ->
300                        CertUtils.buildCertPath(
301                                CertUtils.buildPkixParams(
302                                        TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
303                                        leafCert)));
304    }
305
306    @Test
307    public void buildCertPath_throwsIfMissingIntermediateCert() throws Exception {
308        X509Certificate rootCert = TestData.ROOT_CA_DIFFERENT_COMMON_NAME;
309        List<X509Certificate> intermediateCerts = Collections.emptyList();
310        X509Certificate leafCert = TestData.LEAF_CERT_1;
311
312        assertThrows(
313                CertValidationException.class,
314                () ->
315                        CertUtils.buildCertPath(
316                                CertUtils.buildPkixParams(
317                                        TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
318                                        leafCert)));
319    }
320
321    @Test
322    public void validateCertPath_succeeds() throws Exception {
323        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
324        List<X509Certificate> intermediateCerts =
325                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
326        X509Certificate leafCert = TestData.LEAF_CERT_2;
327        CertPath certPath =
328                CertUtils.buildCertPath(
329                        CertUtils.buildPkixParams(
330                                TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
331                                leafCert));
332        CertUtils.validateCertPath(
333                TestData.DATE_ALL_CERTS_VALID, TestData.ROOT_CA_TRUSTED, certPath);
334    }
335
336    @Test
337    public void validateCertPath_throwsIfEmptyCertPath() throws Exception {
338        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
339        CertPath emptyCertPath = certFactory.generateCertPath(new ArrayList<X509Certificate>());
340        CertValidationException expected =
341                expectThrows(
342                        CertValidationException.class,
343                        () -> CertUtils.validateCertPath(TestData.DATE_ALL_CERTS_VALID,
344                                TestData.ROOT_CA_TRUSTED, emptyCertPath));
345        assertThat(expected.getMessage()).contains("empty");
346    }
347
348    @Test
349    public void validateCertPath_throwsIfNotValidated() throws Exception {
350        assertThrows(
351                CertValidationException.class,
352                () -> CertUtils.validateCertPath(TestData.DATE_ALL_CERTS_VALID,
353                        TestData.ROOT_CA_DIFFERENT_COMMON_NAME,
354                        com.android.server.locksettings.recoverablekeystore.TestData.CERT_PATH_1));
355    }
356
357    @Test
358    public void validateCert_succeeds() throws Exception {
359        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
360        List<X509Certificate> intermediateCerts =
361                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
362        X509Certificate leafCert = TestData.LEAF_CERT_2;
363        CertUtils.validateCert(TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
364                leafCert);
365    }
366
367    @Test
368    public void validateCert_throwsIfExpired() throws Exception {
369        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
370        List<X509Certificate> intermediateCerts =
371                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
372        X509Certificate leafCert = TestData.LEAF_CERT_2;
373        assertThrows(
374                CertValidationException.class,
375                () ->
376                        CertUtils.validateCert(
377                                TestData.DATE_LEAF_CERT_2_EXPIRED, rootCert, intermediateCerts,
378                                leafCert));
379    }
380
381    @Test
382    public void validateCert_throwsIfWrongRootWithTheSameCommonName() throws Exception {
383        X509Certificate rootCert = TestData.ROOT_CA_DIFFERENT_KEY;
384        List<X509Certificate> intermediateCerts =
385                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
386        X509Certificate leafCert = TestData.LEAF_CERT_2;
387        assertThrows(
388                CertValidationException.class,
389                () ->
390                        CertUtils.validateCert(
391                                TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
392                                leafCert));
393    }
394}
395