PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
kcenon::pacs::ihe::xds::detail Namespace Reference

Classes

struct  built_envelope
 Result of envelope construction. More...
 
struct  http_response
 HTTP response captured from a POST call. More...
 
struct  mtom_part
 One attachment part, mapped 1:1 to an xop:Include in the envelope. More...
 
struct  packaged_mtom
 Fully assembled MTOM request body. More...
 
struct  parsed_mtom
 Result of parsing an inbound multipart/related MTOM response. More...
 
struct  transport_request
 Optional test hook: replaces the real libcurl transport. More...
 

Typedefs

using transport_override
 

Enumerations

enum class  stored_query_kind { find_documents , get_documents }
 Discriminator for the two stored queries this actor supports. More...
 

Functions

void set_http_transport_override (transport_override fn)
 
void clear_http_transport_override ()
 
kcenon::common::Result< http_response > http_post (const http_options &opts, const std::string &content_type, const std::string &body)
 POST body to opts.endpoint with the given Content-Type.
 
kcenon::common::Result< packaged_mtom > package_mtom (const std::string &envelope_xml, const std::vector< mtom_part > &parts)
 Assemble a multipart/related MTOM body.
 
kcenon::common::Result< parsed_mtom > parse_mtom_response (const std::string &content_type_header, const std::string &body)
 Parse a multipart/related MTOM response body.
 
kcenon::common::Result< built_envelope > build_iti41_envelope (const submission_set &s, const std::vector< std::string > &cid_refs)
 Build a SOAP envelope for ITI-41 from submission metadata and a list of content-id references for MTOM-attached document bytes.
 
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 its own BinarySecurityToken.
 
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< built_envelope > build_iti43_envelope (const retrieve_request &req)
 Build a SOAP envelope for ITI-43 from a retrieve_request.
 
kcenon::common::Result< document_response > parse_retrieve_response (const std::string &root_xml, const std::vector< mtom_part > &parts, const retrieve_request &req)
 Parse a RetrieveDocumentSetResponse envelope and resolve the attachment that matches req.
 
kcenon::common::Result< built_envelope > build_iti18_envelope (const registry_query_request &req, stored_query_kind kind)
 Build a SOAP envelope for ITI-18 from a registry_query_request.
 
kcenon::common::Result< registry_query_result > parse_query_response (const std::string &root_xml)
 Parse an AdhocQueryResponse envelope into a registry_query_result.
 

Typedef Documentation

◆ transport_override

Initial value:
std::function<kcenon::common::Result<http_response>(const transport_request&)>

Definition at line 57 of file http_client.h.

Enumeration Type Documentation

◆ stored_query_kind

Discriminator for the two stored queries this actor supports.

Enumerator
find_documents 
get_documents 

Definition at line 33 of file query_envelope.h.

Function Documentation

◆ base64_decode_bytes()

std::vector< std::uint8_t > kcenon::pacs::ihe::xds::detail::base64_decode_bytes ( std::string_view b64)

Base64-decode b64 using the same BIO-backed decoder the signer and verifier use internally.

Exported so callers outside wss_signer.cpp (in particular the retrieve response parser, which needs to decode inline Document payloads on non-MTOM responses) do not have to re-implement the OpenSSL BIO dance. Returns an empty vector on decode failure; callers must check.

Definition at line 608 of file wss_signer.cpp.

608 {
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));
614 }
615 return out;
616}
std::string out

References out.

Referenced by parse_retrieve_response().

Here is the caller graph for this function:

◆ build_iti18_envelope()

kcenon::common::Result< built_envelope > kcenon::pacs::ihe::xds::detail::build_iti18_envelope ( const registry_query_request & req,
stored_query_kind kind )

Build a SOAP envelope for ITI-18 from a registry_query_request.

Emits an AdhocQueryRequest containing one AdhocQuery with the stored query id corresponding to kind. The envelope carries a wsa:Action of "urn:ihe:iti:2007:RegistryStoredQuery" and an empty wsse:Security header containing a Timestamp and BinarySecurityToken that sign_envelope fills in and signs afterwards.

For find_documents: emits $XDSDocumentEntryPatientId and $XDSDocumentEntryStatus slots, plus optional $XDSDocumentEntryCreationTimeFrom / $XDSDocumentEntryCreationTimeTo slots when req.options populates them.

For get_documents: emits $XDSDocumentEntryEntryUUID with the list literal form that ebRIM StoredQuery slots require.

Returns registry_query_missing_patient_id / registry_query_empty_uuid_list on empty required fields; envelope_serialization_failed if pugixml cannot serialize the document.

Definition at line 149 of file query_envelope.cpp.

