Skip to content

Commit 8d75590

Browse files
Merge #92
92: feat: allow minting NUCs in `nillion` cli r=mfontanini a=mfontanini This allows minting NUCs in the `nillion` cli. As part of this I restructured the cli code because it was becoming insane; now there's different "handlers" for every subset of commands we have. This otherwise was causing issues where some commands needed some things (e.g. nilvm client), others needed others (nilauth client) and the initialization of these was hell-ish. Now you can do something like this: ```bash nillion nuc mint --audience did:nil:03e3ba1eb887b4e972fbf395d479ff6cdb2cec91ba477ffc287b2b9cb5ec2161aa \ --delegation '[["==", ".foo", "hi"]]' \ --extending "$(nillion nilauth token | jq .token -r)" \ --command /nil/bar \ --metadata '{"arg": 42}' \ --expires-in 60m | \ jq .token -r | \ nillion nuc validate --root-public-key 03e3ba1eb887b4e972fbf395d479ff6cdb2cec91ba477ffc287b2b9cb5ec2161aa - ``` And it does what you're expect. I also added a couple of extras like being able to do `nillion nuc validate -` and have it read from stdin, and a `nillion nilauth about` that returns the pubkey/did to easily test stuff. PS: I should have moved the code in one commit and added this in another #sorry. The new code is in handlers/nuc.rs. Co-authored-by: Matias Fontanini <[email protected]>
2 parents e0bf7c8 + e428f01 commit 8d75590

File tree

14 files changed

+1217
-890
lines changed

14 files changed

+1217
-890
lines changed

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/nucs/src/builder.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use chrono::{DateTime, Utc};
88
use k256::ecdsa::{Signature, SigningKey, VerifyingKey};
99
use serde::Serialize;
1010
use signature::Signer;
11-
use std::ops::Deref;
11+
use std::{ops::Deref, time::Duration};
1212

