Files
EetCpp/libEet/eetv4signer.cpp
T

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;
}