150 {
151 if (kind == stored_query_kind::find_documents) {
152 if (req.patient_id.empty()) {
153 return kcenon::common::make_error<built_envelope>(
154 static_cast<int>(
155 error_code::registry_query_missing_patient_id),
156 "registry_query_request.patient_id is empty",
157 std::string(error_source));
158 }
159 if (contains_slot_literal_metachar(req.patient_id)) {
160 return kcenon::common::make_error<built_envelope>(
161 static_cast<int>(
162 error_code::registry_query_invalid_patient_id),
163 "patient_id contains a character that is not legal inside "
164 "an ebRIM StoredQuery slot literal ('(),\\' or control "
165 "character)",
166 std::string(error_source));
167 }
168 for (const auto& s : req.options.status_values) {
169 if (contains_slot_literal_metachar(s)) {
170 return kcenon::common::make_error<built_envelope>(
171 static_cast<int>(
172 error_code::registry_query_invalid_patient_id),
173 "options.status_values contains an element with a "
174 "character that is not legal inside an ebRIM slot "
175 "literal",
176 std::string(error_source));
177 }
178 }
179 if (contains_slot_literal_metachar(req.options.creation_time_from) ||
180 contains_slot_literal_metachar(req.options.creation_time_to)) {
181 return kcenon::common::make_error<built_envelope>(
182 static_cast<int>(
183 error_code::registry_query_invalid_patient_id),
184 "options.creation_time_* contains a character that is not "
185 "legal inside an ebRIM slot literal",
186 std::string(error_source));
187 }
188 }
189 if (kind == stored_query_kind::get_documents) {
190 if (req.document_uuids.empty()) {
191 return kcenon::common::make_error<built_envelope>(
192 static_cast<int>(
193 error_code::registry_query_empty_uuid_list),
194 "registry_query_request.document_uuids is empty",
195 std::string(error_source));
196 }
197 for (const auto& u : req.document_uuids) {
198 if (u.empty()) {
199 return kcenon::common::make_error<built_envelope>(
200 static_cast<int>(
201 error_code::registry_query_missing_document_uuid),
202 "registry_query_request.document_uuids contains an "
203 "empty element",
204 std::string(error_source));
205 }
206 if (contains_slot_literal_metachar(u)) {
207 return kcenon::common::make_error<built_envelope>(
208 static_cast<int>(
209 error_code::registry_query_missing_document_uuid),
210 "registry_query_request.document_uuids contains an "
211 "element with a character that is not legal inside "
212 "an ebRIM StoredQuery slot literal",
213 std::string(error_source));
214 }
215 }
216 }
217
218 pugi::xml_document doc;
219 auto decl = doc.append_child(pugi::node_declaration);
220 decl.append_attribute("version") = "1.0";
221 decl.append_attribute("encoding") = "UTF-8";
222
223 auto env = doc.append_child("soap:Envelope");
224 env.append_attribute("xmlns:soap") = kSoapEnv;
225 env.append_attribute("xmlns:wsa") = kWsa;
226 env.append_attribute("xmlns:wsse") = kWsse;
227 env.append_attribute("xmlns:wsu") = kWsu;
228 env.append_attribute("xmlns:query") = kQuery;
229 env.append_attribute("xmlns:rim") = kRim;
230
231 auto header = env.append_child("soap:Header");
232
233 auto action = header.append_child("wsa:Action");
234 action.append_attribute("soap:mustUnderstand") = "true";
235 action.text().set("urn:ihe:iti:2007:RegistryStoredQuery");
236
237 auto message_id = header.append_child("wsa:MessageID");
238 message_id.text().set(("urn:uuid:" + make_xml_id("msg")).c_str());
239
240 auto security = header.append_child("wsse:Security");
241 security.append_attribute("soap:mustUnderstand") = "true";
242
243 built_envelope out{};
244 out.timestamp_id = make_xml_id("ts");
245 out.binary_security_token_id = make_xml_id("bst");
246 out.body_id = make_xml_id("body");
247
248 auto timestamp = security.append_child("wsu:Timestamp");
249 timestamp.append_attribute("wsu:Id") = out.timestamp_id.c_str();
250 auto created = timestamp.append_child("wsu:Created");
251 const std::string now = utc_iso8601_now();
252 created.text().set(now.c_str());
253
254 auto bst = security.append_child("wsse:BinarySecurityToken");
255 bst.append_attribute("wsu:Id") = out.binary_security_token_id.c_str();
256 bst.append_attribute("EncodingType") =
257 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-"
258 "security-1.0#Base64Binary";
259 bst.append_attribute("ValueType") =
260 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-"
261 "profile-1.0#X509v3";
262
263 auto body = env.append_child("soap:Body");
264 body.append_attribute("wsu:Id") = out.body_id.c_str();
265
266 auto adhoc_request = body.append_child("query:AdhocQueryRequest");
267 // ReturnType=LeafClass is the only form the XDS.b profile of ebRS
268 // accepts for FindDocuments/GetDocuments. ObjectRef returns IDs only
269 // and is unusable for the registry_query_result we assemble later.
270 auto response_option =
271 adhoc_request.append_child("query:ResponseOption");
272 response_option.append_attribute("returnComposedObjects") = "true";
273 response_option.append_attribute("returnType") = "LeafClass";
274
275 auto adhoc_query = adhoc_request.append_child("rim:AdhocQuery");
276 adhoc_query.append_attribute("id") =
277 (kind == stored_query_kind::find_documents) ? kFindDocumentsQueryId
278 : kGetDocumentsQueryId;
279
280 if (kind == stored_query_kind::find_documents) {
281 append_slot_single(adhoc_query, "$XDSDocumentEntryPatientId",
282 req.patient_id);
283 if (req.options.status_values.empty()) {
284 append_slot_single(adhoc_query, "$XDSDocumentEntryStatus",
285 kDefaultApprovedStatus);
286 } else {
287 append_slot_list(adhoc_query, "$XDSDocumentEntryStatus",
289 }
290 if (!req.options.creation_time_from.empty()) {
291 append_slot_single(adhoc_query,
292 "$XDSDocumentEntryCreationTimeFrom",
294 }
295 if (!req.options.creation_time_to.empty()) {
296 append_slot_single(adhoc_query,
297 "$XDSDocumentEntryCreationTimeTo",
299 }
300 } else {
301 append_slot_list(adhoc_query, "$XDSDocumentEntryEntryUUID",
302 req.document_uuids);
303 }
304
305 xml_string_writer writer;
306 doc.save(writer, "", pugi::format_raw | pugi::format_no_declaration,
307 pugi::encoding_utf8);
308 if (writer.out.empty()) {
309 return kcenon::common::make_error<built_envelope>(
310 static_cast<int>(error_code::envelope_serialization_failed),
311 "pugixml produced an empty envelope",
312 std::string(error_source));
313 }
314
315 out.xml = R"(<?xml version="1.0" encoding="UTF-8"?>)";
316 out.xml += writer.out;
317 return out;
318}
constexpr dicom_tag message_id
Message ID.
std::string out

References kcenon::pacs::ihe::xds::registry_query_options::creation_time_from, kcenon::pacs::ihe::xds::registry_query_options::creation_time_to, kcenon::pacs::ihe::xds::registry_query_request::document_uuids, kcenon::pacs::ihe::xds::envelope_serialization_failed, kcenon::pacs::ihe::xds::error_source, find_documents, get_documents, kcenon::pacs::ihe::xds::registry_query_request::options, out, kcenon::pacs::ihe::xds::registry_query_request::patient_id, kcenon::pacs::ihe::xds::registry_query_empty_uuid_list, kcenon::pacs::ihe::xds::registry_query_invalid_patient_id, kcenon::pacs::ihe::xds::registry_query_missing_document_uuid, kcenon::pacs::ihe::xds::registry_query_missing_patient_id, and kcenon::pacs::ihe::xds::registry_query_options::status_values.

Referenced by kcenon::pacs::ihe::xds::registry_query::impl::execute().

Here is the caller graph for this function:

◆ build_iti41_envelope()

kcenon::common::Result< built_envelope > kcenon::pacs::ihe::xds::detail::build_iti41_envelope ( const submission_set & s,
const std::vector< std::string > & cid_refs )

Build a SOAP envelope for ITI-41 from submission metadata and a list of content-id references for MTOM-attached document bytes.

The cid_refs vector must be the same length as s.documents. Each cid is placed inside the matching ExtrinsicObject as an xop:Include.

Returns envelope_build_failed on any input inconsistency; envelope_serialization_failed if pugixml cannot serialize the document.

Definition at line 201 of file soap_envelope.cpp.

202 {
203 if (cid_refs.size() != s.documents.size()) {
204 return kcenon::common::make_error<built_envelope>(
205 static_cast<int>(error_code::envelope_build_failed),
206 "content-id list size does not match document count",
207 std::string(error_source));
208 }
209
210 pugi::xml_document doc;
211 auto decl = doc.append_child(pugi::node_declaration);
212 decl.append_attribute("version") = "1.0";
213 decl.append_attribute("encoding") = "UTF-8";
214
215 auto env = doc.append_child("soap:Envelope");
216 env.append_attribute("xmlns:soap") = kSoapEnv;
217 env.append_attribute("xmlns:wsa") = kWsa;
218 env.append_attribute("xmlns:wsse") = kWsse;
219 env.append_attribute("xmlns:wsu") = kWsu;
220 env.append_attribute("xmlns:lcm") = kLcm;
221 env.append_attribute("xmlns:rim") = kRim;
222 env.append_attribute("xmlns:xop") = kXop;
223
224 auto header = env.append_child("soap:Header");
225
226 auto action = header.append_child("wsa:Action");
227 action.append_attribute("soap:mustUnderstand") = "true";
228 action.text().set("urn:ihe:iti:2007:RegisterDocumentSet-b");
229
230 auto message_id = header.append_child("wsa:MessageID");
231 message_id.text().set(("urn:uuid:" + make_xml_id("msg")).c_str());
232
233 auto security = header.append_child("wsse:Security");
234 security.append_attribute("soap:mustUnderstand") = "true";
235
236 built_envelope out{};
237 out.timestamp_id = make_xml_id("ts");
238 out.binary_security_token_id = make_xml_id("bst");
239 out.body_id = make_xml_id("body");
240
241 auto timestamp = security.append_child("wsu:Timestamp");
242 timestamp.append_attribute("wsu:Id") = out.timestamp_id.c_str();
243 auto created = timestamp.append_child("wsu:Created");
244 const std::string now = utc_iso8601_now();
245 created.text().set(now.c_str());
246
247 auto bst = security.append_child("wsse:BinarySecurityToken");
248 bst.append_attribute("wsu:Id") = out.binary_security_token_id.c_str();
249 bst.append_attribute("EncodingType") =
250 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-"
251 "security-1.0#Base64Binary";
252 bst.append_attribute("ValueType") =
253 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-"
254 "profile-1.0#X509v3";
255
256 auto body = env.append_child("soap:Body");
257 body.append_attribute("wsu:Id") = out.body_id.c_str();
258
259 auto req = body.append_child("lcm:SubmitObjectsRequest");
260
261 auto leaf_list = req.append_child("rim:RegistryObjectList");
262
263 if (s.patient_id.empty()) {
264 return kcenon::common::make_error<built_envelope>(
265 static_cast<int>(error_code::metadata_missing_patient_id),
266 "submission_set.patient_id is empty",
267 std::string(error_source));
268 }
269 if (s.source_id.empty()) {
270 return kcenon::common::make_error<built_envelope>(
271 static_cast<int>(error_code::metadata_missing_source_id),
272 "submission_set.source_id is empty",
273 std::string(error_source));
274 }
275 if (s.submission_set_unique_id.empty()) {
276 return kcenon::common::make_error<built_envelope>(
277 static_cast<int>(error_code::metadata_missing_submission_set_uid),
278 "submission_set.submission_set_unique_id is empty",
279 std::string(error_source));
280 }
281 if (s.documents.empty()) {
282 return kcenon::common::make_error<built_envelope>(
283 static_cast<int>(error_code::metadata_no_documents),
284 "submission_set contains no documents",
285 std::string(error_source));
286 }
287
288 for (std::size_t i = 0; i < s.documents.size(); ++i) {
289 const auto& d = s.documents[i];
290 if (d.unique_id.empty()) {
291 return kcenon::common::make_error<built_envelope>(
292 static_cast<int>(
293 error_code::metadata_document_missing_unique_id),
294 "document[" + std::to_string(i) + "].unique_id is empty",
295 std::string(error_source));
296 }
297 if (d.mime_type.empty()) {
298 return kcenon::common::make_error<built_envelope>(
299 static_cast<int>(
300 error_code::metadata_document_missing_mime_type),
301 "document[" + std::to_string(i) + "].mime_type is empty",
302 std::string(error_source));
303 }
304 if (d.content.empty()) {
305 return kcenon::common::make_error<built_envelope>(
306 static_cast<int>(error_code::metadata_document_empty),
307 "document[" + std::to_string(i) + "].content is empty",
308 std::string(error_source));
309 }
310 add_document_entry(leaf_list, d, cid_refs[i]);
311 }
312
313 add_registry_package(leaf_list, s);
314
315 xml_string_writer writer;
316 doc.save(writer, "", pugi::format_raw | pugi::format_no_declaration,
317 pugi::encoding_utf8);
318 if (writer.out.empty()) {
319 return kcenon::common::make_error<built_envelope>(
320 static_cast<int>(error_code::envelope_serialization_failed),
321 "pugixml produced an empty envelope",
322 std::string(error_source));
323 }
324
325 out.xml = R"(<?xml version="1.0" encoding="UTF-8"?>)";
326 out.xml += writer.out;
327 return out;
328}
std::string out
std::vector< xds_document > documents

References kcenon::pacs::ihe::xds::submission_set::documents, kcenon::pacs::ihe::xds::envelope_build_failed, kcenon::pacs::ihe::xds::envelope_serialization_failed, kcenon::pacs::ihe::xds::error_source, kcenon::pacs::ihe::xds::metadata_document_empty, kcenon::pacs::ihe::xds::metadata_document_missing_mime_type, kcenon::pacs::ihe::xds::metadata_document_missing_unique_id, kcenon::pacs::ihe::xds::metadata_missing_patient_id, kcenon::pacs::ihe::xds::metadata_missing_source_id, kcenon::pacs::ihe::xds::metadata_missing_submission_set_uid, kcenon::pacs::ihe::xds::metadata_no_documents, out, kcenon::pacs::ihe::xds::submission_set::patient_id, kcenon::pacs::ihe::xds::submission_set::source_id, and kcenon::pacs::ihe::xds::submission_set::submission_set_unique_id.

Referenced by kcenon::pacs::ihe::xds::document_source::impl::submit().

Here is the caller graph for this function:

◆ build_iti43_envelope()

kcenon::common::Result< built_envelope > kcenon::pacs::ihe::xds::detail::build_iti43_envelope ( const retrieve_request & req)

Build a SOAP envelope for ITI-43 from a retrieve_request.

Emits one xdsb:DocumentRequest element per call. The envelope carries a wsa:Action of "urn:ihe:iti:2007:RetrieveDocumentSet" and an empty wsse:Security header containing a Timestamp and BinarySecurityToken that sign_envelope fills in and signs afterwards.

Returns consumer_retrieve_missing_document_id / consumer_retrieve_missing_repository_id on empty fields; envelope_serialization_failed if pugixml cannot serialize the document.

Definition at line 68 of file retrieve_envelope.cpp.

69 {
70 if (req.document_unique_id.empty()) {
71 return kcenon::common::make_error<built_envelope>(
72 static_cast<int>(
73 error_code::consumer_retrieve_missing_document_id),
74 "retrieve_request.document_unique_id is empty",
75 std::string(error_source));
76 }
77 if (req.repository_unique_id.empty()) {
78 return kcenon::common::make_error<built_envelope>(
79 static_cast<int>(
80 error_code::consumer_retrieve_missing_repository_id),
81 "retrieve_request.repository_unique_id is empty",
82 std::string(error_source));
83 }
84
85 pugi::xml_document doc;
86 auto decl = doc.append_child(pugi::node_declaration);
87 decl.append_attribute("version") = "1.0";
88 decl.append_attribute("encoding") = "UTF-8";
89
90 auto env = doc.append_child("soap:Envelope");
91 env.append_attribute("xmlns:soap") = kSoapEnv;
92 env.append_attribute("xmlns:wsa") = kWsa;
93 env.append_attribute("xmlns:wsse") = kWsse;
94 env.append_attribute("xmlns:wsu") = kWsu;
95 env.append_attribute("xmlns:xdsb") = kXdsb;
96
97 auto header = env.append_child("soap:Header");
98
99 auto action = header.append_child("wsa:Action");
100 action.append_attribute("soap:mustUnderstand") = "true";
101 action.text().set("urn:ihe:iti:2007:RetrieveDocumentSet");
102
103 auto message_id = header.append_child("wsa:MessageID");
104 message_id.text().set(("urn:uuid:" + make_xml_id("msg")).c_str());
105
106 auto security = header.append_child("wsse:Security");
107 security.append_attribute("soap:mustUnderstand") = "true";
108
109 built_envelope out{};
110 out.timestamp_id = make_xml_id("ts");
111 out.binary_security_token_id = make_xml_id("bst");
112 out.body_id = make_xml_id("body");
113
114 auto timestamp = security.append_child("wsu:Timestamp");
115 timestamp.append_attribute("wsu:Id") = out.timestamp_id.c_str();
116 auto created = timestamp.append_child("wsu:Created");
117 const std::string now = utc_iso8601_now();
118 created.text().set(now.c_str());
119
120 auto bst = security.append_child("wsse:BinarySecurityToken");
121 bst.append_attribute("wsu:Id") = out.binary_security_token_id.c_str();
122 bst.append_attribute("EncodingType") =
123 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-"
124 "security-1.0#Base64Binary";
125 bst.append_attribute("ValueType") =
126 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-"
127 "profile-1.0#X509v3";
128
129 auto body = env.append_child("soap:Body");
130 body.append_attribute("wsu:Id") = out.body_id.c_str();
131
132 auto rq = body.append_child("xdsb:RetrieveDocumentSetRequest");
133 auto doc_req = rq.append_child("xdsb:DocumentRequest");
134
135 // TODO(#1129-follow-up): emit xdsb:HomeCommunityId here when XCA
136 // Cross-Community Access support lands. ITI-43 intra-community
137 // retrieve (the scope of this PR) omits the element.
138 doc_req.append_child("xdsb:RepositoryUniqueId")
139 .text()
140 .set(req.repository_unique_id.c_str());
141 doc_req.append_child("xdsb:DocumentUniqueId")
142 .text()
143 .set(req.document_unique_id.c_str());
144
145 xml_string_writer writer;
146 doc.save(writer, "", pugi::format_raw | pugi::format_no_declaration,
147 pugi::encoding_utf8);
148 if (writer.out.empty()) {
149 return kcenon::common::make_error<built_envelope>(
150 static_cast<int>(error_code::envelope_serialization_failed),
151 "pugixml produced an empty envelope",
152 std::string(error_source));
153 }
154
155 out.xml = R"(<?xml version="1.0" encoding="UTF-8"?>)";
156 out.xml += writer.out;
157 return out;
158}
std::string out

References kcenon::pacs::ihe::xds::consumer_retrieve_missing_document_id, kcenon::pacs::ihe::xds::consumer_retrieve_missing_repository_id, kcenon::pacs::ihe::xds::retrieve_request::document_unique_id, kcenon::pacs::ihe::xds::envelope_serialization_failed, kcenon::pacs::ihe::xds::error_source, out, and kcenon::pacs::ihe::xds::retrieve_request::repository_unique_id.

Referenced by kcenon::pacs::ihe::xds::document_consumer::impl::retrieve().

Here is the caller graph for this function:

◆ clear_http_transport_override()

void kcenon::pacs::ihe::xds::detail::clear_http_transport_override ( )

Definition at line 111 of file http_client.cpp.

111 {
112 std::lock_guard<std::mutex> lock(override_mutex());
113 override_fn() = {};
114}

◆ http_post()

kcenon::common::Result< http_response > kcenon::pacs::ihe::xds::detail::http_post ( const http_options & opts,
const std::string & content_type,
const std::string & body )

POST body to opts.endpoint with the given Content-Type.

Adds a SOAPAction header, a User-Agent, and forwards the TLS settings from opts. Returns transport_curl_failed / transport_tls_error / transport_http_error with context in the message.

Definition at line 116 of file http_client.cpp.

118 {
119 if (opts.endpoint.empty()) {
120 return kcenon::common::make_error<http_response>(
121 static_cast<int>(error_code::transport_not_configured),
122 "http_options.endpoint is empty", std::string(error_source));
123 }
124
125 {
126 transport_override fn_copy;
127 {
128 std::lock_guard<std::mutex> lock(override_mutex());
129 fn_copy = override_fn();
130 }
131 if (fn_copy) {
132 transport_request req;
133 req.url = opts.endpoint;
134 req.content_type = content_type;
135 req.soap_action = opts.soap_action;
136 req.body = body;
137 req.verify_peer = opts.verify_peer;
138 return fn_copy(req);
139 }
140 }
141
142 // libcurl 7.84+ auto-inits on first curl_easy_init, but that implicit
143 // path is not thread-safe. Call curl_global_init once per process
144 // under a std::once_flag so concurrent first-use from different
145 // threads does not race. curl_global_cleanup is deliberately NOT
146 // paired here - libcurl documents that a single init for process
147 // lifetime is the supported pattern.
148 static std::once_flag s_curl_once;
149 std::call_once(s_curl_once, [] {
150 (void)curl_global_init(CURL_GLOBAL_DEFAULT);
151 });
152
153 curl_ptr curl(curl_easy_init());
154 if (!curl) {
155 return kcenon::common::make_error<http_response>(
156 static_cast<int>(error_code::transport_init_failed),
157 "curl_easy_init failed", std::string(error_source));
158 }
159
160 slist_ptr headers;
161 {
162 curl_slist* raw = nullptr;
163 const std::string ct_header = "Content-Type: " + content_type;
164 raw = curl_slist_append(raw, ct_header.c_str());
165 const std::string action_header =
166 "SOAPAction: \"" + opts.soap_action + "\"";
167 raw = curl_slist_append(raw, action_header.c_str());
168 raw = curl_slist_append(raw, "Accept: multipart/related, application/xop+xml, application/soap+xml");
169 raw = curl_slist_append(raw, "Expect:");
170 headers.reset(raw);
171 }
172
173 response_sink response;
174
175 curl_easy_setopt(curl.get(), CURLOPT_URL, opts.endpoint.c_str());
176 curl_easy_setopt(curl.get(), CURLOPT_POST, 1L);
177 curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, headers.get());
178 curl_easy_setopt(curl.get(), CURLOPT_USERAGENT, opts.user_agent.c_str());
179 curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, body.data());
180 curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDSIZE_LARGE,
181 static_cast<curl_off_t>(body.size()));
182 curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, write_callback);
183 curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response);
184 curl_easy_setopt(curl.get(), CURLOPT_HEADERFUNCTION, header_callback);
185 curl_easy_setopt(curl.get(), CURLOPT_HEADERDATA, &response);
186 curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 0L);
187 curl_easy_setopt(curl.get(), CURLOPT_NOSIGNAL, 1L);
188
189 curl_easy_setopt(curl.get(), CURLOPT_CONNECTTIMEOUT_MS,
190 static_cast<long>(opts.connect_timeout.count()));
191 curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT_MS,
192 static_cast<long>(opts.request_timeout.count()));
193
194 curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER,
195 opts.verify_peer ? 1L : 0L);
196 curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST,
197 opts.verify_host ? 2L : 0L);
198 if (!opts.ca_bundle_path.empty()) {
199 curl_easy_setopt(curl.get(), CURLOPT_CAINFO,
200 opts.ca_bundle_path.c_str());
201 }
202 if (!opts.client_certificate_path.empty()) {
203 curl_easy_setopt(curl.get(), CURLOPT_SSLCERT,
204 opts.client_certificate_path.c_str());
205 }
206 if (!opts.client_private_key_path.empty()) {
207 curl_easy_setopt(curl.get(), CURLOPT_SSLKEY,
208 opts.client_private_key_path.c_str());
209 }
210
211 const CURLcode rc = curl_easy_perform(curl.get());
212 if (rc != CURLE_OK) {
213 // write_callback returned 0 once the response exceeded
214 // kMaxResponseBytes, which libcurl surfaces as CURLE_WRITE_ERROR.
215 // Promote that specific case to a dedicated error code so callers
216 // can distinguish DoS protection from other transport failures.
217 if (rc == CURLE_WRITE_ERROR && response.oversize) {
218 return kcenon::common::make_error<http_response>(
219 static_cast<int>(error_code::transport_response_too_large),
220 "registry response exceeds " +
221 std::to_string(kMaxResponseBytes) +
222 " byte cap; transfer aborted",
223 std::string(error_source));
224 }
225 const bool tls =
226 rc == CURLE_PEER_FAILED_VERIFICATION || rc == CURLE_SSL_CONNECT_ERROR ||
227 rc == CURLE_SSL_CERTPROBLEM || rc == CURLE_SSL_CIPHER ||
228 rc == CURLE_SSL_ISSUER_ERROR || rc == CURLE_SSL_CACERT_BADFILE;
229 const auto code = tls ? error_code::transport_tls_error
230 : (rc == CURLE_OPERATION_TIMEDOUT
231 ? error_code::transport_timeout
232 : error_code::transport_curl_failed);
233 return kcenon::common::make_error<http_response>(
234 static_cast<int>(code),
235 std::string("libcurl error ") + std::to_string(static_cast<int>(rc)) +
236 ": " + curl_easy_strerror(rc),
237 std::string(error_source));
238 }
239
241 curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &out.status_code);
242 out.content_type = std::move(response.content_type);
243 out.body = std::move(response.buffer);
244
245 if (out.status_code < 200 || out.status_code >= 300) {
246 return kcenon::common::make_error<http_response>(
247 static_cast<int>(error_code::transport_http_error),
248 std::string("unexpected HTTP status ") +
249 std::to_string(out.status_code),
250 std::string(error_source));
251 }
252
253 return out;
254}
std::string content_type
@ tls
TLS over TCP (RFC 5425) — Secure.
std::string_view code
Simple HTTP response structure.
std::chrono::milliseconds connect_timeout
std::chrono::milliseconds request_timeout

