Skip to content

Commit c72bc28

Browse files
docs: add WS-Security (WSSE) UsernameToken+Sign+Encrypt example
Add a recipe for a common WS-Security 1.1 shape (UsernameToken plus a signed Body plus a detached EncryptedKey/encrypted Body) not covered by the existing sign/encrypt examples. Documents why EncryptionContext.encrypt_binary/encrypt_xml reject an EncryptedKey template node (confirmed against xmlsec1 1.2.39), hence the manual wrap/unwrap via `cryptography`, and that sign-then-encrypt vs encrypt-then-sign requires opposite decrypt/verify ordering on the receiving side. Adds `cryptography` to requirements-test.txt since the new examples depend on it.
1 parent ab5b099 commit c72bc28

8 files changed

Lines changed: 399 additions & 0 deletions

File tree

‎doc/source/examples.rst‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,66 @@ Verify-Binary
3333
-------------
3434

3535
.. literalinclude:: examples/verify_binary.py
36+
37+
38+
WS-Security (WSSE): UsernameToken, Sign, Encrypt
39+
--------------------------------------------------
40+
41+
These two scripts show one way to build and process a WS-Security secured
42+
SOAP envelope with xmlsec: a ``UsernameToken`` plus an XML signature over
43+
the Body plus an XML encryption of the Body, using a "detached"
44+
``EncryptedKey`` under ``wsse:Security`` (the shape most SOAP stacks
45+
expect for WS-Security 1.1), instead of the ``EncryptedKey``-nested-inside-
46+
``EncryptedData`` shape that :func:`~xmlsec.EncryptionContext.encrypt_xml`
47+
produces by default.
48+
49+
xmlsec's ``EncryptionContext.encrypt_binary``/``encrypt_xml`` only accept
50+
an ``xenc:EncryptedData`` node as their template, so they cannot fill in a
51+
detached ``EncryptedKey`` directly; these examples wrap/unwrap the AES
52+
session key with the ``cryptography`` package instead, while still using
53+
xmlsec's template helpers to build the element and xmlsec's own contexts
54+
to sign/verify and encrypt/decrypt the Body. See the docstrings in each
55+
script for the full explanation, including why the two scripts process
56+
Sign/Encrypt and Decrypt/Verify in mirrored (not identical) order.
57+
58+
.. warning::
59+
60+
``wssekey.pem`` and ``wssecert.pem`` in this directory are a self-signed
61+
key pair generated only for these examples (the certificate's
62+
Subject/Issuer CN says as much: "test key, DO NOT USE IN PRODUCTION").
63+
Do not reuse them for anything real -- generate your own pair, e.g.::
64+
65+
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
66+
-keyout wssekey.pem -out wssecert.pem \
67+
-subj "/CN=your identity here"
68+
69+
To keep the example short, both scripts reuse this *same* key pair for
70+
every role, but in a real deployment each party has its own key pair and
71+
only ever holds its own private key plus the other side's public
72+
certificate:
73+
74+
* The **sender** (whoever calls ``sign_body``) signs with its own private
75+
key -- ``signing_key_file`` in ``wsse_outgoing.py``. Only the sender
76+
ever holds this key.
77+
* To **encrypt** the Body, the sender needs the *recipient's* public
78+
certificate -- ``recipient_cert_file`` in ``encrypt_body``. This is the
79+
public cert of whoever will receive and decrypt the message (the
80+
service being called, or the calling client, depending on which
81+
direction the message flows), obtained out-of-band ahead of time.
82+
* On the receiving side, ``verify_signature`` needs the *sender's* public
83+
certificate, to check who signed the message. ``unwrap_session_key``,
84+
on the other hand, needs the *recipient's own* private key, to decrypt
85+
the session key that was encrypted for it.
86+
87+
In other words: a real integration needs two independent key pairs, one
88+
per party, not the single shared pair used here for brevity.
89+
90+
Outgoing (build a secured request)
91+
+++++++++++++++++++++++++++++++++
92+
93+
.. literalinclude:: examples/wsse_outgoing.py
94+
95+
Incoming (process a secured response)
96+
++++++++++++++++++++++++++++++++++++
97+
98+
.. literalinclude:: examples/wsse_incoming.py
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ex="urn:example:wsse-demo"><soapenv:Header><wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"><EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#" Id="body-key">
2+
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/>
3+
<CipherData>
4+
<CipherValue>pJoXEOU2aFv0uy/p4DmrewBLwPC4EnSifah2uyjay94lD2o9DxH+KFA67CqbM+sqb+Uw1l/ktBw81Hvx+TIwAzhDMJqQwPR97bANjfnEb04EiEdxqUMMnzJunx9xCAqNbU3mPxIVtchqqq3IN2MSY8nGZXFZzrFGt3GWfBrU84JjT8Z6Qsax1/60Evx/SIIhPb+J+e0GauOuqqUvulyg8A2CTLkPZDrbsXDQxyyvl6Hs1JO7nYzufmmy8dlt6L22ryaTQeE179UQhA0g1xxi4urqWW8c4p7DyFfGOYSV6ZIM0QL26ExGqfmJj47crnoeLoB7C1apeOBx4PJrLMSdAA==</CipherValue>
5+
</CipherData>
6+
<ReferenceList><DataReference URI="#body-data"/></ReferenceList></EncryptedKey>
7+
<wsse:UsernameToken><wsse:Username>demo-user</wsse:Username><wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">demo-password</wsse:Password></wsse:UsernameToken><Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
8+
<SignedInfo>
9+
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
10+
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
11+
<Reference URI="#body">
12+
<Transforms>
13+
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
14+
</Transforms>
15+
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
16+
<DigestValue>CVIISH/pUxaPBQOdrr5VmOvwb+Yc19VyJRaTc8y7q0k=</DigestValue>
17+
</Reference>
18+
</SignedInfo>
19+
<SignatureValue>YPw0lnnztPmRCQGdr7UvsqLDVKVjcFjo5SbBfC9ZqhCvyP+5mBChj/G8gDC1tPuV
20+
L8t20xUtHpO6qjW4emnncXHTZqnaD9BGqGzl6KEX0tzA16nkvTaVxfc52h4vc+Xv
21+
rUl0wfwkK0bxkVKTN8cwmUYtH0MNEENG5ogP4ql6A3yyGZamxGSvoRHmnM+wBInd
22+
cUKlTM7OinB70kDzKjkKu/ZfNa8EZIfy2VzAS5u01Co9ZKdaAd9ahyre8Y0MDscc
23+
LokWJ0A88dCvtdq06ymxHtQTufz4ZiTsH6G9zUWK7aoZKDxO/d/wYr8SJDZ0P8BC
24+
fCtKC9GM5w89FFMRwmZDGg==</SignatureValue>
25+
</Signature></wsse:Security></soapenv:Header><soapenv:Body xmlns:ns0="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" ns0:Id="body"><xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Content" Id="body-data">
26+
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
27+
<xenc:CipherData>
28+
<xenc:CipherValue>aplU5Dg8X8x19a7O2XlV1PouZe1PEl2Lh+k2l79OKnp4ddD7p3XGpAHNXMwN13lx
29+
VSyP3+JBn+q5NvLvHJwdEqDoHntUAVC+c/4vTABfWdH3qEq8ObG4QZiAbw9uncvm
30+
utZyrJsZcl9org9p1QWCjw==</xenc:CipherValue>
31+
</xenc:CipherData>
32+
</xenc:EncryptedData></soapenv:Body></soapenv:Envelope>

