pqc_asn1 0.1.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d82d0cdf76e9156af39368f55311d62bc7bf1a0f8b7c50e1a59cf8d49a90ff95
4
- data.tar.gz: d922d07e2f12177fe0a0b1a88df589aa6edf9f9ff45f4ec6278a20926d628071
3
+ metadata.gz: c30efe239594f245393a2ec72d0794c685d469d064be4da9f480343cf25435fd
4
+ data.tar.gz: 733d22810457f379abc602ff94fbac6c510df30e2e8f33fe0006d2f0a55af4ec
5
5
  SHA512:
6
- metadata.gz: dd25d378e2719c6b29abbb6cb68d8c2c1e76b974b7532c97cdb4399e19f24e90aa38ff36b12bbcf2b191f7f8f5bc8207a90ed76016c5bbaafe11ae588d842082
7
- data.tar.gz: f77499b592e7cd301780cf33d9a9cf5112a167a1cf4c0c624d97ebdbcc3f1e9dd30ba25cc0d9c82831f8d208faec920d89ea21740f5f12af05dc08df066b2b28
6
+ metadata.gz: ad3dcf7c1a955dd30c59c10e96708dc9aa806af18373625f4301843be5d06c488b157ea9f86b33d30d31c3c6a0c41b3e82e252862713ee8f5e0b17a2c2fcb67c
7
+ data.tar.gz: 767ceaedb4cfddf337a0367b2a6fa0ea3786203a57d8113904d4ef0dbcf460d1ae7fc68bae90767924768fdce6ab5d751cac701fd63a082d311aa722f2e36b90
data/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.1] — 2026-03-19
11
+
12
+ ### Changed
13
+
14
+ - **libpqcsb integration**: Replaced the 997-line inline secure buffer
15
+ implementation with the standalone [libpqcsb](https://github.com/msuliq/libpqcsb)
16
+ C library. This eliminates code duplication and improves maintainability.
17
+ - SecureBuffer now uses libpqcsb's opaque buffer API, guard-based access control,
18
+ and improved memory protection.
19
+ - Updated `extconf.rb` to vendor libpqcsb with pkg-config fallback.
20
+ - All existing SecureBuffer methods and their semantics remain unchanged.
21
+ - Public API is fully backward-compatible.
22
+ - Updated vendored libpqcasn1 to version 0.1.5 (from 0.1.3).
23
+
10
24
  ### Fixed
11
25
 
12
26
  - `SecureBuffer#wipe!` now returns `self` instead of `nil`, enabling method
data/README.md CHANGED
@@ -249,6 +249,17 @@ Error categories:
249
249
  `PqcAsn1::DER::KEY_SIZES` maps each OID to `{ public:, secret: }` byte counts.
250
250
  Used automatically when `validate: true` is passed to `build_spki` / `build_pkcs8`.
251
251
 
252
+ ## Implementation
253
+
254
+ The gem is implemented in C with no OpenSSL dependency:
255
+
256
+ - **libpqcasn1** (v0.1.5) — Vendored pure C ASN.1/DER codec for key serialization
257
+ - **libpqcsb** (v0.1.0) — Vendored secure memory buffer library providing `mmap`-backed
258
+ allocation, guard pages, memory locking, and secure zeroing
259
+
260
+ Both libraries are vendored; the gem builds a single native extension with no external
261
+ C dependencies beyond POSIX APIs and the C standard library.
262
+
252
263
  ## Requirements
253
264
 
254
265
  - Ruby >= 2.7.2
data/ext/pqc_asn1/der.c CHANGED
@@ -265,7 +265,7 @@ typedef struct {
265
265
  size_t written;
266
266
  } pkcs8_build_ctx_t;
267
267
 
268
- static void
268
+ static pqcsb_status_t
269
269
  pkcs8_build_fill(uint8_t *data, size_t len, void *ctx_ptr)
270
270
  {
271
271
  pkcs8_build_ctx_t *ctx = (pkcs8_build_ctx_t *)ctx_ptr;
@@ -276,6 +276,7 @@ pkcs8_build_fill(uint8_t *data, size_t len, void *ctx_ptr)
276
276
  ctx->sk_ptr, ctx->sk_len,
277
277
  ctx->pk_ptr, ctx->pk_len,
278
278
  &ctx->written);
279
+ return ctx->rc == PQC_ASN1_OK ? PQCSB_OK : PQCSB_ERR_ALLOC;
279
280
  }
