From 4eb9539b76f501bcdb6b4622d7e6696a27f24587 Mon Sep 17 00:00:00 2001 From: Matthias Kruk Date: Mon, 14 Jun 2021 08:57:28 +0900 Subject: [PATCH] include/ipc: Add module for messaging-based IPC This commit adds the ipc module, which implements a messaging-based approach to IPC using JSON objects that are transmitted via queues. The current implementation authenticates messages using GPG, which allows simple GPG-based authorization schemes to be implemented on top of this mechanism. This commit includes a JSON Schema for toolbox IPC messages and a Python script for validation, both of which will be integrated into the test suite. --- include/ipc.sh | 460 +++++++++++++++++++++++++++++++++++++++ spec/ipc_msg.schema.json | 56 +++++ spec/validate.py | 29 +++ 3 files changed, 545 insertions(+) create mode 100644 include/ipc.sh create mode 100644 spec/ipc_msg.schema.json create mode 100755 spec/validate.py diff --git a/include/ipc.sh b/include/ipc.sh new file mode 100644 index 0000000..29bd5ce --- /dev/null +++ b/include/ipc.sh @@ -0,0 +1,460 @@ +#!/bin/bash + +__init() { + if ! include "json" "queue"; then + return 1 + fi + + declare -gxr __ipc_root="/var/lib/toolbox/ipc" + declare -gxr __ipc_public="$__ipc_root/pub" + declare -gxr __ipc_private="$__ipc_root/priv/$USER" + declare -gxr __ipc_group="toolbox_ipc" + + declare -gxi __ipc_authentication=1 + declare -gxir __ipc_version=1 + + if ! mkdir -p "$__ipc_private" || + ! chgrp "$__ipc_group" "$__ipc_private"; then + log_error "Could not initialize private IPC directory $__ipc_private" + return 1 + fi + + return 0 +} + +_ipc_msg_encode() { + local decoded="$1" + + if (( $# > 0 )); then + base64 -w 0 <<< "$decoded" + else + base64 -w 0 < /dev/stdin + fi +} + +_ipc_msg_decode() { + local encoded="$1" + + if (( $# > 0 )); then + base64 -d <<< "$encoded" + else + base64 -d < /dev/stdin + fi +} + +ipc_authentication_enable() { + log_info "MESSAGE AUTHENTICATION ENABLED" + __ipc_authentication=1 + return 0 +} + +ipc_authentication_disable() { + log_error "MESSAGE AUTHENTICATION DISABLED" + __ipc_authentication=0 + return 0 +} + +_ipc_msg_get() { + local msg="$1" + local field="$2" + + local value + + if ! value=$(_ipc_msg_decode "$msg" | jq -e -r ".$field" 2>/dev/null); then + return 1 + fi + + echo "$value" + return 0 +} + +_ipc_msg_get_signature() { + local msg="$1" + + local data + local signature + local output + + data=$(_ipc_msg_get "$msg" "data") + signature=$(_ipc_msg_get "$msg" "signature") + + if ! gpg --verify <(base64 -d <<< "$signature") <(echo "$data") 2>&1; then + return 1 + fi + + return 0 +} + +_ipc_msg_verify() { + local msg="$1" + + local error + + if ! error=$(_ipc_msg_get_signature "$msg"); then + log_error "Invalid signature on message" + log_highlight "GPG output" <<< "$error" | log_error + return 1 + fi + + return 0 +} + +_ipc_msg_version_supported() { + local msg="$1" + + local -i version + + if ! version=$(_ipc_msg_get "$msg" "version"); then + log_error "Could not get version from message" + return 1 + fi + + if (( version != __ipc_version )); then + log_error "Unsupported message version" + return 1 + fi + + return 0 +} + +ipc_msg_validate() { + local msg="$1" + + if (( __ipc_authentication == 1 )) && + ! _ipc_msg_verify "$msg"; then + return 1 + fi + + if ! _ipc_msg_version_supported "$msg"; then + return 2 + fi + + return 0 +} + +ipc_msg_get_signature_info() { + local msg="$1" + + local signature + + local sig_nameregex + local sig_keyregex + + local sig_valid + local sig_name + local sig_email + local sig_key + + sig_nameregex='"(.*) <([^>]*)>"' + sig_keyregex='([0-9a-fA-F]{32,})' + + sig_valid="bad" + sig_name="(unknown)" + sig_email="(unknown)" + sig_key="(unknown)" + + if signature=$(_ipc_msg_get_signature "$msg"); then + sig_valid="good" + fi + + if [[ "$signature" =~ $sig_nameregex ]]; then + sig_name="${BASH_REMATCH[1]}" + sig_email="${BASH_REMATCH[2]}" + fi + + if [[ "$signature" =~ $sig_keyregex ]]; then + sig_key="${BASH_REMATCH[1]}" + fi + + echo "$sig_valid $sig_key $sig_email $sig_name" + return 0 +} + +ipc_msg_get_signing_key() { + local msg="$1" + + local signature + local keyregex + + keyregex='([0-9a-fA-F]{32,})' + + if ! signature=$(_ipc_msg_get_signature "$msg"); then + return 1 + fi + + if [[ "$signature" =~ $keyregex ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + + return 1 +} + +ipc_msg_dump() { + local msg="$1" + + local version + local data + local signature + + local version_ok + local signature_ok + local validation_status + + version=$(_ipc_msg_get "$msg" "version") + data=$(_ipc_msg_get "$msg" "data") + signature=$(_ipc_msg_get "$msg" "signature") + + version_ok="no" + signature_ok="no" + validation_status="disabled" + + if _ipc_msg_version_supported "$msg"; then + version_ok="yes" + fi + + if _ipc_msg_verify "$msg"; then + signature_ok="yes" + fi + + if (( __ipc_authentication == 1 )); then + validation_status="enabled" + fi + + cat < "$endpoint/owner"; then + if ! rm -rf "$endpoint"; then + log_error "Could not clean up $endpoint" + fi + + return 1 + fi + fi + + echo "$name" + return 0 +} + +ipc_endpoint_close() { + local name="$1" + + local endpoint + + endpoint="$__ipc_root/$name" + + if ! queue_destroy "$endpoint/queue"; then + return 1 + fi + + if ! rm -rf "$endpoint"; then + return 1 + fi + + return 0 +} + +_ipc_endpoint_put() { + local endpoint="$1" + local msg="$2" + + local queue + + queue="$__ipc_root/$endpoint/queue" + + if ! queue_put "$queue" "$msg"; then + return 1 + fi + + return 0 +} + +_ipc_endpoint_get() { + local endpoint="$1" + local -i timeout="$2" + + local queue + local msg + + queue="$__ipc_root/$endpoint/queue" + + if ! msg=$(queue_get "$queue" "$timeout"); then + return 1 + fi + + echo "$msg" + return 0 +} + +ipc_endpoint_send() { + local endpoint="$1" + local msg="$2" + + if ! _ipc_endpoint_put "$endpoint" "$msg"; then + return 1 + fi + + return 0 +} + +ipc_endpoint_recv() { + local endpoint="$1" + local -i timeout="$2" + + local msg + + if ! msg=$(_ipc_endpoint_get "$endpoint" "$timeout"); then + return 1 + fi + + echo "$msg" + return 0 +} diff --git a/spec/ipc_msg.schema.json b/spec/ipc_msg.schema.json new file mode 100644 index 0000000..0b21932 --- /dev/null +++ b/spec/ipc_msg.schema.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://m10k.eu/toolbox/ipc.msg.json", + "title": "Toolbox IPC Base message", + "description": "The base type for toolbox IPC messages", + "type": "object", + + "properties": { + "version": { + "description": "The message format version", + "type": "integer" + }, + + "source": { + "description": "The endpoint that sent the message", + "type": "string" + }, + + "destination": { + "description": "The endpoint that the message is intended for", + "type": "string" + }, + + "timestamp": { + "description": "The UNIX timestamp when the message was sent", + "type": "integer" + }, + + "user": { + "description": "The login name of the sender", + "type": "string" + }, + + "data": { + "description": "The base64 encoded content of the message", + "type": "string", + "pattern": "^[0-9a-zA-Z+/]+[=]*$" + }, + + "signature": { + "description": "The base64 encoded signature of the encoded data", + "type": "string", + "pattern": "^[0-9a-zA-Z+/]+[=]*$" + } + }, + + "required": [ + "version", + "source", + "destination", + "timestamp", + "user", + "data", + "signature" + ] +} diff --git a/spec/validate.py b/spec/validate.py new file mode 100755 index 0000000..0541f85 --- /dev/null +++ b/spec/validate.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +import jsonschema +import json +import sys + +def validate_json_with_schema(json_path, schema_path): + json_file = open(json_path, "r") + schema_file = open(schema_path, "r") + + instance = json.load(json_file) + schema = json.load(schema_file) + + jsonschema.validate(instance=instance, schema=schema) + return True + +def main(argv): + if len(argv) < 3: + print("Usage: %s schema object" % (sys.argv[0], )) + return 1 + + sch = sys.argv[1] + obj = sys.argv[2] + + validate_json_with_schema(obj, sch) + return 0 + +if __name__ == "__main__": + sys.exit(main(sys.argv)) -- 2.47.3