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