414 lines
14 KiB
C++
414 lines
14 KiB
C++
#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;
|
|
}
|