• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zhaozg / lua-openssl / 25776463823

13 May 2026 03:28AM UTC coverage: 91.231% (-2.6%) from 93.832%
25776463823

Pull #408

travis-ci

zhaozg
feat(pqc): Phase 2.4 - Provider Management for PQC

Add PQC provider management capabilities to the provider module:

- Add `provider.query_pqc_algorithms()` to probe and list available PQC
  algorithms by attempting key generation for known PQC algorithm names
- Add `provider.load_pqc_providers()` to auto-detect and load common
  PQC providers (oqsprovider, liboqs, oqs, oqs-provider)
- Auto-load common PQC providers on module initialization (best-effort)
- Support both old OQS names (DILITHIUM2, KYBER768, etc.) and
  standardized NIST names (ML-DSA-44, ML-KEM-768, SLH-DSA-SHA2-*, etc.)
- Add comprehensive LDoc documentation for all new functions
- Add test suite covering query, load, and combined scenarios

This completes Phase 2.4 of the PQC implementation roadmap.
Pull Request #408: Feat/pqc

913 of 1124 new or added lines in 10 files covered. (81.23%)

45 existing lines in 10 files now uncovered.

9519 of 10434 relevant lines covered (91.23%)

1598.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.87
/src/dh.c
1
/*=========================================================================*\
2
* dh.c
3
* DH routines for lua-openssl binding
4
*
5
* Author:  george zhao <zhaozg(at)gmail.com>
6
\*=========================================================================*/
7

8
/***
9
dh module for lua-openssl binding
10

11
Diffie-Hellman (DH) key exchange is a method of securely exchanging
12
cryptographic keys over a public channel. The module provides functionality
13
for DH parameter generation, key generation and key agreement operations.
14

15
@module dh
16
@usage
17
  dh = require('openssl').dh
18
*/
19
#include <openssl/dh.h>
20
#include <openssl/engine.h>
21

22
#include "openssl.h"
23
#include "private.h"
24

25
#if !defined(OPENSSL_NO_DH)
26

27
#if OPENSSL_VERSION_NUMBER >= 0x30000000L && !defined(LIBRESSL_VERSION_NUMBER)
28
EVP_PKEY* openssl_new_pkey_dh_with(const BIGNUM *p,
29
                                   const BIGNUM *q,
30
                                   const BIGNUM *g,
31
                                   const BIGNUM *pub_key,
32
                                   const BIGNUM *priv_key)
33
{
34
  EVP_PKEY *pkey = NULL;
35
  OSSL_PARAM_BLD *param_bld = OSSL_PARAM_BLD_new();
36
  if (param_bld) {
37
    EVP_PKEY_CTX *ctx = NULL;
38
    OSSL_PARAM *params = NULL;
39

40
    if (p && !OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_FFC_P, p)) goto cleanup;
41
    if (q && !OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_FFC_Q, q)) goto cleanup;
42
    if (g && !OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_FFC_G, g)) goto cleanup;
43
    if (pub_key && !OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_PUB_KEY, pub_key)) goto cleanup;
44

45
    if (priv_key && !OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_PRIV_KEY, priv_key)) goto cleanup;
46
    params = OSSL_PARAM_BLD_to_param(param_bld);
47
    if (!params) goto cleanup;
48

49
    ctx = EVP_PKEY_CTX_new_from_name(NULL, "DH", NULL);
50
    if (!ctx) goto cleanup;
51

52
    if (EVP_PKEY_fromdata_init(ctx) <= 0) goto cleanup;
53
    if (EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_KEYPAIR, params) <= 0) {
54
      pkey = NULL;
55
    }
56
  cleanup:
57
    OSSL_PARAM_free(params);
58
    OSSL_PARAM_BLD_free(param_bld);
59
    EVP_PKEY_CTX_free(ctx);
60
  }
61
  return pkey;
62
}
63
#endif
64

65
/* Suppress deprecation warnings for DH_free which is unavoidable
66
 * since we manage DH objects for Lua interface compatibility */
