feat: add ssh server (sshd)
This commit is contained in:
+12
-4
@@ -49,7 +49,7 @@ BINDIR := bin
|
||||
PROGRAMS := $(notdir $(wildcard src/*))
|
||||
|
||||
# Programs with custom Makefiles (built separately).
|
||||
CUSTOM_BUILDS := 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display terminal klog procmgr powermgr calculator charmap desktop login shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs libloader crashpad
|
||||
CUSTOM_BUILDS := 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display sshserver terminal klog procmgr powermgr calculator charmap desktop login shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs libloader crashpad sshd
|
||||
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||
|
||||
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
||||
@@ -89,9 +89,9 @@ WPDIR := data/wallpapers
|
||||
WPSRC := $(wildcard $(WPDIR)/*.jpg)
|
||||
WPDST := $(patsubst $(WPDIR)/%,$(BINDIR)/os/wallpapers/%,$(WPSRC))
|
||||
|
||||
.PHONY: all clean 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display terminal klog procmgr powermgr calculator charmap login desktop shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs bearssl libc tls libjpeg libjpegwrite install-apps libloader crashpad check-syscalls gen-syscalls
|
||||
.PHONY: all clean 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display sshserver sshd terminal klog procmgr powermgr calculator charmap login desktop shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs bearssl libc tls libjpeg libjpegwrite install-apps libloader crashpad check-syscalls gen-syscalls
|
||||
|
||||
all: bearssl libc libjpeg libjpegwrite tls libloader devkit $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display terminal klog procmgr powermgr calculator charmap 2048 paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell icons fonts install-apps crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(FWDST) $(LICDST) $(WPDST)
|
||||
all: bearssl libc libjpeg libjpegwrite tls libloader devkit $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display sshserver terminal klog procmgr powermgr calculator charmap 2048 paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell sshd icons fonts install-apps crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(FWDST) $(LICDST) $(WPDST)
|
||||
|
||||
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
||||
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
||||
@@ -212,6 +212,14 @@ network: libc
|
||||
display: libc
|
||||
$(MAKE) -C src/display
|
||||
|
||||
# Build SSH Server settings applet.
|
||||
sshserver: bearssl libc
|
||||
$(MAKE) -C src/sshserver
|
||||
|
||||
# Build the SSH daemon.
|
||||
sshd: bearssl libc
|
||||
$(MAKE) -C src/sshd
|
||||
|
||||
# Build terminal standalone GUI tool (depends on libc).
|
||||
terminal: libc
|
||||
$(MAKE) -C src/terminal
|
||||
@@ -329,7 +337,7 @@ crashpad: libc
|
||||
$(MAKE) -C src/crashpad
|
||||
|
||||
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
||||
install-apps: 2048 paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer audio music video bluetooth network display terminal klog procmgr powermgr calculator charmap screenshot texteditor mandelbrot printers timezone crashpad
|
||||
install-apps: 2048 paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer audio music video bluetooth network display sshserver terminal klog procmgr powermgr calculator charmap screenshot texteditor mandelbrot printers timezone crashpad
|
||||
../scripts/install_apps.sh
|
||||
|
||||
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
|
||||
|
||||
@@ -27,6 +27,14 @@ enabled = true
|
||||
wait = false
|
||||
optional = true
|
||||
|
||||
# Remote shell access is opt-in. Enable this from Settings > SSH Server.
|
||||
[services.ssh]
|
||||
path = "0:/os/sshd.elf"
|
||||
name = "SSH server"
|
||||
enabled = false
|
||||
wait = false
|
||||
optional = true
|
||||
|
||||
[services.login]
|
||||
path = "0:/os/login.elf"
|
||||
enabled = true
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# SSH server policy. The host key is generated locally on first start.
|
||||
[server]
|
||||
port = 22
|
||||
|
||||
# Per-user access defaults to false when a user is not listed. The SSH Server
|
||||
# Settings applet maintains this table.
|
||||
[allow]
|
||||
@@ -0,0 +1,27 @@
|
||||
/* Shared SSH server policy helpers. */
|
||||
#pragma once
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/user.h>
|
||||
#include <libc/stdio.h>
|
||||
|
||||
namespace montauk::ssh {
|
||||
inline void allow_key(char* out, int cap, const char* username) {
|
||||
snprintf(out, cap, "allow.%s", username);
|
||||
}
|
||||
|
||||
inline bool user_allowed(const montauk::toml::Doc& doc, const char* username) {
|
||||
char key[64];
|
||||
allow_key(key, sizeof(key), username);
|
||||
return doc.get_bool(key, false);
|
||||
}
|
||||
|
||||
inline bool is_admin(const char* username) {
|
||||
user::UserInfo users[user::MAX_USERS];
|
||||
int count = user::load_users(users, user::MAX_USERS);
|
||||
for (int i = 0; i < count; i++)
|
||||
if (montauk::streq(users[i].username, username))
|
||||
return montauk::streq(users[i].role, "admin");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
.TH SSHD 1
|
||||
.SH NAME
|
||||
sshd \- MontaukOS SSH-2 server
|
||||
.SH SYNOPSIS
|
||||
.B sshd
|
||||
.SH DESCRIPTION
|
||||
.B sshd
|
||||
accepts SSH-2 connections and gives each authenticated user a MontaukOS shell.
|
||||
|
||||
It is normally launched automatically by
|
||||
.BR init (1)
|
||||
and does not require direct user interaction. Remote access is disabled by
|
||||
default; enable it from Settings > SSH Server, which sets
|
||||
.I services.ssh.enabled
|
||||
in
|
||||
.IR 0:/config/init.toml .
|
||||
The change takes effect on the next boot.
|
||||
.SH AUTHENTICATION
|
||||
Passwords are the only supported method, checked against the MontaukOS account
|
||||
database. A user must additionally be listed in the
|
||||
.I [allow]
|
||||
table of
|
||||
.I 0:/config/ssh.toml
|
||||
before a login is accepted; users absent from that table are refused. The
|
||||
Settings applet maintains this table.
|
||||
|
||||
Failed passwords are delayed, and a connection is dropped after eight attempts.
|
||||
.SH CONFIGURATION
|
||||
.TP
|
||||
.I 0:/config/ssh.toml
|
||||
Listening port
|
||||
.RI ( server.port ,
|
||||
default 22) and the per-user
|
||||
.I [allow]
|
||||
table.
|
||||
.TP
|
||||
.I 0:/config/ssh_hostkey.toml
|
||||
The RSA host key, generated on first start. Note that the filesystem has no
|
||||
permission model, so this file is readable by any local process; keeping it
|
||||
separate from
|
||||
.I ssh.toml
|
||||
limits exposure but is not access control.
|
||||
.SH DIAGNOSTICS
|
||||
Messages go to the boot console only. They do not appear in
|
||||
.BR klog (1):
|
||||
.B SYS_PRINT
|
||||
writes the kernel terminal rather than the kernel log ring, and is discarded
|
||||
entirely once the desktop suppresses console output. There is currently nowhere
|
||||
to read them from on a running desktop.
|
||||
.SH PROTOCOL
|
||||
Key exchange is diffie-hellman-group14-sha256 with an rsa-sha2-256 host key;
|
||||
the transport uses aes128-ctr with hmac-sha2-256. Client-initiated rekeying is
|
||||
supported. Only one session channel per connection is served, and connections
|
||||
are handled one at a time.
|
||||
.SH LIMITATIONS
|
||||
Public key authentication, port forwarding, X11 forwarding, SFTP and SCP are not
|
||||
implemented.
|
||||
|
||||
.I exec
|
||||
requests run the command by feeding it to an interactive shell, so the shell
|
||||
prompt and the echoed command line appear in the output. Scripts that parse
|
||||
remote command output should account for this.
|
||||
.SH SEE ALSO
|
||||
.BR shell (1),
|
||||
.BR init (1)
|
||||
@@ -0,0 +1,26 @@
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
PROG_INC := ../../include
|
||||
LIBDIR := ../../lib
|
||||
LINK_LD := ../../link.ld
|
||||
OBJDIR := obj
|
||||
TARGET := ../../bin/os/sshd.elf
|
||||
CXXFLAGS := -std=gnu++20 -g -O2 -pipe -Wall -Wextra -ffreestanding \
|
||||
-fno-stack-protector -fno-stack-check -fno-rtti -fno-exceptions \
|
||||
-ffunction-sections -fdata-sections -mno-80387 -mno-mmx -mno-sse -mno-sse2 \
|
||||
-MMD -MP -I $(PROG_INC) -I $(LIBDIR)/bearssl/inc -isystem $(PROG_INC)/libc
|
||||
LDFLAGS := -nostdlib -Wl,--gc-sections -T $(LINK_LD)
|
||||
LIBS := $(LIBDIR)/bearssl/libbearssl.a $(LIBDIR)/libc/liblibc.a
|
||||
.PHONY: all clean
|
||||
all: $(TARGET)
|
||||
$(TARGET): $(OBJDIR)/main.o $(LIBS) $(LINK_LD) Makefile
|
||||
mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJDIR)/main.o $(LIBS) -o $@
|
||||
$(OBJDIR)/main.o: main.cpp crypto.hpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -c main.cpp -o $@
|
||||
-include $(OBJDIR)/main.d
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1,365 @@
|
||||
#pragma once
|
||||
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/user.h>
|
||||
|
||||
#include <bearssl_block.h>
|
||||
#include <bearssl_hash.h>
|
||||
#include <bearssl_hmac.h>
|
||||
#include <bearssl_rand.h>
|
||||
#include <bearssl_rsa.h>
|
||||
|
||||
namespace sshcrypto {
|
||||
|
||||
static inline uint32_t be32(const uint8_t* data) {
|
||||
return ((uint32_t)data[0] << 24) |
|
||||
((uint32_t)data[1] << 16) |
|
||||
((uint32_t)data[2] << 8) |
|
||||
data[3];
|
||||
}
|
||||
|
||||
static inline void put32(uint8_t* data, uint32_t value) {
|
||||
data[0] = value >> 24;
|
||||
data[1] = value >> 16;
|
||||
data[2] = value >> 8;
|
||||
data[3] = value;
|
||||
}
|
||||
|
||||
/*
|
||||
* Wipe key material. Written through a volatile pointer so the optimiser
|
||||
* cannot drop the store as dead when the buffer dies with the frame.
|
||||
*/
|
||||
static inline void zeroize(void* data, int size) {
|
||||
volatile uint8_t* p = (volatile uint8_t*)data;
|
||||
for (int i = 0; i < size; i++) p[i] = 0;
|
||||
}
|
||||
|
||||
static inline void sha256(const void* data, int size, uint8_t output[32]) {
|
||||
br_sha256_context context;
|
||||
br_sha256_init(&context);
|
||||
br_sha256_update(&context, data, size);
|
||||
br_sha256_out(&context, output);
|
||||
}
|
||||
|
||||
struct Rng {
|
||||
br_hmac_drbg_context context;
|
||||
|
||||
bool init() {
|
||||
uint8_t seed[64];
|
||||
int64_t received = montauk::getrandom(seed, sizeof(seed));
|
||||
if (received != (int64_t)sizeof(seed)) {
|
||||
montauk::memset(seed, 0, sizeof(seed));
|
||||
return false;
|
||||
}
|
||||
|
||||
br_hmac_drbg_init(&context, &br_sha256_vtable, seed, sizeof(seed));
|
||||
montauk::memset(seed, 0, sizeof(seed));
|
||||
return true;
|
||||
}
|
||||
|
||||
void bytes(void* output, int size) {
|
||||
br_hmac_drbg_generate(&context, output, size);
|
||||
}
|
||||
};
|
||||
|
||||
static inline void hex(const uint8_t* input, int size, char* output) {
|
||||
static const char* digits = "0123456789abcdef";
|
||||
for (int i = 0; i < size; i++) {
|
||||
output[i * 2] = digits[input[i] >> 4];
|
||||
output[i * 2 + 1] = digits[input[i] & 15];
|
||||
}
|
||||
output[size * 2] = 0;
|
||||
}
|
||||
|
||||
static inline int unhex(const char* input, uint8_t* output, int capacity) {
|
||||
int size = 0;
|
||||
while (input && input[0] && input[1] && size < capacity) {
|
||||
int high = montauk::user::hex_char_val(input[0]);
|
||||
int low = montauk::user::hex_char_val(input[1]);
|
||||
if (high < 0 || low < 0) break;
|
||||
output[size++] = (high << 4) | low;
|
||||
input += 2;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/*
|
||||
* The host key lives in its own config file rather than in ssh.toml so that
|
||||
* the unprivileged Settings applet, which rewrites ssh.toml whenever the
|
||||
* per-user allow table changes, never reads or round-trips private key
|
||||
* material. Note this is separation, not protection: the filesystem has no
|
||||
* permission model yet, so any local process can still read the file. Real
|
||||
* confidentiality needs FS-level access control.
|
||||
*/
|
||||
static const char HOSTKEY_CONFIG[] = "ssh_hostkey";
|
||||
static const char HOSTKEY_LEGACY_CONFIG[] = "ssh";
|
||||
|
||||
struct HostKey {
|
||||
uint8_t privateBuffer[BR_RSA_KBUF_PRIV_SIZE(2048)];
|
||||
uint8_t publicBuffer[BR_RSA_KBUF_PUB_SIZE(2048)];
|
||||
br_rsa_private_key sk = {};
|
||||
br_rsa_public_key pk = {};
|
||||
|
||||
/*
|
||||
* Earlier builds kept the key in ssh.toml. Move any such key into its own
|
||||
* file on first start so an upgrade does not change the host key (which
|
||||
* would trip every client's known-hosts check).
|
||||
*/
|
||||
bool load() {
|
||||
if (load_from(HOSTKEY_CONFIG)) return true;
|
||||
if (!load_from(HOSTKEY_LEGACY_CONFIG)) return false;
|
||||
migrate_from_legacy();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool load_from(const char* configName) {
|
||||
auto config = montauk::config::load(configName);
|
||||
const char* modulus = config.get_string("host_key.n", "");
|
||||
const char* exponent = config.get_string("host_key.e", "");
|
||||
const char* primeP = config.get_string("host_key.p", "");
|
||||
const char* primeQ = config.get_string("host_key.q", "");
|
||||
const char* exponentP = config.get_string("host_key.dp", "");
|
||||
const char* exponentQ = config.get_string("host_key.dq", "");
|
||||
const char* coefficient = config.get_string("host_key.iq", "");
|
||||
|
||||
int offset = 0;
|
||||
pk.n = publicBuffer;
|
||||
pk.nlen = unhex(modulus, publicBuffer, sizeof(publicBuffer));
|
||||
pk.e = publicBuffer + pk.nlen;
|
||||
pk.elen = unhex(exponent, publicBuffer + pk.nlen,
|
||||
sizeof(publicBuffer) - pk.nlen);
|
||||
|
||||
sk.p = privateBuffer + offset;
|
||||
sk.plen = unhex(primeP, sk.p, sizeof(privateBuffer) - offset);
|
||||
offset += sk.plen;
|
||||
sk.q = privateBuffer + offset;
|
||||
sk.qlen = unhex(primeQ, sk.q, sizeof(privateBuffer) - offset);
|
||||
offset += sk.qlen;
|
||||
sk.dp = privateBuffer + offset;
|
||||
sk.dplen = unhex(exponentP, sk.dp, sizeof(privateBuffer) - offset);
|
||||
offset += sk.dplen;
|
||||
sk.dq = privateBuffer + offset;
|
||||
sk.dqlen = unhex(exponentQ, sk.dq, sizeof(privateBuffer) - offset);
|
||||
offset += sk.dqlen;
|
||||
sk.iq = privateBuffer + offset;
|
||||
sk.iqlen = unhex(coefficient, sk.iq, sizeof(privateBuffer) - offset);
|
||||
sk.n_bitlen = (uint32_t)config.get_int("host_key.bits", 2048);
|
||||
config.destroy();
|
||||
|
||||
return pk.nlen >= 256 && pk.elen > 0 &&
|
||||
sk.plen > 0 && sk.qlen > 0 &&
|
||||
sk.dplen > 0 && sk.dqlen > 0 && sk.iqlen > 0;
|
||||
}
|
||||
|
||||
/* Rewrite the in-memory key to its own file and strip it from ssh.toml. */
|
||||
void migrate_from_legacy() {
|
||||
if (save_to(HOSTKEY_CONFIG) < 0) return;
|
||||
|
||||
auto legacy = montauk::config::load(HOSTKEY_LEGACY_CONFIG);
|
||||
static const char* const parts[] = {
|
||||
"host_key.n", "host_key.e", "host_key.p", "host_key.q",
|
||||
"host_key.dp", "host_key.dq", "host_key.iq", "host_key.bits",
|
||||
};
|
||||
for (unsigned i = 0; i < sizeof(parts) / sizeof(parts[0]); i++) {
|
||||
montauk::config::unset(&legacy, parts[i]);
|
||||
}
|
||||
montauk::config::save(HOSTKEY_LEGACY_CONFIG, &legacy);
|
||||
legacy.destroy();
|
||||
}
|
||||
|
||||
int save_to(const char* configName) {
|
||||
auto config = montauk::config::load(configName);
|
||||
char encoded[520];
|
||||
|
||||
#define SAVE_PART(key, data, size) \
|
||||
do { \
|
||||
hex((const uint8_t*)(data), (int)(size), encoded); \
|
||||
montauk::config::set_string(&config, "host_key." key, encoded); \
|
||||
} while (0)
|
||||
|
||||
SAVE_PART("n", pk.n, pk.nlen);
|
||||
SAVE_PART("e", pk.e, pk.elen);
|
||||
SAVE_PART("p", sk.p, sk.plen);
|
||||
SAVE_PART("q", sk.q, sk.qlen);
|
||||
SAVE_PART("dp", sk.dp, sk.dplen);
|
||||
SAVE_PART("dq", sk.dq, sk.dqlen);
|
||||
SAVE_PART("iq", sk.iq, sk.iqlen);
|
||||
|
||||
#undef SAVE_PART
|
||||
|
||||
montauk::config::set_int(&config, "host_key.bits", sk.n_bitlen);
|
||||
int result = montauk::config::save(configName, &config);
|
||||
config.destroy();
|
||||
montauk::memset(encoded, 0, sizeof(encoded));
|
||||
return result;
|
||||
}
|
||||
|
||||
bool generate(Rng& rng) {
|
||||
if (!br_rsa_i31_keygen((const br_prng_class**)&rng.context,
|
||||
&sk, privateBuffer,
|
||||
&pk, publicBuffer,
|
||||
2048, 65537)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return save_to(HOSTKEY_CONFIG) >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
/* RFC 3526 MODP group 14, represented as little-endian 32-bit limbs. */
|
||||
static constexpr int NL = 64;
|
||||
static const uint32_t P[NL] = {
|
||||
0xFFFFFFFF, 0xFFFFFFFF, 0x8AACAA68, 0x15728E5A,
|
||||
0x98FA0510, 0x15D22618, 0xEA956AE5, 0x3995497C,
|
||||
0x95581718, 0xDE2BCBF6, 0x6F4C52C9, 0xB5C55DF0,
|
||||
0xEC07A28F, 0x9B2783A2, 0x180E8603, 0xE39E772C,
|
||||
0x2E36CE3B, 0x32905E46, 0xCA18217C, 0xF1746C08,
|
||||
0x4ABC9804, 0x670C354E, 0x7096966D, 0x9ED52907,
|
||||
0x208552BB, 0x1C62F356, 0xDCA3AD96, 0x83655D23,
|
||||
0xFD24CF5F, 0x69163FA8, 0x1C55D39A, 0x98DA4836,
|
||||
0xA163BF05, 0xC2007CB8, 0xECE45B3D, 0x49286651,
|
||||
0x7C4B1FE6, 0xAE9F2411, 0x5A899FA5, 0xEE386BFB,
|
||||
0xF406B7ED, 0x0BFF5CB6, 0xA637ED6B, 0xF44C42E9,
|
||||
0x625E7EC6, 0xE485B576, 0x6D51C245, 0x4FE1356D,
|
||||
0xF25F1437, 0x302B0A6D, 0xCD3A431B, 0xEF9519B3,
|
||||
0x8E3404DD, 0x514A0879, 0x3B139B22, 0x020BBEA6,
|
||||
0x8A67CC74, 0x29024E08, 0x80DC1CD1, 0xC4C6628B,
|
||||
0x2168C234, 0xC90FDAA2, 0xFFFFFFFF, 0xFFFFFFFF,
|
||||
};
|
||||
|
||||
static inline int cmp(const uint32_t* left, const uint32_t* right) {
|
||||
for (int i = NL - 1; i >= 0; i--) {
|
||||
if (left[i] != right[i]) return left[i] > right[i] ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline void subp(uint32_t* value) {
|
||||
uint64_t borrow = 0;
|
||||
for (int i = 0; i < NL; i++) {
|
||||
uint64_t difference = (uint64_t)value[i] - P[i] - borrow;
|
||||
value[i] = (uint32_t)difference;
|
||||
borrow = (difference >> 63) & 1;
|
||||
}
|
||||
}
|
||||
|
||||
static inline void dbl(uint32_t* value) {
|
||||
uint64_t carry = 0;
|
||||
for (int i = 0; i < NL; i++) {
|
||||
uint64_t doubled = ((uint64_t)value[i] << 1) | carry;
|
||||
value[i] = doubled;
|
||||
carry = doubled >> 32;
|
||||
}
|
||||
if (carry || cmp(value, P) >= 0) subp(value);
|
||||
}
|
||||
|
||||
static inline void montmul(const uint32_t* left, const uint32_t* right,
|
||||
uint32_t* output) {
|
||||
uint32_t temporary[NL + 1] = {};
|
||||
|
||||
/* P[0] is -1, therefore -P^-1 mod 2^32 is 1. */
|
||||
for (int i = 0; i < NL; i++) {
|
||||
uint64_t carry = 0;
|
||||
for (int j = 0; j < NL; j++) {
|
||||
uint64_t product = (uint64_t)left[j] * right[i] +
|
||||
temporary[j] + carry;
|
||||
temporary[j] = product;
|
||||
carry = product >> 32;
|
||||
}
|
||||
|
||||
uint64_t sum = (uint64_t)temporary[NL] + carry;
|
||||
temporary[NL] = (uint32_t)sum;
|
||||
|
||||
uint32_t multiplier = temporary[0];
|
||||
carry = 0;
|
||||
for (int j = 0; j < NL; j++) {
|
||||
sum = (uint64_t)multiplier * P[j] + temporary[j] + carry;
|
||||
if (j > 0) temporary[j - 1] = (uint32_t)sum;
|
||||
carry = sum >> 32;
|
||||
}
|
||||
|
||||
sum = (uint64_t)temporary[NL] + carry;
|
||||
temporary[NL - 1] = (uint32_t)sum;
|
||||
temporary[NL] = (uint32_t)(sum >> 32);
|
||||
}
|
||||
|
||||
for (int i = 0; i < NL; i++) output[i] = temporary[i];
|
||||
if (temporary[NL] || cmp(output, P) >= 0) subp(output);
|
||||
}
|
||||
|
||||
static inline void from_be(const uint8_t* input, int size, uint32_t* output) {
|
||||
montauk::memset(output, 0, NL * 4);
|
||||
for (int i = 0; i < size && i < 256; i++) {
|
||||
output[i / 4] |= (uint32_t)input[size - 1 - i] << ((i & 3) * 8);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void to_be(const uint32_t* input, uint8_t output[256]) {
|
||||
for (int i = 0; i < 256; i++) {
|
||||
output[255 - i] = (uint8_t)(input[i / 4] >> ((i & 3) * 8));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void modexp(const uint32_t* base, const uint8_t* exponent,
|
||||
int exponentSize, uint32_t* output) {
|
||||
uint32_t one[NL] = {};
|
||||
uint32_t rSquared[NL];
|
||||
uint32_t result[NL];
|
||||
uint32_t montgomeryBase[NL];
|
||||
uint32_t temporary[NL];
|
||||
|
||||
one[0] = 1;
|
||||
for (int i = 0; i < NL * 32; i++) dbl(one);
|
||||
for (int i = 0; i < NL; i++) rSquared[i] = one[i];
|
||||
for (int i = 0; i < NL * 32; i++) dbl(rSquared);
|
||||
for (int i = 0; i < NL; i++) result[i] = one[i];
|
||||
montmul(base, rSquared, montgomeryBase);
|
||||
|
||||
for (int i = 0; i < exponentSize; i++) {
|
||||
for (int bit = 7; bit >= 0; bit--) {
|
||||
montmul(result, result, temporary);
|
||||
for (int j = 0; j < NL; j++) result[j] = temporary[j];
|
||||
if ((exponent[i] >> bit) & 1) {
|
||||
montmul(result, montgomeryBase, temporary);
|
||||
for (int j = 0; j < NL; j++) result[j] = temporary[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t normal[NL] = {};
|
||||
normal[0] = 1;
|
||||
montmul(result, normal, output);
|
||||
}
|
||||
|
||||
struct Cipher {
|
||||
br_aes_ct_ctr_keys aes;
|
||||
uint8_t iv[12];
|
||||
uint32_t counter = 0;
|
||||
uint8_t mac[32];
|
||||
|
||||
void init(const uint8_t* key, const uint8_t* initializationVector,
|
||||
const uint8_t* macKey) {
|
||||
br_aes_ct_ctr_init(&aes, key, 16);
|
||||
montauk::memcpy(iv, initializationVector, sizeof(iv));
|
||||
counter = be32(initializationVector + 12);
|
||||
montauk::memcpy(mac, macKey, sizeof(mac));
|
||||
}
|
||||
|
||||
void crypt(void* data, int size) {
|
||||
counter = br_aes_ct_ctr_run(&aes, iv, counter, data, size);
|
||||
}
|
||||
};
|
||||
|
||||
static inline void hmac(const uint8_t key[32], const void* data, int size,
|
||||
uint8_t output[32]) {
|
||||
br_hmac_key_context keyContext;
|
||||
br_hmac_context context;
|
||||
br_hmac_key_init(&keyContext, &br_sha256_vtable, key, 32);
|
||||
br_hmac_init(&context, &keyContext, 32);
|
||||
br_hmac_update(&context, data, size);
|
||||
br_hmac_out(&context, output);
|
||||
}
|
||||
|
||||
} // namespace sshcrypto
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
LIBDIR := ../../lib
|
||||
CXXFLAGS := -std=gnu++20 -g -O2 -pipe -Wall -Wextra -Wno-unused-parameter \
|
||||
-ffreestanding -fno-stack-protector -fno-stack-check -fno-rtti -fno-exceptions \
|
||||
-ffunction-sections -fdata-sections -msse -msse2 -MMD -MP -I $(PROG_INC) \
|
||||
-I $(LIBDIR)/bearssl/inc -isystem $(PROG_INC)/libc
|
||||
LDFLAGS := -nostdlib -Wl,--gc-sections -T $(LINK_LD)
|
||||
SRCS := main.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
TARGET := $(BINDIR)/apps/sshserver/sshserver.elf
|
||||
LIBS := $(LIBDIR)/bearssl/libbearssl.a $(LIBDIR)/libc/liblibc.a
|
||||
.PHONY: all clean
|
||||
all: $(TARGET)
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS)
|
||||
mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
-include $(OBJS:.o=.d)
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS SSH Server settings applet
|
||||
*/
|
||||
|
||||
#include <gui/mtk.hpp>
|
||||
#include <gui/mtk/settings.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/ssh.h>
|
||||
#include <montauk/syscall.h>
|
||||
|
||||
extern "C" {
|
||||
#include <stdio.h>
|
||||
}
|
||||
|
||||
using namespace gui;
|
||||
|
||||
static constexpr int WIN_W = 620;
|
||||
static constexpr int WIN_H = 500;
|
||||
static constexpr int PAD = 20;
|
||||
static constexpr int FOOTER_H = 48;
|
||||
static constexpr int USER_ROW_H = 38;
|
||||
static constexpr int VISIBLE_USERS = 7;
|
||||
|
||||
static WsWindow g_win;
|
||||
static Color g_accent = colors::ACCENT;
|
||||
static int g_mouse_x = -1;
|
||||
static int g_mouse_y = -1;
|
||||
static bool g_is_admin = false;
|
||||
static bool g_enabled = false;
|
||||
static bool g_saved_enabled = false;
|
||||
static bool g_dirty = false;
|
||||
static montauk::user::UserInfo g_users[montauk::user::MAX_USERS];
|
||||
static bool g_allowed[montauk::user::MAX_USERS];
|
||||
static bool g_saved_allowed[montauk::user::MAX_USERS];
|
||||
static int g_user_count = 0;
|
||||
static int g_user_scroll = 0;
|
||||
static char g_status[128] = {};
|
||||
|
||||
static Rect service_row() {
|
||||
return {PAD, 76, WIN_W - PAD * 2, 32};
|
||||
}
|
||||
|
||||
static Rect user_row(int visibleIndex) {
|
||||
return {PAD, 164 + visibleIndex * USER_ROW_H,
|
||||
WIN_W - PAD * 2, USER_ROW_H};
|
||||
}
|
||||
|
||||
static Rect apply_button() {
|
||||
return {WIN_W - PAD - 92, WIN_H - FOOTER_H + 8, 92, 31};
|
||||
}
|
||||
|
||||
static Rect revert_button() {
|
||||
Rect apply = apply_button();
|
||||
return {apply.x - 104, apply.y, 92, apply.h};
|
||||
}
|
||||
|
||||
static mtk::Theme app_theme() {
|
||||
return mtk::make_theme(g_accent);
|
||||
}
|
||||
|
||||
static void load_state() {
|
||||
char currentUser[32] = {};
|
||||
montauk::getuser(currentUser, sizeof(currentUser));
|
||||
|
||||
auto init = montauk::config::load("init");
|
||||
g_enabled = init.get_bool("services.ssh.enabled", false);
|
||||
init.destroy();
|
||||
|
||||
auto ssh = montauk::config::load("ssh");
|
||||
g_user_count = montauk::user::load_users(
|
||||
g_users, montauk::user::MAX_USERS);
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
g_allowed[i] = montauk::ssh::user_allowed(ssh, g_users[i].username);
|
||||
}
|
||||
ssh.destroy();
|
||||
|
||||
/* Reuse the table just loaded rather than parsing the user file twice. */
|
||||
g_is_admin = false;
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
if (montauk::streq(g_users[i].username, currentUser)) {
|
||||
g_is_admin = montauk::streq(g_users[i].role, "admin");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
g_status[0] = 0;
|
||||
|
||||
g_saved_enabled = g_enabled;
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
g_saved_allowed[i] = g_allowed[i];
|
||||
}
|
||||
g_dirty = false;
|
||||
}
|
||||
|
||||
static void recompute_dirty() {
|
||||
g_dirty = g_enabled != g_saved_enabled;
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
if (g_allowed[i] != g_saved_allowed[i]) g_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool save_state() {
|
||||
if (!g_is_admin) {
|
||||
snprintf(g_status, sizeof(g_status),
|
||||
"Administrator access is required");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto init = montauk::config::load("init");
|
||||
/*
|
||||
* init discovers services by enumerating [services.<id>] tables and needs
|
||||
* a path to launch. On a system whose init.toml predates the SSH service,
|
||||
* writing only the enabled flag would produce a key init cannot act on, so
|
||||
* write the whole service definition and let set_* overwrite in place where
|
||||
* it already exists.
|
||||
*/
|
||||
if (!init.get_string("services.ssh.path", nullptr)) {
|
||||
montauk::config::set_string(&init, "services.ssh.path",
|
||||
"0:/os/sshd.elf");
|
||||
montauk::config::set_string(&init, "services.ssh.name", "SSH server");
|
||||
montauk::config::set_bool(&init, "services.ssh.wait", false);
|
||||
montauk::config::set_bool(&init, "services.ssh.optional", true);
|
||||
}
|
||||
montauk::config::set_bool(&init, "services.ssh.enabled", g_enabled);
|
||||
int initResult = montauk::config::save("init", &init);
|
||||
init.destroy();
|
||||
|
||||
auto ssh = montauk::config::load("ssh");
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
char key[64];
|
||||
montauk::ssh::allow_key(key, sizeof(key), g_users[i].username);
|
||||
montauk::config::set_bool(&ssh, key, g_allowed[i]);
|
||||
}
|
||||
int sshResult = montauk::config::save("ssh", &ssh);
|
||||
ssh.destroy();
|
||||
|
||||
if (initResult < 0 || sshResult < 0) {
|
||||
snprintf(g_status, sizeof(g_status),
|
||||
"Could not save SSH configuration");
|
||||
return false;
|
||||
}
|
||||
|
||||
g_saved_enabled = g_enabled;
|
||||
for (int i = 0; i < g_user_count; i++) {
|
||||
g_saved_allowed[i] = g_allowed[i];
|
||||
}
|
||||
g_dirty = false;
|
||||
|
||||
snprintf(g_status, sizeof(g_status),
|
||||
g_enabled ? "Saved; SSH starts on the next boot"
|
||||
: "Saved; SSH is disabled on the next boot");
|
||||
return true;
|
||||
}
|
||||
|
||||
static void render() {
|
||||
mtk::StandaloneHost host(&g_win);
|
||||
Canvas canvas = host.canvas();
|
||||
mtk::Theme theme = app_theme();
|
||||
canvas.fill(theme.window_bg);
|
||||
|
||||
canvas.text(PAD, 20, "SSH Server", theme.text);
|
||||
canvas.text(PAD, 44,
|
||||
"Secure remote access to the MontaukOS shell",
|
||||
theme.text_subtle);
|
||||
|
||||
Rect service = service_row();
|
||||
mtk::draw_checkbox(canvas, service,
|
||||
"Start the SSH server at boot",
|
||||
mtk::check_state(g_enabled), theme,
|
||||
g_is_admin,
|
||||
service.contains(g_mouse_x, g_mouse_y));
|
||||
canvas.text(PAD + 24, 112,
|
||||
"Listens on TCP port 22 and uses MontaukOS account passwords.",
|
||||
theme.text_subtle);
|
||||
canvas.text(PAD, 142, "USERS ALLOWED TO CONNECT", theme.text_muted);
|
||||
|
||||
for (int row = 0; row < VISIBLE_USERS; row++) {
|
||||
int userIndex = g_user_scroll + row;
|
||||
if (userIndex >= g_user_count) break;
|
||||
|
||||
Rect bounds = user_row(row);
|
||||
if (userIndex & 1) {
|
||||
canvas.fill_rect(bounds.x, bounds.y, bounds.w, bounds.h,
|
||||
mtk::mix(theme.window_bg, theme.surface, 55));
|
||||
}
|
||||
|
||||
char label[112];
|
||||
const char* displayName = g_users[userIndex].display_name[0]
|
||||
? g_users[userIndex].display_name
|
||||
: g_users[userIndex].username;
|
||||
snprintf(label, sizeof(label), "%s (%s)",
|
||||
displayName, g_users[userIndex].username);
|
||||
mtk::draw_checkbox(canvas, bounds, label,
|
||||
mtk::check_state(g_allowed[userIndex]),
|
||||
theme, g_is_admin,
|
||||
bounds.contains(g_mouse_x, g_mouse_y));
|
||||
|
||||
if (montauk::streq(g_users[userIndex].role, "admin")) {
|
||||
const char* role = "Administrator";
|
||||
canvas.text(bounds.x + bounds.w - text_width(role) - 10,
|
||||
bounds.y + 10, role, theme.text_muted);
|
||||
}
|
||||
}
|
||||
|
||||
/* Without an indicator there is nothing to show the list continues. */
|
||||
if (g_user_count > VISIBLE_USERS) {
|
||||
Rect track = {WIN_W - PAD + 4, user_row(0).y, 4,
|
||||
VISIBLE_USERS * USER_ROW_H};
|
||||
canvas.fill_rect(track.x, track.y, track.w, track.h,
|
||||
mtk::mix(theme.window_bg, theme.surface, 120));
|
||||
|
||||
int thumbHeight = track.h * VISIBLE_USERS / g_user_count;
|
||||
if (thumbHeight < 20) thumbHeight = 20;
|
||||
int span = g_user_count - VISIBLE_USERS;
|
||||
int thumbY = track.y +
|
||||
(track.h - thumbHeight) * g_user_scroll / (span ? span : 1);
|
||||
canvas.fill_rect(track.x, thumbY, track.w, thumbHeight,
|
||||
theme.text_muted);
|
||||
}
|
||||
|
||||
if (!g_is_admin) {
|
||||
canvas.text(PAD, WIN_H - FOOTER_H - 27,
|
||||
"Only administrators can change system SSH settings.",
|
||||
theme.danger);
|
||||
}
|
||||
|
||||
Rect footer = {0, WIN_H - FOOTER_H, WIN_W, FOOTER_H};
|
||||
canvas.fill_rect(footer.x, footer.y, footer.w, footer.h, theme.surface);
|
||||
mtk::draw_separator(canvas, 0, footer.y, WIN_W, theme);
|
||||
const char* footerStatus = g_status[0]
|
||||
? g_status
|
||||
: (g_dirty ? "Unsaved SSH changes" : "SSH configuration ready");
|
||||
canvas.text(PAD, footer.y + 16, footerStatus, theme.text_subtle);
|
||||
|
||||
Rect revert = revert_button();
|
||||
Rect apply = apply_button();
|
||||
mtk::draw_button(
|
||||
canvas, revert, "Revert", mtk::BUTTON_SECONDARY,
|
||||
mtk::widget_state(false, revert.contains(g_mouse_x, g_mouse_y),
|
||||
g_dirty && g_is_admin),
|
||||
theme);
|
||||
mtk::draw_button(
|
||||
canvas, apply, "Apply", mtk::BUTTON_PRIMARY,
|
||||
mtk::widget_state(false, apply.contains(g_mouse_x, g_mouse_y),
|
||||
g_dirty && g_is_admin),
|
||||
theme);
|
||||
|
||||
host.present();
|
||||
}
|
||||
|
||||
static void handle_mouse(const montauk::abi::WinEvent& event) {
|
||||
g_mouse_x = event.mouse.x;
|
||||
g_mouse_y = event.mouse.y;
|
||||
|
||||
bool pressed = (event.mouse.buttons & 1) &&
|
||||
!(event.mouse.prev_buttons & 1);
|
||||
|
||||
if (event.mouse.scroll) {
|
||||
g_user_scroll += event.mouse.scroll > 0 ? -1 : 1;
|
||||
int maximum = g_user_count > VISIBLE_USERS
|
||||
? g_user_count - VISIBLE_USERS
|
||||
: 0;
|
||||
if (g_user_scroll < 0) g_user_scroll = 0;
|
||||
if (g_user_scroll > maximum) g_user_scroll = maximum;
|
||||
}
|
||||
|
||||
if (!pressed || !g_is_admin) return;
|
||||
|
||||
if (service_row().contains(g_mouse_x, g_mouse_y)) {
|
||||
g_enabled = !g_enabled;
|
||||
/* A stale "Saved" message would otherwise hide the unsaved state. */
|
||||
g_status[0] = 0;
|
||||
} else {
|
||||
for (int row = 0; row < VISIBLE_USERS; row++) {
|
||||
int userIndex = g_user_scroll + row;
|
||||
if (userIndex < g_user_count &&
|
||||
user_row(row).contains(g_mouse_x, g_mouse_y)) {
|
||||
g_allowed[userIndex] = !g_allowed[userIndex];
|
||||
g_status[0] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (revert_button().contains(g_mouse_x, g_mouse_y) && g_dirty) {
|
||||
load_state();
|
||||
} else if (apply_button().contains(g_mouse_x, g_mouse_y) && g_dirty) {
|
||||
save_state();
|
||||
}
|
||||
recompute_dirty();
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
if (!fonts::init()) montauk::exit(1);
|
||||
|
||||
g_accent = mtk::load_system_accent();
|
||||
load_state();
|
||||
if (!g_win.create("SSH Server", WIN_W, WIN_H)) montauk::exit(1);
|
||||
render();
|
||||
|
||||
while (g_win.id >= 0 && !g_win.closed) {
|
||||
montauk::abi::WinEvent event;
|
||||
int count = g_win.poll(&event);
|
||||
if (count <= 0) {
|
||||
montauk::yield();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type == 1) {
|
||||
handle_mouse(event);
|
||||
} else if (event.type == 2) {
|
||||
g_win.width = event.resize.w;
|
||||
g_win.height = event.resize.h;
|
||||
} else if (event.type == 3) {
|
||||
g_win.closed = true;
|
||||
} else if (event.type == 0 && event.key.pressed &&
|
||||
event.key.scancode == 1) {
|
||||
g_win.closed = true;
|
||||
}
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
g_win.destroy();
|
||||
montauk::exit(0);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[app]
|
||||
id = "sshserver"
|
||||
name = "SSH Server"
|
||||
binary = "sshserver.elf"
|
||||
icon = "utilities-terminal.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
visible = false
|
||||
|
||||
[desktop]
|
||||
section = "settings"
|
||||
admin_only = false
|
||||
@@ -0,0 +1,2 @@
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
Reference in New Issue
Block a user