Skip to main content

rustfs_targets/runtime/tls/
fingerprint.rs

1// Copyright 2024 RustFS Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! TLS fingerprint types for per-target certificate hot-reload detection.
16
17use crate::error::TargetError;
18
19/// SHA256 digest per TLS file component used to detect certificate changes.
20#[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/// Monotonically increasing generation counter bumped on each successful reload.
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub struct TargetTlsGeneration(pub u64);
30
31/// Combined TLS state held per-target for tracking reload progress.
32#[derive(Debug, Clone, Default, PartialEq, Eq)]
33pub struct TargetTlsState {
34    pub generation: TargetTlsGeneration,
35    pub fingerprint: Option<TargetTlsFingerprint>,
36}
37
38impl TargetTlsState {
39    /// Compares `next_fingerprint` with the current one. If different, bumps
40    /// generation and stores the new fingerprint. Returns `true` when changed.
41    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    /// Checks whether `candidate` differs from the stored fingerprint without
52    /// mutating state. Use this to gate a rebuild, then call `refresh` only
53    /// after the rebuild succeeds.
54    pub fn needs_update(&self, candidate: &TargetTlsFingerprint) -> bool {
55        self.fingerprint.as_ref() != Some(candidate)
56    }
57
58    /// Resets state to default (generation 0, no fingerprint).
59    pub fn reset(&mut self) {
60        *self = Self::default();
61    }
62}
63
64/// Reads the three TLS material files from disk and returns a fingerprint
65/// computed from their SHA256 digests. Empty paths produce `None` digests.
66pub 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}