penumbra_sdk_custody/
soft_kms.rs

1//! A basic software key management system that stores keys in memory but
2//! presents as an asynchronous signer.
3
4use decaf377_rdsa::{Signature, SpendAuth};
5use penumbra_sdk_proto::{
6    core::component::{
7        governance::v1::ValidatorVoteBody as ProtoValidatorVoteBody,
8        stake::v1::Validator as ProtoValidator,
9    },
10    custody::v1::{self as pb, AuthorizeResponse},
11    Message as _,
12};
13use penumbra_sdk_transaction::AuthorizationData;
14use rand_core::OsRng;
15use tonic::{async_trait, Request, Response, Status};
16
17use crate::{
18    policy::Policy, AuthorizeRequest, AuthorizeValidatorDefinitionRequest,
19    AuthorizeValidatorVoteRequest,
20};
21
22mod config;
23
24pub use config::Config;
25
26/// A basic software key management system that stores keys in memory but
27/// presents as an asynchronous signer.
28pub struct SoftKms {
29    config: Config,
30}
31
32impl SoftKms {
33    /// Initialize with the given [`Config`].
34    pub fn new(config: Config) -> Self {
35        Self { config }
36    }
37
38    /// Attempt to authorize the requested [`TransactionPlan`](penumbra_sdk_transaction::TransactionPlan).
39    #[tracing::instrument(skip(self, request), name = "softhsm_sign")]
40    pub fn sign(&self, request: &AuthorizeRequest) -> anyhow::Result<AuthorizationData> {
41        tracing::debug!(?request.plan);
42
43        for policy in &self.config.auth_policy {
44            policy.check_transaction(request)?;
45        }
46
47        Ok(request.plan.authorize(OsRng, &self.config.spend_key)?)
48    }
49
50    /// Attempt to authorize the requested validator definition.
51    #[tracing::instrument(skip(self, request), name = "softhsm_sign_validator_definition")]
52    pub fn sign_validator_definition(
53        &self,
54        request: &AuthorizeValidatorDefinitionRequest,
55    ) -> anyhow::Result<Signature<SpendAuth>> {
56        tracing::debug!(?request.validator_definition);
57
58        for policy in &self.config.auth_policy {
59            policy.check_validator_definition(request)?;
60        }
61
62        let protobuf_serialized: ProtoValidator = request.validator_definition.clone().into();
63        let validator_definition_bytes = protobuf_serialized.encode_to_vec();
64
65        Ok(self
66            .config
67            .spend_key
68            .spend_auth_key()
69            .sign(OsRng, &validator_definition_bytes))
70    }
71
72    /// Attempt to authorize the requested validator vote.
73    #[tracing::instrument(skip(self, request), name = "softhsm_sign_validator_vote")]
74    pub fn sign_validator_vote(
75        &self,
76        request: &AuthorizeValidatorVoteRequest,
77    ) -> anyhow::Result<Signature<SpendAuth>> {
78        tracing::debug!(?request.validator_vote);
79
80        for policy in &self.config.auth_policy {
81            policy.check_validator_vote(request)?;
82        }
83
84        let protobuf_serialized: ProtoValidatorVoteBody = request.validator_vote.clone().into();
85        let validator_vote_bytes = protobuf_serialized.encode_to_vec();
86
87        Ok(self
88            .config
89            .spend_key
90            .spend_auth_key()
91            .sign(OsRng, &validator_vote_bytes))
92    }
93}
94
95#[async_trait]
96impl pb::custody_service_server::CustodyService for SoftKms {
97    async fn authorize(
98        &self,
99        request: Request<pb::AuthorizeRequest>,
100    ) -> Result<Response<AuthorizeResponse>, Status> {
101        let request = request
102            .into_inner()
103            .try_into()
104            .map_err(|e: anyhow::Error| Status::invalid_argument(e.to_string()))?;
105
106        let authorization_data = self
107            .sign(&request)
108            .map_err(|e| Status::unauthenticated(format!("{e:#}")))?;
109
110        let authorization_response = AuthorizeResponse {
111            data: Some(authorization_data.into()),
112        };
113
114        Ok(Response::new(authorization_response))
115    }
116
117    async fn authorize_validator_definition(
118        &self,
119        request: Request<pb::AuthorizeValidatorDefinitionRequest>,
120    ) -> Result<Response<pb::AuthorizeValidatorDefinitionResponse>, Status> {
121        let request = request
122            .into_inner()
123            .try_into()
124            .map_err(|e: anyhow::Error| Status::invalid_argument(e.to_string()))?;
125
126        let validator_definition_auth = self
127            .sign_validator_definition(&request)
128            .map_err(|e| Status::unauthenticated(format!("{e:#}")))?;
129
130        let authorization_response = pb::AuthorizeValidatorDefinitionResponse {
131            validator_definition_auth: Some(validator_definition_auth.into()),
132        };
133
134        Ok(Response::new(authorization_response))
135    }
136
137    async fn authorize_validator_vote(
138        &self,
139        request: Request<pb::AuthorizeValidatorVoteRequest>,
140    ) -> Result<Response<pb::AuthorizeValidatorVoteResponse>, Status> {
141        let request = request
142            .into_inner()
143            .try_into()
144            .map_err(|e: anyhow::Error| Status::invalid_argument(e.to_string()))?;
145
146        let validator_vote_auth = self
147            .sign_validator_vote(&request)
148            .map_err(|e| Status::unauthenticated(format!("{e:#}")))?;
149
150        let authorization_response = pb::AuthorizeValidatorVoteResponse {
151            validator_vote_auth: Some(validator_vote_auth.into()),
152        };
153
154        Ok(Response::new(authorization_response))
155    }
156
157    async fn export_full_viewing_key(
158        &self,
159        _request: Request<pb::ExportFullViewingKeyRequest>,
160    ) -> Result<Response<pb::ExportFullViewingKeyResponse>, Status> {
161        Ok(Response::new(pb::ExportFullViewingKeyResponse {
162            full_viewing_key: Some(self.config.spend_key.full_viewing_key().clone().into()),
163        }))
164    }
165
166    async fn confirm_address(
167        &self,
168        request: Request<pb::ConfirmAddressRequest>,
169    ) -> Result<Response<pb::ConfirmAddressResponse>, Status> {
170        let address_index = request
171            .into_inner()
172            .address_index
173            .ok_or_else(|| {
174                Status::invalid_argument("missing address index in confirm address request")
175            })?
176            .try_into()
177            .map_err(|e| {
178                Status::invalid_argument(format!(
179                    "invalid address index in confirm address request: {e:#}"
180                ))
181            })?;
182
183        let (address, _dtk) = self
184            .config
185            .spend_key
186            .full_viewing_key()
187            .payment_address(address_index);
188
189        Ok(Response::new(pb::ConfirmAddressResponse {
190            address: Some(address.into()),
191        }))
192    }
193}