From 93457ab2d29662938009a7cd71ab96e146aefe2d Mon Sep 17 00:00:00 2001 From: Paul Flynn Date: Fri, 13 Feb 2026 09:37:57 -0500 Subject: [PATCH 1/4] feat(xtest): add exp-go-sdk using experimental TDF writer Add a new SDK target that wraps the platform's experimental streaming TDF writer for encrypt and the standard SDK for decrypt. This validates interoperability of TDFs produced by the experimental writer with all existing SDKs. New files: - xtest/sdk/exp-go-sdk/main.go: Go CLI with encrypt/decrypt/supports - xtest/sdk/exp-go-sdk/cli.sh: shell wrapper for xtest integration - xtest/sdk/exp-go-sdk/Makefile: build rules - xtest/sdk/exp-go-sdk/go.mod: module with platform SDK dependencies Modified files: - xtest/sdk/Makefile: add exp-go-sdk build target - xtest/tdfs.py: register exp-go-sdk as sdk_type with feature flags - .github/workflows/xtest.yml: add to CI matrix with go.mod replace Co-Authored-By: Claude Opus 4.6 --- .github/workflows/xtest.yml | 25 +- xtest/sdk/Makefile | 9 +- xtest/sdk/exp-go-sdk/Makefile | 15 + xtest/sdk/exp-go-sdk/cli.sh | 93 ++++++ xtest/sdk/exp-go-sdk/go.mod | 42 +++ xtest/sdk/exp-go-sdk/go.sum | 186 +++++++++++ xtest/sdk/exp-go-sdk/main.go | 585 ++++++++++++++++++++++++++++++++++ xtest/tdfs.py | 6 +- 8 files changed, 952 insertions(+), 9 deletions(-) create mode 100644 xtest/sdk/exp-go-sdk/Makefile create mode 100755 xtest/sdk/exp-go-sdk/cli.sh create mode 100644 xtest/sdk/exp-go-sdk/go.mod create mode 100644 xtest/sdk/exp-go-sdk/go.sum create mode 100644 xtest/sdk/exp-go-sdk/main.go diff --git a/.github/workflows/xtest.yml b/.github/workflows/xtest.yml index 65b541f2..f6ead08a 100644 --- a/.github/workflows/xtest.yml +++ b/.github/workflows/xtest.yml @@ -27,7 +27,7 @@ on: required: false type: string default: all - description: "The SDK to focus on (go, js, java, all)" + description: "The SDK to focus on (go, js, java, exp-go-sdk, all)" workflow_call: inputs: platform-ref: @@ -84,8 +84,8 @@ jobs: env: FOCUS_SDK_INPUT: ${{ inputs.focus-sdk }} run: |- - if [[ ! "all go java js" =~ (^|[[:space:]])${FOCUS_SDK_INPUT}($|[[:space:]]) ]]; then - echo "Invalid focus-sdk input: ${FOCUS_SDK_INPUT}. Must be one of: all, go, java, js." >> "$GITHUB_STEP_SUMMARY" + if [[ ! "all go java js exp-go-sdk" =~ (^|[[:space:]])${FOCUS_SDK_INPUT}($|[[:space:]]) ]]; then + echo "Invalid focus-sdk input: ${FOCUS_SDK_INPUT}. Must be one of: all, go, java, js, exp-go-sdk." >> "$GITHUB_STEP_SUMMARY" exit 1 fi - name: Default Versions depend on context @@ -224,7 +224,7 @@ jobs: fail-fast: false matrix: platform-tag: ${{ fromJSON(needs.resolve-versions.outputs.platform-tag-list) }} - sdk: ["go", "java", "js"] + sdk: ["go", "java", "js", "exp-go-sdk"] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: @@ -341,6 +341,23 @@ jobs: make working-directory: otdftests/xtest/sdk/go + ######## SETUP EXP-GO-SDK ############# + - name: Replace exp-go-sdk go.mod packages + if: contains(fromJSON(needs.resolve-versions.outputs.heads), matrix.platform-tag) + env: + PLATFORM_WORKING_DIR: ${{ steps.run-platform.outputs.platform-working-dir }} + run: |- + PLATFORM_DIR_ABS="$(pwd)/${PLATFORM_WORKING_DIR}" + cd otdftests/xtest/sdk/exp-go-sdk + for m in lib/fixtures lib/ocrypto protocol/go sdk; do + go mod edit -replace "github.com/opentdf/platform/$m=${PLATFORM_DIR_ABS}/$m" + done + go mod tidy + + - name: Build exp-go-sdk + run: make + working-directory: otdftests/xtest/sdk/exp-go-sdk + ####### CHECKOUT JAVA SDK ############## - name: Configure java-sdk diff --git a/xtest/sdk/Makefile b/xtest/sdk/Makefile index b2d21b70..015453d2 100644 --- a/xtest/sdk/Makefile +++ b/xtest/sdk/Makefile @@ -1,9 +1,9 @@ # Makefile # Targets -.PHONY: all js go java +.PHONY: all js go java exp-go-sdk -all: js go java +all: js go java exp-go-sdk @echo "Setup all sdk clis" js: @@ -20,3 +20,8 @@ java: @echo "Building Java SDK..." @cd java && make all @echo "Java SDK built successfully" + +exp-go-sdk: + @echo "Building Experimental Go SDK..." + @cd exp-go-sdk && make all + @echo "Experimental Go SDK built successfully" diff --git a/xtest/sdk/exp-go-sdk/Makefile b/xtest/sdk/exp-go-sdk/Makefile new file mode 100644 index 00000000..f9e5c46f --- /dev/null +++ b/xtest/sdk/exp-go-sdk/Makefile @@ -0,0 +1,15 @@ +.PHONY: all clean + +all: dist/main/cli.sh dist/main/exp-go-sdk + +dist/main/exp-go-sdk: main.go go.mod go.sum + mkdir -p dist/main + go build -o dist/main/exp-go-sdk . + +dist/main/cli.sh: cli.sh + mkdir -p dist/main + cp cli.sh dist/main/cli.sh + chmod +x dist/main/cli.sh + +clean: + rm -rf dist diff --git a/xtest/sdk/exp-go-sdk/cli.sh b/xtest/sdk/exp-go-sdk/cli.sh new file mode 100755 index 00000000..caf745d5 --- /dev/null +++ b/xtest/sdk/exp-go-sdk/cli.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# +# Common shell wrapper used to interface to SDK implementation. +# +# Usage: ./cli.sh +# +# Extended Utilities: +# +# ./cli.sh supports +# Check if the SDK supports a specific feature. +# +# Extended Configuration: +# XT_WITH_ECDSA_BINDING [boolean] - Use ECDSA binding for encryption +# XT_WITH_ECWRAP [boolean] - Use EC wrap for encryption/decryption +# XT_WITH_VERIFY_ASSERTIONS [boolean] - Verify assertions during decryption +# XT_WITH_ASSERTIONS [string] - Path to assertions file, or JSON encoded as string +# XT_WITH_ASSERTION_VERIFICATION_KEYS [string] - Path to assertion verification private key file +# XT_WITH_ATTRIBUTES [string] - Attributes to be used for encryption +# XT_WITH_MIME_TYPE [string] - MIME type for the encrypted file +# +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) + +cmd="$SCRIPT_DIR/exp-go-sdk" +if [ ! -f "$cmd" ]; then + echo "exp-go-sdk binary not found at $cmd" + exit 1 +fi + +if [ "$1" == "supports" ]; then + "$cmd" supports "$2" + exit $? +fi + +XTEST_DIR="$SCRIPT_DIR" +while [ ! -f "$XTEST_DIR/test.env" ] && [ "$(basename "$XTEST_DIR")" != "xtest" ]; do + XTEST_DIR=$(dirname "$XTEST_DIR") +done + +if [ -f "$XTEST_DIR/test.env" ]; then + # shellcheck disable=SC1091 + source "$XTEST_DIR/test.env" +else + echo "test.env not found, stopping at xtest directory." + exit 1 +fi + +if [ "$4" != "ztdf" ]; then + echo "Unsupported container format: $4" + exit 2 +fi + +args=( + --output "$3" + --platform-endpoint "$PLATFORMURL" + --client-id "$CLIENTID" + --client-secret "$CLIENTSECRET" +) + +if [ "$1" == "encrypt" ]; then + if [ -n "$XT_WITH_MIME_TYPE" ]; then + args+=(--mime-type "$XT_WITH_MIME_TYPE") + fi + + if [ -n "$XT_WITH_ATTRIBUTES" ]; then + args+=(--attributes "$XT_WITH_ATTRIBUTES") + fi + + if [ -n "$XT_WITH_ASSERTIONS" ]; then + args+=(--assertions "$XT_WITH_ASSERTIONS") + fi + + echo "$cmd" encrypt "${args[@]}" "$2" + "$cmd" encrypt "${args[@]}" "$2" +elif [ "$1" == "decrypt" ]; then + if [ -n "$XT_WITH_ASSERTION_VERIFICATION_KEYS" ]; then + args+=(--assertion-verification-keys "$XT_WITH_ASSERTION_VERIFICATION_KEYS") + fi + if [ "$XT_WITH_VERIFY_ASSERTIONS" == 'false' ]; then + args+=(--no-verify-assertions) + fi + if [ -n "$XT_WITH_KAS_ALLOW_LIST" ]; then + args+=(--kas-allowlist "$XT_WITH_KAS_ALLOW_LIST") + fi + if [ "$XT_WITH_IGNORE_KAS_ALLOWLIST" == "true" ]; then + args+=(--ignore-kas-allowlist) + fi + + echo "$cmd" decrypt "${args[@]}" "$2" + "$cmd" decrypt "${args[@]}" "$2" +else + echo "Incorrect argument provided" + exit 1 +fi diff --git a/xtest/sdk/exp-go-sdk/go.mod b/xtest/sdk/exp-go-sdk/go.mod new file mode 100644 index 00000000..7a204ab9 --- /dev/null +++ b/xtest/sdk/exp-go-sdk/go.mod @@ -0,0 +1,42 @@ +module github.com/opentdf/tests/xtest/sdk/exp-go-sdk + +go 1.25.0 + +require ( + github.com/opentdf/platform/protocol/go v0.15.0 + github.com/opentdf/platform/sdk v0.4.1 +) + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250603165357-b52ab10f4468.1 // indirect + connectrpc.com/connect v1.19.1 // indirect + github.com/CosmWasm/tinyjson v0.9.0 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gowebpki/jcs v1.0.1 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/lestrrat-go/blackmagic v1.0.4 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.6 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/opentdf/platform/lib/ocrypto v0.9.0 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect +) diff --git a/xtest/sdk/exp-go-sdk/go.sum b/xtest/sdk/exp-go-sdk/go.sum new file mode 100644 index 00000000..ceba89c7 --- /dev/null +++ b/xtest/sdk/exp-go-sdk/go.sum @@ -0,0 +1,186 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250603165357-b52ab10f4468.1 h1:uwSqFkn8DDTzNlaV9TxgSXY5OCaNdb4rH+Axd2FujkE= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250603165357-b52ab10f4468.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= +connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= +connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/CosmWasm/tinyjson v0.9.0 h1:sPjgikATp5W0vD/v/Qz99uQ6G/lh/SuK0Wfskqua4Co= +github.com/CosmWasm/tinyjson v0.9.0/go.mod h1:5+7QnSKrkIWnpIdhUT2t2EYzXnII3/3MlM0oDsBSbc8= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Nerzal/gocloak/v13 v13.9.0 h1:YWsJsdM5b0yhM2Ba3MLydiOlujkBry4TtdzfIzSVZhw= +github.com/Nerzal/gocloak/v13 v13.9.0/go.mod h1:YYuDcXZ7K2zKECyVP7pPqjKxx2AzYSpKDj8d6GuyM10= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gowebpki/jcs v1.0.1 h1:Qjzg8EOkrOTuWP7DqQ1FbYtcpEbeTzUoTN9bptp8FOU= +github.com/gowebpki/jcs v1.0.1/go.mod h1:CID1cNZ+sHp1CCpAR8mPf6QRtagFBgPJE0FCUQ6+BrI= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= +github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= +github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA= +github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= +github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= +github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/xtest/sdk/exp-go-sdk/main.go b/xtest/sdk/exp-go-sdk/main.go new file mode 100644 index 00000000..eba2bf7e --- /dev/null +++ b/xtest/sdk/exp-go-sdk/main.go @@ -0,0 +1,585 @@ +package main + +import ( + "context" + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "os" + "strings" + + "github.com/opentdf/platform/protocol/go/policy" + "github.com/opentdf/platform/protocol/go/policy/attributes" + "github.com/opentdf/platform/sdk" + exptdf "github.com/opentdf/platform/sdk/experimental/tdf" +) + +const segmentSize = 2 * 1024 * 1024 // 2 MiB, matches standard SDK default + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "usage: exp-go-sdk ...") + os.Exit(1) + } + switch os.Args[1] { + case "encrypt": + if err := doEncrypt(os.Args[2:]); err != nil { + fmt.Fprintf(os.Stderr, "encrypt error: %v\n", err) + os.Exit(1) + } + case "decrypt": + if err := doDecrypt(os.Args[2:]); err != nil { + fmt.Fprintf(os.Stderr, "decrypt error: %v\n", err) + os.Exit(1) + } + case "supports": + if len(os.Args) < 3 { + fmt.Fprintln(os.Stderr, "usage: exp-go-sdk supports ") + os.Exit(2) + } + doSupports(os.Args[2]) + default: + fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1]) + os.Exit(1) + } +} + +// ----- supports ----- + +func doSupports(feature string) { + switch feature { + case "assertions", + "assertion_verification", + "autoconfigure", + "ns_grants", + "ecwrap", + "hexless", + "connectrpc", + "kasallowlist", + "better-messages-2024": + os.Exit(0) + default: + // Unsupported: hexaflexible, obligations, key_management, bulk_rewrap + fmt.Fprintf(os.Stderr, "unsupported feature: %s\n", feature) + os.Exit(1) + } +} + +// ----- encrypt ----- + +type encryptFlags struct { + inputFile string + output string + platformEndpoint string + clientID string + clientSecret string + tokenEndpoint string + attributes string + assertions string + mimeType string + wrappingAlgorithm string + policyMode string + targetMode string + tlsNoVerify bool +} + +func parseEncryptFlags(args []string) (*encryptFlags, error) { + f := &encryptFlags{ + mimeType: "application/octet-stream", + } + for i := 0; i < len(args); i++ { + switch args[i] { + case "--output", "-o": + i++ + f.output = args[i] + case "--platform-endpoint": + i++ + f.platformEndpoint = args[i] + case "--client-id": + i++ + f.clientID = args[i] + case "--client-secret": + i++ + f.clientSecret = args[i] + case "--token-endpoint": + i++ + f.tokenEndpoint = args[i] + case "--attributes": + i++ + f.attributes = args[i] + case "--assertions": + i++ + f.assertions = args[i] + case "--mime-type": + i++ + f.mimeType = args[i] + case "--wrapping-algorithm": + i++ + f.wrappingAlgorithm = args[i] + case "--policy-mode": + i++ + f.policyMode = args[i] + case "--target-mode": + i++ + f.targetMode = args[i] + case "--tls-no-verify": + f.tlsNoVerify = true + default: + if f.inputFile == "" && !strings.HasPrefix(args[i], "-") { + f.inputFile = args[i] + } else { + return nil, fmt.Errorf("unknown flag or duplicate input: %s", args[i]) + } + } + } + if f.inputFile == "" { + return nil, fmt.Errorf("input file required") + } + if f.output == "" { + return nil, fmt.Errorf("--output required") + } + return f, nil +} + +func doEncrypt(args []string) error { + f, err := parseEncryptFlags(args) + if err != nil { + return err + } + + ctx := context.Background() + + // Build SDK client + client, err := newSDKClient(f.platformEndpoint, f.clientID, f.clientSecret, f.tokenEndpoint, f.tlsNoVerify) + if err != nil { + return fmt.Errorf("creating SDK client: %w", err) + } + defer client.Close() + + // Get base KAS key + baseKey, err := client.GetBaseKey(ctx) + if err != nil { + return fmt.Errorf("getting base key: %w", err) + } + + // Resolve attributes if specified + var attrValues []*policy.Value + if f.attributes != "" { + attrValues, err = resolveAttributes(ctx, client, f.attributes) + if err != nil { + return fmt.Errorf("resolving attributes: %w", err) + } + } + + // Parse assertions if specified + var assertionConfigs []exptdf.AssertionConfig + if f.assertions != "" { + assertionConfigs, err = parseAssertions(f.assertions) + if err != nil { + return fmt.Errorf("parsing assertions: %w", err) + } + } + + // Create experimental writer + writer, err := exptdf.NewWriter(ctx, + exptdf.WithIntegrityAlgorithm(exptdf.HS256), + exptdf.WithSegmentIntegrityAlgorithm(exptdf.HS256), + ) + if err != nil { + return fmt.Errorf("creating writer: %w", err) + } + + // Read input and write segments + data, err := os.ReadFile(f.inputFile) + if err != nil { + return fmt.Errorf("reading input file: %w", err) + } + + segIdx := 0 + for offset := 0; offset < len(data); offset += segmentSize { + end := offset + segmentSize + if end > len(data) { + end = len(data) + } + if _, err := writer.WriteSegment(ctx, segIdx, data[offset:end]); err != nil { + return fmt.Errorf("writing segment %d: %w", segIdx, err) + } + segIdx++ + } + // Handle empty file: write one empty segment + if len(data) == 0 { + if _, err := writer.WriteSegment(ctx, 0, []byte{}); err != nil { + return fmt.Errorf("writing empty segment: %w", err) + } + } + + // Build finalize options + finalizeOpts := []exptdf.Option[*exptdf.WriterFinalizeConfig]{ + exptdf.WithDefaultKAS(baseKey), + } + if len(attrValues) > 0 { + finalizeOpts = append(finalizeOpts, exptdf.WithAttributeValues(attrValues)) + } + if len(assertionConfigs) > 0 { + finalizeOpts = append(finalizeOpts, exptdf.WithAssertions(assertionConfigs...)) + } + if f.mimeType != "" { + finalizeOpts = append(finalizeOpts, exptdf.WithPayloadMimeType(f.mimeType)) + } + + // Finalize + result, err := writer.Finalize(ctx, finalizeOpts...) + if err != nil { + return fmt.Errorf("finalizing TDF: %w", err) + } + + // Write output + if err := os.WriteFile(f.output, result.Data, 0o644); err != nil { + return fmt.Errorf("writing output: %w", err) + } + + return nil +} + +// ----- decrypt ----- + +type decryptFlags struct { + inputFile string + output string + platformEndpoint string + clientID string + clientSecret string + tokenEndpoint string + wrappingAlgorithm string + assertionVerificationKeys string + noVerifyAssertions bool + kasAllowlist string + tlsNoVerify bool + ignoreKasAllowlist bool +} + +func parseDecryptFlags(args []string) (*decryptFlags, error) { + f := &decryptFlags{} + for i := 0; i < len(args); i++ { + switch args[i] { + case "--output", "-o": + i++ + f.output = args[i] + case "--platform-endpoint": + i++ + f.platformEndpoint = args[i] + case "--client-id": + i++ + f.clientID = args[i] + case "--client-secret": + i++ + f.clientSecret = args[i] + case "--token-endpoint": + i++ + f.tokenEndpoint = args[i] + case "--wrapping-algorithm": + i++ + f.wrappingAlgorithm = args[i] + case "--assertion-verification-keys": + i++ + f.assertionVerificationKeys = args[i] + case "--no-verify-assertions": + f.noVerifyAssertions = true + case "--kas-allowlist": + i++ + f.kasAllowlist = args[i] + case "--tls-no-verify": + f.tlsNoVerify = true + case "--ignore-kas-allowlist": + f.ignoreKasAllowlist = true + default: + if f.inputFile == "" && !strings.HasPrefix(args[i], "-") { + f.inputFile = args[i] + } else { + return nil, fmt.Errorf("unknown flag or duplicate input: %s", args[i]) + } + } + } + if f.inputFile == "" { + return nil, fmt.Errorf("input file required") + } + if f.output == "" { + return nil, fmt.Errorf("--output required") + } + return f, nil +} + +func doDecrypt(args []string) error { + f, err := parseDecryptFlags(args) + if err != nil { + return err + } + + // Build SDK client + client, err := newSDKClient(f.platformEndpoint, f.clientID, f.clientSecret, f.tokenEndpoint, f.tlsNoVerify) + if err != nil { + return fmt.Errorf("creating SDK client: %w", err) + } + defer client.Close() + + // Open input file + inFile, err := os.Open(f.inputFile) + if err != nil { + return fmt.Errorf("opening input: %w", err) + } + defer inFile.Close() + + // Build reader options + var readerOpts []sdk.TDFReaderOption + if f.noVerifyAssertions { + readerOpts = append(readerOpts, sdk.WithDisableAssertionVerification(true)) + } + if f.assertionVerificationKeys != "" { + keys, err := loadAssertionVerificationKeys(f.assertionVerificationKeys) + if err != nil { + return fmt.Errorf("loading assertion verification keys: %w", err) + } + readerOpts = append(readerOpts, sdk.WithAssertionVerificationKeys(keys)) + } + if f.kasAllowlist != "" { + list := strings.Split(f.kasAllowlist, ",") + readerOpts = append(readerOpts, sdk.WithKasAllowlist(list)) + } else if f.ignoreKasAllowlist { + readerOpts = append(readerOpts, sdk.WithKasAllowlist([]string{"*"})) + } + + // Load and decrypt TDF + reader, err := client.LoadTDF(inFile, readerOpts...) + if err != nil { + return fmt.Errorf("loading TDF: %w", err) + } + + // Write decrypted output + outFile, err := os.Create(f.output) + if err != nil { + return fmt.Errorf("creating output: %w", err) + } + defer outFile.Close() + + if _, err := io.Copy(outFile, reader); err != nil { + return fmt.Errorf("writing decrypted data: %w", err) + } + + return nil +} + +// ----- helpers ----- + +func newSDKClient(endpoint, clientID, clientSecret, tokenEndpoint string, tlsNoVerify bool) (*sdk.SDK, error) { + opts := []sdk.Option{ + sdk.WithClientCredentials(clientID, clientSecret, nil), + } + if tokenEndpoint != "" { + opts = append(opts, sdk.WithTokenEndpoint(tokenEndpoint)) + } + if tlsNoVerify { + opts = append(opts, sdk.WithInsecureSkipVerifyConn()) + } + if strings.HasPrefix(endpoint, "http://") { + opts = append(opts, sdk.WithInsecurePlaintextConn()) + } + return sdk.New(endpoint, opts...) +} + +func resolveAttributes(ctx context.Context, client *sdk.SDK, attrStr string) ([]*policy.Value, error) { + fqns := strings.Split(attrStr, ",") + for i := range fqns { + fqns[i] = strings.TrimSpace(fqns[i]) + } + + resp, err := client.Attributes.GetAttributeValuesByFqns(ctx, &attributes.GetAttributeValuesByFqnsRequest{ + Fqns: fqns, + }) + if err != nil { + return nil, fmt.Errorf("GetAttributeValuesByFqns: %w", err) + } + + var values []*policy.Value + for _, fqn := range fqns { + av, ok := resp.GetFqnAttributeValues()[fqn] + if !ok { + return nil, fmt.Errorf("attribute not found in response: %s", fqn) + } + v := av.GetValue() + if v == nil { + return nil, fmt.Errorf("no value for attribute: %s", fqn) + } + values = append(values, v) + } + return values, nil +} + +// assertionJSON matches the JSON format used by xtest assertion fixtures. +type assertionJSON struct { + ID string `json:"id"` + Type string `json:"type"` + Scope string `json:"scope"` + AppliesToState string `json:"appliesToState"` + Statement struct { + Format string `json:"format"` + Schema string `json:"schema"` + Value string `json:"value"` + } `json:"statement"` + SigningKey *struct { + Alg string `json:"alg"` + Key string `json:"key"` + } `json:"signingKey,omitempty"` +} + +func parseAssertions(input string) ([]exptdf.AssertionConfig, error) { + var raw []byte + // Check if input is a file path + if _, err := os.Stat(input); err == nil { + raw, err = os.ReadFile(input) + if err != nil { + return nil, fmt.Errorf("reading assertions file: %w", err) + } + } else { + raw = []byte(input) + } + + var ajList []assertionJSON + if err := json.Unmarshal(raw, &ajList); err != nil { + return nil, fmt.Errorf("unmarshaling assertions JSON: %w", err) + } + + var configs []exptdf.AssertionConfig + for _, aj := range ajList { + cfg := exptdf.AssertionConfig{ + ID: aj.ID, + Type: exptdf.AssertionType(aj.Type), + Scope: exptdf.Scope(aj.Scope), + AppliesToState: exptdf.AppliesToState(aj.AppliesToState), + Statement: exptdf.Statement{ + Format: aj.Statement.Format, + Schema: aj.Statement.Schema, + Value: aj.Statement.Value, + }, + } + if aj.SigningKey != nil && aj.SigningKey.Key != "" { + key, err := loadSigningKey(aj.SigningKey.Alg, aj.SigningKey.Key) + if err != nil { + return nil, fmt.Errorf("loading signing key for assertion %s: %w", aj.ID, err) + } + cfg.SigningKey = key + } + configs = append(configs, cfg) + } + return configs, nil +} + +func loadSigningKey(alg, keyData string) (exptdf.AssertionKey, error) { + switch exptdf.AssertionKeyAlg(alg) { + case exptdf.AssertionKeyAlgRS256: + key, err := parseRSAPrivateKey(keyData) + if err != nil { + return exptdf.AssertionKey{}, err + } + return exptdf.AssertionKey{Alg: exptdf.AssertionKeyAlgRS256, Key: key}, nil + case exptdf.AssertionKeyAlgHS256: + return exptdf.AssertionKey{Alg: exptdf.AssertionKeyAlgHS256, Key: []byte(keyData)}, nil + default: + return exptdf.AssertionKey{}, fmt.Errorf("unsupported signing algorithm: %s", alg) + } +} + +func parseRSAPrivateKey(keyData string) (*rsa.PrivateKey, error) { + // Try as file path first + if data, err := os.ReadFile(keyData); err == nil { + keyData = string(data) + } + block, _ := pem.Decode([]byte(keyData)) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + // Try PKCS1 + key2, err2 := x509.ParsePKCS1PrivateKey(block.Bytes) + if err2 != nil { + return nil, fmt.Errorf("failed to parse private key (PKCS8: %v, PKCS1: %v)", err, err2) + } + return key2, nil + } + rsaKey, ok := key.(*rsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("key is not RSA") + } + return rsaKey, nil +} + +// assertionVerificationKeysJSON matches the JSON format used by xtest. +type assertionVerificationKeysJSON struct { + DefaultKey *assertionKeyJSON `json:"defaultKey,omitempty"` + Keys map[string]assertionKeyJSON `json:"keys,omitempty"` +} + +type assertionKeyJSON struct { + Alg string `json:"alg"` + Key string `json:"key"` +} + +func loadAssertionVerificationKeys(input string) (sdk.AssertionVerificationKeys, error) { + var raw []byte + var err error + + // Try as file path first + if _, statErr := os.Stat(input); statErr == nil { + raw, err = os.ReadFile(input) + if err != nil { + return sdk.AssertionVerificationKeys{}, fmt.Errorf("reading verification keys file: %w", err) + } + } else { + raw = []byte(input) + } + + var vkJSON assertionVerificationKeysJSON + if err := json.Unmarshal(raw, &vkJSON); err != nil { + return sdk.AssertionVerificationKeys{}, fmt.Errorf("unmarshaling verification keys JSON: %w", err) + } + + result := sdk.AssertionVerificationKeys{ + Keys: make(map[string]sdk.AssertionKey), + } + + if vkJSON.DefaultKey != nil { + k, err := loadSDKAssertionKey(vkJSON.DefaultKey.Alg, vkJSON.DefaultKey.Key) + if err != nil { + return sdk.AssertionVerificationKeys{}, fmt.Errorf("loading default verification key: %w", err) + } + result.DefaultKey = k + } + + for id, kj := range vkJSON.Keys { + k, err := loadSDKAssertionKey(kj.Alg, kj.Key) + if err != nil { + return sdk.AssertionVerificationKeys{}, fmt.Errorf("loading verification key for %s: %w", id, err) + } + result.Keys[id] = k + } + + return result, nil +} + +func loadSDKAssertionKey(alg, keyData string) (sdk.AssertionKey, error) { + switch sdk.AssertionKeyAlg(alg) { + case sdk.AssertionKeyAlgRS256: + key, err := parseRSAPrivateKey(keyData) + if err != nil { + return sdk.AssertionKey{}, err + } + return sdk.AssertionKey{Alg: sdk.AssertionKeyAlgRS256, Key: key}, nil + case sdk.AssertionKeyAlgHS256: + return sdk.AssertionKey{Alg: sdk.AssertionKeyAlgHS256, Key: []byte(keyData)}, nil + default: + return sdk.AssertionKey{}, fmt.Errorf("unsupported verification algorithm: %s", alg) + } +} diff --git a/xtest/tdfs.py b/xtest/tdfs.py index 244bdd16..119cc71a 100644 --- a/xtest/tdfs.py +++ b/xtest/tdfs.py @@ -21,7 +21,7 @@ logging.getLogger().setLevel(logging.DEBUG) -sdk_type = Literal["go", "java", "js"] +sdk_type = Literal["go", "java", "js", "exp-go-sdk"] focus_type = Literal[sdk_type, "all"] @@ -441,11 +441,11 @@ def _uncached_supports(self, feature: feature_type) -> bool: case ("key_management", "js") if self.version == "v0.2.0": # JS SDK v0.2.0 incorrectly reports support for key_management. return False - case ("autoconfigure", ("go" | "java")): + case ("autoconfigure", ("go" | "java" | "exp-go-sdk")): return True case ("better-messages-2024", ("js" | "java")): return True - case ("ns_grants", ("go" | "java")): + case ("ns_grants", ("go" | "java" | "exp-go-sdk")): return True case _: pass From 6906bcd7dec80363f4d757dd2fd02023111f5aee Mon Sep 17 00:00:00 2001 From: Paul Flynn Date: Fri, 13 Feb 2026 09:55:30 -0500 Subject: [PATCH 2/4] fix(exp-go-sdk): fix build and GetBaseKey fallback - Remove `if: heads` condition from CI replace step so exp-go-sdk always gets go.mod replacements (it always builds from platform checkout) - Regenerate go.sum against published module versions so builds work without replace directives - Add fallback to direct KAS public key fetch when GetBaseKey returns "base key is empty" (older platforms without well-known base_key) Co-Authored-By: Claude Opus 4.6 --- .github/workflows/xtest.yml | 1 - xtest/sdk/exp-go-sdk/go.mod | 4 +-- xtest/sdk/exp-go-sdk/go.sum | 12 +++++--- xtest/sdk/exp-go-sdk/main.go | 53 ++++++++++++++++++++++++++++++++++-- 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/.github/workflows/xtest.yml b/.github/workflows/xtest.yml index f6ead08a..13bfffa7 100644 --- a/.github/workflows/xtest.yml +++ b/.github/workflows/xtest.yml @@ -343,7 +343,6 @@ jobs: ######## SETUP EXP-GO-SDK ############# - name: Replace exp-go-sdk go.mod packages - if: contains(fromJSON(needs.resolve-versions.outputs.heads), matrix.platform-tag) env: PLATFORM_WORKING_DIR: ${{ steps.run-platform.outputs.platform-working-dir }} run: |- diff --git a/xtest/sdk/exp-go-sdk/go.mod b/xtest/sdk/exp-go-sdk/go.mod index 7a204ab9..0b81ca26 100644 --- a/xtest/sdk/exp-go-sdk/go.mod +++ b/xtest/sdk/exp-go-sdk/go.mod @@ -4,13 +4,12 @@ go 1.25.0 require ( github.com/opentdf/platform/protocol/go v0.15.0 - github.com/opentdf/platform/sdk v0.4.1 + github.com/opentdf/platform/sdk v0.12.0 ) require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250603165357-b52ab10f4468.1 // indirect connectrpc.com/connect v1.19.1 // indirect - github.com/CosmWasm/tinyjson v0.9.0 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect @@ -18,7 +17,6 @@ require ( github.com/gowebpki/jcs v1.0.1 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.6 // indirect diff --git a/xtest/sdk/exp-go-sdk/go.sum b/xtest/sdk/exp-go-sdk/go.sum index ceba89c7..cec2914b 100644 --- a/xtest/sdk/exp-go-sdk/go.sum +++ b/xtest/sdk/exp-go-sdk/go.sum @@ -6,8 +6,6 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/CosmWasm/tinyjson v0.9.0 h1:sPjgikATp5W0vD/v/Qz99uQ6G/lh/SuK0Wfskqua4Co= -github.com/CosmWasm/tinyjson v0.9.0/go.mod h1:5+7QnSKrkIWnpIdhUT2t2EYzXnII3/3MlM0oDsBSbc8= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -67,8 +65,6 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajR github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= @@ -107,6 +103,14 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opentdf/platform/lib/fixtures v0.4.0 h1:p3Y5MLJEBaWiSmo+QyRNTirvI8LqYDj+HtaE9vYrEJ8= +github.com/opentdf/platform/lib/fixtures v0.4.0/go.mod h1:ctyrVn+eTObHAPy3vrdPO0O1mc3vgQ6lc9pBTdhBAfo= +github.com/opentdf/platform/lib/ocrypto v0.9.0 h1:ZEJRFLR549unvP6aMWt2j3HT29wqBBhO9P7uudho6Ho= +github.com/opentdf/platform/lib/ocrypto v0.9.0/go.mod h1:/TtiJldbP/LO1cvX8bwhnd7SVHSUImBt1EfjG9qEo78= +github.com/opentdf/platform/protocol/go v0.15.0 h1:7m1iBCxklQy/inIonmGJnhfjkr4ZFLXVt1dL5aiO+sY= +github.com/opentdf/platform/protocol/go v0.15.0/go.mod h1:m6hTbcBrtp2jRhsAstLvPSAnm8v055fUppveG3iI6tw= +github.com/opentdf/platform/sdk v0.12.0 h1:5LkVf5Ktjt5tsc5YBxloJUYNHJ9pE5IMqjswZwBwrRE= +github.com/opentdf/platform/sdk v0.12.0/go.mod h1:jLXYHV3Am2Fq5RSaCLUDLVocwA9iO7mJkGTUqX/HOr8= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/xtest/sdk/exp-go-sdk/main.go b/xtest/sdk/exp-go-sdk/main.go index eba2bf7e..9a872da2 100644 --- a/xtest/sdk/exp-go-sdk/main.go +++ b/xtest/sdk/exp-go-sdk/main.go @@ -3,11 +3,13 @@ package main import ( "context" "crypto/rsa" + "crypto/tls" "crypto/x509" "encoding/json" "encoding/pem" "fmt" "io" + "net/http" "os" "strings" @@ -159,10 +161,14 @@ func doEncrypt(args []string) error { } defer client.Close() - // Get base KAS key + // Get base KAS key — try well-known config first, fall back to direct KAS query baseKey, err := client.GetBaseKey(ctx) if err != nil { - return fmt.Errorf("getting base key: %w", err) + fmt.Fprintf(os.Stderr, "GetBaseKey failed (%v), falling back to direct KAS public key fetch\n", err) + baseKey, err = fetchKASPublicKey(f.platformEndpoint, f.tlsNoVerify) + if err != nil { + return fmt.Errorf("fetching KAS public key: %w", err) + } } // Resolve attributes if specified @@ -389,6 +395,49 @@ func newSDKClient(endpoint, clientID, clientSecret, tokenEndpoint string, tlsNoV return sdk.New(endpoint, opts...) } +// kasPublicKeyResponse matches the JSON returned by the KAS /kas/v2/kas_public_key endpoint. +type kasPublicKeyResponse struct { + PublicKey string `json:"publicKey"` + KID string `json:"kid"` +} + +func fetchKASPublicKey(platformEndpoint string, tlsNoVerify bool) (*policy.SimpleKasKey, error) { + kasURL := strings.TrimSuffix(platformEndpoint, "/") + "/kas" + pubKeyURL := kasURL + "/v2/kas_public_key?algorithm=rsa:2048" + + httpClient := &http.Client{} + if tlsNoVerify { + httpClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12, InsecureSkipVerify: true}, //nolint:gosec // user-requested TLS skip + } + } + + resp, err := httpClient.Get(pubKeyURL) //nolint:gosec // URL is from trusted platform endpoint + if err != nil { + return nil, fmt.Errorf("GET %s: %w", pubKeyURL, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("GET %s returned %d: %s", pubKeyURL, resp.StatusCode, string(body)) + } + + var pkResp kasPublicKeyResponse + if err := json.NewDecoder(resp.Body).Decode(&pkResp); err != nil { + return nil, fmt.Errorf("decoding KAS public key response: %w", err) + } + + return &policy.SimpleKasKey{ + KasUri: kasURL, + PublicKey: &policy.SimpleKasPublicKey{ + Algorithm: policy.Algorithm_ALGORITHM_RSA_2048, + Kid: pkResp.KID, + Pem: pkResp.PublicKey, + }, + }, nil +} + func resolveAttributes(ctx context.Context, client *sdk.SDK, attrStr string) ([]*policy.Value, error) { fqns := strings.Split(attrStr, ",") for i := range fqns { From 4241d13e8be7e38032a144786fc85b236d5ab00d Mon Sep 17 00:00:00 2001 From: Paul Flynn Date: Fri, 13 Feb 2026 10:12:43 -0500 Subject: [PATCH 3/4] fix(exp-go-sdk): assemble TDF from segments + trailer, populate attribute defs Two fixes for test failures: 1. TDF assembly: The experimental writer's Finalize().Data only contains the manifest + central directory trailer. The payload segments are returned by each WriteSegment().TDFData call. The complete TDF zip is: concat(segment data...) + finalize data. Without this, Python's zipfile saw a corrupt zip64 end-of-central-directory locator. 2. Attribute definitions: The experimental writer's boolean expression builder requires Value.Attribute (parent definition) to be set. Ensure we copy it from the GetAttributeValuesByFqns response's Attribute field when the Value doesn't already have it populated. Co-Authored-By: Claude Opus 4.6 --- xtest/sdk/exp-go-sdk/main.go | 43 ++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/xtest/sdk/exp-go-sdk/main.go b/xtest/sdk/exp-go-sdk/main.go index 9a872da2..5406c92c 100644 --- a/xtest/sdk/exp-go-sdk/main.go +++ b/xtest/sdk/exp-go-sdk/main.go @@ -198,28 +198,41 @@ func doEncrypt(args []string) error { return fmt.Errorf("creating writer: %w", err) } - // Read input and write segments + // Read input and write segments, collecting segment TDF data data, err := os.ReadFile(f.inputFile) if err != nil { return fmt.Errorf("reading input file: %w", err) } + var segmentChunks [][]byte segIdx := 0 for offset := 0; offset < len(data); offset += segmentSize { end := offset + segmentSize if end > len(data) { end = len(data) } - if _, err := writer.WriteSegment(ctx, segIdx, data[offset:end]); err != nil { + segResult, err := writer.WriteSegment(ctx, segIdx, data[offset:end]) + if err != nil { return fmt.Errorf("writing segment %d: %w", segIdx, err) } + segData, err := io.ReadAll(segResult.TDFData) + if err != nil { + return fmt.Errorf("reading segment %d TDF data: %w", segIdx, err) + } + segmentChunks = append(segmentChunks, segData) segIdx++ } // Handle empty file: write one empty segment if len(data) == 0 { - if _, err := writer.WriteSegment(ctx, 0, []byte{}); err != nil { + segResult, err := writer.WriteSegment(ctx, 0, []byte{}) + if err != nil { return fmt.Errorf("writing empty segment: %w", err) } + segData, err := io.ReadAll(segResult.TDFData) + if err != nil { + return fmt.Errorf("reading empty segment TDF data: %w", err) + } + segmentChunks = append(segmentChunks, segData) } // Build finalize options @@ -236,15 +249,26 @@ func doEncrypt(args []string) error { finalizeOpts = append(finalizeOpts, exptdf.WithPayloadMimeType(f.mimeType)) } - // Finalize + // Finalize — returns manifest + trailer bytes result, err := writer.Finalize(ctx, finalizeOpts...) if err != nil { return fmt.Errorf("finalizing TDF: %w", err) } - // Write output - if err := os.WriteFile(f.output, result.Data, 0o644); err != nil { - return fmt.Errorf("writing output: %w", err) + // Write output: segment data + finalize trailer = complete TDF zip + outFile, err := os.Create(f.output) + if err != nil { + return fmt.Errorf("creating output file: %w", err) + } + defer outFile.Close() + + for i, chunk := range segmentChunks { + if _, err := outFile.Write(chunk); err != nil { + return fmt.Errorf("writing segment %d to output: %w", i, err) + } + } + if _, err := outFile.Write(result.Data); err != nil { + return fmt.Errorf("writing finalize data to output: %w", err) } return nil @@ -461,6 +485,11 @@ func resolveAttributes(ctx context.Context, client *sdk.SDK, attrStr string) ([] if v == nil { return nil, fmt.Errorf("no value for attribute: %s", fqn) } + // Ensure the Value has its parent Attribute definition set + // (the experimental writer's boolean expression builder requires it) + if v.GetAttribute() == nil && av.GetAttribute() != nil { + v.Attribute = av.GetAttribute() + } values = append(values, v) } return values, nil From a959cd55dd5cc7217e3e0717ab88168c9ec2b692 Mon Sep 17 00:00:00 2001 From: Paul Flynn Date: Fri, 13 Feb 2026 11:00:30 -0500 Subject: [PATCH 4/4] fix(exp-go-sdk): EC wrapping, assertion verification keys, KAS allowlist env var MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add --ecwrap flag to fetch EC public key from KAS for ec-wrapped TDFs - Support PEM public keys (PKIX/PKCS1) in assertion verification, not just private keys — test fixtures use -----BEGIN PUBLIC KEY----- for RS256 - Fix XT_WITH_KAS_ALLOWLIST env var name to match tdfs.py (was KAS_ALLOW_LIST) - Make fetchKASPublicKey accept algorithm parameter (rsa:2048 or ec:secp256r1) Co-Authored-By: Claude Opus 4.6 --- xtest/sdk/exp-go-sdk/cli.sh | 8 +++-- xtest/sdk/exp-go-sdk/main.go | 66 ++++++++++++++++++++++++++++++++---- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/xtest/sdk/exp-go-sdk/cli.sh b/xtest/sdk/exp-go-sdk/cli.sh index caf745d5..82d30f28 100755 --- a/xtest/sdk/exp-go-sdk/cli.sh +++ b/xtest/sdk/exp-go-sdk/cli.sh @@ -69,6 +69,10 @@ if [ "$1" == "encrypt" ]; then args+=(--assertions "$XT_WITH_ASSERTIONS") fi + if [ "$XT_WITH_ECWRAP" == "true" ]; then + args+=(--ecwrap) + fi + echo "$cmd" encrypt "${args[@]}" "$2" "$cmd" encrypt "${args[@]}" "$2" elif [ "$1" == "decrypt" ]; then @@ -78,8 +82,8 @@ elif [ "$1" == "decrypt" ]; then if [ "$XT_WITH_VERIFY_ASSERTIONS" == 'false' ]; then args+=(--no-verify-assertions) fi - if [ -n "$XT_WITH_KAS_ALLOW_LIST" ]; then - args+=(--kas-allowlist "$XT_WITH_KAS_ALLOW_LIST") + if [ -n "$XT_WITH_KAS_ALLOWLIST" ]; then + args+=(--kas-allowlist "$XT_WITH_KAS_ALLOWLIST") fi if [ "$XT_WITH_IGNORE_KAS_ALLOWLIST" == "true" ]; then args+=(--ignore-kas-allowlist) diff --git a/xtest/sdk/exp-go-sdk/main.go b/xtest/sdk/exp-go-sdk/main.go index 5406c92c..6c8cf290 100644 --- a/xtest/sdk/exp-go-sdk/main.go +++ b/xtest/sdk/exp-go-sdk/main.go @@ -86,6 +86,7 @@ type encryptFlags struct { policyMode string targetMode string tlsNoVerify bool + ecwrap bool } func parseEncryptFlags(args []string) (*encryptFlags, error) { @@ -129,6 +130,8 @@ func parseEncryptFlags(args []string) (*encryptFlags, error) { f.targetMode = args[i] case "--tls-no-verify": f.tlsNoVerify = true + case "--ecwrap": + f.ecwrap = true default: if f.inputFile == "" && !strings.HasPrefix(args[i], "-") { f.inputFile = args[i] @@ -163,9 +166,15 @@ func doEncrypt(args []string) error { // Get base KAS key — try well-known config first, fall back to direct KAS query baseKey, err := client.GetBaseKey(ctx) - if err != nil { - fmt.Fprintf(os.Stderr, "GetBaseKey failed (%v), falling back to direct KAS public key fetch\n", err) - baseKey, err = fetchKASPublicKey(f.platformEndpoint, f.tlsNoVerify) + if err != nil || (f.ecwrap && baseKey.GetPublicKey().GetAlgorithm() == policy.Algorithm_ALGORITHM_RSA_2048) { + if err != nil { + fmt.Fprintf(os.Stderr, "GetBaseKey failed (%v), falling back to direct KAS public key fetch\n", err) + } + algo := "rsa:2048" + if f.ecwrap { + algo = "ec:secp256r1" + } + baseKey, err = fetchKASPublicKey(f.platformEndpoint, f.tlsNoVerify, algo) if err != nil { return fmt.Errorf("fetching KAS public key: %w", err) } @@ -425,9 +434,9 @@ type kasPublicKeyResponse struct { KID string `json:"kid"` } -func fetchKASPublicKey(platformEndpoint string, tlsNoVerify bool) (*policy.SimpleKasKey, error) { +func fetchKASPublicKey(platformEndpoint string, tlsNoVerify bool, algorithm string) (*policy.SimpleKasKey, error) { kasURL := strings.TrimSuffix(platformEndpoint, "/") + "/kas" - pubKeyURL := kasURL + "/v2/kas_public_key?algorithm=rsa:2048" + pubKeyURL := kasURL + "/v2/kas_public_key?algorithm=" + algorithm httpClient := &http.Client{} if tlsNoVerify { @@ -452,10 +461,15 @@ func fetchKASPublicKey(platformEndpoint string, tlsNoVerify bool) (*policy.Simpl return nil, fmt.Errorf("decoding KAS public key response: %w", err) } + alg := policy.Algorithm_ALGORITHM_RSA_2048 + if strings.HasPrefix(algorithm, "ec:") { + alg = policy.Algorithm_ALGORITHM_EC_P256 + } + return &policy.SimpleKasKey{ KasUri: kasURL, PublicKey: &policy.SimpleKasPublicKey{ - Algorithm: policy.Algorithm_ALGORITHM_RSA_2048, + Algorithm: alg, Kid: pkResp.KID, Pem: pkResp.PublicKey, }, @@ -650,7 +664,7 @@ func loadAssertionVerificationKeys(input string) (sdk.AssertionVerificationKeys, func loadSDKAssertionKey(alg, keyData string) (sdk.AssertionKey, error) { switch sdk.AssertionKeyAlg(alg) { case sdk.AssertionKeyAlgRS256: - key, err := parseRSAPrivateKey(keyData) + key, err := parseRSAKey(keyData) if err != nil { return sdk.AssertionKey{}, err } @@ -661,3 +675,41 @@ func loadSDKAssertionKey(alg, keyData string) (sdk.AssertionKey, error) { return sdk.AssertionKey{}, fmt.Errorf("unsupported verification algorithm: %s", alg) } } + +// parseRSAKey parses a PEM-encoded RSA key, accepting both private and public key formats. +// For verification, the test fixtures use public keys (-----BEGIN PUBLIC KEY-----). +func parseRSAKey(keyData string) (any, error) { + // Try as file path first + if data, err := os.ReadFile(keyData); err == nil { + keyData = string(data) + } + block, _ := pem.Decode([]byte(keyData)) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + switch block.Type { + case "PUBLIC KEY": + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse PKIX public key: %w", err) + } + return pub, nil + case "RSA PUBLIC KEY": + pub, err := x509.ParsePKCS1PublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse PKCS1 public key: %w", err) + } + return pub, nil + default: + // Try as private key (PRIVATE KEY or RSA PRIVATE KEY) + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + key2, err2 := x509.ParsePKCS1PrivateKey(block.Bytes) + if err2 != nil { + return nil, fmt.Errorf("failed to parse key (PKCS8: %v, PKCS1: %v)", err, err2) + } + return key2, nil + } + return key, nil + } +}