Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Contexts #177

Merged
merged 4 commits into from
Apr 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion sros2/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ def package_files(directory):
':CreatePermissionVerb',
'distribute_key = sros2.verb.distribute_key:DistributeKeyVerb',
'generate_artifacts = sros2.verb.generate_artifacts:GenerateArtifactsVerb',
'generate_policy = sros2.verb.generate_policy:GeneratePolicyVerb',
# TODO(ivanpauno): Reactivate this after having a way to introspect
# security context names in rclpy.
# Related with https://github.com/ros2/rclpy/issues/529.
# 'generate_policy = sros2.verb.generate_policy:GeneratePolicyVerb',
'list_keys = sros2.verb.list_keys:ListKeysVerb',
],
},
Expand Down
193 changes: 120 additions & 73 deletions sros2/sros2/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

from collections import namedtuple
import datetime
import errno
import os
import shutil
import sys

from cryptography import x509
Expand All @@ -28,9 +28,8 @@
from lxml import etree

from rclpy.exceptions import InvalidNamespaceException
from rclpy.exceptions import InvalidNodeNameException
from rclpy.utilities import get_rmw_implementation_identifier
from rclpy.validate_namespace import validate_namespace
from rclpy.validate_node_name import validate_node_name

from sros2.policy import (
get_policy_default,
Expand All @@ -48,6 +47,12 @@
NodeName = namedtuple('NodeName', ('node', 'ns', 'fqn'))
TopicInfo = namedtuple('Topic', ('fqn', 'type'))

KS_CONTEXT = 'contexts'
KS_PUBLIC = 'public'
KS_PRIVATE = 'private'

RMW_WITH_ROS_GRAPH_INFO_TOPIC = ('rmw_fastrtps_cpp', 'rmw_fastrtps_dynamic_cpp')


def get_node_names(*, node, include_hidden_nodes=False):
node_names_and_namespaces = node.get_node_names_and_namespaces()
Expand Down Expand Up @@ -141,24 +146,57 @@ def create_governance_file(path, domain_id):
f.write(etree.tostring(governance_xml, pretty_print=True))


def _create_symlink(*, src, dst):
if os.path.exists(dst):
src_abs_path = os.path.join(os.path.dirname(dst), src)
if os.path.samefile(src_abs_path, dst):
return
print(f"Existing symlink '{dst}' does not match '{src_abs_path}', overriding it!")
os.remove(dst)
os.symlink(src=src, dst=dst)


def create_keystore(keystore_path):
if not os.path.exists(keystore_path):
print('creating directory: %s' % keystore_path)
os.makedirs(keystore_path, exist_ok=True)
if not is_valid_keystore(keystore_path):
print('creating keystore: %s' % keystore_path)
else:
print('directory already exists: %s' % keystore_path)

ca_key_path = os.path.join(keystore_path, 'ca.key.pem')
ca_cert_path = os.path.join(keystore_path, 'ca.cert.pem')
print('keystore already exists: %s' % keystore_path)
ivanpauno marked this conversation as resolved.
Show resolved Hide resolved
ivanpauno marked this conversation as resolved.
Show resolved Hide resolved
return

os.makedirs(keystore_path, exist_ok=True)
os.makedirs(os.path.join(keystore_path, KS_PUBLIC), exist_ok=True)
os.makedirs(os.path.join(keystore_path, KS_PRIVATE), exist_ok=True)
os.makedirs(os.path.join(keystore_path, KS_CONTEXT), exist_ok=True)

keystore_ca_cert_path = os.path.join(keystore_path, KS_PUBLIC, 'ca.cert.pem')
keystore_ca_key_path = os.path.join(keystore_path, KS_PRIVATE, 'ca.key.pem')

keystore_permissions_ca_cert_path = os.path.join(
keystore_path, KS_PUBLIC, 'permissions_ca.cert.pem')
keystore_permissions_ca_key_path = os.path.join(
keystore_path, KS_PRIVATE, 'permissions_ca.key.pem')
keystore_identity_ca_cert_path = os.path.join(keystore_path, KS_PUBLIC, 'identity_ca.cert.pem')
keystore_identity_ca_key_path = os.path.join(keystore_path, KS_PRIVATE, 'identity_ca.key.pem')

required_files = (
keystore_permissions_ca_cert_path,
keystore_permissions_ca_key_path,
keystore_identity_ca_cert_path,
keystore_identity_ca_key_path,
)

if not (os.path.isfile(ca_key_path) and os.path.isfile(ca_cert_path)):
if not all(os.path.isfile(x) for x in required_files):
print('creating new CA key/cert pair')
create_ca_key_cert(ca_key_path, ca_cert_path)
create_ca_key_cert(keystore_ca_key_path, keystore_ca_cert_path)
_create_symlink(src='ca.cert.pem', dst=keystore_permissions_ca_cert_path)
_create_symlink(src='ca.key.pem', dst=keystore_permissions_ca_key_path)
_create_symlink(src='ca.cert.pem', dst=keystore_identity_ca_cert_path)
_create_symlink(src='ca.key.pem', dst=keystore_identity_ca_key_path)
else:
print('found CA key and cert, not creating new ones!')

# create governance file
gov_path = os.path.join(keystore_path, 'governance.xml')
gov_path = os.path.join(keystore_path, KS_CONTEXT, 'governance.xml')
if not os.path.isfile(gov_path):
print('creating governance file: %s' % gov_path)
domain_id = os.getenv(DOMAIN_ID_ENV, '0')
Expand All @@ -167,10 +205,14 @@ def create_keystore(keystore_path):
print('found governance file, not creating a new one!')

# sign governance file
signed_gov_path = os.path.join(keystore_path, 'governance.p7s')
signed_gov_path = os.path.join(keystore_path, KS_CONTEXT, 'governance.p7s')
if not os.path.isfile(signed_gov_path):
print('creating signed governance file: %s' % signed_gov_path)
_create_smime_signed_file(ca_cert_path, ca_key_path, gov_path, signed_gov_path)
_create_smime_signed_file(
keystore_permissions_ca_cert_path,
keystore_permissions_ca_key_path,
gov_path,
signed_gov_path)
else:
print('found signed governance file, not creating a new one!')

Expand All @@ -181,34 +223,36 @@ def create_keystore(keystore_path):

ruffsl marked this conversation as resolved.
Show resolved Hide resolved
def is_valid_keystore(path):
return (
os.path.isfile(os.path.join(path, 'ca.key.pem')) and
os.path.isfile(os.path.join(path, 'ca.cert.pem')) and
os.path.isfile(os.path.join(path, 'governance.p7s'))
os.path.isfile(os.path.join(path, KS_PUBLIC, 'permissions_ca.cert.pem')) and
os.path.isfile(os.path.join(path, KS_PUBLIC, 'identity_ca.cert.pem')) and
os.path.isfile(os.path.join(path, KS_PRIVATE, 'permissions_ca.key.pem')) and
os.path.isfile(os.path.join(path, KS_PRIVATE, 'identity_ca.key.pem')) and
os.path.isfile(os.path.join(path, KS_CONTEXT, 'governance.p7s'))
)


def is_key_name_valid(name):
ns_and_name = name.rsplit('/', 1)
if len(ns_and_name) != 2:
print("The key name needs to start with '/'")
return False
node_ns = ns_and_name[0] if ns_and_name[0] else '/'
node_name = ns_and_name[1]

# TODO(ivanpauno): Use validate_security_context_name when it's propagated to `rclpy`.
# This is not to bad for the moment.
ivanpauno marked this conversation as resolved.
Show resolved Hide resolved
# Related with https://github.com/ros2/rclpy/issues/528.
try:
return (validate_namespace(node_ns) and validate_node_name(node_name))
except (InvalidNamespaceException, InvalidNodeNameException) as e:
print('{}'.format(e))
return validate_namespace(name)
except InvalidNamespaceException as e:
print(e)
return False


def create_permission_file(path, domain_id, policy_element):
print('creating permission')
permissions_xsl_path = get_transport_template('dds', 'permissions.xsl')
permissions_xsl = etree.XSLT(etree.parse(permissions_xsl_path))
permissions_xsd_path = get_transport_schema('dds', 'permissions.xsd')
permissions_xsd = etree.XMLSchema(etree.parse(permissions_xsd_path))

permissions_xml = permissions_xsl(policy_element)
kwargs = {}
if get_rmw_implementation_identifier() in RMW_WITH_ROS_GRAPH_INFO_TOPIC:
kwargs['allow_ros_discovery_topic'] = etree.XSLT.strparam('1')
permissions_xml = permissions_xsl(policy_element, **kwargs)
ivanpauno marked this conversation as resolved.
Show resolved Hide resolved

domain_id_elements = permissions_xml.findall('permissions/grant/*/domains/id')
for domain_id_element in domain_id_elements:
Expand All @@ -229,20 +273,14 @@ def get_policy(name, policy_file_path):


def get_policy_from_tree(name, policy_tree):
ns, node = name.rsplit('/', 1)
ns = '/' if not ns else ns
profile_element = policy_tree.find(
path='profiles/profile[@ns="{ns}"][@node="{node}"]'.format(
ns=ns,
node=node))
if profile_element is None:
raise RuntimeError('unable to find profile "{name}"'.format(
name=name
))
profiles_element = etree.Element('profiles')
profiles_element.append(profile_element)
context_element = policy_tree.find(
path=f'contexts/context[@path="{name}"]')
if context_element is None:
raise RuntimeError(f'unable to find context "{name}"')
contexts_element = etree.Element('contexts')
contexts_element.append(context_element)
policy_element = etree.Element('policy')
policy_element.append(profiles_element)
policy_element.append(contexts_element)
return policy_element


Expand All @@ -255,14 +293,14 @@ def create_permission(keystore_path, identity, policy_file_path):
def create_permissions_from_policy_element(keystore_path, identity, policy_element):
domain_id = os.getenv(DOMAIN_ID_ENV, '0')
relative_path = os.path.normpath(identity.lstrip('/'))
key_dir = os.path.join(keystore_path, relative_path)
key_dir = os.path.join(keystore_path, KS_CONTEXT, relative_path)
print("creating permission file for identity: '%s'" % identity)
permissions_path = os.path.join(key_dir, 'permissions.xml')
create_permission_file(permissions_path, domain_id, policy_element)

signed_permissions_path = os.path.join(key_dir, 'permissions.p7s')
keystore_ca_cert_path = os.path.join(keystore_path, 'ca.cert.pem')
keystore_ca_key_path = os.path.join(keystore_path, 'ca.key.pem')
keystore_ca_cert_path = os.path.join(keystore_path, KS_PUBLIC, 'ca.cert.pem')
keystore_ca_key_path = os.path.join(keystore_path, KS_PRIVATE, 'ca.key.pem')
_create_smime_signed_file(
keystore_ca_cert_path, keystore_ca_key_path, permissions_path, signed_permissions_path)

Expand All @@ -276,63 +314,72 @@ def create_key(keystore_path, identity):
print("creating key for identity: '%s'" % identity)

relative_path = os.path.normpath(identity.lstrip('/'))
key_dir = os.path.join(keystore_path, relative_path)
key_dir = os.path.join(keystore_path, KS_CONTEXT, relative_path)
os.makedirs(key_dir, exist_ok=True)

keystore_ca_key_path = os.path.join(keystore_path, 'ca.key.pem')
keystore_ca_cert_path = os.path.join(keystore_path, 'ca.cert.pem')

# symlink the CA cert in there
public_certs = ['identity_ca.cert.pem', 'permissions_ca.cert.pem']
for public_cert in public_certs:
dst = os.path.join(key_dir, public_cert)
keystore_ca_cert_path = os.path.join(keystore_path, KS_PUBLIC, public_cert)
relativepath = os.path.relpath(keystore_ca_cert_path, key_dir)
try:
os.symlink(src=relativepath, dst=dst)
except FileExistsError as e:
if not os.path.samefile(keystore_ca_cert_path, dst):
print('Existing symlink does not match!')
raise RuntimeError(str(e))

# copy the governance file in there
keystore_governance_path = os.path.join(keystore_path, 'governance.p7s')
_create_symlink(src=relativepath, dst=dst)

# symlink the governance file in there
keystore_governance_path = os.path.join(keystore_path, KS_CONTEXT, 'governance.p7s')
dest_governance_path = os.path.join(key_dir, 'governance.p7s')
shutil.copyfile(keystore_governance_path, dest_governance_path)
relativepath = os.path.relpath(keystore_governance_path, key_dir)
_create_symlink(src=relativepath, dst=dest_governance_path)

keystore_identity_ca_cert_path = os.path.join(keystore_path, KS_PUBLIC, 'identity_ca.cert.pem')
keystore_identity_ca_key_path = os.path.join(keystore_path, KS_PRIVATE, 'identity_ca.key.pem')

cert_path = os.path.join(key_dir, 'cert.pem')
key_path = os.path.join(key_dir, 'key.pem')
if not os.path.isfile(cert_path) or not os.path.isfile(key_path):
print('creating cert and key')
_create_key_and_cert(
keystore_ca_cert_path, keystore_ca_key_path, identity, cert_path, key_path)
keystore_identity_ca_cert_path,
keystore_identity_ca_key_path,
identity,
cert_path,
key_path
)
else:
print('found cert and key; not creating new ones!')

