| //======================================================================== |
| // |
| // GPGMECryptoSignBackend.cc |
| // |
| // This file is licensed under the GPLv2 or later |
| // |
| // Copyright 2023, 2024 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk> |
| //======================================================================== |
| |
| #include "CryptoSignBackend.h" |
| #include "config.h" |
| #include "GPGMECryptoSignBackend.h" |
| #include "DistinguishedNameParser.h" |
| #include <gpgme.h> |
| #include <gpgme++/key.h> |
| #include <gpgme++/gpgmepp_version.h> |
| #include <gpgme++/signingresult.h> |
| #include <gpgme++/engineinfo.h> |
| #if DUMP_SIGNATURE_DATA |
| # include <fstream> |
| #endif |
| |
| bool GpgSignatureBackend::hasSufficientVersion() |
| { |
| // gpg 2.4.0 does not support padded signatures. |
| // Most gpg signatures are padded. This is fixed for 2.4.1 |
| // gpg 2.4.0 does not support generating signatures |
| // with definite lengths. This is also fixed for 2.4.1. |
| // this has also been fixed in 2.2.42 in the 2.2 branch |
| auto version = GpgME::engineInfo(GpgME::GpgSMEngine).engineVersion(); |
| if (version > "2.4.0") { |
| return true; |
| } |
| if (version >= "2.3.0") { // development branch for 2.4 releases; no more releases here |
| return false; |
| } |
| return version >= "2.2.42"; |
| } |
| |
| /// GPGME helper methods |
| |
| // gpgme++'s string-like functions returns char pointers that can be nullptr |
| // Creating std::string from nullptr is, depending on c++ standards versions |
| // either undefined behavior or illegal, so we need a helper. |
| |
| static std::string fromCharPtr(const char *data) |
| { |
| if (data) { |
| return std::string { data }; |
| } |
| return {}; |
| } |
| |
| static bool isSuccess(const GpgME::Error &err) |
| { |
| if (err) { |
| return false; |
| } |
| if (err.isCanceled()) { |
| return false; |
| } |
| return true; |
| } |
| |
| template<typename Result> |
| static bool isValidResult(const Result &result) |
| { |
| return isSuccess(result.error()); |
| } |
| |
| template<typename Result> |
| static bool hasValidResult(const std::optional<Result> &result) |
| { |
| if (!result) { |
| return false; |
| } |
| return isValidResult(result.value()); |
| } |
| |
| static std::optional<GpgME::Signature> getSignature(const GpgME::VerificationResult &result, size_t signatureNumber) |
| { |
| if (result.numSignatures() > signatureNumber) { |
| return result.signature(signatureNumber); |
| } |
| return std::nullopt; |
| } |
| |
| static X509CertificateInfo::Validity getValidityFromSubkey(const GpgME::Subkey &key) |
| { |
| X509CertificateInfo::Validity validity; |
| validity.notBefore = key.creationTime(); |
| validity.notAfter = key.expirationTime(); |
| return validity; |
| } |
| |
| static X509CertificateInfo::EntityInfo getEntityInfoFromKey(std::string_view dnString) |
| { |
| const auto dn = DN::parseString(dnString); |
| X509CertificateInfo::EntityInfo info; |
| info.commonName = DN::FindFirstValue(dn, "CN").value_or(std::string {}); |
| info.organization = DN::FindFirstValue(dn, "O").value_or(std::string {}); |
| info.email = DN::FindFirstValue(dn, "EMAIL").value_or(std::string {}); |
| info.distinguishedName = std::string { dnString }; |
| return info; |
| } |
| |
| static std::unique_ptr<X509CertificateInfo> getCertificateInfoFromKey(const GpgME::Key &key) |
| { |
| auto certificateInfo = std::make_unique<X509CertificateInfo>(); |
| certificateInfo->setIssuerInfo(getEntityInfoFromKey(fromCharPtr(key.issuerName()))); |
| certificateInfo->setSerialNumber(GooString { DN::detail::parseHexString(fromCharPtr(key.issuerSerial())).value_or("") }); |
| auto subjectInfo = getEntityInfoFromKey(fromCharPtr(key.userID(0).id())); |
| if (subjectInfo.email.empty()) { |
| subjectInfo.email = fromCharPtr(key.userID(1).email()); |
| } |
| certificateInfo->setSubjectInfo(std::move(subjectInfo)); |
| certificateInfo->setValidity(getValidityFromSubkey(key.subkey(0))); |
| certificateInfo->setNickName(GooString(fromCharPtr(key.primaryFingerprint()))); |
| X509CertificateInfo::PublicKeyInfo pkInfo; |
| pkInfo.publicKeyStrength = key.subkey(0).length(); |
| switch (key.subkey(0).publicKeyAlgorithm()) { |
| case GpgME::Subkey::AlgoDSA: |
| pkInfo.publicKeyType = DSAKEY; |
| break; |
| case GpgME::Subkey::AlgoECC: |
| case GpgME::Subkey::AlgoECDH: |
| case GpgME::Subkey::AlgoECDSA: |
| case GpgME::Subkey::AlgoEDDSA: |
| pkInfo.publicKeyType = ECKEY; |
| break; |
| case GpgME::Subkey::AlgoRSA: |
| case GpgME::Subkey::AlgoRSA_E: |
| case GpgME::Subkey::AlgoRSA_S: |
| pkInfo.publicKeyType = RSAKEY; |
| break; |
| case GpgME::Subkey::AlgoELG: |
| case GpgME::Subkey::AlgoELG_E: |
| case GpgME::Subkey::AlgoMax: |
| case GpgME::Subkey::AlgoUnknown: |
| #if GPGMEPP_VERSION > ((1 << 16) | (24 << 8) | (0)) |
| case GpgME::Subkey::AlgoKyber: |
| #endif |
| pkInfo.publicKeyType = OTHERKEY; |
| } |
| { |
| auto ctx = GpgME::Context::create(GpgME::CMS); |
| GpgME::Data pubkeydata; |
| const auto err = ctx->exportPublicKeys(key.primaryFingerprint(), pubkeydata); |
| if (isSuccess(err)) { |
| certificateInfo->setCertificateDER(GooString(pubkeydata.toString())); |
| } |
| } |
| |
| certificateInfo->setPublicKeyInfo(std::move(pkInfo)); |
| |
| int kue = 0; |
| // this block is kind of a hack. GPGSM collapses multiple |
| // into one bit, so trying to match it back can never be good |
| if (key.canSign()) { |
| kue |= KU_NON_REPUDIATION; |
| kue |= KU_DIGITAL_SIGNATURE; |
| } |
| if (key.canEncrypt()) { |
| kue |= KU_KEY_ENCIPHERMENT; |
| kue |= KU_DATA_ENCIPHERMENT; |
| } |
| if (key.canCertify()) { |
| kue |= KU_KEY_CERT_SIGN; |
| } |
| certificateInfo->setKeyUsageExtensions(kue); |
| |
| auto subkey = key.subkey(0); |
| if (subkey.isCardKey()) { |
| certificateInfo->setKeyLocation(KeyLocation::HardwareToken); |
| } else if (subkey.isSecret()) { |
| certificateInfo->setKeyLocation(KeyLocation::Computer); |
| } |
| |
| certificateInfo->setQualified(subkey.isQualified()); |
| |
| return certificateInfo; |
| } |
| |
| /// implementation of header file |
| |
| GpgSignatureBackend::GpgSignatureBackend() |
| { |
| GpgME::initializeLibrary(); |
| } |
| |
| std::unique_ptr<CryptoSign::SigningInterface> GpgSignatureBackend::createSigningHandler(const std::string &certID, HashAlgorithm /*digestAlgTag*/) |
| { |
| return std::make_unique<GpgSignatureCreation>(certID); |
| } |
| |
| std::unique_ptr<CryptoSign::VerificationInterface> GpgSignatureBackend::createVerificationHandler(std::vector<unsigned char> &&pkcs7, CryptoSign::SignatureType type) |
| { |
| switch (type) { |
| case CryptoSign::SignatureType::unknown_signature_type: |
| case CryptoSign::SignatureType::unsigned_signature_field: |
| return {}; |
| case CryptoSign::SignatureType::ETSI_CAdES_detached: |
| case CryptoSign::SignatureType::adbe_pkcs7_detached: |
| case CryptoSign::SignatureType::adbe_pkcs7_sha1: |
| return std::make_unique<GpgSignatureVerification>(std::move(pkcs7)); |
| } |
| return {}; |
| } |
| |
| std::vector<std::unique_ptr<X509CertificateInfo>> GpgSignatureBackend::getAvailableSigningCertificates() |
| { |
| std::vector<std::unique_ptr<X509CertificateInfo>> certificates; |
| const auto context = GpgME::Context::create(GpgME::CMS); |
| auto err = context->startKeyListing(static_cast<const char *>(nullptr), true /*secretOnly*/); |
| while (isSuccess(err)) { |
| const auto key = context->nextKey(err); |
| if (!key.isNull() && isSuccess(err)) { |
| if (key.isBad()) { |
| continue; |
| } |
| if (!key.canSign()) { |
| continue; |
| } |
| certificates.push_back(getCertificateInfoFromKey(key)); |
| } else { |
| break; |
| } |
| } |
| return certificates; |
| } |
| |
| GpgSignatureCreation::GpgSignatureCreation(const std::string &certId) : gpgContext { GpgME::Context::create(GpgME::CMS) } |
| { |
| GpgME::Error error; |
| const auto signingKey = gpgContext->key(certId.c_str(), error, true); |
| if (isSuccess(error)) { |
| gpgContext->addSigningKey(signingKey); |
| key = signingKey; |
| } |
| } |
| |
| void GpgSignatureCreation::addData(unsigned char *dataBlock, int dataLen) |
| { |
| gpgData.write(dataBlock, dataLen); |
| } |
| std::variant<GooString, CryptoSign::SigningError> GpgSignatureCreation::signDetached(const std::string &password) |
| { |
| if (!key) { |
| return CryptoSign::SigningError::KeyMissing; |
| } |
| gpgData.rewind(); |
| GpgME::Data signatureData; |
| const auto signingResult = gpgContext->sign(gpgData, signatureData, GpgME::SignatureMode::Detached); |
| if (!isValidResult(signingResult)) { |
| if (signingResult.error().isCanceled()) { |
| return CryptoSign::SigningError::UserCancelled; |
| } else { |
| return CryptoSign::SigningError::GenericError; |
| } |
| } |
| |
| const auto signatureString = signatureData.toString(); |
| return GooString(std::move(signatureString)); |
| } |
| |
| CryptoSign::SignatureType GpgSignatureCreation::signatureType() const |
| { |
| return CryptoSign::SignatureType::adbe_pkcs7_detached; |
| } |
| |
| std::unique_ptr<X509CertificateInfo> GpgSignatureCreation::getCertificateInfo() const |
| { |
| if (!key) { |
| return nullptr; |
| } |
| return getCertificateInfoFromKey(*key); |
| } |
| |
| GpgSignatureVerification::GpgSignatureVerification(const std::vector<unsigned char> &p7data) : gpgContext { GpgME::Context::create(GpgME::CMS) }, signatureData(reinterpret_cast<const char *>(p7data.data()), p7data.size()) |
| { |
| gpgContext->setOffline(true); |
| signatureData.setEncoding(GpgME::Data::BinaryEncoding); |
| #if DUMP_SIGNATURE_DATA |
| static int debugFileCounter = 0; |
| debugFileCounter++; |
| std::ofstream debugSignatureData("/tmp/popplerstuff/signatureData" + std::to_string(debugFileCounter) + ".sig", std::ofstream::out | std::ofstream::trunc | std::ofstream::binary); |
| debugSignedData = std::make_unique<std::ofstream>("/tmp/popplerstuff/signedData" + std::to_string(debugFileCounter) + ".data", std::ofstream::out | std::ofstream::trunc | std::ofstream::binary); |
| debugSignatureData.write(reinterpret_cast<const char *>(p7data.data()), p7data.size()); |
| #endif |
| } |
| |
| void GpgSignatureVerification::addData(unsigned char *dataBlock, int dataLen) |
| { |
| signedData.write(dataBlock, dataLen); |
| #if DUMP_SIGNATURE_DATA |
| debugSignedData->write(reinterpret_cast<char *>(dataBlock), dataLen); |
| #endif |
| } |
| |
| std::unique_ptr<X509CertificateInfo> GpgSignatureVerification::getCertificateInfo() const |
| { |
| if (!hasValidResult(gpgResult)) { |
| return nullptr; |
| } |
| auto signature = getSignature(gpgResult.value(), 0); |
| if (!signature) { |
| return nullptr; |
| } |
| auto gpgInfo = getCertificateInfoFromKey(signature->key(true, false)); |
| return gpgInfo; |
| } |
| |
| HashAlgorithm GpgSignatureVerification::getHashAlgorithm() const |
| { |
| if (gpgResult) { |
| const auto signature = getSignature(gpgResult.value(), 0); |
| if (!signature) { |
| return HashAlgorithm::Unknown; |
| } |
| switch (signature->hashAlgorithm()) { |
| case GPGME_MD_MD5: |
| return HashAlgorithm::Md5; |
| case GPGME_MD_SHA1: |
| return HashAlgorithm::Sha1; |
| case GPGME_MD_MD2: |
| return HashAlgorithm::Md2; |
| case GPGME_MD_SHA256: |
| return HashAlgorithm::Sha256; |
| case GPGME_MD_SHA384: |
| return HashAlgorithm::Sha384; |
| case GPGME_MD_SHA512: |
| return HashAlgorithm::Sha512; |
| case GPGME_MD_SHA224: |
| return HashAlgorithm::Sha224; |
| case GPGME_MD_NONE: |
| case GPGME_MD_RMD160: |
| case GPGME_MD_TIGER: |
| case GPGME_MD_HAVAL: |
| case GPGME_MD_MD4: |
| case GPGME_MD_CRC32: |
| case GPGME_MD_CRC32_RFC1510: |
| case GPGME_MD_CRC24_RFC2440: |
| default: |
| return HashAlgorithm::Unknown; |
| } |
| } |
| return HashAlgorithm::Unknown; |
| } |
| |
| std::string GpgSignatureVerification::getSignerName() const |
| { |
| if (!hasValidResult(gpgResult)) { |
| return {}; |
| } |
| |
| const auto signature = getSignature(gpgResult.value(), 0); |
| if (!signature) { |
| return {}; |
| } |
| const auto dn = DN::parseString(fromCharPtr(signature->key(true, false).userID(0).id())); |
| return DN::FindFirstValue(dn, "CN").value_or(""); |
| } |
| |
| std::string GpgSignatureVerification::getSignerSubjectDN() const |
| { |
| if (!hasValidResult(gpgResult)) { |
| return {}; |
| } |
| const auto signature = getSignature(gpgResult.value(), 0); |
| if (!signature) { |
| return {}; |
| } |
| return fromCharPtr(signature->key(true, false).userID(0).id()); |
| } |
| |
| std::chrono::system_clock::time_point GpgSignatureVerification::getSigningTime() const |
| { |
| if (!hasValidResult(gpgResult)) { |
| return {}; |
| } |
| const auto signature = getSignature(gpgResult.value(), 0); |
| if (!signature) { |
| return {}; |
| } |
| return std::chrono::system_clock::from_time_t(signature->creationTime()); |
| } |
| |
| void GpgSignatureVerification::validateCertificateAsync(std::chrono::system_clock::time_point validation_time, bool ocspRevocationCheck, bool useAIACertFetch, const std::function<void()> &doneFunction) |
| { |
| cachedValidationStatus.reset(); |
| if (!gpgResult) { |
| validationStatus = std::async([doneFunction]() { |
| if (doneFunction) { |
| doneFunction(); |
| } |
| return CERTIFICATE_NOT_VERIFIED; |
| }); |
| return; |
| } |
| if (gpgResult->error()) { |
| validationStatus = std::async([doneFunction]() { |
| if (doneFunction) { |
| doneFunction(); |
| } |
| return CERTIFICATE_GENERIC_ERROR; |
| }); |
| return; |
| } |
| const auto signature = getSignature(gpgResult.value(), 0); |
| if (!signature) { |
| validationStatus = std::async([doneFunction]() { |
| if (doneFunction) { |
| doneFunction(); |
| } |
| return CERTIFICATE_GENERIC_ERROR; |
| }); |
| return; |
| } |
| std::string keyFP = fromCharPtr(signature->key().primaryFingerprint()); |
| validationStatus = std::async([keyFP = std::move(keyFP), doneFunction, ocspRevocationCheck, useAIACertFetch]() { |
| auto context = GpgME::Context::create(GpgME::CMS); |
| context->setOffline((!ocspRevocationCheck) || useAIACertFetch); |
| context->setKeyListMode(GpgME::KeyListMode::Local | GpgME::KeyListMode::Validate); |
| GpgME::Error e; |
| const auto key = context->key(keyFP.c_str(), e, false); |
| if (doneFunction) { |
| doneFunction(); |
| } |
| if (e.isCanceled()) { |
| return CERTIFICATE_NOT_VERIFIED; |
| } |
| if (e) { |
| return CERTIFICATE_GENERIC_ERROR; |
| } |
| if (key.isExpired()) { |
| return CERTIFICATE_EXPIRED; |
| } |
| if (key.isRevoked()) { |
| return CERTIFICATE_REVOKED; |
| } |
| if (key.isBad()) { |
| return CERTIFICATE_NOT_VERIFIED; |
| } |
| return CERTIFICATE_TRUSTED; |
| }); |
| } |
| |
| CertificateValidationStatus GpgSignatureVerification::validateCertificateResult() |
| { |
| if (cachedValidationStatus) { |
| return cachedValidationStatus.value(); |
| } |
| if (!validationStatus.valid()) { |
| return CERTIFICATE_NOT_VERIFIED; |
| } |
| validationStatus.wait(); |
| cachedValidationStatus = validationStatus.get(); |
| return cachedValidationStatus.value(); |
| } |
| |
| SignatureValidationStatus GpgSignatureVerification::validateSignature() |
| { |
| signedData.rewind(); |
| const auto result = gpgContext->verifyDetachedSignature(signatureData, signedData); |
| gpgResult = result; |
| |
| if (!isValidResult(result)) { |
| return SIGNATURE_DECODING_ERROR; |
| } |
| const auto signature = getSignature(result, 0); |
| if (!signature) { |
| return SIGNATURE_DECODING_ERROR; |
| } |
| // Ensure key is actually available |
| signature->key(true, true); |
| const auto summary = signature->summary(); |
| |
| using Summary = GpgME::Signature::Summary; |
| if (summary & Summary::Red) { |
| return SIGNATURE_INVALID; |
| } |
| if (summary & Summary::Green || summary & Summary::Valid) { |
| return SIGNATURE_VALID; |
| } |
| return SIGNATURE_GENERIC_ERROR; |
| } |