fleetforge_signer_gcp_kms/
lib.rs

1use anyhow::{anyhow, Context, Result};
2use base64::engine::general_purpose::STANDARD as BASE64;
3use base64::Engine as _;
4use fleetforge_trust::{
5    digest_for_algorithm, jwk_from_gcp_public_key, normalize_ecdsa_signature, Jwk,
6    SignatureEnvelope, Signer, SigningAlgorithm,
7};
8use gcp_auth::AuthenticationManager;
9use once_cell::sync::OnceCell;
10use reqwest::Client;
11use serde::{Deserialize, Serialize};
12use std::sync::{Arc, Mutex};
13use tokio::runtime::Runtime;
14
15const KMS_SCOPE: &str = "https://www.googleapis.com/auth/cloudkms";
16
17#[derive(Clone, Debug, Serialize, Deserialize)]
18pub struct GcpKmsSdkSignerConfig {
19    pub key_resource: String,
20    pub algorithm: SigningAlgorithm,
21    pub public_key: Option<Jwk>,
22}
23
24impl GcpKmsSdkSignerConfig {
25    pub fn kid(&self) -> String {
26        format!("gcp-kms:{}", self.key_resource)
27    }
28}
29
30pub struct GcpKmsSdkSigner {
31    client: Client,
32    auth: AuthenticationManager,
33    runtime: Arc<Mutex<Runtime>>,
34    key_resource: String,
35    kid: String,
36    algorithm: SigningAlgorithm,
37    public_key: Option<Jwk>,
38    fetched_public_key: OnceCell<Jwk>,
39    signing_algorithm_flag: Option<&'static str>,
40}
41
42impl GcpKmsSdkSigner {
43    pub fn new(config: GcpKmsSdkSignerConfig) -> Result<Self> {
44        let runtime = Arc::new(Mutex::new(
45            Runtime::new().context("failed to create tokio runtime")?,
46        ));
47        let auth = AuthenticationManager::new().context("failed to initialize GCP auth manager")?;
48        let client = Client::builder()
49            .build()
50            .context("failed to build reqwest client")?;
51        let signing_algorithm_flag = if config.algorithm.is_edwards() {
52            Some("ec-sign-ed25519")
53        } else {
54            None
55        };
56        Ok(Self {
57            client,
58            auth,
59            runtime,
60            key_resource: config.key_resource.clone(),
61            kid: config.kid(),
62            algorithm: config.algorithm,
63            public_key: config.public_key,
64            fetched_public_key: OnceCell::new(),
65            signing_algorithm_flag,
66        })
67    }
68
69    fn block_on<F, T>(&self, fut: F) -> T
70    where
71        F: std::future::Future<Output = T>,
72    {
73        self.runtime.lock().expect("runtime poisoned").block_on(fut)
74    }
75
76    fn auth_header(&self) -> Result<String> {
77        let token = self
78            .block_on(self.auth.get_token(&[KMS_SCOPE]))
79            .context("failed to fetch GCP access token")?;
80        Ok(format!("Bearer {}", token.as_str()))
81    }
82
83    fn fetch_public_key(&self) -> Result<Jwk> {
84        #[derive(Deserialize)]
85        struct GetPublicKeyResponse {
86            pem: String,
87        }
88        let url = format!(
89            "https://cloudkms.googleapis.com/v1/{}:getPublicKey",
90            self.key_resource
91        );
92        let token = self.auth_header()?;
93        let response: GetPublicKeyResponse = self.block_on(async {
94            let resp = self
95                .client
96                .get(&url)
97                .header("Authorization", &token)
98                .send()
99                .await
100                .context("gcp get public key request failed")?;
101            if !resp.status().is_success() {
102                let body = resp.text().await.unwrap_or_default();
103                return Err(anyhow!("gcp get public key failed: {}", body));
104            }
105            resp.json::<GetPublicKeyResponse>()
106                .await
107                .context("failed to parse gcp public key response")
108        })?;
109        jwk_from_gcp_public_key(
110            &response.pem,
111            &self.kid,
112            &self.algorithm,
113            "configure FLEETFORGE_*_SIGNER_PUBLIC_KEY to skip fetching",
114        )
115    }
116
117    fn gcp_sign(&self, payload: &[u8]) -> Result<Vec<u8>> {
118        #[derive(Serialize)]
119        struct DigestPayload<'a> {
120            #[serde(skip_serializing_if = "Option::is_none")]
121            digest: Option<DigestFields<'a>>,
122            #[serde(skip_serializing_if = "Option::is_none")]
123            data: Option<String>,
124        }
125        #[derive(Serialize)]
126        struct DigestFields<'a> {
127            #[serde(skip_serializing_if = "Option::is_none")]
128            sha256: Option<&'a str>,
129            #[serde(skip_serializing_if = "Option::is_none")]
130            sha384: Option<&'a str>,
131            #[serde(skip_serializing_if = "Option::is_none")]
132            sha512: Option<&'a str>,
133        }
134        #[derive(Deserialize)]
135        struct SignResponse {
136            signature: String,
137        }
138        let url = format!(
139            "https://cloudkms.googleapis.com/v1/{}:asymmetricSign",
140            self.key_resource
141        );
142        let payload_body = if self.algorithm.is_edwards() {
143            DigestPayload {
144                digest: None,
145                data: Some(BASE64.encode(payload)),
146            }
147        } else {
148            let digest = digest_for_algorithm(&self.algorithm, payload)?;
149            let encoded = BASE64.encode(digest);
150            let mut fields = DigestFields {
151                sha256: None,
152                sha384: None,
153                sha512: None,
154            };
155            match self.algorithm.as_str() {
156                "ES256" | "PS256" | "RS256" => fields.sha256 = Some(&encoded),
157                "ES384" | "PS384" | "RS384" => fields.sha384 = Some(&encoded),
158                _ => fields.sha512 = Some(&encoded),
159            }
160            DigestPayload {
161                digest: Some(fields),
162                data: None,
163            }
164        };
165        let token = self.auth_header()?;
166        let body = self.block_on(async {
167            let resp = self
168                .client
169                .post(&url)
170                .header("Authorization", &token)
171                .json(&payload_body)
172                .send()
173                .await
174                .context("gcp asymmetricSign request failed")?;
175            if !resp.status().is_success() {
176                let body = resp.text().await.unwrap_or_default();
177                return Err(anyhow!("gcp asymmetricSign failed: {}", body));
178            }
179            resp.json::<SignResponse>()
180                .await
181                .context("failed to parse gcp asymmetricSign response")
182        })?;
183        BASE64
184            .decode(body.signature.as_bytes())
185            .context("invalid gcp signature output")
186    }
187
188    fn gcp_verify(&self, payload: &[u8], signature: &[u8]) -> Result<bool> {
189        #[derive(Serialize)]
190        struct DigestPayload<'a> {
191            #[serde(skip_serializing_if = "Option::is_none")]
192            digest: Option<DigestFields<'a>>,
193            #[serde(skip_serializing_if = "Option::is_none")]
194            data: Option<String>,
195            signature: String,
196        }
197        #[derive(Serialize)]
198        struct DigestFields<'a> {
199            #[serde(skip_serializing_if = "Option::is_none")]
200            sha256: Option<&'a str>,
201            #[serde(skip_serializing_if = "Option::is_none")]
202            sha384: Option<&'a str>,
203            #[serde(skip_serializing_if = "Option::is_none")]
204            sha512: Option<&'a str>,
205        }
206        #[derive(Deserialize)]
207        struct VerifyResponse {
208            verified: bool,
209        }
210        let url = format!(
211            "https://cloudkms.googleapis.com/v1/{}:asymmetricVerify",
212            self.key_resource
213        );
214        let signature_b64 = BASE64.encode(signature);
215        let payload_body = if self.algorithm.is_edwards() {
216            DigestPayload {
217                digest: None,
218                data: Some(BASE64.encode(payload)),
219                signature: signature_b64,
220            }
221        } else {
222            let digest = digest_for_algorithm(&self.algorithm, payload)?;
223            let encoded = BASE64.encode(digest);
224            let mut fields = DigestFields {
225                sha256: None,
226                sha384: None,
227                sha512: None,
228            };
229            match self.algorithm.as_str() {
230                "ES256" | "PS256" | "RS256" => fields.sha256 = Some(&encoded),
231                "ES384" | "PS384" | "RS384" => fields.sha384 = Some(&encoded),
232                _ => fields.sha512 = Some(&encoded),
233            }
234            DigestPayload {
235                digest: Some(fields),
236                data: None,
237                signature: signature_b64,
238            }
239        };
240        let token = self.auth_header()?;
241        let verified = self.block_on(async {
242            let resp = self
243                .client
244                .post(&url)
245                .header("Authorization", &token)
246                .json(&payload_body)
247                .send()
248                .await
249                .context("gcp asymmetricVerify request failed")?;
250            if !resp.status().is_success() {
251                let body = resp.text().await.unwrap_or_default();
252                return Err(anyhow!("gcp asymmetricVerify failed: {}", body));
253            }
254            let parsed = resp
255                .json::<VerifyResponse>()
256                .await
257                .context("failed to parse gcp verify response")?;
258            Ok(parsed.verified)
259        })?;
260        Ok(verified)
261    }
262}
263
264impl Signer for GcpKmsSdkSigner {
265    fn algorithm(&self) -> SigningAlgorithm {
266        self.algorithm.clone()
267    }
268
269    fn key_id(&self) -> &str {
270        &self.kid
271    }
272
273    fn public_key_jwk(&self) -> Result<Jwk> {
274        if let Some(jwk) = &self.public_key {
275            return Ok(jwk.clone());
276        }
277        if let Some(jwk) = self.fetched_public_key.get() {
278            return Ok(jwk.clone());
279        }
280        let jwk = self.fetch_public_key()?;
281        let _ = self.fetched_public_key.set(jwk.clone());
282        Ok(jwk)
283    }
284
285    fn sign(&self, payload: &[u8]) -> Result<SignatureEnvelope> {
286        let mut signature = self.gcp_sign(payload)?;
287        signature = normalize_ecdsa_signature(&signature, &self.algorithm)?;
288        Ok(SignatureEnvelope {
289            algorithm: self.algorithm(),
290            signature,
291            key_id: self.kid.clone(),
292            public_key: Some(self.public_key_jwk()?),
293        })
294    }
295
296    fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<bool> {
297        self.gcp_verify(payload, signature)
298    }
299}