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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/Cargo.lock +12 -432
- data/README.adoc +6 -10
- data/ext/confium_native/Cargo.toml +0 -4
- data/ext/confium_native/src/attributes.rs +4 -4
- data/ext/confium_native/src/audit.rs +8 -8
- data/ext/confium_native/src/composite.rs +10 -14
- data/ext/confium_native/src/deployment.rs +7 -6
- data/ext/confium_native/src/ers.rs +7 -11
- data/ext/confium_native/src/lib.rs +0 -2
- data/ext/confium_native/src/path.rs +8 -8
- data/ext/confium_native/src/pki.rs +26 -30
- data/ext/confium_native/src/tc.rs +27 -59
- data/ext/confium_native/src/transparency.rs +16 -57
- data/ext/confium_native/src/util.rs +32 -11
- data/lib/confium/openpgp.rb +138 -16
- data/lib/confium/version.rb +1 -1
- data/lib/confium.rb +7 -0
- metadata +2 -3
- data/ext/confium_native/src/openpgp.rs +0 -55
|
@@ -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::{
|
|
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|
|
|
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(
|
|
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
|
-
|
|
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|
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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|
|
|
99
|
-
let ruby = Ruby::get().map_err(|e|
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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|
|
|
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|
|
|
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|
|
|
214
|
-
let ruby = Ruby::get().map_err(|e|
|
|
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.
|
|
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|
|
|
242
|
-
let ruby = Ruby::get().map_err(|e|
|
|
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.
|
|
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.
|
|
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
|
-
|
|
14
|
-
Module, Object,
|
|
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(
|
|
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|
|
|
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|
|
|
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(
|
|
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(
|
|
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.
|
|
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(
|
|
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|
|
|
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|
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
59
|
-
pub fn bytes_to_rstring(
|
|
60
|
-
let s =
|
|
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
|
}
|
data/lib/confium/openpgp.rb
CHANGED
|
@@ -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]
|
|
19
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/confium/version.rb
CHANGED
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.
|
|
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-
|
|
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
|