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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +152 -0
- data/Cargo.lock +2634 -0
- data/Cargo.toml +9 -0
- data/README.adoc +114 -14
- data/Rakefile +11 -6
- data/confium.gemspec +50 -29
- data/ext/confium_native/Cargo.toml +63 -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 +343 -0
- data/ext/confium_native/src/util.rs +201 -0
- data/lib/confium/audit.rb +125 -0
- data/lib/confium/cfm.rb +4 -5
- data/lib/confium/crypto.rb +50 -0
- data/lib/confium/digest.rb +11 -9
- data/lib/confium/errors/coerce.rb +47 -0
- data/lib/confium/errors/crypto_error.rb +15 -0
- data/lib/confium/errors/index_error.rb +15 -0
- data/lib/confium/errors/not_found_error.rb +15 -0
- data/lib/confium/errors/parse_error.rb +15 -0
- data/lib/confium/errors/policy_violation_error.rb +15 -0
- data/lib/confium/errors/threshold_error.rb +16 -0
- data/lib/confium/errors/unresolved_signer_error.rb +14 -0
- data/lib/confium/errors/validation_error.rb +17 -0
- data/lib/confium/errors/verification_error.rb +15 -0
- data/lib/confium/errors.rb +26 -0
- data/lib/confium/ffi.rb +23 -0
- data/lib/confium/lib.rb +18 -56
- 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 +126 -0
- data/lib/confium/tc/coordinator.rb +68 -0
- data/lib/confium/tc/session.rb +51 -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,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
|
+
}
|