glass/browser/session/
identity.rs1use super::*;
7use base64::Engine;
8use base64::engine::general_purpose::STANDARD;
9use ed25519_dalek::{Signer, SigningKey};
10
11const MAX_IDENTITY_TEXT_BYTES: usize = 256;
12
13pub struct AgentIdentity {
16 pub agent: String,
17 pub key_id: String,
18 pub directory_url: String,
19 signing_key: SigningKey,
20}
21
22impl std::fmt::Debug for AgentIdentity {
23 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 formatter
25 .debug_struct("AgentIdentity")
26 .field("agent", &self.agent)
27 .field("key_id", &self.key_id)
28 .field("directory_url", &self.directory_url)
29 .finish_non_exhaustive()
30 }
31}
32
33#[derive(Debug, Clone, Serialize)]
35pub struct SignedHttpRequest {
36 #[serde(rename = "signatureAgent")]
37 pub signature_agent: String,
38 #[serde(rename = "signatureInput")]
39 pub signature_input: String,
40 pub signature: String,
41}
42
43impl AgentIdentity {
44 pub fn from_base64(
46 agent: impl Into<String>,
47 key_id: impl Into<String>,
48 directory_url: impl Into<String>,
49 private_key_base64: &str,
50 ) -> BrowserResult<Self> {
51 let bytes = STANDARD.decode(private_key_base64.trim())?;
52 let key_bytes: [u8; 32] = bytes
53 .try_into()
54 .map_err(|_| "agent identity private key must decode to exactly 32 bytes")?;
55 let agent = bounded_identity_text(agent.into(), "agent")?;
56 let key_id = bounded_identity_text(key_id.into(), "key_id")?;
57 let directory_url = bounded_identity_text(directory_url.into(), "directory_url")?;
58 Ok(Self {
59 agent,
60 key_id,
61 directory_url,
62 signing_key: SigningKey::from_bytes(&key_bytes),
63 })
64 }
65
66 pub fn sign_request(
68 &self,
69 method: &str,
70 target_uri: &str,
71 content_digest: Option<&str>,
72 created: i64,
73 ) -> BrowserResult<SignedHttpRequest> {
74 let method = bounded_identity_text(method.to_ascii_lowercase(), "method")?;
75 let target_uri = bounded_identity_text(target_uri.to_string(), "target_uri")?;
76 if created < 0 {
77 return Err("signature created timestamp must be non-negative".into());
78 }
79 let mut covered = format!("\"@method\": {method}\n\"@target-uri\": {target_uri}");
80 let mut components = "(\"@method\" \"@target-uri\")".to_string();
81 if let Some(content_digest) = content_digest {
82 let digest = bounded_identity_text(content_digest.to_string(), "content_digest")?;
83 covered.push_str(&format!("\n\"content-digest\": {digest}"));
84 components = "(\"@method\" \"@target-uri\" \"content-digest\")".to_string();
85 }
86 let parameters = format!(
87 "{components};created={created};keyid=\"{}\";alg=\"ed25519\";tag=\"glass\"",
88 self.key_id
89 );
90 let signature = STANDARD.encode(self.signing_key.sign(covered.as_bytes()).to_bytes());
91 Ok(SignedHttpRequest {
92 signature_agent: format!("\"{}\"", self.agent),
93 signature_input: format!("sig1={parameters}"),
94 signature: format!("sig1=:{signature}:"),
95 })
96 }
97}
98
99impl BrowserSession {
100 pub fn sign_http_request(
102 &self,
103 identity: &AgentIdentity,
104 method: &str,
105 target_uri: &str,
106 content_digest: Option<&str>,
107 created: i64,
108 ) -> BrowserResult<SignedHttpRequest> {
109 self.policy
110 .require(PolicyCapability::DeclaredAgentIdentity)?;
111 identity.sign_request(method, target_uri, content_digest, created)
112 }
113}
114
115fn bounded_identity_text(value: String, field: &str) -> BrowserResult<String> {
116 if value.is_empty() || value.len() > MAX_IDENTITY_TEXT_BYTES {
117 return Err(
118 format!("agent identity {field} must be 1..={MAX_IDENTITY_TEXT_BYTES} bytes").into(),
119 );
120 }
121 Ok(value)
122}