confium 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,10 +19,10 @@ use confium_tc_frost_p256::{
19
19
  };
20
20
  use confium_tc_cmp20::inprocess as cmp20_inprocess;
21
21
  use confium_tc_gg18::inprocess as gg18_inprocess;
22
- use magnus::{exception, function, method, prelude::*, DataTypeFunctions, Error, Module, Object, RHash, Ruby, TryConvert, TypedData, Value};
22
+ use magnus::{function, method, DataTypeFunctions, Error, Module, RHash, Ruby, TryConvert, TypedData, Value};
23
23
  use p256::Scalar;
24
24
 
25
- use crate::util::threshold_error;
25
+ use crate::util::{bytes_from_value, bytes_to_rstring, threshold_error};
26
26
 
27
27
  #[derive(TypedData, DataTypeFunctions)]
28
28
  #[magnus(class = "Confium::TC::FrostP256::Share", size)]
@@ -37,7 +37,7 @@ impl ShareWrap {
37
37
  }
38
38
 
39
39
  fn y_bytes(&self) -> Result<magnus::RString, Error> {
40
- let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
40
+ let ruby = Ruby::get().map_err(|e| crate::util::runtime(e.to_string()))?;
41
41
  Ok(bytes_to_rstring(&ruby, &self.y_bytes))
42
42
  }
43
43
  }
