Skip to main content

rocketmq_auth/authentication/chain/
acl_signer.rs

1// Copyright 2023 The RocketMQ Rust Authors
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//! ACL signature calculator.
16
17use base64::engine::general_purpose::STANDARD;
18use base64::Engine;
19use serde::Deserialize;
20use serde::Serialize;
21
22use crate::AuthFailureKind;
23use crate::AuthOperation;
24use crate::AuthServiceError;
25use crate::AuthServiceResult;
26
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
28pub enum SignatureAlgorithm {
29    #[default]
30    #[serde(
31        rename = "HmacSHA1",
32        alias = "hmacSHA1",
33        alias = "HMAC_SHA1",
34        alias = "HmacSha1",
35        alias = "sha1"
36    )]
37    HmacSha1,
38    #[serde(
39        rename = "HmacSHA256",
40        alias = "hmacSHA256",
41        alias = "HMAC_SHA256",
42        alias = "HmacSha256",
43        alias = "sha256"
44    )]
45    HmacSha256,
46    #[serde(
47        rename = "HmacMD5",
48        alias = "hmacMD5",
49        alias = "HMAC_MD5",
50        alias = "HmacMd5",
51        alias = "md5"
52    )]
53    HmacMd5,
54}
55
56impl SignatureAlgorithm {
57    pub const fn java_name(self) -> &'static str {
58        match self {
59            Self::HmacSha1 => "HmacSHA1",
60            Self::HmacSha256 => "HmacSHA256",
61            Self::HmacMd5 => "HmacMD5",
62        }
63    }
64
65    pub fn from_java_name(value: &str) -> Option<Self> {
66        match value.trim().to_ascii_uppercase().replace(['-', '_'], "").as_str() {
67            "HMACSHA1" | "SHA1" => Some(Self::HmacSha1),
68            "HMACSHA256" | "SHA256" => Some(Self::HmacSha256),
69            "HMACMD5" | "MD5" => Some(Self::HmacMd5),
70            _ => None,
71        }
72    }
73}
74
75/// Calculate ACL signature using HMAC-SHA1.
76///
77/// # Arguments
78///
79/// * `content` - The content to sign
80/// * `secret_key` - The secret key for signing
81///
82/// # Returns
83///
84/// Base64-encoded signature
85pub fn cal_signature(content: &[u8], secret_key: &str) -> AuthServiceResult<String> {
86    cal_signature_with_algorithm(content, secret_key, SignatureAlgorithm::default())
87}
88
89pub fn cal_signature_segments<'a, I>(segments: I, secret_key: &str) -> AuthServiceResult<String>
90where
91    I: IntoIterator<Item = &'a [u8]>,
92{
93    cal_signature_segments_with_algorithm(segments, secret_key, SignatureAlgorithm::default())
94}
95
96pub fn cal_signature_with_algorithm(
97    content: &[u8],
98    secret_key: &str,
99    algorithm: SignatureAlgorithm,
100) -> AuthServiceResult<String> {
101    cal_signature_segments_with_algorithm([content], secret_key, algorithm)
102}
103
104pub fn cal_signature_segments_with_algorithm<'a, I>(
105    segments: I,
106    secret_key: &str,
107    algorithm: SignatureAlgorithm,
108) -> AuthServiceResult<String>
109where
110    I: IntoIterator<Item = &'a [u8]>,
111{
112    use hmac::digest::KeyInit;
113    use hmac::Hmac;
114    use hmac::Mac;
115
116    match algorithm {
117        SignatureAlgorithm::HmacSha1 => {
118            type HmacSha1 = Hmac<sha1::Sha1>;
119            let mut mac = HmacSha1::new_from_slice(secret_key.as_bytes()).map_err(|source| {
120                AuthServiceError::with_source(AuthOperation::Authenticate, AuthFailureKind::Internal, source)
121            })?;
122            for segment in segments {
123                mac.update(segment);
124            }
125            Ok(STANDARD.encode(mac.finalize().into_bytes().as_slice()))
126        }
127        SignatureAlgorithm::HmacSha256 => {
128            type HmacSha256 = Hmac<sha2::Sha256>;
129            let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()).map_err(|source| {
130                AuthServiceError::with_source(AuthOperation::Authenticate, AuthFailureKind::Internal, source)
131            })?;
132            for segment in segments {
133                mac.update(segment);
134            }
135            Ok(STANDARD.encode(mac.finalize().into_bytes().as_slice()))
136        }
137        SignatureAlgorithm::HmacMd5 => {
138            type HmacMd5 = Hmac<md5::Md5>;
139            let mut mac = HmacMd5::new_from_slice(secret_key.as_bytes()).map_err(|source| {
140                AuthServiceError::with_source(AuthOperation::Authenticate, AuthFailureKind::Internal, source)
141            })?;
142            for segment in segments {
143                mac.update(segment);
144            }
145            Ok(STANDARD.encode(mac.finalize().into_bytes().as_slice()))
146        }
147    }
148}
149
150/// Default charset for ACL signing.
151pub const DEFAULT_CHARSET: &str = "UTF-8";
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_cal_signature() {
159        let content = b"test content";
160        let secret_key = "test_secret_key";
161
162        let result = cal_signature(content, secret_key);
163        assert!(result.is_ok());
164
165        let signature = result.unwrap();
166        assert!(!signature.is_empty());
167    }
168
169    #[test]
170    fn test_cal_signature_consistency() {
171        let content = b"consistent content";
172        let secret_key = "my_secret";
173
174        let sig1 = cal_signature(content, secret_key).unwrap();
175        let sig2 = cal_signature(content, secret_key).unwrap();
176
177        assert_eq!(sig1, sig2);
178    }
179
180    #[test]
181    fn test_cal_signature_different_keys() {
182        let content = b"same content";
183        let key1 = "key1";
184        let key2 = "key2";
185
186        let sig1 = cal_signature(content, key1).unwrap();
187        let sig2 = cal_signature(content, key2).unwrap();
188
189        assert_ne!(sig1, sig2);
190    }
191
192    #[test]
193    fn test_cal_signature_empty_content() {
194        let content = b"";
195        let secret_key = "secret";
196
197        let result = cal_signature(content, secret_key);
198        assert!(result.is_ok());
199    }
200
201    #[test]
202    fn test_cal_signature_matches_java_hmac_sha1_base64() {
203        let signature = cal_signature(b"alicetopic-a", "secret").unwrap();
204
205        assert_eq!(signature, "3yomro7y0WqWcbV+9tQa4av5w3Q=");
206    }
207
208    #[test]
209    fn test_cal_signature_segments_matches_contiguous_content() {
210        let contiguous = cal_signature(b"alicetokentopic-abody", "secret").unwrap();
211        let segmented = cal_signature_segments([b"alice".as_slice(), b"token", b"topic-a", b"body"], "secret").unwrap();
212
213        assert_eq!(segmented, contiguous);
214    }
215
216    #[test]
217    fn test_cal_signature_segments_supports_all_java_algorithms() {
218        for algorithm in [
219            SignatureAlgorithm::HmacSha1,
220            SignatureAlgorithm::HmacSha256,
221            SignatureAlgorithm::HmacMd5,
222        ] {
223            let contiguous = cal_signature_with_algorithm(b"alicetopic-a", "secret", algorithm).unwrap();
224            let segmented =
225                cal_signature_segments_with_algorithm([b"alice".as_slice(), b"topic-a"], "secret", algorithm).unwrap();
226
227            assert_eq!(segmented, contiguous);
228        }
229    }
230
231    #[test]
232    fn test_cal_signature_supports_java_signing_algorithm_enum() {
233        assert_eq!(
234            cal_signature_with_algorithm(b"alicetopic-a", "secret", SignatureAlgorithm::HmacSha1).unwrap(),
235            "3yomro7y0WqWcbV+9tQa4av5w3Q="
236        );
237        assert_eq!(
238            cal_signature_with_algorithm(b"alicetopic-a", "secret", SignatureAlgorithm::HmacSha256).unwrap(),
239            "+VoSl/q1CHGyZYyeI+H1C8SK6N+inDouZi6grscuA/Q="
240        );
241        assert_eq!(
242            cal_signature_with_algorithm(b"alicetopic-a", "secret", SignatureAlgorithm::HmacMd5).unwrap(),
243            "8NLZd12a3GBoWPkyeUsOJQ=="
244        );
245    }
246
247    #[test]
248    fn test_signature_algorithm_deserializes_java_names() {
249        assert_eq!(
250            serde_yaml::from_str::<SignatureAlgorithm>("HmacSHA1").unwrap(),
251            SignatureAlgorithm::HmacSha1
252        );
253        assert_eq!(
254            serde_yaml::from_str::<SignatureAlgorithm>("HmacSHA256").unwrap(),
255            SignatureAlgorithm::HmacSha256
256        );
257        assert_eq!(
258            serde_yaml::from_str::<SignatureAlgorithm>("HmacMD5").unwrap(),
259            SignatureAlgorithm::HmacMd5
260        );
261    }
262
263    #[test]
264    fn test_signature_algorithm_from_java_name_accepts_common_aliases() {
265        assert_eq!(
266            SignatureAlgorithm::from_java_name("HmacSHA1"),
267            Some(SignatureAlgorithm::HmacSha1)
268        );
269        assert_eq!(
270            SignatureAlgorithm::from_java_name("HMAC_SHA256"),
271            Some(SignatureAlgorithm::HmacSha256)
272        );
273        assert_eq!(
274            SignatureAlgorithm::from_java_name("md5"),
275            Some(SignatureAlgorithm::HmacMd5)
276        );
277        assert_eq!(SignatureAlgorithm::from_java_name("unknown"), None);
278    }
279}