67
#if defined(__GNUC__) || defined(__clang__)
68
#pragma GCC diagnostic push
69
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
70
#endif
71

72
static int openssl_dh_free(lua_State *L)
8✔
73
{
74
  DH *dh = CHECK_OBJECT(1, DH, "openssl.dh");
8✔
75
  /* Note: DH_free is still used here as we manage DH objects directly.
76
   * The deprecation warnings are in the generation/manipulation functions
77
   * which have been migrated to EVP_PKEY APIs above. */
78
  DH_free(dh);
8✔
79
  return 0;
8✔
80
};
81

82
#if defined(__GNUC__) || defined(__clang__)
83
#pragma GCC diagnostic pop
84
#endif
85

86
/***
87
parse DH key parameters and components
88
@function parse
89
@treturn table DH parameters including size, bits, p, q, g, public key, and private key (if present)
90
*/
91
static int openssl_dh_parse(lua_State *L)
4✔
92
{
93
  DH *dh = CHECK_OBJECT(1, DH, "openssl.dh");
4✔
94
  lua_newtable(L);
4✔
95

96
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
97
  /* OpenSSL 3.0+ - Use EVP_PKEY APIs */
98
  BIGNUM   *p = NULL, *q = NULL, *g = NULL, *pub = NULL, *pri = NULL;
99
  EVP_PKEY *pkey = NULL;
100
  int       bits = 0;
101

102
  /* Create EVP_PKEY from DH to use new APIs */
103
  pkey = EVP_PKEY_new();
104

105
#if defined(__GNUC__) || defined(__clang__)
106
#pragma GCC diagnostic push
107
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
108
#endif
109

110
  if (pkey && EVP_PKEY_set1_DH(pkey, dh) == 1) {
111

112
#if defined(__GNUC__) || defined(__clang__)
113
#pragma GCC diagnostic pop
114
#endif
115

116
    /* Get bits using EVP_PKEY API */
117
    bits = EVP_PKEY_get_bits(pkey);
118
    lua_pushinteger(L, (bits + 7) / 8); /* size in bytes */
119
    lua_setfield(L, -2, "size");
120

121
    lua_pushinteger(L, bits);
122
    lua_setfield(L, -2, "bits");
123

124
    /* Get parameters using EVP_PKEY_get_bn_param */
125
    if (EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_FFC_P, &p) == 1) {
126
      OPENSSL_PKEY_GET_BN(p, p);
127
      BN_free(p);
128
    }
129

130
    if (EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_FFC_Q, &q) == 1) {
131
      OPENSSL_PKEY_GET_BN(q, q);
132
      BN_free(q);
133
    }
134

135
    if (EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_FFC_G, &g) == 1) {
136
      OPENSSL_PKEY_GET_BN(g, g);
137
      BN_free(g);
138
    }
139

140
    if (EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_PUB_KEY, &pub) == 1) {
141
      OPENSSL_PKEY_GET_BN(pub, pub_key);
142
      BN_free(pub);
143
    }
144

145
    if (EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_PRIV_KEY, &pri) == 1) {
146
      OPENSSL_PKEY_GET_BN(pri, priv_key);
147
      BN_free(pri);
148
    }
149

150
    EVP_PKEY_free(pkey);
151
  } else {
152
    /* Fallback if EVP_PKEY creation fails */
153
    if (pkey) EVP_PKEY_free(pkey);
154
    lua_pushinteger(L, 0);
155
    lua_setfield(L, -2, "size");
156
    lua_pushinteger(L, 0);
157
    lua_setfield(L, -2, "bits");
158
  }
159
#else
160
  /* OpenSSL 1.x - Use legacy DH APIs */
161
  const BIGNUM *p = NULL, *q = NULL, *g = NULL, *pub = NULL, *pri = NULL;
4✔
162

163
  lua_pushinteger(L, DH_size(dh));
4✔
164
  lua_setfield(L, -2, "size");
4✔
165

