43#include <openssl/bio.h>
44#include <openssl/err.h>
45#include <openssl/evp.h>
46#include <openssl/pem.h>
47#include <openssl/sha.h>
48#include <openssl/x509.h>
66constexpr const char* kKcenonC14nUri =
67 "urn:kcenon:xds:c14n:pugixml-format-raw-v1";
70 void operator()(BIO* bio)
const noexcept {
71 if (bio) BIO_free_all(bio);
74using bio_ptr = std::unique_ptr<BIO, bio_deleter>;
77 void operator()(X509* x)
const noexcept {
81using x509_ptr = std::unique_ptr<X509, x509_deleter>;
84 void operator()(EVP_PKEY* p)
const noexcept {
85 if (p) EVP_PKEY_free(p);
88using pkey_ptr = std::unique_ptr<EVP_PKEY, pkey_deleter>;
90struct md_ctx_deleter {
91 void operator()(EVP_MD_CTX* c)
const noexcept {
92 if (c) EVP_MD_CTX_free(c);
95using md_ctx_ptr = std::unique_ptr<EVP_MD_CTX, md_ctx_deleter>;
97int pem_password_callback(
char* buf,
int size,
int ,
void* userdata) {
98 if (userdata ==
nullptr)
return 0;
99 const auto* pass =
static_cast<const std::string*
>(userdata);
101 (
static_cast<int>(pass->size()) < size) ?
static_cast<int>(pass->size())
103 std::memcpy(buf, pass->data(),
static_cast<std::size_t
>(to_copy));
107x509_ptr load_cert(std::string_view pem) {
108 bio_ptr bio(BIO_new_mem_buf(pem.data(),
static_cast<int>(pem.size())));
109 if (!bio)
return nullptr;
110 X509* raw = PEM_read_bio_X509(bio.get(),
nullptr,
nullptr,
nullptr);
111 return x509_ptr(raw);
114pkey_ptr load_key(std::string_view pem, std::string_view password) {
115 bio_ptr bio(BIO_new_mem_buf(pem.data(),
static_cast<int>(pem.size())));
116 if (!bio)
return nullptr;
117 std::string pw(password);
118 EVP_PKEY* raw = PEM_read_bio_PrivateKey(
119 bio.get(),
nullptr, pem_password_callback,
120 pw.empty() ?
nullptr :
static_cast<void*
>(&pw));
121 return pkey_ptr(raw);
124std::string
base64_encode(
const unsigned char* data, std::size_t len) {
125 bio_ptr b64(BIO_new(BIO_f_base64()));
127 BIO* mem = BIO_new(BIO_s_mem());
129 BIO* chain = BIO_push(b64.release(), mem);
130 bio_ptr owner(chain);
131 BIO_set_flags(owner.get(), BIO_FLAGS_BASE64_NO_NL);
132 BIO_write(owner.get(), data,
static_cast<int>(len));
133 BIO_flush(owner.get());
134 BUF_MEM* bptr =
nullptr;
135 BIO_get_mem_ptr(owner.get(), &bptr);
136 if (!bptr)
return {};
137 return std::string(bptr->data, bptr->length);
145std::string strip_b64_whitespace(std::string_view s) {
147 out.reserve(s.size());
148 for (
const char c : s) {
149 if (c !=
'\r' && c !=
'\n' && c !=
' ' && c !=
'\t') {
156std::vector<unsigned char>
base64_decode(std::string_view b64_in) {
157 const std::string cleaned = strip_b64_whitespace(b64_in);
158 if (cleaned.empty())
return {};
159 bio_ptr b64(BIO_new(BIO_f_base64()));
162 BIO_new_mem_buf(cleaned.data(),
static_cast<int>(cleaned.size()));
164 BIO* chain = BIO_push(b64.release(), mem);
165 bio_ptr owner(chain);
166 BIO_set_flags(owner.get(), BIO_FLAGS_BASE64_NO_NL);
167 std::vector<unsigned char>
out(cleaned.size());
168 const int n = BIO_read(owner.get(),
out.data(),
static_cast<int>(
out.size()));
169 if (n <= 0)
return {};
170 out.resize(
static_cast<std::size_t
>(n));
174std::vector<unsigned char> cert_to_der(X509* cert) {
175 int len = i2d_X509(cert,
nullptr);
176 if (len <= 0)
return {};
177 std::vector<unsigned char>
out(
static_cast<std::size_t
>(len));
178 unsigned char* p =
out.data();
179 if (i2d_X509(cert, &p) != len)
return {};
183std::string sha256_base64(std::string_view bytes) {
184 unsigned char digest[SHA256_DIGEST_LENGTH];
185 SHA256(
reinterpret_cast<const unsigned char*
>(bytes.data()), bytes.size(),
190struct xml_string_writer : pugi::xml_writer {
192 void write(
const void* data, std::size_t size)
override {
193 out.append(
static_cast<const char*
>(data), size);
197std::string serialize_subtree(pugi::xml_node node) {
199 node.print(w,
"", pugi::format_raw | pugi::format_no_declaration,
200 pugi::encoding_utf8);
207 std::string_view cert_pem,
208 std::string_view key_pem,
209 std::string_view key_password) {
210 if (cert_pem.empty()) {
211 return kcenon::common::make_error<bool>(
215 if (key_pem.empty()) {
216 return kcenon::common::make_error<bool>(
221 auto cert = load_cert(cert_pem);
223 return kcenon::common::make_error<bool>(
227 auto key = load_key(key_pem, key_password);
229 return kcenon::common::make_error<bool>(
231 "PEM_read_bio_PrivateKey failed", std::string(
error_source));
234 const auto der = cert_to_der(cert.get());
236 return kcenon::common::make_error<bool>(
240 const std::string cert_b64 =
base64_encode(der.data(), der.size());
241 if (cert_b64.empty()) {
242 return kcenon::common::make_error<bool>(
244 "base64 of DER certificate failed", std::string(
error_source));
247 pugi::xml_document doc;
249 doc.load_buffer(env.
xml.data(), env.
xml.size(),
250 pugi::parse_default, pugi::encoding_utf8);
252 return kcenon::common::make_error<bool>(
254 std::string(
"failed to reparse envelope: ") +
255 parse_result.description(),
259 auto envelope = doc.child(
"soap:Envelope");
260 auto header = envelope.child(
"soap:Header");
261 auto security = header.child(
"wsse:Security");
262 auto bst = security.child(
"wsse:BinarySecurityToken");
263 auto timestamp = security.child(
"wsu:Timestamp");
264 auto body = envelope.child(
"soap:Body");
266 if (!envelope || !header || !security || !bst || !timestamp || !body) {
267 return kcenon::common::make_error<bool>(
269 "envelope is missing required WS-Security scaffolding",
273 bst.text().set(cert_b64.c_str());
275 const std::string body_c14n = serialize_subtree(body);
276 const std::string ts_c14n = serialize_subtree(timestamp);
278 if (body_c14n.empty() || ts_c14n.empty()) {
279 return kcenon::common::make_error<bool>(
281 "serialized Body or Timestamp is empty",
285 const std::string body_digest = sha256_base64(body_c14n);
286 const std::string ts_digest = sha256_base64(ts_c14n);
287 if (body_digest.empty() || ts_digest.empty()) {
288 return kcenon::common::make_error<bool>(
293 auto signature = security.append_child(
"ds:Signature");
294 signature.append_attribute(
"xmlns:ds") =
"http://www.w3.org/2000/09/xmldsig#";
296 auto signed_info = signature.append_child(
"ds:SignedInfo");
297 auto c14n_method = signed_info.append_child(
"ds:CanonicalizationMethod");
298 c14n_method.append_attribute(
"Algorithm") = kKcenonC14nUri;
299 auto sig_method = signed_info.append_child(
"ds:SignatureMethod");
300 sig_method.append_attribute(
"Algorithm") =
301 "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
303 const auto add_reference = [&](
const std::string& uri,
304 const std::string& digest_b64) {
305 auto ref = signed_info.append_child(
"ds:Reference");
306 ref.append_attribute(
"URI") = (
"#" + uri).c_str();
307 auto transforms = ref.append_child(
"ds:Transforms");
308 auto xform = transforms.append_child(
"ds:Transform");
309 xform.append_attribute(
"Algorithm") = kKcenonC14nUri;
310 auto digest_method = ref.append_child(
"ds:DigestMethod");
311 digest_method.append_attribute(
"Algorithm") =
312 "http://www.w3.org/2001/04/xmlenc#sha256";
313 ref.append_child(
"ds:DigestValue").text().set(digest_b64.c_str());
315 add_reference(env.
body_id, body_digest);
318 const std::string signed_info_bytes = serialize_subtree(signed_info);
319 if (signed_info_bytes.empty()) {
320 return kcenon::common::make_error<bool>(
322 "serialized SignedInfo is empty", std::string(
error_source));
325 md_ctx_ptr md(EVP_MD_CTX_new());
327 return kcenon::common::make_error<bool>(
331 if (EVP_DigestSignInit(md.get(),
nullptr, EVP_sha256(),
nullptr,
333 return kcenon::common::make_error<bool>(
335 "EVP_DigestSignInit failed", std::string(
error_source));
337 if (EVP_DigestSignUpdate(
338 md.get(), signed_info_bytes.data(), signed_info_bytes.size()) != 1) {
339 return kcenon::common::make_error<bool>(
341 "EVP_DigestSignUpdate failed", std::string(
error_source));
344 std::size_t sig_len = 0;
345 if (EVP_DigestSignFinal(md.get(),
nullptr, &sig_len) != 1 || sig_len == 0) {
346 return kcenon::common::make_error<bool>(
348 "EVP_DigestSignFinal probe failed", std::string(
error_source));
350 std::vector<unsigned char> sig(sig_len);
351 if (EVP_DigestSignFinal(md.get(), sig.data(), &sig_len) != 1) {
352 return kcenon::common::make_error<bool>(
354 "EVP_DigestSignFinal failed", std::string(
error_source));
357 const std::string sig_b64 =
base64_encode(sig.data(), sig.size());
359 signature.append_child(
"ds:SignatureValue").text().set(sig_b64.c_str());
361 auto key_info = signature.append_child(
"ds:KeyInfo");
362 auto sec_ref = key_info.append_child(
"wsse:SecurityTokenReference");
363 auto ref = sec_ref.append_child(
"wsse:Reference");
365 ref.append_attribute(
"ValueType") =
366 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-"
367 "profile-1.0#X509v3";
370 doc.save(w,
"", pugi::format_raw | pugi::format_no_declaration,
371 pugi::encoding_utf8);
372 env.
xml = R
"(<?xml version="1.0" encoding="UTF-8"?>)" + w.out;
381std::string strip_uri_fragment(
const char* uri) {
382 if (uri ==
nullptr)
return {};
383 if (uri[0] ==
'#')
return std::string(uri + 1);
384 return std::string(uri);
391struct wsu_id_walker : pugi::xml_tree_walker {
394 bool for_each(pugi::xml_node& n)
override {
395 const auto attr = n.attribute(
"wsu:Id");
396 if (!attr.empty() && attr.as_string() ==
needle) {
404pugi::xml_node find_by_wsu_id(pugi::xml_node root,
const std::string&
id) {
414 std::string_view signed_xml) {
415 if (signed_xml.empty()) {
416 return kcenon::common::make_error<bool>(
422 pugi::xml_document doc;
423 auto parse = doc.load_buffer(signed_xml.data(), signed_xml.size(),
424 pugi::parse_default, pugi::encoding_utf8);
426 return kcenon::common::make_error<bool>(
429 std::string(
"failed to reparse signed envelope: ") +
434 auto envelope = doc.child(
"soap:Envelope");
435 auto header = envelope.child(
"soap:Header");
436 auto security = header.child(
"wsse:Security");
437 auto signature = security.child(
"ds:Signature");
438 auto bst = security.child(
"wsse:BinarySecurityToken");
440 if (!envelope || !header || !security) {
441 return kcenon::common::make_error<bool>(
444 "envelope lacks WS-Security scaffolding",
448 return kcenon::common::make_error<bool>(
450 "envelope has no ds:Signature element",
454 return kcenon::common::make_error<bool>(
457 "envelope has no wsse:BinarySecurityToken",
463 auto signed_info = signature.child(
"ds:SignedInfo");
465 return kcenon::common::make_error<bool>(
468 "Signature has no SignedInfo", std::string(
error_source));
470 auto c14n = signed_info.child(
"ds:CanonicalizationMethod");
472 std::string(c14n.attribute(
"Algorithm").as_string()) != kKcenonC14nUri) {
473 return kcenon::common::make_error<bool>(
476 "CanonicalizationMethod is not the kcenon project-local URI",
481 const std::string bst_b64 = strip_b64_whitespace(bst.text().as_string());
482 const auto der = base64_decode(bst_b64);
484 return kcenon::common::make_error<bool>(
487 "BinarySecurityToken base64 decode failed",
496 const unsigned char* der_ptr = der.data();
497 x509_ptr cert(d2i_X509(
nullptr, &der_ptr,
static_cast<long>(der.size())));
499 return kcenon::common::make_error<bool>(
502 "d2i_X509 failed on BinarySecurityToken",
505 pkey_ptr pkey(X509_get_pubkey(cert.get()));
507 return kcenon::common::make_error<bool>(
515 for (
auto ref = signed_info.child(
"ds:Reference"); ref;
516 ref = ref.next_sibling(
"ds:Reference")) {
517 for (
auto xform = ref.child(
"ds:Transforms").child(
"ds:Transform");
518 xform; xform = xform.next_sibling(
"ds:Transform")) {
519 if (std::string(xform.attribute(
"Algorithm").as_string()) !=
521 return kcenon::common::make_error<bool>(
524 "Reference Transform is not the kcenon project-local URI",
528 const std::string uri = strip_uri_fragment(
529 ref.attribute(
"URI").as_string());
531 return kcenon::common::make_error<bool>(
536 auto target = find_by_wsu_id(envelope, uri);
538 return kcenon::common::make_error<bool>(
541 "Reference target not found: " + uri,
544 const std::string bytes = serialize_subtree(target);
545 const std::string want_digest = sha256_base64(bytes);
546 const std::string have_digest = strip_b64_whitespace(
547 ref.child(
"ds:DigestValue").text().as_string());
548 if (want_digest != have_digest) {
549 return kcenon::common::make_error<bool>(
552 "DigestValue mismatch for reference: " + uri,
558 const std::string signed_info_bytes = serialize_subtree(signed_info);
559 if (signed_info_bytes.empty()) {
560 return kcenon::common::make_error<bool>(
563 "serialized SignedInfo is empty", std::string(
error_source));
565 const auto sig_bytes = base64_decode(
566 signature.child(
"ds:SignatureValue").text().as_string());
567 if (sig_bytes.empty()) {
568 return kcenon::common::make_error<bool>(
571 "SignatureValue base64 decode failed",
575 md_ctx_ptr md(EVP_MD_CTX_new());
577 return kcenon::common::make_error<bool>(
582 if (EVP_DigestVerifyInit(md.get(),
nullptr, EVP_sha256(),
nullptr,
584 return kcenon::common::make_error<bool>(
587 "EVP_DigestVerifyInit failed", std::string(
error_source));
589 if (EVP_DigestVerifyUpdate(md.get(), signed_info_bytes.data(),
590 signed_info_bytes.size()) != 1) {
591 return kcenon::common::make_error<bool>(
594 "EVP_DigestVerifyUpdate failed", std::string(
error_source));
597 EVP_DigestVerifyFinal(md.get(), sig_bytes.data(), sig_bytes.size());
599 return kcenon::common::make_error<bool>(
602 "EVP_DigestVerifyFinal rejected the signature",
609 const auto raw = base64_decode(b64);
610 std::vector<std::uint8_t>
out;
611 out.reserve(raw.size());
612 for (
const unsigned char c : raw) {
613 out.push_back(
static_cast<std::uint8_t
>(c));
std::string base64_encode(std::string_view input)
Encode data to Base64.
std::vector< std::uint8_t > base64_decode_bytes(std::string_view b64)
Base64-decode b64 using the same BIO-backed decoder the signer and verifier use internally.
kcenon::common::Result< bool > sign_envelope(built_envelope &env, std::string_view cert_pem, std::string_view key_pem, std::string_view key_password)
Sign the body + timestamp of env in place.
kcenon::common::Result< bool > verify_envelope_integrity(std::string_view signed_xml)
Verify the structural integrity of a previously signed envelope against the certificate embedded in i...
constexpr std::string_view error_source
Short identifier used as the "source" field of make_error.
@ consumer_signature_verification_failed
@ signer_invalid_private_key
@ signer_invalid_certificate
@ consumer_signature_missing
bool base64_decode(const std::string &in, std::vector< std::uint8_t > &out)
Standard base64 decode; returns false on malformed input.
@ id
Implant Displaced (alternate code)
SOAP 1.2 envelope builder for IHE XDS.b ITI-41 requests.
Result of envelope construction.
std::string binary_security_token_id
WS-Security XML-DSig signer backed by OpenSSL EVP_DigestSign.