Skip to main content

dpp_vc/
local_service.rs

1//! In-process `IdentityPort` implementation backed by a local `KeyStore`.
2
3use 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 dpp_crypto::jws::signer;
13use dpp_crypto::jws::verifier::{
14    extract_key_by_fingerprint, extract_kid_from_jws, extract_primary_public_key, verify_jws,
15};
16use dpp_crypto::keystore::KeyStore;
17
18use crate::did_builder::build_did_document;
19use crate::passport_credential::build_passport_credential;
20
21/// Concrete `IdentityPort` backed by a local `KeyStore`.
22///
23/// Signs passports **in-process** using Ed25519 keys from the store and verifies
24/// JWS signatures against the issuer's published DID document. Holds the store
25/// behind an `Arc` so one store can be shared with the did:web document endpoint
26/// — signing and the published key are then guaranteed identical, and there is
27/// no network-reachable signing endpoint to attack.
28pub 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 = dpp_crypto::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 = build_passport_credential(issuer_did.clone(), passport_id, payload_hash);
64
65        Ok(SignedCredential {
66            credential: passport_vc,
67            jws,
68            issuer_did,
69        })
70    }
71
72    async fn verify_signature(
73        &self,
74        jws: &str,
75        payload: &serde_json::Value,
76    ) -> Result<bool, DppError> {
77        let did_doc = build_did_document(&self.store, &self.base_url, &self.key_id)
78            .map_err(|e| DppError::Signing(e.to_string()))?;
79
80        let pub_key_b64 = extract_kid_from_jws(jws)
81            .and_then(|kid| extract_key_by_fingerprint(&did_doc, &kid))
82            .or_else(|| extract_primary_public_key(&did_doc))
83            .ok_or_else(|| DppError::Signing("no matching public key in DID document".into()))?;
84
85        if !verify_jws(jws, &pub_key_b64).map_err(|e| DppError::Signing(e.to_string()))? {
86            return Ok(false);
87        }
88
89        let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
90        let signed_payload = match jws.split('.').nth(1).and_then(|p| b64.decode(p).ok()) {
91            Some(bytes) => bytes,
92            None => return Ok(false),
93        };
94        let expected = dpp_crypto::jws::canonical::canonicalize(payload)
95            .map_err(|e| DppError::Signing(e.to_string()))?;
96        Ok(signed_payload == expected)
97    }
98
99    async fn own_did_document(&self) -> Result<serde_json::Value, DppError> {
100        build_did_document(&self.store, &self.base_url, &self.key_id)
101            .map_err(|e| DppError::Signing(e.to_string()))
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use dpp_crypto::keystore::KeyStore;
109    use dpp_domain::PassportId;
110    use std::sync::Arc;
111
112    fn service() -> LocalIdentityService {
113        let path = std::env::temp_dir().join(format!("ls-test-{}.json", uuid::Uuid::new_v4()));
114        let store = KeyStore::open(&path, "test-pass").expect("open store");
115        store.generate_key("root").expect("generate key");
116        LocalIdentityService::new(
117            Arc::new(store),
118            "root".to_owned(),
119            "https://node.example.com".to_owned(),
120        )
121    }
122
123    #[tokio::test]
124    async fn signs_then_verifies_round_trip() {
125        let svc = service();
126        let payload = serde_json::json!({ "productName": "Widget", "sector": "battery" });
127        let signed = svc
128            .sign_passport(PassportId::new(), &payload)
129            .await
130            .expect("sign");
131
132        assert!(!signed.jws.is_empty());
133        assert!(signed.issuer_did.starts_with("did:web:"));
134        assert_eq!(signed.credential.issuer, signed.issuer_did);
135
136        let ok = svc
137            .verify_signature(&signed.jws, &payload)
138            .await
139            .expect("verify");
140        assert!(ok, "freshly signed payload must verify");
141    }
142
143    #[tokio::test]
144    async fn verify_rejects_tampered_payload() {
145        let svc = service();
146        let payload = serde_json::json!({ "value": 1 });
147        let signed = svc
148            .sign_passport(PassportId::new(), &payload)
149            .await
150            .expect("sign");
151
152        let tampered = serde_json::json!({ "value": 2 });
153        let ok = svc
154            .verify_signature(&signed.jws, &tampered)
155            .await
156            .expect("verify");
157        assert!(!ok, "a tampered payload must not verify");
158    }
159}