forked from rh-ecosystem-edge/recert
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcrypto_objects.rs
More file actions
249 lines (211 loc) · 9.25 KB
/
crypto_objects.rs
File metadata and controls
249 lines (211 loc) · 9.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
use super::{
certificate::{self, Certificate},
jwt,
keys::{PrivateKey, PublicKey},
locations::Location,
scanning::ExternalCerts,
};
use anyhow::{bail, Context, Result};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use bytes::Bytes;
use p256::SecretKey;
use pkcs1::DecodeRsaPrivateKey;
use std::{
io::Write,
process::{Command, Stdio},
};
use x509_certificate::InMemorySigningKeyPair;
#[allow(clippy::large_enum_variant)]
pub(crate) enum CryptoObject {
PrivateKey(PrivateKey, PublicKey),
PublicKey(PublicKey),
Certificate(Certificate),
Jwt(jwt::Jwt),
}
impl From<(PrivateKey, PublicKey)> for CryptoObject {
fn from(keys: (PrivateKey, PublicKey)) -> Self {
let (private_key, public_key) = keys;
CryptoObject::PrivateKey(private_key, public_key)
}
}
impl From<PublicKey> for CryptoObject {
fn from(public_key: PublicKey) -> Self {
CryptoObject::PublicKey(public_key)
}
}
impl From<certificate::Certificate> for CryptoObject {
fn from(certificate: certificate::Certificate) -> Self {
CryptoObject::Certificate(certificate)
}
}
impl From<jwt::Jwt> for CryptoObject {
fn from(jwt: jwt::Jwt) -> Self {
CryptoObject::Jwt(jwt)
}
}
pub(crate) struct DiscoveredCryptoObect {
pub(crate) crypto_object: CryptoObject,
pub(crate) location: Location,
}
impl DiscoveredCryptoObect {
pub(crate) fn new(crypto_object: CryptoObject, location: Location) -> Self {
Self { crypto_object, location }
}
}
/// Given a value taken from a YAML field or the entire contents of a file, scan it for
/// cryptographic keys and certificates and record them in the appropriate data structures.
pub(crate) fn process_unknown_value(
value: String,
location: &Location,
external_certs: &ExternalCerts,
) -> Result<Vec<DiscoveredCryptoObect>> {
let pem_bundle_objects = process_pem_bundle(&value, location, external_certs).context("processing pem bundle");
// We intentionally ignore errors from processing PEM bundles because that function easily
// trips up from values that kinda look like PEM (e.g. a serialized install config yaml
// embedded in a configmap entry that contains an additionalTrustBundle PEM, which is
// inherently external, so we don't care about it)
match pem_bundle_objects {
Ok(objects) => {
if !objects.is_empty() {
return Ok(objects);
}
}
Err(err) => log::warn!("ignoring error from processing pem-looking text at location {}: {}", location, err),
};
// If we didn't find any PEM objects, try to process the value as a JWT
if let Some(jwt) = process_jwt(&value, location)? {
Ok(vec![jwt])
} else {
Ok(vec![])
}
}
/// Given a value taken from a YAML field, check if it looks like a JWT and record it in the
/// appropriate data structures.
pub(crate) fn process_jwt(value: &str, location: &Location) -> Result<Option<DiscoveredCryptoObect>> {
// Need a cheap way to detect jwts that doesn't involve parsing them because we run this
// against every secret/configmap data entry
let parts = value.split('.').collect::<Vec<_>>();
if parts.len() != 3 {
return Ok(None);
}
let header = parts[0];
let payload = parts[1];
let signature = parts[2];
if URL_SAFE_NO_PAD.decode(header.as_bytes()).is_err() {
return Ok(None);
}
if URL_SAFE_NO_PAD.decode(payload.as_bytes()).is_err() {
return Ok(None);
}
if URL_SAFE_NO_PAD.decode(signature.as_bytes()).is_err() {
return Ok(None);
}
let jwt = jwt::Jwt { str: value.to_string() };
let location = location.with_jwt()?;
Ok(Some(DiscoveredCryptoObect::new(jwt.into(), location)))
}
/// Given a PEM bundle, scan it for cryptographic keys and certificates and record them in the
/// appropriate data structures.
pub(crate) fn process_pem_bundle(value: &str, location: &Location, external_certs: &ExternalCerts) -> Result<Vec<DiscoveredCryptoObect>> {
let pems = pem::parse_many(value).context("parsing pem")?;
#[allow(clippy::unwrap_used)] // The filter ensures that unwrap will never panic. We can't use
// a filter_map because we want to maintain the index of the pem in the bundle.
pems.iter()
.enumerate()
.map(|(pem_index, pem)| {
process_single_pem(pem, external_certs).with_context(|| format!("processing pem at index {} in the bundle", pem_index))
})
.collect::<Result<Vec<_>>>()
.context("error processing PEM")?
.into_iter()
.enumerate()
.filter(|(_, crypto_object)| crypto_object.is_some())
.map(|(pem_index, crypto_object)| (pem_index, crypto_object.unwrap()))
.map(|(pem_index, crypto_object)| {
Ok(DiscoveredCryptoObect::new(
crypto_object,
location.with_pem_bundle_index(pem_index.try_into()?)?,
))
})
.collect::<Result<Vec<_>>>()
}
/// Given a single PEM, scan it for cryptographic keys and certificates and record them in the
/// appropriate data structures.
pub(crate) fn process_single_pem(pem: &pem::Pem, external_certs: &ExternalCerts) -> Result<Option<CryptoObject>> {
match pem.tag() {
"CERTIFICATE" => process_pem_cert(pem, external_certs).context("processing pem cert"),
"TRUSTED CERTIFICATE" => process_pem_cert(pem, external_certs).context("processing trusted pem cert"), // TODO: we'll have to save it back as TRUSTED
"RSA PRIVATE KEY" => process_pem_rsa_private_key(pem).context("processing pem rsa private key"),
"EC PRIVATE KEY" => process_pem_ec_private_key(pem).context("processing pem ec private key"),
"PRIVATE KEY" => process_pem_private_key(pem).context("processing pem private key"),
"PUBLIC KEY" => bail!("private pkcs8 unsupported"),
"RSA PUBLIC KEY" => Ok(process_pem_public_key(pem)),
"ENTITLEMENT DATA" | "RSA SIGNATURE" => Ok(None),
_ => bail!("unknown pem tag {}", pem.tag()),
}
}
fn process_pem_private_key(pem: &pem::Pem) -> Result<Option<CryptoObject>> {
let pair = InMemorySigningKeyPair::from_pkcs8_der(pem.contents())?;
Ok(match pair {
InMemorySigningKeyPair::Ecdsa(_, _, _) => bail!("private ecdsa pkcs8 unsupported"),
InMemorySigningKeyPair::Ed25519(_) => bail!("private ed25519 pkcs8 unsupported"),
InMemorySigningKeyPair::Rsa(_, bytes) => {
let rsa_private_key = rsa::RsaPrivateKey::from_pkcs1_der(&bytes)?;
let private_part = PrivateKey::Rsa(rsa_private_key);
let public_part = PublicKey::try_from(&private_part)?;
Some((private_part, public_part).into())
}
})
}
pub(crate) fn process_pem_public_key(pem: &pem::Pem) -> Option<CryptoObject> {
Some(PublicKey::from_rsa_bytes(&bytes::Bytes::copy_from_slice(pem.contents())).into())
}
/// Given an RSA private key PEM, record it in the appropriate data structures.
pub(crate) fn process_pem_rsa_private_key(pem: &pem::Pem) -> Result<Option<CryptoObject>> {
let rsa_private_key = rsa::RsaPrivateKey::from_pkcs1_pem(&pem.to_string())?;
let private_part = PrivateKey::Rsa(rsa_private_key);
let public_part = PublicKey::try_from(&private_part)?;
Ok(Some((private_part, public_part).into()))
}
/// Given an EC private key PEM, record it in the appropriate data structures.
pub(crate) fn process_pem_ec_private_key(pem: &pem::Pem) -> Result<Option<CryptoObject>> {
// First convert to pkcs#8 by shelling out to openssl pkcs8 -topk8 -nocrypt:
let mut command = Command::new("openssl")
.arg("pkcs8")
.arg("-topk8")
.arg("-nocrypt")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
command
.stdin
.take()
.context("failed to take openssl stdin pipe")?
.write_all(pem.to_string().as_bytes())?;
let output = command.wait_with_output()?;
let pem = pem::parse(output.stdout)?;
let key = pem.to_string().parse::<SecretKey>()?;
let public_key = key.public_key();
let private_part = PrivateKey::Ec(Bytes::copy_from_slice(pem.contents()));
let public_part = PublicKey::Ec(Bytes::copy_from_slice(public_key.to_string().as_bytes()));
Ok(Some((private_part, public_part).into()))
}
/// Given a certificate PEM, record it in the appropriate data structures.
pub(crate) fn process_pem_cert(pem: &pem::Pem, external_certs: &ExternalCerts) -> Result<Option<CryptoObject>> {
let x509_certificate = &x509_certificate::CapturedX509Certificate::from_der(pem.contents()).context("parsing DER")?;
let hashable_cert = certificate::Certificate::try_from(x509_certificate).context("parsing cert")?;
if external_certs.has_cert(&hashable_cert).context("has external cert")? {
log::trace!("ignoring external cert {}", hashable_cert.subject);
return Ok(None);
} else {
log::trace!("not ignoring internal cert {}", hashable_cert.subject);
}
match hashable_cert.cert.key_algorithm().context("failed to get cert key algorithm")? {
x509_certificate::KeyAlgorithm::Rsa => {}
x509_certificate::KeyAlgorithm::Ecdsa(_) => {}
x509_certificate::KeyAlgorithm::Ed25519 => {
bail!("ed25519 certs unsupported");
}
}
Ok(Some(CryptoObject::from(hashable_cert)))
}