Skip to content

Commit 966b270

Browse files
committed
HUE-3102 [libsaml] Add support for private key passwords
This is also tracked by IdentityPython/pysaml2#278. This patch adds support for SAML certificates that are protected with a password. The way it does so is with a bit of trickiness, due to the fact that `xmlsec1`, which is an external program that pysaml2 uses to sign the XML requests, which does not have great support for password protected certificates. It either supports passing in the password on the command line (which is not safe since someone else on the machine could see the password), or through an interactive prompt. The proper way to fix this would be to update pysaml2 to use another xmlsec library, but implementing that may take some time. In the short/medium term, this patch implements this instead by decrypting the certificate in memory, and passing this decrypted certificate to xmlsec1 through a named pipe. This protects us from the decrypted certificate ever hitting the disk. Unfortunately, this solution is only portable to POSIX-compatible platforms. That is fine for Hue, but it probably means we cannot push this patch to the upstream pysaml2 repository. This patch will tied us over until the upstream project switches to a better xmlsec library.
1 parent 7686bfb commit 966b270

File tree

2 files changed

+60
-13
lines changed

2 files changed

+60
-13
lines changed

desktop/core/ext-py/pysaml2-4.9.0/src/saml2/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"entityid",
3333
"xmlsec_binary",
3434
"key_file",
35+
"key_file_passphrase",
3536
"cert_file",
3637
"encryption_keypairs",
3738
"additional_cert_files",
@@ -193,6 +194,7 @@ def __init__(self, homedir="."):
193194
self.xmlsec_path = []
194195
self.debug = False
195196
self.key_file = None
197+
self.key_file_passphrase = None
196198
self.cert_file = None
197199
self.encryption_keypairs = None
198200
self.additional_cert_files = None

desktop/core/ext-py/pysaml2-4.9.0/src/saml2/sigver.py

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import saml2.cryptography.asymmetric
2020
import saml2.cryptography.pki
2121

22-
from tempfile import NamedTemporaryFile
22+
from tempfile import NamedTemporaryFile, mkdtemp
2323
from subprocess import Popen
2424
from subprocess import PIPE
2525

@@ -483,6 +483,19 @@ def parse_xmlsec_output(output):
483483
def sha1_digest(msg):
484484
return hashlib.sha1(msg).digest()
485485

486+
class NamedPipe(object):
487+
def __init__(self):
488+
self._tempdir = mkdtemp()
489+
self.name = os.path.join(self._tempdir, 'fifo')
490+
491+
try:
492+
os.mkfifo(self.name)
493+
except:
494+
os.rmdir(self._tempdir)
495+
496+
def close(self):
497+
os.remove(self.name)
498+
os.rmdir(self._tempdir)
486499

487500
class Signer(object):
488501
"""Abstract base class for signing algorithms."""
@@ -651,10 +664,10 @@ def encrypt(self, text, recv_key, template, key_type):
651664
def encrypt_assertion(self, statement, enc_key, template, key_type, node_xpath):
652665
raise NotImplementedError()
653666

654-
def decrypt(self, enctext, key_file, id_attr):
667+
def decrypt(self, enctext, key_file, id_attr, passphrase=None):
655668
raise NotImplementedError()
656669

657-
def sign_statement(self, statement, node_name, key_file, node_id, id_attr):
670+
def sign_statement(self, statement, node_name, key_file, node_id, id_attr, passphrase=None):
658671
raise NotImplementedError()
659672

