confium 0.2.0 → 0.3.1

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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +152 -0
  3. data/Cargo.lock +2634 -0
  4. data/Cargo.toml +9 -0
  5. data/README.adoc +114 -14
  6. data/Rakefile +11 -6
  7. data/confium.gemspec +50 -29
  8. data/ext/confium_native/Cargo.toml +63 -0
  9. data/ext/confium_native/build.rs +72 -0
  10. data/ext/confium_native/extconf.rb +10 -0
  11. data/ext/confium_native/src/attributes.rs +88 -0
  12. data/ext/confium_native/src/audit.rs +169 -0
  13. data/ext/confium_native/src/composite.rs +302 -0
  14. data/ext/confium_native/src/deployment.rs +176 -0
  15. data/ext/confium_native/src/ers.rs +93 -0
  16. data/ext/confium_native/src/lib.rs +56 -0
  17. data/ext/confium_native/src/openpgp.rs +55 -0
  18. data/ext/confium_native/src/path.rs +118 -0
  19. data/ext/confium_native/src/pki.rs +431 -0
  20. data/ext/confium_native/src/tc.rs +420 -0
  21. data/ext/confium_native/src/transparency.rs +343 -0
  22. data/ext/confium_native/src/util.rs +201 -0
  23. data/lib/confium/audit.rb +125 -0
  24. data/lib/confium/cfm.rb +4 -5
  25. data/lib/confium/crypto.rb +50 -0
  26. data/lib/confium/digest.rb +11 -9
  27. data/lib/confium/errors/coerce.rb +47 -0
  28. data/lib/confium/errors/crypto_error.rb +15 -0
  29. data/lib/confium/errors/index_error.rb +15 -0
  30. data/lib/confium/errors/not_found_error.rb +15 -0
  31. data/lib/confium/errors/parse_error.rb +15 -0
  32. data/lib/confium/errors/policy_violation_error.rb +15 -0
  33. data/lib/confium/errors/threshold_error.rb +16 -0
  34. data/lib/confium/errors/unresolved_signer_error.rb +14 -0
  35. data/lib/confium/errors/validation_error.rb +17 -0
  36. data/lib/confium/errors/verification_error.rb +15 -0
  37. data/lib/confium/errors.rb +26 -0
  38. data/lib/confium/ffi.rb +23 -0
  39. data/lib/confium/lib.rb +18 -56
  40. data/lib/confium/openpgp.rb +34 -0
  41. data/lib/confium/pki/certificate_builder.rb +60 -0
  42. data/lib/confium/pki/cms/signed_data_builder.rb +92 -0
  43. data/lib/confium/pki/cms.rb +15 -0
  44. data/lib/confium/pki/cnml.rb +80 -0
  45. data/lib/confium/pki.rb +13 -0
  46. data/lib/confium/policy.rb +138 -0
  47. data/lib/confium/secure_bytes.rb +126 -0
  48. data/lib/confium/tc/coordinator.rb +68 -0
  49. data/lib/confium/tc/session.rb +51 -0
  50. data/lib/confium/tc/session_stub.rb +43 -0
  51. data/lib/confium/tc/share_file.rb +87 -0
  52. data/lib/confium/tc.rb +17 -0
  53. data/lib/confium/transparency/ots.rb +63 -0
  54. data/lib/confium/version.rb +1 -1
  55. data/lib/confium.rb +50 -20
  56. metadata +142 -25
  57. data/CODE_OF_CONDUCT.md +0 -84
  58. data/Gemfile +0 -10
  59. data/sig/confium.rbs +0 -4
@@ -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
+ }