Skip to content
Merged
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
45 changes: 34 additions & 11 deletions activestate.yaml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -470,21 +471,43 @@ 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)"
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
Comment thread
icanhasmath marked this conversation as resolved.
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
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 ^<org-name^> [^<key^>]
echo Error: missing organization name
exit /b 1
)
set "key=%~2"

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.
if "%key%"=="" (
echo Using random encryption key ^(see below^)
%PY% scripts\orgkeyserver.py --org "%org%"
) else (
Comment thread
Copilot marked this conversation as resolved.
%PY% scripts\orgkeyserver.py --org "%org%" --key "%key%"
)

events:
- name: activate
Expand Down
88 changes: 76 additions & 12 deletions scripts/orgkeyserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,19 @@
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"
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):
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 /path/to/cert.pem
state config set privateingredient.key_service_ca /path/to/test/ssl/cert.pem
# Optional bearer auth (start the server with --token <token>):
state config set privateingredient.bearer_token_env ORGKEY_TOKEN
export ORGKEY_TOKEN=<token>
Expand All @@ -31,13 +29,78 @@
import base64
import hashlib
import json
import os
import secrets
import ssl
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer

KEY_SIZE = 32 # AES-256
ENDPOINT = "/v1/org-key"
CERT_DIR = "test/ssl"


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 (bundled in the project runtime) so no
external openssl binary is required.
"""
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_dir = os.path.abspath(cert_dir)
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")])
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())
)

if cert_dir:
os.makedirs(cert_dir, exist_ok=True)
with open(key_path, "wb") as f:
f.write(key.private_bytes(
serialization.Encoding.PEM,
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))

return cert_path, key_path


def build_contract(org, key_id, raw_key):
Expand Down Expand Up @@ -81,8 +144,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("--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")
Expand All @@ -98,9 +159,12 @@ def main():
raw_key = secrets.token_bytes(KEY_SIZE)
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
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)
Expand Down
Loading