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