|
@@ -37,6 +37,27 @@ using std::string;
|
|
|
// 1980-01-01 00:00:00
|
|
// 1980-01-01 00:00:00
|
|
|
static const time_t dos_epoch = 315532800;
|
|
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;
|
|
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
|
|
* 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
|
|
* disk. In particular, this causes the central directory to be rewritten at
|