Skip to content

Commit a98ba31

Browse files
authored
Merge pull request #1 from osrf/security_extension
Add security extensions
2 parents 7c92021 + 5b67af9 commit a98ba31

File tree

16 files changed

+645
-0
lines changed

16 files changed

+645
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__/

package.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0"?>
2+
<?xml-model
3+
href="http://download.ros.org/schema/package_format2.xsd"
4+
schematypens="http://www.w3.org/2001/XMLSchema"?>
5+
<package format="3">
6+
<name>ros2launch_security</name>
7+
<version>0.1.0</version>
8+
<description>Security extensions for ros2 launch</description>
9+
<maintainer email="[email protected]">Ted Kern</maintainer>
10+
<license>Apache License 2.0</license>
11+
<author email="[email protected]">Geoffrey Biggs</author>
12+
<author email="[email protected]">Ted Kern</author>
13+
14+
<depend>ament_index_python</depend>
15+
<depend>ros2launch</depend>
16+
<depend>nodl_python</depend>
17+
<depend>sros2</depend>
18+
19+
<test_depend>ament_copyright</test_depend>
20+
<test_depend>ament_flake8</test_depend>
21+
<test_depend>ament_pep257</test_depend>
22+
<test_depend>demo_nodes_py</test_depend>
23+
<test_depend>launch_ros</test_depend>
24+
<test_depend>python3-pytest</test_depend>
25+
<test_depend>ros2launch</test_depend>
26+
<test_depend>sros2</test_depend>
27+
28+
<export>
29+
<build_type>ament_python</build_type>
30+
</export>
31+
</package>

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
junit_family=xunit2

resource/ros2launch_security

Whitespace-only changes.

