diff options
| author | Adam Young <ayoung@redhat.com> | 2014-02-04 20:43:07 -0500 |
|---|---|---|
| committer | Morgan Fainberg <morgan.fainberg@gmail.com> | 2014-05-09 11:48:17 -0700 |
| commit | 3d6d749e6f0fef682a88758e1a2f6c9e8e7bd23c (patch) | |
| tree | b5ffa079f9bdd6214c581e872ea668217834d312 /keystoneclient/common/cms.py | |
| parent | a95edc7f38d7c267f9efa6c4a413b4a04d2686b0 (diff) | |
| download | python-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.py | 110 |
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') |
