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
data/Cargo.toml ADDED
@@ -0,0 +1,9 @@
1
+ [workspace]
2
+ resolver = "2"
3
+ members = ["ext/confium_native"]
4
+
5
+ [profile.release]
6
+ opt-level = 3
7
+ lto = "off"
8
+ codegen-units = 1
9
+ debug = true
data/README.adoc CHANGED
@@ -1,39 +1,139 @@
1
1
  = Confium Ruby bindings
2
+ :toc: macro
3
+ :toclevels: 2
2
4
 
3
- == Purpose
5
+ Ruby bindings for the Confium open-source framework for multi-stakeholder threshold cryptography.
4
6
 
5
- Ruby bindings for Confium.
7
+ Confium supports three deployment modes:
8
+
9
+ * **Mode 1 — Peer-to-peer threshold cryptography**: nodes do TC directly (MPC, distributed custody, BFT)
10
+ * **Mode 2 — TC PKI replacement**: drop-in for existing PKI consumers (PKCS#11 server, OpenSSL 3.0 provider, JCE)
11
+ * **Mode 3 — TC Certificate PKI**: institutional deployments with custom certificate formats (OIML CNML, BIPM, pharma, accreditation)
12
+
13
+ This gem wraps the high-value Confium subsystems via a Rust native extension (built at `gem install` time with `rb_sys` + `magnus`). No separate C ABI library to install; everything is statically linked into the extension.
6
14
 
7
15
  == Installation
8
16
 
9
- Install the gem and add to the application's Gemfile by executing:
17
+ Add to your Gemfile:
10
18
 
11
- [source,sh]
19
+ [source,ruby]
12
20
  ----
13
- $ bundle add confium
21
+ gem "confium", "~> 0.1"
14
22
  ----
15
23
 
16
- If bundler is not being used to manage dependencies, install the gem by executing:
24
+ Or install directly:
17
25
 
18
26
  [source,sh]
19
27
  ----
20
28
  $ gem install confium
21
29
  ----
22
30
 
23
- == Usage
31
+ === Prerequisites
32
+
33
+ * Ruby ≥ 3.1
34
+ * Rust stable toolchain (`rustup default stable`)
35
+ * C toolchain (clang/gcc/Xcode CLT)
36
+
37
+ The gem compiles a Rust cdylib at install time and links it into Ruby as a native extension. There is no separate `libconfium` to install.
38
+
39
+ == Quick start
24
40
 
25
- TODO: Write usage instructions here
41
+ [source,ruby]
42
+ ----
43
+ require "confium"
44
+
45
+ # Transparency log
46
+ tree = Confium::Transparency::MerkleTree.new
47
+ seq = tree.append(artifact_hash_bytes_32)
48
+ root = tree.root # binary String, 32 bytes
49
+ proof = tree.inclusion_proof(seq)
50
+ proof.verify(root) # => true
51
+
52
+ # Composite signature (PQ migration)
53
+ kp = Confium::Composite.generate_ed25519_keypair
54
+ component = Confium::Composite.sign_ed25519(kp["private_key"], "message")
55
+ sig = Confium::Composite::Signature.new([component])
56
+ result = sig.verify("message")
57
+ result.all_verified? # => true
58
+
59
+ # Attribute-based threshold policy
60
+ pred = Confium::Attributes.parse(%q{and(min_count("role:director", 3), min_distinct("region", 3))})
61
+ alice = Confium::Attributes::Signer.new
62
+ alice.add("role:director", "yes")
63
+ alice.add("region", "europe")
64
+ # ... bob, carol similarly
65
+ pred.satisfied_by?([alice, bob, carol]) # => true
66
+
67
+ # X.509 certificate
68
+ cert = Confium::PKI::Certificate.from_pem(File.read("cert.pem"))
69
+ puts cert.fingerprint_sha256
70
+ puts cert.valid_at?(Time.now.utc.iso8601) # => true
71
+
72
+ # Real threshold cryptography: P-256 Shamir
73
+ kp = Confium::TC::FrostP256.generate_keypair
74
+ shares = Confium::TC::FrostP256.split_secret(kp["private_key"], 3, 5)
75
+ recovered = Confium::TC::FrostP256.recover_secret(shares.first(3).map { |s| { "x" => s.x, "y" => s.y_bytes } })
76
+ recovered == kp["private_key"] # => true
77
+ ----
78
+
79
+ == API surface (v0.1.0)
80
+
81
+ === `Confium::Transparency`
82
+ - `MerkleTree.new` / `#append(artifact_hash)` / `#root` / `#length` / `#empty?` / `#inclusion_proof(seq)`
83
+ - `InclusionProof#sequence` / `#steps` / `#verify(root)`
84
+
85
+ === `Confium::Composite` — PQ migration
86
+ - `.generate_ed25519_keypair` → `{ private_key:, public_key: }`
87
+ - `.sign_ed25519(private_key, message)` → component Hash
88
+ - `Signature.new(components)` / `#verify(message)` / `#component_count` / `#algorithms`
89
+ - `VerificationResult#all_verified?` / `#per_component`
90
+
91
+ === `Confium::Attributes` — threshold policy DSL
92
+ - `.parse(dsl_expr)` → `Predicate`
93
+ - `Predicate#satisfied_by?(signers)`
94
+ - `Signer.new` / `#add(key, value)` / `#has?(key)` / `#values(key)`
95
+ - DSL: `min_count("attr", n)`, `min_distinct("attr", n)`, `any("attr")`, `all("attr")`, `none("attr")`, `and(...)`, `or(...)`, `not(p)`
96
+
97
+ === `Confium::PKI`
98
+ - `Certificate.from_der(bytes)` / `.from_pem(str)` / `#to_der` / `#to_pem` / `#fingerprint_sha256` / `#serial_hex` / `#not_before` / `#not_after` / `#valid_at?(iso8601)` / `#public_key_bytes`
99
+ - `CSR.from_der` / `.from_pem` / `#to_der` / `#to_pem`
100
+ - `CMS::SignedData.from_json` / `#to_json` / `#signer_count` / `#content_type` / `#content` / `#certificate_count` / `#certificate_at(i)`
101
+ - `CMS::Content#bytes` / `#length`
102
+ - `XMLDSig.canonicalize(xml)` / `.canonicalize_exclusive(xml)` (Canonical XML RFC 3076 + Exclusive C14N)
103
+
104
+ === `Confium::Identity` — actor roles
105
+ - `.actor_types` → `["manufacturer", "testing_lab", "issuing_authority_officer", "biml_director", "quorum_coordinator", "verifier"]`
106
+ - `Actor.from_json(json)` / `#to_json` / `#actor_id` / `#actor_type` / `#quorum_id` / `#registered_at` / `#expires_at` / `#certificate_count`
107
+
108
+ === `Confium::Config` — deployment manifest
109
+ - `Manifest.from_toml(toml_str)` / `#deployment_name` / `#operator` / `#manifest_version` / `#tier_count` / `#tier_name_at(i)` / `#quorum_count` / `#validate` / `#valid?`
110
+
111
+ === `Confium::TC::FrostP256` — real P-256 Shamir + ECDSA
112
+ - `.generate_keypair` → `{ private_key: (32 bytes), public_key: (65 bytes SEC1) }`
113
+ - `.split_secret(secret, t, n)` → array of `Share` with `#x`, `#y_bytes`
114
+ - `.recover_secret([{x:, y:}, ...])` → secret bytes
115
+ - `.sign(private_key, message)` → `{ der:, fixed: }`
116
+
117
+ === `Confium::TC::ElGamalP256` — threshold ElGamal-P256 KEM
118
+ - `.encapsulate(public_key_bytes)` → `{ ciphertext: { c1:, c2: }, shared_secret: }`
119
+ - `.partial_decrypt(party_index, share_bytes, ciphertext)` → `{ party_index:, bytes: }`
120
+ - `.aggregate_partials(partials, threshold, ciphertext)` → shared_secret bytes
26
121
 
27
122
  == Development
28
123
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
124
+ After checking out the repo:
30
125
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
126
+ [source,sh]
127
+ ----
128
+ $ bundle install
129
+ $ bundle exec rake compile # build the Rust extension
130
+ $ bundle exec rspec # 75+ specs
131
+ ----
32
132
 
33
- == Contributing
133
+ == Architecture
34
134
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/confium. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/confium/blob/main/CODE_OF_CONDUCT.md).
135
+ The native extension lives at `ext/confium_native/` as a Cargo workspace. It depends on the `confium-*` crates from crates.io (not on the local Rust workspace). The Ruby-side API is defined entirely in the extension via magnus — `lib/confium.rb` just requires the compiled `.bundle`.
36
136
 