References kcenon::pacs::ihe::xds::detail::transport_request::body, kcenon::pacs::ihe::xds::http_options::ca_bundle_path, kcenon::pacs::ihe::xds::http_options::client_certificate_path, kcenon::pacs::ihe::xds::http_options::client_private_key_path, code, kcenon::pacs::ihe::xds::http_options::connect_timeout, content_type, kcenon::pacs::ihe::xds::detail::transport_request::content_type, kcenon::pacs::ihe::xds::http_options::endpoint, kcenon::pacs::ihe::xds::error_source, out, kcenon::pacs::ihe::xds::http_options::request_timeout, kcenon::pacs::ihe::xds::detail::transport_request::soap_action, kcenon::pacs::ihe::xds::http_options::soap_action, kcenon::pacs::ihe::xds::transport_curl_failed, kcenon::pacs::ihe::xds::transport_http_error, kcenon::pacs::ihe::xds::transport_init_failed, kcenon::pacs::ihe::xds::transport_not_configured, kcenon::pacs::ihe::xds::transport_response_too_large, kcenon::pacs::ihe::xds::transport_timeout, kcenon::pacs::ihe::xds::transport_tls_error, kcenon::pacs::ihe::xds::detail::transport_request::url, kcenon::pacs::ihe::xds::http_options::user_agent, kcenon::pacs::ihe::xds::http_options::verify_host, kcenon::pacs::ihe::xds::detail::transport_request::verify_peer, and kcenon::pacs::ihe::xds::http_options::verify_peer.

