summaryrefslogtreecommitdiff
path: root/keystoneclient/common/cms.py
diff options
context:
space:
mode:
Diffstat (limited to 'keystoneclient/common/cms.py')
-rw-r--r--keystoneclient/common/cms.py169
1 files changed, 169 insertions, 0 deletions
diff --git a/keystoneclient/common/cms.py b/keystoneclient/common/cms.py
new file mode 100644
index 0000000..93c0505
--- /dev/null
+++ b/keystoneclient/common/cms.py
@@ -0,0 +1,169 @@
+import hashlib
+
+import logging
+
+
+subprocess = None
+LOG = logging.getLogger(__name__)
+PKI_ANS1_PREFIX = 'MII'
+
+
+def _ensure_subprocess():
+ # NOTE(vish): late loading subprocess so we can
+ # use the green version if we are in
+ # eventlet.
+ global subprocess
+ if not subprocess:
+ try:
+ from eventlet import patcher
+ if patcher.already_patched.get('os'):
+ from eventlet.green import subprocess
+ else:
+ import subprocess
+ except ImportError:
+ import subprocess
+
+
+def cms_verify(formatted, signing_cert_file_name, ca_file_name):
+ """
+ verifies the signature of the contents IAW CMS syntax
+ """
+ _ensure_subprocess()
+ process = subprocess.Popen(["openssl", "cms", "-verify",
+ "-certfile", signing_cert_file_name,
+ "-CAfile", ca_file_name,
+ "-inform", "PEM",
+ "-nosmimecap", "-nodetach",
+ "-nocerts", "-noattr"],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ output, err = process.communicate(formatted)
+ retcode = process.poll()
+ if retcode:
+ LOG.error('Verify error: %s' % err)
+ raise subprocess.CalledProcessError(retcode, "openssl", output=err)
+ return output
+
+
+def token_to_cms(signed_text):
+ copy_of_text = signed_text.replace('-', '/')
+
+ formatted = "-----BEGIN CMS-----\n"
+ line_length = 64
+ while len(copy_of_text) > 0:
+ if (len(copy_of_text) > line_length):
+ formatted += copy_of_text[:line_length]
+ copy_of_text = copy_of_text[line_length:]
+ else:
+ formatted += copy_of_text
+ copy_of_text = ""
+ formatted += "\n"
+
+ formatted += "-----END CMS-----\n"
+
+ return formatted
+
+
+def verify_token(token, signing_cert_file_name, ca_file_name):
+ return cms_verify(token_to_cms(token),
+ signing_cert_file_name,
+ ca_file_name)
+
+
+def is_ans1_token(token):
+ '''
+ thx to ayoung for sorting this out.
+
+ base64 decoded hex representation of MII is 3082
+ In [3]: binascii.hexlify(base64.b64decode('MII='))
+ Out[3]: '3082'
+
+ re: http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf
+
+ pg4: For tags from 0 to 30 the first octet is the identfier
+ pg10: Hex 30 means sequence, followed by the length of that sequence.
+ pg5: Second octet is the length octet
+ first bit indicates short or long form, next 7 bits encode the number
+ of subsequent octets that make up the content length octets as an
+ unsigned binary int
+
+ 82 = 10000010 (first bit indicates long form)
+ 0000010 = 2 octets of content length
+ so read the next 2 octets to get the length of the content.
+
+ In the case of a very large content length there could be a requirement to
+ have more than 2 octets to designate the content length, therefore
+ requiring us to check for MIM, MIQ, etc.
+ In [4]: base64.b64encode(binascii.a2b_hex('3083'))
+ Out[4]: 'MIM='
+ In [5]: base64.b64encode(binascii.a2b_hex('3084'))
+ Out[5]: 'MIQ='
+ Checking for MI would become invalid at 16 octets of content length
+ 10010000 = 90
+ In [6]: base64.b64encode(binascii.a2b_hex('3090'))
+ Out[6]: 'MJA='
+ Checking for just M is insufficient
+
+ But we will only check for MII:
+ Max length of the content using 2 octets is 7FFF or 32767
+ It's not practical to support a token of this length or greater in http
+ therefore, we will check for MII only and ignore the case of larger tokens
+ '''
+ return token[:3] == PKI_ANS1_PREFIX
+
+
+def cms_sign_text(text, signing_cert_file_name, signing_key_file_name):
+ """ Uses OpenSSL to sign a document
+ Produces a Base64 encoding of a DER formatted CMS Document
+ http://en.wikipedia.org/wiki/Cryptographic_Message_Syntax
+ """
+ _ensure_subprocess()
+ process = subprocess.Popen(["openssl", "cms", "-sign",
+ "-signer", signing_cert_file_name,
+ "-inkey", signing_key_file_name,
+ "-outform", "PEM",
+ "-nosmimecap", "-nodetach",
+ "-nocerts", "-noattr"],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ output, err = process.communicate(text)
+ retcode = process.poll()
+ if retcode or "Error" in err:
+ LOG.error('Signing error: %s' % err)
+ raise subprocess.CalledProcessError(retcode, "openssl")
+ return output
+
+
+def cms_sign_token(text, signing_cert_file_name, signing_key_file_name):
+ output = cms_sign_text(text, signing_cert_file_name, signing_key_file_name)
+ return cms_to_token(output)
+
+
+def cms_to_token(cms_text):
+
+ start_delim = "-----BEGIN CMS-----"
+ end_delim = "-----END CMS-----"
+ signed_text = cms_text
+ signed_text = signed_text.replace('/', '-')
+ signed_text = signed_text.replace(start_delim, '')
+ signed_text = signed_text.replace(end_delim, '')
+ signed_text = signed_text.replace('\n', '')
+
+ return signed_text
+
+
+def cms_hash_token(token_id):
+ """
+ return: for ans1_token, returns the hash of the passed in token
+ otherwise, returns what it was passed in.
+ """
+ if token_id is None:
+ return None
+ if is_ans1_token(token_id):
+ hasher = hashlib.md5()
+ hasher.update(token_id)
+ return hasher.hexdigest()
+ else:
+ return token_id