diff options
Diffstat (limited to 'verifier.c')
-rw-r--r-- | verifier.c | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/verifier.c b/verifier.c new file mode 100644 index 000000000..67a4f390a --- /dev/null +++ b/verifier.c @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "common.h" +#include "verifier.h" + +#include "minzip/Zip.h" +#include "mincrypt/rsa.h" +#include "mincrypt/sha.h" + +#include <netinet/in.h> /* required for resolv.h */ +#include <resolv.h> /* for base64 codec */ +#include <string.h> + +/* Return an allocated buffer with the contents of a zip file entry. */ +static char *slurpEntry(const ZipArchive *pArchive, const ZipEntry *pEntry) { + if (!mzIsZipEntryIntact(pArchive, pEntry)) { + UnterminatedString fn = mzGetZipEntryFileName(pEntry); + LOGE("Invalid %.*s\n", fn.len, fn.str); + return NULL; + } + + int len = mzGetZipEntryUncompLen(pEntry); + char *buf = malloc(len + 1); + if (buf == NULL) { + UnterminatedString fn = mzGetZipEntryFileName(pEntry); + LOGE("Can't allocate %d bytes for %.*s\n", len, fn.len, fn.str); + return NULL; + } + + if (!mzReadZipEntry(pArchive, pEntry, buf, len)) { + UnterminatedString fn = mzGetZipEntryFileName(pEntry); + LOGE("Can't read %.*s\n", fn.len, fn.str); + free(buf); + return NULL; + } + + buf[len] = '\0'; + return buf; +} + + +struct DigestContext { + SHA_CTX digest; + unsigned *doneBytes; + unsigned totalBytes; +}; + + +/* mzProcessZipEntryContents callback to update an SHA-1 hash context. */ +static bool updateHash(const unsigned char *data, int dataLen, void *cookie) { + struct DigestContext *context = (struct DigestContext *) cookie; + SHA_update(&context->digest, data, dataLen); + if (context->doneBytes != NULL) { + *context->doneBytes += dataLen; + if (context->totalBytes > 0) { + ui_set_progress(*context->doneBytes * 1.0 / context->totalBytes); + } + } + return true; +} + + +/* Get the SHA-1 digest of a zip file entry. */ +static bool digestEntry(const ZipArchive *pArchive, const ZipEntry *pEntry, + unsigned *doneBytes, unsigned totalBytes, + uint8_t digest[SHA_DIGEST_SIZE]) { + struct DigestContext context; + SHA_init(&context.digest); + context.doneBytes = doneBytes; + context.totalBytes = totalBytes; + if (!mzProcessZipEntryContents(pArchive, pEntry, updateHash, &context)) { + UnterminatedString fn = mzGetZipEntryFileName(pEntry); + LOGE("Can't digest %.*s\n", fn.len, fn.str); + return false; + } + + memcpy(digest, SHA_final(&context.digest), SHA_DIGEST_SIZE); + +#ifdef LOG_VERBOSE + UnterminatedString fn = mzGetZipEntryFileName(pEntry); + char base64[SHA_DIGEST_SIZE * 3]; + b64_ntop(digest, SHA_DIGEST_SIZE, base64, sizeof(base64)); + LOGV("sha1(%.*s) = %s\n", fn.len, fn.str, base64); +#endif + + return true; +} + + +/* Find a /META-INF/xxx.SF signature file signed by a matching xxx.RSA file. */ +static const ZipEntry *verifySignature(const ZipArchive *pArchive, + const RSAPublicKey *pKeys, unsigned int numKeys) { + static const char prefix[] = "META-INF/"; + static const char rsa[] = ".RSA", sf[] = ".SF"; + + unsigned int i, j; + for (i = 0; i < mzZipEntryCount(pArchive); ++i) { + const ZipEntry *rsaEntry = mzGetZipEntryAt(pArchive, i); + UnterminatedString rsaName = mzGetZipEntryFileName(rsaEntry); + int rsaLen = mzGetZipEntryUncompLen(rsaEntry); + if (rsaLen >= RSANUMBYTES && rsaName.len > sizeof(prefix) && + !strncmp(rsaName.str, prefix, sizeof(prefix) - 1) && + !strncmp(rsaName.str + rsaName.len - sizeof(rsa) + 1, + rsa, sizeof(rsa) - 1)) { + char *sfName = malloc(rsaName.len - sizeof(rsa) + sizeof(sf) + 1); + if (sfName == NULL) { + LOGE("Can't allocate %d bytes for filename\n", rsaName.len); + continue; + } + + /* Replace .RSA with .SF */ + strncpy(sfName, rsaName.str, rsaName.len - sizeof(rsa) + 1); + strcpy(sfName + rsaName.len - sizeof(rsa) + 1, sf); + const ZipEntry *sfEntry = mzFindZipEntry(pArchive, sfName); + free(sfName); + + if (sfEntry == NULL) { + LOGW("Missing signature file %s\n", sfName); + continue; + } + + uint8_t sfDigest[SHA_DIGEST_SIZE]; + if (!digestEntry(pArchive, sfEntry, NULL, 0, sfDigest)) continue; + + char *rsaBuf = slurpEntry(pArchive, rsaEntry); + if (rsaBuf == NULL) continue; + + /* Try to verify the signature with all the keys. */ + uint8_t *sig = (uint8_t *) rsaBuf + rsaLen - RSANUMBYTES; + for (j = 0; j < numKeys; ++j) { + if (RSA_verify(&pKeys[j], sig, RSANUMBYTES, sfDigest)) { + free(rsaBuf); + LOGI("Verified %.*s\n", rsaName.len, rsaName.str); + return sfEntry; + } + } + + free(rsaBuf); + LOGW("Can't verify %.*s\n", rsaName.len, rsaName.str); + } + } + + LOGE("No signature (%d files)\n", mzZipEntryCount(pArchive)); + return NULL; +} + + +/* Verify /META-INF/MANIFEST.MF against the digest in a signature file. */ +static const ZipEntry *verifyManifest(const ZipArchive *pArchive, + const ZipEntry *sfEntry) { + static const char prefix[] = "SHA1-Digest-Manifest: ", eol[] = "\r\n"; + uint8_t expected[SHA_DIGEST_SIZE + 3], actual[SHA_DIGEST_SIZE]; + + char *sfBuf = slurpEntry(pArchive, sfEntry); + if (sfBuf == NULL) return NULL; + + char *line, *save; + for (line = strtok_r(sfBuf, eol, &save); line != NULL; + line = strtok_r(NULL, eol, &save)) { + if (!strncasecmp(prefix, line, sizeof(prefix) - 1)) { + UnterminatedString fn = mzGetZipEntryFileName(sfEntry); + const char *digest = line + sizeof(prefix) - 1; + int n = b64_pton(digest, expected, sizeof(expected)); + if (n != SHA_DIGEST_SIZE) { + LOGE("Invalid base64 in %.*s: %s (%d)\n", + fn.len, fn.str, digest, n); + line = NULL; + } + break; + } + } + + free(sfBuf); + + if (line == NULL) { + LOGE("No digest manifest in signature file\n"); + return false; + } + + const char *mfName = "META-INF/MANIFEST.MF"; + const ZipEntry *mfEntry = mzFindZipEntry(pArchive, mfName); + if (mfEntry == NULL) { + LOGE("No manifest file %s\n", mfName); + return NULL; + } + + if (!digestEntry(pArchive, mfEntry, NULL, 0, actual)) return NULL; + if (memcmp(expected, actual, SHA_DIGEST_SIZE)) { + UnterminatedString fn = mzGetZipEntryFileName(sfEntry); + LOGE("Wrong digest for %s in %.*s\n", mfName, fn.len, fn.str); + return NULL; + } + + LOGI("Verified %s\n", mfName); + return mfEntry; +} + + +/* Verify all the files in a Zip archive against the manifest. */ +static bool verifyArchive(const ZipArchive *pArchive, const ZipEntry *mfEntry) { + static const char namePrefix[] = "Name: "; + static const char contPrefix[] = " "; // Continuation of the filename + static const char digestPrefix[] = "SHA1-Digest: "; + static const char eol[] = "\r\n"; + + char *mfBuf = slurpEntry(pArchive, mfEntry); + if (mfBuf == NULL) return false; + + /* we're using calloc() here, so the initial state of the array is false */ + bool *unverified = (bool *) calloc(mzZipEntryCount(pArchive), sizeof(bool)); + if (unverified == NULL) { + LOGE("Can't allocate valid flags\n"); + free(mfBuf); + return false; + } + + /* Mark all the files in the archive that need to be verified. + * As we scan the manifest and check signatures, we'll unset these flags. + * At the end, we'll make sure that all the flags are unset. + */ + + unsigned i, totalBytes = 0; + for (i = 0; i < mzZipEntryCount(pArchive); ++i) { + const ZipEntry *entry = mzGetZipEntryAt(pArchive, i); + UnterminatedString fn = mzGetZipEntryFileName(entry); + int len = mzGetZipEntryUncompLen(entry); + + // Don't validate: directories, the manifest, *.RSA, and *.SF. + + if (entry == mfEntry) { + LOGV("Skipping manifest %.*s\n", fn.len, fn.str); + } else if (fn.len > 0 && fn.str[fn.len-1] == '/' && len == 0) { + LOGV("Skipping directory %.*s\n", fn.len, fn.str); + } else if (!strncasecmp(fn.str, "META-INF/", 9) && ( + !strncasecmp(fn.str + fn.len - 4, ".RSA", 4) || + !strncasecmp(fn.str + fn.len - 3, ".SF", 3))) { + LOGV("Skipping signature %.*s\n", fn.len, fn.str); + } else { + unverified[i] = true; + totalBytes += len; + } + } + + unsigned doneBytes = 0; + char *line, *save, *name = NULL; + for (line = strtok_r(mfBuf, eol, &save); line != NULL; + line = strtok_r(NULL, eol, &save)) { + if (!strncasecmp(line, namePrefix, sizeof(namePrefix) - 1)) { + // "Name:" introducing a new stanza + if (name != NULL) { + LOGE("No digest:\n %s\n", name); + break; + } + + name = strdup(line + sizeof(namePrefix) - 1); + if (name == NULL) { + LOGE("Can't copy filename in %s\n", line); + break; + } + } else if (!strncasecmp(line, contPrefix, sizeof(contPrefix) - 1)) { + // Continuing a long name (nothing else should be continued) + const char *tail = line + sizeof(contPrefix) - 1; + if (name == NULL) { + LOGE("Unexpected continuation:\n %s\n", tail); + } + + char *concat; + if (asprintf(&concat, "%s%s", name, tail) < 0) { + LOGE("Can't append continuation %s\n", tail); + break; + } + free(name); + name = concat; + } else if (!strncasecmp(line, digestPrefix, sizeof(digestPrefix) - 1)) { + // "Digest:" supplying a hash code for the current stanza + const char *base64 = line + sizeof(digestPrefix) - 1; + if (name == NULL) { + LOGE("Unexpected digest:\n %s\n", base64); + break; + } + + const ZipEntry *entry = mzFindZipEntry(pArchive, name); + if (entry == NULL) { + LOGE("Missing file:\n %s\n", name); + break; + } + if (!mzIsZipEntryIntact(pArchive, entry)) { + LOGE("Corrupt file:\n %s\n", name); + break; + } + if (!unverified[mzGetZipEntryIndex(pArchive, entry)]) { + LOGE("Unexpected file:\n %s\n", name); + break; + } + + uint8_t expected[SHA_DIGEST_SIZE + 3], actual[SHA_DIGEST_SIZE]; + int n = b64_pton(base64, expected, sizeof(expected)); + if (n != SHA_DIGEST_SIZE) { + LOGE("Invalid base64:\n %s\n %s\n", name, base64); + break; + } + + if (!digestEntry(pArchive, entry, &doneBytes, totalBytes, actual) || + memcmp(expected, actual, SHA_DIGEST_SIZE) != 0) { + LOGE("Wrong digest:\n %s\n", name); + break; + } + + LOGI("Verified %s\n", name); + unverified[mzGetZipEntryIndex(pArchive, entry)] = false; + free(name); + name = NULL; + } + } + + if (name != NULL) free(name); + free(mfBuf); + + for (i = 0; i < mzZipEntryCount(pArchive) && !unverified[i]; ++i) ; + free(unverified); + + // This means we didn't get to the end of the manifest successfully. + if (line != NULL) return false; + + if (i < mzZipEntryCount(pArchive)) { + const ZipEntry *entry = mzGetZipEntryAt(pArchive, i); + UnterminatedString fn = mzGetZipEntryFileName(entry); + LOGE("No digest for %.*s\n", fn.len, fn.str); + return false; + } + + return true; +} + + +bool verify_jar_signature(const ZipArchive *pArchive, + const RSAPublicKey *pKeys, int numKeys) { + const ZipEntry *sfEntry = verifySignature(pArchive, pKeys, numKeys); + if (sfEntry == NULL) return false; + + const ZipEntry *mfEntry = verifyManifest(pArchive, sfEntry); + if (mfEntry == NULL) return false; + + return verifyArchive(pArchive, mfEntry); +} |