37
- == Code of Conduct
137
+ == License
38
138
 
39
- Everyone interacting in the Confium project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/confium/blob/main/CODE_OF_CONDUCT.md).
139
+ BSD-2-Clause, same as the rest of the Confium workspace.
data/Rakefile CHANGED
@@ -1,12 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
-
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
6
5
  RSpec::Core::RakeTask.new(:spec)
7
6
 
8
- require "rubocop/rake_task"
9
-
7
+ require 'rubocop/rake_task'
10
8
  RuboCop::RakeTask.new
11
9
 
12
- task default: %i[spec rubocop]
10
+ require 'rb_sys/extensiontask'
11
+ gemspec = Gem::Specification.load('confium.gemspec')
12
+ RbSys::ExtensionTask.new('confium_native', gemspec) do |ext|
13
+ ext.lib_dir = 'lib/confium_native'
14
+ end
15
+
16
+ task compile: :clean
17
+ task default: %i[compile spec rubocop]
data/confium.gemspec CHANGED
@@ -1,35 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "lib/confium/version"
3
+ require_relative 'lib/confium/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = "confium"
6
+ spec.name = 'confium'
7
7
  spec.version = Confium::VERSION
8
- spec.authors = ["Ribose Open"]
9
- spec.email = ["open.source@ribose.com"]
10
-
11
- spec.summary = "FFI bindings for Confium."
12
- # spec.description = "TODO: Write a longer description or delete this line."
13
- spec.homepage = "https://www.confium.org"
14
- spec.required_ruby_version = ">= 2.7.0"
15
-
16
- # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
17
-
18
- spec.metadata["homepage_uri"] = spec.homepage
19
- spec.metadata["source_code_uri"] = "https://github.com/confium/confium-ruby"
20
- spec.metadata["changelog_uri"] = "https://github.com/confium/confium-ruby"
21
-
22
- # Specify which files should be added to the gem when it is released.
23
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
- spec.files = Dir.chdir(__dir__) do
25
- `git ls-files -z`.split("\x0").reject do |f|
26
- (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
27
- end
28
- end
29
- spec.bindir = "exe"
30
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
- spec.require_paths = ["lib"]
32
-
33
- spec.add_dependency "ffi"
34
- spec.add_development_dependency "rspec"
8
+ spec.platform = Gem::Platform::RUBY
9
+
10
+ spec.authors = ['Ribose Open']
11
+ spec.email = ['open.source@ribose.com']
12
+
13
+ spec.summary = 'Ruby bindings for the Confium multi-stakeholder threshold cryptography framework.'
14
+ spec.description = 'Confium provides threshold cryptography for Ruby: FROST / CMP20 / GG18 ' \
15
+ 'signing sessions, X.509 certificate issuance (CNML-ready), composite ' \
16
+ 'PQ-migration signatures, and transparency-log anchoring. Powered by a ' \
17
+ 'Rust native extension (magnus + rb_sys) — no separate C ABI to install.'
18
+ spec.homepage = 'https://www.confium.org'
19
+ spec.license = 'BSD-2-Clause'
20
+
21
+ spec.metadata = {
22
+ 'bug_tracker_uri' => 'https://github.com/confium/confium-ruby/issues',
23
+ 'changelog_uri' => 'https://github.com/confium/confium-ruby/blob/main/CHANGELOG.md',
24
+ 'homepage_uri' => spec.homepage,
25
+ 'source_code_uri' => 'https://github.com/confium/confium-ruby',
26
+ 'rubygems_mfa_required' => 'true'
27
+ }
28
+
29
+ # Rust extension — compiled at gem install via rb_sys.
30
+ spec.extensions = ['ext/confium_native/extconf.rb']
31
+
32
+ spec.files = Dir.glob('{lib,ext}/**/*') + %w[
33
+ CHANGELOG.md
34
+ LICENSE.txt
35
+ Rakefile
36
+ README.adoc
37
+ confium.gemspec
38
+ Cargo.toml
39
+ Cargo.lock
40
+ ]
41
+ spec.files.reject! { |f| File.directory?(f) }
42
+ spec.files.reject! { |f| f =~ /\.(dll|so|dylib|lib|bundle)\Z/ }
43
+ spec.require_paths = ['lib']
44
+
45
+ spec.required_ruby_version = '>= 3.1.0'
46
+
47
+ # Required for the Rust extension.
48
+ spec.add_dependency 'rb_sys', '~> 0.9.39'
49
+
50
+ spec.add_development_dependency 'rake', '~> 13.0'
51
+ spec.add_development_dependency 'rake-compiler', '~> 1.2.0'
52
+ spec.add_development_dependency 'rake-compiler-dock', '~> 1.3'
53
+ spec.add_development_dependency 'rspec', '~> 3.0'
54
+ spec.add_development_dependency 'rubocop', '~> 1.0'
55
+ spec.add_development_dependency 'steep', '~> 1.5'
35
56
  end
@@ -0,0 +1,63 @@
1
+ # confium_native — Rust native extension for the confium Ruby gem.
2
+ #
3
+ # Compiles to a Ruby-loadable cdylib (`confium_native.bundle` / `.so` / `.dll`)
4
+ # via `rb_sys` + `magnus`. The Ruby side loads it through `ext/confium_native/extconf.rb`
5
+ # (invoked by `gem install` or `bundle exec rake compile`).
6
+
7
+ [package]
8
+ name = "confium_native"
9
+ version = "0.3.0"
10
+ edition = "2024"
11
+ rust-version = "1.85"
12
+ publish = false
13
+
14
+ [lib]
15
+ crate-type = ["cdylib"]
16
+
17
+ build = "build.rs"
18
+
19
+ [dependencies]
20
+ # rb-sys for Ruby C API access.
21
+ rb-sys = { version = "0.9.124", features = ["global-allocator"] }
22
+
23
+ # magnus for idiomatic Ruby <-> Rust bindings.
24
+ magnus = { version = "0.8", features = ["bytes"] }
25
+
26
+ # Confium crates — all from crates.io so gem install works without workspace checkout.
27
+ confium-core = "0.2"
28
+ confium-transparency = "0.4"
29
+ confium-composite = "0.3.1"
30
+ confium-attributes = "0.3.1"
31
+ confium-pki = "0.3.1"
32
+ confium-deployment = "0.3"
33
+ confium-tc = "0.3.1"
34
+ confium-tc-frost-p256 = "0.3"
35
+ confium-tc-elgamal-p256 = "0.3"
36
+ confium-tc-cmp20 = "0.3.1"
37
+ confium-tc-gg18 = "0.3.1"
38
+
39
+ # Newly published shared crypto crates (confium product restructuring).
40
+ confium-tc-core = "0.3"
41
+ confium-crypto-vss = "0.3"
42
+ confium-crypto-zk = "0.3"
43
+ confium-privacy = "0.3"
44
+ confium-observability = "0.3"
45
+
46
+ # P-256 scalar/point types used by the TC surface.
47
+ p256 = { version = "0.13", features = ["ecdsa"] }
48
+
49
+ ed25519-dalek = { version = "2", features = ["rand_core"] }
50
+ rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
51
+
52
+ sha2 = "0.10"
53
+ subtle = "2"
54
+ chrono = "0.4"
55
+ hex = "0.4"
56
+ serde_json = "1"
57
+
58
+ # RNP (OpenPGP, RFC 9580) — hard-bundled with vendored feature so
59
+ # librnp is compiled from source via rnp-src. No system librnp needed.
60
+ rnp = { package = "rnp-rs", version = "0.1.10", features = ["vendored"] }
61
+
62
+ [profile.release]
63
+ lto = "off"
@@ -0,0 +1,72 @@
1
+ // build.rs — read the version of confium-core from Cargo.lock and emit
2
+ // it as a CARGO_RUSTC_ENV so lib.rs can read it at compile time.
3
+ //
4
+ // Note: cargo invokes build scripts with a custom cwd (the staging dir),
5
+ // so we cannot rely on `Path::new("Cargo.lock")` being present. Instead
6
+ // we walk up from CARGO_MANIFEST_DIR looking for a Cargo.toml that
7
+ // declares a [workspace] section. That Cargo.lock is the one we need.
8
+
9
+ use std::fs;
10
+ use std::path::PathBuf;
11
+
12
+ fn main() {
13
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
14
+ let workspace_dir = find_workspace_root(&manifest_dir);
15
+ let lockfile_path = workspace_dir.join("Cargo.lock");
16
+ let lockfile = fs::read_to_string(&lockfile_path).expect("could not read workspace Cargo.lock");
17
+
18
+ let version = lockfile
19
+ .lines()
20
+ // find each `[[package]]` block then the `name = "..."` line inside
21
+ .scan(String::new(), |state, line| {
22
+ if line.trim().starts_with("[[package]]") {
23
+ *state = String::new();
24
+ } else {
25
+ state.push_str(line);
26
+ state.push('\n');
27
+ }
28
+ Some(state.clone())
29
+ })
30
+ .filter_map(|block| {
31
+ let name = block.lines().find_map(|l| {
32
+ let trimmed = l.trim();
33
+ trimmed.strip_prefix("name = \"")?.strip_suffix('"').map(String::from)
34
+ })?;
35
+ let version = block.lines().find_map(|l| {
36
+ let trimmed = l.trim();
37
+ trimmed.strip_prefix("version = \"")?.strip_suffix('"').map(String::from)
38
+ })?;
39
+ Some((name, version))
40
+ })
41
+ .find(|(name, _)| name == "confium-core")
42
+ .map(|(_, v)| v);
43
+
44
+ match version {
45
+ Some(v) => {
46
+ println!("cargo:rerun-if-changed={}", lockfile_path.display());
47
+ println!("cargo:rustc-env=CONFIUM_CORE_VERSION={v}");
48
+ }
49
+ None => panic!("confium-core not found in Cargo.lock"),
50
+ }
51
+ }
52
+
53
+ /// Walk up the directory tree from `start` until we find a Cargo.toml
54
+ /// containing a `[workspace]` section. That directory is the workspace
55
+ /// root; its Cargo.lock is what we need.
56
+ fn find_workspace_root(start: &std::path::Path) -> PathBuf {
57
+ let mut current = start.to_path_buf();
58
+ loop {
59
+ let candidate = current.join("Cargo.toml");
60
+ if candidate.exists() {
61
+ if let Ok(contents) = fs::read_to_string(&candidate) {
62
+ if contents.contains("[workspace]") {
63
+ return current;
64
+ }
65
+ }
66
+ }
67
+ match current.parent() {
68
+ Some(parent) => current = parent.to_path_buf(),
69
+ None => panic!("no workspace root found above {}", start.display()),
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+ require 'rb_sys/mkmf'
5
+
6
+ create_rust_makefile('confium_native') do |r|
7
+ r.profile = ENV.fetch('RB_SYS_CARGO_PROFILE', :dev).to_sym
8
+ r.use_stable_api_compiled_fallback = true
9
+ r.force_install_rust_toolchain = false
10
+ end
@@ -0,0 +1,88 @@
1
+ //! Confium::Attributes — Ruby surface for `confium_attributes`.
2
+ //!
3
+ //! Exposes the predicate DSL + evaluator for threshold-signing attribute
4
+ //! policies (e.g., "5-of-9 directors, spanning 3 regions").
5
+ //!
6
+ //! - `Confium::Attributes.parse(expr)` — parse a DSL string into a
7
+ //! `Confium::Attributes::Predicate`.
8
+ //! - `Confium::Attributes::Predicate#satisfied_by?(signers)` — evaluate
9
+ //! against a list of `Confium::Attributes::Signer`.
10
+ //! - `Confium::Attributes::Signer` — a signer's attribute map, with
11
+ //! `#add(key, value)`, `#has?(key)`, `#values(key)`.
12
+
13
+ use confium_attributes::{evaluate, parse as dsl_parse, Predicate, SignerAttributes};
14
+ use magnus::{
15
+ exception, function, method, typed_data::Obj, DataTypeFunctions, Error, Module,
16
+ Object, Ruby, TryConvert, TypedData, Value,
17
+ };
18
+
19
+ #[derive(TypedData, DataTypeFunctions)]
20
+ #[magnus(class = "Confium::Attributes::Predicate", size)]
21
+ pub struct PredicateWrap {
22
+ pub inner: Predicate,
23
+ }
24
+
25
+ impl PredicateWrap {
26
+ fn satisfied_by(&self, signers_value: Value) -> Result<bool, Error> {
27
+ let arr = magnus::RArray::try_convert(signers_value)?;
28
+ let mut owned: Vec<SignerAttributes> = Vec::with_capacity(arr.len());
29
+ for v in arr.each() {
30
+ let signer_wrap = Obj::<SignerWrap>::try_convert(v?)?;
31
+ owned.push(signer_wrap.inner.borrow().clone());
32
+ }
33
+ let refs: Vec<&SignerAttributes> = owned.iter().collect();
34
+ Ok(evaluate(&self.inner, &refs))
35
+ }
36
+ }
37
+
38
+ #[derive(TypedData, DataTypeFunctions)]
39
+ #[magnus(class = "Confium::Attributes::Signer", size)]
40
+ pub struct SignerWrap {
41
+ pub inner: std::cell::RefCell<SignerAttributes>,
42
+ }
43
+
44
+ impl SignerWrap {
45
+ fn new() -> Self {
46
+ Self {
47
+ inner: std::cell::RefCell::new(SignerAttributes::new()),
48
+ }
49
+ }
50
+
51
+ fn add(&self, key: String, value: String) {
52
+ self.inner.borrow_mut().add(key, value);
53
+ }
54
+
55
+ fn has(&self, key: String) -> bool {
56
+ self.inner.borrow().has(&key)
57
+ }
58
+
59
+ fn values(&self, key: String) -> Vec<String> {
60
+ self.inner.borrow().values(&key)
61
+ }
62
+ }
63
+
64
+ fn parse(expr: String) -> Result<Obj<PredicateWrap>, Error> {
65
+ let predicate = dsl_parse(&expr)
66
+ .map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
67
+ let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
68
+ Ok(ruby.obj_wrap(PredicateWrap { inner: predicate }))
69
+ }
70
+
71
+ pub fn init(ruby: &Ruby, parent: magnus::RModule) -> Result<(), Error> {
72
+ let attributes = parent.define_module("Attributes")?;
73
+ attributes.define_module_function("parse", function!(parse, 1))?;
74
+
75
+ let pred_class = attributes.define_class("Predicate", ruby.class_object())?;
76
+ pred_class.define_method(
77
+ "satisfied_by?",
78
+ method!(PredicateWrap::satisfied_by, 1),
79
+ )?;
80
+
81
+ let signer_class = attributes.define_class("Signer", ruby.class_object())?;
82
+ signer_class.define_singleton_method("new", function!(SignerWrap::new, 0))?;
83
+ signer_class.define_method("add", method!(SignerWrap::add, 2))?;
84
+ signer_class.define_method("has?", method!(SignerWrap::has, 1))?;
85
+ signer_class.define_method("values", method!(SignerWrap::values, 1))?;
86
+
87
+ Ok(())
88
+ }