Skip to content

Commit 7339b1e

Browse files
feat: add key_id signature cache (NR-356780) (#1004)
1 parent 88ca4d5 commit 7339b1e

File tree

11 files changed

+560
-134
lines changed

11 files changed

+560
-134
lines changed

Cargo.lock

Lines changed: 95 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

THIRD_PARTY_NOTICES.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,27 @@ Distributed under the following license(s):
206206
* Apache-2.0
207207

208208

209+
## asn1-rs https://crates.io/crates/asn1-rs
210+
211+
Distributed under the following license(s):
212+
* MIT
213+
* Apache-2.0
214+
215+
216+
## asn1-rs-derive https://crates.io/crates/asn1-rs-derive
217+
218+
Distributed under the following license(s):
219+
* MIT
220+
* Apache-2.0
221+
222+
223+
## asn1-rs-impl https://crates.io/crates/asn1-rs-impl
224+
225+
Distributed under the following license(s):
226+
* MIT
227+
* Apache-2.0
228+
229+
209230
## async-broadcast https://crates.io/crates/async-broadcast
210231

211232
Distributed under the following license(s):
@@ -605,6 +626,19 @@ Distributed under the following license(s):
605626
* Apache-2.0
606627

607628

629+
## data-encoding https://crates.io/crates/data-encoding
630+
631+
Distributed under the following license(s):
632+
* MIT
633+
634+
635+
## der-parser https://crates.io/crates/der-parser
636+
637+
Distributed under the following license(s):
638+
* MIT
639+
* Apache-2.0
640+
641+
608642
## deranged https://crates.io/crates/deranged
609643

610644
Distributed under the following license(s):
@@ -1441,6 +1475,13 @@ Distributed under the following license(s):
14411475
* MIT
14421476

14431477

1478+
## oid-registry https://crates.io/crates/oid-registry
1479+
1480+
Distributed under the following license(s):
1481+
* MIT
1482+
* Apache-2.0
1483+
1484+
14441485
## once_cell https://crates.io/crates/once_cell
14451486

14461487
Distributed under the following license(s):
@@ -1765,6 +1806,13 @@ Distributed under the following license(s):
17651806
* Apache-2.0
17661807

17671808

1809+
## rusticata-macros https://crates.io/crates/rusticata-macros
1810+
1811+
Distributed under the following license(s):
1812+
* MIT
1813+
* Apache-2.0
1814+
1815+
17681816
## rustix https://crates.io/crates/rustix
17691817

17701818
Distributed under the following license(s):
@@ -2590,6 +2638,13 @@ Distributed under the following license(s):
25902638
* Unicode-3.0
25912639

25922640

2641+
## x509-parser https://crates.io/crates/x509-parser
2642+
2643+
Distributed under the following license(s):
2644+
* MIT
2645+
* Apache-2.0
2646+
2647+
25932648
## yaml-rust2 https://crates.io/crates/yaml-rust2
25942649

25952650
Distributed under the following license(s):

agent-control/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ rustls = { version = "0.23.18", features = ["ring"] }
6464
rustls-pemfile = { version = "2.1.3" }
6565
rustls-native-certs = "0.8.1"
6666
webpki = { version = "0.22.4", features = ["alloc"] }
67+
x509-parser = "0.16.0"
68+
ring = "0.17.8"
6769

6870
[dev-dependencies]
6971
assert_cmd = { workspace = true }
@@ -89,7 +91,7 @@ httpmock = { version = "0.8.0-alpha.1", features = ["proxy"] }
8991
serial_test = "3.1.1"
9092
futures = "0.3.30"
9193
rcgen = { version = "0.13.2", features = ["crypto"] }
92-
ring = "0.17.8"
94+
9395

9496
[build-dependencies]
9597
glob = "0.3.1"

agent-control/src/agent_control/http_server/status_updater.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::event::{AgentControlEvent, SubAgentEvent};
33
use std::sync::Arc;
44
use tokio::sync::mpsc::UnboundedReceiver;
55
use tokio::sync::RwLock;
6-
use tracing::debug;
6+
use tracing::{debug, trace};
77

88
pub(super) async fn on_agent_control_event_update_status(
99
mut agent_control_event_consumer: UnboundedReceiver<AgentControlEvent>,
@@ -50,7 +50,7 @@ async fn update_agent_control_status(
5050
unreachable!("AgentControlStopped is controlled outside");
5151
}
5252
AgentControlEvent::OpAMPConnected => {
53-
debug!("opamp server is reachable");
53+
trace!("opamp server is reachable");
5454
status.opamp.reachable();
5555
}
5656
AgentControlEvent::OpAMPConnectFailed(error_code, error_message) => {

agent-control/src/opamp/remote_config/signature.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,10 @@ impl SignatureData {
275275
pub fn signature_algorithm(&self) -> &SignatureAlgorithm {
276276
(&self.signing_algorithm).into()
277277
}
278+
279+
pub fn key_id(&self) -> &str {
280+
&self.key_id
281+
}
278282
}
279283

280284
/// CRC32 checksum of the config data. Currently this field is ignored since the current implementation of RemoteConfig
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
mod certificate;
12
mod certificate_fetcher;
23
mod certificate_store;
34
pub mod validator;
5+
pub use certificate::public_key_fingerprint;
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use ring::digest;
2+
use std::fmt::Write;
3+
use thiserror::Error;
4+
use webpki::EndEntityCert;
5+
use x509_parser::prelude::{FromDer, X509Certificate};
6+
7+
#[derive(Error, Debug)]
8+
pub enum CertificateError {
9+
#[error("parsing certificate from bytes: `{0}`")]
10+
ParseCertificate(String),
11+
#[error("verifying signature: `{0}`")]
12+
VerifySignature(String),
13+
}
14+
#[derive(Debug, Clone)]
15+
pub struct Certificate {
16+
cert_der: Vec<u8>,
17+
// sha256 digest of the public key
18+
public_key_id: String,
19+
}
20+
21+
impl Certificate {
22+
pub fn try_new(cert_der: Vec<u8>) -> Result<Self, CertificateError> {
23+
let _ = EndEntityCert::try_from(cert_der.as_slice())
24+
.map_err(|e| CertificateError::ParseCertificate(e.to_string()))?;
25+
26+
let (_, cer) = X509Certificate::from_der(&cert_der)
27+
.map_err(|e| CertificateError::ParseCertificate(e.to_string()))?;
28+
29+
Ok(Self {
30+
public_key_id: public_key_fingerprint(cer.public_key().raw),
31+
cert_der,
32+
})
33+
}
34+
pub fn public_key_id(&self) -> &str {
35+
&self.public_key_id
36+
}
37+
pub fn verify_signature(
38+
&self,
39+
algorithm: &webpki::SignatureAlgorithm,
40+
msg: &[u8],
41+
signature: &[u8],
42+
) -> Result<(), CertificateError> {
43+
let certificate = EndEntityCert::try_from(self.cert_der.as_slice())
44+
.map_err(|e| CertificateError::VerifySignature(e.to_string()))?;
45+
46+
certificate
47+
.verify_signature(algorithm, msg, signature)
48+
.map_err(|e| CertificateError::VerifySignature(e.to_string()))
49+
}
50+
}
51+
52+
pub fn public_key_fingerprint(public_key: &[u8]) -> String {
53+
let key_id_bytes = digest::digest(&digest::SHA256, public_key);
54+
55+
// encode the digest as hex string
56+
key_id_bytes
57+
.as_ref()
58+
.iter()
59+
.fold(String::new(), |mut output, b| {
60+
let _ = write!(output, "{b:02x}");
61+
output
62+
})
63+
}
64+
65+
#[cfg(test)]
66+
mod tests {
67+
use super::*;
68+
use std::io::Cursor;
69+
70+
#[test]
71+
fn test_certificate_key_id() {
72+
let mut cursor = Cursor::new(CERT_PEM.as_bytes());
73+
let cert = rustls_pemfile::certs(cursor.get_mut())
74+
.next()
75+
.unwrap()
76+
.unwrap();
77+
78+
let key_id = Certificate::try_new(cert.as_ref().to_vec())
79+
.unwrap()
80+
.public_key_id()
81+
.to_string();
82+
83+
assert_eq!(key_id, CERT_PUBLIC_KEY_ID);
84+
}
85+
86+
const CERT_PUBLIC_KEY_ID: &str =
87+
"3c333a786b8f1e93f3a099a09cf591c5faac126ea48699e9e290e72b0b6bf06c";
88+
const CERT_PEM: &str = r#"-----BEGIN CERTIFICATE-----
89+
MIIDLzCCAhegAwIBAgIUc5RF25ZGKeFSMlB8EK0EuDxZUFgwDQYJKoZIhvcNAQEL
90+
BQAwHjELMAkGA1UEBhMCRkkxDzANBgNVBAMMBmNhbmFtZTAeFw0yNTAxMjAwNzU2
91+
NTBaFw0yNjAxMjAwNzU2NTBaMCAxCzAJBgNVBAYTAkZJMREwDwYDVQQDDAh0ZXN0
92+
bmFtZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL6pXsPX+0HpRdp+
93+
xD88Ut/SL26kmYSCaY9U1nCo45bARlTlhW62Bf5WMETJhGGi/Kq93MjPMkmNFNF/
94+
2qQx+XpxmKQR+B/iQzrg9bD1evRQPQvnSFBHKMh8cbqVpsLq/p6ee2iMoDpQ8C8p
95+
Y1WjmGhcpp7EpDLUwx2x8NOu+uZp7NjT2rFBni7KMcWKJXEYh59EHkL/J/DeTUtQ
96+
0Jxrq6k2hbEBOxRzO3XdwZ3w+LlurankJBOBljLpXn7Du9iA/0BicWczBhwJqv3T
97+
96gyxoClmyGpXRiaiHyP+6t7/xfNfwJ6AEuifyVIUnxEyP+lgx6stWnV2j58a4kT
98+
asRIASECAwEAAaNjMGEwHwYDVR0jBBgwFoAUwg0OUU2UnO8UnMGFAjUdIl2S5Jow
99+
CQYDVR0TBAIwADAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFLoNRu6n
100+
UepmUndgCwPr7tHQ84N0MA0GCSqGSIb3DQEBCwUAA4IBAQB4yKCYrdbz4FGxfA4K
101+
GbgXe0ylio1OCA/4Db3Xo/UYJwKG+sG5YWKJiOTqJqdOPSczZE8ROA9BNLKpfUXj
102+
hIffqUXca298j+8Ag+gFE5oOnUF1RUwE+xLWj94Fby4yFeadPcn1E7amSGoK1kE2
103+
ksQmmplpaVP9lOKnk6pX9NbMsAW2IeuDROuCYyTE9XOUxzdNnQp2Uk7rnxbGHHIl
104+
ag5JWpNv/SRwijhGyVKiLFINYILDaNZc56RxxNWgfKj8mTiRvFV5OiM0MrIjBCUu
105+
O0jhqIc+AEbSGU0jdfFxs4f9fJklHDphUxqE1MSvqzOMaFNrt/8jEupa2ujLCVId
106+
XeFA
107+
-----END CERTIFICATE-----"#;
108+
}

0 commit comments

Comments
 (0)