dpp_crypto/identity/
local_service.rs1use std::sync::Arc;
4
5use async_trait::async_trait;
6use base64::Engine;
7use sha2::{Digest, Sha256};
8
9use dpp_domain::ports::identity_port::IdentityPort;
10use dpp_domain::{DppError, PassportId, SignedCredential};
11
12use crate::jws::signer;
13use crate::jws::verifier::{
14 extract_key_by_fingerprint, extract_kid_from_jws, extract_primary_public_key, verify_jws,
15};
16use crate::keystore::KeyStore;
17
18use super::did_builder::build_did_document;
19use super::passport_credential::{PassportCredential, PassportCredentialSubject};
20
21pub struct LocalIdentityService {
29 store: Arc<KeyStore>,
30 key_id: String,
31 base_url: String,
32}
33
34impl LocalIdentityService {
35 pub fn new(store: Arc<KeyStore>, key_id: String, base_url: String) -> Self {
36 Self {
37 store,
38 key_id,
39 base_url,
40 }
41 }
42}
43
44#[async_trait]
45impl IdentityPort for LocalIdentityService {
46 async fn sign_passport(
47 &self,
48 passport_id: PassportId,
49 payload: &serde_json::Value,
50 ) -> Result<SignedCredential, DppError> {
51 let canonical = crate::jws::canonical::canonicalize(payload)
52 .map_err(|e| DppError::Signing(e.to_string()))?;
53 let payload_hash = hex::encode(Sha256::digest(&canonical));
54
55 let jws = signer::sign(&self.store, &self.key_id, payload)
56 .map_err(|e| DppError::Signing(e.to_string()))?;
57
58 let did_doc = build_did_document(&self.store, &self.base_url, &self.key_id)
59 .map_err(|e| DppError::Signing(e.to_string()))?;
60
61 let issuer_did = did_doc["id"].as_str().unwrap_or_default().to_string();
62
63 let passport_vc = PassportCredential::new(
64 issuer_did.clone(),
65 PassportCredentialSubject {
66 id: format!("urn:uuid:{}", passport_id),
67 payload_hash,
68 },
69 );
70
71 Ok(SignedCredential {
72 credential: passport_vc,
73 jws,
74 issuer_did,
75 })
76 }
77
78 async fn verify_signature(
79 &self,
80 jws: &str,
81 payload: &serde_json::Value,
82 ) -> Result<bool, DppError> {
83 let did_doc = build_did_document(&self.store, &self.base_url, &self.key_id)
84 .map_err(|e| DppError::Signing(e.to_string()))?;
85
86 let pub_key_b64 = extract_kid_from_jws(jws)
87 .and_then(|kid| extract_key_by_fingerprint(&did_doc, &kid))
88 .or_else(|| extract_primary_public_key(&did_doc))
89 .ok_or_else(|| DppError::Signing("no matching public key in DID document".into()))?;
90
91 if !verify_jws(jws, &pub_key_b64).map_err(|e| DppError::Signing(e.to_string()))? {
92 return Ok(false);
93 }
94
95 let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
96 let signed_payload = match jws.split('.').nth(1).and_then(|p| b64.decode(p).ok()) {
97 Some(bytes) => bytes,
98 None => return Ok(false),
99 };
100 let expected = crate::jws::canonical::canonicalize(payload)
101 .map_err(|e| DppError::Signing(e.to_string()))?;
102 Ok(signed_payload == expected)
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::keystore::KeyStore;
110 use dpp_domain::PassportId;
111 use std::sync::Arc;
112
113 fn service() -> LocalIdentityService {
114 let path = std::env::temp_dir().join(format!("ls-test-{}.json", uuid::Uuid::new_v4()));
115 let store = KeyStore::open(&path, "test-pass").expect("open store");
116 store.generate_key("root").expect("generate key");
117 LocalIdentityService::new(
118 Arc::new(store),
119 "root".to_owned(),
120 "https://node.example.com".to_owned(),
121 )
122 }
123
124 #[tokio::test]
125 async fn signs_then_verifies_round_trip() {
126 let svc = service();
127 let payload = serde_json::json!({ "productName": "Widget", "sector": "battery" });
128 let signed = svc
129 .sign_passport(PassportId::new(), &payload)
130 .await
131 .expect("sign");
132
133 assert!(!signed.jws.is_empty());
134 assert!(signed.issuer_did.starts_with("did:web:"));
135 assert_eq!(signed.credential.issuer, signed.issuer_did);
136
137 let ok = svc
138 .verify_signature(&signed.jws, &payload)
139 .await
140 .expect("verify");
141 assert!(ok, "freshly signed payload must verify");
142 }
143
144 #[tokio::test]
145 async fn verify_rejects_tampered_payload() {
146 let svc = service();
147 let payload = serde_json::json!({ "value": 1 });
148 let signed = svc
149 .sign_passport(PassportId::new(), &payload)
150 .await
151 .expect("sign");
152
153 let tampered = serde_json::json!({ "value": 2 });
154 let ok = svc
155 .verify_signature(&signed.jws, &tampered)
156 .await
157 .expect("verify");
158 assert!(!ok, "a tampered payload must not verify");
159 }
160}