From 5c40b3a9ddafc7870a7986be8d90ba2a284b1cb6 Mon Sep 17 00:00:00 2001 From: icanhasmath Date: Mon, 13 Jul 2026 14:10:14 -0500 Subject: [PATCH 1/6] Make serve-org-keys work in cmd shell as well as bash Add a batch twin of serve-org-keys constrained to cmd, and constrain the existing bash version to non-cmd shells, so the script resolves per shell the same way install-deps-dev does. Co-Authored-By: Claude Opus 4.8 (1M context) --- activestate.yaml | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/activestate.yaml b/activestate.yaml index 731439730c..e39638c4a1 100644 --- a/activestate.yaml +++ b/activestate.yaml @@ -461,6 +461,7 @@ scripts: value: go run $project.path()/scripts/to-buildexpression/main.go $@ - name: serve-org-keys language: bash + if: ne .Shell "cmd" description: Runs a server that serves org keys for testing encrypted private ingredients value: | org="$1" @@ -470,14 +471,14 @@ scripts: exit 1 fi key="$2" - + echo "Generating HTTPS certificate." if [ ! -d test/ssl ]; then mkdir test/ssl; fi openssl req -x509 -newkey rsa:2048 -nodes -days 365 \ -keyout test/ssl/key.pem -out test/ssl/cert.pem \ -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 2> /dev/null - + echo "Starting org key server." if [ -z "$key" ]; then echo "Using random encryption key (see below)" @@ -485,6 +486,33 @@ scripts: else python3 -- scripts/orgkeyserver.py --tls-cert test/ssl/cert.pem --tls-key test/ssl/key.pem --org "$org" --key "$key" fi + - name: serve-org-keys + language: batch + if: eq .Shell "cmd" + description: Runs a server that serves org keys for testing encrypted private ingredients + value: | + set "org=%~1" + if "%org%"=="" ( + echo Usage: state run serve-org-keys ^ [^] + echo Error: missing organization name + exit /b 1 + ) + set "key=%~2" + + echo Generating HTTPS certificate. + if not exist test\ssl mkdir test\ssl + openssl req -x509 -newkey rsa:2048 -nodes -days 365 ^ + -keyout test\ssl\key.pem -out test\ssl\cert.pem ^ + -subj "/CN=localhost" ^ + -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 2> nul + + echo Starting org key server. + if "%key%"=="" ( + echo Using random encryption key ^(see below^) + python3 -- scripts\orgkeyserver.py --tls-cert test\ssl\cert.pem --tls-key test\ssl\key.pem --org %org% + ) else ( + python3 -- scripts\orgkeyserver.py --tls-cert test\ssl\cert.pem --tls-key test\ssl\key.pem --org "%org%" --key "%key%" + ) events: - name: activate From 91c58da8e54c6ddde7e580a3a92f3786c7609b67 Mon Sep 17 00:00:00 2001 From: icanhasmath Date: Mon, 13 Jul 2026 15:56:28 -0500 Subject: [PATCH 2/6] serve-org-keys: run in cmd without openssl or bash Add --gen-cert to orgkeyserver.py so it generates its own self-signed cert/key via the cryptography package, removing the openssl dependency that on Windows only comes from git-bash. Update the cmd/batch variant to detect the Python interpreter (python, py -3, python3) and pass --gen-cert instead of shelling out to openssl. Co-Authored-By: Claude Opus 4.8 (1M context) --- activestate.yaml | 20 ++++++----- scripts/orgkeyserver.py | 74 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/activestate.yaml b/activestate.yaml index e39638c4a1..5325b8ef7c 100644 --- a/activestate.yaml +++ b/activestate.yaml @@ -499,19 +499,21 @@ scripts: ) set "key=%~2" - echo Generating HTTPS certificate. - if not exist test\ssl mkdir test\ssl - openssl req -x509 -newkey rsa:2048 -nodes -days 365 ^ - -keyout test\ssl\key.pem -out test\ssl\cert.pem ^ - -subj "/CN=localhost" ^ - -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 2> nul + set "PY=" + where python >nul 2>nul && set "PY=python" + if not defined PY (where py >nul 2>nul && set "PY=py -3") + if not defined PY (where python3 >nul 2>nul && set "PY=python3") + if not defined PY ( + echo Error: no Python interpreter found on PATH ^(tried python, py, python3^). + exit /b 1 + ) - echo Starting org key server. + echo Generating HTTPS certificate and starting org key server. if "%key%"=="" ( echo Using random encryption key ^(see below^) - python3 -- scripts\orgkeyserver.py --tls-cert test\ssl\cert.pem --tls-key test\ssl\key.pem --org %org% + %PY% scripts\orgkeyserver.py --gen-cert --tls-cert test\ssl\cert.pem --tls-key test\ssl\key.pem --org %org% ) else ( - python3 -- scripts\orgkeyserver.py --tls-cert test\ssl\cert.pem --tls-key test\ssl\key.pem --org "%org%" --key "%key%" + %PY% scripts\orgkeyserver.py --gen-cert --tls-cert test\ssl\cert.pem --tls-key test\ssl\key.pem --org "%org%" --key "%key%" ) events: diff --git a/scripts/orgkeyserver.py b/scripts/orgkeyserver.py index a25cdc6ab1..32e6eafd77 100644 --- a/scripts/orgkeyserver.py +++ b/scripts/orgkeyserver.py @@ -11,6 +11,11 @@ -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" +Or let this script generate one for you (needs the 'cryptography' package, no +openssl binary required -- useful on native Windows / cmd): + + python3 scripts/orgkeyserver.py --gen-cert --tls-cert cert.pem --tls-key key.pem + Run: python3 scripts/orgkeyserver.py --tls-cert cert.pem --tls-key key.pem @@ -31,6 +36,7 @@ import base64 import hashlib import json +import os import secrets import ssl import sys @@ -40,6 +46,68 @@ ENDPOINT = "/v1/org-key" +def generate_self_signed(cert_path, key_path, host): + """Write a self-signed cert/key pair to the given paths. + + Uses the 'cryptography' package so no external openssl binary is required + (openssl is frequently unavailable on native Windows / cmd environments). + """ + try: + import datetime + import ipaddress + + from cryptography import x509 + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import rsa + from cryptography.x509.oid import NameOID + except ImportError: + raise SystemExit( + "--gen-cert requires the 'cryptography' package (pip install " + "cryptography), or supply --tls-cert/--tls-key generated with openssl." + ) + + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "localhost")]) + dns_names = ["localhost"] + ip_addrs = ["127.0.0.1"] + try: + ipaddress.ip_address(host) + if host not in ip_addrs: + ip_addrs.append(host) + except ValueError: + if host not in dns_names: + dns_names.append(host) + alt_names = [x509.DNSName(n) for n in dns_names] + alt_names += [x509.IPAddress(ipaddress.ip_address(ip)) for ip in ip_addrs] + now = datetime.datetime.utcnow() + cert = ( + x509.CertificateBuilder() + .subject_name(name) + .issuer_name(name) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(now - datetime.timedelta(minutes=1)) + .not_valid_after(now + datetime.timedelta(days=365)) + .add_extension(x509.SubjectAlternativeName(alt_names), critical=False) + .sign(key, hashes.SHA256()) + ) + + cert_dir = os.path.dirname(cert_path) + if cert_dir: + os.makedirs(cert_dir, exist_ok=True) + key_dir = os.path.dirname(key_path) + if key_dir: + os.makedirs(key_dir, exist_ok=True) + with open(key_path, "wb") as f: + f.write(key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.TraditionalOpenSSL, + serialization.NoEncryption(), + )) + with open(cert_path, "wb") as f: + f.write(cert.public_bytes(serialization.Encoding.PEM)) + + def build_contract(org, key_id, raw_key): return { "schema": "activestate.pim.orgkey/v1", @@ -83,6 +151,9 @@ def main(): p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--tls-cert", required=True, help="server TLS certificate (PEM)") p.add_argument("--tls-key", required=True, help="server TLS private key (PEM)") + p.add_argument("--gen-cert", action="store_true", + help="generate a self-signed cert/key at --tls-cert/--tls-key " + "before serving (no openssl binary required)") p.add_argument("--org", default="ActiveState-CLI-Testing", help="organization the key belongs to; must match the project owner") p.add_argument("--key", help="base64-encoded 32-byte AES key; generated and printed if omitted") @@ -98,6 +169,9 @@ def main(): raw_key = secrets.token_bytes(KEY_SIZE) print("--key", base64.standard_b64encode(raw_key).decode("ascii"), file=sys.stderr) + if args.gen_cert: + generate_self_signed(args.tls_cert, args.tls_key, args.host) + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ctx.minimum_version = ssl.TLSVersion.TLSv1_2 ctx.load_cert_chain(certfile=args.tls_cert, keyfile=args.tls_key) From 1103c8c1b27d88f51536ef7735d4cdfcaadf4218 Mon Sep 17 00:00:00 2001 From: icanhasmath Date: Tue, 14 Jul 2026 11:14:25 -0500 Subject: [PATCH 3/6] serve-org-keys: address review comments - Quote --org in the no-key branches (bash and batch) so org names with spaces or shell metacharacters are passed as data, matching the --key branches. - chmod the generated TLS private key to 0600 so it isn't world-readable. Co-Authored-By: Claude Opus 4.8 (1M context) --- activestate.yaml | 4 ++-- scripts/orgkeyserver.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/activestate.yaml b/activestate.yaml index 5325b8ef7c..b563fd9018 100644 --- a/activestate.yaml +++ b/activestate.yaml @@ -482,7 +482,7 @@ scripts: echo "Starting org key server." if [ -z "$key" ]; then echo "Using random encryption key (see below)" - python3 -- scripts/orgkeyserver.py --tls-cert test/ssl/cert.pem --tls-key test/ssl/key.pem --org $org + python3 -- scripts/orgkeyserver.py --tls-cert test/ssl/cert.pem --tls-key test/ssl/key.pem --org "$org" else python3 -- scripts/orgkeyserver.py --tls-cert test/ssl/cert.pem --tls-key test/ssl/key.pem --org "$org" --key "$key" fi @@ -511,7 +511,7 @@ scripts: echo Generating HTTPS certificate and starting org key server. if "%key%"=="" ( echo Using random encryption key ^(see below^) - %PY% scripts\orgkeyserver.py --gen-cert --tls-cert test\ssl\cert.pem --tls-key test\ssl\key.pem --org %org% + %PY% scripts\orgkeyserver.py --gen-cert --tls-cert test\ssl\cert.pem --tls-key test\ssl\key.pem --org "%org%" ) else ( %PY% scripts\orgkeyserver.py --gen-cert --tls-cert test\ssl\cert.pem --tls-key test\ssl\key.pem --org "%org%" --key "%key%" ) diff --git a/scripts/orgkeyserver.py b/scripts/orgkeyserver.py index 32e6eafd77..45512de4fc 100644 --- a/scripts/orgkeyserver.py +++ b/scripts/orgkeyserver.py @@ -104,6 +104,11 @@ def generate_self_signed(cert_path, key_path, host): serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption(), )) + # Restrict the private key to owner-only; no-op-ish on Windows. + try: + os.chmod(key_path, 0o600) + except OSError: + pass with open(cert_path, "wb") as f: f.write(cert.public_bytes(serialization.Encoding.PEM)) From dcc45c26f8c7424204a4499abd90a14363861d9a Mon Sep 17 00:00:00 2001 From: icanhasmath Date: Tue, 14 Jul 2026 11:44:04 -0500 Subject: [PATCH 4/6] serve-org-keys: always generate cert in Python (address review) Per review: drop openssl entirely and have orgkeyserver.py always generate its self-signed cert. - Add CERT_DIR="test/ssl" constant; generate_self_signed(host) writes cert.pem/key.pem there and returns their paths. - Remove --tls-cert/--tls-key/--gen-cert; cert generation is now the default on every run. - Assume cryptography is present (bundled in the runtime); drop the ImportError guard. - Simplify both activestate.yaml variants to just run the script with --org (and optional --key); remove the openssl/mkdir prep. Co-Authored-By: Claude Opus 4.8 (1M context) --- activestate.yaml | 17 ++++------- scripts/orgkeyserver.py | 66 +++++++++++++++-------------------------- 2 files changed, 29 insertions(+), 54 deletions(-) diff --git a/activestate.yaml b/activestate.yaml index b563fd9018..011e708150 100644 --- a/activestate.yaml +++ b/activestate.yaml @@ -472,19 +472,12 @@ scripts: fi key="$2" - echo "Generating HTTPS certificate." - if [ ! -d test/ssl ]; then mkdir test/ssl; fi - openssl req -x509 -newkey rsa:2048 -nodes -days 365 \ - -keyout test/ssl/key.pem -out test/ssl/cert.pem \ - -subj "/CN=localhost" \ - -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 2> /dev/null - echo "Starting org key server." if [ -z "$key" ]; then echo "Using random encryption key (see below)" - python3 -- scripts/orgkeyserver.py --tls-cert test/ssl/cert.pem --tls-key test/ssl/key.pem --org "$org" + python3 -- scripts/orgkeyserver.py --org "$org" else - python3 -- scripts/orgkeyserver.py --tls-cert test/ssl/cert.pem --tls-key test/ssl/key.pem --org "$org" --key "$key" + python3 -- scripts/orgkeyserver.py --org "$org" --key "$key" fi - name: serve-org-keys language: batch @@ -508,12 +501,12 @@ scripts: exit /b 1 ) - echo Generating HTTPS certificate and starting org key server. + echo Starting org key server. if "%key%"=="" ( echo Using random encryption key ^(see below^) - %PY% scripts\orgkeyserver.py --gen-cert --tls-cert test\ssl\cert.pem --tls-key test\ssl\key.pem --org "%org%" + %PY% scripts\orgkeyserver.py --org "%org%" ) else ( - %PY% scripts\orgkeyserver.py --gen-cert --tls-cert test\ssl\cert.pem --tls-key test\ssl\key.pem --org "%org%" --key "%key%" + %PY% scripts\orgkeyserver.py --org "%org%" --key "%key%" ) events: diff --git a/scripts/orgkeyserver.py b/scripts/orgkeyserver.py index 45512de4fc..788239edd8 100644 --- a/scripts/orgkeyserver.py +++ b/scripts/orgkeyserver.py @@ -4,26 +4,18 @@ Serves the organization encryption key over HTTPS at GET /v1/org-key in the contract the State Tool expects. For local testing only; not a shipped artifact. -Generate a self-signed certificate (the SAN must cover the host you connect to): - - openssl req -x509 -newkey rsa:2048 -nodes -days 365 \ - -keyout key.pem -out cert.pem \ - -subj "/CN=localhost" \ - -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" - -Or let this script generate one for you (needs the 'cryptography' package, no -openssl binary required -- useful on native Windows / cmd): - - python3 scripts/orgkeyserver.py --gen-cert --tls-cert cert.pem --tls-key key.pem +A self-signed certificate/key pair (SAN covering localhost + 127.0.0.1) is +generated automatically into test/ssl/ on startup, so no openssl binary is +needed. Run: - python3 scripts/orgkeyserver.py --tls-cert cert.pem --tls-key key.pem + python3 scripts/orgkeyserver.py Point the State Tool at it (the base URL only; the tool appends /v1/org-key): state config set privateingredient.key_service_url https://127.0.0.1:8443 - state config set privateingredient.key_service_ca /path/to/cert.pem + state config set privateingredient.key_service_ca test/ssl/cert.pem # Optional bearer auth (start the server with --token ): state config set privateingredient.bearer_token_env ORGKEY_TOKEN export ORGKEY_TOKEN= @@ -44,27 +36,25 @@ KEY_SIZE = 32 # AES-256 ENDPOINT = "/v1/org-key" +CERT_DIR = "test/ssl" -def generate_self_signed(cert_path, key_path, host): - """Write a self-signed cert/key pair to the given paths. +def generate_self_signed(host, cert_dir=CERT_DIR): + """Write a self-signed cert/key pair into cert_dir; return their paths. - Uses the 'cryptography' package so no external openssl binary is required - (openssl is frequently unavailable on native Windows / cmd environments). + Uses the 'cryptography' package (bundled in the project runtime) so no + external openssl binary is required. """ - try: - import datetime - import ipaddress - - from cryptography import x509 - from cryptography.hazmat.primitives import hashes, serialization - from cryptography.hazmat.primitives.asymmetric import rsa - from cryptography.x509.oid import NameOID - except ImportError: - raise SystemExit( - "--gen-cert requires the 'cryptography' package (pip install " - "cryptography), or supply --tls-cert/--tls-key generated with openssl." - ) + import datetime + import ipaddress + + from cryptography import x509 + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import rsa + from cryptography.x509.oid import NameOID + + cert_path = os.path.join(cert_dir, "cert.pem") + key_path = os.path.join(cert_dir, "key.pem") key = rsa.generate_private_key(public_exponent=65537, key_size=2048) name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "localhost")]) @@ -92,12 +82,8 @@ def generate_self_signed(cert_path, key_path, host): .sign(key, hashes.SHA256()) ) - cert_dir = os.path.dirname(cert_path) if cert_dir: os.makedirs(cert_dir, exist_ok=True) - key_dir = os.path.dirname(key_path) - if key_dir: - os.makedirs(key_dir, exist_ok=True) with open(key_path, "wb") as f: f.write(key.private_bytes( serialization.Encoding.PEM, @@ -112,6 +98,8 @@ def generate_self_signed(cert_path, key_path, host): with open(cert_path, "wb") as f: f.write(cert.public_bytes(serialization.Encoding.PEM)) + return cert_path, key_path + def build_contract(org, key_id, raw_key): return { @@ -154,11 +142,6 @@ def parse_key(encoded): def main(): p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) - p.add_argument("--tls-cert", required=True, help="server TLS certificate (PEM)") - p.add_argument("--tls-key", required=True, help="server TLS private key (PEM)") - p.add_argument("--gen-cert", action="store_true", - help="generate a self-signed cert/key at --tls-cert/--tls-key " - "before serving (no openssl binary required)") p.add_argument("--org", default="ActiveState-CLI-Testing", help="organization the key belongs to; must match the project owner") p.add_argument("--key", help="base64-encoded 32-byte AES key; generated and printed if omitted") @@ -174,12 +157,11 @@ def main(): raw_key = secrets.token_bytes(KEY_SIZE) print("--key", base64.standard_b64encode(raw_key).decode("ascii"), file=sys.stderr) - if args.gen_cert: - generate_self_signed(args.tls_cert, args.tls_key, args.host) + cert_path, key_path = generate_self_signed(args.host) ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ctx.minimum_version = ssl.TLSVersion.TLSv1_2 - ctx.load_cert_chain(certfile=args.tls_cert, keyfile=args.tls_key) + ctx.load_cert_chain(certfile=cert_path, keyfile=key_path) handler = make_handler(build_contract(args.org, args.key_id, raw_key), args.token) httpd = HTTPServer((args.host, args.port), handler) From f10fc89b05fc353a09c3dc37c613ec065a189fc7 Mon Sep 17 00:00:00 2001 From: icanhasmath Date: Tue, 14 Jul 2026 15:36:41 -0500 Subject: [PATCH 5/6] serve-org-keys: use absolute cert paths (address review) Per review: resolve the cert dir to an absolute path in generate_self_signed(), and print the absolute key_service_ca path on startup so the value is copy-pasteable regardless of cwd. Docstring example updated to an absolute /path/to/test/ssl/cert.pem placeholder. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/orgkeyserver.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/orgkeyserver.py b/scripts/orgkeyserver.py index 788239edd8..b57c9d4b38 100644 --- a/scripts/orgkeyserver.py +++ b/scripts/orgkeyserver.py @@ -12,10 +12,11 @@ python3 scripts/orgkeyserver.py -Point the State Tool at it (the base URL only; the tool appends /v1/org-key): +Point the State Tool at it (the base URL only; the tool appends /v1/org-key). +Use the absolute cert path the server prints on startup, e.g.: state config set privateingredient.key_service_url https://127.0.0.1:8443 - state config set privateingredient.key_service_ca test/ssl/cert.pem + state config set privateingredient.key_service_ca /path/to/test/ssl/cert.pem # Optional bearer auth (start the server with --token ): state config set privateingredient.bearer_token_env ORGKEY_TOKEN export ORGKEY_TOKEN= @@ -53,6 +54,7 @@ def generate_self_signed(host, cert_dir=CERT_DIR): from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID + cert_dir = os.path.abspath(cert_dir) cert_path = os.path.join(cert_dir, "cert.pem") key_path = os.path.join(cert_dir, "key.pem") @@ -158,6 +160,7 @@ def main(): print("--key", base64.standard_b64encode(raw_key).decode("ascii"), file=sys.stderr) cert_path, key_path = generate_self_signed(args.host) + print("key_service_ca", cert_path, file=sys.stderr) ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ctx.minimum_version = ssl.TLSVersion.TLSv1_2 From 853fcb1e7fb89b623a96cb174b47113df4a45b52 Mon Sep 17 00:00:00 2001 From: icanhasmath Date: Tue, 14 Jul 2026 15:44:44 -0500 Subject: [PATCH 6/6] Bump project runtime commitID Update to a runtime that bundles cryptography, which serve-org-keys' orgkeyserver.py relies on for cert generation. Co-Authored-By: Claude Opus 4.8 (1M context) --- activestate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activestate.yaml b/activestate.yaml index 011e708150..369cc03bc5 100644 --- a/activestate.yaml +++ b/activestate.yaml @@ -1,4 +1,4 @@ -project: https://platform.activestate.com/ActiveState/cli?branch=main&commitID=9eee7512-b2ab-4600-b78b-ab0cf2e817d8 +project: https://platform.activestate.com/ActiveState/cli?branch=main&commitID=ce30761b-3c98-4d7a-b737-bcb1beeef87c constants: - name: CLI_BUILDFLAGS value: -ldflags="-s -w"