polimec_common/credentials/
mod.rs

1// Polimec Blockchain – https://www.polimec.org/
2// Copyright (C) Polimec 2022. All rights reserved.
3
4// The Polimec Blockchain is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Polimec Blockchain is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17use frame_support::{pallet_prelude::*, parameter_types, traits::OriginTrait, Deserialize, Serialize};
18use pallet_timestamp::Now;
19use parity_scale_codec::{Decode, Encode};
20use scale_info::{prelude::string::String, TypeInfo};
21use serde::{de::Error, ser::SerializeStruct, Serializer};
22use sp_runtime::{traits::BadOrigin, DeserializeOwned, RuntimeDebug};
23
24pub use jwt_compact_frame::{
25	alg::{Ed25519, VerifyingKey},
26	Claims as StandardClaims, *,
27};
28use serde::Deserializer;
29
30#[derive(Clone, Encode, Decode, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, TypeInfo, Deserialize, Serialize)]
31#[serde(rename_all = "lowercase")]
32pub enum InvestorType {
33	Retail,
34	Professional,
35	Institutional,
36}
37
38impl InvestorType {
39	#[must_use]
40	pub fn as_str(&self) -> &'static str {
41		match self {
42			InvestorType::Retail => "retail",
43			InvestorType::Professional => "professional",
44			InvestorType::Institutional => "institutional",
45		}
46	}
47}
48
49parameter_types! {
50	pub const Retail: InvestorType = InvestorType::Retail;
51	pub const Professional: InvestorType = InvestorType::Professional;
52	pub const Institutional: InvestorType = InvestorType::Institutional;
53}
54
55#[derive(Clone, Encode, Decode, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, TypeInfo, Deserialize)]
56pub struct SampleClaims<AccountId> {
57	#[serde(rename = "sub")]
58	pub subject: AccountId,
59	#[serde(rename = "iss")]
60	pub issuer: String,
61	#[serde(rename = "aud", deserialize_with = "from_bounded_cid")]
62	pub ipfs_cid: Cid,
63	pub investor_type: InvestorType,
64	#[serde(deserialize_with = "from_bounded_did")]
65	pub did: Did,
66}
67
68pub type Did = BoundedVec<u8, ConstU32<57>>;
69pub type Cid = BoundedVec<u8, ConstU32<96>>;
70
71pub struct EnsureInvestor<T>(sp_std::marker::PhantomData<T>);
72impl<T> EnsureOriginWithCredentials<T::RuntimeOrigin> for EnsureInvestor<T>
73where
74	T: frame_system::Config + pallet_timestamp::Config,
75{
76	type Claims = SampleClaims<T::AccountId>;
77	type Success = (T::AccountId, Did, InvestorType, Cid);
78
79	fn try_origin(
80		origin: T::RuntimeOrigin,
81		token: &jwt_compact_frame::UntrustedToken,
82		verifying_key: [u8; 32],
83	) -> Result<Self::Success, T::RuntimeOrigin> {
84		let Some(who) = origin.clone().into_signer() else { return Err(origin) };
85		let Ok(token) = Self::verify_token(token, verifying_key) else { return Err(origin) };
86		let claims = token.claims();
87		// Get the current timestamp from the pallet_timestamp. It is in milliseconds.
88		let Ok(now) = Now::<T>::get().try_into() else { return Err(origin) };
89		let Some(date_time) = claims.expiration else { return Err(origin) };
90
91		let timestamp: u64 = date_time.timestamp_millis().try_into().map_err(|_| origin.clone())?;
92
93		if claims.custom.subject == who && timestamp >= now {
94			return Ok((
95				who,
96				claims.custom.did.clone(),
97				claims.custom.investor_type.clone(),
98				claims.custom.ipfs_cid.clone(),
99			));
100		}
101
102		Err(origin)
103	}
104}
105
106#[allow(clippy::module_name_repetitions)]
107pub trait EnsureOriginWithCredentials<OuterOrigin>
108where
109	OuterOrigin: OriginTrait,
110{
111	type Success;
112	type Claims: Clone + Encode + Decode + Eq + PartialEq + Ord + PartialOrd + TypeInfo + DeserializeOwned;
113
114	fn try_origin(
115		origin: OuterOrigin,
116		token: &jwt_compact_frame::UntrustedToken,
117		verifying_key: [u8; 32],
118	) -> Result<Self::Success, OuterOrigin>;
119
120	fn ensure_origin(
121		origin: OuterOrigin,
122		token: &jwt_compact_frame::UntrustedToken,
123		verifying_key: [u8; 32],
124	) -> Result<Self::Success, BadOrigin> {
125		Self::try_origin(origin, token, verifying_key).map_err(|_| BadOrigin)
126	}
127
128	fn verify_token(
129		token: &jwt_compact_frame::UntrustedToken,
130		verifying_key: [u8; 32],
131	) -> Result<jwt_compact_frame::Token<Self::Claims>, ValidationError> {
132		let signing_key =
133			<<Ed25519 as Algorithm>::VerifyingKey>::from_slice(&verifying_key).expect("The Key is always valid");
134		Ed25519.validator::<Self::Claims>(&signing_key).validate(token)
135	}
136}
137
138pub fn from_bounded_did<'de, D>(deserializer: D) -> Result<Did, D::Error>
139where
140	D: Deserializer<'de>,
141{
142	String::deserialize(deserializer)
143		.map(|string| string.as_bytes().to_vec())
144		.and_then(|vec| vec.try_into().map_err(|_| Error::custom("failed to deserialize")))
145}
146
147pub fn from_bounded_cid<'de, D>(deserializer: D) -> Result<Cid, D::Error>
148where
149	D: Deserializer<'de>,
150{
151	String::deserialize(deserializer)
152		.map(|string| string.as_bytes().to_vec())
153		.and_then(|vec| vec.try_into().map_err(|_| Error::custom("failed to deserialize")))
154}
155
156impl<AccountId> Serialize for SampleClaims<AccountId>
157where
158	AccountId: Serialize, // Ensure AccountId can be serialized
159{
160	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
161	where
162		S: Serializer,
163	{
164		// Define how many fields we are serializing.
165		let mut state = serializer.serialize_struct("SampleClaims", 5)?;
166
167		// Serialize each field.
168		// Fields like `subject`, `issuer`, and `investor_type` can be serialized directly.
169		state.serialize_field("sub", &self.subject)?;
170		state.serialize_field("iss", &self.issuer)?;
171		// For the `ipfs_cid_string` field, you'd use your custom logic to convert it to a string or another format suitable for serialization.
172		// Assuming `cid` is a `BoundedVec<u8, ConstU32<96>>` and you're encoding it as a UTF-8 string.
173		let ipfs_cid_bytes: scale_info::prelude::vec::Vec<u8> = self.ipfs_cid.clone().into(); // Convert BoundedVec to Vec<u8>
174		let ipfs_cid_string = String::from_utf8_lossy(&ipfs_cid_bytes); // Convert Vec<u8> to String
175		state.serialize_field("aud", &ipfs_cid_string)?;
176
177		state.serialize_field("investor_type", &self.investor_type)?;
178
179		// For the `did` field, you'd use your custom logic to convert it to a string or another format suitable for serialization.
180		// Assuming `did` is a `BoundedVec<u8, ConstU32<57>>` and you're encoding it as a UTF-8 string.
181		let did_bytes: scale_info::prelude::vec::Vec<u8> = self.did.clone().into(); // Convert BoundedVec to Vec<u8>
182		let did_string = String::from_utf8_lossy(&did_bytes); // Convert Vec<u8> to String
183		state.serialize_field("did", &did_string)?;
184
185		// End the serialization
186		state.end()
187	}
188}