use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD as BASE64;
use serde::{Deserialize, Serialize};
use super::error::SealedTransferError;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "openapi", schema(as = SealedBootstrapRequest))]
#[serde(deny_unknown_fields)]
pub struct BootstrapRequest {
pub version: u8,
pub client_did: String,
pub nonce: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub claim_signature: Option<String>,
}
const CLAIM_SIGNATURE_DOMAIN: &[u8] = b"vti/bootstrap-claim/v1\0";
pub fn claim_signing_input(client_ed25519_pub: &[u8; 32], nonce: &[u8; 16]) -> Vec<u8> {
let mut input = Vec::with_capacity(CLAIM_SIGNATURE_DOMAIN.len() + 32 + 16);
input.extend_from_slice(CLAIM_SIGNATURE_DOMAIN);
input.extend_from_slice(client_ed25519_pub);
input.extend_from_slice(nonce);
input
}
impl BootstrapRequest {
pub fn new(client_ed25519_pub: [u8; 32], nonce: [u8; 16], label: Option<String>) -> Self {
Self {
version: 1,
client_did: affinidi_crypto::did_key::ed25519_pub_to_did_key(&client_ed25519_pub),
nonce: BASE64.encode(nonce),
label,
claim_signature: None,
}
}
pub fn new_signed_claim(
client_ed25519_seed: &[u8; 32],
nonce: [u8; 16],
label: Option<String>,
) -> Self {
use ed25519_dalek::Signer;
let signing = ed25519_dalek::SigningKey::from_bytes(client_ed25519_seed);
let public = signing.verifying_key().to_bytes();
let signature = signing.sign(&claim_signing_input(&public, &nonce));
Self {
claim_signature: Some(BASE64.encode(signature.to_bytes())),
..Self::new(public, nonce, label)
}
}
pub fn verify_claim_signature(&self) -> Result<(), SealedTransferError> {
let encoded = self
.claim_signature
.as_deref()
.ok_or_else(|| SealedTransferError::Wire("claim_signature is missing".into()))?;
let raw: [u8; 64] = BASE64
.decode(encoded)
.map_err(|e| SealedTransferError::Base64(e.to_string()))?
.try_into()
.map_err(|_| SealedTransferError::Wire("claim_signature must be 64 bytes".into()))?;
let public = self.decode_client_ed25519_pub()?;
let nonce = self.decode_nonce()?;
let key = ed25519_dalek::VerifyingKey::from_bytes(&public)
.map_err(|e| SealedTransferError::Wire(format!("client_did key: {e}")))?;
key.verify_strict(
&claim_signing_input(&public, &nonce),
&ed25519_dalek::Signature::from_bytes(&raw),
)
.map_err(|_| SealedTransferError::Wire("claim_signature does not verify".into()))
}
pub fn decode_client_ed25519_pub(&self) -> Result<[u8; 32], SealedTransferError> {
affinidi_crypto::did_key::did_key_to_ed25519_pub(&self.client_did)
.map_err(|e| SealedTransferError::Wire(format!("client_did: {e}")))
}
pub fn decode_client_x25519_pub(&self) -> Result<[u8; 32], SealedTransferError> {
let ed = self.decode_client_ed25519_pub()?;
affinidi_crypto::did_key::ed25519_pub_to_x25519_bytes(&ed)
.map_err(|e| SealedTransferError::Wire(format!("client_did X25519 derivation: {e}")))
}
pub fn decode_nonce(&self) -> Result<[u8; 16], SealedTransferError> {
let raw = BASE64
.decode(&self.nonce)
.map_err(|e| SealedTransferError::Base64(e.to_string()))?;
raw.try_into()
.map_err(|_| SealedTransferError::Wire("nonce must be 16 bytes".into()))
}
}