cat_gateway/service/api/documents/common/
mod.rs

1//! A module for placing common structs, functions, and variables across the `document`
2//! endpoint module not specified to a specific endpoint.
3
4use std::collections::HashMap;
5
6use catalyst_signed_doc::CatalystSignedDocument;
7
8use crate::{
9    db::event::{error::NotFoundError, signed_docs::FullSignedDoc},
10    service::common::auth::rbac::token::CatalystRBACTokenV1,
11    settings::Settings,
12};
13
14/// Get document cbor bytes from the database
15pub(crate) async fn get_document_cbor_bytes(
16    document_id: &uuid::Uuid,
17    version: Option<&uuid::Uuid>,
18) -> anyhow::Result<Vec<u8>> {
19    // If doesn't exist in the static templates, try to find it in the database
20    let db_doc = FullSignedDoc::retrieve(document_id, version).await?;
21    Ok(db_doc.raw().to_vec())
22}
23
24/// A struct which implements a
25/// `catalyst_signed_doc::providers::CatalystSignedDocumentProvider` trait
26pub(crate) struct DocProvider;
27
28impl catalyst_signed_doc::providers::CatalystSignedDocumentProvider for DocProvider {
29    async fn try_get_doc(
30        &self,
31        doc_ref: &catalyst_signed_doc::DocumentRef,
32    ) -> anyhow::Result<Option<CatalystSignedDocument>> {
33        let id = doc_ref.id().uuid();
34        let ver = doc_ref.ver().uuid();
35        match get_document_cbor_bytes(&id, Some(&ver)).await {
36            Ok(doc_cbor_bytes) => Ok(Some(doc_cbor_bytes.as_slice().try_into()?)),
37            Err(err) if err.is::<NotFoundError>() => Ok(None),
38            Err(err) => Err(err),
39        }
40    }
41
42    fn future_threshold(&self) -> Option<std::time::Duration> {
43        let signed_doc_cfg = Settings::signed_doc_cfg();
44        Some(signed_doc_cfg.future_threshold())
45    }
46
47    fn past_threshold(&self) -> Option<std::time::Duration> {
48        let signed_doc_cfg = Settings::signed_doc_cfg();
49        Some(signed_doc_cfg.past_threshold())
50    }
51}
52
53impl catalyst_signed_doc_v1::providers::CatalystSignedDocumentProvider for DocProvider {
54    async fn try_get_doc(
55        &self,
56        doc_ref: &catalyst_signed_doc_v1::DocumentRef,
57    ) -> anyhow::Result<Option<catalyst_signed_doc_v1::CatalystSignedDocument>> {
58        let id = doc_ref.id.uuid();
59        let ver = doc_ref.ver.uuid();
60        match get_document_cbor_bytes(&id, Some(&ver)).await {
61            Ok(doc_cbor_bytes) => Ok(Some(doc_cbor_bytes.as_slice().try_into()?)),
62            Err(err) if err.is::<NotFoundError>() => Ok(None),
63            Err(err) => Err(err),
64        }
65    }
66
67    fn future_threshold(&self) -> Option<std::time::Duration> {
68        <Self as catalyst_signed_doc::providers::CatalystSignedDocumentProvider>::future_threshold(
69            self,
70        )
71    }
72
73    fn past_threshold(&self) -> Option<std::time::Duration> {
74        <Self as catalyst_signed_doc::providers::CatalystSignedDocumentProvider>::past_threshold(
75            self,
76        )
77    }
78}
79
80// TODO: make the struct to support multi sigs validation
81/// A struct which implements a
82/// `catalyst_signed_doc::providers::CatalystSignedDocumentProvider` trait
83pub(crate) struct VerifyingKeyProvider(
84    HashMap<catalyst_signed_doc::CatalystId, ed25519_dalek::VerifyingKey>,
85);
86
87impl catalyst_signed_doc::providers::VerifyingKeyProvider for VerifyingKeyProvider {
88    async fn try_get_key(
89        &self,
90        kid: &catalyst_signed_doc::CatalystId,
91    ) -> anyhow::Result<Option<ed25519_dalek::VerifyingKey>> {
92        Ok(self.0.get(kid).copied())
93    }
94}
95
96impl VerifyingKeyProvider {
97    /// Attempts to construct an instance of `Self` by validating and resolving a list of
98    /// Catalyst Document KIDs against a provided RBAC token.
99    ///
100    /// This method performs the following steps:
101    /// 1. Verifies that only a single KID is provided with a document (as multi-signature
102    ///    is currently unsupported).
103    /// 2. Verifies that **all** provided KIDs match the Catalyst ID from the RBAC token.
104    /// 3. Verifies that each provided KID is actually a signing key.
105    /// 4. Extracts the role index and rotation from each KID.
106    /// 5. Retrieves the latest signing public key and rotation state associated with the
107    ///    role for each KID from the registration chain.
108    /// 6. Verifies that each provided KID uses its latest rotation.
109    /// 7. Collects and returns a vector of tuples containing the KID, and its latest
110    ///    signing key.
111    ///
112    /// # Errors
113    ///
114    /// Returns an `anyhow::Error` if:
115    /// - Any KID's short Catalyst ID does not match the one in the token.
116    /// - Indexed registration queries or chain building fail.
117    /// - The KID's role index and rotation parsing fails.
118    /// - The KID is not a singing key.
119    /// - The latest signing key for a required role cannot be found.
120    /// - The KID is not using the latest rotation.
121    pub(crate) async fn try_from_kids(
122        token: &mut CatalystRBACTokenV1,
123        kids: &[catalyst_signed_doc::CatalystId],
124    ) -> anyhow::Result<Self> {
125        if kids.len() > 1 {
126            anyhow::bail!("Multi-signature document is currently unsupported");
127        }
128
129        if kids
130            .iter()
131            .any(|kid| kid.as_short_id() != token.catalyst_id().as_short_id())
132        {
133            anyhow::bail!("RBAC Token CatID does not match with the document KIDs");
134        }
135
136        let Some(reg_chain) = token.reg_chain().await? else {
137            anyhow::bail!("Failed to retrieve a registration from corresponding Catalyst ID");
138        };
139
140        let result = kids.iter().map(|kid| {
141            if !kid.is_signature_key() {
142                anyhow::bail!("Invalid KID {kid}: KID must be a signing key not an encryption key");
143            }
144
145            let (kid_role_index, kid_rotation) = kid.role_and_rotation();
146            let (latest_pk, rotation) = reg_chain
147                .get_latest_signing_pk_for_role(&kid_role_index)
148                .ok_or_else(|| {
149                anyhow::anyhow!(
150                    "Failed to get last signing key for the proposer role for {kid} Catalyst ID"
151                )
152            })?;
153
154            if rotation != kid_rotation {
155                anyhow::bail!("Invalid KID {kid}: KID's rotation ({kid_rotation}) is not the latest rotation ({rotation})");
156            }
157
158            Ok((kid.clone(), latest_pk))
159        })
160        .collect::<Result<_, _>>()?;
161
162        Ok(Self(result))
163    }
164}