#![forbid(unsafe_code)]
pub mod dev;
use diem_crypto::{
bls::BLS_PRIVATE_KEY_LENGTH, PrivateKey, ValidCryptoMaterial,
};
use diem_types::validator_config::{
ConsensusPrivateKey, ConsensusPublicKey, ConsensusSignature,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::{
collections::BTreeMap,
convert::{TryFrom, TryInto},
sync::Arc,
time::Duration,
};
use thiserror::Error;
use ureq::Response;
#[cfg(any(test, feature = "fuzzing"))]
pub mod fuzzing;
const MAX_NUM_KEY_VERSIONS: u32 = 4;
const DEFAULT_CONNECTION_TIMEOUT_MS: u64 = 1_000;
const DEFAULT_RESPONSE_TIMEOUT_MS: u64 = 1_000;
#[derive(Debug, Error, PartialEq)]
pub enum Error {
#[error("Http error, status code: {0}, status text: {1}, body: {2}")]
HttpError(u16, String, String),
#[error("Internal error: {0}")]
InternalError(String),
#[error("Missing field {0}")]
MissingField(String),
#[error("404: Not Found: {0}/{1}")]
NotFound(String, String),
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("Synthetic error returned: {0}")]
SyntheticError(String),
}
impl From<base64::DecodeError> for Error {
fn from(error: base64::DecodeError) -> Self {
Self::SerializationError(format!("{}", error))
}
}
impl From<diem_crypto::traits::CryptoMaterialError> for Error {
fn from(error: diem_crypto::traits::CryptoMaterialError) -> Self {
Self::SerializationError(format!("{}", error))
}
}
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Self::SerializationError(format!("{}", error))
}
}
impl From<ureq::Response> for Error {
fn from(resp: ureq::Response) -> Self {
if resp.synthetic() {
match resp.into_string() {
Ok(resp) => Error::SyntheticError(resp),
Err(error) => Error::InternalError(error.to_string()),
}
} else {
let status = resp.status();
let status_text = resp.status_text().to_string();
match resp.into_string() {
Ok(body) => Error::HttpError(status, status_text, body),
Err(error) => Error::InternalError(error.to_string()),
}
}
}
}
impl From<serde_json::Error> for Error {
fn from(error: serde_json::Error) -> Self {
Self::SerializationError(format!("{}", error))
}
}
pub struct Client {
agent: ureq::Agent,
host: String,
token: String,
tls_connector: Arc<native_tls::TlsConnector>,
connection_timeout_ms: u64,
response_timeout_ms: u64,
}
impl Client {
pub fn new(
host: String, token: String, ca_certificate: Option<String>,
connection_timeout_ms: Option<u64>, response_timeout_ms: Option<u64>,
) -> Self {
let mut tls_builder = native_tls::TlsConnector::builder();
tls_builder.min_protocol_version(Some(native_tls::Protocol::Tlsv12));
if let Some(certificate) = ca_certificate {
let mut cert =
native_tls::Certificate::from_pem(certificate.as_bytes());
if cert.is_err() {
cert =
native_tls::Certificate::from_der(certificate.as_bytes());
}
tls_builder.add_root_certificate(cert.unwrap());
}
let tls_connector = Arc::new(tls_builder.build().unwrap());
let connection_timeout_ms =
connection_timeout_ms.unwrap_or(DEFAULT_CONNECTION_TIMEOUT_MS);
let response_timeout_ms =
response_timeout_ms.unwrap_or(DEFAULT_RESPONSE_TIMEOUT_MS);
Self {
agent: ureq::Agent::new().set("connection", "keep-alive").build(),
host,
token,
tls_connector,
connection_timeout_ms,
response_timeout_ms,
}
}
pub fn delete_policy(&self, policy_name: &str) -> Result<(), Error> {
let request = self
.agent
.delete(&format!("{}/v1/sys/policy/{}", self.host, policy_name));
let resp = self.upgrade_request(request).call();
process_generic_response(resp)
}
pub fn list_policies(&self) -> Result<Vec<String>, Error> {
let request = self.agent.get(&format!("{}/v1/sys/policy", self.host));
let resp = self.upgrade_request(request).call();
process_policy_list_response(resp)
}
pub fn read_policy(&self, policy_name: &str) -> Result<Policy, Error> {
let request = self
.agent
.get(&format!("{}/v1/sys/policy/{}", self.host, policy_name));
let resp = self.upgrade_request(request).call();
process_policy_read_response(resp)
}
pub fn set_policy(
&self, policy_name: &str, policy: &Policy,
) -> Result<(), Error> {
let request = self
.agent
.post(&format!("{}/v1/sys/policy/{}", self.host, policy_name));
let resp = self.upgrade_request(request).send_json(policy.try_into()?);
process_generic_response(resp)
}
pub fn create_token(&self, policies: Vec<&str>) -> Result<String, Error> {
let request = self
.agent
.post(&format!("{}/v1/auth/token/create", self.host));
let resp = self
.upgrade_request(request)
.send_json(json!({ "policies": policies }));
process_token_create_response(resp)
}
pub fn renew_token_self(
&self, increment: Option<u32>,
) -> Result<u32, Error> {
let request = self
.agent
.post(&format!("{}/v1/auth/token/renew-self", self.host));
let mut request = self.upgrade_request(request);
let resp = if let Some(increment) = increment {
request.send_json(json!({ "increment": increment }))
} else {
request.call()
};
process_token_renew_response(resp)
}
pub fn revoke_token_self(&self) -> Result<(), Error> {
let request = self
.agent
.post(&format!("{}/v1/auth/token/revoke-self", self.host));
let mut request = self.upgrade_request(request);
let resp = request.call();
process_generic_response(resp)
}
pub fn list_secrets(&self, secret: &str) -> Result<Vec<String>, Error> {
let request = self.agent.request(
"LIST",
&format!("{}/v1/secret/metadata/{}", self.host, secret),
);
let resp = self.upgrade_request(request).call();
process_secret_list_response(resp)
}
pub fn delete_secret(&self, secret: &str) -> Result<(), Error> {
let request = self
.agent
.delete(&format!("{}/v1/secret/metadata/{}", self.host, secret));
let resp = self.upgrade_request(request).call();
process_generic_response(resp)
}
pub fn read_secret(
&self, secret: &str, key: &str,
) -> Result<ReadResponse<Value>, Error> {
let request = self
.agent
.get(&format!("{}/v1/secret/data/{}", self.host, secret));
let resp = self.upgrade_request(request).call();
process_secret_read_response(secret, key, resp)
}
pub fn create_ed25519_key(
&self, name: &str, exportable: bool,
) -> Result<(), Error> {
let request = self
.agent
.post(&format!("{}/v1/transit/keys/{}", self.host, name));
let resp = self
.upgrade_request(request)
.send_json(json!({ "type": "ed25519", "exportable": exportable }));
process_transit_create_response(name, resp)
}
pub fn delete_key(&self, name: &str) -> Result<(), Error> {
let request = self
.agent
.post(&format!("{}/v1/transit/keys/{}/config", self.host, name));
let resp = self
.upgrade_request(request)
.send_json(json!({ "deletion_allowed": true }));
process_generic_response(resp)?;
let request = self
.agent
.delete(&format!("{}/v1/transit/keys/{}", self.host, name));
let resp = self.upgrade_request(request).call();
process_generic_response(resp)
}
pub fn export_ed25519_key(
&self, name: &str, version: Option<u32>,
) -> Result<ConsensusPrivateKey, Error> {
let request = self.agent.get(&format!(
"{}/v1/transit/export/signing-key/{}",
self.host, name
));
let resp = self.upgrade_request(request).call();
process_transit_export_response(name, version, resp)
}
pub fn import_consensus_key(
&self, name: &str, key: &ConsensusPrivateKey,
) -> Result<(), Error> {
let backup =
base64::encode(serde_json::to_string(&KeyBackup::new(key))?);
let request = self
.agent
.post(&format!("{}/v1/transit/restore/{}", self.host, name));
let resp = self
.upgrade_request(request)
.send_json(json!({ "backup": backup }));
process_transit_restore_response(resp)
}
pub fn list_keys(&self) -> Result<Vec<String>, Error> {
let request = self
.agent
.request("LIST", &format!("{}/v1/transit/keys", self.host));
let resp = self.upgrade_request(request).call();
process_transit_list_response(resp)
}
pub fn read_consensus_key(
&self, name: &str,
) -> Result<Vec<ReadResponse<ConsensusPublicKey>>, Error> {
let request = self
.agent
.get(&format!("{}/v1/transit/keys/{}", self.host, name));
let resp = self.upgrade_request(request).call();
process_transit_read_response(name, resp)
}
pub fn rotate_key(&self, name: &str) -> Result<(), Error> {
let request = self
.agent
.post(&format!("{}/v1/transit/keys/{}/rotate", self.host, name));
let resp = self.upgrade_request(request).call();
process_generic_response(resp)
}
pub fn trim_key_versions(
&self, name: &str,
) -> Result<ConsensusPublicKey, Error> {
let all_pub_keys = self.read_consensus_key(name)?;
let max_version = all_pub_keys
.iter()
.map(|resp| resp.version)
.max()
.ok_or_else(|| Error::NotFound("transit/".into(), name.into()))?;
let min_version = all_pub_keys
.iter()
.map(|resp| resp.version)
.min()
.ok_or_else(|| Error::NotFound("transit/".into(), name.into()))?;
if (max_version - min_version) >= MAX_NUM_KEY_VERSIONS {
let min_available_version = max_version - MAX_NUM_KEY_VERSIONS + 1;
self.set_minimum_encrypt_decrypt_version(
name,
min_available_version,
)?;
self.set_minimum_available_version(name, min_available_version)?;
};
let newest_pub_key = all_pub_keys
.iter()
.find(|pub_key| pub_key.version == max_version)
.ok_or_else(|| Error::NotFound("transit/".into(), name.into()))?;
Ok(newest_pub_key.value.clone())
}
fn set_minimum_available_version(
&self, name: &str, min_available_version: u32,
) -> Result<(), Error> {
let request = self
.agent
.post(&format!("{}/v1/transit/keys/{}/trim", self.host, name));
let resp = self.upgrade_request(request).send_json(json!({
"min_available_version": min_available_version
}));
process_generic_response(resp)
}
fn set_minimum_encrypt_decrypt_version(
&self, name: &str, min_version: u32,
) -> Result<(), Error> {
let request = self
.agent
.post(&format!("{}/v1/transit/keys/{}/config", self.host, name));
let resp = self.upgrade_request(request).send_json(
json!({ "min_encryption_version": min_version, "min_decryption_version": min_version }),
);
process_generic_response(resp)
}
pub fn sign_ed25519(
&self, name: &str, data: &[u8], version: Option<u32>,
) -> Result<ConsensusSignature, Error> {
let data = if let Some(version) = version {
json!({ "input": base64::encode(&data), "key_version": version })
} else {
json!({ "input": base64::encode(&data) })
};
let request = self
.agent
.post(&format!("{}/v1/transit/sign/{}", self.host, name));
let resp = self.upgrade_request(request).send_json(data);
process_transit_sign_response(resp)
}
pub fn write_secret(
&self, secret: &str, key: &str, value: &Value, version: Option<u32>,
) -> Result<u32, Error> {
let payload = if let Some(version) = version {
json!({ "data": { key: value }, "options": {"cas": version} })
} else {
json!({ "data": { key: value } })
};
let request = self
.agent
.put(&format!("{}/v1/secret/data/{}", self.host, secret));
let resp = self.upgrade_request(request).send_json(payload);
if resp.ok() {
let resp: WriteSecretResponse =
serde_json::from_str(&resp.into_string()?)?;
Ok(resp.data.version)
} else {
Err(resp.into())
}
}
pub fn unsealed(&self) -> Result<bool, Error> {
let request =
self.agent.get(&format!("{}/v1/sys/seal-status", self.host));
let resp = self.upgrade_request_without_token(request).call();
process_unsealed_response(resp)
}
fn upgrade_request(&self, request: ureq::Request) -> ureq::Request {
let mut request = self.upgrade_request_without_token(request);
request.set("X-Vault-Token", &self.token);
request
}
fn upgrade_request_without_token(
&self, mut request: ureq::Request,
) -> ureq::Request {
request.timeout_connect(self.connection_timeout_ms);
request.timeout(Duration::from_millis(self.response_timeout_ms));
request.set_tls_connector(self.tls_connector.clone());
request
}
}
pub fn process_generic_response(resp: Response) -> Result<(), Error> {
if resp.ok() {
resp.into_string()?;
Ok(())
} else {
Err(resp.into())
}
}
pub fn process_policy_list_response(
resp: Response,
) -> Result<Vec<String>, Error> {
match resp.status() {
200 => {
let policies: ListPoliciesResponse =
serde_json::from_str(&resp.into_string()?)?;
Ok(policies.policies)
}
404 => {
resp.into_string()?;
Ok(vec![])
}
_ => Err(resp.into()),
}
}
pub fn process_policy_read_response(resp: Response) -> Result<Policy, Error> {
match resp.status() {
200 => Ok(Policy::try_from(resp.into_json()?)?),
_ => Err(resp.into()),
}
}
pub fn process_secret_list_response(
resp: Response,
) -> Result<Vec<String>, Error> {
match resp.status() {
200 => {
let resp: ReadSecretListResponse =
serde_json::from_str(&resp.into_string()?)?;
Ok(resp.data.keys)
}
404 => {
resp.into_string()?;
Ok(vec![])
}
_ => Err(resp.into()),
}
}
pub fn process_secret_read_response(
secret: &str, key: &str, resp: Response,
) -> Result<ReadResponse<Value>, Error> {
match resp.status() {
200 => {
let mut resp: ReadSecretResponse =
serde_json::from_str(&resp.into_string()?)?;
let data = &mut resp.data;
let value = data
.data
.remove(key)
.ok_or_else(|| Error::NotFound(secret.into(), key.into()))?;
let created_time = data.metadata.created_time.clone();
let version = data.metadata.version;
Ok(ReadResponse::new(created_time, value, version))
}
404 => {
resp.into_string()?;
Err(Error::NotFound(secret.into(), key.into()))
}
_ => Err(resp.into()),
}
}
pub fn process_token_create_response(resp: Response) -> Result<String, Error> {
if resp.ok() {
let resp: CreateTokenResponse =
serde_json::from_str(&resp.into_string()?)?;
Ok(resp.auth.client_token)
} else {
Err(resp.into())
}
}
pub fn process_token_renew_response(resp: Response) -> Result<u32, Error> {
if resp.ok() {
let resp: RenewTokenResponse =
serde_json::from_str(&resp.into_string()?)?;
Ok(resp.auth.lease_duration)
} else {
Err(resp.into())
}
}
pub fn process_transit_create_response(
name: &str, resp: Response,
) -> Result<(), Error> {
match resp.status() {
200 | 204 => {
resp.into_string()?;
Ok(())
}
404 => {
resp.into_string()?;
Err(Error::NotFound("transit/".into(), name.into()))
}
_ => Err(resp.into()),
}
}
pub fn process_transit_export_response(
name: &str, version: Option<u32>, resp: Response,
) -> Result<ConsensusPrivateKey, Error> {
if resp.ok() {
let export_key: ExportKeyResponse =
serde_json::from_str(&resp.into_string()?)?;
let composite_key = if let Some(version) = version {
let key =
export_key.data.keys.iter().find(|(k, _v)| **k == version);
let (_, key) = key.ok_or_else(|| {
Error::NotFound("transit/".into(), name.into())
})?;
key
} else if let Some(key) = export_key.data.keys.values().last() {
key
} else {
return Err(Error::NotFound("transit/".into(), name.into()));
};
let composite_key = base64::decode(composite_key)?;
if let Some(composite_key) =
composite_key.get(0..BLS_PRIVATE_KEY_LENGTH)
{
Ok(ConsensusPrivateKey::try_from(composite_key)?)
} else {
Err(Error::InternalError(
"Insufficient key length returned by vault export key request"
.into(),
))
}
} else {
Err(resp.into())
}
}
pub fn process_transit_list_response(
resp: Response,
) -> Result<Vec<String>, Error> {
match resp.status() {
200 => {
let list_keys: ListKeysResponse =
serde_json::from_str(&resp.into_string()?)?;
Ok(list_keys.data.keys)
}
404 => {
resp.into_string()?;
Err(Error::NotFound("transit/".into(), "keys".into()))
}
_ => Err(resp.into()),
}
}
pub fn process_transit_read_response(
name: &str, resp: Response,
) -> Result<Vec<ReadResponse<ConsensusPublicKey>>, Error> {
match resp.status() {
200 => {
let read_key: ReadKeyResponse =
serde_json::from_str(&resp.into_string()?)?;
let mut read_resp = Vec::new();
for (version, value) in read_key.data.keys {
read_resp.push(ReadResponse::new(
value.creation_time,
ConsensusPublicKey::try_from(
base64::decode(&value.public_key)?.as_slice(),
)?,
version,
));
}
Ok(read_resp)
}
404 => {
resp.into_string()?;
Err(Error::NotFound("transit/".into(), name.into()))
}
_ => Err(resp.into()),
}
}
pub fn process_transit_restore_response(resp: Response) -> Result<(), Error> {
match resp.status() {
204 => {
resp.into_string()?;
Ok(())
}
_ => Err(resp.into()),
}
}
pub fn process_transit_sign_response(
resp: Response,
) -> Result<ConsensusSignature, Error> {
if resp.ok() {
let signature: SignatureResponse =
serde_json::from_str(&resp.into_string()?)?;
let signature = &signature.data.signature;
let signature_pieces: Vec<_> = signature.split(':').collect();
let signature = signature_pieces
.get(2)
.ok_or_else(|| Error::SerializationError(signature.into()))?;
Ok(ConsensusSignature::try_from(
base64::decode(&signature)?.as_slice(),
)?)
} else {
Err(resp.into())
}
}
pub fn process_unsealed_response(resp: Response) -> Result<bool, Error> {
if resp.ok() {
let resp: SealStatusResponse =
serde_json::from_str(&resp.into_string()?)?;
Ok(!resp.sealed)
} else {
Err(resp.into())
}
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct KeyBackup {
policy: KeyBackupPolicy,
}
impl KeyBackup {
pub fn new(key: &ConsensusPrivateKey) -> Self {
let mut key_bytes = key.to_bytes().to_vec();
let pub_key_bytes = key.public_key().to_bytes();
key_bytes.extend(&pub_key_bytes);
let now = chrono::Utc::now();
let time_as_str = now.to_rfc3339();
let info = KeyBackupInfo {
key: Some(base64::encode(key_bytes)),
public_key: Some(base64::encode(pub_key_bytes)),
creation_time: now.timestamp_subsec_millis(),
time: time_as_str.clone(),
..Default::default()
};
let mut key_backup = Self {
policy: KeyBackupPolicy {
exportable: true,
min_decryption_version: 1,
latest_version: 1,
archive_version: 1,
backup_type: 2,
backup_info: BackupInfo {
time: time_as_str,
version: 1,
},
..Default::default()
},
};
key_backup.policy.keys.insert(1, info);
key_backup
}
}
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct KeyBackupPolicy {
name: String,
keys: BTreeMap<u32, KeyBackupInfo>,
derived: bool,
kdf: u32,
convergent_encryption: bool,
exportable: bool,
min_decryption_version: u32,
min_encryption_version: u32,
latest_version: u32,
archive_version: u32,
archive_min_version: u32,
min_available_version: u32,
deletion_allowed: bool,
convergent_version: u32,
#[serde(rename = "type")]
backup_type: u32,
backup_info: BackupInfo,
restore_info: Option<()>,
allow_plaintext_backup: bool,
version_template: String,
storage_prefix: String,
}
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct KeyBackupInfo {
key: Option<String>,
hhmac_key: Option<String>,
time: String,
ec_x: Option<String>,
ec_y: Option<String>,
ec_d: Option<String>,
rsa_key: Option<String>,
public_key: Option<String>,
convergent_version: u32,
creation_time: u32,
}
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct BackupInfo {
time: String,
version: u32,
}
#[derive(Debug)]
pub struct ReadResponse<T> {
pub creation_time: String,
pub value: T,
pub version: u32,
}
impl<T> ReadResponse<T> {
pub fn new(creation_time: String, value: T, version: u32) -> Self {
Self {
creation_time,
value,
version,
}
}
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct CreateTokenResponse {
auth: CreateTokenAuth,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct CreateTokenAuth {
client_token: String,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct ExportKeyResponse {
data: ExportKey,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct ExportKey {
name: String,
keys: BTreeMap<u32, String>,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct ListPoliciesResponse {
policies: Vec<String>,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct ListKeysResponse {
data: ListKeys,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct ListKeys {
keys: Vec<String>,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct ReadKeyResponse {
data: ReadKeys,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct ReadKeys {
keys: BTreeMap<u32, ReadKey>,
name: String,
#[serde(rename = "type")]
key_type: String,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct ReadKey {
creation_time: String,
public_key: String,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct ReadSecretListResponse {
data: ReadSecretListData,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct ReadSecretListData {
keys: Vec<String>,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct ReadSecretResponse {
data: ReadSecretData,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct ReadSecretData {
data: BTreeMap<String, Value>,
metadata: ReadSecretMetadata,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct ReadSecretMetadata {
created_time: String,
version: u32,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct WriteSecretResponse {
data: ReadSecretMetadata,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct RenewTokenResponse {
auth: RenewTokenAuth,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct RenewTokenAuth {
lease_duration: u32,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct Policy {
#[serde(skip)]
internal_rules: PolicyPaths,
rules: String,
}
impl Policy {
pub fn new() -> Self {
Self {
internal_rules: PolicyPaths {
path: BTreeMap::new(),
},
rules: "".to_string(),
}
}
pub fn add_policy(&mut self, path: &str, capabilities: Vec<Capability>) {
let path_policy = PathPolicy { capabilities };
self.internal_rules
.path
.insert(path.to_string(), path_policy);
}
}
impl TryFrom<serde_json::Value> for Policy {
type Error = serde_json::Error;
fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
let mut policy: Self = serde_json::from_value(value)?;
policy.internal_rules = serde_json::from_str(&policy.rules)?;
Ok(policy)
}
}
impl TryFrom<&Policy> for serde_json::Value {
type Error = serde_json::Error;
fn try_from(policy: &Policy) -> Result<Self, Self::Error> {
Ok(json!({"rules": serde_json::to_string(&policy.internal_rules)?}))
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct PolicyPaths {
path: BTreeMap<String, PathPolicy>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct PathPolicy {
capabilities: Vec<Capability>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Capability {
Create,
Delete,
Deny,
List,
Read,
Sudo,
Update,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct SignatureResponse {
data: Signature,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct Signature {
signature: String,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct SealStatusResponse {
sealed: bool,
}