1 #----------------------------------------------------------------------
\r
2 # Copyright (c) 2014 Raytheon BBN Technologies
\r
4 # Permission is hereby granted, free of charge, to any person obtaining
\r
5 # a copy of this software and/or hardware specification (the "Work") to
\r
6 # deal in the Work without restriction, including without limitation the
\r
7 # rights to use, copy, modify, merge, publish, distribute, sublicense,
\r
8 # and/or sell copies of the Work, and to permit persons to whom the Work
\r
9 # is furnished to do so, subject to the following conditions:
\r
11 # The above copyright notice and this permission notice shall be
\r
12 # included in all copies or substantial portions of the Work.
\r
14 # THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
\r
15 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
\r
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
\r
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
\r
18 # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
\r
19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
\r
20 # OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS
\r
22 #----------------------------------------------------------------------
\r
25 from dateutil import parser as du_parser, tz as du_tz
\r
31 from xml.dom.minidom import *
\r
32 from StringIO import StringIO
\r
34 from sfa.trust.certificate import Certificate
\r
35 from sfa.trust.credential import Credential, signature_template, HAVELXML
\r
36 from sfa.trust.abac_credential import ABACCredential, ABACElement
\r
37 from sfa.trust.credential_factory import CredentialFactory
\r
38 from sfa.trust.gid import GID
\r
40 # Routine to validate that a speaks-for credential
\r
41 # says what it claims to say:
\r
42 # It is a signed credential wherein the signer S is attesting to the
\r
44 # S.speaks_for(S)<-T Or "S says that T speaks for S"
\r
46 # Requires that openssl be installed and in the path
\r
47 # create_speaks_for requires that xmlsec1 be on the path
\r
49 # Simple XML helper functions
\r
51 # Find the text associated with first child text node
\r
52 def findTextChildValue(root):
\r
53 child = findChildNamed(root, '#text')
\r
54 if child: return str(child.nodeValue)
\r
57 # Find first child with given name
\r
58 def findChildNamed(root, name):
\r
59 for child in root.childNodes:
\r
60 if child.nodeName == name:
\r
64 # Write a string to a tempfile, returning name of tempfile
\r
65 def write_to_tempfile(str):
\r
66 str_fd, str_file = tempfile.mkstemp()
\r
68 os.write(str_fd, str)
\r
72 # Run a subprocess and return output
\r
73 def run_subprocess(cmd, stdout, stderr):
\r
75 proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr)
\r
78 output = proc.stdout.read()
\r
80 output = proc.returncode
\r
82 except Exception as e:
\r
83 raise Exception("Failed call to subprocess '%s': %s" % (" ".join(cmd), e))
\r
85 def get_cert_keyid(gid):
\r
86 """Extract the subject key identifier from the given certificate.
\r
87 Return they key id as lowercase string with no colon separators
\r
88 between pairs. The key id as shown in the text output of a
\r
89 certificate are in uppercase with colon separators.
\r
92 raw_key_id = gid.get_extension('subjectKeyIdentifier')
\r
93 # Raw has colons separating pairs, and all characters are upper case.
\r
94 # Remove the colons and convert to lower case.
\r
95 keyid = raw_key_id.replace(':', '').lower()
\r
98 # Pull the cert out of a list of certs in a PEM formatted cert string
\r
99 def grab_toplevel_cert(cert):
\r
100 start_label = '-----BEGIN CERTIFICATE-----'
\r
101 if cert.find(start_label) > -1:
\r
102 start_index = cert.find(start_label) + len(start_label)
\r
105 end_label = '-----END CERTIFICATE-----'
\r
106 end_index = cert.find(end_label)
\r
107 first_cert = cert[start_index:end_index]
\r
108 pieces = first_cert.split('\n')
\r
109 first_cert = "".join(pieces)
\r
112 # Validate that the given speaks-for credential represents the
\r
113 # statement User.speaks_for(User)<-Tool for the given user and tool certs
\r
114 # and was signed by the user
\r
116 # Boolean indicating whether the given credential
\r
118 # is an ABAC credential
\r
119 # was signed by the user associated with the speaking_for_urn
\r
120 # is verified by xmlsec1
\r
121 # asserts U.speaks_for(U)<-T ("user says that T may speak for user")
\r
122 # If schema provided, validate against schema
\r
123 # is trusted by given set of trusted roots (both user cert and tool cert)
\r
124 # String user certificate of speaking_for user if the above tests succeed
\r
126 # Error message indicating why the speaks_for call failed ("" otherwise)
\r
127 def verify_speaks_for(cred, tool_gid, speaking_for_urn, \
\r
128 trusted_roots, schema=None, logger=None):
\r
130 # Credential has not expired
\r
131 if cred.expiration and cred.expiration < datetime.datetime.utcnow():
\r
132 return False, None, "ABAC Credential expired at %s (%s)" % (cred.expiration.isoformat(), cred.get_summary_tostring())
\r
135 if cred.get_cred_type() != ABACCredential.ABAC_CREDENTIAL_TYPE:
\r
136 return False, None, "Credential not of type ABAC but %s" % cred.get_cred_type
\r
138 if cred.signature is None or cred.signature.gid is None:
\r
139 return False, None, "Credential malformed: missing signature or signer cert. Cred: %s" % cred.get_summary_tostring()
\r
140 user_gid = cred.signature.gid
\r
141 user_urn = user_gid.get_urn()
\r
143 # URN of signer from cert must match URN of 'speaking-for' argument
\r
144 if user_urn != speaking_for_urn:
\r
145 return False, None, "User URN from cred doesn't match speaking_for URN: %s != %s (cred %s)" % \
\r
146 (user_urn, speaking_for_urn, cred.get_summary_tostring())
\r
148 tails = cred.get_tails()
\r
149 if len(tails) != 1:
\r
150 return False, None, "Invalid ABAC-SF credential: Need exactly 1 tail element, got %d (%s)" % \
\r
151 (len(tails), cred.get_summary_tostring())
\r
153 user_keyid = get_cert_keyid(user_gid)
\r
154 tool_keyid = get_cert_keyid(tool_gid)
\r
155 subject_keyid = tails[0].get_principal_keyid()
\r
157 head = cred.get_head()
\r
158 principal_keyid = head.get_principal_keyid()
\r
159 role = head.get_role()
\r
161 logger.info('user keyid: %s' % user_keyid)
\r
162 logger.info('principal keyid: %s' % principal_keyid)
\r
163 logger.info('tool keyid: %s' % tool_keyid)
\r
164 logger.info('subject keyid: %s' % subject_keyid)
\r
165 logger.info('role: %s' % role)
\r
166 logger.info('user gid: %s' % user_gid.dump_string())
\r
167 f = open('/tmp/speaksfor/tool.gid', 'w')
\r
168 f.write(tool_gid.dump_string())
\r
171 # Credential must pass xmlsec1 verify
\r
172 cred_file = write_to_tempfile(cred.save_to_string())
\r
175 for x in trusted_roots:
\r
176 cert_args += ['--trusted-pem', x.filename]
\r
177 # FIXME: Why do we not need to specify the --node-id option as credential.py does?
\r
178 xmlsec1_args = [cred.xmlsec_path, '--verify'] + cert_args + [ cred_file]
\r
179 output = run_subprocess(xmlsec1_args, stdout=None, stderr=subprocess.PIPE)
\r
180 os.unlink(cred_file)
\r
183 # xmlsec errors have a msg= which is the interesting bit.
\r
184 # But does this go to stderr or stdout? Do we have it here?
\r
185 mstart = verified.find("msg=")
\r
187 if mstart > -1 and len(verified) > 4:
\r
188 mstart = mstart + 4
\r
189 mend = verified.find('\\', mstart)
\r
190 msg = verified[mstart:mend]
\r
193 return False, None, "ABAC credential failed to xmlsec1 verify: %s" % msg
\r
195 # Must say U.speaks_for(U)<-T
\r
196 if user_keyid != principal_keyid or \
\r
197 tool_keyid != subject_keyid or \
\r
198 role != ('speaks_for_%s' % user_keyid):
\r
199 return False, None, "ABAC statement doesn't assert U.speaks_for(U)<-T (%s)" % cred.get_summary_tostring()
\r
201 # If schema provided, validate against schema
\r
202 if HAVELXML and schema and os.path.exists(schema):
\r
203 from lxml import etree
\r
204 tree = etree.parse(StringIO(cred.xml))
\r
205 schema_doc = etree.parse(schema)
\r
206 xmlschema = etree.XMLSchema(schema_doc)
\r
207 if not xmlschema.validate(tree):
\r
208 error = xmlschema.error_log.last_error
\r
209 message = "%s: %s (line %s)" % (cred.get_summary_tostring(), error.message, error.line)
\r
210 return False, None, ("XML Credential schema invalid: %s" % message)
\r
213 # User certificate must validate against trusted roots
\r
215 user_gid.verify_chain(trusted_roots)
\r
216 except Exception, e:
\r
217 return False, None, \
\r
218 "Cred signer (user) cert not trusted: %s" % e
\r
220 # Tool certificate must validate against trusted roots
\r
222 tool_gid.verify_chain(trusted_roots)
\r
223 except Exception, e:
\r
224 return False, None, \
\r
225 "Tool cert not trusted: %s" % e
\r
227 return True, user_gid, ""
\r
229 # Determine if this is a speaks-for context. If so, validate
\r
230 # And return either the tool_cert (not speaks-for or not validated)
\r
231 # or the user cert (validated speaks-for)
\r
233 # credentials is a list of GENI-style credentials:
\r
234 # Either a cred string xml string, or Credential object of a tuple
\r
235 # [{'geni_type' : geni_type, 'geni_value : cred_value,
\r
236 # 'geni_version' : version}]
\r
237 # caller_gid is the raw X509 cert gid
\r
238 # options is the dictionary of API-provided options
\r
239 # trusted_roots is a list of Certificate objects from the system
\r
240 # trusted_root directory
\r
241 # Optionally, provide an XML schema against which to validate the credential
\r
242 def determine_speaks_for(logger, credentials, caller_gid, options, \
\r
243 trusted_roots, schema=None):
\r
244 logger.info(options)
\r
245 logger.info("geni speaking for:%s " % 'geni_speaking_for' in options)
\r
246 if options and 'geni_speaking_for' in options:
\r
247 speaking_for_urn = options['geni_speaking_for'].strip()
\r
248 for cred in credentials:
\r
249 # Skip things that aren't ABAC credentials
\r
250 if type(cred) == dict:
\r
251 if cred['geni_type'] != ABACCredential.ABAC_CREDENTIAL_TYPE: continue
\r
252 cred_value = cred['geni_value']
\r
253 elif isinstance(cred, Credential):
\r
254 if not isinstance(cred, ABACCredential):
\r
259 if CredentialFactory.getType(cred) != ABACCredential.ABAC_CREDENTIAL_TYPE: continue
\r
262 # If the cred_value is xml, create the object
\r
263 if not isinstance(cred_value, ABACCredential):
\r
264 cred = CredentialFactory.createCred(cred_value)
\r
266 # print "Got a cred to check speaksfor for: %s" % cred.get_summary_tostring()
\r
267 # #cred.dump(True, True)
\r
268 # print "Caller: %s" % caller_gid.dump_string(2, True)
\r
269 logger.info(cred.dump_string())
\r
270 f = open('/tmp/speaksfor/%s.cred' % cred, 'w')
\r
273 # See if this is a valid speaks_for
\r
274 is_valid_speaks_for, user_gid, msg = \
\r
275 verify_speaks_for(cred,
\r
276 caller_gid, speaking_for_urn, \
\r
277 trusted_roots, schema, logger=logger)
\r
279 if is_valid_speaks_for:
\r
280 return user_gid # speaks-for
\r
283 logger.info("Got speaks-for option but not a valid speaks_for with this credential: %s" % msg)
\r
285 print "Got a speaks-for option but not a valid speaks_for with this credential: " + msg
\r
286 return caller_gid # Not speaks-for
\r
288 # Create an ABAC Speaks For credential using the ABACCredential object and it's encode&sign methods
\r
289 def create_sign_abaccred(tool_gid, user_gid, ma_gid, user_key_file, cred_filename, dur_days=365):
\r
290 print "Creating ABAC SpeaksFor using ABACCredential...\n"
\r
291 # Write out the user cert
\r
292 from tempfile import mkstemp
\r
293 ma_str = ma_gid.save_to_string()
\r
294 user_cert_str = user_gid.save_to_string()
\r
295 if not user_cert_str.endswith(ma_str):
\r
296 user_cert_str += ma_str
\r
297 fp, user_cert_filename = mkstemp(suffix='cred', text=True)
\r
298 fp = os.fdopen(fp, "w")
\r
299 fp.write(user_cert_str)
\r
303 cred = ABACCredential()
\r
304 cred.set_issuer_keys(user_key_file, user_cert_filename)
\r
305 tool_urn = tool_gid.get_urn()
\r
306 user_urn = user_gid.get_urn()
\r
307 user_keyid = get_cert_keyid(user_gid)
\r
308 tool_keyid = get_cert_keyid(tool_gid)
\r
309 cred.head = ABACElement(user_keyid, user_urn, "speaks_for_%s" % user_keyid)
\r
310 cred.tails.append(ABACElement(tool_keyid, tool_urn))
\r
311 cred.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(days=dur_days))
\r
312 cred.expiration = cred.expiration.replace(microsecond=0)
\r
314 # Produce the cred XML
\r
320 cred.save_to_file(cred_filename)
\r
321 print "Created ABAC credential: '%s' in file %s" % \
\r
322 (cred.get_summary_tostring(), cred_filename)
\r
324 # FIXME: Assumes xmlsec1 is on path
\r
325 # FIXME: Assumes signer is itself signed by an 'ma_gid' that can be trusted
\r
326 def create_speaks_for(tool_gid, user_gid, ma_gid, \
\r
327 user_key_file, cred_filename, dur_days=365):
\r
328 tool_urn = tool_gid.get_urn()
\r
329 user_urn = user_gid.get_urn()
\r
331 header = '<?xml version="1.0" encoding="UTF-8"?>'
\r
333 signature_block = \
\r
334 '<signatures>\n' + \
\r
335 signature_template + \
\r
337 template = header + '\n' + \
\r
338 '<signed-credential '
\r
339 template += 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.geni.net/resources/credential/2/credential.xsd" xsi:schemaLocation="http://www.protogeni.net/resources/credential/ext/policy/1 http://www.protogeni.net/resources/credential/ext/policy/1/policy.xsd"'
\r
340 template += '>\n' + \
\r
341 '<credential xml:id="%s">\n' + \
\r
342 '<type>abac</type>\n' + \
\r
344 '<owner_gid/>\n' + \
\r
345 '<owner_urn/>\n' + \
\r
346 '<target_gid/>\n' + \
\r
347 '<target_urn/>\n' + \
\r
349 '<expires>%s</expires>' +\
\r
352 '<version>%s</version>\n' + \
\r
354 '<ABACprincipal><keyid>%s</keyid><mnemonic>%s</mnemonic></ABACprincipal>\n' +\
\r
355 '<role>speaks_for_%s</role>\n' + \
\r
358 '<ABACprincipal><keyid>%s</keyid><mnemonic>%s</mnemonic></ABACprincipal>\n' +\
\r
362 '</credential>\n' + \
\r
363 signature_block + \
\r
364 '</signed-credential>\n'
\r
367 credential_duration = datetime.timedelta(days=dur_days)
\r
368 expiration = datetime.datetime.now(du_tz.tzutc()) + credential_duration
\r
369 expiration_str = expiration.strftime('%Y-%m-%dT%H:%M:%SZ') # FIXME: libabac can't handle .isoformat()
\r
372 user_keyid = get_cert_keyid(user_gid)
\r
373 tool_keyid = get_cert_keyid(tool_gid)
\r
374 unsigned_cred = template % (reference, expiration_str, version, \
\r
375 user_keyid, user_urn, user_keyid, tool_keyid, tool_urn, \
\r
376 reference, reference)
\r
377 unsigned_cred_filename = write_to_tempfile(unsigned_cred)
\r
379 # Now sign the file with xmlsec1
\r
380 # xmlsec1 --sign --privkey-pem privkey.pem,cert.pem
\r
381 # --output signed.xml tosign.xml
\r
382 pems = "%s,%s,%s" % (user_key_file, user_gid.get_filename(),
\r
383 ma_gid.get_filename())
\r
384 # FIXME: assumes xmlsec1 is on path
\r
385 cmd = ['xmlsec1', '--sign', '--privkey-pem', pems,
\r
386 '--output', cred_filename, unsigned_cred_filename]
\r
388 # print " ".join(cmd)
\r
389 sign_proc_output = run_subprocess(cmd, stdout=subprocess.PIPE, stderr=None)
\r
390 if sign_proc_output == None:
\r
391 print "OUTPUT = %s" % sign_proc_output
\r
393 print "Created ABAC credential: '%s speaks_for %s' in file %s" % \
\r
394 (tool_urn, user_urn, cred_filename)
\r
395 os.unlink(unsigned_cred_filename)
\r
399 if __name__ == "__main__":
\r
401 parser = optparse.OptionParser()
\r
402 parser.add_option('--cred_file',
\r
403 help='Name of credential file')
\r
404 parser.add_option('--tool_cert_file',
\r
405 help='Name of file containing tool certificate')
\r
406 parser.add_option('--user_urn',
\r
407 help='URN of speaks-for user')
\r
408 parser.add_option('--user_cert_file',
\r
409 help="filename of x509 certificate of signing user")
\r
410 parser.add_option('--ma_cert_file',
\r
411 help="filename of x509 cert of MA that signed user cert")
\r
412 parser.add_option('--user_key_file',
\r
413 help="filename of private key of signing user")
\r
414 parser.add_option('--trusted_roots_directory',
\r
415 help='Directory of trusted root certs')
\r
416 parser.add_option('--create',
\r
417 help="name of file of ABAC speaksfor cred to create")
\r
418 parser.add_option('--useObject', action='store_true', default=False,
\r
419 help='Use the ABACCredential object to create the credential (default False)')
\r
421 options, args = parser.parse_args(sys.argv)
\r
423 tool_gid = GID(filename=options.tool_cert_file)
\r
426 if options.user_cert_file and options.user_key_file \
\r
427 and options.ma_cert_file:
\r
428 user_gid = GID(filename=options.user_cert_file)
\r
429 ma_gid = GID(filename=options.ma_cert_file)
\r
430 if options.useObject:
\r
431 create_sign_abaccred(tool_gid, user_gid, ma_gid, \
\r
432 options.user_key_file, \
\r
435 create_speaks_for(tool_gid, user_gid, ma_gid, \
\r
436 options.user_key_file, \
\r
439 print "Usage: --create cred_file " + \
\r
440 "--user_cert_file user_cert_file" + \
\r
441 " --user_key_file user_key_file --ma_cert_file ma_cert_file"
\r
444 user_urn = options.user_urn
\r
446 # Get list of trusted rootcerts
\r
447 if options.cred_file and not options.trusted_roots_directory:
\r
448 sys.exit("Must supply --trusted_roots_directory to validate a credential")
\r
450 trusted_roots_directory = options.trusted_roots_directory
\r
452 [Certificate(filename=os.path.join(trusted_roots_directory, file)) \
\r
453 for file in os.listdir(trusted_roots_directory) \
\r
454 if file.endswith('.pem') and file != 'CATedCACerts.pem']
\r
456 cred = open(options.cred_file).read()
\r
458 creds = [{'geni_type' : ABACCredential.ABAC_CREDENTIAL_TYPE, 'geni_value' : cred,
\r
459 'geni_version' : '1'}]
\r
460 gid = determine_speaks_for(None, creds, tool_gid, \
\r
461 {'geni_speaking_for' : user_urn}, \
\r
465 print 'SPEAKS_FOR = %s' % (gid != tool_gid)
\r
466 print "CERT URN = %s" % gid.get_urn()
\r