166
  lua_pushinteger(L, DH_bits(dh));
4✔
167
  lua_setfield(L, -2, "bits");
4✔
168

169
  DH_get0_pqg(dh, &p, &q, &g);
4✔
170
  DH_get0_key(dh, &pub, &pri);
4✔
171

172
  OPENSSL_PKEY_GET_BN(p, p);
4✔
173
  OPENSSL_PKEY_GET_BN(q, q);
4✔
174
  OPENSSL_PKEY_GET_BN(g, g);
4✔
175
  OPENSSL_PKEY_GET_BN(pub, pub_key);
4✔
176
  OPENSSL_PKEY_GET_BN(pri, priv_key);
4✔
177
#endif
178

179
  return 1;
4✔
180
}
181

182
/***
183
check DH parameters for validity
184
@function check
185
@treturn boolean true if parameters are valid
186
@treturn[opt] table error codes if parameters are invalid
187
*/
188
static int openssl_dh_check(lua_State *L)
4✔
189
{
190
  const DH *dh = CHECK_OBJECT(1, DH, "openssl.dh");
4✔
191
  int       ret = 0;
4✔
192
  int       codes = 0;
4✔
193

194
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
195
  /* OpenSSL 3.0+ - Use EVP_PKEY APIs */
196
  EVP_PKEY     *pkey = NULL;
197
  EVP_PKEY_CTX *ctx = NULL;
198

199
#if defined(__GNUC__) || defined(__clang__)
200
#pragma GCC diagnostic push
201
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
202
#endif
203

204
  if (lua_isuserdata(L, 2)) {
205
    /* Check public key - convert to EVP_PKEY and use EVP_PKEY_public_check */
206
    pkey = EVP_PKEY_new();
207
    if (pkey && EVP_PKEY_set1_DH(pkey, (DH*)dh) == 1) {
208
      ctx = EVP_PKEY_CTX_new(pkey, NULL);
209
      if (ctx) {
210
        ret = EVP_PKEY_public_check(ctx);
211
        /* Map result to legacy codes for compatibility */
212
        if (ret != 1) {
213
          codes = DH_CHECK_PUBKEY_TOO_SMALL; /* Simplified mapping */
214
        }
215
        EVP_PKEY_CTX_free(ctx);
216
      }
217
      EVP_PKEY_free(pkey);
218
    }
219
  } else {
220
    /* Check parameters - convert to EVP_PKEY and use EVP_PKEY_param_check */
221
    pkey = EVP_PKEY_new();
222
    if (pkey && EVP_PKEY_set1_DH(pkey, (DH*)dh) == 1) {
223
      ctx = EVP_PKEY_CTX_new(pkey, NULL);
224
      if (ctx) {
225
        ret = EVP_PKEY_param_check(ctx);
226
        /* EVP_PKEY_param_check returns 1 for success, 0 for failure */
227
        /* We need to map this to legacy DH_check codes for compatibility */
228
        if (ret != 1) {
229
          codes = DH_CHECK_P_NOT_PRIME; /* Simplified mapping */
230
        }
231
        EVP_PKEY_CTX_free(ctx);
232
      }
233
      EVP_PKEY_free(pkey);
234
    }
235
  }
236

237
#if defined(__GNUC__) || defined(__clang__)
238
#pragma GCC diagnostic pop
239
#endif
240

241
#else
242
  /* OpenSSL 1.x - Use legacy DH APIs */
243
  if (lua_isuserdata(L, 2)) {
4✔
244
    const BIGNUM *pub = CHECK_OBJECT(2, BIGNUM, "openssl.bn");
2✔
245
    ret = DH_check_pub_key(dh, pub, &codes);
2✔
246
  } else {
247
    ret = DH_check(dh, &codes);
2✔
248
  }
249
#endif
250

251
  lua_pushboolean(L, ret);
4✔
252
  lua_pushinteger(L, codes);
4✔
253
  return 2;
4✔
254
}
255

