#include "eetv4signer.h" #include "eetv4soap.h" #include #include #include #include #include #include #if OPENSSL_VERSION_NUMBER >= 0x30000000L #include #endif #include #include #include #include #include #include #include #include #include namespace { constexpr auto SoapNamespace = "http://schemas.xmlsoap.org/soap/envelope/"; constexpr auto XmlDsigNamespace = "http://www.w3.org/2000/09/xmldsig#"; constexpr auto WsseNamespace = "http://docs.oasis-open.org/wss/2004/01/" "oasis-200401-wss-wssecurity-secext-1.0.xsd"; constexpr auto WsuNamespace = "http://docs.oasis-open.org/wss/2004/01/" "oasis-200401-wss-wssecurity-utility-1.0.xsd"; constexpr auto X509ValueType = "http://docs.oasis-open.org/wss/2004/01/" "oasis-200401-wss-x509-token-profile-1.0#X509v3"; constexpr auto Base64Encoding = "http://docs.oasis-open.org/wss/2004/01/" "oasis-200401-wss-soap-message-security-1.0#Base64Binary"; constexpr auto BodyId = "Id"; constexpr auto CertificateId = "eet-certificate"; bool initializeXmlSec() { OSSL_PROVIDER_load(nullptr, "default"); return xmlSecCheckVersion() == 1 && xmlSecInit() >= 0 && xmlSecOpenSSLAppInit(nullptr) >= 0 && xmlSecCryptoInit() >= 0; } bool ensureXmlSecInitialized() { static std::once_flag flag; static bool initialized = false; std::call_once(flag, [] { initialized = initializeXmlSec(); }); return initialized; } void ensureOpenSslProvider() { static std::once_flag flag; std::call_once(flag, [] { OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CONFIG, nullptr); #if OPENSSL_VERSION_NUMBER >= 0x30000000L OSSL_PROVIDER_load(nullptr, "default"); OSSL_PROVIDER_load(nullptr, "legacy"); #endif }); } void setError(QString *error, const QString &message) { if (error != nullptr) { *error = message; } } QByteArray certificateDer(const QByteArray &pkcs12, const QByteArray &password, QString *error) { BIO *bio = BIO_new_mem_buf(pkcs12.constData(), pkcs12.size()); if (bio == nullptr) { setError(error, QStringLiteral("Cannot create a PKCS#12 memory buffer.")); return {}; } PKCS12 *container = d2i_PKCS12_bio(bio, nullptr); BIO_free(bio); if (container == nullptr) { setError(error, QStringLiteral("The PKCS#12 file is invalid.")); return {}; } EVP_PKEY *privateKey = nullptr; X509 *certificate = nullptr; STACK_OF(X509) *caCertificates = nullptr; const bool parsed = PKCS12_parse(container, password.constData(), &privateKey, &certificate, &caCertificates) == 1; PKCS12_free(container); EVP_PKEY_free(privateKey); sk_X509_pop_free(caCertificates, X509_free); if (!parsed || certificate == nullptr) { X509_free(certificate); char buffer[256] = {}; const unsigned long reason = ERR_get_error(); if (reason != 0) { ERR_error_string_n(reason, buffer, sizeof(buffer)); } setError(error, QStringLiteral("The PKCS#12 file cannot be opened with the supplied password (%1).") .arg(QString::fromLatin1(buffer))); return {}; } const int length = i2d_X509(certificate, nullptr); QByteArray der; if (length > 0) { der.resize(length); unsigned char *destination = reinterpret_cast(der.data()); if (i2d_X509(certificate, &destination) != length) { der.clear(); } } X509_free(certificate); if (der.isEmpty()) { setError(error, QStringLiteral("The certificate could not be encoded.")); } return der; } xmlNodePtr findChild(xmlNodePtr parent, const char *name, const char *namespaceUri) { for (xmlNodePtr child = parent == nullptr ? nullptr : parent->children; child != nullptr; child = child->next) { if (child->type == XML_ELEMENT_NODE && xmlStrEqual(child->name, BAD_CAST name) && child->ns != nullptr && xmlStrEqual(child->ns->href, BAD_CAST namespaceUri)) { return child; } } return nullptr; } xmlNodePtr findDescendant(xmlNodePtr parent, const char *name, const char *namespaceUri) { if (parent == nullptr) { return nullptr; } for (xmlNodePtr child = parent->children; child != nullptr; child = child->next) { if (child->type == XML_ELEMENT_NODE && xmlStrEqual(child->name, BAD_CAST name) && child->ns != nullptr && xmlStrEqual(child->ns->href, BAD_CAST namespaceUri)) { return child; } if (xmlNodePtr descendant = findDescendant(child, name, namespaceUri); descendant != nullptr) { return descendant; } } return nullptr; } } QByteArray EetV4Signer::signSoap(const QByteArray &unsignedSoap, const QString &pkcs12Path, const QByteArray &pkcs12Password, QString *error) { if (unsignedSoap.isEmpty() || pkcs12Path.isEmpty()) { setError(error, QStringLiteral("SOAP data and PKCS#12 path are required.")); return {}; } ensureOpenSslProvider(); QFile pkcs12File(pkcs12Path); if (!pkcs12File.open(QIODevice::ReadOnly)) { setError(error, QStringLiteral("Cannot read the PKCS#12 file.")); return {}; } const QByteArray pkcs12 = pkcs12File.readAll(); const QByteArray certificate = certificateDer(pkcs12, pkcs12Password, error); if (certificate.isEmpty()) { return {}; } if (!ensureXmlSecInitialized()) { setError(error, QStringLiteral("XML security initialization failed.")); return {}; } xmlDocPtr document = xmlReadMemory(unsignedSoap.constData(), unsignedSoap.size(), nullptr, "UTF-8", XML_PARSE_NONET); if (document == nullptr) { setError(error, QStringLiteral("The unsigned SOAP document is not valid XML.")); return {}; } QByteArray result; xmlNodePtr envelope = xmlDocGetRootElement(document); xmlNodePtr header = findChild(envelope, "Header", SoapNamespace); xmlNodePtr body = findChild(envelope, "Body", SoapNamespace); if (header == nullptr || body == nullptr) { setError(error, QStringLiteral("SOAP Envelope must contain Header and Body.")); xmlFreeDoc(document); return {}; } xmlAttrPtr bodyIdAttribute = xmlHasNsProp(body, BAD_CAST BodyId, BAD_CAST WsuNamespace); if (bodyIdAttribute == nullptr || bodyIdAttribute->children == nullptr) { setError(error, QStringLiteral("SOAP Body does not contain a wsu:Id.")); xmlFreeDoc(document); return {}; } QByteArray bodyReference = QByteArray("#") + QByteArray(reinterpret_cast( bodyIdAttribute->children->content)); const xmlChar *ids[] = { BAD_CAST BodyId, nullptr }; xmlSecAddIDs(document, nullptr, ids); xmlNsPtr wsse = xmlSearchNsByHref(document, envelope, BAD_CAST WsseNamespace); xmlNsPtr wsu = xmlSearchNsByHref(document, envelope, BAD_CAST WsuNamespace); if (wsse == nullptr || wsu == nullptr) { setError(error, QStringLiteral("SOAP security namespaces are missing.")); xmlFreeDoc(document); return {}; } xmlNodePtr security = xmlNewNode(wsse, BAD_CAST "Security"); xmlNodePtr token = xmlNewChild(security, wsse, BAD_CAST "BinarySecurityToken", BAD_CAST certificate.toBase64().constData()); xmlSetNsProp(token, wsu, BAD_CAST "Id", BAD_CAST CertificateId); xmlNewProp(token, BAD_CAST "EncodingType", BAD_CAST Base64Encoding); xmlNewProp(token, BAD_CAST "ValueType", BAD_CAST X509ValueType); xmlAddChild(header, security); xmlNodePtr signature = xmlSecTmplSignatureCreateNsPref( document, xmlSecTransformExclC14NId, xmlSecTransformRsaSha256Id, nullptr, BAD_CAST "ds"); if (signature == nullptr) { setError(error, QStringLiteral("Cannot create the XML signature template.")); xmlFreeDoc(document); return {}; } xmlNodePtr reference = xmlSecTmplSignatureAddReference( signature, xmlSecTransformSha256Id, nullptr, BAD_CAST bodyReference.constData(), nullptr); if (reference == nullptr || xmlSecTmplReferenceAddTransform(reference, xmlSecTransformExclC14NId) == nullptr) { setError(error, QStringLiteral("Cannot create the SOAP Body signature reference.")); xmlFreeDoc(document); return {}; } xmlNodePtr keyInfo = xmlSecTmplSignatureEnsureKeyInfo(signature, nullptr); xmlNodePtr tokenReference = xmlNewChild(keyInfo, wsse, BAD_CAST "SecurityTokenReference", nullptr); xmlNodePtr tokenReferenceValue = xmlNewChild(tokenReference, wsse, BAD_CAST "Reference", nullptr); xmlNewProp(tokenReferenceValue, BAD_CAST "URI", BAD_CAST "#eet-certificate"); xmlNewProp(tokenReferenceValue, BAD_CAST "ValueType", BAD_CAST X509ValueType); xmlAddChild(security, signature); xmlSecKeyPtr key = xmlSecCryptoAppPkcs12LoadMemory( reinterpret_cast(pkcs12.constData()), pkcs12.size(), pkcs12Password.constData(), nullptr, nullptr); if (key == nullptr) { setError(error, QStringLiteral("XML security could not load the PKCS#12 key.")); xmlFreeDoc(document); return {}; } xmlSecDSigCtxPtr context = xmlSecDSigCtxCreate(nullptr); if (context == nullptr) { setError(error, QStringLiteral("Cannot create the XML signature context.")); xmlSecKeyDestroy(key); xmlFreeDoc(document); return {}; } context->signKey = key; const bool signedSuccessfully = xmlSecDSigCtxSign(context, signature) == 0; xmlSecDSigCtxDestroy(context); if (!signedSuccessfully) { setError(error, QStringLiteral("XML security could not sign the SOAP Body.")); xmlFreeDoc(document); return {}; } xmlChar *serialized = nullptr; int serializedSize = 0; xmlDocDumpMemoryEnc(document, &serialized, &serializedSize, "UTF-8"); if (serialized != nullptr && serializedSize > 0) { result = QByteArray(reinterpret_cast(serialized), serializedSize); } xmlFree(serialized); xmlFreeDoc(document); if (result.isEmpty()) { setError(error, QStringLiteral("Cannot serialize the signed SOAP document.")); } else if (result.size() > EetV4Soap::MaximumMessageSize) { result.clear(); setError(error, QStringLiteral("Signed SOAP message exceeds the 12 KiB EET limit.")); } return result; } bool EetV4Signer::verifySoap(const QByteArray &signedSoap, QString *error) { if (signedSoap.isEmpty()) { setError(error, QStringLiteral("Signed SOAP data is required.")); return false; } ensureOpenSslProvider(); if (!ensureXmlSecInitialized()) { setError(error, QStringLiteral("XML security initialization failed.")); return false; } xmlDocPtr document = xmlReadMemory(signedSoap.constData(), signedSoap.size(), nullptr, "UTF-8", XML_PARSE_NONET); if (document == nullptr) { setError(error, QStringLiteral("The signed SOAP document is not valid XML.")); return false; } xmlNodePtr envelope = xmlDocGetRootElement(document); xmlNodePtr body = findChild(envelope, "Body", SoapNamespace); xmlNodePtr signature = findDescendant(envelope, "Signature", XmlDsigNamespace); xmlNodePtr token = findDescendant(envelope, "BinarySecurityToken", WsseNamespace); if (body == nullptr || signature == nullptr || token == nullptr) { setError(error, QStringLiteral("Signed SOAP is missing Body, Signature, or certificate.")); xmlFreeDoc(document); return false; } xmlAttrPtr bodyId = xmlHasNsProp(body, BAD_CAST BodyId, BAD_CAST WsuNamespace); if (bodyId == nullptr || bodyId->children == nullptr) { setError(error, QStringLiteral("SOAP Body does not contain a wsu:Id.")); xmlFreeDoc(document); return false; } const xmlChar *ids[] = { BAD_CAST BodyId, nullptr }; xmlSecAddIDs(document, nullptr, ids); xmlChar *tokenContent = xmlNodeGetContent(token); const QByteArray certificate = tokenContent == nullptr ? QByteArray() : QByteArray::fromBase64( QByteArray(reinterpret_cast(tokenContent))); xmlFree(tokenContent); if (certificate.isEmpty()) { setError(error, QStringLiteral("The embedded certificate is invalid.")); xmlFreeDoc(document); return false; } xmlSecKeyPtr key = xmlSecCryptoAppKeyLoadMemory( reinterpret_cast(certificate.constData()), certificate.size(), xmlSecKeyDataFormatCertDer, nullptr, nullptr, nullptr); if (key == nullptr) { setError(error, QStringLiteral("XML security could not load the embedded certificate.")); xmlFreeDoc(document); return false; } xmlSecDSigCtxPtr context = xmlSecDSigCtxCreate(nullptr); if (context == nullptr) { setError(error, QStringLiteral("Cannot create the XML verification context.")); xmlSecKeyDestroy(key); xmlFreeDoc(document); return false; } context->signKey = key; const bool verified = xmlSecDSigCtxVerify(context, signature) == 0 && context->status == xmlSecDSigStatusSucceeded; xmlSecDSigCtxDestroy(context); xmlFreeDoc(document); if (!verified) { setError(error, QStringLiteral("SOAP XML signature verification failed.")); } return verified; }