feat: add notnet crate and YubikeySource, thread Tailscale into notstrap
- New crates/notnet: TailscaleOptions, ensure_connected(), auth key chain (TS_AUTHKEY env → YubiKey PIV 9d → interactive prompt), install via apt/brew/pacman or official install.sh, peer reachability verification - notsecrets: YubikeySource implementing IdentitySource, reads age key from PIV slot 9c; gated behind optional `yubikey` feature - notstrap: Tailscale step inserted between prerequisites and clone; BootstrapOptions.tailscale controls override/skip; NotstrapConfig gains optional [tailscale] section; dotfiles_repo is now Option<String> (Tailscale gitea_url takes precedence); identity chain adds YubikeySource before Bitwarden - Updated workspace Cargo.toml: notnet added to members and workspace deps - Integration tests: tailscale: Some(None) to skip Tailscale in all test opts
This commit is contained in:
@@ -23,6 +23,11 @@ hmac = "0.12"
|
||||
rand = "0.8"
|
||||
zeroize = "1"
|
||||
curve25519-dalek = "4"
|
||||
yubikey = { version = "0.8", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
yubikey = ["dep:yubikey"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
@@ -18,7 +18,7 @@ pub use ports::IdentitySource;
|
||||
pub use recipients::{
|
||||
Recipient, ScryptRecipient, SshEd25519Recipient, SshRsaRecipient, X25519Recipient,
|
||||
};
|
||||
pub use sources::{BitwardenSource, FileSource, PromptSource};
|
||||
pub use sources::{BitwardenSource, FileSource, PromptSource, YubikeySource};
|
||||
|
||||
/// Try each `IdentitySource` in order; collect all identities that load successfully.
|
||||
///
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
pub mod bitwarden;
|
||||
pub mod file;
|
||||
pub mod prompt;
|
||||
pub mod yubikey;
|
||||
|
||||
pub use crate::ports::IdentitySource;
|
||||
pub use bitwarden::BitwardenSource;
|
||||
pub use file::FileSource;
|
||||
pub use prompt::PromptSource;
|
||||
pub use yubikey::YubikeySource;
|
||||
|
||||
72
crates/notsecrets/src/sources/yubikey.rs
Normal file
72
crates/notsecrets/src/sources/yubikey.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use crate::error::AgeError;
|
||||
use crate::identities::Identity;
|
||||
use crate::ports::IdentitySource;
|
||||
|
||||
/// Reads an age private key from YubiKey PIV slot 9c (Digital Signature slot).
|
||||
///
|
||||
/// Requires the `yubikey` feature to be enabled. When the feature is absent this
|
||||
/// source is compiled out entirely — the struct still exists for type-level use but
|
||||
/// `load()` always returns an error.
|
||||
pub struct YubikeySource;
|
||||
|
||||
impl IdentitySource for YubikeySource {
|
||||
fn name(&self) -> &str {
|
||||
"yubikey-piv-9c"
|
||||
}
|
||||
|
||||
fn load(&self) -> Result<Box<dyn Identity>, AgeError> {
|
||||
load_impl()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "yubikey")]
|
||||
fn load_impl() -> Result<Box<dyn Identity>, AgeError> {
|
||||
use crate::identities::x25519::X25519Identity;
|
||||
use yubikey::{YubiKey, piv};
|
||||
|
||||
let mut yk = YubiKey::open().map_err(|e| AgeError::SourceError {
|
||||
name: "yubikey-piv-9c".to_string(),
|
||||
source: anyhow::anyhow!("cannot open YubiKey: {e}"),
|
||||
})?;
|
||||
|
||||
// Slot 9c — Digital Signature slot, used here to store the age private key.
|
||||
let data =
|
||||
piv::read_object(&mut yk, piv::ObjectId::from(piv::SlotId::Signature)).map_err(|e| {
|
||||
AgeError::SourceError {
|
||||
name: "yubikey-piv-9c".to_string(),
|
||||
source: anyhow::anyhow!("cannot read PIV slot 9c: {e}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let key = std::str::from_utf8(&data)
|
||||
.map_err(|e| AgeError::SourceError {
|
||||
name: "yubikey-piv-9c".to_string(),
|
||||
source: anyhow::anyhow!("PIV slot 9c data is not valid UTF-8: {e}"),
|
||||
})?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if key.is_empty() {
|
||||
return Err(AgeError::SourceError {
|
||||
name: "yubikey-piv-9c".to_string(),
|
||||
source: anyhow::anyhow!("PIV slot 9c is empty"),
|
||||
});
|
||||
}
|
||||
|
||||
let identity = X25519Identity::from_bech32(&key).map_err(|e| AgeError::SourceError {
|
||||
name: "yubikey-piv-9c".to_string(),
|
||||
source: anyhow::anyhow!("slot 9c content is not a valid AGE-SECRET-KEY-1: {e}"),
|
||||
})?;
|
||||
Ok(Box::new(identity))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "yubikey"))]
|
||||
fn load_impl() -> Result<Box<dyn Identity>, AgeError> {
|
||||
Err(AgeError::SourceError {
|
||||
name: "yubikey-piv-9c".to_string(),
|
||||
source: anyhow::anyhow!(
|
||||
"notsecrets was compiled without the `yubikey` feature; \
|
||||
YubikeySource is unavailable"
|
||||
),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user