Referenced by kcenon::pacs::ihe::xds::registry_query::impl::execute(), kcenon::pacs::ihe::xds::document_consumer::impl::retrieve(), and kcenon::pacs::ihe::xds::document_source::impl::submit().

Here is the caller graph for this function:

◆ package_mtom()

kcenon::common::Result< packaged_mtom > kcenon::pacs::ihe::xds::detail::package_mtom ( const std::string & envelope_xml,
const std::vector< mtom_part > & parts )

Assemble a multipart/related MTOM body.

envelope_xml is the signed SOAP 1.2 envelope; parts are the document binaries referenced from the envelope by xop:Include.

The boundary is generated from a thread-local PRNG and guaranteed not to appear in any of the inputs via a fallback re-seed if a collision is detected.

Definition at line 59 of file mtom_packager.cpp.

60 {
61 if (envelope_xml.empty()) {
62 return kcenon::common::make_error<packaged_mtom>(
63 static_cast<int>(error_code::mtom_empty_payload),
64 "envelope XML is empty", std::string(error_source));
65 }
66
67 std::string boundary;
68 bool ok = false;
69 for (int attempt = 0; attempt < 8; ++attempt) {
70 boundary = make_boundary();
71 if (contains(envelope_xml, boundary)) continue;
72 bool collides = false;
73 for (const auto& p : parts) {
74 if (bytes_contain(p.content, boundary)) {
75 collides = true;
76 break;
77 }
78 }
79 if (!collides) {
80 ok = true;
81 break;
82 }
83 }
84 if (!ok) {
85 return kcenon::common::make_error<packaged_mtom>(
86 static_cast<int>(error_code::mtom_boundary_generation_failed),
87 "failed to generate a collision-free MIME boundary",
88 std::string(error_source));
89 }
90
91 packaged_mtom out;
92 out.content_type =
93 std::string("multipart/related; boundary=\"") + boundary +
94 std::string("\"; type=\"application/xop+xml\"; start=\"<") +
95 kSoapStartCid +
96 std::string(">\"; start-info=\"application/soap+xml; action=\\\"urn:ihe:iti:2007:RegisterDocumentSet-b\\\"\"");
97
98 std::string body;
99 body.reserve(envelope_xml.size() + 4096);
100
101 body += "--";
102 body += boundary;
103 body += kCrLf;
104 body += "Content-Type: application/xop+xml; charset=UTF-8; type=\"application/soap+xml\"";
105 body += kCrLf;
106 body += "Content-Transfer-Encoding: 8bit";
107 body += kCrLf;
108 body += "Content-ID: <";
109 body += kSoapStartCid;
110 body += ">";
111 body += kCrLf;
112 body += kCrLf;
113 body += envelope_xml;
114 body += kCrLf;
115
116 for (const auto& p : parts) {
117 if (p.content_id.empty()) {
118 return kcenon::common::make_error<packaged_mtom>(
119 static_cast<int>(error_code::mtom_empty_payload),
120 "part has empty content-id", std::string(error_source));
121 }
122 if (p.content.empty()) {
123 return kcenon::common::make_error<packaged_mtom>(
124 static_cast<int>(error_code::mtom_empty_payload),
125 "part '" + p.content_id + "' has empty content",
126 std::string(error_source));
127 }
128
129 body += "--";
130 body += boundary;
131 body += kCrLf;
132 body += "Content-Type: ";
133 body += (p.mime_type.empty() ? "application/octet-stream" : p.mime_type);
134 body += kCrLf;
135 body += "Content-Transfer-Encoding: binary";
136 body += kCrLf;
137 body += "Content-ID: <";
138 body += p.content_id;
139 body += ">";
140 body += kCrLf;
141 body += kCrLf;
142 body.append(reinterpret_cast<const char*>(p.content.data()),
143 p.content.size());
144 body += kCrLf;
145 }
146
147 body += "--";
148 body += boundary;
149 body += "--";
150 body += kCrLf;
151
152 out.body = std::move(body);
153 return out;
154}

