summaryrefslogtreecommitdiff
path: root/keystoneclient/common/cms.py
diff options
context:
space:
mode:
authorAdam Young <ayoung@redhat.com>2014-02-04 20:43:07 -0500
committerMorgan Fainberg <morgan.fainberg@gmail.com>2014-05-09 11:48:17 -0700
commit3d6d749e6f0fef682a88758e1a2f6c9e8e7bd23c (patch)
treeb5ffa079f9bdd6214c581e872ea668217834d312 /keystoneclient/common/cms.py
parenta95edc7f38d7c267f9efa6c4a413b4a04d2686b0 (diff)
downloadpython-keystoneclient-3d6d749e6f0fef682a88758e1a2f6c9e8e7bd23c.tar.gz
Compressed Signature and Validation
Allows for a new form of document signature. pkiz_sign will take data and encode it in a string that starts with the substring "PKIZ_". This prefix indicates that the data has been: 1) Signed via PKI in Crypto Message Syntax (CMS) in binary (DER) format 2) Compressed using zlib (comparable to gzip) 3) urlsafe-base64 decoded This process is reversed to validate the data. middleware/auth_token.py will be capable of validating Keystone tokens that are marshalled in the new format. The current existing "PKI" tokens will continue to be identified with "MII", issued by default, and validated as well. It will require corresponding changes on the Keystone server to issue the new token format. A separate script for generating the sample data used in the unit tests, examples/pki/gen_cmsz.py, also serves as an example of how to call the API from Python code. Some of the sample data for the old tests had to be regenerated. A stray comma in one of the JSON files made for non-parsing JSON. Blueprint: compress-tokens Closes-Bug: #1255321 Change-Id: Ia9a66ba3742da0bcd58c4c096b28cc8a66ad6569
Diffstat (limited to 'keystoneclient/common/cms.py')
-rw-r--r--keystoneclient/common/cms.py110
1 files changed, 101 insertions, 9 deletions
diff --git a/keystoneclient/common/cms.py b/keystoneclient/common/cms.py
index 96f8b61..156b1a1 100644
--- a/keystoneclient/common/cms.py
+++ b/keystoneclient/common/cms.py
@@ -19,9 +19,12 @@ If set_subprocess() is not called, this module will pick Python's subprocess
or eventlet.green.subprocess based on if os module is patched by eventlet.
"""
+import base64
import errno
import hashlib
import logging
+import zlib
+
import six
from keystoneclient import exceptions
@@ -30,6 +33,9 @@ from keystoneclient import exceptions
subprocess = None
LOG = logging.getLogger(__name__)
PKI_ASN1_PREFIX = 'MII'
+PKIZ_PREFIX = 'PKIZ_'
+PKIZ_CMS_FORM = 'DER'
+PKI_ASN1_FORM = 'PEM'
def _ensure_subprocess():
@@ -99,14 +105,30 @@ def _process_communicate_handle_oserror(process, data, files):
return output, err, retcode
-def cms_verify(formatted, signing_cert_file_name, ca_file_name):
+def _encoding_for_form(inform):
+ if inform == PKI_ASN1_FORM:
+ encoding = 'UTF-8'
+ elif inform == PKIZ_CMS_FORM:
+ encoding = 'hex'
+ else:
+ raise ValueError('"inform" must be either %s or %s' %
+ (PKI_ASN1_FORM, PKIZ_CMS_FORM))
+
+ return encoding
+
+
+def cms_verify(formatted, signing_cert_file_name, ca_file_name,
+ inform=PKI_ASN1_FORM):
"""Verifies the signature of the contents IAW CMS syntax.
:raises: subprocess.CalledProcessError
:raises: CertificateConfigError if certificate is not configured properly.
"""
_ensure_subprocess()
- data = bytearray(formatted, encoding='utf-8')
+ if isinstance(formatted, six.string_types):
+ data = bytearray(formatted, _encoding_for_form(inform))
+ else:
+ data = formatted
process = subprocess.Popen(['openssl', 'cms', '-verify',
'-certfile', signing_cert_file_name,
'-CAfile', ca_file_name,
@@ -133,7 +155,10 @@ def cms_verify(formatted, signing_cert_file_name, ca_file_name):
# Error opening certificate file not_exist_file
#
if retcode == 2:
- raise exceptions.CertificateConfigError(err)
+ if err.startswith('Error reading S/MIME message'):
+ raise exceptions.CMSError(err)
+ else:
+ raise exceptions.CertificateConfigError(err)
elif retcode:
# NOTE(dmllr): Python 2.6 compatibility:
# CalledProcessError did not have output keyword argument
@@ -143,6 +168,45 @@ def cms_verify(formatted, signing_cert_file_name, ca_file_name):
return output
+def is_pkiz(token_text):
+ """Determine if a token a cmsz token
+
+ Checks if the string has the prefix that indicates it is a
+ Crypto Message Syntax, Z compressed token.
+ """
+ return token_text.startswith(PKIZ_PREFIX)
+
+
+def pkiz_sign(text,
+ signing_cert_file_name,
+ signing_key_file_name,
+ compression_level=6):
+ signed = cms_sign_data(text,
+ signing_cert_file_name,
+ signing_key_file_name,
+ PKIZ_CMS_FORM)
+
+ compressed = zlib.compress(signed, compression_level)
+ encoded = PKIZ_PREFIX + base64.urlsafe_b64encode(
+ compressed).decode('utf-8')
+ return encoded
+
+
+def pkiz_uncompress(signed_text):
+ text = signed_text[len(PKIZ_PREFIX):].encode('utf-8')
+ unencoded = base64.urlsafe_b64decode(text)
+ uncompressed = zlib.decompress(unencoded)
+ return uncompressed
+
+
+def pkiz_verify(signed_text, signing_cert_file_name, ca_file_name):
+ uncompressed = pkiz_uncompress(signed_text)
+ return cms_verify(uncompressed, signing_cert_file_name, ca_file_name,
+ inform=PKIZ_CMS_FORM)
+
+
+# This function is deprecated and will be removed once the ASN1 token format
+# is no longer required. It is only here to be used for testing.
def token_to_cms(signed_text):
copy_of_text = signed_text.replace('-', '/')
@@ -225,14 +289,33 @@ def is_ans1_token(token):
return is_asn1_token(token)
-def cms_sign_text(text, signing_cert_file_name, signing_key_file_name):
+def cms_sign_text(data_to_sign, signing_cert_file_name, signing_key_file_name):
+ return cms_sign_data(data_to_sign, signing_cert_file_name,
+ signing_key_file_name)
+
+
+def cms_sign_data(data_to_sign, signing_cert_file_name, signing_key_file_name,
+ outform=PKI_ASN1_FORM):
"""Uses OpenSSL to sign a document.
Produces a Base64 encoding of a DER formatted CMS Document
http://en.wikipedia.org/wiki/Cryptographic_Message_Syntax
+
+ :param data_to_sign: data to sign
+ :param signing_cert_file_name: path to the X509 certificate containing
+ the public key associated with the private key used to sign the data
+ :param signing_key_file_name: path to the private key used to sign
+ the data
+ :param outform: Format for the signed document PKIZ_CMS_FORM or
+ PKI_ASN1_FORM
+
+
"""
_ensure_subprocess()
- data = bytearray(text, encoding='utf-8')
+ if isinstance(data_to_sign, six.string_types):
+ data = bytearray(data_to_sign, encoding='utf-8')
+ else:
+ data = data_to_sign
process = subprocess.Popen(['openssl', 'cms', '-sign',
'-signer', signing_cert_file_name,
'-inkey', signing_key_file_name,
@@ -248,12 +331,21 @@ def cms_sign_text(text, signing_cert_file_name, signing_key_file_name):
if retcode or ('Error' in err):
LOG.error('Signing error: %s' % err)
+ if retcode == 3:
+ LOG.error('Signing error: Unable to load certificate - '
+ 'ensure you have configured PKI with '
+ '"keystone-manage pki_setup"')
+ else:
+ LOG.error('Signing error: %s', err)
raise subprocess.CalledProcessError(retcode, 'openssl')
- return output.decode('utf-8')
+ if outform == PKI_ASN1_FORM:
+ return output.decode('utf-8')
+ else:
+ 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)
+ output = cms_sign_data(text, signing_cert_file_name, signing_key_file_name)
return cms_to_token(output)
@@ -273,12 +365,12 @@ def cms_to_token(cms_text):
def cms_hash_token(token_id, mode='md5'):
"""Hash PKI tokens.
- return: for asn1_token, returns the hash of the passed in token
+ return: for asn1 or pkiz tokens, returns the hash of the passed in token
otherwise, returns what it was passed in.
"""
if token_id is None:
return None
- if is_asn1_token(token_id):
+ if is_asn1_token(token_id) or is_pkiz(token_id):
hasher = hashlib.new(mode)
if isinstance(token_id, six.text_type):
token_id = token_id.encode('utf-8')