1 /**
2  * Copyright (C) 2022 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.rkpdapp.utils;
18 
19 import android.util.Base64;
20 import android.util.Log;
21 
22 import com.android.rkpdapp.RkpdException;
23 
24 import java.io.ByteArrayInputStream;
25 import java.math.BigInteger;
26 import java.security.InvalidAlgorithmParameterException;
27 import java.security.InvalidKeyException;
28 import java.security.NoSuchAlgorithmException;
29 import java.security.NoSuchProviderException;
30 import java.security.PublicKey;
31 import java.security.SignatureException;
32 import java.security.cert.CertPathValidator;
33 import java.security.cert.CertPathValidatorException;
34 import java.security.cert.Certificate;
35 import java.security.cert.CertificateException;
36 import java.security.cert.CertificateFactory;
37 import java.security.cert.PKIXParameters;
38 import java.security.cert.TrustAnchor;
39 import java.security.cert.X509Certificate;
40 import java.security.interfaces.ECPublicKey;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.Date;
44 import java.util.Set;
45 
46 /**
47  * Provides convenience methods for parsing certificates and extracting information.
48  */
49 public class X509Utils {
50 
51     private static final String TAG = "RkpdX509Utils";
52 
53     /**
54      * Takes a byte array composed of DER encoded certificates and returns the X.509 certificates
55      * contained within as an X509Certificate array.
56      */
formatX509Certs(byte[] certStream)57     public static X509Certificate[] formatX509Certs(byte[] certStream) throws RkpdException {
58         try {
59             CertificateFactory fact = CertificateFactory.getInstance("X.509");
60             ByteArrayInputStream in = new ByteArrayInputStream(certStream);
61             ArrayList<Certificate> certs = new ArrayList<>(fact.generateCertificates(in));
62             X509Certificate[] certChain = certs.toArray(new X509Certificate[0]);
63             if (isCertChainValid(certChain)) {
64                 return certChain;
65             } else {
66                 throw new RkpdException(RkpdException.ErrorCode.INTERNAL_ERROR,
67                         "Could not validate certificate chain.");
68             }
69         } catch (CertificateException | NoSuchAlgorithmException | NoSuchProviderException
70                  | InvalidAlgorithmParameterException e) {
71             Log.e(TAG, "Unable to parse certificate chain."
72                     + Base64.encodeToString(certStream, Base64.DEFAULT), e);
73             throw new RkpdException(RkpdException.ErrorCode.INTERNAL_ERROR,
74                     "Failed to interpret DER encoded certificate chain", e);
75         }
76     }
77 
78     /**
79      * Extracts an ECDSA-P256 key from a certificate and formats it so that it can be used to match
80      * the certificate chain to the proper key when passed into the keystore database.
81      */
getAndFormatRawPublicKey(X509Certificate cert)82     public static byte[] getAndFormatRawPublicKey(X509Certificate cert) {
83         PublicKey pubKey = cert.getPublicKey();
84         if (!(pubKey instanceof ECPublicKey)) {
85             Log.e(TAG, "Certificate public key is not an instance of ECPublicKey");
86             return null;
87         }
88         ECPublicKey key = (ECPublicKey) cert.getPublicKey();
89         // Remote key provisioning internally supports the default, uncompressed public key
90         // format for ECDSA. This defines the format as (s | x | y), where s is the byte
91         // indicating if the key is compressed or not, and x and y make up the EC point.
92         // However, the key as stored in a COSE_Key object is just the two points. As such,
93         // the raw public key is stored in the database as (x | y), so the compression byte
94         // should be dropped here. Leading 0's must be preserved.
95         //
96         // s: 1 byte, x: 32 bytes, y: 32 bytes
97         BigInteger xCoord = key.getW().getAffineX();
98         BigInteger yCoord = key.getW().getAffineY();
99         byte[] formattedKey = new byte[64];
100         byte[] xBytes = xCoord.toByteArray();
101         // BigInteger returns the value as two's complement big endian byte encoding. This means
102         // that a positive, 32-byte value with a leading 1 bit will be converted to a byte array of
103         // length 33 in order to include a leading 0 bit.
104         if (xBytes.length == 33) {
105             System.arraycopy(xBytes, 1 /* offset */, formattedKey, 0 /* offset */, 32);
106         } else {
107             System.arraycopy(xBytes, 0 /* offset */,
108                              formattedKey, 32 - xBytes.length, xBytes.length);
109         }
110         byte[] yBytes = yCoord.toByteArray();
111         if (yBytes.length == 33) {
112             System.arraycopy(yBytes, 1 /* offset */, formattedKey, 32 /* offset */, 32);
113         } else {
114             System.arraycopy(yBytes, 0 /* offset */,
115                              formattedKey, 64 - yBytes.length, yBytes.length);
116         }
117         return formattedKey;
118     }
119 
120     /**
121      * Validates the X509 certificate chain and returns appropriate boolean result.
122      */
isCertChainValid(X509Certificate[] certChain)123     public static boolean isCertChainValid(X509Certificate[] certChain)
124             throws NoSuchAlgorithmException, NoSuchProviderException,
125             InvalidAlgorithmParameterException {
126         X509Certificate rootCert = certChain[certChain.length - 1];
127         return isSelfSignedCertificate(rootCert) && verifyCertChain(rootCert, certChain);
128     }
129 
verifyCertChain(X509Certificate rootCert, X509Certificate[] certChain)130     private static boolean verifyCertChain(X509Certificate rootCert, X509Certificate[] certChain)
131             throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
132         try {
133             // Only add the self-signed root certificate as trust anchor.
134             // All the other certificates in the chain should be signed by the previous cert's key.
135             Set<TrustAnchor> trustedAnchors = Set.of(new TrustAnchor(rootCert, null));
136 
137             CertificateFactory fact = CertificateFactory.getInstance("X.509");
138             CertPathValidator validator = CertPathValidator.getInstance("PKIX");
139             PKIXParameters parameters = new PKIXParameters(trustedAnchors);
140             parameters.setRevocationEnabled(false);
141             validator.validate(fact.generateCertPath(Arrays.asList(certChain)), parameters);
142             return true;
143         } catch (CertificateException | CertPathValidatorException e) {
144             Log.e(TAG, "certificate chain validation failed.", e);
145             return false;
146         }
147     }
148 
149     /**
150      * Verifies whether an X509Certificate is a self-signed certificate.
151      */
isSelfSignedCertificate(X509Certificate certificate)152     public static boolean isSelfSignedCertificate(X509Certificate certificate)
153             throws NoSuchAlgorithmException, NoSuchProviderException {
154         try {
155             certificate.verify(certificate.getPublicKey());
156             return true;
157         } catch (SignatureException | InvalidKeyException | CertificateException e) {
158             Log.e(TAG, "Error verifying self signed certificate", e);
159             return false;
160         }
161     }
162 
163     /**
164      * Gets the date when the entire cert chain expires. This can be calculated by
165      * checking the individual certificate times and returning the minimum of all.
166      * @return Expiration time for the certificate chain
167      */
getExpirationTimeForCertificateChain(X509Certificate[] certChain)168     public static Date getExpirationTimeForCertificateChain(X509Certificate[] certChain) {
169         return Arrays.stream(certChain)
170                 .map(X509Certificate::getNotAfter)
171                 .min(Date::compareTo)
172                 .orElse(null);
173     }
174 }
175