References kcenon::pacs::ihe::xds::error_source, kcenon::pacs::ihe::xds::mtom_boundary_generation_failed, kcenon::pacs::ihe::xds::mtom_empty_payload, and out.

Referenced by kcenon::pacs::ihe::xds::document_source::impl::submit().

Here is the caller graph for this function:

◆ parse_mtom_response()

kcenon::common::Result< parsed_mtom > kcenon::pacs::ihe::xds::detail::parse_mtom_response ( const std::string & content_type_header,
const std::string & body )

Parse a multipart/related MTOM response body.

Prefers the boundary parameter from content_type_header when it is non-empty and carries a boundary="..." (or bare boundary=...) attribute. Falls back to sniffing the first --<boundary> marker in body when the header is absent or malformed. The 8 MiB response cap in http_client.cpp bounds the walk in either path.

The body is split into parts, each part is stripped of its headers, and the first encountered part is treated as the root envelope.

Non-MTOM responses (the repository returned plain SOAP with no multipart framing) are detected and returned with root_xml set to the whole body and parts empty, so the caller can still invoke the signer and response parser uniformly. This matches the fallback behavior of Apache CXF repositories that omit MTOM when no attachments are present.

Definition at line 257 of file mtom_packager.cpp.

258 {
260
261 if (body.empty()) {
262 return kcenon::common::make_error<parsed_mtom>(
263 static_cast<int>(error_code::consumer_response_mtom_malformed),
264 "response body is empty", std::string(error_source));
265 }
266
267 // Plain SOAP (no multipart framing) fallback: if the body starts with
268 // "<?xml" or "<soap" it is the envelope itself and we surface it as a
269 // root-only parsed_mtom. This keeps the caller's pipeline uniform.
270 {
271 std::size_t leading = 0;
272 while (leading < body.size() &&
273 (body[leading] == ' ' || body[leading] == '\t' ||
274 body[leading] == '\r' || body[leading] == '\n')) {
275 ++leading;
276 }
277 if (body.compare(leading, 5, "<?xml") == 0 ||
278 body.compare(leading, 5, "<soap") == 0 ||
279 body.compare(leading, 14, "<soap:Envelope") == 0) {
280 out.root_xml = body;
281 return out;
282 }
283 }
284
285 // Prefer the Content-Type header's boundary parameter - this is the
286 // authoritative source per RFC 2387. The body-sniffing fallback in
287 // detect_boundary only runs when the header was absent or missing a
288 // boundary parameter, which happens in closed test harnesses that
289 // bypass libcurl via the transport override.
290 std::string boundary = extract_boundary_from_header(content_type_header);
291 if (boundary.empty()) {
292 boundary = detect_boundary(body);
293 }
294 if (boundary.empty()) {
295 return kcenon::common::make_error<parsed_mtom>(
296 static_cast<int>(error_code::consumer_response_mtom_malformed),
297 "no MIME boundary detected in response body or Content-Type",
298 std::string(error_source));
299 }
300
301 const std::string delim = "--" + boundary;
302
303 // Split body on every occurrence of "--<boundary>". The last segment
304 // before "--<boundary>--" is the final part; anything after the closer
305 // is the epilogue (ignored).
306 std::vector<std::string> segments;
307 std::size_t cursor = 0;
308 while (cursor < body.size()) {
309 const auto next = body.find(delim, cursor);
310 if (next == std::string::npos) break;
311 if (next > cursor) {
312 segments.push_back(body.substr(cursor, next - cursor));
313 }
314 cursor = next + delim.size();
315 // Close marker: "--<boundary>--"
316 if (cursor + 2 <= body.size() && body[cursor] == '-' &&
317 body[cursor + 1] == '-') {
318 cursor += 2;
319 break;
320 }
321 }
322 if (segments.empty()) {
323 // The preamble before the first boundary is allowed by RFC 2046,
324 // so an "empty" segments list only means the body had no proper
325 // first-part marker.
326 return kcenon::common::make_error<parsed_mtom>(
327 static_cast<int>(error_code::consumer_response_mtom_malformed),
328 "multipart body yielded zero parts",
329 std::string(error_source));
330 }
331
332 // Discard any preamble-only first segment that isn't actually a part
333 // (i.e. the bytes between the MIME body start and the first "--B").
334 // Our parser starts cursor at 0, so the first segment is whatever
335 // preceded the first boundary - usually empty after CRLFs. A real part
336 // always has a header-body split we can detect.
337 bool first_part_consumed = false;
338 for (auto& seg : segments) {
339 // A valid MIME part starts with CRLF, then headers, then CRLF CRLF,
340 // then content, then CRLF. Strip the leading CRLF if present.
341 std::size_t start = 0;
342 while (start < seg.size() &&
343 (seg[start] == '\r' || seg[start] == '\n')) {
344 ++start;
345 }
346 if (start >= seg.size()) continue;
347
348 const auto header_end = seg.find("\r\n\r\n", start);
349 const std::size_t content_start =
350 (header_end == std::string::npos) ? seg.size() : header_end + 4;
351 if (content_start > seg.size()) continue;
352
353 // Parse headers.
354 const std::string_view header_block(
355 seg.data() + start,
356 (header_end == std::string::npos ? seg.size() - start
357 : header_end - start));
358 std::string mime_type;
359 std::string content_id;
360 std::size_t pos = 0;
361 while (pos < header_block.size()) {
362 const auto eol = header_block.find("\r\n", pos);
363 const std::size_t line_end =
364 (eol == std::string::npos) ? header_block.size() : eol;
365 const std::string_view line = header_block.substr(pos, line_end - pos);
366 if (iequals_prefix(line, "Content-Type:")) {
367 mime_type = trim(line.substr(std::strlen("Content-Type:")));
368 const auto semi = mime_type.find(';');
369 if (semi != std::string::npos) {
370 mime_type = trim(std::string_view(mime_type).substr(0, semi));
371 }
372 } else if (iequals_prefix(line, "Content-ID:")) {
373 content_id = strip_cid_brackets(
374 line.substr(std::strlen("Content-ID:")));
375 }
376 if (eol == std::string::npos) break;
377 pos = eol + 2;
378 }
379
380 // Extract content, trimming the trailing CRLF that precedes the
381 // next boundary delimiter.
382 std::size_t content_end = seg.size();
383 while (content_end > content_start &&
384 (seg[content_end - 1] == '\r' ||
385 seg[content_end - 1] == '\n')) {
386 --content_end;
387 }
388 if (content_end < content_start) content_end = content_start;
389 const std::size_t content_len = content_end - content_start;
390
391 if (!first_part_consumed) {
392 out.root_xml.assign(seg, content_start, content_len);
393 first_part_consumed = true;
394 continue;
395 }
396 mtom_part mp;
397 mp.content_id = std::move(content_id);
398 mp.mime_type = std::move(mime_type);
399 mp.content.assign(
400 reinterpret_cast<const std::uint8_t*>(seg.data() + content_start),
401 reinterpret_cast<const std::uint8_t*>(seg.data() + content_end));
402 out.parts.push_back(std::move(mp));
403 }
404
405 if (out.root_xml.empty()) {
406 return kcenon::common::make_error<parsed_mtom>(
407 static_cast<int>(error_code::consumer_response_missing_mtom_root),
408 "multipart body had no root part with XML payload",
409 std::string(error_source));
410 }
411
412 return out;
413}
Result of parsing an inbound multipart/related MTOM response.

References kcenon::pacs::ihe::xds::consumer_response_missing_mtom_root, kcenon::pacs::ihe::xds::consumer_response_mtom_malformed, kcenon::pacs::ihe::xds::detail::mtom_part::content, kcenon::pacs::ihe::xds::detail::mtom_part::content_id, kcenon::pacs::ihe::xds::error_source, kcenon::pacs::ihe::xds::detail::mtom_part::mime_type, and out.

Referenced by kcenon::pacs::ihe::xds::document_consumer::impl::retrieve().

Here is the caller graph for this function:

◆ parse_query_response()

kcenon::common::Result< registry_query_result > kcenon::pacs::ihe::xds::detail::parse_query_response ( const std::string & root_xml)

Parse an AdhocQueryResponse envelope into a registry_query_result.

root_xml is the response envelope (already signature-verified).

Returns registry_failure_response when the registry reports a Failure status. Returns registry_partial_success on PartialSuccess - callers wanting to tolerate that state must inspect the status string of the returned error context.

An empty RegistryObjectList on Success is a successful empty result, not an error: the function returns a registry_query_result with an empty entries vector.

Definition at line 146 of file query_response_parser.cpp.

