20
20
""" ``fedmsg.crypto.x509`` - X.509 backend for :mod:`fedmsg.crypto`. """
21
21
22
22
import logging
23
- import os
24
- import tempfile
25
23
import warnings
26
24
27
- from requests .exceptions import RequestException
28
25
import six
29
- try :
30
- # Else we need M2Crypto and m2ext
31
- import M2Crypto
32
- import m2ext
33
- _m2crypto = True
34
- except ImportError :
35
- _m2crypto = False
36
-
37
- from . import utils
38
26
from .x509_ng import _cryptography , sign as _crypto_sign , validate as _crypto_validate
39
- import fedmsg .crypto # noqa: E402
40
- import fedmsg .encoding # noqa: E402
41
27
42
28
43
29
_log = logging .getLogger (__name__ )
@@ -58,181 +44,10 @@ def _disabled_validate(*args, **kwargs):
58
44
' and "pyopenssl") or "m2crypto" are not available.' )
59
45
60
46
61
- def _m2crypto_sign (message , ssldir = None , certname = None , ** config ):
62
- """ Insert two new fields into the message dict and return it.
63
-
64
- Those fields are:
65
-
66
- - 'signature' - the computed RSA message digest of the JSON repr.
67
- - 'certificate' - the base64 X509 certificate of the sending host.
68
- """
69
- if ssldir is None or certname is None :
70
- error = "You must set the ssldir and certname keyword arguments."
71
- raise ValueError (error )
72
-
73
- message ['crypto' ] = 'x509'
74
-
75
- certificate = M2Crypto .X509 .load_cert (
76
- "%s/%s.crt" % (ssldir , certname )).as_pem ()
77
- # Opening this file requires elevated privileges in stg/prod.
78
- rsa_private = M2Crypto .RSA .load_key (
79
- "%s/%s.key" % (ssldir , certname ))
80
-
81
- digest = M2Crypto .EVP .MessageDigest ('sha1' )
82
- digest .update (fedmsg .encoding .dumps (message ))
83
-
84
- signature = rsa_private .sign (digest .digest ())
85
-
86
- # Return a new dict containing the pairs in the original message as well
87
- # as the new authn fields.
88
- return dict (message .items () + [
89
- ('signature' , signature .encode ('base64' ).decode ('ascii' )),
90
- ('certificate' , certificate .encode ('base64' ).decode ('ascii' )),
91
- ])
92
-
93
-
94
- def _m2crypto_validate (message , ssldir = None , ** config ):
95
- """ Return true or false if the message is signed appropriately.
96
-
97
- Four things must be true:
98
-
99
- 1) The X509 cert must be signed by our CA
100
- 2) The cert must not be in our CRL.
101
- 3) We must be able to verify the signature using the RSA public key
102
- contained in the X509 cert.
103
- 4) The topic of the message and the CN on the cert must appear in the
104
- :ref:`conf-routing-policy` dict.
105
-
106
- """
107
-
108
- if ssldir is None :
109
- raise ValueError ("You must set the ssldir keyword argument." )
110
-
111
- def fail (reason ):
112
- _log .warn ("Failed validation. %s" % reason )
113
- return False
114
-
115
- # Some sanity checking
116
- for field in ['signature' , 'certificate' ]:
117
- if field not in message :
118
- return fail ("No %r field found." % field )
119
- if not isinstance (message [field ], six .text_type ):
120
- _log .error ('msg[%r] is not a unicode string' % field )
121
- try :
122
- # Make an effort to decode it, it's very likely utf-8 since that's what
123
- # is hardcoded throughout fedmsg. Worst case scenario is it'll cause a
124
- # validation error when there shouldn't be one.
125
- message [field ] = message [field ].decode ('utf-8' )
126
- except UnicodeError as e :
127
- _log .error ("Unable to decode the message '%s' field: %s" , field , str (e ))
128
- return False
129
-
130
- # Peal off the auth datums
131
- signature = message ['signature' ].decode ('base64' )
132
- certificate = message ['certificate' ].decode ('base64' )
133
- message = fedmsg .crypto .strip_credentials (message )
134
-
135
- # Build an X509 object
136
- cert = M2Crypto .X509 .load_cert_string (certificate )
137
-
138
- # Validate the cert. Make sure it is signed by our CA.
139
- # validate_certificate will one day be a part of M2Crypto.SSL.Context
140
- # https://bugzilla.osafoundation.org/show_bug.cgi?id=11690
141
-
142
- ca_location = config .get ('ca_cert_location' , 'https://fedoraproject.org/fedmsg/ca.crt' )
143
- crl_location = config .get ('crl_location' , 'https://fedoraproject.org/fedmsg/crl.pem' )
144
- fd , cafile = tempfile .mkstemp ()
145
- try :
146
- ca_certificate , crl = utils .load_certificates (ca_location , crl_location )
147
- os .write (fd , ca_certificate .encode ('ascii' ))
148
- os .fsync (fd )
149
- ctx = m2ext .SSL .Context ()
150
- ctx .load_verify_locations (cafile = cafile )
151
- if not ctx .validate_certificate (cert ):
152
- ca_certificate , crl = utils .load_certificates (
153
- ca_location , crl_location , invalidate_cache = True )
154
- with open (cafile , 'w' ) as f :
155
- f .write (ca_certificate )
156
- ctx = m2ext .SSL .Context ()
157
- ctx .load_verify_locations (cafile = cafile )
158
- if not ctx .validate_certificate (cert ):
159
- return fail ("X509 certificate is not valid." )
160
- except (IOError , RequestException ) as e :
161
- _log .error (str (e ))
162
- return False
163
- finally :
164
- os .close (fd )
165
- os .remove (cafile )
166
-
167
- if crl :
168
- try :
169
- fd , crlfile = tempfile .mkstemp (text = True )
170
- os .write (fd , crl .encode ('ascii' ))
171
- os .fsync (fd )
172
- crl = M2Crypto .X509 .load_crl (crlfile )
173
- finally :
174
- os .close (fd )
175
- os .remove (crlfile )
176
- # FIXME -- We need to check that the CRL is signed by our own CA.
177
- # See https://bugzilla.osafoundation.org/show_bug.cgi?id=12954#c2
178
- # if not ctx.validate_certificate(crl):
179
- # return fail("X509 CRL is not valid.")
180
-
181
- # FIXME -- we check the CRL, but by doing string comparison ourselves.
182
- # This is not what we want to be doing.
183
- # There is a patch into M2Crypto to handle this for us. We should use it
184
- # once its integrated upstream.
185
- # See https://bugzilla.osafoundation.org/show_bug.cgi?id=12954#c2
186
- revoked_serials = [long (line .split (': ' )[1 ].strip (), base = 16 )
187
- for line in crl .as_text ().split ('\n ' )
188
- if 'Serial Number:' in line ]
189
- if cert .get_serial_number () in revoked_serials :
190
- subject = cert .get_subject ()
191
-
192
- signer = '(no CN)'
193
- if subject .nid .get ('CN' ):
194
- entry = subject .get_entries_by_nid (subject .nid ['CN' ])[0 ]
195
- if entry :
196
- signer = entry .get_data ().as_text ()
197
-
198
- return fail ("X509 cert %r, %r is in the Revocation List (CRL)" % (
199
- signer , cert .get_serial_number ()))
200
-
201
- # If the cert is good, then test to see if the signature in the messages
202
- # matches up with the provided cert.
203
- rsa_public = cert .get_pubkey ().get_rsa ()
204
- digest = M2Crypto .EVP .MessageDigest ('sha1' )
205
- digest .update (fedmsg .encoding .dumps (message ))
206
- try :
207
- if not rsa_public .verify (digest .digest (), signature ):
208
- raise M2Crypto .RSA .RSAError ("RSA signature failed to validate." )
209
- except M2Crypto .RSA .RSAError as e :
210
- return fail (str (e ))
211
-
212
- # Now we know that the cert is valid. The message is *authenticated*.
213
- # * Next step: Authorization *
214
-
215
- # Load our policy from the config dict.
216
- routing_policy = config .get ('routing_policy' , {})
217
-
218
- # Determine the name of the signer of the message.
219
- # This will be something like "shell-pkgs01.stg.phx2.fedoraproject.org"
220
- subject = cert .get_subject ()
221
- signer = subject .get_entries_by_nid (subject .nid ['CN' ])[0 ]\
222
- .get_data ().as_text ()
223
-
224
- return utils .validate_policy (
225
- message .get ('topic' ), signer , routing_policy , config .get ('routing_nitpicky' , False ))
226
-
227
-
228
- # Maintain the ``sign`` and ``validate`` APIs while preferring cryptography and
229
- # pyOpenSSL over M2Crypto.
47
+ # Maintain the ``sign`` and ``validate`` APIs
230
48
if _cryptography :
231
49
sign = _crypto_sign
232
50
validate = _crypto_validate
233
- elif _m2crypto :
234
- sign = _m2crypto_sign
235
- validate = _m2crypto_validate
236
51
else :
237
52
sign = _disabled_sign
238
53
validate = _disabled_validate
0 commit comments