Skip to main content

appcore_security/
dnt.rs

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