Browse Source

express: Add support for adding JAR-style signatures to .zip files

This is useful for writing apk/aab files in bdist_apps
rdb 4 years ago
parent
commit
6a50a657be
2 changed files with 262 additions and 0 deletions
  1. 247 0
      panda/src/express/zipArchive.cxx
  2. 15 0
      panda/src/express/zipArchive.h

+ 247 - 0
panda/src/express/zipArchive.cxx

@@ -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(&section_ctx);
+      SHA256_Update(&section_ctx, section.data(), section.size());
+      SHA256_Final(digest, &section_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

+ 15 - 0
panda/src/express/zipArchive.h

@@ -26,6 +26,11 @@
 #include "pvector.h"
 #include "pvector.h"
 #include "vector_uchar.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.
 // Defined by Cocoa, conflicts with the definition below.
 #undef verify
 #undef verify
 
 
@@ -67,6 +72,12 @@ PUBLISHED:
   std::string update_subfile(const std::string &subfile_name, const Filename &filename,
   std::string update_subfile(const std::string &subfile_name, const Filename &filename,
                              int compression_level);
                              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 flush();
   BLOCKING bool repack();
   BLOCKING bool repack();
 
 
@@ -101,6 +112,10 @@ PUBLISHED:
   INLINE const std::string &get_comment() const;
   INLINE const std::string &get_comment() const;
 
 
 public:
 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, std::string &result);
   bool read_subfile(int index, vector_uchar &result);
   bool read_subfile(int index, vector_uchar &result);