256
/***
257
generate DH parameters for key exchange
258
@function generate_parameters
259
@tparam[opt=1024] number bits parameter size in bits
260
@tparam[opt=2] number generator generator value (typically 2 or 5)
261
@tparam[opt] openssl.engine eng engine to use for parameter generation
262
@treturn dh|nil generated DH parameters or nil on error
263
*/
264
static int
265
openssl_dh_generate_parameters(lua_State *L)
2✔
266
{
267
  int     bits = luaL_optint(L, 1, 1024);
2✔
268
  int     generator = luaL_optint(L, 2, 2);
2✔
269
  ENGINE *eng = lua_isnoneornil(L, 3) ? NULL : CHECK_OBJECT(3, ENGINE, "openssl.engine");
2✔
270
  int     ret = 0;
2✔
271
  DH     *dh = NULL;
2✔
272

273
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
274
  /* OpenSSL 3.0+ - Use EVP_PKEY APIs */
275
  EVP_PKEY_CTX *pctx = NULL;
276
  EVP_PKEY     *pkey = NULL;
277

278
  /* Note: ENGINE support is not directly available with EVP_PKEY_CTX_new_from_name.
279
   * For ENGINE support, the old API would need to be used. */
280
  (void)eng;
281

282
  /* Create parameter generation context */
283
  pctx = EVP_PKEY_CTX_new_from_name(NULL, "DH", NULL);
284
  if (!pctx) {
285
    return openssl_pushresult(L, 0);
286
  }
287

288
  /* Initialize parameter generation */
289
  ret = EVP_PKEY_paramgen_init(pctx);
290
  if (ret != 1) {
291
    EVP_PKEY_CTX_free(pctx);
292
    return openssl_pushresult(L, ret);
293
  }
294

295
  /* Set parameter generation options */
296
  if (EVP_PKEY_CTX_set_dh_paramgen_prime_len(pctx, bits) <= 0) {
297
    EVP_PKEY_CTX_free(pctx);
298
    return openssl_pushresult(L, 0);
299
  }
300

301
  if (EVP_PKEY_CTX_set_dh_paramgen_generator(pctx, generator) <= 0) {
302
    EVP_PKEY_CTX_free(pctx);
303
    return openssl_pushresult(L, 0);
304
  }
305

306
  /* Generate parameters */
307
  ret = EVP_PKEY_paramgen(pctx, &pkey);
308
  EVP_PKEY_CTX_free(pctx);
309

310
  if (ret == 1 && pkey) {
311
    /* Extract DH from EVP_PKEY for compatibility with Lua API */
312
#if defined(__GNUC__) || defined(__clang__)
313
#pragma GCC diagnostic push
314
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
315
#endif
316
    dh = EVP_PKEY_get1_DH(pkey);
317
#if defined(__GNUC__) || defined(__clang__)
318
#pragma GCC diagnostic pop
319
#endif
320

321
    EVP_PKEY_free(pkey);
322

323
    if (dh) {
324
      PUSH_OBJECT(dh, "openssl.dh");
325
      return 1;
326
    }
327
  }
328

329
  if (pkey) EVP_PKEY_free(pkey);
330
  return openssl_pushresult(L, ret);
331
#else
332
  /* OpenSSL 1.x - Use legacy DH APIs */
333
  dh = eng ? DH_new_method(eng) : DH_new();
2✔
334
  ret = DH_generate_parameters_ex(dh, bits, generator, NULL);
2✔
335

336
  if (ret == 1) {
2✔
337
    PUSH_OBJECT(dh, "openssl.dh");
2✔
338
    return 1;
2✔
339
  }
UNCOV
340
  DH_free(dh);
×
UNCOV
341
  return openssl_pushresult(L, ret);
×
342
#endif
343
}
344

