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.adservices.service.adselection.signature; 18 19 import android.adservices.adselection.SignedContextualAds; 20 import android.adservices.common.AdTechIdentifier; 21 import android.annotation.NonNull; 22 23 import com.android.adservices.LoggerFactory; 24 import com.android.adservices.data.encryptionkey.EncryptionKeyDao; 25 import com.android.adservices.data.enrollment.EnrollmentDao; 26 import com.android.adservices.service.encryptionkey.EncryptionKey; 27 import com.android.adservices.service.enrollment.EnrollmentData; 28 import com.android.adservices.service.stats.SignatureVerificationLogger; 29 import com.android.adservices.service.stats.SignatureVerificationStats; 30 31 import com.google.common.annotations.VisibleForTesting; 32 33 import java.util.Base64; 34 import java.util.Collections; 35 import java.util.Comparator; 36 import java.util.List; 37 import java.util.Objects; 38 import java.util.stream.Collectors; 39 40 /** 41 * Manages signature verification for Protected Audience Contextual Ads 42 * 43 * <p>See {@link android.adservices.adselection.SignedContextualAds} for more details. 44 */ 45 public class ProtectedAudienceSignatureManager { 46 private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); 47 @VisibleForTesting public static final String EMPTY_STRING_FOR_MISSING_ENROLLMENT_ID = ""; 48 49 /** 50 * This P-256 ECDSA key is used to verify signatures if {@link 51 * com.android.adservices.service.FlagsConstants #KEY_DISABLE_FLEDGE_ENROLLMENT_CHECK} is set to 52 * true. 53 * 54 * <p>This enables CTS and integration testing. 55 * 56 * <p>To test with this key, {@link SignedContextualAds} should be signed with {@link 57 * ProtectedAudienceSignatureManager#PRIVATE_TEST_KEY_STRING}. 58 */ 59 public static final String PUBLIC_TEST_KEY_STRING = 60 "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+Eyo0TOllW8as2pTTzxawQ57pXJiH16VERgHqcV1/YpADt3iq6" 61 + "9vbhwW8Ksi3M0GrxacOuge/AwiM7Uh6+V3PA=="; 62 63 /** 64 * Private key pair of the {@link ProtectedAudienceSignatureManager#PUBLIC_TEST_KEY_STRING} 65 * 66 * <p>See {@link ProtectedAudienceSignatureManager#PUBLIC_TEST_KEY_STRING} 67 */ 68 public static final String PRIVATE_TEST_KEY_STRING = 69 "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgECetqRr9eE9DKKjILR+hP66Y1niEw/bqPD/MNx" 70 + "PTMvmhRANCAAT4TKjRM6WVbxqzalNPPFrBDnulcmIfXpURGAepxXX9ikAO3eKrr29uHBbwqyLczQ" 71 + "avFpw66B78DCIztSHr5Xc8"; 72 73 @NonNull private final EnrollmentDao mEnrollmentDao; 74 @NonNull private final EncryptionKeyDao mEncryptionKeyDao; 75 @NonNull private final SignatureVerificationLogger mSignatureVerificationLogger; 76 private final boolean mIsEnrollmentCheckEnabled; 77 private final SignatureVerifier mSignatureVerifier; 78 ProtectedAudienceSignatureManager( @onNull EnrollmentDao enrollmentDao, @NonNull EncryptionKeyDao encryptionKeyDao, @NonNull SignatureVerificationLogger signatureVerificationLogger, boolean isEnrollmentCheckEnabled)79 public ProtectedAudienceSignatureManager( 80 @NonNull EnrollmentDao enrollmentDao, 81 @NonNull EncryptionKeyDao encryptionKeyDao, 82 @NonNull SignatureVerificationLogger signatureVerificationLogger, 83 boolean isEnrollmentCheckEnabled) { 84 Objects.requireNonNull(enrollmentDao); 85 Objects.requireNonNull(encryptionKeyDao); 86 Objects.requireNonNull(signatureVerificationLogger); 87 88 mEnrollmentDao = enrollmentDao; 89 mEncryptionKeyDao = encryptionKeyDao; 90 mSignatureVerificationLogger = signatureVerificationLogger; 91 mIsEnrollmentCheckEnabled = isEnrollmentCheckEnabled; 92 93 mSignatureVerifier = new ECDSASignatureVerifier(mSignatureVerificationLogger); 94 } 95 96 /** 97 * Returns whether is the given {@link SignedContextualAds} object is valid or not 98 * 99 * @param buyer Ad tech's identifier to resolve their public key 100 * @param signedContextualAds contextual ads object to verify 101 * @return true if the object is valid else false 102 */ isVerified( @onNull AdTechIdentifier buyer, @NonNull AdTechIdentifier seller, @NonNull String callerPackageName, @NonNull SignedContextualAds signedContextualAds)103 public boolean isVerified( 104 @NonNull AdTechIdentifier buyer, 105 @NonNull AdTechIdentifier seller, 106 @NonNull String callerPackageName, 107 @NonNull SignedContextualAds signedContextualAds) { 108 Objects.requireNonNull(buyer); 109 Objects.requireNonNull(seller); 110 Objects.requireNonNull(callerPackageName); 111 Objects.requireNonNull(signedContextualAds); 112 113 try { 114 List<byte[]> publicKeys = fetchPublicKeyForAdTech(buyer); 115 sLogger.v("Received %s keys", publicKeys.size()); 116 117 byte[] serialized = serializeSignedContextualAds(signedContextualAds); 118 sLogger.v("Serialized contextual ads object"); 119 120 logStartSignatureVerification(); 121 for (byte[] publicKey : publicKeys) { 122 if (mSignatureVerifier.verify( 123 publicKey, serialized, signedContextualAds.getSignature())) { 124 sLogger.v( 125 "Signature is verified with key: '%s'.", 126 Base64.getEncoder().encodeToString(publicKey)); 127 endSuccessfulSigningProcess(); 128 return true; 129 } else { 130 logKeyFailedSignatureVerification(); 131 } 132 sLogger.v( 133 "Key '%s' didn't verify the signature. Trying the next key...", 134 Base64.getEncoder().encodeToString(publicKey)); 135 } 136 sLogger.v("All keys are exhausted and signature is not verified!"); 137 endFailedSigningProcess(buyer, seller, callerPackageName); 138 return false; 139 } catch (Exception e) { 140 sLogger.v("Unknown error during signature verification: %s", e); 141 boolean isUnknownError = true; 142 endFailedSigningProcess(buyer, seller, callerPackageName, isUnknownError); 143 return false; 144 } 145 } 146 147 @VisibleForTesting fetchPublicKeyForAdTech(AdTechIdentifier adTech)148 List<byte[]> fetchPublicKeyForAdTech(AdTechIdentifier adTech) { 149 logStartKeyFetch(); 150 Base64.Decoder decoder = Base64.getDecoder(); 151 if (!mIsEnrollmentCheckEnabled) { 152 sLogger.v("Enrollment check is disabled, returning the default key"); 153 List<byte[]> toReturn = 154 Collections.singletonList(decoder.decode(PUBLIC_TEST_KEY_STRING)); 155 logSuccessfulKeyFetch(toReturn.size()); 156 return toReturn; 157 } 158 159 try { 160 sLogger.v("Fetching EnrollmentData for %s", adTech); 161 EnrollmentData enrollmentData = 162 mEnrollmentDao.getEnrollmentDataForFledgeByAdTechIdentifier(adTech); 163 164 if (enrollmentData == null || enrollmentData.getEnrollmentId() == null) { 165 sLogger.v("Enrollment data or id is not found for ad tech: %s", adTech); 166 logNoEnrollmentDataForBuyer(); 167 logFailedKeyFetch(); 168 return Collections.emptyList(); 169 } 170 171 sLogger.v("Fetching signature keys for %s", enrollmentData.getEnrollmentId()); 172 List<EncryptionKey> encryptionKeys = 173 mEncryptionKeyDao.getEncryptionKeyFromEnrollmentIdAndKeyType( 174 enrollmentData.getEnrollmentId(), EncryptionKey.KeyType.SIGNING); 175 176 sLogger.v("Received %s signing key(s)", encryptionKeys.size()); 177 List<byte[]> publicKeys = 178 encryptionKeys.stream() 179 .sorted(Comparator.comparingLong(EncryptionKey::getExpiration)) 180 .map(key -> decoder.decode(key.getBody())) 181 .collect(Collectors.toList()); 182 logSuccessfulKeyFetch(publicKeys.size()); 183 return publicKeys; 184 } catch (Exception e) { 185 logFailedKeyFetch(); 186 sLogger.e(e, "Unknown error during key fetch for signature verification"); 187 throw e; 188 } 189 } 190 serializeSignedContextualAds(SignedContextualAds signedContextualAds)191 private byte[] serializeSignedContextualAds(SignedContextualAds signedContextualAds) { 192 logStartSerialization(); 193 byte[] toReturn = new SignedContextualAdsHashUtil().serialize(signedContextualAds); 194 logEndSerialization(); 195 return toReturn; 196 } 197 logStartKeyFetch()198 private void logStartKeyFetch() { 199 mSignatureVerificationLogger.startKeyFetchForSignatureVerification(); 200 } 201 logFailedKeyFetch()202 private void logFailedKeyFetch() { 203 mSignatureVerificationLogger.endKeyFetchForSignatureVerification(); 204 mSignatureVerificationLogger.setNumOfKeysFetched(0); 205 } 206 logStartSerialization()207 private void logStartSerialization() { 208 mSignatureVerificationLogger.startSerializationForSignatureVerification(); 209 } 210 logEndSerialization()211 private void logEndSerialization() { 212 mSignatureVerificationLogger.endSerializationForSignatureVerification(); 213 } 214 logSuccessfulKeyFetch(int numOfKeysFetched)215 private void logSuccessfulKeyFetch(int numOfKeysFetched) { 216 mSignatureVerificationLogger.endKeyFetchForSignatureVerification(); 217 mSignatureVerificationLogger.setNumOfKeysFetched(numOfKeysFetched); 218 if (numOfKeysFetched == 0) { 219 mSignatureVerificationLogger.setFailureDetailNoKeysFetchedForBuyer(); 220 } 221 } 222 logStartSignatureVerification()223 private void logStartSignatureVerification() { 224 mSignatureVerificationLogger.startSignatureVerification(); 225 } 226 logKeyFailedSignatureVerification()227 private void logKeyFailedSignatureVerification() { 228 mSignatureVerificationLogger.addFailureDetailCountOfKeysFailedToVerifySignature(); 229 } 230 logNoEnrollmentDataForBuyer()231 private void logNoEnrollmentDataForBuyer() { 232 mSignatureVerificationLogger.setFailureDetailNoEnrollmentDataForBuyer(); 233 } 234 endFailedSigningProcess( AdTechIdentifier buyer, AdTechIdentifier seller, String callerPackageName)235 private void endFailedSigningProcess( 236 AdTechIdentifier buyer, AdTechIdentifier seller, String callerPackageName) { 237 boolean isKnownError = false; 238 endFailedSigningProcess(buyer, seller, callerPackageName, isKnownError); 239 } 240 endFailedSigningProcess( AdTechIdentifier buyer, AdTechIdentifier seller, String callerPackageName, boolean isUnknownError)241 private void endFailedSigningProcess( 242 AdTechIdentifier buyer, 243 AdTechIdentifier seller, 244 String callerPackageName, 245 boolean isUnknownError) { 246 mSignatureVerificationLogger.endSignatureVerification(); 247 mSignatureVerificationLogger.setFailedSignatureBuyerEnrollmentId( 248 resolveEnrollmentIdFromAdTechIdentifier(buyer)); 249 mSignatureVerificationLogger.setFailedSignatureSellerEnrollmentId( 250 resolveEnrollmentIdFromAdTechIdentifier(seller)); 251 mSignatureVerificationLogger.setFailedSignatureCallerPackageName(callerPackageName); 252 if (isUnknownError) { 253 mSignatureVerificationLogger.setFailureDetailUnknownError(); 254 } 255 mSignatureVerificationLogger.close( 256 SignatureVerificationStats.VerificationStatus.VERIFICATION_FAILED.getValue()); 257 } 258 endSuccessfulSigningProcess()259 private void endSuccessfulSigningProcess() { 260 mSignatureVerificationLogger.endSignatureVerification(); 261 mSignatureVerificationLogger.close( 262 SignatureVerificationStats.VerificationStatus.VERIFIED.getValue()); 263 } 264 resolveEnrollmentIdFromAdTechIdentifier(AdTechIdentifier adTechIdentifier)265 private String resolveEnrollmentIdFromAdTechIdentifier(AdTechIdentifier adTechIdentifier) { 266 EnrollmentData enrollmentData = 267 mEnrollmentDao.getEnrollmentDataForFledgeByAdTechIdentifier(adTechIdentifier); 268 if (Objects.isNull(enrollmentData) || Objects.isNull(enrollmentData.getEnrollmentId())) { 269 return EMPTY_STRING_FOR_MISSING_ENROLLMENT_ID; 270 } 271 return enrollmentData.getEnrollmentId(); 272 } 273 } 274