Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/zos-py-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ on:
paths:
- ".github/workflows/zos-py-build.yml"
- "native/python/**"
# zkr_py shares these native/c sources with zowex (see native/python/bindings/
# setup.py); listed individually rather than all of native/c/** since this job
# shares the zos-build concurrency group and takes ~20 minutes.
- "native/c/zkr.hpp"
- "native/c/zkr.cpp"
- "native/c/zkrio.hpp"
- "native/c/zkrio.cpp"
# push:
# branches:
# - main
Expand Down Expand Up @@ -68,6 +75,7 @@ jobs:
OWNER: ${{ secrets.SSH_MARIST_ZNP_ID }}
DSN_PREFIX: ${{ secrets.SSH_MARIST_ZNP_ID }}.PY#BI
USS_BASE_DIR: /ZOWE/tmp/test_uss_functions_for_python_bindings
KEYRING_PREFIX: ZKRUT
EOF

- name: Install Dependencies
Expand Down
23 changes: 23 additions & 0 deletions doc/apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,29 @@
| List proclib | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ➖ | ❌ |
| List subsystems | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ➖ | ❌ |

## Certificates

See [doc/certificates-test-plan.md](./certificates-test-plan.md) for the service layer
(`native/c/zkr.hpp`/`zkr.cpp`, R_datalib/IRRSDL64 + System SSL GSKCMS), the 14 JSON-RPC
methods below, and their test coverage.

| Operation | z/OSMF | FTP | Backend | Middleware | SDK | CLI | VSCE | Python |
| ------------------------ | ------ | --- | ------- | ---------- | --- | --- | ---- | ------ |
| Create key ring | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Delete key ring | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| List key rings | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Count certificates | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Refresh DIGTCERT class | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| List certificates | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Show certificate | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Set default certificate | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Connect certificate | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Delete certificate | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Trust certificate | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Rename certificate | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Export certificate | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Import certificate | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |

## Other

| Operation | z/OSMF | FTP | Backend | Middleware | SDK | CLI | VSCE | Python |
Expand Down
42 changes: 26 additions & 16 deletions doc/certificates-test-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,38 @@ the repo. Gated tests **skip** (not fail) when their gate is closed, so the
suite passes for any user — but skipped tests provide no coverage. CI currently
runs with both gates closed.

