Skip to main content

dpp_crypto/identity/
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 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
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 = 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}