The Android Open Source Project | c24a8e6 | 2009-03-03 19:28:42 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2008 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 | #include "common.h" |
| 18 | #include "verifier.h" |
| 19 | |
| 20 | #include "minzip/Zip.h" |
| 21 | #include "mincrypt/rsa.h" |
| 22 | #include "mincrypt/sha.h" |
| 23 | |
| 24 | #include <netinet/in.h> /* required for resolv.h */ |
| 25 | #include <resolv.h> /* for base64 codec */ |
| 26 | #include <string.h> |
| 27 | |
| 28 | /* Return an allocated buffer with the contents of a zip file entry. */ |
| 29 | static char *slurpEntry(const ZipArchive *pArchive, const ZipEntry *pEntry) { |
| 30 | if (!mzIsZipEntryIntact(pArchive, pEntry)) { |
| 31 | UnterminatedString fn = mzGetZipEntryFileName(pEntry); |
| 32 | LOGE("Invalid %.*s\n", fn.len, fn.str); |
| 33 | return NULL; |
| 34 | } |
| 35 | |
| 36 | int len = mzGetZipEntryUncompLen(pEntry); |
| 37 | char *buf = malloc(len + 1); |
| 38 | if (buf == NULL) { |
| 39 | UnterminatedString fn = mzGetZipEntryFileName(pEntry); |
| 40 | LOGE("Can't allocate %d bytes for %.*s\n", len, fn.len, fn.str); |
| 41 | return NULL; |
| 42 | } |
| 43 | |
| 44 | if (!mzReadZipEntry(pArchive, pEntry, buf, len)) { |
| 45 | UnterminatedString fn = mzGetZipEntryFileName(pEntry); |
| 46 | LOGE("Can't read %.*s\n", fn.len, fn.str); |
| 47 | free(buf); |
| 48 | return NULL; |
| 49 | } |
| 50 | |
| 51 | buf[len] = '\0'; |
| 52 | return buf; |
| 53 | } |
| 54 | |
| 55 | |
| 56 | struct DigestContext { |
| 57 | SHA_CTX digest; |
| 58 | unsigned *doneBytes; |
| 59 | unsigned totalBytes; |
| 60 | }; |
| 61 | |
| 62 | |
| 63 | /* mzProcessZipEntryContents callback to update an SHA-1 hash context. */ |
| 64 | static bool updateHash(const unsigned char *data, int dataLen, void *cookie) { |
| 65 | struct DigestContext *context = (struct DigestContext *) cookie; |
| 66 | SHA_update(&context->digest, data, dataLen); |
| 67 | if (context->doneBytes != NULL) { |
| 68 | *context->doneBytes += dataLen; |
| 69 | if (context->totalBytes > 0) { |
| 70 | ui_set_progress(*context->doneBytes * 1.0 / context->totalBytes); |
| 71 | } |
| 72 | } |
| 73 | return true; |
| 74 | } |
| 75 | |
| 76 | |
| 77 | /* Get the SHA-1 digest of a zip file entry. */ |
| 78 | static bool digestEntry(const ZipArchive *pArchive, const ZipEntry *pEntry, |
| 79 | unsigned *doneBytes, unsigned totalBytes, |
| 80 | uint8_t digest[SHA_DIGEST_SIZE]) { |
| 81 | struct DigestContext context; |
| 82 | SHA_init(&context.digest); |
| 83 | context.doneBytes = doneBytes; |
| 84 | context.totalBytes = totalBytes; |
| 85 | if (!mzProcessZipEntryContents(pArchive, pEntry, updateHash, &context)) { |
| 86 | UnterminatedString fn = mzGetZipEntryFileName(pEntry); |
| 87 | LOGE("Can't digest %.*s\n", fn.len, fn.str); |
| 88 | return false; |
| 89 | } |
| 90 | |
| 91 | memcpy(digest, SHA_final(&context.digest), SHA_DIGEST_SIZE); |
| 92 | |
| 93 | #ifdef LOG_VERBOSE |
| 94 | UnterminatedString fn = mzGetZipEntryFileName(pEntry); |
| 95 | char base64[SHA_DIGEST_SIZE * 3]; |
| 96 | b64_ntop(digest, SHA_DIGEST_SIZE, base64, sizeof(base64)); |
| 97 | LOGV("sha1(%.*s) = %s\n", fn.len, fn.str, base64); |
| 98 | #endif |
| 99 | |
| 100 | return true; |
| 101 | } |
| 102 | |
| 103 | |
| 104 | /* Find a /META-INF/xxx.SF signature file signed by a matching xxx.RSA file. */ |
| 105 | static const ZipEntry *verifySignature(const ZipArchive *pArchive, |
| 106 | const RSAPublicKey *pKeys, unsigned int numKeys) { |
| 107 | static const char prefix[] = "META-INF/"; |
| 108 | static const char rsa[] = ".RSA", sf[] = ".SF"; |
| 109 | |
| 110 | unsigned int i, j; |
| 111 | for (i = 0; i < mzZipEntryCount(pArchive); ++i) { |
| 112 | const ZipEntry *rsaEntry = mzGetZipEntryAt(pArchive, i); |
| 113 | UnterminatedString rsaName = mzGetZipEntryFileName(rsaEntry); |
| 114 | int rsaLen = mzGetZipEntryUncompLen(rsaEntry); |
| 115 | if (rsaLen >= RSANUMBYTES && rsaName.len > sizeof(prefix) && |
| 116 | !strncmp(rsaName.str, prefix, sizeof(prefix) - 1) && |
| 117 | !strncmp(rsaName.str + rsaName.len - sizeof(rsa) + 1, |
| 118 | rsa, sizeof(rsa) - 1)) { |
| 119 | char *sfName = malloc(rsaName.len - sizeof(rsa) + sizeof(sf) + 1); |
| 120 | if (sfName == NULL) { |
| 121 | LOGE("Can't allocate %d bytes for filename\n", rsaName.len); |
| 122 | continue; |
| 123 | } |
| 124 | |
| 125 | /* Replace .RSA with .SF */ |
| 126 | strncpy(sfName, rsaName.str, rsaName.len - sizeof(rsa) + 1); |
| 127 | strcpy(sfName + rsaName.len - sizeof(rsa) + 1, sf); |
| 128 | const ZipEntry *sfEntry = mzFindZipEntry(pArchive, sfName); |
| 129 | |
| 130 | if (sfEntry == NULL) { |
| 131 | LOGW("Missing signature file %s\n", sfName); |
| 132 | free(sfName); |
| 133 | continue; |
| 134 | } |
| 135 | |
| 136 | free(sfName); |
| 137 | |
| 138 | uint8_t sfDigest[SHA_DIGEST_SIZE]; |
| 139 | if (!digestEntry(pArchive, sfEntry, NULL, 0, sfDigest)) continue; |
| 140 | |
| 141 | char *rsaBuf = slurpEntry(pArchive, rsaEntry); |
| 142 | if (rsaBuf == NULL) continue; |
| 143 | |
| 144 | /* Try to verify the signature with all the keys. */ |
| 145 | uint8_t *sig = (uint8_t *) rsaBuf + rsaLen - RSANUMBYTES; |
| 146 | for (j = 0; j < numKeys; ++j) { |
| 147 | if (RSA_verify(&pKeys[j], sig, RSANUMBYTES, sfDigest)) { |
| 148 | free(rsaBuf); |
| 149 | LOGI("Verified %.*s\n", rsaName.len, rsaName.str); |
| 150 | return sfEntry; |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | free(rsaBuf); |
| 155 | LOGW("Can't verify %.*s\n", rsaName.len, rsaName.str); |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | LOGE("No signature (%d files)\n", mzZipEntryCount(pArchive)); |
| 160 | return NULL; |
| 161 | } |
| 162 | |
| 163 | |
| 164 | /* Verify /META-INF/MANIFEST.MF against the digest in a signature file. */ |
| 165 | static const ZipEntry *verifyManifest(const ZipArchive *pArchive, |
| 166 | const ZipEntry *sfEntry) { |
| 167 | static const char prefix[] = "SHA1-Digest-Manifest: ", eol[] = "\r\n"; |
| 168 | uint8_t expected[SHA_DIGEST_SIZE + 3], actual[SHA_DIGEST_SIZE]; |
| 169 | |
| 170 | char *sfBuf = slurpEntry(pArchive, sfEntry); |
| 171 | if (sfBuf == NULL) return NULL; |
| 172 | |
| 173 | char *line, *save; |
| 174 | for (line = strtok_r(sfBuf, eol, &save); line != NULL; |
| 175 | line = strtok_r(NULL, eol, &save)) { |
| 176 | if (!strncasecmp(prefix, line, sizeof(prefix) - 1)) { |
| 177 | UnterminatedString fn = mzGetZipEntryFileName(sfEntry); |
| 178 | const char *digest = line + sizeof(prefix) - 1; |
| 179 | int n = b64_pton(digest, expected, sizeof(expected)); |
| 180 | if (n != SHA_DIGEST_SIZE) { |
| 181 | LOGE("Invalid base64 in %.*s: %s (%d)\n", |
| 182 | fn.len, fn.str, digest, n); |
| 183 | line = NULL; |
| 184 | } |
| 185 | break; |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | free(sfBuf); |
| 190 | |
| 191 | if (line == NULL) { |
| 192 | LOGE("No digest manifest in signature file\n"); |
| 193 | return false; |
| 194 | } |
| 195 | |
| 196 | const char *mfName = "META-INF/MANIFEST.MF"; |
| 197 | const ZipEntry *mfEntry = mzFindZipEntry(pArchive, mfName); |
| 198 | if (mfEntry == NULL) { |
| 199 | LOGE("No manifest file %s\n", mfName); |
| 200 | return NULL; |
| 201 | } |
| 202 | |
| 203 | if (!digestEntry(pArchive, mfEntry, NULL, 0, actual)) return NULL; |
| 204 | if (memcmp(expected, actual, SHA_DIGEST_SIZE)) { |
| 205 | UnterminatedString fn = mzGetZipEntryFileName(sfEntry); |
| 206 | LOGE("Wrong digest for %s in %.*s\n", mfName, fn.len, fn.str); |
| 207 | return NULL; |
| 208 | } |
| 209 | |
| 210 | LOGI("Verified %s\n", mfName); |
| 211 | return mfEntry; |
| 212 | } |
| 213 | |
| 214 | |
| 215 | /* Verify all the files in a Zip archive against the manifest. */ |
| 216 | static bool verifyArchive(const ZipArchive *pArchive, const ZipEntry *mfEntry) { |
| 217 | static const char namePrefix[] = "Name: "; |
| 218 | static const char contPrefix[] = " "; // Continuation of the filename |
| 219 | static const char digestPrefix[] = "SHA1-Digest: "; |
| 220 | static const char eol[] = "\r\n"; |
| 221 | |
| 222 | char *mfBuf = slurpEntry(pArchive, mfEntry); |
| 223 | if (mfBuf == NULL) return false; |
| 224 | |
| 225 | /* we're using calloc() here, so the initial state of the array is false */ |
| 226 | bool *unverified = (bool *) calloc(mzZipEntryCount(pArchive), sizeof(bool)); |
| 227 | if (unverified == NULL) { |
| 228 | LOGE("Can't allocate valid flags\n"); |
| 229 | free(mfBuf); |
| 230 | return false; |
| 231 | } |
| 232 | |
| 233 | /* Mark all the files in the archive that need to be verified. |
| 234 | * As we scan the manifest and check signatures, we'll unset these flags. |
| 235 | * At the end, we'll make sure that all the flags are unset. |
| 236 | */ |
| 237 | |
| 238 | unsigned i, totalBytes = 0; |
| 239 | for (i = 0; i < mzZipEntryCount(pArchive); ++i) { |
| 240 | const ZipEntry *entry = mzGetZipEntryAt(pArchive, i); |
| 241 | UnterminatedString fn = mzGetZipEntryFileName(entry); |
| 242 | int len = mzGetZipEntryUncompLen(entry); |
| 243 | |
| 244 | // Don't validate: directories, the manifest, *.RSA, and *.SF. |
| 245 | |
| 246 | if (entry == mfEntry) { |
| 247 | LOGV("Skipping manifest %.*s\n", fn.len, fn.str); |
| 248 | } else if (fn.len > 0 && fn.str[fn.len-1] == '/' && len == 0) { |
| 249 | LOGV("Skipping directory %.*s\n", fn.len, fn.str); |
| 250 | } else if (!strncasecmp(fn.str, "META-INF/", 9) && ( |
| 251 | !strncasecmp(fn.str + fn.len - 4, ".RSA", 4) || |
| 252 | !strncasecmp(fn.str + fn.len - 3, ".SF", 3))) { |
| 253 | LOGV("Skipping signature %.*s\n", fn.len, fn.str); |
| 254 | } else { |
| 255 | unverified[i] = true; |
| 256 | totalBytes += len; |
| 257 | } |
| 258 | } |
| 259 | |
| 260 | unsigned doneBytes = 0; |
| 261 | char *line, *save, *name = NULL; |
| 262 | for (line = strtok_r(mfBuf, eol, &save); line != NULL; |
| 263 | line = strtok_r(NULL, eol, &save)) { |
| 264 | if (!strncasecmp(line, namePrefix, sizeof(namePrefix) - 1)) { |
| 265 | // "Name:" introducing a new stanza |
| 266 | if (name != NULL) { |
| 267 | LOGE("No digest:\n %s\n", name); |
| 268 | break; |
| 269 | } |
| 270 | |
| 271 | name = strdup(line + sizeof(namePrefix) - 1); |
| 272 | if (name == NULL) { |
| 273 | LOGE("Can't copy filename in %s\n", line); |
| 274 | break; |
| 275 | } |
| 276 | } else if (!strncasecmp(line, contPrefix, sizeof(contPrefix) - 1)) { |
| 277 | // Continuing a long name (nothing else should be continued) |
| 278 | const char *tail = line + sizeof(contPrefix) - 1; |
| 279 | if (name == NULL) { |
| 280 | LOGE("Unexpected continuation:\n %s\n", tail); |
| 281 | } |
| 282 | |
| 283 | char *concat; |
| 284 | if (asprintf(&concat, "%s%s", name, tail) < 0) { |
| 285 | LOGE("Can't append continuation %s\n", tail); |
| 286 | break; |
| 287 | } |
| 288 | free(name); |
| 289 | name = concat; |
| 290 | } else if (!strncasecmp(line, digestPrefix, sizeof(digestPrefix) - 1)) { |
| 291 | // "Digest:" supplying a hash code for the current stanza |
| 292 | const char *base64 = line + sizeof(digestPrefix) - 1; |
| 293 | if (name == NULL) { |
| 294 | LOGE("Unexpected digest:\n %s\n", base64); |
| 295 | break; |
| 296 | } |
| 297 | |
| 298 | const ZipEntry *entry = mzFindZipEntry(pArchive, name); |
| 299 | if (entry == NULL) { |
| 300 | LOGE("Missing file:\n %s\n", name); |
| 301 | break; |
| 302 | } |
| 303 | if (!mzIsZipEntryIntact(pArchive, entry)) { |
| 304 | LOGE("Corrupt file:\n %s\n", name); |
| 305 | break; |
| 306 | } |
| 307 | if (!unverified[mzGetZipEntryIndex(pArchive, entry)]) { |
| 308 | LOGE("Unexpected file:\n %s\n", name); |
| 309 | break; |
| 310 | } |
| 311 | |
| 312 | uint8_t expected[SHA_DIGEST_SIZE + 3], actual[SHA_DIGEST_SIZE]; |
| 313 | int n = b64_pton(base64, expected, sizeof(expected)); |
| 314 | if (n != SHA_DIGEST_SIZE) { |
| 315 | LOGE("Invalid base64:\n %s\n %s\n", name, base64); |
| 316 | break; |
| 317 | } |
| 318 | |
| 319 | if (!digestEntry(pArchive, entry, &doneBytes, totalBytes, actual) || |
| 320 | memcmp(expected, actual, SHA_DIGEST_SIZE) != 0) { |
| 321 | LOGE("Wrong digest:\n %s\n", name); |
| 322 | break; |
| 323 | } |
| 324 | |
| 325 | LOGI("Verified %s\n", name); |
| 326 | unverified[mzGetZipEntryIndex(pArchive, entry)] = false; |
| 327 | free(name); |
| 328 | name = NULL; |
| 329 | } |
| 330 | } |
| 331 | |
| 332 | if (name != NULL) free(name); |
| 333 | free(mfBuf); |
| 334 | |
| 335 | for (i = 0; i < mzZipEntryCount(pArchive) && !unverified[i]; ++i) ; |
| 336 | free(unverified); |
| 337 | |
| 338 | // This means we didn't get to the end of the manifest successfully. |
| 339 | if (line != NULL) return false; |
| 340 | |
| 341 | if (i < mzZipEntryCount(pArchive)) { |
| 342 | const ZipEntry *entry = mzGetZipEntryAt(pArchive, i); |
| 343 | UnterminatedString fn = mzGetZipEntryFileName(entry); |
| 344 | LOGE("No digest for %.*s\n", fn.len, fn.str); |
| 345 | return false; |
| 346 | } |
| 347 | |
| 348 | return true; |
| 349 | } |
| 350 | |
| 351 | |
| 352 | bool verify_jar_signature(const ZipArchive *pArchive, |
| 353 | const RSAPublicKey *pKeys, int numKeys) { |
| 354 | const ZipEntry *sfEntry = verifySignature(pArchive, pKeys, numKeys); |
| 355 | if (sfEntry == NULL) return false; |
| 356 | |
| 357 | const ZipEntry *mfEntry = verifyManifest(pArchive, sfEntry); |
| 358 | if (mfEntry == NULL) return false; |
| 359 | |
| 360 | return verifyArchive(pArchive, mfEntry); |
| 361 | } |