280
281
 
281
282
  /* ------------------------------------------------------------------ */
@@ -289,6 +290,7 @@ pkcs8_build_fill(uint8_t *data, size_t len, void *ctx_ptr)
289
290
  typedef struct {
290
291
  VALUE rb_oid_der;
291
292
  pqcsb_buf_t *sk_buf;
293
+ pqcsb_read_guard_t guard; /* Read access guard to sk_buf */
292
294
  VALUE result;
293
295
  pqc_asn1_status_t rc;
294
296
  /* Extended fields (NULL/0 when not using keywords). */
@@ -310,19 +312,19 @@ pkcs8_sb_build_body(VALUE arg)
310
312
  pqc_asn1_status_t rc = pqc_asn1_pkcs8_size_ex(
311
313
  oid_ptr, oid_len,
312
314
  ctx->params_ptr, ctx->params_len,
313
- ctx->sk_buf->len, ctx->pk_len, &total);
315
+ ctx->guard.len, ctx->pk_len, &total);
314
316
  if (rc != PQC_ASN1_OK)
315
317
  raise_status(rc, NULL);
316
318
 
317
319
  pkcs8_build_ctx_t build_ctx = {
318
320
  oid_ptr, oid_len,
319
321
  ctx->params_ptr, ctx->params_len,
320
- ctx->sk_buf->data, ctx->sk_buf->len,
322
+ ctx->guard.data, ctx->guard.len,
321
323
  ctx->pk_ptr, ctx->pk_len,
322
324
  PQC_ASN1_OK, 0
323
325
  };
324
- ctx->result = pqcsb_create_inplace(pqcsb_class(), total,
325
- pkcs8_build_fill, &build_ctx);
326
+ ctx->result = pqcsb_rb_create_inplace(pqcsb_class(), total,
327
+ pkcs8_build_fill, &build_ctx);
326
328
  ctx->rc = build_ctx.rc;
327
329
  return Qnil;
328
330
  }
@@ -330,7 +332,8 @@ pkcs8_sb_build_body(VALUE arg)
330
332
  static VALUE
331
333
  pkcs8_sb_ensure_end_read(VALUE arg)
332
334
  {
333
- pqcsb_end_read((pqcsb_buf_t *)arg);
335
+ pkcs8_sb_ctx_t *ctx = (pkcs8_sb_ctx_t *)arg;
336
+ pqcsb_end_read(&ctx->guard);
334
337
  return Qnil;
335
338
  }
336
339
 
@@ -385,21 +388,23 @@ rb_der_build_pkcs8(int argc, VALUE *argv, VALUE _self)
385
388
  /* Accept SecureBuffer input: read directly from its mmap region
386
389
  * without ever placing secret key material on the Ruby heap. */
387
390
  if (rb_obj_is_kind_of(rb_sk, pqcsb_class())) {
388
- pqcsb_buf_t *sk_buf;
389
- TypedData_Get_Struct(rb_sk, pqcsb_buf_t, &pqcsb_buf_type, sk_buf);
390
- if (sk_buf->wiped)
391
+ pqcsb_buf_t *sk_buf = (pqcsb_buf_t *)RTYPEDDATA_DATA(rb_sk);
392
+ if (pqcsb_is_wiped(sk_buf))
391
393
  rb_raise(rb_eRuntimeError, "SecureBuffer has been wiped");
392
394
 
393
395
  pkcs8_sb_ctx_t sb_ctx = {
394
- rb_oid_der, sk_buf, Qnil, PQC_ASN1_OK,
396
+ rb_oid_der, sk_buf, {NULL, 0, PQCSB_OK, NULL}, Qnil, PQC_ASN1_OK,
395
397
  params_ptr, params_len, pk_ptr, pk_len
396
398
  };
397
- pqcsb_begin_read(sk_buf);
399
+ sb_ctx.guard = pqcsb_begin_read(sk_buf);
400
+ if (sb_ctx.guard.status != PQCSB_OK)
401
+ rb_raise(rb_eRuntimeError, "pqcsb_begin_read failed");
402
+
398
403
  rb_ensure(pkcs8_sb_build_body, (VALUE)&sb_ctx,
399
- pkcs8_sb_ensure_end_read, (VALUE)sk_buf);
404
+ pkcs8_sb_ensure_end_read, (VALUE)&sb_ctx);
400
405
 
