1 /* 2 * Copyright (C) 2023 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; 18 19 import android.hardware.security.keymint.DeviceInfo; 20 import android.hardware.security.keymint.IRemotelyProvisionedComponent; 21 import android.hardware.security.keymint.MacedPublicKey; 22 import android.hardware.security.keymint.ProtectedData; 23 import android.hardware.security.keymint.RpcHardwareInfo; 24 import android.net.Uri; 25 import android.os.Build; 26 import android.os.RemoteException; 27 import android.os.ServiceManager; 28 import android.os.ServiceSpecificException; 29 import android.os.SystemProperties; 30 import android.util.Base64; 31 import android.util.Log; 32 33 import java.io.BufferedInputStream; 34 import java.io.ByteArrayInputStream; 35 import java.io.ByteArrayOutputStream; 36 import java.io.IOException; 37 import java.io.OutputStream; 38 import java.net.HttpURLConnection; 39 import java.net.URL; 40 import java.security.cert.Certificate; 41 import java.security.cert.CertificateException; 42 import java.security.cert.CertificateFactory; 43 import java.security.cert.X509Certificate; 44 import java.util.ArrayList; 45 import java.util.HashMap; 46 import java.util.List; 47 import java.util.UUID; 48 49 import co.nstant.in.cbor.CborBuilder; 50 import co.nstant.in.cbor.CborDecoder; 51 import co.nstant.in.cbor.CborEncoder; 52 import co.nstant.in.cbor.CborException; 53 import co.nstant.in.cbor.model.Array; 54 import co.nstant.in.cbor.model.ByteString; 55 import co.nstant.in.cbor.model.DataItem; 56 import co.nstant.in.cbor.model.Map; 57 import co.nstant.in.cbor.model.UnicodeString; 58 import co.nstant.in.cbor.model.UnsignedInteger; 59 60 /** 61 * Command-line utility that verifies each KeyMint instance on this device is able to 62 * get production RKP keys. 63 */ 64 public class RkpRegistrationCheck { 65 private static final String TAG = "RegistrationTest"; 66 private static final int COSE_HEADER_ALGORITHM = 1; 67 private static final int COSE_ALGORITHM_HMAC_256 = 5; 68 69 private static final int SHARED_CERTIFICATES_INDEX = 0; 70 private static final int UNIQUE_CERTIFICATES_INDEX = 1; 71 72 private static final int TIMEOUT_MS = 20_000; 73 private final String mRequestId = UUID.randomUUID().toString(); 74 private final String mInstanceName; 75 76 private static class NotRegisteredException extends Exception { 77 } 78 79 private static class FetchEekResponse { 80 private static final int EEK_AND_CURVE_INDEX = 0; 81 private static final int CHALLENGE_INDEX = 1; 82 83 private static final int CURVE_INDEX = 0; 84 private static final int EEK_CERT_CHAIN_INDEX = 1; 85 86 private final byte[] mChallenge; 87 private final HashMap<Integer, byte[]> mCurveToGeek = new HashMap<>(); 88 FetchEekResponse(DataItem response)89 FetchEekResponse(DataItem response) throws CborException, RemoteException { 90 List<DataItem> respItems = ((Array) response).getDataItems(); 91 List<DataItem> allEekChains = 92 ((Array) respItems.get(EEK_AND_CURVE_INDEX)).getDataItems(); 93 for (DataItem entry : allEekChains) { 94 List<DataItem> curveAndEekChain = ((Array) entry).getDataItems(); 95 UnsignedInteger curve = (UnsignedInteger) curveAndEekChain.get(CURVE_INDEX); 96 mCurveToGeek.put(curve.getValue().intValue(), 97 encodeCbor(curveAndEekChain.get(EEK_CERT_CHAIN_INDEX))); 98 } 99 100 mChallenge = ((ByteString) respItems.get(CHALLENGE_INDEX)).getBytes(); 101 } 102 getEekChain(int curve)103 public byte[] getEekChain(int curve) { 104 return mCurveToGeek.get(curve); 105 } 106 getChallenge()107 public byte[] getChallenge() { 108 return mChallenge; 109 } 110 } 111 112 /** Main entry point. */ main(String[] args)113 public static void main(String[] args) { 114 if (SystemProperties.get("remote_provisioning.hostname").isEmpty()) { 115 System.out.println( 116 "The RKP server hostname is not configured -- RKP is disabled."); 117 } 118 119 new RkpRegistrationCheck("default").checkNow(); 120 new RkpRegistrationCheck("strongbox").checkNow(); 121 } 122 RkpRegistrationCheck(String instanceName)123 RkpRegistrationCheck(String instanceName) { 124 mInstanceName = instanceName; 125 } 126 checkNow()127 void checkNow() { 128 System.out.println(); 129 System.out.println("Checking to see if the device key for HAL '" + mInstanceName 130 + "' has been registered..."); 131 132 if (!isValidInstance()) { 133 System.err.println("Skipping registration check for '" + mInstanceName + "'."); 134 System.err.println("The HAL does not exist."); 135 return; 136 } 137 138 try { 139 FetchEekResponse eekResponse = fetchEek(); 140 String serviceName = IRemotelyProvisionedComponent.DESCRIPTOR + "/" + mInstanceName; 141 IRemotelyProvisionedComponent binder = IRemotelyProvisionedComponent.Stub.asInterface( 142 ServiceManager.waitForDeclaredService(serviceName)); 143 byte[] csr = generateCsr(binder, eekResponse); 144 X509Certificate[] certs = signCertificates(csr, eekResponse.getChallenge()); 145 Log.i(TAG, "Cert chain:"); 146 for (X509Certificate c : certs) { 147 Log.i(TAG, " " + c.toString()); 148 } 149 System.out.println("SUCCESS: Device key for '" + mInstanceName + "' is registered"); 150 } catch (ServiceSpecificException e) { 151 Log.e(TAG, e.getMessage(), e); 152 System.err.println("Error getting CSR for '" + mInstanceName + "': '" + e 153 + "', skipping."); 154 } catch (NotRegisteredException e) { 155 Log.e(TAG, e.getMessage(), e); 156 System.out.println("FAIL: Device key for '" + mInstanceName + "' is NOT registered"); 157 } catch (IOException | CborException | RemoteException | CertificateException e) { 158 Log.e(TAG, e.getMessage(), e); 159 System.err.println("Error checking device registration for '" + mInstanceName 160 + "': '" + e + "', skipping."); 161 } 162 } 163 isValidInstance()164 private boolean isValidInstance() { 165 // The SE policy checks appear to be very strict for shell, and we'll get a security 166 // exception for any HALs not actually declared. Instead, check to see if the given 167 // instance is in the list we can query. 168 String[] instances = ServiceManager.getDeclaredInstances( 169 IRemotelyProvisionedComponent.DESCRIPTOR); 170 for (String i : instances) { 171 if (i.equals(mInstanceName)) { 172 return true; 173 } 174 } 175 return false; 176 } 177 getBaseUri()178 private Uri getBaseUri() { 179 String hostnameProperty = "remote_provisioning.hostname"; 180 String hostname = SystemProperties.get(hostnameProperty); 181 if (hostname.isEmpty()) { 182 throw new RuntimeException("System property '" + hostnameProperty + "' is empty. " 183 + "This device does not support RKP."); 184 } 185 return new Uri.Builder().scheme("https").authority(hostname).appendPath("v1").build(); 186 } 187 fetchEek()188 FetchEekResponse fetchEek() 189 throws IOException, CborException, RemoteException, NotRegisteredException { 190 final Uri uri = getBaseUri().buildUpon().appendEncodedPath(":fetchEekChain").build(); 191 192 final ByteArrayOutputStream input = new ByteArrayOutputStream(); 193 new CborEncoder(input).encode(new CborBuilder() 194 .addMap() 195 .put("fingerprint", getFingerprint()) 196 .put(new UnicodeString("id"), new UnsignedInteger(0)) 197 .end() 198 .build()); 199 200 return new FetchEekResponse(httpPost(uri, input.toByteArray())); 201 } 202 getFingerprint()203 private String getFingerprint() { 204 // Fake a user build fingerprint so that we will get 444 on unregistered devices instead 205 // of test certs. 206 Log.i(TAG, "Original fingerprint: " + Build.FINGERPRINT); 207 String fingerprint = Build.FINGERPRINT 208 .replace(":userdebug", ":user") 209 .replace(":eng", ":user") 210 .replace("cf_", "cephalopod_"); 211 Log.i(TAG, "Modified (prod-like) fingerprint: " + fingerprint); 212 return fingerprint; 213 } 214 signCertificates(byte[] csr, byte[] challenge)215 X509Certificate[] signCertificates(byte[] csr, byte[] challenge) 216 throws IOException, CborException, CertificateException, 217 NotRegisteredException { 218 String encodedChallenge = Base64.encodeToString(challenge, 219 Base64.URL_SAFE | Base64.NO_WRAP); 220 final Uri uri = getBaseUri().buildUpon() 221 .appendEncodedPath(":signCertificates") 222 .appendQueryParameter("challenge", encodedChallenge) 223 .build(); 224 DataItem response = httpPost(uri, csr); 225 List<DataItem> dataItems = ((Array) response).getDataItems(); 226 byte[] sharedCertificates = ((ByteString) dataItems.get( 227 SHARED_CERTIFICATES_INDEX)).getBytes(); 228 DataItem leaf = ((Array) dataItems.get(UNIQUE_CERTIFICATES_INDEX)).getDataItems().get(0); 229 230 ByteArrayOutputStream fullChainWriter = new ByteArrayOutputStream(); 231 fullChainWriter.write(((ByteString) leaf).getBytes()); 232 fullChainWriter.write(sharedCertificates); 233 234 ByteArrayInputStream fullChainReader = new ByteArrayInputStream( 235 fullChainWriter.toByteArray()); 236 CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); 237 ArrayList<Certificate> parsedCerts = new ArrayList<>( 238 certFactory.generateCertificates(fullChainReader)); 239 return parsedCerts.toArray(new X509Certificate[0]); 240 } 241 httpPost(Uri uri, byte[] input)242 DataItem httpPost(Uri uri, byte[] input) 243 throws IOException, CborException, NotRegisteredException { 244 uri = uri.buildUpon().appendQueryParameter("requestId", mRequestId).build(); 245 Log.i(TAG, "querying " + uri); 246 HttpURLConnection con = (HttpURLConnection) new URL(uri.toString()).openConnection(); 247 con.setRequestMethod("POST"); 248 con.setConnectTimeout(TIMEOUT_MS); 249 con.setReadTimeout(TIMEOUT_MS); 250 con.setDoOutput(true); 251 252 try (OutputStream os = con.getOutputStream()) { 253 os.write(input, 0, input.length); 254 } 255 256 Log.i(TAG, "HTTP status: " + con.getResponseCode()); 257 258 if (con.getResponseCode() == 444) { 259 throw new NotRegisteredException(); 260 } 261 262 if (con.getResponseCode() != HttpURLConnection.HTTP_OK) { 263 throw new RuntimeException("Server connection failed for url: " + uri 264 + ", HTTP response code: " + con.getResponseCode()); 265 } 266 267 BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream()); 268 ByteArrayOutputStream cborBytes = new ByteArrayOutputStream(); 269 byte[] buffer = new byte[1024]; 270 int read; 271 while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) { 272 cborBytes.write(buffer, 0, read); 273 } 274 inputStream.close(); 275 byte[] response = cborBytes.toByteArray(); 276 Log.i(TAG, "response (CBOR): " + Base64.encodeToString(response, 277 Base64.URL_SAFE | Base64.NO_WRAP)); 278 return decodeCbor(response); 279 } 280 generateCsr(IRemotelyProvisionedComponent irpc, FetchEekResponse eekResponse)281 byte[] generateCsr(IRemotelyProvisionedComponent irpc, FetchEekResponse eekResponse) 282 throws RemoteException, CborException { 283 Map unverifiedDeviceInfo = new Map().put( 284 new UnicodeString("fingerprint"), new UnicodeString(getFingerprint())); 285 286 RpcHardwareInfo hwInfo = irpc.getHardwareInfo(); 287 288 MacedPublicKey[] macedKeysToSign = new MacedPublicKey[]{new MacedPublicKey()}; 289 irpc.generateEcdsaP256KeyPair(false, macedKeysToSign[0]); 290 291 if (hwInfo.versionNumber < 3) { 292 Log.i(TAG, "Generating CSRv1"); 293 DeviceInfo deviceInfo = new DeviceInfo(); 294 ProtectedData protectedData = new ProtectedData(); 295 byte[] geekChain = eekResponse.getEekChain(hwInfo.supportedEekCurve); 296 byte[] csrTag = irpc.generateCertificateRequest(false, macedKeysToSign, geekChain, 297 eekResponse.getChallenge(), deviceInfo, protectedData); 298 Array mac0Message = buildMac0MessageForV1Csr(macedKeysToSign[0], csrTag); 299 return encodeCbor(new CborBuilder() 300 .addArray() 301 .addArray() 302 .add(decodeCbor(deviceInfo.deviceInfo)) 303 .add(unverifiedDeviceInfo) 304 .end() 305 .add(eekResponse.getChallenge()) 306 .add(decodeCbor(protectedData.protectedData)) 307 .add(mac0Message) 308 .end() 309 .build().get(0)); 310 } else { 311 Log.i(TAG, "Generating CSRv2"); 312 byte[] csrBytes = irpc.generateCertificateRequestV2(macedKeysToSign, 313 eekResponse.getChallenge()); 314 Array array = (Array) decodeCbor(csrBytes); 315 array.add(unverifiedDeviceInfo); 316 return encodeCbor(array); 317 } 318 } 319 buildMac0MessageForV1Csr(MacedPublicKey macedKeyToSign, byte[] csrTag)320 Array buildMac0MessageForV1Csr(MacedPublicKey macedKeyToSign, byte[] csrTag) 321 throws CborException { 322 DataItem macedPayload = ((Array) decodeCbor( 323 macedKeyToSign.macedKey)).getDataItems().get(2); 324 Map macedCoseKey = (Map) decodeCbor(((ByteString) macedPayload).getBytes()); 325 byte[] macedKeys = encodeCbor(new Array().add(macedCoseKey)); 326 327 Map protectedHeaders = new Map().put( 328 new UnsignedInteger(COSE_HEADER_ALGORITHM), 329 new UnsignedInteger(COSE_ALGORITHM_HMAC_256)); 330 return new Array() 331 .add(new ByteString(encodeCbor(protectedHeaders))) 332 .add(new Map()) 333 .add(new ByteString(macedKeys)) 334 .add(new ByteString(csrTag)); 335 } 336 decodeCbor(byte[] encodedBytes)337 static DataItem decodeCbor(byte[] encodedBytes) throws CborException { 338 ByteArrayInputStream inputStream = new ByteArrayInputStream(encodedBytes); 339 return new CborDecoder(inputStream).decode().get(0); 340 } 341 encodeCbor(final DataItem dataItem)342 static byte[] encodeCbor(final DataItem dataItem) throws CborException { 343 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 344 new CborEncoder(outputStream).encode(dataItem); 345 return outputStream.toByteArray(); 346 } 347 } 348