fleetforge_signer_gcp_kms/
lib.rs1use 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}