rewrite for eet 2.0 api version v4
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
#include "eetv4signer.h"
|
||||
#include "eetv4soap.h"
|
||||
|
||||
#include <QFile>
|
||||
|
||||
#include <libxml/parser.h>
|
||||
#include <libxml/tree.h>
|
||||
#include <openssl/pkcs12.h>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/crypto.h>
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||
#include <openssl/provider.h>
|
||||
#endif
|
||||
#include <openssl/x509.h>
|
||||
#include <xmlsec/app.h>
|
||||
#include <xmlsec/crypto.h>
|
||||
#include <xmlsec/templates.h>
|
||||
#include <xmlsec/xmlsec.h>
|
||||
#include <xmlsec/xmltree.h>
|
||||
#include <xmlsec/xmldsig.h>
|
||||
#include <xmlsec/openssl/crypto.h>
|
||||
|
||||
#include <mutex>
|
||||
|
||||
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<unsigned char *>(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<const char *>(
|
||||
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<const xmlSecByte *>(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<const char *>(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<const char *>(tokenContent)));
|
||||
xmlFree(tokenContent);
|
||||
if (certificate.isEmpty())
|
||||
{
|
||||
setError(error, QStringLiteral("The embedded certificate is invalid."));
|
||||
xmlFreeDoc(document);
|
||||
return false;
|
||||
}
|
||||
|
||||
xmlSecKeyPtr key = xmlSecCryptoAppKeyLoadMemory(
|
||||
reinterpret_cast<const xmlSecByte *>(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;
|
||||
}
|
||||
Reference in New Issue
Block a user