use crate::proto::FormatError;
use crate::proto::api::{Scope, SignedBundle, report_signing_input};
use crate::proto::codec::TokenString;
use crate::proto::frame::label;
use crate::proto::ids::{Key32, Sig64, TokenId, VaultId};
use crate::proto::integrity::IntegrityError;
use crate::proto::sig::{SigActor, SigParams, SignedRequest, signing_input};
use ed25519_dalek::{Signer, SigningKey};
use zeroize::Zeroizing;
use crate::keys::KeyError;
use crate::keys::bundle::{Binding, Bundle, expect_scope, open_bundle};
use crate::keys::kdf::{derive32, info};
use crate::keys::random::random_bytes;
pub struct TokenKeys {
id: TokenId,
vault_id: VaultId,
secret: Zeroizing<[u8; 32]>,
auth: SigningKey,
auth_pub: Key32,
boxing: crypto_box::SecretKey,
box_pub: Key32,
}
impl std::fmt::Debug for TokenKeys {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TokenKeys")
.field("id", &self.id)
.field("vault_id", &self.vault_id)
.finish_non_exhaustive()
}
}
impl TokenKeys {
pub fn generate(vault_id: VaultId) -> TokenKeys {
TokenKeys::from_parts(
TokenId(*random_bytes::<16>()),
vault_id,
random_bytes::<32>(),
)
}
pub fn from_parts(id: TokenId, vault_id: VaultId, secret: Zeroizing<[u8; 32]>) -> TokenKeys {
let auth_seed = derive32(secret.as_slice(), &info(label::TOKEN_AUTH, &id.0));
let auth = SigningKey::from_bytes(&auth_seed);
let box_secret = derive32(secret.as_slice(), &info(label::TOKEN_BOX, &id.0));
let boxing = crypto_box::SecretKey::from_bytes(*box_secret);
TokenKeys {
auth_pub: Key32(auth.verifying_key().to_bytes()),
box_pub: Key32(*boxing.public_key().as_bytes()),
id,
vault_id,
secret,
auth,
boxing,
}
}
pub fn parse(s: &str) -> Result<TokenKeys, FormatError> {
let t = TokenString::parse(s)?;
Ok(TokenKeys::from_parts(t.id, t.vault_id, t.secret))
}
pub fn token_string(&self) -> Zeroizing<String> {
TokenString::encode(&self.id, &self.vault_id, &self.secret)
}
pub fn id(&self) -> TokenId {
self.id
}
pub fn vault_id(&self) -> VaultId {
self.vault_id
}
pub fn auth_pub(&self) -> Key32 {
self.auth_pub
}
pub fn box_pub(&self) -> Key32 {
self.box_pub
}
pub fn sign_request(&self, request: &SignedRequest<'_>, ts: i64) -> SigParams {
let nonce = *random_bytes::<16>();
let actor = SigActor::Token(self.id);
let input = signing_input(&actor, &self.vault_id, ts, &nonce, request);
SigParams {
actor,
vault_id: self.vault_id,
ts,
nonce,
sig: self.auth.sign(&input).to_bytes(),
}
}
#[cfg(feature = "vectors")]
pub(crate) fn sign_raw(&self, message: &[u8]) -> Sig64 {
Sig64(self.auth.sign(message).to_bytes())
}
pub fn sign_report(&self, ts: i64) -> Sig64 {
Sig64(
self.auth
.sign(&report_signing_input(&self.id, ts))
.to_bytes(),
)
}
pub fn open_bundle(
&self,
signed: &SignedBundle,
owner_sign_pub: &Key32,
scope: Scope,
generation: u32,
) -> Result<Bundle, KeyError> {
if VaultId::from_owner_sign_pub(owner_sign_pub) != self.vault_id {
return Err(IntegrityError::KeyMismatch("owner signing key").into());
}
signed.verify(
owner_sign_pub,
&self.vault_id,
&self.id,
scope.code(),
generation,
)?;
let bundle = open_bundle(
&self.boxing,
&signed.sealed.0,
&Binding {
vault_id: self.vault_id,
token_id: self.id,
generation,
},
)?;
expect_scope(&bundle, scope)?;
Ok(bundle)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proto::integrity::verify_ed25519;
#[test]
fn string_roundtrip_preserves_keys_and_vault() {
let t = TokenKeys::generate(VaultId([3; 16]));
let s = t.token_string();
assert!(s.starts_with("gvt1_"));
let back = TokenKeys::parse(&s).unwrap();
assert_eq!(back.id(), t.id());
assert_eq!(back.vault_id(), VaultId([3; 16]));
assert_eq!(back.box_pub(), t.box_pub());
assert_eq!(back.auth_pub(), t.auth_pub());
}
#[test]
fn auth_and_box_keys_are_independent_and_id_bound() {
let secret = Zeroizing::new([5u8; 32]);
let a = TokenKeys::from_parts(TokenId([1; 16]), VaultId([0; 16]), secret.clone());
let b = TokenKeys::from_parts(TokenId([2; 16]), VaultId([0; 16]), secret);
assert_ne!(a.box_pub(), b.box_pub());
assert_ne!(a.auth_pub(), b.auth_pub());
assert_ne!(a.auth_pub(), a.box_pub());
assert!(!format!("{a:?}").contains("secret"));
}
#[test]
fn requests_and_reports_verify_under_the_auth_key() {
let t = TokenKeys::generate(VaultId([4; 16]));
let req = SignedRequest::new("GET", "/v1/vault", b"");
let p = t.sign_request(&req, 100);
assert_eq!(p.actor, SigActor::Token(t.id()));
assert_eq!(p.vault_id, t.vault_id());
assert!(
verify_ed25519(
&t.auth_pub(),
&p.signing_input(&req),
&Sig64(p.sig),
"request"
)
.is_ok()
);
let report = t.sign_report(100);
assert!(
verify_ed25519(
&t.auth_pub(),
&report_signing_input(&t.id(), 100),
&report,
"report"
)
.is_ok()
);
assert!(
verify_ed25519(
&t.auth_pub(),
&report_signing_input(&t.id(), 101),
&report,
"report"
)
.is_err()
);
}
#[test]
fn a_token_of_another_version_is_refused() {
let other = crate::proto::codec::encode_checked("gvt2_", &[1u8; 48]);
assert!(matches!(
TokenKeys::parse(&other),
Err(FormatError::UnknownVersion { found: '2' })
));
}
}