401
406
  if (sb_ctx.rc != PQC_ASN1_OK) {
402
- pqcsb_wipe(sb_ctx.result);
407
+ pqcsb_rb_wipe(sb_ctx.result);
403
408
  raise_status(sb_ctx.rc, NULL);
404
409
  }
405
410
  return sb_ctx.result;
@@ -428,12 +433,12 @@ rb_der_build_pkcs8(int argc, VALUE *argv, VALUE _self)
428
433
  pk_ptr, pk_len,
429
434
  PQC_ASN1_OK, 0
430
435
  };
431
- VALUE result = pqcsb_create_inplace(pqcsb_class(), total,
432
- pkcs8_build_fill, &build_ctx);
436
+ VALUE result = pqcsb_rb_create_inplace(pqcsb_class(), total,
437
+ pkcs8_build_fill, &build_ctx);
433
438
  if (build_ctx.rc != PQC_ASN1_OK) {
434
439
  /* Wipe the SecureBuffer so garbage content doesn't escape if
435
440
  * the caller rescues the exception. */
436
- pqcsb_wipe(result);
441
+ pqcsb_rb_wipe(result);
437
442
  raise_status(build_ctx.rc, NULL);
438
443
  }
439
444
  return result;
@@ -527,7 +532,7 @@ pkcs8_parse_use_cb(VALUE rb_bytes, VALUE data2,
527
532
  ? frozen_bin_str((const char *)alg_params, (long)alg_params_len)
528
533
  : Qnil;
529
534
 
530
- ctx->rb_key = pqcsb_create(pqcsb_class(), sk_bytes, sk_len);
535
+ ctx->rb_key = pqcsb_rb_create(pqcsb_class(), sk_bytes, sk_len);
531
536
 
532
537
  ctx->rb_pub = pub_key && pub_key_len > 0
533
538
  ? frozen_bin_str((const char *)pub_key, (long)pub_key_len)
@@ -582,7 +587,7 @@ rb_der_parse_pkcs8(UNUSED VALUE _self, VALUE rb_der)
582
587
  ? frozen_bin_str((const char *)alg_params, (long)alg_params_len)
583
588
  : Qnil;
584
589
 
585
- VALUE key_val = pqcsb_create(pqcsb_class(), sk_bytes, sk_len);
590
+ VALUE key_val = pqcsb_rb_create(pqcsb_class(), sk_bytes, sk_len);
586
591
 
587
592
  VALUE pub_val = pub_key && pub_key_len > 0
588
593
  ? frozen_bin_str((const char *)pub_key, (long)pub_key_len)
@@ -2,15 +2,17 @@
2
2
 
3
3
  require "mkmf"
4
4
 
5
- # Try to find libpqcasn1 as a system-installed library first.
6
- # If pkg-config finds it, the system headers and library are used and the
7
- # vendored pqc_asn1.c is excluded from compilation.
8
- # If not found, fall back to the vendored copy bundled in ext/pqc_asn1/.
9
- if pkg_config("pqcasn1")
10
- $srcs = %w[pqc_asn1_ext.c error.c secure_buffer.c oid.c cursor.c der.c pem.c base64_ext.c]
11
- else
12
- $srcs = %w[pqc_asn1_ext.c pqc_asn1.c error.c secure_buffer.c oid.c cursor.c der.c pem.c base64_ext.c]
13
- end
5
+ # Try to find system-installed libraries first.
6
+ # If pkg-config finds them, the system headers and libraries are used and
7
+ # the vendored sources are excluded from compilation.
8
+ # If not found, fall back to the vendored copies bundled in ext/pqc_asn1/.
9
+ pqcsb_available = pkg_config("pqcsb")
10
+ pqcasn1_available = pkg_config("pqcasn1")
11
+
12
+ $srcs_pqcsb = pqcsb_available ? [] : %w[pqcsb.c]
13
+ $srcs_pqcasn1 = pqcasn1_available ? [] : %w[pqc_asn1.c]
14
+
15
+ $srcs = %w[pqc_asn1_ext.c error.c secure_buffer.c oid.c cursor.c der.c pem.c base64_ext.c] + $srcs_pqcasn1 + $srcs_pqcsb
14
16
 
15
17
  # -Wpedantic generates excessive noise from Ruby's own UCRT headers on Windows
16
18
  # (C23 attributes like [[nodiscard]], [[maybe_unused]], __VA_OPT__).
@@ -0,0 +1,116 @@
1
+ /*
2
+ * internal.h — private definitions shared across libpqcsb source files.
3
+ *
4
+ * This header is NOT installed and is NOT part of the public API.
5
+ */
6
+
7
+ #ifndef PQCSB_INTERNAL_H
8
+ #define PQCSB_INTERNAL_H
9
+
10
+ /* Include generated config before anything else. */
11
+ #include "pqcsb_config.h"
12
+
13
+ /* Enable memset_s() on Apple/BSD via C11 Annex K.
14
+ * Must appear before any system header that may include string.h. */
15
+ #ifndef __STDC_WANT_LIB_EXT1__
16
+ #define __STDC_WANT_LIB_EXT1__ 1
17
+ #endif
18
+
19
+ #include "pqcsb.h"
20
+
21
+ #include <string.h>
22
+ #include <stdio.h>
23
+ #include <stdatomic.h>
24
+
25
+ #ifndef _WIN32
26
+ #include <unistd.h>
27
+ #endif
28
+
29
+ #ifdef HAVE_SYS_MMAN_H
30
+ #include <sys/mman.h>
31
+ #endif
32
+
33
+ #ifdef HAVE_SYS_RESOURCE_H
34
+ #include <sys/resource.h>
35
+ #endif
36
+
37
+ #ifdef _WIN32
38
+ #include <windows.h>
39
+ #include <ntsecapi.h>
40
+ #endif
41
+
42
+ /* ------------------------------------------------------------------ */
43
+ /* MAP_ANONYMOUS portability */
44
+ /* ------------------------------------------------------------------ */
45
+
46
+ #if defined(HAVE_MMAP) && !defined(MAP_ANONYMOUS) && defined(MAP_ANON)
47
+ #define MAP_ANONYMOUS MAP_ANON
48
+ #endif
49
+
50
+ /* ------------------------------------------------------------------ */
51
+ /* Platform backend selection */
52
+ /* ------------------------------------------------------------------ */
53
+
54
+ #if defined(HAVE_MMAP) && defined(MAP_ANONYMOUS)
55
+ # define PQCSB_USE_MMAP 1
56
+ #elif defined(_WIN32)
57
+ # define PQCSB_USE_VIRTUALALLOC 1
58
+ #elif defined(PQCSB_ALLOW_MALLOC_FALLBACK)
59
+ /* Weak protection: canaries only, no guard pages, no mprotect */
60
+ #else
61
+ # error "No mmap or VirtualAlloc available. Define PQCSB_ALLOW_MALLOC_FALLBACK "\
62
+ "to opt into the malloc fallback (no guard pages — reduced security guarantees)."
63
+ #endif
64
+
65
+ /* ------------------------------------------------------------------ */
66
+ /* Protection macros */
67
+ /* ------------------------------------------------------------------ */
68
+
69
+ #if defined(PQCSB_USE_MMAP) && defined(HAVE_MPROTECT)
70
+ # define PQCSB_SET_PROT(base, ps, dp, prot) \
71
+ mprotect((base) + (ps), (dp), (prot))
72
+ # define PQCSB_PROT_NONE PROT_NONE
73
+ # define PQCSB_PROT_READ PROT_READ
74
+ # define PQCSB_PROT_RW (PROT_READ | PROT_WRITE)
75
+ # define PQCSB_HAS_PROTECT 1
76
+ #elif defined(PQCSB_USE_VIRTUALALLOC)
77
+ # define PQCSB_SET_PROT(base, ps, dp, prot) do { \
78
+ DWORD _old; \
79
+ VirtualProtect((base) + (ps), (dp), (prot), &_old); \
80
+ } while (0)
81
+ # define PQCSB_PROT_NONE PAGE_NOACCESS
82
+ # define PQCSB_PROT_READ PAGE_READONLY
83
+ # define PQCSB_PROT_RW PAGE_READWRITE
84
+ # define PQCSB_HAS_PROTECT 1
85
+ #else
86
+ # define PQCSB_HAS_PROTECT 0
87
+ #endif
88
+
89
+ /* ------------------------------------------------------------------ */
90
+ /* Buffer struct (opaque to consumers) */
91
+ /* ------------------------------------------------------------------ */
92
+
93
+ struct pqcsb_buf {
94
+ uint8_t *data; /* pointer to user data (inside mmap or malloc'd) */
95
+ size_t len; /* user-requested byte length */
96
+ uint8_t *mmap_base; /* base of entire mmap region (NULL if malloc) */
97
+ size_t mmap_len; /* total mmap size including guard pages */
98
+ size_t data_pages; /* size of data region (between guard pages) */
99
+ int wiped; /* 1 if wipe() was called */
100
+ int locked; /* 1 if mlock succeeded on the data pages */
101
+ _Atomic int read_refs; /* reference count for nested begin_read/end_read */
102
+ uint8_t canary[PQCSB_CANARY_SIZE]; /* per-buffer random canary */
103
+ pqcsb_config_t config; /* runtime configuration options for this buffer */
104
+ };
105
+
106
+ /* ------------------------------------------------------------------ */
107
+ /* Internal function declarations */
108
+ /* ------------------------------------------------------------------ */
109
+
110
+ /* secure_zero.c */
111
+ /* (pqcsb_secure_zero is public, declared in pqcsb.h) */
112
+
113
+ /* random.c */
114
+ /* (pqcsb_fill_random is public, declared in pqcsb.h) */
115
+
116
+ #endif /* PQCSB_INTERNAL_H */
data/ext/pqc_asn1/pem.c CHANGED
@@ -77,18 +77,18 @@ pem_decoded_cleanup(VALUE arg)
77
77
  * pem_encode_sb_ctx_t — SecureBuffer encode context.