@@ -49,20 +49,16 @@ fn split_secret_into_shares(
49
49
  ) -> Result<magnus::RArray, Error> {
50
50
  let bytes = bytes_from_value(secret_bytes)?;
51
51
  if bytes.len() != 32 {
52
- return Err(Error::new(
53
- exception::arg_error(),
54
- format!("secret must be exactly 32 bytes, got {}", bytes.len()),
52
+ return Err(crate::util::arg_error(format!("secret must be exactly 32 bytes, got {}", bytes.len()),
55
53
  ));
56
54
  }
57
55
  let secret_arr: [u8; 32] = bytes.as_slice().try_into().unwrap();
58
56
  let secret = scalar_from_bytes(&secret_arr).ok_or_else(|| {
59
- Error::new(
60
- exception::arg_error(),
61
- "secret is not a valid P-256 scalar (reduce mod n failed)",
57
+ crate::util::arg_error("secret is not a valid P-256 scalar (reduce mod n failed)",
62
58
  )
63
59
  })?;
64
60
  let shares: Vec<Share> = split_secret(&secret, threshold, party_count);
65
- let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
61
+ let ruby = Ruby::get().map_err(|e| crate::util::runtime(e.to_string()))?;
66
62
  let result = ruby.ary_new_capa(shares.len());
67
63
  for s in shares {
68
64
  result.push(ruby.obj_wrap(ShareWrap {
@@ -76,27 +72,25 @@ fn split_secret_into_shares(
76
72
  fn recover(shares_value: Value) -> Result<magnus::RString, Error> {
77
73
  let arr = magnus::RArray::try_convert(shares_value)?;
78
74
  let mut shares: Vec<Share> = Vec::with_capacity(arr.len());
79
- for v in arr.each() {
80
- let h: RHash = RHash::try_convert(v?)?;
75
+ for v in arr.into_iter() {
76
+ let h: RHash = RHash::try_convert(v)?;
81
77
  let x: u32 = h.fetch::<_, u32>("x")?;
82
78
  let y_value: Value = h.fetch::<_, Value>("y")?;
83
79
  let y_bytes = bytes_from_value(y_value)?;
84
80
  if y_bytes.len() != 32 {
85
- return Err(Error::new(
86
- exception::arg_error(),
87
- format!("share y must be 32 bytes, got {}", y_bytes.len()),
81
+ return Err(crate::util::arg_error(format!("share y must be 32 bytes, got {}", y_bytes.len()),
88
82
  ));
89
83
  }
90
84
  let y_arr: [u8; 32] = y_bytes.as_slice().try_into().unwrap();
91
85
  let y = scalar_from_bytes(&y_arr).ok_or_else(|| {
92
- Error::new(exception::arg_error(), "share y is not a valid P-256 scalar")
86
+ crate::util::arg_error("share y is not a valid P-256 scalar")
93
87
  })?;
94
88
  shares.push(Share { x, y });
95
89
  }
96
90
  let refs: Vec<&Share> = shares.iter().collect();
97
91
  let secret = recover_secret(&refs)
98
- .map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
99
- let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
92
+ .map_err(|e| crate::util::runtime(e.to_string()))?;
93
+ let ruby = Ruby::get().map_err(|e| crate::util::runtime(e.to_string()))?;
100
94
  Ok(bytes_to_rstring(&ruby, scalar_to_bytes(&secret).as_ref()))
101
95
  }
102
96
 
@@ -114,14 +108,12 @@ fn sign(private_key_bytes: Value, message: Value) -> Result<RHash, Error> {
114
108
  let pk_bytes = bytes_from_value(private_key_bytes)?;
115
109
  let msg = bytes_from_value(message)?;
116
110
  if pk_bytes.len() != 32 {
117
- return Err(Error::new(
118
- exception::arg_error(),
119
- format!("private key must be 32 bytes, got {}", pk_bytes.len()),
111
+ return Err(crate::util::arg_error(format!("private key must be 32 bytes, got {}", pk_bytes.len()),
120
112
  ));
121
113
  }
122
114
  let pk_arr: [u8; 32] = pk_bytes.as_slice().try_into().unwrap();
123
115
  let secret = scalar_from_bytes(&pk_arr).ok_or_else(|| {
124
- Error::new(exception::arg_error(), "private key not a valid P-256 scalar")
116
+ crate::util::arg_error("private key not a valid P-256 scalar")
125
117
  })?;
126
118
  let kp = Keypair {
127
119
  secret_scalar: secret,
@@ -137,7 +129,7 @@ fn sign(private_key_bytes: Value, message: Value) -> Result<RHash, Error> {
137
129
  Some(&msg),
138
130
  Some(&e.to_string()),
139
131
  );
140
- return Err(Error::new(exception::runtime_error(), e.to_string()));
132
+ return Err(crate::util::crypto_error(e.to_string(), "FrostP256.sign", "ecdsa-p256"));
141
133
  }
142
134
  };
143
135
  crate::audit::fire_event(
@@ -147,38 +139,14 @@ fn sign(private_key_bytes: Value, message: Value) -> Result<RHash, Error> {
147
139
  Some(&msg),
148
140
  None,
149
141
  );
150
- let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
142
+ let ruby = Ruby::get().map_err(|e| crate::util::runtime(e.to_string()))?;
151
143
  let result = ruby.hash_new();
152
144
  result.aset("der", bytes_to_rstring(&ruby, &signed.der_bytes))?;
153
145
  result.aset("fixed", bytes_to_rstring(&ruby, &signed.fixed_bytes))?;
154
146
  Ok(result)
155
147
  }
156
148
 
157
- fn bytes_from_value(v: Value) -> Result<Vec<u8>, Error> {
158
- use magnus::RString;
159
- if let Ok(s) = RString::try_convert(v) {
160
- return Ok(unsafe { s.as_slice() }.to_vec());
161
- }
162
- let arr: Vec<i64> = Vec::<i64>::try_convert(v)?;
163
- arr.into_iter()
164
- .map(|i| {
165
- if !(0..=255).contains(&i) {
166
- Err(Error::new(
167
- exception::arg_error(),
168
- format!("byte out of range 0..255: {i}"),
169
- ))
170
- } else {
171
- Ok(i as u8)
172
- }
173
- })
174
- .collect()
175
- }
176
149
 
177
- fn bytes_to_rstring(_ruby: &Ruby, bytes: &[u8]) -> magnus::RString {
178
- let s = magnus::RString::buf_new(0);
179
- s.cat(bytes);
180
- s
181
- }
182
150
 
183
151
  // ===== ElGamal-P256 threshold encryption (KEM-style) =====
184
152
 
@@ -186,7 +154,7 @@ fn elgamal_encapsulate(ruby: &Ruby, public_key_bytes: Value) -> Result<RHash, Er
186
154
  let bytes = bytes_from_value(public_key_bytes)?;
187
155
  let pk = ElGamalPublicKey { bytes };
188
156
  let (ciphertext, shared_secret) = encapsulate(&pk)
189
- .map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
157
+ .map_err(|e| crate::util::crypto_error(e.to_string(), "ElGamalP256.encapsulate", "elgamal-p256"))?;
190
158
  let result = ruby.hash_new();
191
159
  let ct_hash = ruby.hash_new();
192
160
  ct_hash.aset("c1", bytes_to_rstring(ruby, &ciphertext.c1))?;
@@ -210,8 +178,8 @@ fn elgamal_partial_decrypt(party_index: u32, share_bytes: Value, ciphertext_valu
210
178
  bytes: share_b,
211
179
  };
212
180
  let partial = partial_decrypt(&share, &ciphertext)
213
- .map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
214
- let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
181
+ .map_err(|e| crate::util::crypto_error(e.to_string(), "ElGamalP256.partial_decrypt", "elgamal-p256"))?;
182
+ let ruby = Ruby::get().map_err(|e| crate::util::runtime(e.to_string()))?;
215
183
  let result = ruby.hash_new();
216
184
  result.aset("party_index", partial.party_index)?;
217
185
  result.aset("bytes", bytes_to_rstring(&ruby, &partial.bytes))?;
@@ -221,8 +189,8 @@ fn elgamal_partial_decrypt(party_index: u32, share_bytes: Value, ciphertext_valu
221
189
  fn elgamal_aggregate(partials_value: Value, threshold: u32, ciphertext_value: Value) -> Result<magnus::RString, Error> {
222
190
  let arr = magnus::RArray::try_convert(partials_value)?;
223
191
  let mut partials: Vec<PartialDecryption> = Vec::with_capacity(arr.len());
224
- for v in arr.each() {
225
- let h: RHash = RHash::try_convert(v?)?;
192
+ for v in arr.into_iter() {
193
+ let h: RHash = RHash::try_convert(v)?;
226
194
  let party_index: u32 = h.fetch::<_, u32>("party_index")?;
227
195
  let bytes_value: Value = h.fetch::<_, Value>("bytes")?;
228
196
  partials.push(PartialDecryption {
@@ -238,8 +206,8 @@ fn elgamal_aggregate(partials_value: Value, threshold: u32, ciphertext_value: Va
238
206
  c2: bytes_from_value(c2_value)?,
239
207
  };
240
208
  let shared = aggregate_partials(&partials, threshold, &ciphertext)
241
- .map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
242
- let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
209
+ .map_err(|e| crate::util::crypto_error(e.to_string(), "ElGamalP256.aggregate_partials", "elgamal-p256"))?;
210
+ let ruby = Ruby::get().map_err(|e| crate::util::runtime(e.to_string()))?;
243
211
  Ok(bytes_to_rstring(&ruby, &shared))
244
212
  }
245
213
 
@@ -317,8 +285,8 @@ fn cmp20_sign(
317
285
  ) -> Result<magnus::RString, Error> {
318
286
  let arr = magnus::RArray::try_convert(shares_value)?;
319
287
  let mut share_bytes: Vec<Vec<u8>> = Vec::with_capacity(arr.len());
320
- for v in arr.each() {
321
- let s = bytes_from_value(v?)?;
288
+ for v in arr.into_iter() {
289
+ let s = bytes_from_value(v)?;
322
290
  share_bytes.push(s);
323
291
  }
324
292
  let supplied = share_bytes.len();
@@ -389,8 +357,8 @@ fn gg18_sign(
389
357
  ) -> Result<magnus::RString, Error> {
390
358
  let arr = magnus::RArray::try_convert(shares_value)?;
391
359
  let mut share_bytes: Vec<Vec<u8>> = Vec::with_capacity(arr.len());
392
- for v in arr.each() {
393
- let s = bytes_from_value(v?)?;
360
+ for v in arr.into_iter() {
361
+ let s = bytes_from_value(v)?;
394
362
  share_bytes.push(s);
395
363
  }
396
364
  let supplied = share_bytes.len();
@@ -9,9 +9,10 @@ use confium_transparency::{
9
9
  entry::{ArtifactType, MerkleEntry},
10
10
  merkle::{Hash, InclusionProof as RustInclusionProof, MerkleTree as RustMerkleTree, Side},
11
11
  };
12
+ use crate::util::{bytes_from_value, bytes_to_rstring};
12
13
  use magnus::{
13
- exception, function, method, prelude::*, typed_data::Obj, DataTypeFunctions, Error, IntoValue,
14
- Module, Object, RString, Ruby, TryConvert, TypedData, Value,
14
+ function, method, prelude::*, typed_data::Obj, DataTypeFunctions, Error, IntoValue,
15
+ Module, Object, Ruby, TypedData, Value,
15
16
  };
16
17
 
17
18
  #[derive(TypedData, DataTypeFunctions)]
@@ -30,9 +31,7 @@ impl MerkleTree {
30
31
  fn append(&self, artifact_hash: Value) -> Result<u64, Error> {
31
32
  let bytes = bytes_from_value(artifact_hash)?;
32
33
  if bytes.len() != 32 {
33
- return Err(Error::new(
34
- exception::arg_error(),
35
- format!("artifact_hash must be exactly 32 bytes, got {}", bytes.len()),
34
+ return Err(crate::util::arg_error(format!("artifact_hash must be exactly 32 bytes, got {}", bytes.len()),
36
35
  ));
37
36
  }
38
37
  let mut artifact_hash = [0u8; 32];
@@ -54,7 +53,7 @@ impl MerkleTree {
54
53
  /// to iterate: `tree.to_a.map(&:sequence)` etc.
55
54
  fn entries(&self) -> Result<magnus::RArray, Error> {
56
55
  let len = self.inner.borrow().len();
57
- let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
56
+ let ruby = Ruby::get().map_err(|e| crate::util::runtime(e.to_string()))?;
58
57
  let arr = ruby.ary_new_capa(len);
59
58
  for i in 0..(len as u64) {
60
59
  let proof = self.inclusion_proof(i)?;
@@ -74,7 +73,7 @@ impl MerkleTree {
74
73
  .consistency_proof(old_size)
75
74
  .map_err(|e| crate::util::index_error(e.to_string(), "MerkleTree.consistency_proof", Some(old_size as u64)))?;
76
75
  let ruby = Ruby::get()
77
- .map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
76
+ .map_err(|e| crate::util::runtime(e.to_string()))?;
78
77
  let arr = ruby.ary_new_capa(proof.len());
79
78
  for h in &proof {
80
79
  arr.push(bytes_to_rstring(&ruby, h))?;
@@ -107,16 +106,12 @@ impl MerkleTree {
107
106
  ) -> Result<bool, Error> {
108
107
  let old_bytes = bytes_from_value(old_root)?;
109
108
  if old_bytes.len() != 32 {
110
- return Err(Error::new(
111
- exception::arg_error(),
112
- format!("old_root must be 32 bytes, got {}", old_bytes.len()),
109
+ return Err(crate::util::arg_error(format!("old_root must be 32 bytes, got {}", old_bytes.len()),
113
110
  ));
114
111
  }
115
112
  let new_bytes = bytes_from_value(new_root)?;
116
113
  if new_bytes.len() != 32 {
117
- return Err(Error::new(
118
- exception::arg_error(),
119
- format!("new_root must be 32 bytes, got {}", new_bytes.len()),
114
+ return Err(crate::util::arg_error(format!("new_root must be 32 bytes, got {}", new_bytes.len()),
120
115
  ));
121
116
  }
122
117
  let mut old_root_hash: Hash = [0u8; 32];
@@ -125,12 +120,10 @@ impl MerkleTree {
125
120
  new_root_hash.copy_from_slice(&new_bytes);
126
121
 
127
122
  let mut proof_hashes: Vec<Hash> = Vec::with_capacity(proof.len());
128
- for item in proof.each() {
129
- let bytes = bytes_from_value(item?)?;
123
+ for item in proof.into_iter() {
124
+ let bytes = bytes_from_value(item)?;
130
125
  if bytes.len() != 32 {
131
- return Err(Error::new(
132
- exception::arg_error(),
133
- format!("proof entries must be 32 bytes, got {}", bytes.len()),
126
+ return Err(crate::util::arg_error(format!("proof entries must be 32 bytes, got {}", bytes.len()),
134
127
  ));
135
128
  }
136
129
  let mut h: Hash = [0u8; 32];
@@ -162,7 +155,7 @@ impl MerkleTree {
162
155
  // Re-derive the leaf hash with the same algorithm the tree uses
163
156
  // internally (H(0x01 | entry_hash)).
164
157
  let leaf_hash = hash_leaf(entry.entry_hash());
165
- let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
158
+ let ruby = Ruby::get().map_err(|e| crate::util::runtime(e.to_string()))?;
166
159
  Ok(ruby.obj_wrap(InclusionProofWrap {
167
160
  inner: proof,
168
161
  leaf_hash,
@@ -187,7 +180,7 @@ impl InclusionProofWrap {
187
180
  }
188
181
 
189
182
  fn steps(&self) -> Result<Value, Error> {
190
- let ruby = Ruby::get().map_err(|e| Error::new(exception::runtime_error(), e.to_string()))?;
183
+ let ruby = Ruby::get().map_err(|e| crate::util::runtime(e.to_string()))?;
191
184
  let result = ruby.hash_new();
192
185
  for (i, step) in self.inner.steps.iter().enumerate() {
193
186
  let step_hash = ruby.hash_new();
@@ -207,9 +200,7 @@ impl InclusionProofWrap {
207
200
  fn verify(&self, root_bytes: Value) -> Result<bool, Error> {
208
201
  let bytes = bytes_from_value(root_bytes)?;
209
202
  if bytes.len() != 32 {
210
- return Err(Error::new(
211
- exception::arg_error(),
212
- format!("root must be exactly 32 bytes, got {}", bytes.len()),
203
+ return Err(crate::util::arg_error(format!("root must be exactly 32 bytes, got {}", bytes.len()),
213
204
  ));
214
205
  }
215
206
  let mut root: Hash = [0u8; 32];
@@ -234,16 +225,12 @@ impl InclusionProofWrap {
234
225
  fn verify_with_leaf(&self, leaf_bytes: Value, root_bytes: Value) -> Result<bool, Error> {
235
226
  let leaf = bytes_from_value(leaf_bytes)?;
236
227
  if leaf.len() != 32 {
237
- return Err(Error::new(
238
- exception::arg_error(),
239
- format!("leaf_hash must be exactly 32 bytes, got {}", leaf.len()),
228
+ return Err(crate::util::arg_error(format!("leaf_hash must be exactly 32 bytes, got {}", leaf.len()),
240
229
  ));
241
230
  }
242
231
  let root_raw = bytes_from_value(root_bytes)?;
243
232
  if root_raw.len() != 32 {
244
- return Err(Error::new(
245
- exception::arg_error(),
246
- format!("root must be exactly 32 bytes, got {}", root_raw.len()),
233
+ return Err(crate::util::arg_error(format!("root must be exactly 32 bytes, got {}", root_raw.len()),
247
234
  ));
248
235
  }
249
236
  let mut current: Hash = [0u8; 32];
@@ -261,35 +248,7 @@ impl InclusionProofWrap {
261
248
  }
262
249
  }
263
250
 
264
- fn bytes_from_value(v: Value) -> Result<Vec<u8>, Error> {
265
- // Accept either a binary String (preferred for byte data) or an Array
266
- // of small integers (also commonly used in Ruby crypto code).
267
- if let Ok(s) = RString::try_convert(v) {
268
- // SAFETY: we treat the string's raw bytes as opaque cryptographic
269
- // input — we never interpret them as a UTF-8 string. Encoding is
270
- // irrelevant for hash input.
271
- return Ok(unsafe { s.as_slice() }.to_vec());
272
- }
273
- let arr: Vec<i64> = Vec::<i64>::try_convert(v)?;
274
- arr.into_iter()
275
- .map(|i| {
276
- if !(0..=255).contains(&i) {
277
- Err(Error::new(
278
- exception::arg_error(),
279
- format!("byte out of range 0..255: {i}"),
280
- ))
281
- } else {
282
- Ok(i as u8)
283
- }
284
- })
285
- .collect()
286
- }
287
251
 
288
- fn bytes_to_rstring(_ruby: &Ruby, bytes: &[u8]) -> RString {
289
- let s = RString::buf_new(0);
290
- s.cat(bytes);
291
- s
292
- }
293
252
 
294
253
  fn hash_leaf(entry_hash: Hash) -> Hash {
295
254
  use sha2::{Digest, Sha256};
@@ -3,6 +3,11 @@
3
3
  //! DRY consolidation: a single `bytes_from_value` + size cap + string
4
4
  //! conversion + typed-error helper shared by every subsystem module
5
5
  //! (composite, pki, tc, transparency, deployment, attributes).
6
+ //!
7
+ //! This module wraps magnus's deprecated `exception::*` constructors
8
+ //! so the rest of the crate never touches them; their replacements
9
+ //! need a `Ruby` handle, which the failure fallbacks here lack.
10
+ #![allow(deprecated)]
6
11
 
7
12
  use magnus::prelude::*;
8
13
  use magnus::{exception, Error, RHash, RString, Ruby, TryConvert, Value};
@@ -14,6 +19,28 @@ use magnus::{exception, Error, RHash, RString, Ruby, TryConvert, Value};
14
19
  /// prevent trivial memory-exhaustion attacks.
15
20
  pub const MAX_INPUT_SIZE: usize = 1 << 20;
16
21
 
22
+ /// Build a plain `RuntimeError`. Centralizes every raise site so the
23
+ /// deprecated `magnus::exception::runtime_error()` fallback lives in
24
+ /// exactly one place (its replacement needs a `Ruby` handle, which is
25
+ /// unavailable when `Ruby::get()` itself fails).
26
+ pub fn runtime(msg: impl Into<String>) -> Error {
27
+ let msg: String = msg.into();
28
+ match Ruby::get() {
29
+ Ok(ruby) => Error::new(ruby.exception_runtime_error(), msg),
30
+ Err(_) => Error::new(exception::runtime_error(), msg),
31
+ }
32
+ }
33
+
34
+ /// Build an `ArgumentError` — for bad argument shapes/ranges, the
35
+ /// class callers should see before any Confium semantics apply.
36
+ pub fn arg_error(msg: impl Into<String>) -> Error {
37
+ let msg: String = msg.into();
38
+ match Ruby::get() {
39
+ Ok(ruby) => Error::new(ruby.exception_arg_error(), msg),
40
+ Err(_) => Error::new(exception::arg_error(), msg),
41
+ }
42
+ }
43
+
17
44
  /// Convert a Ruby value to bytes. Accepts a binary `String` (any
18
45
  /// encoding) or an `Array<Integer>`. Enforces a 1 MiB size cap.
19
46
  pub fn bytes_from_value(v: Value) -> Result<Vec<u8>, Error> {
@@ -30,10 +57,7 @@ pub fn bytes_from_value(v: Value) -> Result<Vec<u8>, Error> {
30
57
  arr.into_iter()
31
58
  .map(|i| {
32
59
  if !(0..=255).contains(&i) {
33
- Err(Error::new(
34
- exception::arg_error(),
35
- format!("byte out of range 0..255: {i}"),
36
- ))
60
+ Err(arg_error(format!("byte out of range 0..255: {i}")))
37
61
  } else {
38
62
  Ok(i as u8)
39
63
  }
@@ -46,18 +70,15 @@ pub fn bytes_from_value(v: Value) -> Result<Vec<u8>, Error> {
46
70
  /// memory-exhaustion attacks.
47
71
  pub fn enforce_size(len: usize) -> Result<(), Error> {
48
72
  if len > MAX_INPUT_SIZE {
49
- return Err(Error::new(
50
- exception::arg_error(),
51
- format!("input size {0} exceeds max {MAX_INPUT_SIZE}", len),
52
- ));
73
+ return Err(arg_error(format!("input size {0} exceeds max {MAX_INPUT_SIZE}", len)));
53
74
  }
54
75
  Ok(())
55
76
  }
56
77
 
57
78
  /// Build a Ruby binary `String` from a byte slice. Avoids the UTF-8
58
- /// round-trip in `RString::buf_new` + `cat` for already-binary input.
59
- pub fn bytes_to_rstring(_ruby: &Ruby, bytes: &[u8]) -> RString {
60
- let s = RString::buf_new(0);
79
+ /// round-trip for already-binary input.
80
+ pub fn bytes_to_rstring(ruby: &Ruby, bytes: &[u8]) -> RString {
81
+ let s = ruby.str_buf_new(0);
61
82
  s.cat(bytes);
62
83
  s
63
84
  }
@@ -1,33 +1,155 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Confium::OpenPGP — OpenPGP (RFC 9580) via bundled rnp-rs.
4
- #
5
- # The native extension provides _native_armor / _native_dearmor.
6
- # This file adds the idiomatic Ruby wrappers with default args.
7
- #
8
- # Architecture: RNP is HARD-BUNDLED in the native extension. No
9
- # external gem dependency. Users get OpenPGP armor encode/decode
10
- # out of the box.
11
-
12
3
  module Confium
4
+ # OpenPGP ASCII armor (RFC 9580 §6) — Radix-64 framing with a
5
+ # CRC-24 checksum, implemented in pure Ruby.
6
+ #
7
+ # This replaced the earlier native wrapper around librnp, which
8
+ # pulled a full vendored Botan/json-c C/C++ build into every
9
+ # install just to do armor framing. The wire format is unchanged;
10
+ # spec/fixtures/openpgp_armor_vectors.json holds differential
11
+ # vectors captured from the native implementation.
13
12
  module OpenPGP
13
+ MESSAGE = 'message'
14
+ PUBLIC_KEY = 'public key'
15
+ SECRET_KEY = 'secret key'
16
+ SIGNATURE = 'signature'
17
+ CLEARTEXT = 'cleartext signed message'
18
+
19
+ LABELS = {
20
+ nil => 'MESSAGE',
21
+ MESSAGE => 'MESSAGE',
22
+ 'public key' => 'PUBLIC KEY BLOCK',
23
+ SECRET_KEY => 'PRIVATE KEY BLOCK',
24
+ 'private key' => 'PRIVATE KEY BLOCK',
25
+ SIGNATURE => 'SIGNATURE',
26
+ # Raw bytes cannot form a cleartext-signed message (that
27
+ # requires a signature packet); armor them as a plain message.
28
+ CLEARTEXT => 'MESSAGE',
29
+ 'cleartext' => 'MESSAGE'
30
+ }.freeze
31
+
32
+ CRC_POLY = 0x1864CFB
33
+ CRC_INIT = 0xB704CE
34
+ B64_CHARS = [('A'..'Z').to_a, ('a'..'z').to_a, ('0'..'9').to_a, %w[+ /]].flatten.freeze
35
+ CRC_TABLE = (0..255).map do |i|
36
+ c = i << 16
37
+ 8.times do
38
+ c = c.nobits?(0x800_000) ? c << 1 : ((c << 1) ^ CRC_POLY)
39
+ c &= 0xFFFFFF
40
+ end
41
+ c
42
+ end.freeze
43
+
44
+ private_constant :LABELS, :CRC_POLY, :CRC_INIT, :B64_CHARS, :CRC_TABLE
45
+
14
46
  class << self
15
- # ASCII-armor encode raw bytes.
47
+ # ASCII-armor encode raw bytes. Output uses CRLF line endings
48
+ # and 76-character data lines, byte-for-byte matching the
49
+ # earlier native (rnp) implementation.
16
50
  #
17
51
  # @param data [String] Binary data to encode.
18
- # @param type [String] Armor type — one of MESSAGE, PUBLIC_KEY,
19
- # SECRET_KEY, SIGNATURE, CLEARTEXT. Defaults to MESSAGE.
52
+ # @param type [String] One of MESSAGE, PUBLIC_KEY, SECRET_KEY,
53
+ # SIGNATURE, CLEARTEXT (armored as a plain message). Defaults
54
+ # to MESSAGE.
20
55
  # @return [String] Armored ASCII string.
56
+ # @raise [ArgumentError] if +type+ is not a known armor type.
21
57
  def armor(data, type = MESSAGE)
22
- _native_armor(data, type)
58
+ label = LABELS[type]
59
+ raise ArgumentError, "unknown armor type: #{type.inspect}" unless label
60
+
61
+ bytes = data.to_s.b
62
+ b64 = [bytes].pack('m0')
63
+ lines = b64.scan(/.{1,76}/)
64
+ <<~ARMOR.gsub("\n", "\r\n")
65
+ -----BEGIN PGP #{label}-----
66
+
67
+ #{lines.join("\n")}
68
+ =#{crc24_armor(bytes)}
69
+ -----END PGP #{label}-----
70
+ ARMOR
23
71
  end
24
72
 
25
- # Decode ASCII-armored data to raw bytes.
73
+ # Decode ASCII-armored data to raw bytes. Accepts LF or CRLF
74
+ # line endings, arbitrary line widths, and Armor Headers
75
+ # (Comment:, Version:, ...) between the BEGIN line and the
76
+ # blank line. The CRC-24 checksum line is verified when
77
+ # present.
26
78
  #
27
79
  # @param data [String] Armored ASCII string.
28
- # @return [String] Raw binary data.
80
+ # @return [String] Raw binary data (ASCII-8BIT).
81
+ # @raise [Confium::ParseError] on missing delimiters, non-base64
82
+ # content, or a CRC mismatch.
29
83
  def dearmor(data)
30
- _native_dearmor(data)
84
+ b64, crc_line = extract_body(data.to_s)
85
+ # @type var bytes: String
86
+ bytes = b64.unpack1('m0')
87
+ return ''.b if crc_line == crc24_armor(''.b) && b64.empty?
88
+
89
+ raise ParseError, 'armor CRC-24 checksum mismatch' if crc_line && crc24_armor(bytes) != crc_line.to_s
90
+
91
+ bytes
92
+ end
93
+
94
+ private
95
+
96
+ # Locate the armored block, skip the BEGIN line and any Armor
97
+ # Headers, and split the remaining lines into the joined
98
+ # Radix-64 data and the checksum line (if present).
99
+ def extract_body(text)
100
+ lines = block_lines(text)
101
+ b64, crc_line = collect_data(lines)
102
+ raise ParseError, 'armored block has no data' if b64.empty? && crc_line.nil?
103
+
104
+ [b64, crc_line]
105
+ end
106
+
107
+ # The lines of the armored block between BEGIN and END, with
108
+ # the BEGIN line and any Armor Headers removed.
109
+ def block_lines(text)
110
+ slice = block_slice(text.gsub("\r\n", "\n"))
111
+ lines = slice.split("\n")
112
+ lines.shift # BEGIN
113
+ lines.shift while lines.first&.match?(/^\s|^[A-Za-z0-9-]+: /)
114
+ lines.shift if lines.first == ''
115
+ lines
116
+ end
117
+
118
+ # The text between the BEGIN and END delimiter lines.
119
+ def block_slice(normalized)
120
+ begin_line = normalized.index(/^-----BEGIN PGP [A-Z ]+-----$/)
121
+ raise ParseError, 'not an ASCII-armored block (no BEGIN line)' unless begin_line
122
+
123
+ end_match = normalized.match(/^-----END PGP [A-Z ]+-----$/)
124
+ raise ParseError, 'not an ASCII-armored block (no END line)' unless end_match
125
+
126
+ normalized[begin_line...end_match.begin(0)].to_s
127
+ end
128
+
129
+ def collect_data(lines)
130
+ b64 = +''
131
+ crc_line = nil
132
+ lines.each do |line|
133
+ next if line.empty?
134
+
135
+ if line.start_with?('=')
136
+ crc_line = line[1..]
137
+ elsif line.match?(%r{\A[A-Za-z0-9+/]+={0,2}\z})
138
+ b64 << line
139
+ else
140
+ raise ParseError, "invalid armor data line: #{line[0, 20].inspect}"
141
+ end
142
+ end
143
+ [b64, crc_line]
144
+ end
145
+
146
+ # CRC-24 (RFC 9580 §6.1), encoded as the four Radix-64
147
+ # characters of the 24-bit value as three big-endian bytes.
148
+ def crc24_armor(bytes)
149
+ crc = CRC_INIT
150
+ bytes.each_byte { |b| crc = ((crc << 8) & 0xFFFFFF) ^ CRC_TABLE[((crc >> 16) ^ b) & 0xFF] }
151
+ three_bytes = [crc >> 16, (crc >> 8) & 0xFF, crc & 0xFF].pack('C3')
152
+ [three_bytes].pack('m0')
31
153
  end
32
154
  end
33
155
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Confium
4
- VERSION = '0.3.3'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/confium.rb CHANGED
@@ -22,6 +22,13 @@ begin
22
22
  begin
23
23
  require_relative "confium_native/#{window}/confium_native"
24
24
  rescue LoadError
25
+ # Fall back to the flat source-build path only when the windowed
26
+ # binary is absent; a present-but-unloadable binary must raise its
27
+ # real dlopen error instead of masquerading as "not built".
28
+ dlext = RbConfig::CONFIG['DLEXT'] || 'so'
29
+ windowed = File.expand_path("confium_native/#{window}/confium_native.#{dlext}", __dir__ || '.')
30
+ raise if File.exist?(windowed)
31
+
25
32
  require_relative 'confium_native/confium_native'
26
33
  end
27
34
  rescue LoadError => e
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: confium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Open
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-23 00:00:00.000000000 Z
11
+ date: 2026-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys
@@ -121,7 +121,6 @@ files:
121
121
  - ext/confium_native/src/deployment.rs
122
122
  - ext/confium_native/src/ers.rs
123
123
  - ext/confium_native/src/lib.rs
124
- - ext/confium_native/src/openpgp.rs
125
124
  - ext/confium_native/src/path.rs
126
125
  - ext/confium_native/src/pki.rs
127
126
  - ext/confium_native/src/tc.rs