Skip to content

Commit

Permalink
Cli and config work
Browse files Browse the repository at this point in the history
  • Loading branch information
augustuswm committed Sep 25, 2023
1 parent 9793583 commit 8b72722
Show file tree
Hide file tree
Showing 36 changed files with 1,246 additions and 513 deletions.
254 changes: 165 additions & 89 deletions Cargo.lock

Large diffs are not rendered by default.

14 changes: 10 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,22 @@ mockall = "0.11.3"
newline-converter = "0.2.2"
oauth2 = "4.1.0"
octorust = "0.7.0-rc.1"
openssl = "0.10.57"
partial-struct = { git = "https://github.com/oxidecomputer/partial-struct" }
progenitor = { git = "https://github.com/oxidecomputer/progenitor" }
progenitor-client = { git = "https://github.com/oxidecomputer/progenitor" }
rand = "0.8.5"
rand_core = "0.6"
regex = "1.7.1"
reqwest = { version = "0.11", features = ["json", "stream"] }
rsa = "0.8.2"
reqwest-middleware = "0.2"
reqwest-retry = "0.2.2"
reqwest-tracing = "0.4.6"
ring = "0.16.20"
rsa = "0.9.2"
rustfmt-wrapper = "0.2.0"
schemars = "0.8.11"
sha2 = "0.10.6"
sha2 = "0.10.7"
serde = "1"
serde_bytes = "0.11.9"
serde_json = "1"
Expand All @@ -66,6 +71,7 @@ slog = "2.7.0"
slog-async = "2.7.0"
tabwriter = "1.3.0"
tap = "1.0.1"
textwrap = "0.16.0"
thiserror = "1.0.38"
tokio = "1.25.0"
toml = "0.5.10"
Expand All @@ -77,8 +83,8 @@ uuid = "1.2.2"
valuable = "0.1.0"
yup-oauth2 = "8.1.0"

# [patch."https://github.com/oxidecomputer/progenitor"]
# progenitor = { path = "../progenitor/progenitor" }
[patch."https://github.com/oxidecomputer/progenitor"]
progenitor = { path = "../progenitor/progenitor" }

# [patch."https://github.com/oxidecomputer/typify"]
# typify = { path = "../typify/typify" }
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Work in progress replacement for RFD processing and programmatic access.

## TODO

Add job state and locking

## Authentication