78
78
  *
79
79
  * pem_encode_sb_body calls pqc_asn1_pem_encode with the SecureBuffer's
80
- * data pointer (valid only inside begin_read/end_read).
80
+ * data pointer (valid only inside the read guard).
81
81
  * pem_encode_sb_cleanup always calls pqcsb_end_read to restore guard
82
82
  * pages even when pqc_asn1_pem_encode raises via ruby_xmalloc. On
83
83
  * exception paths it also frees any partially-allocated pem_buf.
84
84
  */
85
85
  typedef struct {
86
- pqcsb_buf_t *sb;
87
- const char *label;
88
- size_t label_len;
89
- char *pem_buf;
90
- size_t pem_len;
91
- pqc_asn1_status_t rc;
86
+ pqcsb_read_guard_t guard;
87
+ const char *label;
88
+ size_t label_len;
89
+ char *pem_buf;
90
+ size_t pem_len;
91
+ pqc_asn1_status_t rc;
92
92
  } pem_encode_sb_ctx_t;
93
93
 
94
94
  static VALUE
@@ -96,7 +96,7 @@ pem_encode_sb_body(VALUE arg)
96
96
  {
97
97
  pem_encode_sb_ctx_t *ctx = (pem_encode_sb_ctx_t *)arg;
98
98
  ctx->rc = pqc_asn1_pem_encode(
99
- ctx->sb->data, ctx->sb->len,
99
+ ctx->guard.data, ctx->guard.len,
100
100
  ctx->label, ctx->label_len,
101
101
  &ctx->pem_buf, &ctx->pem_len);
102
102
  return Qnil;
@@ -107,7 +107,7 @@ pem_encode_sb_cleanup(VALUE arg)
107
107
  {
108
108
  pem_encode_sb_ctx_t *ctx = (pem_encode_sb_ctx_t *)arg;
109
109
  /* Always restore mprotect — this is the primary purpose of rb_ensure here. */
110
- pqcsb_end_read(ctx->sb);
110
+ pqcsb_end_read(&ctx->guard);
111
111
  /* On exception paths pem_buf may have been allocated inside
112
112
  * pqc_asn1_pem_encode before ruby_xmalloc raised NoMemoryError.
113
113
  * Zero and free it here to prevent leaking PEM-encoded key material.
@@ -263,13 +263,15 @@ rb_pem_encode(UNUSED VALUE _self, VALUE rb_der, VALUE rb_label)
263
263
  if (rb_obj_is_kind_of(rb_der, pqcsb_class())) {
264
264
  /* SecureBuffer input: use rb_ensure to restore PROT_NONE even when
265
265
  * pqc_asn1_pem_encode raises via ruby_xmalloc under memory pressure. */
266
- pqcsb_buf_t *sb;
267
- TypedData_Get_Struct(rb_der, pqcsb_buf_t, &pqcsb_buf_type, sb);
268
- if (sb->wiped)
266
+ pqcsb_buf_t *sb = (pqcsb_buf_t *)RTYPEDDATA_DATA(rb_der);
267
+ if (pqcsb_is_wiped(sb))
269
268
  rb_raise(rb_eRuntimeError, "SecureBuffer has been wiped");
270
269
 
271
- pem_encode_sb_ctx_t ctx = {sb, label, label_len, NULL, 0, PQC_ASN1_OK};
272
- pqcsb_begin_read(sb);
270
+ pqcsb_read_guard_t guard = pqcsb_begin_read(sb);
271
+ if (guard.status != PQCSB_OK)
272
+ rb_raise(rb_eRuntimeError, "pqcsb_begin_read failed");
273
+
274
+ pem_encode_sb_ctx_t ctx = {guard, label, label_len, NULL, 0, PQC_ASN1_OK};
273
275
  rb_ensure(pem_encode_sb_body, (VALUE)&ctx,
274
276
  pem_encode_sb_cleanup, (VALUE)&ctx);
275
277
  rc = ctx.rc;
@@ -1,10 +1,18 @@
1
1
  /*
2
2
  * pqc_asn1.c — DER/PEM/Base64 utilities for post-quantum key serialization.
3
3
  *
4
- * VENDORED FILE — do not edit directly.
5
- * Origin: libpqcasn1 (https://github.com/msuliq/libpqcasn1)
6
- * To update: run `bundle exec rake vendor:concat` (local checkout) or
7
- * run `bundle exec rake vendor:update[VERSION]` (release tarball).
4
+ * GENERATED FILE — do not edit directly.
5
+ * Edit the individual source files in src/ and run scripts/concat.sh to
6
+ * regenerate this file.
7
+ *
8
+ * Source files (in concatenation order):
9
+ * src/tlv.c — version, safe arithmetic, secure zeroing, DER TLV helpers
10
+ * src/builder.c — SPKI/PKCS#8 layout structs and DER builders
11
+ * src/parser.c — SPKI/PKCS#8 DER parsers
12
+ * src/base64.c — RFC 4648 Base64 encode/decode
13
+ * src/pem.c — RFC 7468 PEM encode/decode
14
+ *
15
+ * Version: 0.1.5
8
16
  *
9
17
  * Standalone C library — no external dependencies beyond the C standard library.
10
18
  *