1313
// Helper to simplify unwrapping options in the builder
1414
macro_rules! try_get {
@@ -97,6 +97,12 @@ impl NucTokenBuilder {
9797
self
9898
}
9999

100+
/// Set the expiration time for this token based on an offset from the current time.
101+
pub fn expires_in(mut self, offset: Duration) -> Self {
102+
self.expires_at = Some(Utc::now() + offset);
103+
self
104+
}
105+
100106
/// Set the timestamp when this token first becomes valid for this token.
101107
pub fn not_before(mut self, timestamp: DateTime<Utc>) -> Self {
102108
self.not_before = Some(timestamp);

tools/nillion/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ config = { version = "0.14", default-features = false, features = ["yaml"] }
1010
env_logger = "0.11.5"
1111
chrono = { version = "0.4", features = ["serde"] }
1212
hex = { version = "0.4", features = ["serde"] }
13+
humantime = "2.2"
1314
futures = "0.3.30"
1415
log = "0.4"
1516
nillion-nucs = { path = "../../libs/nucs" }

tools/nillion/src/args.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use clap_utils::shell_completions::ShellCompletionsArgs;
44
use hex::FromHexError;
55
use nada_values_args::NadaValueArgs;
66
use nillion_client::{grpc::membership::NodeId, Clear, NadaValue, UserId, Uuid};
7+
use nillion_nucs::token::Did;
78
use serde::Deserialize;
89
use serde_with::{serde_as, DisplayFromStr};
910
use std::{collections::HashMap, path::PathBuf, str::FromStr};
@@ -97,7 +98,7 @@ pub enum Command {
9798
#[clap(subcommand)]
9899
Config(ConfigCommand),
99100

100-
/// Mint or inspect a NUC.
101+
/// NUC token utilities.
101102
#[clap(subcommand)]
102103
Nuc(NucCommand),
103104

@@ -555,13 +556,17 @@ pub struct ClusterConfigArgs {
555556
}
556557

557558
/// The NUC command.
559+
#[allow(clippy::large_enum_variant)]
558560
#[derive(Subcommand)]
559561
pub enum NucCommand {
560562
/// Inspect a NUC.
561563
Inspect(InspectNucArgs),
562564

563565
/// Validate a NUC.
564566
Validate(ValidateNucArgs),
567+
568+
/// Mint a NUC.
569+
Mint(MintNucArgs),
565570
}
566571

567572
/// Inspect a NUC.
@@ -582,6 +587,58 @@ pub struct ValidateNucArgs {
582587
pub root_public_keys: Vec<HexBytes>,
583588
}
584589

590+
/// Mint a NUC.
591+
#[derive(Args)]
592+
pub struct MintNucArgs {
593+
/// The token to extend as a base for the newly minted token.
594+
#[clap(long)]
595+
pub extending: Option<String>,
596+
597+
/// The audience of the token.
598+
#[clap(long)]
599+
pub audience: Did,
600+
601+
/// The subject of the token.
602+
#[clap(long)]
603+
pub subject: Option<Did>,
604+
605+
/// The UNIX epoch timestamp to set as an expiration time for the token.
606+
#[clap(long, group = "expires")]
607+
pub expires_at: Option<i64>,
608+
609+
/// The amount of time in which the token will expire.
610+
#[clap(long, group = "expires")]
611+
pub expires_in: Option<humantime::Duration>,
612+
613+
/// The UNIT epoch timestamp at which this token can start being used.
614+
#[clap(long)]
615+
pub not_before: Option<i64>,
616+
617+
/// The command to be used in the token.
618+
#[clap(long)]
619+
pub command: Option<nillion_nucs::token::Command>,
620+
621+
/// A JSON map that contains metadata about the generated token.
622+
#[clap(long)]
623+
pub metadata: Option<String>,
624+
625+
/// The nonce to be used. Defaults to a random 16 byte sequence.
626+
#[clap(long)]
627+
pub nonce: Option<HexBytes>,
628+
629+
/// A token to be used as proof. Not needed when using `extending`.
630+
#[clap(long)]
631+
pub proof: Option<String>,
632+
633+
/// Make this an invocation token with the given policy.
634+
#[clap(long, group = "body")]
635+
pub invocation: Option<String>,
636+
637+
/// Make this a delegation token with the given arguments.
638+
#[clap(long, group = "body")]
639+
pub delegation: Option<String>,
640+
}
641+
585642
/// A nilauth command.
586643
#[derive(Subcommand)]
587644
pub enum NilauthCommand {
@@ -597,6 +654,9 @@ pub enum NilauthCommand {
597654

598655
/// Check whether a token is revoked.
599656
CheckRevoked(CheckRevokedArgs),
657+
658+
/// Get information about the server.
659+
About,
600660
}
601661

602662
/// A nilauth subscription command.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
use super::HandlerResult;
2+
use crate::{
3+
args::{
4+
ContextCommand, IdentitiesCommand, NetworksCommand, ShowContextArgs, ShowIdentityArgs, ShowNetworkArgs,
5+
UseContextArgs,
6+
},
7+
context::ContextConfig,
8+
handlers::{identities::IdentitiesHandler, networks::NetworksHandler},
9+
serialize::SerializeAsAny,
10+
};
11+
use anyhow::bail;
12+
use serde::Serialize;
13+
use std::collections::HashMap;
14+
use tools_config::{identities::Identity, networks::NetworkConfig, ToolConfig};
15+
16+
pub struct ContextHandler;
17+
18+
impl ContextHandler {
19+
pub fn handle(command: ContextCommand) -> HandlerResult {
20+
match command {
21+
ContextCommand::Use(args) => Self::use_context(args),
22+
ContextCommand::Show(ShowContextArgs { verbose: true }) => Self::show_detailed(),
23+
ContextCommand::Show(ShowContextArgs { verbose: false }) => Self::show(),
24+
}
25+
}
26+
27+
pub fn use_context(args: UseContextArgs) -> HandlerResult {
28+
#[derive(Serialize)]
29+
struct Output {
30+
identity: String,
31+
network: String,
32+
}
33+
34+
let UseContextArgs { identity, network }: UseContextArgs = args;
35+
if let Err(e) = Identity::read_from_config(&identity) {
36+
bail!("Invalid identity: {e}");
37+
}
38+
if let Err(e) = NetworkConfig::read_from_config(&network) {
39+
bail!("Invalid network: {e}");
40+
}
41+
let config = ContextConfig { identity: identity.clone(), network: network.clone() };
42+
config.store()?;
43+
Ok(Box::new(Output { identity, network }))
44+
}
45+
46+
pub fn show() -> HandlerResult {
47+
#[derive(Serialize)]
48+
struct Output {
49+
identity: String,
50+
network: String,
51+
}
52+
53+
let Some(context) = ContextConfig::load() else {
54+
return Ok(Box::new(HashMap::<(), ()>::new()));
55+
};
56+
let ContextConfig { identity, network } = context;
57+
Ok(Box::new(Output { identity, network }))
58+
}
59+
60+
pub fn show_detailed() -> HandlerResult {
61+
#[derive(Serialize)]
62+
struct Output {
63+
identity: Box<dyn SerializeAsAny>,
64+
network: Box<dyn SerializeAsAny>,
65+
}
66+
67+
let Some(context) = ContextConfig::load() else {
68+
return Ok(Box::new(HashMap::<(), ()>::new()));
69+
};
70+
let ContextConfig { identity, network } = context;
71+
let identity = IdentitiesHandler::handle(IdentitiesCommand::Show(ShowIdentityArgs { name: identity }))?;
72+
let network = NetworksHandler::handle(NetworksCommand::Show(ShowNetworkArgs { name: network }))?;
73+
Ok(Box::new(Output { identity, network }))
74+
}
75+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use super::{open_in_editor, HandlerResult};
2+
use crate::{
3+
args::{
4+
AddIdentityArgs, EditIdentityArgs, IdentitiesCommand, IdentityGenArgs, RemoveIdentityArgs, ShowIdentityArgs,
5+
},
6+
serialize::NoOutput,
7+
};
8+
use anyhow::{bail, Result};
9+
use nillion_client::{Ed25519SigningKey, Secp256k1SigningKey, UserId};
10+
use serde::Serialize;
11+
use serde_with::{serde_as, DisplayFromStr};
12+
use std::fs;
13+
use tools_config::{
14+
identities::{Identity, Kind},
15+
NamedConfig, ToolConfig,
16+
};
17+
use tracing::info;
18+
use user_keypair::SigningKey;
19+
20+
pub struct IdentitiesHandler;
21+
22+
impl IdentitiesHandler {
23+
pub fn handle(command: IdentitiesCommand) -> HandlerResult {
24+
match command {
25+
IdentitiesCommand::Add(args) => Self::add(args),
26+
IdentitiesCommand::Edit(args) => Self::edit(args),
27+
IdentitiesCommand::List => Self::list(),
28+
IdentitiesCommand::Show(args) => Self::show(args),
29+
IdentitiesCommand::Remove(args) => Self::remove(args),
30+
}
31+
}
32+
33+
pub fn identities_gen(args: IdentityGenArgs) -> HandlerResult {
34+
info!("Generating user identities");
35+
let user_key = Self::generate_key(args.seed, &args.curve)?.as_bytes();
36+
let identity = Identity { private_key: user_key, kind: args.curve };
37+
identity.write_to_file(&args.name)?;
38+
Ok(Box::new(format!("Identity {} generated", args.name)))
39+
}
40+
41+
fn add(args: AddIdentityArgs) -> HandlerResult {
42+
let kind = Kind::Secp256k1;
43+
let user_key = Self::generate_key(args.seed, &kind)?.as_bytes();
44+
let identity = Identity { private_key: user_key, kind };
45+
identity.write_to_file(&args.name)?;
46+
Ok(Box::new(format!("Identity {} added", args.name)))
47+
}
48+
49+
fn list() -> HandlerResult {
50+
#[derive(Serialize)]
51+
struct Output {
52+
identities: Vec<String>,
53+
}
54+
55+
let identities = Identity::read_all()?;
56+
let identities = identities.into_iter().map(|NamedConfig { name, .. }| name).collect::<Vec<_>>();
57+
Ok(Box::new(Output { identities }))
58+
}
59+
60+
fn edit(args: EditIdentityArgs) -> HandlerResult {
61+
let EditIdentityArgs { name } = args;
62+
let path = Identity::config_path(&name)?;
63+
if !fs::exists(&path).unwrap_or(false) {
64+
bail!("identity file does not exist");
65+
}
66+
open_in_editor(&path)?;
67+
Ok(Box::new(NoOutput))
68+
}
69+
70+
fn show(args: ShowIdentityArgs) -> HandlerResult {
71+
#[serde_as]
72+
#[derive(Serialize)]
73+
struct Output {
74+
#[serde_as(as = "DisplayFromStr")]
75+
user_id: UserId,
76+
77+
#[serde(serialize_with = "hex::serde::serialize")]
78+
public_key: Vec<u8>,
79+
80+
#[serde_as(as = "DisplayFromStr")]
81+
kind: Kind,
82+
}
83+
84+
let Identity { private_key: user_key, kind } = Identity::read_from_config(&args.name)?;
85+
let private_key = match kind {
86+
Kind::Ed25519 => SigningKey::from(Ed25519SigningKey::try_from(user_key.as_ref())?),
87+
Kind::Secp256k1 => SigningKey::from(Secp256k1SigningKey::try_from(user_key.as_ref())?),
88+
};
89+
let public_key = private_key.public_key().as_bytes();
90+
let user_id = UserId::from_bytes(&public_key);
91+
Ok(Box::new(Output { public_key, user_id, kind }))
92+
}
93+
94+
fn remove(args: RemoveIdentityArgs) -> HandlerResult {
95+
Identity::remove_config(&args.name)?;
96+
Ok(Box::new(format!("Identity {} removed", args.name)))
97+
}
98+
99+
fn generate_key(seed: Option<String>, curve: &Kind) -> Result<SigningKey> {
100+
let key = match (seed, curve) {
101+
(Some(seed), Kind::Ed25519) => {
102+
info!("Generating ed25519 key using provided seed");
103+
Ed25519SigningKey::from_seed(&seed).into()
104+
}
105+
(None, Kind::Ed25519) => {
106+
info!("Generating random ed25519 key");
107+
Ed25519SigningKey::generate().into()
108+
}
109+
(Some(seed), Kind::Secp256k1) => {
110+
info!("Generating secp256k1 key using provided seed");
111+
Secp256k1SigningKey::try_from_seed(&seed)?.into()
112+
}
113+
(None, Kind::Secp256k1) => {
114+
info!("Generating random secp256k1 key");
115+
Secp256k1SigningKey::generate().into()
116+
}
117+
};
118+
Ok(key)
119+
}
120+
}

tools/nillion/src/handlers/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use crate::serialize::SerializeAsAny;
2+
use anyhow::{anyhow, Result};
3+
use std::{env, path::Path};
4+
5+
pub mod context;
6+
pub mod identities;
7+
pub mod networks;
8+
pub mod nilauth;
9+
pub mod nilvm;
10+
pub mod nuc;
11+
12+
pub type HandlerResult = Result<Box<dyn SerializeAsAny>>;
13+
14+
pub(crate) fn open_in_editor(path: &Path) -> Result<()> {
15+
// Use the editor specified in VISUAL, otherwise EDITOR, otherwise default to vim.
16+
let editor = env::var("VISUAL").or_else(|_| env::var("EDITOR")).unwrap_or_else(|_| "vim".into());
17+
let mut child =
18+
std::process::Command::new(&editor).arg(path).spawn().map_err(|e| anyhow!("failed to run {editor}: {e}"))?;
19+
child.wait().map_err(|e| anyhow!("failed to wait for {editor}: {e}"))?;
20+
Ok(())
21+
}

0 commit comments

Comments
 (0)