From ef5fde78d74f6f5968330c423f2abefa191501d4 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Tue, 28 Oct 2025 15:30:52 +0000 Subject: [PATCH] appview/oauth: use client attestation Change-Id: qryntruoqzmttxsusswwkwrvqnsyyksk this change makes our tangled appview a "confidential" client. this change includes breaking changes to the appview service, it now requires two different environment variables: - TANGLED_OAUTH_CLIENT_SECRET: the secret component of the old JWKs object - TANGLED_OAUTH_CLIENT_KID: the key ID the old JWKs object both of these can be extracted from the old JWKs object: `obj.d` and `obj.kid` respectively. Signed-off-by: oppiliappan --- appview/config/config.go | 3 ++- appview/oauth/handler.go | 16 +++------------ appview/oauth/oauth.go | 23 ++++++++++----------- cmd/genjwks/main.go | 43 ---------------------------------------- docs/hacking.md | 18 +++++++++++++---- flake.nix | 9 +++++---- nix/pkgs/genjwks.nix | 18 ----------------- nix/pkgs/goat.nix | 13 ++++++++++++ scripts/appview.sh | 26 ------------------------ scripts/generate-jwks.sh | 5 ----- 10 files changed, 47 insertions(+), 127 deletions(-) delete mode 100644 cmd/genjwks/main.go delete mode 100644 nix/pkgs/genjwks.nix create mode 100644 nix/pkgs/goat.nix delete mode 100755 scripts/appview.sh delete mode 100755 scripts/generate-jwks.sh diff --git a/appview/config/config.go b/appview/config/config.go index b2d21fb0..f898af12 100644 --- a/appview/config/config.go +++ b/appview/config/config.go @@ -25,7 +25,8 @@ type CoreConfig struct { } type OAuthConfig struct { - Jwks string `env:"JWKS"` + ClientSecret string `env:"CLIENT_SECRET"` + ClientKid string `env:"CLIENT_KID"` } type JetstreamConfig struct { diff --git a/appview/oauth/handler.go b/appview/oauth/handler.go index 10c77b60..2f5985e4 100644 --- a/appview/oauth/handler.go +++ b/appview/oauth/handler.go @@ -12,7 +12,6 @@ import ( "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/go-chi/chi/v5" - "github.com/lestrrat-go/jwx/v2/jwk" "github.com/posthog/posthog-go" "tangled.org/core/api/tangled" "tangled.org/core/appview/db" @@ -41,21 +40,12 @@ func (o *OAuth) clientMetadata(w http.ResponseWriter, r *http.Request) { } func (o *OAuth) jwks(w http.ResponseWriter, r *http.Request) { - jwks := o.Config.OAuth.Jwks - pubKey, err := pubKeyFromJwk(jwks) - if err != nil { - o.Logger.Error("error parsing public key", "err", err) + w.Header().Set("Content-Type", "application/json") + body := o.ClientApp.Config.PublicJWKS() + if err := json.NewEncoder(w).Encode(body); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - - response := map[string]any{ - "keys": []jwk.Key{pubKey}, - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(response) } func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) { diff --git a/appview/oauth/oauth.go b/appview/oauth/oauth.go index f81500bc..f86d5850 100644 --- a/appview/oauth/oauth.go +++ b/appview/oauth/oauth.go @@ -10,10 +10,10 @@ import ( comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/auth/oauth" atpclient "github.com/bluesky-social/indigo/atproto/client" + atcrypto "github.com/bluesky-social/indigo/atproto/crypto" "github.com/bluesky-social/indigo/atproto/syntax" xrpc "github.com/bluesky-social/indigo/xrpc" "github.com/gorilla/sessions" - "github.com/lestrrat-go/jwx/v2/jwk" "github.com/posthog/posthog-go" "tangled.org/core/appview/config" "tangled.org/core/appview/db" @@ -49,6 +49,15 @@ func New(config *config.Config, ph posthog.Client, db *db.DB, enforcer *rbac.Enf oauthConfig = oauth.NewPublicConfig(clientId, callbackUri, []string{"atproto", "transition:generic"}) } + // configure client secret + priv, err := atcrypto.ParsePrivateMultibase(config.OAuth.ClientSecret) + if err != nil { + return nil, err + } + if err := oauthConfig.SetClientSecret(priv, config.OAuth.ClientKid); err != nil { + return nil, err + } + jwksUri := clientUri + "/oauth/jwks.json" authStore, err := NewRedisStore(config.Redis.ToURL()) @@ -140,18 +149,6 @@ func (o *OAuth) DeleteSession(w http.ResponseWriter, r *http.Request) error { return errors.Join(err1, err2) } -func pubKeyFromJwk(jwks string) (jwk.Key, error) { - k, err := jwk.ParseKey([]byte(jwks)) - if err != nil { - return nil, err - } - pubKey, err := k.PublicKey() - if err != nil { - return nil, err - } - return pubKey, nil -} - type User struct { Did string Pds string diff --git a/cmd/genjwks/main.go b/cmd/genjwks/main.go deleted file mode 100644 index 74751978..00000000 --- a/cmd/genjwks/main.go +++ /dev/null @@ -1,43 +0,0 @@ -// adapted from https://tangled.org/anirudh.fi/atproto-oauth - -package main - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "encoding/json" - "fmt" - "time" - - "github.com/lestrrat-go/jwx/v2/jwk" -) - -func main() { - privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - panic(err) - } - - key, err := jwk.FromRaw(privKey) - if err != nil { - panic(err) - } - - kid := fmt.Sprintf("%d", time.Now().Unix()) - - if err := key.Set(jwk.KeyIDKey, kid); err != nil { - panic(err) - } - - if err := key.Set("use", "sig"); err != nil { - panic(err) - } - - b, err := json.Marshal(key) - if err != nil { - panic(err) - } - - fmt.Println(string(b)) -} diff --git a/docs/hacking.md b/docs/hacking.md index 4c39153a..01cb8969 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -37,12 +37,22 @@ OAUTH JWKs to be setup: ``` # oauth jwks should already be setup by the nix devshell: -echo $TANGLED_OAUTH_JWKS -{"crv":"P-256","d":"tELKHYH-Dko6qo4ozYcVPE1ah6LvXHFV2wpcWpi8ab4","kid":"1753352226","kty":"EC","x":"mRzYpLzAGq74kJez9UbgGfV040DxgsXpMbaVsdy8RZs","y":"azqqXzUYywMlLb2Uc5AVG18nuLXyPnXr4kI4T39eeIc"} +echo $TANGLED_OAUTH_CLIENT_SECRET +z42ty4RT1ovnTopY8B8ekz9NuziF2CuMkZ7rbRFpAR9jBqMc + +echo $TANGLED_OAUTH_CLIENT_KID +1761667908 # if not, you can set it up yourself: -go build -o genjwks.out ./cmd/genjwks -export TANGLED_OAUTH_JWKS="$(./genjwks.out)" +goat key generate -t P-256 +Key Type: P-256 / secp256r1 / ES256 private key +Secret Key (Multibase Syntax): save this securely (eg, add to password manager) + z42tuPDKRfM2mz2Kv953ARen2jmrPA8S9LX9tRq4RVcUMwwL +Public Key (DID Key Syntax): share or publish this (eg, in DID document) + did:key:zDnaeUBxtG6Xuv3ATJE4GaWeyXM3jyamJsZw3bSPpxx4bNXDR + +# the secret key from above +export TANGLED_OAUTH_CLIENT_SECRET="z42tuP..." # run redis in at a new shell to store oauth sessions redis-server diff --git a/flake.nix b/flake.nix index 01efbeee..20d6a60a 100644 --- a/flake.nix +++ b/flake.nix @@ -78,8 +78,8 @@ inherit (pkgs) gcc; inherit sqlite-lib-src; }; - genjwks = self.callPackage ./nix/pkgs/genjwks.nix {}; lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;}; + goat = self.callPackage ./nix/pkgs/goat.nix {inherit indigo;}; appview-static-files = self.callPackage ./nix/pkgs/appview-static-files.nix { inherit htmx-src htmx-ws-src lucide-src inter-fonts-src ibm-plex-mono-src; }; @@ -90,7 +90,7 @@ }); in { overlays.default = final: prev: { - inherit (mkPackageSet final) lexgen sqlite-lib genjwks spindle knot-unwrapped knot appview; + inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview; }; packages = forAllSystems (system: let @@ -99,7 +99,7 @@ staticPackages = mkPackageSet pkgs.pkgsStatic; crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic; in { - inherit (packages) appview appview-static-files lexgen genjwks spindle knot knot-unwrapped sqlite-lib; + inherit (packages) appview appview-static-files lexgen goat spindle knot knot-unwrapped sqlite-lib; pkgsStatic-appview = staticPackages.appview; pkgsStatic-knot = staticPackages.knot; @@ -167,7 +167,8 @@ mkdir -p appview/pages/static # no preserve is needed because watch-tailwind will want to be able to overwrite cp -fr --no-preserve=ownership ${packages'.appview-static-files}/* appview/pages/static - export TANGLED_OAUTH_JWKS="$(${packages'.genjwks}/bin/genjwks)" + export TANGLED_OAUTH_CLIENT_KID="$(date +%s)" + export TANGLED_OAUTH_CLIENT_SECRET="$(${packages'.goat}/bin/goat key generate -t P-256 | grep -A1 "Secret Key" | tail -n1 | awk '{print $1}')" ''; env.CGO_ENABLED = 1; }; diff --git a/nix/pkgs/genjwks.nix b/nix/pkgs/genjwks.nix deleted file mode 100644 index e16fcedb..00000000 --- a/nix/pkgs/genjwks.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ - buildGoApplication, - modules, -}: -buildGoApplication { - pname = "genjwks"; - version = "0.1.0"; - src = ../../cmd/genjwks; - postPatch = '' - ln -s ${../../go.mod} ./go.mod - ''; - postInstall = '' - mv $out/bin/core $out/bin/genjwks - ''; - inherit modules; - doCheck = false; - CGO_ENABLED = 0; -} diff --git a/nix/pkgs/goat.nix b/nix/pkgs/goat.nix new file mode 100644 index 00000000..cc7edb87 --- /dev/null +++ b/nix/pkgs/goat.nix @@ -0,0 +1,13 @@ +{ + buildGoModule, + indigo, +}: +buildGoModule { + pname = "goat"; + version = "0.1.0"; + src = indigo; + subPackages = ["cmd/goat"]; + vendorHash = "sha256-VbDrcN4r5b7utRFQzVsKgDsVgdQLSXl7oZ5kdPA/huw="; + doCheck = false; +} + diff --git a/scripts/appview.sh b/scripts/appview.sh deleted file mode 100755 index 045fc26e..00000000 --- a/scripts/appview.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# Variables -BINARY_NAME="appview" -BINARY_PATH=".bin/app" -SERVER="95.111.206.63" -USER="appview" - -# SCP the binary to root's home directory -scp "$BINARY_PATH" root@$SERVER:/root/"$BINARY_NAME" - -# SSH into the server and perform the necessary operations -ssh root@$SERVER <