diff --git a/.github/workflows/xtest.yml b/.github/workflows/xtest.yml index 65b541f2..13bfffa7 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,22 @@ jobs: make working-directory: otdftests/xtest/sdk/go + ######## SETUP EXP-GO-SDK ############# + - name: Replace exp-go-sdk go.mod packages + 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..82d30f28 --- /dev/null +++ b/xtest/sdk/exp-go-sdk/cli.sh @@ -0,0 +1,97 @@ +#!/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 + + if [ "$XT_WITH_ECWRAP" == "true" ]; then + args+=(--ecwrap) + 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_ALLOWLIST" ]; then + args+=(--kas-allowlist "$XT_WITH_KAS_ALLOWLIST") + 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..0b81ca26 --- /dev/null +++ b/xtest/sdk/exp-go-sdk/go.mod @@ -0,0 +1,40 @@ +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.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/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/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..cec2914b --- /dev/null +++ b/xtest/sdk/exp-go-sdk/go.sum @@ -0,0 +1,190 @@ +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/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/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/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= +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..6c8cf290 --- /dev/null +++ b/xtest/sdk/exp-go-sdk/main.go @@ -0,0 +1,715 @@ +package main + +import ( + "context" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "net/http" + "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 + ecwrap 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 + case "--ecwrap": + f.ecwrap = 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 — try well-known config first, fall back to direct KAS query + baseKey, err := client.GetBaseKey(ctx) + 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) + } + } + + // 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, 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) + } + 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 { + 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 + 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 — returns manifest + trailer bytes + result, err := writer.Finalize(ctx, finalizeOpts...) + if err != nil { + return fmt.Errorf("finalizing TDF: %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 +} + +// ----- 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...) +} + +// 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, algorithm string) (*policy.SimpleKasKey, error) { + kasURL := strings.TrimSuffix(platformEndpoint, "/") + "/kas" + pubKeyURL := kasURL + "/v2/kas_public_key?algorithm=" + algorithm + + 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) + } + + 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: alg, + 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 { + 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) + } + // 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 +} + +// 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 := parseRSAKey(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) + } +} + +// 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 + } +} 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