diff --git a/panda/src/express/zipArchive.cxx b/panda/src/express/zipArchive.cxx index 737eb931a2..64cc2ec497 100644 --- a/panda/src/express/zipArchive.cxx +++ b/panda/src/express/zipArchive.cxx @@ -37,6 +37,27 @@ using std::string; // 1980-01-01 00:00:00 static const time_t dos_epoch = 315532800; +#ifdef HAVE_OPENSSL +/** + * Encodes the given string using base64 encoding. + */ +static std::string base64_encode(const void *buf, int len) { + BIO *b64 = BIO_new(BIO_f_base64()); + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + + BIO *sink = BIO_new(BIO_s_mem()); + BIO_push(b64, sink); + BIO_write(b64, buf, len); + BIO_flush(b64); + + const char *encoded; + const long encoded_len = BIO_get_mem_data(sink, &encoded); + std::string result(encoded, encoded_len); + BIO_free_all(b64); + return result; +} +#endif + /** * */ @@ -412,6 +433,232 @@ update_subfile(const std::string &subfile_name, const Filename &filename, return name; } +#ifdef HAVE_OPENSSL +/** + * Adds a new JAR-style signature to the .zip file. The file must have been + * opened in read/write mode. + * + * This implicitly causes a repack() operation if one is needed. Returns true + * on success, false on failure. + * + * This flavor of add_jar_signature() reads the certificate and private key + * from a PEM-formatted file, for instance as generated by the openssl command. + * If the private key file is password-encrypted, the third parameter will be + * used as the password to decrypt it. + * + * It's possible to add multiple signatures, by providing multiple unique + * aliases. Note that aliases are considered case-insensitively and only the + * first 8 characters are considered. + * + * There is no separate parameter to pass a certificate chain. Instead, any + * necessary certificates are expected to be in the certificate file. + */ +bool ZipArchive:: +add_jar_signature(const Filename &certificate, const Filename &pkey, + const string &password, const string &alias) { + VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr(); + + // Read the certificate file from VFS. First, read the complete file into + // memory. + string certificate_data; + if (!vfs->read_file(certificate, certificate_data, true)) { + express_cat.error() + << "Could not read " << certificate << ".\n"; + return false; + } + + // Now do the same thing with the private key. This one may be password- + // encrypted on disk. + string pkey_data; + if (!vfs->read_file(pkey, pkey_data, true)) { + express_cat.error() + << "Could not read " << pkey << ".\n"; + return false; + } + + // Create an in-memory BIO to read the "file" from the buffer. + BIO *certificate_mbio = BIO_new_mem_buf((void *)certificate_data.data(), certificate_data.size()); + X509 *cert = PEM_read_bio_X509(certificate_mbio, nullptr, nullptr, (void *)""); + BIO_free(certificate_mbio); + if (cert == nullptr) { + express_cat.error() + << "Could not read certificate in " << certificate << ".\n"; + return false; + } + + // Same with private key. + BIO *pkey_mbio = BIO_new_mem_buf((void *)pkey_data.data(), pkey_data.size()); + EVP_PKEY *evp_pkey = PEM_read_bio_PrivateKey(pkey_mbio, nullptr, nullptr, + (void *)password.c_str()); + BIO_free(pkey_mbio); + if (evp_pkey == nullptr) { + express_cat.error() + << "Could not read private key in " << pkey << ".\n"; + + X509_free(cert); + return false; + } + + bool result = add_jar_signature(cert, evp_pkey, alias); + + X509_free(cert); + EVP_PKEY_free(evp_pkey); + + return result; +} +#endif // HAVE_OPENSSL + +#ifdef HAVE_OPENSSL +/** + * Adds a new JAR-style signature to the .zip file. The file must have been + * opened in read/write mode. + * + * This implicitly causes a repack() operation if one is needed. Returns true + * on success, false on failure. + * + * It's possible to add multiple signatures, by providing multiple unique + * aliases. Note that aliases are considered case-insensitively and only the + * first 8 characters are considered. + * + * The private key is expected to match the first certificate in the chain. + */ +bool ZipArchive:: +add_jar_signature(X509 *cert, EVP_PKEY *pkey, const std::string &alias) { + nassertr(is_write_valid() && is_read_valid(), false); + nassertr(cert != nullptr, false); + nassertr(pkey != nullptr, false); + + if (!X509_check_private_key(cert, pkey)) { + express_cat.error() + << "Private key does not match certificate.\n"; + return false; + } + + const char *ext; + int algo = EVP_PKEY_base_id(pkey); + switch (algo) { + case EVP_PKEY_RSA: + ext = ".RSA"; + break; + case EVP_PKEY_DSA: + ext = ".DSA"; + break; + case EVP_PKEY_EC: + ext = ".EC"; + break; + default: + express_cat.error() + << "Private key has unsupported algorithm.\n"; + return false; + } + + // Sanitize alias to be used in a filename. + std::string basename; + for (char c : alias.substr(0, 8)) { + if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || c == '-' || c == '_') { + basename += c; + } + else if (c >= 'a' && c <= 'z') { + basename += (c - 0x20); + } + else if (((uint8_t)c & 0xc0) != 0x80) { + basename += '_'; + } + } + + // Generate a MANIFEST.MF file. + const std::string header = "Manifest-Version: 1.0\r\n\r\n"; + const std::string header_digest = "VmrRqAIgAm0FCZViZFzpaP8OfDbN4iY0MyYFuzTMPv8="; + + std::stringstream manifest; + SHA256_CTX manifest_ctx; + SHA256_Init(&manifest_ctx); + + manifest << header; + SHA256_Update(&manifest_ctx, header.data(), header.size()); + + std::ostringstream sigfile_body; + + for (Subfile *subfile : _subfiles) { + nassertr(subfile != nullptr, false); + + if (subfile->_name.compare(0, 9, "META-INF/") == 0) { + continue; + } + + std::string section = "Name: " + subfile->_name + "\r\n"; + sigfile_body << section; + + // Hash the subfile. + unsigned char digest[SHA256_DIGEST_LENGTH]; + { + std::istream *stream = open_read_subfile(subfile); + + SHA256_CTX subfile_ctx; + SHA256_Init(&subfile_ctx); + + char buffer[4096]; + stream->read(buffer, sizeof(buffer)); + size_t count = stream->gcount(); + while (count > 0) { + SHA256_Update(&subfile_ctx, buffer, count); + stream->read(buffer, sizeof(buffer)); + count = stream->gcount(); + } + delete stream; + + SHA256_Final(digest, &subfile_ctx); + } + + // Encode to base64. + section += "SHA-256-Digest: " + base64_encode(digest, SHA256_DIGEST_LENGTH) + "\r\n\r\n"; + + // Encode what we just wrote to the manifest file as well. + { + unsigned char digest[SHA256_DIGEST_LENGTH]; + + SHA256_CTX section_ctx; + SHA256_Init(§ion_ctx); + SHA256_Update(§ion_ctx, section.data(), section.size()); + SHA256_Final(digest, §ion_ctx); + + sigfile_body << "SHA-256-Digest: " << base64_encode(digest, SHA256_DIGEST_LENGTH) << "\r\n\r\n"; + } + + manifest << section; + SHA256_Update(&manifest_ctx, section.data(), section.size()); + } + + // The hash for the whole manifest file goes at the beginning of the .SF file. + std::stringstream sigfile; + { + unsigned char digest[SHA256_DIGEST_LENGTH]; + SHA256_Final(digest, &manifest_ctx); + sigfile << "Signature-Version: 1.0\r\n"; + sigfile << "SHA-256-Digest-Manifest-Main-Attributes: " << header_digest << "\r\n"; + sigfile << "SHA-256-Digest-Manifest: " << base64_encode(digest, SHA256_DIGEST_LENGTH) << "\r\n\r\n"; + sigfile << sigfile_body.str(); + } + + // Sign and convert to to DER format + std::string sigfile_data = sigfile.str(); + BIO *sigfile_mbio = BIO_new_mem_buf((void *)sigfile_data.data(), sigfile_data.size()); + PKCS7 *p7 = PKCS7_sign(cert, pkey, nullptr, sigfile_mbio, PKCS7_DETACHED | PKCS7_NOATTR); + int der_len = i2d_PKCS7(p7, nullptr); + std::string signature_str(der_len, '\0'); + unsigned char *p = (unsigned char *)signature_str.data(); + i2d_PKCS7(p7, &p); + std::istringstream signature(std::move(signature_str)); + PKCS7_free(p7); + + add_subfile("META-INF/MANIFEST.MF", &manifest, 9); + add_subfile("META-INF/" + basename + ".SF", &sigfile, 9); + add_subfile("META-INF/" + basename + ext, &signature, 9); + + return true; +} +#endif // HAVE_OPENSSL + /** * Ensures that any changes made to the ZIP archive have been synchronized to * disk. In particular, this causes the central directory to be rewritten at diff --git a/panda/src/express/zipArchive.h b/panda/src/express/zipArchive.h index cc0153cf67..8cfc49ac94 100644 --- a/panda/src/express/zipArchive.h +++ b/panda/src/express/zipArchive.h @@ -26,6 +26,11 @@ #include "pvector.h" #include "vector_uchar.h" +#ifdef HAVE_OPENSSL +typedef struct x509_st X509; +typedef struct evp_pkey_st EVP_PKEY; +#endif + // Defined by Cocoa, conflicts with the definition below. #undef verify @@ -67,6 +72,12 @@ PUBLISHED: std::string update_subfile(const std::string &subfile_name, const Filename &filename, int compression_level); +#ifdef HAVE_OPENSSL + bool add_jar_signature(const Filename &certificate, const Filename &pkey, + const std::string &password = "", + const std::string &alias = "cert"); +#endif + BLOCKING bool flush(); BLOCKING bool repack(); @@ -101,6 +112,10 @@ PUBLISHED: INLINE const std::string &get_comment() const; public: +#ifdef HAVE_OPENSSL + bool add_jar_signature(X509 *cert, EVP_PKEY *pkey, const std::string &alias); +#endif // HAVE_OPENSSL + bool read_subfile(int index, std::string &result); bool read_subfile(int index, vector_uchar &result);