mz_license_keys/
signing.rs1use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
12use anyhow::anyhow;
13use aws_lc_rs::digest;
14use aws_sdk_kms::{
15 primitives::Blob,
16 types::{MessageType, SigningAlgorithmSpec},
17};
18use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
19use jsonwebtoken::{Algorithm, Header};
20use pem::Pem;
21use serde::Serialize;
22use uuid::Uuid;
23
24use crate::{
25 ExpirationBehavior, INTERMEDIATE_HEADER, INTERMEDIATE_TYP, ISSUER, IntermediatePayload, Payload,
26};
27
28const VERSION: u64 = 1;
29
30pub async fn get_pubkey_pem(client: &aws_sdk_kms::Client, key_id: &str) -> anyhow::Result<String> {
31 let pubkey = get_pubkey(client, key_id).await?;
32 let pem = Pem::new("PUBLIC KEY", pubkey);
33 Ok(pem.to_string())
34}
35
36#[allow(clippy::too_many_arguments)]
41pub async fn make_license_key(
42 client: &aws_sdk_kms::Client,
43 key_id: &str,
44 intermediate_jwt: Option<String>,
45 validity: Duration,
46 organization_id: String,
47 environment_id: String,
48 max_credit_consumption_rate: f64,
49 allow_credit_consumption_override: bool,
50 expiration_behavior: ExpirationBehavior,
51 entitlements: Vec<String>,
52) -> anyhow::Result<String> {
53 let now = SystemTime::now();
54 let expiration = now + validity;
55 let payload = Payload {
56 sub: organization_id,
57 exp: format_time(&expiration),
58 nbf: format_time(&now),
59 iss: ISSUER.to_string(),
60 aud: environment_id,
61 iat: format_time(&now),
62 jti: Uuid::new_v4().to_string(),
63 version: VERSION,
64 max_credit_consumption_rate,
65 allow_credit_consumption_override,
66 expiration_behavior,
67 entitlements,
68 };
69 let mut header = Header::new(Algorithm::PS256);
70 header.typ = Some("JWT".to_string());
71 if let Some(intermediate_jwt) = intermediate_jwt {
72 header.extras.insert(INTERMEDIATE_HEADER, intermediate_jwt);
73 }
74 sign_jwt(client, key_id, &header, &payload).await
75}
76
77pub async fn make_intermediate_jwt(
80 client: &aws_sdk_kms::Client,
81 root_key_id: &str,
82 validity: Duration,
83 name: String,
84 public_key_pem: String,
85) -> anyhow::Result<String> {
86 let now = SystemTime::now();
87 let expiration = now + validity;
88 let payload = IntermediatePayload {
89 sub: name,
90 exp: format_time(&expiration),
91 nbf: format_time(&now),
92 iss: ISSUER.to_string(),
93 iat: format_time(&now),
94 jti: Uuid::new_v4().to_string(),
95 version: VERSION,
96 public_key: public_key_pem,
97 };
98 let mut header = Header::new(Algorithm::PS256);
99 header.typ = Some(INTERMEDIATE_TYP.to_string());
100 sign_jwt(client, root_key_id, &header, &payload).await
101}
102
103async fn sign_jwt(
104 client: &aws_sdk_kms::Client,
105 key_id: &str,
106 header: &Header,
107 payload: &impl Serialize,
108) -> anyhow::Result<String> {
109 let headers = URL_SAFE_NO_PAD.encode(serde_json::to_string(header).unwrap().as_bytes());
110 let payload = URL_SAFE_NO_PAD.encode(serde_json::to_string(payload).unwrap().as_bytes());
111
112 let signing_string = format!("{}.{}", headers, payload);
113 let signature = URL_SAFE_NO_PAD.encode(sign(client, key_id, signing_string.as_bytes()).await?);
114
115 Ok(format!("{}.{}", signing_string, signature))
116}
117
118async fn get_pubkey(client: &aws_sdk_kms::Client, key_id: &str) -> anyhow::Result<Vec<u8>> {
119 if let Some(pubkey) = client
120 .get_public_key()
121 .key_id(key_id)
122 .send()
123 .await?
124 .public_key
125 {
126 Ok(pubkey.into_inner())
127 } else {
128 Err(anyhow!("failed to get pubkey"))
129 }
130}
131
132async fn sign(
133 client: &aws_sdk_kms::Client,
134 key_id: &str,
135 message: &[u8],
136) -> anyhow::Result<Vec<u8>> {
137 let hash = digest::digest(&digest::SHA256, message);
138 let digest = hash.as_ref().to_vec();
139
140 if let Some(sig) = client
141 .sign()
142 .key_id(key_id)
143 .signing_algorithm(SigningAlgorithmSpec::RsassaPssSha256)
144 .message_type(MessageType::Digest)
145 .message(Blob::new(digest))
146 .send()
147 .await?
148 .signature
149 {
150 Ok(sig.into_inner())
151 } else {
152 Err(anyhow!("failed to get signature"))
153 }
154}
155
156fn format_time(t: &SystemTime) -> u64 {
157 t.duration_since(UNIX_EPOCH).unwrap().as_secs()
158}