use std::{fmt, pin::Pin, sync::Arc};
use pubky_common::{
auth::jws::{finish_jws, jws_signing_input},
crypto::{Keypair, PublicKey},
};
use serde::Serialize;
#[doc(hidden)]
#[derive(Debug, thiserror::Error)]
pub enum GrantSigningError {
#[error("Signing key unavailable: {0}")]
KeyUnavailable(String),
#[error("Signing failed: {0}")]
SigningFailed(String),
}
type SigningResult<T> = std::result::Result<T, GrantSigningError>;
#[doc(hidden)]
#[cfg(not(target_arch = "wasm32"))]
pub type BoxSignFuture = Pin<Box<dyn Future<Output = SigningResult<Vec<u8>>> + Send + 'static>>;
#[doc(hidden)]
#[cfg(target_arch = "wasm32")]
pub type BoxSignFuture = Pin<Box<dyn Future<Output = SigningResult<Vec<u8>>> + 'static>>;
#[doc(hidden)]
pub type DelegatedSignFn = Arc<dyn Fn(String) -> BoxSignFuture + Send + Sync + 'static>;
#[derive(Clone)]
pub(crate) enum GrantPopSigner {
Local(Keypair),
Delegated(DelegatedGrantPopSigner),
}
impl fmt::Debug for GrantPopSigner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Local(keypair) => f.debug_tuple("Local").field(&keypair.public_key()).finish(),
Self::Delegated(signer) => f.debug_tuple("Delegated").field(signer).finish(),
}
}
}
impl GrantPopSigner {
pub(crate) fn local(keypair: Keypair) -> Self {
Self::Local(keypair)
}
pub(crate) fn delegated(key_id: String, public_key: PublicKey, sign: DelegatedSignFn) -> Self {
Self::Delegated(DelegatedGrantPopSigner {
key_id,
public_key,
sign,
})
}
pub(crate) fn public_key(&self) -> PublicKey {
match self {
Self::Local(keypair) => keypair.public_key(),
Self::Delegated(signer) => signer.public_key.clone(),
}
}
pub(crate) async fn sign_jws<T: Serialize>(
&self,
typ: &str,
claims: &T,
) -> SigningResult<String> {
let signing_input = jws_signing_input(typ, claims);
match self {
Self::Local(keypair) => {
let signature = keypair.sign(signing_input.as_bytes());
Ok(finish_jws(signing_input, signature.to_bytes()))
}
Self::Delegated(signer) => signer.sign_jws(signing_input).await,
}
}
pub(crate) fn local_secret(&self) -> Option<[u8; 32]> {
match self {
Self::Local(keypair) => Some(keypair.secret()),
Self::Delegated(_) => None,
}
}
pub(crate) fn delegated_state(&self) -> Option<DelegatedGrantPopSigner> {
match self {
Self::Local(_) => None,
Self::Delegated(signer) => Some(signer.clone()),
}
}
}
#[derive(Clone)]
pub struct DelegatedGrantPopSigner {
pub key_id: String,
pub public_key: PublicKey,
sign: DelegatedSignFn,
}
impl fmt::Debug for DelegatedGrantPopSigner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DelegatedGrantPopSigner")
.field("key_id", &self.key_id)
.field("public_key", &self.public_key)
.finish_non_exhaustive()
}
}
impl DelegatedGrantPopSigner {
async fn sign_jws(&self, signing_input: String) -> SigningResult<String> {
let signature = (self.sign)(signing_input.clone()).await?;
Ok(finish_jws(signing_input, signature))
}
}
#[doc(hidden)]
#[cfg(not(target_arch = "wasm32"))]
pub fn delegated_sign_callback<F, Fut>(sign: F) -> DelegatedSignFn
where
F: Fn(String) -> Fut + Send + Sync + 'static,
Fut: Future<Output = SigningResult<Vec<u8>>> + Send + 'static,
{
Arc::new(move |signing_input| Box::pin(sign(signing_input)))
}
#[doc(hidden)]
#[cfg(target_arch = "wasm32")]
pub fn delegated_sign_callback<F, Fut>(sign: F) -> DelegatedSignFn
where
F: Fn(String) -> Fut + Send + Sync + 'static,
Fut: Future<Output = SigningResult<Vec<u8>>> + 'static,
{
Arc::new(move |signing_input| Box::pin(sign(signing_input)))
}
#[cfg(test)]
mod tests {
use pubky_common::auth::{
jws::{POP_JWS_TYP, sign_jws},
pop::PopProofClaims,
};
use super::*;
use crate::actors::auth::grant::credential::now_unix;
#[tokio::test]
async fn local_signer_matches_common_jws_signing() {
let keypair = Keypair::random();
let claims = PopProofClaims {
aud: Keypair::random().public_key(),
gid: pubky_common::auth::jws::GrantId::generate(),
nonce: pubky_common::auth::jws::PopNonce::generate(),
iat: now_unix(),
};
let signer = GrantPopSigner::local(keypair.clone());
let actual = signer.sign_jws(POP_JWS_TYP, &claims).await.unwrap();
let expected = sign_jws(&keypair, POP_JWS_TYP, &claims);
assert_eq!(actual, expected);
}
#[tokio::test]
async fn delegated_signer_sign_value() {
#[derive(serde::Serialize)]
struct DummyClaims;
let claims = DummyClaims {};
let signer = GrantPopSigner::delegated(
"delegated-test-key".into(),
Keypair::random().public_key(),
delegated_sign_callback(|_| async { Ok(vec![0; 64]) }),
);
let signed_jwt = signer.sign_jws("test", &claims).await.unwrap();
let base64_signature = signed_jwt.split('.').nth(2).unwrap();
assert_eq!(
base64_signature,
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"Signature is all zeros which is all 'A' in base64"
);
}
#[tokio::test]
async fn homeserver_proof_keeps_the_existing_authentication_error_contract() {
let signer = GrantPopSigner::delegated(
"missing-key".into(),
Keypair::random().public_key(),
delegated_sign_callback(|_| async {
Err(GrantSigningError::KeyUnavailable("deleted key".into()))
}),
);
let error = super::super::credential::sign_pop_for_grant(
&signer,
&Keypair::random().public_key(),
&pubky_common::auth::jws::GrantId::generate(),
)
.await
.unwrap_err();
assert!(matches!(error,
crate::Error::Authentication(crate::errors::AuthError::Validation(message))
if message.contains("deleted key")));
}
}