1"""Functions for WS-Security (WSSE) signature creation and verification. 2 3Heavily based on test examples in https://github.com/mehcode/python-xmlsec as 4well as the xmlsec documentation at https://www.aleksey.com/xmlsec/. 5 6Reading the xmldsig, xmlenc, and ws-security standards documents, though 7admittedly painful, will likely assist in understanding the code in this 8module. 9 10""" 11from lxml import etree 12from lxml.etree import QName 13 14from zeep import ns 15from zeep.exceptions import SignatureVerificationFailed 16from zeep.utils import detect_soap_env 17from zeep.wsse.utils import ensure_id, get_security_header 18 19try: 20 import xmlsec 21except ImportError: 22 xmlsec = None 23 24 25# SOAP envelope 26SOAP_NS = "http://schemas.xmlsoap.org/soap/envelope/" 27 28 29def _read_file(f_name): 30 with open(f_name, "rb") as f: 31 return f.read() 32 33 34def _make_sign_key(key_data, cert_data, password): 35 key = xmlsec.Key.from_memory(key_data, xmlsec.KeyFormat.PEM, password) 36 key.load_cert_from_memory(cert_data, xmlsec.KeyFormat.PEM) 37 return key 38 39 40def _make_verify_key(cert_data): 41 key = xmlsec.Key.from_memory(cert_data, xmlsec.KeyFormat.CERT_PEM, None) 42 return key 43 44 45class MemorySignature: 46 """Sign given SOAP envelope with WSSE sig using given key and cert.""" 47 48 def __init__( 49 self, 50 key_data, 51 cert_data, 52 password=None, 53 signature_method=None, 54 digest_method=None, 55 ): 56 check_xmlsec_import() 57 58 self.key_data = key_data 59 self.cert_data = cert_data 60 self.password = password 61 self.digest_method = digest_method 62 self.signature_method = signature_method 63 64 def apply(self, envelope, headers): 65 key = _make_sign_key(self.key_data, self.cert_data, self.password) 66 _sign_envelope_with_key( 67 envelope, key, self.signature_method, self.digest_method 68 ) 69 return envelope, headers 70 71 def verify(self, envelope): 72 key = _make_verify_key(self.cert_data) 73 _verify_envelope_with_key(envelope, key) 74 return envelope 75 76 77class Signature(MemorySignature): 78 """Sign given SOAP envelope with WSSE sig using given key file and cert file.""" 79 80 def __init__( 81 self, 82 key_file, 83 certfile, 84 password=None, 85 signature_method=None, 86 digest_method=None, 87 ): 88 super().__init__( 89 _read_file(key_file), 90 _read_file(certfile), 91 password, 92 signature_method, 93 digest_method, 94 ) 95 96 97class BinarySignature(Signature): 98 """Sign given SOAP envelope with WSSE sig using given key file and cert file. 99 100 Place the key information into BinarySecurityElement.""" 101 102 def apply(self, envelope, headers): 103 key = _make_sign_key(self.key_data, self.cert_data, self.password) 104 _sign_envelope_with_key_binary( 105 envelope, key, self.signature_method, self.digest_method 106 ) 107 return envelope, headers 108 109 110def check_xmlsec_import(): 111 if xmlsec is None: 112 raise ImportError( 113 "The xmlsec module is required for wsse.Signature()\n" 114 + "You can install xmlsec with: pip install xmlsec\n" 115 + "or install zeep via: pip install zeep[xmlsec]\n" 116 ) 117 118 119def sign_envelope( 120 envelope, 121 keyfile, 122 certfile, 123 password=None, 124 signature_method=None, 125 digest_method=None, 126): 127 """Sign given SOAP envelope with WSSE sig using given key and cert. 128 129 Sign the wsu:Timestamp node in the wsse:Security header and the soap:Body; 130 both must be present. 131 132 Add a ds:Signature node in the wsse:Security header containing the 133 signature. 134 135 Use EXCL-C14N transforms to normalize the signed XML (so that irrelevant 136 whitespace or attribute ordering changes don't invalidate the 137 signature). Use SHA1 signatures. 138 139 Expects to sign an incoming document something like this (xmlns attributes 140 omitted for readability): 141 142 <soap:Envelope> 143 <soap:Header> 144 <wsse:Security mustUnderstand="true"> 145 <wsu:Timestamp> 146 <wsu:Created>2015-06-25T21:53:25.246276+00:00</wsu:Created> 147 <wsu:Expires>2015-06-25T21:58:25.246276+00:00</wsu:Expires> 148 </wsu:Timestamp> 149 </wsse:Security> 150 </soap:Header> 151 <soap:Body> 152 ... 153 </soap:Body> 154 </soap:Envelope> 155 156 After signing, the sample document would look something like this (note the 157 added wsu:Id attr on the soap:Body and wsu:Timestamp nodes, and the added 158 ds:Signature node in the header, with ds:Reference nodes with URI attribute 159 referencing the wsu:Id of the signed nodes): 160 161 <soap:Envelope> 162 <soap:Header> 163 <wsse:Security mustUnderstand="true"> 164 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#"> 165 <SignedInfo> 166 <CanonicalizationMethod 167 Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> 168 <SignatureMethod 169 Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/> 170 <Reference URI="#id-d0f9fd77-f193-471f-8bab-ba9c5afa3e76"> 171 <Transforms> 172 <Transform 173 Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> 174 </Transforms> 175 <DigestMethod 176 Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/> 177 <DigestValue>nnjjqTKxwl1hT/2RUsBuszgjTbI=</DigestValue> 178 </Reference> 179 <Reference URI="#id-7c425ac1-534a-4478-b5fe-6cae0690f08d"> 180 <Transforms> 181 <Transform 182 Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> 183 </Transforms> 184 <DigestMethod 185 Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/> 186 <DigestValue>qAATZaSqAr9fta9ApbGrFWDuCCQ=</DigestValue> 187 </Reference> 188 </SignedInfo> 189 <SignatureValue>Hz8jtQb...bOdT6ZdTQ==</SignatureValue> 190 <KeyInfo> 191 <wsse:SecurityTokenReference> 192 <X509Data> 193 <X509Certificate>MIIDnzC...Ia2qKQ==</X509Certificate> 194 <X509IssuerSerial> 195 <X509IssuerName>...</X509IssuerName> 196 <X509SerialNumber>...</X509SerialNumber> 197 </X509IssuerSerial> 198 </X509Data> 199 </wsse:SecurityTokenReference> 200 </KeyInfo> 201 </Signature> 202 <wsu:Timestamp wsu:Id="id-7c425ac1-534a-4478-b5fe-6cae0690f08d"> 203 <wsu:Created>2015-06-25T22:00:29.821700+00:00</wsu:Created> 204 <wsu:Expires>2015-06-25T22:05:29.821700+00:00</wsu:Expires> 205 </wsu:Timestamp> 206 </wsse:Security> 207 </soap:Header> 208 <soap:Body wsu:Id="id-d0f9fd77-f193-471f-8bab-ba9c5afa3e76"> 209 ... 210 </soap:Body> 211 </soap:Envelope> 212 213 """ 214 # Load the signing key and certificate. 215 key = _make_sign_key(_read_file(keyfile), _read_file(certfile), password) 216 return _sign_envelope_with_key(envelope, key, signature_method, digest_method) 217 218 219def _signature_prepare(envelope, key, signature_method, digest_method): 220 """Prepare envelope and sign.""" 221 soap_env = detect_soap_env(envelope) 222 223 # Create the Signature node. 224 signature = xmlsec.template.create( 225 envelope, 226 xmlsec.Transform.EXCL_C14N, 227 signature_method or xmlsec.Transform.RSA_SHA1, 228 ) 229 230 # Add a KeyInfo node with X509Data child to the Signature. XMLSec will fill 231 # in this template with the actual certificate details when it signs. 232 key_info = xmlsec.template.ensure_key_info(signature) 233 x509_data = xmlsec.template.add_x509_data(key_info) 234 xmlsec.template.x509_data_add_issuer_serial(x509_data) 235 xmlsec.template.x509_data_add_certificate(x509_data) 236 237 # Insert the Signature node in the wsse:Security header. 238 security = get_security_header(envelope) 239 security.insert(0, signature) 240 241 # Perform the actual signing. 242 ctx = xmlsec.SignatureContext() 243 ctx.key = key 244 _sign_node(ctx, signature, envelope.find(QName(soap_env, "Body")), digest_method) 245 timestamp = security.find(QName(ns.WSU, "Timestamp")) 246 if timestamp != None: 247 _sign_node(ctx, signature, timestamp, digest_method) 248 ctx.sign(signature) 249 250 # Place the X509 data inside a WSSE SecurityTokenReference within 251 # KeyInfo. The recipient expects this structure, but we can't rearrange 252 # like this until after signing, because otherwise xmlsec won't populate 253 # the X509 data (because it doesn't understand WSSE). 254 sec_token_ref = etree.SubElement(key_info, QName(ns.WSSE, "SecurityTokenReference")) 255 return security, sec_token_ref, x509_data 256 257 258def _sign_envelope_with_key(envelope, key, signature_method, digest_method): 259 _, sec_token_ref, x509_data = _signature_prepare( 260 envelope, key, signature_method, digest_method 261 ) 262 sec_token_ref.append(x509_data) 263 264 265def _sign_envelope_with_key_binary(envelope, key, signature_method, digest_method): 266 security, sec_token_ref, x509_data = _signature_prepare( 267 envelope, key, signature_method, digest_method 268 ) 269 ref = etree.SubElement( 270 sec_token_ref, 271 QName(ns.WSSE, "Reference"), 272 { 273 "ValueType": "http://docs.oasis-open.org/wss/2004/01/" 274 "oasis-200401-wss-x509-token-profile-1.0#X509v3" 275 }, 276 ) 277 bintok = etree.Element( 278 QName(ns.WSSE, "BinarySecurityToken"), 279 { 280 "ValueType": "http://docs.oasis-open.org/wss/2004/01/" 281 "oasis-200401-wss-x509-token-profile-1.0#X509v3", 282 "EncodingType": "http://docs.oasis-open.org/wss/2004/01/" 283 "oasis-200401-wss-soap-message-security-1.0#Base64Binary", 284 }, 285 ) 286 ref.attrib["URI"] = "#" + ensure_id(bintok) 287 bintok.text = x509_data.find(QName(ns.DS, "X509Certificate")).text 288 security.insert(1, bintok) 289 x509_data.getparent().remove(x509_data) 290 291 292def verify_envelope(envelope, certfile): 293 """Verify WS-Security signature on given SOAP envelope with given cert. 294 295 Expects a document like that found in the sample XML in the ``sign()`` 296 docstring. 297 298 Raise SignatureVerificationFailed on failure, silent on success. 299 300 """ 301 key = _make_verify_key(_read_file(certfile)) 302 return _verify_envelope_with_key(envelope, key) 303 304 305def _verify_envelope_with_key(envelope, key): 306 soap_env = detect_soap_env(envelope) 307 308 header = envelope.find(QName(soap_env, "Header")) 309 if header is None: 310 raise SignatureVerificationFailed() 311 312 security = header.find(QName(ns.WSSE, "Security")) 313 signature = security.find(QName(ns.DS, "Signature")) 314 315 ctx = xmlsec.SignatureContext() 316 317 # Find each signed element and register its ID with the signing context. 318 refs = signature.xpath("ds:SignedInfo/ds:Reference", namespaces={"ds": ns.DS}) 319 for ref in refs: 320 # Get the reference URI and cut off the initial '#' 321 referenced_id = ref.get("URI")[1:] 322 referenced = envelope.xpath( 323 "//*[@wsu:Id='%s']" % referenced_id, namespaces={"wsu": ns.WSU} 324 )[0] 325 ctx.register_id(referenced, "Id", ns.WSU) 326 327 ctx.key = key 328 329 try: 330 ctx.verify(signature) 331 except xmlsec.Error: 332 # Sadly xmlsec gives us no details about the reason for the failure, so 333 # we have nothing to pass on except that verification failed. 334 raise SignatureVerificationFailed() 335 336 337def _sign_node(ctx, signature, target, digest_method=None): 338 """Add sig for ``target`` in ``signature`` node, using ``ctx`` context. 339 340 Doesn't actually perform the signing; ``ctx.sign(signature)`` should be 341 called later to do that. 342 343 Adds a Reference node to the signature with URI attribute pointing to the 344 target node, and registers the target node's ID so XMLSec will be able to 345 find the target node by ID when it signs. 346 347 """ 348 349 # Ensure the target node has a wsu:Id attribute and get its value. 350 node_id = ensure_id(target) 351 352 # Unlike HTML, XML doesn't have a single standardized Id. WSSE suggests the 353 # use of the wsu:Id attribute for this purpose, but XMLSec doesn't 354 # understand that natively. So for XMLSec to be able to find the referenced 355 # node by id, we have to tell xmlsec about it using the register_id method. 356 ctx.register_id(target, "Id", ns.WSU) 357 358 # Add reference to signature with URI attribute pointing to that ID. 359 ref = xmlsec.template.add_reference( 360 signature, digest_method or xmlsec.Transform.SHA1, uri="#" + node_id 361 ) 362 # This is an XML normalization transform which will be performed on the 363 # target node contents before signing. This ensures that changes to 364 # irrelevant whitespace, attribute ordering, etc won't invalidate the 365 # signature. 366 xmlsec.template.add_transform(ref, xmlsec.Transform.EXCL_C14N) 367