‎doc/source/examples/wsse-tmpl.xml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
2+
xmlns:ex="urn:example:wsse-demo">
3+
<soapenv:Header/>
4+
<soapenv:Body>
5+
<ex:ExampleRequest>
6+
<ex:Field1>hello</ex:Field1>
7+
<ex:Field2>42</ex:Field2>
8+
</ex:ExampleRequest>
9+
</soapenv:Body>
10+
</soapenv:Envelope>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Process a WS-Security (WSSE) secured SOAP response: Decrypt + Verify.
2+
3+
Companion to ``wsse_outgoing.py``. Loads a pre-built, signed-then-encrypted
4+
envelope (``wsse-secured.xml``, produced the same way ``wsse_outgoing.py``
5+
builds one) and reverses the two operations.
6+
7+
Order matters and is the mirror image of how the message was built: the
8+
sender in this example signed the Body *before* encrypting it, so the
9+
Signature's digest covers the plaintext Body. Verifying against the
10+
still-encrypted Body would always fail, so this example decrypts first
11+
(which restores the original Body content in place) and verifies second.
12+
If a peer instead encrypts-then-signs (common for some backend-originated
13+
responses, since it lets the signature cover exactly the bytes on the
14+
wire), reverse the two steps here: verify first, decrypt second.
15+
"""
16+
17+
import base64
18+
19+
from cryptography.hazmat.primitives import hashes, serialization
20+
from cryptography.hazmat.primitives.asymmetric import padding
21+
from lxml import etree
22+
23+
import xmlsec
24+
25+
NS = {
26+
'soapenv': 'http://schemas.xmlsoap.org/soap/envelope/',
27+
'wsse': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
28+
'wsu': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd',
29+
'ds': 'http://www.w3.org/2000/09/xmldsig#',
30+
'xenc': 'http://www.w3.org/2001/04/xmlenc#',
31+
}
32+
33+
# Must match the peer's choices; see the note in wsse_outgoing.py about
34+
# legacy (sha1 / tripledes / rsa-1_5) variants.
35+
KEY_UNWRAP_HASH = hashes.SHA256()
36+
37+
38+
def unwrap_session_key(security, private_key_file):
39+
"""Recover the AES session key from the detached EncryptedKey.
40+
41+
xmlsec's decrypt() only understands EncryptedData nodes (the mirror
42+
image of the encrypt_binary/encrypt_xml limitation described in
43+
wsse_outgoing.py), so a detached EncryptedKey has to be unwrapped by
44+
hand with the recipient's RSA private key.
45+
"""
46+
with open(private_key_file, 'rb') as fp:
47+
private_key = serialization.load_pem_private_key(fp.read(), password=None)
48+
49+
enc_key_node = security.find('xenc:EncryptedKey', NS)
50+
cipher_value = enc_key_node.find('.//xenc:CipherValue', NS)
51+
wrapped_key = base64.b64decode(cipher_value.text)
52+
53+
return private_key.decrypt(
54+
wrapped_key,
55+
padding.OAEP(mgf=padding.MGF1(algorithm=KEY_UNWRAP_HASH), algorithm=KEY_UNWRAP_HASH, label=None),
56+
)
57+
58+
59+
def decrypt_body(envelope, security, session_key_bytes):
60+
enc_data_node = envelope.find('.//soapenv:Body/xenc:EncryptedData', NS)
61+
ctx = xmlsec.EncryptionContext()
62+
ctx.key = xmlsec.Key.from_binary_data(xmlsec.constants.KeyDataAes, session_key_bytes)
63+
ctx.decrypt(enc_data_node)
64+
65+
66+
def verify_signature(envelope, sender_cert_file):
67+
xmlsec.tree.add_ids(envelope, [f'{{{NS["wsu"]}}}Id', 'Id', 'id'])
68+
signature_node = envelope.find('.//ds:Signature', NS)
69+
ctx = xmlsec.SignatureContext()
70+
ctx.key = xmlsec.Key.from_file(sender_cert_file, xmlsec.constants.KeyDataFormatCertPem)
71+
ctx.verify(signature_node)
72+
73+
74+
if __name__ == '__main__':
75+
with open('wsse-secured.xml', 'rb') as fp:
76+
envelope = etree.parse(fp).getroot()
77+
78+
security = envelope.find('.//wsse:Security', NS)
79+
80+
session_key_bytes = unwrap_session_key(security, private_key_file='wssekey.pem')
81+
decrypt_body(envelope, security, session_key_bytes)
82+
verify_signature(envelope, sender_cert_file='wssecert.pem')
83+
84+
print('Signature OK. Decrypted Body:')
85+
print(etree.tostring(envelope.find('soapenv:Body', NS)).decode())
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"""Build a WS-Security (WSSE) secured SOAP request: UsernameToken + Sign + Encrypt.
2+
3+
xmlsec exposes XML-DSig and XML-Enc primitives, but WS-Security profiles
4+
build a specific *combination* of them inside a SOAP ``<wsse:Security>``
5+
header that xmlsec does not template for you. This example shows one
6+
common, interoperable combination:
7+
8+
1. A UsernameToken (plain-text password) in the Security header.
9+
2. An enveloped-style XML signature over the SOAP Body, referenced by an
10+
``Id`` attribute (WS-Security "Reference" pattern, not the enveloped
11+
XPath transform).
12+
3. A "detached" ``xenc:EncryptedKey`` living directly under
13+
``wsse:Security`` (not nested inside the ``EncryptedData`` it protects,
14+
which is where ``encrypt_xml``/``encrypt_binary`` would normally put
15+
it). This is the shape most SOAP stacks expect for WS-Security 1.1.
16+
17+
Because that detached ``EncryptedKey`` is not part of the ``EncryptedData``
18+
tree, xmlsec's ``EncryptionContext.encrypt_binary``/``encrypt_xml`` cannot
19+
fill it in directly -- both require an ``xenc:EncryptedData`` node as the
20+
target template (see ``xmlSecEncCtxEncDataNodeRead`` in xmlsec1, which
21+
rejects an ``EncryptedKey`` node with "invalid node"). So this example
22+
wraps the AES session key with RSA-OAEP using the ``cryptography`` package
23+
and places the result in the ``EncryptedKey``'s ``CipherValue`` by hand,
24+
while still using xmlsec's own template helpers to build the element and
25+
xmlsec's ``EncryptionContext`` to encrypt the Body itself.
26+
27+
Order matters: this example signs the Body *before* encrypting it, so the
28+
signature covers the plaintext. A receiver must therefore decrypt first
29+
and verify second -- see ``wsse_incoming.py``. If you instead need to
30+
encrypt before signing (e.g. because a peer's stack requires the signature
31+
to cover ciphertext), reverse the two steps below and sign the
32+
``EncryptedData``'s ``Id`` instead of the Body's.
33+
"""
34+
35+
import base64
36+
import os
37+
38+
from cryptography import x509
39+
from cryptography.hazmat.primitives import hashes
40+
from cryptography.hazmat.primitives.asymmetric import padding
41+
from lxml import etree
42+
43+
import xmlsec
44+
45+
NS = {
46+
'soapenv': 'http://schemas.xmlsoap.org/soap/envelope/',
47+
'wsse': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
48+
'wsu': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd',
49+
'ds': 'http://www.w3.org/2000/09/xmldsig#',
50+
'xenc': 'http://www.w3.org/2001/04/xmlenc#',
51+
}
52+
53+
# Recommended defaults. Some legacy WS-Security 1.0-era stacks only accept
54+
# rsa-sha1 / tripledes-cbc / rsa-1_5 -- swap the constants below (and the
55+
# `cryptography` padding/hash objects to match) if you must interop with
56+
# one of those, but prefer these unless you are told otherwise.
57+
SIGNATURE_TRANSFORM = xmlsec.constants.TransformRsaSha256
58+
DIGEST_TRANSFORM = xmlsec.constants.TransformSha256
59+
BODY_ENCRYPTION_TRANSFORM = xmlsec.constants.TransformAes128Cbc
60+
KEY_WRAP_HASH = hashes.SHA256()
61+
62+
63+
def add_security_header(envelope):
64+
header = envelope.find('soapenv:Header', NS)
65+
if header is None:
66+
header = etree.SubElement(envelope, f'{{{NS["soapenv"]}}}Header')
67+
envelope.insert(0, header)
68+
return etree.SubElement(
69+
header,
70+
f'{{{NS["wsse"]}}}Security',
71+
nsmap={'wsse': NS['wsse'], 'wsu': NS['wsu']},
72+
)
73+
74+
75+
def add_username_token(security, username, password):
76+
token = etree.SubElement(security, f'{{{NS["wsse"]}}}UsernameToken')
77+
etree.SubElement(token, f'{{{NS["wsse"]}}}Username').text = username
78+
password_elem = etree.SubElement(token, f'{{{NS["wsse"]}}}Password')
79+
password_elem.set(
80+
'Type',
81+
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText',
82+
)
83+
password_elem.text = password
84+
return token
85+
86+
87+
def sign_body(envelope, security, signing_key_file, body_id='body'):
88+
body = envelope.find('soapenv:Body', NS)
89+
body.set(f'{{{NS["wsu"]}}}Id', body_id)
90+
xmlsec.tree.add_ids(envelope, [f'{{{NS["wsu"]}}}Id', 'Id', 'id'])
91+
92+
signature_node = xmlsec.template.create(envelope, xmlsec.constants.TransformExclC14N, SIGNATURE_TRANSFORM)
93+
security.append(signature_node)
94+
95+
reference = xmlsec.template.add_reference(signature_node, DIGEST_TRANSFORM, uri='#' + body_id)
96+
xmlsec.template.add_transform(reference, xmlsec.constants.TransformExclC14N)
97+
98+
ctx = xmlsec.SignatureContext()
99+
ctx.key = xmlsec.Key.from_file(signing_key_file, xmlsec.constants.KeyDataFormatPem)
100+
ctx.sign(signature_node)
101+
return signature_node
102+
103+
104+
def encrypt_body(envelope, security, recipient_cert_file, data_id='body-data', key_id='body-key'):
105+
body = envelope.find('soapenv:Body', NS)
106+
107+
with open(recipient_cert_file, 'rb') as fp:
108+
recipient_cert = x509.load_pem_x509_certificate(fp.read())
109+
110+
# 1. Generate a random session key and use it to encrypt the Body
111+
# content with xmlsec, exactly like the plain `encrypt.py` example.
112+
session_key_bytes = os.urandom(16) # 16 bytes = AES-128
113+
114+
enc_data_template = xmlsec.template.encrypted_data_create(
115+
body,
116+
BODY_ENCRYPTION_TRANSFORM,
117+
type=xmlsec.constants.TypeEncContent,
118+
ns='xenc',
119+
)
120+
enc_data_template.set('Id', data_id)
121+
xmlsec.template.encrypted_data_ensure_cipher_value(enc_data_template)
122+
123+
enc_ctx = xmlsec.EncryptionContext()
124+
enc_ctx.key = xmlsec.Key.from_binary_data(xmlsec.constants.KeyDataAes, session_key_bytes)
125+
enc_ctx.encrypt_xml(enc_data_template, body)
126+
127+
# 2. Wrap the session key with the recipient's RSA public key ourselves
128+
# (see module docstring for why xmlsec can't do this part for a
129+
# detached EncryptedKey), and place it directly under Security.
130+
wrapped_key = recipient_cert.public_key().encrypt(
131+
session_key_bytes,
132+
padding.OAEP(mgf=padding.MGF1(algorithm=KEY_WRAP_HASH), algorithm=KEY_WRAP_HASH, label=None),
133+
)
134+
135+
enc_key_node = xmlsec.template.add_encrypted_key(security, xmlsec.constants.TransformRsaOaep, id=key_id)
136+
xmlsec.template.encrypted_data_ensure_cipher_value(enc_key_node).text = base64.b64encode(wrapped_key).decode()
137+
138+
reference_list = etree.SubElement(enc_key_node, f'{{{NS["xenc"]}}}ReferenceList')
139+
etree.SubElement(reference_list, f'{{{NS["xenc"]}}}DataReference').set('URI', '#' + data_id)
140+
141+
# A detached EncryptedKey must come before anything that references it,
142+
# so it is expected as the first child of Security by most consumers.
143+
security.remove(enc_key_node)
144+
security.insert(0, enc_key_node)
145+
146+
147+
if __name__ == '__main__':
148+
with open('wsse-tmpl.xml') as fp:
149+
envelope = etree.parse(fp, etree.XMLParser(remove_blank_text=True)).getroot()
150+
151+
security = add_security_header(envelope)
152+
add_username_token(security, username='demo-user', password='demo-password')
153+
sign_body(envelope, security, signing_key_file='wssekey.pem')
154+
encrypt_body(envelope, security, recipient_cert_file='wssecert.pem')
155+
156+
# NOTE: do not pretty-print before/after signing -- inserting
157+
# whitespace-only text nodes changes what exclusive C14N canonicalizes,
158+
# so a pretty-printed copy would no longer verify.
159+
print(etree.tostring(envelope).decode())

‎doc/source/examples/wssecert.pem‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDdTCCAl2gAwIBAgIUDfID7AItWUsM2kHiLPcGZNb9rJMwDQYJKoZIhvcNAQEL
3+
BQAwSjFIMEYGA1UEAww/cHl0aG9uLXhtbHNlYyBXU1NFIGV4YW1wbGUgKHRlc3Qg
4+
a2V5LCBETyBOT1QgVVNFIElOIFBST0RVQ1RJT04pMB4XDTI2MDkyMjE1NDQwMloX
5+
DTM2MDkxOTE1NDQwMlowSjFIMEYGA1UEAww/cHl0aG9uLXhtbHNlYyBXU1NFIGV4
6+
YW1wbGUgKHRlc3Qga2V5LCBETyBOT1QgVVNFIElOIFBST0RVQ1RJT04pMIIBIjAN
7+
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5B6SRkKzfIQzp8Iqx1FgMqr8QRnK
8+
NNvl+7NdmG493uWGyC2lucgB6mtSh4oEdrPTeLaXkk12TKLv/TMHFzUCjdnQniJo
9+
gvwB3abdSxl7F8wRAgzDok56E00PZqpsDz+g8NAP6xBwA9CNgUTmWh9OMJeiTUXC
10+
NBgYBldjm8CyqF/DONRB49+vHIV/ciSzvqU7YudG4A6wcyoaokn4X/eIgt8Ar0B4
11+
WjwjGKyntvB26Fk7nQiFJ7V0FLlXYob9TXe6Ff4H5fvMcmVeEVfpLN/ryojFTh8F
12+
VoDoqjV5sneheCBNW3rGygAHwNBxLkJSICHl/LaRl4czObXCf6TNF3ByawIDAQAB
13+
o1MwUTAdBgNVHQ4EFgQULqjD0z8t1wcR6xWcNjqZFtaRylwwHwYDVR0jBBgwFoAU
14+
LqjD0z8t1wcR6xWcNjqZFtaRylwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B
15+
AQsFAAOCAQEAXVxQdjpnMsJb7v/hBYVbuUxFvwSB+x9cGaux/g+louSr1/Q7fZ+c
16+
/j3Ai0DGZ8k8xcKpPHFr2/VcruB9OLF6c86enCcx85naU2g+u3TqVllQ6sN2I2N5
17+
bj5aHF3HRkJt7vI7ilg+efjOgfRa66U71M4yEgfgWt8rHEQoT9oC2zWUyGX+0kp+
18+
bqm1a0MqrCmT/mL0cuhwg5gawz4bnuXSU6+XiczKtWrdnMf6dejbZho9YOcp8GN3
19+
UJ+uEAQTIlN32wQQFBIBEidEYp4Q1Dg+0wkEQ/2uXKE9+QkizCobiJ5qRncflnlr
20+
FVcTWKDVeTK/2av0S3GCnvZ832i1+GThJw==
21+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)