660673
def validate_signature(self, enctext, cert_file, cert_type, node_name, node_id, id_attr):
@@ -775,7 +788,7 @@ def encrypt_assertion(self, statement, enc_key, template, key_type='des-192', no
775788

776789
return output.decode('utf-8')
777790

778-
def decrypt(self, enctext, key_file, id_attr):
791+
def decrypt(self, enctext, key_file, id_attr, passphrase=None):
779792
"""
780793
781794
:param enctext: XML document containing an encrypted part
@@ -786,6 +799,16 @@ def decrypt(self, enctext, key_file, id_attr):
786799
logger.debug('Decrypt input len: %d', len(enctext))
787800
_, fil = make_temp(enctext, decode=False)
788801

802+
named_pipe = None
803+
if key_file is not None:
804+
if passphrase is not None:
805+
named_pipe = NamedPipe()
806+
# Decrypt the certificate
807+
with open(key_file) as f, open(named_pipe.name, 'wb') as g:
808+
key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read(), passphrase=passphrase)
809+
g.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
810+
key_file = named_pipe.name
811+
789812
com_list = [
790813
self.xmlsec,
791814
'--decrypt',
@@ -801,7 +824,7 @@ def decrypt(self, enctext, key_file, id_attr):
801824

802825
return output.decode('utf-8')
803826

804-
def sign_statement(self, statement, node_name, key_file, node_id, id_attr):
827+
def sign_statement(self, statement, node_name, key_file, node_id, id_attr, passphrase=None):
805828
"""
806829
Sign an XML statement.
807830
@@ -823,6 +846,16 @@ def sign_statement(self, statement, node_name, key_file, node_id, id_attr):
823846
delete=self._xmlsec_delete_tmpfiles,
824847
)
825848

849+
named_pipe = None
850+
if key_file is not None:
851+
if passphrase is not None:
852+
named_pipe = NamedPipe()
853+
# Decrypt the certificate
854+
with open(key_file) as f, open(named_pipe.name, 'wb') as g:
855+
key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read(), passphrase=passphrase)
856+
g.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
857+
key_file = named_pipe.name
858+
826859
com_list = [
827860
self.xmlsec,
828861
'--sign',
@@ -939,7 +972,7 @@ def version(self):
939972
# better than static 0.0 here.
940973
return 'XMLSecurity 0.0'
941974

942-
def sign_statement(self, statement, node_name, key_file, node_id, id_attr):
975+
def sign_statement(self, statement, node_name, key_file, node_id, id_attr, passphrase=None):
943976
"""
944977
Sign an XML statement.
945978
@@ -955,6 +988,8 @@ def sign_statement(self, statement, node_name, key_file, node_id, id_attr):
955988
import xmlsec
956989
import lxml.etree
957990

991+
assert passphrase is None, "Encrypted key files is not supported"
992+
958993
xml = xmlsec.parse_xml(statement)
959994
signed = xmlsec.sign(xml, key_file)
960995
signed_str = lxml.etree.tostring(signed, xml_declaration=False, encoding="UTF-8")
@@ -1062,6 +1097,7 @@ def security_context(conf):
10621097
tmp_cert_file=conf.tmp_cert_file,
10631098
tmp_key_file=conf.tmp_key_file,
10641099
validate_certificate=conf.validate_certificate,
1100+
key_file_passphrase=conf.key_file_passphrase,
10651101
enc_key_files=enc_key_files,
10661102
encryption_keypairs=conf.encryption_keypairs,
10671103
sec_backend=sec_backend,
@@ -1251,6 +1287,7 @@ def __init__(
12511287
generate_cert_info=None,
12521288
tmp_cert_file=None, tmp_key_file=None,
12531289
validate_certificate=None,
1290+
key_file_passphrase=None,
12541291
enc_key_files=None, enc_key_type='pem',
12551292
encryption_keypairs=None,
12561293
enc_cert_type='pem',
@@ -1268,6 +1305,7 @@ def __init__(
12681305

12691306
# Your private key for signing
12701307
self.key_file = key_file
1308+
self.key_file_passphrase = key_file_passphrase
12711309
self.key_type = key_type
12721310

12731311
# Your public key for signing
@@ -1358,6 +1396,7 @@ def decrypt_keys(self, enctext, keys=None, id_attr=''):
13581396
:return: The decrypted text
13591397
"""
13601398
key_files = []
1399+
passphrase = self.key_file_passphrase
13611400

13621401
if not isinstance(keys, list):
13631402
keys = [keys]
@@ -1370,7 +1409,7 @@ def decrypt_keys(self, enctext, keys=None, id_attr=''):
13701409
key_files.append(key_file)
13711410

13721411
try:
1373-
dectext = self.decrypt(enctext, key_file=key_files, id_attr=id_attr)
1412+
dectext = self.decrypt(enctext, key_file=key_files, id_attr=id_attr, passphrase=passphrase)
13741413
except DecryptError as e:
13751414
raise
13761415
else:
@@ -1379,12 +1418,15 @@ def decrypt_keys(self, enctext, keys=None, id_attr=''):
13791418
for key_file in key_files:
13801419
os.unlink(key_file)
13811420

1382-
def decrypt(self, enctext, key_file=None, id_attr=''):
1421+
def decrypt(self, enctext, key_file=None, id_attr='', passphrase=None):
13831422
""" Decrypting an encrypted text by the use of a private key.
13841423
13851424
:param enctext: The encrypted text as a string
13861425
:return: The decrypted text
13871426
"""
1427+
if passphrase is None:
1428+
passphrase = self.key_file_passphrase
1429+
13881430
if not id_attr:
13891431
id_attr = self.id_attr
13901432

@@ -1396,7 +1438,7 @@ def decrypt(self, enctext, key_file=None, id_attr=''):
13961438
]
13971439
for key_file in key_files:
13981440
try:
1399-
dectext = self.crypto.decrypt(enctext, key_file, id_attr)
1441+
dectext = self.crypto.decrypt(enctext, key_file, id_attr, passphrase=passphrase)
14001442
except XmlsecError as e:
14011443
continue
14021444
else:
@@ -1650,7 +1692,7 @@ def sign_statement_using_xmlsec(self, statement, **kwargs):
16501692
""" Deprecated function. See sign_statement(). """
16511693
return self.sign_statement(statement, **kwargs)
16521694

