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.federatedcompute.services.security; 18 19 import android.content.Context; 20 import android.content.pm.PackageManager; 21 import android.security.keystore.KeyGenParameterSpec; 22 import android.security.keystore.KeyProperties; 23 import android.util.Base64; 24 25 import com.android.federatedcompute.internal.util.LogUtil; 26 import com.android.internal.annotations.VisibleForTesting; 27 28 import java.security.KeyPair; 29 import java.security.KeyPairGenerator; 30 import java.security.KeyStore; 31 import java.security.KeyStoreException; 32 import java.security.NoSuchAlgorithmException; 33 import java.security.NoSuchProviderException; 34 import java.security.cert.Certificate; 35 import java.security.spec.ECGenParameterSpec; 36 import java.util.ArrayList; 37 import java.util.List; 38 39 public class KeyAttestation { 40 41 private static final String TAG = "KeyAttestation"; 42 private static final String ANDROID_KEY_STORE = "AndroidKeyStore"; 43 44 private static final String ODP_KEY_ALIAS = "ODPKeyAttestation"; 45 46 private static volatile KeyAttestation sSingletonInstance; 47 48 private final boolean mUseStrongBox; 49 50 static class Injector { getKeyStore()51 KeyStore getKeyStore() throws KeyStoreException { 52 return KeyStore.getInstance(ANDROID_KEY_STORE); 53 } 54 getKeyPairGenerator()55 KeyPairGenerator getKeyPairGenerator() 56 throws NoSuchAlgorithmException, NoSuchProviderException { 57 return KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEY_STORE); 58 } 59 } 60 61 private final Injector mInjector; 62 KeyAttestation(boolean useStrongBox, Injector injector)63 KeyAttestation(boolean useStrongBox, Injector injector) { 64 this.mUseStrongBox = useStrongBox; 65 this.mInjector = injector; 66 } 67 68 /** 69 * @return a singleton instance for KeyAttestation. 70 */ getInstance(Context context)71 public static KeyAttestation getInstance(Context context) { 72 if (sSingletonInstance == null) { 73 synchronized (KeyAttestation.class) { 74 if (sSingletonInstance == null) { 75 boolean useStrongBox = 76 context.getPackageManager() 77 .hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE); 78 sSingletonInstance = new KeyAttestation(useStrongBox, new Injector()); 79 } 80 } 81 } 82 return sSingletonInstance; 83 } 84 85 /** 86 * For test only, return an singleton instance given the context and an injector for keyStore nd 87 * key pair generator. 88 */ 89 @VisibleForTesting getInstanceForTest(Context context, Injector injector)90 static KeyAttestation getInstanceForTest(Context context, Injector injector) { 91 if (sSingletonInstance == null) { 92 synchronized (KeyAttestation.class) { 93 if (sSingletonInstance == null) { 94 boolean useStrongBox = 95 context.getPackageManager() 96 .hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE); 97 return new KeyAttestation(useStrongBox, injector); 98 } 99 } 100 } 101 return sSingletonInstance; 102 } 103 104 /** 105 * Given a challenge, return a list of base64 encoded strings as the attestation record. The 106 * attestation is performed using a 256-bit Elliptical Curve (EC) key-pair generated by the 107 * secure keymaster. 108 */ generateAttestationRecord( final byte[] challenge, final String callingPackage)109 public List<String> generateAttestationRecord( 110 final byte[] challenge, final String callingPackage) { 111 final String keyAlias = callingPackage + "-" + ODP_KEY_ALIAS; 112 KeyPair kp = generateHybridKey(challenge, keyAlias); 113 if (kp == null) { 114 return new ArrayList<>(); 115 } 116 return getAttestationRecordFromKeyAlias(keyAlias); 117 } 118 119 @VisibleForTesting generateHybridKey(final byte[] challenge, final String keyAlias)120 KeyPair generateHybridKey(final byte[] challenge, final String keyAlias) { 121 try { 122 KeyPairGenerator keyPairGenerator = mInjector.getKeyPairGenerator(); 123 keyPairGenerator.initialize( 124 new KeyGenParameterSpec.Builder( 125 /* keystoreAlias= */ keyAlias, 126 /* purposes= */ KeyProperties.PURPOSE_SIGN) 127 .setDigests(KeyProperties.DIGEST_SHA256) 128 .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")) 129 .setAttestationChallenge(challenge) 130 // device properties are not specified when acquiring the challenge 131 .setDevicePropertiesAttestationIncluded(false) 132 .setIsStrongBoxBacked(mUseStrongBox) 133 .build()); 134 return keyPairGenerator.generateKeyPair(); 135 } catch (Exception e) { 136 LogUtil.e(TAG, e, "Failed to generate hybrid key attestation."); 137 } 138 return null; 139 } 140 141 @VisibleForTesting getAttestationRecordFromKeyAlias(String keyAlias)142 List<String> getAttestationRecordFromKeyAlias(String keyAlias) { 143 try { 144 KeyStore keyStore = mInjector.getKeyStore(); 145 keyStore.load(null); 146 Certificate[] certificateChain = keyStore.getCertificateChain(keyAlias); 147 if (certificateChain == null) { 148 return new ArrayList<>(); 149 } 150 ArrayList<String> attestationRecord = new ArrayList<>(); 151 for (Certificate certificate : certificateChain) { 152 attestationRecord.add( 153 Base64.encodeToString(certificate.getEncoded(), Base64.NO_WRAP)); 154 } 155 return attestationRecord; 156 } catch (Exception e) { 157 LogUtil.e(TAG, e, "Got exception when generate attestation record"); 158 } 159 return new ArrayList<>(); 160 } 161 } 162