The Python bindings suite (`native/python/bindings/test/test_zkr.py`, part of
`npm run z:python:test` / `.github/workflows/zos-py-build.yml`) mirrors the same
three tiers against the `zkr_py` module (`create_keyring`, `list_certificates`,
`export_certificate`, etc. — see
[native/python/bindings/README.md](../native/python/bindings/README.md#zkr_py--certificates-and-key-rings)).
It calls the same `zkr.hpp`/`zkr.cpp` service layer **in-process** (no JSON-RPC, no
`zowex server`), so it is complementary to, not a substitute for, Item 4 below, which
drives the RPC layer out-of-process over JSON-RPC. Same gates, same expectation in
CI: the CI user has no certificate authority, so Tiers B and C skip there too.

## 2. Coverage matrix

"zowe-mcp" marks methods exercised on a real system by
[zowe-mcp#45](https://github.com/zowe/zowe-mcp/pull/45) — the field-proven
"don't break" set.

| RPC method | zowe-mcp | Automation today | Gap |
|---|---|---|---|
| `showCertificate` | ✅ | gated lifecycle | — |
| `connectCertificate` | ✅ | gated (`--from-ring`) | `--from-database` never exercised anywhere |
| `deleteCertificate` | ✅ | gated (database delete only) | ring-scoped disconnect + auto-refresh (SAF 4/4/12) untested |
| `exportCertificate` | ✅ | gated (PEM + p12, `--file` and `--dsn` incl. PDS/E member) | byte-for-byte parity against `keyring-util`/RACDCERT DSN output untested |
| `importCertificate` | ✅ | gated happy path, incl. `--dsn` (sequential + PDS/E member) | warning branches untested: already-exists (rsn 8/12/16), refresh-failed fallback |
| `setDefaultCertificate` | ✅ | gated lifecycle | — |
| `trustCertificate` | ✅ | gated (NOTRUST↔TRUST) | HIGHTRUST never exercised |
| `renameCertificate` | ✅ | gated (rename + back) | — |
| `refreshDigtcert` | ✅ | indirect only (delete auto-refresh) | no standalone test |
| `listCertificates` | — | gated list + unconditional filter unit suite | cap/`moreAvailable` never hit against a real >10-cert ring |
| `listRings` | — | gated (one call) | binary result-area parser + 256 KB truncation path: zero real exposure |
| `countRing` | — | **none** | both branches (virtual-ring enumeration, GetRingInfo sum) |
| `createKeyring` | — | gated ring lifecycle | — |
| `deleteKeyring` | — | gated (incl. error path) | — |
| RPC method | zowe-mcp | Automation today | zkr_py (in-process, Tier) | Gap |
|---|---|---|---|---|
| `showCertificate` | ✅ | gated lifecycle | C | — |
| `connectCertificate` | ✅ | gated (`--from-ring`) | C | `--from-database` never exercised anywhere |
| `deleteCertificate` | ✅ | gated (database delete only) | C | ring-scoped disconnect + auto-refresh (SAF 4/4/12) untested |
| `exportCertificate` | ✅ | gated (PEM + p12, `--file` and `--dsn` incl. PDS/E member) | C | byte-for-byte parity against `keyring-util`/RACDCERT DSN output untested |
| `importCertificate` | ✅ | gated happy path, incl. `--dsn` (sequential + PDS/E member) | C | warning branches untested: already-exists (rsn 8/12/16), refresh-failed fallback |
| `setDefaultCertificate` | ✅ | gated lifecycle | C | — |
| `trustCertificate` | ✅ | gated (NOTRUST↔TRUST) | C | HIGHTRUST never exercised |
| `renameCertificate` | ✅ | gated (rename + back) | C | — |
| `refreshDigtcert` | ✅ | indirect only (delete auto-refresh) | C (standalone) | no standalone test |
| `listCertificates` | — | gated list + unconditional filter unit suite | A + C | cap/`moreAvailable` never hit against a real >10-cert ring |
| `listRings` | — | gated (one call) | A + B | binary result-area parser + 256 KB truncation path: zero real exposure |
| `countRing` | — | **none** | C | both branches (virtual-ring enumeration, GetRingInfo sum) |
| `createKeyring` | — | gated ring lifecycle | B | — |
| `deleteKeyring` | — | gated (incl. error path) | A + B | — |

Layers with **no automation at any tier**: JSON-RPC dispatch + schema
validation for these 14 methods, the SDK TypeScript client, the CLI TypeScript
Expand Down
3 changes: 3 additions & 0 deletions native/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how

## Recent Changes

- `python`: Added the `zkr_py` module (certificate/key ring operations: `create_keyring`, `list_certificates`, `export_certificate`, `import_certificate`, and 14 others) — an in-process binding over the same `native/c/zkr.hpp`/`zkr.cpp` service layer `zowex system cert`/`system keyring` use. [#NNNN](https://github.com/zowe/zowex/pull/NNNN)
- `python`: `zkr_py`'s PKCS#12/PEM payloads cross as Python `bytes`, not `str`, so binary certificate material round-trips byte-exact; PEM returned to Python is portable ASCII, but PEM written to a file or data set stays EBCDIC, byte-identical to `keyring-util`. [#NNNN](https://github.com/zowe/zowex/pull/NNNN)
- `c`: Added `zkrio` (`native/c/zkrio.hpp`/`zkrio.cpp`), shared certificate-material data set/file I/O extracted from `commands/certificates.cpp` so `zkr_py` can reuse it without linking the CLI command layer. [#NNNN](https://github.com/zowe/zowex/pull/NNNN)
- `c`: Added `--dsn` to `zowex system cert import`/`export`, letting a certificate be read from or written to a sequential data set or PDS/E member instead of a USS file. [#NNNN](https://github.com/zowe/zowex/pull/NNNN)
- `c`: Added `zds_write_binary`, a byte-exact data set writer (BPAM for members, binary `fopen` for sequential) with no code-page conversion or line splitting; requires a V-format target. [#NNNN](https://github.com/zowe/zowex/pull/NNNN)
- `c`: Guard against OPEN hanging on a password-protected data set (legacy MVS password protection issues a WTOR with no console to answer it) by failing fast, pre-OPEN, in the BPAM write path. [#NNNN](https://github.com/zowe/zowex/pull/NNNN)
Expand Down
93 changes: 4 additions & 89 deletions native/c/commands/certificates.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

#include "certificates.hpp"
#include "../zkr.hpp"
#include "../zkrio.hpp"
#include "../zbase64.h"
#include "../zds.hpp"
#include "../ztype.h"
#include "../zut.hpp"
#include <cctype>
Expand Down Expand Up @@ -110,91 +110,6 @@ int run_simple(InvocationContext &context, int rc, ZKR &zkr, const std::string &
return RTNCD_SUCCESS;
}

// Read a PKCS#12 blob out of a sequential data set or PDS/E member. Binary mode so
// LE concatenates V-format record data with no RDWs and no code-page conversion --
// byte-identical to `cp -B "//'DSN'"`.
int read_cert_dsn(const std::string &dsn, std::string &data, std::string &err)
{
if (!zds_dataset_exists(dsn))
{
err = "Could not access data set: " + dsn;
return RTNCD_FAILURE;
}

ZDS zds{};
zut_prepare_encoding("binary", &zds.encoding_opts);
const int rc = zds_read(ZDSReadOpts{.zds = &zds, .dsname = dsn}, data);
if (rc != RTNCD_SUCCESS)
{
err = zds.diag.e_msg;
return RTNCD_FAILURE;
}
if (data.empty())
{
err = "PKCS#12 data set is empty: " + dsn;
return RTNCD_FAILURE;
}
return RTNCD_SUCCESS;
}

// Write exported certificate bytes to a data set, creating it when absent.
// is_binary=true (p12): raw bytes, chunked into V-format records (zds_write_binary).
// is_binary=false (pem): the EBCDIC PEM text is written as records via zds_write in
// text mode -- one line per record.
int write_cert_dsn(const std::string &dsn, const std::string &data, bool is_binary, std::string &err)
{
if (!zds_dataset_exists(dsn))
{
const auto open_paren = dsn.find('(');
const auto close_paren = dsn.find(')');
const bool has_member = open_paren != std::string::npos && close_paren != std::string::npos && close_paren > open_paren;
const std::string base_dsn = has_member ? dsn.substr(0, open_paren) : dsn;

if (has_member)
{
const std::string member = dsn.substr(open_paren + 1, close_paren - open_paren - 1);
if (!zds_is_valid_member_name(member))
{
err = "Invalid member name: " + member;
return RTNCD_FAILURE;
}
}

// RACDCERT FORMAT(PKCS12DER) parity: PS/VB/LRECL(84)/BLKSIZE(27998).
DS_ATTRIBUTES a{}; // zero-init: zds_create_dsn preserves 0, only negative means "unset"
a.dsorg = has_member ? "PO" : "PS";
a.recfm = "V,B"; // mandatory -- FB pads records with blanks, which would change the byte count
a.lrecl = 84;
a.blksize = 27998;
a.alcunit = "TRK";
a.primary = 5;
a.secondary = 5;
if (has_member)
{
a.dirblk = 5;
a.dsntype = ZDS_DSNTYPE_LIBRARY;
}

std::string response;
if (zds_create_dsn(nullptr, base_dsn, a, response) != RTNCD_SUCCESS)
{
err = "Could not create data set '" + base_dsn + "': " + response;
return RTNCD_FAILURE;
}
}

ZDS zds{}; // eDataTypeText + empty codepage -> zds_use_codepage() is false -> no iconv for PEM
const int rc = is_binary
? zds_write_binary(ZDSWriteOpts{.zds = &zds, .dsname = dsn}, data)
: zds_write(ZDSWriteOpts{.zds = &zds, .dsname = dsn}, data);
if (rc != RTNCD_SUCCESS)
{
err = zds.diag.e_msg;
return RTNCD_FAILURE;
}
return RTNCD_SUCCESS;
}

} // namespace

int handle_cert_list(InvocationContext &context)
Expand Down Expand Up @@ -338,7 +253,7 @@ int handle_cert_export(InvocationContext &context)
// the file is created (or truncated) owner-read/write only, independent of
// the process umask.
std::string err;
if (zut_write_file_private(file, data, err) != RTNCD_SUCCESS)
if (zkrio_write_file(file, data, err) != RTNCD_SUCCESS)
{
context.error_stream() << "Error: could not write output file: " << file << " (" << err << ")" << std::endl;
return RTNCD_FAILURE;
Expand All @@ -351,7 +266,7 @@ int handle_cert_export(InvocationContext &context)
else if (!dsn.empty())
{
std::string err;
if (write_cert_dsn(dsn, data, is_p12, err) != RTNCD_SUCCESS)
if (zkrio_write_dsn(dsn, data, is_p12, err) != RTNCD_SUCCESS)
{
context.error_stream() << "Error: could not write output data set: " << dsn << " (" << err << ")" << std::endl;
return RTNCD_FAILURE;
Expand Down Expand Up @@ -428,7 +343,7 @@ int handle_cert_import(InvocationContext &context)
if (!dsn.empty())
{
std::string err;
if (read_cert_dsn(dsn, opts.p12_data, err) != RTNCD_SUCCESS)
if (zkrio_read_dsn(dsn, opts.p12_data, err) != RTNCD_SUCCESS)
{
context.error_stream() << "Error: could not read source data set: " << dsn << " (" << err << ")" << std::endl;
return RTNCD_FAILURE;
Expand Down
19 changes: 17 additions & 2 deletions native/c/makefile
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ SERVER_OBJS = $(OUT_DIR)/server/builder.o \
$(OUT_DIR)/server/validator.o \
$(OUT_DIR)/server/worker.o

SWIG_EXTENDER_OBJS = $(OUT_DIR_SWIG)/zut.o $(OUT_DIR_SWIG)/zds.o $(OUT_DIR_SWIG)/zjb.o $(OUT_DIR_SWIG)/zcn.o $(OUT_DIR_SWIG)/zusf.o $(OUT_DIR_SWIG)/ztso.o
SWIG_EXTENDER_OBJS = $(OUT_DIR_SWIG)/zut.o $(OUT_DIR_SWIG)/zds.o $(OUT_DIR_SWIG)/zjb.o $(OUT_DIR_SWIG)/zcn.o $(OUT_DIR_SWIG)/zusf.o $(OUT_DIR_SWIG)/ztso.o $(OUT_DIR_SWIG)/zkr.o $(OUT_DIR_SWIG)/zkrio.o

all: libzut.so libzut.a libzds.so libzds.a libzusf.so libzusf.a libzcn.so libzcn.a libzjb.so libzjb.a libzkr.so libzkr.a zowex zoweax
swig-extenders: $(OUT_DIR_SWIG) $(SWIG_EXTENDER_OBJS)
Expand Down Expand Up @@ -207,6 +207,10 @@ $(OUT_DIR)/zkr.o: zkr.cpp
@echo 'Building $(OUT_DIR)/zkr.o'
$(CXX) $(CPP_FLAGS) -o $@ $^

$(OUT_DIR_SWIG)/zkr.o: $(OUT_DIR_SWIG) zkr.cpp
@echo 'Building $(OUT_DIR_SWIG)/zkr.o with SWIG macro'
$(CXX) $(SWIG_FLAGS) -o $@ zkr.cpp

$(OUT_DIR)/libzkr.so: $(OUT_DIR)/zkr.o
@echo 'Building $(OUT_DIR)/libzkr.so'
$(CXX) $(DLL_BND_FLAGS) -o $@ $^ $(GSKCMS_SIDEDECK)
Expand All @@ -219,6 +223,17 @@ $(OUT_DIR)/libzkr.a: $(OUT_DIR)/zkr.o

libzkr.a: $(OUT_DIR) $(OUT_DIR)/libzkr.a

# zkrio is a plain (non-SWIG) helper used only by the zowex command layer today;
# it deliberately stays out of libzkr.{so,a}, which binds standalone against
# nothing but the GSKCMS side deck (see zkr_py plan D8).
$(OUT_DIR)/zkrio.o: zkrio.cpp
@echo 'Building $(OUT_DIR)/zkrio.o'
$(CXX) $(CPP_FLAGS) -o $@ $^

$(OUT_DIR_SWIG)/zkrio.o: $(OUT_DIR_SWIG) zkrio.cpp
@echo 'Building $(OUT_DIR_SWIG)/zkrio.o with SWIG macro'
$(CXX) $(SWIG_FLAGS) -o $@ zkrio.cpp

#
# Jobs
#
Expand Down Expand Up @@ -382,7 +397,7 @@ $(OUT_DIR)/extend/plugin.o: extend/plugin.cpp
#
# Test CLI
#
$(OUT_DIR)/zowex: $(OUT_DIR)/zowex.o $(OUT_DIR)/extend/plugin.o $(COMMAND_OBJS) $(SERVER_OBJS) $(OUT_DIR)/zjsonm.o $(OUT_DIR)/libzut.a $(OUT_DIR)/libzjb.a $(OUT_DIR)/libzds.a $(OUT_DIR)/libzusf.a $(OUT_DIR)/libztso.a $(OUT_DIR)/libzkr.a
$(OUT_DIR)/zowex: $(OUT_DIR)/zowex.o $(OUT_DIR)/extend/plugin.o $(OUT_DIR)/zkrio.o $(COMMAND_OBJS) $(SERVER_OBJS) $(OUT_DIR)/zjsonm.o $(OUT_DIR)/libzut.a $(OUT_DIR)/libzjb.a $(OUT_DIR)/libzds.a $(OUT_DIR)/libzusf.a $(OUT_DIR)/libztso.a $(OUT_DIR)/libzkr.a
@echo 'Building zowex (z/OS target level: $(ZosMinLevel))'
$(CXX) $(CPP_BND_FLAGS) -o $@ $^ $(GSKCMS_SIDEDECK)

Expand Down
17 changes: 12 additions & 5 deletions native/c/zkr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -513,15 +513,13 @@ int zkr_list_ring(ZKR *zkr, const std::string &owner, const std::string &ring,
return result;
}

std::vector<ZKRCertInfo> zkr_filter_certs(const std::vector<ZKRCertInfo> &certs,
const std::string &label,
const std::string &usage,
size_t max_entries, bool *more_available)
void zkr_filter_certs_into(const std::vector<ZKRCertInfo> &certs, const std::string &label,
const std::string &usage, size_t max_entries, bool *more_available,
std::vector<ZKRCertInfo> &out)
{
if (more_available != nullptr)
*more_available = false;

std::vector<ZKRCertInfo> out;
for (std::vector<ZKRCertInfo>::const_iterator it = certs.begin(); it != certs.end(); ++it)
{
// RACDCERT LABEL semantics: exact and case-sensitive, no wildcards.
Expand All @@ -539,6 +537,15 @@ std::vector<ZKRCertInfo> zkr_filter_certs(const std::vector<ZKRCertInfo> &certs,
}
out.push_back(*it);
}
}

std::vector<ZKRCertInfo> zkr_filter_certs(const std::vector<ZKRCertInfo> &certs,
const std::string &label,
const std::string &usage,
size_t max_entries, bool *more_available)
{
std::vector<ZKRCertInfo> out;
zkr_filter_certs_into(certs, label, usage, max_entries, more_available, out);
return out;
}

Expand Down
38 changes: 38 additions & 0 deletions native/c/zkr.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ struct ZKRImportOptions
bool skip_refresh = false; // do not auto-refresh DIGTCERT when the ESM signals it is required
};

// The bindings compile this header EBCDIC and their SWIG wrappers ASCII. libc++ uses a distinct
// inline namespace per char mode (std::__1 vs std::__1_a), so a mangled name is unresolvable
// across that boundary -- everything the bindings call needs C linkage.
#ifdef SWIG
extern "C"
{
#endif

/**
* @brief Create a new key ring (R_datalib NEWRING).
* @return 0 on success; non-zero otherwise (details in zkr->diag)
Expand Down Expand Up @@ -194,6 +202,27 @@ int zkr_list_ring(ZKR *zkr, const std::string &owner, const std::string &ring,
* exact, case-sensitive comparison, no wildcards or generics. usage is
* matched the same way against the PERSONAL/CERTAUTH/OTHER strings
* produced by zkr_list_ring. An empty label/usage means "no filter".
* @param certs matching entries are appended here, in enumeration order
* @param max_entries cap on the number of MATCHING entries returned (0 = all)
* @param more_available if non-null, set to true when the cap cut off further
* matching entries
*/
void zkr_filter_certs_into(const std::vector<ZKRCertInfo> &certs, const std::string &label,
const std::string &usage, size_t max_entries, bool *more_available,
std::vector<ZKRCertInfo> &out);

#ifdef SWIG
}
#endif

/**
* @brief Filter a certificate list the way RACDCERT treats the LABEL keyword:
* exact, case-sensitive comparison, no wildcards or generics. usage is
* matched the same way against the PERSONAL/CERTAUTH/OTHER strings
* produced by zkr_list_ring. An empty label/usage means "no filter".
* Thin by-value wrapper around zkr_filter_certs_into -- kept outside the
* extern "C" block below because C linkage cannot express a std::vector
* return by value; the bindings call zkr_filter_certs_into directly.
* @param max_entries cap on the number of MATCHING entries returned (0 = all)
* @param more_available if non-null, set to true when the cap cut off further
* matching entries
Expand All @@ -205,6 +234,11 @@ std::vector<ZKRCertInfo> zkr_filter_certs(const std::vector<ZKRCertInfo> &certs,
size_t max_entries = 0,
bool *more_available = nullptr);

#ifdef SWIG
extern "C"
{
#endif

/**
* @brief Export a certificate from a key ring.
* format "pem" returns PEM text; format "p12" returns raw PKCS#12 bytes.
Expand Down Expand Up @@ -267,4 +301,8 @@ int zkr_set_default(ZKR *zkr, const std::string &owner, const std::string &ring,
*/
int zkr_alter_cert(ZKR *zkr, const ZKRAlterOptions &opts);

#ifdef SWIG
}
#endif

#endif // ZKR_HPP
Loading
Loading