345
/***
346
generate a DH key pair from parameters
347
@function generate_key
348
@treturn openssl.dh new DH object with generated key pair on success
349
*/
350
static int
351
openssl_dh_generate_key(lua_State *L)
2✔
352
{
353
  DH  *dhparameter = CHECK_OBJECT(1, DH, "openssl.dh");
2✔
354
  DH  *dh = NULL;
2✔
355
  int  ret = 0;
2✔
356

357
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
358
  /* OpenSSL 3.0+ - Use EVP_PKEY APIs */
359
  EVP_PKEY     *param_pkey = NULL;
360
  EVP_PKEY_CTX *kctx = NULL;
361
  EVP_PKEY     *pkey = NULL;
362

363
#if defined(__GNUC__) || defined(__clang__)
364
#pragma GCC diagnostic push
365
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
366
#endif
367

368
  /* Convert DH parameters to EVP_PKEY */
369
  param_pkey = EVP_PKEY_new();
370
  if (!param_pkey || EVP_PKEY_set1_DH(param_pkey, dhparameter) != 1) {
371
    if (param_pkey) EVP_PKEY_free(param_pkey);
372
    return openssl_pushresult(L, 0);
373
  }
374

375
#if defined(__GNUC__) || defined(__clang__)
376
#pragma GCC diagnostic pop
377
#endif
378

379
  /* Create key generation context from parameters */
380
  kctx = EVP_PKEY_CTX_new(param_pkey, NULL);
381
  EVP_PKEY_free(param_pkey);
382

383
  if (!kctx) {
384
    return openssl_pushresult(L, 0);
385
  }
386

387
  /* Initialize key generation */
388
  ret = EVP_PKEY_keygen_init(kctx);
389
  if (ret != 1) {
390
    EVP_PKEY_CTX_free(kctx);
391
    return openssl_pushresult(L, ret);
392
  }
393

394
  /* Generate key pair */
395
  ret = EVP_PKEY_keygen(kctx, &pkey);
396
  EVP_PKEY_CTX_free(kctx);
397

398
  if (ret == 1 && pkey) {
399
    /* Extract DH from EVP_PKEY for compatibility with Lua API */
400
#if defined(__GNUC__) || defined(__clang__)
401
#pragma GCC diagnostic push
402
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
403
#endif
404
    dh = EVP_PKEY_get1_DH(pkey);
405
#if defined(__GNUC__) || defined(__clang__)
406
#pragma GCC diagnostic pop
407
#endif
408

409
    EVP_PKEY_free(pkey);
410

411
    if (dh) {
412
      PUSH_OBJECT(dh, "openssl.dh");
413
      return 1;
414
    }
415
  }
416

417
  if (pkey) EVP_PKEY_free(pkey);
418
  return openssl_pushresult(L, ret);
419
#else
420
  /* OpenSSL 1.x - Use legacy DH APIs */
421
  dh = DHparams_dup(dhparameter);
2✔
422
  ret = DH_generate_key(dh);
2✔
423

424
  if (ret == 1) {
2✔
425
    PUSH_OBJECT(dh, "openssl.dh");
2✔
426
    return 1;
2✔
427
  }
UNCOV
428
  DH_free(dh);
×
UNCOV
429
  return openssl_pushresult(L, ret);
×
430
#endif
431
}
432

433
static luaL_Reg dh_funs[] = {
434
  { "generate_key", openssl_dh_generate_key },
435
  { "parse",        openssl_dh_parse        },
436
  { "check",        openssl_dh_check        },
437

438
  { "__gc",         openssl_dh_free         },
439
  { "__tostring",   auxiliar_tostring       },
440

441
  { NULL,           NULL                    }
442
};
443

