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 # Credential must pass xmlsec1 verify
\r
162 cred_file = write_to_tempfile(cred.save_to_string())
\r
165 for x in trusted_roots:
\r
166 cert_args += ['--trusted-pem', x.filename]
\r
167 # FIXME: Why do we not need to specify the --node-id option as credential.py does?
\r
168 xmlsec1_args = [cred.xmlsec_path, '--verify'] + cert_args + [ cred_file]
\r
169 output = run_subprocess(xmlsec1_args, stdout=None, stderr=subprocess.PIPE)
\r
170 os.unlink(cred_file)
\r
173 # xmlsec errors have a msg= which is the interesting bit.
\r
174 # But does this go to stderr or stdout? Do we have it here?
\r
175 mstart = verified.find("msg=")
\r
177 if mstart > -1 and len(verified) > 4:
\r
178 mstart = mstart + 4
\r
179 mend = verified.find('\\', mstart)
\r
180 msg = verified[mstart:mend]
\r
183 return False, None, "ABAC credential failed to xmlsec1 verify: %s" % msg
\r
185 # Must say U.speaks_for(U)<-T
\r
186 if user_keyid != principal_keyid or \
\r
187 tool_keyid != subject_keyid or \
\r
188 role != ('speaks_for_%s' % user_keyid):
\r
189 return False, None, "ABAC statement doesn't assert U.speaks_for(U)<-T (%s)" % cred.get_summary_tostring()
\r
191 # If schema provided, validate against schema
\r
192 if HAVELXML and schema and os.path.exists(schema):
\r
193 from lxml import etree
\r
194 tree = etree.parse(StringIO(cred.xml))
\r
195 schema_doc = etree.parse(schema)
\r
196 xmlschema = etree.XMLSchema(schema_doc)
\r
197 if not xmlschema.validate(tree):
\r
198 error = xmlschema.error_log.last_error
\r
199 message = "%s: %s (line %s)" % (cred.get_summary_tostring(), error.message, error.line)
\r
200 return False, None, ("XML Credential schema invalid: %s" % message)
\r
203 # User certificate must validate against trusted roots
\r
205 user_gid.verify_chain(trusted_roots)
\r
206 except Exception, e:
\r
207 return False, None, \
\r
208 "Cred signer (user) cert not trusted: %s" % e
\r
210 # Tool certificate must validate against trusted roots
\r
212 tool_gid.verify_chain(trusted_roots)
\r
213 except Exception, e:
\r
214 return False, None, \
\r
215 "Tool cert not trusted: %s" % e
\r
217 return True, user_gid, ""
\r
219 # Determine if this is a speaks-for context. If so, validate
\r
220 # And return either the tool_cert (not speaks-for or not validated)
\r
221 # or the user cert (validated speaks-for)
\r
223 # credentials is a list of GENI-style credentials:
\r
224 # Either a cred string xml string, or Credential object of a tuple
\r
225 # [{'geni_type' : geni_type, 'geni_value : cred_value,
\r
226 # 'geni_version' : version}]
\r
227 # caller_gid is the raw X509 cert gid
\r
228 # options is the dictionary of API-provided options
\r
229 # trusted_roots is a list of Certificate objects from the system
\r
230 # trusted_root directory
\r
231 # Optionally, provide an XML schema against which to validate the credential
\r
232 def determine_speaks_for(logger, credentials, caller_gid, options, \
\r
233 trusted_roots, schema=None):
\r
234 if options and 'geni_speaking_for' in options:
\r
235 speaking_for_urn = options['geni_speaking_for'].strip()
\r
236 for cred in credentials:
\r
237 # Skip things that aren't ABAC credentials
\r
238 if type(cred) == dict:
\r
239 if cred['geni_type'] != ABACCredential.ABAC_CREDENTIAL_TYPE: continue
\r
240 cred_value = cred['geni_value']
\r
241 elif isinstance(cred, Credential):
\r
242 if not isinstance(cred, ABACCredential):
\r
247 if CredentialFactory.getType(cred) != ABACCredential.ABAC_CREDENTIAL_TYPE: continue
\r
250 # If the cred_value is xml, create the object
\r
251 if not isinstance(cred_value, ABACCredential):
\r
252 cred = CredentialFactory.createCred(cred_value)
\r
254 # print "Got a cred to check speaksfor for: %s" % cred.get_summary_tostring()
\r
255 # #cred.dump(True, True)
\r
256 # print "Caller: %s" % caller_gid.dump_string(2, True)
\r
257 # See if this is a valid speaks_for
\r
258 is_valid_speaks_for, user_gid, msg = \
\r
259 verify_speaks_for(cred,
\r
260 caller_gid, speaking_for_urn, \
\r
261 trusted_roots, schema, logger=logger)
\r
263 if is_valid_speaks_for:
\r
264 return user_gid # speaks-for
\r
267 logger.info("Got speaks-for option but not a valid speaks_for with this credential: %s" % msg)
\r
269 print "Got a speaks-for option but not a valid speaks_for with this credential: " + msg
\r
270 return caller_gid # Not speaks-for
\r
272 # Create an ABAC Speaks For credential using the ABACCredential object and it's encode&sign methods
\r
273 def create_sign_abaccred(tool_gid, user_gid, ma_gid, user_key_file, cred_filename, dur_days=365):
\r
274 print "Creating ABAC SpeaksFor using ABACCredential...\n"
\r
275 # Write out the user cert
\r
276 from tempfile import mkstemp
\r
277 ma_str = ma_gid.save_to_string()
\r
278 user_cert_str = user_gid.save_to_string()
\r
279 if not user_cert_str.endswith(ma_str):
\r
280 user_cert_str += ma_str
\r
281 fp, user_cert_filename = mkstemp(suffix='cred', text=True)
\r
282 fp = os.fdopen(fp, "w")
\r
283 fp.write(user_cert_str)
\r
287 cred = ABACCredential()
\r
288 cred.set_issuer_keys(user_key_file, user_cert_filename)
\r
289 tool_urn = tool_gid.get_urn()
\r
290 user_urn = user_gid.get_urn()
\r
291 user_keyid = get_cert_keyid(user_gid)
\r
292 tool_keyid = get_cert_keyid(tool_gid)
\r
293 cred.head = ABACElement(user_keyid, user_urn, "speaks_for_%s" % user_keyid)
\r
294 cred.tails.append(ABACElement(tool_keyid, tool_urn))
\r
295 cred.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(days=dur_days))
\r
296 cred.expiration = cred.expiration.replace(microsecond=0)
\r
298 # Produce the cred XML
\r
304 cred.save_to_file(cred_filename)
\r
305 print "Created ABAC credential: '%s' in file %s" % \
\r
306 (cred.get_summary_tostring(), cred_filename)
\r
308 # FIXME: Assumes xmlsec1 is on path
\r
309 # FIXME: Assumes signer is itself signed by an 'ma_gid' that can be trusted
\r
310 def create_speaks_for(tool_gid, user_gid, ma_gid, \
\r
311 user_key_file, cred_filename, dur_days=365):
\r
312 tool_urn = tool_gid.get_urn()
\r
313 user_urn = user_gid.get_urn()
\r
315 header = '<?xml version="1.0" encoding="UTF-8"?>'
\r
317 signature_block = \
\r
318 '<signatures>\n' + \
\r
319 signature_template + \
\r
321 template = header + '\n' + \
\r
322 '<signed-credential '
\r
323 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
324 template += '>\n' + \
\r
325 '<credential xml:id="%s">\n' + \
\r
326 '<type>abac</type>\n' + \
\r
328 '<owner_gid/>\n' + \
\r
329 '<owner_urn/>\n' + \
\r
330 '<target_gid/>\n' + \
\r
331 '<target_urn/>\n' + \
\r
333 '<expires>%s</expires>' +\
\r
336 '<version>%s</version>\n' + \
\r
338 '<ABACprincipal><keyid>%s</keyid><mnemonic>%s</mnemonic></ABACprincipal>\n' +\
\r
339 '<role>speaks_for_%s</role>\n' + \
\r
342 '<ABACprincipal><keyid>%s</keyid><mnemonic>%s</mnemonic></ABACprincipal>\n' +\
\r
346 '</credential>\n' + \
\r
347 signature_block + \
\r
348 '</signed-credential>\n'
\r
351 credential_duration = datetime.timedelta(days=dur_days)
\r
352 expiration = datetime.datetime.now(du_tz.tzutc()) + credential_duration
\r
353 expiration_str = expiration.strftime('%Y-%m-%dT%H:%M:%SZ') # FIXME: libabac can't handle .isoformat()
\r
356 user_keyid = get_cert_keyid(user_gid)
\r
357 tool_keyid = get_cert_keyid(tool_gid)
\r
358 unsigned_cred = template % (reference, expiration_str, version, \
\r
359 user_keyid, user_urn, user_keyid, tool_keyid, tool_urn, \
\r
360 reference, reference)
\r
361 unsigned_cred_filename = write_to_tempfile(unsigned_cred)
\r
363 # Now sign the file with xmlsec1
\r
364 # xmlsec1 --sign --privkey-pem privkey.pem,cert.pem
\r
365 # --output signed.xml tosign.xml
\r
366 pems = "%s,%s,%s" % (user_key_file, user_gid.get_filename(),
\r
367 ma_gid.get_filename())
\r
368 # FIXME: assumes xmlsec1 is on path
\r
369 cmd = ['xmlsec1', '--sign', '--privkey-pem', pems,
\r
370 '--output', cred_filename, unsigned_cred_filename]
\r
372 # print " ".join(cmd)
\r
373 sign_proc_output = run_subprocess(cmd, stdout=subprocess.PIPE, stderr=None)
\r
374 if sign_proc_output == None:
\r
375 print "OUTPUT = %s" % sign_proc_output
\r
377 print "Created ABAC credential: '%s speaks_for %s' in file %s" % \
\r
378 (tool_urn, user_urn, cred_filename)
\r
379 os.unlink(unsigned_cred_filename)
\r
383 if __name__ == "__main__":
\r
385 parser = optparse.OptionParser()
\r
386 parser.add_option('--cred_file',
\r
387 help='Name of credential file')
\r
388 parser.add_option('--tool_cert_file',
\r
389 help='Name of file containing tool certificate')
\r
390 parser.add_option('--user_urn',
\r
391 help='URN of speaks-for user')
\r
392 parser.add_option('--user_cert_file',
\r
393 help="filename of x509 certificate of signing user")
\r
394 parser.add_option('--ma_cert_file',
\r
395 help="filename of x509 cert of MA that signed user cert")
\r
396 parser.add_option('--user_key_file',
\r
397 help="filename of private key of signing user")
\r
398 parser.add_option('--trusted_roots_directory',
\r
399 help='Directory of trusted root certs')
\r
400 parser.add_option('--create',
\r
401 help="name of file of ABAC speaksfor cred to create")
\r
402 parser.add_option('--useObject', action='store_true', default=False,
\r
403 help='Use the ABACCredential object to create the credential (default False)')
\r
405 options, args = parser.parse_args(sys.argv)
\r
407 tool_gid = GID(filename=options.tool_cert_file)
\r
410 if options.user_cert_file and options.user_key_file \
\r
411 and options.ma_cert_file:
\r
412 user_gid = GID(filename=options.user_cert_file)
\r
413 ma_gid = GID(filename=options.ma_cert_file)
\r
414 if options.useObject:
\r
415 create_sign_abaccred(tool_gid, user_gid, ma_gid, \
\r
416 options.user_key_file, \
\r
419 create_speaks_for(tool_gid, user_gid, ma_gid, \
\r
420 options.user_key_file, \
\r
423 print "Usage: --create cred_file " + \
\r
424 "--user_cert_file user_cert_file" + \
\r
425 " --user_key_file user_key_file --ma_cert_file ma_cert_file"
\r
428 user_urn = options.user_urn
\r
430 # Get list of trusted rootcerts
\r
431 if options.cred_file and not options.trusted_roots_directory:
\r
432 sys.exit("Must supply --trusted_roots_directory to validate a credential")
\r
434 trusted_roots_directory = options.trusted_roots_directory
\r
436 [Certificate(filename=os.path.join(trusted_roots_directory, file)) \
\r
437 for file in os.listdir(trusted_roots_directory) \
\r
438 if file.endswith('.pem') and file != 'CATedCACerts.pem']
\r
440 cred = open(options.cred_file).read()
\r
442 creds = [{'geni_type' : ABACCredential.ABAC_CREDENTIAL_TYPE, 'geni_value' : cred,
\r
443 'geni_version' : '1'}]
\r
444 gid = determine_speaks_for(None, creds, tool_gid, \
\r
445 {'geni_speaking_for' : user_urn}, \
\r
449 print 'SPEAKS_FOR = %s' % (gid != tool_gid)
\r
450 print "CERT URN = %s" % gid.get_urn()
\r