147 {
148 if (root_xml.size() > kMaxRootXmlBytes) {
149 return kcenon::common::make_error<registry_query_result>(
150 static_cast<int>(error_code::transport_response_too_large),
151 "AdhocQueryResponse root part exceeds 8 MiB cap (" +
152 std::to_string(root_xml.size()) + " bytes)",
153 std::string(error_source));
154 }
155
156 pugi::xml_document doc;
157 auto parse = doc.load_buffer(root_xml.data(), root_xml.size(),
158 pugi::parse_default, pugi::encoding_utf8);
159 if (!parse) {
160 return kcenon::common::make_error<registry_query_result>(
161 static_cast<int>(error_code::transport_invalid_response),
162 std::string("failed to parse response envelope: ") +
163 parse.description(),
164 std::string(error_source));
165 }
166
167 // Use local-name() selectors so we do not depend on the specific
168 // namespace prefix the registry emits. ebRS stacks (Apache CXF,
169 // .NET WIF, Gazelle, Open Health Tools) pick different prefixes for
170 // the query and rim namespaces.
171 auto adhoc_response =
172 doc.select_node(
173 "//*[local-name()='AdhocQueryResponse']").node();
174 if (!adhoc_response) {
175 return kcenon::common::make_error<registry_query_result>(
176 static_cast<int>(error_code::registry_query_response_malformed),
177 "response contains no AdhocQueryResponse element",
178 std::string(error_source));
179 }
180
181 const std::string status =
182 adhoc_response.attribute("status").as_string();
183 if (status.empty()) {
184 return kcenon::common::make_error<registry_query_result>(
185 static_cast<int>(error_code::registry_query_response_malformed),
186 "AdhocQueryResponse is missing status attribute",
187 std::string(error_source));
188 }
189 // PartialSuccess means "some entries returned with warnings" in
190 // IHE ITI-18 - the registry has produced a valid RegistryObjectList
191 // alongside a RegistryErrorList. An earlier draft returned
192 // registry_partial_success as an error, which dropped the entries
193 // and contradicted the public-header contract that callers branch on
194 // entries.empty() and inspect registry_response_status. Treat
195 // PartialSuccess as a non-fatal status: fall through to the
196 // ExtrinsicObject walk and surface the raw status string in the
197 // returned result so callers can distinguish Success from
198 // PartialSuccess if their policy differs (review M2).
199 if (status != kSuccessStatus && status != kPartialSuccessStatus) {
200 return kcenon::common::make_error<registry_query_result>(
201 static_cast<int>(error_code::registry_failure_response),
202 "registry reported non-success status: " + status,
203 std::string(error_source));
204 }
205
206 registry_query_result out;
207 out.registry_response_status = status;
208
209 // Narrow child xpath (review m6): match ExtrinsicObject children of
210 // RegistryObjectList only, not arbitrary descendants. This avoids
211 // picking up nested ExtrinsicObject elements that could appear inside
212 // a RegistryError's detail payload on non-conformant responses.
213 // The RegistryObjectList is optional when no entries match; a Success
214 // response with zero results is valid and maps to out.entries.empty().
215 for (auto node : adhoc_response.select_nodes(
216 "*[local-name()='RegistryObjectList']/"
217 "*[local-name()='ExtrinsicObject']")) {
218 out.entries.push_back(extract_entry(node.node()));
219 }
220
221 return out;
222}

References kcenon::pacs::ihe::xds::error_source, out, kcenon::pacs::ihe::xds::registry_failure_response, kcenon::pacs::ihe::xds::registry_query_response_malformed, kcenon::pacs::ihe::xds::transport_invalid_response, and kcenon::pacs::ihe::xds::transport_response_too_large.

Referenced by kcenon::pacs::ihe::xds::registry_query::impl::execute().

Here is the caller graph for this function:

◆ parse_retrieve_response()

kcenon::common::Result< document_response > kcenon::pacs::ihe::xds::detail::parse_retrieve_response ( const std::string & root_xml,
const std::vector< mtom_part > & parts,
const retrieve_request & req )

Parse a RetrieveDocumentSetResponse envelope and resolve the attachment that matches req.

root_xml is the root part of the MTOM response (already verified). parts are the remaining attachment parts, keyed on the bare content-id that the xop:Include elements reference via "cid:".

Returns consumer_response_document_not_found when the registry reports a Failure status, when no DocumentResponse matches the requested document_unique_id, or when the matching xop:Include points to a cid that is not present in parts.

Definition at line 52 of file retrieve_response_parser.cpp.

54 {
55 if (root_xml.size() > kMaxRootXmlBytes) {
56 return kcenon::common::make_error<document_response>(
57 static_cast<int>(error_code::transport_response_too_large),
58 "RetrieveDocumentSetResponse root part exceeds 8 MiB cap (" +
59 std::to_string(root_xml.size()) + " bytes)",
60 std::string(error_source));
61 }
62
63 pugi::xml_document doc;
64 auto parse = doc.load_buffer(root_xml.data(), root_xml.size(),
65 pugi::parse_default, pugi::encoding_utf8);
66 if (!parse) {
67 return kcenon::common::make_error<document_response>(
68 static_cast<int>(error_code::transport_invalid_response),
69 std::string("failed to parse response envelope: ") +
70 parse.description(),
71 std::string(error_source));
72 }
73
74 // Use local-name() selectors so we do not depend on the specific
75 // namespace prefix the repository emits. Different XDS.b stacks
76 // (Apache CXF, .NET WIF, Gazelle) pick different prefixes for the
77 // xdsb namespace.
78 auto rds_response =
79 doc.select_node(
80 "//*[local-name()='RetrieveDocumentSetResponse']").node();
81 if (!rds_response) {
82 return kcenon::common::make_error<document_response>(
83 static_cast<int>(error_code::transport_invalid_response),
84 "response contains no RetrieveDocumentSetResponse element",
85 std::string(error_source));
86 }
87
88 auto registry_response =
89 rds_response.select_node("*[local-name()='RegistryResponse']").node();
90 if (!registry_response) {
91 return kcenon::common::make_error<document_response>(
92 static_cast<int>(error_code::transport_invalid_response),
93 "response has no RegistryResponse",
94 std::string(error_source));
95 }
96 const std::string status =
97 registry_response.attribute("status").as_string();
98 if (status.empty()) {
99 return kcenon::common::make_error<document_response>(
100 static_cast<int>(error_code::transport_invalid_response),
101 "RegistryResponse is missing status attribute",
102 std::string(error_source));
103 }
104 if (status == kPartialSuccessStatus) {
105 return kcenon::common::make_error<document_response>(
106 static_cast<int>(error_code::registry_partial_success),
107 "repository reported PartialSuccess: " + status,
108 std::string(error_source));
109 }
110 if (status != kSuccessStatus) {
111 // A Failure here means the repository could not locate the
112 // requested document - either the uid is unknown or the requester
113 // has no access. Either way, surface as not-found to the caller.
114 return kcenon::common::make_error<document_response>(
115 static_cast<int>(error_code::consumer_response_document_not_found),
116 "repository reported non-success status: " + status,
117 std::string(error_source));
118 }
119
120 // Walk each DocumentResponse looking for the requested uid. Use
121 // local-name() selectors throughout because repositories emit the
122 // xdsb namespace with different prefixes (xdsb, ns2, ihe-xdsb, etc.).
123 pugi::xml_node match;
124 for (auto dr :
125 rds_response.select_nodes("*[local-name()='DocumentResponse']")) {
126 auto node = dr.node();
127 const std::string uid =
128 node.select_node("*[local-name()='DocumentUniqueId']")
129 .node()
130 .text()
131 .as_string();
132 if (uid == req.document_unique_id) {
133 match = node;
134 break;
135 }
136 }
137 if (!match) {
138 return kcenon::common::make_error<document_response>(
139 static_cast<int>(error_code::consumer_response_document_not_found),
140 "no DocumentResponse matched requested uid " +
142 std::string(error_source));
143 }
144
145 const std::string mime_type =
146 match.select_node("*[local-name()='mimeType']").node().text().as_string();
147
148 auto document_node =
149 match.select_node("*[local-name()='Document']").node();
150 if (!document_node) {
151 return kcenon::common::make_error<document_response>(
152 static_cast<int>(error_code::consumer_response_document_not_found),
153 "matching DocumentResponse has no Document element",
154 std::string(error_source));
155 }
156
157 // Prefer resolving an xop:Include reference to one of the MTOM
158 // attachment parts. Fall back to base64-inlined Document text when the
159 // repository returned a non-MTOM response (some stacks do that for
160 // small payloads or test responders).
161 auto include =
162 document_node.select_node("*[local-name()='Include']").node();
163
164 document_response out;
165 out.registry_response_status = status;
166 out.document_unique_id = req.document_unique_id;
167 out.repository_unique_id = req.repository_unique_id;
168 out.mime_type = mime_type;
169
170 if (include) {
171 const std::string raw_href = include.attribute("href").as_string();
172 const std::string cid = strip_cid_href(raw_href);
173 if (cid.empty()) {
174 return kcenon::common::make_error<document_response>(
175 static_cast<int>(
176 error_code::consumer_response_document_not_found),
177 "xop:Include has empty href",
178 std::string(error_source));
179 }
180 const mtom_part* found = nullptr;
181 for (const auto& p : parts) {
182 if (p.content_id == cid) {
183 found = &p;
184 break;
185 }
186 }
187 if (found == nullptr) {
188 return kcenon::common::make_error<document_response>(
189 static_cast<int>(
190 error_code::consumer_response_document_not_found),
191 "xop:Include references unknown cid: " + cid,
192 std::string(error_source));
193 }
194 out.content = found->content;
195 if (out.mime_type.empty()) {
196 out.mime_type = found->mime_type;
197 }
198 return out;
199 }
200
201 // Non-MTOM fallback: Document carries inline base64.
202 const std::string inline_b64 = document_node.text().as_string();
203 if (inline_b64.empty()) {
204 return kcenon::common::make_error<document_response>(
205 static_cast<int>(error_code::consumer_response_document_not_found),
206 "Document element has neither xop:Include nor inline content",
207 std::string(error_source));
208 }
209 // Decode the inline base64 so document_response.content has the
210 // same "already-decoded binary bytes" semantics regardless of
211 // whether the repository used MTOM or inline-base64 framing. Review
212 // Minor-1 fix: avoid divergent caller expectations.
213 out.content = base64_decode_bytes(inline_b64);
214 if (out.content.empty()) {
215 return kcenon::common::make_error<document_response>(
216 static_cast<int>(error_code::consumer_response_document_not_found),
217 "inline Document base64 decode failed or yielded empty bytes",
218 std::string(error_source));
219 }
220 return out;
221}
constexpr dicom_tag status
Status.
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.
std::string_view uid
pugi::xml_node match

