diff --git a/Cargo.lock b/Cargo.lock index 680ff07..e3b5ead 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4004,6 +4004,7 @@ dependencies = [ "tracing-subscriber", "url", "warg-client", + "warg-crypto", "warg-protocol", "wasm-pkg-common", "wit-component 0.216.0", diff --git a/README.md b/README.md index bcb7c11..757772c 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,12 @@ default = "warg" # A path to a valid warg config file. If this is not set, the `wkg` CLI (but not the libraries) # will attempt to load the config from the default location(s). config_file = "/a/path" +# An optional authentication token to use when authenticating with a registry. +auth_token = "an-auth-token" +# An optional key for signing the component. Ideally, you should just let warg use the keychain +# or programmatically set this key in the config without writing to disk. This offers an escape +# hatch for when you need to use a key that isn't in the keychain. +signing_key = "ecdsa-p256:2CV1EpLaSYEn4In4OAEDAj5O4Hzu8AFAxgHXuG310Ew=" [registry."acme.registry.com".oci] # The auth field can either be a username/password pair, or a base64 encoded `username:password` # string. If no auth is set, the `wkg` CLI (but not the libraries) will also attempt to load the diff --git a/crates/wasm-pkg-client/Cargo.toml b/crates/wasm-pkg-client/Cargo.toml index 127608f..c998a65 100644 --- a/crates/wasm-pkg-client/Cargo.toml +++ b/crates/wasm-pkg-client/Cargo.toml @@ -29,6 +29,7 @@ tracing = "0.1.40" tracing-subscriber = { workspace = true } url = "2.5.0" warg-client = "0.8.0" +warg-crypto = "0.8.0" warg-protocol = "0.8.0" wasm-pkg-common = { workspace = true, features = ["metadata-client", "tokio"] } wit-component = { workspace = true } diff --git a/crates/wasm-pkg-client/src/warg/config.rs b/crates/wasm-pkg-client/src/warg/config.rs index 2e510dc..64b1a82 100644 --- a/crates/wasm-pkg-client/src/warg/config.rs +++ b/crates/wasm-pkg-client/src/warg/config.rs @@ -1,29 +1,46 @@ -use std::path::PathBuf; +use std::{fmt::Debug, path::PathBuf, sync::Arc}; use secrecy::{ExposeSecret, SecretString}; use serde::{Deserialize, Serialize, Serializer}; +use warg_crypto::signing::PrivateKey; use wasm_pkg_common::{config::RegistryConfig, Error}; /// Registry configuration for Warg backends. /// /// See: [`RegistryConfig::backend_config`] -#[derive(Clone, Debug, Default, Serialize)] +#[derive(Clone, Default, Serialize)] #[serde(into = "WargRegistryConfigToml")] pub struct WargRegistryConfig { /// The configuration for the Warg client. pub client_config: warg_client::Config, /// The authentication token for the Warg registry. pub auth_token: Option, + /// A signing key to use for publishing packages. + // NOTE(thomastaylor312): This couldn't be wrapped in a secret because the outer type doesn't + // implement Zeroize. However, the inner type is zeroized. + pub signing_key: Option>, /// The path to the Warg config file, if specified. pub config_file: Option, } +impl Debug for WargRegistryConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WargRegistryConfig") + .field("client_config", &self.client_config) + .field("auth_token", &self.auth_token) + .field("signing_key", &"[redacted]") + .field("config_file", &self.config_file) + .finish() + } +} + impl TryFrom<&RegistryConfig> for WargRegistryConfig { type Error = Error; fn try_from(registry_config: &RegistryConfig) -> Result { let WargRegistryConfigToml { auth_token, + signing_key, config_file, } = registry_config.backend_config("warg")?.unwrap_or_default(); let (client_config, config_file) = match config_file { @@ -43,9 +60,16 @@ impl TryFrom<&RegistryConfig> for WargRegistryConfig { ) } }; + Ok(Self { client_config, auth_token, + signing_key: signing_key + .map(|k| PrivateKey::decode(k).map(Arc::new)) + .transpose() + .map_err(|e| { + Error::InvalidConfig(anyhow::anyhow!("invalid signing key in config file: {e}")) + })?, config_file, }) } @@ -60,6 +84,11 @@ struct WargRegistryConfigToml { serialize_with = "serialize_secret" )] auth_token: Option, + #[serde( + skip_serializing_if = "Option::is_none", + serialize_with = "serialize_secret" + )] + signing_key: Option, } impl From for WargRegistryConfigToml { @@ -67,6 +96,9 @@ impl From for WargRegistryConfigToml { WargRegistryConfigToml { auth_token: value.auth_token, config_file: value.config_file, + signing_key: value + .signing_key + .map(|k| SecretString::new(k.encode().to_string())), } } } @@ -90,12 +122,14 @@ mod tests { async fn test_warg_config_roundtrip() { let dir = tempfile::tempdir().expect("Unable to create tempdir"); let warg_config_path = dir.path().join("warg_config.json"); + let (_, key) = warg_crypto::signing::generate_p256_pair(); let config = WargRegistryConfig { client_config: warg_client::Config { home_url: Some("https://example.com".to_owned()), ..Default::default() }, auth_token: Some("imsecret".to_owned().into()), + signing_key: Some(Arc::new(key)), config_file: Some(warg_config_path.clone()), }; @@ -135,5 +169,13 @@ mod tests { config.auth_token.unwrap().expose_secret(), "Auth token should be set to the right value" ); + assert_eq!( + roundtripped + .signing_key + .expect("Should have a signing key set") + .encode(), + config.signing_key.unwrap().encode(), + "Signing key should be set to the right value" + ); } } diff --git a/crates/wasm-pkg-client/src/warg/mod.rs b/crates/wasm-pkg-client/src/warg/mod.rs index 309a086..c444868 100644 --- a/crates/wasm-pkg-client/src/warg/mod.rs +++ b/crates/wasm-pkg-client/src/warg/mod.rs @@ -1,17 +1,20 @@ //! Warg package backend. -mod config; -mod loader; -mod publisher; +use std::sync::Arc; use serde::Deserialize; use warg_client::{storage::PackageInfo, ClientError, FileSystemClient}; +use warg_crypto::signing::PrivateKey; use warg_protocol::registry::PackageName; use wasm_pkg_common::{ config::RegistryConfig, metadata::RegistryMetadata, package::PackageRef, registry::Registry, Error, }; +mod config; +mod loader; +mod publisher; + /// Re-exported for convenience. pub use warg_client as client; @@ -25,6 +28,7 @@ struct WargRegistryMetadata { pub(crate) struct WargBackend { client: FileSystemClient, + signing_key: Option>, } impl WargBackend { @@ -40,6 +44,7 @@ impl WargBackend { let WargRegistryConfig { client_config, auth_token, + signing_key, .. } = registry_config.try_into()?; @@ -57,7 +62,10 @@ impl WargBackend { FileSystemClient::new_with_config(Some(url.as_str()), &client_config, auth_token) .await .map_err(warg_registry_error)?; - Ok(Self { client }) + Ok(Self { + client, + signing_key, + }) } pub(crate) async fn fetch_package_info( diff --git a/crates/wasm-pkg-client/src/warg/publisher.rs b/crates/wasm-pkg-client/src/warg/publisher.rs index 580ff2f..5150dbc 100644 --- a/crates/wasm-pkg-client/src/warg/publisher.rs +++ b/crates/wasm-pkg-client/src/warg/publisher.rs @@ -39,15 +39,26 @@ impl PackagePublisher for WargBackend { // start Warg publish, using the keyring to sign let version = version.clone(); - let record_id = self - .client - .sign_with_keyring_and_publish(Some(PublishInfo { - name: name.clone(), - head: None, - entries: vec![PublishEntry::Release { version, content }], - })) - .await - .map_err(super::warg_registry_error)?; + // Check if the package already exists so we can init it if needed + let release = PublishEntry::Release { version, content }; + let entries = if let Err(warg_client::ClientError::PackageDoesNotExist { .. }) = + self.client.fetch_package(&name).await + { + vec![PublishEntry::Init, release] + } else { + vec![release] + }; + let info = PublishInfo { + name: name.clone(), + head: None, + entries, + }; + let record_id = if let Some(key) = self.signing_key.as_ref() { + self.client.publish_with_info(key, info).await + } else { + self.client.sign_with_keyring_and_publish(Some(info)).await + } + .map_err(super::warg_registry_error)?; // wait for the Warg publish to finish self.client