1use std::sync::Arc;
17
18use auths_core::signing::{PassphraseProvider, SecureSigner, StorageSigner};
19use auths_core::storage::keychain::{KeyAlias, KeyStorage};
20use auths_crypto::CurveType;
21use auths_keri::KeriPublicKey;
22use base64::Engine;
23use base64::engine::general_purpose::STANDARD as BASE64;
24use thiserror::Error;
25
26use crate::domains::compliance::dsse::{DSSE_INTOTO_PAYLOAD_TYPE, DsseEnvelope, DsseSignature};
27use crate::domains::signing::service::dsse_pae;
28
29#[derive(Debug, Error)]
31pub enum DsseError {
32 #[error("invalid statement: {0}")]
34 InvalidStatement(String),
35
36 #[error("signing failed: {0}")]
38 Signing(String),
39
40 #[error("decode error: {0}")]
42 Decode(String),
43
44 #[error("verification failed: {0}")]
46 Verification(String),
47}
48
49fn curve_tag(curve: CurveType) -> &'static str {
51 match curve {
52 CurveType::Ed25519 => "ed25519",
53 CurveType::P256 => "p256",
54 }
55}
56
57fn curve_from_tag(tag: &str) -> CurveType {
59 match tag {
60 "ed25519" => CurveType::Ed25519,
61 _ => CurveType::P256,
62 }
63}
64
65fn validate_intoto_statement(value: &serde_json::Value) -> Result<(), DsseError> {
68 let has = |k: &str| value.get(k).and_then(|v| v.as_str()).is_some();
69 if !has("_type") || !has("predicateType") {
70 return Err(DsseError::InvalidStatement(
71 "an in-toto Statement must carry string `_type` and `predicateType` fields".into(),
72 ));
73 }
74 Ok(())
75}
76
77pub fn sign_intoto_statement(
96 key_storage: Arc<dyn KeyStorage + Send + Sync>,
97 passphrase_provider: &dyn PassphraseProvider,
98 keyid: &str,
99 alias: &KeyAlias,
100 curve: CurveType,
101 statement_json: &str,
102) -> Result<DsseEnvelope, DsseError> {
103 let value: serde_json::Value = serde_json::from_str(statement_json)
104 .map_err(|e| DsseError::InvalidStatement(format!("statement is not JSON: {e}")))?;
105 validate_intoto_statement(&value)?;
106
107 let payload = statement_json.as_bytes();
108 let to_sign = dsse_pae(DSSE_INTOTO_PAYLOAD_TYPE, payload);
109 let signer = StorageSigner::new(key_storage);
110 let sig = signer
111 .sign_with_alias(alias, passphrase_provider, &to_sign)
112 .map_err(|e| DsseError::Signing(e.to_string()))?;
113
114 Ok(DsseEnvelope {
115 payload_type: DSSE_INTOTO_PAYLOAD_TYPE.to_string(),
116 payload: BASE64.encode(payload),
117 signatures: vec![DsseSignature {
118 keyid: keyid.to_string(),
119 curve: curve_tag(curve).to_string(),
120 sig: BASE64.encode(&sig),
121 }],
122 })
123}
124
125pub fn sign_intoto_statement_with_seed(
143 seed: &[u8; 32],
144 curve: CurveType,
145 keyid: &str,
146 statement_json: &str,
147) -> Result<DsseEnvelope, DsseError> {
148 let value: serde_json::Value = serde_json::from_str(statement_json)
149 .map_err(|e| DsseError::InvalidStatement(format!("statement is not JSON: {e}")))?;
150 validate_intoto_statement(&value)?;
151
152 let payload = statement_json.as_bytes();
153 let to_sign = dsse_pae(DSSE_INTOTO_PAYLOAD_TYPE, payload);
154 let typed_seed = auths_crypto::TypedSeed::from_curve(curve, *seed);
155 let sig = auths_crypto::typed_sign(&typed_seed, &to_sign)
156 .map_err(|e| DsseError::Signing(e.to_string()))?;
157
158 Ok(DsseEnvelope {
159 payload_type: DSSE_INTOTO_PAYLOAD_TYPE.to_string(),
160 payload: BASE64.encode(payload),
161 signatures: vec![DsseSignature {
162 keyid: keyid.to_string(),
163 curve: curve_tag(curve).to_string(),
164 sig: BASE64.encode(&sig),
165 }],
166 })
167}
168
169pub fn verify_intoto_statement(
186 envelope_json: &str,
187 pinned_public_key: &[u8],
188) -> Result<serde_json::Value, DsseError> {
189 let envelope: DsseEnvelope = serde_json::from_str(envelope_json)
190 .map_err(|e| DsseError::Decode(format!("envelope is not JSON: {e}")))?;
191 let payload = BASE64
192 .decode(envelope.payload.as_bytes())
193 .map_err(|e| DsseError::Decode(format!("payload base64: {e}")))?;
194 let to_verify = dsse_pae(&envelope.payload_type, &payload);
195
196 let verified = envelope.signatures.iter().any(|s| {
197 let curve = curve_from_tag(&s.curve);
198 let Ok(key) = KeriPublicKey::from_verkey_bytes(pinned_public_key, curve) else {
199 return false;
200 };
201 let Ok(sig) = BASE64.decode(s.sig.as_bytes()) else {
202 return false;
203 };
204 key.verify_signature(&to_verify, &sig).is_ok()
205 });
206
207 if !verified {
208 return Err(DsseError::Verification(
209 "no DSSE signature verified against the pinned key".into(),
210 ));
211 }
212
213 let statement: serde_json::Value = serde_json::from_slice(&payload)
214 .map_err(|e| DsseError::Decode(format!("payload is not JSON: {e}")))?;
215 validate_intoto_statement(&statement)?;
216 Ok(statement)
217}
218
219#[cfg(test)]
220#[allow(clippy::unwrap_used, clippy::expect_used)]
221mod tests {
222 use super::*;
223 use auths_crypto::testing::generate_typed_signer;
224
225 fn intoto_statement(gate: &str) -> String {
226 serde_json::json!({
227 "_type": "https://in-toto.io/Statement/v1",
228 "subject": [{"name": "recurve.gate", "digest": {"sha256": "ab".repeat(32)}}],
229 "predicateType": "https://recurve.dev/verdict/v1",
230 "predicate": {"gate": gate}
231 })
232 .to_string()
233 }
234
235 fn signed_envelope(curve: CurveType, statement: &str) -> (String, Vec<u8>) {
236 let signer = generate_typed_signer(curve);
237 let payload = statement.as_bytes();
238 let pae = dsse_pae(DSSE_INTOTO_PAYLOAD_TYPE, payload);
239 let sig = signer.sign(&pae).unwrap();
240 let env = DsseEnvelope {
241 payload_type: DSSE_INTOTO_PAYLOAD_TYPE.to_string(),
242 payload: BASE64.encode(payload),
243 signatures: vec![DsseSignature {
244 keyid: "did:keri:EAgent".into(),
245 curve: curve_tag(curve).to_string(),
246 sig: BASE64.encode(&sig),
247 }],
248 };
249 (
250 serde_json::to_string(&env).unwrap(),
251 signer.public_key().to_vec(),
252 )
253 }
254
255 #[test]
256 fn verify_round_trips_ed25519() {
257 let (env, pk) = signed_envelope(CurveType::Ed25519, &intoto_statement("GREEN"));
258 let stmt = verify_intoto_statement(&env, &pk).unwrap();
259 assert_eq!(stmt["predicateType"], "https://recurve.dev/verdict/v1");
260 assert_eq!(stmt["predicate"]["gate"], "GREEN");
261 }
262
263 #[test]
264 fn verify_round_trips_p256() {
265 let (env, pk) = signed_envelope(CurveType::P256, &intoto_statement("GREEN"));
266 verify_intoto_statement(&env, &pk).unwrap();
267 }
268
269 #[test]
270 fn verify_rejects_tampered_payload() {
271 let (env, pk) = signed_envelope(CurveType::Ed25519, &intoto_statement("GREEN"));
272 let mut envelope: DsseEnvelope = serde_json::from_str(&env).unwrap();
273 envelope.payload = BASE64.encode(intoto_statement("RED").as_bytes());
275 let err =
276 verify_intoto_statement(&serde_json::to_string(&envelope).unwrap(), &pk).unwrap_err();
277 assert!(matches!(err, DsseError::Verification(_)));
278 }
279
280 #[test]
281 fn sign_with_seed_round_trips_and_binds() {
282 let signer = generate_typed_signer(CurveType::P256);
283 let seed = *signer.seed().as_bytes();
284 let env = sign_intoto_statement_with_seed(
285 &seed,
286 signer.curve(),
287 "did:keri:EAgent",
288 &intoto_statement("GREEN"),
289 )
290 .unwrap();
291 let stmt =
292 verify_intoto_statement(&serde_json::to_string(&env).unwrap(), signer.public_key())
293 .unwrap();
294 assert_eq!(stmt["predicate"]["gate"], "GREEN");
295
296 let mut envelope: DsseEnvelope =
297 serde_json::from_str(&serde_json::to_string(&env).unwrap()).unwrap();
298 envelope.payload = BASE64.encode(intoto_statement("RED").as_bytes());
299 let err = verify_intoto_statement(
300 &serde_json::to_string(&envelope).unwrap(),
301 signer.public_key(),
302 )
303 .unwrap_err();
304 assert!(matches!(err, DsseError::Verification(_)));
305 }
306
307 #[test]
308 fn verify_rejects_wrong_key() {
309 let (env, _pk) = signed_envelope(CurveType::Ed25519, &intoto_statement("GREEN"));
310 let stranger = generate_typed_signer(CurveType::Ed25519)
311 .public_key()
312 .to_vec();
313 let err = verify_intoto_statement(&env, &stranger).unwrap_err();
314 assert!(matches!(err, DsseError::Verification(_)));
315 }
316
317 #[test]
318 fn validate_rejects_non_intoto_statement() {
319 let bad = serde_json::json!({"_type": "https://in-toto.io/Statement/v1"});
321 assert!(validate_intoto_statement(&bad).is_err());
322 }
323}