References base64_decode_bytes(), kcenon::pacs::ihe::xds::consumer_response_document_not_found, kcenon::pacs::ihe::xds::detail::mtom_part::content, kcenon::pacs::ihe::xds::retrieve_request::document_unique_id, kcenon::pacs::ihe::xds::error_source, match, kcenon::pacs::ihe::xds::detail::mtom_part::mime_type, out, kcenon::pacs::ihe::xds::registry_partial_success, kcenon::pacs::ihe::xds::retrieve_request::repository_unique_id, kcenon::pacs::ihe::xds::transport_invalid_response, kcenon::pacs::ihe::xds::transport_response_too_large, and uid.

Referenced by kcenon::pacs::ihe::xds::document_consumer::impl::retrieve().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ set_http_transport_override()

void kcenon::pacs::ihe::xds::detail::set_http_transport_override ( transport_override fn)

Definition at line 106 of file http_client.cpp.

106 {
107 std::lock_guard<std::mutex> lock(override_mutex());
108 override_fn() = std::move(fn);
109}

◆ sign_envelope()

kcenon::common::Result< bool > kcenon::pacs::ihe::xds::detail::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.

cert_pem is the PEM-encoded X.509 signer certificate. key_pem is the matching PEM RSA or EC private key; key_password decrypts it when the key is encrypted and is ignored otherwise.

The envelope's BinarySecurityToken is populated with the DER-encoded certificate (Base64). A ds:Signature element is appended to the wsse:Security header covering the body and timestamp by wsu:Id reference.

On success, env.xml is replaced by the signed envelope and the Result<bool> value is true (the bool is a sentinel; the signature itself is inside env.xml).

Definition at line 206 of file wss_signer.cpp.

209 {
210 if (cert_pem.empty()) {
211 return kcenon::common::make_error<bool>(
212 static_cast<int>(error_code::signer_invalid_certificate),
213 "certificate PEM is empty", std::string(error_source));
214 }
215 if (key_pem.empty()) {
216 return kcenon::common::make_error<bool>(
217 static_cast<int>(error_code::signer_invalid_private_key),
218 "private key PEM is empty", std::string(error_source));
219 }
220
221 auto cert = load_cert(cert_pem);
222 if (!cert) {
223 return kcenon::common::make_error<bool>(
224 static_cast<int>(error_code::signer_invalid_certificate),
225 "PEM_read_bio_X509 failed", std::string(error_source));
226 }
227 auto key = load_key(key_pem, key_password);
228 if (!key) {
229 return kcenon::common::make_error<bool>(
230 static_cast<int>(error_code::signer_invalid_private_key),
231 "PEM_read_bio_PrivateKey failed", std::string(error_source));
232 }
233
234 const auto der = cert_to_der(cert.get());
235 if (der.empty()) {
236 return kcenon::common::make_error<bool>(
237 static_cast<int>(error_code::signer_invalid_certificate),
238 "i2d_X509 failed", std::string(error_source));
239 }
240 const std::string cert_b64 = base64_encode(der.data(), der.size());
241 if (cert_b64.empty()) {
242 return kcenon::common::make_error<bool>(
243 static_cast<int>(error_code::signer_invalid_certificate),
244 "base64 of DER certificate failed", std::string(error_source));
245 }
246
247 pugi::xml_document doc;
248 auto parse_result =
249 doc.load_buffer(env.xml.data(), env.xml.size(),
250 pugi::parse_default, pugi::encoding_utf8);
251 if (!parse_result) {
252 return kcenon::common::make_error<bool>(
253 static_cast<int>(error_code::envelope_build_failed),
254 std::string("failed to reparse envelope: ") +
255 parse_result.description(),
256 std::string(error_source));
257 }
258
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");
265
266 if (!envelope || !header || !security || !bst || !timestamp || !body) {
267 return kcenon::common::make_error<bool>(
268 static_cast<int>(error_code::envelope_build_failed),
269 "envelope is missing required WS-Security scaffolding",
270 std::string(error_source));
271 }
272
273 bst.text().set(cert_b64.c_str());
274
275 const std::string body_c14n = serialize_subtree(body);
276 const std::string ts_c14n = serialize_subtree(timestamp);
277
278 if (body_c14n.empty() || ts_c14n.empty()) {
279 return kcenon::common::make_error<bool>(
280 static_cast<int>(error_code::signer_digest_failed),
281 "serialized Body or Timestamp is empty",
282 std::string(error_source));
283 }
284
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>(
289 static_cast<int>(error_code::signer_digest_failed),
290 "SHA-256 digest failed", std::string(error_source));
291 }
292
293 auto signature = security.append_child("ds:Signature");
294 signature.append_attribute("xmlns:ds") = "http://www.w3.org/2000/09/xmldsig#";
295
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";
302
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());
314 };
315 add_reference(env.body_id, body_digest);
316 add_reference(env.timestamp_id, ts_digest);
317
318 const std::string signed_info_bytes = serialize_subtree(signed_info);
319 if (signed_info_bytes.empty()) {
320 return kcenon::common::make_error<bool>(
321 static_cast<int>(error_code::signer_sign_failed),
322 "serialized SignedInfo is empty", std::string(error_source));
323 }
324
325 md_ctx_ptr md(EVP_MD_CTX_new());
326 if (!md) {
327 return kcenon::common::make_error<bool>(
328 static_cast<int>(error_code::signer_sign_failed),
329 "EVP_MD_CTX_new failed", std::string(error_source));
330 }
331 if (EVP_DigestSignInit(md.get(), nullptr, EVP_sha256(), nullptr,
332 key.get()) != 1) {
333 return kcenon::common::make_error<bool>(
334 static_cast<int>(error_code::signer_sign_failed),
335 "EVP_DigestSignInit failed", std::string(error_source));
336 }
337 if (EVP_DigestSignUpdate(
338 md.get(), signed_info_bytes.data(), signed_info_bytes.size()) != 1) {
339 return kcenon::common::make_error<bool>(
340 static_cast<int>(error_code::signer_sign_failed),
341 "EVP_DigestSignUpdate failed", std::string(error_source));
342 }
343
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>(
347 static_cast<int>(error_code::signer_sign_failed),
348 "EVP_DigestSignFinal probe failed", std::string(error_source));
349 }
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>(
353 static_cast<int>(error_code::signer_sign_failed),
354 "EVP_DigestSignFinal failed", std::string(error_source));
355 }
356 sig.resize(sig_len);
357 const std::string sig_b64 = base64_encode(sig.data(), sig.size());
358
359 signature.append_child("ds:SignatureValue").text().set(sig_b64.c_str());
360
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");
364 ref.append_attribute("URI") = ("#" + env.binary_security_token_id).c_str();
365 ref.append_attribute("ValueType") =
366 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-"
367 "profile-1.0#X509v3";
368
369 xml_string_writer w;
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;
373 return true;
374}
std::string base64_encode(std::string_view input)
Encode data to Base64.

References base64_encode(), kcenon::pacs::ihe::xds::detail::built_envelope::binary_security_token_id, kcenon::pacs::ihe::xds::detail::built_envelope::body_id, kcenon::pacs::ihe::xds::envelope_build_failed, kcenon::pacs::ihe::xds::error_source, kcenon::pacs::ihe::xds::signer_digest_failed, kcenon::pacs::ihe::xds::signer_invalid_certificate, kcenon::pacs::ihe::xds::signer_invalid_private_key, kcenon::pacs::ihe::xds::signer_sign_failed, kcenon::pacs::ihe::xds::detail::built_envelope::timestamp_id, and kcenon::pacs::ihe::xds::detail::built_envelope::xml.

Referenced by kcenon::pacs::ihe::xds::registry_query::impl::execute(), kcenon::pacs::ihe::xds::document_consumer::impl::retrieve(), and kcenon::pacs::ihe::xds::document_source::impl::submit().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ verify_envelope_integrity()

kcenon::common::Result< bool > kcenon::pacs::ihe::xds::detail::verify_envelope_integrity ( std::string_view signed_xml)

