Skip to content

Commit

Permalink
Use security contexts
Browse files Browse the repository at this point in the history
Signed-off-by: Ivan Santiago Paunovic <[email protected]>
  • Loading branch information
ivanpauno committed Mar 31, 2020
1 parent 2552283 commit 671e73d
Show file tree
Hide file tree
Showing 25 changed files with 640 additions and 270 deletions.
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)
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):

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.
# 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(f'{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)

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):
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

0 comments on commit 671e73d

Please sign in to comment.