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