Verify the structural integrity of a previously signed envelope against the certificate embedded in its own BinarySecurityToken.

Reparses signed_xml, locates the ds:Signature in wsse:Security, decodes the BinarySecurityToken into an X.509 public key, re-digests Body and Timestamp with the same pugixml format_raw serializer used for signing, and calls EVP_DigestVerify against the SignedInfo bytes.

SECURITY NOTE - SELF-AUTHENTICATING CHECK, NOT TRUST VALIDATION: this function verifies that the ds:Signature binds the payload to the certificate embedded in the response's BST. It does NOT validate that certificate against a trust anchor, CRL, OCSP responder, or peer TLS identity. A man-in-the-middle who can serve any valid X.509 certificate can produce a response that passes this check - the embedded key, the embedded signature, and the embedded payload are all self-consistent by construction. Full trust-chain validation will land with the Gazelle conformance integration tracked in #1131. Until that lands, callers relying solely on this function for authenticity guarantees are trusting whatever party the TLS peer is.

The rename from verify_envelope to verify_envelope_integrity in the #1129 review cycle is deliberate: the new name closes the "what does this actually guarantee" gap at the call site.

Honest-URI policy: the CanonicalizationMethod and every Transform Algorithm URI inside SignedInfo MUST equal "urn:kcenon:xds:c14n:pugixml-format-raw-v1". If any Algorithm advertises exc-c14n or any other URI, verification is refused with consumer_signature_verification_failed rather than silently digesting a mismatched byte stream. This mirrors the signer's behavior.

Canonicalization caveat: verification is only reproducible for envelopes produced by this project's sign_envelope. Real exc-c14n interop is a follow-up issue that must land before production use against a third-party IHE XDS.b repository.

Returns consumer_signature_missing if the envelope carries no ds:Signature element; consumer_signature_verification_failed on any other mismatch (digest, signature, algorithm, KeyInfo shape).

Definition at line 413 of file wss_signer.cpp.

414 {
415 if (signed_xml.empty()) {
416 return kcenon::common::make_error<bool>(
417 static_cast<int>(
418 error_code::consumer_signature_verification_failed),
419 "signed envelope is empty", std::string(error_source));
420 }
421
422 pugi::xml_document doc;
423 auto parse = doc.load_buffer(signed_xml.data(), signed_xml.size(),
424 pugi::parse_default, pugi::encoding_utf8);
425 if (!parse) {
426 return kcenon::common::make_error<bool>(
427 static_cast<int>(
428 error_code::consumer_signature_verification_failed),
429 std::string("failed to reparse signed envelope: ") +
430 parse.description(),
431 std::string(error_source));
432 }
433
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");
439
440 if (!envelope || !header || !security) {
441 return kcenon::common::make_error<bool>(
442 static_cast<int>(
443 error_code::consumer_signature_verification_failed),
444 "envelope lacks WS-Security scaffolding",
445 std::string(error_source));
446 }
447 if (!signature) {
448 return kcenon::common::make_error<bool>(
449 static_cast<int>(error_code::consumer_signature_missing),
450 "envelope has no ds:Signature element",
451 std::string(error_source));
452 }
453 if (!bst) {
454 return kcenon::common::make_error<bool>(
455 static_cast<int>(
456 error_code::consumer_signature_verification_failed),
457 "envelope has no wsse:BinarySecurityToken",
458 std::string(error_source));
459 }
460
461 // Enforce honest-URI policy on every Algorithm attribute within
462 // SignedInfo before trusting the digests.
463 auto signed_info = signature.child("ds:SignedInfo");
464 if (!signed_info) {
465 return kcenon::common::make_error<bool>(
466 static_cast<int>(
467 error_code::consumer_signature_verification_failed),
468 "Signature has no SignedInfo", std::string(error_source));
469 }
470 auto c14n = signed_info.child("ds:CanonicalizationMethod");
471 if (!c14n ||
472 std::string(c14n.attribute("Algorithm").as_string()) != kKcenonC14nUri) {
473 return kcenon::common::make_error<bool>(
474 static_cast<int>(
475 error_code::consumer_signature_verification_failed),
476 "CanonicalizationMethod is not the kcenon project-local URI",
477 std::string(error_source));
478 }
479
480 // Load public key from BST.
481 const std::string bst_b64 = strip_b64_whitespace(bst.text().as_string());
482 const auto der = base64_decode(bst_b64);
483 if (der.empty()) {
484 return kcenon::common::make_error<bool>(
485 static_cast<int>(
486 error_code::consumer_signature_verification_failed),
487 "BinarySecurityToken base64 decode failed",
488 std::string(error_source));
489 }
490
491 // SECURITY NOTE: the X.509 we are about to pull out of the BST is
492 // used only to recover a public key for EVP_DigestVerify; it is NOT
493 // validated against a trust anchor. See the Doxygen on
494 // verify_envelope_integrity for the deferral and tracking issue.
495
496 const unsigned char* der_ptr = der.data();
497 x509_ptr cert(d2i_X509(nullptr, &der_ptr, static_cast<long>(der.size())));
498 if (!cert) {
499 return kcenon::common::make_error<bool>(
500 static_cast<int>(
501 error_code::consumer_signature_verification_failed),
502 "d2i_X509 failed on BinarySecurityToken",
503 std::string(error_source));
504 }
505 pkey_ptr pkey(X509_get_pubkey(cert.get()));
506 if (!pkey) {
507 return kcenon::common::make_error<bool>(
508 static_cast<int>(
509 error_code::consumer_signature_verification_failed),
510 "X509_get_pubkey failed", std::string(error_source));
511 }
512
513 // Recompute digests for every Reference in SignedInfo and compare
514 // against the advertised DigestValue.
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()) !=
520 kKcenonC14nUri) {
521 return kcenon::common::make_error<bool>(
522 static_cast<int>(
523 error_code::consumer_signature_verification_failed),
524 "Reference Transform is not the kcenon project-local URI",
525 std::string(error_source));
526 }
527 }
528 const std::string uri = strip_uri_fragment(
529 ref.attribute("URI").as_string());
530 if (uri.empty()) {
531 return kcenon::common::make_error<bool>(
532 static_cast<int>(
533 error_code::consumer_signature_verification_failed),
534 "Reference URI is empty", std::string(error_source));
535 }
536 auto target = find_by_wsu_id(envelope, uri);
537 if (!target) {
538 return kcenon::common::make_error<bool>(
539 static_cast<int>(
540 error_code::consumer_signature_verification_failed),
541 "Reference target not found: " + uri,
542 std::string(error_source));
543 }
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>(
550 static_cast<int>(
551 error_code::consumer_signature_verification_failed),
552 "DigestValue mismatch for reference: " + uri,
553 std::string(error_source));
554 }
555 }
556
557 // Verify the signature over SignedInfo.
558 const std::string signed_info_bytes = serialize_subtree(signed_info);
559 if (signed_info_bytes.empty()) {
560 return kcenon::common::make_error<bool>(
561 static_cast<int>(
562 error_code::consumer_signature_verification_failed),
563 "serialized SignedInfo is empty", std::string(error_source));
564 }
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>(
569 static_cast<int>(
570 error_code::consumer_signature_verification_failed),
571 "SignatureValue base64 decode failed",
572 std::string(error_source));
573 }
574
575 md_ctx_ptr md(EVP_MD_CTX_new());
576 if (!md) {
577 return kcenon::common::make_error<bool>(
578 static_cast<int>(
579 error_code::consumer_signature_verification_failed),
580 "EVP_MD_CTX_new failed", std::string(error_source));
581 }
582 if (EVP_DigestVerifyInit(md.get(), nullptr, EVP_sha256(), nullptr,
583 pkey.get()) != 1) {
584 return kcenon::common::make_error<bool>(
585 static_cast<int>(
586 error_code::consumer_signature_verification_failed),
587 "EVP_DigestVerifyInit failed", std::string(error_source));
588 }
589 if (EVP_DigestVerifyUpdate(md.get(), signed_info_bytes.data(),
590 signed_info_bytes.size()) != 1) {
591 return kcenon::common::make_error<bool>(
592 static_cast<int>(
593 error_code::consumer_signature_verification_failed),
594 "EVP_DigestVerifyUpdate failed", std::string(error_source));
595 }
596 const int verdict =
597 EVP_DigestVerifyFinal(md.get(), sig_bytes.data(), sig_bytes.size());
598 if (verdict != 1) {
599 return kcenon::common::make_error<bool>(
600 static_cast<int>(
601 error_code::consumer_signature_verification_failed),
602 "EVP_DigestVerifyFinal rejected the signature",
603 std::string(error_source));
604 }
605 return true;
606}
bool base64_decode(const std::string &in, std::vector< std::uint8_t > &out)
Standard base64 decode; returns false on malformed input.

References kcenon::pacs::ihe::xds::consumer_signature_missing, kcenon::pacs::ihe::xds::consumer_signature_verification_failed, and kcenon::pacs::ihe::xds::error_source.

Referenced by kcenon::pacs::ihe::xds::registry_query::impl::execute(), and kcenon::pacs::ihe::xds::document_consumer::impl::retrieve().

Here is the caller graph for this function: