diff --git a/go.mod b/go.mod index 526124f..125c570 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,13 @@ require ( github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/huh v1.0.0 github.com/charmbracelet/lipgloss v1.1.0 - github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/descope/virtualwebauthn v1.0.5 + github.com/go-webauthn/webauthn v0.17.4 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/jackc/pgx/v5 v5.7.1 github.com/prometheus/client_golang v1.19.1 github.com/prometheus/client_model v0.6.1 - golang.org/x/crypto v0.27.0 + golang.org/x/crypto v0.52.0 k8s.io/api v0.31.3 k8s.io/apimachinery v0.31.3 k8s.io/client-go v0.31.0 @@ -39,16 +41,19 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/go-webauthn/x v0.2.6 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-tpm v0.9.8 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/imdario/mergo v0.3.6 // indirect @@ -69,20 +74,22 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/philhofer/fwd v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/tinylib/msgp v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.54.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.24.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.3.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.34.2 // indirect diff --git a/go.sum b/go.sum index 06a2db0..0b2f4e5 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/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/descope/virtualwebauthn v1.0.5 h1:fMXji5UMepJC51Ge6d4v5IAjiJQRKmXE9hlo/B9SczQ= +github.com/descope/virtualwebauthn v1.0.5/go.mod h1:lLCfN+DpCM3iisM4bCILZlFEWkC1Zo7ZgsxC45CUapI= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= @@ -65,8 +67,8 @@ github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8 github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= +github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= @@ -80,10 +82,16 @@ github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogB github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-webauthn/webauthn v0.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHlqWhlxk= +github.com/go-webauthn/webauthn v0.17.4/go.mod h1:pZk63EE/BdztlmyS4Yc+9H5g4a8blNlbtGmdHQHbZX8= +github.com/go-webauthn/x v0.2.6 h1:TEyDuQAIiEgYpx60nKiBJIX/5nSUC8LxNbH+uf5U9uk= +github.com/go-webauthn/x v0.2.6/go.mod h1:45bA7YEqyQhRcQJ/TiBb46Ww8yqHBGvgEhQ3WWF0aDo= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -93,6 +101,10 @@ github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYu github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc= +github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -152,6 +164,8 @@ github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= 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= @@ -179,8 +193,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= +github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= @@ -189,6 +205,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= @@ -196,8 +214,8 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -206,36 +224,36 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/passkey/verifier.go b/internal/passkey/verifier.go new file mode 100644 index 0000000..1b30375 --- /dev/null +++ b/internal/passkey/verifier.go @@ -0,0 +1,186 @@ +// Package passkey wraps github.com/go-webauthn/webauthn behind the api.PasskeyVerifier +// seam. The api package deliberately never imports go-webauthn — ceremony state crosses +// the boundary as opaque bytes, the attestation as an io.Reader, and the verified result +// as a plain api.VerifiedCredential — so the real relying-party crypto lives here and is +// wired in only at the composition root (cmd/felis). The dependency arrow points one way: +// this package imports api for the seam types; api never imports this package, which is +// what keeps the seam (and the api test suite's fake verifier) honest. +// +// Scope: ENROLLMENT only, matching handlers_passkey.go. This wraps BeginRegistration and +// CreateCredential (the credential-creation ceremony). The login/assertion path +// (BeginLogin/ValidateLogin) is a deferred slice and is intentionally not adapted here. +package passkey + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + + "felis.lolicon.best/internal/api" +) + +// Verifier is the production api.PasskeyVerifier: a thin adapter over a configured +// *webauthn.WebAuthn relying party. It holds no per-ceremony state — the SessionData the +// server stashes between begin and finish is the only ceremony state, and it travels +// through the seam as opaque bytes (marshaled webauthn.SessionData). +type Verifier struct { + wa *webauthn.WebAuthn +} + +// compile-time proof the adapter satisfies the seam the handlers depend on. +var _ api.PasskeyVerifier = (*Verifier)(nil) + +// New builds a Verifier for a relying party identified by rpID (the WebAuthn RP ID — the +// registrable-domain-suffix host, e.g. the panel hostname, without scheme or port) whose +// permitted browser origins are origins (each a full "https://host" string). displayName +// is the human-facing RP name an authenticator may show. It returns an error if the +// go-webauthn config is invalid (e.g. an empty rpID), so a misconfigured deployment fails +// loudly at construction rather than silently minting unverifiable challenges. +func New(rpID, displayName string, origins []string) (*Verifier, error) { + // go-webauthn defers RP-id validation to the first ceremony; guard here so a + // misconfigured deployment fails at construction (loudly, once) rather than minting + // challenges that only fail later when a user tries to enroll. + if rpID == "" { + return nil, fmt.Errorf("passkey: relying-party id must not be empty") + } + if len(origins) == 0 { + return nil, fmt.Errorf("passkey: at least one relying-party origin is required") + } + wa, err := webauthn.New(&webauthn.Config{ + RPID: rpID, + RPDisplayName: displayName, + RPOrigins: origins, + }) + if err != nil { + return nil, err + } + return &Verifier{wa: wa}, nil +} + +// BeginRegistration starts a credential-creation ceremony. It returns the WebAuthn +// {"publicKey": {...}} creation options (marshaled verbatim for navigator.credentials.create()) +// and the opaque, marshaled SessionData the handler stashes for finish. The passkeys the +// principal has already bound are passed as excludeCredentials so an authenticator that +// already holds a credential for this account refuses to create a second one. +func (v *Verifier) BeginRegistration(user api.PasskeyUser) (json.RawMessage, []byte, error) { + var opts []webauthn.RegistrationOption + if excl := excludeDescriptors(user.Credentials); len(excl) > 0 { + opts = append(opts, webauthn.WithExclusions(excl)) + } + creation, session, err := v.wa.BeginRegistration(webauthnUser{u: user}, opts...) + if err != nil { + return nil, nil, err + } + // CredentialCreation marshals to {"publicKey": {...}} (its Response field carries the + // `publicKey` json tag), which is exactly the document the browser hands to + // navigator.credentials.create(). + options, err := json.Marshal(creation) + if err != nil { + return nil, nil, err + } + // SessionData is JSON-marshalable; the handler treats the result as opaque and replays + // the exact bytes at finish. We do NOT set an expiry inside SessionData — the challenge + // row's TTL (passkeyChallengeTTL) is the single authority on liveness. + sessionData, err := json.Marshal(session) + if err != nil { + return nil, nil, err + } + return options, sessionData, nil +} + +// FinishRegistration verifies the browser's attestation against the stashed SessionData +// and returns the persist-ready credential. The user handle must match the one bound at +// begin (go-webauthn enforces bytes.Equal(user.WebAuthnID(), session.UserID)); the +// challenge, RP ID, and origin are all checked against server-held values, never against +// anything the client echoes. +func (v *Verifier) FinishRegistration(user api.PasskeyUser, sessionData []byte, attestation io.Reader) (api.VerifiedCredential, error) { + var session webauthn.SessionData + if err := json.Unmarshal(sessionData, &session); err != nil { + return api.VerifiedCredential{}, err + } + parsed, err := protocol.ParseCredentialCreationResponseBody(attestation) + if err != nil { + return api.VerifiedCredential{}, err + } + cred, err := v.wa.CreateCredential(webauthnUser{u: user}, session, parsed) + if err != nil { + return api.VerifiedCredential{}, err + } + return api.VerifiedCredential{ + CredentialID: base64.RawURLEncoding.EncodeToString(cred.ID), + PublicKey: base64.StdEncoding.EncodeToString(cred.PublicKey), + SignCount: cred.Authenticator.SignCount, + AAGUID: aaguidString(cred.Authenticator.AAGUID), + }, nil +} + +// excludeDescriptors turns the principal's already-bound passkeys into the +// excludeCredentials list for a creation ceremony. A stored credential id that does not +// decode as base64url is skipped rather than aborting the whole ceremony — a single +// malformed row must not lock a user out of enrolling a new key. +func excludeDescriptors(creds []api.PasskeyCredential) []protocol.CredentialDescriptor { + out := make([]protocol.CredentialDescriptor, 0, len(creds)) + for _, c := range creds { + id, err := base64.RawURLEncoding.DecodeString(c.CredentialID) + if err != nil { + continue + } + out = append(out, protocol.CredentialDescriptor{ + Type: protocol.PublicKeyCredentialType, + CredentialID: protocol.URLEncodedBase64(id), + }) + } + return out +} + +// aaguidString renders the 16-byte authenticator AAGUID as a canonical UUID string. An +// absent (wrong-length) or all-zero AAGUID — common for privacy-preserving platform +// authenticators — renders as "" so the display view omits it rather than showing a +// meaningless all-zero UUID. +func aaguidString(b []byte) string { + if len(b) != 16 { + return "" + } + allZero := true + for _, x := range b { + if x != 0 { + allZero = false + break + } + } + if allZero { + return "" + } + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) +} + +// webauthnUser adapts the seam's api.PasskeyUser to the webauthn.User interface. The user +// handle is the account's stable user id bytes; the same principal at begin and finish +// therefore yields the same handle, which is what lets go-webauthn's user/session identity +// check pass across the two calls. +type webauthnUser struct { + u api.PasskeyUser +} + +func (w webauthnUser) WebAuthnID() []byte { return []byte(w.u.ID) } +func (w webauthnUser) WebAuthnName() string { return w.u.Name } +func (w webauthnUser) WebAuthnDisplayName() string { return w.u.DisplayName } + +// WebAuthnCredentials returns the principal's bound passkeys as webauthn.Credentials. +// Enrollment only needs the credential ids (for identity/exclusion bookkeeping), so only +// the id is populated; a row whose id does not decode is skipped. +func (w webauthnUser) WebAuthnCredentials() []webauthn.Credential { + out := make([]webauthn.Credential, 0, len(w.u.Credentials)) + for _, c := range w.u.Credentials { + id, err := base64.RawURLEncoding.DecodeString(c.CredentialID) + if err != nil { + continue + } + out = append(out, webauthn.Credential{ID: id}) + } + return out +} diff --git a/internal/passkey/verifier_test.go b/internal/passkey/verifier_test.go new file mode 100644 index 0000000..f8a2545 --- /dev/null +++ b/internal/passkey/verifier_test.go @@ -0,0 +1,178 @@ +package passkey + +import ( + "encoding/base64" + "strings" + "testing" + + virtualwebauthn "github.com/descope/virtualwebauthn" + + "felis.lolicon.best/internal/api" +) + +// Test relying party. RPID is a bare host; the origin is that host as an https URL. Both +// the go-webauthn config (via New) and the virtual authenticator (via RelyingParty) must +// agree on these, exactly as a real deployment's config and a real browser must agree. +const ( + testRPID = "mc.example.net" + testRPName = "Felis" + testOrigin = "https://mc.example.net" + testUserID = "u1" + testUserEml = "u1@example.net" +) + +func newTestVerifier(t *testing.T) *Verifier { + t.Helper() + v, err := New(testRPID, testRPName, []string{testOrigin}) + if err != nil { + t.Fatalf("New: %v", err) + } + return v +} + +func testUser(creds ...api.PasskeyCredential) api.PasskeyUser { + return api.PasskeyUser{ID: testUserID, Name: testUserEml, DisplayName: testUserEml, Credentials: creds} +} + +// virtualRP mirrors the verifier's RP so the authenticator signs clientDataJSON with the +// origin go-webauthn will check against. +func virtualRP() virtualwebauthn.RelyingParty { + return virtualwebauthn.RelyingParty{ID: testRPID, Name: testRPName, Origin: testOrigin} +} + +// TestRegisterRoundTrip is the PARITY check: a real go-webauthn relying party (through our +// adapter) issues a creation challenge, a virtual authenticator produces a real attestation +// response, and the adapter verifies it end to end. This exercises the pieces a CODE-ONLY +// adapter can silently get wrong: the {"publicKey":{...}} options marshal, the SessionData +// []byte round-trip through the seam, and the base64url/base64 encoding of the verified +// credential id and public key. A green run means the adapter's ceremony logic and the +// underlying crypto agree — it does NOT prove production works: the RP id/origin here are +// the test's, so whether cfg.Auth.PanelHostname matches the real browser origin stays an +// integration concern, and the pgrepo persistence remains unverified until a real DB run. +func TestRegisterRoundTrip(t *testing.T) { + v := newTestVerifier(t) + rp := virtualRP() + authenticator := virtualwebauthn.NewAuthenticator() + cred := virtualwebauthn.NewCredential(virtualwebauthn.KeyTypeEC2) + + options, sessionData, err := v.BeginRegistration(testUser()) + if err != nil { + t.Fatalf("BeginRegistration: %v", err) + } + // The options the browser receives must be a WebAuthn creation document. The virtual + // authenticator's parser (like a browser) reads the publicKey member. + attestationOpts, err := virtualwebauthn.ParseAttestationOptions(string(options)) + if err != nil { + t.Fatalf("ParseAttestationOptions: %v (options=%s)", err, options) + } + if attestationOpts.RelyingPartyID != testRPID { + t.Fatalf("options RP id = %q, want %q", attestationOpts.RelyingPartyID, testRPID) + } + + attestationResponse := virtualwebauthn.CreateAttestationResponse(rp, authenticator, cred, *attestationOpts) + + vc, err := v.FinishRegistration(testUser(), sessionData, strings.NewReader(attestationResponse)) + if err != nil { + t.Fatalf("FinishRegistration: %v", err) + } + + // The verified credential id must be the authenticator's credential id, base64url. + wantID := base64.RawURLEncoding.EncodeToString(cred.ID) + if vc.CredentialID != wantID { + t.Errorf("CredentialID = %q, want %q", vc.CredentialID, wantID) + } + if vc.PublicKey == "" { + t.Error("PublicKey is empty; expected the COSE public key") + } + if _, err := base64.StdEncoding.DecodeString(vc.PublicKey); err != nil { + t.Errorf("PublicKey is not valid base64: %v", err) + } +} + +// TestRegisterOriginMismatchRejected proves the adapter is really checking the origin: an +// authenticator that signs a DIFFERENT origin than the RP is configured for must fail +// verification. If this passed, the round-trip test above would be meaningless (it would +// accept anything). +func TestRegisterOriginMismatchRejected(t *testing.T) { + v := newTestVerifier(t) + // Authenticator signs an origin the verifier does not permit. + rp := virtualwebauthn.RelyingParty{ID: testRPID, Name: testRPName, Origin: "https://evil.example.net"} + authenticator := virtualwebauthn.NewAuthenticator() + cred := virtualwebauthn.NewCredential(virtualwebauthn.KeyTypeEC2) + + options, sessionData, err := v.BeginRegistration(testUser()) + if err != nil { + t.Fatalf("BeginRegistration: %v", err) + } + attestationOpts, err := virtualwebauthn.ParseAttestationOptions(string(options)) + if err != nil { + t.Fatalf("ParseAttestationOptions: %v", err) + } + attestationResponse := virtualwebauthn.CreateAttestationResponse(rp, authenticator, cred, *attestationOpts) + + if _, err := v.FinishRegistration(testUser(), sessionData, strings.NewReader(attestationResponse)); err == nil { + t.Fatal("FinishRegistration accepted an attestation signed for a foreign origin; want rejection") + } +} + +// TestRegisterUserMismatchRejected proves the user handle is bound across the ceremony: +// finishing as a different principal than began must fail (go-webauthn checks +// bytes.Equal(user.WebAuthnID(), session.UserID)). This is the guard that a stashed +// challenge cannot be redeemed for a different account. +func TestRegisterUserMismatchRejected(t *testing.T) { + v := newTestVerifier(t) + rp := virtualRP() + authenticator := virtualwebauthn.NewAuthenticator() + cred := virtualwebauthn.NewCredential(virtualwebauthn.KeyTypeEC2) + + options, sessionData, err := v.BeginRegistration(testUser()) + if err != nil { + t.Fatalf("BeginRegistration: %v", err) + } + attestationOpts, err := virtualwebauthn.ParseAttestationOptions(string(options)) + if err != nil { + t.Fatalf("ParseAttestationOptions: %v", err) + } + attestationResponse := virtualwebauthn.CreateAttestationResponse(rp, authenticator, cred, *attestationOpts) + + other := api.PasskeyUser{ID: "u2", Name: "u2@example.net", DisplayName: "u2@example.net"} + if _, err := v.FinishRegistration(other, sessionData, strings.NewReader(attestationResponse)); err == nil { + t.Fatal("FinishRegistration accepted a finish by a different principal; want rejection") + } +} + +// TestBeginExcludesBoundCredentials proves an already-bound passkey is surfaced to the +// authenticator as an excludeCredentials entry, so a device cannot double-bind. The +// exclusion id must be the stored credential id, base64url. +func TestBeginExcludesBoundCredentials(t *testing.T) { + v := newTestVerifier(t) + existingRaw := []byte("existing-credential-id-bytes") + existingID := base64.RawURLEncoding.EncodeToString(existingRaw) + + options, _, err := v.BeginRegistration(testUser(api.PasskeyCredential{CredentialID: existingID})) + if err != nil { + t.Fatalf("BeginRegistration: %v", err) + } + attestationOpts, err := virtualwebauthn.ParseAttestationOptions(string(options)) + if err != nil { + t.Fatalf("ParseAttestationOptions: %v", err) + } + found := false + for _, ex := range attestationOpts.ExcludeCredentials { + if ex == existingID { + found = true + break + } + } + if !found { + t.Errorf("excludeCredentials = %v, want it to contain %q", attestationOpts.ExcludeCredentials, existingID) + } +} + +// TestNewRejectsEmptyRPID proves a misconfigured deployment fails loudly at construction +// rather than minting challenges no browser can honor. +func TestNewRejectsEmptyRPID(t *testing.T) { + if _, err := New("", testRPName, []string{testOrigin}); err == nil { + t.Fatal("New accepted an empty RP id; want an error") + } +}