Rough sketch of how users can authenticate to the RFD API
Expand Down
39 changes: 39 additions & 0 deletions rfd-api-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,44 @@
}
}
},
"/rfd-search": {
"get": {
"summary": "Search the RFD index and get a list of results",
"operationId": "search_rfds",
"parameters": [
{
"in": "query",
"name": "q",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/json": {
"schema": {
"title": "Array_of_ListRfd",
"type": "array",
"items": {
"$ref": "#/components/schemas/ListRfd"
}
}
}
}
},
"4XX": {
"$ref": "#/components/responses/Error"
},
"5XX": {
"$ref": "#/components/responses/Error"
}
}
}
},
"/self": {
"get": {
"summary": "Retrieve the user information of the calling user",
Expand Down Expand Up @@ -1000,6 +1038,7 @@
"GetAssignedRfds",
"GetAllDiscussions",
"GetAssignedDiscussions",
"SearchRfds",
"CreateOAuthClient",
"GetAssignedOAuthClients",
"UpdateAssignedOAuthClients",
Expand Down
5 changes: 4 additions & 1 deletion rfd-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,18 @@ http = { workspace = true }
hyper = { workspace = true }
hyper-tls = { workspace = true }
jsonwebtoken = { workspace = true }
meilisearch-sdk = { workspace = true }
oauth2 = { workspace = true }
octorust = { workspace = true }
openssl = { workspace = true }
partial-struct = { workspace = true }
rand = { workspace = true, features = ["std"] }
rand_core = { workspace = true, features = ["std"] }
regex = { workspace = true }
reqwest = { workspace = true }
ring = { workspace = true }
rfd-model = { path = "../rfd-model" }
rsa = { workspace = true }
rsa = { workspace = true, features = ["sha2"] }
schemars = { workspace = true, features = ["chrono"] }
sha2 = { workspace = true }
serde = { workspace = true, features = ["derive"] }
Expand Down
16 changes: 9 additions & 7 deletions rfd-api/src/authn/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use jsonwebtoken::{
jwk::{AlgorithmParameters, CommonParameters, Jwk, PublicKeyUse, RSAKeyParameters, RSAKeyType},
Algorithm, DecodingKey, Header, Validation,
};
use rsa::PublicKeyParts;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::instrument;
Expand Down Expand Up @@ -68,7 +67,7 @@ impl Jwt {
.map(|key| (key, Jwt::algo(&jwk)))
.map_err(JwtError::InvalidJwk)?;

tracing::debug!("Kid matched known decoding key");
tracing::debug!(?jwk, ?algorithm, "Kid matched known decoding key");

let data = decode(token, &key, &Validation::new(algorithm?)).map_err(JwtError::Decode)?;

Expand All @@ -93,6 +92,8 @@ impl Jwt {

#[derive(Debug, Error)]
pub enum JwtSignerError {
#[error("Failed to encode header")]
Header(serde_json::Error),
#[error("Failed to generate signer: {0}")]
InvalidKey(SigningKeyError),
#[error("Failed to serialize claims: {0}")]
Expand All @@ -113,11 +114,13 @@ impl JwtSigner {
let jwk = key.as_jwk().await?;
let mut header = Header::new(Algorithm::RS256);
header.kid = Some(key.kid().to_string());
let encoded_header = to_base64_json(&header)?;

let signer = key.as_signer().await.map_err(JwtSignerError::InvalidKey)?;

Ok(Self {
header,
encoded_header: String::new(),
encoded_header,
jwk,
signer,
})
Expand Down Expand Up @@ -145,10 +148,7 @@ impl JwtSigner {
.map_err(JwtSignerError::Signature)?;

let enc_signature = URL_SAFE_NO_PAD.encode(signature);
Ok(format!(
"{}.{}.{}",
self.encoded_header, encoded_claims, enc_signature
))
Ok(format!("{}.{}", message, enc_signature))
}

pub fn header(&self) -> &Header {
Expand All @@ -160,6 +160,8 @@ impl JwtSigner {
}
}

use rsa::traits::PublicKeyParts;

impl AsymmetricKey {
pub async fn as_jwk(&self) -> Result<Jwk, JwtSignerError> {
let key_id = self.kid();
Expand Down
34 changes: 11 additions & 23 deletions rfd-api/src/authn/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,7 @@ impl RawApiKey {

pub async fn sign(self, signer: &dyn Signer) -> Result<SignedApiKey, ApiKeyError> {
let key = format!("{}.{}", self.id, self.clear);
let signature = hex::encode(
signer
.sign(&key)
.await
.map_err(ApiKeyError::Signing)?,
);
let signature = hex::encode(signer.sign(&key).await.map_err(ApiKeyError::Signing)?);
Ok(SignedApiKey::new(key, signature))
}
}
Expand All @@ -55,18 +50,14 @@ impl TryFrom<&str> for RawApiKey {

fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.split_once(".") {
Some((id, key)) => {
Ok(RawApiKey {
id: id.parse().map_err(|err| {
tracing::info!(?err, "Api key prefix is not a valid uuid");
ApiKeyError::FailedToParse
})?,
clear: key.to_string(),
})
}
None => {
Err(ApiKeyError::FailedToParse)
}
Some((id, key)) => Ok(RawApiKey {
id: id.parse().map_err(|err| {
tracing::info!(?err, "Api key prefix is not a valid uuid");
ApiKeyError::FailedToParse
})?,
clear: key.to_string(),
}),
None => Err(ApiKeyError::FailedToParse),
}
}
}
Expand All @@ -78,10 +69,7 @@ pub struct SignedApiKey {

impl SignedApiKey {
fn new(key: String, signature: String) -> Self {
Self {
key,
signature,
}
Self { key, signature }
}

pub fn key(self) -> String {
Expand All @@ -101,7 +89,7 @@ mod tests {
use crate::util::tests::mock_key;

#[tokio::test]
async fn test_rejects_invalid_source() {
async fn test_generates_signatures() {
let id = Uuid::new_v4();
let signer = mock_key().as_signer().await.unwrap();

Expand Down
32 changes: 26 additions & 6 deletions rfd-api/src/authn/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ use dropshot_authorization_header::bearer::BearerAuth;
use google_cloudkms1::{api::AsymmetricSignRequest, hyper_rustls::HttpsConnector, CloudKMS};
use hyper::client::HttpConnector;
use rsa::{
pkcs1v15::Signature,
pkcs1v15::{SigningKey, VerifyingKey},
pkcs8::{DecodePrivateKey, DecodePublicKey},
pss::{BlindedSigningKey, Signature, VerifyingKey},
signature::{Keypair, RandomizedSigner, SignatureEncoding, Verifier},
RsaPrivateKey, RsaPublicKey,
};
Expand All @@ -18,9 +19,10 @@ use thiserror::Error;
use tracing::instrument;

use crate::{
authn::key::RawApiKey,
config::AsymmetricKey,
context::ApiContext,
util::{cloud_kms_client, response::unauthorized}, authn::key::RawApiKey,
util::{cloud_kms_client, response::unauthorized},
};

use self::jwt::Jwt;
Expand Down Expand Up @@ -93,6 +95,8 @@ impl From<AuthError> for HttpError {
pub enum SigningKeyError {
#[error("Cloud signing failed: {0}")]
CloudKmsError(#[from] CloudKmsError),
#[error("Failed to immediately verify generated signature")]
GeneratedInvalidSignature,
#[error("Failed to parse public key: {0}")]
InvalidPublicKey(#[from] rsa::pkcs8::spki::Error),
#[error("Invalid signature: {0}")]
Expand All @@ -107,22 +111,29 @@ pub trait Signer: Send + Sync {

// A signer that stores a local in memory key for signing new JWTs
pub struct LocalKey {
signing_key: BlindedSigningKey<Sha256>,
signing_key: SigningKey<Sha256>,
verifying_key: VerifyingKey<Sha256>,
}

#[async_trait]
impl Signer for LocalKey {
#[instrument(skip(self, message), err(Debug))]
async fn sign(&self, message: &str) -> Result<Vec<u8>, SigningKeyError> {
tracing::trace!("Signing message");
let mut rng = rand::thread_rng();
Ok(self
let signature = self
.signing_key
.sign_with_rng(&mut rng, message.as_bytes())
.to_vec())
.to_vec();

self.verify(message, &Signature::try_from(signature.as_ref()).unwrap())
.map_err(|_| SigningKeyError::GeneratedInvalidSignature)?;

Ok(signature)
}

fn verify(&self, message: &str, signature: &Signature) -> Result<(), SigningKeyError> {
tracing::trace!("Verifying message");
Ok(self.verifying_key.verify(message.as_bytes(), &signature)?)
}
}
Expand Down Expand Up @@ -257,6 +268,15 @@ impl AsymmetricKey {
}
}

pub async fn private_key(&self) -> Result<RsaPrivateKey, SigningKeyError> {
Ok(match self {
AsymmetricKey::Local { private, .. } => {
RsaPrivateKey::from_pkcs8_pem(&private).unwrap()
}
_ => unimplemented!(),
})
}

pub async fn public_key(&self) -> Result<RsaPublicKey, SigningKeyError> {
Ok(match self {
AsymmetricKey::Local { public, .. } => RsaPublicKey::from_public_key_pem(&public)?,
Expand All @@ -283,7 +303,7 @@ impl AsymmetricKey {
Ok(match self {
AsymmetricKey::Local { private, .. } => {
let private_key = RsaPrivateKey::from_pkcs8_pem(&private).unwrap();
let signing_key = BlindedSigningKey::new(private_key);
let signing_key = SigningKey::new(private_key);
let verifying_key = signing_key.verifying_key();

Arc::new(LocalKey {
Expand Down
22 changes: 8 additions & 14 deletions rfd-api/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub struct AppConfig {
pub jwt: JwtConfig,
pub spec: Option<SpecConfig>,
pub authn: AuthnProviders,
pub search: SearchConfig,
}

#[derive(Debug)]
Expand Down Expand Up @@ -102,20 +103,6 @@ impl AsymmetricKey {
Self::Ckms { kid, .. } => kid,
}
}

pub fn mock_private_key(&self) -> &str {
match &self {
Self::Local { private, .. } => private,
_ => unimplemented!(),
}
}

pub fn mock_public_key(&self) -> &str {
match &self {
Self::Local { public, .. } => public,
_ => unimplemented!(),
}
}
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -155,6 +142,13 @@ pub struct GoogleOAuthWebConfig {
pub redirect_uri: String,
}

#[derive(Debug, Default, Deserialize)]
pub struct SearchConfig {
pub host: String,
pub key: String,
pub index: String,
}

impl AppConfig {
pub fn new() -> Result<Self, ConfigError> {
let config = Config::builder()
Expand Down
Loading

0 comments on commit 8b72722

Please sign in to comment.