qcicada 0.2.2

SDK for the QCicada QRNG (Crypta Labs) — macOS-first, works on Linux too
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
# qcicada

Rust and Python SDK for the [QCicada quantum random number generator](https://cryptalabs.com/) by Crypta Labs.

macOS-first — fixes FTDI serial driver issues that break the official SDK. Works on Linux too.

## Install

**Python**
```bash
pip install qcicada
```

**Rust**
```toml
[dependencies]
qcicada = "0.1"
```

## Quick Start

<table>
<tr><th>Python</th><th>Rust</th></tr>
<tr><td>

```python
from qcicada import QCicada

with QCicada() as qrng:
    print(qrng.random(32).hex())
```

</td><td>

```rust
use qcicada::QCicada;

let mut qrng = QCicada::open(None, None)?;
let bytes = qrng.random(32)?;
println!("{:02x?}", bytes);
```

</td></tr>
</table>

The device is auto-detected. If you have multiple USB-serial devices, pass the port explicitly:

```python
QCicada(port="/dev/cu.usbserial-DK0HFP4T")      # Python
```
```rust
QCicada::open(Some("/dev/cu.usbserial-DK0HFP4T"), None)?;  // Rust
```

## Device Discovery

<table>
<tr><th>Python</th><th>Rust</th></tr>
<tr><td>

```python
from qcicada import (
    find_devices,
    discover_devices,
    open_by_serial,
)

# Fast port scan (no device I/O)
find_devices()
# ['/dev/cu.usbserial-DK0HFP4T']

# Probe and verify each device
for dev in discover_devices():
    print(dev.port, dev.info.serial)

# Open by serial number
qrng = open_by_serial("QC0000000217")
```

</td><td>

```rust
use qcicada::{
    find_devices,
    discover_devices,
    open_by_serial,
};

// Fast port scan (no device I/O)
let ports = find_devices();

// Probe and verify each device
for dev in discover_devices() {
    println!("{} {}", dev.port, dev.info.serial);
}

// Open by serial number
let mut qrng = open_by_serial("QC0000000217")?;
```

</td></tr>
</table>

## Entropy Modes

The QCicada supports three post-processing modes:

| Mode | What you get |
|------|-------------|
| **SHA256** (default) | NIST SP 800-90B conditioned output — use for cryptography |
| **Raw Noise** | After health-test conditioning — use for entropy research |
| **Raw Samples** | Unprocessed samples from the quantum optical module |

<table>
<tr><th>Python</th><th>Rust</th></tr>
<tr><td>

```python
from qcicada import QCicada, PostProcess

with QCicada() as qrng:
    qrng.random(32)  # SHA256 (default)

    qrng.set_postprocess(PostProcess.RAW_NOISE)
    qrng.random(32)

    qrng.set_postprocess(PostProcess.RAW_SAMPLES)
    qrng.random(32)
```

</td><td>

```rust
use qcicada::{QCicada, PostProcess};

let mut qrng = QCicada::open(None, None)?;
qrng.random(32)?;  // SHA256 (default)

qrng.set_postprocess(PostProcess::RawNoise)?;
qrng.random(32)?;

qrng.set_postprocess(PostProcess::RawSamples)?;
qrng.random(32)?;
```

</td></tr>
</table>

## Signed Reads

Random bytes with a 64-byte cryptographic signature from the device's internal key. Requires firmware 5.13+.

<table>
<tr><th>Python</th><th>Rust</th></tr>
<tr><td>

```python
result = qrng.signed_read(32)
result.data       # 32 random bytes
result.signature  # 64-byte signature
```

</td><td>

```rust
let result = qrng.signed_read(32)?;
result.data       // 32 random bytes
result.signature  // 64-byte signature
```

</td></tr>
</table>

## Certificate Verification

Verify the device's identity using its ECDSA P-256 certificate chain. The device holds an internal keypair; a Certificate Authority (CA) signs the device's public key along with its hardware version and serial number.

<table>
<tr><th>Python</th><th>Rust</th></tr>
<tr><td>

```python
# CA public key (64 bytes, from Crypta Labs)
ca_pub_key = bytes.fromhex("...")

# Verify device and get its public key
dev_pub = qrng.get_verified_pub_key(ca_pub_key)

# Signed read with signature verification
result = qrng.signed_read_verified(32, dev_pub)
result.data       # 32 verified random bytes
result.signature  # 64-byte signature
```

</td><td>

```rust
// CA public key (64 bytes, from Crypta Labs)
let ca_pub_key = hex::decode("...").unwrap();

// Verify device and get its public key
let dev_pub = qrng.get_verified_pub_key(&ca_pub_key)?;

// Signed read with signature verification
let result = qrng.signed_read_verified(32, &dev_pub)?;
result.data       // 32 verified random bytes
result.signature  // 64-byte signature
```

</td></tr>
</table>

You can also access the raw primitives directly:

<table>
<tr><th>Python</th><th>Rust</th></tr>
<tr><td>

```python
pub_key = qrng.get_dev_pub_key()      # 64 bytes
cert = qrng.get_dev_certificate()     # 64 bytes

from qcicada import verify_certificate
valid = verify_certificate(
    ca_pub_key, pub_key, cert,
    hw_major=1, hw_minor=1, serial_int=217,
)
```

</td><td>

```rust
let pub_key = qrng.get_dev_pub_key()?;   // 64 bytes
let cert = qrng.get_dev_certificate()?;  // 64 bytes

use qcicada::crypto::verify_certificate;
let valid = verify_certificate(
    &ca_pub_key, &pub_key, &cert, 1, 1, 217,
)?;
```

</td></tr>
</table>

## Continuous Mode

High-throughput streaming with no per-request overhead:

<table>
<tr><th>Python</th><th>Rust</th></tr>
<tr><td>

```python
qrng.start_continuous()
for _ in range(100):
    chunk = qrng.read_continuous(1024)
qrng.stop()
```

</td><td>

```rust
qrng.start_continuous()?;
for _ in 0..100 {
    let chunk = qrng.read_continuous(1024)?;
}
qrng.stop()?;
```

</td></tr>
</table>

If you care about the first continuous read being as fresh as possible after
entering continuous mode, use the fresh-start helper. It starts continuous mode
and drains any already-buffered input bytes once:

<table>
<tr><th>Python</th><th>Rust</th></tr>
<tr><td>

```python
drained = qrng.start_continuous_fresh()
chunk = qrng.read_continuous(1024)
qrng.stop()
```

</td><td>

```rust
let drained = qrng.start_continuous_fresh()?;
let chunk = qrng.read_continuous(1024)?;
qrng.stop()?;
```

</td></tr>
</table>

## Device Info & Status

<table>
<tr><th>Python</th><th>Rust</th></tr>
<tr><td>

```python
info = qrng.get_info()
# serial, fw_version, core_version, hw_info

status = qrng.get_status()
# initialized, ready_bytes, health flags...

stats = qrng.get_statistics()
# generated_bytes, speed, failure counts...

config = qrng.get_config()
# postprocess, block_size, auto_calibration...
```

</td><td>

```rust
let info = qrng.get_info()?;
// serial, fw_version, core_version, hw_info

let status = qrng.get_status()?;
// initialized, ready_bytes, health flags...

let stats = qrng.get_statistics()?;
// generated_bytes, speed, failure counts...

let config = qrng.get_config()?;
// postprocess, block_size, auto_calibration...
```

</td></tr>
</table>

## Configuration

Every device setting is readable and writable:

<table>
<tr><th>Python</th><th>Rust</th></tr>
<tr><td>

```python
from dataclasses import replace

config = qrng.get_config()
config = replace(config,
    block_size=256,
    auto_calibration=False,
)
qrng.set_config(config)
```

</td><td>

```rust
let mut config = qrng.get_config()?;
config.block_size = 256;
config.auto_calibration = false;
qrng.set_config(&config)?;
```

</td></tr>
</table>

| Field | Type | Description |
|-------|------|-------------|
| `postprocess` | `PostProcess` | SHA256, RawNoise, or RawSamples |
| `initial_level` | `f32` | LED initial level |
| `startup_test` | `bool` | Run health test on startup |
| `auto_calibration` | `bool` | Auto-calibrate light source |
| `repetition_count` | `bool` | NIST SP 800-90B repetition count test |
| `adaptive_proportion` | `bool` | NIST SP 800-90B adaptive proportion test |
| `bit_count` | `bool` | Crypta Labs bit balance test |
| `generate_on_error` | `bool` | Keep generating if a health test fails |
| `n_lsbits` | `u8` | Number of LSBs to extract per sample |
| `hash_input_size` | `u8` | Bytes fed into SHA256 per output block |
| `block_size` | `u16` | Output block size in bytes |
| `autocalibration_target` | `u16` | Target value for auto-calibration |

## API Reference

| Method | Description |
|--------|-------------|
| `random(n)` | Get `n` random bytes (1–65535, one-shot) |
| `signed_read(n)` | Get `n` random bytes + 64-byte signature (FW 5.13+) |
| `signed_read_verified(n, pub_key)` | Signed read + ECDSA signature verification |
| `start_continuous()` | Start continuous streaming mode |
| `start_continuous_fresh()` | Start continuous mode and discard buffered input |
| `drain_input()` | Discard queued input bytes and return the number drained |
| `read_continuous(n)` | Read `n` bytes from continuous stream |
| `fill_bytes(buf)` | Fill a buffer of any size (auto-chunks) |
| `get_info()` | Serial number, firmware version, hardware |
| `get_status()` | Health flags, ready byte count |
| `get_config()` | Full device configuration |
| `set_config(config)` | Write device configuration |
| `set_postprocess(mode)` | Shortcut to change entropy mode |
| `get_statistics()` | Bytes generated, speed, failure counts |
| `get_dev_pub_key()` | Device's ECDSA P-256 public key (64 bytes) |
| `get_dev_certificate()` | CA-signed device certificate (64 bytes) |
| `get_verified_pub_key(ca_key)` | Verify certificate chain, return device public key |
| `reboot()` | Reboot the device (reconnect required) |
| `reset()` | Restart generation and clear statistics |
| `stop()` | Halt any active generation |
| `close()` | Close serial port |

Rust also implements `std::io::Read`, so `QCicada` works anywhere a reader is expected.

## Project Structure

```
qcicada/
├── src/              # Rust crate
│   ├── lib.rs
│   ├── device.rs     # QCicada high-level API
│   ├── protocol.rs   # Wire protocol (pure, no I/O)
│   ├── crypto.rs     # ECDSA P-256 certificate verification
│   ├── serial.rs     # Serial transport + macOS fixes
│   ├── discovery.rs  # Device discovery
│   └── types.rs      # Shared data types
├── tests/            # Rust integration tests (device required)
├── examples/         # Rust examples
├── python/
│   ├── src/qcicada/  # Python package (mirrors Rust API)
│   ├── tests/        # Python unit + integration tests
│   └── examples/     # Python examples
├── Cargo.toml
└── python/pyproject.toml
```

Both SDKs implement the same wire protocol and share the same test vectors. Changes to one should be reflected in the other.

## Why Not pyqcc?

The official Crypta Labs SDK (`pyqcc`) has macOS issues:

- Uses `/dev/tty.*` ports — macOS needs `/dev/cu.*`
- Sets `inter_byte_timeout` — causes FTDI read failures on macOS
- Timeouts too short — macOS FTDI driver needs at least 500ms
- No flush delay — FTDI driver drops bytes without a post-write pause
- Device may be left in continuous mode — no drain on connect

This SDK fixes all of these. It also works fine on Linux.

## License

MIT