Skip to main content

appcore_security/
dnt.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: dnt.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/02 00:04:12 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/02 00:04:12 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! DNT key-provider adapter backed by security secret resolvers.
12
13use crate::{SecretResolver, SecuritySecretRef};
14use appcore_dnt::{DntContext, DntKeyError, DntKeyProvider, KeyId, SecretKey};
15use std::fmt;
16
17/// Mapping policy from DNT key IDs to security secret references.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct DntSecretRefPolicy {
20    prefix: String,
21}
22
23impl DntSecretRefPolicy {
24    /// Creates a mapping policy with an explicit non-secret reference prefix.
25    pub fn new(prefix: impl Into<String>) -> Self {
26        Self {
27            prefix: prefix.into(),
28        }
29    }
30
31    /// Returns the default provider-owned DNT key reference policy.
32    pub fn provider_default() -> Self {
33        Self::new("provider:dnt-key/")
34    }
35
36    /// Maps a contextual key ID to an opaque security secret reference.
37    pub fn reference_for(&self, key_id: &KeyId, context: &DntContext) -> SecuritySecretRef {
38        let tenant = context
39            .tenant_id
40            .as_ref()
41            .map(|value| value.as_str())
42            .unwrap_or("_");
43        SecuritySecretRef(format!(
44            "{}{}/{}/{}",
45            self.prefix,
46            context.application_id.as_str(),
47            tenant,
48            key_id.as_str()
49        ))
50    }
51}
52
53impl Default for DntSecretRefPolicy {
54    fn default() -> Self {
55        Self::provider_default()
56    }
57}
58
59/// DNT key provider backed by an existing AppCore secret resolver.
60pub struct DntSecretKeyProvider<R> {
61    resolver: R,
62    policy: DntSecretRefPolicy,
63}
64
65impl<R> fmt::Debug for DntSecretKeyProvider<R> {
66    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67        formatter
68            .debug_struct("DntSecretKeyProvider")
69            .field("policy", &self.policy)
70            .finish_non_exhaustive()
71    }
72}
73
74impl<R> DntSecretKeyProvider<R>
75where
76    R: SecretResolver,
77{
78    /// Creates a provider using the default DNT key reference policy.
79    pub fn new(resolver: R) -> Self {
80        Self {
81            resolver,
82            policy: DntSecretRefPolicy::default(),
83        }
84    }
85
86    /// Creates a provider using an explicit reference mapping policy.
87    pub fn with_policy(resolver: R, policy: DntSecretRefPolicy) -> Self {
88        Self { resolver, policy }
89    }
90
91    /// Returns the configured non-secret key-reference mapping policy.
92    pub fn policy(&self) -> &DntSecretRefPolicy {
93        &self.policy
94    }
95}
96
97impl<R> DntKeyProvider for DntSecretKeyProvider<R>
98where
99    R: SecretResolver,
100{
101    fn resolve_key(&self, key_id: &KeyId, context: &DntContext) -> Result<SecretKey, DntKeyError> {
102        let reference = self.policy.reference_for(key_id, context);
103        let secret = self
104            .resolver
105            .resolve(&reference)
106            .map_err(|_| DntKeyError::Unavailable)?;
107        SecretKey::from_slice(secret.as_bytes())
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::{SecretBytes, StaticSecretResolver};
115    use appcore_contracts::ApplicationId;
116    use appcore_dnt::{
117        open, seal, BytesCodec, ContentType, DntOpenOptions, DntSealOptions, DNT_CONTENT_SECRET,
118    };
119    use appcore_types::TenantId;
120    use std::collections::HashMap;
121
122    #[test]
123    fn secret_resolver_key_provider_opens_dnt_secret() {
124        let key_id = KeyId::new("local-root").unwrap();
125        let mut secrets = HashMap::new();
126        secrets.insert(
127            format!("provider:dnt-key/app-a/tenant-a/{}", key_id.as_str()),
128            SecretBytes::new(vec![5; 32]),
129        );
130        let resolver = StaticSecretResolver::new(secrets);
131        let provider = DntSecretKeyProvider::new(resolver);
132        let codec = BytesCodec;
133        let seal_options = DntSealOptions {
134            application_id: ApplicationId::new("app-a").unwrap(),
135            tenant_id: Some(TenantId::new("tenant-a").unwrap()),
136            content_type: ContentType::new(DNT_CONTENT_SECRET).unwrap(),
137            schema_version: 1,
138            key_id,
139            created_at_ms: 1,
140            public_metadata: Vec::new(),
141            encrypted_metadata: Vec::new(),
142            flags: 0,
143            max_payload_bytes: Some(1024),
144        };
145        let open_options = DntOpenOptions {
146            application_id: ApplicationId::new("app-a").unwrap(),
147            tenant_id: Some(TenantId::new("tenant-a").unwrap()),
148            content_type: ContentType::new(DNT_CONTENT_SECRET).unwrap(),
149            max_payload_bytes: Some(1024),
150        };
151
152        let sealed = seal(b"secret", &provider, &codec, seal_options).unwrap();
153        let opened = open(&sealed, &provider, &codec, &open_options).unwrap();
154
155        assert_eq!(opened.payload, b"secret");
156    }
157
158    #[test]
159    fn missing_dnt_key_fails_closed() {
160        let resolver = StaticSecretResolver::new(HashMap::new());
161        let provider = DntSecretKeyProvider::new(resolver);
162        let context = DntContext {
163            application_id: ApplicationId::new("app-a").unwrap(),
164            tenant_id: None,
165            content_type: ContentType::new(DNT_CONTENT_SECRET).unwrap(),
166            codec_id: appcore_dnt::CodecId::new("bytes").unwrap(),
167            schema_version: 1,
168        };
169
170        assert_eq!(
171            provider.resolve_key(&KeyId::new("missing").unwrap(), &context),
172            Err(DntKeyError::Unavailable)
173        );
174    }
175}