# create a wildcard permissions file for this node which can be overridden
# later using a policy if desired
policy_file_path = get_policy_default('policy.xml')
policy_element = get_policy('/default', policy_file_path)
profile_element = policy_element.find('profiles/profile')
ns, node = identity.rsplit('/', 1)
ns = '/' if not ns else ns
profile_element.attrib['ns'] = ns
profile_element.attrib['node'] = node
policy_element = get_policy('/', policy_file_path)
context_element = policy_element.find('contexts/context')
context_element.attrib['path'] = identity

permissions_path = os.path.join(key_dir, 'permissions.xml')
domain_id = os.getenv(DOMAIN_ID_ENV, '0')
create_permission_file(permissions_path, domain_id, policy_element)

signed_permissions_path = os.path.join(key_dir, 'permissions.p7s')
keystore_ca_key_path = os.path.join(keystore_path, 'ca.key.pem')
keystore_permissions_ca_key_path = os.path.join(
keystore_path, KS_PRIVATE, 'permissions_ca.key.pem')
_create_smime_signed_file(
keystore_ca_cert_path, keystore_ca_key_path, permissions_path, signed_permissions_path)
keystore_ca_cert_path,
keystore_permissions_ca_key_path,
permissions_path,
signed_permissions_path
)

return True


def list_keys(keystore_path):
for name in os.listdir(keystore_path):
if os.path.isdir(os.path.join(keystore_path, name)):
contexts_path = os.path.join(keystore_path, KS_CONTEXT)
if not os.path.isdir(keystore_path):
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), keystore_path)
if not os.path.isdir(contexts_path):
return True
for name in os.listdir(contexts_path):
Copy link
Member Author

