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.server.uwb.secure.provisioning;
18 
19 import androidx.annotation.NonNull;
20 
21 import com.android.internal.annotations.VisibleForTesting;
22 import com.android.server.uwb.util.ObjectIdentifier;
23 
24 import com.google.common.collect.ImmutableSet;
25 
26 import org.bouncycastle.cert.X509CertificateHolder;
27 import org.bouncycastle.cms.CMSException;
28 import org.bouncycastle.cms.CMSSignedData;
29 import org.bouncycastle.cms.SignerInformation;
30 import org.bouncycastle.cms.SignerInformationStore;
31 import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
32 import org.bouncycastle.operator.OperatorCreationException;
33 import org.bouncycastle.util.Store;
34 
35 import java.io.ByteArrayInputStream;
36 import java.io.IOException;
37 import java.security.GeneralSecurityException;
38 import java.security.KeyStore;
39 import java.security.cert.CertPathBuilder;
40 import java.security.cert.CertStore;
41 import java.security.cert.CertificateException;
42 import java.security.cert.CertificateFactory;
43 import java.security.cert.CollectionCertStoreParameters;
44 import java.security.cert.PKIXBuilderParameters;
45 import java.security.cert.X509CertSelector;
46 import java.security.cert.X509Certificate;
47 import java.util.ArrayList;
48 import java.util.Collection;
49 import java.util.Iterator;
50 import java.util.List;
51 import java.util.Optional;
52 
53 import co.nstant.in.cbor.CborDecoder;
54 import co.nstant.in.cbor.CborException;
55 import co.nstant.in.cbor.model.Array;
56 import co.nstant.in.cbor.model.ByteString;
57 import co.nstant.in.cbor.model.DataItem;
58 import co.nstant.in.cbor.model.MajorType;
59 import co.nstant.in.cbor.model.UnsignedInteger;
60 
61 class ScriptParser {
62     private static final String LOG_TAG = "ScriptParser";
63 
64     private static final int VALID_DATA_ITEM_SIZE_2 = 2;
65     private static final int VALID_DATA_ITEM_SIZE_3 = 3; // contains ADF OID
66     private static final int VERSION_INDEX = 0;
67     private static final int APDUS_INDEX = 1;
68     private static final int ADF_OID_INDEX = 2;
69     private static final String FIELD_VERSION = "ver";
70     private static final String FIELD_APDUS = "APDUs";
71     private static final String FIELD_ADF_OID = "adf_oid";
72     private static final String SUB_FIELD_APDU = "APDU";
73 
ScriptParser()74     private ScriptParser() {}
75 
76     /**
77      * Verify the digital signature and parse the ADF provisioning script.
78      * @param signedScript The CMS (PKCS#7) signed data along with script encapsulated
79      * @return the ScriptContent if the signedScript is verified and parsed successfully,
80      * empty otherwise.
81      */
82     @NonNull
parseSignedScript(@onNull byte[] signedScript)83     static ScriptContent parseSignedScript(@NonNull byte[] signedScript)
84             throws ProvisioningException {
85         if (signedScript == null || signedScript.length == 0) {
86             throw new ProvisioningException("No script content.");
87         }
88         Optional<byte[]> script = verifyAndExtractScript(signedScript);
89         return parseScript(script.get());
90     }
91 
verifyAndExtractScript(byte[] signedScript)92     private static Optional<byte[]> verifyAndExtractScript(byte[] signedScript)
93             throws ProvisioningException {
94         try {
95             CMSSignedData cmsSignedData = new CMSSignedData(signedScript);
96             boolean verified = false;
97             Store certStore = cmsSignedData.getCertificates();
98             ImmutableSet<X509Certificate> untrustedCerts = getAllProvidedCerts(certStore);
99 
100             SignerInformationStore signers = cmsSignedData.getSignerInfos();
101             Iterator iterator = signers.getSigners().iterator();
102 
103             while (iterator.hasNext()) {
104                 SignerInformation signer = (SignerInformation) iterator.next();
105                 Collection<?> certCollection =
106                         (Collection<?>) certStore.getMatches(signer.getSID());
107                 if (!certCollection.isEmpty()) {
108                     X509CertificateHolder certHolder =
109                             (X509CertificateHolder) certCollection.iterator().next();
110                     CertificateFactory cFact = CertificateFactory.getInstance("X.509");
111                     X509Certificate leafCert = (X509Certificate) cFact.generateCertificate(
112                             new ByteArrayInputStream(certHolder.getEncoded()));
113                     if (!verifyCertAgainstTrustedCas(leafCert, untrustedCerts)) {
114                         // The cert is not trusted, try next signer.
115                         continue;
116                     }
117 
118                     if (signer.verify(
119                             new JcaSimpleSignerInfoVerifierBuilder()
120                                     .setProvider("BC").build(certHolder))) {
121                         verified = true;
122                         break;
123                     }
124                 }
125             }
126 
127             if (verified) {
128                 return Optional.of((byte[]) cmsSignedData.getSignedContent().getContent());
129             }
130         } catch (IOException | CMSException | OperatorCreationException | CertificateException e) {
131             throw new ProvisioningException("Invalid Input", e);
132         }
133         throw new ProvisioningException("the content cannot be trusted.");
134     }
135 
getAllProvidedCerts(@onNull Store certStore)136     private static ImmutableSet<X509Certificate> getAllProvidedCerts(@NonNull Store certStore) {
137         ImmutableSet.Builder<X509Certificate> builder = ImmutableSet.builder();
138         Collection<X509CertificateHolder> certHolders = certStore.getMatches(null);
139         for (X509CertificateHolder holder : certHolders) {
140             try {
141                 CertificateFactory cFact = CertificateFactory.getInstance("X.509");
142                 X509Certificate x509Certificate = (X509Certificate) cFact.generateCertificate(
143                         new ByteArrayInputStream(holder.getEncoded()));
144                 builder.add(x509Certificate);
145             } catch (IOException | CertificateException e) {
146                 // ignore, get next
147             }
148         }
149         return builder.build();
150     }
151 
verifyCertAgainstTrustedCas( @onNull X509Certificate leafCert, @NonNull ImmutableSet<X509Certificate> untrustedCerts)152     private static boolean verifyCertAgainstTrustedCas(
153             @NonNull X509Certificate leafCert,
154             @NonNull ImmutableSet<X509Certificate> untrustedCerts) {
155         try {
156             KeyStore ks = KeyStore.getInstance("AndroidCAStore");
157             ks.load(null, null);
158 
159             X509CertSelector selector = new X509CertSelector();
160             selector.setCertificate(leafCert);
161             CertStore certStore = CertStore.getInstance(
162                     "Collection", new CollectionCertStoreParameters(untrustedCerts));
163             PKIXBuilderParameters parameters = new PKIXBuilderParameters(ks, selector);
164             parameters.addCertStore(certStore);
165             parameters.setRevocationEnabled(false);
166             CertPathBuilder certPathBuilder = CertPathBuilder.getInstance("PKIX", "BC");
167             certPathBuilder.build(parameters);
168 
169         } catch (GeneralSecurityException | IOException e) {
170             // failed to verify the certificate.
171             return false;
172         }
173         return true;
174     }
175 
176     /**
177      * Parses the CBOR(RFC 7949) encoded provisioning script.
178      * @param cborEncodedData CBOR encoded script.
179      * @return empty if the script is not encoded as correct CBOR, otherwise return the content.
180      * @throws ProvisioningException The script is not encoded correctly.
181      */
182     @VisibleForTesting
183     @NonNull
parseScript(@onNull byte[] cborEncodedData)184     static ScriptContent parseScript(@NonNull byte[] cborEncodedData)
185             throws ProvisioningException {
186         try {
187             List<DataItem> dataItems = CborDecoder.decode(cborEncodedData);
188             if (dataItems.size() != VALID_DATA_ITEM_SIZE_2
189                     && dataItems.size() != VALID_DATA_ITEM_SIZE_3) {
190                 throw new ProvisioningException(dataItems.size() + " The script only allows "
191                         + VALID_DATA_ITEM_SIZE_2 + " or " + VALID_DATA_ITEM_SIZE_3 + "data items");
192             }
193             DataItem versionDataItem = dataItems.get(VERSION_INDEX);
194             if (!checkType(versionDataItem, MajorType.UNSIGNED_INTEGER, FIELD_VERSION)) {
195                 throw new ProvisioningException("the data type is not correct for version");
196             }
197             int version = ((UnsignedInteger) versionDataItem).getValue().intValue();
198             int minorVersion = version & 0xFF;
199             int majorVersion = version >> 8 & 0xFF;
200 
201             DataItem apdusDataItem = dataItems.get(APDUS_INDEX);
202             if (!checkType(apdusDataItem, MajorType.ARRAY, FIELD_APDUS)) {
203                 throw new ProvisioningException("the data type is not correct for APDUs");
204             }
205             List<byte[]> apdus = new ArrayList<>();
206             List<DataItem> apduDataItemList = ((Array) apdusDataItem).getDataItems();
207             for (DataItem apduDataItem : apduDataItemList) {
208                 if (!checkType(apduDataItem, MajorType.BYTE_STRING, SUB_FIELD_APDU)) {
209                     throw new ProvisioningException("the data type is not correct for APDU");
210                 }
211                 apdus.add(((ByteString) apduDataItem).getBytes());
212             }
213             Optional<ObjectIdentifier> adfOid = Optional.empty();
214             if (dataItems.size() == VALID_DATA_ITEM_SIZE_3) {
215                 DataItem adfOidDataItem = dataItems.get(ADF_OID_INDEX);
216                 if (!checkType(adfOidDataItem, MajorType.BYTE_STRING, FIELD_ADF_OID)) {
217                     throw new ProvisioningException("the data type is not correct for ADF_OID");
218                 }
219                 adfOid = Optional.of(ObjectIdentifier.fromBytes(
220                         ((ByteString) adfOidDataItem).getBytes()));
221             }
222             return new ScriptContent(majorVersion, minorVersion, apdus, adfOid);
223 
224         } catch (CborException e) {
225             throw new ProvisioningException("the script is not correct CBOR encoded.", e);
226         }
227     }
228 
checkType(DataItem item, MajorType majorType, String field)229     private static boolean checkType(DataItem item, MajorType majorType, String field) {
230         if (item.getMajorType() != majorType) {
231             logw("Wrong CBOR type for field: " + field
232                     + ". Expected " + majorType.name()
233                     + ", actual: " + item.getMajorType().name());
234             return false;
235         }
236 
237         return true;
238     }
239 
logw(String dbgMsg)240     private static void logw(String dbgMsg) {
241         android.util.Log.w(LOG_TAG, dbgMsg);
242     }
243 
244     static class ScriptContent {
245         final int mMajorVersion;
246         final int mMinorVersion;
247         final List<byte[]> mProvisioningApdus;
248         final Optional<ObjectIdentifier> mAdfOid;
249 
250         @VisibleForTesting
ScriptContent(int majorVersion, int minorVersion, List<byte[]> provisioningApdus, Optional<ObjectIdentifier> adfOid)251         ScriptContent(int majorVersion, int minorVersion,
252                 List<byte[]> provisioningApdus, Optional<ObjectIdentifier> adfOid) {
253             this.mMajorVersion = majorVersion;
254             this.mMinorVersion = minorVersion;
255             this.mProvisioningApdus = provisioningApdus;
256             this.mAdfOid = adfOid;
257         }
258     }
259 }
260