444
static LuaL_Enumeration dh_problems[] = {
445
  { "DH_CHECK_P_NOT_PRIME",         DH_CHECK_P_NOT_PRIME         },
446
  { "DH_CHECK_P_NOT_SAFE_PRIME",    DH_CHECK_P_NOT_SAFE_PRIME    },
447
  { "DH_UNABLE_TO_CHECK_GENERATOR", DH_UNABLE_TO_CHECK_GENERATOR },
448
  { "DH_NOT_SUITABLE_GENERATOR",    DH_NOT_SUITABLE_GENERATOR    },
449
#ifdef DH_CHECK_Q_NOT_PRIME
450
  { "DH_CHECK_Q_NOT_PRIME",         DH_CHECK_Q_NOT_PRIME         },
451
#endif
452
#ifdef DH_CHECK_INVALID_Q_VALUE
453
  { "DH_CHECK_INVALID_Q_VALUE",     DH_CHECK_INVALID_Q_VALUE     },
454
#endif
455
#ifdef DH_CHECK_INVALID_J_VALUE
456
  { "DH_CHECK_INVALID_J_VALUE",     DH_CHECK_INVALID_J_VALUE     },
457
#endif
458

459
  { "DH_CHECK_PUBKEY_TOO_SMALL",    DH_CHECK_PUBKEY_TOO_SMALL    },
460
  { "DH_CHECK_PUBKEY_TOO_LARGE",    DH_CHECK_PUBKEY_TOO_LARGE    },
461
#ifdef DH_CHECK_PUBKEY_INVALID
462
  { "DH_CHECK_PUBKEY_INVALID",      DH_CHECK_PUBKEY_INVALID      },
463
#endif
464

465
  { NULL,                           -1                           }
466
};
467

468
/***
469
interpret DH parameter check problems
470
@function problems
471
@tparam number reason the problem codes returned by check functions
472
@tparam[opt=false] boolean pub whether to include public key problems
473
@treturn table array of problem descriptions
474
*/
475
static int
476
openssl_dh_problems(lua_State *L)
4✔
477
{
478
  int reason = luaL_checkint(L, 1);
4✔
479
  int pub = lua_toboolean(L, 2);
4✔
480
  int i = 1;
4✔
481

482
#define VAL(r, v)                                                                                  \
483
  if (r & DH_##v) {                                                                                \
484
    lua_pushliteral(L, #v);                                                                        \
485
    lua_rawseti(L, -2, i++);                                                                       \
486
  }
487

488
  lua_newtable(L);
4✔
489
  if (pub) {
4✔
490
    VAL(reason, CHECK_PUBKEY_TOO_SMALL);
2✔
491
    VAL(reason, CHECK_PUBKEY_TOO_LARGE);
2✔
492
#ifdef DH_CHECK_PUBKEY_INVALID
493
    VAL(reason, CHECK_PUBKEY_INVALID);
2✔
494
#endif
495
  } else {
496
    VAL(reason, CHECK_P_NOT_PRIME);
2✔
497
    VAL(reason, CHECK_PUBKEY_TOO_SMALL);
2✔
498
    VAL(reason, UNABLE_TO_CHECK_GENERATOR);
2✔
499
    VAL(reason, NOT_SUITABLE_GENERATOR);
2✔
500

501
#ifdef DH_CHECK_Q_NOT_PRIME
502
    VAL(reason, CHECK_Q_NOT_PRIME);
2✔
503
#endif
504
#ifdef DH_CHECK_INVALID_Q_VALUE
505
    VAL(reason, CHECK_INVALID_Q_VALUE);
2✔
506
#endif
507
#ifdef DH_CHECK_INVALID_J_VALUE
508
    VAL(reason, CHECK_INVALID_J_VALUE);
2✔
509
#endif
510
  }
511

512
#undef VAL
513

514
  return 1;
4✔
515
}
516

517
static luaL_Reg R[] = {
518
  { "generate_parameters", openssl_dh_generate_parameters },
519
  { "problems",            openssl_dh_problems            },
520

521
  { NULL,                  NULL                           }
522
};
523

524
int
525
luaopen_dh(lua_State *L)
15✔
526
{
527
  auxiliar_newclass(L, "openssl.dh", dh_funs);
15✔
528

529
  lua_newtable(L);
15✔
530
  luaL_setfuncs(L, R, 0);
15✔
531

532
  auxiliar_enumerate(L, -1, dh_problems);
15✔
533

534
  return 1;
15✔
535
}
536
#endif
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc