penumbra_sdk_custody/
policy.rs

1//! A set of basic spend authorization policies.
2
3use std::collections::HashSet;
4
5use penumbra_sdk_keys::Address;
6use penumbra_sdk_proto::{
7    core::{
8        component::{
9            governance::v1::ValidatorVoteBody as ProtoValidatorVoteBody,
10            stake::v1::Validator as ProtoValidator,
11        },
12        transaction::v1::TransactionPlan as ProtoTransactionPlan,
13    },
14    Message as _,
15};
16use penumbra_sdk_transaction::plan::ActionPlan;
17use serde::{Deserialize, Serialize};
18
19use crate::{
20    AuthorizeRequest, AuthorizeValidatorDefinitionRequest, AuthorizeValidatorVoteRequest,
21    PreAuthorization,
22};
23
24/// A trait for checking whether a transaction plan is allowed by a policy.
25pub trait Policy {
26    /// Checks whether the proposed transaction plan is allowed by this policy.
27    fn check_transaction(&self, request: &AuthorizeRequest) -> anyhow::Result<()>;
28
29    /// Checks whether the proposed validator definition is allowed by this policy.
30    fn check_validator_definition(
31        &self,
32        _request: &AuthorizeValidatorDefinitionRequest,
33    ) -> anyhow::Result<()>;
34
35    /// Checks whether the proposed validator vote is allowed by this policy.
36    fn check_validator_vote(&self, _request: &AuthorizeValidatorVoteRequest) -> anyhow::Result<()>;
37}
38
39/// A set of basic spend authorization policies.
40///
41/// These policies are intended to be simple enough that they can be written by hand in a config
42/// file.  More complex policy logic than should be implemented by a custom implementation of
43/// the [`Policy`] trait.
44///
45/// These policies do not permit validator votes or validator definition updates, so a custom policy
46/// must be used to approve these actions.
47#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
48#[serde(tag = "type")]
49pub enum AuthPolicy {
50    /// Only allow transactions whose outputs are controlled by one of the
51    /// allowed destination addresses.
52    DestinationAllowList {
53        #[serde(with = "address_as_string")]
54        allowed_destination_addresses: Vec<Address>,
55    },
56    /// Intended for relayers, only allows `Spend`, `Output`, and `IbcAction`
57    /// actions in transactions.
58    ///
59    /// This policy should be combined with an `AllowList` to prevent sending
60    /// funds outside of the relayer account.
61    OnlyIbcRelay,
62    /// Require specific pre-authorizations for submitted [`TransactionPlan`](penumbra_sdk_transaction::TransactionPlan)s.
63    PreAuthorization(PreAuthorizationPolicy),
64}
65
66/// A set of pre-authorization policies.
67#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
68// We need to use a different tag name here, so we can stack it with the
69// SpendPolicy tag; in toml, for instance, this will turn into
70// [[spend_policy]]
71// type = 'PreAuthorization'
72// method = 'Ed25519'
73#[serde(tag = "method")]
74pub enum PreAuthorizationPolicy {
75    Ed25519 {
76        /// The number of distinct pre-authorizations required to authorize a transaction plan.
77        ///
78        /// Each `allowed_signer`'s contributions count only once towards this total.
79        required_signatures: u32,
80        /// A list of pre-authorization keys that can be used to authorize a transaction plan.
81        #[serde(with = "ed25519_vec_base64")]
82        allowed_signers: Vec<ed25519_consensus::VerificationKey>,
83    },
84}
85
86impl PreAuthorizationPolicy {
87    fn check_pre_authorizations(
88        &self,
89        pre_authorizations: &[PreAuthorization],
90        signed_data: impl AsRef<[u8]>,
91    ) -> anyhow::Result<()> {
92        let signed_data = signed_data.as_ref();
93        match self {
94            PreAuthorizationPolicy::Ed25519 {
95                required_signatures,
96                allowed_signers,
97            } => {
98                #[allow(clippy::unnecessary_filter_map)]
99                let ed25519_pre_auths =
100                    pre_authorizations
101                        .iter()
102                        .filter_map(|pre_auth| match pre_auth {
103                            PreAuthorization::Ed25519(pre_auth) => Some(pre_auth),
104                            // _ => None,
105                        });
106
107                let mut allowed_signers = allowed_signers.iter().cloned().collect::<HashSet<_>>();
108                let mut seen_signers = HashSet::new();
109
110                for pre_auth in ed25519_pre_auths {
111                    // Remove the signer from the allowed signers set, so that
112                    // each signer can only submit one pre-authorization.
113                    if let Some(signer) = allowed_signers.take(&pre_auth.vk) {
114                        pre_auth.verify(signed_data)?;
115                        seen_signers.insert(signer);
116                    }
117                }
118
119                if seen_signers.len() < *required_signatures as usize {
120                    anyhow::bail!(
121                        "required {} pre-authorization signatures but only saw {}",
122                        required_signatures,
123                        seen_signers.len(),
124                    );
125                }
126                Ok(())
127            }
128        }
129    }
130}
131
132mod address_as_string {
133    use std::str::FromStr;
134
135    use penumbra_sdk_keys::Address;
136
137    pub fn serialize<S: serde::Serializer>(
138        addresses: &[Address],
139        serializer: S,
140    ) -> Result<S::Ok, S::Error> {
141        use serde::Serialize;
142        let mut string_addresses = Vec::with_capacity(addresses.len());
143        for address in addresses {
144            string_addresses.push(address.to_string());
145        }
146        string_addresses.serialize(serializer)
147    }
148    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Address>, D::Error>
149    where
150        D: serde::Deserializer<'de>,
151    {
152        use serde::Deserialize;
153        let string_addresses: Vec<String> = Vec::deserialize(deserializer)?;
154        let mut addresses = Vec::with_capacity(string_addresses.len());
155        for string_address in string_addresses {
156            let address = Address::from_str(&string_address).map_err(serde::de::Error::custom)?;
157            addresses.push(address);
158        }
159        Ok(addresses)
160    }
161}
162
163/// A serde helper to serialize pre-authorization keys as base64-encoded data.
164/// Because Go's encoding/json will encode byte[] as base64-encoded strings,
165/// and Go's Ed25519 keys are byte[] values, this hopefully makes it easier to
166/// copy-paste pre-authorization keys from Go programs into the Rust config.
167// TODO: remove this after <https://github.com/penumbra-zone/ed25519-consensus/issues/7>
168mod ed25519_vec_base64 {
169    use base64::prelude::*;
170
171    pub fn serialize<S: serde::Serializer>(
172        keys: &[ed25519_consensus::VerificationKey],
173        serializer: S,
174    ) -> Result<S::Ok, S::Error> {
175        use serde::Serialize;
176        let mut base64_keys = Vec::with_capacity(keys.len());
177        for key in keys {
178            base64_keys.push(BASE64_STANDARD.encode(key.as_bytes()));
179        }
180        base64_keys.serialize(serializer)
181    }
182    pub fn deserialize<'de, D>(
183        deserializer: D,
184    ) -> Result<Vec<ed25519_consensus::VerificationKey>, D::Error>
185    where
186        D: serde::Deserializer<'de>,
187    {
188        use serde::Deserialize;
189        let base64_keys: Vec<String> = Vec::deserialize(deserializer)?;
190        let mut vks = Vec::with_capacity(base64_keys.len());
191        for base64_key in base64_keys {
192            let bytes = BASE64_STANDARD
193                .decode(base64_key)
194                .map_err(serde::de::Error::custom)?;
195            let vk = ed25519_consensus::VerificationKey::try_from(bytes.as_slice())
196                .map_err(serde::de::Error::custom)?;
197            vks.push(vk);
198        }
199        Ok(vks)
200    }
201}
202
203impl Policy for AuthPolicy {
204    fn check_transaction(&self, request: &AuthorizeRequest) -> anyhow::Result<()> {
205        let plan = &request.plan;
206        match self {
207            AuthPolicy::DestinationAllowList {
208                allowed_destination_addresses,
209            } => {
210                for output in plan.output_plans() {
211                    if !allowed_destination_addresses.contains(&output.dest_address) {
212                        anyhow::bail!("output {:?} has dest_address not in allow list", output);
213                    }
214                }
215                for swap in plan.swap_plans() {
216                    if !allowed_destination_addresses.contains(&swap.swap_plaintext.claim_address) {
217                        anyhow::bail!("swap {:?} has claim_address not in allow list", swap);
218                    }
219                }
220                Ok(())
221            }
222            AuthPolicy::OnlyIbcRelay => {
223                for action in &plan.actions {
224                    match action {
225                        ActionPlan::Spend { .. }
226                        | ActionPlan::Output { .. }
227                        | ActionPlan::IbcAction { .. } => {}
228                        _ => {
229                            anyhow::bail!("action {:?} not allowed by OnlyRelay policy", action);
230                        }
231                    }
232                }
233                Ok(())
234            }
235            AuthPolicy::PreAuthorization(policy) => policy.check_transaction(request),
236        }
237    }
238
239    fn check_validator_definition(
240        &self,
241        _request: &AuthorizeValidatorDefinitionRequest,
242    ) -> anyhow::Result<()> {
243        anyhow::bail!("validator definitions are not allowed by this policy")
244    }
245
246    fn check_validator_vote(&self, _request: &AuthorizeValidatorVoteRequest) -> anyhow::Result<()> {
247        anyhow::bail!("validator votes are not allowed by this policy")
248    }
249}
250
251impl Policy for PreAuthorizationPolicy {
252    fn check_transaction(&self, request: &AuthorizeRequest) -> anyhow::Result<()> {
253        self.check_pre_authorizations(
254            &request.pre_authorizations,
255            ProtoTransactionPlan::from(request.plan.clone()).encode_to_vec(),
256        )
257    }
258
259    fn check_validator_definition(
260        &self,
261        request: &AuthorizeValidatorDefinitionRequest,
262    ) -> anyhow::Result<()> {
263        self.check_pre_authorizations(
264            &request.pre_authorizations,
265            ProtoValidator::from(request.validator_definition.clone()).encode_to_vec(),
266        )
267    }
268
269    fn check_validator_vote(&self, request: &AuthorizeValidatorVoteRequest) -> anyhow::Result<()> {
270        self.check_pre_authorizations(
271            &request.pre_authorizations,
272            ProtoValidatorVoteBody::from(request.validator_vote.clone()).encode_to_vec(),
273        )
274    }
275}