Skip to main content

cratestack_cose/auth/
service_signer.rs

1//! `ServiceSigningKey` -> [`CoseSigner`].
2
3use std::fmt;
4
5use cratestack_auth::ServiceSigningKey;
6use cratestack_core::CratestackError;
7use ed25519_dalek::Signer as _;
8use ed25519_dalek::ed25519::signature::MultipartSigner as _;
9
10use crate::alg::CoseAlg;
11use crate::keys::{CoseSigner, CoseVerifyKey};
12use crate::thumbprint::KID_LEN;
13
14/// A service's `cratestack_auth::ServiceSigningKey` signing COSE_Sign1
15/// messages with Ed25519 (`-19`).
16///
17/// **Two different key ids.** [`ServiceSigningKey::kid`] is the human JWKS
18/// label the service chose (`"vendor-service-v1"`), which is what its
19/// JWTs carry. The COSE `kid` this signer puts on the wire is something
20/// else: the first 8 bytes of the RFC 9679 thumbprint of the key's public
21/// half (ยง3), computed here from the key, never from the label. A
22/// verifier resolves this signer by that thumbprint prefix (for example
23/// with a `StaticVerifierResolver` holding [`verify_key`](Self::verify_key)),
24/// so a label that is renamed, reused or shared between two keys cannot
25/// make one key verify as another.
26///
27/// It holds the `ServiceSigningKey` itself (whose secret sits behind an
28/// `Arc`) rather than copying the seed into an
29/// [`Ed25519Signer`](crate::Ed25519Signer), so the secret stays in one
30/// place. Its signatures are byte-identical to that signer's for the same
31/// seed, streaming included (`tests/auth_service_signer.rs` checks it).
32#[derive(Clone)]
33pub struct ServiceKeySigner {
34    key: ServiceSigningKey,
35    kid: [u8; KID_LEN],
36}
37
38impl ServiceKeySigner {
39    pub fn new(key: ServiceSigningKey) -> Self {
40        let kid = CoseVerifyKey::from_ed25519(key.signing_key().verifying_key()).kid();
41        Self { key, kid }
42    }
43
44    /// The matching verification key, for a peer's resolver.
45    pub fn verify_key(&self) -> CoseVerifyKey {
46        CoseVerifyKey::from_ed25519(self.key.signing_key().verifying_key())
47    }
48
49    /// The wrapped service identity (its issuer, JWKS label and JWKS).
50    pub fn service_key(&self) -> &ServiceSigningKey {
51        &self.key
52    }
53}
54
55impl From<ServiceSigningKey> for ServiceKeySigner {
56    fn from(key: ServiceSigningKey) -> Self {
57        Self::new(key)
58    }
59}
60
61impl fmt::Debug for ServiceKeySigner {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        f.debug_struct("ServiceKeySigner")
64            .field("issuer", &self.key.issuer())
65            .field("jwks_kid", &self.key.kid())
66            .field("cose_kid", &self.kid)
67            .finish_non_exhaustive()
68    }
69}
70
71#[async_trait::async_trait]
72impl CoseSigner for ServiceKeySigner {
73    fn alg(&self) -> CoseAlg {
74        CoseAlg::Ed25519
75    }
76
77    fn kid(&self) -> &[u8] {
78        &self.kid
79    }
80
81    async fn sign(&self, to_be_signed: &[u8]) -> Result<Vec<u8>, CratestackError> {
82        Ok(self
83            .key
84            .signing_key()
85            .sign(to_be_signed)
86            .to_bytes()
87            .to_vec())
88    }
89
90    /// Both PureEdDSA passes over the pieces, exactly as
91    /// [`Ed25519Signer`](crate::Ed25519Signer) does.
92    fn sign_chunks(&self, to_be_signed: &[&[u8]]) -> Option<Result<Vec<u8>, CratestackError>> {
93        Some(
94            self.key
95                .signing_key()
96                .try_multipart_sign(to_be_signed)
97                .map(|signature| signature.to_bytes().to_vec())
98                .map_err(|_| CratestackError::Internal("Ed25519 signing failed".to_owned())),
99        )
100    }
101}