@ruffsl ruffsl Mar 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context paths can be nested. Is listdir recursive?
I.e. will it walk over contexts that aren't just flat in the root of the context sub folder?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 to that.

To be fair it was already the case before this change so doesn't have to be addressed here. Should be ticketed though if not addressed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be fair it was already the case before this change so doesn't have to be addressed here. Should be ticketed though if not addressed

Agreed.
What should be the output of:

-> contexts
  -> context1
    -> security artifacts ...
    -> nested1
      -> security artifacts ...
  ->context2
    ->nested2
      ->security artifacts...

?

IMO, it should be:

/context1
/context1/nested1
/context2/nested2

If the contexts folder have valid security artifacts, / should be showed too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me

if os.path.isdir(os.path.join(contexts_path, name)):
print(name)
return True

Expand Down Expand Up @@ -364,9 +411,9 @@ def generate_artifacts(keystore_path=None, identity_names=[], policy_files=[]):
return False
for policy_file in policy_files:
policy_tree = load_policy(policy_file)
profiles_element = policy_tree.find('profiles')
for profile in profiles_element:
identity_name = profile.get('ns').rstrip('/') + '/' + profile.get('node')
contexts_element = policy_tree.find('contexts')
for context in contexts_element:
identity_name = context.get('path')
if identity_name not in identity_names:
if not create_key(keystore_path, identity_name):
return False
Expand Down
2 changes: 1 addition & 1 deletion sros2/sros2/policy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import pkg_resources

POLICY_VERSION = '0.1.0'
POLICY_VERSION = '0.2.0'


def get_policy_default(name):
Expand Down
33 changes: 19 additions & 14 deletions sros2/sros2/policy/defaults/policy.xml
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<policy version="0.1.0">
<profiles>
<profile ns="/" node="default">
<topics publish="ALLOW" subscribe="ALLOW">
<topic>/*</topic>
</topics>
<services reply="ALLOW" request="ALLOW">
<service>/*</service>
</services>
<actions call="ALLOW" execute="ALLOW">
<action>/*</action>
</actions>
</profile>
</profiles>
<policy version="0.2.0"
xmlns:xi="http://www.w3.org/2001/XInclude">
<contexts>
<context path="/">
<profiles>
<profile ns="/" node="default">
<topics publish="ALLOW" subscribe="ALLOW">
<topic>/*</topic>
</topics>
<services reply="ALLOW" request="ALLOW">
<service>/*</service>
</services>
<actions call="ALLOW" execute="ALLOW">
<action>/*</action>
</actions>
</profile>
</profiles>
</context>
</contexts>
</policy>
Loading