├── .devcontainer └── devcontainer.json ├── .envrc ├── .github └── workflows │ ├── config_check.yml │ ├── integration.yml │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── docker ├── Dockerfile.aarch64 ├── Dockerfile.amd64 ├── Dockerfile_homelab ├── Dockerfile_influx.aarch64 └── Dockerfile_influx.amd64 ├── examples ├── example.db ├── rustica_agent_local.toml ├── rustica_external.toml ├── rustica_local_amazonkms.toml ├── rustica_local_multi.toml └── rustica_local_yubikey.toml ├── flake.lock ├── flake.nix ├── proto ├── author.proto └── rustica.proto ├── resources ├── create_certs.sh ├── package.sh └── systemd-config │ ├── README.md │ ├── environment.d │ └── rustica_ssh_socket.conf │ └── systemd │ └── rustica.service ├── rustica-agent-cli ├── Cargo.toml ├── README.md └── src │ ├── config │ ├── allowed_signers.rs │ ├── fidosetup.rs │ ├── gitconfig.rs │ ├── immediatemode.rs │ ├── listpivkeys.rs │ ├── mod.rs │ ├── multimode.rs │ ├── provisionpiv.rs │ ├── refresh_attested_x509_certificate.rs │ ├── register.rs │ └── singlemode.rs │ └── main.rs ├── rustica-agent-gui ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── rustica-agent ├── Cargo.toml ├── build.rs └── src │ ├── config │ └── mod.rs │ ├── ffi │ ├── agent.rs │ ├── allowed_signer.rs │ ├── enrollment.rs │ ├── mod.rs │ ├── mtls.rs │ ├── signing.rs │ ├── utils.rs │ └── yubikey_utils.rs │ ├── lib.rs │ ├── rustica │ ├── allowed_signer.rs │ ├── cert.rs │ ├── error.rs │ ├── key.rs │ ├── mod.rs │ └── x509.rs │ └── sshagent │ ├── agent.rs │ ├── error.rs │ ├── handler.rs │ ├── mod.rs │ └── protocol.rs ├── rustica ├── .env ├── Cargo.toml ├── README.md ├── bash │ ├── verify-hostname.sh │ └── verify.sh ├── build.rs ├── diesel.toml ├── migrations │ ├── .gitkeep │ ├── 2021-01-14-051956_hosts │ │ ├── down.sql │ │ └── up.sql │ ├── 2021-03-18-175456_registered-keys │ │ ├── down.sql │ │ └── up.sql │ ├── 2022-02-23-205236_add_u2f │ │ ├── down.sql │ │ └── up.sql │ ├── 2022-07-17-022706_add_authority │ │ ├── down.sql │ │ └── up.sql │ └── 2023-04-10-043746_add_x509_by_serial │ │ ├── down.sql │ │ └── up.sql └── src │ ├── auth │ ├── database.rs │ ├── database │ │ ├── models.rs │ │ └── schema.rs │ ├── external.rs │ └── mod.rs │ ├── config │ └── mod.rs │ ├── error.rs │ ├── key.rs │ ├── logging │ ├── influx.rs │ ├── mod.rs │ ├── splunk.rs │ ├── stdout.rs │ └── webhook.rs │ ├── main.rs │ ├── server.rs │ ├── signing │ ├── amazon_kms.rs │ ├── file.rs │ ├── mod.rs │ └── yubikey.rs │ └── verification.rs └── tests ├── integration.sh ├── ssh_server ├── Dockerfile ├── authorized_keys ├── sshd_config └── user-ca.pub ├── test_configs ├── rustica_local_file.toml ├── rustica_local_file_alt.toml ├── rustica_local_file_multi.toml ├── rustica_local_file_with_influx.toml ├── rustica_local_file_with_splunk.toml └── rustica_local_file_with_webhook.toml ├── test_ec256 ├── test_ec384 ├── test_ed25519 └── validate_file_configs.sh /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/universal:2", 3 | "features": { 4 | "ghcr.io/devcontainers/features/rust:1": { 5 | "version": "latest", 6 | "profile": "default" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/workflows/config_check.yml: -------------------------------------------------------------------------------- 1 | name: Check Configuration Examples 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | pull_request: 7 | branches: [ master, develop ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | ubuntu-integration-test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Install Protoc 18 | uses: arduino/setup-protoc@v3 19 | - uses: actions/checkout@v2 20 | - name: Install libpcsc 21 | run: sudo apt install -y libpcsclite-dev libusb-1.0-0-dev libudev-dev 22 | - name: Run Configuration File Checks 23 | run: ./tests/validate_file_configs.sh 24 | -------------------------------------------------------------------------------- /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | pull_request: 7 | branches: [ master, develop ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | ubuntu-integration-test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Install Protoc 18 | uses: arduino/setup-protoc@v3 19 | - uses: actions/checkout@v2 20 | - name: Install libpcsc 21 | run: sudo apt install -y libpcsclite-dev libusb-1.0-0-dev libudev-dev 22 | - name: Run Integration Tests 23 | run: ./tests/integration.sh 24 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Feature Compilation Check 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | pull_request: 7 | branches: [ master, develop ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | macos-build-with-all: 14 | runs-on: macos-latest 15 | 16 | steps: 17 | - name: Install Protoc 18 | uses: arduino/setup-protoc@v3 19 | - uses: actions/checkout@v2 20 | - name: Build 21 | run: cargo build --package=rustica --features=all 22 | 23 | ubuntu-build-with-splunk: 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - name: Install Protoc 28 | uses: arduino/setup-protoc@v3 29 | - uses: actions/checkout@v2 30 | - name: Build 31 | run: cargo build --package=rustica --features=splunk 32 | 33 | ubuntu-build-with-influx: 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - name: Install Protoc 38 | uses: arduino/setup-protoc@v3 39 | - uses: actions/checkout@v2 40 | - name: Build 41 | run: cargo build --package=rustica --features=influx 42 | 43 | ubuntu-build-with-local-db: 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - name: Install Protoc 48 | uses: arduino/setup-protoc@v3 49 | - uses: actions/checkout@v2 50 | - name: Build 51 | run: cargo build --package=rustica --features=local-db 52 | 53 | ubuntu-build-with-amazon-kms: 54 | runs-on: ubuntu-latest 55 | 56 | steps: 57 | - name: Install Protoc 58 | uses: arduino/setup-protoc@v3 59 | - uses: actions/checkout@v2 60 | - name: Build 61 | run: cargo build --package=rustica --features=amazon-kms 62 | 63 | ubuntu-build-with-all-except-yubikey: 64 | runs-on: ubuntu-latest 65 | 66 | steps: 67 | - name: Install Protoc 68 | uses: arduino/setup-protoc@v3 69 | - uses: actions/checkout@v2 70 | - name: Build 71 | run: cargo build --package=rustica --features="splunk,influx,local-db,amazon-kms" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | /target 3 | /private 4 | .DS_Store 5 | /rustica/rustica.db 6 | *.pem 7 | *.key 8 | *.env 9 | /resources/**/* 10 | !/resources/create_certs.sh 11 | !/resources/package.sh 12 | !/resources/systemd-config/ 13 | !/resources/systemd-config/**/* 14 | /kubernetes 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = ["rustica", "rustica-agent", "rustica-agent-cli", "rustica-agent-gui"] 5 | 6 | [profile.release] 7 | strip = "debuginfo" 8 | lto = true 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mitchell Grenier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | debug ?= 2 | 3 | $(info debug is $(debug)) 4 | 5 | ifdef debug 6 | release := 7 | target :=debug 8 | extension :=debug 9 | else 10 | release :=--release 11 | target :=release 12 | extension := 13 | endif 14 | 15 | all: 16 | cargo build $(release) --features=all 17 | 18 | build: all 19 | 20 | cli: 21 | cargo build $(release) --features=all --bin=rustica-agent-cli 22 | 23 | gui: 24 | cargo build $(release) --features=all --bin=rustica-agent-gui 25 | 26 | server: 27 | cargo build $(release) --features=all --bin=rustica 28 | 29 | server-no-yk: 30 | cargo build $(release) --features="amazon-kms,influx,splunk,local-db,webhook" --bin=rustica 31 | 32 | 33 | help: 34 | @echo "usage: make [debug=1]" 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rustica 2 | 3 | Rustica is a Yubikey backed SSHCA written in Rust. It is designed to be used with the accompanying `rustica-agent` tool for certificate handling but speaks gRPC so other integrations are possible. 4 | 5 | ## Features 6 | - Multiple Ways To Secure Server Private Keys 7 | - File 8 | - Yubikey 4/5 (non HSM) 9 | - AmazonKMS 10 | - Multiple Ways To Secure Client Private Keys 11 | - File 12 | - Yubikey PIV 13 | - FIDO 14 | - Multiple Ways To Store Permissions 15 | - Built in SQLite Database Support 16 | - External Authorization Server 17 | - Multiple Supported Logging Systems 18 | - Stdout 19 | - InfluxDB 20 | - Splunk 21 | - JSON Webhook 22 | - Just In Time Certificate Generation 23 | - Use Different Keys For User and Hosts 24 | - gRPC With mTLS 25 | - Docker Scratch Container Support (if used without server-side Yubikey signing) 26 | - Extensive Feature Support 27 | - Serve Multiple CAs of Different Kinds Simultaneously (e.g Yubikey + AmazonKMS) 28 | 29 | ### Protected Key Material 30 | Malicious access to the Rustica private key would result in serious compromise and thus Rustica provides two ways to mitigate this risk with Yubikey and AmazonKMS support. These signing modules use keys that cannot be exported resulting in more control over how the private key is being used. If using AmazonKMS, Amazon logs can be compared with Rustica logs to provide assurance no misuse has occured. 31 | 32 | ### Just-In-Time Certificate Generation 33 | Rustica and RusticaAgent work together to use short lived certificates that are generated on the fly only when needed. In effect this means your deployment will never need to deal with revocation because after ten seconds (the default) all issued certificates will have expired. 34 | 35 | ### Multiple Supported Logging Systems 36 | All certificate issues can be logged to InfluxDB or Splunk if desired. See the logging submodule and the examples in `examples/` for more information. 37 | 38 | ### gRPC With mTLS 39 | Rustica requires all connections be made using mutually authenticated TLS. This provides an extra level of authentication to the service and allows the tying of x509 certificates to SSH logins. 40 | 41 | ### Docker Scratch Container 42 | When using either AmazonKMS or file based keys, Rustica can be compiled to a statically linked binary capable of running in a docker container with no external dependencies. The `docker/` folder contains `Dockerfile`s to compile Rustica this way for both amd64 (standard x86_64 architectures) and aarch64 (capable of running on Amazon Graviton servers). 43 | 44 | ### Extensive Feature Support 45 | Compile in only what you need to reduce binary size and dependency bloat. If you're planning on using AmazonKMS for storing your keys, Rustica can be compiled without Yubikey dependencies and vice versa. The same is also true for authorization, if using a remote authorization service, Rustica can be compiled without Diesel and SQLite. 46 | 47 | ## Key Support 48 | The following key types have client support via FIDO: 49 | - ECDSA 256 (untested) 50 | - Ed25519 51 | 52 | The following key types have Yubikey support (client and server): 53 | - ECDSA 256 54 | - ECDSA 384 55 | 56 | The following key types have file support (client and server): 57 | - ECDSA 256 58 | - ECDSA 384 59 | - Ed25519 60 | 61 | The following key types have no support: 62 | - ECDSA 521 63 | 64 | ## Running An Example Deployment 65 | This repository comes with a set of configuration files and database to be used as an example. New certificates can be easily generated using the scripts in `resources/`. The integration tests also contain information about spinning up a Rustica deployment. 66 | 67 | ### Start Rustica 68 | `rustica --config examples/rustica_local_file.toml` 69 | 70 | ### Pull a certificate with RusticaAgent 71 | `rustica-agent --config examples/rustica_agent_local.toml -i` 72 | 73 | The details of the certificate will be printed to the screen. 74 | 75 | ## Running Tests 76 | Rustica ships with a small suite of integration tests aimed at ensuring some of the lesser known features do not get broken with updates. They require docker to be installed and can be run with the script in `tests/integration.sh` 77 | 78 | ## Security Warning 79 | No review has been done. I built it because I thought people could find it useful. Be wary about using this in production without doing a thorough code review. If you find mistakes, please open a pull request or if it's a security bug, email me. 80 | 81 | 82 | ## Licence 83 | This software is provided under the MIT licence so you may use it basically however you wish so long as all distributions and derivatives (source and binary) include the copyright from the `LICENSE`. 84 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a Vulnerability 2 | 3 | If you have found a security vulnerability within this project, please use the [GitHub vulnerability reporting tool](https://github.com/obelisk/rustica/security/advisories) to submit it. 4 | 5 | Please do not open an issue or pull request. 6 | -------------------------------------------------------------------------------- /docker/Dockerfile.aarch64: -------------------------------------------------------------------------------- 1 | FROM messense/rust-musl-cross:aarch64-musl as builder 2 | 3 | RUN apt-get update && apt install -y protobuf-compiler 4 | 5 | RUN rustup component add rustfmt 6 | RUN mkdir /rustica 7 | COPY proto /tmp/proto 8 | COPY rustica /tmp/rustica 9 | WORKDIR /tmp/rustica 10 | 11 | RUN cargo build --features="amazon-kms,splunk,webhook" --release 12 | 13 | FROM alpine:3.6 as alpine 14 | RUN apk add -U --no-cache ca-certificates 15 | 16 | from scratch as runtime 17 | COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 18 | COPY --from=builder /tmp/rustica/target/aarch64-unknown-linux-musl/release/rustica /rustica 19 | USER 1000 20 | ENTRYPOINT [ "/rustica" ] 21 | -------------------------------------------------------------------------------- /docker/Dockerfile.amd64: -------------------------------------------------------------------------------- 1 | FROM messense/rust-musl-cross:x86_64-musl as builder 2 | 3 | ENV TARGET x86_64-unknown-linux-musl 4 | RUN rustup target add "$TARGET" 5 | 6 | RUN apt-get update && apt-get install -y musl-tools 7 | RUN rustup component add rustfmt 8 | RUN mkdir /rustica 9 | COPY proto /tmp/proto 10 | COPY rustica /tmp/rustica 11 | WORKDIR /tmp/rustica 12 | 13 | RUN cargo build --target="$TARGET" --features="splunk,amazon-kms,webhook" --release 14 | 15 | FROM alpine:3.6 as alpine 16 | RUN apk add -U --no-cache ca-certificates 17 | 18 | from scratch as runtime 19 | COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 20 | COPY --from=builder /tmp/rustica/target/x86_64-unknown-linux-musl/release/rustica /rustica 21 | USER 1000 22 | ENTRYPOINT [ "/rustica" ] 23 | -------------------------------------------------------------------------------- /docker/Dockerfile_homelab: -------------------------------------------------------------------------------- 1 | FROM rust:1.65 as builder 2 | 3 | RUN apt update 4 | RUN apt install -y pkg-config libpcsclite-dev 5 | #RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- -y 6 | 7 | RUN rustup component add rustfmt 8 | RUN mkdir /rustica 9 | COPY proto /tmp/proto 10 | COPY rustica /tmp/rustica 11 | WORKDIR /tmp/rustica 12 | 13 | RUN cargo build --features="yubikey-support,webhook,amazon-kms,influx,local-db" --release 14 | 15 | 16 | FROM ubuntu:20.04 17 | RUN apt update 18 | RUN apt install -y libpcsclite-dev 19 | 20 | COPY --from=builder /tmp/rustica/target/release/rustica /rustica 21 | USER 1000 22 | ENTRYPOINT [ "/rustica" ] 23 | 24 | -------------------------------------------------------------------------------- /docker/Dockerfile_influx.aarch64: -------------------------------------------------------------------------------- 1 | FROM messense/rust-musl-cross:aarch64-musl as builder 2 | 3 | RUN rustup component add rustfmt 4 | RUN mkdir /rustica 5 | COPY proto /tmp/proto 6 | COPY rustica /tmp/rustica 7 | WORKDIR /tmp/rustica 8 | 9 | RUN cargo build --features="influx" --release 10 | 11 | FROM alpine:3.6 as alpine 12 | RUN apk add -U --no-cache ca-certificates 13 | 14 | from scratch as runtime 15 | COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 16 | COPY --from=builder /tmp/rustica/target/aarch64-unknown-linux-musl/release/rustica /rustica 17 | USER 1000 18 | ENTRYPOINT [ "/rustica" ] 19 | -------------------------------------------------------------------------------- /docker/Dockerfile_influx.amd64: -------------------------------------------------------------------------------- 1 | FROM ekidd/rust-musl-builder:1.57.0 as builder 2 | USER root 3 | RUN apt update && apt upgrade -y && apt install -y git 4 | RUN rustup component add rustfmt 5 | RUN mkdir /rustica 6 | COPY proto /tmp/proto 7 | COPY rustica /tmp/rustica 8 | WORKDIR /tmp/rustica 9 | 10 | RUN cargo build --target=x86_64-unknown-linux-musl --features="influx" --release 11 | 12 | FROM alpine:3.6 as alpine 13 | RUN apk add -U --no-cache ca-certificates 14 | 15 | from scratch as runtime 16 | COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 17 | COPY --from=builder /tmp/rustica/target/x86_64-unknown-linux-musl/release/rustica /rustica 18 | USER 1000 19 | ENTRYPOINT [ "/rustica" ] 20 | -------------------------------------------------------------------------------- /examples/example.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obelisk/rustica/ea8a5df6ed9884b2e2f603f3e6c64846783d8ec5/examples/example.db -------------------------------------------------------------------------------- /examples/rustica_agent_local.toml: -------------------------------------------------------------------------------- 1 | version = 2 2 | key = """ 3 | -----BEGIN OPENSSH PRIVATE KEY----- 4 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 5 | QyNTUxOQAAACDunlbgsYd3ghDSnC1jbZoXkt0u7AYId/lpHemDS94M5AAAAKBcmh6OXJoe 6 | jgAAAAtzc2gtZWQyNTUxOQAAACDunlbgsYd3ghDSnC1jbZoXkt0u7AYId/lpHemDS94M5A 7 | AAAEAadOY5EF0mGYUOXxRvfpBJOzBu8IXLVS+UJ95dFDfAuu6eVuCxh3eCENKcLWNtmheS 8 | 3S7sBgh3+Wkd6YNL3gzkAAAAGW9iZWxpc2tAbWl0Y2hlbGxzLW1icC5sYW4BAgME 9 | -----END OPENSSH PRIVATE KEY----- 10 | """ 11 | socket = "/tmp/rustica_socket_dev3" 12 | 13 | [[servers]] 14 | address = "https://localhost:50052" 15 | ca_pem = """ 16 | -----BEGIN CERTIFICATE----- 17 | MIIBizCCATGgAwIBAgIUSxuBkb/vlm8p9VQRVlGeC7KWjQswCgYIKoZIzj0EAwIw 18 | GzEZMBcGA1UEAwwQRW50ZXJwcmlzZVJvb3RDQTAeFw0yNDA2MDYwNDA4MThaFw0z 19 | NDA2MDQwNDA4MThaMBsxGTAXBgNVBAMMEEVudGVycHJpc2VSb290Q0EwWTATBgcq 20 | hkjOPQIBBggqhkjOPQMBBwNCAAQhZaDE0x7484SonAR5Ogufm7Qg5CttNfOr6Lk4 21 | uqmJaUpxNsFJOclZqGUHgIPMfK7qQIU0K8EDD+mxJBIYY2V5o1MwUTAdBgNVHQ4E 22 | FgQUOhiiXYkz9/H/i5F87/PRfqg/6E4wHwYDVR0jBBgwFoAUOhiiXYkz9/H/i5F8 23 | 7/PRfqg/6E4wDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiBm3CqE 24 | bvVHnNt5gd4NDUkJ3ALrp3pzm24UFJU/sRssEgIhAKsSFHVza/vSJ/6KWqvZoogB 25 | DF1DluRk6qknMiXlDjpI 26 | -----END CERTIFICATE----- 27 | """ 28 | mtls_cert = """ 29 | -----BEGIN CERTIFICATE----- 30 | MIIBQjCB6aADAgECAhUArfRIp1jEPZCLmVbQRzsQrCRcPGAwCgYIKoZIzj0EAwIw 31 | GDEWMBQGA1UEAwwNUnVzdGljYUFjY2VzczAeFw0yNDA2MDYwNDM2NTRaFw0zMDAz 32 | MDcwNDM2NTRaMBIxEDAOBgNVBAMMB29iZWxpc2swWTATBgcqhkjOPQIBBggqhkjO 33 | PQMBBwNCAAQvKOaSK5vGPjbxk/kjAIxbyRFsKb1DSub5L1DFsfg2OlsrNt4/g3Ra 34 | NCSkcA99y25LD5txN1vnAHZOqbACKZIooxYwFDASBgNVHREECzAJggdvYmVsaXNr 35 | MAoGCCqGSM49BAMCA0gAMEUCIAN0yMvU4Keidu14KLV+q4BWG6LR6nIhuiHphA/K 36 | DGfLAiEAnm9/rz+QrR9jLsvf90sWUkXdf3/Yv5KYSIPtH5XUnYM= 37 | -----END CERTIFICATE----- 38 | """ 39 | mtls_key = """ 40 | -----BEGIN PRIVATE KEY----- 41 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgSFyNGs01I6JPXwpn 42 | Ac1arqHBIvwAAI7tvwFlVp3yO7yhRANCAAQvKOaSK5vGPjbxk/kjAIxbyRFsKb1D 43 | Sub5L1DFsfg2OlsrNt4/g3RaNCSkcA99y25LD5txN1vnAHZOqbACKZIo 44 | -----END PRIVATE KEY----- 45 | """ 46 | 47 | [options] 48 | principals = ["testuser"] 49 | kind = "user" 50 | duration = 15 51 | authority = "example_test_environment" 52 | -------------------------------------------------------------------------------- /examples/rustica_local_amazonkms.toml: -------------------------------------------------------------------------------- 1 | # This is the listen address that will be used for the Rustica service 2 | listen_address = "0.0.0.0:50052" 3 | 4 | # This setting controls if the agent has to prove that it 5 | # controls the private key to Rustica. Setting this to true means a user needs 6 | # to generate two signatures (one to Rustica, and one to the host). The 7 | # advantage of using this, is a compromised host cannot get certificates 8 | # from the server without physical interaction. 9 | # 10 | # A client will always need to sign the challenge from the host they 11 | # are attempting to connect to however so a physical tap will always 12 | # be required. 13 | require_rustica_proof = false 14 | 15 | # This setting controls if Rustica will allow users to register keys that 16 | # cannot have their providence validated. If set to true, if a registration 17 | # either does not provide an attestation or the attestation fails, the key 18 | # will be rejected and the API call will fail. In the case of using an 19 | # external authorizer, a call will not be made to inform it of this event. 20 | require_attestation_chain = true 21 | 22 | 23 | # The certificate presented to connecting clients 24 | server_cert = ''' 25 | -----BEGIN CERTIFICATE----- 26 | MIIBqjCCAVCgAwIBAgIJAOI2FtcQeixVMAoGCCqGSM49BAMCMBsxGTAXBgNVBAMM 27 | EEVudGVycHJpc2VSb290Q0EwHhcNMjIwMTIwMDQwMjA2WhcNMjQwNDI0MDQwMjA2 28 | WjAxMRAwDgYDVQQDDAdydXN0aWNhMRAwDgYDVQQKDAdSdXN0aWNhMQswCQYDVQQG 29 | EwJDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAINhoFW/5twPqAHLxjFjmns 30 | lE1jJMJQXmijymZTJxR0DsNZlwvUgNH+WYQFfq4IVMwypVHgyTYJO+lAAPEeyPOj 31 | ZzBlMDUGA1UdIwQuMCyhH6QdMBsxGTAXBgNVBAMMEEVudGVycHJpc2VSb290Q0GC 32 | CQCRg096sVtP0zAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAUBgNVHREEDTALggls 33 | b2NhbGhvc3QwCgYIKoZIzj0EAwIDSAAwRQIhAMfjW/PMrA9/cCg6O835sr22ZrNk 34 | k/lFOODLqAJPbh3+AiAzeCUyrmxT5VTf6uyFoNT8zMoWSi79rudcdgl+32RqMg== 35 | -----END CERTIFICATE----- 36 | ''' 37 | 38 | # The key for the certificate presented to clients 39 | server_key = ''' 40 | -----BEGIN PRIVATE KEY----- 41 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkTd0C69xWFX9PmVf 42 | BeD0ySfG+O0e7p7SXR9xo/enbvahRANCAAQCDYaBVv+bcD6gBy8YxY5p7JRNYyTC 43 | UF5oo8pmUycUdA7DWZcL1IDR/lmEBX6uCFTMMqVR4Mk2CTvpQADxHsjz 44 | -----END PRIVATE KEY----- 45 | ''' 46 | 47 | # Configuration for authenticating connecting clients as well as defining 48 | # automatic renewal settings. 49 | [client_authority] 50 | # This must be one of the signing authorities defined in the signing structure 51 | authority = "amazonkms" 52 | # When we issue a new access certificate, how long is it valid for. 53 | validity_length = 181440000 # Three weeks * 100 54 | 55 | # I think the easiest way to explain this is with an example. 56 | # 57 | # If a certificate is issued for three months, setting this to one week means 58 | # in the week before it expires, when they request a new SSH certificate, the 59 | # server will generate a new mTLS access certificate and send that back with 60 | # the request. 61 | # 62 | # This value should always be less than the validity length 63 | expiration_renewal_period = 181439980 #60480000 # One Week * 100 64 | 65 | 66 | [signing] 67 | default_authority = "amazonkms" 68 | 69 | # Rustica has many ways it can sign SSH certificates which are sent to 70 | # clients. This method uses private keys embedded in the configuration 71 | # file. This will mean the hosts which you want to login to via Rustica 72 | # must respect the public portion of the user key variable below. 73 | [signing.authority_configurations."amazonkms"] 74 | kind = "AmazonKMS" 75 | aws_access_key_id = "XXXXXXXXXXXXXXXXXXXX" 76 | aws_secret_access_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 77 | aws_region = "us-west-2" 78 | 79 | [signing.authority_configurations."amazonkms".user_key] 80 | id = "mrk-00000000000000000000000000000000" 81 | algorithm = "ECDSA_SHA_384" 82 | 83 | [signing.authority_configurations."amazonkms".host_key] 84 | id = "mrk-00000000000000000000000000000000" 85 | algorithm = "ECDSA_SHA_384" 86 | 87 | [signing.authority_configurations."amazonkms".x509_key] 88 | id = "mrk-00000000000000000000000000000000" 89 | algorithm = "ECDSA_SHA_384" 90 | 91 | [signing.authority_configurations."amazonkms".client_certificate_authority_key] 92 | id = "mrk-00000000000000000000000000000000" 93 | algorithm = "ECDSA_SHA_384" 94 | common_name = "RusticaAccess" 95 | 96 | 97 | [logging."stdout"] 98 | 99 | [authorization."database"] 100 | path = "examples/example.db" 101 | 102 | [allowed_signers] 103 | cache_validity_length.secs = 900 104 | cache_validity_length.nanos = 0 105 | lru_rate_limiter_size = 16 106 | rate_limit_cooldown.secs = 15 107 | rate_limit_cooldown.nanos = 0 108 | -------------------------------------------------------------------------------- /examples/rustica_local_multi.toml: -------------------------------------------------------------------------------- 1 | # This is the listen address that will be used for the Rustica service 2 | listen_address = "0.0.0.0:50052" 3 | 4 | # This setting controls if the agent has to prove that it 5 | # controls the private key to Rustica. Setting this to true means a user needs 6 | # to generate two signatures (one to Rustica, and one to the host). The 7 | # advantage of using this, is a compromised host cannot get certificates 8 | # from the server without physical interaction. 9 | # 10 | # A client will always need to sign the challenge from the host they 11 | # are attempting to connect to however so a physical tap will always 12 | # be required. 13 | require_rustica_proof = false 14 | 15 | # This setting controls if Rustica will allow users to register keys that 16 | # cannot have their providence validated. If set to true, if a registration 17 | # either does not provide an attestation or the attestation fails, the key 18 | # will be rejected and the API call will fail. In the case of using an 19 | # external authorizer, a call will not be made to inform it of this event. 20 | require_attestation_chain = true 21 | 22 | 23 | # The certificate presented to connecting clients 24 | server_cert = ''' 25 | -----BEGIN CERTIFICATE----- 26 | MIIBqjCCAVCgAwIBAgIJAOI2FtcQeixVMAoGCCqGSM49BAMCMBsxGTAXBgNVBAMM 27 | EEVudGVycHJpc2VSb290Q0EwHhcNMjIwMTIwMDQwMjA2WhcNMjQwNDI0MDQwMjA2 28 | WjAxMRAwDgYDVQQDDAdydXN0aWNhMRAwDgYDVQQKDAdSdXN0aWNhMQswCQYDVQQG 29 | EwJDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAINhoFW/5twPqAHLxjFjmns 30 | lE1jJMJQXmijymZTJxR0DsNZlwvUgNH+WYQFfq4IVMwypVHgyTYJO+lAAPEeyPOj 31 | ZzBlMDUGA1UdIwQuMCyhH6QdMBsxGTAXBgNVBAMMEEVudGVycHJpc2VSb290Q0GC 32 | CQCRg096sVtP0zAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAUBgNVHREEDTALggls 33 | b2NhbGhvc3QwCgYIKoZIzj0EAwIDSAAwRQIhAMfjW/PMrA9/cCg6O835sr22ZrNk 34 | k/lFOODLqAJPbh3+AiAzeCUyrmxT5VTf6uyFoNT8zMoWSi79rudcdgl+32RqMg== 35 | -----END CERTIFICATE----- 36 | ''' 37 | 38 | # The key for the certificate presented to clients 39 | server_key = ''' 40 | -----BEGIN PRIVATE KEY----- 41 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkTd0C69xWFX9PmVf 42 | BeD0ySfG+O0e7p7SXR9xo/enbvahRANCAAQCDYaBVv+bcD6gBy8YxY5p7JRNYyTC 43 | UF5oo8pmUycUdA7DWZcL1IDR/lmEBX6uCFTMMqVR4Mk2CTvpQADxHsjz 44 | -----END PRIVATE KEY----- 45 | ''' 46 | 47 | # Configuration for authenticating connecting clients as well as defining 48 | # automatic renewal settings. 49 | [client_authority] 50 | # This must be one of the signing authorities defined in the signing structure 51 | authority = "example_corp_hardware_mtls" 52 | # When we issue a new access certificate, how long is it valid for. 53 | validity_length = 181440000 # Three weeks * 100 54 | 55 | # I think the easiest way to explain this is with an example. 56 | # 57 | # If a certificate is issued for three months, setting this to one week means 58 | # in the week before it expires, when they request a new SSH certificate, the 59 | # server will generate a new mTLS access certificate and send that back with 60 | # the request. 61 | # 62 | # This value should always be less than the validity length 63 | expiration_renewal_period = 181439980 #60480000 # One Week * 100 64 | 65 | [logging."stdout"] 66 | 67 | [signing] 68 | default_authority = "example_test_environment" 69 | 70 | [signing.authority_configurations.example_test_environment] 71 | kind = "File" 72 | user_key = ''' 73 | -----BEGIN OPENSSH PRIVATE KEY----- 74 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 75 | QyNTUxOQAAACDhq7U0gcW66mJNWpsL/4ADNXoOUMLHXYbM8PIHbqPT+gAAAKhxqRWZcakV 76 | mQAAAAtzc2gtZWQyNTUxOQAAACDhq7U0gcW66mJNWpsL/4ADNXoOUMLHXYbM8PIHbqPT+g 77 | AAAEA8yRG/XWtjlY007gj8tNflVX9fnHPDcQYH2HTImTKvPeGrtTSBxbrqYk1amwv/gAM1 78 | eg5Qwsddhszw8gduo9P6AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg 79 | ECAwQ= 80 | -----END OPENSSH PRIVATE KEY----- 81 | ''' 82 | 83 | host_key = ''' 84 | -----BEGIN OPENSSH PRIVATE KEY----- 85 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 86 | QyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnewAAAKhQSP5+UEj+ 87 | fgAAAAtzc2gtZWQyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnew 88 | AAAEAevZOed5UnsVdAASUn+sJ+dUfUnG1kQ1wRH9L758mSCxcC2QuSbJKphPaKx0fRxE52 89 | sDNPZwaD2kv14ulnBCd7AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg 90 | ECAwQ= 91 | -----END OPENSSH PRIVATE KEY----- 92 | ''' 93 | 94 | [signing.authority_configurations."example_corp_hardware_mtls"] 95 | kind = "Yubikey" 96 | user_slot = "R2" 97 | host_slot = "R3" 98 | x509_slot = "R4" 99 | client_certificate_authority_slot = "R5" 100 | client_certificate_authority_common_name = "RusticaAccess" 101 | 102 | [signing.authority_configurations."example_prod_ssh_environment"] 103 | kind = "AmazonKMS" 104 | aws_access_key_id = "XXXXXXXXXXXXXXXXXXXX" 105 | aws_secret_access_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 106 | aws_region = "us-west-2" 107 | 108 | [signing.authority_configurations."amazonkms".user_key] 109 | id = "mrk-00000000000000000000000000000000" 110 | algorithm = "ECDSA_SHA_384" 111 | 112 | [signing.authority_configurations."amazonkms".host_key] 113 | id = "mrk-00000000000000000000000000000000" 114 | algorithm = "ECDSA_SHA_384" 115 | 116 | 117 | [authorization."database"] 118 | path = "examples/example.db" 119 | 120 | [allowed_signers] 121 | cache_validity_length.secs = 900 122 | cache_validity_length.nanos = 0 123 | lru_rate_limiter_size = 16 124 | rate_limit_cooldown.secs = 15 125 | rate_limit_cooldown.nanos = 0 126 | -------------------------------------------------------------------------------- /examples/rustica_local_yubikey.toml: -------------------------------------------------------------------------------- 1 | # This is the listen address that will be used for the Rustica service 2 | listen_address = "0.0.0.0:50052" 3 | 4 | # This setting controls if the agent has to prove that it 5 | # controls the private key to Rustica. Setting this to true means a user needs 6 | # to generate two signatures (one to Rustica, and one to the host). The 7 | # advantage of using this, is a compromised host cannot get certificates 8 | # from the server without physical interaction. 9 | # 10 | # A client will always need to sign the challenge from the host they 11 | # are attempting to connect to however so a physical tap will always 12 | # be required. 13 | require_rustica_proof = false 14 | 15 | # This setting controls if Rustica will allow users to register keys that 16 | # cannot have their providence validated. If set to true, if a registration 17 | # either does not provide an attestation or the attestation fails, the key 18 | # will be rejected and the API call will fail. In the case of using an 19 | # external authorizer, a call will not be made to inform it of this event. 20 | require_attestation_chain = true 21 | 22 | 23 | # The certificate presented to connecting clients 24 | server_cert = ''' 25 | -----BEGIN CERTIFICATE----- 26 | MIIBqjCCAVCgAwIBAgIJAOI2FtcQeixVMAoGCCqGSM49BAMCMBsxGTAXBgNVBAMM 27 | EEVudGVycHJpc2VSb290Q0EwHhcNMjIwMTIwMDQwMjA2WhcNMjQwNDI0MDQwMjA2 28 | WjAxMRAwDgYDVQQDDAdydXN0aWNhMRAwDgYDVQQKDAdSdXN0aWNhMQswCQYDVQQG 29 | EwJDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAINhoFW/5twPqAHLxjFjmns 30 | lE1jJMJQXmijymZTJxR0DsNZlwvUgNH+WYQFfq4IVMwypVHgyTYJO+lAAPEeyPOj 31 | ZzBlMDUGA1UdIwQuMCyhH6QdMBsxGTAXBgNVBAMMEEVudGVycHJpc2VSb290Q0GC 32 | CQCRg096sVtP0zAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAUBgNVHREEDTALggls 33 | b2NhbGhvc3QwCgYIKoZIzj0EAwIDSAAwRQIhAMfjW/PMrA9/cCg6O835sr22ZrNk 34 | k/lFOODLqAJPbh3+AiAzeCUyrmxT5VTf6uyFoNT8zMoWSi79rudcdgl+32RqMg== 35 | -----END CERTIFICATE----- 36 | ''' 37 | 38 | # The key for the certificate presented to clients 39 | server_key = ''' 40 | -----BEGIN PRIVATE KEY----- 41 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkTd0C69xWFX9PmVf 42 | BeD0ySfG+O0e7p7SXR9xo/enbvahRANCAAQCDYaBVv+bcD6gBy8YxY5p7JRNYyTC 43 | UF5oo8pmUycUdA7DWZcL1IDR/lmEBX6uCFTMMqVR4Mk2CTvpQADxHsjz 44 | -----END PRIVATE KEY----- 45 | ''' 46 | 47 | # Configuration for authenticating connecting clients as well as defining 48 | # automatic renewal settings. 49 | [client_authority] 50 | # This must be one of the signing authorities defined in the signing structure 51 | authority = "example_prod_environment" 52 | # When we issue a new access certificate, how long is it valid for. 53 | validity_length = 181440000 # Three weeks * 100 54 | 55 | # I think the easiest way to explain this is with an example. 56 | # 57 | # If a certificate is issued for three months, setting this to one week means 58 | # in the week before it expires, when they request a new SSH certificate, the 59 | # server will generate a new mTLS access certificate and send that back with 60 | # the request. 61 | # 62 | # This value should always be less than the validity length 63 | expiration_renewal_period = 181439980 #60480000 # One Week * 100 64 | 65 | [signing] 66 | default_authority = "example_prod_environment" 67 | 68 | [signing.authority_configurations."example_prod_environment"] 69 | kind = "Yubikey" 70 | user_slot = "R1" 71 | host_slot = "R2" 72 | #x509_slot = "0x9a" 73 | client_certificate_authority_slot = "R1" 74 | client_certificate_authority_common_name = "RusticaAccess" 75 | 76 | [logging."stdout"] 77 | 78 | [authorization."database"] 79 | path = "examples/example.db" 80 | 81 | [allowed_signers] 82 | cache_validity_length.secs = 900 83 | cache_validity_length.nanos = 0 84 | lru_rate_limiter_size = 16 85 | rate_limit_cooldown.secs = 15 86 | rate_limit_cooldown.nanos = 0 87 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 0, 6 | "narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=", 7 | "path": "/nix/store/zq2axpgzd5kykk1v446rkffj3bxa2m2h-source", 8 | "type": "path" 9 | }, 10 | "original": { 11 | "id": "nixpkgs", 12 | "type": "indirect" 13 | } 14 | }, 15 | "root": { 16 | "inputs": { 17 | "nixpkgs": "nixpkgs" 18 | } 19 | } 20 | }, 21 | "root": "root", 22 | "version": 7 23 | } 24 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "flake for rustica-agent"; 3 | 4 | outputs = { 5 | self, 6 | nixpkgs, 7 | }: let 8 | systems = ["x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"]; 9 | inherit (nixpkgs) lib; 10 | forAllSystems = f: lib.genAttrs systems (system: f system); 11 | in { 12 | packages = forAllSystems (system: let 13 | pkgs = import nixpkgs {inherit system;}; 14 | in rec { 15 | rustica = pkgs.rustPlatform.buildRustPackage { 16 | pname = "rustica"; 17 | src = self; 18 | version = "${lib.substring 0 8 self.lastModifiedDate}_${self.shortRev or self.dirtyShortRev}"; 19 | 20 | nativeBuildInputs = [pkgs.pkg-config]; 21 | buildInputs = with pkgs; [rustc cargo openssl udev pcsclite protobuf]; 22 | cargoLock = {lockFile = self + "/Cargo.lock";}; 23 | 24 | OPENSSL_NO_VENDOR = 1; 25 | CARGO_FEATURE_USE_SYSTEM_LIBS = 1; 26 | PROTOC = "${pkgs.protobuf}/bin/protoc"; 27 | }; 28 | default = rustica; 29 | }); 30 | defaultPackage = forAllSystems (system: self.packages.${system}.default); 31 | 32 | apps = forAllSystems (system: rec { 33 | rustica = { 34 | type = "app"; 35 | program = "${self.packages.${system}.rustica}/bin/rustica"; 36 | }; 37 | rustica-agent-cli = { 38 | type = "app"; 39 | program = "${self.packages.${system}.rustica}/bin/rustica-agent-cli"; 40 | }; 41 | rustica-agent-gui = { 42 | type = "app"; 43 | program = "${self.packages.${system}.rustica}/bin/rustica-agent-gui"; 44 | }; 45 | default = rustica-agent-cli; 46 | }); 47 | defaultApp = forAllSystems (system: self.apps.${system}.default); 48 | 49 | overlay = final: prev: { 50 | inherit (self.packages.${final.system}) rustica; 51 | }; 52 | 53 | devShell = forAllSystems ( 54 | system: 55 | nixpkgs.legacyPackages.${system}.mkShell { 56 | inputsFrom = builtins.attrValues self.packages.${system}; 57 | buildInputs = [self.packages.${system}.rustica]; 58 | } 59 | ); 60 | 61 | hmModules = rec { 62 | rustica-agent = { 63 | pkgs, 64 | lib, 65 | config, 66 | ... 67 | }: let 68 | cfg = config.services.rustica-agent; 69 | in { 70 | options.services.rustica-agent = { 71 | enable = lib.mkEnableOption "Enable rustica-agent service"; 72 | package = lib.mkOption { 73 | type = lib.types.package; 74 | default = self.packages.${pkgs.system}.default; 75 | description = "The rustica package to use"; 76 | }; 77 | configDir = lib.mkOption { 78 | type = lib.types.path; 79 | default = "${config.home.homeDirectory}/.rusticaagent"; 80 | description = "The directory where rustica-agent configuration is stored"; 81 | }; 82 | environment = lib.mkOption { 83 | type = lib.types.str; 84 | description = "The name of the environment to use (under ${cfg.configDir}/environments)"; 85 | example = "prod"; 86 | }; 87 | socket = lib.mkOption { 88 | type = lib.types.path; 89 | default = "${cfg.configDir}/${cfg.environment}.sock"; 90 | }; 91 | extraOptions = lib.mkOption { 92 | type = lib.types.str; 93 | default = ""; 94 | description = "Extra options to pass to rustica-agent-cli"; 95 | example = "-s 2"; 96 | }; 97 | }; 98 | 99 | config = lib.mkIf cfg.enable { 100 | home.packages = [cfg.package]; 101 | home.sessionVariables = { 102 | SSH_AUTH_SOCK = cfg.socket; 103 | }; 104 | systemd.user.services.rustica-agent = { 105 | Unit.Description = "Rustica-Agent SSH service"; 106 | Install.WantedBy = ["default.target"]; 107 | Service = { 108 | ExecStartPre = "${pkgs.coreutils}/bin/rm -vf ${cfg.socket}"; 109 | ExecStart = 110 | "${cfg.package}/bin/rustica-agent-cli single --config ${cfg.configDir}/environments/${cfg.environment} --file ${cfg.configDir}/keys/${cfg.environment} --socket ${cfg.socket}" 111 | + (lib.optionalString (cfg.extraOptions != "") (" " + cfg.extraOptions)); 112 | Restart = "on-failure"; 113 | }; 114 | }; 115 | }; 116 | }; 117 | default = rustica-agent; 118 | }; 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /proto/author.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package author; 3 | 4 | // Identities are pieces of data that Author can use to determine 5 | // who is asking for authorization when it is happening through 6 | // a proxy service. For example, when someone requests access through 7 | // the editor `Rustica`, identities the following from the requesting client 8 | // are sent through the `AuthorizeRequest` call: 9 | // Source IP 10 | // mTLS Identity data 11 | // SSH key fingerprint 12 | 13 | // Important To Note 14 | // The `identities` is not checked for correctness (though it may have other 15 | // heurisitics applied to it). It should only contain data that has been 16 | // verified by the editor. 17 | 18 | message AuthorizeRequest { 19 | map identities = 1; 20 | map authorization_request = 2; 21 | } 22 | 23 | message AuthorizeResponse { 24 | map approval_response = 1; 25 | } 26 | 27 | // This call adds data into Author for use in further authorization requests. 28 | // For example, when a user adds a new key via the editor `Rustica`, it feeds 29 | // into this. 30 | message AddIdentityDataRequest { 31 | map identities = 1; 32 | map identity_data = 2; 33 | } 34 | 35 | message AddIdentityDataResponse {} 36 | 37 | message AllowedSigner { 38 | string identity = 1; 39 | string pubkey = 2; 40 | } 41 | 42 | // This call fetches a list of all allowed signers and their signing pubkeys 43 | message AllowedSignersRequest {} 44 | 45 | message AllowedSignersResponse { 46 | repeated AllowedSigner allowed_signers = 1; 47 | } 48 | 49 | service Author { 50 | rpc Authorize(AuthorizeRequest) returns (AuthorizeResponse); 51 | rpc AddIdentityData(AddIdentityDataRequest) returns (AddIdentityDataResponse); 52 | rpc GetAllowedSigners(AllowedSignersRequest) returns (AllowedSignersResponse); 53 | } 54 | -------------------------------------------------------------------------------- /proto/rustica.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package rustica; 3 | 4 | message ChallengeRequest { 5 | string pubkey = 1; 6 | } 7 | 8 | message ChallengeResponse { 9 | string time = 1; 10 | string challenge = 2; 11 | // We have to use the inverse of what we would normally because the default 12 | // value for a bool in Tonic is false. If we don't do this, it won't be 13 | // backwards compatible (a new client will see the lack of this field 14 | // as not requiring a signature). 15 | bool no_signature_required = 3; 16 | } 17 | 18 | message Challenge { 19 | string pubkey = 1; 20 | string challenge = 2; 21 | string challenge_signature = 3; 22 | string challenge_time = 4; 23 | } 24 | 25 | message CertificateRequest { 26 | uint32 cert_type = 1; 27 | string key_id = 2; 28 | repeated string principals = 3; 29 | repeated string servers = 4; 30 | map extensions = 5; 31 | map critical_options = 6; 32 | uint64 valid_after = 7; 33 | uint64 valid_before = 8; 34 | Challenge challenge = 9; 35 | } 36 | 37 | message CertificateResponse { 38 | string certificate = 1; 39 | string error = 2; 40 | int64 error_code = 3; 41 | string new_client_certificate = 4; 42 | string new_client_key = 5; 43 | } 44 | 45 | message RegisterKeyRequest { 46 | bytes certificate = 1; 47 | bytes intermediate = 2; 48 | Challenge challenge = 3; 49 | } 50 | 51 | message RegisterKeyResponse {} 52 | 53 | message RegisterU2FKeyRequest { 54 | bytes auth_data = 1; 55 | bytes auth_data_signature = 2; 56 | bytes sk_application = 3; 57 | bytes u2f_challenge = 4; 58 | bytes intermediate = 5; 59 | int32 alg = 6; 60 | Challenge challenge = 7; 61 | bool u2f_challenge_hashed = 8; 62 | } 63 | 64 | message RegisterU2FKeyResponse {} 65 | 66 | message AttestedX509CertificateRequest { 67 | // Which signing configuration are they requesting an X509 certificate from 68 | // to support the new multi-environment system 69 | string key_id = 1; 70 | // The CSR proves possession of the private key as well as requesting other 71 | // elements of the final certificate 72 | bytes csr = 2; 73 | // The attestation is a certificate generated from the F9 slot signed by the 74 | // F9 slot 75 | bytes attestation = 3; 76 | // The certificate from the F9 slot, signed by the trusted root authority 77 | // Generally this will be the Yubico Root CA. 78 | bytes attestation_intermediate = 4; 79 | } 80 | 81 | message AttestedX509CertificateResponse { 82 | bytes certificate = 1; 83 | string error = 2; 84 | int64 error_code = 3; 85 | } 86 | 87 | // This call fetches a list of all authorized signers and their signing pubkeys 88 | message AllowedSignersRequest {} 89 | 90 | message AllowedSignersResponse { 91 | bytes compressed_allowed_signers = 1; 92 | } 93 | 94 | service Rustica { 95 | rpc Challenge(ChallengeRequest) returns (ChallengeResponse); 96 | rpc Certificate(CertificateRequest) returns (CertificateResponse); 97 | rpc RegisterKey(RegisterKeyRequest) returns (RegisterKeyResponse); 98 | rpc RegisterU2FKey(RegisterU2FKeyRequest) returns (RegisterU2FKeyResponse); 99 | rpc AttestedX509Certificate(AttestedX509CertificateRequest) returns (AttestedX509CertificateResponse); 100 | rpc AllowedSigners(AllowedSignersRequest) returns (AllowedSignersResponse); 101 | } 102 | -------------------------------------------------------------------------------- /resources/create_certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | if [ "$1" = "clean" ]; then 3 | rm -rf rustica/ 4 | rm -rf author/ 5 | rm -rf clients/ 6 | rm -rf copyeditor/ 7 | rm *.key 8 | rm *.pem 9 | rm *.csr 10 | rm *.srl 11 | rm user_ssh_ca* 12 | rm host_ssh_ca* 13 | rm example_user_key* 14 | exit 15 | fi 16 | 17 | if [ "$1" = "client" ]; then 18 | CLIENT_NAME=$2 19 | mkdir -p clients/$CLIENT_NAME 20 | cd clients/$CLIENT_NAME 21 | 22 | # ------------ Generate Example User Key ------------ # 23 | ssh-keygen -t ed25519 -f $CLIENT_NAME -q -N "" 24 | 25 | CLIENT_CONFIG=""" 26 | authorityKeyIdentifier=keyid,issuer 27 | basicConstraints=CA:FALSE 28 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 29 | subjectAltName = @alt_names 30 | 31 | [alt_names] 32 | DNS.1 = ${CLIENT_NAME}""" 33 | 34 | echo $CLIENT_CONFIG > client.ext 35 | 36 | # ------------ Generate Signed Certificate For Client ------------ # 37 | # Generate TestHost key 38 | openssl ecparam -genkey -name prime256v1 -noout -out ${CLIENT_NAME}_private.pem 39 | openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in ${CLIENT_NAME}_private.pem -out ${CLIENT_NAME}.key 40 | rm ${CLIENT_NAME}_private.pem 41 | 42 | # Generate TestHost CSR 43 | openssl req -new -key ${CLIENT_NAME}.key -out ${CLIENT_NAME}.csr -subj "/CN=${CLIENT_NAME}/" 44 | 45 | # Generate TestHost Certificate 46 | openssl x509 -req -in ${CLIENT_NAME}.csr -CA ../../client_ca.pem -CAkey ../../client_ca.key -CAcreateserial -out ${CLIENT_NAME}.pem -days 825 -sha256 -extfile client.ext 47 | rm client.ext 48 | exit 49 | fi 50 | 51 | if [ "$1" = "build_local" ]; then 52 | echo "Building certifications for local testing!" 53 | export BUILD_LOCAL=true 54 | fi 55 | 56 | create_editor_certs () { 57 | if [ "$BUILD_LOCAL" ]; then 58 | DNSNAME="localhost" 59 | else 60 | DNSNAME=$1 61 | fi 62 | 63 | CONFIG=""" 64 | authorityKeyIdentifier=keyid,issuer 65 | basicConstraints=CA:FALSE 66 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 67 | subjectAltName = @alt_names 68 | 69 | [alt_names] 70 | DNS.1 = ${DNSNAME}""" 71 | NAME=$1 72 | mkdir -p ${NAME} 73 | echo $CONFIG > ${NAME}/${NAME}.ext 74 | 75 | openssl ecparam -genkey -name prime256v1 -noout -out ${NAME}/${NAME}_private.pem 76 | openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in ${NAME}/${NAME}_private.pem -out ${NAME}/${NAME}.key 77 | rm ${NAME}/${NAME}_private.pem 78 | 79 | openssl req -new -key ${NAME}/${NAME}.key -out ${NAME}/${NAME}.csr -subj "/CN=${NAME}/O=Rustica/C=CA" 80 | openssl x509 -req -in ${NAME}/${NAME}.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out ${NAME}/${NAME}.pem -days 825 -sha256 -extfile ${NAME}/${NAME}.ext 81 | } 82 | 83 | # Generate CA key and cert 84 | openssl ecparam -genkey -name prime256v1 -noout -out ca.key 85 | openssl req -x509 -new -key ca.key -nodes -days 3650 -out ca.pem -subj '/CN=EnterpriseRootCA' 86 | 87 | # Generate Client CA key and cert 88 | openssl ecparam -genkey -name prime256v1 -noout -out client_ca.key 89 | # Convert EC key format to PKCS#8 key format to comply with ring's key format requirement 90 | openssl pkcs8 -topk8 -nocrypt -in client_ca.key -out client_ca_pkcs8.key 91 | openssl req -new -key client_ca.key -x509 -nodes -days 3650 -out client_ca.pem -subj '/CN=RusticaAccess' 92 | 93 | # ------------ Generate Private Keys For Test Infra ------------ # 94 | # Generate Rustica Certificates 95 | create_editor_certs "rustica" 96 | # Generate Author Certificates 97 | create_editor_certs "author" 98 | # Generate CopyEditor Certificates 99 | create_editor_certs "copyeditor" 100 | # Generate Quroum Certificates 101 | create_editor_certs "quorum" 102 | 103 | create_editor_certs "okta-adapter" 104 | 105 | # ------------ Generate User and Host CA Keys ------------ # 106 | ssh-keygen -t ed25519 -f rustica/user_ssh_ca -q -N "" 107 | ssh-keygen -t ed25519 -f rustica/host_ssh_ca -q -N "" 108 | 109 | -------------------------------------------------------------------------------- /resources/package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | export X86_64_APPLE_DARWIN_OPENSSL_LIB_DIR=/usr/local/Cellar/openssl@3/3.2.0/lib/ 3 | export X86_64_APPLE_DARWIN_OPENSSL_INCLUDE_DIR=/usr/local/Cellar/openssl@3/3.2.0/include/ 4 | export OPENSSL_STATIC=1 5 | 6 | BINARY=rustica-agent-cli 7 | 8 | # Remove the old version of the package 9 | rm RusticaAgent.pkg RusticaAgentInterim.pkg 10 | 11 | # Build universal application binary 12 | cd .. 13 | cargo build --bin ${BINARY} --release 14 | cargo build --bin ${BINARY} --release --target x86_64-apple-darwin 15 | mkdir -p resources/root/usr/local/bin/ 16 | lipo -create target/release/${BINARY} target/x86_64-apple-darwin/release/${BINARY} -output resources/root/usr/local/bin/rustica-agent-cli 17 | 18 | cargo build --bin ${BINARY} --release --no-default-features --features "ctap2_hid" 19 | cargo build --bin ${BINARY} --release --no-default-features --features "ctap2_hid" --target x86_64-apple-darwin 20 | mkdir -p resources/root/usr/local/bin/ 21 | lipo -create target/release/${BINARY} target/x86_64-apple-darwin/release/${BINARY} -output resources/root/usr/local/bin/rustica-agent-cli-ctap2 22 | 23 | # Codesign the binary 24 | codesign --options=runtime --timestamp -f -s "5QY" --identifier "io.confurious.RusticaAgent" resources/root/usr/local/bin/rustica-agent 25 | codesign --options=runtime --timestamp -f -s "5QY" --identifier "io.confurious.RusticaAgent" resources/root/usr/local/bin/rustica-agent-ctap2 26 | 27 | 28 | echo "Done!" 29 | 30 | # Use the below if you need to build a package 31 | 32 | # # Build the package that installs the application 33 | # pkgbuild --sign 5q --root resources/root --identifier io.confurious.RusticaAgent --install-location / --timestamp resources/RusticaAgentInterim.pkg 34 | 35 | # # Build the final product 36 | # productbuild --sign 5Q --package resources/RusticaAgentInterim.pkg resources/RusticaAgent.pkg 37 | 38 | # # Clean up artifacts 39 | # rm -rf RusticaAgentInterim.pkg root/ 40 | -------------------------------------------------------------------------------- /resources/systemd-config/README.md: -------------------------------------------------------------------------------- 1 | # Systemd config 2 | 3 | Within this dir, you can place these files with the path prefix of ${HOME}/.config/ to add a user level systemd config for managing the rustica agent. 4 | The examples given are specifically for serving multiple ssh keys within a dir. 5 | 6 | ``` 7 | systemd-config/ 8 | ├── environment.d 9 | │   └── rustica_ssh_socket.conf # Exports the SSH_AUTH_SOCK to your session 10 | ├── README.md 11 | └── systemd 12 | └── rustica.service # The service unit file 13 | ``` 14 | -------------------------------------------------------------------------------- /resources/systemd-config/environment.d/rustica_ssh_socket.conf: -------------------------------------------------------------------------------- 1 | SSH_AUTH_SOCK="${XDG_RUNTIME_DIR}/rustica-ssh-agent.socket" 2 | -------------------------------------------------------------------------------- /resources/systemd-config/systemd/rustica.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Rustica SSH key agent 3 | 4 | [Service] 5 | Type=simple 6 | 7 | Environment=SSH_AUTH_SOCK=%t/rustica-ssh-agent.socket 8 | Environment=YK_PIN=123456 9 | Environment=RUSTICA_CERT_FOR= 10 | 11 | # Change the execstart to the path of where your rustica agent cli lives 12 | # You can specify just the cli name if it lives within one of the standard /bin paths 13 | # You _cannot_ use environment variables within the command line 14 | ExecStart=/home/myuser/src/rustica/target/debug/rustica-agent-cli multi \ 15 | --dir ${HOME}/.ssh/rustica_ssh_keys/ \ 16 | --config ${HOME}/.config/rustica-agent/config.toml \ 17 | --certfor $RUSTICA_CERT_FOR \ 18 | --socket $SSH_AUTH_SOCK 19 | 20 | ExecStopPost=rm -f $SSH_AUTH_SOCK 21 | 22 | [Install] 23 | WantedBy=default.target 24 | -------------------------------------------------------------------------------- /rustica-agent-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustica-agent-cli" 3 | version = "0.12.0" 4 | edition = "2021" 5 | 6 | [features] 7 | default = ["mozilla"] 8 | mozilla = ["rustica-agent/mozilla"] 9 | ctap2_hid = ["rustica-agent/ctap2_hid"] 10 | 11 | [dependencies] 12 | clap = "3.0.5" 13 | env_logger = "0.8.2" 14 | hex = "0.4" 15 | log = "0.4.13" 16 | notify-rust = "4" 17 | rustica-agent = { path = "../rustica-agent", default-features = false } 18 | tokio = { version = "1", features = ["full"] } 19 | toml = "0.7" 20 | yubikey = "0.7" 21 | -------------------------------------------------------------------------------- /rustica-agent-cli/README.md: -------------------------------------------------------------------------------- 1 | # Rustica Agent CLI 2 | 3 | ## Introduction 4 | This is the canonical agent for Rustica. Used for generating, registering, providing, and fetching certificates for your (generally hardware) keys. Generally when new features are added, they are added and tested here first, before being ported to any of the other agent crates. 5 | 6 | ## Limitations 7 | RusticaAgent does not support the normal array of SSH-Agent calls, the currently supported calls are: 8 | 9 | - `Identities` - Called when connecting to a host or running `ssh-add -L` 10 | - `Sign` - Called when connecting to a host and a public key has been accepted. 11 | - `AddIdentity` - Called when running `ssh-add ` 12 | 13 | ## Usage 14 | When using RusticaAgent it is preferable to provide a configuration file that contains all the parameters needed for normal operation. Any configuration file setting may be override by also providing it on the command line. RusticaAgent also only presents a single Yubikey backed key to the remote server but will present any other keys added with the `AddIdentity` call (keys added with `ssh-add`). 15 | 16 | An example configuration files can be found in the root/examples directory. 17 | 18 | ## Sub Commands 19 | There are several subcommands available for determining proper configuration as well as handling key registration of both PIV and FIDO keys with the Rustica backend. Run `rustica-agent-cli --help` to see more details. 20 | 21 | ## Running via systemd 22 | See [the systemd resources for more information](../resources/systemd-config/README.md) 23 | -------------------------------------------------------------------------------- /rustica-agent-cli/src/config/allowed_signers.rs: -------------------------------------------------------------------------------- 1 | use clap::ArgMatches; 2 | use rustica_agent::config::UpdatableConfiguration; 3 | 4 | use super::{ 5 | parse_config_from_args, ConfigurationError, 6 | RusticaAgentAction, 7 | }; 8 | 9 | pub struct GetAllowedSignersConfig { 10 | pub updatable_configuration: UpdatableConfiguration, 11 | } 12 | 13 | pub async fn configure_allowed_signers( 14 | matches: &ArgMatches, 15 | ) -> Result { 16 | let updatable_configuration = parse_config_from_args(&matches)?; 17 | 18 | Ok(RusticaAgentAction::GetAllowedSigners(GetAllowedSignersConfig { 19 | updatable_configuration, 20 | })) 21 | } 22 | -------------------------------------------------------------------------------- /rustica-agent-cli/src/config/fidosetup.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use super::{parse_config_from_args, ConfigurationError, RusticaAgentAction}; 4 | 5 | use clap::{Arg, ArgMatches, Command}; 6 | use rustica_agent::config::UpdatableConfiguration; 7 | 8 | pub enum SKType { 9 | Ed25519, 10 | Ecdsa, 11 | } 12 | 13 | pub struct ProvisionAndRegisterFidoConfig { 14 | pub updatable_configuration: UpdatableConfiguration, 15 | pub app_name: String, 16 | pub comment: String, 17 | pub key_type: SKType, 18 | pub pin: Option, 19 | pub out: Option, 20 | } 21 | 22 | pub async fn configure_fido_setup( 23 | matches: &ArgMatches, 24 | ) -> Result { 25 | let updatable_configuration = parse_config_from_args(&matches)?; 26 | 27 | let app_name = matches.value_of("application").unwrap().to_string(); 28 | 29 | if !app_name.starts_with("ssh:") { 30 | return Err(ConfigurationError::InvalidFidoKeyName); 31 | } 32 | 33 | let comment = matches.value_of("comment").unwrap().to_string(); 34 | let out = matches.value_of("out").map(String::from); 35 | 36 | let key_type = match matches.value_of("kind") { 37 | Some("ecdsa") => SKType::Ecdsa, 38 | _ => SKType::Ed25519, 39 | }; 40 | 41 | let pin_env = matches.value_of("pin-env").unwrap().to_string(); 42 | let pin = match env::var(pin_env) { 43 | Ok(val) => Some(val), 44 | Err(_e) => None, 45 | }; 46 | 47 | let provision_config = ProvisionAndRegisterFidoConfig { 48 | updatable_configuration, 49 | app_name, 50 | comment, 51 | key_type, 52 | pin, 53 | out, 54 | }; 55 | 56 | return Ok(RusticaAgentAction::ProvisionAndRegisterFido( 57 | provision_config, 58 | )); 59 | } 60 | 61 | pub fn add_configuration(cmd: Command) -> Command { 62 | cmd.arg( 63 | Arg::new("application") 64 | .help("Specify application you are creating the key for") 65 | .default_value("ssh:") 66 | .long("application") 67 | .short('a') 68 | .required(false) 69 | .takes_value(true), 70 | ) 71 | .arg( 72 | Arg::new("comment") 73 | .help("A comment about what this SSH key will be for") 74 | .long("comment") 75 | .required(false) 76 | .default_value("RusticaAgentProvisionedKey"), 77 | ) 78 | .arg( 79 | Arg::new("kind") 80 | .help("Whether you'd like an Ed25519 or ECDSA P256 key") 81 | .possible_values(vec!["ed25519", "ecdsa"]) 82 | .default_value("ed25519") 83 | .long("kind") 84 | .short('k'), 85 | ) 86 | .arg( 87 | Arg::new("pin-env") 88 | .help("Specify the pin environment variable") 89 | .default_value("YK_PIN") 90 | .long("pinenv") 91 | .short('p') 92 | .required(false) 93 | .takes_value(true), 94 | ) 95 | .arg( 96 | Arg::new("out") 97 | .help("Relative path to write your new private key handle to") 98 | .required(false) 99 | .long("out") 100 | .takes_value(true) 101 | .short('o'), 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /rustica-agent-cli/src/config/gitconfig.rs: -------------------------------------------------------------------------------- 1 | use clap::{Arg, ArgMatches, Command}; 2 | use rustica_agent::{get_all_piv_keys, slot_parser, slot_validator}; 3 | use rustica_agent::{PrivateKey, PublicKey}; 4 | 5 | use super::{ConfigurationError, RusticaAgentAction}; 6 | 7 | pub fn configure_git_config( 8 | matches: &ArgMatches, 9 | ) -> Result { 10 | let file = matches.value_of("file").map(|x| x.to_string()); 11 | let slot = matches 12 | .value_of("slot") 13 | .map(slot_parser) 14 | .map(|x| x.unwrap()); // This should be a safe unwrap because we have passed the slot validator 15 | let serial = matches.value_of("serial").map(|x| x.to_string()); 16 | 17 | let public_key = match (file, slot, serial) { 18 | (Some(f), None, _) => match (PrivateKey::from_path(&f), PublicKey::from_path(&f)) { 19 | (Ok(p), _) => p.pubkey.clone(), 20 | (_, Ok(p)) => p, 21 | (_, _) => { 22 | return Err(ConfigurationError::CannotReadFile( 23 | "Could not read the key as either a private or public key".to_owned(), 24 | )) 25 | } 26 | }, 27 | (None, Some(slot), serial) => { 28 | let all_keys = 29 | get_all_piv_keys().map_err(|x| ConfigurationError::YubikeyError(x.to_string()))?; 30 | 31 | let mut candidate_keys: Vec<_> = 32 | all_keys.into_iter().filter(|x| x.1.slot == slot).collect(); 33 | 34 | match (candidate_keys.len(), serial) { 35 | (0, _) => return Err(ConfigurationError::YubikeyNoKeypairFound), 36 | (1, _) => candidate_keys.pop().unwrap().1.public_key, 37 | (_, None) => return Err(ConfigurationError::UnableToDetermineKey), 38 | (_, Some(serial)) => { 39 | candidate_keys 40 | .into_iter() 41 | .filter(|x| x.1.serial.to_string() == serial) 42 | .collect::>() 43 | .pop() 44 | .ok_or(ConfigurationError::YubikeyNoKeypairFound)? 45 | .1 46 | .public_key 47 | } 48 | } 49 | } 50 | (None, None, _) => return Err(ConfigurationError::UnableToDetermineKey), // They didn't provide anything 51 | (Some(_), Some(_), _) => return Err(ConfigurationError::UnableToDetermineKey), // They provided both and we can't disambiguate 52 | }; 53 | 54 | return Ok(RusticaAgentAction::GitConfig(public_key)); 55 | } 56 | 57 | pub fn add_configuration(cmd: Command) -> Command { 58 | cmd.arg( 59 | Arg::new("file") 60 | .help("Used instead of a slot to provide a private key via file") 61 | .long("file") 62 | .short('f') 63 | .takes_value(true), 64 | ) 65 | .arg( 66 | Arg::new("slot") 67 | .help("Numerical value for the slot on the yubikey to use for your private key") 68 | .long("slot") 69 | .short('s') 70 | .validator(slot_validator) 71 | .takes_value(true), 72 | ) 73 | .arg( 74 | Arg::new("serial") 75 | .help("If multiple Yubikeys are connected and the same slot is used on both, this is required to disambiguate") 76 | .long("serial") 77 | .short('S') 78 | .requires("slot") 79 | .takes_value(true), 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /rustica-agent-cli/src/config/immediatemode.rs: -------------------------------------------------------------------------------- 1 | use clap::{Arg, ArgMatches, Command}; 2 | use rustica_agent::{slot_validator, CertificateConfig, Signatory, config::UpdatableConfiguration}; 3 | 4 | use super::{ 5 | get_signatory, parse_certificate_config_from_args, parse_config_from_args, ConfigurationError, 6 | RusticaAgentAction, 7 | }; 8 | 9 | pub struct ImmediateConfig { 10 | pub updatable_configuration: UpdatableConfiguration, 11 | pub certificate_options: CertificateConfig, 12 | pub signatory: Signatory, 13 | pub out: Option, 14 | } 15 | 16 | pub async fn configure_immediate( 17 | matches: &ArgMatches, 18 | ) -> Result { 19 | let updatable_configuration = parse_config_from_args(&matches)?; 20 | let config = updatable_configuration.get_configuration(); 21 | 22 | let certificate_options = parse_certificate_config_from_args(&matches, &config)?; 23 | let out = matches.value_of("out").map(|x| x.to_string()); 24 | let slot = matches.value_of("slot").map(|x| x.to_string()); 25 | let file = matches.value_of("file").map(|x| x.to_string()); 26 | 27 | let signatory = get_signatory(&slot, &config.slot, &file, &config.key)?; 28 | 29 | return Ok(RusticaAgentAction::Immediate(ImmediateConfig { 30 | updatable_configuration, 31 | certificate_options, 32 | signatory, 33 | out, 34 | })); 35 | } 36 | 37 | pub fn add_configuration(cmd: Command) -> Command { 38 | let cmd = super::add_request_options(cmd); 39 | 40 | cmd 41 | .arg( 42 | Arg::new("out") 43 | .help("Output the certificate to a file and exit. Useful for refreshing host certificates") 44 | .short('o') 45 | .long("out") 46 | .takes_value(true) 47 | ) 48 | .arg( 49 | Arg::new("slot") 50 | .help("Numerical value for the slot on the yubikey to use for your private key") 51 | .long("slot") 52 | .short('s') 53 | .validator(slot_validator) 54 | .takes_value(true), 55 | ) 56 | .arg( 57 | Arg::new("file") 58 | .help("Used instead of a slot to provide a private key via file") 59 | .long("file") 60 | .short('f') 61 | .takes_value(true), 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /rustica-agent-cli/src/config/listpivkeys.rs: -------------------------------------------------------------------------------- 1 | use clap::{Arg, ArgMatches, Command}; 2 | 3 | use super::{ConfigurationError, RusticaAgentAction}; 4 | 5 | pub struct ListPIVKeysConfig { 6 | pub show_full: bool, 7 | } 8 | 9 | pub fn configure_list_piv_keys( 10 | matches: &ArgMatches, 11 | ) -> Result { 12 | Ok(RusticaAgentAction::ListPIVKeys(ListPIVKeysConfig { 13 | show_full: matches.is_present("full"), 14 | })) 15 | } 16 | 17 | pub fn add_configuration(cmd: Command) -> Command { 18 | cmd.arg( 19 | Arg::new("full") 20 | .help("Show the full key instead of the fingerprint") 21 | .long("full") 22 | .short('L'), 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /rustica-agent-cli/src/config/provisionpiv.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use clap::{Arg, ArgMatches, Command}; 4 | use rustica_agent::{slot_validator, Signatory, YubikeySigner}; 5 | use yubikey::PinPolicy; 6 | 7 | use super::{get_signatory, ConfigurationError, RusticaAgentAction}; 8 | 9 | pub struct ProvisionPIVConfig { 10 | pub yubikey: YubikeySigner, 11 | pub pin: String, 12 | pub management_key: Vec, 13 | pub require_touch: bool, 14 | pub pin_policy: PinPolicy, 15 | pub subject: String, 16 | } 17 | 18 | pub fn pin_policy_parser(policy: &str) -> Result { 19 | match policy { 20 | "always" | "a" => Ok(PinPolicy::Always), 21 | "once" | "o" => Ok(PinPolicy::Once), 22 | "never" | "n" => Ok(PinPolicy::Never), 23 | _ => Err(format!("{policy} is not a policy for pin I understand")), 24 | } 25 | } 26 | 27 | pub fn configure_provision_piv( 28 | matches: &ArgMatches, 29 | ) -> Result { 30 | let slot = matches.value_of("slot").map(|x| x.to_string()); 31 | 32 | let signatory = get_signatory(&slot, &None, &None, &None)?; 33 | 34 | let yubikey = match signatory { 35 | Signatory::Yubikey(yk_sig) => yk_sig, 36 | Signatory::Direct(_) => return Err(ConfigurationError::CannotProvisionFile), 37 | }; 38 | 39 | let require_touch = matches.is_present("require-touch"); 40 | let subject = matches.value_of("subject").unwrap().to_string(); 41 | let management_key = match hex::decode(matches.value_of("management-key").unwrap()) { 42 | Ok(mgm) => mgm, 43 | Err(_) => return Err(ConfigurationError::YubikeyManagementKeyInvalid), 44 | }; 45 | 46 | let pin_env = matches.value_of("pin-env").unwrap().to_string(); 47 | let pin = match env::var(pin_env) { 48 | Ok(val) => val, 49 | Err(_e) => "123456".to_string(), 50 | }; 51 | 52 | let pin_policy = matches 53 | .get_one("pin-policy") 54 | .map(|x: &PinPolicy| x.clone()) 55 | .unwrap_or(PinPolicy::Once); 56 | 57 | let provision_config = ProvisionPIVConfig { 58 | yubikey, 59 | pin, 60 | management_key, 61 | subject, 62 | require_touch, 63 | pin_policy, 64 | }; 65 | 66 | return Ok(RusticaAgentAction::ProvisionPIV(provision_config)); 67 | } 68 | 69 | pub fn add_configuration(cmd: Command) -> Command { 70 | cmd.arg( 71 | Arg::new("management-key") 72 | .help("Specify the management key") 73 | .default_value("010203040506070801020304050607080102030405060708") 74 | .long("mgmkey") 75 | .short('m') 76 | .required(false) 77 | .takes_value(true), 78 | ) 79 | .arg( 80 | Arg::new("slot") 81 | .help("Numerical value for the slot on the yubikey to use for your private key") 82 | .long("slot") 83 | .short('s') 84 | .validator(slot_validator) 85 | .takes_value(true), 86 | ) 87 | .arg( 88 | Arg::new("pin-env") 89 | .help("Specify a different pin environment variable") 90 | .default_value("YK_PIN") 91 | .long("pinenv") 92 | .short('p') 93 | .required(false) 94 | .takes_value(true), 95 | ) 96 | .arg( 97 | Arg::new("require-touch") 98 | .help("Require the key to always be tapped. If this is not selected, a tap will be required if not tapped in the last 15 seconds.") 99 | .long("require-touch") 100 | .short('r') 101 | ) 102 | .arg( 103 | Arg::new("pin-policy") 104 | .help("Require the pin be provided to use this key. Can be \"always\", \"once\", or \"never\". The chosen policy cannot be changed later and may affect the ability to enroll the key with a backend.") 105 | .long("pin-policy") 106 | .short('P') 107 | .value_parser(pin_policy_parser) 108 | .multiple_occurrences(false) 109 | .multiple_values(false) 110 | .takes_value(true) 111 | ) 112 | .arg( 113 | Arg::new("subject") 114 | .help("Subject of the new cert you're creating (this is only used as a note)") 115 | .default_value("Rustica-AgentQuickProvision") 116 | .long("subj") 117 | .short('j') 118 | ) 119 | } 120 | -------------------------------------------------------------------------------- /rustica-agent-cli/src/config/refresh_attested_x509_certificate.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use clap::{Arg, ArgMatches, Command}; 4 | use rustica_agent::Yubikey; 5 | use rustica_agent::{ 6 | config::UpdatableConfiguration, slot_parser, slot_validator, Signatory, YubikeySigner, 7 | }; 8 | 9 | use super::{parse_config_from_args, ConfigurationError, RusticaAgentAction}; 10 | 11 | pub struct RefreshAttestedX509Config { 12 | pub updatable_configuration: UpdatableConfiguration, 13 | pub signatory: Signatory, 14 | pub pin: String, 15 | pub management_key: Vec, 16 | } 17 | 18 | pub async fn configure_refresh_x509_certificate( 19 | matches: &ArgMatches, 20 | ) -> Result { 21 | let updatable_configuration = parse_config_from_args(&matches)?; 22 | 23 | let slot = matches.value_of("slot").map(|x| x.to_string()).unwrap(); 24 | let slot = slot_parser(&slot).unwrap(); 25 | 26 | let signatory = Signatory::Yubikey(YubikeySigner { 27 | yk: Yubikey::new().unwrap().into(), 28 | slot, 29 | }); 30 | 31 | let pin_env = matches.value_of("pin-env").unwrap().to_string(); 32 | let pin = match env::var(pin_env) { 33 | Ok(val) => val, 34 | Err(_e) => "123456".to_string(), 35 | }; 36 | 37 | let management_key = match hex::decode(matches.value_of("management-key").unwrap()) { 38 | Ok(mgm) => mgm, 39 | Err(_) => return Err(ConfigurationError::YubikeyManagementKeyInvalid), 40 | }; 41 | 42 | Ok(RusticaAgentAction::RefreshAttestedX509( 43 | RefreshAttestedX509Config { 44 | updatable_configuration, 45 | signatory, 46 | pin, 47 | management_key, 48 | }, 49 | )) 50 | } 51 | 52 | pub fn add_configuration(cmd: Command) -> Command { 53 | cmd.arg( 54 | Arg::new("slot") 55 | .help("Numerical value for the slot on the yubikey to use for your private key") 56 | .long("slot") 57 | .short('s') 58 | .required(true) 59 | .validator(slot_validator) 60 | .takes_value(true), 61 | ) 62 | .arg( 63 | Arg::new("pin-env") 64 | .help("Specify a different pin environment variable") 65 | .default_value("YK_PIN") 66 | .long("pinenv") 67 | .short('p') 68 | .required(false) 69 | .takes_value(true), 70 | ) 71 | .arg( 72 | Arg::new("management-key") 73 | .help("Specify the management key") 74 | .default_value("010203040506070801020304050607080102030405060708") 75 | .long("mgmkey") 76 | .short('m') 77 | .required(false) 78 | .takes_value(true), 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /rustica-agent-cli/src/config/register.rs: -------------------------------------------------------------------------------- 1 | use clap::{Arg, ArgMatches, Command}; 2 | use rustica_agent::{config::UpdatableConfiguration, slot_validator, PIVAttestation, Signatory}; 3 | use yubikey::piv::SlotId; 4 | 5 | use super::{get_signatory, parse_config_from_args, ConfigurationError, RusticaAgentAction}; 6 | 7 | pub struct RegisterConfig { 8 | pub updatable_configuration: UpdatableConfiguration, 9 | pub signatory: Signatory, 10 | pub attestation: PIVAttestation, 11 | } 12 | 13 | pub async fn configure_register( 14 | matches: &ArgMatches, 15 | ) -> Result { 16 | let updatable_configuration = parse_config_from_args(&matches)?; 17 | let config = updatable_configuration.get_configuration(); 18 | 19 | let slot = matches.value_of("slot").map(|x| x.to_string()); 20 | let file = matches.value_of("file").map(|x| x.to_string()); 21 | 22 | let mut signatory = get_signatory(&slot, &config.slot, &file, &config.key)?; 23 | 24 | let mut attestation = PIVAttestation { 25 | certificate: vec![], 26 | intermediate: vec![], 27 | }; 28 | 29 | if !matches.is_present("no-attest") { 30 | let signer = match &mut signatory { 31 | Signatory::Yubikey(s) => s, 32 | Signatory::Direct(_) => return Err(ConfigurationError::CannotAttestFileBasedKey), 33 | }; 34 | 35 | let mut yk = signer.yk.lock().await; 36 | 37 | attestation.certificate = yk.fetch_attestation(&signer.slot).unwrap_or_default(); 38 | attestation.intermediate = yk 39 | .fetch_certificate(&SlotId::Attestation) 40 | .unwrap_or_default(); 41 | 42 | if attestation.certificate.is_empty() || attestation.intermediate.is_empty() { 43 | error!("Part of the attestation could not be generated. Registration may fail"); 44 | } 45 | } 46 | 47 | return Ok(RusticaAgentAction::Register(RegisterConfig { 48 | updatable_configuration, 49 | signatory, 50 | attestation, 51 | })); 52 | } 53 | 54 | pub fn add_configuration(cmd: Command) -> Command { 55 | cmd 56 | .arg( 57 | Arg::new("no-attest") 58 | .help("Don't send attestation data for this key. This may make registration fail depending on server configurations.") 59 | .long("no-attest") 60 | ) 61 | .arg( 62 | Arg::new("slot") 63 | .help("Numerical value for the slot on the yubikey to use for your private key") 64 | .long("slot") 65 | .short('s') 66 | .validator(slot_validator) 67 | .takes_value(true), 68 | ) 69 | .arg( 70 | Arg::new("file") 71 | .help("Used instead of a slot to provide a private key via file") 72 | .long("file") 73 | .short('f') 74 | .takes_value(true), 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /rustica-agent-cli/src/config/singlemode.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::Arc; 3 | 4 | use clap::{Arg, ArgMatches, Command}; 5 | use rustica_agent::{slot_validator, Handler, Signatory}; 6 | 7 | use notify_rust::Notification; 8 | 9 | use super::{ 10 | get_signatory, parse_certificate_config_from_args, parse_config_from_args, 11 | parse_socket_path_from_args, ConfigurationError, RunConfig, RusticaAgentAction, 12 | }; 13 | 14 | pub async fn configure_singlemode( 15 | matches: &ArgMatches, 16 | ) -> Result { 17 | let updatable_configuration = parse_config_from_args(&matches)?; 18 | let config = updatable_configuration.get_configuration(); 19 | 20 | let certificate_options = parse_certificate_config_from_args(&matches, &config)?; 21 | let socket_path = parse_socket_path_from_args(matches, &config); 22 | 23 | let slot = matches.value_of("slot").map(|x| x.to_string()); 24 | let file = matches.value_of("file").map(|x| x.to_string()); 25 | 26 | let mut signatory = get_signatory(&slot, &config.slot, &file, &config.key)?; 27 | let pubkey = match &mut signatory { 28 | Signatory::Yubikey(signer) => { 29 | match signer.yk.lock().await.ssh_cert_fetch_pubkey(&signer.slot) { 30 | Ok(cert) => cert, 31 | Err(_) => return Err(ConfigurationError::YubikeyNoKeypairFound), 32 | } 33 | } 34 | Signatory::Direct(privkey) => { 35 | let mut privkey = privkey.lock().await; 36 | if let Some(path) = matches.value_of("fido-device-path") { 37 | privkey.set_device_path(path); 38 | } 39 | 40 | privkey.pubkey.clone() 41 | } 42 | }; 43 | 44 | let notification_f = move || { 45 | println!("Trying to send a notification"); 46 | if let Err(e) = Notification::new() 47 | .summary("RusticaAgent") 48 | .body("An application is requesting a signature. Please tap your Yubikey.") 49 | .show() 50 | { 51 | error!("Notification system errored: {e}"); 52 | } 53 | }; 54 | 55 | let handler = Handler { 56 | updatable_configuration: updatable_configuration.into(), 57 | cert: None.into(), 58 | pubkey: pubkey.clone(), 59 | signatory, 60 | stale_at: 0.into(), 61 | certificate_options, 62 | identities: HashMap::new().into(), 63 | piv_identities: HashMap::new(), 64 | notification_function: Some(Box::new(notification_f)), 65 | certificate_priority: matches.is_present("certificate-priority"), 66 | }; 67 | 68 | let handler = Arc::new(handler); 69 | Ok(RusticaAgentAction::Run(RunConfig { 70 | socket_path, 71 | pubkey, 72 | handler, 73 | })) 74 | } 75 | 76 | pub fn add_configuration(cmd: Command) -> Command { 77 | // Add socket path and certificate priority 78 | let cmd = super::add_daemon_options(cmd); 79 | 80 | // Add options for setting the fields on requested certificates 81 | let cmd = super::add_request_options(cmd); 82 | 83 | cmd.arg( 84 | Arg::new("slot") 85 | .help("Numerical value for the slot on the yubikey to use for your private key") 86 | .long("slot") 87 | .short('s') 88 | .validator(slot_validator) 89 | .takes_value(true), 90 | ) 91 | .arg( 92 | Arg::new("file") 93 | .help("Used instead of a slot to provide a private key via file") 94 | .long("file") 95 | .short('f') 96 | .takes_value(true), 97 | ) 98 | .arg( 99 | Arg::new("fido-device-path") 100 | .help("The device path to use for FIDO2 keys. If not provided, we'll pick a device randomly. Should be set when multiple FIDO2 devices connected.") 101 | .long("fido") 102 | .required(false) 103 | .takes_value(true), 104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /rustica-agent-gui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustica-agent-gui" 3 | version = "0.12.0" 4 | edition = "2021" 5 | 6 | [features] 7 | default = ["mozilla"] 8 | mozilla = ["rustica-agent/mozilla"] 9 | ctap2_hid = ["rustica-agent/ctap2_hid"] 10 | 11 | [dependencies] 12 | base64 = "0.12" 13 | eframe = "0.22" 14 | hex = "0.4" 15 | home = "0.5" 16 | rustica-agent = { path = "../rustica-agent", default-features = false } 17 | tokio = { version = "1", features = ["full"] } 18 | toml = "0.5" 19 | tracing-subscriber = "0.3" 20 | -------------------------------------------------------------------------------- /rustica-agent-gui/README.md: -------------------------------------------------------------------------------- 1 | # Rustica Agent GUI 2 | Rustica agent was always envisioned as a commandline utility, similar to standard SSH agents. Unfortunately as Rustica has increased in complexity, the set it and forget it nature of the normal SSH agent model has not scaled well. 3 | 4 | Generally SSH agents can be light weight and started in every shell if needed, but since we speak to Yubikeys we have USB handles, since we talk to backends we have cached certificates, and the user might want to flip settings for certificate priority depending on what they're doing at a particular moment. 5 | 6 | Thus Rustica agent was reworked into a library only crate and the CLI split out so this crate can exist for a GUI. 7 | 8 | ## Screenshot 9 | ![Image of Rustica Agent GUI](https://user-images.githubusercontent.com/2386877/202832593-e27308cd-c2ec-4e31-b1c9-32f58d533b69.png) 10 | 11 | 12 | ## Why egui? 13 | I wanted a cross platform library that would allow me to make a relatively simple UI without too much effort. If there are better libraries I'm open to switching. 14 | 15 | ## Why not tui? 16 | I think [tui](https://github.com/fdehau/tui-rs) fills a slightly different niche which was not what I was trying to build with this. However I would love for another crate here, `rustica-agent-tui`, if someone really wants it. 17 | 18 | ## Current limitation 19 | - No support for multi-mode 20 | - No support for PIV mode 21 | - No support for registration of FIDO keys 22 | - No support for git-config generation -------------------------------------------------------------------------------- /rustica-agent/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustica-agent" 3 | version = "0.12.1" 4 | authors = ["Mitchell Grenier "] 5 | edition = "2021" 6 | 7 | [features] 8 | default = ["mozilla"] 9 | mozilla = ["sshcerts/fido-support-mozilla", "sshcerts/yubikey-support"] 10 | ctap2_hid = ["sshcerts/fido-support", "sshcerts/yubikey-support"] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | async-trait = "0.1" 16 | base64 = "0.12.1" 17 | byteorder = "1.4.2" 18 | env_logger = "0.8.2" 19 | hex = "0.4.2" 20 | log = "0.4.13" 21 | prost = "0.11" 22 | ring = "0.17" 23 | serde = "1.0.97" 24 | serde_derive = "1.0" 25 | sha2 = "0.9.2" 26 | # For Production 27 | sshcerts = { version = "0.14.0" } 28 | # For Development 29 | # sshcerts = { path = "../../sshcerts", features = [ 30 | # "yubikey-support", 31 | # "fido-support", 32 | # ] } 33 | tokio = { version = "1", features = ["full"] } 34 | toml = "0.7" 35 | tonic = { version = "0.9", features = ["tls"] } 36 | yubikey = { version = "0.7", features = ["untested"] } 37 | x509-parser = { version = "0.15", features = ["verify"] } 38 | 39 | # Dependencies for allowed_signers feature 40 | zstd = "0.13.1" 41 | 42 | [build-dependencies] 43 | tonic-build = "0.9" 44 | 45 | [lib] 46 | name = "rustica_agent" 47 | crate-type = ["staticlib", "rlib"] 48 | -------------------------------------------------------------------------------- /rustica-agent/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tonic_build::compile_protos("../proto/rustica.proto") 3 | .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e)); 4 | } 5 | -------------------------------------------------------------------------------- /rustica-agent/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use crate::{RusticaAgentLibraryError, RusticaServer}; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[derive(Clone, Debug, Deserialize, Serialize)] 10 | pub struct Options { 11 | pub principals: Option>, 12 | pub hosts: Option>, 13 | pub kind: Option, 14 | pub duration: Option, 15 | pub authority: Option, 16 | } 17 | 18 | #[derive(Clone, Debug, Deserialize)] 19 | struct Version { 20 | version: u64, 21 | } 22 | 23 | #[derive(Debug, Deserialize, Serialize)] 24 | pub struct Config { 25 | version: u64, 26 | pub servers: Vec, 27 | pub slot: Option, 28 | pub key: Option, 29 | pub options: Option, 30 | pub socket: Option, 31 | } 32 | 33 | pub struct UpdatableConfiguration { 34 | path: PathBuf, 35 | configuration: Config, 36 | } 37 | 38 | impl UpdatableConfiguration { 39 | pub fn new>(path: P) -> Result { 40 | let configuration = parse_config_path(&path)?; 41 | 42 | Ok(Self { 43 | path: path.as_ref().to_owned(), 44 | configuration, 45 | }) 46 | } 47 | 48 | pub fn write(&self) -> Result<(), std::io::Error> { 49 | // We unwrap here becasue there is no way this should ever fail as this 50 | // is our configuration type. If it somehow does fail, then this is a 51 | // critical failure. 52 | let contents = toml::to_string(&self.configuration) 53 | .expect("The configuration type is fundamentally wrong"); 54 | 55 | std::fs::write(&self.path, contents) 56 | } 57 | 58 | pub fn update_servers(&mut self, servers: Vec) -> Result<(), std::io::Error> { 59 | self.configuration.servers = servers; 60 | self.write() 61 | } 62 | 63 | pub fn get_servers_mut(&mut self) -> std::slice::IterMut<'_, RusticaServer> { 64 | self.configuration.servers.iter_mut() 65 | } 66 | 67 | pub fn get_configuration(&self) -> &Config { 68 | &self.configuration 69 | } 70 | 71 | pub fn get_configuration_mut(&mut self) -> &mut Config { 72 | &mut self.configuration 73 | } 74 | } 75 | 76 | /// Parse a RusticaAgent configuration from a path 77 | pub fn parse_config_path>(path: P) -> Result { 78 | let config = fs::read_to_string(path) 79 | .map_err(|x| RusticaAgentLibraryError::CouldNotReadConfigurationFile(x.to_string()))?; 80 | 81 | parse_config(&config) 82 | } 83 | 84 | /// Parse a RusticaAgent configuration from a string 85 | pub fn parse_config(config: &str) -> Result { 86 | match toml::from_str(&config) { 87 | Err(_) => parse_v1_config(config), 88 | Ok(Version { version: 2 }) => parse_v2_config(config), 89 | Ok(Version { version: x }) => { 90 | return Err(RusticaAgentLibraryError::UnknownConfigurationVersion(x)) 91 | } 92 | } 93 | } 94 | 95 | /// Parses the original format of the RusticaAgent configuration. There is no 96 | /// version field in this format so we will always try this if that is missing. 97 | fn parse_v1_config(config: &str) -> Result { 98 | #[derive(Debug, Deserialize)] 99 | pub struct ConfigV1 { 100 | pub server: String, 101 | pub ca_pem: String, 102 | pub mtls_cert: String, 103 | pub mtls_key: String, 104 | pub slot: Option, 105 | pub key: Option, 106 | pub options: Option, 107 | pub socket: Option, 108 | } 109 | 110 | let config_v1: ConfigV1 = match toml::from_str(&config) { 111 | Ok(t) => t, 112 | Err(e) => return Err(RusticaAgentLibraryError::BadConfiguration(e.to_string())), 113 | }; 114 | 115 | let server_config = RusticaServer { 116 | address: config_v1.server, 117 | ca_pem: config_v1.ca_pem, 118 | mtls_cert: config_v1.mtls_cert, 119 | mtls_key: config_v1.mtls_key, 120 | }; 121 | 122 | Ok(Config { 123 | version: 2, 124 | servers: vec![server_config], 125 | slot: config_v1.slot, 126 | key: config_v1.key, 127 | options: config_v1.options, 128 | socket: config_v1.socket, 129 | }) 130 | } 131 | 132 | fn parse_v2_config(config: &str) -> Result { 133 | match toml::from_str(&config) { 134 | Ok(t) => Ok(t), 135 | Err(e) => return Err(RusticaAgentLibraryError::BadConfiguration(e.to_string())), 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /rustica-agent/src/ffi/allowed_signer.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{c_char, CStr}; 2 | use std::fs::File; 3 | use std::io::Write; 4 | 5 | use crate::config::UpdatableConfiguration; 6 | 7 | use tokio::runtime::Runtime; 8 | 9 | pub enum GetAllowedSignersStatus { 10 | Success = 0, 11 | ConfigurationError = 1, 12 | ParameterError, 13 | InternalError, 14 | AllowedSignersFileError, 15 | } 16 | 17 | /// Request all allowed signers 18 | #[no_mangle] 19 | pub unsafe extern "C" fn ffi_get_allowed_signers( 20 | config_path: *const c_char, 21 | out_path: *const c_char, 22 | ) -> i32 { 23 | let cf = CStr::from_ptr(config_path); 24 | let config_path = match cf.to_str() { 25 | Ok(s) => s, 26 | Err(e) => { 27 | error!("Unable to marshall config_path to &str: {e}"); 28 | return GetAllowedSignersStatus::ConfigurationError as i32; 29 | }, 30 | }; 31 | 32 | let updatable_configuration = match UpdatableConfiguration::new(config_path) { 33 | Ok(c) => c, 34 | Err(e) => { 35 | error!("Configuration was invalid: {e}"); 36 | return GetAllowedSignersStatus::ConfigurationError as i32; 37 | }, 38 | }; 39 | 40 | let out_path = CStr::from_ptr(out_path); 41 | let out_path = match out_path.to_str() { 42 | Ok(s) => s, 43 | Err(e) => { 44 | error!("Unable to marshall out_path to &str: {e}"); 45 | return GetAllowedSignersStatus::ParameterError as i32; 46 | }, 47 | }; 48 | 49 | let runtime = match Runtime::new() { 50 | Ok(rt) => rt, 51 | Err(e) => { 52 | error!("Unable to initialize tokio runtime: {e}"); 53 | return GetAllowedSignersStatus::InternalError as i32; 54 | }, 55 | }; 56 | let runtime_handle = runtime.handle().to_owned(); 57 | 58 | let mut out_file = match File::create(out_path) { 59 | Ok(f) => f, 60 | Err(e) => { 61 | error!("Could not create Allowed Signers file at {}: {}", out_path, e); 62 | return GetAllowedSignersStatus::AllowedSignersFileError as i32; 63 | } 64 | }; 65 | 66 | for server in &updatable_configuration.get_configuration().servers { 67 | let allowed_signers = match server.get_allowed_signers(&runtime_handle) { 68 | Ok(data) => { 69 | println!( 70 | "Allowed signers were successfully fetched from server: {}", 71 | server.address 72 | ); 73 | data 74 | } 75 | Err(e) => { 76 | error!("Allowed signers could not be fetched. Server said: {}", e); 77 | continue; 78 | }, 79 | }; 80 | 81 | match out_file.write_all(allowed_signers.as_bytes()) { 82 | Ok(()) => return GetAllowedSignersStatus::Success as i32, 83 | Err(e) => { 84 | error!("Could not write to file {}: {}", out_path, e); 85 | return GetAllowedSignersStatus::AllowedSignersFileError as i32; 86 | }, 87 | } 88 | } 89 | 90 | GetAllowedSignersStatus::InternalError as i32 91 | } 92 | -------------------------------------------------------------------------------- /rustica-agent/src/ffi/mod.rs: -------------------------------------------------------------------------------- 1 | mod agent; 2 | mod enrollment; 3 | mod allowed_signer; 4 | mod signing; 5 | mod utils; 6 | mod yubikey_utils; 7 | mod mtls; 8 | 9 | use std::ffi::{c_char, c_long, CStr}; 10 | 11 | /// For functions related to starting (and stopping) RusticaAgent instances 12 | pub use agent::*; 13 | 14 | /// For generating and enrolling keys that will be used with RusticaAgent 15 | pub use enrollment::*; 16 | 17 | /// For fetching all signer SSH keys that were registered 18 | pub use allowed_signer::*; 19 | 20 | /// For functions that handle signing arbitrary data using SSH keys 21 | pub use signing::*; 22 | 23 | /// For functions that handle memory management and other utilities 24 | pub use utils::*; 25 | 26 | /// For functions that handle YubiKey specific functionality (generally PIV) 27 | pub use yubikey_utils::*; 28 | 29 | /// For functions that handle mTLS configs 30 | pub use mtls::*; 31 | 32 | use crate::config::UpdatableConfiguration; 33 | 34 | #[no_mangle] 35 | /// Read a configuration file and return the expiry time of the primary server (the first one) 36 | pub unsafe extern "C" fn ffi_get_expiry_of_primary_server(config_path: *const c_char) -> c_long { 37 | let cf = CStr::from_ptr(config_path); 38 | let config_path = match cf.to_str() { 39 | Err(_) => return -1, 40 | Ok(s) => s, 41 | }; 42 | 43 | let updatable_configuration = match UpdatableConfiguration::new(config_path) { 44 | Ok(c) => c, 45 | Err(e) => { 46 | error!("Configuration was invalid: {e}"); 47 | return GenerateAndEnrollStatus::ConfigurationError as i64; 48 | } 49 | }; 50 | 51 | let server = match updatable_configuration.get_configuration().servers.first() { 52 | Some(s) => &s.mtls_cert, 53 | None => return -1, 54 | }; 55 | 56 | match x509_parser::pem::parse_x509_pem(server.as_bytes()) { 57 | Err(_) => return -1, 58 | Ok((_, s)) => match s.parse_x509() { 59 | Err(_) => return -2, 60 | Ok(cert) => cert.validity().not_after.timestamp(), 61 | }, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /rustica-agent/src/ffi/mtls.rs: -------------------------------------------------------------------------------- 1 | use crate::config::UpdatableConfiguration; 2 | use std::{ffi::{c_char, CStr, CString}, ptr::null}; 3 | 4 | #[no_mangle] 5 | /// Read the mTLS identities of the primary server (the first one) given a config path 6 | pub unsafe extern "C" fn ffi_get_identities_of_primary_server(config_path: *const c_char) -> *const c_char { 7 | let cf = CStr::from_ptr(config_path); 8 | let config_path = match cf.to_str() { 9 | Err(_) => return null(), 10 | Ok(s) => s, 11 | }; 12 | 13 | let updatable_configuration = match UpdatableConfiguration::new(config_path) { 14 | 15 | Ok(c) => c, 16 | Err(e) => { 17 | error!("Configuration was invalid: {e}"); 18 | return null(); 19 | } 20 | }; 21 | 22 | let server = match updatable_configuration.get_configuration().servers.first() { 23 | Some(s) => &s.mtls_cert, 24 | None => return null(), 25 | }; 26 | 27 | let cert = match x509_parser::pem::parse_x509_pem(server.as_bytes()) { 28 | Err(e) => { 29 | error!("Unable to parse mTLS cert PEM: {e}"); 30 | return null(); 31 | }, 32 | Ok((_, s)) => s, 33 | }; 34 | 35 | let subject = match cert.parse_x509() { 36 | Err(e) => { 37 | error!("Unable to parse mTLS cert: {e}"); 38 | return null(); 39 | }, 40 | Ok(c) => c.tbs_certificate.subject().to_string(), 41 | }; 42 | 43 | match CString::new(subject) { 44 | Err(e) => { 45 | error!("Unable to marshall subject to CString: {e}"); 46 | return null(); 47 | }, 48 | Ok(s) => s.into_raw(), 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /rustica-agent/src/ffi/signing.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{c_char, c_uchar, c_ulong, CStr, CString}; 2 | 3 | use sshcerts::{ 4 | ssh::{SshSignature, VerifiedSshSignature}, 5 | PrivateKey, PublicKey, 6 | }; 7 | 8 | #[no_mangle] 9 | /// Take a private key and sign arbitrary data with it under the given namespace. 10 | /// Returns the signature as a string which then needs to be freed. All failures 11 | /// return a null pointer. 12 | pub unsafe extern "C" fn ffi_sign_data( 13 | key_path: *const c_char, 14 | namespace: *const c_char, 15 | data: *const c_uchar, 16 | data_len: c_ulong, 17 | ) -> *const c_char { 18 | let key_path = match CStr::from_ptr(key_path).to_str() { 19 | Err(_) => return std::ptr::null(), 20 | Ok(s) => s, 21 | }; 22 | 23 | let private_key = match PrivateKey::from_path(key_path) { 24 | Err(_) => return std::ptr::null(), 25 | Ok(k) => k, 26 | }; 27 | 28 | let namespace = match CStr::from_ptr(namespace).to_str() { 29 | Err(_) => return std::ptr::null(), 30 | Ok(s) => s, 31 | }; 32 | 33 | let data = std::slice::from_raw_parts(data, data_len as usize); 34 | 35 | match VerifiedSshSignature::new_with_private_key(data, namespace, private_key, None) { 36 | Err(_) => return std::ptr::null(), 37 | Ok(signature) => match CString::new(signature.to_string()) { 38 | Err(_) => return std::ptr::null(), 39 | Ok(s) => s.into_raw(), 40 | }, 41 | } 42 | } 43 | 44 | fn parse_allowed_signer<'a>(allowed_signer: &'a str) -> Option<(PublicKey, &'a str)> { 45 | let allowed_signer = allowed_signer.splitn(2, ' ').collect::>(); 46 | if allowed_signer.len() != 2 { 47 | return None; 48 | } 49 | 50 | match PublicKey::from_string(allowed_signer[1]) { 51 | Err(_) => None, 52 | Ok(k) => Some((k, allowed_signer[0])), 53 | } 54 | } 55 | 56 | #[no_mangle] 57 | /// Verify a signature against the given allowed_signers, data, and namespace. 58 | /// Returns the name of the allowed signer which then needs to be freed. All failures 59 | /// return a null pointer. 60 | pub unsafe extern "C" fn ffi_verify_signed_data( 61 | allowed_signers_path: *const c_char, 62 | namespace: *const c_char, 63 | data: *const c_uchar, 64 | data_len: c_ulong, 65 | signature_contents: *const c_char, 66 | ) -> *const c_char { 67 | let signature_contents = match CStr::from_ptr(signature_contents).to_str() { 68 | Err(_) => return std::ptr::null(), 69 | Ok(s) => s, 70 | }; 71 | 72 | let ssh_signature = match SshSignature::from_armored_string(&signature_contents) { 73 | Err(_) => return std::ptr::null(), 74 | Ok(s) => s, 75 | }; 76 | 77 | let allowed_signers_path = match CStr::from_ptr(allowed_signers_path).to_str() { 78 | Err(_) => return std::ptr::null(), 79 | Ok(s) => s, 80 | }; 81 | 82 | let allowed_signers = match std::fs::read_to_string(allowed_signers_path) { 83 | Ok(s) => s, 84 | Err(_) => return std::ptr::null(), 85 | }; 86 | 87 | let allowed_signer = allowed_signers 88 | .lines() 89 | .filter_map(parse_allowed_signer) 90 | .filter(|x| ssh_signature.pubkey == x.0) 91 | .next(); 92 | 93 | let allowed_signer = match allowed_signer { 94 | None => return std::ptr::null(), 95 | Some(s) => s, 96 | }; 97 | 98 | let message = std::slice::from_raw_parts(data, data_len as usize); 99 | 100 | let namespace = match CStr::from_ptr(namespace).to_str() { 101 | Err(_) => return std::ptr::null(), 102 | Ok(s) => s, 103 | }; 104 | 105 | match VerifiedSshSignature::from_ssh_signature( 106 | message, 107 | ssh_signature, 108 | namespace, 109 | Some(allowed_signer.0), 110 | ) { 111 | Err(_) => return std::ptr::null(), 112 | Ok(_) => match CString::new(allowed_signer.1) { 113 | Err(_) => return std::ptr::null(), 114 | Ok(s) => s.into_raw(), 115 | }, 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /rustica-agent/src/ffi/utils.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{c_char, CString}; 2 | 3 | /// Free a string allocated by Rust 4 | #[no_mangle] 5 | pub unsafe extern "C" fn ffi_free_rust_string(string_ptr: *mut c_char) { 6 | drop(CString::from_raw(string_ptr)); 7 | } 8 | -------------------------------------------------------------------------------- /rustica-agent/src/rustica/allowed_signer.rs: -------------------------------------------------------------------------------- 1 | use super::error::RefreshError; 2 | use crate::RusticaServer; 3 | 4 | use super::AllowedSignersRequest; 5 | 6 | use std::io::Read; 7 | 8 | use x509_parser::nom::AsBytes; 9 | use tokio::runtime::Handle; 10 | 11 | impl RusticaServer { 12 | pub async fn get_allowed_signers_async( 13 | &self, 14 | ) -> Result { 15 | let request = AllowedSignersRequest{}; 16 | let request = tonic::Request::new(request); 17 | 18 | let mut client = super::get_rustica_client(&self).await?; 19 | 20 | let response = client.allowed_signers(request).await?; 21 | let response = response.into_inner(); 22 | 23 | // Decode zstd-compressed allowed_signers 24 | let mut allowed_signers_decoder = match zstd::stream::Decoder::new(response.compressed_allowed_signers.as_bytes()) { 25 | Ok(decoder) => decoder, 26 | Err(e) => { 27 | error!("Unable to initialize zstd decoder: {}", e.to_string()); 28 | return Err(RefreshError::UnknownError); 29 | }, 30 | }; 31 | let mut allowed_signers = String::new(); 32 | if let Err(e) = allowed_signers_decoder.read_to_string(&mut allowed_signers) { 33 | error!("Unable to decompress allowed signers: {}", e.to_string()); 34 | return Err(RefreshError::BadAllowedSigners); 35 | } 36 | 37 | Ok(allowed_signers) 38 | } 39 | 40 | pub fn get_allowed_signers( 41 | &self, 42 | handle: &Handle, 43 | ) -> Result { 44 | handle.block_on(async { 45 | self.get_allowed_signers_async() 46 | .await 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /rustica-agent/src/rustica/cert.rs: -------------------------------------------------------------------------------- 1 | use super::error::{RefreshError, ServerError}; 2 | use super::{CertificateRequest, RusticaCert, Signatory}; 3 | use crate::{CertificateConfig, MtlsCredentials, RusticaServer}; 4 | use sshcerts::Certificate; 5 | use tokio::runtime::Handle; 6 | 7 | use std::collections::HashMap; 8 | use std::time::SystemTime; 9 | 10 | impl RusticaServer { 11 | pub async fn refresh_certificate_async( 12 | &self, 13 | signatory: &Signatory, 14 | options: &CertificateConfig, 15 | notification_function: &Option>, 16 | ) -> Result<(RusticaCert, Option), RefreshError> { 17 | let (mut client, challenge) = 18 | super::complete_rustica_challenge(self, signatory, notification_function).await?; 19 | 20 | let current_timestamp = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { 21 | Ok(ts) => ts.as_secs(), 22 | Err(_e) => 0xFFFFFFFFFFFFFFFF, 23 | }; 24 | 25 | let request = tonic::Request::new(CertificateRequest { 26 | cert_type: options.cert_type as u32, 27 | key_id: options.authority.clone(), 28 | critical_options: HashMap::new(), 29 | extensions: Certificate::standard_extensions(), 30 | servers: options.hosts.clone(), 31 | principals: options.principals.clone(), 32 | valid_before: current_timestamp + options.duration, 33 | valid_after: current_timestamp, 34 | challenge: Some(challenge), 35 | }); 36 | 37 | let response = client.certificate(request).await?; 38 | let response = response.into_inner(); 39 | 40 | if response.error_code != 0 { 41 | return Err(RefreshError::RusticaServerError(ServerError { 42 | code: response.error_code, 43 | message: response.error, 44 | })); 45 | } 46 | 47 | // If there is a certificate, then create a new MtlsCredentials struct 48 | // and return it. It's possible in the future the server will only 49 | // return the certificate which is why we only check the certificate. 50 | let mtls_credentials = if !response.new_client_certificate.is_empty() { 51 | Some(MtlsCredentials { 52 | certificate: response.new_client_certificate, 53 | key: response.new_client_key, 54 | }) 55 | } else { 56 | None 57 | }; 58 | 59 | Ok(( 60 | RusticaCert { 61 | cert: response.certificate, 62 | comment: "JITC".to_string(), 63 | }, 64 | mtls_credentials, 65 | )) 66 | } 67 | 68 | pub fn get_custom_certificate( 69 | &self, 70 | signatory: &mut Signatory, 71 | options: &CertificateConfig, 72 | handle: &Handle, 73 | notification_function: &Option>, 74 | ) -> Result<(RusticaCert, Option), RefreshError> { 75 | handle.block_on(async { 76 | self.refresh_certificate_async(signatory, options, notification_function) 77 | .await 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /rustica-agent/src/rustica/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::error; 3 | 4 | #[derive(Debug)] 5 | pub struct ServerError { 6 | pub code: i64, 7 | pub message: String, 8 | } 9 | 10 | #[derive(Debug)] 11 | pub enum RefreshError { 12 | TransportError, 13 | SigningError, 14 | ServerChallengeNotForClientKey, 15 | UnsupportedMode, 16 | InvalidUri, 17 | ConfigurationError(String), 18 | TransportBadStatus(tonic::Status), 19 | BadEncodedData(hex::FromHexError), 20 | RusticaServerError(ServerError), 21 | BadAllowedSigners, 22 | UnknownError, 23 | } 24 | 25 | 26 | impl fmt::Display for RefreshError { 27 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 28 | match *self { 29 | RefreshError::ConfigurationError(ref err) => write!(f, "Configuration is invalid: {}", err), 30 | RefreshError::TransportError => write!(f, "Transport Error. Generally a TLS issue"), 31 | RefreshError::ServerChallengeNotForClientKey => write!(f, "Server challenge is not for your key"), 32 | RefreshError::SigningError => write!(f, "Signing or verification failed"), 33 | RefreshError::UnsupportedMode => write!(f, "Attempted to use a curve or cipher not supported by rustica-agent"), 34 | RefreshError::InvalidUri => write!(f, "Provided address of remote service was invalid"), 35 | RefreshError::TransportBadStatus(ref err) => write!(f, "Bad status from server: {}", err), 36 | RefreshError::BadEncodedData(ref err) => write!(f, "Bad hex encoding: {}", err), 37 | RefreshError::RusticaServerError(ref err) => write!(f, "Error from server: {}", err.message), 38 | RefreshError::BadAllowedSigners => write!(f, "Bad allowed signers data"), 39 | RefreshError::UnknownError => write!(f, "Unknown error occured"), 40 | } 41 | } 42 | } 43 | 44 | impl error::Error for RefreshError {} 45 | 46 | impl From for RefreshError { 47 | fn from(e: tonic::transport::Error) -> Self { 48 | debug!("Transport Error: {}", e); 49 | RefreshError::TransportError 50 | } 51 | } 52 | 53 | impl From for RefreshError { 54 | fn from(e: tonic::Status) -> Self { 55 | RefreshError::TransportBadStatus(e) 56 | } 57 | } 58 | 59 | impl From for RefreshError { 60 | fn from(e: hex::FromHexError) -> Self { 61 | RefreshError::BadEncodedData(e) 62 | } 63 | } 64 | 65 | impl From for RefreshError { 66 | fn from(_e: sshcerts::yubikey::piv::Error) -> Self { 67 | RefreshError::SigningError 68 | } 69 | } 70 | 71 | impl From for RefreshError { 72 | fn from(_: ring::error::Unspecified) -> Self { 73 | RefreshError::SigningError 74 | } 75 | } 76 | 77 | impl From for RefreshError { 78 | fn from(_: ring::error::KeyRejected) -> Self { 79 | RefreshError::SigningError 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /rustica-agent/src/rustica/key.rs: -------------------------------------------------------------------------------- 1 | use tokio::runtime::Handle; 2 | 3 | use super::error::RefreshError; 4 | use super::{RegisterKeyRequest, RegisterU2fKeyRequest, RusticaServer, Signatory}; 5 | 6 | pub mod rustica { 7 | tonic::include_proto!("rustica"); 8 | } 9 | 10 | #[derive(Debug)] 11 | pub struct PIVAttestation { 12 | pub certificate: Vec, 13 | pub intermediate: Vec, 14 | } 15 | 16 | #[derive(Debug)] 17 | pub struct U2FAttestation { 18 | pub auth_data: Vec, 19 | pub auth_data_sig: Vec, 20 | pub intermediate: Vec, 21 | pub challenge: Vec, 22 | pub alg: i32, 23 | } 24 | 25 | impl RusticaServer { 26 | pub async fn register_key_async( 27 | &self, 28 | signatory: &mut Signatory, 29 | attestation: &PIVAttestation, 30 | ) -> Result<(), RefreshError> { 31 | let (mut client, challenge) = 32 | super::complete_rustica_challenge(self, signatory, &None).await?; 33 | 34 | let request = RegisterKeyRequest { 35 | certificate: attestation.certificate.clone(), 36 | intermediate: attestation.intermediate.clone(), 37 | challenge: Some(challenge), 38 | }; 39 | 40 | let request = tonic::Request::new(request); 41 | 42 | client.register_key(request).await?; 43 | Ok(()) 44 | } 45 | 46 | pub fn register_key( 47 | &self, 48 | signatory: &mut Signatory, 49 | key: &PIVAttestation, 50 | handle: &Handle, 51 | ) -> Result<(), RefreshError> { 52 | handle.block_on(async { self.register_key_async(signatory, key).await }) 53 | } 54 | 55 | pub async fn register_u2f_key_async( 56 | &self, 57 | signatory: &mut Signatory, 58 | application: &str, 59 | attestation: &U2FAttestation, 60 | ) -> Result<(), RefreshError> { 61 | let (mut client, challenge) = 62 | super::complete_rustica_challenge(self, signatory, &None).await?; 63 | 64 | let request = RegisterU2fKeyRequest { 65 | auth_data: attestation.auth_data.clone(), 66 | auth_data_signature: attestation.auth_data_sig.clone(), 67 | sk_application: application.as_bytes().to_vec(), 68 | u2f_challenge: attestation.challenge.clone(), 69 | intermediate: attestation.intermediate.clone(), 70 | alg: attestation.alg, 71 | challenge: Some(challenge), 72 | u2f_challenge_hashed: true, 73 | }; 74 | 75 | let request = tonic::Request::new(request); 76 | 77 | client.register_u2f_key(request).await?; 78 | Ok(()) 79 | } 80 | 81 | pub fn register_u2f_key( 82 | &self, 83 | signatory: &mut Signatory, 84 | application: &str, 85 | key: &U2FAttestation, 86 | handle: &Handle, 87 | ) -> Result<(), RefreshError> { 88 | handle.block_on(async { 89 | self.register_u2f_key_async(signatory, application, key) 90 | .await 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /rustica-agent/src/rustica/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cert; 2 | pub mod error; 3 | pub mod key; 4 | pub mod x509; 5 | pub mod allowed_signer; 6 | 7 | use std::ops::Deref; 8 | use std::time::Duration; 9 | 10 | pub use error::RefreshError; 11 | 12 | pub use rustica_proto::rustica_client::RusticaClient; 13 | pub use rustica_proto::{ 14 | AttestedX509CertificateRequest, AttestedX509CertificateResponse, CertificateRequest, 15 | CertificateResponse, Challenge, ChallengeRequest, RegisterKeyRequest, RegisterU2fKeyRequest, 16 | AllowedSignersRequest, AllowedSignersResponse, 17 | }; 18 | 19 | use sshcerts::ssh::Certificate as SSHCertificate; 20 | 21 | use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; 22 | 23 | use crate::{RusticaServer, Signatory}; 24 | 25 | pub mod rustica_proto { 26 | tonic::include_proto!("rustica"); 27 | } 28 | 29 | pub struct RusticaCert { 30 | pub cert: String, 31 | pub comment: String, 32 | } 33 | 34 | pub async fn get_rustica_client( 35 | server: &RusticaServer, 36 | ) -> Result, RefreshError> { 37 | let client_identity = Identity::from_pem(&server.mtls_cert, &server.mtls_key); 38 | 39 | let channel = match Channel::from_shared(server.address.clone()) { 40 | Ok(c) => c, 41 | Err(_) => return Err(RefreshError::InvalidUri), 42 | }; 43 | 44 | let ca = Certificate::from_pem(&server.ca_pem); 45 | let tls = ClientTlsConfig::new() 46 | .ca_certificate(ca) 47 | .identity(client_identity); 48 | let channel = channel 49 | .timeout(Duration::from_secs(10)) 50 | .connect_timeout(Duration::from_secs(5)) 51 | .tls_config(tls)? 52 | .connect() 53 | .await?; 54 | 55 | let client = RusticaClient::new(channel); 56 | 57 | Ok(client) 58 | } 59 | 60 | pub async fn complete_rustica_challenge( 61 | server: &RusticaServer, 62 | signatory: &Signatory, 63 | notification_function: &Option>, 64 | ) -> Result<(RusticaClient, Challenge), RefreshError> { 65 | let ssh_pubkey = match signatory { 66 | Signatory::Yubikey(signer) => { 67 | let mut yk = signer.yk.lock().await; 68 | yk.reconnect()?; 69 | match yk.ssh_cert_fetch_pubkey(&signer.slot) { 70 | Ok(pkey) => pkey, 71 | Err(_) => return Err(RefreshError::SigningError), 72 | } 73 | } 74 | Signatory::Direct(privkey) => privkey.lock().await.pubkey.clone(), 75 | }; 76 | 77 | let encoded_key = format!("{}", ssh_pubkey); 78 | debug!( 79 | "Requesting cert for key with fingerprint: {}", 80 | ssh_pubkey.fingerprint() 81 | ); 82 | let request = tonic::Request::new(ChallengeRequest { 83 | pubkey: encoded_key.to_string(), 84 | }); 85 | 86 | let mut client = get_rustica_client(server).await?; 87 | let response = client.challenge(request).await?; 88 | 89 | let response = response.into_inner(); 90 | 91 | if response.no_signature_required { 92 | debug!("This server does not require signatures be sent, not resigning the certificate"); 93 | return Ok(( 94 | client, 95 | Challenge { 96 | pubkey: encoded_key.to_string(), 97 | challenge_time: response.time, 98 | challenge: response.challenge, 99 | challenge_signature: String::new(), 100 | }, 101 | )); 102 | } 103 | 104 | debug!("{}", &response.challenge); 105 | 106 | let mut challenge_certificate = 107 | SSHCertificate::from_string(&response.challenge).map_err(|_| RefreshError::SigningError)?; 108 | challenge_certificate.signature_key = challenge_certificate.key.clone(); 109 | 110 | // We assert that the pubkey in the challenge belongs to the client 111 | // This prevents a malicious Rustica server from tricking the client into signing a 112 | // malicious SSH certificate for some unknown key. 113 | if challenge_certificate.key.fingerprint().hash != ssh_pubkey.fingerprint().hash { 114 | error!("The public key in the challenge doesn't match the client's public key"); 115 | return Err(RefreshError::ServerChallengeNotForClientKey); 116 | } 117 | 118 | // We need to sign the challenge so let's notify the user they 119 | // will need to interact with their device if (if a device is being used) 120 | if let Some(f) = notification_function { 121 | f(); 122 | } 123 | 124 | let resigned_certificate = match signatory { 125 | Signatory::Yubikey(signer) => { 126 | let signature = signer 127 | .yk 128 | .lock() 129 | .await 130 | .ssh_cert_signer(&challenge_certificate.tbs_certificate(), &signer.slot) 131 | .map_err(|_| RefreshError::SigningError)?; 132 | challenge_certificate 133 | .add_signature(&signature) 134 | .map_err(|_| RefreshError::SigningError)? 135 | } 136 | Signatory::Direct(privkey) => { 137 | let privkey = privkey.lock().await; 138 | challenge_certificate 139 | .sign(privkey.deref()) 140 | .map_err(|_| RefreshError::SigningError)? 141 | } 142 | }; 143 | 144 | Ok(( 145 | client, 146 | Challenge { 147 | pubkey: encoded_key.to_string(), 148 | challenge_time: response.time, 149 | challenge: format!("{}", resigned_certificate), 150 | challenge_signature: String::new(), 151 | }, 152 | )) 153 | } 154 | -------------------------------------------------------------------------------- /rustica-agent/src/rustica/x509.rs: -------------------------------------------------------------------------------- 1 | use tokio::runtime::Handle; 2 | use yubikey::piv::SlotId; 3 | 4 | use crate::{RusticaServer, Signatory}; 5 | 6 | use super::{error::ServerError, get_rustica_client, AttestedX509CertificateRequest, RefreshError}; 7 | 8 | impl RusticaServer { 9 | pub async fn refresh_attested_x509_certificate_async( 10 | &self, 11 | signatory: &mut Signatory, 12 | ) -> Result, RefreshError> { 13 | let (mut yk, slot) = match signatory { 14 | Signatory::Yubikey(yk) => (yk.yk.lock().await, yk.slot), 15 | _ => return Err(RefreshError::UnsupportedMode), 16 | }; 17 | 18 | // The CN will be ignored by the backend 19 | let csr = yk.generate_csr(&slot, "common_name").map_err(|_| { 20 | RefreshError::ConfigurationError(format!( 21 | "Could not generate CSR for slot {}. Is it provisioned?", 22 | slot 23 | )) 24 | })?; 25 | 26 | yk.reconnect().unwrap(); 27 | 28 | let attestation = yk.fetch_attestation(&slot).map_err(|e| 29 | RefreshError::ConfigurationError(format!("Could not generate attestation for slot {slot}. Is it attestable (not imported)? Error {e}")))?; 30 | 31 | yk.reconnect().unwrap(); 32 | 33 | let attestation_intermediate = 34 | yk.fetch_certificate(&SlotId::Attestation).map_err(|_| { 35 | RefreshError::ConfigurationError(format!( 36 | "Could not fetch attestation intermediate. Have you manually removed it?" 37 | )) 38 | })?; 39 | 40 | let request = tonic::Request::new(AttestedX509CertificateRequest { 41 | // TODO: We need to start taking in key IDs 42 | key_id: String::new(), 43 | csr, 44 | attestation, 45 | attestation_intermediate, 46 | }); 47 | 48 | let mut client = get_rustica_client(self).await?; 49 | 50 | let response = client 51 | .attested_x509_certificate(request) 52 | .await? 53 | .into_inner(); 54 | 55 | match response.error_code { 56 | 0 => Ok(response.certificate), 57 | _ => Err(RefreshError::RusticaServerError(ServerError { 58 | code: response.error_code, 59 | message: response.error, 60 | })), 61 | } 62 | } 63 | 64 | pub fn refresh_x509_certificate( 65 | &self, 66 | signatory: &mut Signatory, 67 | handle: &Handle, 68 | ) -> Result, RefreshError> { 69 | handle.block_on(async { 70 | self.refresh_attested_x509_certificate_async(signatory) 71 | .await 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /rustica-agent/src/sshagent/agent.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use tokio::net::UnixListener; 4 | use tokio::net::UnixStream; 5 | use tokio::select; 6 | use tokio::sync::mpsc::Receiver; 7 | 8 | use super::protocol::Request; 9 | 10 | use super::handler::SshAgentHandler; 11 | 12 | use super::error::HandleResult; 13 | pub struct Agent; 14 | 15 | impl Agent { 16 | async fn handle_client( 17 | handler: Arc, 18 | mut stream: UnixStream, 19 | ) -> HandleResult<()> { 20 | loop { 21 | let req = Request::read(&mut stream).await?; 22 | trace!("request: {:?}", req); 23 | let response = handler.handle_request(req).await?; 24 | trace!("handler: {:?}", response); 25 | response.write(&mut stream).await?; 26 | } 27 | } 28 | 29 | pub async fn run(handler: Arc, socket_path: String) { 30 | return Self::run_with_termination_channel(handler, socket_path, None).await; 31 | } 32 | 33 | pub async fn run_with_termination_channel( 34 | handler: Arc, 35 | socket_path: String, 36 | term_channel: Option>, 37 | ) { 38 | let listener = UnixListener::bind(socket_path).unwrap(); 39 | let handler = handler.clone(); 40 | 41 | if let Some(mut term_channel) = term_channel { 42 | loop { 43 | select! { 44 | _ = term_channel.recv() => { 45 | println!("Received termination request. Exiting..."); 46 | return 47 | }, 48 | v = listener.accept() => { 49 | match v { 50 | Ok(stream) => { 51 | debug!("Got connection from: {:?}. Spawning thread to handle.", stream.1); 52 | let handler = handler.clone(); 53 | tokio::spawn(async move { 54 | match Agent::handle_client(handler, stream.0).await { 55 | Ok(_) => {} 56 | Err(e) => debug!("handler: {:?}", e), 57 | } 58 | }); 59 | } 60 | Err(e) => { 61 | // connection failed 62 | println!("Encountered an error: {e}. Exiting..."); 63 | return; 64 | } 65 | } 66 | }, 67 | } 68 | } 69 | } else { 70 | loop { 71 | select! { 72 | v = listener.accept() => { 73 | match v { 74 | Ok(stream) => { 75 | let handler = handler.clone(); 76 | tokio::spawn(async move { 77 | match Agent::handle_client(handler, stream.0).await { 78 | Ok(_) => {} 79 | Err(e) => debug!("handler: {:?}", e), 80 | } 81 | }); 82 | } 83 | Err(e) => { 84 | // connection failed 85 | println!("Encountered an error: {e}. Exiting..."); 86 | return; 87 | } 88 | } 89 | }, 90 | } 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /rustica-agent/src/sshagent/error.rs: -------------------------------------------------------------------------------- 1 | use std::{io}; 2 | 3 | pub type ParsingError = Result; 4 | pub type WrittingError = Result; 5 | pub type HandleResult = Result; 6 | 7 | 8 | #[derive(Debug)] 9 | pub struct Error { 10 | pub details: String, 11 | } 12 | 13 | impl Error { 14 | fn new>(details: T) -> Error { 15 | Error { 16 | details: String::from(details.as_ref()), 17 | } 18 | } 19 | } 20 | 21 | 22 | impl From for Error { 23 | fn from(err: io::Error) -> Error { 24 | Error::new(format!("IOError: {}", err)) 25 | } 26 | } 27 | 28 | 29 | impl<'a> From<&'a str> for Error { 30 | fn from(err: &'a str) -> Error { 31 | Error::new(err) 32 | } 33 | } -------------------------------------------------------------------------------- /rustica-agent/src/sshagent/handler.rs: -------------------------------------------------------------------------------- 1 | use super::protocol::Request; 2 | use super::protocol::Response; 3 | 4 | use super::error::HandleResult; 5 | 6 | use async_trait::async_trait; 7 | use sshcerts::PrivateKey; 8 | 9 | #[async_trait] 10 | pub trait SshAgentHandler: Send + Sync { 11 | async fn add_identity(&self, key: PrivateKey) -> HandleResult; 12 | async fn identities(&self) -> HandleResult; 13 | async fn sign_request( 14 | &self, 15 | pubkey: Vec, 16 | data: Vec, 17 | flags: u32, 18 | ) -> HandleResult; 19 | 20 | async fn handle_request(&self, request: Request) -> HandleResult { 21 | match request { 22 | Request::Identities => self.identities().await, 23 | Request::Sign { 24 | ref pubkey_blob, 25 | ref data, 26 | ref flags, 27 | } => { 28 | self.sign_request(pubkey_blob.clone(), data.clone(), *flags) 29 | .await 30 | } 31 | Request::AddIdentity { private_key } => self.add_identity(private_key).await, 32 | Request::Unknown => Ok(Response::Failure), 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rustica-agent/src/sshagent/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate byteorder; 2 | 3 | mod agent; 4 | mod protocol; 5 | mod handler; 6 | pub mod error; 7 | 8 | pub use handler::SshAgentHandler; 9 | pub use agent::Agent; 10 | pub use protocol::Response; 11 | pub use protocol::Identity; -------------------------------------------------------------------------------- /rustica-agent/src/sshagent/protocol.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, WriteBytesExt}; 2 | use tokio::{ 3 | io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, 4 | net::UnixStream, 5 | }; 6 | 7 | use super::error::{ParsingError, WrittingError}; 8 | 9 | #[derive(Debug, Copy, Clone)] 10 | enum MessageRequest { 11 | Identities, 12 | Sign, 13 | AddIdentity, 14 | RemoveIdentity, 15 | RemoveAllIdentities, 16 | AddIdConstrained, 17 | AddSmartcardKey, 18 | RemoveSmartcardKey, 19 | Lock, 20 | Unlock, 21 | AddSmartcardKeyConstrained, 22 | Extension, 23 | Unknown, 24 | } 25 | 26 | impl MessageRequest { 27 | fn from_u8(value: u8) -> MessageRequest { 28 | match value { 29 | 11 => MessageRequest::Identities, 30 | 13 => MessageRequest::Sign, 31 | 17 => MessageRequest::AddIdentity, 32 | 18 => MessageRequest::RemoveIdentity, 33 | 19 => MessageRequest::RemoveAllIdentities, 34 | 25 => MessageRequest::AddIdConstrained, 35 | 20 => MessageRequest::AddSmartcardKey, 36 | 21 => MessageRequest::RemoveSmartcardKey, 37 | 22 => MessageRequest::Lock, 38 | 23 => MessageRequest::Unlock, 39 | 26 => MessageRequest::AddSmartcardKeyConstrained, 40 | 27 => MessageRequest::Extension, 41 | _ => MessageRequest::Unknown, 42 | } 43 | } 44 | } 45 | 46 | async fn read_message(stream: &mut R) -> ParsingError> { 47 | let len = AsyncReadExt::read_u32(stream).await?; 48 | 49 | let mut buf = vec![0; len as usize]; 50 | AsyncReadExt::read_exact(stream, &mut buf).await?; 51 | 52 | Ok(buf) 53 | } 54 | 55 | async fn write_message( 56 | w: &mut W, 57 | string: &[u8], 58 | ) -> WrittingError<()> { 59 | AsyncWriteExt::write_u32(w, string.len() as u32).await?; 60 | AsyncWriteExt::write_all(w, string).await?; 61 | Ok(()) 62 | } 63 | 64 | #[derive(Debug)] 65 | pub enum Request { 66 | Identities, 67 | Sign { 68 | // Blob of the public key 69 | // (encoded as per RFC4253 "6.6. Public Key Algorithms"). 70 | pubkey_blob: Vec, 71 | // The data to sign. 72 | data: Vec, 73 | // Request flags. 74 | flags: u32, 75 | }, 76 | AddIdentity { 77 | private_key: sshcerts::PrivateKey, 78 | }, 79 | Unknown, 80 | } 81 | 82 | impl Request { 83 | pub async fn read(stream: &mut UnixStream) -> ParsingError { 84 | debug!("reading request"); 85 | let raw_msg = read_message(stream).await?; 86 | let mut buf = raw_msg.as_slice(); 87 | 88 | let msg = AsyncReadExt::read_u8(&mut buf).await?; 89 | match MessageRequest::from_u8(msg) { 90 | MessageRequest::Identities => Ok(Request::Identities), 91 | MessageRequest::Sign => Ok(Request::Sign { 92 | pubkey_blob: read_message(&mut buf).await?, 93 | data: read_message(&mut buf).await?, 94 | flags: AsyncReadExt::read_u32(&mut buf).await?, 95 | }), 96 | MessageRequest::AddIdentity => match sshcerts::PrivateKey::from_bytes(buf) { 97 | Ok(private_key) => Ok(Request::AddIdentity { private_key }), 98 | Err(_) => Ok(Request::Unknown), 99 | }, 100 | MessageRequest::RemoveIdentity => Ok(Request::Unknown), 101 | MessageRequest::RemoveAllIdentities => Ok(Request::Unknown), 102 | MessageRequest::AddIdConstrained => Ok(Request::Unknown), 103 | MessageRequest::AddSmartcardKey => Ok(Request::Unknown), 104 | MessageRequest::RemoveSmartcardKey => Ok(Request::Unknown), 105 | MessageRequest::Lock => Ok(Request::Unknown), 106 | MessageRequest::Unlock => Ok(Request::Unknown), 107 | MessageRequest::AddSmartcardKeyConstrained => Ok(Request::Unknown), 108 | MessageRequest::Extension => Ok(Request::Unknown), 109 | MessageRequest::Unknown => { 110 | debug!("Unknown request {}", msg); 111 | Ok(Request::Unknown) 112 | } 113 | } 114 | } 115 | } 116 | 117 | enum AgentMessageResponse { 118 | Failure = 5, 119 | Success = 6, 120 | IdentitiesAnswer = 12, 121 | SignResponse = 14, 122 | } 123 | 124 | #[derive(Clone, Debug)] 125 | pub struct Identity { 126 | pub key_blob: Vec, 127 | pub key_comment: String, 128 | } 129 | 130 | #[allow(dead_code)] 131 | #[derive(Debug)] 132 | pub enum Response { 133 | Success, 134 | Failure, 135 | Identities(Vec), 136 | SignResponse { signature: Vec }, 137 | } 138 | 139 | impl Response { 140 | pub async fn write(&self, stream: &mut UnixStream) -> WrittingError<()> { 141 | let mut buf = Vec::new(); 142 | match *self { 143 | Response::Success => { 144 | WriteBytesExt::write_u8(&mut buf, AgentMessageResponse::Success as u8)? 145 | } 146 | Response::Failure => { 147 | WriteBytesExt::write_u8(&mut buf, AgentMessageResponse::Failure as u8)? 148 | } 149 | Response::Identities(ref identities) => { 150 | WriteBytesExt::write_u8(&mut buf, AgentMessageResponse::IdentitiesAnswer as u8)?; 151 | WriteBytesExt::write_u32::(&mut buf, identities.len() as u32)?; 152 | 153 | for identity in identities { 154 | write_message(&mut buf, &identity.key_blob).await?; 155 | write_message(&mut buf, identity.key_comment.as_bytes()).await?; 156 | } 157 | } 158 | Response::SignResponse { ref signature } => { 159 | WriteBytesExt::write_u8(&mut buf, AgentMessageResponse::SignResponse as u8)?; 160 | 161 | write_message(&mut buf, signature.as_slice()).await?; 162 | } 163 | } 164 | AsyncWriteExt::write_u32(stream, buf.len() as u32).await?; 165 | AsyncWriteExt::write_all(stream, &buf).await?; 166 | Ok(()) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /rustica/.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=file:../examples/example.db 2 | -------------------------------------------------------------------------------- /rustica/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustica" 3 | version = "0.12.1" 4 | authors = ["Mitchell Grenier "] 5 | edition = "2021" 6 | 7 | [features] 8 | default = [] 9 | 10 | all = [ 11 | "amazon-kms", 12 | "influx", 13 | "splunk", 14 | "yubikey-support", 15 | "local-db", 16 | "webhook", 17 | ] 18 | 19 | amazon-kms = ["aws-config", "aws-credential-types", "aws-sdk-kms", "aws-types"] 20 | influx = ["influxdb"] 21 | local-db = ["diesel"] 22 | splunk = ["webhook"] 23 | webhook = ["reqwest", "serde_json"] 24 | yubikey-support = ["sshcerts/yubikey-support"] 25 | 26 | [dependencies] 27 | async-trait = "0.1.56" 28 | base64 = "0.12.1" 29 | clap = "3.0.5" 30 | crossbeam-channel = "0.5" 31 | env_logger = "0.8.2" 32 | hex = "0.4.2" 33 | log = "0.4.13" 34 | prost = "0.11" 35 | ring = "0.17" 36 | serde = { version = "1.0", features = ["derive"] } 37 | # For Production 38 | # sshcerts = { version = "0.12", default-features = false, features = [ 39 | # "fido-lite", 40 | # "x509-support", 41 | # "yubikey-lite", 42 | # ] } 43 | # For Development 44 | sshcerts = { version = "0.14.0", default-features = false, features = [ 45 | "fido-lite", 46 | "x509-support", 47 | "yubikey-lite", 48 | ] } 49 | # sshcerts = { path = "../../sshcerts", default-features = false, features = [ 50 | # "fido-lite", 51 | # "x509-support", 52 | # "yubikey-lite", 53 | # ] } 54 | tokio = { version = "1", features = ["full"] } 55 | toml = "0.5" 56 | tonic = { version = "0.9", features = ["tls"] } 57 | x509-parser = { version = "0.15", features = ["verify"] } 58 | 59 | # These are needed for the X509 certificate integrations 60 | rcgen = { version = "0.11", features = ["x509-parser"] } 61 | asn1 = "0.14" 62 | 63 | # Dependencies for amazon-kms 64 | aws-credential-types = { version = "0.57", optional = true } 65 | aws-config = { version = "0.57", optional = true } 66 | aws-sdk-kms = { version = "0.35", optional = true } 67 | aws-types = { version = "0.57", optional = true } 68 | 69 | # Dependencies for local-db 70 | diesel = { version = "2", features = ["sqlite"], optional = true } 71 | 72 | # Dependencies for Influx 73 | influxdb = { version = "0.6", optional = true } 74 | 75 | # Dependencies for Splunk/Webhook 76 | reqwest = { version = "0.11", default-features = false, features = [ 77 | "rustls-tls", 78 | ], optional = true } 79 | serde_json = { version = "1.0", optional = true } 80 | 81 | # Dependencies for allowed_signers feature 82 | zstd = "0.13.1" 83 | lru = "0.12.3" 84 | 85 | [build-dependencies] 86 | tonic-build = "0.9" 87 | -------------------------------------------------------------------------------- /rustica/README.md: -------------------------------------------------------------------------------- 1 | # Rustica 2 | 3 | The server portion of the Rustica project. 4 | 5 | ## Building 6 | Depending on your needs, Rustica can be built with several different features to enable different use cases. To build them all (generally for testing), run: 7 | `cargo build --features=all`. Below is summary of all the optional features, what they do, and how to configure them. 8 | 9 | ## amazon-kms 10 | This compiles in support to use AmazonKMS as the backend for signing. This requires defining two key identifiers as well as AWS credentials that can access them. These keys must be asymettric, with the Sign/Verify capabilities. Encrypt/Decrypt will not work. 11 | 12 | ### Example Configuration 13 | ```toml 14 | [signing."amazonkms"] 15 | aws_access_key_id = "XXXXXXXXXXXXXXXXXXXX" 16 | aws_secret_access_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 17 | aws_region = "us-west-2" 18 | user_key_id = "mrk-00000000000000000000000000000000" 19 | user_key_signing_algorithm = "ECDSA_SHA_384" 20 | host_key_id = "mrk-00000000000000000000000000000000" 21 | host_key_signing_algorithm = "ECDSA_SHA_384" 22 | ``` 23 | 24 | ### yubikey-support 25 | This compiles in support to use a connected Yubikey 4/5 as the backend for signing. This requires defining two slot identifiers in the form of R followed by a number from 1 to 20 inclusive. For example R1, R9, R12, R20. These keys must be ECDSA 256/384. RSA keys are not supported at this time. 26 | 27 | ### Example Configuration 28 | ```toml 29 | [signing."yubikey"] 30 | user_slot = "R2" 31 | host_slot = "R3" 32 | ``` 33 | 34 | ## influx 35 | Compiles in support to log to an InfluxDB backend. See the example configurations for more details on how to set this up. 36 | 37 | ### Example Configuration 38 | ```toml 39 | [logging."influx"] 40 | address = "http://some-local-influx-instance:8080" 41 | database = "rustica" 42 | dataset = "rustica_logs" 43 | user = "influx_user" 44 | password = "influx_password" 45 | ``` 46 | 47 | ## splunk 48 | Compiles in support to log to an Splunk backend. See the example configurations for more details on how to set this up. 49 | 50 | ### Example Configuration 51 | ```toml 52 | [logging."splunk"] 53 | token = "c46d7213-19ea-4a66-b83b-e4b06188d197" 54 | url = "https://http-inputs-examplecompany.splunkcloud.com/services/collector" 55 | timeout = 5 56 | ``` 57 | 58 | ## local-db 59 | Compiles in support for Rustica to handle authorization without talking to an external service. This requires a local SQLite database with all configured permissions and grants. See `rustica/migrations/2021-01-14-051956_hosts/up.sql` for a detailed explanation of how to configure this database. 60 | 61 | ### Example Configuration 62 | ```toml 63 | [authorization."database"] 64 | path = "examples/example.db" 65 | ``` 66 | 67 | ## HomeLab 68 | One of the best ways to get familiar with Rustica is to run it in a homelab using a Yubikey 5 as your server side signing authority. The recommended way to achieve this is to use the homelab Dockerfile and mount the PCSC socket inside the docker container. 69 | 70 | This will give you the benefites of service resilliancy but you also do not have to run your container in privileged mode. -------------------------------------------------------------------------------- /rustica/bash/verify-hostname.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm $LOGIN_SCRIPT 3 | 4 | function start_user_shell() { 5 | if [ "$FORCE_COMMAND" != "" ]; then 6 | eval $FORCE_COMMAND 7 | exit 0; 8 | fi 9 | 10 | USER_INFO=$(cat /etc/passwd | grep \^`whoami`\:) 11 | SHELL=$(echo $USER_INFO | awk '{split($0,p,":"); print p[7]}') 12 | if [ "$SHELL" = "" ]; then 13 | echo "Could not locate appropriate shell" 14 | SHELL="/bin/bash" 15 | fi 16 | eval $SHELL 17 | } 18 | 19 | IFS=',' read -ra HOSTNAME <<< "$RUSTICA_AUTHORIZED_HOSTS" 20 | for i in "${HOSTNAME[@]}"; do 21 | if [ "$i" = $(hostname) ]; then 22 | echo "Authentication Successful." 23 | start_user_shell 24 | exit 0; 25 | fi 26 | done 27 | 28 | echo "Not authorized for this server." 29 | exit 1; -------------------------------------------------------------------------------- /rustica/bash/verify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm $LOGIN_SCRIPT 3 | 4 | function start_user_shell() { 5 | if [ "$FORCE_COMMAND" != "" ]; then 6 | eval $FORCE_COMMAND 7 | exit 0; 8 | fi 9 | 10 | USER_INFO=$(cat /etc/passwd | grep \^`whoami`\:) 11 | SHELL=$(echo $USER_INFO | awk '{split($0,p,":"); print p[7]}') 12 | if [ "$SHELL" = "" ]; then 13 | echo "Could not locate appropriate shell" 14 | SHELL="/bin/bash" 15 | fi 16 | eval $SHELL 17 | } 18 | 19 | IFS=',' read -ra HOSTNAME <<< "$RUSTICA_AUTHORIZED_HOSTS" 20 | HOSTKEY=$(ssh-keygen -lf /etc/ssh/ssh_host_ecdsa_key.pub | cut -d ' ' -f 2 | cut -c 8-) 21 | for i in "${HOSTNAME[@]}"; do 22 | if [ "$i" = "$HOSTKEY" ]; then 23 | echo "Authentication Successful." 24 | start_user_shell 25 | exit 0; 26 | fi 27 | done 28 | 29 | echo "Not authorized for this server." 30 | exit 1; -------------------------------------------------------------------------------- /rustica/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tonic_build::compile_protos("../proto/rustica.proto") 3 | .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e)); 4 | 5 | tonic_build::compile_protos("../proto/author.proto") 6 | .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e)); 7 | } 8 | -------------------------------------------------------------------------------- /rustica/diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/auth/database/schema.rs" 6 | -------------------------------------------------------------------------------- /rustica/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obelisk/rustica/ea8a5df6ed9884b2e2f603f3e6c64846783d8ec5/rustica/migrations/.gitkeep -------------------------------------------------------------------------------- /rustica/migrations/2021-01-14-051956_hosts/down.sql: -------------------------------------------------------------------------------- 1 | drop table hosts; 2 | drop table fingerprint_principal_authorizations; 3 | drop table fingerprint_host_authorizations; 4 | drop table fingerprint_permissions; 5 | drop table fingerprint_extensions; 6 | drop table fingerprint_critical_options; -------------------------------------------------------------------------------- /rustica/migrations/2021-03-18-175456_registered-keys/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE registered_keys; -------------------------------------------------------------------------------- /rustica/migrations/2021-03-18-175456_registered-keys/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE registered_keys ( 2 | fingerprint TEXT PRIMARY KEY NOT NULL, 3 | pubkey TEXT NOT NULL, 4 | user TEXT NOT NULL, 5 | pin_policy TEXT NULL, 6 | touch_policy TEXT NULL, 7 | hsm_serial TEXT NULL, 8 | firmware TEXT NULL, 9 | attestation_certificate TEXT NULL, 10 | attestation_intermediate TEXT NULL, 11 | auth_data TEXT NULL, 12 | auth_data_signature TEXT NULL, 13 | aaguid TEXT NULL, 14 | challenge TEXT NULL, 15 | alg INTEGER NULL, 16 | application TEXT NULL 17 | ); 18 | -------------------------------------------------------------------------------- /rustica/migrations/2022-02-23-205236_add_u2f/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE registered_keys; 2 | 3 | CREATE TABLE registered_keys ( 4 | fingerprint TEXT PRIMARY KEY NOT NULL, 5 | user TEXT NOT NULL, 6 | pin_policy TEXT NULL, 7 | touch_policy TEXT NULL, 8 | hsm_serial TEXT NULL, 9 | firmware TEXT NULL, 10 | attestation_certificate TEXT NULL, 11 | attestation_intermediate TEXT NULL 12 | ); -------------------------------------------------------------------------------- /rustica/migrations/2022-02-23-205236_add_u2f/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE registered_keys ADD COLUMN auth_data text; 2 | ALTER TABLE registered_keys ADD COLUMN auth_data_signature text; 3 | ALTER TABLE registered_keys ADD COLUMN aaguid text; 4 | ALTER TABLE registered_keys ADD COLUMN challenge text; 5 | ALTER TABLE registered_keys ADD COLUMN alg integer; 6 | ALTER TABLE registered_keys ADD COLUMN application text; -------------------------------------------------------------------------------- /rustica/migrations/2022-07-17-022706_add_authority/down.sql: -------------------------------------------------------------------------------- 1 | -- Revert registered keys 2 | DROP TABLE registered_keys; 3 | CREATE TABLE registered_keys ( 4 | fingerprint TEXT PRIMARY KEY NOT NULL, 5 | user TEXT NOT NULL, 6 | pin_policy TEXT NULL, 7 | touch_policy TEXT NULL, 8 | hsm_serial TEXT NULL, 9 | firmware TEXT NULL, 10 | attestation_certificate TEXT NULL, 11 | attestation_intermediate TEXT NULL, 12 | auth_data TEXT, 13 | auth_data_signature TEXT, 14 | aaguid TEXT, 15 | challenge TEXT, 16 | alg INTEGER, 17 | application TEXT 18 | ); 19 | 20 | DROP TABLE fingerprint_principal_authorizations; 21 | CREATE TABLE fingerprint_principal_authorizations ( 22 | id BIGINT PRIMARY KEY NOT NULL, 23 | fingerprint TEXT NOT NULL, 24 | principal TEXT NOT NULL 25 | ); 26 | 27 | DROP TABLE fingerprint_extensions; 28 | CREATE TABLE fingerprint_extensions ( 29 | id BIGINT PRIMARY KEY NOT NULL, 30 | fingerprint TEXT NOT NULL, 31 | extension_name TEXT NOT NULL, 32 | extension_value TEXT NULL 33 | ); 34 | 35 | DROP TABLE fingerprint_critical_options; 36 | CREATE TABLE fingerprint_critical_options ( 37 | id BIGINT PRIMARY KEY NOT NULL, 38 | fingerprint TEXT NOT NULL, 39 | critical_option_name TEXT NOT NULL, 40 | critical_option_value TEXT NULL 41 | ); 42 | 43 | DROP TABLE fingerprint_host_authorizations; 44 | CREATE TABLE fingerprint_host_authorizations ( 45 | id BIGINT PRIMARY KEY NOT NULL, 46 | fingerprint TEXT NOT NULL, 47 | hostname TEXT NOT NULL 48 | ); -------------------------------------------------------------------------------- /rustica/migrations/2022-07-17-022706_add_authority/up.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE fingerprint_permissions; 2 | DROP TABLE fingerprint_principal_authorizations; 3 | DROP TABLE fingerprint_extensions; 4 | DROP TABLE fingerprint_critical_options; 5 | DROP TABLE fingerprint_host_authorizations; 6 | 7 | CREATE TABLE fingerprint_principal_authorizations ( 8 | fingerprint TEXT NOT NULL, 9 | principal TEXT NOT NULL, 10 | authority TEXT NOT NULL, 11 | PRIMARY KEY (fingerprint, principal, authority) 12 | ); 13 | 14 | CREATE TABLE fingerprint_host_authorizations ( 15 | fingerprint TEXT NOT NULL, 16 | hostname TEXT NOT NULL, 17 | authority TEXT NOT NULL, 18 | PRIMARY KEY (fingerprint, hostname, authority) 19 | ); 20 | 21 | CREATE TABLE fingerprint_permissions ( 22 | fingerprint TEXT NOT NULL, 23 | host_unrestricted BOOLEAN DEFAULT FALSE NOT NULL, 24 | principal_unrestricted BOOLEAN DEFAULT FALSE NOT NULL, 25 | can_create_host_certs BOOLEAN DEFAULT FALSE NOT NULL, 26 | can_create_user_certs BOOLEAN DEFAULT FALSE NOT NULL, 27 | max_creation_time BIGINT DEFAULT 10 NOT NULL, 28 | authority TEXT NOT NULL, 29 | PRIMARY KEY (fingerprint, authority) 30 | ); 31 | 32 | CREATE TABLE fingerprint_extensions ( 33 | fingerprint TEXT NOT NULL, 34 | extension_name TEXT NOT NULL, 35 | extension_value TEXT NULL, 36 | authority TEXT NOT NULL, 37 | PRIMARY KEY (fingerprint, authority, extension_name) 38 | ); 39 | 40 | CREATE TABLE fingerprint_critical_options ( 41 | fingerprint TEXT NOT NULL, 42 | critical_option_name TEXT NOT NULL, 43 | critical_option_value TEXT NULL, 44 | authority TEXT NOT NULL, 45 | PRIMARY KEY (fingerprint, authority, critical_option_name) 46 | ); -------------------------------------------------------------------------------- /rustica/migrations/2023-04-10-043746_add_x509_by_serial/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE x509_authorizations; -------------------------------------------------------------------------------- /rustica/migrations/2023-04-10-043746_add_x509_by_serial/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE TABLE x509_authorizations ( 3 | user TEXT NOT NULL, 4 | hsm_serial TEXT NOT NULL, 5 | require_touch BOOLEAN NOT NULL, 6 | authority TEXT NOT NULL, 7 | PRIMARY KEY (user, hsm_serial) 8 | ); 9 | -------------------------------------------------------------------------------- /rustica/src/auth/database/models.rs: -------------------------------------------------------------------------------- 1 | use super::schema::registered_keys; 2 | 3 | #[derive(Queryable)] 4 | pub struct Host { 5 | pub hostname: String, 6 | pub fingerprint: String, 7 | } 8 | 9 | #[derive(Queryable)] 10 | pub struct FingerprintPrincipalAuthorization { 11 | pub fingerprint: String, 12 | pub principal: String, 13 | pub authority: String, 14 | } 15 | 16 | #[derive(Queryable)] 17 | pub struct FingerprintHostAuthorization { 18 | pub fingerprint: String, 19 | pub hostname: String, 20 | pub authority: String, 21 | } 22 | 23 | #[derive(Queryable)] 24 | pub struct FingerprintExtension { 25 | pub fingerprint: String, 26 | pub extension_name: String, 27 | pub extension_value: Option, 28 | pub authority: String, 29 | } 30 | 31 | #[derive(Queryable)] 32 | pub struct FingerprintPermission { 33 | pub fingerprint: String, 34 | pub host_unrestricted: bool, 35 | pub principal_unrestricted: bool, 36 | pub can_create_host_certs: bool, 37 | pub can_create_user_certs: bool, 38 | pub max_creation_time: i64, 39 | pub authority: String, 40 | } 41 | 42 | #[derive(Insertable)] 43 | #[diesel(table_name = registered_keys)] 44 | pub struct RegisteredKey { 45 | pub fingerprint: String, 46 | pub pubkey: String, 47 | pub user: String, 48 | pub pin_policy: Option, 49 | pub touch_policy: Option, 50 | pub hsm_serial: Option, 51 | pub firmware: Option, 52 | pub attestation_certificate: Option, 53 | pub attestation_intermediate: Option, 54 | pub auth_data: Option, 55 | pub auth_data_signature: Option, 56 | pub aaguid: Option, 57 | pub challenge: Option, 58 | pub alg: Option, 59 | pub application: Option, 60 | } 61 | 62 | #[derive(Queryable)] 63 | pub struct X509Authorization { 64 | pub user: String, 65 | pub hsm_serial: String, 66 | pub require_touch: bool, 67 | pub authority: String, 68 | } 69 | -------------------------------------------------------------------------------- /rustica/src/auth/database/schema.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | fingerprint_critical_options (fingerprint, critical_option_name, authority) { 3 | fingerprint -> Text, 4 | critical_option_name -> Text, 5 | critical_option_value -> Nullable, 6 | authority -> Text, 7 | } 8 | } 9 | 10 | table! { 11 | fingerprint_extensions (fingerprint, extension_name, authority) { 12 | fingerprint -> Text, 13 | extension_name -> Text, 14 | extension_value -> Nullable, 15 | authority -> Text, 16 | } 17 | } 18 | 19 | table! { 20 | fingerprint_host_authorizations (fingerprint, hostname, authority) { 21 | fingerprint -> Text, 22 | hostname -> Text, 23 | authority -> Text, 24 | } 25 | } 26 | 27 | table! { 28 | fingerprint_permissions (fingerprint, authority) { 29 | fingerprint -> Text, 30 | host_unrestricted -> Bool, 31 | principal_unrestricted -> Bool, 32 | can_create_host_certs -> Bool, 33 | can_create_user_certs -> Bool, 34 | max_creation_time -> BigInt, 35 | authority -> Text, 36 | } 37 | } 38 | 39 | table! { 40 | fingerprint_principal_authorizations (fingerprint, principal, authority) { 41 | fingerprint -> Text, 42 | principal -> Text, 43 | authority -> Text, 44 | } 45 | } 46 | 47 | table! { 48 | hosts (hostname) { 49 | hostname -> Nullable, 50 | fingerprint -> Text, 51 | } 52 | } 53 | 54 | table! { 55 | registered_keys (fingerprint) { 56 | fingerprint -> Text, 57 | pubkey -> Text, 58 | user -> Text, 59 | pin_policy -> Nullable, 60 | touch_policy -> Nullable, 61 | hsm_serial -> Nullable, 62 | firmware -> Nullable, 63 | attestation_certificate -> Nullable, 64 | attestation_intermediate -> Nullable, 65 | auth_data -> Nullable, 66 | auth_data_signature -> Nullable, 67 | aaguid -> Nullable, 68 | challenge -> Nullable, 69 | alg -> Nullable, 70 | application -> Nullable, 71 | } 72 | } 73 | 74 | table! { 75 | x509_authorizations (user, hsm_serial) { 76 | user -> Text, 77 | hsm_serial -> Text, 78 | require_touch -> Bool, 79 | authority -> Text, 80 | } 81 | } 82 | 83 | allow_tables_to_appear_in_same_query!( 84 | fingerprint_critical_options, 85 | fingerprint_extensions, 86 | fingerprint_host_authorizations, 87 | fingerprint_permissions, 88 | fingerprint_principal_authorizations, 89 | hosts, 90 | registered_keys, 91 | x509_authorizations, 92 | ); 93 | -------------------------------------------------------------------------------- /rustica/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::auth::AuthorizationError; 2 | 3 | #[derive(Debug)] 4 | pub enum RusticaServerError { 5 | Success = 0, 6 | TimeExpired = 1, 7 | BadChallenge = 2, 8 | #[allow(dead_code)] 9 | InvalidKey = 3, 10 | #[allow(dead_code)] 11 | UnsupportedKeyType = 4, 12 | BadCertOptions = 5, 13 | NotAuthorized = 6, 14 | BadRequest = 7, 15 | PivClientCertTooBig = 8, 16 | PivIntermediateCertTooBig = 9, 17 | U2fAttestationTooBig = 10, 18 | U2fIntermediateCertTooBig = 11, 19 | Unknown = 9001, 20 | } 21 | 22 | impl From for RusticaServerError { 23 | fn from(e: AuthorizationError) -> RusticaServerError { 24 | match e { 25 | AuthorizationError::CertType => RusticaServerError::BadCertOptions, 26 | AuthorizationError::NotAuthorized => RusticaServerError::NotAuthorized, 27 | _ => RusticaServerError::Unknown, 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rustica/src/key.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::fmt; 3 | 4 | #[derive(Debug)] 5 | pub struct Key { 6 | pub fingerprint: String, 7 | pub attestation: Option 8 | } 9 | 10 | #[derive(Debug)] 11 | pub struct PIVAttestation { 12 | pub pin_policy: PinPolicy, 13 | pub touch_policy: TouchPolicy, 14 | pub serial: u64, 15 | pub firmware: String, 16 | pub certificate: Vec, 17 | pub intermediate: Vec, 18 | } 19 | 20 | #[derive(Debug)] 21 | pub struct U2fAttestation { 22 | pub aaguid: String, 23 | pub firmware: String, 24 | pub auth_data: Vec, 25 | pub auth_data_signature: Vec, 26 | pub intermediate: Vec, 27 | pub challenge: Vec, 28 | pub application: Vec, 29 | pub alg: i32, 30 | } 31 | 32 | #[derive(Debug)] 33 | pub enum KeyAttestation { 34 | Piv(PIVAttestation), 35 | U2f(U2fAttestation), 36 | } 37 | 38 | #[derive(Debug, PartialEq)] 39 | pub enum TouchPolicy { 40 | Never, 41 | Always, 42 | Cached, 43 | } 44 | 45 | #[derive(Debug, PartialEq)] 46 | pub enum PinPolicy { 47 | Never = 1, 48 | Once = 2, 49 | Always = 3, 50 | } 51 | 52 | impl fmt::Display for PinPolicy { 53 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 54 | write!(f, "{:?}", self) 55 | } 56 | } 57 | 58 | impl fmt::Display for TouchPolicy { 59 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 60 | write!(f, "{:?}", self) 61 | } 62 | } 63 | 64 | impl TryFrom for TouchPolicy { 65 | type Error = (); 66 | 67 | fn try_from(v: u8) -> Result { 68 | match v { 69 | 1 => Ok(TouchPolicy::Never), 70 | 2 => Ok(TouchPolicy::Always), 71 | 3 => Ok(TouchPolicy::Cached), 72 | _ => Err(()), 73 | } 74 | } 75 | } 76 | 77 | impl TryFrom for PinPolicy { 78 | type Error = (); 79 | 80 | fn try_from(v: u8) -> Result { 81 | match v { 82 | 1 => Ok(PinPolicy::Never), 83 | 2 => Ok(PinPolicy::Once), 84 | 3 => Ok(PinPolicy::Always), 85 | _ => Err(()), 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /rustica/src/logging/influx.rs: -------------------------------------------------------------------------------- 1 | use super::{Log, LoggingError, RusticaLogger, WrappedLog}; 2 | 3 | use influxdb::InfluxDbWriteable; 4 | use influxdb::{Client, Timestamp}; 5 | 6 | use tokio::runtime::Handle; 7 | 8 | use serde::Deserialize; 9 | 10 | use std::time::{SystemTime, UNIX_EPOCH}; 11 | 12 | #[derive(Deserialize)] 13 | pub struct Config { 14 | address: String, 15 | database: String, 16 | dataset: String, 17 | user: String, 18 | password: String, 19 | } 20 | 21 | pub struct InfluxLogger { 22 | client: Client, 23 | runtime: Handle, 24 | dataset: String, 25 | } 26 | 27 | impl InfluxLogger { 28 | /// Create a new InfluxDB logger from the provided configuration 29 | pub fn new(config: Config, handle: Handle) -> Self { 30 | Self { 31 | client: Client::new(config.address, config.database) 32 | .with_auth(config.user, config.password), 33 | runtime: handle, 34 | dataset: config.dataset, 35 | } 36 | } 37 | } 38 | 39 | impl RusticaLogger for InfluxLogger { 40 | /// Sends a log to the configured InfluxDB database and dataset. This call 41 | /// happens asynchronous which has the benefit of not blocking the logging 42 | /// thread (meaning it will not hold out other loggers like stdout), but 43 | /// has the drawback that we cannot return a proper LoggingError on failure 44 | /// because we cannot wait for the call to complete. 45 | fn send_log(&self, log: &WrappedLog) -> Result<(), LoggingError> { 46 | match &log.log { 47 | Log::CertificateIssued(ci) => { 48 | let start = SystemTime::now(); 49 | let timestamp = start 50 | .duration_since(UNIX_EPOCH) 51 | .expect("Time went backwards"); 52 | 53 | let point_query = Timestamp::Seconds(timestamp.as_secs().into()) 54 | .into_query(&self.dataset) 55 | .add_tag("fingerprint", ci.fingerprint.clone()) 56 | .add_tag("mtls_identities", ci.mtls_identities.join(",")) 57 | .add_tag("serial", ci.serial) 58 | .add_field("principals", ci.principals.join(",")); 59 | 60 | let client = self.client.clone(); 61 | 62 | self.runtime.spawn(async move { 63 | if let Err(e) = client.query(point_query).await { 64 | error!("Could not send log to Influx: {}", e); 65 | } 66 | }); 67 | } 68 | Log::KeyRegistered(_kr) => (), 69 | Log::KeyRegistrationFailure(_krf) => (), 70 | Log::InternalMessage(_im) => (), 71 | Log::Heartbeat(_) => (), 72 | Log::X509CertificateIssued(_) => (), 73 | } 74 | Ok(()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /rustica/src/logging/splunk.rs: -------------------------------------------------------------------------------- 1 | use super::{LoggingError, RusticaLogger, WrappedLog}; 2 | 3 | use reqwest; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use std::time::Duration; 7 | 8 | use tokio::runtime::Handle; 9 | 10 | /// The struct that defines the Splunk specific configuration of the logging 11 | /// service. 12 | #[derive(Deserialize)] 13 | pub struct Config { 14 | pub token: String, 15 | pub url: String, 16 | pub timeout: u8, 17 | } 18 | 19 | /// The Splunk specific logger that is configured from the Splunk 20 | /// `Config` struct. 21 | pub struct SplunkLogger { 22 | /// A tokio runtime to send logs on 23 | runtime: Handle, 24 | /// A reqwest client configured with the Splunk endpoint and authentication 25 | client: reqwest::Client, 26 | /// An API token to send with our logs for authentication 27 | token: String, 28 | /// The endpoint to send the logs to 29 | url: String, 30 | } 31 | 32 | 33 | /// Splunk needs it in the format of the whole log within the event key 34 | /// This uses a lifetime because it only contains a reference to a gauntlet 35 | /// log allowing us to skip a clone into this struct. 36 | #[derive(Clone, Serialize)] 37 | struct SplunkLogWrapper<'a> { 38 | /// Splunk requires this specific structure when sending logs so we have 39 | /// to wrap again unfortunately to get the entire log in the event field 40 | /// of the JSON. 41 | event: &'a WrappedLog 42 | } 43 | 44 | impl SplunkLogger { 45 | /// Implement the new function for the Splunk logger. This converts 46 | /// the configuration struct into a type that can handle sending 47 | /// logs directly to a Splunk HEC endpoint. 48 | pub fn new(config: Config, handle: Handle) -> Self { 49 | // I don't think this can fail with our settings so we do an unwrap 50 | let client = reqwest::Client::builder() 51 | .danger_accept_invalid_certs(true) 52 | .timeout(Duration::from_secs(config.timeout.into())) 53 | .build().unwrap(); 54 | 55 | Self { 56 | runtime: handle, 57 | client, 58 | token: config.token.clone(), 59 | url: config.url.clone(), 60 | } 61 | } 62 | } 63 | 64 | impl RusticaLogger for SplunkLogger { 65 | /// Send a log to Splunk via an HEC endpoint. This function uses a tokio 66 | /// runtime within the SplunkLogger type. This means that sending a log 67 | /// will not block sending logs to other services (like stdout) but it 68 | /// does mean we cannot return a proper LoggingError to the caller since 69 | /// we cannot wait for it to complete. 70 | fn send_log(&self, log: &WrappedLog) -> Result<(), LoggingError> { 71 | let splunk_log = SplunkLogWrapper {event: log}; 72 | 73 | let data = match serde_json::to_string(&splunk_log) { 74 | Ok(json) => json, 75 | Err(e) => return Err(LoggingError::SerializationError(e.to_string())) 76 | }; 77 | 78 | let res = self.client.post(&self.url) 79 | .header("Authorization", format!("Splunk {}", &self.token)) 80 | .header("Content-Type", "application/x-www-form-urlencoded") 81 | .header("Content-Length", data.len()) 82 | .body(data); 83 | 84 | self.runtime.spawn(async move { 85 | match res.send().await { 86 | Ok(_) => (), 87 | Err(e) => error!("Could not log to Splunk: {}", e.to_string()), 88 | }; 89 | }); 90 | 91 | Ok(()) 92 | } 93 | } -------------------------------------------------------------------------------- /rustica/src/logging/stdout.rs: -------------------------------------------------------------------------------- 1 | use super::{Log, LoggingError, RusticaLogger, Severity, WrappedLog}; 2 | 3 | use serde::Deserialize; 4 | 5 | #[derive(Deserialize)] 6 | pub struct Config {} 7 | 8 | pub struct StdoutLogger {} 9 | 10 | impl StdoutLogger { 11 | pub fn new(_config: Config) -> Self { 12 | Self {} 13 | } 14 | } 15 | 16 | impl RusticaLogger for StdoutLogger { 17 | fn send_log(&self, log: &WrappedLog) -> Result<(), LoggingError> { 18 | match &log.log { 19 | Log::CertificateIssued(ci) => { 20 | info!( 21 | "[{}] Certificate issued for: [{}] Authority: [{}] Identified by: [{}] Principals granted: [{}] Extensions: [{:?}] CriticalOptions: [{:?}] Valid After: [{}] Valid Before: [{}] Serial Number: [{}]", 22 | ci.certificate_type, 23 | ci.fingerprint, 24 | ci.authority, 25 | ci.mtls_identities.join(", "), 26 | ci.principals.join(", "), 27 | ci.extensions, 28 | ci.critical_options, 29 | ci.valid_after, 30 | ci.valid_before, 31 | ci.serial, 32 | ) 33 | } 34 | Log::KeyRegistered(kr) => info!("Key registered: [{}] Identified by: [{}]", kr.fingerprint, kr.mtls_identities.join(", ")), 35 | Log::KeyRegistrationFailure(krf) => info!("Failed to register key: [{}] Identified by: [{}]", krf.key_info.fingerprint, krf.key_info.mtls_identities.join(", ")), 36 | Log::InternalMessage(im) => match im.severity { 37 | Severity::Error => error!("{}", im.message), 38 | Severity::Warning => warn!("{}", im.message), 39 | Severity::Info => info!("{}", im.message), 40 | }, 41 | Log::Heartbeat(_) => (), 42 | Log::X509CertificateIssued(x509) => info!( 43 | "X509 Certificate issued. Authority: [{}] Identified by: [{}] Extensions: [{:?}] Valid After: [{}] Valid Before: [{}] Serial: [{}]", 44 | x509.authority, 45 | x509.mtls_identities.join(", "), 46 | x509.extensions, 47 | x509.valid_after, 48 | x509.valid_before, 49 | x509.serial, 50 | ) 51 | } 52 | Ok(()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rustica/src/logging/webhook.rs: -------------------------------------------------------------------------------- 1 | use super::{LoggingError, RusticaLogger, WrappedLog}; 2 | 3 | use reqwest; 4 | 5 | use serde::Deserialize; 6 | use std::time::Duration; 7 | 8 | use tokio::runtime::Handle; 9 | 10 | /// The struct that defines the Webhook specific configuration of the logging 11 | /// service. 12 | #[derive(Deserialize)] 13 | pub struct Config { 14 | pub auth_header: Option, 15 | pub url: String, 16 | pub timeout: u8, 17 | } 18 | 19 | /// The specific logger that is configured from the `Config` struct. 20 | pub struct WebhookLogger { 21 | /// A tokio runtime to send logs on 22 | runtime: Handle, 23 | /// A reqwest client configured with the Splunk endpoint and authentication 24 | client: reqwest::Client, 25 | /// The configuration struct 26 | config: Config, 27 | } 28 | 29 | 30 | impl WebhookLogger { 31 | /// Implement the new function for the Splunk logger. This converts 32 | /// the configuration struct into a type that can handle sending 33 | /// logs directly to a Splunk HEC endpoint. 34 | pub fn new(config: Config, handle: Handle) -> Self { 35 | // I don't think this can fail with our settings so we do an unwrap 36 | let client = reqwest::Client::builder() 37 | .timeout(Duration::from_secs(config.timeout.into())) 38 | .build().unwrap(); 39 | 40 | Self { 41 | runtime: handle, 42 | client, 43 | config, 44 | } 45 | } 46 | } 47 | 48 | impl RusticaLogger for WebhookLogger { 49 | /// Send a log to the webhook. Sending a log 50 | /// will not block sending logs to other services (like stdout) but it 51 | /// does mean we cannot return a proper LoggingError to the caller since 52 | /// we cannot wait for it to complete. 53 | fn send_log(&self, log: &WrappedLog) -> Result<(), LoggingError> { 54 | let data = match serde_json::to_string(&log) { 55 | Ok(json) => json, 56 | Err(e) => return Err(LoggingError::SerializationError(e.to_string())) 57 | }; 58 | 59 | let res = self.client.post(&self.config.url) 60 | .header("Content-Type", "application/x-www-form-urlencoded") 61 | .header("Content-Length", data.len()) 62 | .body(data); 63 | 64 | let res = if let Some(auth) = &self.config.auth_header { 65 | res.header("Authorization", auth) 66 | } else { 67 | res 68 | }; 69 | 70 | self.runtime.spawn(async move { 71 | match res.send().await { 72 | Ok(_) => (), 73 | Err(e) => error!("Could not log to webhook: {}", e.to_string()), 74 | }; 75 | }); 76 | 77 | Ok(()) 78 | } 79 | } -------------------------------------------------------------------------------- /rustica/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | #[cfg(feature = "local-db")] 5 | #[macro_use] 6 | extern crate diesel; 7 | 8 | mod auth; 9 | mod config; 10 | mod error; 11 | mod key; 12 | mod logging; 13 | mod server; 14 | mod signing; 15 | mod verification; 16 | 17 | use rustica::rustica_server::RusticaServer as GRPCRusticaServer; 18 | use tonic::transport::{Certificate as TonicCertificate, Identity, Server, ServerTlsConfig}; 19 | 20 | use std::thread; 21 | 22 | use config::ConfigurationError; 23 | 24 | pub mod rustica { 25 | tonic::include_proto!("rustica"); 26 | } 27 | 28 | #[tokio::main] 29 | async fn main() -> Result<(), Box> { 30 | env_logger::init(); 31 | let settings = match config::configure().await { 32 | Ok(settings) => settings, 33 | Err(ConfigurationError::ValidateOnly) => { 34 | println!("Configuration successfully validated"); 35 | return Ok(()); 36 | } 37 | Err(e) => return Err(e)?, 38 | }; 39 | 40 | let identity = Identity::from_pem(settings.server_cert, settings.server_key); 41 | let client_ca_cert = TonicCertificate::from_pem(settings.client_ca_cert); 42 | 43 | println!("Starting Rustica on: {}", settings.address); 44 | println!("{}", settings.server.signer); 45 | println!("{}", settings.server.authorizer.info()); 46 | 47 | let logging_configuration = settings.logging_configuration; 48 | let log_receiver = settings.log_receiver; 49 | 50 | thread::spawn(|| { 51 | logging::start_logging_thread(logging_configuration, log_receiver); 52 | }); 53 | 54 | Server::builder() 55 | .tls_config( 56 | ServerTlsConfig::new() 57 | .identity(identity) 58 | .client_ca_root(client_ca_cert), 59 | )? 60 | .max_frame_size(1024 * 1024 * 4) // 4 MiB 61 | .add_service(GRPCRusticaServer::new(settings.server)) 62 | .serve(settings.address) 63 | .await?; 64 | 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /rustica/src/signing/file.rs: -------------------------------------------------------------------------------- 1 | use rcgen::{Certificate as X509Certificate, CertificateParams, DnType, IsCa}; 2 | use serde::Deserialize; 3 | /// The file signer uses private keys stored inside the Rustica 4 | /// configuration to sign certificate requests. This is currently the only 5 | /// signer which is present regardless of the features enabled at build 6 | /// time. It supports Ecdsa256, Ecdsa384, and Ed25519. 7 | use sshcerts::{ssh::CertType, Certificate, PrivateKey, PublicKey}; 8 | 9 | use super::{Signer, SignerConfig, SigningError}; 10 | 11 | use async_trait::async_trait; 12 | 13 | #[derive(Deserialize)] 14 | pub struct Config { 15 | /// The private key used to sign user certificates 16 | #[serde(default)] 17 | #[serde(deserialize_with = "option_parse_private_key")] 18 | user_key: Option, 19 | /// The private key used to sign host certificates 20 | #[serde(default)] 21 | #[serde(deserialize_with = "option_parse_private_key")] 22 | host_key: Option, 23 | /// X509 base64 encoded private key that will be used to issue certificates 24 | /// from the request_x509 API 25 | x509_private_key: Option, 26 | /// X509 private key type to use, either ECDSA 256 or ECDSA 384. 27 | /// This should be one of p256 or p384 28 | x509_private_key_algorithm: Option, 29 | /// X509 base64 encoded private key that will be used to issue client 30 | /// certificates 31 | client_certificate_authority_private_key: Option, 32 | /// X509 private key type to use, either ECDSA 256 or ECDSA 384. 33 | /// This should be one of p256 or p384 34 | client_certificate_authority_private_key_algorithm: Option, 35 | /// The common name to use in the client certificate authority 36 | client_certificate_authority_common_name: Option, 37 | } 38 | 39 | pub struct SshKeys { 40 | /// The private key used to sign user certificates 41 | user: PrivateKey, 42 | /// The private key used to sign host certificates 43 | host: PrivateKey, 44 | } 45 | 46 | pub struct FileSigner { 47 | /// The SSH keys used to sign SSH Certificate requests 48 | ssh_keys: Option, 49 | /// The public portion of the key that will be used to sign X509 50 | /// certificates 51 | x509_certificate: Option, 52 | /// The public portion of the key that will be used to sign client 53 | /// certificates 54 | client_certificate_authority: Option, 55 | } 56 | 57 | fn rcgen_certificate_from_private_key( 58 | common_name: &str, 59 | private_key: &str, 60 | alg: &str, 61 | ) -> Result { 62 | let mut ca_params = CertificateParams::new(vec![]); 63 | ca_params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Unconstrained); 64 | ca_params 65 | .distinguished_name 66 | .push(DnType::CommonName, common_name); 67 | 68 | let key_bytes = base64::decode(&private_key).map_err(|_| SigningError::ParsingError)?; 69 | 70 | let kp = rcgen::KeyPair::from_der(&key_bytes).map_err(|_| SigningError::ParsingError)?; 71 | ca_params.alg = match alg { 72 | "p256" => &rcgen::PKCS_ECDSA_P256_SHA256, 73 | "p384" => &rcgen::PKCS_ECDSA_P384_SHA384, 74 | _ => return Err(SigningError::ParsingError), 75 | }; 76 | 77 | ca_params.key_pair = Some(kp); 78 | X509Certificate::from_params(ca_params).map_err(|_| SigningError::SigningFailure) 79 | } 80 | 81 | #[async_trait] 82 | impl Signer for FileSigner { 83 | async fn sign(&self, cert: Certificate) -> Result { 84 | let keys = self.ssh_keys.as_ref().ok_or(SigningError::SignerDoesNotHaveSSHKeys)?; 85 | let final_cert = match cert.cert_type { 86 | CertType::User => cert.sign(&keys.user), 87 | CertType::Host => cert.sign(&keys.host), 88 | }; 89 | 90 | final_cert.map_err(|_| SigningError::SigningFailure) 91 | } 92 | 93 | fn get_signer_public_key(&self, cert_type: CertType) -> Option { 94 | let keys = self.ssh_keys.as_ref()?; 95 | 96 | match cert_type { 97 | CertType::User => Some(keys.user.pubkey.clone()), 98 | CertType::Host => Some(keys.host.pubkey.clone()), 99 | } 100 | } 101 | 102 | fn get_attested_x509_certificate_authority(&self) -> Option<&rcgen::Certificate> { 103 | self.x509_certificate.as_ref() 104 | } 105 | 106 | fn get_client_certificate_authority(&self) -> Option<&rcgen::Certificate> { 107 | self.client_certificate_authority.as_ref() 108 | } 109 | } 110 | 111 | #[async_trait] 112 | impl SignerConfig for Config { 113 | async fn into_signer(self) -> Result, SigningError> { 114 | let x509_certificate = match (&self.x509_private_key, &self.x509_private_key_algorithm) { 115 | (Some(pk), Some(pka)) => Some(rcgen_certificate_from_private_key( 116 | "Rustica", 117 | pk, 118 | pka 119 | )?), 120 | _ => None 121 | }; 122 | 123 | let client_certificate_authority = match ( 124 | self.client_certificate_authority_private_key, 125 | self.client_certificate_authority_private_key_algorithm, 126 | self.client_certificate_authority_common_name, 127 | ) { 128 | (Some(ccapk), Some(ccapka), Some(cn)) => { 129 | Some(rcgen_certificate_from_private_key(&cn, &ccapk, &ccapka)?) 130 | } 131 | _ => None, 132 | }; 133 | 134 | let ssh_keys = match (self.user_key, self.host_key) { 135 | (Some(user), Some(host)) => Some(SshKeys{user, host}), 136 | (None, None) => None, 137 | _ => return Err(SigningError::SignerDoesNotAllRequiredSSHKeys), 138 | }; 139 | 140 | Ok(Box::new(FileSigner { 141 | ssh_keys, 142 | x509_certificate, 143 | client_certificate_authority, 144 | })) 145 | } 146 | } 147 | 148 | fn option_parse_private_key<'de, D>(deserializer: D) -> Result, D::Error> 149 | where 150 | D: serde::Deserializer<'de>, 151 | { 152 | let key = match String::deserialize(deserializer) { 153 | Ok(s) => s, 154 | _ => return Ok(None), 155 | }; 156 | 157 | match PrivateKey::from_string(&key) { 158 | Ok(key) => Ok(Some(key)), 159 | Err(e) => Err(serde::de::Error::custom(e.to_string())), 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /rustica/src/verification.rs: -------------------------------------------------------------------------------- 1 | use crate::key::{Key, KeyAttestation, PinPolicy, TouchPolicy}; 2 | use crate::key::{PIVAttestation, U2fAttestation}; 3 | 4 | use crate::error::RusticaServerError; 5 | 6 | use ring::digest::{self}; 7 | use sshcerts::{ 8 | fido::verification::verify_auth_data, yubikey::verification::verify_certificate_chain, 9 | }; 10 | use std::convert::TryFrom; 11 | 12 | // For Yubikey 5 Nano: 13 | // - PIV intermediate cert size is approx 800 bytes 14 | // - PIV client cert is approx 700 bytes 15 | // - U2F intermediate cert is approx 800 bytes 16 | // - U2F attestation statement is approx 256 bytes 17 | const CERT_MAX_SIZE: usize = 1024 * 2; // 2 KiB 18 | 19 | /// Verify a provided yubikey attestation certification and intermediate 20 | /// certificate are valid against the Yubico attestation Root CA. 21 | pub fn verify_piv_certificate_chain( 22 | client: &[u8], 23 | intermediate: &[u8], 24 | ) -> Result { 25 | // Restrict the max size of certificates 26 | // For Yubikey 5 Nano, actual intermediate cert size is approx 800 bytes 27 | if intermediate.len() > CERT_MAX_SIZE { 28 | return Err(RusticaServerError::PivIntermediateCertTooBig); 29 | } 30 | // For Yubikey 5 Nano, actual client cert size is approx 700 bytes 31 | if client.len() > CERT_MAX_SIZE { 32 | return Err(RusticaServerError::PivClientCertTooBig); 33 | } 34 | 35 | // Extract the certificate public key and convert to an sshcerts PublicKey 36 | let validated_piv_data = verify_certificate_chain(client, intermediate, None) 37 | .map_err(|_| RusticaServerError::InvalidKey)?; 38 | let pin_policy = PinPolicy::try_from(validated_piv_data.pin_policy) 39 | .map_err(|_| RusticaServerError::InvalidKey)?; 40 | let touch_policy = TouchPolicy::try_from(validated_piv_data.touch_policy) 41 | .map_err(|_| RusticaServerError::InvalidKey)?; 42 | 43 | Ok(Key { 44 | fingerprint: validated_piv_data.public_key.fingerprint().hash, 45 | attestation: Some(KeyAttestation::Piv(PIVAttestation { 46 | firmware: validated_piv_data.firmware, 47 | serial: validated_piv_data.serial, 48 | pin_policy, 49 | touch_policy, 50 | certificate: client.to_vec(), 51 | intermediate: intermediate.to_vec(), 52 | })), 53 | }) 54 | } 55 | 56 | /// Verify a provided U2F attestation, signature, and certificate are valid 57 | /// against the Yubico U2F Root CA. 58 | pub fn verify_u2f_certificate_chain( 59 | auth_data: &[u8], 60 | auth_data_signature: &[u8], 61 | intermediate: &[u8], 62 | alg: i32, 63 | challenge: &[u8], 64 | application: &[u8], 65 | u2f_challenge_hashed: bool, 66 | ) -> Result { 67 | // Restrict the max size for the attestation data and intermediate certificate 68 | // For Yubikey 5 Nano, actual intermediate cert size is approx 800 bytes 69 | if intermediate.len() > CERT_MAX_SIZE { 70 | return Err(RusticaServerError::U2fIntermediateCertTooBig); 71 | } 72 | // For Yubikey 5 Nano, actual auth_data size is approx 256 bytes 73 | if auth_data.len() > CERT_MAX_SIZE { 74 | return Err(RusticaServerError::U2fAttestationTooBig); 75 | } 76 | 77 | // Take all the provided data and validate it up to the Yubico U2F Root CA 78 | 79 | let challenge = if u2f_challenge_hashed { 80 | challenge.to_vec() 81 | } else { 82 | digest::digest(&digest::SHA256, challenge).as_ref().to_vec() 83 | }; 84 | // Earlier versions of RusticaAgent did not send the u2f_challenge hashed so 85 | // 86 | let validated_u2f_data = verify_auth_data( 87 | auth_data, 88 | auth_data_signature, 89 | &challenge, 90 | alg, 91 | intermediate, 92 | None, 93 | ) 94 | .map_err(|_| RusticaServerError::InvalidKey)?; 95 | let parsed_application = 96 | String::from_utf8(application.to_vec()).map_err(|_| RusticaServerError::InvalidKey)?; 97 | let ssh_public_key = validated_u2f_data 98 | .auth_data 99 | .ssh_public_key(&parsed_application) 100 | .map_err(|_| RusticaServerError::InvalidKey)?; 101 | 102 | Ok(Key { 103 | fingerprint: ssh_public_key.fingerprint().hash, 104 | attestation: Some(KeyAttestation::U2f(U2fAttestation { 105 | aaguid: hex::encode(validated_u2f_data.auth_data.aaguid), 106 | firmware: validated_u2f_data 107 | .firmware 108 | .unwrap_or_else(|| "Unknown".to_string()), 109 | auth_data: auth_data.to_vec(), 110 | auth_data_signature: auth_data_signature.to_vec(), 111 | intermediate: intermediate.to_vec(), 112 | challenge: challenge.to_vec(), 113 | alg, 114 | application: application.to_vec(), 115 | })), 116 | }) 117 | } 118 | -------------------------------------------------------------------------------- /tests/ssh_server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | USER root 3 | RUN apt update && apt upgrade -y && apt install -y openssh-server 4 | 5 | # SSH Configuration 6 | COPY sshd_config /etc/ssh/sshd_config 7 | COPY user-ca.pub /etc/ssh/user-ca.pub 8 | RUN chmod 600 /etc/ssh/user-ca.pub 9 | RUN service ssh start 10 | 11 | # User Configuration 12 | RUN useradd -m -d /home/testuser -s /bin/bash -g root -G sudo -u 1001 testuser 13 | USER 1001 14 | RUN mkdir /home/testuser/.ssh 15 | COPY authorized_keys /home/testuser/.ssh/authorized_keys 16 | 17 | USER root 18 | 19 | EXPOSE 22 20 | CMD ["/usr/sbin/sshd", "-D"] 21 | -------------------------------------------------------------------------------- /tests/ssh_server/authorized_keys: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBA0ImLwIBPw8g/0jbZXmFP6d6XeNAlJyzgJeJw+Btjo7jfwwJKEXfJb/hgDA5/I6E6wBQ1KZ7LMUjpAsNQvv6Xc= 2 | ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBNk5GZ6aXu1VBk2mbffXCRVvOlPyzsSeZRTILMdu6ligA7b3gELbcPDX8tckrBsjITCRlLI1LYxKNiqQ35QY9CjY+QfRsXGCw44ANfz6LQw2UX987gZmhsKGpN9vvBKfnA== 3 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICKlW2+4j8k/GLI2RFYSOIET8m1CLyIaPPixJ4uYEzP/ -------------------------------------------------------------------------------- /tests/ssh_server/sshd_config: -------------------------------------------------------------------------------- 1 | # $OpenBSD: sshd_config,v 1.103 2018/04/09 20:41:22 tj Exp $ 2 | 3 | # This is the sshd server system-wide configuration file. See 4 | # sshd_config(5) for more information. 5 | 6 | # This sshd was compiled with PATH=/usr/bin:/bin:/usr/sbin:/sbin 7 | 8 | # The strategy used for options in the default sshd_config shipped with 9 | # OpenSSH is to specify options with their default value where 10 | # possible, but leave them commented. Uncommented options override the 11 | # default value. 12 | 13 | #Include /etc/ssh/sshd_config.d/*.conf 14 | 15 | TrustedUserCAKeys /etc/ssh/user-ca.pub 16 | 17 | #Port 22 18 | #AddressFamily any 19 | #ListenAddress 0.0.0.0 20 | #ListenAddress :: 21 | 22 | #HostKey /etc/ssh/ssh_host_rsa_key 23 | #HostKey /etc/ssh/ssh_host_ecdsa_key 24 | #HostKey /etc/ssh/ssh_host_ed25519_key 25 | 26 | # Ciphers and keying 27 | #RekeyLimit default none 28 | 29 | # Logging 30 | #SyslogFacility AUTH 31 | #LogLevel INFO 32 | 33 | # Authentication: 34 | 35 | #LoginGraceTime 2m 36 | #PermitRootLogin prohibit-password 37 | #StrictModes yes 38 | #MaxAuthTries 6 39 | #MaxSessions 10 40 | 41 | PubkeyAuthentication yes 42 | 43 | # Expect .ssh/authorized_keys2 to be disregarded by default in future. 44 | #AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2 45 | 46 | #AuthorizedPrincipalsFile none 47 | 48 | #AuthorizedKeysCommand none 49 | #AuthorizedKeysCommandUser nobody 50 | 51 | # For this to work you will also need host keys in /etc/ssh/ssh_known_hosts 52 | #HostbasedAuthentication no 53 | # Change to yes if you don't trust ~/.ssh/known_hosts for 54 | # HostbasedAuthentication 55 | #IgnoreUserKnownHosts no 56 | # Don't read the user's ~/.rhosts and ~/.shosts files 57 | #IgnoreRhosts yes 58 | 59 | # To disable tunneled clear text passwords, change to no here! 60 | PasswordAuthentication no 61 | #PermitEmptyPasswords no 62 | 63 | # Change to yes to enable challenge-response passwords (beware issues with 64 | # some PAM modules and threads) 65 | ChallengeResponseAuthentication no 66 | 67 | # Kerberos options 68 | #KerberosAuthentication no 69 | #KerberosOrLocalPasswd yes 70 | #KerberosTicketCleanup yes 71 | #KerberosGetAFSToken no 72 | 73 | # GSSAPI options 74 | #GSSAPIAuthentication no 75 | #GSSAPICleanupCredentials yes 76 | #GSSAPIStrictAcceptorCheck yes 77 | #GSSAPIKeyExchange no 78 | 79 | # Set this to 'yes' to enable PAM authentication, account processing, 80 | # and session processing. If this is enabled, PAM authentication will 81 | # be allowed through the ChallengeResponseAuthentication and 82 | # PasswordAuthentication. Depending on your PAM configuration, 83 | # PAM authentication via ChallengeResponseAuthentication may bypass 84 | # the setting of "PermitRootLogin without-password". 85 | # If you just want the PAM account and session checks to run without 86 | # PAM authentication, then enable this but set PasswordAuthentication 87 | # and ChallengeResponseAuthentication to 'no'. 88 | UsePAM yes 89 | 90 | #AllowAgentForwarding yes 91 | #AllowTcpForwarding yes 92 | #GatewayPorts no 93 | X11Forwarding yes 94 | #X11DisplayOffset 10 95 | #X11UseLocalhost yes 96 | #PermitTTY yes 97 | PrintMotd no 98 | #PrintLastLog yes 99 | #TCPKeepAlive yes 100 | #PermitUserEnvironment no 101 | #Compression delayed 102 | #ClientAliveInterval 0 103 | #ClientAliveCountMax 3 104 | #UseDNS no 105 | #PidFile /var/run/sshd.pid 106 | #MaxStartups 10:30:100 107 | #PermitTunnel no 108 | #ChrootDirectory none 109 | #VersionAddendum none 110 | 111 | # no default banner path 112 | #Banner none 113 | 114 | # Allow client to pass locale environment variables 115 | AcceptEnv LANG LC_* 116 | 117 | # override default of no subsystems 118 | Subsystem sftp /usr/lib/openssh/sftp-server 119 | 120 | # Example of overriding settings on a per-user basis 121 | #Match User anoncvs 122 | # X11Forwarding no 123 | # AllowTcpForwarding no 124 | # PermitTTY no 125 | # ForceCommand cvs server 126 | -------------------------------------------------------------------------------- /tests/ssh_server/user-ca.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOGrtTSBxbrqYk1amwv/gAM1eg5Qwsddhszw8gduo9P6 obelisk@Mitchells-MBP.localdomain -------------------------------------------------------------------------------- /tests/test_configs/rustica_local_file.toml: -------------------------------------------------------------------------------- 1 | # This is the listen address that will be used for the Rustica service 2 | listen_address = "0.0.0.0:50052" 3 | 4 | # This setting controls if the agent has to prove that it 5 | # controls the private key to Rustica. Setting this to true means a user needs 6 | # to generate two signatures (one to Rustica, and one to the host). The 7 | # advantage of using this, is a compromised host cannot get certificates 8 | # from the server without physical interaction. 9 | # 10 | # A client will always need to sign the challenge from the host they 11 | # are attempting to connect to however so a physical tap will always 12 | # be required. 13 | require_rustica_proof = false 14 | 15 | # This setting controls if Rustica will allow users to register keys that 16 | # cannot have their providence validated. If set to true, if a registration 17 | # either does not provide an attestation or the attestation fails, the key 18 | # will be rejected and the API call will fail. In the case of using an 19 | # external authorizer, a call will not be made to inform it of this event. 20 | require_attestation_chain = true 21 | 22 | 23 | # The certificate presented to connecting clients 24 | server_cert = ''' 25 | -----BEGIN CERTIFICATE----- 26 | MIIBvTCCAWSgAwIBAgIUac6/skXLRQCSfqjAd0REJJxOGvwwCgYIKoZIzj0EAwIw 27 | GzEZMBcGA1UEAwwQRW50ZXJwcmlzZVJvb3RDQTAeFw0yNDA2MDYwNDA4MThaFw0y 28 | NjA5MDkwNDA4MThaMDExEDAOBgNVBAMMB3J1c3RpY2ExEDAOBgNVBAoMB1J1c3Rp 29 | Y2ExCzAJBgNVBAYTAkNBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqzdtAcXS 30 | 9j3ECPWlucXR0yma0vQUU8PIioV3g7LthqtMTLwZJmtqDlhJE6PZUPdtSALeA6Xt 31 | GxwpOv8sEA2zDaNwMG4wHwYDVR0jBBgwFoAUOhiiXYkz9/H/i5F87/PRfqg/6E4w 32 | CQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MB0G 33 | A1UdDgQWBBReU9iTPgqopxzmL9s3DLM6HKqb9DAKBggqhkjOPQQDAgNHADBEAiAy 34 | oOcGRjuYhrn89g2PxntRYD5mnBYqgiAmxIy04GpcjgIgQNbAu0KO7vIB3FIicjtJ 35 | ALZO9s3gY1HbIz18rVHKBNk= 36 | -----END CERTIFICATE----- 37 | ''' 38 | 39 | # The key for the certificate presented to clients 40 | server_key = ''' 41 | -----BEGIN PRIVATE KEY----- 42 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgQbGgAy6FMW/NcVeS 43 | zQCaBCQgK1QA+Xk/6JdJhgoYqQqhRANCAASrN20BxdL2PcQI9aW5xdHTKZrS9BRT 44 | w8iKhXeDsu2Gq0xMvBkma2oOWEkTo9lQ921IAt4Dpe0bHCk6/ywQDbMN 45 | -----END PRIVATE KEY----- 46 | ''' 47 | 48 | # Configuration for authenticating connecting clients as well as defining 49 | # automatic renewal settings. 50 | [client_authority] 51 | # This must be one of the signing authorities defined in the signing structure 52 | authority = "example_test_environment" 53 | # When we issue a new access certificate, how long is it valid for. 54 | validity_length = 181440000 # Three weeks * 100 55 | 56 | # I think the easiest way to explain this is with an example. 57 | # 58 | # If a certificate is issued for three months, setting this to one week means 59 | # in the week before it expires, when they request a new SSH certificate, the 60 | # server will generate a new mTLS access certificate and send that back with 61 | # the request. 62 | # 63 | # This value should always be less than the validity length 64 | expiration_renewal_period = 181439980 #60480000 # One Week * 100 65 | 66 | [signing] 67 | default_authority = "example_test_environment" 68 | 69 | # Rustica has many ways it can sign SSH certificates which are sent to 70 | # clients. This method uses private keys embedded in the configuration 71 | # file. This will mean the hosts which you want to login to via Rustica 72 | # must respect the public portion of the user key variable below. 73 | [signing.authority_configurations.example_test_environment] 74 | kind = "File" 75 | user_key = ''' 76 | -----BEGIN OPENSSH PRIVATE KEY----- 77 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 78 | QyNTUxOQAAACDhq7U0gcW66mJNWpsL/4ADNXoOUMLHXYbM8PIHbqPT+gAAAKhxqRWZcakV 79 | mQAAAAtzc2gtZWQyNTUxOQAAACDhq7U0gcW66mJNWpsL/4ADNXoOUMLHXYbM8PIHbqPT+g 80 | AAAEA8yRG/XWtjlY007gj8tNflVX9fnHPDcQYH2HTImTKvPeGrtTSBxbrqYk1amwv/gAM1 81 | eg5Qwsddhszw8gduo9P6AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg 82 | ECAwQ= 83 | -----END OPENSSH PRIVATE KEY----- 84 | ''' 85 | 86 | host_key = ''' 87 | -----BEGIN OPENSSH PRIVATE KEY----- 88 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 89 | QyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnewAAAKhQSP5+UEj+ 90 | fgAAAAtzc2gtZWQyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnew 91 | AAAEAevZOed5UnsVdAASUn+sJ+dUfUnG1kQ1wRH9L758mSCxcC2QuSbJKphPaKx0fRxE52 92 | sDNPZwaD2kv14ulnBCd7AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg 93 | ECAwQ= 94 | -----END OPENSSH PRIVATE KEY----- 95 | ''' 96 | 97 | x509_private_key = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDOLp3ZkQZasW1BKZ+fG3ODQgNThvI7pV38DOEFCz6c+gr8whSiV6EHWT04VrddShehZANiAARKbU0hcFy5+9qqHxGx/FBQb2dh6u+pAYh4ASh7skBkPv5DK/46FH6pvyPp6Gfkp8gagcFsr9nAKbwjkVTtBopuhh45KUM5k4VqIqaNox7g+XCrgG29oVqA5WZpW8DFH2c=" 98 | x509_private_key_alg = "p384" 99 | 100 | client_certificate_authority_private_key = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgFEwFOjsK54VOGLVajOMpV6PXEbOHKS8EXIMxRwmLQ/qhRANCAAQ+F90NcFu0EucoggNcbOGI4KP70/Mdb9hMxbd2NYx0DAeEvFiIjP2CI8QV6JgNW32zBKibV2iMtcwEyjMG7bR8" 101 | client_certificate_authority_private_key_algorithm = "p256" 102 | client_certificate_authority_common_name = "RusticaAccess" 103 | 104 | [logging."stdout"] 105 | 106 | [authorization."database"] 107 | path = "examples/example.db" 108 | 109 | [allowed_signers] 110 | cache_validity_length.secs = 900 111 | cache_validity_length.nanos = 0 112 | lru_rate_limiter_size = 16 113 | rate_limit_cooldown.secs = 15 114 | rate_limit_cooldown.nanos = 0 115 | -------------------------------------------------------------------------------- /tests/test_configs/rustica_local_file_alt.toml: -------------------------------------------------------------------------------- 1 | # This is the listen address that will be used for the Rustica service 2 | listen_address = "0.0.0.0:50052" 3 | 4 | # This setting controls if the agent has to prove that it 5 | # controls the private key to Rustica. Setting this to true means a user needs 6 | # to generate two signatures (one to Rustica, and one to the host). The 7 | # advantage of using this, is a compromised host cannot get certificates 8 | # from the server without physical interaction. 9 | # 10 | # A client will always need to sign the challenge from the host they 11 | # are attempting to connect to however so a physical tap will always 12 | # be required. 13 | require_rustica_proof = false 14 | 15 | # This setting controls if Rustica will allow users to register keys that 16 | # cannot have their providence validated. If set to true, if a registration 17 | # either does not provide an attestation or the attestation fails, the key 18 | # will be rejected and the API call will fail. In the case of using an 19 | # external authorizer, a call will not be made to inform it of this event. 20 | require_attestation_chain = true 21 | 22 | 23 | # The certificate presented to connecting clients 24 | server_cert = ''' 25 | -----BEGIN CERTIFICATE----- 26 | MIIBvTCCAWSgAwIBAgIUac6/skXLRQCSfqjAd0REJJxOGvwwCgYIKoZIzj0EAwIw 27 | GzEZMBcGA1UEAwwQRW50ZXJwcmlzZVJvb3RDQTAeFw0yNDA2MDYwNDA4MThaFw0y 28 | NjA5MDkwNDA4MThaMDExEDAOBgNVBAMMB3J1c3RpY2ExEDAOBgNVBAoMB1J1c3Rp 29 | Y2ExCzAJBgNVBAYTAkNBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqzdtAcXS 30 | 9j3ECPWlucXR0yma0vQUU8PIioV3g7LthqtMTLwZJmtqDlhJE6PZUPdtSALeA6Xt 31 | GxwpOv8sEA2zDaNwMG4wHwYDVR0jBBgwFoAUOhiiXYkz9/H/i5F87/PRfqg/6E4w 32 | CQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MB0G 33 | A1UdDgQWBBReU9iTPgqopxzmL9s3DLM6HKqb9DAKBggqhkjOPQQDAgNHADBEAiAy 34 | oOcGRjuYhrn89g2PxntRYD5mnBYqgiAmxIy04GpcjgIgQNbAu0KO7vIB3FIicjtJ 35 | ALZO9s3gY1HbIz18rVHKBNk= 36 | -----END CERTIFICATE----- 37 | ''' 38 | 39 | # The key for the certificate presented to clients 40 | server_key = ''' 41 | -----BEGIN PRIVATE KEY----- 42 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgQbGgAy6FMW/NcVeS 43 | zQCaBCQgK1QA+Xk/6JdJhgoYqQqhRANCAASrN20BxdL2PcQI9aW5xdHTKZrS9BRT 44 | w8iKhXeDsu2Gq0xMvBkma2oOWEkTo9lQ921IAt4Dpe0bHCk6/ywQDbMN 45 | -----END PRIVATE KEY----- 46 | ''' 47 | 48 | # Configuration for authenticating connecting clients as well as defining 49 | # automatic renewal settings. 50 | [client_authority] 51 | # This must be one of the signing authorities defined in the signing structure 52 | authority = "example_test_environment" 53 | # When we issue a new access certificate, how long is it valid for. 54 | validity_length = 181440000 # Three weeks * 100 55 | 56 | # I think the easiest way to explain this is with an example. 57 | # 58 | # If a certificate is issued for three months, setting this to one week means 59 | # in the week before it expires, when they request a new SSH certificate, the 60 | # server will generate a new mTLS access certificate and send that back with 61 | # the request. 62 | # 63 | # This value should always be less than the validity length 64 | expiration_renewal_period = 181439980 #60480000 # One Week * 100 65 | 66 | [signing] 67 | default_authority = "example_test_environment" 68 | 69 | # Rustica has many ways it can sign SSH certificates which are sent to 70 | # clients. This method uses private keys embedded in the configuration 71 | # file. This will mean the hosts which you want to login to via Rustica 72 | # must respect the public portion of the user key variable below. 73 | [signing.authority_configurations.example_test_environment] 74 | kind = "File" 75 | user_key = ''' 76 | -----BEGIN OPENSSH PRIVATE KEY----- 77 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 78 | QyNTUxOQAAACAipVtvuI/JPxiyNkRWEjiBE/JtQi8iGjz4sSeLmBMz/wAAAKhiepqoYnqa 79 | qAAAAAtzc2gtZWQyNTUxOQAAACAipVtvuI/JPxiyNkRWEjiBE/JtQi8iGjz4sSeLmBMz/w 80 | AAAEBHwGHZTQ6oGSiiz7kB6/g5g2mNWSX3U4e5WnVZFCv8jSKlW2+4j8k/GLI2RFYSOIET 81 | 8m1CLyIaPPixJ4uYEzP/AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg 82 | ECAwQ= 83 | -----END OPENSSH PRIVATE KEY----- 84 | ''' 85 | 86 | host_key = ''' 87 | -----BEGIN OPENSSH PRIVATE KEY----- 88 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 89 | QyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnewAAAKhQSP5+UEj+ 90 | fgAAAAtzc2gtZWQyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnew 91 | AAAEAevZOed5UnsVdAASUn+sJ+dUfUnG1kQ1wRH9L758mSCxcC2QuSbJKphPaKx0fRxE52 92 | sDNPZwaD2kv14ulnBCd7AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg 93 | ECAwQ= 94 | -----END OPENSSH PRIVATE KEY----- 95 | ''' 96 | 97 | x509_private_key = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDOLp3ZkQZasW1BKZ+fG3ODQgNThvI7pV38DOEFCz6c+gr8whSiV6EHWT04VrddShehZANiAARKbU0hcFy5+9qqHxGx/FBQb2dh6u+pAYh4ASh7skBkPv5DK/46FH6pvyPp6Gfkp8gagcFsr9nAKbwjkVTtBopuhh45KUM5k4VqIqaNox7g+XCrgG29oVqA5WZpW8DFH2c=" 98 | x509_private_key_algorithm = "p384" 99 | 100 | client_certificate_authority_private_key = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgFEwFOjsK54VOGLVajOMpV6PXEbOHKS8EXIMxRwmLQ/qhRANCAAQ+F90NcFu0EucoggNcbOGI4KP70/Mdb9hMxbd2NYx0DAeEvFiIjP2CI8QV6JgNW32zBKibV2iMtcwEyjMG7bR8" 101 | client_certificate_authority_private_key_algorithm = "p256" 102 | client_certificate_authority_common_name = "RusticaAccess" 103 | 104 | [logging."stdout"] 105 | 106 | [authorization."database"] 107 | path = "examples/example.db" 108 | 109 | [allowed_signers] 110 | cache_validity_length.secs = 900 111 | cache_validity_length.nanos = 0 112 | lru_rate_limiter_size = 16 113 | rate_limit_cooldown.secs = 300 114 | rate_limit_cooldown.nanos = 0 115 | -------------------------------------------------------------------------------- /tests/test_configs/rustica_local_file_multi.toml: -------------------------------------------------------------------------------- 1 | # This is the listen address that will be used for the Rustica service 2 | listen_address = "0.0.0.0:50052" 3 | 4 | # This setting controls if the agent has to prove that it 5 | # controls the private key to Rustica. Setting this to true means a user needs 6 | # to generate two signatures (one to Rustica, and one to the host). The 7 | # advantage of using this, is a compromised host cannot get certificates 8 | # from the server without physical interaction. 9 | # 10 | # A client will always need to sign the challenge from the host they 11 | # are attempting to connect to however so a physical tap will always 12 | # be required. 13 | require_rustica_proof = false 14 | 15 | # This setting controls if Rustica will allow users to register keys that 16 | # cannot have their providence validated. If set to true, if a registration 17 | # either does not provide an attestation or the attestation fails, the key 18 | # will be rejected and the API call will fail. In the case of using an 19 | # external authorizer, a call will not be made to inform it of this event. 20 | require_attestation_chain = true 21 | 22 | 23 | # The certificate presented to connecting clients 24 | server_cert = ''' 25 | -----BEGIN CERTIFICATE----- 26 | MIIBqjCCAVCgAwIBAgIJAOI2FtcQeixVMAoGCCqGSM49BAMCMBsxGTAXBgNVBAMM 27 | EEVudGVycHJpc2VSb290Q0EwHhcNMjIwMTIwMDQwMjA2WhcNMjQwNDI0MDQwMjA2 28 | WjAxMRAwDgYDVQQDDAdydXN0aWNhMRAwDgYDVQQKDAdSdXN0aWNhMQswCQYDVQQG 29 | EwJDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAINhoFW/5twPqAHLxjFjmns 30 | lE1jJMJQXmijymZTJxR0DsNZlwvUgNH+WYQFfq4IVMwypVHgyTYJO+lAAPEeyPOj 31 | ZzBlMDUGA1UdIwQuMCyhH6QdMBsxGTAXBgNVBAMMEEVudGVycHJpc2VSb290Q0GC 32 | CQCRg096sVtP0zAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAUBgNVHREEDTALggls 33 | b2NhbGhvc3QwCgYIKoZIzj0EAwIDSAAwRQIhAMfjW/PMrA9/cCg6O835sr22ZrNk 34 | k/lFOODLqAJPbh3+AiAzeCUyrmxT5VTf6uyFoNT8zMoWSi79rudcdgl+32RqMg== 35 | -----END CERTIFICATE----- 36 | ''' 37 | 38 | # The key for the certificate presented to clients 39 | server_key = ''' 40 | -----BEGIN PRIVATE KEY----- 41 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkTd0C69xWFX9PmVf 42 | BeD0ySfG+O0e7p7SXR9xo/enbvahRANCAAQCDYaBVv+bcD6gBy8YxY5p7JRNYyTC 43 | UF5oo8pmUycUdA7DWZcL1IDR/lmEBX6uCFTMMqVR4Mk2CTvpQADxHsjz 44 | -----END PRIVATE KEY----- 45 | ''' 46 | 47 | # Configuration for authenticating connecting clients as well as defining 48 | # automatic renewal settings. 49 | [client_authority] 50 | # This must be one of the signing authorities defined in the signing structure 51 | authority = "example_test_environment" 52 | # When we issue a new access certificate, how long is it valid for. 53 | validity_length = 181440000 # Three weeks * 100 54 | 55 | # I think the easiest way to explain this is with an example. 56 | # 57 | # If a certificate is issued for three months, setting this to one week means 58 | # in the week before it expires, when they request a new SSH certificate, the 59 | # server will generate a new mTLS access certificate and send that back with 60 | # the request. 61 | # 62 | # This value should always be less than the validity length 63 | expiration_renewal_period = 181439980 #60480000 # One Week * 100 64 | 65 | [signing] 66 | default_authority = "example_test_environment" 67 | 68 | # Rustica has many ways it can sign SSH certificates which are sent to 69 | # clients. This method uses private keys embedded in the configuration 70 | # file. This will mean the hosts which you want to login to via Rustica 71 | # must respect the public portion of the user key variable below. 72 | [signing.authority_configurations.example_test_environment] 73 | kind = "File" 74 | user_key = ''' 75 | -----BEGIN OPENSSH PRIVATE KEY----- 76 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 77 | QyNTUxOQAAACDhq7U0gcW66mJNWpsL/4ADNXoOUMLHXYbM8PIHbqPT+gAAAKhxqRWZcakV 78 | mQAAAAtzc2gtZWQyNTUxOQAAACDhq7U0gcW66mJNWpsL/4ADNXoOUMLHXYbM8PIHbqPT+g 79 | AAAEA8yRG/XWtjlY007gj8tNflVX9fnHPDcQYH2HTImTKvPeGrtTSBxbrqYk1amwv/gAM1 80 | eg5Qwsddhszw8gduo9P6AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg 81 | ECAwQ= 82 | -----END OPENSSH PRIVATE KEY----- 83 | ''' 84 | 85 | host_key = ''' 86 | -----BEGIN OPENSSH PRIVATE KEY----- 87 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 88 | QyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnewAAAKhQSP5+UEj+ 89 | fgAAAAtzc2gtZWQyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnew 90 | AAAEAevZOed5UnsVdAASUn+sJ+dUfUnG1kQ1wRH9L758mSCxcC2QuSbJKphPaKx0fRxE52 91 | sDNPZwaD2kv14ulnBCd7AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg 92 | ECAwQ= 93 | -----END OPENSSH PRIVATE KEY----- 94 | ''' 95 | 96 | x509_private_key = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDOLp3ZkQZasW1BKZ+fG3ODQgNThvI7pV38DOEFCz6c+gr8whSiV6EHWT04VrddShehZANiAARKbU0hcFy5+9qqHxGx/FBQb2dh6u+pAYh4ASh7skBkPv5DK/46FH6pvyPp6Gfkp8gagcFsr9nAKbwjkVTtBopuhh45KUM5k4VqIqaNox7g+XCrgG29oVqA5WZpW8DFH2c=" 97 | x509_private_key_alg = "p384" 98 | 99 | client_certificate_authority_private_key = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDOLp3ZkQZasW1BKZ+fG3ODQgNThvI7pV38DOEFCz6c+gr8whSiV6EHWT04VrddShehZANiAARKbU0hcFy5+9qqHxGx/FBQb2dh6u+pAYh4ASh7skBkPv5DK/46FH6pvyPp6Gfkp8gagcFsr9nAKbwjkVTtBopuhh45KUM5k4VqIqaNox7g+XCrgG29oVqA5WZpW8DFH2c=" 100 | client_certificate_authority_private_key_algorithm = "p384" 101 | client_certificate_authority_common_name = "RusticaAccess" 102 | 103 | [signing.authority_configurations.example_prod_environment] 104 | kind = "File" 105 | user_key = ''' 106 | -----BEGIN OPENSSH PRIVATE KEY----- 107 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 108 | QyNTUxOQAAACB6GWweeLV8JUaw6su+TwZc+dGmEmV1crFVLjRnviX0vQAAAKC4yomjuMqJ 109 | owAAAAtzc2gtZWQyNTUxOQAAACB6GWweeLV8JUaw6su+TwZc+dGmEmV1crFVLjRnviX0vQ 110 | AAAEC5ut4epWWUojssOgAlAlGfiBJ4AGZ8G1eWchiRfhk2mnoZbB54tXwlRrDqy75PBlz5 111 | 0aYSZXVysVUuNGe+JfS9AAAAG29iZWxpc2tATWFjQm9vay1Qcm8tMy5sb2NhbAEC 112 | -----END OPENSSH PRIVATE KEY----- 113 | ''' 114 | 115 | host_key = ''' 116 | -----BEGIN OPENSSH PRIVATE KEY----- 117 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 118 | QyNTUxOQAAACB5w+QCbSSFLH0hWI04OY0JFSNF5yg4kg2Po0XFgtGy/wAAAKDqg4KG6oOC 119 | hgAAAAtzc2gtZWQyNTUxOQAAACB5w+QCbSSFLH0hWI04OY0JFSNF5yg4kg2Po0XFgtGy/w 120 | AAAECPJOmHKCJNl7CyEn0HoPpMT2aVg/l8JYNJWu/ClUHDNnnD5AJtJIUsfSFYjTg5jQkV 121 | I0XnKDiSDY+jRcWC0bL/AAAAG29iZWxpc2tATWFjQm9vay1Qcm8tMy5sb2NhbAEC 122 | -----END OPENSSH PRIVATE KEY----- 123 | ''' 124 | 125 | x509_private_key = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDOLp3ZkQZasW1BKZ+fG3ODQgNThvI7pV38DOEFCz6c+gr8whSiV6EHWT04VrddShehZANiAARKbU0hcFy5+9qqHxGx/FBQb2dh6u+pAYh4ASh7skBkPv5DK/46FH6pvyPp6Gfkp8gagcFsr9nAKbwjkVTtBopuhh45KUM5k4VqIqaNox7g+XCrgG29oVqA5WZpW8DFH2c=" 126 | x509_private_key_alg = "p384" 127 | 128 | [logging."stdout"] 129 | 130 | [authorization."database"] 131 | path = "examples/example.db" 132 | 133 | [allowed_signers] 134 | cache_validity_length.secs = 900 135 | cache_validity_length.nanos = 0 136 | lru_rate_limiter_size = 16 137 | rate_limit_cooldown.secs = 15 138 | rate_limit_cooldown.nanos = 0 139 | -------------------------------------------------------------------------------- /tests/test_configs/rustica_local_file_with_influx.toml: -------------------------------------------------------------------------------- 1 | # This is the listen address that will be used for the Rustica service 2 | listen_address = "0.0.0.0:50052" 3 | 4 | # This setting controls if the agent has to prove that it 5 | # controls the private key to Rustica. Setting this to true means a user needs 6 | # to generate two signatures (one to Rustica, and one to the host). The 7 | # advantage of using this, is a compromised host cannot get certificates 8 | # from the server without physical interaction. 9 | # 10 | # A client will always need to sign the challenge from the host they 11 | # are attempting to connect to however so a physical tap will always 12 | # be required. 13 | require_rustica_proof = false 14 | 15 | # This setting controls if Rustica will allow users to register keys that 16 | # cannot have their providence validated. If set to true, if a registration 17 | # either does not provide an attestation or the attestation fails, the key 18 | # will be rejected and the API call will fail. In the case of using an 19 | # external authorizer, a call will not be made to inform it of this event. 20 | require_attestation_chain = true 21 | 22 | 23 | # The certificate presented to connecting clients 24 | server_cert = ''' 25 | -----BEGIN CERTIFICATE----- 26 | MIIBqjCCAVCgAwIBAgIJAOI2FtcQeixVMAoGCCqGSM49BAMCMBsxGTAXBgNVBAMM 27 | EEVudGVycHJpc2VSb290Q0EwHhcNMjIwMTIwMDQwMjA2WhcNMjQwNDI0MDQwMjA2 28 | WjAxMRAwDgYDVQQDDAdydXN0aWNhMRAwDgYDVQQKDAdSdXN0aWNhMQswCQYDVQQG 29 | EwJDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAINhoFW/5twPqAHLxjFjmns 30 | lE1jJMJQXmijymZTJxR0DsNZlwvUgNH+WYQFfq4IVMwypVHgyTYJO+lAAPEeyPOj 31 | ZzBlMDUGA1UdIwQuMCyhH6QdMBsxGTAXBgNVBAMMEEVudGVycHJpc2VSb290Q0GC 32 | CQCRg096sVtP0zAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAUBgNVHREEDTALggls 33 | b2NhbGhvc3QwCgYIKoZIzj0EAwIDSAAwRQIhAMfjW/PMrA9/cCg6O835sr22ZrNk 34 | k/lFOODLqAJPbh3+AiAzeCUyrmxT5VTf6uyFoNT8zMoWSi79rudcdgl+32RqMg== 35 | -----END CERTIFICATE----- 36 | ''' 37 | 38 | # The key for the certificate presented to clients 39 | server_key = ''' 40 | -----BEGIN PRIVATE KEY----- 41 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkTd0C69xWFX9PmVf 42 | BeD0ySfG+O0e7p7SXR9xo/enbvahRANCAAQCDYaBVv+bcD6gBy8YxY5p7JRNYyTC 43 | UF5oo8pmUycUdA7DWZcL1IDR/lmEBX6uCFTMMqVR4Mk2CTvpQADxHsjz 44 | -----END PRIVATE KEY----- 45 | ''' 46 | 47 | # Configuration for authenticating connecting clients as well as defining 48 | # automatic renewal settings. 49 | [client_authority] 50 | # This must be one of the signing authorities defined in the signing structure 51 | authority = "example_test_environment" 52 | # When we issue a new access certificate, how long is it valid for. 53 | validity_length = 181440000 # Three weeks * 100 54 | 55 | # I think the easiest way to explain this is with an example. 56 | # 57 | # If a certificate is issued for three months, setting this to one week means 58 | # in the week before it expires, when they request a new SSH certificate, the 59 | # server will generate a new mTLS access certificate and send that back with 60 | # the request. 61 | # 62 | # This value should always be less than the validity length 63 | expiration_renewal_period = 181439980 #60480000 # One Week * 100 64 | 65 | [signing] 66 | default_authority = "example_test_environment" 67 | 68 | # Rustica has many ways it can sign SSH certificates which are sent to 69 | # clients. This method uses private keys embedded in the configuration 70 | # file. This will mean the hosts which you want to login to via Rustica 71 | # must respect the public portion of the user key variable below. 72 | [signing.authority_configurations.example_test_environment] 73 | kind = "File" 74 | user_key = ''' 75 | -----BEGIN OPENSSH PRIVATE KEY----- 76 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 77 | QyNTUxOQAAACDhq7U0gcW66mJNWpsL/4ADNXoOUMLHXYbM8PIHbqPT+gAAAKhxqRWZcakV 78 | mQAAAAtzc2gtZWQyNTUxOQAAACDhq7U0gcW66mJNWpsL/4ADNXoOUMLHXYbM8PIHbqPT+g 79 | AAAEA8yRG/XWtjlY007gj8tNflVX9fnHPDcQYH2HTImTKvPeGrtTSBxbrqYk1amwv/gAM1 80 | eg5Qwsddhszw8gduo9P6AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg 81 | ECAwQ= 82 | -----END OPENSSH PRIVATE KEY----- 83 | ''' 84 | 85 | host_key = ''' 86 | -----BEGIN OPENSSH PRIVATE KEY----- 87 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 88 | QyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnewAAAKhQSP5+UEj+ 89 | fgAAAAtzc2gtZWQyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnew 90 | AAAEAevZOed5UnsVdAASUn+sJ+dUfUnG1kQ1wRH9L758mSCxcC2QuSbJKphPaKx0fRxE52 91 | sDNPZwaD2kv14ulnBCd7AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg 92 | ECAwQ= 93 | -----END OPENSSH PRIVATE KEY----- 94 | ''' 95 | 96 | x509_private_key = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDOLp3ZkQZasW1BKZ+fG3ODQgNThvI7pV38DOEFCz6c+gr8whSiV6EHWT04VrddShehZANiAARKbU0hcFy5+9qqHxGx/FBQb2dh6u+pAYh4ASh7skBkPv5DK/46FH6pvyPp6Gfkp8gagcFsr9nAKbwjkVTtBopuhh45KUM5k4VqIqaNox7g+XCrgG29oVqA5WZpW8DFH2c=" 97 | x509_private_key_alg = "p384" 98 | 99 | client_certificate_authority_private_key = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDOLp3ZkQZasW1BKZ+fG3ODQgNThvI7pV38DOEFCz6c+gr8whSiV6EHWT04VrddShehZANiAARKbU0hcFy5+9qqHxGx/FBQb2dh6u+pAYh4ASh7skBkPv5DK/46FH6pvyPp6Gfkp8gagcFsr9nAKbwjkVTtBopuhh45KUM5k4VqIqaNox7g+XCrgG29oVqA5WZpW8DFH2c=" 100 | client_certificate_authority_private_key_algorithm = "p384" 101 | client_certificate_authority_common_name = "RusticaAccess" 102 | 103 | [logging."stdout"] 104 | 105 | [logging."influx"] 106 | address = "http://some-local-influx-instance:8080" 107 | database = "rustica" 108 | dataset = "rustica_logs" 109 | user = "influx_user" 110 | password = "influx_password" 111 | 112 | [authorization."database"] 113 | path = "examples/example.db" 114 | 115 | [allowed_signers] 116 | cache_validity_length.secs = 900 117 | cache_validity_length.nanos = 0 118 | lru_rate_limiter_size = 16 119 | rate_limit_cooldown.secs = 15 120 | rate_limit_cooldown.nanos = 0 121 | -------------------------------------------------------------------------------- /tests/test_configs/rustica_local_file_with_splunk.toml: -------------------------------------------------------------------------------- 1 | # This is the listen address that will be used for the Rustica service 2 | listen_address = "0.0.0.0:50052" 3 | 4 | # This setting controls if the agent has to prove that it 5 | # controls the private key to Rustica. Setting this to true means a user needs 6 | # to generate two signatures (one to Rustica, and one to the host). The 7 | # advantage of using this, is a compromised host cannot get certificates 8 | # from the server without physical interaction. 9 | # 10 | # A client will always need to sign the challenge from the host they 11 | # are attempting to connect to however so a physical tap will always 12 | # be required. 13 | require_rustica_proof = false 14 | 15 | # This setting controls if Rustica will allow users to register keys that 16 | # cannot have their providence validated. If set to true, if a registration 17 | # either does not provide an attestation or the attestation fails, the key 18 | # will be rejected and the API call will fail. In the case of using an 19 | # external authorizer, a call will not be made to inform it of this event. 20 | require_attestation_chain = true 21 | 22 | 23 | # The certificate presented to connecting clients 24 | server_cert = ''' 25 | -----BEGIN CERTIFICATE----- 26 | MIIBqjCCAVCgAwIBAgIJAOI2FtcQeixVMAoGCCqGSM49BAMCMBsxGTAXBgNVBAMM 27 | EEVudGVycHJpc2VSb290Q0EwHhcNMjIwMTIwMDQwMjA2WhcNMjQwNDI0MDQwMjA2 28 | WjAxMRAwDgYDVQQDDAdydXN0aWNhMRAwDgYDVQQKDAdSdXN0aWNhMQswCQYDVQQG 29 | EwJDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAINhoFW/5twPqAHLxjFjmns 30 | lE1jJMJQXmijymZTJxR0DsNZlwvUgNH+WYQFfq4IVMwypVHgyTYJO+lAAPEeyPOj 31 | ZzBlMDUGA1UdIwQuMCyhH6QdMBsxGTAXBgNVBAMMEEVudGVycHJpc2VSb290Q0GC 32 | CQCRg096sVtP0zAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAUBgNVHREEDTALggls 33 | b2NhbGhvc3QwCgYIKoZIzj0EAwIDSAAwRQIhAMfjW/PMrA9/cCg6O835sr22ZrNk 34 | k/lFOODLqAJPbh3+AiAzeCUyrmxT5VTf6uyFoNT8zMoWSi79rudcdgl+32RqMg== 35 | -----END CERTIFICATE----- 36 | ''' 37 | 38 | # The key for the certificate presented to clients 39 | server_key = ''' 40 | -----BEGIN PRIVATE KEY----- 41 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkTd0C69xWFX9PmVf 42 | BeD0ySfG+O0e7p7SXR9xo/enbvahRANCAAQCDYaBVv+bcD6gBy8YxY5p7JRNYyTC 43 | UF5oo8pmUycUdA7DWZcL1IDR/lmEBX6uCFTMMqVR4Mk2CTvpQADxHsjz 44 | -----END PRIVATE KEY----- 45 | ''' 46 | 47 | # Configuration for authenticating connecting clients as well as defining 48 | # automatic renewal settings. 49 | [client_authority] 50 | # This must be one of the signing authorities defined in the signing structure 51 | authority = "example_test_environment" 52 | # When we issue a new access certificate, how long is it valid for. 53 | validity_length = 181440000 # Three weeks * 100 54 | 55 | # I think the easiest way to explain this is with an example. 56 | # 57 | # If a certificate is issued for three months, setting this to one week means 58 | # in the week before it expires, when they request a new SSH certificate, the 59 | # server will generate a new mTLS access certificate and send that back with 60 | # the request. 61 | # 62 | # This value should always be less than the validity length 63 | expiration_renewal_period = 181439980 #60480000 # One Week * 100 64 | 65 | [signing] 66 | default_authority = "example_test_environment" 67 | 68 | # Rustica has many ways it can sign SSH certificates which are sent to 69 | # clients. This method uses private keys embedded in the configuration 70 | # file. This will mean the hosts which you want to login to via Rustica 71 | # must respect the public portion of the user key variable below. 72 | [signing.authority_configurations.example_test_environment] 73 | kind = "File" 74 | user_key = ''' 75 | -----BEGIN OPENSSH PRIVATE KEY----- 76 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 77 | QyNTUxOQAAACDhq7U0gcW66mJNWpsL/4ADNXoOUMLHXYbM8PIHbqPT+gAAAKhxqRWZcakV 78 | mQAAAAtzc2gtZWQyNTUxOQAAACDhq7U0gcW66mJNWpsL/4ADNXoOUMLHXYbM8PIHbqPT+g 79 | AAAEA8yRG/XWtjlY007gj8tNflVX9fnHPDcQYH2HTImTKvPeGrtTSBxbrqYk1amwv/gAM1 80 | eg5Qwsddhszw8gduo9P6AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg 81 | ECAwQ= 82 | -----END OPENSSH PRIVATE KEY----- 83 | ''' 84 | 85 | host_key = ''' 86 | -----BEGIN OPENSSH PRIVATE KEY----- 87 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 88 | QyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnewAAAKhQSP5+UEj+ 89 | fgAAAAtzc2gtZWQyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnew 90 | AAAEAevZOed5UnsVdAASUn+sJ+dUfUnG1kQ1wRH9L758mSCxcC2QuSbJKphPaKx0fRxE52 91 | sDNPZwaD2kv14ulnBCd7AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg 92 | ECAwQ= 93 | -----END OPENSSH PRIVATE KEY----- 94 | ''' 95 | 96 | x509_private_key = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDOLp3ZkQZasW1BKZ+fG3ODQgNThvI7pV38DOEFCz6c+gr8whSiV6EHWT04VrddShehZANiAARKbU0hcFy5+9qqHxGx/FBQb2dh6u+pAYh4ASh7skBkPv5DK/46FH6pvyPp6Gfkp8gagcFsr9nAKbwjkVTtBopuhh45KUM5k4VqIqaNox7g+XCrgG29oVqA5WZpW8DFH2c=" 97 | x509_private_key_alg = "p384" 98 | 99 | client_certificate_authority_private_key = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDOLp3ZkQZasW1BKZ+fG3ODQgNThvI7pV38DOEFCz6c+gr8whSiV6EHWT04VrddShehZANiAARKbU0hcFy5+9qqHxGx/FBQb2dh6u+pAYh4ASh7skBkPv5DK/46FH6pvyPp6Gfkp8gagcFsr9nAKbwjkVTtBopuhh45KUM5k4VqIqaNox7g+XCrgG29oVqA5WZpW8DFH2c=" 100 | client_certificate_authority_private_key_algorithm = "p384" 101 | client_certificate_authority_common_name = "RusticaAccess" 102 | 103 | [logging] 104 | identifier = "Some Instance" 105 | heartbeat_interval = 240 106 | 107 | [logging."stdout"] 108 | 109 | # This is not a real token, I randomly generated a UUID 110 | [logging."splunk"] 111 | token = "c46d7213-19ea-4a66-b83b-e4b06188d197" 112 | url = "https://http-inputs-examplecompany.splunkcloud.com/services/collector" 113 | timeout = 5 114 | 115 | [authorization."database"] 116 | path = "examples/example.db" 117 | 118 | [allowed_signers] 119 | cache_validity_length.secs = 900 120 | cache_validity_length.nanos = 0 121 | lru_rate_limiter_size = 16 122 | rate_limit_cooldown.secs = 15 123 | rate_limit_cooldown.nanos = 0 124 | -------------------------------------------------------------------------------- /tests/test_configs/rustica_local_file_with_webhook.toml: -------------------------------------------------------------------------------- 1 | # This is the listen address that will be used for the Rustica service 2 | listen_address = "0.0.0.0:50052" 3 | 4 | # This setting controls if the agent has to prove that it 5 | # controls the private key to Rustica. Setting this to true means a user needs 6 | # to generate two signatures (one to Rustica, and one to the host). The 7 | # advantage of using this, is a compromised host cannot get certificates 8 | # from the server without physical interaction. 9 | # 10 | # A client will always need to sign the challenge from the host they 11 | # are attempting to connect to however so a physical tap will always 12 | # be required. 13 | require_rustica_proof = false 14 | 15 | # This setting controls if Rustica will allow users to register keys that 16 | # cannot have their providence validated. If set to true, if a registration 17 | # either does not provide an attestation or the attestation fails, the key 18 | # will be rejected and the API call will fail. In the case of using an 19 | # external authorizer, a call will not be made to inform it of this event. 20 | require_attestation_chain = true 21 | 22 | 23 | # The certificate presented to connecting clients 24 | server_cert = ''' 25 | -----BEGIN CERTIFICATE----- 26 | MIIBqjCCAVCgAwIBAgIJAOI2FtcQeixVMAoGCCqGSM49BAMCMBsxGTAXBgNVBAMM 27 | EEVudGVycHJpc2VSb290Q0EwHhcNMjIwMTIwMDQwMjA2WhcNMjQwNDI0MDQwMjA2 28 | WjAxMRAwDgYDVQQDDAdydXN0aWNhMRAwDgYDVQQKDAdSdXN0aWNhMQswCQYDVQQG 29 | EwJDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAINhoFW/5twPqAHLxjFjmns 30 | lE1jJMJQXmijymZTJxR0DsNZlwvUgNH+WYQFfq4IVMwypVHgyTYJO+lAAPEeyPOj 31 | ZzBlMDUGA1UdIwQuMCyhH6QdMBsxGTAXBgNVBAMMEEVudGVycHJpc2VSb290Q0GC 32 | CQCRg096sVtP0zAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAUBgNVHREEDTALggls 33 | b2NhbGhvc3QwCgYIKoZIzj0EAwIDSAAwRQIhAMfjW/PMrA9/cCg6O835sr22ZrNk 34 | k/lFOODLqAJPbh3+AiAzeCUyrmxT5VTf6uyFoNT8zMoWSi79rudcdgl+32RqMg== 35 | -----END CERTIFICATE----- 36 | ''' 37 | 38 | # The key for the certificate presented to clients 39 | server_key = ''' 40 | -----BEGIN PRIVATE KEY----- 41 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkTd0C69xWFX9PmVf 42 | BeD0ySfG+O0e7p7SXR9xo/enbvahRANCAAQCDYaBVv+bcD6gBy8YxY5p7JRNYyTC 43 | UF5oo8pmUycUdA7DWZcL1IDR/lmEBX6uCFTMMqVR4Mk2CTvpQADxHsjz 44 | -----END PRIVATE KEY----- 45 | ''' 46 | 47 | # Configuration for authenticating connecting clients as well as defining 48 | # automatic renewal settings. 49 | [client_authority] 50 | # This must be one of the signing authorities defined in the signing structure 51 | authority = "example_test_environment" 52 | # When we issue a new access certificate, how long is it valid for. 53 | validity_length = 181440000 # Three weeks * 100 54 | 55 | # I think the easiest way to explain this is with an example. 56 | # 57 | # If a certificate is issued for three months, setting this to one week means 58 | # in the week before it expires, when they request a new SSH certificate, the 59 | # server will generate a new mTLS access certificate and send that back with 60 | # the request. 61 | # 62 | # This value should always be less than the validity length 63 | expiration_renewal_period = 181439980 #60480000 # One Week * 100 64 | 65 | [signing] 66 | default_authority = "example_test_environment" 67 | 68 | # Rustica has many ways it can sign SSH certificates which are sent to 69 | # clients. This method uses private keys embedded in the configuration 70 | # file. This will mean the hosts which you want to login to via Rustica 71 | # must respect the public portion of the user key variable below. 72 | [signing.authority_configurations.example_test_environment] 73 | kind = "File" 74 | user_key = ''' 75 | -----BEGIN OPENSSH PRIVATE KEY----- 76 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 77 | QyNTUxOQAAACDhq7U0gcW66mJNWpsL/4ADNXoOUMLHXYbM8PIHbqPT+gAAAKhxqRWZcakV 78 | mQAAAAtzc2gtZWQyNTUxOQAAACDhq7U0gcW66mJNWpsL/4ADNXoOUMLHXYbM8PIHbqPT+g 79 | AAAEA8yRG/XWtjlY007gj8tNflVX9fnHPDcQYH2HTImTKvPeGrtTSBxbrqYk1amwv/gAM1 80 | eg5Qwsddhszw8gduo9P6AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg 81 | ECAwQ= 82 | -----END OPENSSH PRIVATE KEY----- 83 | ''' 84 | 85 | host_key = ''' 86 | -----BEGIN OPENSSH PRIVATE KEY----- 87 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 88 | QyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnewAAAKhQSP5+UEj+ 89 | fgAAAAtzc2gtZWQyNTUxOQAAACAXAtkLkmySqYT2isdH0cROdrAzT2cGg9pL9eLpZwQnew 90 | AAAEAevZOed5UnsVdAASUn+sJ+dUfUnG1kQ1wRH9L758mSCxcC2QuSbJKphPaKx0fRxE52 91 | sDNPZwaD2kv14ulnBCd7AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg 92 | ECAwQ= 93 | -----END OPENSSH PRIVATE KEY----- 94 | ''' 95 | 96 | x509_private_key = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDOLp3ZkQZasW1BKZ+fG3ODQgNThvI7pV38DOEFCz6c+gr8whSiV6EHWT04VrddShehZANiAARKbU0hcFy5+9qqHxGx/FBQb2dh6u+pAYh4ASh7skBkPv5DK/46FH6pvyPp6Gfkp8gagcFsr9nAKbwjkVTtBopuhh45KUM5k4VqIqaNox7g+XCrgG29oVqA5WZpW8DFH2c=" 97 | x509_private_key_alg = "p384" 98 | 99 | client_certificate_authority_private_key = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDOLp3ZkQZasW1BKZ+fG3ODQgNThvI7pV38DOEFCz6c+gr8whSiV6EHWT04VrddShehZANiAARKbU0hcFy5+9qqHxGx/FBQb2dh6u+pAYh4ASh7skBkPv5DK/46FH6pvyPp6Gfkp8gagcFsr9nAKbwjkVTtBopuhh45KUM5k4VqIqaNox7g+XCrgG29oVqA5WZpW8DFH2c=" 100 | client_certificate_authority_private_key_algorithm = "p384" 101 | client_certificate_authority_common_name = "RusticaAccess" 102 | 103 | [logging] 104 | identifier = "Some Instance" 105 | heartbeat_interval = 240 106 | 107 | [logging."stdout"] 108 | 109 | # This is not a real token, I randomly generated a UUID 110 | [logging."webhook"] 111 | auth_header = "Basic c46d7213-19ea-4a66-b83b-e4b06188d197" 112 | url = "http://localhost:4554/some/webhook/another_long_string_that_can_act_as_a_webhook_secret" 113 | timeout = 5 114 | 115 | [authorization."database"] 116 | path = "examples/example.db" 117 | 118 | [allowed_signers] 119 | cache_validity_length.secs = 900 120 | cache_validity_length.nanos = 0 121 | lru_rate_limiter_size = 16 122 | rate_limit_cooldown.secs = 15 123 | rate_limit_cooldown.nanos = 0 124 | -------------------------------------------------------------------------------- /tests/test_ec256: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS 3 | 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQNCJi8CAT8PIP9I22V5hT+nel3jQJS 4 | cs4CXicPgbY6O438MCShF3yW/4YAwOfyOhOsAUNSmeyzFI6QLDUL7+l3AAAAwMk1lDjJNZ 5 | Q4AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBA0ImLwIBPw8g/0j 6 | bZXmFP6d6XeNAlJyzgJeJw+Btjo7jfwwJKEXfJb/hgDA5/I6E6wBQ1KZ7LMUjpAsNQvv6X 7 | cAAAAgHABbRLTsuNBOMkOx8wYub5CVtwP3555YViLpBhe6YXYAAAAhb2JlbGlza0BNaXRj 8 | aGVsbHMtTUJQLmxvY2FsZG9tYWluAQIDBAUGBw== 9 | -----END OPENSSH PRIVATE KEY----- 10 | -------------------------------------------------------------------------------- /tests/test_ec384: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS 3 | 1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQTZORmeml7tVQZNpm331wkVbzpT8s7E 4 | nmUUyCzHbupYoAO294BC23Dw1/LXJKwbIyEwkZSyNS2MSjYqkN+UGPQo2PkH0bFxgsOOAD 5 | X8+i0MNlF/fO4GZobChqTfb7wSn5wAAADw/TXNfv01zX4AAAATZWNkc2Etc2hhMi1uaXN0 6 | cDM4NAAAAAhuaXN0cDM4NAAAAGEE2TkZnppe7VUGTaZt99cJFW86U/LOxJ5lFMgsx27qWK 7 | ADtveAQttw8Nfy1ySsGyMhMJGUsjUtjEo2KpDflBj0KNj5B9GxcYLDjgA1/PotDDZRf3zu 8 | BmaGwoak32+8Ep+cAAAAMGvgw0+thkQve6+Ugxxsms3Pn2vjL9cnulh6cIE6P7UMSiim/P 9 | 0SiOzi1o9RXi9T5gAAACFvYmVsaXNrQE1pdGNoZWxscy1NQlAubG9jYWxkb21haW4BAgME 10 | BQYH 11 | -----END OPENSSH PRIVATE KEY----- 12 | -------------------------------------------------------------------------------- /tests/test_ed25519: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACAipVtvuI/JPxiyNkRWEjiBE/JtQi8iGjz4sSeLmBMz/wAAAKhiepqoYnqa 4 | qAAAAAtzc2gtZWQyNTUxOQAAACAipVtvuI/JPxiyNkRWEjiBE/JtQi8iGjz4sSeLmBMz/w 5 | AAAEBHwGHZTQ6oGSiiz7kB6/g5g2mNWSX3U4e5WnVZFCv8jSKlW2+4j8k/GLI2RFYSOIET 6 | 8m1CLyIaPPixJ4uYEzP/AAAAIW9iZWxpc2tATWl0Y2hlbGxzLU1CUC5sb2NhbGRvbWFpbg 7 | ECAwQ= 8 | -----END OPENSSH PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /tests/validate_file_configs.sh: -------------------------------------------------------------------------------- 1 | # Exit when any command fails 2 | set -e 3 | 4 | ## Full Verification 5 | ## Parse keys, generate certificates 6 | cargo run --features=all --bin rustica -- -vv --config examples/rustica_external.toml 7 | cargo run --features=all --bin rustica -- -vv --config tests/test_configs/rustica_local_file.toml 8 | cargo run --features=all --bin rustica -- -vv --config tests/test_configs/rustica_local_file_alt.toml 9 | cargo run --features=all --bin rustica -- -vv --config tests/test_configs/rustica_local_file_multi.toml 10 | cargo run --features=all --bin rustica -- -vv --config tests/test_configs/rustica_local_file_with_influx.toml 11 | cargo run --features=all --bin rustica -- -vv --config tests/test_configs/rustica_local_file_with_splunk.toml 12 | cargo run --features=all --bin rustica -- -vv --config tests/test_configs/rustica_local_file_with_webhook.toml 13 | 14 | ## Configuration Structure Verification 15 | ## Only verify that the example configurations parse correctly. This allows us 16 | ## to test Yubikey and AmazonKMS configurations without having them connected 17 | ## or validly configured 18 | cargo run --features=all --bin rustica -- -v --config examples/rustica_local_amazonkms.toml 19 | cargo run --features=all --bin rustica -- -v --config examples/rustica_local_yubikey.toml --------------------------------------------------------------------------------