confium 0.7.0 → 0.7.2
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 +59 -0
- data/Cargo.lock +222 -515
- data/README.adoc +1 -1
- data/ext/confium_native/Cargo.toml +10 -13
- data/ext/confium_native/src/gvl.rs +56 -0
- data/ext/confium_native/src/lib.rs +5 -0
- data/ext/confium_native/src/ots.rs +183 -0
- data/ext/confium_native/src/tc.rs +1 -0
- data/ext/confium_native/src/tc_mta.rs +378 -0
- data/lib/confium/transparency/ots.rb +42 -39
- data/lib/confium/transparency.rb +5 -5
- data/lib/confium/version.rb +1 -1
- metadata +5 -2
data/README.adoc
CHANGED
|
@@ -103,7 +103,7 @@ end
|
|
|
103
103
|
=== `Confium::Transparency`
|
|
104
104
|
- `MerkleTree.new` / `#append(artifact_hash)` / `#root` / `#length` / `#empty?` / `#inclusion_proof(seq)`
|
|
105
105
|
- `InclusionProof#sequence` / `#steps` / `#verify(root)`
|
|
106
|
-
- `Ots.stamp(hash)` / `Ots.verify(
|
|
106
|
+
- `Ots.stamp(hash)` / `Ots.verify(proof)` / `Ots.upgrade(proof)` — real OpenTimestamps anchoring over the wire protocol (calendar HTTP, servers tried in order; the network round trip releases the GVL). `Ots::Client.new(servers)` for a custom pool; `Ots::Proof#verify` replays the op tree and classifies attestations. Network failures raise — there is no silent nil
|
|
107
107
|
|
|
108
108
|
=== `Confium::Composite` — PQ migration
|
|
109
109
|
- `.generate_ed25519_keypair` → `{ private_key:, public_key: }`
|
|
@@ -36,33 +36,30 @@ magnus = { version = "0.8", features = ["bytes"] }
|
|
|
36
36
|
|
|
37
37
|
# Confium crates — all from crates.io so gem install works without workspace checkout.
|
|
38
38
|
confium-core = "0.2"
|
|
39
|
-
confium-transparency = "0.
|
|
39
|
+
confium-transparency = { version = "0.9", features = ["calendar"] }
|
|
40
40
|
confium-composite = "0.3.1"
|
|
41
41
|
confium-attributes = "0.3.1"
|
|
42
42
|
confium-pki = "0.5.6"
|
|
43
43
|
confium-store = "0.5.8"
|
|
44
44
|
confium-deployment = "0.3"
|
|
45
|
-
confium-tc = "0.
|
|
45
|
+
confium-tc = "0.9"
|
|
46
46
|
confium-tc-frost-p256 = "0.4"
|
|
47
47
|
confium-tc-elgamal-p256 = "0.4"
|
|
48
|
-
confium-tc-cmp20 = "0.
|
|
49
|
-
confium-tc-gg18 = "0.
|
|
48
|
+
confium-tc-cmp20 = "0.9"
|
|
49
|
+
confium-tc-gg18 = "0.9"
|
|
50
50
|
|
|
51
51
|
# Per-party session protocol: the tc-core 0.4 registry line. The 0.3-era
|
|
52
52
|
# in-process drivers above keep their own registry harmlessly.
|
|
53
|
-
confium-tc-session = { package = "confium-tc-core", version = "0.
|
|
54
|
-
confium-tc-frost-ed25519 = "0.
|
|
55
|
-
|
|
56
|
-
# Newly published shared crypto crates (confium product restructuring).
|
|
57
|
-
confium-tc-core = "0.3"
|
|
58
|
-
confium-crypto-vss = "0.3"
|
|
59
|
-
confium-crypto-zk = "0.3"
|
|
60
|
-
confium-privacy = "0.3"
|
|
61
|
-
confium-observability = "0.3"
|
|
53
|
+
confium-tc-session = { package = "confium-tc-core", version = "0.9" }
|
|
54
|
+
confium-tc-frost-ed25519 = "0.9"
|
|
62
55
|
|
|
63
56
|
# P-256 scalar/point types used by the TC surface.
|
|
64
57
|
p256 = { version = "0.13", features = ["ecdsa"] }
|
|
65
58
|
|
|
59
|
+
# Hex integer codecs for the MtA surface.
|
|
60
|
+
num-bigint = "0.4"
|
|
61
|
+
num-traits = "0.2"
|
|
62
|
+
|
|
66
63
|
ed25519-dalek = { version = "2", features = ["rand_core"] }
|
|
67
64
|
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
|
|
68
65
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//! GVL release for long blocking calls.
|
|
2
|
+
//!
|
|
3
|
+
//! magnus 0.8 does not wrap `rb_thread_call_without_gvl`, so the OTS
|
|
4
|
+
//! calendar round trip (up to 30s) would hold the GVL and freeze the
|
|
5
|
+
//! entire Ruby VM — including any Ruby thread serving as the peer.
|
|
6
|
+
//! This is the standard MRI mechanism: release the GVL for the
|
|
7
|
+
//! duration of a blocking FFI call, reacquire before touching Ruby
|
|
8
|
+
//! objects again. The closure must not touch the Ruby API.
|
|
9
|
+
|
|
10
|
+
use std::ffi::c_void;
|
|
11
|
+
|
|
12
|
+
unsafe extern "C" {
|
|
13
|
+
fn rb_thread_call_without_gvl(
|
|
14
|
+
func: extern "C" fn(*mut c_void) -> *mut c_void,
|
|
15
|
+
data1: *mut c_void,
|
|
16
|
+
ubf: Option<extern "C" fn(*mut c_void) -> *mut c_void>,
|
|
17
|
+
data2: *mut c_void,
|
|
18
|
+
) -> *mut c_void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
struct CallBox<F: FnOnce() -> T, T> {
|
|
22
|
+
func: Option<F>,
|
|
23
|
+
out: Option<T>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
extern "C" fn trampoline<F: FnOnce() -> T, T>(data: *mut c_void) -> *mut c_void {
|
|
27
|
+
// SAFETY: data points at the CallBox constructed by without_gvl,
|
|
28
|
+
// live for the duration of the call, on this same thread.
|
|
29
|
+
let box_ = unsafe { &mut *(data as *mut CallBox<F, T>) };
|
|
30
|
+
let func = box_.func.take().expect("trampoline runs once");
|
|
31
|
+
box_.out = Some(func());
|
|
32
|
+
std::ptr::null_mut()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Run `func` with the GVL released. Reacquires before returning.
|
|
36
|
+
///
|
|
37
|
+
/// # Safety (caller obligations)
|
|
38
|
+
///
|
|
39
|
+
/// `func` must not call the Ruby C API (no Ruby objects) and must be
|
|
40
|
+
/// callable from this thread while the GVL is released.
|
|
41
|
+
pub unsafe fn without_gvl<F: FnOnce() -> T, T>(func: F) -> T {
|
|
42
|
+
let mut call = CallBox {
|
|
43
|
+
func: Some(func),
|
|
44
|
+
out: None,
|
|
45
|
+
};
|
|
46
|
+
// SAFETY: the trampoline and box are used per the contract above.
|
|
47
|
+
unsafe {
|
|
48
|
+
rb_thread_call_without_gvl(
|
|
49
|
+
trampoline::<F, T>,
|
|
50
|
+
&mut call as *mut CallBox<F, T> as *mut c_void,
|
|
51
|
+
None,
|
|
52
|
+
std::ptr::null_mut(),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
call.out.take().expect("trampoline stored the result")
|
|
56
|
+
}
|
|
@@ -9,12 +9,15 @@ mod attributes;
|
|
|
9
9
|
mod composite;
|
|
10
10
|
mod deployment;
|
|
11
11
|
mod ers;
|
|
12
|
+
mod gvl;
|
|
12
13
|
mod openpgp_verify;
|
|
13
14
|
mod path;
|
|
14
15
|
mod pki;
|
|
15
16
|
mod store;
|
|
16
17
|
mod tc;
|
|
18
|
+
mod tc_mta;
|
|
17
19
|
mod net;
|
|
20
|
+
mod ots;
|
|
18
21
|
mod tc_session;
|
|
19
22
|
mod transparency;
|
|
20
23
|
mod util;
|
|
@@ -66,6 +69,8 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
66
69
|
native.define_module_function("winsock_probe", function!(winsock_probe_fn, 1))?;
|
|
67
70
|
|
|
68
71
|
transparency::init(ruby, confium)?;
|
|
72
|
+
let transparency = confium.define_module("Transparency")?;
|
|
73
|
+
ots::init(ruby, transparency)?;
|
|
69
74
|
openpgp_verify::init(ruby, confium)?;
|
|
70
75
|
#[cfg(feature = "pgp")]
|
|
71
76
|
openpgp_verify::init_pgp(ruby, &confium)?;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
//! Confium::Transparency::OTS — OpenTimestamps calendar client.
|
|
2
|
+
//!
|
|
3
|
+
//! Binds confium-transparency's real wire-protocol client (0.9):
|
|
4
|
+
//! `stamp` POSTs the 32-byte digest to a calendar server and parses
|
|
5
|
+
//! the returned partial proof (op stream + pending attestation);
|
|
6
|
+
//! `upgrade` fetches a more complete proof; `verify` replays the op
|
|
7
|
+
//! tree from the digest and classifies the attestations. Network
|
|
8
|
+
//! errors surface as Ruby exceptions — there is no silent nil.
|
|
9
|
+
|
|
10
|
+
use std::cell::RefCell;
|
|
11
|
+
|
|
12
|
+
use confium_transparency::ots::OtsClient;
|
|
13
|
+
use confium_transparency::ots::wire;
|
|
14
|
+
use magnus::prelude::*;
|
|
15
|
+
use magnus::Error;
|
|
16
|
+
use magnus::RHash;
|
|
17
|
+
use magnus::RString;
|
|
18
|
+
use magnus::Ruby;
|
|
19
|
+
use magnus::TypedData;
|
|
20
|
+
use magnus::DataTypeFunctions;
|
|
21
|
+
|
|
22
|
+
/// An OTS proof: the raw .ots file bytes plus the digest it anchors.
|
|
23
|
+
#[derive(TypedData, DataTypeFunctions)]
|
|
24
|
+
#[magnus(class = "Confium::Transparency::OTS::Proof", size)]
|
|
25
|
+
pub struct OtsProof {
|
|
26
|
+
digest: Vec<u8>,
|
|
27
|
+
bytes: Vec<u8>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn digest_arg_error() -> Error {
|
|
31
|
+
Error::new(magnus::exception::arg_error(), "digest must be exactly 32 bytes")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
impl OtsProof {
|
|
35
|
+
fn initialize(_ruby: &Ruby, digest: RString, bytes: RString) -> Result<Self, Error> {
|
|
36
|
+
let digest = digest.to_bytes().to_vec();
|
|
37
|
+
if digest.len() != 32 {
|
|
38
|
+
return Err(digest_arg_error());
|
|
39
|
+
}
|
|
40
|
+
Ok(Self {
|
|
41
|
+
digest,
|
|
42
|
+
bytes: bytes.to_bytes().to_vec(),
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn digest(&self) -> RString {
|
|
47
|
+
RString::from_slice(&self.digest)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn to_bytes(&self) -> RString {
|
|
51
|
+
RString::from_slice(&self.bytes)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Replay the proof tree and classify attestations. Returns a Hash:
|
|
55
|
+
/// { pending: [uri, ...], bitcoin: [height, ...],
|
|
56
|
+
/// litecoin: [height, ...], anchored: bool }
|
|
57
|
+
fn verify(&self) -> Result<RHash, Error> {
|
|
58
|
+
let ruby = Ruby::get().map_err(|_| Error::new(magnus::exception::runtime_error(), "not on Ruby thread"))?;
|
|
59
|
+
let file = wire::parse(&self.digest, &self.bytes)
|
|
60
|
+
.map_err(|e| Error::new(parse_error_class(&ruby), format!("invalid OTS proof: {e}")))?;
|
|
61
|
+
let summary = wire::verify(&file)
|
|
62
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), format!("replay failed: {e}")))?;
|
|
63
|
+
|
|
64
|
+
let out = RHash::new();
|
|
65
|
+
let pending: Vec<String> = summary
|
|
66
|
+
.pending
|
|
67
|
+
.iter()
|
|
68
|
+
.map(|(_, uri)| uri.clone())
|
|
69
|
+
.collect();
|
|
70
|
+
out.aset(
|
|
71
|
+
"pending",
|
|
72
|
+
ruby.into_value(ruby.ary_from_vec(pending)),
|
|
73
|
+
)?;
|
|
74
|
+
let bitcoin: Vec<i64> = summary.bitcoin.iter().map(|(_, h)| i64::from(*h)).collect();
|
|
75
|
+
out.aset(
|
|
76
|
+
"bitcoin",
|
|
77
|
+
ruby.into_value(ruby.ary_from_vec(bitcoin)),
|
|
78
|
+
)?;
|
|
79
|
+
let litecoin: Vec<i64> = summary.litecoin.iter().map(|(_, h)| i64::from(*h)).collect();
|
|
80
|
+
out.aset(
|
|
81
|
+
"litecoin",
|
|
82
|
+
ruby.into_value(ruby.ary_from_vec(litecoin)),
|
|
83
|
+
)?;
|
|
84
|
+
// Bitcoin/Litecoin attestations are on-chain anchors; pending
|
|
85
|
+
// means recorded at a calendar awaiting confirmation.
|
|
86
|
+
let anchored = !summary.bitcoin.is_empty() || !summary.litecoin.is_empty();
|
|
87
|
+
out.aset("anchored", ruby.into_value(anchored))?;
|
|
88
|
+
Ok(out)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// A calendar client bound to a server pool.
|
|
93
|
+
#[derive(TypedData, DataTypeFunctions)]
|
|
94
|
+
#[magnus(class = "Confium::Transparency::OTS::Client", size)]
|
|
95
|
+
pub struct Client {
|
|
96
|
+
inner: RefCell<OtsClient>,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
impl Client {
|
|
100
|
+
fn initialize(_ruby: &Ruby, servers: Vec<String>) -> Result<Self, Error> {
|
|
101
|
+
Ok(Self {
|
|
102
|
+
inner: RefCell::new(OtsClient::with_servers(servers)),
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Submit a 32-byte digest to the calendar pool. Returns a Proof
|
|
107
|
+
/// carrying the returned partial proof (pending attestation).
|
|
108
|
+
fn stamp(&self, digest: RString) -> Result<OtsProof, Error> {
|
|
109
|
+
let ruby = Ruby::get().map_err(|_| Error::new(magnus::exception::runtime_error(), "not on Ruby thread"))?;
|
|
110
|
+
let digest = digest.to_bytes().to_vec();
|
|
111
|
+
if digest.len() != 32 {
|
|
112
|
+
return Err(digest_arg_error());
|
|
113
|
+
}
|
|
114
|
+
let arr: [u8; 32] = digest.as_slice().try_into().map_err(|_| digest_arg_error())?;
|
|
115
|
+
let mut inner = self.inner.borrow_mut();
|
|
116
|
+
// Release the GVL for the network round trip: calendars take
|
|
117
|
+
// seconds-to-tens-of-seconds, and holding the VM would freeze
|
|
118
|
+
// every Ruby thread (including any in-process peer).
|
|
119
|
+
let stamped = unsafe {
|
|
120
|
+
crate::gvl::without_gvl(|| inner.stamp_wire(arr))
|
|
121
|
+
};
|
|
122
|
+
let file = stamped
|
|
123
|
+
.map_err(|e| Error::new(ruby.exception_io_error(), format!("calendar stamp failed: {e}")))?;
|
|
124
|
+
let bytes = wire::serialize(&file).map_err(|e| {
|
|
125
|
+
Error::new(
|
|
126
|
+
ruby.exception_runtime_error(),
|
|
127
|
+
format!("calendar returned an unserializable proof: {e}"),
|
|
128
|
+
)
|
|
129
|
+
})?;
|
|
130
|
+
Ok(OtsProof { digest, bytes })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Fetch a more complete proof for a pending attestation. Returns
|
|
134
|
+
/// the upgraded Proof (replaces the pending one).
|
|
135
|
+
fn upgrade(&self, proof: &OtsProof) -> Result<OtsProof, Error> {
|
|
136
|
+
let ruby = Ruby::get().map_err(|_| Error::new(magnus::exception::runtime_error(), "not on Ruby thread"))?;
|
|
137
|
+
let file = wire::parse(&proof.digest, &proof.bytes)
|
|
138
|
+
.map_err(|e| Error::new(parse_error_class(&ruby), format!("invalid OTS proof: {e}")))?;
|
|
139
|
+
let mut inner = self.inner.borrow_mut();
|
|
140
|
+
let upgraded_result = unsafe { crate::gvl::without_gvl(|| inner.upgrade(&file)) };
|
|
141
|
+
let upgraded = upgraded_result
|
|
142
|
+
.map_err(|e| Error::new(ruby.exception_io_error(), format!("calendar upgrade failed: {e}")))?;
|
|
143
|
+
let bytes = wire::serialize(&upgraded).map_err(|e| {
|
|
144
|
+
Error::new(
|
|
145
|
+
ruby.exception_runtime_error(),
|
|
146
|
+
format!("calendar returned an unserializable proof: {e}"),
|
|
147
|
+
)
|
|
148
|
+
})?;
|
|
149
|
+
Ok(OtsProof {
|
|
150
|
+
digest: proof.digest.clone(),
|
|
151
|
+
bytes,
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
pub fn init(ruby: &Ruby, parent: magnus::RModule) -> Result<(), Error> {
|
|
157
|
+
let ots = parent.define_module("OTS")?;
|
|
158
|
+
let parse_error = ots.define_error("ParseError", ruby.exception_standard_error())?;
|
|
159
|
+
ots.const_set("PARSE_ERROR", parse_error)?;
|
|
160
|
+
|
|
161
|
+
let proof = ots.define_class("Proof", ruby.class_object())?;
|
|
162
|
+
proof.define_singleton_method("new", magnus::function!(OtsProof::initialize, 2))?;
|
|
163
|
+
proof.define_method("digest", magnus::method!(OtsProof::digest, 0))?;
|
|
164
|
+
proof.define_method("to_bytes", magnus::method!(OtsProof::to_bytes, 0))?;
|
|
165
|
+
proof.define_method("verify", magnus::method!(OtsProof::verify, 0))?;
|
|
166
|
+
|
|
167
|
+
let client = ots.define_class("Client", ruby.class_object())?;
|
|
168
|
+
client.define_singleton_method("new", magnus::function!(Client::initialize, 1))?;
|
|
169
|
+
client.define_method("stamp", magnus::method!(Client::stamp, 1))?;
|
|
170
|
+
client.define_method("upgrade", magnus::method!(Client::upgrade, 1))?;
|
|
171
|
+
|
|
172
|
+
Ok(())
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fn parse_error_class(ruby: &Ruby) -> magnus::ExceptionClass {
|
|
176
|
+
ruby
|
|
177
|
+
.class_object()
|
|
178
|
+
.const_get::<_, magnus::RModule>("Confium")
|
|
179
|
+
.and_then(|m| m.const_get::<_, magnus::RModule>("Transparency"))
|
|
180
|
+
.and_then(|m| m.const_get::<_, magnus::RModule>("OTS"))
|
|
181
|
+
.and_then(|m| m.const_get::<_, magnus::ExceptionClass>("PARSE_ERROR"))
|
|
182
|
+
.unwrap_or_else(|_| ruby.exception_runtime_error())
|
|
183
|
+
}
|
|
@@ -232,6 +232,7 @@ pub fn init(ruby: &Ruby, parent: magnus::RModule) -> Result<(), Error> {
|
|
|
232
232
|
let cmp20 = tc.define_module("Cmp20")?;
|
|
233
233
|
cmp20.define_module_function("keygen", function!(cmp20_keygen, 2))?;
|
|
234
234
|
cmp20.define_module_function("sign", function!(cmp20_sign, 3))?;
|
|
235
|
+
crate::tc_mta::init(ruby, &cmp20)?;
|
|
235
236
|
|
|
236
237
|
let gg18 = tc.define_module("Gg18")?;
|
|
237
238
|
gg18.define_module_function("keygen", function!(gg18_keygen, 2))?;
|