rustfs_targets/runtime/tls/
fingerprint.rs1use crate::error::TargetError;
18
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
21pub struct TargetTlsFingerprint {
22 pub ca_sha256: Option<[u8; 32]>,
23 pub client_cert_sha256: Option<[u8; 32]>,
24 pub client_key_sha256: Option<[u8; 32]>,
25}
26
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub struct TargetTlsGeneration(pub u64);
30
31#[derive(Debug, Clone, Default, PartialEq, Eq)]
33pub struct TargetTlsState {
34 pub generation: TargetTlsGeneration,
35 pub fingerprint: Option<TargetTlsFingerprint>,
36}
37
38impl TargetTlsState {
39 pub fn refresh(&mut self, next_fingerprint: TargetTlsFingerprint) -> bool {
42 if self.fingerprint.as_ref() == Some(&next_fingerprint) {
43 return false;
44 }
45
46 self.generation = TargetTlsGeneration(self.generation.0.saturating_add(1));
47 self.fingerprint = Some(next_fingerprint);
48 true
49 }
50
51 pub fn needs_update(&self, candidate: &TargetTlsFingerprint) -> bool {
55 self.fingerprint.as_ref() != Some(candidate)
56 }
57
58 pub fn reset(&mut self) {
60 *self = Self::default();
61 }
62}
63
64pub async fn build_target_tls_fingerprint(
67 ca_path: &str,
68 client_cert_path: &str,
69 client_key_path: &str,
70) -> Result<TargetTlsFingerprint, TargetError> {
71 async fn load_optional_digest(path: &str) -> Result<Option<[u8; 32]>, TargetError> {
72 if path.is_empty() {
73 return Ok(None);
74 }
75
76 let bytes = tokio::fs::read(path)
77 .await
78 .map_err(|e| TargetError::Configuration(format!("Failed to read TLS material '{path}': {e}")))?;
79 let digest = rustfs_tls_runtime::TlsFingerprint::from_optional_bytes(Some(&bytes), None, None, None, None).server_sha256;
80 Ok(digest)
81 }
82
83 Ok(TargetTlsFingerprint {
84 ca_sha256: load_optional_digest(ca_path).await?,
85 client_cert_sha256: load_optional_digest(client_cert_path).await?,
86 client_key_sha256: load_optional_digest(client_key_path).await?,
87 })
88}
89
90#[cfg(test)]
91mod tests {
92 use super::{TargetTlsFingerprint, TargetTlsGeneration, TargetTlsState};
93
94 #[test]
95 fn refresh_increments_generation_only_when_fingerprint_changes() {
96 let mut state = TargetTlsState::default();
97 let first = TargetTlsFingerprint {
98 ca_sha256: Some([1; 32]),
99 client_cert_sha256: None,
100 client_key_sha256: None,
101 };
102 let second = TargetTlsFingerprint {
103 ca_sha256: Some([2; 32]),
104 client_cert_sha256: None,
105 client_key_sha256: None,
106 };
107
108 assert!(state.refresh(first.clone()));
109 assert_eq!(state.generation, TargetTlsGeneration(1));
110 assert!(!state.refresh(first));
111 assert_eq!(state.generation, TargetTlsGeneration(1));
112 assert!(state.refresh(second));
113 assert_eq!(state.generation, TargetTlsGeneration(2));
114 }
115
116 #[test]
117 fn reset_clears_generation_and_fingerprint() {
118 let mut state = TargetTlsState {
119 generation: TargetTlsGeneration(5),
120 fingerprint: Some(TargetTlsFingerprint {
121 ca_sha256: Some([9; 32]),
122 client_cert_sha256: None,
123 client_key_sha256: None,
124 }),
125 };
126
127 state.reset();
128 assert_eq!(state, TargetTlsState::default());
129 }
130
131 #[test]
132 fn fingerprint_eq_when_all_fields_match() {
133 let a = TargetTlsFingerprint {
134 ca_sha256: Some([42; 32]),
135 client_cert_sha256: Some([1; 32]),
136 client_key_sha256: None,
137 };
138 let b = TargetTlsFingerprint {
139 ca_sha256: Some([42; 32]),
140 client_cert_sha256: Some([1; 32]),
141 client_key_sha256: None,
142 };
143 assert_eq!(a, b);
144 }
145
146 #[test]
147 fn fingerprint_ne_when_ca_differs() {
148 let a = TargetTlsFingerprint {
149 ca_sha256: Some([1; 32]),
150 client_cert_sha256: None,
151 client_key_sha256: None,
152 };
153 let b = TargetTlsFingerprint {
154 ca_sha256: Some([2; 32]),
155 client_cert_sha256: None,
156 client_key_sha256: None,
157 };
158 assert_ne!(a, b);
159 }
160}