ros2launch_security/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2020 Canonical, Ltd.
2+
# Copyright 2021 Open Source Robotics Foundation, Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from . import option
17+
18+
__all__ = [
19+
'option'
20+
]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2020 Canonical, Ltd.
2+
# Copyright 2021 Open Source Robotics Foundation, Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from . import security
17+
18+
__all__ = [
19+
'security'
20+
]
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright 2020 Canonical, Ltd.
2+
# Copyright 2021 Open Source Robotics Foundation, Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import pathlib
17+
from typing import Dict
18+
from typing import List
19+
from typing import Union
20+
21+
from launch.launch_context import LaunchContext
22+
from launch.substitutions import LocalSubstitution
23+
from launch.utilities import normalize_to_list_of_substitutions
24+
from launch_ros.actions.node import Node, NodeActionExtension
25+
26+
import nodl
27+
import sros2.keystore._enclave
28+
29+
30+
class SecurityNodeActionExtension(NodeActionExtension):
31+
32+
def prepare_for_execute(self, context, ros_specific_arguments, node_action):
33+
if context.launch_configurations.get('__secure', None) is not None:
34+
cmd_extension = ['--enclave', LocalSubstitution("ros_specific_arguments['enclave']")]
35+
cmd_extension = [normalize_to_list_of_substitutions(x) for x in cmd_extension]
36+
ros_specific_arguments = self._setup_security(
37+
context,
38+
ros_specific_arguments,
39+
node_action
40+
)
41+
return cmd_extension, ros_specific_arguments
42+
else:
43+
return [], ros_specific_arguments
44+
45+
def _setup_security(
46+
self,
47+
context: LaunchContext,
48+
ros_specific_arguments: Dict[str, Union[str, List[str]]],
49+
node_action: Node
50+
) -> None:
51+
"""Enable encryption, creating a key for the node if necessary."""
52+
nodl_node = nodl.get_node_by_executable(
53+
package_name=node_action.node_package,
54+
executable_name=node_action.node_executable
55+
)
56+
57+
self.__enclave = node_action.node_name.replace(
58+
Node.UNSPECIFIED_NODE_NAME, nodl_node.name
59+
).replace(Node.UNSPECIFIED_NODE_NAMESPACE, '')
60+
61+
sros2.keystore._enclave.create_enclave(
62+
keystore_path=pathlib.Path(context.launch_configurations.get('__keystore')),
63+
identity=self.__enclave
64+
)
65+
66+
ros_specific_arguments['enclave'] = self.__enclave
67+
return ros_specific_arguments
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2020 Canonical, Ltd.
2+
# Copyright 2021 Open Source Robotics Foundation, Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from . import security
17+
18+
__all__ = [
19+
'security'
20+
]
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Copyright 2020 Canonical, Ltd.
2+
# Copyright 2021 Open Source Robotics Foundation, Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
17+
import pathlib
18+
from tempfile import TemporaryDirectory
19+
from typing import Optional
20+
from typing import Tuple
21+
22+
import launch
23+
from ros2launch.option import OptionExtension
24+
import sros2.keystore._keystore
25+
26+
27+
class NoKeystoreProvidedError(Exception):
28+
"""Exception raised when a keystore is not provided."""
29+
30+
def __init__(self):
31+
super().__init__(('--no-create-keystore was specified and '
32+
'no keystore was provided'))
33+
34+
35+
class NonexistentKeystoreError(Exception):
36+
"""Exception raised when keystore is not on disk."""
37+
38+
def __init__(self, keystore_path: pathlib.Path):
39+
super().__init__(('--no-create-keystore was specified and '
40+
f'the keystore "{keystore_path}" '
41+
'does not exist'))
42+
43+
44+
class InvalidKeystoreError(Exception):
45+
"""Exception raised when provided keystore isn't valid."""
46+
47+
def __init__(self, keystore_path: pathlib.Path):
48+
super().__init__(('--no-create-keystore was specified and '
49+
f'the keystore "{keystore_path}" is not initialized. \n\t'
50+
f'(Try running: ros2 security create_keystore {keystore_path})'))
51+
52+
53+
class SecurityOption(OptionExtension):
54+
55+
def add_arguments(self, parser, cli_name, *, argv=None):
56+
sec_args = parser.add_argument_group(
57+
title='security',
58+
description='Security-related arguments'
59+
)
60+
arg = sec_args.add_argument(
61+
'--secure',
62+
metavar='keystore',
63+
nargs='?',
64+
const='',
65+
help=('Launch with ROS 2 security features using the specified keystore directory. '
66+
'Will set up an ephemeral keystore if one is not specified.'),
67+
)
68+
try:
69+
arg.completer = DirectoriesCompleter() # argcomplete is optional
70+
except NameError:
71+
pass
72+
73+
sec_args.add_argument(
74+
'--no-create-keystore',
75+
dest='create_keystore',
76+
action='store_false',
77+
help='Disable automatic keystore creation and/or initialization'
78+
)
79+
80+
def prelaunch(
81+
self,
82+
launch_description: launch.LaunchDescription,
83+
args
84+
) -> Tuple[launch.LaunchDescription, '_Keystore']:
85+
if args.secure is None:
86+
return (launch_description,)
87+
keystore_path = pathlib.Path(args.secure) if args.secure != '' else None
88+
keystore = _Keystore(keystore_path=keystore_path, create_keystore=args.create_keystore)
89+
90+
launch_description = launch.LaunchDescription(
91+
[
92+
launch.actions.DeclareLaunchArgument(
93+
name='__keystore', default_value=str(keystore.path)
94+
),
95+
launch.actions.DeclareLaunchArgument(
96+
name='__secure', default_value='true'
97+
),
98+
launch.actions.SetEnvironmentVariable(
99+
name='ROS_SECURITY_KEYSTORE', value=str(keystore.path)
100+
),
101+
launch.actions.SetEnvironmentVariable(
102+
name='ROS_SECURITY_STRATEGY',
103+
value='Enforce'),
104+
launch.actions.SetEnvironmentVariable(name='ROS_SECURITY_ENABLE', value='true'),
105+
]
106+
+ launch_description.entities
107+
)
108+
109+
return launch_description, keystore
110+
111+
112+
class _Keystore:
113+
"""
114+
Object that contains a keystore.
115+
116+
If a transient keystore is created, contains the temporary directory, assuring
117+
it is destroyed alongside the _Keystore object.
118+
"""
119+
120+
def __init__(self, *, keystore_path: Optional[pathlib.Path], create_keystore: bool):
121+
if not create_keystore:
122+
if keystore_path is None:
123+
raise NoKeystoreProvidedError()
124+
if not keystore_path.exists():
125+
raise NonexistentKeystoreError(keystore_path)
126+
if not sros2.keystore._keystore.is_valid_keystore(keystore_path):
127+
raise InvalidKeystoreError(keystore_path)
128+
129+
# If keystore path is blank, create a transient keystore
130+
if keystore_path is None:
131+
self._temp_keystore = TemporaryDirectory()
132+
self._keystore_path = pathlib.Path(self._temp_keystore.name)
133+
else:
134+
self._keystore_path = keystore_path
135+
self._keystore_path = self._keystore_path.resolve()
136+
# If keystore is not initialized, create a keystore
137+
if not self._keystore_path.is_dir():
138+
self._keystore_path.mkdir()
139+
if not sros2.keystore._keystore.is_valid_keystore(self._keystore_path):
140+
sros2.keystore._keystore.create_keystore(self._keystore_path)
141+
142+
@property
143+
def path(self) -> pathlib.Path:
144+
return self._keystore_path
145+
146+
def __str__(self) -> str:
147+
return str(self._keystore_path)

setup.cfg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[develop]
2+
script-dir=$base/lib/test_ros2launch
3+
[install]
4+
install-scripts=$base/lib/test_ros2launch

0 commit comments

Comments
 (0)