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

Implement RFD001 in C #170

Merged
merged 6 commits into from
Oct 2, 2023
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
112 changes: 20 additions & 92 deletions cli/commands.c
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,10 @@ int verify(int argc, char **argv) {
return ret;
}

static bool parse_login_path(char *path, char **handshake, char **host,
char **action) {
static bool parse_login_path(const char *path, char **handshake,
char **message) {
size_t path_len = strlen(path);
if (path_len < 3 || path[0] != 'v' || path[1] != '1' || path[2] != '/') {
if (path_len < 3 || path[0] != 'v' || path[1] != '2' || path[2] != '/') {
fprintf(stderr, "unexpected challenge prefix: %s\n", path);
return false;
}
Expand All @@ -282,7 +282,7 @@ static bool parse_login_path(char *path, char **handshake, char **host,
return false;
}

char *start = path + 3;
const char *start = path + 3;
char *slash = strchr(start, '/');
if (slash == NULL || slash - start == 0) {
fprintf(stderr, "could not parse handshake from %s\n", start);
Expand All @@ -294,93 +294,23 @@ static bool parse_login_path(char *path, char **handshake, char **host,
return false;
}

// Everything left (not including the trailing slash) is the message.
start = slash + 1;
slash = strchr(start, '/');
if (slash == NULL || slash - start == 0) {
free(*handshake);
*handshake = NULL;
fprintf(stderr, "could not parse host from %s\n", start);
return false;
}
*host = strndup(start, slash - start);
if (*host == NULL) {
*message = strndup(start, path + path_len - 1 - start);
if (*message == NULL) {
free(*handshake);
*handshake = NULL;
fprintf(stderr, "failed to duplicate host\n");
return false;
}

// Everything left (not including the trailing slash) is an action. It may
// include slashes and it can also be empty.
start = slash + 1;
*action = strndup(start, path + path_len - 1 - start);
if (*action == NULL) {
free(*host);
*host = NULL;
free(*handshake);
*handshake = NULL;
fprintf(stderr, "failed to duplicate action\n");
fprintf(stderr, "failed to duplicate message\n");
return false;
}

return true;
}

static int unhex(char c) {
if (c >= '0' && c <= '9') {
return c - '0';
} else if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
} else if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
} else {
return -1;
}
}

static char *uri_unescape(char *e) {
size_t len = strlen(e);
char *u = malloc(len + 1);
if (u == NULL) {
return NULL;
}

size_t i, j;
for (i = 0, j = 0; i < len; j++) {
if (e[i] != '%') {
u[j] = e[i];
i += 1;
continue;
}
if (i + 2 >= len) {
goto fail;
}
int n = unhex(e[i + 1]);
if (n < 0) {
goto fail;
}
u[j] = (char)(n << 4);
n = unhex(e[i + 2]);
if (n < 0) {
goto fail;
}
u[j] |= (char)n;
i += 3;
}
u[j] = '\0';
return u;

fail:
free(u);
return NULL;
}

int login(int argc, char **argv) {
char *handshake = NULL;
char *handshake_b64 = NULL;
char *host = NULL;
char *host_esc = NULL;
char *action = NULL;
char *message = NULL;
int ret = EXIT_FAILURE;

if (!parse_args(argc, argv)) {
Expand All @@ -401,16 +331,14 @@ int login(int argc, char **argv) {
return EXIT_FAILURE;
}

char *path = strstr(argv[optind], "v1/");
if (!parse_login_path(path, &handshake_b64, &host_esc, &action)) {
return EXIT_FAILURE;
}

host = uri_unescape(host_esc);
if (host == NULL) {
fprintf(stderr, "failed to parse hostname in path %s\n", path);
char *path = strstr(argv[optind], "v2/");
if (path == NULL) {
fprintf(stderr, "unsupported challenge format\n");
goto out;
}
if (!parse_login_path(path, &handshake_b64, &message)) {
return EXIT_FAILURE;
}

size_t handshake_b64_len = strlen(handshake_b64);
handshake = malloc(DECODED_BUFSIZE(handshake_b64_len));
Expand Down Expand Up @@ -440,7 +368,8 @@ int login(int argc, char **argv) {
memcpy(peer_key, handshake + 1, GLOME_MAX_PUBLIC_KEY_LENGTH);

uint8_t tag[GLOME_MAX_TAG_LENGTH] = {0};
if (get_authcode(host, action, peer_key, private_key, tag)) {
if (glome_tag(true, 0, private_key, peer_key, (uint8_t *)message,
strlen(message), tag)) {
fprintf(stderr, "MAC authcode generation failed\n");
goto out;
}
Expand All @@ -453,7 +382,8 @@ int login(int argc, char **argv) {
goto out;
}

if (get_msg_tag(host, action, peer_key, private_key, tag)) {
if (glome_tag(false, 0, private_key, peer_key, (uint8_t *)message,
strlen(message), tag)) {
fprintf(stderr, "GLOME tag generation failed\n");
goto out;
}
Expand All @@ -468,9 +398,7 @@ int login(int argc, char **argv) {

out:
free(handshake);
free(host);
free(handshake_b64);
free(host_esc);
free(action);
free(message);
return ret;
}
16 changes: 2 additions & 14 deletions cli/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,9 @@ for n in 1 2; do
fi
done

# Test login subcommand according to specification.
key="$t/vector-1/b"
path="v1/AYUg8AmJMKdUdIt93LQ-91oNvzoNJjga9OukqY6qm05q0PU=/my-server.local/shell/root/"
expected_tag="lyHuaHuCcknb5sJEukWSFs8B1SUBIWMCXfNY64fIkFk="
tag=$("$binary" login --key "$key" "$path")
if [ "$tag" != "$expected_tag" ]; then
echo "Generated wrong tag for test path $path" >&2
echo "$expected_tag <- expected" >&2
echo "$tag <- actual" >&2
errors=$((errors + 1))
fi

key="$t/vector-2/a"
path="v1/UYcvQ1u4uJ0OOtYqouURB07hleHDnvaogAFBi-ZW48N2/serial-number:1234567890=ABCDFGH%2F%23%3F/reboot/"
expected_tag="p8M_BUKj7zXBVM2JlQhNYFxs4J-DzxRAps83ZaNDquY="
path="v2/R4cvQ1u4uJ0OOtYqouURB07hleHDnvaogAFBi-ZW48N2/myhost/exec=%2Fbin%2Fsh/"
expected_tag="ZmxczN4x3g4goXu-A2AuuEEVftgS6xM-6gYj-dRrlis="
tag=$("$binary" login --key "$key" "$path")
if [ "$tag" != "$expected_tag" ]; then
echo "Generated wrong tag for test path $path" >&2
Expand Down
109 changes: 75 additions & 34 deletions login/crypto.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#include "crypto.h"

#include <ctype.h>
#include <glome.h>
#include <stdio.h>
#include <stdlib.h>
Expand All @@ -38,44 +39,84 @@ int derive_or_generate_key(uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH],
}
}

static int login_tag(bool verify, const char* host_id, const char* action,
const uint8_t peer_key[GLOME_MAX_PUBLIC_KEY_LENGTH],
const uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH],
uint8_t output[GLOME_MAX_TAG_LENGTH]) {
size_t message_len = strlen(host_id) + 1 + strlen(action) + 1;
char* message = calloc(message_len, 1);
if (message == NULL) {
return -1;
}
int ret = snprintf(message, message_len, "%s/%s", host_id, action);
if (ret < 0) {
free(message);
return -1;
static const char* valid_url_path_chars = "-._~!$&'()*+,;=";

static const size_t escaped_char_length = 3;

// Escape a string for use as an URL path segment. All characters in the extra
// string are escaped, even if they would not need to be by the spec, so that
// they can be used as delimiters, too.
//
// See: https://url.spec.whatwg.org/#url-path-segment-string
static char* urlescape_path(const char* src, const char* extra) {
if (!src) return NULL;
if (!extra) extra = "";

// First pass: output length

size_t output_length = 1; // We need at least the trailing NUL byte.
for (const char* c = src; *c != '\0'; c++) {
if (!strchr(extra, *c) &&
(isalnum(*c) || strchr(valid_url_path_chars, *c))) {
output_length += 1;
} else {
output_length += escaped_char_length;
}
}
if ((size_t)ret >= message_len) {
free(message);
return -1;
char* dst = calloc(output_length, 1);
if (!dst) return dst;

// Second pass: copy over and escape

int dst_offset = 0;
for (const char* next_char = src; *next_char != '\0'; next_char++) {
if (!strchr(extra, *next_char) &&
(isalnum(*next_char) || strchr(valid_url_path_chars, *next_char))) {
dst[dst_offset] = *next_char;
dst_offset++;
} else {
snprintf(dst + dst_offset, escaped_char_length + 1, "%%%02X", *next_char);
dst_offset += escaped_char_length;
}
}
if (glome_tag(verify, 0, private_key, peer_key, (uint8_t*)message,
strlen(message), output) != 0) {
free(message);
return -1;
return dst;
}

char* glome_login_message(const char* host_id_type, const char* host_id,
const char* action) {
char *host_id_type_escaped = NULL, *host_id_escaped = NULL,
*action_escaped = NULL, *message = NULL;

host_id_escaped = urlescape_path(host_id, ":");
action_escaped = urlescape_path(action, "");
if (!host_id_escaped || !action_escaped) goto end;

size_t message_len = strlen(host_id_escaped) + 1 + strlen(action_escaped) + 1;

// Only prefix host_id_type if it's not empty.
if (host_id_type && *host_id_type) {
host_id_type_escaped = urlescape_path(host_id_type, ":");
if (!host_id_type_escaped) goto end;
message_len += strlen(host_id_type_escaped) + 1;
}

free(message);
return 0;
}
message = calloc(message_len, 1);
if (message == NULL) {
goto end;
}

int get_authcode(const char* host_id, const char* action,
const uint8_t peer_key[GLOME_MAX_PUBLIC_KEY_LENGTH],
const uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH],
uint8_t authcode[GLOME_MAX_TAG_LENGTH]) {
return login_tag(true, host_id, action, peer_key, private_key, authcode);
}
char* dst = message;
if (host_id_type_escaped) {
dst = stpcpy(dst, host_id_type_escaped);
*(dst++) = ':';
}
dst = stpcpy(dst, host_id_escaped);
*(dst++) = '/';
dst = stpcpy(dst, action_escaped);

int get_msg_tag(const char* host_id, const char* action,
const uint8_t peer_key[GLOME_MAX_PUBLIC_KEY_LENGTH],
const uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH],
uint8_t tag[GLOME_MAX_TAG_LENGTH]) {
return login_tag(false, host_id, action, peer_key, private_key, tag);
end:
free(host_id_type_escaped);
free(host_id_escaped);
free(action_escaped);
return message;
}
19 changes: 9 additions & 10 deletions login/crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,17 @@
int derive_or_generate_key(uint8_t private_key[PRIVATE_KEY_LENGTH],
uint8_t public_key[PUBLIC_KEY_LENGTH]);

int get_authcode(const char* host_id, const char* action,
const uint8_t peer_key[PUBLIC_KEY_LENGTH],
const uint8_t private_key[PRIVATE_KEY_LENGTH],
uint8_t authcode[GLOME_MAX_TAG_LENGTH]);

int get_msg_tag(const char* host_id, const char* action,
const uint8_t peer_key[PUBLIC_KEY_LENGTH],
const uint8_t private_key[PRIVATE_KEY_LENGTH],
uint8_t tag[GLOME_MAX_TAG_LENGTH]);

// is_zeroed() checks (in constant time) if all len bytes of buf are zeros.
// This is to avoid timing attacks.
int is_zeroed(const uint8_t* buf, size_t len);

// Create an encoded GLOME Login message from its constituent parts.
// The host_id_type may be empty, in which case only the host_id will be part
// of the host path segment. host_id and action must not be NULL.
// On error, a NULL pointer is returned.
// On success, a pointer to a NUL-terminated string is returned that needs to
// be freed by the caller.
char* glome_login_message(const char* host_id_type, const char* host_id,
const char* action);

#endif // LOGIN_CRYPTO_H_
Loading
Loading