diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e8bf4f..09d59fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,8 +13,10 @@ enable_testing() project(appx) include(CheckCXXSourceCompiles) +include(FindPkgConfig) find_package(OpenSSL REQUIRED) find_package(ZLIB REQUIRED) +pkg_check_modules(LIBP11 libp11 REQUIRED) add_executable(appx Sources/APPX.cpp @@ -28,11 +30,13 @@ target_include_directories(appx PRIVATE PrivateHeaders ${OPENSSL_INCLUDE_DIR} - ${ZLIB_INCLUDE_DIRS}) + ${ZLIB_INCLUDE_DIRS} + ${LIBP11_INCLUDE_DIRS}) target_link_libraries(appx PRIVATE ${OPENSSL_LIBRARIES} - ${ZLIB_LIBRARIES}) + ${ZLIB_LIBRARIES} + ${LIBP11_LIBRARIES}) install(TARGETS appx RUNTIME DESTINATION bin) # Check for C++11 support. diff --git a/PrivateHeaders/APPX/APPX.h b/PrivateHeaders/APPX/APPX.h index d2ae3cc..209925d 100644 --- a/PrivateHeaders/APPX/APPX.h +++ b/PrivateHeaders/APPX/APPX.h @@ -16,13 +16,25 @@ namespace facebook { namespace appx { + + // A tuple containing: + // - The certificate file if signing using one + // - The OpenSC module path if signing using a smartcard + // - The smartcard slot ID containing the signing key + // - The signing key ID + // - The PIV PIN to unlock the private key + // If signing using a certificate file, the last 4 entries will be default + // initialized + using SigningParams = std::tuple; + // Creates and optionally signs an APPX file. // // fileNames maps APPX archive names to local filesystem paths. // - // certPath, if specified, causes the APPX to be signed. certPath points to - // the path to the PKCS12 certificate file containing the private signing - // key. + // certParams, if specified, contains the signing parameters, which can be + // either a path to a certificate file provided as the first string member, + // or a set of parameters to sign using a smart card, as second to fifth members // // compressionLevel indicates how much to compress individual files. // Z_DEFAULT_COMPRESSION and any value between Z_NO_COMPRESSION and @@ -30,6 +42,6 @@ namespace appx { void WriteAppx( const FilePtr &zip, const std::unordered_map &fileNames, - const std::string *certPath, int compressionLevel, bool bundle); + const SigningParams *signingParams, int compressionLevel, bool bundle); } } diff --git a/PrivateHeaders/APPX/Sign.h b/PrivateHeaders/APPX/Sign.h index 1f55cb2..672dfc7 100644 --- a/PrivateHeaders/APPX/Sign.h +++ b/PrivateHeaders/APPX/Sign.h @@ -20,8 +20,14 @@ namespace appx { // Creates a PKCS7 signature for the given APPX digest using the given // certificate. - OpenSSLPtr Sign(const std::string &certPath, - const APPXDigests &digests); + OpenSSLPtr SignFromCertFile(const std::string &certPath, + const APPXDigests &digests); + + OpenSSLPtr SignFromSmartCard(const std::string& modulePath, + uint32_t slotId, + uint8_t keyId, + const std::string& pivPin, + const APPXDigests &digests); // A set of digests required when signing APPX files. struct APPXDigests diff --git a/Sources/APPX.cpp b/Sources/APPX.cpp index e3555f2..5ac95fc 100644 --- a/Sources/APPX.cpp +++ b/Sources/APPX.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -44,7 +45,7 @@ namespace appx { // Creates the AppxSignature.p7x file and inserts it into the ZIP. template - ZIPFileEntry WriteSignature(TSink &sink, const std::string &certPath, + ZIPFileEntry WriteSignature(TSink &sink, const SigningParams &signingParams, const APPXDigests &digests, off_t offset) { // AppxSignature.p7x *must* be DEFLATEd. @@ -52,8 +53,16 @@ namespace appx { std::uint32_t crc32; off_t uncompressedSize; { - OpenSSLPtr signature = - Sign(certPath, digests); + const auto& certPath = std::get<0>( signingParams ); + OpenSSLPtr signature; + if (!certPath.empty()) + signature = SignFromCertFile(certPath, digests); + else + signature = SignFromSmartCard(std::get<1>(signingParams), + std::get<2>(signingParams), + std::get<3>(signingParams), + std::get<4>(signingParams), + digests); std::vector signatureData = GetSignatureBytes(signature.get()); @@ -87,7 +96,7 @@ namespace appx { void WriteAppx( const FilePtr &zip, const std::unordered_map &fileNames, - const std::string *certPath, int compressionLevel, bool isBundle) + const SigningParams *signingParams, int compressionLevel, bool isBundle) { FileSink zipRawSink(zip.get()); OffsetSink zipOffsetSink; @@ -156,9 +165,9 @@ namespace appx { } // Sign and write the signature. - if (certPath) { + if (signingParams) { zipFileEntries.emplace_back(WriteSignature( - zipSink, *certPath, digests, zipOffsetSink.Offset())); + zipSink, *signingParams, digests, zipOffsetSink.Offset())); } // Write the directory. diff --git a/Sources/Sign.cpp b/Sources/Sign.cpp index 9209df5..041e83f 100644 --- a/Sources/Sign.cpp +++ b/Sources/Sign.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include namespace facebook { @@ -354,90 +355,172 @@ namespace appx { return CertificateFile{std::move(privateKey), std::move(certificate)}; } + + OpenSSLPtr Sign(CertificateFile certFile, + const APPXDigests &digests) + { + // Create the signature. + OpenSSLPtr signature(PKCS7_new()); + if (!signature) { + throw OpenSSLException(); + } + if (!PKCS7_set_type(signature.get(), NID_pkcs7_signed)) { + throw OpenSSLException(); + } + PKCS7_SIGNER_INFO *signerInfo = + PKCS7_add_signature(signature.get(), certFile.certificate.get(), + certFile.privateKey.get(), EVP_sha256()); + if (!signerInfo) { + throw OpenSSLException(); + } + AddAttributes(signerInfo); + + if (!PKCS7_content_new(signature.get(), NID_pkcs7_data)) { + throw OpenSSLException(); + } + if (!PKCS7_add_certificate(signature.get(), + certFile.certificate.get())) { + throw OpenSSLException(); + } + + asn1::SPCIndirectDataContentPtr idc(asn1::SPCIndirectDataContent_new()); + MakeIndirectDataContent(*idc, digests); + EncodedASN1 idcEncoded = + EncodedASN1::FromItem(idc.get()); + + // TODO(strager): Use lower-level APIs to avoid OpenSSL injecting the + // signingTime attribute. + BIOPtr signedData(PKCS7_dataInit(signature.get(), NULL)); + if (!signedData) { + throw OpenSSLException(); + } + // Per RFC 2315 section 9.3: + // "Only the contents octets of the DER encoding of that field are + // digested, not the identifier octets or the length octets." + // Strip off the length. + if (idcEncoded.Size() < 2) { + throw std::runtime_error("NYI"); + } + if ((idcEncoded.Data()[1] & 0x80) == 0x00) { + throw std::runtime_error("NYI"); + } + std::size_t skip = 4; + if (BIO_write(signedData.get(), idcEncoded.Data() + skip, + idcEncoded.Size() - skip) != idcEncoded.Size() - skip) { + throw OpenSSLException(); + } + if (BIO_flush(signedData.get()) != 1) { + throw OpenSSLException(); + } + if (!PKCS7_dataFinal(signature.get(), signedData.get())) { + throw OpenSSLException(); + } + + // Set the content to an SpcIndirectDataContent. Must be done after + // digesting the signed data. + OpenSSLPtr content(PKCS7_new()); + if (!content) { + throw OpenSSLException(); + } + content->type = OBJ_txt2obj(oid::kSPCIndirectData, 1); + ASN1_TYPEPtr idcSequence = idcEncoded.ToSequenceType(); + content->d.other = idcSequence.get(); + if (!PKCS7_set_content(signature.get(), content.get())) { + throw OpenSSLException(); + } + content.release(); + idcSequence.release(); + + return signature; + } } - OpenSSLPtr Sign(const std::string &certPath, - const APPXDigests &digests) + OpenSSLPtr SignFromCertFile(const std::string &certPath, + const APPXDigests &digests) { OpenSSL_add_all_algorithms(); oid::Register(); CertificateFile certFile = ReadCertificateFile(certPath); + return Sign(std::move(certFile), digests); + } - // Create the signature. - OpenSSLPtr signature(PKCS7_new()); - if (!signature) { - throw OpenSSLException(); - } - if (!PKCS7_set_type(signature.get(), NID_pkcs7_signed)) { - throw OpenSSLException(); - } - PKCS7_SIGNER_INFO *signerInfo = - PKCS7_add_signature(signature.get(), certFile.certificate.get(), - certFile.privateKey.get(), EVP_sha256()); - if (!signerInfo) { + OpenSSLPtr SignFromSmartCard(const std::string& modulePath, + uint32_t slotId, + uint8_t keyId, + const std::string &pivPin, + const APPXDigests &digests) + { + OpenSSL_add_all_algorithms(); + oid::Register(); + OpenSSLPtr ctx{ PKCS11_CTX_new() }; + if (!ctx) { throw OpenSSLException(); } - AddAttributes(signerInfo); - if (!PKCS7_content_new(signature.get(), NID_pkcs7_data)) { - throw OpenSSLException(); - } - if (!PKCS7_add_certificate(signature.get(), - certFile.certificate.get())) { - throw OpenSSLException(); - } + PKCS11_CTX_load( ctx.get(), modulePath.c_str() ); + OpenSSLPtr loadedCtx{ ctx.get() }; - asn1::SPCIndirectDataContentPtr idc(asn1::SPCIndirectDataContent_new()); - MakeIndirectDataContent(*idc, digests); - EncodedASN1 idcEncoded = - EncodedASN1::FromItem(idc.get()); + PKCS11_SLOT* slots; + uint32_t nbSlots; - // TODO(strager): Use lower-level APIs to avoid OpenSSL injecting the - // signingTime attribute. - BIOPtr signedData(PKCS7_dataInit(signature.get(), NULL)); - if (!signedData) { - throw OpenSSLException(); - } - // Per RFC 2315 section 9.3: - // "Only the contents octets of the DER encoding of that field are - // digested, not the identifier octets or the length octets." - // Strip off the length. - if (idcEncoded.Size() < 2) { - throw std::runtime_error("NYI"); - } - if ((idcEncoded.Data()[1] & 0x80) == 0x00) { - throw std::runtime_error("NYI"); - } - std::size_t skip = 4; - if (BIO_write(signedData.get(), idcEncoded.Data() + skip, - idcEncoded.Size() - skip) != idcEncoded.Size() - skip) { - throw OpenSSLException(); - } - if (BIO_flush(signedData.get()) != 1) { - throw OpenSSLException(); - } - if (!PKCS7_dataFinal(signature.get(), signedData.get())) { + if (PKCS11_enumerate_slots( ctx.get(), &slots, &nbSlots )) { throw OpenSSLException(); } + std::unique_ptr> + slotsReleaser{slots, [nbSlots, c = ctx.get()](PKCS11_SLOT* slots) { + PKCS11_release_all_slots(c, slots, nbSlots); + }}; + + PKCS11_SLOT* s = nullptr; + while ((s = PKCS11_find_next_token(ctx.get(), slots, nbSlots, s))) { + PKCS11_KEY* keys; + uint32_t nbKeys; + if (PKCS11_get_slotid_from_slot(s) != slotId) + continue; + + PKCS11_CERT* certs; + uint32_t nbCerts; + + if (PKCS11_enumerate_certs( s->token, &certs, &nbCerts)) + continue; + auto cert = &certs[0]; + + // Check that there is a key that matches our request before loging in + if (PKCS11_enumerate_public_keys(s->token, &keys, &nbKeys)) + throw OpenSSLException(); + PKCS11_KEY* k = nullptr; + for (auto i = 0u; i < nbKeys; ++i) { + k = &keys[i]; + // If there is an id and it matches, this is the correct slot/id + // pair we're looking for. Now we'll need to fetch the private key + if (k->id && k->id[0] == keyId) + break; + } + if (!k) + continue; - // Set the content to an SpcIndirectDataContent. Must be done after - // digesting the signed data. - OpenSSLPtr content(PKCS7_new()); - if (!content) { - throw OpenSSLException(); - } - content->type = OBJ_txt2obj(oid::kSPCIndirectData, 1); - ASN1_TYPEPtr idcSequence = idcEncoded.ToSequenceType(); - content->d.other = idcSequence.get(); - if (!PKCS7_set_content(signature.get(), content.get())) { - throw OpenSSLException(); - } - content.release(); - idcSequence.release(); + PKCS11_login( s, 0, pivPin.c_str() ); - return signature; + OpenSSLPtr privateKey{ + PKCS11_get_private_key(k) + }; + if (!privateKey) + continue; + OpenSSLPtr certificate{ + X509_dup(cert->x509) + }; + if (!X509_check_private_key(certificate.get(), privateKey.get())) + continue; + + CertificateFile certFile{std::move(privateKey), + std::move(certificate)}; + return Sign(std::move(certFile), digests); + } + fprintf(stderr, "No usable key was found with slot %u and key id %u\n", + slotId, keyId); + return nullptr; } } } diff --git a/Sources/main.cpp b/Sources/main.cpp index b686d60..54ae267 100644 --- a/Sources/main.cpp +++ b/Sources/main.cpp @@ -256,6 +256,9 @@ void PrintUsage(const char *programName) "\n" "Options:\n" " -c pfx-file sign the APPX with the private key file\n" + " -m module-file an opensc module to use for signing\n" + " -s slot a smartcart slot id\n" + " -k key-id a smartcard key id\n" " -f map-file specify inputs from a mapping file\n" " -f - specify a mapping file through standard input\n" " -h show this usage text and exit\n" @@ -279,6 +282,10 @@ void PrintUsage(const char *programName) " [Files]\n" " \"/path/to/local/file.exe\" \"appx_file.exe\"\n" "\n" + "Signing through a smartcard can be achieved as such:\n" + "-m /usr/lib/x86_64-linux-gnu/opensc-pkcs11.so -s 1 -k 0 -p passphrase\n" + "If no passphrase is provided, APPX_PIV_PIN environment variable will be used\n" + "\n" "Supported target systems:\n" " Windows 10 (UAP)\n" " Windows 10 Mobile\n", @@ -290,10 +297,14 @@ int main(int argc, char **argv) try { const char *programName = argv[0]; const char *certPath = NULL; const char *appxPath = NULL; + const char *modulePath = NULL; + const char *pivPin = NULL; + int slotId = -1; + int keyId = -1; int compressionLevel = Z_NO_COMPRESSION; bool isBundle = false; std::unordered_map fileNames; - while (int c = getopt(argc, argv, "0123456789bc:f:ho:")) { + while (int c = getopt(argc, argv, "0123456789bc:f:ho:m:s:k:p:")) { if (c == -1) { break; } @@ -337,6 +348,18 @@ int main(int argc, char **argv) try { case 'o': appxPath = optarg; break; + case 'm': + modulePath = optarg; + break; + case 's': + slotId = atoi(optarg); + break; + case 'k': + keyId = atoi(optarg); + break; + case 'p': + pivPin = optarg; + break; case '?': fprintf(stderr, "Unknown option: %c\n", optopt); PrintUsage(programName); @@ -351,6 +374,35 @@ int main(int argc, char **argv) try { PrintUsage(programName); return 1; } + if (modulePath != nullptr && certPath != nullptr) { + fprintf(stderr, "Incompatible -c & -m options provided\n"); + return 1; + } + if (modulePath != nullptr) { + if (slotId == -1) { + fprintf(stderr, "Missing -s parameter for smartcard signing\n"); + return 1; + } + if (slotId < 0) { + fprintf(stderr, "Invalid value provided for -s parameter: %d\n", slotId); + return 1; + } + if (keyId == -1) { + fprintf(stderr, "Missing -k parameter for smartcard signing\n"); + return 1; + } + if (keyId < 0 || keyId > UINT8_MAX) { + fprintf(stderr, "Invalid value provided for -k parameter: %d\n", keyId); + return 1; + } + if (pivPin == nullptr) { + pivPin = getenv("APPX_PIV_PIN"); + if (pivPin == nullptr) { + fprintf(stderr, "No PIV passphrase provided\n"); + return 1; + } + } + } argc -= optind; argv += optind; for (char *const *i = argv; i != argv + argc; ++i) { @@ -374,10 +426,21 @@ int main(int argc, char **argv) try { fprintf(stderr, "You need to provide AppxBundleManifest.xml!\n"); return 1; } - std::string certPathString = certPath ?: ""; FilePtr appx = Open(appxPath, "wb"); - WriteAppx(appx, fileNames, certPath ? &certPathString : nullptr, - compressionLevel, isBundle); + if (certPath != nullptr || modulePath != nullptr ) + { + facebook::appx::SigningParams signingParams; + if (certPath != nullptr) + signingParams = std::make_tuple(certPath, "", 0u, 0u, ""); + else + signingParams = std::make_tuple("", modulePath, slotId, keyId, pivPin); + WriteAppx(appx, fileNames, &signingParams, compressionLevel, isBundle); + } + else + { + WriteAppx(appx, fileNames, nullptr, compressionLevel, isBundle); + } + return 0; } catch (std::exception &e) { fprintf(stderr, "%s\n", e.what());