pax_global_header00006660000000000000000000000064147605666660014540gustar00rootroot0000000000000052 comment=36464267bc2106fd844b34fd6b9c7f80dfd1987e glome-0.2/000077500000000000000000000000001476056666600125045ustar00rootroot00000000000000glome-0.2/.clang-format000066400000000000000000000000251476056666600150540ustar00rootroot00000000000000BasedOnStyle: Google glome-0.2/.github/000077500000000000000000000000001476056666600140445ustar00rootroot00000000000000glome-0.2/.github/workflows/000077500000000000000000000000001476056666600161015ustar00rootroot00000000000000glome-0.2/.github/workflows/presubmit-c.yml000066400000000000000000000055271476056666600210670ustar00rootroot00000000000000name: c-presubmit on: push: branches: ['master'] pull_request: branches: ['master'] workflow_dispatch: {} jobs: clang-format: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: clang-format run: | find . '(' -name '*.c' -or -name '*.h' ')' -print0 | \ xargs -0 --verbose -- clang-format --Werror --dry-run cpplint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: cpplint run: pip install cpplint && cpplint --recursive . test-linux: runs-on: ubuntu-latest strategy: matrix: container: - 'debian:stable' - 'debian:testing' - 'alpine:latest' - null container: ${{ matrix.container }} steps: - uses: actions/checkout@v2 - name: install Debian dependencies if: ${{ startsWith(matrix.container, 'debian:') }} run: ./kokoro/rodete/fetch_dependencies.sh - name: install Alpine dependencies if: ${{ matrix.container == 'alpine:latest' }} run: ./kokoro/alpine/fetch_dependencies.sh - uses: cachix/install-nix-action@v17 if: ${{ matrix.container == null }} with: nix_path: nixpkgs=channel:nixos-unstable - name: install Nix dependencies if: ${{ matrix.container == null }} run: nix-shell --run 'meson --buildtype=release --werror build && ninja -C build && meson test --print-errorlogs -C build' - name: setup build directory if: ${{ matrix.container != null }} run: meson --werror build - name: build if: ${{ matrix.container != null }} run: ninja -C build - name: test if: ${{ matrix.container != null }} run: meson test --print-errorlogs -C build - name: install if: ${{ matrix.container != null }} run: | DESTDIR=out meson install -C build find build/out/ test -x build/out/usr/local/bin/glome test -x build/out/usr/local/sbin/glome-login test -f build/out/usr/local/etc/glome/config test -f build/out/usr/local/include/glome.h # Dereference the library and check that it points to a valid file. test -f build/out/usr/local/lib/libglome.so || \ test -f build/out/usr/local/lib/x86_64-linux-gnu/libglome.so test -f build/out/usr/local/lib/security/pam_glome.so || \ test -f build/out/usr/local/lib/x86_64-linux-gnu/security/pam_glome.so test-macos: runs-on: macos-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies run: python -m pip install meson ninja - name: Setup build directory run: meson --werror build - name: Build run: ninja -C build - name: Test run: meson test --print-errorlogs -C build glome-0.2/.github/workflows/presubmit-go.yml000066400000000000000000000012021476056666600212340ustar00rootroot00000000000000name: go-presubmit on: push: branches: ['master'] pull_request: paths: ['go/**', '.github/workflows/presubmit-go.yml'] workflow_dispatch: {} defaults: run: working-directory: go jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: gofmt run: test -z "$(gofmt -d . | tee >&2)" - name: golint run: | go install golang.org/x/lint/golint@latest $(go env GOPATH)/bin/golint -set_exit_status=1 ./... unit-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Run tests run: go test ./... glome-0.2/.github/workflows/presubmit-python.yml000066400000000000000000000023561476056666600221630ustar00rootroot00000000000000name: python-presubmit on: push: branches: ['master'] pull_request: paths: ['python/**', '.github/workflows/presubmit-python.yml'] workflow_dispatch: {} defaults: run: working-directory: python jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: yapf run: | python -m pip install --upgrade yapf find . -type f -name '*.py' -print0 | \ xargs -0 --verbose yapf -d --style google - name: pylint # pylint checks only pyglome/* run: | python -m pip install --upgrade pylint find pyglome -type f -name '*.py' -print0 | \ xargs -0 --verbose pylint unit-test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: Run tests run: python -m test glome-0.2/.github/workflows/presubmit-rust.yml000066400000000000000000000013071476056666600216320ustar00rootroot00000000000000name: rust-presubmit on: push: branches: ['master'] pull_request: paths: ['rust/**', '.github/workflows/presubmit-rust.yml'] workflow_dispatch: {} defaults: run: working-directory: rust jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: fmt run: cargo fmt --check - name: check run: cargo check --all-targets --all-features - name: clippy run: cargo clippy --all-targets --all-features unit-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: build run: cargo build --all-targets --all-features - name: test run: cargo test --all-features glome-0.2/.github/workflows/presubmit-shell.yml000066400000000000000000000006031476056666600217420ustar00rootroot00000000000000name: shell-presubmit on: push: branches: ['master'] pull_request: paths: ['**.sh', '.github/workflows/presubmit-shell.yml'] workflow_dispatch: {} jobs: shellcheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: shellcheck run: | find . -type f -name '*.sh' -print0 | \ xargs -0 shellcheck --shell=sh glome-0.2/.github/workflows/presubmit-yaml.yml000066400000000000000000000004241476056666600215760ustar00rootroot00000000000000name: yaml-presubmit on: push: branches: ['master'] pull_request: paths: ['**.yml', '**.yaml'] workflow_dispatch: {} jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: yamllint run: yamllint -d relaxed . glome-0.2/.gitignore000066400000000000000000000000141476056666600144670ustar00rootroot00000000000000rust/target glome-0.2/CPPLINT.cfg000066400000000000000000000001211476056666600142700ustar00rootroot00000000000000set noparent filter=-readability/casting,-readability/todo,-build/include_subdir glome-0.2/LICENSE000066400000000000000000000261361476056666600135210ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. glome-0.2/README.md000066400000000000000000000127451476056666600137740ustar00rootroot00000000000000# Generic Low Overhead Message Exchange (GLOME) **GLOME Login** is a [challenge-response] authentication mechanism. It resembles [one-time authorization codes][OTP] (aka OTPs) but is different from [HOTP] and [TOTP] in the following ways: - It is stateless (unlike [HOTP]). - It does not depend on time (unlike [TOTP]). - It does not require predefined secret sharing (unlike [HOTP] and [TOTP]). These properties make it a good choice for low dependency environments (e.g., devices with no persistent storage a real-time clock). It can be also useful for managing access to a large fleet of hosts where synchronising state or sharing predefined secrets can be a challenge. GLOME Login can be easily integrated with existing systems through [PAM](https://en.wikipedia.org/wiki/Pluggable_authentication_module) (`libglome`) or through the [login(1)](https://manpages.debian.org/testing/login/login.1.en.html) wrapper ([glome-login](login)). [GLOME Login protocol](docs/glome-login.md) is is built on top of the [Generic Low Overhead Message Exchange (GLOME) protocol](docs/protocol.md). [challenge-response]: https://en.wikipedia.org/wiki/Challenge%E2%80%93response_authentication [OTP]: https://en.wikipedia.org/wiki/One-time_password [TOTP]: https://www.rfc-editor.org/rfc/rfc6238 [HOTP]: https://www.rfc-editor.org/rfc/rfc4226 ## How does it work? Let's imagine the following scenario: Alice is a system engineer who got paged to investigate an unresponsive machine that happens to be located far away. She calls Bob, a datacenter technican with physical access to the machine. Alice is authorized to access the machine but has no connectivity. Bob faces the opposite problem, he can access the machine's serial port but does not have credentials to log in. Alice is able to use GLOME Login to grant Bob one-time access to the machine. First, Bob connects to the machine over serial port and types `root` on the login prompt. He is then provided with a challenge that he forwards to Alice. The challenge contains information about the identity of accessed host and the requested action (i.e., root shell access). Alice verifies that the request is legitimate (e.g., the accessed host is indeed the one she's trying to diagnose), and uses the [`glome` CLI](cli) to generate an authorization code. She forwards that authorization code to Bob who provides it as a challenge response. The authorization succeeds and Bob is able to run diagnostic commands and share the results with Alice. ## Getting started ### Installation on the client host These steps should be followed on the host you are planning to use to generate authorization codes (e.g., a laptop). 1. Follow [build](docs/build) to build the `glome` CLI binary. 1. Generate a key pair using the `glome` command. Note that if the `glome` command is not in your `$PATH`, you might need to provide a full path to the binary. ``` $ glome genkey | tee glome-private.key | glome pubkey | tee glome-public.key | xxd -c 32 -p 4242424242424242424242424242424242424242424242424242424242424242 ``` The output of that command is the approver public key that will be used to configure the target host. ### Installation on the target host 1. Follow [instructions](login) to configure your host to use PAM module (recommended) or `glome-login`. 1. Edit the configuration file (by default located at `/etc/glome/config`) and replace the key value with the approver public key generated in the previous section. ``` $ cat /etc/glome/config key=4242424242424242424242424242424242424242424242424242424242424242 key-version=1 ``` ### Usage Try to log in to the target host. You should see the prompt with the challenge: ``` GLOME: v1/AU7U7GiFDG-ITgOh8K_ND9u41S3S-joGp7MAdhIp_rQt/myhost/shell/root/ Password: ``` Use the `glome` CLI on the client host to obtain an authorization code: ``` $ glome --key glome-private.key login v1/AU7U7GiFDG-ITgOh8K_ND9u41S3S-joGp7MAdhIp_rQt/myhost/shell/root/Tm90aGluZyB0byBzZWUgaGVyZSwgbW92ZSBhbG9uZy4K ``` Provide the generated authcode as a response to the challenge. ## Repository This repository consists of a number of components of the GLOME ecosystem. Documentation: - [GLOME protocol](docs/protocol.md) - [GLOME Login protocol](docs/glome-login.md) Core libraries: - [libglome](glome.h) *C* - [PyGLOME](python) *Python* - [GLOME-Go](go/glome) *Go* Binaries: - [glome](cli) *Command-line interface for GLOME* - [glome-login](login) *Replacement of login(1) implementing GLOME Login protocol* ## Building Building the GLOME library requires - Compiler conforming to C99 (e.g. gcc, clang) - Meson >=0.49.2 - OpenSSL headers >=1.1.1 - glib-2.0 (for glome-login as well as tests) - libpam (for PAM module) Alternatively, on systems with [Nix](https://nixos.org/), you can simply run `nix-shell` in the root directory of this repository. ### Instructions GLOME is built using [Meson](https://mesonbuild.com/). First, initialize the Meson build directory. You only have to do this once per Meson configuration. ```shell $ meson build ``` NOTE: You can customize the installation target by passing the `--prefix` flag. Build the shared library `libglome.so` and the command line utility `glome` inside the build root `./build`. ```shell $ ninja -C build ``` Now run the tests. ```shell $ meson test -C build ``` Install both the binary and the library into the configured prefix (the default prefix is `/usr/local/`, which will require admin privileges). ```shell $ meson install -C build ``` ## Disclaimer **This is not an officially supported Google product.** glome-0.2/cli/000077500000000000000000000000001476056666600132535ustar00rootroot00000000000000glome-0.2/cli/README.md000066400000000000000000000020031476056666600145250ustar00rootroot00000000000000# GLOME CLI This is a CLI utility to facilitate GLOME operations from the command line. ## Usage Generating two key pairs: ```shell $ glome genkey | tee Alice | glome pubkey >Alice.pub $ glome genkey | tee Bob | glome pubkey >Bob.pub ``` Alice calculates a tag and send it together with message and counter to Bob: ```shell $ tag=$(echo "Hello world!" | glome tag --key Alice --peer Bob.pub) $ echo "${tag?}" _QuyLz_nkj5exUJscocS8LDnCMszvSmp9wpQuRshi30= ``` Bob can verify that the tag matches: ```shell $ echo "Hello world!" | glome verify --key Bob --peer Alice.pub --tag "${tag?}" $ echo $? 0 ``` Both parties can agree to shorten the tag to reduce the protocol overhead: ```shell $ echo "Hello world!" | glome verify --key Bob --peer Alice.pub --tag "${tag:0:12}" $ echo $? 0 ``` CLI also supports ganerating tags for the GLOME Login requests: ```shell $ glome login --key Bob v1/AYUg8AmJMKdUdIt93LQ-91oNvzoNJjga9OukqY6qm05q0PU=/my-server.local/shell/root/ MT_Zc-hucXRjTXTBEo53ehoeUsFn1oFyVadViXf-I4k= ``` glome-0.2/cli/commands.c000066400000000000000000000305631476056666600152270ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "commands.h" #include #include #include #include #include #include #include #include #include #include #include #include "glome.h" #include "login/base64.h" #include "login/config.h" #include "login/crypto.h" #define GLOME_CLI_MAX_MESSAGE_LENGTH 4095 #define UNUSED(var) (void)(var) // Arguments static const char *key_file = NULL; static const char *peer_file = NULL; static const char *tag_b64 = NULL; static unsigned long counter = 0; // NOLINT(runtime/int) static bool parse_args(int argc, char **argv) { int c; struct option long_options[] = {{"key", required_argument, 0, 'k'}, {"peer", required_argument, 0, 'p'}, {"counter", required_argument, 0, 'c'}, {"tag", required_argument, 0, 't'}, {0, 0, 0, 0}}; // First argument is the command name so skip it. while ((c = getopt_long(argc - 1, argv + 1, "c:k:p:t:", long_options, NULL)) != -1) { switch (c) { case 'c': { char *endptr; errno = 0; counter = strtoul(optarg, &endptr, 0); if (errno || counter > 255 || optarg == endptr || *endptr != '\0') { fprintf(stderr, "'%s' is not a valid counter (0..255)\n", optarg); return false; } break; } case 'k': key_file = optarg; break; case 'p': peer_file = optarg; break; case 't': tag_b64 = optarg; break; case '?': return false; default: // option not implemented abort(); } } return true; } static bool read_file(const char *fname, uint8_t *buf, const size_t num_bytes) { FILE *f = fopen(fname, "r"); if (!f) { fprintf(stderr, "could not open file %s: %s\n", fname, strerror(errno)); return false; } if (fread(buf, 1, num_bytes, f) != num_bytes) { fprintf(stderr, "could not read %zu bytes from file %s", num_bytes, fname); if (ferror(f)) { fprintf(stderr, ": %s\n", strerror(errno)); } else { fputs("\n", stderr); } fclose(f); return false; } fclose(f); return true; } static bool read_public_key_file(const char *fname, uint8_t *buf, size_t buf_len) { FILE *f = fopen(fname, "r"); if (!f) { fprintf(stderr, "could not open file %s: %s\n", fname, strerror(errno)); return false; } // Allocate enough buffer space to fit the public key and a reasonable amount // of whitespace. char encoded_public_key[128] = {0}; if (!fgets(encoded_public_key, sizeof(encoded_public_key), f)) { perror("could not read from public key file"); fclose(f); return false; } fclose(f); if (!glome_login_parse_public_key(encoded_public_key, buf, buf_len)) { fprintf(stderr, "failed to parse public key %s\n", encoded_public_key); return false; } return true; } int genkey(int argc, char **argv) { UNUSED(argc); UNUSED(argv); uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH] = {0}; if (glome_generate_key(private_key, NULL)) { fprintf(stderr, "unable to generate a new key\n"); return EXIT_FAILURE; } if (fwrite(private_key, 1, sizeof private_key, stdout) != sizeof private_key) { perror("unable to write the private key to stdout"); return EXIT_FAILURE; } return EXIT_SUCCESS; } int pubkey(int argc, char **argv) { UNUSED(argc); UNUSED(argv); uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH] = {0}; uint8_t public_key[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; char encoded_public_key[ENCODED_BUFSIZE(GLOME_MAX_PUBLIC_KEY_LENGTH)] = {0}; if (fread(private_key, 1, sizeof private_key, stdin) != sizeof private_key) { perror("unable to read the private key from stdin"); return EXIT_FAILURE; } if (glome_derive_key(private_key, public_key)) { fprintf(stderr, "unable to generate a new key\n"); return EXIT_FAILURE; } if (!base64url_encode(public_key, sizeof public_key, (uint8_t *)encoded_public_key, sizeof encoded_public_key)) { fputs("unable to encode public key\n", stderr); return EXIT_FAILURE; } if (printf("%s %s\n", GLOME_LOGIN_PUBLIC_KEY_ID, encoded_public_key) < 0) { perror("unable to write the public key to stdout"); return EXIT_FAILURE; } return EXIT_SUCCESS; } // tag_impl reads a private key and a peer key from the given files and computes // a tag corresponding to a message read from stdin for the communication // direction determined by verify. int tag_impl(uint8_t tag[GLOME_MAX_TAG_LENGTH], bool verify, const char *key_file, const char *peer_file) { uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH] = {0}; uint8_t peer_key[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; char message[GLOME_CLI_MAX_MESSAGE_LENGTH] = {0}; if (!read_file(key_file, private_key, sizeof private_key) || !read_public_key_file(peer_file, peer_key, sizeof(peer_key))) { return EXIT_FAILURE; } size_t msg_len = fread(message, 1, GLOME_CLI_MAX_MESSAGE_LENGTH, stdin); if (!feof(stdin)) { fprintf(stderr, "message exceeds maximum supported size of %u\n", GLOME_CLI_MAX_MESSAGE_LENGTH); return EXIT_FAILURE; } if (glome_tag(verify, counter, private_key, peer_key, (uint8_t *)message, msg_len, tag)) { fputs("MAC tag generation failed\n", stderr); return EXIT_FAILURE; } return EXIT_SUCCESS; } int tag(int argc, char **argv) { uint8_t tag[GLOME_MAX_TAG_LENGTH] = {0}; if (!parse_args(argc, argv)) { return EXIT_FAILURE; } if (!key_file || !peer_file) { fprintf(stderr, "not enough arguments for subcommand %s\n", argv[1]); return EXIT_FAILURE; } int res = tag_impl(tag, /*verify=*/false, key_file, peer_file); if (res) { return res; } char tag_encoded[ENCODED_BUFSIZE(sizeof tag)] = {0}; if (base64url_encode(tag, sizeof tag, (uint8_t *)tag_encoded, sizeof tag_encoded) == 0) { fprintf(stderr, "GLOME tag encode failed\n"); return EXIT_FAILURE; } puts(tag_encoded); return EXIT_SUCCESS; } int verify(int argc, char **argv) { uint8_t tag[GLOME_MAX_TAG_LENGTH] = {0}; uint8_t *expected_tag = NULL; int ret = EXIT_FAILURE; if (!parse_args(argc, argv)) { goto out; } if (!key_file || !peer_file || !tag_b64) { fprintf(stderr, "not enough arguments for subcommand %s\n", argv[1]); goto out; } int res = tag_impl(tag, /*verify=*/true, key_file, peer_file); if (res) { goto out; } // decode the tag size_t tag_b64_len = strlen(tag_b64); size_t tag_b64_decoded_len = DECODED_BUFSIZE(tag_b64_len); expected_tag = malloc(tag_b64_decoded_len); if (expected_tag == NULL) { fprintf(stderr, "GLOME tag malloc %ld bytes failed\n", tag_b64_decoded_len); goto out; } size_t expected_tag_len = base64url_decode((uint8_t *)tag_b64, tag_b64_len, (uint8_t *)expected_tag, tag_b64_decoded_len); if (expected_tag_len == 0) { fprintf(stderr, "GLOME tag decode failed\n"); goto out; } if (expected_tag_len > sizeof tag) { expected_tag_len = sizeof tag; } // compare the tag if (CRYPTO_memcmp(expected_tag, tag, expected_tag_len) != 0) { fputs("MAC tag verification failed\n", stderr); goto out; } ret = EXIT_SUCCESS; out: free(expected_tag); return ret; } 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] != '2' || path[2] != '/') { fprintf(stderr, "unexpected challenge prefix: %s\n", path); return false; } if (path[path_len - 1] != '/') { fprintf(stderr, "unexpected challenge suffix: %s\n", path); return false; } 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); return false; } *handshake = strndup(start, slash - start); if (*handshake == NULL) { fprintf(stderr, "failed to duplicate handshake\n"); return false; } // Everything left (not including the trailing slash) is the message. start = slash + 1; *message = strndup(start, path + path_len - 1 - start); if (*message == NULL) { free(*handshake); *handshake = NULL; fprintf(stderr, "failed to duplicate message\n"); return false; } return true; } int login(int argc, char **argv) { char *handshake = NULL; char *handshake_b64 = NULL; char *message = NULL; int ret = EXIT_FAILURE; if (!parse_args(argc, argv)) { return EXIT_FAILURE; } char *cmd = argv[optind++]; if (optind >= argc) { fprintf(stderr, "missing challenge for subcommand %s\n", cmd); return EXIT_FAILURE; } if (!key_file) { fprintf(stderr, "not enough arguments for subcommand %s\n", cmd); return EXIT_FAILURE; } uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH] = {0}; if (!read_file(key_file, private_key, sizeof private_key)) { return EXIT_FAILURE; } 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)); if (handshake == NULL) { fprintf(stderr, "failed to malloc %ld bytes for base64 decode\n", DECODED_BUFSIZE(handshake_b64_len)); goto out; } int handshake_len = base64url_decode((uint8_t *)handshake_b64, handshake_b64_len, (uint8_t *)handshake, DECODED_BUFSIZE(handshake_b64_len)); if (handshake_len == 0) { fprintf(stderr, "failed to decode handshake in path %s\n", path); goto out; } if (handshake_len < 1 + GLOME_MAX_PUBLIC_KEY_LENGTH || handshake_len > 1 + GLOME_MAX_PUBLIC_KEY_LENGTH + GLOME_MAX_TAG_LENGTH) { fprintf(stderr, "handshake size is invalid in path %s\n", path); goto out; } if ((handshake[0] & 0x80) == 0) { uint8_t public_key[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; if (glome_derive_key(private_key, public_key)) { fprintf(stderr, "unable to generate a public key\n"); goto out; } // Most significant bit is not set for X25519 key (see RFC 7748). uint8_t public_key_msb = public_key[GLOME_MAX_PUBLIC_KEY_LENGTH - 1]; if (handshake[0] != public_key_msb) { fprintf(stderr, "unexpected public key prefix\n"); goto out; } } uint8_t peer_key[GLOME_MAX_PRIVATE_KEY_LENGTH] = {0}; memcpy(peer_key, handshake + 1, GLOME_MAX_PUBLIC_KEY_LENGTH); uint8_t tag[GLOME_MAX_TAG_LENGTH] = {0}; if (glome_tag(true, 0, private_key, peer_key, (uint8_t *)message, strlen(message), tag)) { fprintf(stderr, "MAC authcode generation failed\n"); goto out; } if (CRYPTO_memcmp(handshake + 1 + GLOME_MAX_PUBLIC_KEY_LENGTH, tag, handshake_len - 1 - GLOME_MAX_PUBLIC_KEY_LENGTH) != 0) { fprintf( stderr, "The challenge includes a message tag prefix which does not match the " "message\n"); goto out; } if (glome_tag(false, 0, private_key, peer_key, (uint8_t *)message, strlen(message), tag)) { fprintf(stderr, "GLOME tag generation failed\n"); goto out; } char tag_encoded[ENCODED_BUFSIZE(sizeof tag)] = {0}; if (base64url_encode(tag, sizeof tag, (uint8_t *)tag_encoded, sizeof tag_encoded) == 0) { fprintf(stderr, "GLOME tag encode failed\n"); goto out; } puts(tag_encoded); ret = EXIT_SUCCESS; out: free(handshake); free(handshake_b64); free(message); return ret; } glome-0.2/cli/commands.h000066400000000000000000000021611476056666600152250ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #ifndef CLI_COMMANDS_H_ #define CLI_COMMANDS_H_ // Generates a new key and writes it to stdout. int genkey(int argc, char **argv); // Reads a private key from stdin and writes the corresponding public key to // stdout. int pubkey(int argc, char **argv); // Tags a message and writes it to stdout. int tag(int argc, char **argv); // Returns 0 iff the tag could be verified. int verify(int argc, char **argv); // Generates a tag for a glome-login challenge and writes it to stdout. int login(int argc, char **argv); #endif // CLI_COMMANDS_H_ glome-0.2/cli/main.c000066400000000000000000000050411476056666600143430ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include #include #include #include #include #include #include #include #include #include "commands.h" #include "glome.h" #define GLOME_CLI_MAX_MESSAGE_LENGTH 4095 #define UNUSED(var) (void)(var) static const char *kUsage = "Usage: \n" " To generate a new keypair\n" " umask 077\n" " %s genkey | tee PRIVATE-KEY-FILE | %s pubkey >PUBLIC-KEY-FILE\n\n" " To generate a tag:\n" " %s tag --key PRIVATE-KEY-FILE --peer PEER-KEY-FILE " "[--counter COUNTER] name && strcmp(c->name, argv[1])) { c++; } return c->run(argc, argv); } glome-0.2/cli/meson.build000066400000000000000000000017351476056666600154230ustar00rootroot00000000000000# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https:#www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. openssl_dep = dependency('openssl', version : '>=1.1') glome_cli = executable( 'glome', [ 'main.c', 'commands.h', 'commands.c', ], dependencies: openssl_dep, link_with : [glome_lib, login_lib], include_directories : glome_incdir, install : true) if get_option('tests') cli_test = find_program('test.sh') test('cli', cli_test, args : glome_cli, timeout : 120) endif glome-0.2/cli/test.sh000077500000000000000000000060171476056666600145750ustar00rootroot00000000000000#!/bin/sh # Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -eu binary="$(dirname "$1")/$(basename "$1")" if ! test -x "$binary"; then echo "ERROR: $binary is not an executable" >&2 exit 1 fi t=$(mktemp -d) cleanup() { # shellcheck disable=SC2317 rm -rf -- "${t?}" } trap cleanup EXIT # Populate directory with keys according to specification. mkdir -p "$t/vector-1" printf '\167\007\155\012\163\030\245\175\074\026\301\162\121\262\146\105\337'\ '\114\057\207\353\300\231\052\261\167\373\245\035\271\054\052' >"$t/vector-1/a" printf '\135\253\010\176\142\112\212\113\171\341\177\213\203\200\016\346\157'\ '\073\261\051\046\030\266\375\034\057\213\047\377\210\340\353' >"$t/vector-1/b" printf "The quick brown fox" >"$t/vector-1/msg" printf "0" >"$t/vector-1/n" printf "nEQ4n0YtNdBnL69zpeEY-Ln1w0C76NNA4rlHwgXqT6M=" >"$t/vector-1/tag" mkdir -p "$t/vector-2" printf '\261\005\360\015\261\005\360\015\261\005\360\015\261\005\360\015\261'\ '\005\360\015\261\005\360\015\261\005\360\015\261\005\360\015' >"$t/vector-2/a" printf '\376\341\336\255\376\341\336\255\376\341\336\255\376\341\336\255\376'\ '\341\336\255\376\341\336\255\376\341\336\255\376\341\336\255' >"$t/vector-2/b" printf "The quick brown fox" >"$t/vector-2/msg" printf "100" >"$t/vector-2/n" printf "BkdvHzFLBsf5bl3GKyMIJoy9thQK7-61WUBzGGMDInc=" >"$t/vector-2/tag" errors=0 for n in 1 2; do testdir="$t/vector-$n" counter=$(cat "$testdir/n") expected_tag="$(cat "$testdir/tag")" for x in a b; do "$binary" pubkey <"$testdir/$x" >"$testdir/$x.pub" done tag=$("$binary" tag --key "$testdir/a" --peer "$testdir/b.pub" --counter "$counter" <"$testdir/msg") if [ "$tag" != "${expected_tag}" ]; then echo "Generated wrong tag for test vector $n" >&2 echo "${expected_tag} <- expected" >&2 echo "$tag <- actual" >&2 errors=$((errors + 1)) fi if ! "$binary" verify -k "$testdir/b" -p "$testdir/a.pub" -c "$counter" -t "$tag" <"$testdir/msg"; then echo "Failed to verify test vector $n" >&2 errors=$((errors + 1)) fi done key="$t/vector-2/a" expected_tag="ZmxczN4x3g4goXu-A2AuuEEVftgS6xM-6gYj-dRrlis=" for path in \ "v2/R4cvQ1u4uJ0OOtYqouURB07hleHDnvaogAFBi-ZW48N2/myhost/exec=%2Fbin%2Fsh/" \ "v2/x4cvQ1u4uJ0OOtYqouURB07hleHDnvaogAFBi-ZW48N2/myhost/exec=%2Fbin%2Fsh/" do 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 done exit "$errors" glome-0.2/docs/000077500000000000000000000000001476056666600134345ustar00rootroot00000000000000glome-0.2/docs/contributing.md000066400000000000000000000021111476056666600164600ustar00rootroot00000000000000# How to Contribute We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project. Head over to to see your current agreements on file or to sign a new one. You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. ## Code reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. ## Community Guidelines This project follows [Google's Open Source Community Guidelines](https://opensource.google/conduct/). glome-0.2/docs/glome-login.md000066400000000000000000000173361476056666600162010ustar00rootroot00000000000000# GLOME login protocol ## Introduction GLOME login is first application of the [GLOME protocol](protocol.md). It is used to authorize serial console access to Linux machines. To achieve that, a client program called `glome-login` is executed by getty (or a similar process) instead of the conventional `/sbin/login`. Instead of prompting the user for the password, it generates an URL that points at the authorization server and contains the GLOME handshake information and the action requested by the operator. The operator follows that URL and upon successful authentication and authorization, the server provides the operator with an authorization code response that needs to be returned to `glome-login`. If the authorization code matches the one calculated internally by `glome-login`, the user is authorized and glome-login executes the requested action - e.g. providing the login shell or rebooting the machine. This document describes version 2 of GLOME Login, see also [RFD001](rfd/001.md) for a rationale of the protocol elements. ## Implementation The current version of the GLOME login protocol uses the [standard GLOME variant](protocol.md#variants). Counters are set to constant `0` since only a single set of messages is exchanged. The protocol assumes that the client (i.e., the machine being accessed), knows the public key of the server. Required elements of the protocol are: * A host identifier that uniquely identifies the client. * An action on the client that needs to be authorized by the server. These optional elements can provide additional information to the server: * A host identifier type, among which the host identifier is unique. * A server key index, to tell the server which private key to use. * A message tag prefix, to allow error detection on the server side. The client combines these elements into a challenge string, which the server validates and responds to with a GLOME tag. GLOME Login challenges are suitable for embedding into a URL. ### Challenge request format The GLOME login client generates the challenge in the form: ```abnf challenge = "v2/" handshake-segment "/" message "/" handshake-segment = Base64_urlsafe( prefix client-public-key [message-tag-prefix] ) message = host-segment "/" action-segment host-segment = EscapePathSegment( [hostid-type ":"] hostid ) action-segment = EscapePathSegment(action) ``` The individual elements of this specification and the encoding functions are described in the subsections below. #### Challenge Transport Considerations The client should then output the resulting challenge prefixed by the configured prompt. In practice, that configurable prefix can be used to present the challenge as an URL which can be used to submit the challenge to a GLOME serve. The challenge must always end in a `/` to make it easy for the GLOME login server to detect truncated requests and reject those early. Without the trailing slash requirement the request will still likely look correct but may result in an invalid request being signed causing confusion for the operator. #### Host ID The client identifies itself as a named host, using the `hostid` field. This ID often is a fully qualified domain name, so adhering to domain name restrictions when choosing host ids is a good idea. However, these restrictions are not enforced by this protocol, but the host id should not need to be encoded for inclusion as a URL path segment, and it should not include a `:` character, as that is used to separate type and id. Providing a host id type is optional, but can help with the interpretation of the host id itself. It is subject to the same encoding considerations as the id itself. If no host id type is provided, host ids should be interpreted as host names. #### Action The `` field represents the action being authorized and should not be ambiguous in a way that affects security. The format of the action is left up to the implementer to decide but it has to take into account these points: * The `` should be suitable for embedding in a URL path element (see also the section on encodings below). * The `` should be human readable and easy to understand both as part of the URL and stand alone. Good examples: * `shell=root` starts a shell as the given user, root in this case. * `reboot` reboots the target. * `show-logs=httpd` outputs debug logs for the `httpd` application. Bad examples: * `exec` executes a command. * This is bad because it does not specify which command is being executed. * `exec=cm0gLWZyIC8=` executes a given command (Base64 encoded). * This is not human readable. * `shell` starts a shell as an user-provided but undisclosed user. * This is bad if there exists ambiguity on which user the shell will launch as. E.g. if the system is hard-coded to only allow login as root, this example is OK - otherwise not. * `shell/root` * This used to be the recommended format in v1, but it creates ambiguity between the host part and the action part and will thus be percent-encoded, which harms legibility. #### Handshake The prefix is one byte, of which the most significant bit disambiguates the use of the low 7 bit. If the MSB is set, the low bits are interpreted as a 7 bit integer, which the server should interpret as the index of the key its supposed to use. If the MSB is not set, the entire byte represents the most significant byte of the public key that the server is supposed to use. The public key corresponding to the client's ephemeral key for this challenge is appended as raw 32 bytes, in the encoding specified in RFC 7748. The message tag prefix is calculated by the client as the MAC tag over the `` field. The client can choose to include as much of the tag as it prefers. The server can verify the integrity of the message doing the same calculation and performing a prefix comparison of the expected tag and the received message tag prefix. The message tag prefix does not offer any additional security properties unless the server enforces its inclusion. However, the message tag prefix is still useful to detect accidental message corruption. It can also be used to resolve ambiguity in which service key was used by the client. For an efficient base64-encoding, the raw message tag prefix should have a length divisible by 3. #### Encodings In order to safely embed the handshake and message in a URL, the individual protocol elements need to be encoded. The handshake is encoded using URL-safe Base64, as specified in . The message consists of two path elements, which are encoded individually, using the percent-encoding scheme specified in , and then joined by a `/` character. ### Response format The response is a Base64 URL-safe (base64url) MAC tag computed over the `` field as provided by the client. The GLOME login client can accept a shortened tag (prefix) to reduce the message cost. Ephemeral keys are valid only for one attempt, thus the brute forcing is severely limited, and can be further slowed down by introducing an artificial delay before comparing the tags. ### Test vectors Test vectors that conform to this specification are defined in [login-v2-test-vectors.yaml](login-v2-test-vectors.yaml). They describe two parties, Alice and Bob, who run through a GLOME Login challenge-response workflow. In these scenarios, Alice is always the client and Bob the server. ## Alternatives The GLOME protocol is based on the assumption that the cost of transmitting messages in the server-to-client direction is higher than in the opposite direction. If that is not the case, then using an existing proven signature scheme (e.g, [Ed25519](https://en.wikipedia.org/wiki/EdDSA#Ed25519)) is recommended. glome-0.2/docs/login-v2-test-vectors.yaml000066400000000000000000000034171476056666600204220ustar00rootroot00000000000000- vector: 1 alice: private-key: hex: 77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a base64: dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo= public-key: hex: 8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a glome-cli: "glome-v1 hSDwCYkwp1R0i33ctD73Wg2_Og0mOBr066SpjqqbTmo=" bob: private-key: hex: 5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb base64: XasIfmJKikt54X+Lg4AO5m87sSkmGLb9HC+LJ/+I4Os= public-key: hex: de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f glome-cli: "glome-v1 3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08=" prefix-length: 3 service-key-id: 0 host-id-type: mytype host-id: myhost action: root message: "v2/gIUg8AmJMKdUdIt93LQ-91oNvzoNJjga9OukqY6qm05qlyPH/mytype:myhost/root/" tag: BB4BYjXonlIRtXZORkQ5bF5xTZwW6o60ylqfCuyAHTQ= - vector: 2 alice: private-key: hex: fee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1dead base64: /uHerf7h3q3+4d6t/uHerf7h3q3+4d6t/uHerf7h3q0= public-key: hex: 872f435bb8b89d0e3ad62aa2e511074ee195e1c39ef6a88001418be656e3c376 glome-cli: "glome-v1 hy9DW7i4nQ461iqi5REHTuGV4cOe9qiAAUGL5lbjw3Y=" bob: private-key: hex: b105f00db105f00db105f00db105f00db105f00db105f00db105f00db105f00d base64: sQXwDbEF8A2xBfANsQXwDbEF8A2xBfANsQXwDbEF8A0= public-key: hex: d1b6941bba120bcd131f335da15778d9c68dadd398ae61cf8e7d94484ee65647 glome-cli: "glome-v1 0baUG7oSC80THzNdoVd42caNrdOYrmHPjn2USE7mVkc=" prefix-length: 0 service-key-id: null host-id-type: "" host-id: myhost action: "exec=/bin/sh" message: "v2/R4cvQ1u4uJ0OOtYqouURB07hleHDnvaogAFBi-ZW48N2/myhost/exec=%2Fbin%2Fsh/" tag: ZmxczN4x3g4goXu-A2AuuEEVftgS6xM-6gYj-dRrlis= glome-0.2/docs/protocol.md000066400000000000000000000256311476056666600156260ustar00rootroot00000000000000# GLOME Protocol Generic Low-Overhead Message Exchange > :information_source: **NOTE**: GLOME provides a solution to a fairly niche > problem. If the following constraints don't apply in your case, you might be > better off using established signature schemes (e.g. > [EdDSA](https://en.wikipedia.org/wiki/EdDSA)). ## Introduction GLOME combines ephemeral-static key exchange (e.g. [X25519](https://en.wikipedia.org/wiki/Curve25519)) between two parties and uses that to enable exchanging authenticated and integrity-protected messages using a truncated tag (e.g. truncated [HMAC](https://en.wikipedia.org/wiki/HMAC)). Ephemeral-static key exchange indicates that only one side can authenticate itself through the key agreement, and in case of GLOME it is the server side. Clients are not automatically authenticated since they are using ephemeral keys. The protocol is designed to keep its overhead to minimum, assuming that sending a message is expensive, and allows the parties to trade some security for reduced overhead by operating on truncated HMAC tags. ## Real world applications An example of a real-world scenario fitting the description above is authorizing a human operator to access a device with the following constraints: * The device does not have a network connectivity (e.g. due to a failure or by design). * The device does not have a synchronized time (e.g. no real-time clock). * The device does not store any secrets (e.g. all its storage is easily readable by an adversary). * The device has access to a [cryptographically secure pseudorandom number generator](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator) (e.g. a hardware-based random number generator). * The device accepts input from a human operator via a very low-bandwidth device (e.g. a keyboard). * The device provides output to a human operator (e.g. via display). With the constraints above, the operator effectively provides a low-bandwidth channel for the device and the authorization server to communicate by passing the messages back and forth. While there are ways to increase the bandwidth from the device to the operator (e.g. via [matrix codes](https://en.wikipedia.org/wiki/Barcode#Matrix_\(2D\)_barcodes)), we must assume that the opposite direction requires the operator to type the message manually on the keyboard, so minimizing the protocol overhead in that direction is crucial. To address this problem, the [GLOME login protocol](glome-login.md) based on GLOME was invented. ## Caveats * GLOME does not protect confidentiality of exchanged messages. This is not a technical limitation (given that the protocol already performs a key exchange) but avoiding introducing unnecessary complexity. This decision can be revised in future revisions of this protocol, once there is a compelling use case to provide this. * The server is unable to authenticate the client just using GLOME due to the usage of ephemeral keys. A protocol built on top of GLOME should implement its own client authentication (if necessary). ## Protocol details Alice and Bob want to exchange messages over an expensive untrusted channel, i.e.: * The channel can be actively MITM-ed. * Cost-per-byte and cost-per-message are relatively high. * The cost function can be asymmetrical, i.e., the cost can be higher in one direction. Alice and Bob can choose to lower the cost (i.e., the overhead) by accepting weaker security. Alice knows Bob's public key. The protocol consists of an ephemeral-static Diffie-Hellman key exchange, and uses the established shared secret to calculate MAC over combined payloads. Alice wants to send a payload $M_a$ to Bob. Alice knows Bob's public key $K_b$. ### Handshake The handshake derives two MAC keys, one for each direction of communication, from a shared secret that is established using a Diffie-Hellman key exchange. Key derivation operations are only described in brief. For full reference, see [RFC 7748 Section 6.1](https://tools.ietf.org/html/rfc7748#section-6). #### Alice 1. Alice generates an ephemeral private key $K_a'$. 1. Alice computes the corresponding public key $K_a$ from $K_a'$. 1. Alice uses $K_a'$ and Bob's public key $K_b$ to derive the shared secret $K_s$. 1. Alice uses $K_a$, $K_b$ and $K_s$ to construct MAC keys: 1. For messages from Alice to Bob: $K_{ab} = K_s ⧺ K_b ⧺ K_a$ 1. For messages from Bob to Alice: $K_{ba} = K_s ⧺ K_a ⧺ K_b$ 1. At this point Alice can forget $K_a'$ and $K_s$ so they cannot be accidentally reused. 1. Alice sends $K_a$ and indicates which $K_b$ was used to Bob. #### Bob 1. Bob receives $K_a$ and an indication of which $K_b$ to be used. 1. Bob uses the corresponding private key $K_b'$ and $K_a$ to derive the shared secret $K_s$. 1. Bob computes the MAC keys $K_{ab}$ and $K_{ba}$ in the same way as Alice did. ### Exchanging messages To prevent replay attacks, Alice and Bob need to maintain a pair of counters: $N_{ab}$ and $N_{ba}$. Each zero-indexed counter represents the number of messages sent in a given direction. Once the handshake is complete, Alice and Bob can send messages $M_n$ to each other by computing a tag $T$ over $N_x ⧺ M_n$ using key $K_x$ and incrementing $N_x$. $x$ is either $ab$ or $ba$, depending on the direction of the message. Upon receiving a message, the other party verifies its authenticity by repeating the tag calculation and comparing the result (in constant-time) with the received tag. ### Variants There is currently only one variant of the protocol defined. This variant uses: * Curve25519 keys ($K_a$, $K_a'$, $K_b$, $K_b'$). * X25519 to derive the shared secret $K_s$. * HMAC-SHA256 to calculate the message tag. * Unsigned 8-bit counters (0..255). While the use of 8-bit counters limits the number of messages exchanged between the parties, it is likely to be sufficient given the constraints that warrant the usage of the protocol. ### Optional optimizations * To reduce the overhead at the cost of security, parties can truncate the exchanged tags and compare only prefixes of an acceptable length. * To reduce the number of messages exchanged, Alice can combine the initial handshake with sending the first message. * Sending the tag in the first message sent from Alice to Bob is not security-relevant since it does not authenticate the message as Alice uses ephemeral keys. It might be useful to detect accidental errors and for Bob to disambiguate between his multiple key pairs (more on that below). * The indication of Bob's public key ($K_b$) can be done in different ways, each leading to varying degrees of communication overhead: 1. Specifying a truncated version of Bob's public key. * The truncation can cause ambiguity if it matches multiple of Bob's keys. 1. Specifying a key identifier, e.g. the key's serial number. * Requires pre-agreeing to key identifiers between both parties. 1. By including an (optionally truncated) tag over the message sent together with the handshake. * This can cause ambiguity, if Bob discovers that multiple key pairs produce the same (truncated) tag. 1. If Bob has only one key, there is no need to indicate which one is being used. * Not recommended, as this makes any key rotation difficult. ### Future improvements * Given that the protocol already establishes a shared secret between Alice and Bob, it could be used to encrypt the exchanged messages. We decided not to add it at this point to keep the protocol simpler. * The protocol could be extended to support multi-party settings (i.e., a client exchanging messages with multiple servers at the same time). ### Test vectors These are some example test cases that can be used to verify an implementation of the GLOME protocol. Octet strings (keys and tags) are represented in hexadecimal encoding, message counters in their decimal represenation and messages in ASCII encoding. #### Vector 1 Message from Alice to Bob. | Variable | Value | |---------:|:-------------------------------------------------------------------| | $K_a'$ | `77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a` | | $K_b'$ | `5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb` | | $N_{ab}$ | `0` | | $M_n$ | `The quick brown fox` | | | | | $K_a$ | `8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a` | | $K_b$ | `de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f` | | $K_s$ | `4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742` | | | | | $T$ | `9c44389f462d35d0672faf73a5e118f8b9f5c340bbe8d340e2b947c205ea4fa3` | #### Vector 2 Message from Bob to Alice. | Variable | Value | |---------:|:-------------------------------------------------------------------| | $K_a'$ | `fee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1dead` | | $K_b'$ | `b105f00db105f00db105f00db105f00db105f00db105f00db105f00db105f00d` | | $N_{ba}$ | `100` | | $M_n$ | `The quick brown fox` | | | | | $K_a$ | `872f435bb8b89d0e3ad62aa2e511074ee195e1c39ef6a88001418be656e3c376` | | $K_b$ | `d1b6941bba120bcd131f335da15778d9c68dadd398ae61cf8e7d94484ee65647` | | $K_s$ | `4b1ee05fcd2ae53ebe4c9ec94915cb057109389a2aa415f26986bddebf379d67` | | | | | $T$ | `06476f1f314b06c7f96e5dc62b2308268cbdb6140aefeeb55940731863032277` | ### Reference implementation The reference implementation consists of a glome binary that implements the following operations. #### Key pair generation ``` $ glome keygen ``` If `` does not exist, the private key is generated and written to ``. Otherwise it reads the secret key from ``. The tool prints out the corresponding public key to stdout (hex-encoded). #### HMAC tag computation ``` $ glome tag [ []] ``` Prints the hex-encoded tag over `` (defaults to empty) with the counter set to `` (defaults to 0). #### HMAC tag verification ``` $ glome verify [ []] ``` Verifies that the provided tag matches the expected tag over message `` with the counter set to `` as produced by peer using ``. The tool exits with 0 on success, 1 on failure (tag mismatch). glome-0.2/docs/rfd/000077500000000000000000000000001476056666600142075ustar00rootroot00000000000000glome-0.2/docs/rfd/001.md000066400000000000000000000163361476056666600150420ustar00rootroot00000000000000--- authors: Markus Rudy (@burgerdev) state: committed --- # RFD 001: GLOME Login v2 ## Objective Make the GLOME Login Protocol unambiguous. ## Background See also [google/glome#62](https://github.com/google/glome/issues/62). - The ambiguous interpretation of `prefix7` may lead to a change of server authorization behaviour that cannot be controlled by the client (e.g. a new key is added to the server whose index conflicts with a public key prefix of an existing one). - It's currently legal to have colon (`:`) and slash (`/`) characters in all message fields, which may cause ambiguity in parsing and, ultimately, lead to authorization of unintended messages. - The protocol gives advice to "maximize the human readability of the URL", which conflicts with an unambiguous presentation of said characters in percent-encoded form. ## Requirements - There must be a well-defined interpretation of the GLOME Login handshake that does not depend on the public keys a server holds. - There must be a well-defined, bijective conversion from the message embedded in a GLOME Login URL to the message being authorized. - Subject to the preceding requirements, the URL layout should be optimized for human readability (e.g. don't encode [unreserved characters](https://www.rfc-editor.org/rfc/rfc3986#section-2.3)) and brevity. - Assuming humans will have to read the message to be authorized much more often than parse the involved keys. ## Design ideas - The `prefix-type` bit determines interpretation of `prefix7`: * 0: `prefix7` is matched with the high byte of the server's public key * 1: `prefix7` is an index into the server's public keys. - The GLOME Login challenge is a URI path. - Completely specify the encoding and decoding of the message part. - Include detailed instructions for server and client into the protocol. - Publish the result as GLOME Login v2, as it is incompatible with v1 URLs. ### `prefix7` The most significant bit of a 256bit X25519 public key should not be interpreted by the Diffie-Hellman key exchange [RFC7748]. We use this fact to define a `prefix7` config that is somewhat self-configuring: `prefix-type` is 0, `prefix7` is the high byte of the server's public key, and thus the 8bit prefix is, too. Alternatively, if indices are to be used, `prefix-type` is 1 and `prefix7` is the index between 0 and 127, inclusive. Note that this is a large amount of public keys, even in case of automatic rotation - if this is a concern, `prefixN` can be used to verify (or pick) the public key on the server side. Note that this is incompatible with _all_ subsets of v1: indices need to be taken `mod 128`, and public key prefixes are now taken from the MSB, not the LSB. [RFC7748]: https://www.rfc-editor.org/rfc/rfc7748#section-5 ### URI Prior versions of GLOME assumed that the challenge would always be rendered as a URL. This is not true in many cases: for example, a URL challenge does not make too much sense for a response generated with the `glome` cli. On the other hand, presenting the challenge as a URL works reasonably well in practice, so we don't want to change the challenge format in an incompatible way. Thus, a challenge in v2 is what used to be the URL path in v1. ```abnf challenge = "v2/" handshake-segment "/" message "/" ``` A URI path can still be prefixed with scheme and host to build a URL. Subsequent sections describe how a challenge is encoded to a valid URI path and how to compute the tag over that encoding. ### Message New restrictions to make message encoding unambiguous: - `hostid-type` and `hostid` must not contain the `:` character. - `hostid-type`, `hostid` and `action` should not contain any characters that would be escaped in a URI path segment (as detailed below). Differing from previous protocol versions, `/` is discouraged. #### URI Path Segments The URI specification [RFC 3986](https://www.ietf.org/rfc/rfc3986.html#section-3.3) defines a path segment as ```abnf segment = *pchar pchar = unreserved / pct-encoded / sub-delims / ":" / "@" unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" pct-encoded = "%" HEXDIG HEXDIG ``` where HEXDIG should refer to a digit or an uppercase letter A-F. This matches the definition in , which supposedly supersedes the RFC. Thus, define `EscapePathSegment` as a function that escapes all characters that are not unreserved, sub delimiters, `:` or `@`. See the Appendix for how this function can be implemented in some of the major programming languages. #### Message Encoding Constructing a message takes three parameters: `hostid`, `action` and (optionally) `hostid-type`. These are encoded into a `message` - a URI (sub)path - using `EscapePathSegment` as follows. ```abnf message = host-segment "/" action-segment host-segment = EscapePathSegment( [hostid-type ":"] hostid ) action-segment = EscapePathSegment(action) ``` The `hostid-type` prefix is added if and only if the `hostid-type` of the message is not empty. Note that this voids some of the existing recommendations for 'good' actions: `shell/root`, for example, would have to be escaped and thus be less readable. Instead, using URI sub-delimiters as in `shell=root` should be recommended. This format would interact nicely with a host-identity-based authorization scheme working with key-value pairs. #### Message Decoding Given a URI path, strip the path prefix up to including the `/` after the handshake message. Split the remaining string on the character `/` and keep only the first and second element, denoted `host-segment` and `action-segment`; or fail if there are less than two elements. Replace all percent-encoded octets in the `host-segment` with their raw, unencoded form. Split the result at the character `:`. If there is one element, assign that element to `hostid` and assign the literal string `hostname` to `hostid-type`; if there are two elements assign the first one to `hostid-type` and the second to `hostid`; if there are more than two elements, fail. Replace all percent-encoded octets in the `action-segment` with their raw, unencoded form, and assign the result to `action`. #### Message Tagging The tag for a message is produced by passing the **encoded message** string into `glome_tag`. ## Alternatives considered ### Allow unescaped slashes in action - Allow an action to span more than one path segment. - This prevents us from having an unambiguous encoding: `xxx/yyy%2Fzzz` vs. `xxx/yyy/zzz`. ### Calculate the tag on the unescaped message - Tag the message before URL escaping. - This would have the benefit of decoupling the tagging from the transport (here, URL segments). - However, we need to encode the message into a byte array before we can tag it. This encoding must be unambiguous as well, simply concatenating the triple won't cut it. ## Appendix ### URI Path Escaping APIs #### Python ```python urllib.parse.quote(segment, safe=":@!$&'()*+,;=") ``` #### Golang :'-( #### C GLib: ```c g_uri_escape_string(segment, ":@!$&'()*+,;=", /*allow_utf8=*/false); ``` #### Java Guava: ```java com.google.common.net.UrlEscapers.urlPathSegmentEscaper().escape(segment) ``` #### OCaml Uri: ```ocaml Uri.pct_encode segment ``` glome-0.2/docs/rfd/002.md000066400000000000000000000043441476056666600150370ustar00rootroot00000000000000--- authors: Markus Rudy (@burgerdev) state: committed --- # Objective Define a format for GLOME public keys at rest. # Background The GLOME protocol definition does not deal with key material handling, and the reference implementation only implements a very rudimentary storage format - 32 raw octets. This causes a variety of problems, e.g. when transferring keys between hosts or when specifying server keys for *GLOME Login*. See also [google/glome#100](https://github.com/google/glome/issues/100). # Requirements * A GLOME public key at rest should be unambiguously identifiable as such. * Public keys should be printable. * Public keys should be easily exchanged over any medium, potentially analog. # Design ideas Public keys are stored in URL-safe base64 encoding and tagged with their protocol variant version. The configuration file format accepts keys in a format similar to [OpenSSH's `authorized_keys` format][1]. [1]: https://man.openbsd.org/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT ## Public Key Format The format of a _GLOME public key_ adheres to the ABNF below: ```abnf public-key = key-type SP key-base64 key-type = "glome-v1" key-base64 = 44urlsafe-base64-char urlsafe-base64-char = "=" / "-" / "_" / ALPHA / DIGIT ``` The key type encodes the GLOME variant this key should be used with. As we only have one variant right now, we're only defining one `key-type` here. An example public key, like it would be printed by `glome pubkey`: ``` glome-v1 lXmlq5jynG6um_w4D4N13TRIE-x7jt0TKVNDMSRS2_I= ``` ## Public Key Interpretation An implementation must verify that the `key-type` matches its expectations and must not produce a tag if it does not. If the `key-type` matches the expectations, the `key-base64` part is decoded as base64, and the resulting 32 octets are interpreted as the _raw GLOME public key_, suitable for use with `glome_tag`. ## Consequences for the GLOME Login Configuration Format The configuration file accepts a new `public-key` field in the `service` section. This field must contain a key as specified in this document. The `key` field is deprecated and will be removed for release 1.0, but will be supported until then. If both `public-key` and `key` are present in the config file, `public-key` will take precedence. glome-0.2/glome.c000066400000000000000000000113621476056666600137560ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "glome.h" #include #include #include #include #include #include #include #define X25519_SHARED_KEY_LEN 32 int glome_generate_key(uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH], uint8_t public_key[GLOME_MAX_PUBLIC_KEY_LENGTH]) { size_t public_key_len = GLOME_MAX_PUBLIC_KEY_LENGTH; size_t private_key_len = GLOME_MAX_PRIVATE_KEY_LENGTH; EVP_PKEY *pkey = NULL; EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); int err = (ctx == NULL || EVP_PKEY_keygen_init(ctx) != 1 || EVP_PKEY_keygen(ctx, &pkey) != 1 || EVP_PKEY_get_raw_public_key(pkey, public_key, &public_key_len) != 1 || public_key_len != GLOME_MAX_PUBLIC_KEY_LENGTH || EVP_PKEY_get_raw_private_key(pkey, private_key, &private_key_len) != 1 || private_key_len != GLOME_MAX_PRIVATE_KEY_LENGTH); EVP_PKEY_CTX_free(ctx); EVP_PKEY_free(pkey); return err; } int glome_derive_key(const uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH], uint8_t public_key[GLOME_MAX_PUBLIC_KEY_LENGTH]) { size_t public_key_length = GLOME_MAX_PUBLIC_KEY_LENGTH; EVP_PKEY *pkey = EVP_PKEY_new_raw_private_key( EVP_PKEY_X25519, NULL, private_key, GLOME_MAX_PRIVATE_KEY_LENGTH); int err = (pkey == NULL || EVP_PKEY_get_raw_public_key(pkey, public_key, &public_key_length) != 1 || public_key_length != GLOME_MAX_PUBLIC_KEY_LENGTH); EVP_PKEY_free(pkey); return err; } int glome_tag(bool verify, unsigned char counter, const uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH], const uint8_t peer_key[GLOME_MAX_PUBLIC_KEY_LENGTH], const uint8_t *message, size_t message_len, uint8_t tag[GLOME_MAX_TAG_LENGTH]) { uint8_t hmac_key[X25519_SHARED_KEY_LEN + 2 * GLOME_MAX_PUBLIC_KEY_LENGTH] = { 0}; uint8_t public_key[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; EVP_PKEY *evp_peer_key = EVP_PKEY_new_raw_public_key( EVP_PKEY_X25519, NULL, peer_key, GLOME_MAX_PUBLIC_KEY_LENGTH); EVP_PKEY *evp_private_key = EVP_PKEY_new_raw_private_key( EVP_PKEY_X25519, NULL, private_key, GLOME_MAX_PRIVATE_KEY_LENGTH); if (evp_private_key == NULL || evp_peer_key == NULL) { EVP_PKEY_free(evp_peer_key); EVP_PKEY_free(evp_private_key); return 1; } EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(evp_private_key, NULL); if (ctx == NULL) { EVP_PKEY_free(evp_peer_key); EVP_PKEY_free(evp_private_key); return 1; } // Derive public key. size_t public_key_length = GLOME_MAX_PUBLIC_KEY_LENGTH; int err = (EVP_PKEY_get_raw_public_key(evp_private_key, public_key, &public_key_length) != 1 || public_key_length != GLOME_MAX_PUBLIC_KEY_LENGTH); if (err) { EVP_PKEY_free(evp_peer_key); EVP_PKEY_free(evp_private_key); return 1; } // X25519 shared secret size_t shared_key_length = X25519_SHARED_KEY_LEN; err = (EVP_PKEY_derive_init(ctx) != 1 || EVP_PKEY_derive_set_peer(ctx, evp_peer_key) != 1 || EVP_PKEY_derive(ctx, hmac_key, &shared_key_length) != 1 || shared_key_length != X25519_SHARED_KEY_LEN); EVP_PKEY_CTX_free(ctx); EVP_PKEY_free(evp_peer_key); EVP_PKEY_free(evp_private_key); if (err) { return 1; } // hmac_key := (sharded_key | verifier_key | signer_key) memcpy(hmac_key + X25519_SHARED_KEY_LEN, (verify ? public_key : peer_key), GLOME_MAX_PUBLIC_KEY_LENGTH); memcpy(hmac_key + X25519_SHARED_KEY_LEN + GLOME_MAX_PUBLIC_KEY_LENGTH, (verify ? peer_key : public_key), GLOME_MAX_PUBLIC_KEY_LENGTH); // data := (counter | message) size_t data_len = message_len + sizeof counter; uint8_t *data = malloc(data_len); if (data == NULL) { return 1; } memcpy(data, &counter, sizeof counter); memcpy(data + sizeof counter, message, message_len); unsigned int tag_length = GLOME_MAX_TAG_LENGTH; int success = (HMAC(EVP_sha256(), hmac_key, sizeof hmac_key, data, data_len, tag, &tag_length) && tag_length == GLOME_MAX_TAG_LENGTH); free(data); return success ? 0 : 1; } glome-0.2/glome.h000066400000000000000000000033411476056666600137610ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #ifndef GLOME_H_ #define GLOME_H_ #include #include #include #define GLOME_MAX_PUBLIC_KEY_LENGTH 32 #define GLOME_MAX_PRIVATE_KEY_LENGTH 32 #define GLOME_MAX_TAG_LENGTH 32 #ifdef __cplusplus extern "C" { #endif // Generates a new public/private key pair for use with GLOME. int glome_generate_key(uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH], uint8_t public_key[GLOME_MAX_PUBLIC_KEY_LENGTH]); // Derives the public key from the private key. int glome_derive_key(const uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH], uint8_t public_key[GLOME_MAX_PUBLIC_KEY_LENGTH]); // Generates or verifies the GLOME tag for the message. Requires passing in the // private key of the local peer and the public key of the remote peer. int glome_tag(bool verify, unsigned char counter, const uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH], const uint8_t peer_key[GLOME_MAX_PUBLIC_KEY_LENGTH], const uint8_t *message, size_t message_len, uint8_t tag[GLOME_MAX_TAG_LENGTH]); #ifdef __cplusplus } // extern "C" #endif #endif // GLOME_H_ glome-0.2/glome_test.c000066400000000000000000000056031476056666600150160ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "glome.h" #include #include #include static void decode_hex(uint8_t *dst, const char *in) { size_t len = strlen(in); size_t i; for (i = 0; i < len / 2; i++) { sscanf(in + (i * 2), "%02hhX", dst + i); } } static void test_vector1(void) { uint8_t ka_priv[GLOME_MAX_PRIVATE_KEY_LENGTH] = {0}; uint8_t ka_pub[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; uint8_t kb_pub[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; uint8_t expected_tag[GLOME_MAX_TAG_LENGTH] = {0}; uint8_t tag[GLOME_MAX_TAG_LENGTH] = {0}; uint8_t counter = 0; const char *msg = "The quick brown fox"; decode_hex( ka_priv, "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"); decode_hex( kb_pub, "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"); decode_hex( expected_tag, "9c44389f462d35d0672faf73a5e118f8b9f5c340bbe8d340e2b947c205ea4fa3"); g_assert_true(glome_derive_key(ka_priv, ka_pub) == 0); g_assert_true(glome_tag(/* verify */ false, counter, ka_priv, kb_pub, (const uint8_t *)msg, strlen(msg), tag) == 0); g_assert_cmpmem(tag, sizeof tag, expected_tag, sizeof expected_tag); } static void test_vector2(void) { uint8_t ka_pub[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; uint8_t kb_priv[GLOME_MAX_PRIVATE_KEY_LENGTH] = {0}; uint8_t kb_pub[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; uint8_t expected_tag[GLOME_MAX_TAG_LENGTH] = {0}; uint8_t tag[GLOME_MAX_TAG_LENGTH] = {0}; uint8_t counter = 100; const char *msg = "The quick brown fox"; decode_hex( ka_pub, "872f435bb8b89d0e3ad62aa2e511074ee195e1c39ef6a88001418be656e3c376"); decode_hex( kb_priv, "b105f00db105f00db105f00db105f00db105f00db105f00db105f00db105f00d"); decode_hex( expected_tag, "06476f1f314b06c7f96e5dc62b2308268cbdb6140aefeeb55940731863032277"); g_assert_true(glome_derive_key(kb_priv, kb_pub) == 0); g_assert_true(glome_tag(/* verify */ false, counter, kb_priv, ka_pub, (const uint8_t *)msg, strlen(msg), tag) == 0); g_assert_cmpmem(tag, sizeof tag, expected_tag, sizeof expected_tag); } int main(int argc, char *argv[]) { g_test_init(&argc, &argv, NULL); g_test_add_func("/test-vector1", test_vector1); g_test_add_func("/test-vector2", test_vector2); return g_test_run(); } glome-0.2/go/000077500000000000000000000000001476056666600131115ustar00rootroot00000000000000glome-0.2/go/README.md000066400000000000000000000032331476056666600143710ustar00rootroot00000000000000# GLOME-Go **This is not an officially supported Google product.** This repository contains a Golang implementation for the GLOME protocol. You can find the library as well as the tests in the folder `glome`. ## Go API ### Note This API is Alpha. Thus, it might be subject to changes in the future. ### Example In order for Alice and Bob to communicate, the first step would be to generate some new keys: ```go import ( "glome" "crypto/rand" ) // Alice generates new random KeyPair alicePub, alicePriv, err := glome.GenerateKeys(rand.Reader) if err != nil { [...] } // Bob generates Private Key from an existing byte array b := [32]byte{0,2,...,7,6} bobPriv := glome.PrivateKey(b) // Bob could have as well generated the key from byte slice s := b[:] bobPriv, err := glome.PrivateKeyFromSlice(s) if err != nil { [...] } // Bob deduces public key bobPub, err := bobPriv.Public() if err != nil { [...] } ``` Suppose that Alice knows `bobPub` and wants to send Bob the message `msg` and no other message have been shared before. Alice will need to generate a `Dialog`: ```go d, err := alicePriv.Exchange(bobPub) if err != nil { [...] } firstTag := d.Tag(msg, 0) secondTag := d.Tag(msg, 1) ``` And Alice will send Bob both `msg`, `firstTag` as well as Alice's public key. On Bob ends he will need to do the following: ```go d, err := bobPriv.Exchange(alicePub) if err != nil { [...] } valid := d.Check(tag, msg, 0) if !valid { // Maybe someone is pretending to be Alice! // Return an error. } // do what you have to do ``` ### Documentation For more information see the in-code documentation. ### Test Tests module can be execute with `go test`. glome-0.2/go/config/000077500000000000000000000000001476056666600143565ustar00rootroot00000000000000glome-0.2/go/config/config.go000066400000000000000000000240631476056666600161570ustar00rootroot00000000000000// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "bufio" "encoding/base64" "encoding/hex" "fmt" "io" "strconv" "strings" "unicode" "github.com/google/glome/go/glome" ) // Config represents the supported GLOME login settings. type Config struct { AuthDelay int InputTimeout int ConfigPath string EphemeralKey glome.PrivateKey MinAuthcodeLen int HostID string HostIDType string LoginPath string DisableSyslog bool PrintSecrets bool Timeout int Verbose bool ServiceConfig ServiceConfig } // ServiceConfig contains GLOME settings from the [service] configuration section. type ServiceConfig struct { PublicKey glome.PublicKey KeyVersion int Prompt string } // ParseErrorType represents different classes of things that can happen during parsing a GLOME configuration. type ParseErrorType string const ( // BadSectionName indicates that a line could not be parsed as a configuration section header. BadSectionName ParseErrorType = "bad section header line" // BadKeyValue indicates that a line could not be parsed as a key=value. BadKeyValue ParseErrorType = "bad key/value line" // UnknownSection indicates that the section name is unknown. UnknownSection ParseErrorType = "unknown section name" // UnknownKeyInDefault indicates that the configuration key in the default section is unknown. UnknownKeyInDefault ParseErrorType = "unknown key in default section" // UnknownKeyInService indicates that the configuration key in the service section is unknown. UnknownKeyInService ParseErrorType = "unknown key in 'service' section" // InvalidValueForKey indicates that parsing the configuration value failed. InvalidValueForKey ParseErrorType = "invalid value for key" // InsecureOptionsProhibited indicates that the configuration specifies a key marked as "insecure", which is not allowed without AllowInsecureOptions. InsecureOptionsProhibited ParseErrorType = "insecure option prohibited" ) // ParseError represents an error that happened while parsing a GLOME configuration. type ParseError struct { LineNum int ErrorType ParseErrorType Description string } // Error satisfies the Go `error` interface. func (e ParseError) Error() string { descriptionSeparator := "" if e.Description != "" { descriptionSeparator = ": " } return fmt.Sprintf("config file parsing failed in line %d (%s%s%s)", e.LineNum, e.ErrorType, descriptionSeparator, e.Description) } var ( sectionAssigners = map[string]func(cfg *Config, lineNum int, key, value string, o *options) error{ "default": assignDefaultSection, "service": assignServiceSection, } ) type options struct { AllowInsecureOptions bool } // OptionFunc modifies the available options. type OptionFunc func(o *options) // AllowInsecureOptions enables the parsed config file to include options that are intended for testing only and should not be used in production. func AllowInsecureOptions(o *options) { o.AllowInsecureOptions = true } // Parse parses a GLOME ini-style configuration file to a Config struct. func Parse(r io.Reader, opts ...OptionFunc) (*Config, error) { o := &options{} for _, opt := range opts { opt(o) } s := bufio.NewScanner(r) currentSection := "default" lineNum := 0 cfg := new(Config) for s.Scan() { lineNum++ txt := strings.TrimSpace(s.Text()) switch { case len(txt) == 0, txt[0] == '#', txt[0] == ';': // Purely whitespace, or a comment. continue case txt[0] == '[': // Section header end := strings.IndexByte(txt, ']') if end == -1 { return nil, ParseError{lineNum, BadSectionName, "couldn't find closing ]"} } currentSection = txt[1:end] if len(currentSection) == 0 { return nil, ParseError{lineNum, BadSectionName, "section name was empty"} } if _, ok := sectionAssigners[currentSection]; !ok { return nil, ParseError{lineNum, UnknownSection, currentSection} } default: // Key value config option. key, value, err := parseKeyValue(txt) if err != nil { return nil, ParseError{lineNum, BadKeyValue, err.Error()} } assignValue, ok := sectionAssigners[currentSection] if !ok { // We shouldn't end up here since we validate section names as we assign them. // However, just in case... return nil, ParseError{lineNum, UnknownSection, currentSection} } if err := assignValue(cfg, lineNum, key, value, o); err != nil { return nil, err } } } return cfg, nil } func assignDefaultSection(cfg *Config, lineNum int, key, value string, o *options) error { var err error switch key { case "auth-delay": err = interpretPositiveInt(value, &cfg.AuthDelay) case "input-timeout": err = interpretPositiveInt(value, &cfg.InputTimeout) case "config-path": cfg.ConfigPath = value case "ephemeral-key": if !o.AllowInsecureOptions { return ParseError{lineNum, InsecureOptionsProhibited, key} } err = interpretPrivateKey(value, hex.DecodeString, &cfg.EphemeralKey) case "min-authcode-len": err = interpretPositiveInt(value, &cfg.MinAuthcodeLen) case "host-id": cfg.HostID = value case "host-id-type": cfg.HostIDType = value case "login-path": cfg.LoginPath = value case "disable-syslog": err = interpretBool(value, &cfg.DisableSyslog) case "print-secrets": err = interpretBool(value, &cfg.PrintSecrets) if !o.AllowInsecureOptions && cfg.PrintSecrets { // We only judge print-secrets as insecure if it's true. return ParseError{lineNum, InsecureOptionsProhibited, key} } case "timeout": err = interpretPositiveInt(value, &cfg.Timeout) case "verbose": err = interpretBool(value, &cfg.Verbose) default: return ParseError{lineNum, UnknownKeyInDefault, key} } if err != nil { return ParseError{lineNum, InvalidValueForKey, fmt.Sprintf("section: default; key: %s; provided value: %s; %s", key, value, err.Error())} } return nil } func assignServiceSection(cfg *Config, lineNum int, key, value string, o *options) error { var err error switch key { case "key": // Provided for backwards-compatibility only. // TODO: to be removed in 1.0. err = interpretPublicKey(value, hex.DecodeString, &cfg.ServiceConfig.PublicKey) case "url-prefix": // Provided for backwards-compatibility only. // TODO: to be removed in 1.0. cfg.ServiceConfig.Prompt = value + "/" case "key-version": err = interpretKeyVersion(value, &cfg.ServiceConfig.KeyVersion) case "prompt": cfg.ServiceConfig.Prompt = value case "public-key": err = interpretPublicKey(value, decodeGLOMEPublicKey, &cfg.ServiceConfig.PublicKey) default: return ParseError{lineNum, UnknownKeyInService, key} } if err != nil { return ParseError{lineNum, InvalidValueForKey, fmt.Sprintf("section: service; key: %s; provided value: %s; %s", key, value, err.Error())} } return nil } // parseKeyValue parses a `key = value` string, where whitespace has been pre-removed from the head and tail. func parseKeyValue(line string) (key, value string, err error) { // Key is the line up to the first space or =. keyEnd := strings.IndexFunc(line, func(r rune) bool { return unicode.IsSpace(r) || r == '=' }) if keyEnd == -1 { return "", "", fmt.Errorf("couldn't find = key/value separator") } key = line[:keyEnd] if key == "" { return "", "", fmt.Errorf("empty key is invalid") } line = line[keyEnd:] // Value is the line starting from the first non-space after =. valueStart := strings.IndexFunc(line, func(r rune) bool { return !unicode.IsSpace(r) && r != '=' }) if valueStart == -1 { // Possibly an empty value. valueStart = len(line) } separator := line[:valueStart] value = line[valueStart:] if strings.IndexByte(separator, '=') == -1 { return "", "", fmt.Errorf("couldn't find = key/value separator") } return key, value, nil } // interpretBool parses a boolean value in the same manner as GLOME's C implementation. func interpretBool(value string, b *bool) error { switch value { case "true", "yes", "on", "1": *b = true return nil case "false", "no", "off", "0": *b = false return nil } return fmt.Errorf("invalid boolean value %q", value) } // interpretPositiveInt parses a positive integer. func interpretPositiveInt(value string, i *int) error { v, err := strconv.Atoi(value) if err != nil { return err } if v < 0 { return fmt.Errorf("expected positive int, got %d", v) } *i = v return nil } // interpretPrivateKey parses a encoded private key. func interpretPrivateKey(value string, decoder func(s string) ([]byte, error), k *glome.PrivateKey) error { bs, err := decoder(value) if err != nil { return err } pk, err := glome.PrivateKeyFromSlice(bs) if err != nil { return err } copy(k[:], pk[:]) return nil } // interpretPublicKey parses a encoded public key. func interpretPublicKey(value string, decoder func(s string) ([]byte, error), k *glome.PublicKey) error { bs, err := decoder(value) if err != nil { return err } pk, err := glome.PublicKeyFromSlice(bs) if err != nil { return err } copy(k[:], pk[:]) return nil } // interpretKeyVersion parses a key version. func interpretKeyVersion(value string, i *int) error { v, err := strconv.Atoi(value) if err != nil { return err } if v < 0 || v > 127 { return fmt.Errorf("expected int in range [0..127], got %d", v) } *i = v return nil } const glomeV1PublicKeyPrefix = "glome-v1 " // decodeGLOMEPublicKey decodes an RFD002-encoded GLOME public key to a byte slice. func decodeGLOMEPublicKey(value string) ([]byte, error) { if !strings.HasPrefix(value, glomeV1PublicKeyPrefix) { return nil, fmt.Errorf("missing %q prefix", glomeV1PublicKeyPrefix) } value = value[len(glomeV1PublicKeyPrefix):] return base64.URLEncoding.DecodeString(value) } glome-0.2/go/config/config_test.go000066400000000000000000000235401476056666600172150ustar00rootroot00000000000000// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "os" "path/filepath" "strings" "testing" "github.com/google/glome/go/glome" "github.com/google/go-cmp/cmp" ) const ( sampleConfigPath = "../../login" ) func TestParseKeyValue(t *testing.T) { tcs := []struct { line string wantKey string wantValue string wantErr bool }{{ line: "a = b", wantKey: "a", wantValue: "b", }, { line: "a=b", wantKey: "a", wantValue: "b", }, { line: "some-hyphenated-key\t\t=some value with spaces", wantKey: "some-hyphenated-key", wantValue: "some value with spaces", }} for _, tc := range tcs { t.Run(tc.line, func(t *testing.T) { gotKey, gotValue, err := parseKeyValue(tc.line) if tc.wantErr != (err != nil) { t.Fatalf("parseKeyValue: %v (want err? %v)", err, tc.wantErr) } if gotKey != tc.wantKey { t.Errorf("parseKeyValue: key = %q; want %q", gotKey, tc.wantKey) } if gotValue != tc.wantValue { t.Errorf("parseKeyValue: key = %q; want %q", gotValue, tc.wantValue) } }) } } func TestParseError(t *testing.T) { tcs := []struct { name string err ParseError wantString string }{{ name: "descriptionless", err: ParseError{1337, BadSectionName, ""}, wantString: "config file parsing failed in line 1337 (bad section header line)", }, { name: "with description", err: ParseError{1337, BadSectionName, "something went wrong"}, wantString: "config file parsing failed in line 1337 (bad section header line: something went wrong)", }} for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { gotString := tc.err.Error() if gotString != tc.wantString { t.Errorf("tc.err.Error() = %q; want %q", gotString, tc.wantString) } }) } } func TestDefaultSectionUndefined(t *testing.T) { oldSectionAssigners := sectionAssigners defer func() { sectionAssigners = oldSectionAssigners }() sectionAssigners = nil wantErr := ParseError{1, UnknownSection, "default"} _, err := Parse(strings.NewReader("test = a\n")) if diff := cmp.Diff(wantErr, err); diff != "" { t.Errorf("Parse: got diff (-want +got)\n%v", diff) } } func TestParseConfig(t *testing.T) { for _, tc := range []struct { name string config string options []OptionFunc want *Config }{{ name: "insecure config", config: ` ; Semicolon comments are allowed # As are hash comments auth-delay = 20 input-timeout = 10 config-path = /etc/glome/glome.cfg ephemeral-key = 77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a min-authcode-len = 5 host-id = myhost.corp.big.example host-id-type = bigcorp-machine-identifier login-path = /opt/bigcorp/bin/login disable-syslog = 0 print-secrets = yes timeout = 60 verbose = true [service] prompt = glome:// key-version = 27 public-key = glome-v1 aqA9yqe1RXoOT6HrmCbF40wVUhYp50FYZR9q8_X5KF4= `, options: []OptionFunc{AllowInsecureOptions}, want: &Config{ AuthDelay: 20, InputTimeout: 10, ConfigPath: "/etc/glome/glome.cfg", EphemeralKey: glome.PrivateKey{ 0x77, 0x07, 0x6d, 0x0a, 0x73, 0x18, 0xa5, 0x7d, 0x3c, 0x16, 0xc1, 0x72, 0x51, 0xb2, 0x66, 0x45, 0xdf, 0x4c, 0x2f, 0x87, 0xeb, 0xc0, 0x99, 0x2a, 0xb1, 0x77, 0xfb, 0xa5, 0x1d, 0xb9, 0x2c, 0x2a, }, MinAuthcodeLen: 5, HostID: "myhost.corp.big.example", HostIDType: "bigcorp-machine-identifier", LoginPath: "/opt/bigcorp/bin/login", DisableSyslog: false, PrintSecrets: true, Timeout: 60, Verbose: true, ServiceConfig: ServiceConfig{ Prompt: "glome://", KeyVersion: 27, PublicKey: glome.PublicKey{ 0x6a, 0xa0, 0x3d, 0xca, 0xa7, 0xb5, 0x45, 0x7a, 0x0e, 0x4f, 0xa1, 0xeb, 0x98, 0x26, 0xc5, 0xe3, 0x4c, 0x15, 0x52, 0x16, 0x29, 0xe7, 0x41, 0x58, 0x65, 0x1f, 0x6a, 0xf3, 0xf5, 0xf9, 0x28, 0x5e, }, }, }, }, { name: "config", config: ` ; Semicolon comments are allowed # As are hash comments auth-delay = 20 input-timeout = 10 config-path = /etc/glome/glome.cfg min-authcode-len = 5 host-id = myhost.corp.big.example host-id-type = bigcorp-machine-identifier login-path = /opt/bigcorp/bin/login disable-syslog = 0 print-secrets = no timeout = 60 verbose = true [service] prompt = glome:// key-version = 27 public-key = glome-v1 aqA9yqe1RXoOT6HrmCbF40wVUhYp50FYZR9q8_X5KF4= `, want: &Config{ AuthDelay: 20, InputTimeout: 10, ConfigPath: "/etc/glome/glome.cfg", MinAuthcodeLen: 5, HostID: "myhost.corp.big.example", HostIDType: "bigcorp-machine-identifier", LoginPath: "/opt/bigcorp/bin/login", DisableSyslog: false, PrintSecrets: false, Timeout: 60, Verbose: true, ServiceConfig: ServiceConfig{ Prompt: "glome://", KeyVersion: 27, PublicKey: glome.PublicKey{ 0x6a, 0xa0, 0x3d, 0xca, 0xa7, 0xb5, 0x45, 0x7a, 0x0e, 0x4f, 0xa1, 0xeb, 0x98, 0x26, 0xc5, 0xe3, 0x4c, 0x15, 0x52, 0x16, 0x29, 0xe7, 0x41, 0x58, 0x65, 0x1f, 0x6a, 0xf3, 0xf5, 0xf9, 0x28, 0x5e, }, }, }, }} { got, err := Parse(strings.NewReader(tc.config), tc.options...) if err != nil { t.Errorf("%v: Parse: %v", tc.name, err) continue } if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("%v: Parse: got diff (-want, +got):\n%s", tc.name, diff) } } } func TestParseConfig_Errors(t *testing.T) { tcs := []struct { name string config string options []OptionFunc wantError ParseErrorType }{{ name: "invalid section header", config: "[", wantError: BadSectionName, }, { name: "empty section name", config: "[]", wantError: BadSectionName, }, { name: "unknown section", config: "[this-section-does-not-exist]", wantError: UnknownSection, }, { name: "invalid config line", config: "hello", wantError: BadKeyValue, }, { name: "invalid config line with spaces", config: "hello a b c", wantError: BadKeyValue, }, { name: "missing key", config: "= true", wantError: BadKeyValue, }, { name: "unknown key in default section", config: "unknown-key = true", wantError: UnknownKeyInDefault, }, { name: "unknown key in service section", config: ` [service] unknown-key = true `, wantError: UnknownKeyInService, }, { name: "missing value for boolean", config: "verbose =", wantError: InvalidValueForKey, }, { name: "invalid value for boolean", config: "verbose = invalid", wantError: InvalidValueForKey, }, { name: "invalid value for positive int (negative)", config: "auth-delay = -1", wantError: InvalidValueForKey, }, { name: "invalid value for positive int (garbage)", config: "auth-delay = invalid", wantError: InvalidValueForKey, }, { name: "invalid value for key version (negative)", config: "[service]\nkey-version = -1", wantError: InvalidValueForKey, }, { name: "invalid value for key version (garbage)", config: "[service]\nkey-version = invalid", wantError: InvalidValueForKey, }, { name: "invalid value for key version (too big)", config: "[service]\nkey-version = 128", wantError: InvalidValueForKey, }, { name: "insecure option specified without AllowInsecureOptions", config: "ephemeral-key = anything", wantError: InsecureOptionsProhibited, }, { name: "print-secrets specified without AllowInsecureOptions", config: "print-secrets = true", wantError: InsecureOptionsProhibited, }, { name: "invalid value for private key (garbage)", options: []OptionFunc{AllowInsecureOptions}, config: "ephemeral-key = invalid", wantError: InvalidValueForKey, }, { name: "invalid value for private key (too short)", config: "ephemeral-key = aa", options: []OptionFunc{AllowInsecureOptions}, wantError: InvalidValueForKey, }, { name: "invalid value for legacy public key (garbage)", config: "[service]\nkey = invalid", wantError: InvalidValueForKey, }, { name: "invalid value for legacy public key (too short)", config: "[service]\nkey = aa", wantError: InvalidValueForKey, }, { name: "invalid value for public key (garbage)", config: "[service]\npublic-key = invalid", wantError: InvalidValueForKey, }, { name: "invalid value for public key (too short)", config: "[service]\npublic-key = glome-v1 aGkK", wantError: InvalidValueForKey, }} for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { _, err := Parse(strings.NewReader(tc.config), tc.options...) if err == nil { t.Fatalf("Parse didn't return an error; I expected one") } cpe, ok := err.(ParseError) if !ok { t.Fatalf("Parse: %v (wanted a ParseError)", err) } if cpe.ErrorType != tc.wantError { t.Errorf("Parse: %v (error type was %q; want %q)", err, cpe.ErrorType, tc.wantError) } }) } } func TestParseConfig_InTreeSamples(t *testing.T) { names, err := filepath.Glob(filepath.Join(sampleConfigPath, "*.cfg")) if err != nil { t.Fatalf("finding sample config files: %v", err) } if len(names) == 0 { t.Fatal("no sample config files found in //login/*.cfg") } for _, name := range names { t.Run(filepath.Base(name), func(t *testing.T) { f, err := os.Open(name) if err != nil { t.Fatalf("os.Open(%q): %v", name, err) } defer f.Close() if _, err := Parse(f, AllowInsecureOptions); err != nil { t.Errorf("Parse: %v", err) } }) } } glome-0.2/go/glome/000077500000000000000000000000001476056666600142145ustar00rootroot00000000000000glome-0.2/go/glome/glome.go000077500000000000000000000170621476056666600156570ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package glome implements GLOME protocol. package glome import ( "crypto/hmac" "crypto/sha256" "fmt" "io" "golang.org/x/crypto/curve25519" ) const ( // PrivateKeySize is the size of a PrivateKey in bytes. PrivateKeySize = 32 // PublicKeySize is the size of a PublicKey in bytes. PublicKeySize = 32 // MaxTagSize is the maximum size allowed for a Tag MaxTagSize = 32 // MinTagSize is the minimum size allowed for a Tag MinTagSize = 1 ) var ( // ErrInvalidPublicKey denotes that a slice that intend to be a public key is not of desired length ErrInvalidPublicKey = fmt.Errorf("invalid public key - byte slice len is not %d", PublicKeySize) // ErrInvalidPrivateKey denotes that a slice that intend to be a private key is not of desired length ErrInvalidPrivateKey = fmt.Errorf("invalid private key - byte slice len is not %d", PrivateKeySize) // ErrInvalidTagSize denotes that provided integer is not suitable to be minPeerTagSize ErrInvalidTagSize = fmt.Errorf("invalid tag size - minPeerTagSize must be in range %d-%d", MinTagSize, MaxTagSize) // ErrInvalidReader denotes that library failed to read PrivateKeySize bytes from given Reader. ErrInvalidReader = fmt.Errorf("invalid reader - failed to read %d bytes", PrivateKeySize) ) // PublicKey is the type of GLOME public Keys. // // It can be initialized either by casting a [PublicKeySize]byte array or from a byte // slice with the PublicKeyFromSlice function. // Examples: // - Generate Public Key as existing byte array // b := [32]byte{0,2,...,7,6} // p := glome.PublicKey(b) // // - Generate from byte slice // s := b[:] // p, err := glome.PublicKeyFromSlice(s) // if err != nil { [...] } // // - Read from File // p, err := ioutil.ReadFile(filename) // if err != nil { [...] } // priv, err := glome.PublicKeyFromSlice(p) // if err != nil { [...] } type PublicKey [PublicKeySize]byte // PublicKeyFromSlice generates a PublicKey object from slice. Return ErrInvalidPublicKey // if slice's length is not PublicKeySize. func PublicKeyFromSlice(b []byte) (*PublicKey, error) { if len(b) != PublicKeySize { return nil, ErrInvalidPublicKey } var p PublicKey copy(p[:], b) return &p, nil } // PrivateKey is the type of GLOME public keys. // // It can be initialized either by casting a [PrivateKeySize]byte array or from a byte // slice with the PrivateKeyFromSlice function. // // Examples: // - Generate Private Key as existing byte array: // b := [32]byte{0,2,...,7,6} // p := glome.PrivateKey(b) // // - Generate from byte slice: // s := b[:] // p, err := glome.PrivateKeyFromSlice(s) // if err != nil { [...] } // // - Read from File: // p, err := ioutil.ReadFile(filename) // if err != nil { [...] } // priv, err := glome.PrivateKeyFromSlice(p) // if err != nil { [...] } type PrivateKey [PrivateKeySize]byte // Public returns the PublicKey corresponding to priv. func (priv *PrivateKey) Public() (*PublicKey, error) { slice, err := curve25519.X25519(priv[:], curve25519.Basepoint) if err != nil { return nil, err } p, _ := PublicKeyFromSlice(slice) return p, nil } // Exchange generates a Dialog struct. It performs GLOME handshake, and stores create // a Dialog from the user to the peer. Sets minPeerTagSize as MaxTagSize. func (priv *PrivateKey) Exchange(peer *PublicKey) (*Dialog, error) { s, err := curve25519.X25519(priv[:], peer[:]) if err != nil { return nil, err } public, err := priv.Public() if err != nil { return nil, err } return &Dialog{shared: s, User: *public, Peer: *peer, minPeerTagSize: MaxTagSize}, nil } // TruncatedExchange generates a Dialog struct. It performs GLOME handshake, // and stores create a Dialog from the user to the peer. Sets param m as minPeerTagSize. func (priv *PrivateKey) TruncatedExchange(peer *PublicKey, m uint) (*Dialog, error) { if m == 0 || m > MaxTagSize { return nil, ErrInvalidTagSize } d, err := priv.Exchange(peer) if err != nil { return nil, err } d.minPeerTagSize = m return d, nil } // PrivateKeyFromSlice generates a private key from a slice. Fail if len of // slice is not PrivateKeySize func PrivateKeyFromSlice(b []byte) (*PrivateKey, error) { if len(b) != PrivateKeySize { return nil, ErrInvalidPrivateKey } var p PrivateKey copy(p[:], b) return &p, nil } // GenerateKeys generates a public/private key pair using entropy from rand. func GenerateKeys(rand io.Reader) (*PublicKey, *PrivateKey, error) { b := make([]byte, PrivateKeySize) n, err := rand.Read(b) if err != nil { return nil, nil, err } if n != PrivateKeySize { return nil, nil, ErrInvalidReader } priv, err := PrivateKeyFromSlice(b) if err != nil { return nil, nil, err } pub, err := priv.Public() if err != nil { return nil, nil, err } return pub, priv, nil } // Dialog allow tag managing functionalities for GLOME protocol. // // Has to be generated with the methods Exchange or TruncatedExchange or Private key. // For example: // pubKey, privKey, err := glome.GenerateKeys(rand.Reader) // if err != nil { [...] } // ex, err := privkey.Exchange(peerKey) // // If TruncatedExchange is selected, minPeerTagSize can be different to MaxTagSize. See // documentation in method Check for more information on truncation. type Dialog struct { shared []byte User PublicKey // User's Public key Peer PublicKey // Peer's Public key minPeerTagSize uint // Minimun Tag size allowed. } func (d *Dialog) sendingKey() []byte { return append(d.shared[:], append(d.Peer[:], d.User[:]...)...) } func (d *Dialog) receivingKey() []byte { return append(d.shared[:], append(d.User[:], d.Peer[:]...)...) } // Generates a tag matching some provided message, counter and password. func generateTag(msg []byte, counter uint8, password []byte) []byte { h := hmac.New(sha256.New, password) h.Write([]byte{counter}) h.Write(msg) return h.Sum(nil) } // Tag generates a tag matching some provided message and counter. // This tag is generated following GLOME protocol specification // in the context of a communication from the users to theirs peers. func (d *Dialog) Tag(msg []byte, counter uint8) []byte { return generateTag(msg, counter, d.sendingKey()) } // Check method checks if a tag matches some provided message and counter. // The method generates the matching tag following GLOME protocol // specification in the context of a communication from the users' // peers to the users and then is compared with the tag provided. // // For the tag to be accepted it has to be equal in all its length // to the correct tag. Also, its length must be at least MinPeerTagLength // and always smaller than MaxTagSize. func (d *Dialog) Check(tag []byte, msg []byte, counter uint8) bool { var prefixSize uint switch { case uint(len(tag)) < d.minPeerTagSize: prefixSize = d.minPeerTagSize case uint(len(tag)) > MaxTagSize: prefixSize = MaxTagSize default: prefixSize = uint(len(tag)) } expected := generateTag(msg, counter, d.receivingKey())[:prefixSize] return hmac.Equal(expected, tag) } glome-0.2/go/glome/glome_test.go000077500000000000000000000122141476056666600167100ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package glome import ( "bytes" "encoding/hex" "fmt" "testing" ) func handle(e error, t *testing.T) { if e != nil { t.Fatalf("Unexpected Error: " + e.Error()) } } // Stores test vectors from protocol reference. Each variable is named after // a row of the test table. For the purpose of testing, we consider that user A // is always the one that sends the message (therefore, we change the role of A // and B in Vector #2) type testVector struct { kap []byte //kap = K_a'(k sub a *p*rime) ka []byte kbp []byte kb []byte counter uint8 msg []byte ks []byte tag []byte } func (tv *testVector) Dialogs(t *testing.T) (*Dialog, *Dialog) { aPriv, err := PrivateKeyFromSlice(tv.kap) if err != nil { t.Fatalf("Unexpected Error: " + err.Error()) } aPub, err := PublicKeyFromSlice(tv.ka) if err != nil { t.Fatalf("Unexpected Error: " + err.Error()) } bPriv, err := PrivateKeyFromSlice(tv.kbp) if err != nil { t.Fatalf("Unexpected Error: " + err.Error()) } bPub, err := PublicKeyFromSlice(tv.kb) if err != nil { t.Fatalf("Unexpected Error: " + err.Error()) } sending, err := aPriv.Exchange(bPub) if err != nil { t.Fatalf("Unexpected Error: " + err.Error()) } receiving, err := bPriv.Exchange(aPub) if err != nil { t.Fatalf("Unexpected Error: " + err.Error()) } return sending, receiving } func decodeString(s string) []byte { b, err := hex.DecodeString(s) if err != nil { panic(fmt.Sprintf("Invalid hexadecimal string %v input in test", s)) } return b } // Stores Tests Samples. Left out for better accessibility func tests() map[string]testVector { return map[string]testVector{ "Test Vector 1": { kap: decodeString("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"), ka: decodeString("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"), kbp: decodeString("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"), kb: decodeString("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"), counter: 0, msg: []byte("The quick brown fox"), ks: decodeString("4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742"), tag: decodeString("9c44389f462d35d0672faf73a5e118f8b9f5c340bbe8d340e2b947c205ea4fa3"), }, "Test Vector 2": { kap: decodeString("b105f00db105f00db105f00db105f00db105f00db105f00db105f00db105f00d"), ka: decodeString("d1b6941bba120bcd131f335da15778d9c68dadd398ae61cf8e7d94484ee65647"), kbp: decodeString("fee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1dead"), kb: decodeString("872f435bb8b89d0e3ad62aa2e511074ee195e1c39ef6a88001418be656e3c376"), counter: 100, msg: []byte("The quick brown fox"), ks: decodeString("4b1ee05fcd2ae53ebe4c9ec94915cb057109389a2aa415f26986bddebf379d67"), tag: decodeString("06476f1f314b06c7f96e5dc62b2308268cbdb6140aefeeb55940731863032277"), }, } } func TestKeyGeneration(t *testing.T) { for name, tv := range tests() { send, rec := tv.Dialogs(t) name := name t.Run(name, func(t *testing.T) { for _, k := range []struct { input []byte want []byte }{ {input: send.sendingKey(), want: append(tv.ks, append(tv.kb, tv.ka...)...)}, {input: send.receivingKey(), want: append(tv.ks, append(tv.ka, tv.kb...)...)}, {input: rec.sendingKey(), want: append(tv.ks, append(tv.ka, tv.kb...)...)}, {input: rec.receivingKey(), want: append(tv.ks, append(tv.kb, tv.ka...)...)}, } { if !bytes.Equal(k.input, k.want) { t.Errorf("%v failed; got: %v, want %v", name, k.want, k.input) } } }) } } func TestTagGeneration(t *testing.T) { for name, tv := range tests() { send, _ := tv.Dialogs(t) if got := send.Tag(tv.msg, tv.counter); !bytes.Equal(tv.tag, got) { t.Errorf("%v failed; got: %v, want %v", name, got, tv.tag) } } } func TestCheckFailIfIncorrectTag(t *testing.T) { for name, tv := range tests() { _, rec := tv.Dialogs(t) name := name type input struct { t []byte msg []byte c uint8 } t.Run(name, func(t *testing.T) { for _, k := range []struct { in input want bool }{ {in: input{t: tv.tag, msg: tv.msg, c: tv.counter}, want: true}, {in: input{t: []byte{23, 45, 67, 87, 65, 43, 22}, msg: tv.msg, c: tv.counter}, want: false}, {in: input{t: tv.tag, msg: []byte("this is not the message"), c: tv.counter}, want: false}, {in: input{t: tv.tag, msg: tv.msg, c: 23}, want: false}, } { got := rec.Check(k.in.t, k.in.msg, k.in.c) if !k.want == got { t.Fatalf("%v failed; got: %v, want: %v", name, got, k.want) } } }) } } glome-0.2/go/go.mod000066400000000000000000000001661476056666600142220ustar00rootroot00000000000000module github.com/google/glome/go go 1.15 require ( github.com/google/go-cmp v0.6.0 golang.org/x/crypto v0.31.0 ) glome-0.2/go/go.sum000066400000000000000000000134001476056666600142420ustar00rootroot00000000000000github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= glome-0.2/go/login/000077500000000000000000000000001476056666600142215ustar00rootroot00000000000000glome-0.2/go/login/login.go000066400000000000000000000313351476056666600156650ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package login import ( "encoding/base64" "fmt" "net/url" "regexp" "strconv" "strings" "github.com/google/glome/go/glome" ) const ( // Minimal acceptable length of a handshake. 1 byte for the Prefix, 32 bytes for the key. minHandshakeLen = 1 + glome.PublicKeySize ) var ( validURLPrefix = regexp.MustCompile(`(?Pv[1-9][0-9]*)/(?P[\w=-]+)(?:/(?P.+))?/`) ) var ( // ErrInvalidHandshakeLen denotes that the handshake is too short. ErrInvalidHandshakeLen = fmt.Errorf("handshake length is too small: should be at least %d", minHandshakeLen) // ErrInvalidPrefixType denotes that the prefix-type is invalid. ErrInvalidPrefixType = fmt.Errorf("invalid prefix type: should be a 0") // ErrIncorrectTag denotes that received tag is incorrect. ErrIncorrectTag = fmt.Errorf("invalid tag") // ErrResponseNotInitialized denotes that the response is not initialized. ErrResponseNotInitialized = fmt.Errorf("response is not initialized") ) // ErrInvalidURLFormat denotes that the URL has a wrong format. type ErrInvalidURLFormat struct { URL string } // ErrServerKeyNotFound denotes that there is no private server key associated with a Prefix. type ErrServerKeyNotFound struct { Prefix byte } // ErrVersionNotSupported denotes that the V of glome-login URL format is not supported. type ErrVersionNotSupported struct { V int } func (err *ErrInvalidURLFormat) Error() string { return fmt.Sprintf("URL %v doesn't satisfy the format %s.", err.URL, validURLPrefix.String()) } func (err *ErrServerKeyNotFound) Error() string { return fmt.Sprintf("Server key not found for prefix %d.", err.Prefix) } func (err *ErrVersionNotSupported) Error() string { return fmt.Sprintf("Version not supported: %d.", err.V) } // Message represents the context required for authorization. type Message struct { HostIDType string // type of identity HostID string // identity of the target (e.g. hostname, serial number, etc.) Action string // action that is being authorized } // Construct returns a message from a Message according to the format: [:][/]. // URL escaping is optional. func (m *Message) Construct(esc bool) []byte { hostIDType := m.HostIDType hostID := m.HostID if esc { hostIDType = escape(hostIDType) hostID = escape(hostID) } action := "" if hostIDType != "" { hostIDType += ":" } if m.Action != "" { action = "/" + m.Action } return []byte(hostIDType + hostID + action) } // Escapes the string so it can be safely placed inside a URL path segment, // replacing "/#?" special characters and not replacing "!*'();:@&=+$,[]" special characters. func escape(s string) string { res := url.PathEscape(s) for _, c := range "!*'();:@&=+$,[]" { st := string(c) strings.Replace(res, url.PathEscape(st), st, -1) } return res } // Handshake struct represents the context required for constructing the handshake. type Handshake struct { Prefix byte // either service key id or its last 7 bits of the first byte UserKey glome.PublicKey // user's public ephemeral key MessageTagPrefix []byte // Prefix of a tag calculated under Message } // URLResponse represents the context required for the construction of the URL. type URLResponse struct { V byte // URL format V (currently always 1) HandshakeInfo Handshake // handshake info including Prefix, user's public key and message tag Prefix Msg Message // message info including host and action d *glome.Dialog // glome.Dialog for the tag managing } // NewResponse returns a new URLResponse corresponding to the given arguments. func NewResponse(serviceKeyID uint8, serviceKey glome.PublicKey, userKey glome.PrivateKey, V byte, hostIDType string, hostID string, action string, tagLen uint) (*URLResponse, error) { var prefix byte var r URLResponse r.V = V d, err := userKey.TruncatedExchange(&serviceKey, 1) if err != nil { return nil, err } r.d = d r.Msg = Message{hostIDType, hostID, action} if serviceKeyID == 0 { // If no key ID was specified, send the first key byte as the ID. // TODO(#60): Fix this up once there is clarify on key Prefix usage. prefix = serviceKey[0] & 0x7f } else { prefix = serviceKeyID & 0x7f } userPublic, err := userKey.Public() if err != nil { return nil, err } r.HandshakeInfo = Handshake{prefix, *userPublic, r.Tag(tagLen)} return &r, nil } // ValidateAuthCode checks if the received tag corresponding to the tag calculated under message constructed from the Message. func (r *URLResponse) ValidateAuthCode(tag []byte) bool { return r.d.Check(tag, r.Msg.Construct(false), 0) } // Tag returns the tag corresponding to the Msg. The returned tag is calculated with usage of sendingKey. func (r *URLResponse) Tag(len uint) []byte { return r.d.Tag(r.Msg.Construct(false), 0)[:len] } // EncToken returns a base64-encoded response token. func (r *URLResponse) EncToken() string { return base64.URLEncoding.EncodeToString(r.Tag(glome.MaxTagSize)) // TODO: passing the tag len as param? } // String returns a string representing the URLResponse. func (r *URLResponse) String() string { var sb strings.Builder fmt.Fprintf(&sb, "Version: %d\n", r.V) fmt.Fprintf(&sb, "Handshake:\n") fmt.Fprintf(&sb, " Prefix: %x\n", r.HandshakeInfo.Prefix) fmt.Fprintf(&sb, " User key: %x\n", r.HandshakeInfo.UserKey) fmt.Fprintf(&sb, " Message tag prefix: %x\n", r.HandshakeInfo.MessageTagPrefix) fmt.Fprintf(&sb, "Message:\n") fmt.Fprintf(&sb, " Host ID type: %s\n", r.Msg.HostIDType) fmt.Fprintf(&sb, " Host ID: %s\n", r.Msg.HostID) fmt.Fprintf(&sb, " Action: %s", r.Msg.Action) return sb.String() } // Client implements the client-side of the glome-login protocol. Should be constructed under NewClient constructor. type Client struct { ServerKey glome.PublicKey // server's public key UserKey glome.PrivateKey // user's private key ServerKeyID uint8 // server's key id TagLen uint // length of a tag to be sent to the server. Should be in [0..glome.MaxTagLength] range. response *URLResponse // URL challenge } // NewClient is a Client constructor. Sets Client.ServerKey, Client.UserKey, Client.ServerKeyID, Client.TagLen // to the corresponding values and Client.response to nil. func NewClient(sk glome.PublicKey, uk glome.PrivateKey, sID uint8, tagLen uint) *Client { return &Client{sk, uk, sID, tagLen, nil} } // Construct returns a request to the server according to the format: /v/[/]/. func (c *Client) Construct(V byte, hostIDType string, hostID string, action string) (string, error) { r, err := NewResponse(c.ServerKeyID, c.ServerKey, c.UserKey, V, hostIDType, hostID, action, c.TagLen) if err != nil { return "", err } c.response = r var handshake = c.constructHandshake() var msg = c.response.Msg.Construct(true) var u = fmt.Sprintf("v%d/%s/", c.response.V, handshake) if len(msg) > 0 { u += fmt.Sprintf("%s/", msg) } return u, nil } // constructHandshake returns base64-url encoded handshake. The handshake is constructed following the format: // // glome-handshake := base64url( // // // // [] // ). func (c *Client) constructHandshake() string { var handshake []byte h := c.response.HandshakeInfo handshake = append(handshake, h.Prefix) handshake = append(handshake, h.UserKey[:]...) handshake = append(handshake, h.MessageTagPrefix[:]...) return base64.URLEncoding.EncodeToString(handshake[:]) } // ValidateAuthCode checks if the received tag corresponding to the tag calculated under message constructed from the Message. // Returns ErrResponseNotInitialized if the Client.response is not initialized. func (c *Client) ValidateAuthCode(tag string) (bool, error) { dTag, err := base64.URLEncoding.DecodeString(completeBase64S(tag)) if err != nil { return false, err } if c.response == nil { return false, ErrResponseNotInitialized } return c.response.ValidateAuthCode(dTag), nil } // completeBase64S completes the base64 string with padding if it was truncated and couldn't be correctly decoded. func completeBase64S(s string) string { n := len(s) switch n % 4 { case 0: return s case 1: return s[:n-1] case 2: return s + "==" case 3: return s + "=" default: panic("math fail") } } // Response is a getter for Client.response. func (c *Client) Response() *URLResponse { return c.response } // Server implements the server-side of the glome-login protocol. type Server struct { // Fetch the server's private key given a version ID. Caller is responsible // for not modifying the returned private key. If the key is authoritatively // found to not exist for a given version it is expected that (nil, nil) is // returned. KeyFetcher func(uint8) (*glome.PrivateKey, error) } // ParseURLResponse parses the url, checks whether it is formed correctly and validates the client's tag, received from the URL. // Returns ErrInvalidURLFormat if the URL is malformed, ErrServerKeyNotFound is there is no key corresponding to prefix, // ErrIncorrectTag if the client's tag is invalid. func (s *Server) ParseURLResponse(url string) (*URLResponse, error) { response := URLResponse{} parsed := validURLPrefix.FindStringSubmatch(url) // save first element (full substring) to be trimmed later in url if parsed == nil { return nil, &ErrInvalidURLFormat{url} } version, err := parseVersion(parsed[1]) if err != nil { return nil, err } response.V = version handshake, err := parseHandshake(parsed[2]) if err != nil { return nil, err } response.HandshakeInfo = *handshake sPrivKey, err := s.KeyFetcher(handshake.Prefix) if err != nil { return nil, err } if sPrivKey == nil { return nil, &ErrServerKeyNotFound{handshake.Prefix} } response.d, err = sPrivKey.TruncatedExchange(&handshake.UserKey, 1) if err != nil { return nil, err } if len(parsed) > 3 { message, err := parseMsg(parsed[3]) if err != nil { return nil, err } response.Msg = *message } if len(handshake.MessageTagPrefix) == 0 { return &response, nil } if response.ValidateAuthCode(handshake.MessageTagPrefix) != true { return nil, ErrIncorrectTag } return &response, nil } // parseVersion returns the parsed version of the URL format version. Returns ErrVersionNotSupported, // if the version is not supported. func parseVersion(v string) (byte, error) { parsed, err := strconv.Atoi(v[1:]) if err != nil { return 0, err } if parsed != 1 { // current parsed return 0, &ErrVersionNotSupported{parsed} } return byte(parsed), nil } // parseHandshake returns the parsed V of the URL handshake. // The handshake should satisfy the following format: // // glome-handshake := base64url( // // // // [] // ). // // Returns ErrInvalidHandshakeLen if the tag length is less than minHandshakeLen, // ErrInvalidPrefixType if prefix-type is different from 0, // glome.ErrInvalidTagSize if the tag length is bigger than glome.MaxTagSize. func parseHandshake(handshake string) (*Handshake, error) { dHandshake, err := base64.URLEncoding.DecodeString(handshake) if err != nil { return nil, err } if len(dHandshake) < minHandshakeLen { return nil, ErrInvalidHandshakeLen } prefix := dHandshake[0] if prefix>>7 != 0 { // check Prefix-type return nil, ErrInvalidPrefixType } userKey, err := glome.PublicKeyFromSlice(dHandshake[1:minHandshakeLen]) if err != nil { return nil, err } msgTagPrefix := dHandshake[minHandshakeLen:] if len(msgTagPrefix) > glome.MaxTagSize { return nil, glome.ErrInvalidTagSize } return &Handshake{prefix, *userKey, msgTagPrefix}, nil } // parseMsg returns the parsed V of the URL message. // The message should satisfy the following format: [:][/]. func parseMsg(hostAndAction string) (*Message, error) { var hostIDType, hostID, action string split := strings.SplitN(hostAndAction, "/", 2) host, err := url.QueryUnescape(split[0]) if err != nil { return nil, err } var h = strings.SplitN(host, ":", 2) if len(h) == 2 { // is present hostIDType = h[0] hostID = h[1] } else { hostID = h[0] } if len(split) == 2 { // is present action = split[1] } return &Message{hostIDType, hostID, action}, nil } glome-0.2/go/login/login_test.go000066400000000000000000000163201476056666600167210ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package login import ( "encoding/hex" "fmt" "strings" "testing" "github.com/google/glome/go/glome" ) var serviceKeyIDs = []uint8{1, 0} type testVector struct { kap []byte ka []byte kbp []byte kb []byte ks []byte prefix byte hostIDType string hostID string action string msg []byte url string prefixN []byte tag []byte token string } func fatal(reason string, t *testing.T, testName string, tv int) { t.Fatalf("%s failed for test vector %d. %s", testName, tv, reason) } type keyPair struct { priv glome.PrivateKey pub glome.PublicKey } func decodeString(t *testing.T, s string) []byte { b, err := hex.DecodeString(s) if err != nil { t.Fatalf("Invalid hexadecimal string %v.", s) } return b } func keys(t *testing.T, kp []byte, k []byte) *keyPair { aPriv, err := glome.PrivateKeyFromSlice(kp) if err != nil { t.Fatalf("PrivateKeyFromSlice failed: %v", err) } aPub, err := glome.PublicKeyFromSlice(k) if err != nil { t.Fatalf("PublicKeyFromSlice failed: %v", err) } return &keyPair{*aPriv, *aPub} } func (tv *testVector) dialog(t *testing.T) (*glome.Dialog, *glome.Dialog) { clientKP := keys(t, tv.kap, tv.ka) serverKP := keys(t, tv.kbp, tv.kb) sending, err := clientKP.priv.Exchange(&serverKP.pub) if err != nil { t.Fatalf("Client key exchange failed: %v", err) } receiving, err := serverKP.priv.Exchange(&clientKP.pub) if err != nil { t.Fatalf("Server key exchange failed: %v", err) } return sending, receiving } func testVectors(t *testing.T) []testVector { return []testVector{ { kap: decodeString(t, "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"), ka: decodeString(t, "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"), kbp: decodeString(t, "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"), kb: decodeString(t, "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"), ks: decodeString(t, "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742"), prefix: byte(1), hostIDType: "", hostID: "my-server.local", action: "shell/root", msg: []byte("my-server.local/shell/root"), url: "v1/AYUg8AmJMKdUdIt93LQ-91oNvzoNJjga9OukqY6qm05q0PU=/my-server.local/shell/root/", prefixN: decodeString(t, "d0f59d0b17cb155a1b9cd2b5cdea3a17f37a200e95e3651af2c88e1c5fc8108e"), tag: decodeString(t, "9c44389f462d35d0672faf73a5e118f8b9f5c340bbe8d340e2b947c205ea4fa3"), token: "lyHuaHuCck", }, { kap: decodeString(t, "fee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1dead"), ka: decodeString(t, "872f435bb8b89d0e3ad62aa2e511074ee195e1c39ef6a88001418be656e3c376"), kbp: decodeString(t, "b105f00db105f00db105f00db105f00db105f00db105f00db105f00db105f00d"), kb: decodeString(t, "d1b6941bba120bcd131f335da15778d9c68dadd398ae61cf8e7d94484ee65647"), ks: decodeString(t, "4b1ee05fcd2ae53ebe4c9ec94915cb057109389a2aa415f26986bddebf379d67"), prefix: byte(0x51), hostIDType: "serial-number", hostID: "1234567890=ABCDFGH/#?", action: "reboot", msg: []byte("serial-number:1234567890=ABCDFGH/#?/reboot"), url: "v1/UYcvQ1u4uJ0OOtYqouURB07hleHDnvaogAFBi-ZW48N2/serial-number:1234567890=ABCDFGH%2F%23%3F/reboot/", prefixN: decodeString(t, "dff5aae753a8bdce06038a20adcdb26c7be19cb6bd05a7850fae542f4af29720"), tag: decodeString(t, "06476f1f314b06c7f96e5dc62b2308268cbdb6140aefeeb55940731863032277"), token: "p8M_BUKj7zXBVM2JlQhNYFxs4J-DzxRAps83ZaNDquY=", }, } } func clientsAndServers(t *testing.T, tvs []testVector) ([]Client, []Server) { clientTagsLen := []uint{2, 0} keyPairs := make([][]keyPair, len(tvs)) for i, tv := range tvs { keyPairs[i] = append(keyPairs[i], *keys(t, tv.kap, tv.ka), *keys(t, tv.kbp, tv.kb)) } var clients []Client var servers []Server for tv := 0; tv < len(tvs); tv++ { clients = append(clients, *NewClient(keyPairs[tv][1].pub, keyPairs[tv][0].priv, serviceKeyIDs[tv], clientTagsLen[tv])) sPrivKey := keyPairs[tv][1].priv servers = append(servers, Server{func(u uint8) (*glome.PrivateKey, error) { return &sPrivKey, nil }}) } return clients, servers } func parsedResponses(t *testing.T, tvs []testVector) []URLResponse { _, servers := clientsAndServers(t, testVectors(t)) var parsedResponses []URLResponse for i, tv := range tvs { t.Run("Test vector "+fmt.Sprint(i+1), func(t *testing.T) { resp, err := servers[i].ParseURLResponse(tv.url) if err != nil { fatal(fmt.Sprintf("Expected: parsed URL, got error: %#v.", err.Error()), t, "parsedResponses", i+1) } parsedResponses = append(parsedResponses, *resp) }) } return parsedResponses } func TestURLParsedCorrectly(t *testing.T) { tvs := testVectors(t) responses := parsedResponses(t, tvs) for i, tv := range tvs { t.Run("Test vector "+fmt.Sprint(i+1), func(t *testing.T) { // Check message parsed correctly msg := responses[i].Msg for _, m := range []struct { expected string got string }{ {expected: tv.hostIDType, got: msg.HostIDType}, {expected: tv.hostID, got: msg.HostID}, {expected: tv.action, got: msg.Action}, } { if m.expected != m.got { fatal(fmt.Sprintf("Expected: %#v, got: %#v.", m.expected, m.got), t, "TestURLParsedCorrectly", i+1) } } }) } } func TestServerToken(t *testing.T) { tvs := testVectors(t) responses := parsedResponses(t, tvs) for i, tv := range tvs { t.Run("Test vector "+fmt.Sprint(i+1), func(t *testing.T) { if !(strings.HasPrefix(responses[i].EncToken(), tv.token)) { fatal(fmt.Sprintf("The tags are different: expected %#v, got %#v.", tv.token, responses[i].EncToken()), t, "TestServerToken", i+1) } }) } } func TestURLResponseConstruction(t *testing.T) { tvs := testVectors(t) clients, _ := clientsAndServers(t, tvs) for i, tv := range tvs { t.Run("Test vector "+fmt.Sprint(i+1), func(t *testing.T) { resp, err := clients[i].Construct(1, tv.hostIDType, tv.hostID, tv.action) if err != nil { fatal(fmt.Sprintf("Error while constructing URL: %s.", err.Error()), t, "TestURLResponseConstruction", i+1) } if resp != tv.url { fatal(fmt.Sprintf("The URLs are different: expected %#v, got %#v.", tv.url, resp), t, "TestURLResponseConstruction", i+1) } eq, err := clients[i].ValidateAuthCode(tv.token) if err != nil { fatal(fmt.Sprintf("Error while validating authorization code: %s.", err.Error()), t, "TestURLResponseConstruction", i+1) } if !eq { fatal("The tokens are different.", t, "TestURLResponseConstruction", i+1) } }) } } glome-0.2/go/login/server/000077500000000000000000000000001476056666600155275ustar00rootroot00000000000000glome-0.2/go/login/server/keymanager.go000066400000000000000000000105031476056666600202000ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package server import ( "fmt" "sync" "github.com/google/glome/go/glome" ) // ErrInvalidKeyIndex denotes that provided index is invalid for a key. Only // indexes in {0,...,127} are valid. type ErrInvalidKeyIndex struct { Index uint8 } func (e ErrInvalidKeyIndex) Error() string { return fmt.Sprintf("key index should be in {0,...,127}, found: %v", e.Index) } // ErrDuplicatedKeyIndex denotes that provided index is already in use, so no // new keys can be assigned to it. type ErrDuplicatedKeyIndex struct { Index uint8 } func (e ErrDuplicatedKeyIndex) Error() string { return fmt.Sprintf("key index already in use, found: %v", e.Index) } // A PrivateKey represent a Private key for the login server. It is a pair composed of a private key // and its pairing index. type PrivateKey struct { Value glome.PrivateKey Index uint8 } // A PublicKey represent a Service key for the login server. It is a pair composed of a public key // and its pairing index. type PublicKey struct { Value glome.PublicKey Index uint8 } // KeyManager performs key maneger task in a concurrent-safe way. It allows for constant // time search of keys by index. KeyManager is constructed with NewKeyManager function. type KeyManager struct { indexToPriv map[uint8]glome.PrivateKey publicKeys []PublicKey lock sync.RWMutex } func (k *KeyManager) locklessAdd(key glome.PrivateKey, index uint8) error { if index > 127 { return ErrInvalidKeyIndex{Index: index} } if _, ok := k.indexToPriv[index]; ok { return ErrDuplicatedKeyIndex{Index: index} } pub, err := key.Public() if err != nil { return err } k.indexToPriv[index] = key k.publicKeys = append(k.publicKeys, PublicKey{Value: *pub, Index: index}) return nil } // Add adds provided key and index to the key manager. Return ErrInvalidindex // if index provided is not in {0,...,127} and ErrDuplicatedIndex if index // provided was already in use. func (k *KeyManager) Add(key glome.PrivateKey, index uint8) error { k.lock.Lock() defer k.lock.Unlock() return k.locklessAdd(key, index) } // Read returns the PrivateKey stored in the KeyManager for a index, or a // zero-value PrivateKey if no PrivateKey is present. The ok result indicates // whether value was found in the KeyManager. func (k *KeyManager) Read(index uint8) (glome.PrivateKey, bool) { k.lock.RLock() defer k.lock.RUnlock() key, ok := k.indexToPriv[index] return key, ok } // DropAllReplace drops all stored keys and replace them with the new ones provided. // This operation is done in a atomic way (no other call to the struct will be handled // while DropAllReplace is). func (k *KeyManager) DropAllReplace(keys []PrivateKey) error { k.lock.Lock() defer k.lock.Unlock() k.indexToPriv = make(map[uint8]glome.PrivateKey) for _, key := range keys { if err := k.locklessAdd(key.Value, key.Index); err != nil { return err } } return nil } // ServiceKeys returns a copy slice of the public keys being at use at this moment by // the KeyManager. func (k *KeyManager) ServiceKeys() []PublicKey { serviceKey := make([]PublicKey, len(k.publicKeys)) copy(serviceKey, k.publicKeys) return serviceKey } // Return a function that implements key fetching. The function // returns ErrInvalidKeyIndex if index is not in {0,...,127}, and nil // if the provided index does not match any key. func (k *KeyManager) keyFetcher() func(uint8) (*glome.PrivateKey, error) { return func(index uint8) (*glome.PrivateKey, error) { if index > 127 { return nil, ErrInvalidKeyIndex{Index: index} } key, found := k.Read(index) if !found { return nil, nil } return &key, nil } } // NewKeyManager returns a new key manager. func NewKeyManager() *KeyManager { return &KeyManager{ indexToPriv: make(map[uint8]glome.PrivateKey), publicKeys: make([]PublicKey, 0), } } glome-0.2/go/login/server/keymanager_test.go000066400000000000000000000140251476056666600212420ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package server import ( "reflect" "testing" "github.com/google/glome/go/glome" ) func Contains(list []PublicKey, pub PublicKey) bool { for _, b := range list { if b == pub { return true } } return false } func TestKeyAdd(t *testing.T) { for name, k := range []struct { priv glome.PrivateKey index uint8 }{ { priv: glome.PrivateKey([32]byte{}), index: 0, }, { priv: glome.PrivateKey([32]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}), index: 1, }, { priv: glome.PrivateKey([32]byte{49, 244, 125, 133, 0, 40, 7, 192, 7, 90, 5, 208, 234, 104, 66, 68, 251, 237, 187, 132, 67, 236, 108, 164, 162, 199, 41, 89, 128, 95, 26, 190}), index: 2, }, } { manager := NewKeyManager() if err := manager.Add(k.priv, k.index); err != nil { t.Fatalf("test %v, unexpected error: %v ", name, err.Error()) } readKey, found := manager.Read(k.index) if !found { t.Errorf("test %v: No private key %v was added in index %v", name, k.priv, k.index) } if readKey != k.priv { t.Errorf("test %v: private key %v was not added in index %v", name, k.priv, k.index) } pub, err := k.priv.Public() if err != nil { t.Fatalf("test %v, unexpected error: %v ", name, err.Error()) } if !Contains(manager.publicKeys, PublicKey{Value: *pub, Index: k.index}) { t.Errorf("test %v: public key %v was not added in index %v", name, pub, k.index) } } } func TestKeyAddExceptions(t *testing.T) { type input struct { manager *KeyManager priv glome.PrivateKey index uint8 } // PreloadManager is manager for test 2 preloadManager := NewKeyManager() preloadManager.Add(glome.PrivateKey([32]byte{}), 0) for name, k := range []struct { in input want error }{ { in: input{ manager: NewKeyManager(), priv: glome.PrivateKey([32]byte{}), index: 0, }, want: nil, }, { in: input{ manager: preloadManager, priv: glome.PrivateKey([32]byte{}), index: 0, }, want: ErrDuplicatedKeyIndex{Index: 0}, }, { in: input{ manager: NewKeyManager(), priv: glome.PrivateKey([32]byte{}), index: 129, }, want: ErrInvalidKeyIndex{Index: 129}, }, } { if err := k.in.manager.Add(k.in.priv, k.in.index); err != k.want { t.Errorf("test %v failed to raises wanted exception on input %#v; got %#v, want %#v", name, k.in, err, k.want) } } } func TestKeyRead(t *testing.T) { type input struct { priv glome.PrivateKey index uint8 } type output struct { priv glome.PrivateKey found bool } for name, k := range []struct { in input want output }{ { in: input{priv: glome.PrivateKey([32]byte{}), index: 0}, want: output{priv: glome.PrivateKey([32]byte{}), found: true}, }, { in: input{ priv: glome.PrivateKey([32]byte{49, 244, 125, 133, 0, 40, 7, 192, 7, 90, 5, 208, 234, 104, 66, 68, 251, 237, 187, 132, 67, 236, 108, 164, 162, 199, 41, 89, 128, 95, 26, 190}), index: 111, }, want: output{ priv: glome.PrivateKey([32]byte{49, 244, 125, 133, 0, 40, 7, 192, 7, 90, 5, 208, 234, 104, 66, 68, 251, 237, 187, 132, 67, 236, 108, 164, 162, 199, 41, 89, 128, 95, 26, 190}), found: true, }, }, } { manager := NewKeyManager() if _, found := manager.Read(k.in.index); found { t.Errorf("test %v failed; found key on index %v", name, k.in.index) } if err := manager.Add(k.in.priv, k.in.index); err != nil { t.Fatalf("test %v, unexpected error: %v ", name, err.Error()) } if key, found := manager.Read(k.in.index); key != k.want.priv || found != k.want.found { t.Errorf("test %v failed on input %#v; want %v, got %v,%v", name, k.in, k.want, key, found) } } } func TestDropAllReplace(t *testing.T) { preloadManager := NewKeyManager() preloadManager.Add(glome.PrivateKey([32]byte{}), 0) type input struct { keys []PrivateKey manager *KeyManager } for name, k := range []struct { in input want map[uint8]glome.PrivateKey }{ { in: input{ keys: []PrivateKey{ PrivateKey{Value: glome.PrivateKey([32]byte{}), Index: 0}, PrivateKey{ Value: glome.PrivateKey([32]byte{49, 244, 125, 133, 0, 40, 7, 192, 7, 90, 5, 208, 234, 104, 66, 68, 251, 237, 187, 132, 67, 236, 108, 164, 162, 199, 41, 89, 128, 95, 26, 190}), Index: 1, }, }, manager: NewKeyManager(), }, want: map[uint8]glome.PrivateKey{ 0: glome.PrivateKey([32]byte{}), 1: glome.PrivateKey([32]byte{49, 244, 125, 133, 0, 40, 7, 192, 7, 90, 5, 208, 234, 104, 66, 68, 251, 237, 187, 132, 67, 236, 108, 164, 162, 199, 41, 89, 128, 95, 26, 190}), }, }, { in: input{ keys: []PrivateKey{ PrivateKey{Value: glome.PrivateKey([32]byte{}), Index: 0}, PrivateKey{ Value: glome.PrivateKey([32]byte{49, 244, 125, 133, 0, 40, 7, 192, 7, 90, 5, 208, 234, 104, 66, 68, 251, 237, 187, 132, 67, 236, 108, 164, 162, 199, 41, 89, 128, 95, 26, 190}), Index: 1, }, }, manager: preloadManager, }, want: map[uint8]glome.PrivateKey{ 0: glome.PrivateKey([32]byte{}), 1: glome.PrivateKey([32]byte{49, 244, 125, 133, 0, 40, 7, 192, 7, 90, 5, 208, 234, 104, 66, 68, 251, 237, 187, 132, 67, 236, 108, 164, 162, 199, 41, 89, 128, 95, 26, 190}), }, }, } { k.in.manager.DropAllReplace(k.in.keys) if !reflect.DeepEqual(k.in.manager.indexToPriv, k.want) { t.Errorf("test %v failed; got %#v, want %#v", name, k.in.manager.indexToPriv, k.want) } } } glome-0.2/go/login/server/server.go000066400000000000000000000132171476056666600173700ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package server implements GLOME-login server framework. package server import ( "encoding/hex" "fmt" "net/http" "sync" "github.com/google/glome/go/login" ) const ( // MaxResponseSize is the maximum size in characaters of the response token MaxResponseSize = 44 // 32 bytes base64 encoded ) // ErrInvalidResponseLen denotes that response length provided is invalid. ResponseLen // should be in range {1,...,MaxResponseSize} type ErrInvalidResponseLen struct { ResponseLen uint8 } func (e ErrInvalidResponseLen) Error() string { return fmt.Sprintf("ResponseLen should be in range {1,...,%v}, got %v", MaxResponseSize, e.ResponseLen) } // Authorizer responds to an authorization request. The method // GrantLogin returns whether an user is allowed to perform a given action on a host. // // Some considerations need to be held while implementing this interface: // - Allow should consider that an empty string as command is a correct input. // - If no user can be obtained from request metadata, an empty string is to be // passed as default value. // - Both hostIDType and hostID can be empty. Whether this refer to a default value // or not is to be user configurable. // - returned boolean will be considered even if an error is returned. type Authorizer interface { GrantLogin(user string, hostID string, hostIDType string, action string) (bool, error) } // AuthorizerFunc type is an adapter to allow the use of ordinary functions as an Authorizer. type AuthorizerFunc func(user string, hostID string, hostIDType string, action string) (bool, error) // GrantLogin calls a(user, hostID, hostIDType, action) func (a AuthorizerFunc) GrantLogin(user string, hostID string, hostIDType string, action string) (bool, error) { return a(user, hostID, hostIDType, action) } // LoginServer is a framework that can be used to implement servers for glome-login. type LoginServer struct { // Keys manages the keys used by the server. Keys *KeyManager auth Authorizer authLock sync.RWMutex loginParser *login.Server responseLen uint8 userHeader string } // Authorizer replaces the server Authorizer with a new one provided, in a secure way for concurrency. func (s *LoginServer) Authorizer(a Authorizer) { s.authLock.Lock() s.auth = a s.authLock.Unlock() } // NewLoginServer creates a new server with provided Authorizer and, optionally, selected options. func NewLoginServer(a Authorizer, options ...func(*LoginServer) error) (*LoginServer, error) { srv := LoginServer{ auth: a, Keys: NewKeyManager(), responseLen: MaxResponseSize, userHeader: "authenticated-user", } srv.loginParser = srv.newLoginParser() for _, option := range options { if err := option(&srv); err != nil { return nil, err } } return &srv, nil } // ResponseLen is an option to be provided to NewServer on creation. Its sets the size of response // to provided length. the size is measured in number of characters in base64. Return // ErrInvalidResponseLen if provided length is not in {1,..,MaxResponseSize}. If not set, // defaults to MaxResponseSize. func ResponseLen(length uint8) func(srv *LoginServer) error { return func(srv *LoginServer) error { if !(0 < length && length <= MaxResponseSize) { return ErrInvalidResponseLen{ResponseLen: length} } srv.responseLen = length return nil } } // UserHeader is an option to be provided to NewServer on creation. It sets the name of the // HTTP header from which to read the user id. It defaults to "authenticated-user". func UserHeader(s string) func(srv *LoginServer) error { return func(srv *LoginServer) error { srv.userHeader = s return nil } } func (s *LoginServer) newLoginParser() *login.Server { return &login.Server{KeyFetcher: s.Keys.keyFetcher()} } // ServeHTTP implements http.Handler interface: // - On "/": List server service keys. // - On a glome login URL: Return a login token or an error message. func (s *LoginServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { s.printServerKeys(w) return } user := r.Header.Get(s.userHeader) path := r.URL.RawPath if path == "" { path = r.URL.Path } response, err := s.loginParser.ParseURLResponse(path) if err != nil { http.Error(w, err.Error(), 400) return } s.printToken(w, response, user) } // Auxiliary function to print login token on response writer. func (s *LoginServer) printToken(w http.ResponseWriter, r *login.URLResponse, user string) { s.authLock.RLock() allowed, err := s.auth.GrantLogin(user, r.Msg.HostID, r.Msg.HostIDType, r.Msg.Action) s.authLock.RUnlock() if !allowed { if err != nil { http.Error(w, err.Error(), 403) } else { http.Error(w, "unauthorized action", 403) } return } responseToken := r.EncToken()[:s.responseLen] fmt.Fprintln(w, responseToken) } // Auxiliary function that prints service keys. func (s *LoginServer) printServerKeys(w http.ResponseWriter) { fmt.Fprintf(w, "List of server keys\n") fmt.Fprintf(w, "-------------------\n") fmt.Fprintf(w, "Index\tValue\n") for _, key := range s.Keys.ServiceKeys() { fmt.Fprintf(w, "%v\t%v\n", key.Index, hex.EncodeToString(key.Value[:])) } } glome-0.2/go/login/server/server_test.go000066400000000000000000000115321476056666600204250ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package server import ( "encoding/hex" "fmt" "io/ioutil" "net/http/httptest" "strings" "testing" "github.com/google/glome/go/glome" ) type testVector struct { kbp []byte index uint8 ka []byte Request string Response string ResponseLen uint8 authFunc Authorizer } // ServerKey return correctly formatted Server Private Key func (t *testVector) ServerKey() glome.PrivateKey { p, err := glome.PrivateKeyFromSlice(t.kbp) if err != nil { panic(fmt.Sprintf("Glome rejected %v:%#v", t.kbp, err)) } return *p } // ClientKey return correctly formatted Client Public Key func (t testVector) ClientKey() glome.PublicKey { p, err := glome.PublicKeyFromSlice(t.ka) if err != nil { panic(fmt.Sprintf("Glome rejected %v:%#v", t.ka, err)) } return *p } func decodeString(s string) []byte { b, err := hex.DecodeString(s) if err != nil { panic(fmt.Sprintf("Invalid hexadecimal string %v input in test", s)) } return b } func constantTrue(user string, hostID string, hostIDType string, action string) (bool, error) { return true, nil } func constantFalse(user string, hostID string, hostIDType string, action string) (bool, error) { return false, nil } func serverTests() map[string]testVector { return map[string]testVector{ "test vector 0": { kbp: decodeString("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"), index: 1, Request: "/", Response: "List of server keys\n" + "-------------------\n" + "Index\tValue\n" + "1\tde9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f\n", ResponseLen: MaxResponseSize, authFunc: AuthorizerFunc(constantTrue), }, "test vector 1": { kbp: decodeString("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"), index: 1, Request: "v1/AYUg8AmJMKdUdIt93LQ-91oNvzoNJjga9OukqY6qm05q0PU=/my-server.local/shell/root/", Response: "lyHuaHuCck\n", ResponseLen: 10, authFunc: AuthorizerFunc(constantTrue), }, "test vector 2": { kbp: decodeString("b105f00db105f00db105f00db105f00db105f00db105f00db105f00db105f00d"), index: 0x51, Request: "v1/UYcvQ1u4uJ0OOtYqouURB07hleHDnvaogAFBi-ZW48N2/serial-number:1234567890=ABCDFGH%2F%23%3F/reboot/", Response: "p8M_BUKj7zXBVM2JlQhNYFxs4J-DzxRAps83ZaNDquY=\n", ResponseLen: MaxResponseSize, authFunc: AuthorizerFunc(constantTrue), }, "test vector 3": { kbp: decodeString("b105f00db105f00db105f00db105f00db105f00db105f00db105f00db105f00d"), index: 0x51, Request: "v1/UycvQ1u4uJ0OOtYqouURB07hleHDnvaogAFBi-ZW48N2/serial-number:1234567890=ABCDFGH%2F%23%3F/reboot/", Response: "Server key not found for prefix 83.\n", ResponseLen: MaxResponseSize, authFunc: AuthorizerFunc(constantTrue), }, "test vector 4": { kbp: decodeString("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"), index: 1, Request: "v1/AYUg8AmJMKdUdIt93LQ-91oNvzoNJjga9OukqY6qm05q0PU=/my-server.local/shell/root/", Response: "unauthorized action\n", ResponseLen: 10, authFunc: AuthorizerFunc(constantFalse), }, "test vector 5": { kbp: decodeString("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"), index: 1, Request: "v1/aYUg8AmJMKdUdIt93LQ-91oNvzoNJjga9OukqY6qm05q0PU=/my-server.local/shell/root/", Response: "Server key not found for prefix 105.\n", ResponseLen: 10, authFunc: AuthorizerFunc(constantFalse), }, } } func TestServer(t *testing.T) { for name, tv := range serverTests() { name := name tv := tv t.Run(name, func(t *testing.T) { url := tv.Request if !strings.HasPrefix(url, "/") { url = "/" + url } r := httptest.NewRequest("GET", url, nil) w := httptest.NewRecorder() login, err := NewLoginServer(tv.authFunc, ResponseLen(tv.ResponseLen)) if err != nil { t.Fatalf("test %v, unexpected error: %v ", name, err.Error()) } login.Keys.Add(tv.ServerKey(), tv.index) login.ServeHTTP(w, r) resp := w.Result() body, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatalf("test %v, unexpected error: %v ", name, err.Error()) } if string(body) != tv.Response { t.Errorf("test %v, got %#v, want %#v", name, string(body), tv.Response) } }) } } glome-0.2/go/login/v2/000077500000000000000000000000001476056666600145505ustar00rootroot00000000000000glome-0.2/go/login/v2/README.md000066400000000000000000000024231476056666600160300ustar00rootroot00000000000000# GLOME Login Golang API v2 This package implements version 2 of the GLOME Login challenge response protocol, as described in the [specification](../../../docs/glome-login.md) and [RFD001](../../../docs/rfd/001.md). ## Design The API is designed with two groups of users in mind: clients and servers. In the GLOME Login protocol, clients generate *challenges* which are *responded to* by servers. This is reflected in the two basic structs defined here, `v2.Challenger` and `v2.Responder`. The other important struct is `v2.Message`, which contains all context for the authorization decision. The genral flow is: 1. Client creates a `v2.Challenger` object including server configuration. This object is long-lived and can be reused. 2. An authorization decision needs to be made. The client phrases it in form of a `v2.Message` and produces an encoded challenge. 3. The challenge is transferred to the server, which holds a long-lived `v2.Responder` object that manages keys. 4. The server accepts the challenge, inspects the message and - if justified - authorizes by handing out the response code. 5. The response code is transferred to the client, which validates the code and grants access. ## Example There's an example GLOME Login flow in [login_test.go](login_test.go). glome-0.2/go/login/v2/challenger.go000066400000000000000000000070451476056666600172110ustar00rootroot00000000000000package v2 import ( "crypto/rand" "encoding/base64" "errors" "io" "strings" "github.com/google/glome/go/glome" ) var ( // defaultMinResponseSize is the recommended minimal size of a tag so that // brute-forcing is infeasible (see MIN_ENCODED_AUTHCODE_LEN in the C // sources). defaultMinResponseSize uint8 = 10 ) // Challenger produces challenges that a Responder can respond to. type Challenger struct { // PublicKey is the server's public key. // // This field must always be set. PublicKey *glome.PublicKey // The fields below are optional, their zero values work as expected. // MinResponseLength is the minimal length of a response string required for verification. // // Recommended and default setting of this field is 10 (see protocol documentation). MinResponseLength uint8 // MessageTagPrefixLength is the number of error detection bytes added to the challenge. // // Setting this to non-zero allows to detect a mismatch between the public key used by the // client and the public key inferred by the server from index or public key prefix. MessageTagPrefixLength uint8 // KeyIndex that the server uses to identify its private key. // // If unset, the challenge will be created with the public key prefix instead. KeyIndex *uint8 // RNG generates ephemeral private keys for this Challenger. // // If unset, crypto/rand.Reader will be used. // WARNING: Don't set this field unless you know what you are doing! RNG io.Reader } // ClientChallenge is the internal representation of a challenge as it would be used on a client. // // ClientChallenge instances must be created by Challenger.Challenge()! type ClientChallenge struct { d *glome.Dialog // The minimum length of an acceptable response. min uint8 h *handshake m []byte } // Challenge creates a clientChallenge object for this message and the Challenger configuration. func (c *Challenger) Challenge(msg *Message) (*ClientChallenge, error) { h := &handshake{} rng := c.RNG if rng == nil { rng = rand.Reader } publicKey, key, err := glome.GenerateKeys(rng) if err != nil { return nil, err } h.PublicKey = publicKey if c.PublicKey == nil { return nil, errors.New("no public key") } if c.KeyIndex != nil { h.Index = *c.KeyIndex } else { h.Prefix = &c.PublicKey[glome.PublicKeySize-1] } minResponseSize := uint8(c.MinResponseLength) if minResponseSize == 0 { minResponseSize = defaultMinResponseSize } d, err := key.TruncatedExchange(c.PublicKey, glome.MinTagSize) if err != nil { return nil, err } encodedMsg := []byte(msg.Encode()) if c.MessageTagPrefixLength > 0 { h.MessageTagPrefix = d.Tag(encodedMsg, 0)[:c.MessageTagPrefixLength] } return &ClientChallenge{h: h, d: d, m: encodedMsg, min: minResponseSize}, nil } // Encode encodes the challenge into its URI path represenation. func (c *ClientChallenge) Encode() string { return strings.Join([]string{"v2", c.h.Encode(), string(c.m), ""}, "/") } // Verify a challenge response string. func (c *ClientChallenge) Verify(s string) bool { // In order to accept truncated base64 data, we need to handle special cases: // - a single byte from an encoded triple can never decode correctly // - 32 byte encode with a trailing padding character, which makes RawURLEncoding unhappy. n := len(s) // We check the response size here so that we don't need to deal with length conversion between // Base64 and HMAC. if n < int(c.min) { return false } if n%4 == 1 || n == 44 { n-- } tag, err := base64.RawURLEncoding.DecodeString(s[:n]) if err != nil { return false } return c.d.Check(tag, c.m, 0) } glome-0.2/go/login/v2/codec.go000066400000000000000000000050411476056666600161540ustar00rootroot00000000000000package v2 import ( "bytes" "encoding/base64" "errors" "net/url" "strings" "github.com/google/glome/go/glome" ) // Message represents the context required for authorization. type Message struct { HostIDType string // type of identity HostID string // identity of the target (e.g. hostname, serial number, etc.) Action string // action that is being authorized } // escape a URI path minimally according to RFD001. func escape(s string) string { res := url.PathEscape(s) for _, c := range "!*'();:@&=+$,[]" { st := string(c) res = strings.Replace(res, url.PathEscape(st), st, -1) } return res } // Encode the message into its URI path representation. func (m *Message) Encode() string { sb := &strings.Builder{} if len(m.HostIDType) > 0 { sb.WriteString(escape(m.HostIDType)) sb.WriteByte(':') } sb.WriteString(escape(m.HostID)) sb.WriteByte('/') sb.WriteString(escape(m.Action)) return sb.String() } func decodeMessage(s string) (*Message, error) { m := &Message{} subs := strings.Split(s, "/") if len(subs) != 2 { return nil, errors.New("message format error") } hostSegment, err := url.PathUnescape(subs[0]) if err != nil { return nil, err } hostParts := strings.SplitN(hostSegment, ":", 2) if len(hostParts) > 1 { m.HostIDType = hostParts[0] m.HostID = hostParts[1] } else { m.HostID = hostParts[0] } action, err := url.PathUnescape(subs[1]) if err != nil { return nil, err } m.Action = action return m, nil } type handshake struct { Index uint8 Prefix *byte PublicKey *glome.PublicKey MessageTagPrefix []byte } func (h *handshake) Encode() string { data := bytes.NewBuffer(nil) if h.Prefix != nil { data.WriteByte(*h.Prefix) } else { data.WriteByte(1<<7 | h.Index) } data.Write(h.PublicKey[:]) data.Write(h.MessageTagPrefix) return base64.URLEncoding.EncodeToString(data.Bytes()) } func decodeHandshake(s string) (*handshake, error) { data, err := base64.URLEncoding.DecodeString(s) if err != nil { return nil, err } if len(data) < 33 { return nil, errors.New("handshake too short") } h := &handshake{} if data[0]>>7 == 0 { // check Prefix-type h.Prefix = &data[0] } else { h.Index = data[0] % (1 << 7) } key, err := glome.PublicKeyFromSlice(data[1 : glome.PublicKeySize+1]) if err != nil { return nil, err } h.PublicKey = key msgTagPrefix := data[glome.PublicKeySize+1:] if len(msgTagPrefix) > glome.MaxTagSize { return nil, errors.New("message tag prefix too long") } if len(msgTagPrefix) > 0 { h.MessageTagPrefix = msgTagPrefix } return h, nil } glome-0.2/go/login/v2/codec_test.go000066400000000000000000000023371476056666600172200ustar00rootroot00000000000000package v2 import ( "reflect" "testing" ) type messageTestCase struct { msg *Message encoded string } var messageTestCases = []messageTestCase{ { encoded: "myhost/root", msg: &Message{HostID: "myhost", Action: "root"}, }, { encoded: "mytype:myhost/root", msg: &Message{HostIDType: "mytype", HostID: "myhost", Action: "root"}, }, { encoded: "escaping/special%20action%CC", msg: &Message{HostID: "escaping", Action: "special action\xcc"}, }, { encoded: "pairs/user=root;exec=%2Fbin%2Fmksh", msg: &Message{HostID: "pairs", Action: "user=root;exec=/bin/mksh"}, }, } func TestEncodeMessage(t *testing.T) { for _, tc := range messageTestCases { t.Run(tc.encoded, func(t *testing.T) { got := tc.msg.Encode() if got != tc.encoded { t.Errorf("%#v.Encode() == %q, want %q", tc.msg, got, tc.encoded) } }) } } func TestDecodeMessage(t *testing.T) { for _, tc := range messageTestCases { t.Run(tc.encoded, func(t *testing.T) { got, err := decodeMessage(tc.encoded) if err != nil { t.Fatalf("decodeMessage(%q) failed: %v", tc.encoded, err) } if !reflect.DeepEqual(got, tc.msg) { t.Errorf("decodeMessage(%q) == %#v, want %#v", tc.encoded, got, tc.msg) } }) } } glome-0.2/go/login/v2/login_test.go000066400000000000000000000160041476056666600172470ustar00rootroot00000000000000package v2_test import ( "bytes" "encoding/hex" "fmt" "reflect" "testing" "github.com/google/glome/go/glome" v2 "github.com/google/glome/go/login/v2" ) func unhexPrivateKey(s string) *glome.PrivateKey { var buf [32]byte n, err := hex.Decode(buf[:], []byte(s)) if err != nil { panic(err) } if n != 32 { panic("hex literal had unexpected length") } key := glome.PrivateKey(buf) return &key } func Example() { // Error handling omitted for clarity. // This example demonstrates the basic flow of a GLOME Login. Our cast has 3 protagonists: // * The _client_ is a short-lived process guarding access to a computer somewhere. // * The _server_ is a long-lived centralized process authorizing access to computers. // * The _operator_ tries to access the computer and interacts with both _client_ and _server_. // ===== Server Side ===== // This is the permanent setup of the authorization server. For this example, it uses only one // key, with index 0. serverKey := unhexPrivateKey("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb") server, _ := v2.NewResponder(map[uint8]*glome.PrivateKey{0: serverKey}) // ===== Client Side ===== // The client needs to be configured with at least a public key of the server. For this // example, we rely on key auto-discovery. To be safe against key mismatches on the server // side, we include a small tag prefix for error detection. serverPublickey, _ := serverKey.Public() client := &v2.Challenger{ PublicKey: serverPublickey, MessageTagPrefixLength: 3, } // The client crafts a message for the server, asking it to authorize a specific action. In // this example, the client identifies itself as myhost and the operator is attempting to log // in as root. msg := &v2.Message{ HostID: "myhost", Action: "user=root", } // The client creates a challenge and hands the encoded version of the challenge to the // operator, who then takes the challenge to the server, somehow. clientChallenge, _ := client.Challenge(msg) encodedChallenge := clientChallenge.Encode() // ===== Server Side ===== serverChallenge, err := server.Accept(encodedChallenge) if err != nil { panic(err) } // The server verifies that the operator is authorized for the message ... fmt.Printf("Message: %s\n", serverChallenge.Message.Encode()) // ... and hands the operator an encoded response. response := serverChallenge.Response // The operator transfers the response back to the client, somehow. // ===== Client Side ===== // The client checks the response provided by the operator and grants access to the computer. if clientChallenge.Verify(response) { fmt.Println("authorized") } else { fmt.Println("forbidden") } // Output: // Message: myhost/user=root // authorized } func Example_withIndex() { // This is like Example(), but using a server key index. // ===== Server Side ===== serverKey := unhexPrivateKey("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb") serverKeyIndex := uint8(42) server, _ := v2.NewResponder(map[uint8]*glome.PrivateKey{serverKeyIndex: serverKey}) // ===== Client Side ===== serverPublickey, _ := serverKey.Public() client := &v2.Challenger{ PublicKey: serverPublickey, KeyIndex: &serverKeyIndex, } msg := &v2.Message{ HostID: "myhost", Action: "user=root", } clientChallenge, _ := client.Challenge(msg) encodedChallenge := clientChallenge.Encode() // ===== Server Side ===== serverChallenge, err := server.Accept(encodedChallenge) if err != nil { panic(err) } fmt.Printf("Message: %s\n", serverChallenge.Message.Encode()) response := serverChallenge.Response // ===== Client Side ===== if clientChallenge.Verify(response) { fmt.Println("authorized") } else { fmt.Println("forbidden") } // Output: // Message: myhost/user=root // authorized } func ptr(i uint8) *uint8 { return &i } type testVector struct { index int alice string bob string keyIndex *uint8 messageTagPrefixLength uint8 minResponseLength *uint8 msg *v2.Message challenge string response string } var testVectors = []testVector{ { index: 1, alice: "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a", bob: "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb", keyIndex: ptr(0), messageTagPrefixLength: 3, minResponseLength: ptr(12), msg: &v2.Message{ HostIDType: "mytype", HostID: "myhost", Action: "root", }, challenge: "v2/gIUg8AmJMKdUdIt93LQ-91oNvzoNJjga9OukqY6qm05qlyPH/mytype:myhost/root/", response: "BB4BYjXonlIRtXZORkQ5bF5xTZwW6o60ylqfCuyAHTQ=", }, { index: 2, alice: "fee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1dead", bob: "b105f00db105f00db105f00db105f00db105f00db105f00db105f00db105f00d", msg: &v2.Message{ HostID: "myhost", Action: "exec=/bin/sh", }, challenge: "v2/R4cvQ1u4uJ0OOtYqouURB07hleHDnvaogAFBi-ZW48N2/myhost/exec=%2Fbin%2Fsh/", response: "ZmxczN4x3g4goXu-A2AuuEEVftgS6xM-6gYj-dRrlis=", }, } func TestSpec(t *testing.T) { for _, tc := range testVectors { t.Run(fmt.Sprintf("vector-%d", tc.index), func(t *testing.T) { alice := unhexPrivateKey(tc.alice) bob := unhexPrivateKey(tc.bob) bobPub, err := bob.Public() if err != nil { t.Fatalf("private key from test vector broken: %v", err) } c := &v2.Challenger{ PublicKey: bobPub, KeyIndex: tc.keyIndex, MessageTagPrefixLength: tc.messageTagPrefixLength, RNG: bytes.NewBuffer(alice[:]), } if tc.minResponseLength != nil { c.MinResponseLength = *tc.minResponseLength } cc, err := c.Challenge(tc.msg) if err != nil { t.Fatalf("Challenge generation failed: %v", err) } gotChallenge := cc.Encode() if gotChallenge != tc.challenge { t.Errorf("Unexpected encoding:\ngot:\t%s\nwant:\t%s", gotChallenge, tc.challenge) } r, err := v2.NewResponder(map[uint8]*glome.PrivateKey{0: bob}) if err != nil { t.Fatalf("NewResponder() failed: %v", err) } sc, err := r.Accept(tc.challenge) if err != nil { t.Fatalf("Accept(%q) failed: %v", gotChallenge, err) } if !reflect.DeepEqual(tc.msg, sc.Message) { t.Errorf("Responder parsed wrong message: got %#v, want %#v", sc.Message, tc.msg) } if sc.Response != tc.response { t.Errorf("Responder generated wrong response: got %q, want %q", sc.Response, tc.response) } min := 10 if tc.minResponseLength != nil { min = int(*tc.minResponseLength) } // Tags shorter than min must be rejected. for i := 1; i < min; i++ { if cc.Verify(tc.response[:i]) { t.Errorf("Verification succeeded with response length %d, although the minimum is %d", i, min) } } // Long enough tags must not be rejected. for i := min; i <= len(tc.response); i++ { if !cc.Verify(tc.response[:i]) { t.Errorf("Verification failed with %d characters, although the minimum is %d", i, min) } } }) } } glome-0.2/go/login/v2/responder.go000066400000000000000000000064231476056666600171050ustar00rootroot00000000000000package v2 import ( "encoding/base64" "errors" "fmt" "strings" "github.com/google/glome/go/glome" ) const versionPrefix = "v2/" // Responder can parse challenges and create responses. // // Instances of Responder must be created with NewResponder(). type Responder struct { keysByIndex map[uint8]*glome.PrivateKey keysByPrefix map[byte]*glome.PrivateKey } // NewResponder creates a Responder that uses the given private keys to respond // to challenges. func NewResponder(keys map[uint8]*glome.PrivateKey) (*Responder, error) { r := &Responder{ keysByIndex: make(map[uint8]*glome.PrivateKey), keysByPrefix: make(map[byte]*glome.PrivateKey), } for i, k := range keys { if i >= 1<<7 { return nil, fmt.Errorf("key index %d is not in range [0; 127]", i) } pk, err := k.Public() if err != nil { return nil, fmt.Errorf("invalid private key at index %d: %w", i, err) } r.keysByIndex[i] = k // We _could_ validate that prefixes are unique here, but we choose not to. r.keysByPrefix[pk[glome.PublicKeySize-1]] = k } return r, nil } // ServerChallenge contains the parsed Message from a challenge and an // appropriate response. // // The Response must only be used after verifying the message content! // // Instances of ServerChallenge should be created by Responder.Accept(). type ServerChallenge struct { Message *Message Response string } // Accept an encoded challenge and produce a response. func (r *Responder) Accept(encodedChallenge string) (*ServerChallenge, error) { s := strings.TrimPrefix(encodedChallenge, "/") if len(s) < len(versionPrefix) { return nil, errors.New("challenge format error: too short") } if s[:len(versionPrefix)] != versionPrefix { return nil, fmt.Errorf("challenge version incompatible: expected %q, got %q", versionPrefix, s[:len(versionPrefix)]) } s = strings.TrimPrefix(s, versionPrefix) s = strings.TrimSuffix(s, "/") subs := strings.SplitN(s, "/", 2) if len(subs) != 2 { return nil, errors.New("challenge format error: wrong number of path segments") } h, err := decodeHandshake(subs[0]) if err != nil { return nil, err } encodedMessage := []byte(subs[1]) m, err := decodeMessage(subs[1]) if err != nil { return nil, err } var key *glome.PrivateKey ok := false if h.Prefix != nil { key, ok = r.keysByPrefix[*h.Prefix] } else { key, ok = r.keysByIndex[h.Index] } if !ok { return nil, &keyNotFoundError{h} } d, err := key.TruncatedExchange(h.PublicKey, 1) if err != nil { return nil, err } if len(h.MessageTagPrefix) > 0 && !d.Check(h.MessageTagPrefix, encodedMessage, 0) { return nil, ErrTagPrefixMismatch } tag := d.Tag(encodedMessage, 0) return &ServerChallenge{ Message: m, Response: base64.URLEncoding.EncodeToString(tag), }, nil } type keyNotFoundError struct { h *handshake } func (e *keyNotFoundError) Error() string { if e.h.Prefix != nil { return fmt.Sprintf("no key found with prefix 0x%02x", *e.h.Prefix) } return fmt.Sprintf("no key found with index %d", e.h.Index) } // ErrTagPrefixMismatch is returned when a tag prefix is included in the // challenge, but it does not verify with the chosen key. This means that the // public key chosen based on handshake information is not the one the client // expected. var ErrTagPrefixMismatch = errors.New("message tag prefix did not match") glome-0.2/kokoro/000077500000000000000000000000001476056666600140105ustar00rootroot00000000000000glome-0.2/kokoro/alpine/000077500000000000000000000000001476056666600152605ustar00rootroot00000000000000glome-0.2/kokoro/alpine/fetch_dependencies.sh000077500000000000000000000001411476056666600214120ustar00rootroot00000000000000#!/bin/sh set -e apk add --no-cache \ alpine-sdk meson \ openssl-dev glib-dev linux-pam-dev glome-0.2/kokoro/docker/000077500000000000000000000000001476056666600152575ustar00rootroot00000000000000glome-0.2/kokoro/docker/Dockerfile000066400000000000000000000011501476056666600172460ustar00rootroot00000000000000FROM docker.io/library/debian:bullseye AS build WORKDIR /app COPY . . RUN kokoro/rodete/fetch_dependencies.sh RUN rm -rf build \ && meson build \ && meson compile -C build \ && meson test --print-errorlogs -C build \ && meson install -C build FROM docker.io/library/debian:bullseye COPY --from=build /usr/local /usr/local COPY kokoro/docker/glome-start /usr/local/sbin RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends \ openssh-server \ socat \ && rm -rf /var/lib/apt/lists/* CMD ["/usr/local/sbin/glome-start"] EXPOSE 22 23 glome-0.2/kokoro/docker/glome-start000077500000000000000000000011601476056666600174410ustar00rootroot00000000000000#!/bin/sh GLOME=/usr/local/bin/glome GLOME_LOGIN=/usr/local/sbin/glome-login CONFIG=/usr/local/etc/glome/config PRIVATE=/usr/local/etc/glome/private.key umask 077 PUBLIC_KEY=$($GLOME genkey | tee $PRIVATE | $GLOME pubkey) sed -i "s/^#public-key = .*/public-key = $PUBLIC_KEY/" $CONFIG sed -i "1 i\auth sufficient /usr/local/lib/x86_64-linux-gnu/security/pam_glome.so" /etc/pam.d/sshd cat < /etc/ssh/sshd_config.d/glome.conf ChallengeResponseAuthentication yes PermitRootLogin yes EOF mkdir /run/sshd /usr/sbin/sshd socat tcp-l:23,reuseaddr,fork exec:"/sbin/agetty -l $GLOME_LOGIN -",pty,setsid,setpgid,stderr,ctty glome-0.2/kokoro/rodete/000077500000000000000000000000001476056666600152725ustar00rootroot00000000000000glome-0.2/kokoro/rodete/fetch_dependencies.sh000077500000000000000000000003431476056666600214300ustar00rootroot00000000000000#!/bin/bash set -e export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y --no-install-recommends \ build-essential meson pkg-config \ libssl-dev libglib2.0-dev libpam0g-dev libpam-wrapper libpamtest0-dev glome-0.2/login/000077500000000000000000000000001476056666600136145ustar00rootroot00000000000000glome-0.2/login/README.md000066400000000000000000000071121476056666600150740ustar00rootroot00000000000000# glome-login This binary implements the client side of the [GLOME Login](../docs/glome-login.md) protocol. It is written to be a replacement of login(1). ## Usage 1. Create a configuration file, see [example.cfg](example.cfg). 1. Try it out by running `glome-login -c glome.cfg -- root` ## Configuration In order to reduce external dependencies, a custom parser is used to read the configuration file. The parser supports a simplified version of the INI syntax with the following limitations: * Quoting and escaping is not supported. * Comments are allowed only at the start of the line and can begin with either `#` or `;`. ## Installation The installation is dependent on what system you are running. ### systemd Create a override file for the getty instance e.g. in `/etc/systemd/system/serial-getty@.service.d/glome.conf`. ``` [Service] ExecStart= ExecStart=-/sbin/agetty -l /usr/local/sbin/glome-login \ -o '-- \\u' --keep-baud 115200,38400,9600 %I $TERM ``` Alternatively or for a normal VTY, use `/etc/systemd/system/getty@.service.d/glome.conf`. ``` [Service] ExecStart= ExecStart=-/sbin/agetty -l /usr/local/sbin/glome-login \ -o '-- \\u' --noclear %I $TERM ``` ## Troubleshooting glome-login uses error tags to communicate errors. ### no-service-key This error means that `glome-login` could not figure out what service key to use. This most likely means that you have not specified a service key in the configuration file (by default `/etc/glome/config`). # PAM module `pam_glome.so` library implements the PAM authentication module for the [GLOME Login](../docs/glome-login.md) protocol. ## Installation 1. Install the library into the system dependent location for PAM modules (for example `/lib/security/pam_glome.so`). 1. Enable and configure PAM module for a specific service (for example `/etc/pam.d/login`): ``` auth requisite pam_glome.so ``` ## Usage PAM module supports the following options: * `config_path=PATH` - location of the configuration file to parse (defaults to `/etc/glome/config`) * `key=KEY` - use hex-encoded `KEY` as the service key (defaults to key from configuration file) * `key_version=N` - use `N` for the service key version (defaults to key version from configuration file) * `prompt=PROMPT` - challenge prompt (defaults to prompt from configuration file) * `debug` - enable verbose logging * `print_secrets` - enable logging of secrets (INSECURE!) * `host_id=NAME` - use `NAME` as the host-id * `ephemeral_key=KEY` - use hex-encoded `KEY` instead of the ephemeral secret key (INSECURE!) ## Troubleshooting PAM module uses error tags to communicate errors in the syslog messages. # Docker Dockerfile included in the repository creates a Docker image that can be used to test `glome-login` and the PAM module. ## Instalation Docker image for GLOME needs to be built first using the following command: ``` $ docker build -t glome -f kokoro/docker/Dockerfile . ``` ## Usage Container is than started in the background with two TCP ports published to the host: ``` $ container=$(docker run -d -p 2022:22 -p 2023:23 glome) ``` Once the container is running it is possible to login using `netcat` or `socat`, for example: ``` $ socat tcp-connect:localhost:2023 file:`tty`,raw,echo=0 ``` Regular SSH client can be used for testing the PAM module: ``` $ ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p 2022 root@localhost ``` Authorization code required for GLOME Login can be obtained by running: ``` $ docker exec $container /usr/local/bin/glome login --key /usr/local/etc/glome/private.key https://glome.example.com/v1/... ``` glome-0.2/login/base64.h000066400000000000000000000022411476056666600150500ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #ifndef LOGIN_BASE64_H_ #define LOGIN_BASE64_H_ #include #include // Base64 needs 4 bytes for every 3 bytes of input (+ padding + NULL byte) // NOTE: Caller is responsible for protecting against integer overflow. #define ENCODED_BUFSIZE(n) ((((n) + 2) / 3) * 4 + 1) #define DECODED_BUFSIZE(n) ((((n) * 3) / 4)) size_t base64url_encode(const uint8_t* src, size_t src_len, uint8_t* dst, size_t dst_len); size_t base64url_decode(const uint8_t* src, size_t src_len, uint8_t* dst, size_t dst_len); #endif // LOGIN_BASE64_H_ glome-0.2/login/config.c000066400000000000000000000266301476056666600152340ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "config.h" #include #include #include #include #include #include #include #include "base64.h" #include "ui.h" static bool is_empty(const char *line) { for (; isspace(*line); line++) { } return *line == '\0'; } static bool is_comment(const char *line) { return line[0] == '#' || line[0] == ';'; } static bool is_section(const char *line) { return line[0] == '['; } static bool is_name(const char *name) { const char *p; for (p = name; isalnum(*p) || *p == '_' || *p == '-'; p++) { } return *p == '\0' && p - name > 0; } static char *section_name(char *line) { char *p; for (p = line + 1; *p != ']' && *p != '\0'; p++) { } if (*p != ']') { return NULL; } char *end = p; if (!is_empty(p + 1)) { return NULL; } *end = '\0'; if (is_name(line + 1)) { return line + 1; } return NULL; } static void key_value(char *line, char **key, char **val) { *key = NULL; *val = NULL; char *p; for (p = line; !isspace(*p) && *p != '=' && *p != '\0'; p++) { } if (*p == '\0') { return; } char *end = p; for (; isspace(*p); p++) { } if (*p != '=') { return; } for (p++; isspace(*p); p++) { } if (*p == '\0') { return; } *end = '\0'; if (!is_name(line)) { return; } // Trim whitespace at the end of the value. int k = strlen(p) - 1; for (; k >= 0 && isspace(p[k]); k--) { } p[k + 1] = '\0'; *key = line; *val = p; } bool glome_login_parse_public_key(const char *encoded_key, uint8_t *public_key, size_t public_key_size) { if (public_key_size < GLOME_MAX_PUBLIC_KEY_LENGTH) { errorf("ERROR: provided buffer has size %zu, need at least %d\n", public_key_size, GLOME_MAX_PUBLIC_KEY_LENGTH); return false; } size_t prefix_length = strlen(GLOME_LOGIN_PUBLIC_KEY_ID); if (strncmp(encoded_key, GLOME_LOGIN_PUBLIC_KEY_ID, prefix_length)) { errorf("ERROR: unsupported public key encoding: %s\n", encoded_key); return false; } // Advance to the start of the base64-encoded key. encoded_key += prefix_length; while (*encoded_key != '\0' && isblank(*encoded_key)) { encoded_key++; } // Truncate the encoded string to allow for appended comments. size_t encoded_length = 0; while (isgraph(encoded_key[encoded_length])) { encoded_length++; } // Unfortunately we need an extra byte because 32B don't pack cleanly in // base64. uint8_t buf[GLOME_MAX_PUBLIC_KEY_LENGTH + 1] = {0}; size_t b = base64url_decode((uint8_t *)encoded_key, encoded_length, buf, sizeof(buf)); if (b != GLOME_MAX_PUBLIC_KEY_LENGTH) { errorf("ERROR: public key decoded to %zu bytes, expected %d\n", b, GLOME_MAX_PUBLIC_KEY_LENGTH); return false; } memcpy(public_key, buf, GLOME_MAX_PUBLIC_KEY_LENGTH); return true; } static status_t assign_string_option(const char **option, const char *val) { const char *copy = strdup(val); if (copy == NULL) { return status_createf("ERROR: failed to allocate memory for value: %s", val); } *option = copy; return STATUS_OK; } static status_t assign_positive_int_option(unsigned int *option, const char *val) { char *end; errno = 0; unsigned long n = strtoul(val, &end, 0); // NOLINT(runtime/int) if (errno || val == end || *end != '\0' || n > UINT_MAX) { return status_createf("ERROR: invalid value for option: %s", val); } *option = (unsigned int)n; return STATUS_OK; } static status_t set_bitfield_option(glome_login_config_t *config, uint8_t bit) { config->options |= bit; return STATUS_OK; } static status_t clear_bitfield_option(glome_login_config_t *config, uint8_t bit) { config->options &= ~bit; return STATUS_OK; } static bool boolean_true(const char *val) { if (strcasecmp(val, "true") == 0) { return true; } else if (strcasecmp(val, "yes") == 0) { return true; } else if (strcasecmp(val, "on") == 0) { return true; } else if (strcmp(val, "1") == 0) { return true; } return false; } static bool boolean_false(const char *val) { if (strcasecmp(val, "false") == 0) { return true; } else if (strcasecmp(val, "no") == 0) { return true; } else if (strcasecmp(val, "off") == 0) { return true; } else if (strcmp(val, "0") == 0) { return true; } return false; } static status_t update_bitfield_option(glome_login_config_t *config, uint8_t bit, bool invert, const char *val) { if (boolean_true(val)) { if (invert) { return clear_bitfield_option(config, bit); } else { return set_bitfield_option(config, bit); } } else if (boolean_false(val)) { if (invert) { return set_bitfield_option(config, bit); } else { return clear_bitfield_option(config, bit); } } else { return status_createf("ERROR: unrecognized boolean value: %s", val); } } static status_t assign_key_option(uint8_t *dest, size_t dest_len, const char *val) { if (is_zeroed(dest, dest_len)) { if (decode_hex(dest, dest_len, val)) { return status_createf("ERROR: failed to hex decode service key: %s", val); } } return STATUS_OK; } static status_t assign_key_version_option(glome_login_config_t *config, const char *val) { char *end; errno = 0; unsigned long n = strtoul(val, &end, 0); // NOLINT(runtime/int) if (errno || val == end || *end != '\0' || n > 127) { return status_createf("ERROR: '%s' is not a valid key version (0..127)", val); } config->service_key_id = (unsigned int)n; return STATUS_OK; } static status_t assign_default_option(glome_login_config_t *config, const char *key, const char *val) { if (strcmp(key, "auth-delay") == 0) { return assign_positive_int_option(&config->auth_delay_sec, val); } else if (strcmp(key, "input-timeout") == 0) { return assign_positive_int_option(&config->input_timeout_sec, val); } else if (strcmp(key, "config-path") == 0) { return assign_string_option(&config->config_path, val); } else if (strcmp(key, "ephemeral-key") == 0) { return assign_key_option(config->secret_key, sizeof config->secret_key, val); } else if (strcmp(key, "min-authcode-len") == 0) { return assign_positive_int_option(&config->min_authcode_len, val); } else if (strcmp(key, "host-id") == 0) { return assign_string_option(&config->host_id, val); } else if (strcmp(key, "host-id-type") == 0) { return assign_string_option(&config->host_id_type, val); } else if (strcmp(key, "login-path") == 0) { return assign_string_option(&config->login_path, val); } else if (strcmp(key, "disable-syslog") == 0) { return update_bitfield_option(config, SYSLOG, true, val); } else if (strcmp(key, "print-secrets") == 0) { return update_bitfield_option(config, INSECURE, false, val); } else if (strcmp(key, "timeout") == 0) { return assign_positive_int_option(&config->input_timeout_sec, val); } else if (strcmp(key, "verbose") == 0) { return update_bitfield_option(config, VERBOSE, false, val); } return status_createf("ERROR: unrecognized default option: %s", key); } static status_t assign_service_option(glome_login_config_t *config, const char *key, const char *val) { if (strcmp(key, "key") == 0) { return assign_key_option(config->service_key, sizeof config->service_key, val); } else if (strcmp(key, "key-version") == 0) { return assign_key_version_option(config, val); } else if (strcmp(key, "url-prefix") == 0) { // `url-prefix` support is provided only for backwards-compatiblity // TODO: to be removed in the 1.0 release size_t len = strlen(val); char *url_prefix = malloc(len + 2); if (url_prefix == NULL) { return status_createf("ERROR: failed to allocate memory for url_prefix"); } strncpy(url_prefix, val, len + 1); url_prefix[len] = '/'; url_prefix[len + 1] = '\0'; config->prompt = url_prefix; return STATUS_OK; } else if (strcmp(key, "prompt") == 0) { return assign_string_option(&config->prompt, val); } else if (strcmp(key, "public-key") == 0) { if (!glome_login_parse_public_key(val, config->service_key, sizeof(config->service_key))) { return status_createf("ERROR: failed to decode public-key"); } return STATUS_OK; } return status_createf("ERROR: unrecognized service option: %s", key); } status_t glome_login_assign_config_option(glome_login_config_t *config, const char *section, const char *key, const char *val) { if (section == NULL) { return status_createf("ERROR: config section not set"); } if (key == NULL) { return status_createf("ERROR: config key not set"); } if (val == NULL) { return status_createf("ERROR: config value not set"); } if (strcmp(section, "service") == 0) { return assign_service_option(config, key, val); } else if (strcmp(section, "default") == 0) { return assign_default_option(config, key, val); } return status_createf("ERROR: config section not recognized: %s", section); } status_t glome_login_parse_config_file(glome_login_config_t *config) { bool required = config->config_path != NULL; if (!required) { config->config_path = DEFAULT_CONFIG_FILE; } FILE *f = fopen(config->config_path, "r"); if (f == NULL) { if (!required) { return 0; } return status_createf("ERROR: config file could not be opened: %s\n", strerror(errno)); } char *line = NULL; char *section = NULL; char *key, *val; size_t len = 0; size_t lines = 0; status_t status = STATUS_OK; while (getline(&line, &len, f) != -1) { lines++; if (is_empty(line) || is_comment(line)) { continue; } else if (is_section(line)) { char *s = section_name(line); if (s == NULL) { status = status_createf( "ERROR: config file parsing failed in line %ld (bad section " "name)\n", lines); break; } free(section); section = strdup(s); } else { key_value(line, &key, &val); if (key == NULL || val == NULL) { status = status_createf( "ERROR: config file parsing failed in line %ld (bad key/value)\n", lines); break; } status = glome_login_assign_config_option( config, section ? section : "default", key, val); if (status != STATUS_OK) { break; } } } free(line); free(section); fclose(f); return status; } glome-0.2/login/config.h000066400000000000000000000060371476056666600152400ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #ifndef LOGIN_CONFIG_H_ #define LOGIN_CONFIG_H_ #include "crypto.h" typedef struct glome_login_config { // Bitfield of options as described above. uint8_t options; // Username to log in as. const char* username; // Configuration file to parse. const char* config_path; // Login binary for fallback authentication. const char* login_path; // Challenge prompt. const char* prompt; // Delay to wait before confirming if the authentication code is valid // or not, to stop brute forcing; in seconds. unsigned int auth_delay_sec; // How long to wait for authentication code input in seconds. unsigned int input_timeout_sec; // Minimum required length of the encoded authentication code. unsigned int min_authcode_len; // Service key of the remote peer. uint8_t service_key[PUBLIC_KEY_LENGTH]; // ID of the service key of the remote peer. (Optional) uint8_t service_key_id; // Local ephemeral secret key. uint8_t secret_key[PRIVATE_KEY_LENGTH]; // Explicitly set host-id to use in the login request. const char* host_id; // Type of host-id to use in the login request. const char* host_id_type; } glome_login_config_t; #define GLOME_LOGIN_PUBLIC_KEY_ID "glome-v1" // glome_login_parse_public_key extracts the public key bytes from an encoded // public key. // Returns true on success. bool glome_login_parse_public_key(const char* encoded_key, uint8_t* public_key, size_t public_key_size); // Error message returned by the config functions. If no error ocurred // return value will be set to STATUS_OK. typedef char* status_t; // Allocate and format an error message. status_t status_createf(const char* format, ...); // Free an error message after it is not needed anymore. void status_free(status_t status); // If no error occurred the value of returned error message will be STATUS_OK. #define STATUS_OK NULL // glome_login_parse_config_file parses the configuration file and fills the // given config struct with the data. The default config file is used in case // no explicit config file has been provided, however in this case failed // attempts to read the default config file will be ignored. status_t glome_login_parse_config_file(glome_login_config_t* config); status_t glome_login_assign_config_option(glome_login_config_t* config, const char* section, const char* key, const char* val); #endif // LOGIN_CONFIG_H_ glome-0.2/login/config_test.c000066400000000000000000000067111476056666600162710ustar00rootroot00000000000000// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "config.h" #include #include #include "ui.h" static const char* ENCODED_PUBLIC_KEY = "glome-v1 aqA9yqe1RXoOT6HrmCbF40wVUhYp50FYZR9q8_X5KF4="; static const uint8_t DECODED_PUBLIC_KEY[32] = { 0x6a, 0xa0, 0x3d, 0xca, 0xa7, 0xb5, 0x45, 0x7a, 0x0e, 0x4f, 0xa1, 0xeb, 0x98, 0x26, 0xc5, 0xe3, 0x4c, 0x15, 0x52, 0x16, 0x29, 0xe7, 0x41, 0x58, 0x65, 0x1f, 0x6a, 0xf3, 0xf5, 0xf9, 0x28, 0x5e}; static void test_parse_public_key(void) { uint8_t decoded[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; g_assert_true(glome_login_parse_public_key(ENCODED_PUBLIC_KEY, decoded, sizeof(decoded))); g_assert_cmpmem(decoded, sizeof(decoded), DECODED_PUBLIC_KEY, sizeof(DECODED_PUBLIC_KEY)); g_assert_false(glome_login_parse_public_key(ENCODED_PUBLIC_KEY, decoded, sizeof(decoded) - 1)); g_assert_false(glome_login_parse_public_key( "glome-group1-md5 QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE=", decoded, sizeof(decoded))); g_assert_false(glome_login_parse_public_key("glome-v1 QUFBQUFBQUFB", decoded, sizeof(decoded))); memset(decoded, 0, sizeof(decoded)); const char* extra_chars = "glome-v1 \t aqA9yqe1RXoOT6HrmCbF40wVUhYp50FYZR9q8_X5KF4= " "root@localhost"; g_assert_true( glome_login_parse_public_key(extra_chars, decoded, sizeof(decoded))); g_assert_cmpmem(decoded, sizeof(decoded), DECODED_PUBLIC_KEY, sizeof(DECODED_PUBLIC_KEY)); } static char* EXAMPLE_CFG = NULL; static void test_parse_config_file(void) { g_assert_true(EXAMPLE_CFG != NULL); glome_login_config_t config = {0}; default_config(&config); config.config_path = EXAMPLE_CFG; status_t s = glome_login_parse_config_file(&config); if (s) { fprintf(stderr, "glome_login_parse_config_file returned error: %s\n", s); } g_assert_true(s == STATUS_OK); g_assert_true(config.auth_delay_sec == 7); g_assert_true(config.min_authcode_len == 15); g_assert_true(config.input_timeout_sec == 321); g_assert_cmpstr("/bin/true", ==, config.login_path); g_assert_cmpstr("my-host", ==, config.host_id); g_assert_cmpstr("hostname", ==, config.host_id_type); g_assert_true(config.options & VERBOSE); g_assert_false(config.options & SYSLOG); g_assert_false(config.options & INSECURE); g_assert_cmpmem(DECODED_PUBLIC_KEY, sizeof(DECODED_PUBLIC_KEY), config.service_key, GLOME_MAX_PUBLIC_KEY_LENGTH); g_assert_true(config.service_key_id == 42); g_assert_cmpstr("glome://", ==, config.prompt); } int main(int argc, char** argv) { g_test_init(&argc, &argv, NULL); g_assert_true(argc > 1); EXAMPLE_CFG = argv[1]; g_test_add_func("/test-parse-public-key", test_parse_public_key); g_test_add_func("/test-parse-config-file", test_parse_config_file); return g_test_run(); } glome-0.2/login/config_test.cfg000066400000000000000000000005771476056666600166120ustar00rootroot00000000000000auth-delay = 7 min-authcode-len = 15 input-timeout = 321 host-id = my-host host-id-type = hostname login-path = /bin/true disable-syslog = yes print-secrets = 0 verbose = true [service] # Corresponding private key: 6aa03dcaa7b5457a0e4fa1eb9826c5e34c15521629e74158651f6af3f5f9285e public-key = glome-v1 aqA9yqe1RXoOT6HrmCbF40wVUhYp50FYZR9q8_X5KF4= key-version = 42 prompt = glome:// glome-0.2/login/config_test_url-prefix.cfg000066400000000000000000000006021476056666600207540ustar00rootroot00000000000000auth-delay = 7 min-authcode-len = 15 input-timeout = 321 host-id = my-host host-id-type = hostname login-path = /bin/true disable-syslog = yes print-secrets = 0 verbose = true [service] # Corresponding private key: 6aa03dcaa7b5457a0e4fa1eb9826c5e34c15521629e74158651f6af3f5f9285e public-key = glome-v1 aqA9yqe1RXoOT6HrmCbF40wVUhYp50FYZR9q8_X5KF4= key-version = 42 url-prefix = glome:/ glome-0.2/login/crypto.c000066400000000000000000000072371476056666600153110ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "crypto.h" #include #include #include #include #include int is_zeroed(const uint8_t* buf, size_t len) { int sum = 0; while (len > 0) { sum |= buf[--len]; } return sum == 0; } int derive_or_generate_key(uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH], uint8_t public_key[GLOME_MAX_PUBLIC_KEY_LENGTH]) { if (is_zeroed(private_key, PRIVATE_KEY_LENGTH)) { // New key pair needs to be generated... return glome_generate_key(private_key, public_key); } else { // ... unless a non-zero private key is provided. return glome_derive_key(private_key, public_key); } } 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; } } 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; } } 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; } message = calloc(message_len, 1); if (message == NULL) { goto end; } 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); end: free(host_id_type_escaped); free(host_id_escaped); free(action_escaped); return message; } glome-0.2/login/crypto.h000066400000000000000000000033721476056666600153120ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #ifndef LOGIN_CRYPTO_H_ #define LOGIN_CRYPTO_H_ #include #include #include #define PUBLIC_KEY_LENGTH 32 #define PRIVATE_KEY_LENGTH 32 #define SHARED_KEY_LENGTH 32 // Given a private key, derive the corresponding public key. // If given an private key consisting of all zeroes a new private // key will be generated in addition to the public key derivation. int derive_or_generate_key(uint8_t private_key[PRIVATE_KEY_LENGTH], uint8_t public_key[PUBLIC_KEY_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_ glome-0.2/login/crypto_test.c000066400000000000000000000062511476056666600163430ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "crypto.h" #include #include #include #include #include "base64.h" #include "login.h" static void test_derive(void) { uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH] = {0}; uint8_t public_key[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; uint8_t expected_public_key[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; decode_hex( private_key, sizeof private_key, "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"); decode_hex( expected_public_key, sizeof expected_public_key, "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"); g_assert_true(derive_or_generate_key(private_key, public_key) == 0); g_assert_cmpmem(expected_public_key, sizeof expected_public_key, public_key, sizeof public_key); } static void test_generate(void) { uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH] = {0}; uint8_t public_key[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; uint8_t empty_public_key[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; uint8_t empty_private_key[GLOME_MAX_PRIVATE_KEY_LENGTH] = {0}; g_assert_true(derive_or_generate_key(private_key, public_key) == 0); g_assert_true(memcmp(empty_public_key, public_key, sizeof empty_public_key)); g_assert_true( memcmp(empty_private_key, private_key, sizeof empty_private_key)); } static void test_authcode(void) { const char* host_id = "myhost"; const char* action = "exec=/bin/sh"; uint8_t service_key[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH] = {0}; decode_hex( private_key, sizeof private_key, "fee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1dead"); decode_hex( service_key, sizeof service_key, "d1b6941bba120bcd131f335da15778d9c68dadd398ae61cf8e7d94484ee65647"); uint8_t authcode[GLOME_MAX_TAG_LENGTH]; uint8_t expected_authcode[GLOME_MAX_TAG_LENGTH]; decode_hex( expected_authcode, sizeof expected_authcode, "666c5cccde31de0e20a17bbe03602eb841157ed812eb133eea0623f9d46b962b"); char* message = glome_login_message(/*host_id_type=*/NULL, host_id, action); g_assert_nonnull(message); g_assert(glome_tag(true, 0, private_key, service_key, (uint8_t*)message, strlen(message), authcode) == 0); g_assert_cmpmem(expected_authcode, sizeof expected_authcode, authcode, sizeof authcode); } int main(int argc, char** argv) { g_test_init(&argc, &argv, NULL); g_test_add_func("/test-derive", test_derive); g_test_add_func("/test-generate", test_generate); g_test_add_func("/test-authcode", test_authcode); return g_test_run(); } glome-0.2/login/example.cfg000066400000000000000000000006671476056666600157410ustar00rootroot00000000000000[service] # Replace this with your own GLOME server-side public key. # # This can be generated as any other GLOME key, where the private key is # given to the server and the public key is distributed to all clients # through this configuration file. # # For more details about the format of this file see README.md. # #public-key = glome-v1 5UvwOKcv-6n_1bo7UAA7-XVqf9ggzHQsbaxptXNsagg= key-version = 1 #prompt = https://glome.example.com/ glome-0.2/login/login.c000066400000000000000000000372531476056666600151020ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "login.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "base64.h" #include "crypto.h" #include "ui.h" #define PROMPT "> " #define BRACKETED_PASTE_FINISH "\033[?2004l\r" #define DMI_UUID_PATH "/sys/class/dmi/id/product_uuid" #define DMI_UUID_SIZE 36 #define UNUSED(var) (void)(var) static int get_hostname(char* buf, size_t buflen) { if (gethostname(buf, buflen) != 0) { return -1; } buf[buflen - 1] = '\0'; // Regular hostname is likely fully qualified, so stop here and return it. if (strchr(buf, '.') != NULL) { return 0; } // Retry using getaddrinfo to get an FQDN. struct addrinfo* res = NULL; struct addrinfo hints; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_socktype = SOCK_DGRAM; hints.ai_flags = AI_CANONNAME; int ret; if ((ret = getaddrinfo(buf, NULL, &hints, &res)) != 0) { return -1; } strncpy(buf, res->ai_canonname, buflen - 1); buf[buflen - 1] = '\0'; freeaddrinfo(res); return 0; } int failure(int code, const char** error_tag, const char* message) { if (error_tag != NULL && *error_tag == NULL) { *error_tag = message; } return code; } int get_machine_id(char* buf, size_t buflen, const char** error_tag) { if (get_hostname(buf, buflen) == 0) { return 0; } if (DMI_UUID_SIZE + 1 > buflen) { return failure(EXITCODE_PANIC, error_tag, "dmi-uuid-size"); } FILE* fd; fd = fopen(DMI_UUID_PATH, "r"); if (fd != NULL) { errorf("Unable to obtain hostname. Using DMI UUID instead.\n"); if (fread(buf, DMI_UUID_SIZE, 1, fd) == 1) { buf[DMI_UUID_SIZE] = '\0'; fclose(fd); return 0; } errorf("ERROR reading DMI product UUID (eof=%d, err=%d)\n", feof(fd), ferror(fd)); fclose(fd); } else { perror("ERROR opening DMI product UUID file"); } return -1; } void timeout_handler(int sig) { UNUSED(sig); errorf("Timed out while waiting for user input.\n"); exit(EXITCODE_TIMEOUT); } int shell_action(const char* user, char** action, size_t* action_len, const char** error_tag) { size_t buf_len = strlen("shell=") + strlen(user) + 1; char* buf = calloc(buf_len, 1); if (buf == NULL) { return failure(EXITCODE_PANIC, error_tag, "message-calloc-error"); } int ret = snprintf(buf, buf_len, "shell=%s", user); if (ret < 0) { free(buf); return failure(EXITCODE_PANIC, error_tag, "message-sprintf-error"); } if ((size_t)ret >= buf_len) { free(buf); return failure(EXITCODE_PANIC, error_tag, "message-sprintf-trunc"); } *action = buf; *action_len = buf_len; return 0; } int request_challenge(const uint8_t service_key[GLOME_MAX_PUBLIC_KEY_LENGTH], int service_key_id, const uint8_t public_key[PUBLIC_KEY_LENGTH], const char* message, const uint8_t prefix_tag[GLOME_MAX_TAG_LENGTH], size_t prefix_tag_len, char** challenge, const char** error_tag) { if (prefix_tag_len > GLOME_MAX_TAG_LENGTH) { return failure(EXITCODE_PANIC, error_tag, "prefix-tag-too-large"); } // glome-handshake := base64url( // // // // [] //) uint8_t handshake[PUBLIC_KEY_LENGTH + 1 + GLOME_MAX_TAG_LENGTH] = {0}; size_t handshake_len = PUBLIC_KEY_LENGTH + 1 + prefix_tag_len; if (service_key_id < 0 || service_key_id > 127) { // If no key ID was specified, send the most significant key byte as the ID. handshake[0] = service_key[GLOME_MAX_PUBLIC_KEY_LENGTH - 1]; // Indicate 'service key prefix' by setting the high bit 0. handshake[0] &= 0x7f; } else { // handshake[0] = (uint8_t)service_key_id; // Indicate 'service key index' by setting the high bit 1. handshake[0] |= 0x80; } memcpy(handshake + 1, public_key, PUBLIC_KEY_LENGTH); if (prefix_tag_len > 0) { memcpy(handshake + PUBLIC_KEY_LENGTH + 1, prefix_tag, prefix_tag_len); } char handshake_encoded[ENCODED_BUFSIZE(sizeof handshake)] = {0}; if (!base64url_encode(handshake, handshake_len, (uint8_t*)handshake_encoded, sizeof handshake_encoded)) { return failure(EXITCODE_PANIC, error_tag, "handshake-encode"); } // Compute the required buffer length for the concatenated challenge string: // "v2/" ++ handshake_encoded ++ "/" ++ message ++ "/\x00" int len = strlen("v2/") + strlen(handshake_encoded) + strlen(message) + 3; char* buf = calloc(len, 1); if (buf == NULL) { return failure(EXITCODE_PANIC, error_tag, "challenge-malloc-error"); } *challenge = buf; buf = stpcpy(buf, "v2/"); buf = stpcpy(buf, handshake_encoded); buf = stpcpy(buf, "/"); buf = stpcpy(buf, message); buf = stpcpy(buf, "/"); return 0; } #ifndef PAM_GLOME void login_error(glome_login_config_t* config, pam_handle_t* pamh, const char* format, ...) { UNUSED(config); UNUSED(pamh); va_list args; va_start(args, format); vfprintf(stderr, format, args); va_end(args); fflush(NULL); } void login_syslog(glome_login_config_t* config, pam_handle_t* pamh, int priority, const char* format, ...) { UNUSED(pamh); if (config->options & SYSLOG) { const size_t buf_size = 1024; char* buf = calloc(buf_size, 1); if (!buf) { return; } va_list args; va_start(args, format); vsnprintf(buf, buf_size, format, args); syslog(priority, "%s", buf); va_end(args); } } // read_stdin reads characters from stdin into buf. It returns: // -1, if it encounters an error while reading // -2, if it encounters invalid characters in the input // (buflen-1) if it read buflen-1 characters // <(buflen-1), if a newline was read before the buffer was full // If the return value is >=0, the buf is NULL-terminated. // Additionally, stdin is always advanced up to a newline (or EOF) // to prevent excess input from being read by a future shell process. static int read_stdin(char* buf, int buflen) { // Return error if we got no characters. if (fgets(buf, buflen, stdin) == NULL) { perror("ERROR when reading from stdin"); return -1; } bool newline = false; int len = strlen(buf); if (buf[len - 1] == '\n') { newline = true; buf[len - 1] = '\0'; len--; } // Return error if we got a non-printable character. for (int i = 0; i < len; i++) { if (buf[i] < 0x20 || buf[i] > 0x7e) { errorf("ERROR invalid characters read from stdin\n"); return -2; } } // Read stdin until a newline to avoid passing junk to shell. if (!newline) { for (int c = 0; c != EOF && c != '\n'; c = fgetc(stdin)) { } } return len; // Number of characters in the buffer without the NULL byte. } static void print_hex(const uint8_t* buf, size_t len) { for (size_t i = 0; i < len; i++) { errorf("%02x", buf[i]); } errorf("\n"); } int login_prompt(glome_login_config_t* config, pam_handle_t* pamh, const char** error_tag, const char* message, char* input, size_t input_size) { UNUSED(pamh); UNUSED(error_tag); // Disable bracketed paste. Special characters in the auth tag cause // authentication to fail. puts(BRACKETED_PASTE_FINISH); puts(message); fputs(PROMPT, stdout); fflush(NULL); if (config->input_timeout_sec) { struct sigaction action = {.sa_handler = &timeout_handler}; if (sigaction(SIGALRM, &action, NULL) < 0) { perror("error while setting up the handler"); // Continue nonetheless as the handler is not critical. } // Set an alarm to prevent waiting for the code indefinitely. alarm(config->input_timeout_sec); } int bytes_read = read_stdin(input, input_size); // Cancel any pending alarms. alarm(0); if (bytes_read < 0) { return EXITCODE_IO_ERROR; } if (config->options & VERBOSE) { errorf("debug: stdin: "); print_hex((uint8_t*)input, bytes_read); } return 0; } #endif static char* create_login_message(glome_login_config_t* config, pam_handle_t* pamh, const char** error_tag) { char* host_id = NULL; int max_hostname_len = sysconf(_SC_HOST_NAME_MAX); if (max_hostname_len == -1) { max_hostname_len = _POSIX_HOST_NAME_MAX; } if (config->host_id != NULL) { host_id = strdup(config->host_id); if (host_id == NULL) { *error_tag = "malloc-host-id"; return NULL; } } else { host_id = calloc(max_hostname_len + 1, 1); if (host_id == NULL) { *error_tag = "malloc-host-id"; return NULL; } if (get_machine_id(host_id, max_hostname_len + 1, error_tag) < 0) { *error_tag = "get-machine-id"; return NULL; } } char* host_id_type = NULL; if (config->host_id_type != NULL) { host_id_type = strdup(config->host_id_type); if (host_id_type == NULL) { *error_tag = "malloc-host-id-type"; free(host_id); return NULL; } } char* action = NULL; size_t action_len = 0; if (shell_action(config->username, &action, &action_len, error_tag)) { free(host_id_type); free(host_id); return NULL; } if (config->options & VERBOSE) { login_syslog(config, pamh, LOG_DEBUG, "host ID type: %s, host ID: %s, action: %s", host_id_type, host_id, action); } return glome_login_message(host_id_type, host_id, action); } int login_authenticate(glome_login_config_t* config, pam_handle_t* pamh, const char** error_tag) { uint8_t public_key[PUBLIC_KEY_LENGTH] = {0}; // Sanity check key material. if (is_zeroed(config->service_key, sizeof config->service_key)) { return failure(EXITCODE_PANIC, error_tag, "no-service-key"); } if (derive_or_generate_key(config->secret_key, public_key)) { return failure(EXITCODE_PANIC, error_tag, "derive-or-generate-key"); } // Derive content for the GLOME Login message. char* message = create_login_message(config, pamh, error_tag); if (!message) { return failure(EXITCODE_PANIC, error_tag, "glome-login-message"); } // Prepare auth code for verification of response. uint8_t authcode[GLOME_MAX_TAG_LENGTH]; if (glome_tag(/*verify=*/true, 0, config->secret_key, config->service_key, (uint8_t*)message, strlen(message), authcode)) { free(message); return failure(EXITCODE_PANIC, error_tag, "get-authcode"); } // Create the final prompt. char* prompt = NULL; { char* challenge = NULL; // TODO: Why does this not do a prefix? if (request_challenge(config->service_key, config->service_key_id, public_key, message, /*prefix_tag=*/NULL, /*prefix_tag_len=*/0, &challenge, error_tag)) { free(message); return EXITCODE_PANIC; } free(message); message = NULL; const char* prompt_prefix = ""; if (config->prompt != NULL) { prompt_prefix = config->prompt; } size_t prompt_len = strlen(prompt_prefix) + strlen(challenge) + 1; prompt = calloc(prompt_len, 1); if (prompt == NULL) { free(challenge); return failure(EXITCODE_PANIC, error_tag, "malloc-message"); } stpcpy(stpcpy(prompt, prompt_prefix), challenge); free(challenge); } char input[ENCODED_BUFSIZE(GLOME_MAX_TAG_LENGTH)]; int rc = login_prompt(config, pamh, error_tag, prompt, input, sizeof(input)); free(prompt); message = NULL; if (rc != 0) { return rc; } int bytes_read = strlen(input); if (config->options & INSECURE) { login_syslog(config, pamh, LOG_DEBUG, "user input: %s", input); } // Calculate the correct authcode. char authcode_encoded[ENCODED_BUFSIZE(sizeof authcode)] = {0}; if (base64url_encode(authcode, sizeof authcode, (uint8_t*)authcode_encoded, sizeof authcode_encoded) == 0) { return failure(EXITCODE_PANIC, error_tag, "authcode-encode"); } if (config->options & INSECURE) { login_syslog(config, pamh, LOG_DEBUG, "expect input: %s", authcode_encoded); } size_t min_len = MIN_ENCODED_AUTHCODE_LEN; if (config->min_authcode_len > min_len) { if (config->min_authcode_len > strlen(authcode_encoded)) { login_syslog(config, pamh, LOG_INFO, "minimum authcode too long: %d bytes (%s)", config->min_authcode_len, config->username); login_error(config, pamh, "Minimum input too long: expected at most %d characters.\n", config->min_authcode_len); return failure(EXITCODE_INVALID_INPUT_SIZE, error_tag, "authcode-length"); } min_len = config->min_authcode_len; } if ((size_t)bytes_read < min_len) { login_syslog(config, pamh, LOG_INFO, "authcode too short: %d bytes (%s)", bytes_read, config->username); login_error(config, pamh, "Input too short: expected at least %d characters, got %d.\n", min_len, bytes_read); return failure(EXITCODE_INVALID_INPUT_SIZE, error_tag, "authcode-length"); } if ((size_t)bytes_read > strlen(authcode_encoded)) { login_syslog(config, pamh, LOG_INFO, "authcode too long: %d bytes (%s)", bytes_read, config->username); login_error(config, pamh, "Input too long: expected at most %zu characters, got %d.\n", strlen(authcode_encoded), bytes_read); return failure(EXITCODE_INVALID_INPUT_SIZE, error_tag, "authcode-length"); } // Since we use (relatively) short auth codes, sleep before confirming the // result to prevent bruteforcing. if (config->auth_delay_sec) { struct timespec delay; delay.tv_sec = (time_t)config->auth_delay_sec; delay.tv_nsec = 0; if (nanosleep(&delay, NULL) != 0) { login_error(config, pamh, "interrupted sleep: %s", strerror(errno)); return failure(EXITCODE_INTERRUPTED, error_tag, "sleep-interrupted"); } } if (CRYPTO_memcmp(input, authcode_encoded, bytes_read) != 0) { login_syslog(config, pamh, LOG_WARNING, "authcode rejected (%s)", config->username); login_error(config, pamh, "Invalid authorization code.\n"); return failure(EXITCODE_INVALID_AUTHCODE, error_tag, "authcode-invalid"); } return 0; } int login_run(glome_login_config_t* config, const char** error_tag) { assert(config != NULL); if (config->options & VERBOSE) { errorf( "debug: options: 0x%x\n" "debug: username: %s\n" "debug: login: %s\n" "debug: auth delay: %d seconds\n", config->options, config->username, config->login_path, config->auth_delay_sec); } int r = login_authenticate(config, NULL, error_tag); if (r != 0) { return r; } if (config->options & SYSLOG) { syslog(LOG_WARNING, "authcode accepted (%s)", config->username); } puts("Authorization code: OK"); fflush(NULL); execl(config->login_path, config->login_path, "-f", config->username, (char*)NULL); perror("ERROR while executing login"); return failure(EXITCODE_PANIC, error_tag, "login-exec"); } glome-0.2/login/login.h000066400000000000000000000103551476056666600151010ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #ifndef LOGIN_LOGIN_H_ #define LOGIN_LOGIN_H_ #include #include "ui.h" // All exit codes from login_run/main #define EXITCODE_USAGE 1 // obsolete: EXITCODE_REBOOT 2 // obsolete: EXITCODE_LOCKDOWN 3 #define EXITCODE_IO_ERROR 4 #define EXITCODE_INVALID_AUTHCODE 5 #define EXITCODE_INVALID_INPUT_SIZE 6 #define EXITCODE_INTERRUPTED 7 // obsolete: EXITCODE_LOCKDOWN_ERROR 8 #define EXITCODE_TIMEOUT 9 #define EXITCODE_PANIC 42 // How many bytes of authorization code do we require. // Each byte has 6-bits of entropy due to Base64 encoding. // // For an auth code consisting of 48 bits of entropy with one second delays // between attempts, the probability of sustaining a brute-force attack lasting // a year is ~99.9999888%. // // This can be calculated using: (1-2**(-N))**(365*24*60*60/delay) // where N is the number of bits of token’s entropy and delay is in seconds. // // We increase this a bit more and choose 60-bits of entropy. #define MIN_ENCODED_AUTHCODE_LEN 10 // login_run executes the main login logic challenging the user for an // authenticate code unless fallback authentication has been requested. // // On error, the error_tag is set to an error token which should NOT be freed. int login_run(glome_login_config_t* config, const char** error_tag); // Constructs the action requesting shell access as a given user. // // Caller is expected to free returned message. // On error, the error_tag is set to an error token which should NOT be freed. int shell_action(const char* user, char** action, size_t* action_len, const char** error_tag); // Construct a challenge given the key parameters, host ID, an action, and // optionally a message prefix tag. // // service_key_id is the numerical 7 bit identifier of the server's public key. // A negative service_key_id indicates to use the public key prefix instead. // // If prefix_tag is supplied, it will be appended to the challenge for error // detection at the server side. // // On success, 0 is returned and challenge contains a pointer to a // NUL-terminated string, which must be freed by the caller. // // On error, a non-zero value is returned and the error_tag is set to an error // token which should NOT be freed. int request_challenge(const uint8_t service_key[GLOME_MAX_PUBLIC_KEY_LENGTH], int service_key_id, const uint8_t public_key[PUBLIC_KEY_LENGTH], const char* message, const uint8_t prefix_tag[GLOME_MAX_TAG_LENGTH], size_t prefix_tag_len, char** challenge, const char** error_tag); // Set the error_tag to the given error token and return the error code. int failure(int code, const char** error_tag, const char* message); // Store the identifier of the current machine in the buf array. // On error, the error_tag is set to an error token which should NOT be freed. int get_machine_id(char* buf, size_t buflen, const char** error_tag); // Helper operations used by the GLOME login authentication. struct pam_handle; typedef struct pam_handle pam_handle_t; void login_error(glome_login_config_t* config, pam_handle_t* pamh, const char* format, ...); void login_syslog(glome_login_config_t* config, pam_handle_t* pamh, int priority, const char* format, ...); int login_prompt(glome_login_config_t* config, pam_handle_t* pamh, const char** error_tag, const char* message, char* input, size_t input_size); // Execute GLOME login authentication for login and PAM binaries. int login_authenticate(glome_login_config_t* config, pam_handle_t* pamh, const char** error_tag); #endif // LOGIN_LOGIN_H_ glome-0.2/login/login_test.c000066400000000000000000000121331476056666600161270ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "login.h" #include #include #include #include #include "base64.h" #include "crypto.h" static void test_shell_action(void) { const char* error_tag = NULL; char* action = NULL; size_t action_len = 0; shell_action("operator", &action, &action_len, &error_tag); g_assert_cmpstr("shell=operator", ==, action); g_assert_true(strlen(action) + 1 == action_len); g_assert_null(error_tag); } static void test_vector_1(void) { const char* host_id_type = "mytype"; const char* host_id = "myhost"; const char* action = "root"; uint8_t service_key[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH] = {0}; uint8_t public_key[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; decode_hex( private_key, sizeof private_key, "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"); decode_hex( service_key, sizeof service_key, "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"); g_assert_true(derive_or_generate_key(private_key, public_key) == 0); char* message = glome_login_message(host_id_type, host_id, action); g_assert_nonnull(message); { uint8_t authcode[GLOME_MAX_TAG_LENGTH]; g_assert(glome_tag(true, 0, private_key, service_key, (uint8_t*)message, strlen(message), authcode) == 0); char authcode_encoded[ENCODED_BUFSIZE(sizeof authcode) + 1] = {0}; g_assert_true(base64url_encode(authcode, sizeof authcode, (uint8_t*)authcode_encoded, sizeof authcode_encoded)); g_assert_cmpmem("BB4BYjXonlIRtXZORkQ5bF5xTZwW6o60ylqfCuyAHTQ=", 44, authcode_encoded, 44); } { const char* error_tag = NULL; char* challenge = NULL; int service_key_id = 0; int messageTagPrefixLength = 3; uint8_t prefix_tag[GLOME_MAX_TAG_LENGTH]; g_assert(glome_tag(/*verify=*/false, 0, private_key, service_key, (uint8_t*)message, strlen(message), prefix_tag) == 0); if (request_challenge(service_key, service_key_id, public_key, message, prefix_tag, messageTagPrefixLength, &challenge, &error_tag)) { g_test_message("construct_request_challenge failed: %s", error_tag); g_test_fail(); } g_assert_cmpstr( "v2/gIUg8AmJMKdUdIt93LQ-91oNvzoNJjga9OukqY6qm05qlyPH/mytype:myhost/" "root/", ==, challenge); g_assert_null(error_tag); } } static void test_vector_2(void) { const char* host_id_type = ""; const char* host_id = "myhost"; const char* action = "exec=/bin/sh"; uint8_t service_key[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; uint8_t private_key[GLOME_MAX_PRIVATE_KEY_LENGTH] = {0}; uint8_t public_key[GLOME_MAX_PUBLIC_KEY_LENGTH] = {0}; decode_hex( private_key, sizeof private_key, "fee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1dead"); decode_hex( service_key, sizeof service_key, "d1b6941bba120bcd131f335da15778d9c68dadd398ae61cf8e7d94484ee65647"); g_assert_true(derive_or_generate_key(private_key, public_key) == 0); char* message = glome_login_message(host_id_type, host_id, action); g_assert_nonnull(message); { uint8_t authcode[GLOME_MAX_TAG_LENGTH]; g_assert(glome_tag(true, 0, private_key, service_key, (uint8_t*)message, strlen(message), authcode) == 0); char authcode_encoded[ENCODED_BUFSIZE(sizeof authcode)] = {0}; g_assert_true(base64url_encode(authcode, sizeof authcode, (uint8_t*)authcode_encoded, sizeof authcode_encoded)); g_assert_cmpmem("ZmxczN4x3g4goXu-A2AuuEEVftgS6xM-6gYj-dRrlis=", 44, authcode_encoded, 44); } { const char* error_tag = NULL; char* challenge = NULL; int service_key_id = -1; if (request_challenge(service_key, service_key_id, public_key, message, NULL, 0, &challenge, &error_tag)) { g_test_message("construct_request_challenge failed: %s", error_tag); g_test_fail(); } g_assert_cmpstr( "v2/R4cvQ1u4uJ0OOtYqouURB07hleHDnvaogAFBi-ZW48N2/myhost/" "exec=%2Fbin%2Fsh/", ==, challenge); g_assert_null(error_tag); } } int main(int argc, char** argv) { g_test_init(&argc, &argv, NULL); g_test_add_func("/test-shell-action", test_shell_action); g_test_add_func("/test-vector-1", test_vector_1); g_test_add_func("/test-vector-2", test_vector_2); return g_test_run(); } glome-0.2/login/main.c000066400000000000000000000041001476056666600146770ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include #include #include "config.h" #include "login.h" #include "ui.h" static void handle_error(const char* error_tag) { if (error_tag != NULL) { errorf("\nError: %s\n", error_tag); } // Let's sleep for a bit in case the console gets cleared after login exits so // the user has a chance to see all the output. fflush(NULL); sleep(2); } int main(int argc, char* argv[]) { glome_login_config_t config = {0}; // Parse arguments to initialize the config path. int r = parse_args(&config, argc, argv); if (r > 0) { return EXITCODE_USAGE; } if (r < 0) { handle_error("parse-args"); return EXITCODE_PANIC; } // Reset config while preserving the config path. const char* config_path = config.config_path; default_config(&config); config.config_path = config_path; // Read configuration file. status_t status = glome_login_parse_config_file(&config); if (status != STATUS_OK) { handle_error(status); return EXITCODE_PANIC; } // Parse arguments again to override config values. r = parse_args(&config, argc, argv); if (r > 0) { return EXITCODE_USAGE; } if (r < 0) { handle_error("parse-args"); return EXITCODE_PANIC; } // Initialize syslog, if we're going to log. if (config.options & SYSLOG) { openlog(NULL, LOG_PID | LOG_CONS, LOG_AUTH); } const char* error_tag = NULL; int rc = login_run(&config, &error_tag); if (rc) { handle_error(error_tag); } return rc; } glome-0.2/login/meson.build000066400000000000000000000077011476056666600157630ustar00rootroot00000000000000# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https:#www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. install_data( 'example.cfg', rename : 'config', install_dir : join_paths(get_option('sysconfdir'), 'glome')) login_lib = static_library( 'glome-login', [ 'base64.h', 'config.c', 'config.h', 'crypto.c', 'crypto.h', 'openssl/base64.c', 'ui.c', 'ui.h', ], dependencies : [openssl_dep], link_with : glome_lib, include_directories : glome_incdir, install : false) pkg.generate(login_lib, description : 'glome-login, an authentication system built upon GLOME') glome_login = executable( 'glome-login', ['main.c', 'login.c'], dependencies : [openssl_dep], link_with : login_lib, include_directories : glome_incdir, install : true, install_dir : get_option('sbindir')) if get_option('tests') login_test = executable( 'login_test', ['login_test.c', 'login.c'], dependencies : [openssl_dep, glib_dep], link_with : [glome_lib, login_lib], include_directories : glome_incdir) test('login test', login_test) crypto_test = executable( 'crypto_test', 'crypto_test.c', dependencies : [openssl_dep, glib_dep], link_with : [glome_lib, login_lib], include_directories : glome_incdir) test('crypto test', crypto_test) config_test = executable( 'config_test', 'config_test.c', dependencies : [openssl_dep, glib_dep], link_with : [glome_lib, login_lib], include_directories : glome_incdir) test('config test', config_test, args: files('config_test.cfg')) test('config test with url-prefix', config_test, args: files('config_test_url-prefix.cfg')) endif if get_option('pam-glome') cc = meson.get_compiler('c') libpam = cc.find_library('pam') args = ['-DPAM_GLOME'] pam_ext_present = cc.has_function('pam_syslog', dependencies: libpam, prefix: '#include ') if pam_ext_present args += ['-DHAVE_PAM_EXT'] endif pam_glome = shared_library( 'pam_glome', ['pam.c', 'login.c'], c_args : args, dependencies : [libpam, openssl_dep], link_with : [glome_lib, login_lib], include_directories : glome_incdir, name_prefix : '', install : true, install_dir : join_paths(get_option('libdir'), 'security')) if get_option('tests') libpamtest = dependency('libpamtest', required : false) if libpamtest.found() oldstyle_run_pamtest = cc.compiles('''#include #include void test() { run_pamtest(NULL, NULL, NULL, NULL); } ''', dependencies : [libpamtest]) pam_test = executable( 'pam_test', 'pam_test.c', dependencies : [libpamtest], c_args : oldstyle_run_pamtest ? '-DOLDSTYLE_RUN_PAMTEST' : []) custom_target('pam_service', build_by_default : true, output : [ 'pam_service' ], command : [ 'mkdir', '@OUTPUT@' ]) test('pam test', pam_test, env: [ 'LD_PRELOAD=libpam_wrapper.so', 'PAM_WRAPPER=1', 'PAM_WRAPPER_SERVICE_DIR=' + join_paths(meson.build_root(), 'login', 'pam_service'), 'PAM_GLOME=' + join_paths(meson.build_root(), 'login', 'pam_glome.so') ]) endif endif endif glome-0.2/login/openssl/000077500000000000000000000000001476056666600152775ustar00rootroot00000000000000glome-0.2/login/openssl/base64.c000066400000000000000000000047771476056666600165460ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "base64.h" #include #include #define CHAR_62_CLASSIC '+' #define CHAR_62_URLSAFE '-' #define CHAR_63_CLASSIC '/' #define CHAR_63_URLSAFE '_' size_t base64url_encode(const uint8_t* src, size_t src_len, uint8_t* dst, size_t dst_len) { size_t len = ENCODED_BUFSIZE(src_len); // The ENCODED_BUFSIZE macro has not been tested for operation close // to the overflow point, but up to SIZE_MAX/2 it behaves fine. if (src_len >= SIZE_MAX / 2) { return 0; } if (len > dst_len) { return 0; } len = EVP_EncodeBlock(dst, src, src_len); // Replacing 62nd and 63rd character with '-' and '_' per RFC4648 section 5 for (size_t i = 0; i < len; i++) { switch (dst[i]) { case CHAR_62_CLASSIC: dst[i] = CHAR_62_URLSAFE; break; case CHAR_63_CLASSIC: dst[i] = CHAR_63_URLSAFE; break; } } return len; } size_t base64url_decode(const uint8_t* urlsafe_src, size_t src_len, uint8_t* dst, size_t dst_len) { if (dst_len < DECODED_BUFSIZE(src_len)) { return 0; } // Restore 62nd and 63rd character from '-' and '_' per RFC4648 section 5 uint8_t* src = (uint8_t*)malloc(src_len); if (src == NULL) { return 0; } memcpy(src, urlsafe_src, src_len); for (size_t i = 0; i < src_len; i++) { switch (src[i]) { case CHAR_62_URLSAFE: src[i] = CHAR_62_CLASSIC; break; case CHAR_63_URLSAFE: src[i] = CHAR_63_CLASSIC; break; } } EVP_ENCODE_CTX* ctx = EVP_ENCODE_CTX_new(); if (ctx == NULL) { free(src); return 0; } EVP_DecodeInit(ctx); int ret, len, total = 0; ret = EVP_DecodeUpdate(ctx, dst, &len, src, src_len); if (ret < 0) { goto out; } total = len; ret = EVP_DecodeFinal(ctx, dst, &len); if (ret < 0) { total = 0; goto out; } total += len; out: free(src); EVP_ENCODE_CTX_free(ctx); return total; } glome-0.2/login/pam.c000066400000000000000000000214541476056666600145430ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include #include #include #ifdef HAVE_PAM_EXT #include #else #include #endif #include #include #include #include #include "base64.h" #include "config.h" #include "login.h" #include "ui.h" #define MODULE_NAME "pam_glome" #ifndef HAVE_PAM_EXT void pam_syslog(void *pamh, ...) { (void)(pamh); } void pam_vsyslog(void *pamh, ...) { (void)(pamh); } #endif #define MAX_ERROR_MESSAGE_SIZE 4095 #define UNUSED(var) (void)(var) static const char *arg_value(const char *arg, const char *key, const char *default_value) { int i, key_len = strlen(key); for (i = 0; i < key_len; i++) { // Compare key with arg char by char while also allowing _ in place of - if (!(key[i] == arg[i] || (key[i] == '-' && arg[i] == '_'))) { return NULL; } } if (arg[key_len] == '=') { return arg + key_len + 1; } if (arg[key_len] == '\0') { return default_value; } return NULL; } static int parse_pam_args(pam_handle_t *pamh, int argc, const char **argv, glome_login_config_t *config) { int errors = 0; status_t status; const char *val; for (int i = 0; i < argc; ++i) { if ((val = arg_value(argv[i], "config-path", NULL))) { status = glome_login_assign_config_option(config, "default", "config-path", val); } else if ((val = arg_value(argv[i], "key", NULL))) { status = glome_login_assign_config_option(config, "service", "key", val); } else if ((val = arg_value(argv[i], "key-version", NULL))) { status = glome_login_assign_config_option(config, "service", "key-version", val); } else if ((val = arg_value(argv[i], "prompt", NULL))) { status = glome_login_assign_config_option(config, "service", "prompt", val); } else if ((val = arg_value(argv[i], "debug", "true"))) { status = glome_login_assign_config_option(config, "default", "verbose", val); } else if ((val = arg_value(argv[i], "print-secrets", "true"))) { status = glome_login_assign_config_option(config, "default", "print-secrets", val); } else if ((val = arg_value(argv[i], "host-id", NULL))) { status = glome_login_assign_config_option(config, "default", "host-id", val); } else if ((val = arg_value(argv[i], "host-id-type", NULL))) { status = glome_login_assign_config_option(config, "default", "host-id-type", val); } else if ((val = arg_value(argv[i], "ephemeral-key", NULL))) { status = glome_login_assign_config_option(config, "default", "ephemeral-key", val); } else if ((val = arg_value(argv[i], "min-authcode-len", NULL))) { status = glome_login_assign_config_option(config, "default", "min-authcode-len", val); } else { pam_syslog(pamh, LOG_ERR, "invalid option %s", argv[i]); errors++; continue; } if (status != STATUS_OK) { pam_syslog(pamh, LOG_ERR, "failed to set config option '%s': %s", argv[i], status); status_free(status); errors++; } } return errors > 0 ? -1 : 0; } static int get_username(pam_handle_t *pamh, glome_login_config_t *config, const char **error_tag) { const char *username; if (pam_get_user(pamh, &username, NULL) != PAM_SUCCESS || !username || !*username) { return failure(EXITCODE_PANIC, error_tag, "get-username"); } config->username = username; return 0; } void login_error(glome_login_config_t *config, pam_handle_t *pamh, const char *format, ...) { UNUSED(config); char message[MAX_ERROR_MESSAGE_SIZE] = {0}; va_list argptr; va_start(argptr, format); int ret = vsnprintf(message, sizeof(message), format, argptr); va_end(argptr); if (ret < 0 || ret >= MAX_ERROR_MESSAGE_SIZE) { return; } struct pam_message msg[1] = { {.msg = (char *)message, .msg_style = PAM_ERROR_MSG}, }; const struct pam_message *pmsg[1] = {&msg[0]}; struct pam_response *resp = NULL; struct pam_conv *conv; if (pam_get_item(pamh, PAM_CONV, (const void **)&conv) != PAM_SUCCESS) { return; } if (conv->conv(1, pmsg, &resp, conv->appdata_ptr) != PAM_SUCCESS) { return; } if (resp != NULL) { free(resp->resp); free(resp); } } void login_syslog(glome_login_config_t *config, pam_handle_t *pamh, int priority, const char *format, ...) { UNUSED(config); va_list argptr; va_start(argptr, format); pam_vsyslog(pamh, priority, format, argptr); va_end(argptr); } int login_prompt(glome_login_config_t *config, pam_handle_t *pamh, const char **error_tag, const char *message, char *input, size_t input_size) { UNUSED(config); struct pam_message msg[1] = { {.msg = (char *)message, .msg_style = PAM_TEXT_INFO}, }; const struct pam_message *pmsg[1] = {&msg[0]}; struct pam_response *resp = NULL; struct pam_conv *conv; if (pam_get_item(pamh, PAM_CONV, (const void **)&conv) != PAM_SUCCESS) { return failure(EXITCODE_PANIC, error_tag, "pam-get-conv"); } if (conv->conv(1, pmsg, &resp, conv->appdata_ptr) != PAM_SUCCESS) { return failure(EXITCODE_PANIC, error_tag, "pam-conv"); } if (resp != NULL) { free(resp->resp); free(resp); } const char *token; if (pam_get_authtok(pamh, PAM_AUTHTOK, &token, NULL) != PAM_SUCCESS) { return failure(EXITCODE_PANIC, error_tag, "pam-get-authtok"); } if (strlen(token) >= input_size) { return failure(EXITCODE_PANIC, error_tag, "pam-authtok-size"); } // OpenSSH provides fake password when login is not allowed, // for example due to PermitRootLogin set to 'no' // https://github.com/openssh/openssh-portable/commit/283b97 const char fake_password[] = "\b\n\r\177INCORRECT"; // auth-pam.c from OpenSSH bool is_fake = true; // Constant-time comparison in case token contains user's password for (size_t i = 0; i < strlen(token); i++) { is_fake &= (token[i] == fake_password[i % (sizeof(fake_password) - 1)]); } if (is_fake) { return failure(EXITCODE_PANIC, error_tag, "pam-authtok-openssh-no-login"); } strncpy(input, token, input_size); return 0; } int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) { UNUSED(flags); const char *error_tag = NULL; glome_login_config_t config = {0}; int rc = PAM_AUTH_ERR; // Parse arguments to initialize the config path. int r = parse_pam_args(pamh, argc, argv, &config); if (r < 0) { pam_syslog(pamh, LOG_ERR, "failed to parse pam module arguments (%d)", r); return rc; } // Reset config while preserving the config path. const char *config_path = config.config_path; default_config(&config); config.config_path = config_path; // Read configuration file. status_t status = glome_login_parse_config_file(&config); if (status != STATUS_OK) { pam_syslog(pamh, LOG_ERR, "failed to read config file %s: %s", config.config_path, status); return rc; } // Parse arguments again to override config values. r = parse_pam_args(pamh, argc, argv, &config); if (r < 0) { pam_syslog(pamh, LOG_ERR, "failed to parse pam module arguments (%d)", r); return rc; } r = get_username(pamh, &config, &error_tag); if (r < 0) { pam_syslog(pamh, LOG_ERR, "failed to get username: %s (%d)", error_tag, r); return rc; } r = login_authenticate(&config, pamh, &error_tag); if (!r) { rc = PAM_SUCCESS; if (config.options & VERBOSE) { pam_syslog(pamh, LOG_ERR, "authenticated user '%s'", config.username); } } else { pam_syslog(pamh, LOG_ERR, "failed to authenticate user '%s': %s (%d)", config.username, error_tag, r); } return rc; } int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { /* This module does not generate any user credentials, so just skip. */ UNUSED(pamh); UNUSED(flags); UNUSED(argc); UNUSED(argv); return PAM_SUCCESS; } glome-0.2/login/pam_test.c000066400000000000000000000121311476056666600155720ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include #include #include /* Needs to go last to get size_t definition. */ #include const char *authtoks[] = { "Xt-yvSPnAz", /* Correct code */ "Xt-yvSPnA", /* Too short */ "INVALIDCODE", /* Wrong code */ /* fake passwords that might be provided by openssh-portable/auth-pam.c */ "\b\n\r\177", "\b\n\r\177INCORRECT", "\b\n\r\177INCORRECT\b\n\r\177", NULL /* Terminator */ }; struct pamtest_conv_data conv_data = { .in_echo_off = authtoks, }; struct pam_testcase tests[] = { pam_test(PAMTEST_AUTHENTICATE, PAM_SUCCESS), pam_test(PAMTEST_AUTHENTICATE, PAM_AUTH_ERR), pam_test(PAMTEST_AUTHENTICATE, PAM_AUTH_ERR), pam_test(PAMTEST_AUTHENTICATE, PAM_AUTH_ERR), pam_test(PAMTEST_AUTHENTICATE, PAM_AUTH_ERR), pam_test(PAMTEST_AUTHENTICATE, PAM_AUTH_ERR), }; /* Setup GLOME using only PAM parameters. */ int test_service() { int len; enum pamtest_err perr; char *runtime_dir, *pam_glome, *service_file; char *service = "test"; char *username = "root"; FILE *f; pam_glome = getenv("PAM_GLOME"); if (pam_glome == NULL) { puts("PAM_GLOME not found"); return 1; } runtime_dir = getenv("PAM_WRAPPER_RUNTIME_DIR"); if (runtime_dir == NULL) { puts("PAM_WRAPPER_RUNTIME_DIR not found"); return 1; } len = strlen(runtime_dir) + 1 + strlen(service) + 1; service_file = calloc(len, 1); if (service_file == NULL) { puts("calloc service_file failed"); return 1; } snprintf(service_file, len, "%s/%s", runtime_dir, service); f = fopen(service_file, "w"); if (f == NULL) { printf("fopen service_file '%s' failed: %s\n", service_file, strerror(errno)); return 1; } free(service_file); fprintf(f, "auth required %s prompt=https://test.service/ " "key=" "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f " "key_version=1 " "ephemeral_key=" "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a " "host_id=my-server.local", pam_glome); fclose(f); #if defined(OLDSTYLE_RUN_PAMTEST) perr = run_pamtest(service, username, &conv_data, tests); #else perr = run_pamtest(service, username, &conv_data, tests, NULL); #endif if (perr != PAMTEST_ERR_OK) { puts(pamtest_strerror(perr)); return 1; } return 0; } /* Setup GLOME using config file and PAM parameters. */ int test_config() { int len; enum pamtest_err perr; char *runtime_dir, *pam_glome, *service_file, *config_file; char *service = "test"; char *config = "config"; char *username = "root"; FILE *f; pam_glome = getenv("PAM_GLOME"); if (pam_glome == NULL) { puts("PAM_GLOME not found"); return 1; } runtime_dir = getenv("PAM_WRAPPER_RUNTIME_DIR"); if (runtime_dir == NULL) { puts("PAM_WRAPPER_RUNTIME_DIR not found"); return 1; } len = strlen(runtime_dir) + 1 + strlen(config) + 1; config_file = calloc(len, 1); if (config_file == NULL) { puts("calloc config_file failed"); return 1; } snprintf(config_file, len, "%s/%s", runtime_dir, config); f = fopen(config_file, "w"); if (f == NULL) { printf("fopen config_file '%s' failed: %s\n", config_file, strerror(errno)); return 1; } fprintf(f, "[service]\n" "key = " "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f\n" "key-version = 1\n" "prompt = https://test.service/\n"); fclose(f); len = strlen(runtime_dir) + 1 + strlen(service) + 1; service_file = calloc(len, 1); if (service_file == NULL) { puts("calloc service_file failed"); return 1; } snprintf(service_file, len, "%s/%s", runtime_dir, service); f = fopen(service_file, "w"); if (f == NULL) { printf("fopen service_file '%s' failed: %s\n", service_file, strerror(errno)); return 1; } free(service_file); fprintf(f, "auth required %s config_path=%s " "ephemeral-key=" "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a " "host-id=my-server.local", pam_glome, config_file); fclose(f); free(config_file); #if defined(OLDSTYLE_RUN_PAMTEST) perr = run_pamtest(service, username, &conv_data, tests); #else perr = run_pamtest(service, username, &conv_data, tests, NULL); #endif if (perr != PAMTEST_ERR_OK) { puts(pamtest_strerror(perr)); return 1; } return 0; } int main() { int rc; rc = test_service(); rc = rc || test_config(); return rc; } glome-0.2/login/ui.c000066400000000000000000000171551476056666600144060ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "ui.h" #include #include #include #include #include #include #include #include static void usage(const char* argv0) { const char* sep = strrchr(argv0, '/'); const char* name = (sep == NULL) ? argv0 : sep + 1; errorf("Usage: %s [OPTIONS] [--] USERNAME\n", name); } #define STATUS_SIZE 256 static char* status_malloc_failed = "ERROR: failed to allocate status buffer"; status_t status_createf(const char* format, ...) { char* message = malloc(STATUS_SIZE); if (message == NULL) { return status_malloc_failed; } va_list argptr; va_start(argptr, format); int ret = vsnprintf(message, STATUS_SIZE, format, argptr); va_end(argptr); if (ret < 0 || ret >= STATUS_SIZE) { snprintf(message, STATUS_SIZE, "ERROR: status message too big: %d", ret); } return message; } void status_free(status_t status) { if (status == status_malloc_failed) { return; } free(status); } int decode_hex(uint8_t* dst, size_t dst_len, const char* in) { size_t len = strlen(in); if (len > 2 && in[0] == '0' && in[1] == 'x') { len -= 2; in += 2; } if (len != dst_len * 2) { errorf( "ERROR: hex-encoded key must have exactly %zu characters (got %zu)\n", dst_len * 2, len); return -1; } for (size_t i = 0; i < dst_len; i++) { if (sscanf(in + (i * 2), "%02hhX", dst + i) != 1) { errorf("ERROR while parsing byte %zu ('%c%c') as hex\n", i, in[2 * i], in[2 * i + 1]); return -2; } } return 0; } static const char flags_help[] = "Available flags:" "\n -h, --help this help" "\n -c, --config-path=PATH configuration file to parse " "(default: " DEFAULT_CONFIG_FILE ")" "\n -a, --min-authcode-len=N minimum length of the encoded authcode" "\n -d, --auth-delay=N sleep N seconds before the authcode check " "(default: %d)" "\n -k, --key=KEY use hex-encoded KEY as the service key " "(default: key from configuration file)" "\n -l, --login-path=PATH use PATH instead of " DEFAULT_LOGIN_PATH "\n -m, --host-id-type=TYPE use TYPE as the host-id type" "\n -p, --prompt=PROMPT print PROMPT before the challenge is " "printed (default: '" DEFAULT_PROMPT "')" "\n -s, --disable-syslog suppress syslog logging (default: false)" "\n -t, --timeout=N abort if the authcode has not been provided " "within N seconds" "\n no timeout if the flag is 0 (default: %d)" "\n -v, --verbose print debug information" "\nUnsafe flags:" "\n -I, --print-secrets print all the secrets (INSECURE!)" "\n -K, --ephemeral-key=KEY use KEY as the hex-encoded ephemeral secret " "key (INSECURE!)" "\n -M, --host-id=NAME use NAME as the host-id" "\n"; static const char* short_options = "ha:c:d:k:l:m:p:st:u:vIK:M:"; static const struct option long_options[] = { {"help", no_argument, 0, 'h'}, {"min-authcode-len", required_argument, 0, 'a'}, {"config-path", required_argument, 0, 'c'}, {"auth-delay", required_argument, 0, 'd'}, {"key", required_argument, 0, 'k'}, {"login-path", required_argument, 0, 'l'}, {"disable-syslog", no_argument, 0, 's'}, {"timeout", required_argument, 0, 't'}, {"prompt", required_argument, 0, 'p'}, {"verbose", no_argument, 0, 'v'}, {"print-secrets", no_argument, 0, 'I'}, {"ephemeral-key", required_argument, 0, 'K'}, {"host-id", required_argument, 0, 'M'}, {"host-id-type", required_argument, 0, 'm'}, {0, 0, 0, 0}, }; void default_config(glome_login_config_t* config) { memset(config, 0, sizeof(glome_login_config_t)); // Setting defaults. config->login_path = DEFAULT_LOGIN_PATH; config->prompt = DEFAULT_PROMPT; config->auth_delay_sec = DEFAULT_AUTH_DELAY; config->input_timeout_sec = DEFAULT_INPUT_TIMEOUT; config->options = SYSLOG; } int parse_args(glome_login_config_t* config, int argc, char* argv[]) { int c; int errors = 0; status_t status; // Reset current position to allow parsing arguments multiple times. optind = 1; while ((c = getopt_long(argc, argv, short_options, long_options, NULL)) != -1) { switch (c) { case 'a': status = glome_login_assign_config_option(config, "default", "min-authcode-len", optarg); break; case 'c': status = glome_login_assign_config_option(config, "default", "config-path", optarg); break; case 'd': status = glome_login_assign_config_option(config, "default", "auth-delay", optarg); break; case 'k': status = glome_login_assign_config_option(config, "service", "key", optarg); break; case 'l': status = glome_login_assign_config_option(config, "default", "login-path", optarg); break; case 'm': status = glome_login_assign_config_option(config, "default", "host-id-type", optarg); break; case 'p': status = glome_login_assign_config_option(config, "service", "prompt", optarg); break; case 's': status = glome_login_assign_config_option(config, "default", "disable-syslog", "true"); break; case 't': status = glome_login_assign_config_option(config, "default", "timeout", optarg); break; case 'v': status = glome_login_assign_config_option(config, "default", "verbose", "true"); break; case 'I': status = glome_login_assign_config_option(config, "default", "print-secrets", "true"); break; case 'K': status = glome_login_assign_config_option(config, "default", "ephemeral-key", optarg); break; case 'M': status = glome_login_assign_config_option(config, "default", "host-id", optarg); break; case '?': case 'h': usage(argv[0]); errorf(flags_help, DEFAULT_AUTH_DELAY, DEFAULT_INPUT_TIMEOUT); return 2; default: return -1; // PANIC } if (status != STATUS_OK) { errorf("%s\n", status); status_free(status); errors++; } } if (optind >= argc) { errorf("ERROR: no username specified\n"); errors++; } if (optind < argc - 1) { errorf("ERROR: only one username is allowed (got %d)\n", argc - optind); errors++; } if (errors > 0) { usage(argv[0]); return 1; } config->username = argv[optind]; return 0; } glome-0.2/login/ui.h000066400000000000000000000032271476056666600144060ustar00rootroot00000000000000// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #ifndef LOGIN_UI_H_ #define LOGIN_UI_H_ #include #include #include "config.h" #include "crypto.h" #define errorf(...) fprintf(stderr, __VA_ARGS__) #if !defined(SYSCONFDIR) #define SYSCONFDIR "/etc" #endif #define DEFAULT_CONFIG_FILE SYSCONFDIR "/glome/config" #define DEFAULT_LOGIN_PATH "/bin/login" #define DEFAULT_AUTH_DELAY 1 #define DEFAULT_INPUT_TIMEOUT 180 #define DEFAULT_USERNAME "root" #define DEFAULT_PROMPT "GLOME: " // Options // obsolete: SKIP_LOCKDOWN (1 << 1) // obsolete: REBOOT (1 << 2) #define VERBOSE (1 << 3) #define INSECURE (1 << 4) #define SYSLOG (1 << 5) // decode_hex converts a hex-encoded string into the equivalent bytes. int decode_hex(uint8_t* dst, size_t dst_len, const char* in); // default_config initializes the config with the default values. void default_config(glome_login_config_t* config); // parse_args parses command-line arguments into a config struct. It will // forcefully initialize the whole content of the struct to zero. int parse_args(glome_login_config_t* config, int argc, char* argv[]); #endif // LOGIN_UI_H_ glome-0.2/meson.build000066400000000000000000000031761476056666600146550ustar00rootroot00000000000000# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https:#www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. project('glome', 'c', license : 'Apache-2.0', version : '0.1', default_options : ['c_std=c99', 'warning_level=3']) pkg = import('pkgconfig') sysconfdir = join_paths(get_option('prefix'), get_option('sysconfdir')) add_project_arguments('-DSYSCONFDIR="' + sysconfdir + '"', language : 'c') add_project_arguments('-D_POSIX_C_SOURCE=200809L', language : 'c') add_project_arguments('-DOPENSSL_API_COMPAT=10100', language : 'c') openssl_dep = dependency('openssl', version : '>=1.1') glome_lib = shared_library('glome', 'glome.c', dependencies : openssl_dep, install : true, version : meson.project_version()) glome_incdir = include_directories('.') pkg.generate(glome_lib, description : 'GLOME, the Generic Low Overhead Message Exchange') install_headers('glome.h') if get_option('tests') glib_dep = dependency('glib-2.0') glome_test = executable('glome_test', 'glome_test.c', dependencies : glib_dep, link_with : glome_lib, include_directories : glome_incdir) test('glome', glome_test) endif subdir('login') if get_option('glome-cli') subdir('cli') endif glome-0.2/meson_options.txt000066400000000000000000000014231476056666600161410ustar00rootroot00000000000000# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https:#www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. option('tests', type: 'boolean', description: 'Build tests') option('pam-glome', type: 'boolean', description: 'Build glome PAM module') option('glome-cli', type: 'boolean', description: 'Build glome CLI tool') glome-0.2/python/000077500000000000000000000000001476056666600140255ustar00rootroot00000000000000glome-0.2/python/.gitignore000066400000000000000000000000751476056666600160170ustar00rootroot00000000000000*__pycache__* *~ # setuptools outputs build dist *.egg-info glome-0.2/python/MANIFEST.in000066400000000000000000000000311476056666600155550ustar00rootroot00000000000000recursive-include test * glome-0.2/python/README.md000066400000000000000000000044141476056666600153070ustar00rootroot00000000000000# PyGLOME **This is not an officially supported Google product.** This repository contains a Python implementation for the GLOME protocol. You can find the library in the folder pyglome. The test files can be found in the test folder. ## Python API ### Requirements - Python >= 3.6 - pyca/cryptography >= 2.5 ### Example We provide a brief example of use. In order for Alice and Bob to communicate, the first step would be to generate some new keys: ```python import pyglome alice_keys = pyglome.generate_keys() bob_keys = pyglome.generate_keys() ``` Suppose that Alice knows Bob's `public_key` and wants to send Bob the message `msg` and no other message have been shared before. Alice will need to: ```python glome = pyglome.Glome(bob_keys.public, alice_keys.private) first_tag = glome.tag(msg, counter=0) ``` And Alice will send Bob both `msg`, `first_tag` as well as Alice's public key. On Bob's end he will need to do the following: ```python glome = pyglome.Glome(alice_keys.public, bob_keys.private) try: glome.check(first_tag, msg, counter=0) except pyglome.TagCheckError as tag_error: ## Handle the exception. ## do what you have to do ``` ### Key generation. Should you want to use a preexisting key, it should match the format `X25519Private/PublicKey` provided in [pyca/cryptography](https://cryptography.io/en/latest/). Such a key can be easily read from a bytes object as follows: ```python from cryptography.hazmat.primitives.asymmetric import x25519 my_private_key = x25519.X25519PrivateKey.from_private_bytes(private_key_bytes) my_public_key = x25519.X25519PublicKey.from_public_bytes(public_key_bytes) ``` We provide a key generation function `generate_keys` that uses these methods to create a new key pair from `os.urandom` bytes. ### Documentation For more information see the in-code documentation. ### Test In the test folder we have scripts that implement test classes based on unittest. To run all unittest use: ``` python -m test ``` from this directory. If you only want to execute a particular test module, then run: ``` python -m test.my_module_name ``` where `my_module_name` is the name of the test module to be executed (the name of the file without the .py). To run the fuzzing test use: ``` python -m test.fuzzing_test ``` from this directory. glome-0.2/python/pyglome/000077500000000000000000000000001476056666600155015ustar00rootroot00000000000000glome-0.2/python/pyglome/__init__.py000066400000000000000000000031721476056666600176150ustar00rootroot00000000000000# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ PyGLOME is a Python library that provides an API for GLOME protocol. Basic Usage: In order for Alice and Bob to communicate, the first step would be to generate some new keys: >>> import pyglome >>> alice_keys = pyglome.generate_keys() >>> bob_keys = pyglome.generate_keys() Suppose that Alice knows Bob's `public_key` and wants to send Bob the message `msg` and no other message have been shared before. Alice will need to: >>> glome = pyglome.Glome(bob_keys.public, alice_keys.private) >>> first_tag = glome.tag(msg, counter=0) And Alice will send Bob both msg, first_tag as well as Alice's public key. On Bob ends he will need to do the following: >>> glome = pyglome.Glome(alice_keys.public, bob_keys.private) >>> try: ... first_tag = glome.check(first_tag, msg, counter=0) ... except pyglome.TagCheckError as tag_error: ... ## Handle the exception. >>> ## do what you have to do """ # Bring glome module to top level from pyglome.glome import (Glome, TagCheckError, IncorrectTagError, TagGenerationError, generate_keys, AutoGlome) glome-0.2/python/pyglome/glome.py000066400000000000000000000334201476056666600171600ustar00rootroot00000000000000# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Python GLOME library. This module contains the Glome class and generate_keys function. Example use: Sender >>> import pyglome >>> tag_manager = pyglome.Glome(peer_key) >>> first_tag = tag_manager.tag(first_msg, 0) # 0 as it is the first msg >>> second_tag = tag_manager.tag(second_msg, 1) Example use: Receiver >>> import pyglome >>> tag_manager = pyglome.Glome(peer_key, my_private_key) >>> ## Need to have a private key (paired to the public key >>> ## that the sender use) >>> try: ... tag_manager.check(tag, msg, counter=0): >>> except pyglome.IncorrectTagError as wte: ... ## Handle the exception >>> ## do what you have to do """ import os import hashlib import hmac from typing import NamedTuple from cryptography.hazmat.primitives.asymmetric import x25519 from cryptography.hazmat.primitives import serialization class KeyPair(NamedTuple): """ NamedTuple-Class that stores a private/public key pair. Attributes: - private: A private key. - public: A public key paired with the private one. """ private: x25519.X25519PrivateKey public: x25519.X25519PublicKey class Error(Exception): """Error super-class for any error that is thrown in PyGLOME.""" class TagCheckError(Error): """Raised whenever a tag is not correct or the method failed to check it.""" class IncorrectTagError(Error): """Raised whenever the tag provided does not match the message and counter.""" class TagGenerationError(Error): """Raised whenever a tag could not be generated.""" class ExchangeError(Error): """Raised whenever the x25519 key exchange fails.""" def _public_key_encode(public_key: x25519.X25519PublicKey): return public_key.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) def _tag(msg: bytes, counter: int, key: bytes) -> bytes: if not 0 <= counter <= 255: raise ValueError(f'tag counter (={counter}) must be within [0, 255]') message = bytes([counter]) + msg # msg: N_x|M_n digester = hmac.new(key=key, msg=message, digestmod=hashlib.sha256) return digester.digest() class Glome: """Implement tag managing functionalities for GLOME protocol. This class is initialized by providing your peer's public key and optionally your private key. If a private key is not provided, one is automatically generated making use of `generate_keys`. Provides methods tag (to generate new tags) and check (to check receiving tags). """ MAX_TAG_LEN = 32 # 32 is maximum tag length MIN_TAG_LEN = 1 def __init__(self, peer_key: x25519.X25519PublicKey, my_private_key: x25519.X25519PrivateKey = None, min_peer_tag_len: int = MAX_TAG_LEN): """Initialize Glome class. Performs the handshake and generates keys. Args: peer_key: Your peer's public key. my_private_key: Your private key. min_peer_tag_len: Desired length (in bytes) for the tag. Must be an integer in range 1-32. Raises: ValueError: Raised whenever min_peer_tag_len is not in range 1-32. ExchangeError: Raised whenever null shared secret is derived from user/peer key pair. """ if my_private_key is None: my_private_key, my_public_key = generate_keys() else: my_public_key = my_private_key.public_key() if not Glome.MIN_TAG_LEN <= min_peer_tag_len <= Glome.MAX_TAG_LEN: raise ValueError( f'min_peer_tag_len (={min_peer_tag_len}) is not within ' f'[{Glome.MIN_TAG_LEN}, {Glome.MAX_TAG_LEN}]') try: shared_secret = my_private_key.exchange(peer_key) except ValueError as value_error: raise ExchangeError( 'Failed to deduce shared secret') from value_error self._send_key = shared_secret + _public_key_encode( peer_key) + _public_key_encode(my_public_key) self._receive_key = shared_secret + _public_key_encode( my_public_key) + _public_key_encode(peer_key) self._peer_key = peer_key self._my_keys = KeyPair(my_private_key, my_public_key) self._min_peer_tag_len = min_peer_tag_len @property def user_keys(self) -> KeyPair: """User's private and public keys used in handshake.""" return self._my_keys @property def peer_key(self) -> x25519.X25519PublicKey: """Peer's public key used in handshake.""" return self._peer_key def tag(self, msg: bytes, counter: int) -> bytes: """Generates a tag from a message and a counter. Generates a tag matching some provided message and counter. This tag is generated following GLOME protocol specification in the context of a communication from the users to theirs peers. Args: msg: Message to be transmitted. counter: Numbers of messages transmitted previously in the conversation in this direction (i.e. from the user to the peer). Must be an integer in {0,...,255}. Returns: tag: Tag matching counter and msg. Raises: TagGenerationError: Raised whenever the method failed to generate tag due to ValueError in the arguments. """ try: return _tag(msg, counter, self._send_key) except ValueError as value_error: raise TagGenerationError('Failed to generate tag') from value_error def check(self, tag: bytes, msg: bytes, counter: int): """Check whether a tag is correct for some message and counter. Checks if a tag matches some provided message and counter. The method generates the matching tag following GLOME protocol specification in the context of a communication from the users' peers to the users and then is compared with the tag provided. Args: tag: Object with the generated tag. msg: Object containing received message. counter: Numbers of messages transmitted previously in the conversation in this direction (i.e. from the peer to the user). Returns: None. Raises: TagCheckError: Raised whenever the method fails to check the tag due to a ValueError in the arguments. IncorrectTagError: Raised whenever the tag is incorrect. """ prefix_length = max(len(tag), self._min_peer_tag_len) prefix_length = min(prefix_length, Glome.MAX_TAG_LEN) try: correct_tag = _tag(msg, counter, self._receive_key)[:prefix_length] except ValueError as value_error: raise TagCheckError('Failed to check the tag') from value_error if not hmac.compare_digest(tag, correct_tag): raise IncorrectTagError('Tag provided does not match correct tag') def generate_keys() -> KeyPair: """Generates a private/public key pair. Provides a random key pair based output of os.urandom. The format matches the one requested by Glome Class. Args: None Returns: A KeyPair, containing a random private key and the public key derived from the generated private key """ private = x25519.X25519PrivateKey.from_private_bytes( os.urandom(Glome.MAX_TAG_LEN)) return KeyPair(private, private.public_key()) class AutoGlome: """Adds counter managing functionalities for GLOME protocol. This class is initialized by providing your peer's public key and optionally your private key. If a private key is not provided, one is automatically generated making use of `generate_keys`. On initialization, two counter (sending and receiving) are created and set to 0. Provides methods tag (to generate new tags) and check (to check receiving tags). """ def __init__(self, peer_key: x25519.X25519PublicKey, my_private_key: x25519.X25519PrivateKey = None, *, min_peer_tag_len: int = Glome.MAX_TAG_LEN, skippable_range: int = 0): """Initialize AutoGlome class. Performs the handshake, generates keys and counters. Args: peer_key: Your peer's public key. my_private_key: Your private key. min_peer_tag_len: Desired length (in bytes) for the tag. Must be an integer in range 1-32. keyword only. skippable_range: Number of messages that can be missed. keyword only. Must be non-negative. For more information please go to check method's documentation. Raises: ValueError: Raised whenever min_peer_tag_len is not in range 1-32 or skippable_length is a negative integer. ExchangeError: Raised whenever null shared secret is derived from user/peer key pair. """ if skippable_range < 0: raise ValueError( f'skippable_range (={skippable_range}) must be non-negative') self.glome = Glome(peer_key, my_private_key, min_peer_tag_len=min_peer_tag_len) self._sending_counter = 0 self._receiving_counter = 0 self.skippable_range = skippable_range @property def sending_counter(self) -> int: """Number of tags shared from the user to the peer. It is incremented each time a new tag is generated. It is always one byte long. When the counter gets past 255 it overflows at 0. Setter raises ValueError if provided integer is not in range 0-255. """ return self._sending_counter @sending_counter.setter def sending_counter(self, value: int): if not 0 <= value <= 255: raise ValueError('Counter must be in range 0-255') self._sending_counter = value @property def receiving_counter(self) -> int: """Number of tags the user receives from the peer. It is always one byte long. When the counter gets past 255 it restarts at 0. Every time a message is successfully checked, the receiving_counter is set to the next value after the last successful one. Note that if skippable_range is n the counter might be increased by any amount in range 1-n+1 after a successful check. Setter raises ValueError if provided counter is not in range 0-255. """ return self._receiving_counter @receiving_counter.setter def receiving_counter(self, value: int): if not 0 <= value <= 255: raise ValueError('Counter must be in range 0-255') self._receiving_counter = value @property def user_keys(self) -> KeyPair: """User's private and public keys used in handshake.""" return self.glome.user_keys @property def peer_key(self) -> x25519.X25519PublicKey: """Peer's public key used in handshake.""" return self.glome.peer_key def tag(self, msg: bytes) -> bytes: """Generates a tag from a message. Generates a tag matching some provided message and the internal sending counter. This tag is generated following GLOME protocol specification in the context of a communication from the users to theirs peers. Args: msg: Message to be transmitted. Returns: tag: Tag matching counter and msg. Raises: TagGenerationError: Raised whenever the method failed to generate tag due to ValueError in the arguments. """ tag = self.glome.tag(msg, self.sending_counter) self._sending_counter = (self._sending_counter + 1) % 256 return tag def check(self, tag: bytes, msg: bytes): """Check whether a tag is correct for some message. Checks if a tag matches some provided message and internal receiving counter. The method generates the matching tag following GLOME protocol specification in the context of a communication from the users' peers to the users and then is compared with the tag provided. If tag checking if not successful, the receiving counter remains unchanged. If skippable_range if greater than 0, the method try to check the tag against all counters in range [receiving_counter, receiving_counter + skippable_range], in order, until one is successful. If no one is successful, an exceptions is raised and receiving counter remains unchanged. Args: tag: Object with the generated tag. msg: Object containing received message. Returns: None. Raises: IncorrectTagError: Raised whenever the tag is incorrect. """ old_counter = self._receiving_counter for _ in range(self.skippable_range + 1): try: self.glome.check(tag, msg, self.receiving_counter) self._receiving_counter = (self._receiving_counter + 1) % 256 return None except IncorrectTagError: self._receiving_counter = (self._receiving_counter + 1) % 256 #If no counter matches. self._receiving_counter = old_counter raise IncorrectTagError('Tag provided does not match correct tag') glome-0.2/python/requirements.txt000066400000000000000000000000501476056666600173040ustar00rootroot00000000000000cryptography >= 2.5 hypothesis # tests glome-0.2/python/setup.py000066400000000000000000000011471476056666600155420ustar00rootroot00000000000000import setuptools with open("README.md", "r") as fh: long_description = fh.read() setuptools.setup( name="pyglome", version="0.0.2", author="Google LLC", description="A Python implementation of the GLOME protocol", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/google/glome", packages=["pyglome"], install_requires=[ "cryptography", ], classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: Apache Software License", ], python_requires='>=3.6', ) glome-0.2/python/test/000077500000000000000000000000001476056666600150045ustar00rootroot00000000000000glome-0.2/python/test/__init__.py000066400000000000000000000000001476056666600171030ustar00rootroot00000000000000glome-0.2/python/test/__main__.py000066400000000000000000000025351476056666600171030ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Module that implements unittests cases for Glome Class. """ import unittest import sys import test.glome_test, test.autoglome_test, test.fuzzing_test def suite(): """Suite of test to run""" glome_tests = unittest.TestLoader().loadTestsFromModule(test.glome_test) autoglome_tests = unittest.TestLoader().loadTestsFromModule( test.autoglome_test) fuzzing_tests = unittest.TestLoader().loadTestsFromModule(test.fuzzing_test) return unittest.TestSuite([glome_tests, autoglome_tests, fuzzing_tests]) if __name__ == '__main__': unittest.TextTestRunner(verbosity=2).run(suite()) # Nice verbosy output result = unittest.TestResult() suite().run(result) sys.exit(len(result.errors) + len(result.failures)) # Correct exitcode glome-0.2/python/test/autoglome_test.py000066400000000000000000000124021476056666600204100ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Module that implements unittests cases for AutoGlome Class. """ import unittest from test import test_vectors from cryptography.hazmat.primitives.asymmetric import x25519 import pyglome class AutoGlomeTestVector: """Class that encapsulate needed components for testing AutoGlome Class.""" def __init__(self, test_vector, min_peer_tag_len, skippable_range): self.data = test_vector self.skippable_range = skippable_range peer_key = x25519.X25519PublicKey.from_public_bytes(self.data.kb) my_key = x25519.X25519PrivateKey.from_private_bytes(self.data.kap) self.sender_glome = pyglome.AutoGlome(peer_key, my_key, min_peer_tag_len=min_peer_tag_len, skippable_range=skippable_range) peer_key = x25519.X25519PublicKey.from_public_bytes(self.data.ka) my_key = x25519.X25519PrivateKey.from_private_bytes(self.data.kbp) self.receiver_glome = pyglome.AutoGlome( peer_key, my_key, min_peer_tag_len=min_peer_tag_len, skippable_range=skippable_range) class AutoGlomeTestBase: """Test keys constructions, tag generation and tag checking for AutoGlome.""" def __init__(self): self.test_vector = None def test_check_counters_raise_exceptions_when_incorrect(self): test_vector = self.test_vector with self.assertRaises(ValueError): test_vector.sender_glome.sending_counter = -1 with self.assertRaises(ValueError): test_vector.receiver_glome.sending_counter = 256 with self.assertRaises(ValueError): test_vector.sender_glome.receiving_counter = 280 with self.assertRaises(ValueError): test_vector.receiver_glome.receiving_counter = 280 def test_check_counters_dont_raise_exceptions_when_correct(self): test_vector = self.test_vector try: test_vector.sender_glome.sending_counter = 0 test_vector.receiver_glome.sending_counter = 23 test_vector.sender_glome.receiving_counter = 123 test_vector.receiver_glome.receiving_counter = 255 except ValueError: self.fail('properties raised ValueError unexpectedly!') def test_check_counters_are_correctly_set(self): test_vector = self.test_vector test_vector.sender_glome.sending_counter = 0 self.assertEqual(test_vector.sender_glome.sending_counter, 0) test_vector.receiver_glome.sending_counter = 23 self.assertEqual(test_vector.receiver_glome.sending_counter, 23) test_vector.sender_glome.receiving_counter = 123 self.assertEqual(test_vector.sender_glome.receiving_counter, 123) test_vector.receiver_glome.receiving_counter = 255 self.assertEqual(test_vector.receiver_glome.receiving_counter, 255) def test_tag(self): test_vector = self.test_vector test_vector.sender_glome.sending_counter = test_vector.data.counter self.assertEqual(test_vector.sender_glome.tag(test_vector.data.msg), test_vector.data.tag) def test_skippable_range(self): test_vector = self.test_vector try: test_vector.receiver_glome.receiving_counter = ( test_vector.data.counter - test_vector.skippable_range) % 256 test_vector.receiver_glome.check(test_vector.data.tag, msg=test_vector.data.msg) self.assertEqual((test_vector.data.counter + 1) % 256, test_vector.receiver_glome.receiving_counter) except pyglome.IncorrectTagError: self.fail('check() raised IncorrectTagError unexpectedly!') class AutoGlomeTest1(unittest.TestCase, AutoGlomeTestBase): """Autoglome test using test vector #1 from the protocol documentation.""" def __init__(self, *args, **kwargs): super(__class__, self).__init__(*args, **kwargs) self.test_vector = AutoGlomeTestVector(test_vectors.TEST1, min_peer_tag_len=32, skippable_range=0) class AutoTest2(unittest.TestCase, AutoGlomeTestBase): """Autoglome test using test vector #2 from the protocol documentation.""" def __init__(self, *args, **kwargs): super(__class__, self).__init__(*args, **kwargs) self.test_vector = AutoGlomeTestVector(test_vectors.TEST2, min_peer_tag_len=8, skippable_range=10) if __name__ == '__main__': unittest.main() glome-0.2/python/test/fuzzing_test.py000066400000000000000000000161711476056666600201170ustar00rootroot00000000000000# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Python GLOME fuzz main. This module that implement some easy fuzz testing for pyglome. """ import hypothesis import unittest from cryptography.hazmat.primitives.asymmetric import x25519 import pyglome def _glome(private_bytes, public_bytes, msg, tag, counter, min_tag_len): """Calls basic utilities of glome class, accepts only documented exceptions. The intention of this function is to be fuzzed over to find cases that throw unexpected exceptions.""" try: private_key = x25519.X25519PrivateKey.from_private_bytes(private_bytes) peer_key = x25519.X25519PublicKey.from_public_bytes(public_bytes) except (ValueError, pyglome.ExchangeError): return try: tag_manager = pyglome.Glome(peer_key, private_key, min_tag_len) except ValueError: return # Call to property method to test side effects tag_manager.user_keys tag_manager.peer_key try: tag_manager.tag(msg, counter) except pyglome.TagGenerationError: pass try: tag_manager.check(tag, msg, counter) except (pyglome.IncorrectTagError, pyglome.TagCheckError): pass def _autoglome(private_bytes, public_bytes, msg, tag, counter, min_tag_len, skippable_range): """Calls basic utilities of autoglome class, accepts only documented exceptions. The intention of this function is to be fuzzed over to find cases that throw unexpected exceptions.""" try: private_key = x25519.X25519PrivateKey.from_private_bytes(private_bytes) peer_key = x25519.X25519PublicKey.from_public_bytes(public_bytes) except (ValueError, pyglome.ExchangeError): return try: tag_manager = pyglome.AutoGlome(peer_key, private_key, min_peer_tag_len=min_tag_len, skippable_range=skippable_range) except ValueError: return # Call to property method to test side effects tag_manager.user_keys tag_manager.peer_key tag_manager.sending_counter tag_manager.receiving_counter try: tag_manager.sending_counter = counter except ValueError: pass try: tag_manager.receiving_counter = counter except ValueError: pass try: tag_manager.tag(msg) except pyglome.TagGenerationError: pass try: tag_manager.check(tag, msg) except (pyglome.IncorrectTagError, pyglome.TagCheckError): pass @hypothesis.settings(max_examples=10**7) @hypothesis.given( hypothesis.strategies.binary(min_size=32, max_size=32), #private_bytes hypothesis.strategies.binary(min_size=32, max_size=32), #public_bytes hypothesis.strategies.binary(), #msg hypothesis.strategies.binary(min_size=32, max_size=32), #tag hypothesis.strategies.integers(), #counter hypothesis.strategies.integers()) #min_tag_len def glome_test(private_bytes, public_bytes, msg, tag, counter, min_tag_len): """Add hypothesis decorator to _glome function""" _glome(private_bytes, public_bytes, msg, tag, counter, min_tag_len) @hypothesis.settings(max_examples=10**7) @hypothesis.given( hypothesis.strategies.binary(min_size=32, max_size=32), #private_bytes hypothesis.strategies.binary(min_size=32, max_size=32), #public_bytes hypothesis.strategies.binary(), #msg hypothesis.strategies.binary(min_size=32, max_size=32), #tag hypothesis.strategies.integers(), #counter hypothesis.strategies.integers(), #min_tag_len hypothesis.strategies.integers()) #skippable def autoglome_test(private_bytes, public_bytes, msg, tag, counter, min_tag_len, skippable): """Add hypothesis decorator to _autoglome function""" _autoglome(private_bytes, public_bytes, msg, tag, counter, min_tag_len, skippable) @hypothesis.settings(max_examples=10**5) @hypothesis.given( hypothesis.strategies.binary(), #private_bytes hypothesis.strategies.binary(), #public_bytes hypothesis.strategies.binary(), #msg hypothesis.strategies.binary(min_size=32, max_size=32), #tag hypothesis.strategies.integers(), #counter hypothesis.strategies.integers()) #min_tag_len def glome_unsized_keys_test(private_bytes, public_bytes, msg, tag, counter, min_tag_len): """Add hypothesis decorator to _glome function""" _glome(private_bytes, public_bytes, msg, tag, counter, min_tag_len) @hypothesis.settings(max_examples=10**5) @hypothesis.given( hypothesis.strategies.binary(), #private_bytes hypothesis.strategies.binary(), #public_bytes hypothesis.strategies.binary(), #msg hypothesis.strategies.binary(min_size=32, max_size=32), #tag hypothesis.strategies.integers(), #counter hypothesis.strategies.integers(), #min_tag_len hypothesis.strategies.integers()) #skippable def autoglome_unsized_keys_test(private_bytes, public_bytes, msg, tag, counter, min_tag_len, skippable): """Add hypothesis decorator to _autoglome function""" _autoglome(private_bytes, public_bytes, msg, tag, counter, min_tag_len, skippable) class GlomeTest1(unittest.TestCase): """Test Class that check one iteration of each function. Uses sample input to test whether the fuzzing function raise unexpected exceptions.""" def __init__(self, *args, **kwargs): super(__class__, self).__init__(*args, **kwargs) constant_one = b'\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11' self.private_bytes = constant_one self.public_bytes = constant_one self.msg = b'\x11' self.tag = constant_one self.counter = 1 self.min_tag_len = 1 self.skippable = 1 def test_glome_fuzz(self): """Test glome fuzzing function with trivial example""" try: _glome(self.private_bytes, self.public_bytes, self.msg, self.tag, self.counter, self.min_tag_len) except: self.fail('Unexpected exception was raised.') def test_autoglome_fuzz(self): """Test autoglome fuzzing function with trivial example""" try: _autoglome(self.private_bytes, self.public_bytes, self.msg, self.tag, self.counter, self.min_tag_len, self.skippable) except: self.fail('Unexpected exception was raised.') if __name__ == "__main__": glome_test() autoglome_test() glome_unsized_keys_test() autoglome_unsized_keys_test() glome-0.2/python/test/glome_test.py000066400000000000000000000143271476056666600175270ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Module that implements unittests cases for Glome Class. """ import unittest from cryptography.hazmat.primitives.asymmetric import x25519 import pyglome from test import test_vectors class GlomeTestVector: """Class that encapsulates needed components for testing Glome Class.""" def __init__(self, test_vector, truncated_length): self.data = test_vector peer_key = x25519.X25519PublicKey.from_public_bytes(self.data.kb) my_key = x25519.X25519PrivateKey.from_private_bytes(self.data.kap) self.sender_glomes = pyglome.Glome(peer_key, my_key) peer_key = x25519.X25519PublicKey.from_public_bytes(self.data.ka) my_key = x25519.X25519PrivateKey.from_private_bytes(self.data.kbp) self.receiver_glomes = pyglome.Glome(peer_key, my_key) self.truncated_tag_length = truncated_length peer_key = x25519.X25519PublicKey.from_public_bytes(self.data.kb) my_key = x25519.X25519PrivateKey.from_private_bytes(self.data.kap) self.truncated_sender_glomes = pyglome.Glome(peer_key, my_key, self.truncated_tag_length) peer_key = x25519.X25519PublicKey.from_public_bytes(self.data.ka) my_key = x25519.X25519PrivateKey.from_private_bytes(self.data.kbp) self.truncated_receiver_glomes = pyglome.Glome( peer_key, my_key, self.truncated_tag_length) class GlomeTestBase: """Test Class for Glome Class. Implements the logic for tests tag and key generation, as well as tag checking. """ def __init__(self, *args, **kwargs): super(__class__, self).__init__(*args, **kwargs) self.test_vector = None def test_keys_generation(self): test_vector = self.test_vector self.assertEqual( test_vector.sender_glomes._send_key, test_vector.data.sk + test_vector.data.kb + test_vector.data.ka) self.assertEqual( test_vector.sender_glomes._receive_key, test_vector.data.sk + test_vector.data.ka + test_vector.data.kb) self.assertEqual( test_vector.receiver_glomes._send_key, test_vector.data.sk + test_vector.data.ka + test_vector.data.kb) self.assertEqual( test_vector.receiver_glomes._receive_key, test_vector.data.sk + test_vector.data.kb + test_vector.data.ka) def test_tag_generation(self): test_vector = self.test_vector self.assertEqual( test_vector.sender_glomes.tag(test_vector.data.msg, test_vector.data.counter), test_vector.data.tag) def test_check_raises_exception_when_incorrect(self): test_vector = self.test_vector with self.assertRaises(pyglome.IncorrectTagError): test_vector.sender_glomes.check(tag=bytes([123]), msg=test_vector.data.msg, counter=0) with self.assertRaises(pyglome.IncorrectTagError): test_vector.receiver_glomes.check(tag=bytes([234]), msg=test_vector.data.msg, counter=0) with self.assertRaises(pyglome.IncorrectTagError): test_vector.sender_glomes.check( tag=test_vector.data.tag[:test_vector.truncated_tag_length], msg=test_vector.data.msg, counter=0) with self.assertRaises(pyglome.IncorrectTagError): test_vector.truncated_receiver_glomes.check( tag=test_vector.data.tag[:test_vector.truncated_tag_length] + test_vector.data.tag[:test_vector.truncated_tag_length], msg=test_vector.data.msg, counter=test_vector.data.counter) with self.assertRaises(pyglome.IncorrectTagError): test_vector.truncated_receiver_glomes.check( tag=test_vector.data.tag[:test_vector.truncated_tag_length] + test_vector.data.tag[test_vector.truncated_tag_length::-1], msg=test_vector.data.msg, counter=test_vector.data.counter) def test_check_doesnt_raise_exception_when_correct(self): test_vector = self.test_vector try: test_vector.receiver_glomes.check(test_vector.data.tag, msg=test_vector.data.msg, counter=test_vector.data.counter) test_vector.truncated_receiver_glomes.check( test_vector.data.tag[:test_vector.truncated_tag_length], msg=test_vector.data.msg, counter=test_vector.data.counter) test_vector.truncated_receiver_glomes.check( test_vector.data.tag[:test_vector.truncated_tag_length + 2], msg=test_vector.data.msg, counter=test_vector.data.counter) except pyglome.IncorrectTagError: self.fail('check() raised IncorrectTagError unexpectedly!') class GlomeTest1(unittest.TestCase, GlomeTestBase): """TestCase based on test vector #1 from protocol documentation""" def __init__(self, *args, **kwargs): super(__class__, self).__init__(*args, **kwargs) self.test_vector = GlomeTestVector(test_vectors.TEST1, 8) class GlomeTest2(unittest.TestCase, GlomeTestBase): """TestCase based on test vector #1 from protocol documentation""" def __init__(self, *args, **kwargs): super(__class__, self).__init__(*args, **kwargs) self.test_vector = GlomeTestVector(test_vectors.TEST2, 8) if __name__ == '__main__': unittest.main() glome-0.2/python/test/test_vectors.py000066400000000000000000000050731476056666600201070ustar00rootroot00000000000000# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ This module includes test vectors from the protocol reference. """ class TestVector: """Class that encapsulate needed components for testing. Consider a use case where an user A sends a message to user B. Attributes: kap: A's private key. ka: A's public key. kbp: B's private key. kb: B's public key. counter: number of messages already shared. msg: message to share. sk: shared secret betweens A and B. tag: tag that matches ka, kb, counter and msg. """ def __init__(self, kap: str, ka: str, kbp: str, kb: str, counter: int, msg: str, sk: str, tag: str): """Constructor for TestVector Class.""" self.kap = bytes.fromhex(kap) self.ka = bytes.fromhex(ka) self.kbp = bytes.fromhex(kbp) self.kb = bytes.fromhex(kb) self.counter = counter self.msg = msg.encode(encoding="ascii") self.sk = bytes.fromhex(sk) self.tag = bytes.fromhex(tag) TEST1 = TestVector( kap='77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a', ka='8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a', kbp='5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb', kb='de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f', counter=0, msg='The quick brown fox', sk='4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742', tag='9c44389f462d35d0672faf73a5e118f8b9f5c340bbe8d340e2b947c205ea4fa3') TEST2 = TestVector( kap='b105f00db105f00db105f00db105f00db105f00db105f00db105f00db105f00d', ka='d1b6941bba120bcd131f335da15778d9c68dadd398ae61cf8e7d94484ee65647', kbp='fee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1dead', kb='872f435bb8b89d0e3ad62aa2e511074ee195e1c39ef6a88001418be656e3c376', counter=100, msg='The quick brown fox', sk='4b1ee05fcd2ae53ebe4c9ec94915cb057109389a2aa415f26986bddebf379d67', tag='06476f1f314b06c7f96e5dc62b2308268cbdb6140aefeeb55940731863032277') glome-0.2/rust/000077500000000000000000000000001476056666600135015ustar00rootroot00000000000000glome-0.2/rust/Cargo.lock000066400000000000000000000434701476056666600154160ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "ahash" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "once_cell", "version_check", "zerocopy", ] [[package]] name = "anstream" version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys", ] [[package]] name = "arraydeque" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "cc" version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_derive" version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "cpufeatures" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "curve25519-dalek" version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", "fiat-crypto", "rustc_version", "subtle", "zeroize", ] [[package]] name = "curve25519-dalek-derive" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "errno" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fiat-crypto" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38793c55593b33412e3ae40c2c9781ffaa6f438f6f8c10f24e71846fbd7ae01e" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "glome" version = "0.1.0" dependencies = [ "base64", "clap", "hex", "hex-literal", "hmac", "openssl", "sha2", "tempfile", "x25519-dalek", "yaml-rust2", ] [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", ] [[package]] name = "hashlink" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ "hashbrown", ] [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hex-literal" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "libc" version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "openssl-sys" version = "0.9.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "proc-macro2" version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ "semver", ] [[package]] name = "rustix" version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "semver" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", "once_cell", "rustix", "windows-sys", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "x25519-dalek" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", "rand_core", "serde", "zeroize", ] [[package]] name = "yaml-rust2" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a1a1c0bc9823338a3bdf8c61f994f23ac004c6fa32c08cd152984499b445e8d" dependencies = [ "arraydeque", "encoding_rs", "hashlink", ] [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", "syn", ] glome-0.2/rust/Cargo.toml000066400000000000000000000015251476056666600154340ustar00rootroot00000000000000[package] name = "glome" version = "0.1.0" edition = "2021" [features] default = [ "dalek" ] dalek = [ "dep:x25519-dalek" ] openssl = [ "dep:openssl" ] cli = [ "dep:base64", "dep:clap" ] [dependencies] # lib sha2 = "0.10" hmac = "0.12" x25519-dalek = { version = "2.0", features = ["getrandom", "static_secrets"], optional = true } openssl = { version = "0.10", optional = true } # cli base64 = { version = "0.21", optional = true } clap = { version = "4", features = ["derive"], optional = true} [dev-dependencies] # test hex = "0.4" hex-literal = "0.3" tempfile = "3.14.0" yaml-rust2 = "0.9" [lib] name = "glome" path = "src/lib.rs" [[bin]] name = "glome" path = "src/cli/bin.rs" # The binary has more dependencies than the library. We allow skipping the binary and its # dependencies by hiding it behind a feature. required-features = ["cli"] glome-0.2/rust/src/000077500000000000000000000000001476056666600142705ustar00rootroot00000000000000glome-0.2/rust/src/cli/000077500000000000000000000000001476056666600150375ustar00rootroot00000000000000glome-0.2/rust/src/cli/bin.rs000066400000000000000000000354431476056666600161660ustar00rootroot00000000000000use base64::{alphabet, engine, engine::general_purpose, Engine as _}; use clap::{Args, Parser, Subcommand}; use glome::PrivateKey; use std::convert::TryInto; use std::error::Error; use std::fs; use std::io; use std::path::PathBuf; use x25519_dalek::{PublicKey, StaticSecret}; #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli { #[command(subcommand)] command: Glome, } #[derive(Args)] struct TagArgs { /// Path to secret key #[arg(short, long, value_name = "FILE")] key: PathBuf, /// Path to peer's public key #[arg(short, long, value_name = "FILE")] peer: PathBuf, /// Message counter index #[arg(short, long, value_name = "n")] counter: Option, } #[derive(Args)] struct VerifyArgs { /// Path to secret key #[arg(short, long, value_name = "FILE")] key: PathBuf, /// Path to peer's public key #[arg(short, long, value_name = "FILE")] peer: PathBuf, /// Message counter index #[arg(short, long, value_name = "n")] counter: Option, /// Minimum tag length /// /// Ideally a multiple of 4, defaults to 10 matching the /// MIN_ENCODED_AUTHCODE_LEN in login/login.h. /// Must be at least 2 and will be increased to 2 if the argument is lower. #[arg(long, value_name = "n", default_value_t = 10)] min_tag_length: u8, /// Tag to verify tag: String, } #[derive(Args)] struct LoginArgs { /// Path to secret key #[arg(short, long, value_name = "FILE")] key: PathBuf, /// Challenge to generate a tag for challenge: String, } #[derive(Subcommand)] enum Glome { /// Generate a new secret key and print it to stdout Genkey, /// Read a private key from stdin and write its public key to stdout Pubkey, /// Tag a message read from stdin Tag(TagArgs), /// Verify a message tag Verify(VerifyArgs), /// Generate a tag for a GLOME-Login challenge Login(LoginArgs), } type CommandResult = Result<(), Box>; fn genkey(stdout: &mut dyn io::Write) -> CommandResult { Ok(stdout.write_all(StaticSecret::random().as_bytes())?) } fn pubkey(stdin: &mut dyn io::Read, stdout: &mut dyn io::Write) -> CommandResult { let mut buf: [u8; 32] = [0; 32]; stdin.read_exact(&mut buf)?; let sk: StaticSecret = buf.into(); let pk: PublicKey = (&sk).into(); Ok(writeln!( stdout, "glome-v1 {}", general_purpose::URL_SAFE.encode(pk.as_bytes()) )?) } fn read_key(path: &PathBuf) -> Result<[u8; 32], Box> { let b: Box<[u8; 32]> = fs::read(path) .map_err(|e| format!("reading file {:?}: {}", path, e))? .into_boxed_slice() .try_into() .map_err(|_| "private key must have exactly 32 bytes")?; Ok(*b) } fn read_pub(path: &PathBuf) -> Result<[u8; 32], Box> { let pubkey = fs::read_to_string(path).map_err(|e| format!("reading file {:?}: {}", path, e))?; let b64 = match pubkey.strip_prefix("glome-v1 ") { Some(tail) => tail.trim_end(), None => return Err("unsupported public key version, expected 'glome-v1'".into()), }; let raw: Box<[u8; 32]> = general_purpose::URL_SAFE .decode(b64) .map_err(|e| format!("decoding public key: {}", e))? .into_boxed_slice() .try_into() .map_err(|_| "public key must have exactly 32 bytes")?; Ok(*raw) } fn gentag(args: &TagArgs, stdin: &mut dyn io::Read, stdout: &mut dyn io::Write) -> CommandResult { let ours: StaticSecret = read_key(&args.key)?.into(); let theirs: PublicKey = read_pub(&args.peer)?.into(); let ctr = args.counter.unwrap_or_default(); let mut msg = Vec::new(); stdin.read_to_end(&mut msg)?; let t = glome::tag(&ours, &theirs, ctr, &msg); let encoded = general_purpose::URL_SAFE.encode(t); Ok(stdout.write_all(encoded.as_bytes())?) } fn verify(args: &VerifyArgs, stdin: &mut dyn io::Read) -> CommandResult { let min_tag_length = if args.min_tag_length > 2 { args.min_tag_length } else { 2 }; let ours: StaticSecret = read_key(&args.key)?.into(); let theirs: PublicKey = read_pub(&args.peer)?.into(); let ctr = args.counter.unwrap_or_default(); let mut msg = Vec::new(); stdin.read_to_end(&mut msg)?; // We want to allow truncated tags, but not all truncations are valid // base64. A single encoded byte only holds 6 bits and can't be decoded // into a byte, so we need to ignore it by stripping it off. let mut tag_b64 = args.tag.clone(); if tag_b64.len() % 4 == 1 { tag_b64.truncate(tag_b64.len() - 1); } // Ensure that we're comparing at least one byte. if tag_b64.len() < min_tag_length.into() { return Err("tag too short".into()); } // Truncation can cause trailing bits and missing padding if the truncated // length is not a multiple of 4. Make sure that the base64 engine can deal // with that. let permissive_config = engine::GeneralPurposeConfig::new() .with_decode_allow_trailing_bits(true) .with_decode_padding_mode(engine::DecodePaddingMode::Indifferent); let permissive_engine = engine::GeneralPurpose::new(&alphabet::URL_SAFE, permissive_config); if !glome::verify( &ours, &theirs, ctr, &msg, &permissive_engine.decode(tag_b64)?, ) { return Err("tags did not match".into()); } Ok(()) } fn login(args: &LoginArgs, stdout: &mut dyn io::Write) -> CommandResult { let ours: StaticSecret = read_key(&args.key)?.into(); let challenge_start = match args.challenge.find("v2/") { Some(n) => n, None => return Err("challenge should have a v2/ prefix".into()), }; let (_, challenge) = args.challenge.split_at(challenge_start + 3); let parts: Vec<_> = challenge.split("/").collect(); if parts.len() != 4 || !parts[3].is_empty() { return Err("unexpected format".into()); } let mut handshake = general_purpose::URL_SAFE.decode(parts[0])?; if handshake.len() < 33 { return Err("handshake too short".into()); } let message_tag_prefix = handshake.split_off(33); let raw_public_key: [u8; 32] = handshake .split_off(1) .try_into() .expect("there should be exactly 33 bytes in the argument"); let theirs: PublicKey = raw_public_key.into(); // Check public key prefix, if present. let prefix = handshake[0]; if prefix & 1 << 7 == 0 { let pubkey = ours.public_key().to_bytes(); if pubkey[31] != prefix { return Err(format!("challenge was generated for a different key: our key has MSB {}, challenge requests {}", pubkey[31], prefix).into()); } } let msg = [parts[1], parts[2]].join("/"); // Check message tag in challenge, if present. let message_tag_prefix_len = message_tag_prefix.len(); if message_tag_prefix_len > 0 && !glome::verify(&ours, &theirs, 0, msg.as_bytes(), &message_tag_prefix) { return Err("unexpected message tag prefix".into()); } let t = glome::tag(&ours, &theirs, 0, msg.as_bytes()); let encoded = general_purpose::URL_SAFE.encode(t); Ok(stdout.write_all(encoded.as_bytes())?) } fn main() -> CommandResult { match &Cli::parse().command { Glome::Genkey => genkey(&mut io::stdout()), Glome::Pubkey => pubkey(&mut io::stdin(), &mut io::stdout()), Glome::Tag(tag_args) => gentag(tag_args, &mut io::stdin(), &mut io::stdout()), Glome::Verify(verify_args) => verify(verify_args, &mut io::stdin()), Glome::Login(login_args) => login(login_args, &mut io::stdout()), } } #[cfg(test)] mod tests { use io::Write; use std::{fmt::Debug, path::Path}; use tempfile::NamedTempFile; use yaml_rust2::{Yaml, YamlLoader}; use super::*; #[derive(Debug)] struct Person { private: [u8; 32], public_cli: String, } impl From<&Yaml> for Person { fn from(case: &Yaml) -> Self { let private: [u8; 32] = hex::decode(case["private-key"]["hex"].as_str().unwrap()) .unwrap() .try_into() .unwrap(); let public_cli = case["public-key"]["glome-cli"] .as_str() .unwrap() .to_string(); Person { private, public_cli, } } } #[derive(Debug)] struct TestVector { name: String, alice: Person, bob: Person, message: String, tag: String, host_id_type: String, host_id: String, action: String, } impl From<&Yaml> for TestVector { fn from(case: &Yaml) -> Self { TestVector { name: format!("vector-{:02}", case["vector"].as_i64().unwrap()), alice: (&case["alice"]).into(), bob: (&case["bob"]).into(), message: case["message"].as_str().unwrap().to_string(), tag: case["tag"].as_str().unwrap().to_string(), host_id_type: case["host-id-type"].as_str().unwrap().to_string(), host_id: case["host-id"].as_str().unwrap().to_string(), action: case["action"].as_str().unwrap().to_string(), } } } fn test_vectors() -> Vec { let rust_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR should be set"); let vectors_file = Path::new(&rust_dir).join("../docs/login-v2-test-vectors.yaml"); let content = fs::read_to_string(vectors_file).expect("test vectors should be readable"); let cases = &YamlLoader::load_from_str(&content).expect("test vectors should be yaml")[0]; let mut vectors: Vec = Vec::new(); for case in cases.as_vec().expect("top level should be a list") { vectors.push(case.into()); } vectors } #[test] fn test_genkey() { let mut stdout = io::Cursor::new(Vec::new()); genkey(&mut stdout).expect("genkey should work"); assert_eq!(32, stdout.get_ref().len()) } fn cursor_to_string(cursor: &io::Cursor>) -> String { std::str::from_utf8(cursor.get_ref().as_slice()) .expect("all test vectors should be UTF-8") .to_string() } #[test] fn test_pubkey() { for tc in test_vectors() { for person in [tc.alice, tc.bob] { let mut stdin = io::Cursor::new(person.private); let mut stdout = io::Cursor::new(Vec::new()); pubkey(&mut stdin, &mut stdout).expect("pubkey should work"); let expected = format!("{}\n", person.public_cli); let actual = cursor_to_string(&stdout); assert_eq!(expected, actual, "vector {}", tc.name) } } } fn temp_file(content: &[u8]) -> NamedTempFile { let mut temp_file = NamedTempFile::new().expect("temp file should be creatable"); temp_file .write_all(content) .expect("temp file should be writable"); temp_file } fn login_message(tc: &TestVector) -> Vec { let host = if tc.host_id_type.is_empty() { &tc.host_id } else { &format!("{}:{}", tc.host_id_type, tc.host_id) }; // Some test messages contain slashes, but we don't want to add a dependency for URL // escaping, so we just replace the one character that occurs in the test vectors. format!("{}/{}", host, tc.action.replace("/", "%2F")).into_bytes() } #[test] fn test_tag() { for tc in test_vectors() { let mut stdin = io::Cursor::new(login_message(&tc)); let mut stdout = io::Cursor::new(Vec::new()); let key_file = temp_file(&tc.bob.private); let peer_file = temp_file(tc.alice.public_cli.as_bytes()); let args = TagArgs { key: key_file.path().to_path_buf(), peer: peer_file.path().to_path_buf(), counter: None, }; gentag(&args, &mut stdin, &mut stdout).expect("gentag should work"); let actual = cursor_to_string(&stdout); assert_eq!(tc.tag, actual, "vector {}", tc.name) } } #[test] fn test_verify() { for tc in test_vectors() { let key_file = temp_file(&tc.alice.private); let peer_file = temp_file(tc.bob.public_cli.as_bytes()); for n in 2..=tc.tag.len() { let mut stdin = io::Cursor::new(login_message(&tc)); let mut tag = tc.tag.clone(); tag.truncate(n); let args = VerifyArgs { key: key_file.path().to_path_buf(), peer: peer_file.path().to_path_buf(), counter: None, min_tag_length: 2, tag, }; verify(&args, &mut stdin) .map_err(|e| format!("test case {}: tag length {}: {}", tc.name, n, e)) .expect("should not fail") } { let mut stdin = io::Cursor::new(login_message(&tc)); let args = VerifyArgs { key: key_file.path().to_path_buf(), peer: peer_file.path().to_path_buf(), counter: None, min_tag_length: 2, tag: "MDEyMzQ1Njc4".to_owned(), }; assert!( verify(&args, &mut stdin).is_err(), "test case {}: verify should fail for bad tag", tc.name ); } for n in 0..16 { let mut stdin = io::Cursor::new(login_message(&tc)); let mut tag = tc.tag.clone(); tag.truncate(n); let args = VerifyArgs { key: key_file.path().to_path_buf(), peer: peer_file.path().to_path_buf(), counter: None, min_tag_length: 16, tag, }; assert!( verify(&args, &mut stdin).is_err(), "test case {}: verify should fail for tag length {}", tc.name, n ); } } } #[test] fn test_login() { for tc in test_vectors() { let mut stdout = io::Cursor::new(Vec::new()); let key_file = temp_file(&tc.bob.private); let args = LoginArgs { key: key_file.path().to_path_buf(), challenge: tc.message, }; login(&args, &mut stdout).expect("login should work"); let actual = cursor_to_string(&stdout); assert_eq!(tc.tag, actual, "vector {}", tc.name) } } } glome-0.2/rust/src/dalek.rs000066400000000000000000000033511476056666600157200ustar00rootroot00000000000000//! [PrivateKey] and [PublicKey] implementations for types in [x25519_dalek]. //! //! Usage example: //! //! ``` //! use hex_literal::hex; //! use x25519_dalek::{PublicKey, StaticSecret}; //! use glome::{tag,verify}; //! //! let alice_private_key: StaticSecret = hex!("fee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1dead").into(); //! let alice_public_key: PublicKey = (&alice_private_key).into(); //! //! let bob_private_key: StaticSecret = hex!("b105f00db105f00db105f00db105f00db105f00db105f00db105f00db105f00d").into(); //! let bob_public_key: PublicKey = (&bob_private_key).into(); //! //! let msg = b"Hello, world!"; //! let t = tag(&alice_private_key, &bob_public_key, 0u8, msg); //! assert!(verify(&bob_private_key, &alice_public_key, 0u8, msg, &t)); //! assert!(!verify(&bob_private_key, &alice_public_key, 0u8, b"kthxbai", &t)); //! ``` use x25519_dalek::{PublicKey as DalekPublicKey, StaticSecret}; use crate::{PrivateKey, PublicKey}; impl PublicKey for DalekPublicKey { fn to_bytes(&self) -> [u8; 32] { self.to_bytes() } } impl PrivateKey for StaticSecret { type PublicKey = DalekPublicKey; fn dh(&self, theirs: &Self::PublicKey) -> [u8; 32] { self.diffie_hellman(theirs).to_bytes() } fn public_key(&self) -> Self::PublicKey { self.into() } } #[cfg(test)] mod tests { use super::*; use crate::tests::{run_vector_1, run_vector_2}; fn keypair(b: [u8; 32]) -> (StaticSecret, DalekPublicKey) { let secret: StaticSecret = b.into(); let public: DalekPublicKey = (&secret).into(); (secret, public) } #[test] fn test_vector_1() { run_vector_1(&keypair) } #[test] fn test_vector_2() { run_vector_2(&keypair) } } glome-0.2/rust/src/lib.rs000066400000000000000000000135111476056666600154050ustar00rootroot00000000000000#![no_std] #![warn(missing_docs)] //! # GLOME - Generic Low-Overhead Message Exchange //! //! GLOME is a lightweight message authentication protocol based on X25519 //! keys and HMAC-SHA256. See for details on //! the protocol, usage patterns and implementations in other languages. //! //! The Rust implementation of GLOME works with its own [PrivateKey] and //! [PublicKey] traits to support more than one backing cryptography crate. //! It aims to provide implementations for the crates most commonly used, which //! can be activated with the corresponding crate feature. The default and //! recommended setting is to use [x25519_dalek]. Implementations should be //! verified with the test vectors in the `tests` module. use hmac::{Hmac, Mac}; use sha2::Sha256; type HmacSha256 = Hmac; /// An X25519 public key. pub trait PublicKey { /// to_bytes encodes the public key as a byte array according to RFC 7748. fn to_bytes(&self) -> [u8; 32]; } /// An X25519 private key. pub trait PrivateKey { /// PublicKey is the type of public keys corresponding to this type. type PublicKey: PublicKey; /// dh corresponds to the x25519 function from RFC 7748, computing a shared secret from a /// private key and a peer key. fn dh(&self, theirs: &Self::PublicKey) -> [u8; 32]; /// public_key computes the public key corresponding to this private key. fn public_key(&self) -> Self::PublicKey; } /// Compute a GLOME tag. /// /// The message counter argument `ctr` is used to prevent replay attacks in a /// series of messages. If only a single message is exchanged between two key /// pairs, it can be set to `0u8`. The counter value is not secret, but needs /// to be integrity protected. Usually, this is accomplished by both parties /// counting sent and received messages internally. /// /// The returned tag can be verified by the /// other party, using [verify] with its private key, our public key and the /// counter. pub fn tag(ours: &T, theirs: &T::PublicKey, ctr: u8, msg: &[u8]) -> [u8; 32] { let key = [ ours.dh(theirs), theirs.to_bytes(), ours.public_key().to_bytes(), ] .concat(); HmacSha256::new_from_slice(&key) .expect("HMAC can take key of any size") .chain_update([ctr]) .chain_update(msg) .finalize() .into_bytes() .into() } /// Verify a GLOME tag. /// /// This verifies tags produced by the [tag] function. The `tag` argument can /// be shorter than the original 32 bytes, in which case the expected tag is /// shortened to match the length of the given tag. The caller needs to ensure /// that the tag is long enough to meet the security requirements, i.e. prevent /// brute-forcing. /// /// The return value is `true` if and only if the given tag matches the expected tag. pub fn verify( ours: &T, theirs: &T::PublicKey, ctr: u8, msg: &[u8], tag: &[u8], ) -> bool { let key = [ ours.dh(theirs), ours.public_key().to_bytes(), theirs.to_bytes(), ] .concat(); HmacSha256::new_from_slice(&key) .expect("HMAC can take key of any size") .chain_update([ctr]) .chain_update(msg) .verify_truncated_left(tag) .is_ok() } #[cfg(feature = "dalek")] pub mod dalek; #[cfg(feature = "openssl")] pub mod openssl; /// The [tests] module provides functions to test implementations of [PrivateKey]/[PublicKey]. #[cfg(test)] pub mod tests { use super::*; use crate::PrivateKey; use hex_literal::hex; #[doc(hidden)] pub fn run_vector_1(load_keypair: &F) where T: PrivateKey, F: Fn([u8; 32]) -> (T, T::PublicKey), { let (apriv, apub) = load_keypair(hex!( "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a" )); let (bpriv, bpub) = load_keypair(hex!( "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb" )); let expected = hex!("9c44389f462d35d0672faf73a5e118f8b9f5c340bbe8d340e2b947c205ea4fa3"); assert_eq!(tag(&apriv, &bpub, 0, b"The quick brown fox"), expected); assert_ne!(tag(&bpriv, &apub, 0, b"The quick brown fox"), expected); assert!(verify( &bpriv, &apub, 0, b"The quick brown fox", &hex!("9c44389f462d") )); assert!(!verify( &bpriv, &apub, 0, b"The quick brown fox", &hex!("ffeeddccbbaa") )); assert!(!verify( &apriv, &bpub, 0, b"The quick brown fox", &hex!("9c44389f462d") )); } #[doc(hidden)] pub fn run_vector_2(load_keypair: &F) where T: PrivateKey, F: Fn([u8; 32]) -> (T, T::PublicKey), { let (apriv, apub) = load_keypair(hex!( "fee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1dead" )); let (bpriv, bpub) = load_keypair(hex!( "b105f00db105f00db105f00db105f00db105f00db105f00db105f00db105f00d" )); let expected = hex!("06476f1f314b06c7f96e5dc62b2308268cbdb6140aefeeb55940731863032277"); assert_eq!(tag(&bpriv, &apub, 100, b"The quick brown fox"), expected); assert_ne!(tag(&apriv, &bpub, 100, b"The quick brown fox"), expected); assert!(verify( &apriv, &bpub, 100, b"The quick brown fox", &hex!("06476f1f314b") )); assert!(!verify( &apriv, &bpub, 100, b"The quick brown fox", &hex!("ffeeddccbbaa") )); assert!(!verify( &bpriv, &apub, 100, b"The quick brown fox", &hex!("06476f1f314b") )); } } glome-0.2/rust/src/openssl.rs000066400000000000000000000073061476056666600163270ustar00rootroot00000000000000//! [PrivateKey] and [PublicKey] implementations for types in [openssl]. //! //! Although X25519 is specified to never need exception handling, the openssl //! crate returns [Result] values. The implementations in this module panic on //! returned errors. //! //! Usage example: //! //! ``` //! use hex_literal::hex; //! use openssl::pkey; //! use glome::{tag,verify}; //! //! let raw_private_key = [15u8; 32]; // <- our private key loaded from somewhere //! let private_key = pkey::PKey::private_key_from_raw_bytes(&raw_private_key, pkey::Id::X25519).unwrap(); //! let raw_public_key = [16u8; 32]; // <- their public key loaded from somewhere //! let public_key = pkey::PKey::public_key_from_raw_bytes(&raw_public_key, pkey::Id::X25519).unwrap(); //! //! let alice_raw_private_key = hex!("fee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1deadfee1dead"); //! let alice_private_key = pkey::PKey::private_key_from_raw_bytes(&alice_raw_private_key, pkey::Id::X25519).unwrap(); //! let alice_raw_public_key = hex!("872f435bb8b89d0e3ad62aa2e511074ee195e1c39ef6a88001418be656e3c376"); //! let alice_public_key = pkey::PKey::public_key_from_raw_bytes(&alice_raw_public_key, pkey::Id::X25519).unwrap(); //! //! let bob_raw_private_key = hex!("b105f00db105f00db105f00db105f00db105f00db105f00db105f00db105f00d"); //! let bob_private_key = pkey::PKey::private_key_from_raw_bytes(&bob_raw_private_key, pkey::Id::X25519).unwrap(); //! let bob_raw_public_key = hex!("d1b6941bba120bcd131f335da15778d9c68dadd398ae61cf8e7d94484ee65647"); //! let bob_public_key = pkey::PKey::public_key_from_raw_bytes(&bob_raw_public_key, pkey::Id::X25519).unwrap(); //! //! let msg = b"Hello, world!"; //! let t = tag(&alice_private_key, &bob_public_key, 0u8, msg); //! assert!(verify(&bob_private_key, &alice_public_key, 0u8, msg, &t)); //! assert!(!verify(&bob_private_key, &alice_public_key, 0u8, b"kthxbai", &t)); //! ``` use openssl::{derive, pkey}; use crate::{PrivateKey, PublicKey}; impl PublicKey for pkey::PKey { fn to_bytes(&self) -> [u8; 32] { self.raw_public_key() .expect("an X25519 key should be convertible to bytes") .try_into() .expect("raw public key should be 32 bytes long") } } impl PrivateKey for pkey::PKey { type PublicKey = pkey::PKey; fn dh(&self, theirs: &Self::PublicKey) -> [u8; 32] { let mut deriver = derive::Deriver::new(self) .expect("should be able to create a deriver from an X25519 key"); deriver .set_peer(theirs) .expect("should be able to set an X25519 public key as peer"); let mut secret = [0u8; 32]; let n = deriver .derive(&mut secret) .expect("should be able to derive shared secret"); assert_eq!(n, 32); secret } fn public_key(&self) -> Self::PublicKey { // TODO(burgerdev): is there a proper API for this? let b: [u8; 32] = self .raw_public_key() .expect("an X25519 key should be convertible to bytes") .try_into() .expect("raw public key should be 32 bytes long"); pkey::PKey::public_key_from_raw_bytes(&b, pkey::Id::X25519).expect("TODO") } } #[cfg(test)] mod tests { use super::*; use crate::tests::{run_vector_1, run_vector_2}; fn load_keypair(b: [u8; 32]) -> (pkey::PKey, pkey::PKey) { let secret = pkey::PKey::private_key_from_raw_bytes(&b, pkey::Id::X25519).unwrap(); let public = secret.public_key(); (secret, public) } #[test] fn test_vector_1() { run_vector_1(&load_keypair); } #[test] fn test_vector_2() { run_vector_2(&load_keypair); } } glome-0.2/shell.nix000066400000000000000000000007451476056666600143410ustar00rootroot00000000000000# run `nix-shell` in the same directory as this file with import { }; stdenv.mkDerivation { name = "glome"; buildInputs = [ # Build dependencies # Compiler conforming to C99 (e.g. gcc, clang) meson # >=0.49.2 ninja pkg-config openssl # >=1.1.1 glib # >=2.0 (glome-login and tests) linux-pam # (PAM module) # for `./go/` go # Test dependencies libpam-wrapper # Development tools clang-tools cpplint ]; }