|
1 |
| -use anyhow::Context; |
2 | 1 | use async_trait::async_trait;
|
3 |
| -use aws_sdk_kms::Client as InnerClient; |
4 |
| -use aws_sdk_kms::primitives::Blob; |
| 2 | +use aws_sdk_kms::{Client as AwsKmsClient, primitives::Blob}; |
5 | 3 | use k256::{
|
6 | 4 | ecdsa::{RecoveryId, Signature, VerifyingKey},
|
7 | 5 | pkcs8::DecodePublicKey,
|
8 | 6 | };
|
9 | 7 | use rust_eigenda_signers::{Message, RecoverableSignature};
|
10 | 8 | use thiserror::Error;
|
11 | 9 |
|
| 10 | +#[derive(Debug, Error)] |
| 11 | +pub enum Error { |
| 12 | + #[error("KMS GetPublicKey operation failed")] |
| 13 | + KmsGetPublicKey(#[from] aws_sdk_kms::operation::get_public_key::GetPublicKeyError), |
| 14 | + |
| 15 | + #[error("KMS Sign operation failed")] |
| 16 | + KmsSign(#[from] aws_sdk_kms::operation::sign::SignError), |
| 17 | + |
| 18 | + #[error("Failed to get public key from KMS")] |
| 19 | + MissingPublicKey, |
| 20 | + |
| 21 | + #[error("Failed to get signature from KMS")] |
| 22 | + MissingSignature, |
| 23 | + |
| 24 | + #[error("Failed to parse DER-encoded public key")] |
| 25 | + InvalidPublicKeyDer(#[source] k256::pkcs8::spki::Error), |
| 26 | + |
| 27 | + #[error("Failed to parse DER-encoded signature")] |
| 28 | + InvalidSignatureDer(#[source] k256::ecdsa::Error), |
| 29 | + |
| 30 | + #[error("Failed to convert public key format")] |
| 31 | + PublicKeyConversion(#[source] rust_eigenda_signers::PublicKeyError), |
| 32 | + |
| 33 | + #[error("Invalid recoverable signature")] |
| 34 | + InvalidRecoverableSignature(#[source] rust_eigenda_signers::InvalidRecoveryId), |
| 35 | + |
| 36 | + #[error("Failed to parse SEC1-encoded public key")] |
| 37 | + InvalidSec1PublicKey(#[source] k256::elliptic_curve::Error), |
| 38 | + |
| 39 | + #[error("Could not determine recovery ID for signature")] |
| 40 | + RecoveryIdNotFound, |
| 41 | + |
| 42 | + #[error("Invalid recovery ID: {0}")] |
| 43 | + InvalidRecoveryId(u8), |
| 44 | +} |
| 45 | + |
| 46 | +type Result<T> = std::result::Result<T, Error>; |
| 47 | + |
12 | 48 | #[derive(Debug, Clone)]
|
13 | 49 | pub struct Signer {
|
14 | 50 | key_id: String,
|
15 |
| - client: InnerClient, |
| 51 | + client: AwsKmsClient, |
16 | 52 | public_key: rust_eigenda_signers::PublicKey,
|
17 | 53 | k256_verifying_key: VerifyingKey,
|
18 | 54 | }
|
19 | 55 |
|
20 | 56 | impl Signer {
|
21 |
| - pub async fn new(client: InnerClient, key_id: String) -> anyhow::Result<Self> { |
22 |
| - let public_key_der = client |
| 57 | + pub async fn new(client: AwsKmsClient, key_id: String) -> Result<Self> { |
| 58 | + let response = client |
23 | 59 | .get_public_key()
|
24 | 60 | .key_id(&key_id)
|
25 | 61 | .send()
|
26 | 62 | .await
|
27 |
| - .context("Failed to get public key")? |
| 63 | + .map_err(|e| e.into_service_error())?; |
| 64 | + |
| 65 | + let public_key_der = response |
28 | 66 | .public_key
|
29 |
| - .context("Public key missing from response")? |
| 67 | + .ok_or(Error::MissingPublicKey)? |
30 | 68 | .into_inner();
|
31 | 69 |
|
32 | 70 | // Parse the DER-encoded public key using k256
|
33 |
| - let k256_pub_key = VerifyingKey::from_public_key_der(&public_key_der) |
34 |
| - .context("Failed to parse public key DER from KMS")?; |
35 |
| - |
36 |
| - // Convert k256 public key to secp256k1 public key |
37 |
| - let mut pub_key: [u8; 65] = [0; 65]; |
38 |
| - pub_key.copy_from_slice(k256_pub_key.to_encoded_point(false).as_bytes()); |
39 |
| - let secp_pub_key = rust_eigenda_signers::PublicKey::new(pub_key) |
40 |
| - .context("Failed to convert k256 pubkey to secp256k1 pubkey")?; |
| 71 | + let k256_verifying_key = VerifyingKey::from_public_key_der(&public_key_der) |
| 72 | + .map_err(Error::InvalidPublicKeyDer)?; |
| 73 | + |
| 74 | + // Convert k256 public key to rust_eigenda_signers public key |
| 75 | + // Use uncompressed format directly |
| 76 | + let uncompressed_bytes = k256_verifying_key.to_encoded_point(false); |
| 77 | + let public_key = rust_eigenda_signers::PublicKey::new( |
| 78 | + uncompressed_bytes |
| 79 | + .as_bytes() |
| 80 | + .try_into() |
| 81 | + .expect("uncompressed point is always 65 bytes"), |
| 82 | + ) |
| 83 | + .map_err(Error::PublicKeyConversion)?; |
41 | 84 |
|
42 | 85 | Ok(Self {
|
43 | 86 | key_id,
|
44 | 87 | client,
|
45 |
| - public_key: secp_pub_key, |
46 |
| - k256_verifying_key: k256_pub_key, |
| 88 | + public_key, |
| 89 | + k256_verifying_key, |
47 | 90 | })
|
48 | 91 | }
|
49 | 92 |
|
50 |
| - pub fn inner(&self) -> &InnerClient { |
| 93 | + pub fn inner(&self) -> &AwsKmsClient { |
51 | 94 | &self.client
|
52 | 95 | }
|
53 | 96 |
|
54 |
| - async fn get_raw_signature(&self, key_id: &str, data: &[u8]) -> anyhow::Result<Signature> { |
| 97 | + pub async fn sign_prehash(&self, key_id: &str, prehash: &[u8]) -> Result<Vec<u8>> { |
| 98 | + let signature = self.sign_with_kms(key_id, prehash).await?; |
| 99 | + let recovery_id = self.compute_recovery_id(prehash, &signature)?; |
| 100 | + |
| 101 | + // Combine signature and recovery ID |
| 102 | + let mut signature_bytes = Vec::with_capacity(65); |
| 103 | + signature_bytes.extend_from_slice(&signature.to_bytes()); |
| 104 | + signature_bytes.push(recovery_id); |
| 105 | + |
| 106 | + Ok(signature_bytes) |
| 107 | + } |
| 108 | + |
| 109 | + pub async fn get_public_key(&self, key_id: &str) -> Result<Vec<u8>> { |
| 110 | + let response = self |
| 111 | + .client |
| 112 | + .get_public_key() |
| 113 | + .key_id(key_id) |
| 114 | + .send() |
| 115 | + .await |
| 116 | + .map_err(|e| e.into_service_error())?; |
| 117 | + |
| 118 | + let der_bytes = response |
| 119 | + .public_key |
| 120 | + .ok_or(Error::MissingPublicKey)? |
| 121 | + .into_inner(); |
| 122 | + |
| 123 | + // Convert to uncompressed SEC1 format |
| 124 | + let verifying_key = |
| 125 | + VerifyingKey::from_public_key_der(&der_bytes).map_err(Error::InvalidPublicKeyDer)?; |
| 126 | + |
| 127 | + Ok(verifying_key.to_encoded_point(false).as_bytes().to_vec()) |
| 128 | + } |
| 129 | + |
| 130 | + async fn sign_with_kms(&self, key_id: &str, digest: &[u8]) -> Result<Signature> { |
55 | 131 | let response = self
|
56 | 132 | .client
|
57 | 133 | .sign()
|
58 | 134 | .key_id(key_id)
|
59 |
| - .message(data.into()) |
| 135 | + .message(Blob::new(digest)) |
60 | 136 | .message_type(aws_sdk_kms::types::MessageType::Digest)
|
61 | 137 | .signing_algorithm(aws_sdk_kms::types::SigningAlgorithmSpec::EcdsaSha256)
|
62 | 138 | .send()
|
63 |
| - .await?; |
| 139 | + .await |
| 140 | + .map_err(|e| e.into_service_error())?; |
64 | 141 |
|
65 |
| - let der_signature = response |
66 |
| - .signature |
67 |
| - .ok_or_else(|| anyhow::anyhow!("kms signature missing"))?; |
| 142 | + let signature_der = response.signature.ok_or(Error::MissingSignature)?; |
68 | 143 |
|
69 |
| - let signature = Signature::from_der(der_signature.as_ref())?; |
| 144 | + let signature = |
| 145 | + Signature::from_der(signature_der.as_ref()).map_err(Error::InvalidSignatureDer)?; |
| 146 | + |
| 147 | + // Normalize S value to ensure it's in the lower half of the order |
70 | 148 | Ok(signature.normalize_s().unwrap_or(signature))
|
71 | 149 | }
|
72 | 150 |
|
73 |
| - fn determine_recovery_id( |
74 |
| - &self, |
75 |
| - prehash: &[u8], |
76 |
| - signature: &Signature, |
77 |
| - public_key: &[u8], |
78 |
| - ) -> anyhow::Result<u8> { |
79 |
| - let expected_key = VerifyingKey::from_sec1_bytes(public_key)?; |
80 |
| - |
81 |
| - // try both possible recovery IDs |
| 151 | + #[allow(clippy::result_large_err)] |
| 152 | + fn compute_recovery_id(&self, prehash: &[u8], signature: &Signature) -> Result<u8> { |
| 153 | + // For secp256k1, only recovery IDs 0 and 1 are valid for uncompressed keys |
82 | 154 | for recovery_id in 0..2 {
|
83 |
| - if let Ok(recovered_key) = VerifyingKey::recover_from_prehash( |
84 |
| - prehash, |
85 |
| - signature, |
86 |
| - RecoveryId::from_byte(recovery_id).expect("valid recovery id"), |
87 |
| - ) { |
88 |
| - if recovered_key == expected_key { |
| 155 | + let rec_id = |
| 156 | + RecoveryId::from_byte(recovery_id).ok_or(Error::InvalidRecoveryId(recovery_id))?; |
| 157 | + |
| 158 | + if let Ok(recovered_key) = |
| 159 | + VerifyingKey::recover_from_prehash(prehash, signature, rec_id) |
| 160 | + { |
| 161 | + if recovered_key == self.k256_verifying_key { |
89 | 162 | return Ok(recovery_id);
|
90 | 163 | }
|
91 | 164 | }
|
92 | 165 | }
|
93 | 166 |
|
94 |
| - Err(anyhow::anyhow!("Could not determine recovery ID")) |
95 |
| - } |
96 |
| - |
97 |
| - pub async fn sign_prehash(&self, key_id: &str, prehash: &[u8]) -> anyhow::Result<Vec<u8>> { |
98 |
| - let public_key = self.get_public_key(key_id).await?; |
99 |
| - let signature = self.get_raw_signature(key_id, prehash).await?; |
100 |
| - |
101 |
| - let recovery_id = self.determine_recovery_id(prehash, &signature, &public_key)?; |
102 |
| - |
103 |
| - // combine into final signature |
104 |
| - let mut signature_bytes = signature.to_bytes().to_vec(); |
105 |
| - signature_bytes.push(recovery_id); |
106 |
| - |
107 |
| - Ok(signature_bytes) |
108 |
| - } |
109 |
| - |
110 |
| - pub async fn get_public_key(&self, key_id: &str) -> anyhow::Result<Vec<u8>> { |
111 |
| - let key_info = self.client.get_public_key().key_id(key_id).send().await?; |
112 |
| - |
113 |
| - let der_bytes: Vec<u8> = key_info |
114 |
| - .public_key |
115 |
| - .ok_or_else(|| anyhow::anyhow!("kms public key missing"))? |
116 |
| - .into(); |
117 |
| - |
118 |
| - // convert to uncompressed form |
119 |
| - let verifying_key = VerifyingKey::from_public_key_der(&der_bytes)?; |
120 |
| - let encoded_point = verifying_key.to_encoded_point(false); |
121 |
| - |
122 |
| - Ok(encoded_point.as_bytes().to_vec()) |
123 |
| - } |
124 |
| - |
125 |
| - fn k256_recovery_id( |
126 |
| - &self, |
127 |
| - signature: &k256::ecdsa::Signature, |
128 |
| - message_hash: &[u8; 32], |
129 |
| - ) -> anyhow::Result<u8> { |
130 |
| - signature |
131 |
| - .get_recovery_id(message_hash, &self.k256_verifying_key) |
132 |
| - .context("Failed to determine recovery ID") |
| 167 | + Err(Error::RecoveryIdNotFound) |
133 | 168 | }
|
134 | 169 | }
|
135 | 170 |
|
136 |
| -#[derive(Error, Debug)] |
137 |
| -#[error(transparent)] |
138 |
| -pub struct Error(#[from] anyhow::Error); |
139 |
| - |
140 | 171 | #[async_trait]
|
141 |
| -impl eigenda::Sign for crate::eigen::kms::Signer { |
| 172 | +impl eigenda::Sign for Signer { |
142 | 173 | type Error = Error;
|
143 | 174 |
|
144 |
| - async fn sign_digest(&self, message: &Message) -> Result<RecoverableSignature, Self::Error> { |
| 175 | + async fn sign_digest(&self, message: &Message) -> Result<RecoverableSignature> { |
145 | 176 | let digest_bytes: &[u8; 32] = message;
|
146 | 177 |
|
147 |
| - let sign_response = self |
148 |
| - .client |
149 |
| - .sign() |
150 |
| - .key_id(&self.key_id) |
151 |
| - .message(Blob::new(digest_bytes)) |
152 |
| - .message_type(aws_sdk_kms::types::MessageType::Digest) |
153 |
| - .signing_algorithm(aws_sdk_kms::types::SigningAlgorithmSpec::EcdsaSha256) |
154 |
| - .send() |
155 |
| - .await |
156 |
| - .context("while requesting KMS to sign the digest")?; |
157 |
| - |
158 |
| - let signature_der = sign_response |
159 |
| - .signature |
160 |
| - .ok_or_else(|| anyhow::anyhow!("Signature missing from KMS response"))? |
161 |
| - .into_inner(); |
| 178 | + // Sign the digest with KMS |
| 179 | + let signature = self.sign_with_kms(&self.key_id, digest_bytes).await?; |
162 | 180 |
|
163 |
| - let k256_sig = k256::ecdsa::Signature::from_der(&signature_der) |
164 |
| - .context("Failed to parse DER signature")?; |
| 181 | + // Compute recovery ID |
| 182 | + let recovery_id = self.compute_recovery_id(digest_bytes, &signature)?; |
165 | 183 |
|
166 |
| - let k256_sig_normalized = k256_sig.normalize_s().unwrap_or(k256_sig); |
| 184 | + // Build recoverable signature |
| 185 | + let mut sig_bytes = [0u8; 65]; |
| 186 | + sig_bytes[..64].copy_from_slice(&signature.to_bytes()); |
| 187 | + sig_bytes[64] = recovery_id; |
167 | 188 |
|
168 |
| - let k256_recid = self |
169 |
| - .k256_recovery_id(&k256_sig_normalized, digest_bytes) |
170 |
| - .context("Failed to determine recovery ID")?; |
171 |
| - |
172 |
| - let mut sig: [u8; 65] = [0; 65]; |
173 |
| - sig[64] = k256_recid; |
174 |
| - sig[..64].copy_from_slice(k256_sig_normalized.to_bytes().as_ref()); |
175 |
| - |
176 |
| - let standard_recoverable_sig = rust_eigenda_signers::RecoverableSignature::from_bytes(&sig) |
177 |
| - .context("Failed to create recoverable signature")?; |
178 |
| - |
179 |
| - Ok(standard_recoverable_sig) |
| 189 | + RecoverableSignature::from_bytes(&sig_bytes).map_err(Error::InvalidRecoverableSignature) |
180 | 190 | }
|
181 | 191 |
|
182 | 192 | fn public_key(&self) -> rust_eigenda_signers::PublicKey {
|
183 | 193 | self.public_key
|
184 | 194 | }
|
185 | 195 | }
|
186 |
| - |
187 |
| -trait RecIdExt { |
188 |
| - fn get_recovery_id( |
189 |
| - &self, |
190 |
| - message_hash: &[u8; 32], |
191 |
| - expected_pubkey: &VerifyingKey, |
192 |
| - ) -> anyhow::Result<u8>; |
193 |
| -} |
194 |
| - |
195 |
| -impl RecIdExt for k256::ecdsa::Signature { |
196 |
| - fn get_recovery_id( |
197 |
| - &self, |
198 |
| - message_hash: &[u8; 32], |
199 |
| - expected_pubkey: &VerifyingKey, |
200 |
| - ) -> anyhow::Result<u8> { |
201 |
| - (0..2) |
202 |
| - .find_map(|id| { |
203 |
| - let recovery_id = k256::ecdsa::RecoveryId::from_byte(id) |
204 |
| - .with_context(|| format!("Bad RecoveryId byte {}", id)) |
205 |
| - .ok()?; |
206 |
| - |
207 |
| - let recovered_key = |
208 |
| - VerifyingKey::recover_from_prehash(message_hash, self, recovery_id).ok()?; |
209 |
| - |
210 |
| - (&recovered_key == expected_pubkey).then_some(id) |
211 |
| - }) |
212 |
| - .ok_or_else(|| { |
213 |
| - anyhow::anyhow!("Could not recover correct public key from k256 signature") |
214 |
| - }) |
215 |
| - } |
216 |
| -} |
0 commit comments