use p256::elliptic_curve::sec1::ToSec1Point;
use rand::Rng;
use serde::{Deserialize, Serialize};
use vta_sdk::keys::KeyType;
use vti_common::error::AppError;
use vti_common::store::KeyspaceHandle;
use zeroize::Zeroize;
const SECRET_PREFIX: &str = "internal:";
#[derive(Serialize, Deserialize)]
struct StoredInternalSecret {
key_type: KeyType,
secret: Vec<u8>,
}
pub async fn generate(
internal_ks: &KeyspaceHandle,
key_id: &str,
key_type: KeyType,
) -> Result<Vec<u8>, AppError> {
if key_id.trim().is_empty() {
return Err(AppError::Validation(
"internal key id must be non-empty".into(),
));
}
if internal_ks
.get_raw(format!("{SECRET_PREFIX}{key_id}"))
.await?
.is_some()
{
return Err(AppError::Conflict(format!(
"internal key `{key_id}` already exists; internal keys are never \
overwritten because the previous key would be unrecoverable"
)));
}
let (mut secret, public) = match key_type {
KeyType::Ed25519 => {
let mut seed = [0u8; 32];
rand::rng().fill_bytes(&mut seed);
let signing = ed25519_dalek::SigningKey::from_bytes(&seed);
seed.zeroize();
let public = signing.verifying_key().to_bytes().to_vec();
(signing.to_bytes().to_vec(), public)
}
KeyType::P256 => {
let mut raw = [0u8; 32];
rand::rng().fill_bytes(&mut raw);
let secret = p256::SecretKey::from_slice(&raw)
.map_err(|e| AppError::Internal(format!("internal P-256 keygen: {e}")))?;
raw.zeroize();
let public = secret.public_key().to_sec1_point(true).as_bytes().to_vec();
(secret.to_bytes().to_vec(), public)
}
KeyType::X25519 => {
return Err(AppError::Validation(
"internal keys are signing keys; X25519 is key-agreement only".into(),
));
}
other => {
return Err(AppError::Validation(format!(
"internal keys do not support {other} yet"
)));
}
};
let record = StoredInternalSecret {
key_type,
secret: secret.clone(),
};
secret.zeroize();
let bytes = serde_json::to_vec(&record)
.map_err(|e| AppError::Internal(format!("serialize internal secret: {e}")))?;
internal_ks
.insert_raw(format!("{SECRET_PREFIX}{key_id}"), bytes)
.await?;
Ok(public)
}
pub(crate) async fn load_for_signing(
internal_ks: &KeyspaceHandle,
key_id: &str,
) -> Result<(KeyType, Vec<u8>), AppError> {
let blob = internal_ks
.get_raw(format!("{SECRET_PREFIX}{key_id}"))
.await?
.ok_or_else(|| AppError::NotFound(format!("internal key `{key_id}` not found")))?;
let record: StoredInternalSecret = serde_json::from_slice(&blob)
.map_err(|e| AppError::Internal(format!("decode internal secret: {e}")))?;
Ok((record.key_type, record.secret))
}
pub async fn sign(
internal_ks: &KeyspaceHandle,
key_id: &str,
payload: &[u8],
) -> Result<Vec<u8>, AppError> {
let (key_type, mut secret) = load_for_signing(internal_ks, key_id).await?;
let result = match key_type {
KeyType::Ed25519 => ed25519_dalek::SigningKey::try_from(secret.as_slice())
.map_err(|e| AppError::Internal(format!("internal Ed25519 key: {e}")))
.map(|k| {
use ed25519_dalek::Signer;
k.sign(payload).to_bytes().to_vec()
}),
KeyType::P256 => affinidi_crypto::p256::sign(&secret, payload)
.map_err(|e| AppError::Internal(format!("internal P-256 sign: {e}"))),
KeyType::X25519 => Err(AppError::Internal(
"an X25519 internal key should never have been stored".into(),
)),
other => Err(AppError::Internal(format!(
"an internal key of type {other} should never have been stored"
))),
};
secret.zeroize();
result
}
pub async fn delete(internal_ks: &KeyspaceHandle, key_id: &str) -> Result<(), AppError> {
internal_ks.remove(format!("{SECRET_PREFIX}{key_id}")).await
}
pub async fn exists(internal_ks: &KeyspaceHandle, key_id: &str) -> Result<bool, AppError> {
Ok(internal_ks
.get_raw(format!("{SECRET_PREFIX}{key_id}"))
.await?
.is_some())
}
#[cfg(test)]
mod tests {
use super::*;
use vti_common::config::StoreConfig;
use vti_common::store::Store;
fn fresh() -> (tempfile::TempDir, Store, KeyspaceHandle) {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(&StoreConfig {
data_dir: dir.path().to_path_buf(),
})
.unwrap();
let ks = store.keyspace(vta_keyspaces::INTERNAL_KEYS).unwrap();
(dir, store, ks)
}
#[tokio::test]
async fn generate_returns_only_the_public_half_and_signs_with_the_private_one() {
let (_d, _s, ks) = fresh();
let public = generate(&ks, "k-internal", KeyType::Ed25519).await.unwrap();
assert_eq!(public.len(), 32, "Ed25519 public key");
let sig = sign(&ks, "k-internal", b"payload").await.unwrap();
assert_eq!(sig.len(), 64, "Ed25519 signature");
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
let vk = VerifyingKey::from_bytes(&public.clone().try_into().unwrap()).unwrap();
vk.verify(b"payload", &Signature::from_slice(&sig).unwrap())
.expect("signature verifies under the advertised public key");
}
#[tokio::test]
async fn generating_over_an_existing_internal_key_is_refused() {
let (_d, _s, ks) = fresh();
generate(&ks, "k1", KeyType::Ed25519).await.unwrap();
let err = generate(&ks, "k1", KeyType::Ed25519).await.unwrap_err();
assert!(
matches!(&err, AppError::Conflict(m) if m.contains("unrecoverable")),
"{err:?}"
);
}
#[tokio::test]
async fn internal_keys_are_independent_of_each_other() {
let (_d, _s, ks) = fresh();
let a = generate(&ks, "a", KeyType::Ed25519).await.unwrap();
let b = generate(&ks, "b", KeyType::Ed25519).await.unwrap();
assert_ne!(a, b, "two internal keys must not share material");
}
#[tokio::test]
async fn p256_internal_keys_sign() {
let (_d, _s, ks) = fresh();
let public = generate(&ks, "p", KeyType::P256).await.unwrap();
assert_eq!(public.len(), 33, "compressed SEC1 point");
let sig = sign(&ks, "p", b"payload").await.unwrap();
assert!(
affinidi_crypto::p256::verify(&public, b"payload", &sig).unwrap(),
"P-256 signature verifies under the advertised public key"
);
}
#[tokio::test]
async fn x25519_internal_keys_are_refused() {
let (_d, _s, ks) = fresh();
let err = generate(&ks, "x", KeyType::X25519).await.unwrap_err();
assert!(
matches!(&err, AppError::Validation(m) if m.contains("key-agreement")),
"{err:?}"
);
}
#[tokio::test]
async fn a_deleted_internal_key_is_gone_for_good() {
let (_d, _s, ks) = fresh();
generate(&ks, "doomed", KeyType::Ed25519).await.unwrap();
assert!(exists(&ks, "doomed").await.unwrap());
delete(&ks, "doomed").await.unwrap();
assert!(!exists(&ks, "doomed").await.unwrap());
assert!(
sign(&ks, "doomed", b"x").await.is_err(),
"a deleted internal key cannot be recovered or reused"
);
}
}