From f51835648d656aaf51c2c1d428befc1c3e45ce69 Mon Sep 17 00:00:00 2001 From: Mikael Arguedas Date: Wed, 18 Jan 2017 10:49:54 -0800 Subject: [PATCH 1/9] parse policies.yaml and set access control --- sros2.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/sros2.py b/sros2.py index d1f84fa5..03379cd4 100644 --- a/sros2.py +++ b/sros2.py @@ -80,7 +80,7 @@ def create_ca_key_cert(ecdsa_param_path, ca_conf_path, ca_key_path, ca_cert_path (ecdsa_param_path, ca_key_path, ca_cert_path, ca_conf_path)) -def create_governance_file(path): +def create_governance_file(path, domain_id): # for this application we are only looking to authenticate and encrypt; # we do not need/want access control at this point. with open(path, 'w') as f: @@ -90,7 +90,7 @@ def create_governance_file(path): xsi:noNamespaceSchemaLocation="http://www.omg.org/spec/DDS-SECURITY/20140301/dds_security_governance.xsd"> - * + %s FALSE TRUE ENCRYPT @@ -100,8 +100,8 @@ def create_governance_file(path): * TRUE - FALSE - FALSE + TRUE + TRUE ENCRYPT ENCRYPT @@ -109,7 +109,7 @@ def create_governance_file(path): -""") +""" % domain_id) def create_signed_governance_file(signed_gov_path, gov_path, ca_cert_path, ca_key_path): @@ -152,9 +152,10 @@ def create_keystore(args): # create governance file gov_path = os.path.join(root, 'governance.xml') + domain_id = os.getenv('ROS_DOMAIN_ID', 0) if not os.path.isfile(gov_path): print("creating governance file: %s" % gov_path) - create_governance_file(gov_path) + create_governance_file(gov_path, domain_id) else: print("found governance file, not creating a new one!") @@ -232,9 +233,8 @@ def create_cert(root_path, name): (req_relpath, cert_relpath), root_path) -def create_permissions_file(path, name): - with open(path, 'w') as f: - f.write("""\ +def create_permissions_file(path, name, domain_id, permissions_dict): + permission_str = """\ @@ -244,10 +244,54 @@ def create_permissions_file(path, name): 2016122000 2026122000 - ALLOW + + %s +""" % (name, domain_id) + # access control only on topics for now + topic_dict = permissions_dict['topics'] + if topic_dict is not None and topic_dict != {}: + # we have some policies to add ! + for topic_name, policy in topic_dict.items(): + if policy['allow'] == 's': + tag = 'subscribe' + elif policy['allow'] == 'p': + tag = 'publish' + else: + print("unknown permission policy '%s', skipping" % policy['allow']) + continue + permission_str += """\ + <%s> + %s + +""" % (tag, topic_name, tag) + # DCPS* is necessary for builtin data readers + permission_str += """\ + + DCPS* + + + DENY -""" % name) +""" + with open(path, 'w') as f: + f.write(permission_str) + + +def get_permissions(name): + import yaml + ros_secure_root = os.getenv('ROS_SECURE_ROOT', '') + if not os.path.isdir(ros_secure_root): + print('ROS_SECURE_ROOT need to be set to an existing folder') + sys.exit(1) + policy_file_path = os.path.join(os.path.dirname(ros_secure_root), 'policies.yaml') + with open(policy_file_path, 'r') as graph_permissions_file: + try: + graph = yaml.load(graph_permissions_file) + except yaml.YAMLError as e: + print(e) + sys.exit(1) + return graph['nodes'][name] def create_signed_permissions_file( @@ -311,10 +355,12 @@ def create_key(args): else: print("found cert; not creating a new one!") - permissions_path = os.path.join(key_dir, 'permissions.txt') + domain_id = os.getenv('ROS_DOMAIN_ID', 0) + permissions_dict = get_permissions(name) + permissions_path = os.path.join(key_dir, 'permissions.xml') if not os.path.isfile(permissions_path): print("creating permissions file") - create_permissions_file(permissions_path, name) + create_permissions_file(permissions_path, name, domain_id, permissions_dict) else: print("found permissions file; not creating a new one!") From 821592e521aeb185226125c9b8fc384dbfd8b564 Mon Sep 17 00:00:00 2001 From: Mikael Arguedas Date: Wed, 18 Jan 2017 10:54:09 -0800 Subject: [PATCH 2/9] add policies.yaml file example --- policies.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 policies.yaml diff --git a/policies.yaml b/policies.yaml new file mode 100644 index 00000000..62a0c5fa --- /dev/null +++ b/policies.yaml @@ -0,0 +1,19 @@ +nodes: + secure_subscriber: + topics: + #'*': + # allow: s # allow secure_subscriber to subscribe to all topics + chatter: # for the topic chatter + allow: s # can subscribe + chatter2: # for the topic chatter2 + allow: s # can subscribe + secure_publisher: + topics: + #'*': + # allow: p # allow secure_publisher to publish on all topics + chatter: + allow: p # allow secure_publisher to publish onto chatter + chatter2: + allow: p # allow secure_publisher to publish onto chatter2 + chatter3: + allow: p # allow secure_publisher to publish onto chatter3 From e0d4494f0094ae20fa979e96b60bad6315947b7f Mon Sep 17 00:00:00 2001 From: Mikael Arguedas Date: Wed, 18 Jan 2017 11:17:04 -0800 Subject: [PATCH 3/9] if no policy found: allow everything --- sros2.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/sros2.py b/sros2.py index 03379cd4..96be2296 100644 --- a/sros2.py +++ b/sros2.py @@ -237,7 +237,7 @@ def create_permissions_file(path, name, domain_id, permissions_dict): permission_str = """\ - + CN=%s @@ -246,7 +246,7 @@ def create_permissions_file(path, name, domain_id, permissions_dict): %s -""" % (name, domain_id) +""" % (name, name, domain_id) # access control only on topics for now topic_dict = permissions_dict['topics'] if topic_dict is not None and topic_dict != {}: @@ -264,11 +264,24 @@ def create_permissions_file(path, name, domain_id, permissions_dict): %s """ % (tag, topic_name, tag) - # DCPS* is necessary for builtin data readers - permission_str += """\ + # DCPS* is necessary for builtin data readers + permission_str += """\ DCPS* +""" + else: + # no policy found: allow everything! + permission_str += """\ + + * + + + * + +""" + + permission_str += """\ DENY From f65836fb615d9129cc7d862f0541b5c1b9ffe251 Mon Sep 17 00:00:00 2001 From: Mikael Arguedas Date: Wed, 18 Jan 2017 12:54:49 -0800 Subject: [PATCH 4/9] add create_permissions verb --- sros2.py | 66 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/sros2.py b/sros2.py index 96be2296..ffbe2d0c 100644 --- a/sros2.py +++ b/sros2.py @@ -233,7 +233,7 @@ def create_cert(root_path, name): (req_relpath, cert_relpath), root_path) -def create_permissions_file(path, name, domain_id, permissions_dict): +def create_permission_file(path, name, domain_id, permissions_dict): permission_str = """\ @@ -291,13 +291,10 @@ def create_permissions_file(path, name, domain_id, permissions_dict): f.write(permission_str) -def get_permissions(name): +def get_permissions(name, policy_file_path): import yaml - ros_secure_root = os.getenv('ROS_SECURE_ROOT', '') - if not os.path.isdir(ros_secure_root): - print('ROS_SECURE_ROOT need to be set to an existing folder') - sys.exit(1) - policy_file_path = os.path.join(os.path.dirname(ros_secure_root), 'policies.yaml') + if not os.path.isfile(policy_file_path): + return {'topics': {}} with open(policy_file_path, 'r') as graph_permissions_file: try: graph = yaml.load(graph_permissions_file) @@ -314,6 +311,34 @@ def create_signed_permissions_file( (permissions_path, signed_permissions_path, ca_cert_path, ca_key_path)) +def create_permission(args): + print(args) + root = args.ROOT + name = args.NAME + policy_file_path = args.POLICY_FILE_PATH + domain_id = os.getenv('ROS_DOMAIN_ID', 0) + + key_dir = os.path.join(root, name) + print('key_dir %s' % key_dir) + permissions_dict = get_permissions(name, policy_file_path) + permissions_path = os.path.join(key_dir, 'permissions.xml') + if not os.path.isfile(permissions_path): + print("creating permissions file") + create_permission_file(permissions_path, name, domain_id, permissions_dict) + else: + print("found permissions file; not creating a new one!") + + signed_permissions_path = os.path.join(key_dir, 'permissions.p7s') + keystore_ca_cert_path = os.path.join(root, 'ca.cert.pem') + keystore_ca_key_path = os.path.join(root, 'ca.key.pem') + if not os.path.isfile(signed_permissions_path): + print("creating signed permissions file") + create_signed_permissions_file( + permissions_path, signed_permissions_path, keystore_ca_cert_path, keystore_ca_key_path) + else: + print("found signed permissions file; not creating a new one!") + + def create_key(args): print(args) root = args.ROOT @@ -368,25 +393,6 @@ def create_key(args): else: print("found cert; not creating a new one!") - domain_id = os.getenv('ROS_DOMAIN_ID', 0) - permissions_dict = get_permissions(name) - permissions_path = os.path.join(key_dir, 'permissions.xml') - if not os.path.isfile(permissions_path): - print("creating permissions file") - create_permissions_file(permissions_path, name, domain_id, permissions_dict) - else: - print("found permissions file; not creating a new one!") - - signed_permissions_path = os.path.join(key_dir, 'permissions.p7s') - keystore_ca_cert_path = os.path.join(root, 'ca.cert.pem') - keystore_ca_key_path = os.path.join(root, 'ca.key.pem') - if not os.path.isfile(signed_permissions_path): - print("creating signed permissions file") - create_signed_permissions_file( - permissions_path, signed_permissions_path, keystore_ca_cert_path, keystore_ca_key_path) - else: - print("found signed permissions file; not creating a new one!") - return True @@ -429,6 +435,12 @@ def main(sysargs=None): parser_distribute_keys.add_argument('ROOT', help='root path of keystore') parser_distribute_keys.add_argument('TARGET', help='target keystore path') + parser_distribute_keys = subparsers.add_parser('create_permission') + parser_distribute_keys.set_defaults(which='create_permission') + parser_distribute_keys.add_argument('ROOT', help='root path of keystore') + parser_distribute_keys.add_argument('NAME', help='key name, aka ROS node name') + parser_distribute_keys.add_argument( + 'POLICY_FILE_PATH', help='path of the permission yaml file') args = parser.parse_args(sysargs) if '-h' in sysargs or '--help' in sysargs: @@ -444,6 +456,8 @@ def main(sysargs=None): result = create_keystore(args) elif args.which == 'create_key': result = create_key(args) + elif args.which == 'create_permission': + result = create_permission(args) elif args.which == 'list_keys': result = list_keys(args) elif args.which == 'distribute_key': From 0d8afdbbf771ec553a68bb25db13aad5e13c5f85 Mon Sep 17 00:00:00 2001 From: Mikael Arguedas Date: Wed, 18 Jan 2017 13:23:04 -0800 Subject: [PATCH 5/9] added cpp talker listener permissions --- policies.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/policies.yaml b/policies.yaml index 62a0c5fa..bb8a45c4 100644 --- a/policies.yaml +++ b/policies.yaml @@ -17,3 +17,15 @@ nodes: allow: p # allow secure_publisher to publish onto chatter2 chatter3: allow: p # allow secure_publisher to publish onto chatter3 + listener: + topics: + chatter: # for the topic chatter + allow: s # can subscribe + parameter_events: + allow: p # can publish + talker: + topics: + chatter: + allow: p # allow secure_publisher to publish onto chatter + parameter_events: + allow: p # can publish From b805d8a83930e76b70162d2c7b92c76bb4491d12 Mon Sep 17 00:00:00 2001 From: Mikael Arguedas Date: Wed, 18 Jan 2017 13:35:50 -0800 Subject: [PATCH 6/9] update readme to generate permissions --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4683ba97..69d2b9d8 100644 --- a/README.md +++ b/README.md @@ -85,12 +85,23 @@ source install/setup.bash In one terminal (after preparing the environment as previously described), we will create a keystore in `~/sros2/demo_keys` : -``` +```bash sros2 create_keystore demo_keys +``` +Then we will generate certificates and keys for our talker and listener nodes: +```bash sros2 create_key demo_keys talker sros2 create_key demo_keys listener ``` - +Then we define the policies of our nodes: +```bash +cp ~/sros2/src/ros2/sros2/policies.yaml ./demo_keys/ +``` +Finally we will create permission files +```bash +sros2 create_permission demo_keys talker demo_keys/policies.yaml +sros2 create_permission demo_keys listener demo_keys/policies.yaml +``` Then, in one terminal (after preparing the terminal as previously described), we can set the `ROS_SECURE_ROOT` to our keystore path, and then run the `talker` demo program: From 0e03de5d7a3a1eaeb8dfd98260aa9ff12261dd13 Mon Sep 17 00:00:00 2001 From: Mikael Arguedas Date: Wed, 25 Jan 2017 10:32:24 -0800 Subject: [PATCH 7/9] README typos and sugar --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 69d2b9d8..e76c6794 100644 --- a/README.md +++ b/README.md @@ -88,12 +88,12 @@ will create a keystore in `~/sros2/demo_keys` : ```bash sros2 create_keystore demo_keys ``` -Then we will generate certificates and keys for our talker and listener nodes: +Generate certificates and keys for our `talker` and `listener` nodes: ```bash sros2 create_key demo_keys talker sros2 create_key demo_keys listener ``` -Then we define the policies of our nodes: +Define the policies of our nodes: ```bash cp ~/sros2/src/ros2/sros2/policies.yaml ./demo_keys/ ``` @@ -115,7 +115,7 @@ will do the same thing with the `listener` program: ROS_SECURE_ROOT=~/sros2/demo_keys listener ``` -At thsi point, your `talker` and `listener` nodes should be communicating +At this point, your `talker` and `listener` nodes should be communicating securely! Hooray! ## Two different machines From 03b6a2010e63637f0a1bc8957ff5ece616108676 Mon Sep 17 00:00:00 2001 From: Morgan Quigley Date: Fri, 27 Jan 2017 16:15:22 -0800 Subject: [PATCH 8/9] a few minor tweaks to allow overwriting of permissions files and minor doc changes --- README.md | 37 ++++++++++++++++++++++++++++++++----- examples/sample_policy.yaml | 31 +++++++++++++++++++++++++++++++ policies.yaml | 31 ------------------------------- sros2.py | 15 ++++----------- 4 files changed, 67 insertions(+), 47 deletions(-) create mode 100644 examples/sample_policy.yaml delete mode 100644 policies.yaml diff --git a/README.md b/README.md index e76c6794..02531a4e 100644 --- a/README.md +++ b/README.md @@ -93,11 +93,39 @@ Generate certificates and keys for our `talker` and `listener` nodes: sros2 create_key demo_keys talker sros2 create_key demo_keys listener ``` -Define the policies of our nodes: + +Then, in one terminal (after preparing the terminal as previously described), +we can set the `ROS_SECURE_ROOT` to our keystore path, and then run the +`talker` demo program: +``` +ROS_SECURE_ROOT=~/sros2/demo_keys talker +``` + +In another terminal (after preparing the terminal as previously described), we +will do the same thing with the `listener` program: +``` +ROS_SECURE_ROOT=~/sros2/demo_keys listener +``` + +At this point, your `talker` and `listener` nodes should be communicating +securely, using authentication and encryption! Hooray! + +## Access Control +The previous demo used authentication and encryption, but not access control, +which means that any authenticated node would be able to publish and subscribe +to any data stream (aka topic). To increase the level of security in the +system, you can define strict limits, known as access control, which restrict +what each node is able to do. For example, one node would be able to publish to +a particular topic, and another node might be able to subscribe to that topic. +To do this, we will use the sample policy file provided in +`examples/sample_policy.yaml` + +First, we will copy this sample policy file into our keystore: ```bash -cp ~/sros2/src/ros2/sros2/policies.yaml ./demo_keys/ +cp ~/sros2/src/ros2/sros2/examples/sample_policy.yaml ./demo_keys/ ``` -Finally we will create permission files +And now we will use it to generate the XML permission files expected by the +middleware: ```bash sros2 create_permission demo_keys talker demo_keys/policies.yaml sros2 create_permission demo_keys listener demo_keys/policies.yaml @@ -108,7 +136,6 @@ we can set the `ROS_SECURE_ROOT` to our keystore path, and then run the ``` ROS_SECURE_ROOT=~/sros2/demo_keys talker ``` - In another terminal (after preparing the terminal as previously described), we will do the same thing with the `listener` program: ``` @@ -116,7 +143,7 @@ ROS_SECURE_ROOT=~/sros2/demo_keys listener ``` At this point, your `talker` and `listener` nodes should be communicating -securely! Hooray! +securely, using explicit access control lists! Hooray! ## Two different machines diff --git a/examples/sample_policy.yaml b/examples/sample_policy.yaml new file mode 100644 index 00000000..5f308c00 --- /dev/null +++ b/examples/sample_policy.yaml @@ -0,0 +1,31 @@ +nodes: + listener: + topics: + chatter: + allow: s # can subscribe to chatter + parameter_events: + allow: p # can publish on parameter_events + talker: + topics: + chatter: + allow: p # allow secure_publisher to publish onto chatter + parameter_events: + allow: p # can publish + listener_py: + topics: + #'*': + # allow: s # this would allow the listener to subscribe to all topics + chatter: + allow: s # can subscribe to chatter + chatter2: + allow: s # can subscribe to chatter2 + talker_py: + topics: + #'*': + # allow: p # this would allow the talker to publish on all topics + chatter: + allow: p # allow publishing on chatter + chatter2: + allow: p # allow publishing on chatter2 + chatter3: + allow: p # allow publishing on chatter3 diff --git a/policies.yaml b/policies.yaml deleted file mode 100644 index bb8a45c4..00000000 --- a/policies.yaml +++ /dev/null @@ -1,31 +0,0 @@ -nodes: - secure_subscriber: - topics: - #'*': - # allow: s # allow secure_subscriber to subscribe to all topics - chatter: # for the topic chatter - allow: s # can subscribe - chatter2: # for the topic chatter2 - allow: s # can subscribe - secure_publisher: - topics: - #'*': - # allow: p # allow secure_publisher to publish on all topics - chatter: - allow: p # allow secure_publisher to publish onto chatter - chatter2: - allow: p # allow secure_publisher to publish onto chatter2 - chatter3: - allow: p # allow secure_publisher to publish onto chatter3 - listener: - topics: - chatter: # for the topic chatter - allow: s # can subscribe - parameter_events: - allow: p # can publish - talker: - topics: - chatter: - allow: p # allow secure_publisher to publish onto chatter - parameter_events: - allow: p # can publish diff --git a/sros2.py b/sros2.py index ffbe2d0c..f1c10489 100644 --- a/sros2.py +++ b/sros2.py @@ -322,21 +322,14 @@ def create_permission(args): print('key_dir %s' % key_dir) permissions_dict = get_permissions(name, policy_file_path) permissions_path = os.path.join(key_dir, 'permissions.xml') - if not os.path.isfile(permissions_path): - print("creating permissions file") - create_permission_file(permissions_path, name, domain_id, permissions_dict) - else: - print("found permissions file; not creating a new one!") + create_permission_file(permissions_path, name, domain_id, permissions_dict) signed_permissions_path = os.path.join(key_dir, 'permissions.p7s') keystore_ca_cert_path = os.path.join(root, 'ca.cert.pem') keystore_ca_key_path = os.path.join(root, 'ca.key.pem') - if not os.path.isfile(signed_permissions_path): - print("creating signed permissions file") - create_signed_permissions_file( - permissions_path, signed_permissions_path, keystore_ca_cert_path, keystore_ca_key_path) - else: - print("found signed permissions file; not creating a new one!") + create_signed_permissions_file( + permissions_path, signed_permissions_path, + keystore_ca_cert_path, keystore_ca_key_path) def create_key(args): From 3bad13cb230ff08a146d51743af854fd75f0b76d Mon Sep 17 00:00:00 2001 From: Morgan Quigley Date: Fri, 27 Jan 2017 16:18:32 -0800 Subject: [PATCH 9/9] trivial tweak to rename subparser --- sros2.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sros2.py b/sros2.py index f1c10489..f1441bcf 100644 --- a/sros2.py +++ b/sros2.py @@ -428,12 +428,13 @@ def main(sysargs=None): parser_distribute_keys.add_argument('ROOT', help='root path of keystore') parser_distribute_keys.add_argument('TARGET', help='target keystore path') - parser_distribute_keys = subparsers.add_parser('create_permission') - parser_distribute_keys.set_defaults(which='create_permission') - parser_distribute_keys.add_argument('ROOT', help='root path of keystore') - parser_distribute_keys.add_argument('NAME', help='key name, aka ROS node name') - parser_distribute_keys.add_argument( + parser_create_perm = subparsers.add_parser('create_permission') + parser_create_perm.set_defaults(which='create_permission') + parser_create_perm.add_argument('ROOT', help='root path of keystore') + parser_create_perm.add_argument('NAME', help='key name, aka ROS node name') + parser_create_perm.add_argument( 'POLICY_FILE_PATH', help='path of the permission yaml file') + args = parser.parse_args(sysargs) if '-h' in sysargs or '--help' in sysargs: