confium 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +126 -0
- data/Cargo.lock +2534 -0
- data/Cargo.toml +9 -0
- data/README.adoc +114 -14
- data/Rakefile +8 -3
- data/confium.gemspec +42 -21
- data/ext/confium_native/Cargo.toml +62 -0
- data/ext/confium_native/build.rs +72 -0
- data/ext/confium_native/extconf.rb +10 -0
- data/ext/confium_native/src/attributes.rs +88 -0
- data/ext/confium_native/src/audit.rs +169 -0
- data/ext/confium_native/src/composite.rs +302 -0
- data/ext/confium_native/src/deployment.rs +176 -0
- data/ext/confium_native/src/ers.rs +93 -0
- data/ext/confium_native/src/lib.rs +56 -0
- data/ext/confium_native/src/openpgp.rs +55 -0
- data/ext/confium_native/src/path.rs +118 -0
- data/ext/confium_native/src/pki.rs +431 -0
- data/ext/confium_native/src/tc.rs +420 -0
- data/ext/confium_native/src/transparency.rs +341 -0
- data/ext/confium_native/src/util.rs +196 -0
- data/lib/confium/audit.rb +125 -0
- data/lib/confium/cfm.rb +2 -4
- data/lib/confium/crypto.rb +50 -0
- data/lib/confium/digest.rb +7 -7
- data/lib/confium/errors/coerce.rb +49 -0
- data/lib/confium/errors/crypto_error.rb +13 -0
- data/lib/confium/errors/index_error.rb +13 -0
- data/lib/confium/errors/not_found_error.rb +13 -0
- data/lib/confium/errors/parse_error.rb +13 -0
- data/lib/confium/errors/policy_violation_error.rb +13 -0
- data/lib/confium/errors/threshold_error.rb +14 -0
- data/lib/confium/errors/unresolved_signer_error.rb +12 -0
- data/lib/confium/errors/validation_error.rb +15 -0
- data/lib/confium/errors/verification_error.rb +13 -0
- data/lib/confium/errors.rb +26 -0
- data/lib/confium/ffi.rb +23 -0
- data/lib/confium/lib.rb +2 -39
- data/lib/confium/openpgp.rb +34 -0
- data/lib/confium/pki/certificate_builder.rb +60 -0
- data/lib/confium/pki/cms/signed_data_builder.rb +92 -0
- data/lib/confium/pki/cms.rb +15 -0
- data/lib/confium/pki/cnml.rb +80 -0
- data/lib/confium/pki.rb +13 -0
- data/lib/confium/policy.rb +138 -0
- data/lib/confium/secure_bytes.rb +124 -0
- data/lib/confium/tc/coordinator.rb +67 -0
- data/lib/confium/tc/session.rb +49 -0
- data/lib/confium/tc/session_stub.rb +43 -0
- data/lib/confium/tc/share_file.rb +87 -0
- data/lib/confium/tc.rb +17 -0
- data/lib/confium/transparency/ots.rb +63 -0
- data/lib/confium/version.rb +1 -1
- data/lib/confium.rb +50 -20
- metadata +142 -25
- data/CODE_OF_CONDUCT.md +0 -84
- data/Gemfile +0 -10
- data/sig/confium.rbs +0 -4
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
//! Confium::PKI::PathValidator — Ruby surface for `confium_pki::path`.
|
|
2
|
+
//!
|
|
3
|
+
//! Validates a certificate chain from leaf to trusted root. Checks
|
|
4
|
+
//! time validity at each link, basic constraints, and (when a
|
|
5
|
+
//! verifier is available) signature validity.
|
|
6
|
+
|
|
7
|
+
use chrono::Utc;
|
|
8
|
+
use confium_pki::{
|
|
9
|
+
cert::Certificate as RustCert,
|
|
10
|
+
path::{validate_path, CertPath},
|
|
11
|
+
result::VerificationResult as PathVerificationResult,
|
|
12
|
+
};
|
|
13
|
+
use magnus::{exception, function, method, prelude::*, DataTypeFunctions, Error, Module, Object, Ruby, TypedData, Value};
|
|
14
|
+
|
|
15
|
+
/// Wraps a confium_pki::path::VerificationResult.
|
|
16
|
+
#[derive(TypedData, DataTypeFunctions)]
|
|
17
|
+
#[magnus(class = "Confium::PKI::PathValidationResult", size)]
|
|
18
|
+
pub struct PathResult {
|
|
19
|
+
pub inner: PathVerificationResult,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl PathResult {
|
|
23
|
+
fn valid(&self) -> bool {
|
|
24
|
+
self.inner.valid
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn checks_json(&self) -> String {
|
|
28
|
+
let entries: Vec<String> = self
|
|
29
|
+
.inner
|
|
30
|
+
.checks
|
|
31
|
+
.iter()
|
|
32
|
+
.map(|c| format!("{:?}", c))
|
|
33
|
+
.collect();
|
|
34
|
+
format!("[{}]", entries.join(","))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn check_count(&self) -> usize {
|
|
38
|
+
self.inner.checks.len()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Validate a certificate path (leaf -> intermediates -> root).
|
|
43
|
+
///
|
|
44
|
+
/// Ruby signature:
|
|
45
|
+
/// Confium::PKI::PathValidator.validate(
|
|
46
|
+
/// leaf: cert, intermediates: [cert, ...], root: cert, now: iso8601
|
|
47
|
+
/// )
|
|
48
|
+
///
|
|
49
|
+
/// All cert arguments are Confium::PKI::Certificate instances.
|
|
50
|
+
/// Returns a Confium::PKI::PathValidationResult.
|
|
51
|
+
fn validate(args: &[Value]) -> Result<magnus::typed_data::Obj<PathResult>, Error> {
|
|
52
|
+
use magnus::scan_args;
|
|
53
|
+
let scanned = scan_args::scan_args::<(Value, Value, Value), (Option<String>,), (), (), (), ()>(args)?;
|
|
54
|
+
let leaf_value = scanned.required.0;
|
|
55
|
+
let intermediates_value = scanned.required.1;
|
|
56
|
+
let root_value = scanned.required.2;
|
|
57
|
+
let now_iso8601 = scanned.optional.0;
|
|
58
|
+
|
|
59
|
+
let leaf = extract_cert(leaf_value, "leaf")?;
|
|
60
|
+
let root = extract_cert(root_value, "root")?;
|
|
61
|
+
|
|
62
|
+
let intermediates = if intermediates_value.is_nil() {
|
|
63
|
+
Vec::new()
|
|
64
|
+
} else {
|
|
65
|
+
let arr = magnus::RArray::try_convert(intermediates_value)
|
|
66
|
+
.map_err(|e| Error::new(exception::arg_error(), e.to_string()))?;
|
|
67
|
+
let mut out = Vec::with_capacity(arr.len());
|
|
68
|
+
for v in arr.each() {
|
|
69
|
+
out.push(extract_cert(v?, "intermediate")?);
|
|
70
|
+
}
|
|
71
|
+
out
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let now = match now_iso8601 {
|
|
75
|
+
Some(s) => chrono::DateTime::parse_from_rfc3339(&s)
|
|
76
|
+
.map_err(|e| Error::new(exception::arg_error(), format!("invalid time: {e}")))?
|
|
77
|
+
.with_timezone(&Utc),
|
|
78
|
+
None => Utc::now(),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
let path = CertPath {
|
|
82
|
+
leaf: &leaf,
|
|
83
|
+
intermediates: intermediates.iter().collect(),
|
|
84
|
+
root: &root,
|
|
85
|
+
};
|
|
86
|
+
let result = validate_path(&path, now);
|
|
87
|
+
|
|
88
|
+
let ruby = Ruby::get()
|
|
89
|
+
.map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
90
|
+
Ok(ruby.obj_wrap(PathResult { inner: result }))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Extract a confium_pki::Certificate from a Ruby Confium::PKI::Certificate
|
|
94
|
+
/// instance. The Ruby object wraps an Obj<Certificate> whose inner field
|
|
95
|
+
/// holds the RustCert. We re-parse from DER to avoid lifetime issues.
|
|
96
|
+
fn extract_cert(value: Value, label: &str) -> Result<RustCert, Error> {
|
|
97
|
+
// The Ruby Certificate object exposes #to_der which returns binary bytes.
|
|
98
|
+
// We re-parse those bytes into a fresh RustCert.
|
|
99
|
+
let der_value: Value = value
|
|
100
|
+
.funcall("to_der", ())
|
|
101
|
+
.map_err(|e| Error::new(exception::runtime_error(), format!("{}: cannot get DER: {e}", label)))?;
|
|
102
|
+
let der = crate::util::bytes_from_value(der_value)?;
|
|
103
|
+
RustCert::from_der(&der)
|
|
104
|
+
.map_err(|e| Error::new(exception::runtime_error(), format!("{}: invalid DER: {e}", label)))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
pub fn init(ruby: &Ruby, parent: magnus::RModule) -> Result<(), Error> {
|
|
108
|
+
let pki = parent.define_module("PKI")?;
|
|
109
|
+
let validator = pki.define_module("PathValidator")?;
|
|
110
|
+
validator.define_module_function("validate", function!(validate, -1))?;
|
|
111
|
+
|
|
112
|
+
let result_class = pki.define_class("PathValidationResult", ruby.class_object())?;
|
|
113
|
+
result_class.define_method("valid?", method!(PathResult::valid, 0))?;
|
|
114
|
+
result_class.define_method("check_count", method!(PathResult::check_count, 0))?;
|
|
115
|
+
result_class.define_method("checks_json", method!(PathResult::checks_json, 0))?;
|
|
116
|
+
|
|
117
|
+
Ok(())
|
|
118
|
+
}
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
//! Confium::PKI — Ruby surface for `confium_pki`.
|
|
2
|
+
//!
|
|
3
|
+
//! Phase 1C scope:
|
|
4
|
+
//! - `Confium::PKI::Certificate` — parse + inspect X.509 v3 certificates.
|
|
5
|
+
//! - `Confium::PKI::CSR` — parse + serialize PKCS#10 certificate signing
|
|
6
|
+
//! requests.
|
|
7
|
+
//! - `Confium::PKI::CMS::SignedData` — JSON-backed CMS SignedData model.
|
|
8
|
+
//!
|
|
9
|
+
//! Verify + parse only on the Ruby side for v0.1.0; full certificate
|
|
10
|
+
//! *building* + signing lands in a follow-up once the Rust builder API
|
|
11
|
+
//! stabilizes (currently `confium_pki::cert::builder` is private).
|
|
12
|
+
|
|
13
|
+
use crate::util::{bytes_from_value, bytes_to_rstring, enforce_size};
|
|
14
|
+
use chrono::{DateTime, Utc};
|
|
15
|
+
use confium_pki::{
|
|
16
|
+
cert::{Certificate as RustCert, CertificateSigningRequest as RustCsr},
|
|
17
|
+
cms::{
|
|
18
|
+
build_detached_signature, encode_signed_data_der, SignedData as RustSignedData,
|
|
19
|
+
},
|
|
20
|
+
xmldsig::{canonicalize, canonicalize_exclusive},
|
|
21
|
+
};
|
|
22
|
+
use magnus::{
|
|
23
|
+
exception, function, method, prelude::*, typed_data::Obj, DataTypeFunctions, Error, Module,
|
|
24
|
+
Object, RHash, RString, Ruby, TryConvert, TypedData, Value,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
#[derive(TypedData, DataTypeFunctions)]
|
|
28
|
+
#[magnus(class = "Confium::PKI::Certificate", size)]
|
|
29
|
+
pub struct Certificate {
|
|
30
|
+
inner: RustCert,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl Certificate {
|
|
34
|
+
fn from_der(bytes: Value) -> Result<Obj<Self>, Error> {
|
|
35
|
+
let der = bytes_from_value(bytes)?;
|
|
36
|
+
let cert = RustCert::from_der(&der)
|
|
37
|
+
.map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
38
|
+
let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
39
|
+
Ok(ruby.obj_wrap(Self { inner: cert }))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn from_pem(pem: String) -> Result<Obj<Self>, Error> {
|
|
43
|
+
enforce_size(pem.len())?;
|
|
44
|
+
let cert = RustCert::from_pem(&pem)
|
|
45
|
+
.map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
46
|
+
let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
47
|
+
Ok(ruby.obj_wrap(Self { inner: cert }))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn to_der(&self) -> Result<RString, Error> {
|
|
51
|
+
let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
52
|
+
Ok(bytes_to_rstring(&ruby, &self.inner.to_der()))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn to_pem(&self) -> String {
|
|
56
|
+
self.inner.to_pem()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn fingerprint_sha256(&self) -> String {
|
|
60
|
+
self.inner.fingerprint_sha256()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fn serial_hex(&self) -> String {
|
|
64
|
+
let bytes = self.inner.serial_bytes();
|
|
65
|
+
let mut out = String::with_capacity(bytes.len() * 2);
|
|
66
|
+
for b in bytes {
|
|
67
|
+
out.push_str(&format!("{:02x}", b));
|
|
68
|
+
}
|
|
69
|
+
out
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn not_before_iso8601(&self) -> String {
|
|
73
|
+
let dt: DateTime<Utc> = self.inner.not_before_chrono();
|
|
74
|
+
dt.to_rfc3339()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn not_after_iso8601(&self) -> String {
|
|
78
|
+
let dt: DateTime<Utc> = self.inner.not_after_chrono();
|
|
79
|
+
dt.to_rfc3339()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn valid_at(&self, iso8601: String) -> Result<bool, Error> {
|
|
83
|
+
let now = DateTime::parse_from_rfc3339(&iso8601)
|
|
84
|
+
.map_err(|e| Error::new(exception::arg_error(), format!("invalid ISO8601 time: {e}")))?
|
|
85
|
+
.with_timezone(&Utc);
|
|
86
|
+
Ok(self.inner.is_within_validity(now))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fn public_key_bytes(&self) -> Result<RString, Error> {
|
|
90
|
+
let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
91
|
+
Ok(bytes_to_rstring(&ruby, self.inner.public_key_bytes()))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[derive(TypedData, DataTypeFunctions)]
|
|
96
|
+
#[magnus(class = "Confium::PKI::CSR", size)]
|
|
97
|
+
pub struct Csr {
|
|
98
|
+
inner: RustCsr,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
impl Csr {
|
|
102
|
+
fn from_der(bytes: Value) -> Result<Obj<Self>, Error> {
|
|
103
|
+
let der = bytes_from_value(bytes)?;
|
|
104
|
+
let csr = RustCsr::from_der(&der)
|
|
105
|
+
.map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
106
|
+
let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
107
|
+
Ok(ruby.obj_wrap(Self { inner: csr }))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fn from_pem(pem: String) -> Result<Obj<Self>, Error> {
|
|
111
|
+
enforce_size(pem.len())?;
|
|
112
|
+
let csr = RustCsr::from_pem(&pem)
|
|
113
|
+
.map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
114
|
+
let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
115
|
+
Ok(ruby.obj_wrap(Self { inner: csr }))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn to_der(&self) -> Result<RString, Error> {
|
|
119
|
+
let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
120
|
+
Ok(bytes_to_rstring(&ruby, &self.inner.to_der()))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn to_pem(&self) -> String {
|
|
124
|
+
self.inner.to_pem()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[derive(TypedData, DataTypeFunctions)]
|
|
129
|
+
#[magnus(class = "Confium::PKI::CMS::SignedData", size)]
|
|
130
|
+
pub struct SignedData {
|
|
131
|
+
inner: std::cell::RefCell<RustSignedData>,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
impl SignedData {
|
|
135
|
+
fn from_json(json: String) -> Result<Obj<Self>, Error> {
|
|
136
|
+
enforce_size(json.len())?;
|
|
137
|
+
let sd: RustSignedData = serde_json::from_str(&json)
|
|
138
|
+
.map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
139
|
+
let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
140
|
+
Ok(ruby.obj_wrap(Self {
|
|
141
|
+
inner: std::cell::RefCell::new(sd),
|
|
142
|
+
}))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Build a detached CMS SignedData with one signer.
|
|
146
|
+
///
|
|
147
|
+
/// Ruby signature:
|
|
148
|
+
/// SignedData.build_detached(signature, algorithm, certificates)
|
|
149
|
+
///
|
|
150
|
+
/// - `signature` — bytes (pre-computed signature over the payload)
|
|
151
|
+
/// - `algorithm` — string (signature algorithm OID)
|
|
152
|
+
/// - `certificates` — array of strings (DER cert bytes per signer)
|
|
153
|
+
///
|
|
154
|
+
/// The caller signs the payload separately (typically via
|
|
155
|
+
/// `Confium::Composite.sign_ed25519` or `Confium::TC::FrostP256.sign`)
|
|
156
|
+
/// and passes the resulting signature bytes here. The first
|
|
157
|
+
/// certificate's first 20 bytes become the SubjectKeyIdentifier per
|
|
158
|
+
/// RFC 5652 §5.3.
|
|
159
|
+
fn build_detached(
|
|
160
|
+
signature: Value,
|
|
161
|
+
algorithm: String,
|
|
162
|
+
certificates: Value,
|
|
163
|
+
) -> Result<Obj<Self>, Error> {
|
|
164
|
+
if signature.is_nil() {
|
|
165
|
+
return Err(Error::new(
|
|
166
|
+
exception::arg_error(),
|
|
167
|
+
"signature is required",
|
|
168
|
+
));
|
|
169
|
+
}
|
|
170
|
+
if certificates.is_nil() {
|
|
171
|
+
return Err(Error::new(
|
|
172
|
+
exception::arg_error(),
|
|
173
|
+
"certificates is required",
|
|
174
|
+
));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let sig_bytes = bytes_from_value(signature)?;
|
|
178
|
+
enforce_size(sig_bytes.len())?;
|
|
179
|
+
|
|
180
|
+
let certs_array: magnus::RArray = magnus::RArray::try_convert(certificates)?;
|
|
181
|
+
let mut cert_ders = Vec::with_capacity(certs_array.len());
|
|
182
|
+
for item in certs_array.each() {
|
|
183
|
+
let v = item?;
|
|
184
|
+
let der = bytes_from_value(v)?;
|
|
185
|
+
enforce_size(der.len())?;
|
|
186
|
+
cert_ders.push(der);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
190
|
+
let sd = build_detached_signature(Vec::new(), algorithm, sig_bytes, cert_ders)
|
|
191
|
+
.map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
192
|
+
Ok(ruby.obj_wrap(Self {
|
|
193
|
+
inner: std::cell::RefCell::new(sd),
|
|
194
|
+
}))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
fn to_json(&self) -> Result<String, Error> {
|
|
198
|
+
serde_json::to_string(&*self.inner.borrow())
|
|
199
|
+
.map_err(|e| Error::new(exception::runtime_error(), e.to_string()))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Encode this SignedData as DER bytes (RFC 5652 ContentInfo).
|
|
203
|
+
///
|
|
204
|
+
/// The output is parseable by `openssl cms` / `openssl pkcs7` and
|
|
205
|
+
/// any standards-compliant CMS consumer.
|
|
206
|
+
fn to_der(&self) -> Result<RString, Error> {
|
|
207
|
+
let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
208
|
+
let der = encode_signed_data_der(&*self.inner.borrow())
|
|
209
|
+
.map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
210
|
+
Ok(bytes_to_rstring(&ruby, &der))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn signer_count(&self) -> usize {
|
|
214
|
+
self.inner.borrow().signer_count()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fn signing_time_iso8601(&self) -> Option<String> {
|
|
218
|
+
self.inner.borrow().signing_time().map(|t| t.to_rfc3339())
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fn content_type(&self) -> String {
|
|
222
|
+
self.inner.borrow().encap_content_info.content_type.clone()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fn content(&self) -> Result<Option<Obj<CertWrapper>>, Error> {
|
|
226
|
+
// Wrap the optional content bytes in a small value object so Ruby
|
|
227
|
+
// can ask `.present?` / `.bytes` without juggling nil-vs-string.
|
|
228
|
+
let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
229
|
+
match &self.inner.borrow().encap_content_info.content {
|
|
230
|
+
Some(bytes) => {
|
|
231
|
+
let wrapper = CertWrapper {
|
|
232
|
+
bytes: bytes.clone(),
|
|
233
|
+
};
|
|
234
|
+
Ok(Some(ruby.obj_wrap(wrapper)))
|
|
235
|
+
}
|
|
236
|
+
None => Ok(None),
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fn certificate_count(&self) -> usize {
|
|
241
|
+
self.inner.borrow().certificates.len()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
fn certificate_at(&self, index: usize) -> Result<RString, Error> {
|
|
245
|
+
let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
246
|
+
self.inner
|
|
247
|
+
.borrow()
|
|
248
|
+
.certificates
|
|
249
|
+
.get(index)
|
|
250
|
+
.map(|c| bytes_to_rstring(&ruby, c))
|
|
251
|
+
.ok_or_else(|| {
|
|
252
|
+
Error::new(
|
|
253
|
+
exception::index_error(),
|
|
254
|
+
format!("certificate index {index} out of range"),
|
|
255
|
+
)
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// Verify every signer's signature. Dispatches by signature algorithm
|
|
260
|
+
/// OID found in each signer_info:
|
|
261
|
+
/// - 1.3.101.112 (Ed25519) -> ed25519-dalek verifier
|
|
262
|
+
/// - 1.2.840.10045.4.3.2 (ECDSA-P256-SHA256) -> p256 verifier
|
|
263
|
+
///
|
|
264
|
+
/// Simplification: assumes the first certificate in `certificates` is
|
|
265
|
+
/// the signing cert for every signer. Production code should resolve
|
|
266
|
+
/// by issuer+serial or subjectKeyIdentifier.
|
|
267
|
+
fn verify_signatures(&self, payload: Value) -> Result<Obj<CmsVerificationResult>, Error> {
|
|
268
|
+
use confium_pki::cms::verify_signed_data;
|
|
269
|
+
let payload_bytes = bytes_from_value(payload)?;
|
|
270
|
+
let sd = self.inner.borrow();
|
|
271
|
+
let result = verify_signed_data(&sd, &payload_bytes, |_signer_index, pubkey_der, signed_bytes, signature| {
|
|
272
|
+
// Inspect the algorithm via the first signer_info's signature_algorithm OID.
|
|
273
|
+
let signer = sd.signer_infos.first().ok_or("no signer infos")?;
|
|
274
|
+
let oid = &signer.signature_algorithm.oid;
|
|
275
|
+
// Strip the DER-encoded public key down to raw key bytes.
|
|
276
|
+
// For Ed25519 SPKI, the last 32 bytes are the raw key.
|
|
277
|
+
// For ECDSA-P256 SPKI, the last 65 bytes are SEC1 uncompressed.
|
|
278
|
+
if oid == "1.3.101.112" {
|
|
279
|
+
// Ed25519.
|
|
280
|
+
if pubkey_der.len() < 32 {
|
|
281
|
+
return Err("Ed25519 public key too short".into());
|
|
282
|
+
}
|
|
283
|
+
let pk_bytes = &pubkey_der[pubkey_der.len() - 32..];
|
|
284
|
+
confium_composite::ed25519_verifier("Ed25519", pk_bytes, signed_bytes, signature)
|
|
285
|
+
} else if oid == "1.2.840.10045.4.3.2" {
|
|
286
|
+
// ECDSA-P256-SHA256.
|
|
287
|
+
if pubkey_der.len() < 65 {
|
|
288
|
+
return Err("ECDSA-P256 public key too short".into());
|
|
289
|
+
}
|
|
290
|
+
let pk_bytes = &pubkey_der[pubkey_der.len() - 65..];
|
|
291
|
+
// confium_composite doesn't have a p256 verifier; reuse the
|
|
292
|
+
// one defined inline in composite.rs (re-implemented here).
|
|
293
|
+
p256_verify_inline(pk_bytes, signed_bytes, signature)
|
|
294
|
+
} else {
|
|
295
|
+
Err(format!("unsupported signature algorithm OID: {oid}"))
|
|
296
|
+
}
|
|
297
|
+
}).map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
298
|
+
let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
299
|
+
Ok(ruby.obj_wrap(CmsVerificationResult { inner: result }))
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#[derive(TypedData, DataTypeFunctions)]
|
|
304
|
+
#[magnus(class = "Confium::PKI::CMS::VerificationResult", size)]
|
|
305
|
+
pub struct CmsVerificationResult {
|
|
306
|
+
pub inner: confium_pki::cms::VerificationResult,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
impl CmsVerificationResult {
|
|
310
|
+
fn all_verified(&self) -> bool {
|
|
311
|
+
self.inner.all_verified
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
fn signer_count(&self) -> usize {
|
|
315
|
+
self.inner.per_signer.len()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fn per_signer_json(&self) -> String {
|
|
319
|
+
// Build JSON manually — the upstream SignerVerification doesn't
|
|
320
|
+
// derive Serialize.
|
|
321
|
+
let entries: Vec<String> = self.inner.per_signer.iter().map(|s| {
|
|
322
|
+
let err = match &s.error {
|
|
323
|
+
Some(e) => format!(",\"error\":{}", serde_json::to_string(e).unwrap_or_else(|_| "null".into())),
|
|
324
|
+
None => String::new(),
|
|
325
|
+
};
|
|
326
|
+
format!(
|
|
327
|
+
"{{\"signer_index\":{},\"verified\":{}{}}}",
|
|
328
|
+
s.signer_index, s.verified, err
|
|
329
|
+
)
|
|
330
|
+
}).collect();
|
|
331
|
+
format!("[{}]", entries.join(","))
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
fn p256_verify_inline(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), String> {
|
|
336
|
+
use p256::ecdsa::{signature::Verifier, Signature, VerifyingKey};
|
|
337
|
+
let vk = VerifyingKey::from_sec1_bytes(public_key)
|
|
338
|
+
.map_err(|e| format!("invalid P-256 public key: {e}"))?;
|
|
339
|
+
let sig = Signature::from_der(signature)
|
|
340
|
+
.map_err(|e| format!("invalid DER signature: {e}"))?;
|
|
341
|
+
vk.verify(message, &sig).map_err(|e| format!("verify: {e}"))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
#[derive(TypedData, DataTypeFunctions)]
|
|
345
|
+
#[magnus(class = "Confium::PKI::CMS::Content", size)]
|
|
346
|
+
pub struct CertWrapper {
|
|
347
|
+
pub bytes: Vec<u8>,
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
impl CertWrapper {
|
|
351
|
+
fn bytes(&self) -> Result<RString, Error> {
|
|
352
|
+
let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
|
|
353
|
+
Ok(bytes_to_rstring(&ruby, &self.bytes))
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
fn length(&self) -> usize {
|
|
357
|
+
self.bytes.len()
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ===== XMLDSig canonicalization (RFC 3076 + Exclusive C14N) =====
|
|
362
|
+
|
|
363
|
+
fn xmldsig_canonicalize(xml: String) -> Result<String, Error> {
|
|
364
|
+
enforce_size(xml.len())?;
|
|
365
|
+
canonicalize(&xml).map_err(|e| Error::new(exception::runtime_error(), e.to_string()))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fn xmldsig_canonicalize_exclusive(xml: String) -> Result<String, Error> {
|
|
369
|
+
enforce_size(xml.len())?;
|
|
370
|
+
canonicalize_exclusive(&xml).map_err(|e| Error::new(exception::runtime_error(), e.to_string()))
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
pub fn init(ruby: &Ruby, parent: magnus::RModule) -> Result<(), Error> {
|
|
374
|
+
let pki = parent.define_module("PKI")?;
|
|
375
|
+
|
|
376
|
+
// Certificate
|
|
377
|
+
let cert_class = pki.define_class("Certificate", ruby.class_object())?;
|
|
378
|
+
cert_class.define_singleton_method("from_der", function!(Certificate::from_der, 1))?;
|
|
379
|
+
cert_class.define_singleton_method("from_pem", function!(Certificate::from_pem, 1))?;
|
|
380
|
+
cert_class.define_method("to_der", method!(Certificate::to_der, 0))?;
|
|
381
|
+
cert_class.define_method("to_pem", method!(Certificate::to_pem, 0))?;
|
|
382
|
+
cert_class.define_method("fingerprint_sha256", method!(Certificate::fingerprint_sha256, 0))?;
|
|
383
|
+
cert_class.define_method("serial_hex", method!(Certificate::serial_hex, 0))?;
|
|
384
|
+
cert_class.define_method("not_before", method!(Certificate::not_before_iso8601, 0))?;
|
|
385
|
+
cert_class.define_method("not_after", method!(Certificate::not_after_iso8601, 0))?;
|
|
386
|
+
cert_class.define_method("valid_at?", method!(Certificate::valid_at, 1))?;
|
|
387
|
+
cert_class.define_method("public_key_bytes", method!(Certificate::public_key_bytes, 0))?;
|
|
388
|
+
|
|
389
|
+
// CSR
|
|
390
|
+
let csr_class = pki.define_class("CSR", ruby.class_object())?;
|
|
391
|
+
csr_class.define_singleton_method("from_der", function!(Csr::from_der, 1))?;
|
|
392
|
+
csr_class.define_singleton_method("from_pem", function!(Csr::from_pem, 1))?;
|
|
393
|
+
csr_class.define_method("to_der", method!(Csr::to_der, 0))?;
|
|
394
|
+
csr_class.define_method("to_pem", method!(Csr::to_pem, 0))?;
|
|
395
|
+
|
|
396
|
+
// CMS submodule
|
|
397
|
+
let cms = pki.define_module("CMS")?;
|
|
398
|
+
let sd_class = cms.define_class("SignedData", ruby.class_object())?;
|
|
399
|
+
sd_class.define_singleton_method("from_json", function!(SignedData::from_json, 1))?;
|
|
400
|
+
sd_class.define_singleton_method("build_detached", function!(SignedData::build_detached, 3))?;
|
|
401
|
+
sd_class.define_method("to_json", method!(SignedData::to_json, 0))?;
|
|
402
|
+
sd_class.define_method("to_der", method!(SignedData::to_der, 0))?;
|
|
403
|
+
sd_class.define_method("signer_count", method!(SignedData::signer_count, 0))?;
|
|
404
|
+
sd_class.define_method("signing_time", method!(SignedData::signing_time_iso8601, 0))?;
|
|
405
|
+
sd_class.define_method("content_type", method!(SignedData::content_type, 0))?;
|
|
406
|
+
sd_class.define_method("content", method!(SignedData::content, 0))?;
|
|
407
|
+
sd_class.define_method("certificate_count", method!(SignedData::certificate_count, 0))?;
|
|
408
|
+
sd_class.define_method("certificate_at", method!(SignedData::certificate_at, 1))?;
|
|
409
|
+
sd_class.define_method("verify_signatures", method!(SignedData::verify_signatures, 1))?;
|
|
410
|
+
|
|
411
|
+
let verify_class = cms.define_class("VerificationResult", ruby.class_object())?;
|
|
412
|
+
verify_class.define_method("all_verified?", method!(CmsVerificationResult::all_verified, 0))?;
|
|
413
|
+
verify_class.define_method("signer_count", method!(CmsVerificationResult::signer_count, 0))?;
|
|
414
|
+
verify_class.define_method("per_signer_json", method!(CmsVerificationResult::per_signer_json, 0))?;
|
|
415
|
+
|
|
416
|
+
let content_class = cms.define_class("Content", ruby.class_object())?;
|
|
417
|
+
content_class.define_method("bytes", method!(CertWrapper::bytes, 0))?;
|
|
418
|
+
content_class.define_method("length", method!(CertWrapper::length, 0))?;
|
|
419
|
+
content_class.define_method("size", method!(CertWrapper::length, 0))?;
|
|
420
|
+
|
|
421
|
+
// XMLDSig submodule — Canonical XML (RFC 3076) and Exclusive C14N.
|
|
422
|
+
let xmldsig = pki.define_module("XMLDSig")?;
|
|
423
|
+
xmldsig.define_module_function("canonicalize", function!(xmldsig_canonicalize, 1))?;
|
|
424
|
+
xmldsig.define_module_function("canonicalize_exclusive", function!(xmldsig_canonicalize_exclusive, 1))?;
|
|
425
|
+
|
|
426
|
+
// Touch RHash to silence dead-code warning from import; the type is
|
|
427
|
+
// used implicitly via Ruby Hash conversion paths.
|
|
428
|
+
let _: Option<RHash> = None;
|
|
429
|
+
|
|
430
|
+
Ok(())
|
|
431
|
+
}
|