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,169 @@
1
+ //! Confium::Audit — Ruby surface for audit logging.
2
+ //!
3
+ //! Stores the sink callback as a module-level instance variable on
4
+ //! Confium::Audit. The sink is a Ruby Proc that receives an audit
5
+ //! record Hash.
6
+ //!
7
+ //! In addition to the user-facing `record` function, this module
8
+ //! exposes `fire_event` for the rest of the native extension to
9
+ //! emit audit records from signing / verification entry points.
10
+ //! `fire_event` is best-effort: if no sink is configured, or the
11
+ //! sink raises, the calling operation still succeeds. Audit
12
+ //! failures must never break a signing ceremony.
13
+
14
+ use magnus::{exception, function, prelude::*, Error, Module, Ruby, Value};
15
+ use sha2::{Digest, Sha256};
16
+
17
+ const SINK_IVAR: &str = "@sink";
18
+
19
+ /// Best-effort audit event emitter. Called from signing / verification
20
+ /// entry points after the operation completes (whether successful or
21
+ /// not). Structured fields:
22
+ ///
23
+ /// - `operation` — short slug like `"composite_sign"` or `"tc_cmp20_sign"`.
24
+ /// - `result` — `"success"` or `"failure"`.
25
+ /// - `algorithm` — optional algorithm identifier.
26
+ /// - `payload_hash` — hex SHA-256 of the signed/verified bytes (when
27
+ /// applicable; pass `None` if no payload is involved).
28
+ /// - `error` — optional error message on failure.
29
+ ///
30
+ /// This function is intentionally silent on errors. If the sink
31
+ /// itself raises, the exception is logged to stderr but does not
32
+ /// propagate — the caller's crypto op has already succeeded.
33
+ pub(crate) fn fire_event(
34
+ operation: &str,
35
+ result: &str,
36
+ algorithm: Option<&str>,
37
+ payload: Option<&[u8]>,
38
+ error: Option<&str>,
39
+ ) {
40
+ let ruby = match Ruby::get() {
41
+ Ok(r) => r,
42
+ Err(_) => return,
43
+ };
44
+ let audit_mod: magnus::RModule = match ruby
45
+ .class_object()
46
+ .const_get::<_, magnus::RModule>("Confium")
47
+ .and_then(|m: magnus::RModule| m.const_get::<_, magnus::RModule>("Audit"))
48
+ {
49
+ Ok(m) => m,
50
+ Err(_) => return,
51
+ };
52
+ let sink: Value = match audit_mod.ivar_get(SINK_IVAR) {
53
+ Ok(v) => v,
54
+ Err(_) => return,
55
+ };
56
+ if sink.is_nil() {
57
+ return;
58
+ }
59
+
60
+ let payload_hex = payload
61
+ .map(|bytes| {
62
+ let mut h = Sha256::new();
63
+ h.update(bytes);
64
+ hex::encode(h.finalize())
65
+ })
66
+ .unwrap_or_default();
67
+
68
+ let h = ruby.hash_new();
69
+ let _ = h.aset("timestamp", chrono::Utc::now().to_rfc3339());
70
+ let _ = h.aset("operation", operation);
71
+ let _ = h.aset("result", result);
72
+ let _ = h.aset("payload_hash", payload_hex);
73
+ if let Some(a) = algorithm {
74
+ let _ = h.aset("algorithm", a);
75
+ }
76
+ if let Some(e) = error {
77
+ let _ = h.aset("error", e);
78
+ }
79
+
80
+ // The sink may be a Proc or any object responding to #call
81
+ // (e.g. Confium::Audit::MemorySink). If it raises, log to stderr
82
+ // but don't propagate — audit must not break signing.
83
+ if let Err(e) = sink.funcall::<_, _, Value>("call", (h,)) {
84
+ eprintln!("confium: audit sink raised (audit event dropped): {e:?}");
85
+ }
86
+ }
87
+
88
+ /// Record an audit entry. Called from Ruby via Confium::Audit.record.
89
+ fn record(
90
+ operation: String,
91
+ payload_hash: String,
92
+ result: String,
93
+ actor: Option<String>,
94
+ algorithm: Option<String>,
95
+ error: Option<String>,
96
+ ) -> Result<(), Error> {
97
+ let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
98
+ let audit_mod: magnus::RModule = ruby
99
+ .class_object()
100
+ .const_get("Confium")
101
+ .and_then(|m: magnus::RModule| m.const_get("Audit"))
102
+ .map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
103
+
104
+ let sink: Value = audit_mod.ivar_get(SINK_IVAR)?;
105
+ if sink.is_nil() {
106
+ return Ok(());
107
+ }
108
+
109
+ let h = ruby.hash_new();
110
+ let _ = h.aset("timestamp", chrono::Utc::now().to_rfc3339());
111
+ let _ = h.aset("operation", operation);
112
+ if let Some(a) = actor {
113
+ let _ = h.aset("actor", a);
114
+ }
115
+ if let Some(alg) = algorithm {
116
+ let _ = h.aset("algorithm", alg);
117
+ }
118
+ let _ = h.aset("payload_hash", payload_hash);
119
+ let _ = h.aset("result", result);
120
+ if let Some(e) = error {
121
+ let _ = h.aset("error", e);
122
+ }
123
+
124
+ let _: Value = sink
125
+ .funcall("call", (h,))
126
+ .map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
127
+ Ok(())
128
+ }
129
+
130
+ /// Set the global audit sink. `callback` is a Proc that receives a Hash
131
+ /// or nil to disable auditing.
132
+ fn set_sink(callback: Value) -> Result<(), Error> {
133
+ let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
134
+ let audit_mod: magnus::RModule = ruby
135
+ .class_object()
136
+ .const_get("Confium")
137
+ .and_then(|m: magnus::RModule| m.const_get("Audit"))
138
+ .map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
139
+ audit_mod.ivar_set(SINK_IVAR, callback)?;
140
+ Ok(())
141
+ }
142
+
143
+ /// Get the current audit sink (or nil if not set).
144
+ fn get_sink() -> Result<Value, Error> {
145
+ let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
146
+ let audit_mod: magnus::RModule = ruby
147
+ .class_object()
148
+ .const_get("Confium")
149
+ .and_then(|m: magnus::RModule| m.const_get("Audit"))
150
+ .map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
151
+ audit_mod.ivar_get(SINK_IVAR)
152
+ }
153
+
154
+ /// Whether audit logging is enabled (a non-nil sink is set).
155
+ fn audit_enabled() -> Result<bool, Error> {
156
+ let sink = get_sink()?;
157
+ Ok(!sink.is_nil())
158
+ }
159
+
160
+ pub fn init(ruby: &Ruby, parent: magnus::RModule) -> Result<(), Error> {
161
+ let audit = parent.define_module("Audit")?;
162
+ // Initialize the ivar to nil.
163
+ audit.ivar_set(SINK_IVAR, ruby.qnil())?;
164
+ audit.define_module_function("sink=", function!(set_sink, 1))?;
165
+ audit.define_module_function("sink", function!(get_sink, 0))?;
166
+ audit.define_module_function("enabled?", function!(audit_enabled, 0))?;
167
+ audit.define_module_function("record", function!(record, 6))?;
168
+ Ok(())
169
+ }
@@ -0,0 +1,302 @@
1
+ //! Confium::Composite — Ruby surface for `confium_composite`.
2
+ //!
3
+ //! Exposes PQ-migration composite signatures (Ed25519 + ML-DSA-65 etc.).
4
+ //!
5
+ //! - `Confium::Composite::Signature` — a composite signature with multiple
6
+ //! algorithm components over the same message. Wraps
7
+ //! `confium_composite::CompositeSignature`.
8
+ //! - `Confium::Composite.sign_ed25519(private_key_bytes, message)` —
9
+ //! helper that builds a real Ed25519 component using
10
+ //! `ed25519_dalek::SigningKey`.
11
+ //! - `Confium::Composite::VerificationResult` — result of `#verify(message)`,
12
+ //! with `#all_verified?` and `#per_component` accessors.
13
+
14
+ use confium_composite::{CompositeSignature, ComponentSignature, VerificationResult};
15
+ use ed25519_dalek::SigningKey;
16
+ use magnus::{
17
+ exception, function, method, prelude::*, scan_args,
18
+ typed_data::Obj, DataTypeFunctions, Error, IntoValue,
19
+ Module, Object, RHash, Ruby, TryConvert, TypedData, Value,
20
+ };
21
+ use crate::util::{bytes_from_value, bytes_to_rstring, confium_error, new_details};
22
+ use rand_core::OsRng;
23
+
24
+ #[derive(TypedData, DataTypeFunctions)]
25
+ #[magnus(class = "Confium::Composite::Signature", size)]
26
+ pub struct CompositeSig {
27
+ inner: std::cell::RefCell<CompositeSignature>,
28
+ }
29
+
30
+ impl CompositeSig {
31
+ fn new(components: Value) -> Result<Self, Error> {
32
+ let comps = parse_components(components)?;
33
+ Ok(Self {
34
+ inner: std::cell::RefCell::new(CompositeSignature::new(comps)),
35
+ })
36
+ }
37
+
38
+ fn component_count(&self) -> usize {
39
+ self.inner.borrow().component_count()
40
+ }
41
+
42
+ fn algorithms(&self) -> Vec<String> {
43
+ self.inner
44
+ .borrow()
45
+ .algorithms()
46
+ .into_iter()
47
+ .map(String::from)
48
+ .collect()
49
+ }
50
+
51
+ /// Verify all components against `message`. Built-in verifiers cover
52
+ /// Ed25519 + ECDSA-P256. Caller-supplied verifiers (a Ruby Hash
53
+ /// `algorithm_string -> Proc(public_key, message, signature)`) plug
54
+ /// in for any other algorithm. Returns a typed VerificationResult.
55
+ fn verify(&self, args: &[Value]) -> Result<Obj<VerificationResultWrap>, Error> {
56
+ let scanned = scan_args::scan_args::<(Value,), (Option<Value>,), (), (), (), ()>(args)?;
57
+ let message = scanned.required.0;
58
+ let verifiers_value = scanned.optional.0;
59
+ let msg = bytes_from_value(message)?;
60
+ let caller_verifiers = match verifiers_value {
61
+ Some(v) => parse_caller_verifiers(v)?,
62
+ None => std::collections::HashMap::new(),
63
+ }; let result = self
64
+ .inner
65
+ .borrow()
66
+ .verify(&msg, |algorithm, public_key, m, signature| {
67
+ if algorithm == confium_composite::ED25519 {
68
+ confium_composite::ed25519_verifier(algorithm, public_key, m, signature)
69
+ } else if algorithm == confium_composite::ECDSA_P256 || algorithm == "ECDSA" {
70
+ confium_composite::p256_verifier(algorithm, public_key, m, signature)
71
+ } else if let Some(callback) = caller_verifiers.get(algorithm) {
72
+ invoke_caller_verifier(callback, public_key, m, signature)
73
+ } else {
74
+ Err(format!("unsupported algorithm: {algorithm}"))
75
+ }
76
+ })
77
+ .map_err(|e| {
78
+ let ruby = Ruby::get().expect("Ruby must be available");
79
+ let details = new_details(&ruby);
80
+ confium_error(e.to_string(), "VerificationError", details)
81
+ })?;
82
+ let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
83
+ Ok(ruby.obj_wrap(VerificationResultWrap { inner: result }))
84
+ }
85
+ }
86
+
87
+ /// Caller-supplied verifier callbacks keyed by algorithm string.
88
+ /// Each value is a Ruby Proc taking (public_key_bytes, message_bytes,
89
+ /// signature_bytes) and returning true/false.
90
+ type CallerVerifiers = std::collections::HashMap<String, magnus::Value>;
91
+
92
+ fn parse_caller_verifiers(v: Value) -> Result<CallerVerifiers, Error> {
93
+ let hash: RHash = RHash::try_convert(v)?;
94
+ let mut out = CallerVerifiers::new();
95
+ hash.foreach(|k: Value, val: Value| {
96
+ let key: String = String::try_convert(k)?;
97
+ out.insert(key, val);
98
+ Ok(magnus::r_hash::ForEach::Continue)
99
+ })?;
100
+ Ok(out)
101
+ }
102
+
103
+ fn invoke_caller_verifier(
104
+ callback: &magnus::Value,
105
+ public_key: &[u8],
106
+ message: &[u8],
107
+ signature: &[u8],
108
+ ) -> Result<(), String> {
109
+ let ruby = Ruby::get().map_err(|e| e.to_string())?;
110
+ let pk = ruby.str_new(std::str::from_utf8(public_key).unwrap_or(""));
111
+ let _ = pk;
112
+ // Use Vec<u8> arguments — magnus converts them to Ruby Array<Integer>.
113
+ let result: magnus::Value = callback
114
+ .funcall("call", (public_key.to_vec(), message.to_vec(), signature.to_vec()))
115
+ .map_err(|e| format!("caller verifier raised: {e}"))?;
116
+ let ok: bool = bool::try_convert(result).map_err(|e| format!("caller verifier returned non-bool: {e}"))?;
117
+ if ok {
118
+ Ok(())
119
+ } else {
120
+ Err("caller verifier returned false".into())
121
+ }
122
+ }
123
+
124
+ #[derive(TypedData, DataTypeFunctions)]
125
+ #[magnus(class = "Confium::Composite::VerificationResult", size)]
126
+ pub struct VerificationResultWrap {
127
+ inner: VerificationResult,
128
+ }
129
+
130
+ impl VerificationResultWrap {
131
+ fn all_verified(&self) -> bool {
132
+ self.inner.all_verified
133
+ }
134
+
135
+ fn per_component(&self) -> Result<Value, Error> {
136
+ let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
137
+ let result = ruby.hash_new();
138
+ for c in &self.inner.per_component {
139
+ let entry = ruby.hash_new();
140
+ entry.aset("algorithm", c.algorithm.clone())?;
141
+ entry.aset("verified", c.verified)?;
142
+ if let Some(err) = &c.error {
143
+ entry.aset("error", err.clone())?;
144
+ }
145
+ result.aset(c.index, entry)?;
146
+ }
147
+ Ok(result.into_value_with(&ruby))
148
+ }
149
+ }
150
+
151
+ /// Build a real Ed25519 component signature.
152
+ ///
153
+ /// `Confium::Composite.sign_ed25519(private_key_bytes, message)` returns
154
+ /// a Hash with `algorithm`, `public_key`, `signature` keys.
155
+ fn sign_ed25519(ruby: &Ruby, private_key: Value, message: Value) -> Result<RHash, Error> {
156
+ let pk_bytes = bytes_from_value(private_key)?;
157
+ let msg = bytes_from_value(message)?;
158
+ if pk_bytes.len() != 32 {
159
+ return Err(Error::new(
160
+ exception::arg_error(),
161
+ format!("Ed25519 private key must be 32 bytes, got {}", pk_bytes.len()),
162
+ ));
163
+ }
164
+ let mut pk_arr = [0u8; 32];
165
+ pk_arr.copy_from_slice(&pk_bytes);
166
+ let signing = SigningKey::from_bytes(&pk_arr);
167
+ let component = match confium_composite::build_ed25519_component(&signing, &msg) {
168
+ Ok(c) => c,
169
+ Err(e) => {
170
+ crate::audit::fire_event(
171
+ "composite_sign_ed25519",
172
+ "failure",
173
+ Some("Ed25519"),
174
+ Some(&msg),
175
+ Some(&e.to_string()),
176
+ );
177
+ return Err(Error::new(exception::runtime_error(), e.to_string()));
178
+ }
179
+ };
180
+
181
+ crate::audit::fire_event(
182
+ "composite_sign_ed25519",
183
+ "success",
184
+ Some("Ed25519"),
185
+ Some(&msg),
186
+ None,
187
+ );
188
+
189
+ let result = ruby.hash_new();
190
+ result.aset("algorithm", component.algorithm)?;
191
+ result.aset("public_key", bytes_to_rstring(ruby, &component.public_key))?;
192
+ result.aset("signature", bytes_to_rstring(ruby, &component.signature))?;
193
+ Ok(result)
194
+ }
195
+
196
+ /// Generate a fresh Ed25519 keypair.
197
+ ///
198
+ /// `Confium::Composite.generate_ed25519_keypair` returns `[private_key, public_key]`
199
+ /// as binary strings (32 bytes each).
200
+ /// Build a real ECDSA-P256 component signature. Useful when the caller
201
+ /// holds a P-256 signing key and wants to participate in a composite
202
+ /// signature alongside e.g. Ed25519 or ML-DSA-65.
203
+ fn sign_p256(ruby: &Ruby, private_key: Value, message: Value) -> Result<RHash, Error> {
204
+ let pk_bytes = bytes_from_value(private_key)?;
205
+ let msg = bytes_from_value(message)?;
206
+ if pk_bytes.len() != 32 {
207
+ return Err(Error::new(
208
+ exception::arg_error(),
209
+ format!("P-256 private key must be 32 bytes, got {}", pk_bytes.len()),
210
+ ));
211
+ }
212
+ let mut arr = [0u8; 32];
213
+ arr.copy_from_slice(&pk_bytes);
214
+ use p256::ecdsa::{Signature, SigningKey, signature::Signer};
215
+ let signing = SigningKey::from_bytes(&arr.into())
216
+ .map_err(|e| Error::new(exception::arg_error(), format!("invalid P-256 private key: {e}")))?;
217
+ let sig: Signature = match signing.try_sign(msg.as_slice()) {
218
+ Ok(s) => s,
219
+ Err(e) => {
220
+ crate::audit::fire_event(
221
+ "composite_sign_p256",
222
+ "failure",
223
+ Some("ECDSA-P256"),
224
+ Some(&msg),
225
+ Some(&format!("sign error: {e}")),
226
+ );
227
+ return Err(Error::new(exception::runtime_error(), format!("sign error: {e}")));
228
+ }
229
+ };
230
+ let verifying = signing.verifying_key();
231
+ let verifying_bytes: Vec<u8> = verifying.to_sec1_bytes().to_vec();
232
+ let sig_bytes: Vec<u8> = sig.to_der().to_bytes().to_vec();
233
+
234
+ crate::audit::fire_event(
235
+ "composite_sign_p256",
236
+ "success",
237
+ Some("ECDSA-P256"),
238
+ Some(&msg),
239
+ None,
240
+ );
241
+
242
+ let result = ruby.hash_new();
243
+ result.aset("algorithm", "ECDSA-P256")?;
244
+ result.aset("public_key", bytes_to_rstring(ruby, &verifying_bytes))?;
245
+ result.aset("signature", bytes_to_rstring(ruby, &sig_bytes))?;
246
+ Ok(result)
247
+ }
248
+
249
+ fn generate_ed25519_keypair(ruby: &Ruby) -> Result<RHash, Error> {
250
+ let mut rng = OsRng;
251
+ let signing = SigningKey::generate(&mut rng);
252
+ let verifying = signing.verifying_key();
253
+ let result = ruby.hash_new();
254
+ result.aset("private_key", bytes_to_rstring(ruby, &signing.to_bytes()))?;
255
+ result.aset("public_key", bytes_to_rstring(ruby, &verifying.to_bytes()))?;
256
+ Ok(result)
257
+ }
258
+
259
+ fn parse_components(value: Value) -> Result<Vec<ComponentSignature>, Error> {
260
+ let arr = magnus::RArray::try_convert(value)?;
261
+ let mut out = Vec::with_capacity(arr.len());
262
+ for v in arr.each() {
263
+ let h: RHash = RHash::try_convert(v?)?;
264
+ let algorithm: String = h.fetch::<_, String>("algorithm")?;
265
+ let public_key: Value = h.fetch::<_, Value>("public_key")?;
266
+ let signature: Value = h.fetch::<_, Value>("signature")?;
267
+ out.push(ComponentSignature {
268
+ algorithm,
269
+ public_key: bytes_from_value(public_key)?,
270
+ signature: bytes_from_value(signature)?,
271
+ });
272
+ }
273
+ Ok(out)
274
+ }
275
+
276
+ pub fn init(ruby: &Ruby, parent: magnus::RModule) -> Result<(), Error> {
277
+ let composite = parent.define_module("Composite")?;
278
+ composite.define_module_function(
279
+ "sign_ed25519",
280
+ function!(sign_ed25519, 2),
281
+ )?;
282
+ composite.define_module_function(
283
+ "sign_p256",
284
+ function!(sign_p256, 2),
285
+ )?;
286
+ composite.define_module_function(
287
+ "generate_ed25519_keypair",
288
+ function!(generate_ed25519_keypair, 0),
289
+ )?;
290
+
291
+ let sig_class = composite.define_class("Signature", ruby.class_object())?;
292
+ sig_class.define_singleton_method("new", function!(CompositeSig::new, 1))?;
293
+ sig_class.define_method("component_count", method!(CompositeSig::component_count, 0))?;
294
+ sig_class.define_method("algorithms", method!(CompositeSig::algorithms, 0))?;
295
+ sig_class.define_method("verify", method!(CompositeSig::verify, -1))?;
296
+
297
+ let result_class = composite.define_class("VerificationResult", ruby.class_object())?;
298
+ result_class.define_method("all_verified?", method!(VerificationResultWrap::all_verified, 0))?;
299
+ result_class.define_method("per_component", method!(VerificationResultWrap::per_component, 0))?;
300
+
301
+ Ok(())
302
+ }
@@ -0,0 +1,176 @@
1
+ //! Confium::Identity + Confium::Config — Ruby surface for the deployment
2
+ //! crate's identity + manifest types.
3
+ //!
4
+ //! Phase 1C-2 scope:
5
+ //! - `Confium::Identity::Actor` — wraps `confium_deployment::identity::ActorIdentity`.
6
+ //! - `Confium::Identity::ACTOR_TYPES` — array of valid actor-type strings.
7
+ //! - `Confium::Config::Manifest` — parse + validate deployment manifest TOML.
8
+
9
+ use confium_deployment::{
10
+ identity::{ActorIdentity, ActorType},
11
+ manifest::{parse_manifest, Manifest as RustManifest},
12
+ validate::validate_manifest,
13
+ };
14
+ use crate::util::enforce_size;
15
+ use magnus::{exception, function, method, typed_data::Obj, DataTypeFunctions, Error, Module, Object, Ruby, TypedData};
16
+
17
+ #[derive(TypedData, DataTypeFunctions)]
18
+ #[magnus(class = "Confium::Identity::Actor", size)]
19
+ pub struct Actor {
20
+ pub inner: std::cell::RefCell<ActorIdentity>,
21
+ }
22
+
23
+ impl Actor {
24
+ fn from_json(json: String) -> Result<Obj<Self>, Error> {
25
+ enforce_size(json.len())?;
26
+ let actor: ActorIdentity = serde_json::from_str(&json)
27
+ .map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
28
+ let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
29
+ Ok(ruby.obj_wrap(Self {
30
+ inner: std::cell::RefCell::new(actor),
31
+ }))
32
+ }
33
+
34
+ fn to_json(&self) -> Result<String, Error> {
35
+ serde_json::to_string(&*self.inner.borrow())
36
+ .map_err(|e| Error::new(exception::runtime_error(), e.to_string()))
37
+ }
38
+
39
+ fn actor_id(&self) -> String {
40
+ self.inner.borrow().actor_id.clone()
41
+ }
42
+
43
+ fn actor_type(&self) -> String {
44
+ actor_type_str(self.inner.borrow().actor_type).to_string()
45
+ }
46
+
47
+ fn quorum_id(&self) -> Option<String> {
48
+ self.inner.borrow().quorum_id.clone()
49
+ }
50
+
51
+ fn registered_at_iso8601(&self) -> String {
52
+ self.inner.borrow().registered_at.to_rfc3339()
53
+ }
54
+
55
+ fn expires_at_iso8601(&self) -> Option<String> {
56
+ self.inner.borrow().expires_at.map(|t| t.to_rfc3339())
57
+ }
58
+
59
+ fn certificate_count(&self) -> usize {
60
+ self.inner.borrow().certificate_chain_der.len()
61
+ }
62
+ }
63
+
64
+ fn actor_type_str(t: ActorType) -> &'static str {
65
+ match t {
66
+ ActorType::Manufacturer => "manufacturer",
67
+ ActorType::TestingLab => "testing_lab",
68
+ ActorType::IssuingAuthorityOfficer => "issuing_authority_officer",
69
+ ActorType::BimlDirector => "biml_director",
70
+ ActorType::QuorumCoordinator => "quorum_coordinator",
71
+ ActorType::Verifier => "verifier",
72
+ }
73
+ }
74
+
75
+ fn actor_types() -> Vec<&'static str> {
76
+ vec![
77
+ actor_type_str(ActorType::Manufacturer),
78
+ actor_type_str(ActorType::TestingLab),
79
+ actor_type_str(ActorType::IssuingAuthorityOfficer),
80
+ actor_type_str(ActorType::BimlDirector),
81
+ actor_type_str(ActorType::QuorumCoordinator),
82
+ actor_type_str(ActorType::Verifier),
83
+ ]
84
+ }
85
+
86
+ #[derive(TypedData, DataTypeFunctions)]
87
+ #[magnus(class = "Confium::Config::Manifest", size)]
88
+ pub struct Manifest {
89
+ pub inner: std::cell::RefCell<RustManifest>,
90
+ }
91
+
92
+ impl Manifest {
93
+ fn from_toml(toml_str: String) -> Result<Obj<Self>, Error> {
94
+ enforce_size(toml_str.len())?;
95
+ let manifest = parse_manifest(&toml_str)
96
+ .map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
97
+ let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
98
+ Ok(ruby.obj_wrap(Self {
99
+ inner: std::cell::RefCell::new(manifest),
100
+ }))
101
+ }
102
+
103
+ fn deployment_name(&self) -> String {
104
+ self.inner.borrow().deployment.name.clone()
105
+ }
106
+
107
+ fn operator(&self) -> String {
108
+ self.inner.borrow().deployment.operator.clone()
109
+ }
110
+
111
+ fn manifest_version(&self) -> u32 {
112
+ self.inner.borrow().deployment.manifest_version
113
+ }
114
+
115
+ fn tier_count(&self) -> usize {
116
+ self.inner.borrow().tiers.len()
117
+ }
118
+
119
+ fn tier_name_at(&self, index: usize) -> Result<String, Error> {
120
+ self.inner
121
+ .borrow()
122
+ .tiers
123
+ .get(index)
124
+ .map(|t| t.name.clone())
125
+ .ok_or_else(|| {
126
+ Error::new(
127
+ exception::index_error(),
128
+ format!("tier index {index} out of range"),
129
+ )
130
+ })
131
+ }
132
+
133
+ fn quorum_count(&self) -> usize {
134
+ self.inner.borrow().quorums.len()
135
+ }
136
+
137
+ fn validate(&self) -> Result<Vec<String>, Error> {
138
+ let m = self.inner.borrow();
139
+ let report = validate_manifest(&m);
140
+ Ok(report.errors)
141
+ }
142
+
143
+ fn is_valid(&self) -> Result<bool, Error> {
144
+ let m = self.inner.borrow();
145
+ let report = validate_manifest(&m);
146
+ Ok(report.errors.is_empty())
147
+ }
148
+ }
149
+
150
+ pub fn init(ruby: &Ruby, parent: magnus::RModule) -> Result<(), Error> {
151
+ let identity = parent.define_module("Identity")?;
152
+ identity.define_module_function("actor_types", function!(actor_types, 0))?;
153
+ let actor_class = identity.define_class("Actor", ruby.class_object())?;
154
+ actor_class.define_singleton_method("from_json", function!(Actor::from_json, 1))?;
155
+ actor_class.define_method("to_json", method!(Actor::to_json, 0))?;
156
+ actor_class.define_method("actor_id", method!(Actor::actor_id, 0))?;
157
+ actor_class.define_method("actor_type", method!(Actor::actor_type, 0))?;
158
+ actor_class.define_method("quorum_id", method!(Actor::quorum_id, 0))?;
159
+ actor_class.define_method("registered_at", method!(Actor::registered_at_iso8601, 0))?;
160
+ actor_class.define_method("expires_at", method!(Actor::expires_at_iso8601, 0))?;
161
+ actor_class.define_method("certificate_count", method!(Actor::certificate_count, 0))?;
162
+
163
+ let config = parent.define_module("Config")?;
164
+ let manifest_class = config.define_class("Manifest", ruby.class_object())?;
165
+ manifest_class.define_singleton_method("from_toml", function!(Manifest::from_toml, 1))?;
166
+ manifest_class.define_method("deployment_name", method!(Manifest::deployment_name, 0))?;
167
+ manifest_class.define_method("operator", method!(Manifest::operator, 0))?;
168
+ manifest_class.define_method("manifest_version", method!(Manifest::manifest_version, 0))?;
169
+ manifest_class.define_method("tier_count", method!(Manifest::tier_count, 0))?;
170
+ manifest_class.define_method("tier_name_at", method!(Manifest::tier_name_at, 1))?;
171
+ manifest_class.define_method("quorum_count", method!(Manifest::quorum_count, 0))?;
172
+ manifest_class.define_method("validate", method!(Manifest::validate, 0))?;
173
+ manifest_class.define_method("valid?", method!(Manifest::is_valid, 0))?;
174
+
175
+ Ok(())
176
+ }