1653-
def sign_statement(self, statement, node_name, key=None, key_file=None, node_id=None, id_attr=''):
1695+
def sign_statement(self, statement, node_name, key=None, key_file=None, node_id=None, id_attr='', passphrase=None):
16541696
"""Sign a SAML statement.
16551697
16561698
:param statement: The statement to be signed
@@ -1671,12 +1713,15 @@ def sign_statement(self, statement, node_name, key=None, key_file=None, node_id=
16711713
if not key and not key_file:
16721714
key_file = self.key_file
16731715

1716+
if not passphrase:
1717+
passphrase = self.key_file_passphrase
1718+
16741719
return self.crypto.sign_statement(
16751720
statement,
16761721
node_name,
16771722
key_file,
16781723
node_id,
1679-
id_attr)
1724+
id_attr, passphrase=passphrase)
16801725

16811726
def sign_assertion_using_xmlsec(self, statement, **kwargs):
16821727
""" Deprecated function. See sign_assertion(). """
@@ -1709,7 +1754,7 @@ def sign_attribute_query(self, statement, **kwargs):
17091754
return self.sign_statement(
17101755
statement, class_name(samlp.AttributeQuery()), **kwargs)
17111756

1712-
def multiple_signatures(self, statement, to_sign, key=None, key_file=None, sign_alg=None, digest_alg=None):
1757+
def multiple_signatures(self, statement, to_sign, key=None, key_file=None, sign_alg=None, digest_alg=None, passphrase=None):
17131758
"""
17141759
Sign multiple parts of a statement
17151760
@@ -1740,7 +1785,7 @@ def multiple_signatures(self, statement, to_sign, key=None, key_file=None, sign_
17401785
key=key,
17411786
key_file=key_file,
17421787
node_id=sid,
1743-
id_attr=id_attr)
1788+
id_attr=id_attr, passphrase=passphrase)
17441789

17451790
return statement
17461791

0 commit comments

Comments
 (0)