diff --git a/go.mod b/go.mod index 9cc3f4ef9a..ff0c2c138d 100644 --- a/go.mod +++ b/go.mod @@ -61,7 +61,7 @@ require ( github.com/onsi/ginkgo v1.16.5 github.com/onsi/ginkgo/v2 v2.28.3 github.com/onsi/gomega v1.40.0 - github.com/open-policy-agent/opa v1.15.2 + github.com/open-policy-agent/opa v1.17.0 github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d github.com/opencloud-eu/reva/v2 v2.46.1-0.20260522124452-c649de391cb1 @@ -95,7 +95,7 @@ require ( go-micro.dev/v4 v4.11.0 go.etcd.io/bbolt v1.4.3 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 go.opentelemetry.io/contrib/zpages v0.68.0 go.opentelemetry.io/otel v1.44.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 @@ -182,7 +182,7 @@ require ( github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deckarep/golang-set v1.8.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/dgraph-io/ristretto v0.2.0 // indirect github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect @@ -199,7 +199,7 @@ require ( github.com/evanphx/json-patch/v5 v5.5.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/gdexlab/go-render v1.0.1 // indirect github.com/go-acme/lego/v4 v4.4.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect @@ -228,7 +228,7 @@ require ( github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.2.1 // indirect - github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/gofrs/flock v0.13.0 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect @@ -264,11 +264,11 @@ require ( github.com/kovidgoyal/go-shm v1.0.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect - github.com/lestrrat-go/dsig v1.0.0 // indirect + github.com/lestrrat-go/dsig v1.2.1 // indirect github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc/v3 v3.0.2 // indirect - github.com/lestrrat-go/jwx/v3 v3.0.13 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.5 // indirect + github.com/lestrrat-go/jwx/v3 v3.1.1 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/libregraph/oidc-go v1.1.0 // indirect github.com/longsleep/go-metrics v1.0.0 // indirect @@ -328,7 +328,7 @@ require ( github.com/prometheus/alertmanager v0.31.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.17.0 // indirect + github.com/prometheus/procfs v0.20.1 // indirect github.com/prometheus/statsd_exporter v0.22.8 // indirect github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect github.com/rs/xid v1.6.0 // indirect @@ -366,8 +366,8 @@ require ( github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect github.com/trustelem/zxcvbn v1.0.1 // indirect github.com/urfave/cli/v2 v2.27.7 // indirect - github.com/valyala/fastjson v1.6.7 // indirect - github.com/vektah/gqlparser/v2 v2.5.32 // indirect + github.com/valyala/fastjson v1.6.10 // indirect + github.com/vektah/gqlparser/v2 v2.5.33 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/wk8/go-ordered-map v1.0.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect @@ -387,7 +387,7 @@ require ( go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/sys v0.45.0 // indirect diff --git a/go.sum b/go.sum index 83cae6c315..d3121a8553 100644 --- a/go.sum +++ b/go.sum @@ -196,8 +196,8 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/ github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/butonic/go-micro/v4 v4.11.1-0.20241115112658-b5d4de5ed9b3 h1:h8Z0hBv5tg/uZMKu8V47+DKWYVQg0lYP8lXDQq7uRpE= github.com/butonic/go-micro/v4 v4.11.1-0.20241115112658-b5d4de5ed9b3/go.mod h1:eE/tD53n3KbVrzrWxKLxdkGw45Fg1qaNLWjpJMvIUF4= -github.com/bytecodealliance/wasmtime-go/v39 v39.0.1 h1:RibaT47yiyCRxMOj/l2cvL8cWiWBSqDXHyqsa9sGcCE= -github.com/bytecodealliance/wasmtime-go/v39 v39.0.1/go.mod h1:miR4NYIEBXeDNamZIzpskhJ0z/p8al+lwMWylQ/ZJb4= +github.com/bytecodealliance/wasmtime-go/v44 v44.0.0 h1:WRZXnLPIer/TWs5aYPaMlmVcOlzmR6Ur6wjLRIQOhTQ= +github.com/bytecodealliance/wasmtime-go/v44 v44.0.0/go.mod h1:GP93piU+39CoFVCQ5xfHrPOUtL0APlMnkbblJ2d3YY0= github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= @@ -277,8 +277,8 @@ github.com/davidbyttow/govips/v2 v2.18.0 h1:pZRshWVYvewP/TZx3yZ7YeC42WyLXg53tHy5 github.com/davidbyttow/govips/v2 v2.18.0/go.mod h1:8+nst5zfMoats12PgmmAPh6p5OfjDaXK0BXMFl/vOcM= github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= -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/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/deepmap/oapi-codegen v1.3.11/go.mod h1:suMvK7+rKlx3+tpa8ByptmvoXbAV70wERKTOGH3hLp0= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= @@ -349,8 +349,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gdexlab/go-render v1.0.1 h1:rxqB3vo5s4n1kF0ySmoNeSPRYkEsyHgln4jFIQY7v0U= @@ -477,8 +477,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= -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/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= @@ -760,16 +760,16 @@ github.com/leonelquinteros/gotext v1.7.3-0.20260422134830-b012b4ccae69 h1:ZLo0bX github.com/leonelquinteros/gotext v1.7.3-0.20260422134830-b012b4ccae69/go.mod h1:ksG5iXViKefoupjy+0qQjAVoaDnylnQ1ejWl9g14wh8= 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/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38= -github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo= +github.com/lestrrat-go/dsig v1.2.1 h1:MwxzZhE4+4fguHi+uDALKVlC3Cn+O1QU1Q/F8D7hVIc= +github.com/lestrrat-go/dsig v1.2.1/go.mod h1:RD2eOaidyPvpc7IJQoO3Qq52RWdy8ZcJs8lrOnoa1Kc= github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY= github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= 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/v3 v3.0.2 h1:7u4HUaD0NQbf2/n5+fyp+T10hNCsAnwKfqn4A4Baif0= -github.com/lestrrat-go/httprc/v3 v3.0.2/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= -github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk= -github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU= +github.com/lestrrat-go/httprc/v3 v3.0.5 h1:S+Mb4L2I+bM6JGTibLmxExhyTOqnXjqx+zi9MoXw/TM= +github.com/lestrrat-go/httprc/v3 v3.0.5/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= +github.com/lestrrat-go/jwx/v3 v3.1.1 h1:yd9AdPmZ4INnQ7k42IrzXYpnEG803+SrQ6hdMvzHJzw= +github.com/lestrrat-go/jwx/v3 v3.1.1/go.mod h1:uw/MN2M/Xiu4FhwcIwH11Zsh9JWx9SWzgALl7/uIEkU= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/libregraph/idm v0.5.0 h1:tDMwKbAOZzdeDYMxVlY5PbSqRKO7dbAW9KT42A51WSk= @@ -942,8 +942,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= -github.com/open-policy-agent/opa v1.15.2 h1:dS9q+0Yvruq/VNvWJc5qCvCchn715OWc3HLHXn/UCCc= -github.com/open-policy-agent/opa v1.15.2/go.mod h1:c6SN+7jSsUcKJLQc5P4yhwx8YYDRbjpAiGkBOTqxaa4= +github.com/open-policy-agent/opa v1.17.0 h1:TMm6bCyb3CEL4wjXsXn1d/kBSBbjF+5sEIyzQvbJiEw= +github.com/open-policy-agent/opa v1.17.0/go.mod h1:lcuZYSlqQpXFzsA6EJCELmfR5+nNOpZYX+eo7xaIIlk= github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a h1:Sakl76blJAaM6NxylVkgSzktjo2dS504iDotEFJsh3M= github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a/go.mod h1:pjcozWijkNPbEtX5SIQaxEW/h8VAVZYTLx+70bmB3LY= github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+lP5lUUIzjRGDg93WrQfZJZCaV1ZP3KeyXi8bzY= @@ -1053,8 +1053,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= -github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI= github.com/prometheus/statsd_exporter v0.22.8 h1:Qo2D9ZzaQG+id9i5NYNGmbf1aa/KxKbB9aKfMS+Yib0= github.com/prometheus/statsd_exporter v0.22.8/go.mod h1:/DzwbTEaFTE0Ojz5PqcSk6+PFHOPWGxdXVr6yC8eFOM= @@ -1231,12 +1231,12 @@ github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/X github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= -github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= +github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc= -github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= +github.com/vektah/gqlparser/v2 v2.5.33 h1:lRp8aIeNUNbimf/axZd7ETg24q06hBtPaas+TcvI/7E= +github.com/vektah/gqlparser/v2 v2.5.33/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14/go.mod h1:RWc47jtnVuQv6+lY3c768WtXCas/Xi+U5UFc5xULmYg= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= @@ -1302,8 +1302,8 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/contrib/zpages v0.68.0 h1:H5yrUwxPrbvhzdBxjQD+VXMtPjIBfp8NWNVvQT8E30M= go.opentelemetry.io/contrib/zpages v0.68.0/go.mod h1:sZGctYYO4UOHItj9bx3F+t/s+u1Fv8CHCJ5s2eR2cjU= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= @@ -1339,8 +1339,8 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/vendor/github.com/decred/dcrd/dcrec/secp256k1/v4/ellipticadaptor.go b/vendor/github.com/decred/dcrd/dcrec/secp256k1/v4/ellipticadaptor.go index a3a45af317..1d22f0a891 100644 --- a/vendor/github.com/decred/dcrd/dcrec/secp256k1/v4/ellipticadaptor.go +++ b/vendor/github.com/decred/dcrd/dcrec/secp256k1/v4/ellipticadaptor.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 The Decred developers +// Copyright 2020-2026 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -63,8 +63,8 @@ type KoblitzCurve struct { // bigAffineToJacobian takes an affine point (x, y) as big integers and converts // it to Jacobian point with Z=1. func bigAffineToJacobian(x, y *big.Int, result *JacobianPoint) { - result.X.SetByteSlice(x.Bytes()) - result.Y.SetByteSlice(y.Bytes()) + result.X.SetByteSlice(new(big.Int).Mod(x, curveParams.P).Bytes()) + result.Y.SetByteSlice(new(big.Int).Mod(y, curveParams.P).Bytes()) result.Z.SetInt(1) } @@ -91,6 +91,15 @@ func (curve *KoblitzCurve) Params() *elliptic.CurveParams { // // This is part of the elliptic.Curve interface implementation. This function // differs from the crypto/elliptic algorithm since a = 0 not -3. +// +// NOTE: Unfortunately, the Go stdlib elliptic.Curve interface requires that the +// conventional point at infinity (0, 0) is not considered on the curve which is +// contrary to what is typically expected since the point at infinity is in fact +// is a valid curve point. +// +// Deprecated: The standard library elliptic.Curve interface is now deprecated +// and callers should interact with the safer, and much faster, specialized +// methods instead. func (curve *KoblitzCurve) IsOnCurve(x, y *big.Int) bool { // Convert big ints to a Jacobian point for faster arithmetic. var point JacobianPoint @@ -101,6 +110,14 @@ func (curve *KoblitzCurve) IsOnCurve(x, y *big.Int) bool { // Add returns the sum of (x1,y1) and (x2,y2). // // This is part of the elliptic.Curve interface implementation. +// +// NOTE: Per the documentation of the elliptic.Curve interface, the behavior +// when the input is not a point on the curve is undefined. Callers must ensure +// they are calling this method with valid points. +// +// Deprecated: The standard library elliptic.Curve interface is now deprecated +// and callers should interact with the safer, and much faster, specialized +// methods instead. func (curve *KoblitzCurve) Add(x1, y1, x2, y2 *big.Int) (*big.Int, *big.Int) { // The point at infinity is the identity according to the group law for // elliptic curve cryptography. Thus, ∞ + P = P and P + ∞ = P. @@ -124,6 +141,14 @@ func (curve *KoblitzCurve) Add(x1, y1, x2, y2 *big.Int) (*big.Int, *big.Int) { // Double returns 2*(x1,y1). // // This is part of the elliptic.Curve interface implementation. +// +// NOTE: Per the documentation of the elliptic.Curve interface, the behavior +// when the input is not a point on the curve is undefined. Callers must ensure +// they are calling this method with valid points. +// +// Deprecated: The standard library elliptic.Curve interface is now deprecated +// and callers should interact with the safer, and much faster, specialized +// methods instead. func (curve *KoblitzCurve) Double(x1, y1 *big.Int) (*big.Int, *big.Int) { if y1.Sign() == 0 { return new(big.Int), new(big.Int) @@ -156,6 +181,14 @@ func moduloReduce(k []byte) []byte { // ScalarMult returns k*(bx, by) where k is a big endian integer. // // This is part of the elliptic.Curve interface implementation. +// +// NOTE: Per the documentation of the elliptic.Curve interface, the behavior +// when the input is not a point on the curve is undefined. Callers must ensure +// they are calling this method with valid points. +// +// Deprecated: The standard library elliptic.Curve interface is now deprecated +// and callers should interact with the safer, and much faster, specialized +// methods instead. func (curve *KoblitzCurve) ScalarMult(bx, by *big.Int, k []byte) (*big.Int, *big.Int) { // Convert the affine coordinates from big integers to Jacobian points, // do the multiplication in Jacobian projective space, and convert the @@ -172,6 +205,10 @@ func (curve *KoblitzCurve) ScalarMult(bx, by *big.Int, k []byte) (*big.Int, *big // big endian integer. // // This is part of the elliptic.Curve interface implementation. +// +// Deprecated: The standard library elliptic.Curve interface is now deprecated +// and callers should interact with the safer, and much faster, specialized +// methods instead. func (curve *KoblitzCurve) ScalarBaseMult(k []byte) (*big.Int, *big.Int) { // Perform the multiplication and convert the Jacobian point back to affine // big.Ints. @@ -250,6 +287,10 @@ var secp256k1 = &KoblitzCurve{ } // S256 returns an elliptic.Curve which implements secp256k1. +// +// Deprecated: The standard library elliptic.Curve interface is now deprecated +// and callers should interact with the safer, and much faster, specialized +// methods instead. func S256() *KoblitzCurve { return secp256k1 } diff --git a/vendor/github.com/fsnotify/fsnotify/.cirrus.yml b/vendor/github.com/fsnotify/fsnotify/.cirrus.yml deleted file mode 100644 index 7f257e99ac..0000000000 --- a/vendor/github.com/fsnotify/fsnotify/.cirrus.yml +++ /dev/null @@ -1,14 +0,0 @@ -freebsd_task: - name: 'FreeBSD' - freebsd_instance: - image_family: freebsd-14-2 - install_script: - - pkg update -f - - pkg install -y go - test_script: - # run tests as user "cirrus" instead of root - - pw useradd cirrus -m - - chown -R cirrus:cirrus . - - FSNOTIFY_BUFFER=4096 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./... - - sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./... - - FSNOTIFY_DEBUG=1 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race -v ./... diff --git a/vendor/github.com/fsnotify/fsnotify/CHANGELOG.md b/vendor/github.com/fsnotify/fsnotify/CHANGELOG.md index 6468d2cf40..3027f3c67a 100644 --- a/vendor/github.com/fsnotify/fsnotify/CHANGELOG.md +++ b/vendor/github.com/fsnotify/fsnotify/CHANGELOG.md @@ -1,5 +1,54 @@ # Changelog +1.10.1 2026-05-04 +----------------- + +### Changes and fixes + +- inotify: don't remove sibling watches sharing a path prefix ([#754]) + +- inotify, windows: don't rename sibling watches sharing a path prefix + ([#755]) + + +[#754]: https://github.com/fsnotify/fsnotify/pull/754 +[#755]: https://github.com/fsnotify/fsnotify/pull/755 + + +1.10.0 2026-04-30 +----------------- +This version of fsnotify needs Go 1.23. + +### Changes and fixes + +- inotify: improve initialization error message ([#731]) + +- inotify: send Rename event if recursive watch is renamed ([#696]) + +- inotify: avoid copying event buffers when reading names ([#741]) + +- kqueue: skip dangling symlinks (ENOENT) in watchDirectoryFiles, so a + bad entry no longer aborts Watcher.Add for the whole directory ([#748]) + +- kqueue: drop watches directly in Close() to fix a file descriptor leak + when recycling watchers ([#740]) + +- windows: fix nil pointer dereference in remWatch ([#736]) + +- windows: lock watch field updates against concurrent WatchList to fix + a race introduced in v1.9.0 ([#709], [#749]) + + +[#696]: https://github.com/fsnotify/fsnotify/pull/696 +[#709]: https://github.com/fsnotify/fsnotify/pull/709 +[#731]: https://github.com/fsnotify/fsnotify/pull/731 +[#736]: https://github.com/fsnotify/fsnotify/pull/736 +[#740]: https://github.com/fsnotify/fsnotify/pull/740 +[#741]: https://github.com/fsnotify/fsnotify/pull/741 +[#748]: https://github.com/fsnotify/fsnotify/pull/748 +[#749]: https://github.com/fsnotify/fsnotify/pull/749 + + 1.9.0 2024-04-04 ---------------- diff --git a/vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md b/vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md index 4cc40fa597..cd0ee612da 100644 --- a/vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md +++ b/vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md @@ -77,6 +77,8 @@ End-of-line escapes with `\` are not supported. debug [yes/no] # Enable/disable FSNOTIFY_DEBUG (tests are run in parallel by default, so -parallel=1 is probably a good idea). + state # Print internal state to stderr (exact output differs + # per backend). print [any strings] # Print text to stdout; for debugging. touch path diff --git a/vendor/github.com/fsnotify/fsnotify/README.md b/vendor/github.com/fsnotify/fsnotify/README.md index 1f4eb583d5..2e56ef4c9a 100644 --- a/vendor/github.com/fsnotify/fsnotify/README.md +++ b/vendor/github.com/fsnotify/fsnotify/README.md @@ -1,7 +1,7 @@ fsnotify is a Go library to provide cross-platform filesystem notifications on Windows, Linux, macOS, BSD, and illumos. -Go 1.17 or newer is required; the full documentation is at +Go 1.23 or newer is required; the full documentation is at https://pkg.go.dev/github.com/fsnotify/fsnotify --- @@ -12,7 +12,7 @@ Platform support: | :-------------------- | :--------- | :------------------------------------------------------------------------ | | inotify | Linux | Supported | | kqueue | BSD, macOS | Supported | -| ReadDirectoryChangesW | Windows | Supported | +| ReadDirectoryChangesW | Windows | Supported ([excluding `Chmod` operations][#487]) | | FEN | illumos | Supported | | fanotify | Linux 5.9+ | [Not yet](https://github.com/fsnotify/fsnotify/issues/114) | | FSEvents | macOS | [Needs support in x/sys/unix][fsevents] | @@ -22,6 +22,7 @@ Platform support: Linux and illumos should include Android and Solaris, but these are currently untested. +[#487]: https://github.com/fsnotify/fsnotify/issues/487 [fsevents]: https://github.com/fsnotify/fsnotify/issues/11#issuecomment-1279133120 [usn]: https://github.com/fsnotify/fsnotify/issues/53#issuecomment-1279829847 @@ -126,7 +127,7 @@ settings* until we have a native FSEvents implementation (see [#11]). ### Watching a file doesn't work well Watching individual files (rather than directories) is generally not recommended as many programs (especially editors) update files atomically: it will write to -a temporary file which is then moved to to destination, overwriting the original +a temporary file which is then moved to a destination, overwriting the original (or some variant thereof). The watcher on the original file is now lost, as that no longer exists. @@ -151,26 +152,57 @@ This is the event that inotify sends, so not much can be changed about this. The `fs.inotify.max_user_watches` sysctl variable specifies the upper limit for the number of watches per user, and `fs.inotify.max_user_instances` specifies the maximum number of inotify instances per user. Every Watcher you create is an -"instance", and every path you add is a "watch". +"instance", and every path you add is a "watch". Reaching the limit will result +in a "no space left on device" or "too many open files" error. These are also exposed in `/proc` as `/proc/sys/fs/inotify/max_user_watches` and -`/proc/sys/fs/inotify/max_user_instances` +`/proc/sys/fs/inotify/max_user_instances`. The default values differ per distro +and available memory. To increase them you can use `sysctl` or write the value to proc file: - # The default values on Linux 5.18 - sysctl fs.inotify.max_user_watches=124983 - sysctl fs.inotify.max_user_instances=128 + sysctl fs.inotify.max_user_watches=200000 + sysctl fs.inotify.max_user_instances=256 To make the changes persist on reboot edit `/etc/sysctl.conf` or `/usr/lib/sysctl.d/50-default.conf` (details differ per Linux distro; check your distro's documentation): - fs.inotify.max_user_watches=124983 - fs.inotify.max_user_instances=128 + fs.inotify.max_user_watches=200000 + fs.inotify.max_user_instances=256 + +### Windows +Recursive watching is not currently enabled through fsnotify's public API +(see the FAQ "Are subdirectories watched?" above). The notes below +describe Windows backend behavior observed when recursive watching is +enabled internally (for example, in fsnotify's own tests). They are kept +here as a reference for maintainers and contributors who encounter the +behavior, since the recursive code path still exists in the backend. + +When recursive watching is enabled and you watch a directory, you may +receive a `Write` event for an intermediate directory whenever a child +entry inside it is created, renamed, or removed. For example, with a +recursive watch on `/a` and a new file `/a/b/c`, you will receive +`Create /a/b/c` and may also receive `Write /a/b`. + +This happens because, on NTFS-backed volumes, modifying the entries of a +directory updates that directory's last-write time, and the Windows +backend requests `FILE_NOTIFY_CHANGE_LAST_WRITE` to support `Write` events +on files. The same `Write` filter therefore picks up the directory's +metadata update. + +kqueue has the same "directory `Write` = directory contents changed" +semantics, so portable code that treats `Write` on a directory as +"something inside it changed" works on Windows and BSD/macOS, but not on +Linux (inotify uses `Write` only for file-content changes). If you only +care about file content, filter out `Write` events whose path refers to a +directory. + +Whether the directory `Write` is actually delivered alongside the child +events is not guaranteed: it depends on `ReadDirectoryChangesW` buffering, +NTFS metadata update timing, and event coalescing, none of which fsnotify +controls. -Reaching the limit will result in a "no space left on device" or "too many open -files" error. ### kqueue (macOS, all BSD systems) kqueue requires opening a file descriptor for every file that's being watched; diff --git a/vendor/github.com/fsnotify/fsnotify/backend_fen.go b/vendor/github.com/fsnotify/fsnotify/backend_fen.go index 57fc692848..e43c6d088c 100644 --- a/vendor/github.com/fsnotify/fsnotify/backend_fen.go +++ b/vendor/github.com/fsnotify/fsnotify/backend_fen.go @@ -158,7 +158,9 @@ func (w *fen) readEvents() { pevents := make([]unix.PortEvent, 8) for { - count, err := w.port.Get(pevents, 1, nil) + count, err := internal.IgnoringEINTR(func() (int, error) { + return w.port.Get(pevents, 1, nil) + }) if err != nil && err != unix.ETIME { // Interrupted system call (count should be 0) ignore and continue if errors.Is(err, unix.EINTR) && count == 0 { diff --git a/vendor/github.com/fsnotify/fsnotify/backend_inotify.go b/vendor/github.com/fsnotify/fsnotify/backend_inotify.go index a36cb89d73..4c3f6f7c28 100644 --- a/vendor/github.com/fsnotify/fsnotify/backend_inotify.go +++ b/vendor/github.com/fsnotify/fsnotify/backend_inotify.go @@ -55,10 +55,10 @@ type ( path map[string]uint32 // pathname → wd } watch struct { - wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall) - flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags) - path string // Watch path. - recurse bool // Recursion with ./...? + wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall) + flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags) + path string // Watch path. + watchFlags watchFlag } koekje struct { cookie uint32 @@ -66,6 +66,9 @@ type ( } ) +func (w watch) byUser() bool { return w.watchFlags&flagByUser != 0 } +func (w watch) recurse() bool { return w.watchFlags&flagRecurse != 0 } + func newWatches() *watches { return &watches{ wd: make(map[uint32]*watch), @@ -79,6 +82,13 @@ func (w *watches) len() int { return len(w.wd) } func (w *watches) add(ww *watch) { w.wd[ww.wd] = ww; w.path[ww.path] = ww.wd } func (w *watches) remove(watch *watch) { delete(w.path, watch.path); delete(w.wd, watch.wd) } +func isSameOrDescendantPath(path, root string) bool { + if path == root { + return true + } + return strings.HasPrefix(path, root+string(os.PathSeparator)) +} + func (w *watches) removePath(path string) ([]uint32, error) { path, recurse := recursivePath(path) wd, ok := w.path[path] @@ -87,20 +97,20 @@ func (w *watches) removePath(path string) ([]uint32, error) { } watch := w.wd[wd] - if recurse && !watch.recurse { + if recurse && !watch.recurse() { return nil, fmt.Errorf("can't use /... with non-recursive watch %q", path) } delete(w.path, path) delete(w.wd, wd) - if !watch.recurse { + if !watch.recurse() { return []uint32{wd}, nil } wds := make([]uint32, 0, 8) wds = append(wds, wd) for p, rwd := range w.path { - if strings.HasPrefix(p, path) { + if isSameOrDescendantPath(p, path) { delete(w.path, p) delete(w.wd, rwd) wds = append(wds, rwd) @@ -139,7 +149,7 @@ func newBackend(ev chan Event, errs chan error) (backend, error) { // I/O operations won't terminate on close. fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK) if fd == -1 { - return nil, errno + return nil, fmt.Errorf("couldn't initialize inotify: %w", errno) } w := &inotify{ @@ -188,11 +198,8 @@ func (w *inotify) AddWith(path string, opts ...addOpt) error { return fmt.Errorf("%w: %s", xErrUnsupported, with.op) } - add := func(path string, with withOpts, recurse bool) error { + add := func(path string, with withOpts, wf watchFlag) error { var flags uint32 - if with.noFollow { - flags |= unix.IN_DONT_FOLLOW - } if with.op.Has(Create) { flags |= unix.IN_CREATE } @@ -220,7 +227,7 @@ func (w *inotify) AddWith(path string, opts ...addOpt) error { if with.op.Has(xUnportableCloseRead) { flags |= unix.IN_CLOSE_NOWRITE } - return w.register(path, flags, recurse) + return w.register(path, flags, wf) } w.mu.Lock() @@ -248,14 +255,18 @@ func (w *inotify) AddWith(path string, opts ...addOpt) error { w.sendEvent(Event{Name: root, Op: Create}) } - return add(root, with, true) + wf := flagRecurse + if root == path { + wf |= flagByUser + } + return add(root, with, wf) }) } - return add(path, with, false) + return add(path, with, 0) } -func (w *inotify) register(path string, flags uint32, recurse bool) error { +func (w *inotify) register(path string, flags uint32, wf watchFlag) error { return w.watches.updatePath(path, func(existing *watch) (*watch, error) { if existing != nil { flags |= existing.flags | unix.IN_MASK_ADD @@ -272,10 +283,10 @@ func (w *inotify) register(path string, flags uint32, recurse bool) error { if existing == nil { return &watch{ - wd: uint32(wd), - path: path, - flags: flags, - recurse: recurse, + wd: uint32(wd), + path: path, + flags: flags, + watchFlags: wf, }, nil } @@ -425,11 +436,7 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs nameLen = uint32(inEvent.Len) ) if nameLen > 0 { - /// Point "bytes" at the first byte of the filename - bb := *buf - bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&bb[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen] - /// The filename is padded with NULL bytes. TrimRight() gets rid of those. - name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\x00") + name += "/" + inotifyEventName(buf, offset, nameLen) } if debug { @@ -450,7 +457,9 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs // We can't really update the state when a watched path is moved; only // IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove the watch. if inEvent.Mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF { - if watch.recurse { // Do nothing + // Watch is set up as part of recurse: do nothing as the move gets + // registered from the parent directory. + if watch.recurse() && !watch.byUser() { return Event{}, true } @@ -460,6 +469,10 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs return Event{}, false } } + + if watch.recurse() { + return Event{Name: watch.path, Op: Rename}, true + } } /// Skip if we're watching both this path and the parent; the parent will @@ -473,11 +486,11 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs ev := w.newEvent(name, inEvent.Mask, inEvent.Cookie) // Need to update watch path for recurse. - if watch.recurse { + if watch.recurse() { isDir := inEvent.Mask&unix.IN_ISDIR == unix.IN_ISDIR /// New directory created: set up watch on it. if isDir && ev.Has(Create) { - err := w.register(ev.Name, watch.flags, true) + err := w.register(ev.Name, watch.flags, flagRecurse) if !w.sendError(err) { return Event{}, false } @@ -495,7 +508,7 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs if k == watch.wd || ww.path == ev.Name { continue } - if strings.HasPrefix(ww.path, ev.renamedFrom) { + if isSameOrDescendantPath(ww.path, ev.renamedFrom) { ww.path = strings.Replace(ww.path, ev.renamedFrom, ev.Name, 1) w.watches.wd[k] = ww } @@ -507,12 +520,13 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs return ev, true } -func (w *inotify) isRecursive(path string) bool { - ww := w.watches.byPath(path) - if ww == nil { // path could be a file, so also check the Dir. - ww = w.watches.byPath(filepath.Dir(path)) +func inotifyEventName(buf *[65536]byte, offset, nameLen uint32) string { + start := int(offset + unix.SizeofInotifyEvent) + bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[start]))[:nameLen:nameLen] + for nameLen > 0 && bytes[nameLen-1] == 0 { + nameLen-- } - return ww != nil && ww.recurse + return string(bytes[:nameLen]) } func (w *inotify) newEvent(name string, mask, cookie uint32) Event { @@ -578,6 +592,6 @@ func (w *inotify) state() { w.mu.Lock() defer w.mu.Unlock() for wd, ww := range w.watches.wd { - fmt.Fprintf(os.Stderr, "%4d: recurse=%t %q\n", wd, ww.recurse, ww.path) + fmt.Fprintf(os.Stderr, "%4d: %q watchFlags=0x%x\n", wd, ww.path, ww.watchFlags) } } diff --git a/vendor/github.com/fsnotify/fsnotify/backend_kqueue.go b/vendor/github.com/fsnotify/fsnotify/backend_kqueue.go index 340aeec061..d2c8cfb6c6 100644 --- a/vendor/github.com/fsnotify/fsnotify/backend_kqueue.go +++ b/vendor/github.com/fsnotify/fsnotify/backend_kqueue.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "runtime" + "sort" "sync" "time" @@ -245,9 +246,26 @@ func (w *kqueue) Close() error { return nil } + // Snapshot and drop all watches directly. w.Remove -> w.remove + // short-circuits on isClosed() (which is already true after + // w.shared.close() above), so calling Remove here in the happy path + // leaked every watched directory + file descriptor. On macOS a + // single directory watch opens an fd for every file in the dir, so + // long-running processes that recreate watchers (hot-reload dev + // servers, etc.) ran out of fds with EMFILE (#732). pathsToRemove := w.watches.listPaths(false) for _, name := range pathsToRemove { - w.Remove(name) + info, ok := w.watches.byPath(name) + if !ok { + // w.path has an entry for name but w.wd doesn't -- + // drop the stale lookup entry so the map state is + // consistent after Close. + w.watches.remove(0, name) + continue + } + _ = w.register([]int{info.wd}, unix.EV_DELETE, 0) + unix.Close(info.wd) + w.watches.remove(info.wd, name) } unix.Close(w.closepipe[1]) // Send "quit" message to readEvents @@ -376,19 +394,12 @@ func (w *kqueue) addWatch(name string, flags uint32, listDir bool) (string, erro } } - // Retry on EINTR; open() can return EINTR in practice on macOS. - // See #354, and Go issues 11180 and 39237. - for { - info.wd, err = unix.Open(name, openMode, 0) - if err == nil { - break - } - if errors.Is(err, unix.EINTR) { - continue - } + info.wd, err = internal.IgnoringEINTR(func() (int, error) { + return unix.Open(name, openMode, 0) + }) + if err != nil { return "", err } - info.isDir = fi.IsDir() } @@ -436,9 +447,10 @@ func (w *kqueue) readEvents() { eventBuffer := make([]unix.Kevent_t, 10) for { - kevents, err := w.read(eventBuffer) - // EINTR is okay, the syscall was interrupted before timeout expired. - if err != nil && err != unix.EINTR { + kevents, err := internal.IgnoringEINTR(func() ([]unix.Kevent_t, error) { + return w.read(eventBuffer) + }) + if err != nil { if !w.sendError(fmt.Errorf("fsnotify.readEvents: %w", err)) { return } @@ -583,12 +595,14 @@ func (w *kqueue) watchDirectoryFiles(dirPath string) error { cleanPath, err := w.internalWatch(path, fi) if err != nil { - // No permission to read the file; that's not a problem: just skip. - // But do add it to w.fileExists to prevent it from being picked up - // as a "new" file later (it still shows up in the directory + // No permission, or the entry resolved to a missing target + // (e.g. a dangling symlink): not a problem, just skip. But + // do mark it as seen to prevent it from being picked up as + // a "new" file later (it still shows up in the directory // listing). switch { - case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM): + case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) || + errors.Is(err, os.ErrNotExist): cleanPath = filepath.Clean(path) default: return fmt.Errorf("%q: %w", path, err) @@ -703,3 +717,19 @@ func (w *kqueue) xSupports(op Op) bool { } return true } + +func (w *kqueue) state() { + w.watches.mu.Lock() + defer w.watches.mu.Unlock() + + all := make([]int, 0, len(w.watches.wd)) + for wd := range w.watches.wd { + all = append(all, wd) + } + sort.Ints(all) + + for _, wd := range all { + ww := w.watches.wd[wd] + fmt.Fprintf(os.Stderr, "%4d %q linkname=%q\n", wd, ww.name, ww.linkName) + } +} diff --git a/vendor/github.com/fsnotify/fsnotify/backend_windows.go b/vendor/github.com/fsnotify/fsnotify/backend_windows.go index 3433642d64..fb9210f24e 100644 --- a/vendor/github.com/fsnotify/fsnotify/backend_windows.go +++ b/vendor/github.com/fsnotify/fsnotify/backend_windows.go @@ -11,7 +11,6 @@ import ( "fmt" "os" "path/filepath" - "reflect" "runtime" "strings" "sync" @@ -37,6 +36,13 @@ type readDirChangesW struct { var defaultBufferSize = 50 +func isSameOrDescendantPath(path, root string) bool { + if path == root { + return true + } + return strings.HasPrefix(path, root+string(os.PathSeparator)) +} + func newBackend(ev chan Event, errs chan error) (backend, error) { port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0) if err != nil { @@ -359,22 +365,26 @@ func (w *readDirChangesW) addWatch(pathname string, flags uint64, bufsize int) e } else { windows.CloseHandle(ino.handle) } + w.mu.Lock() if pathname == dir { watchEntry.mask |= flags } else { watchEntry.names[filepath.Base(pathname)] |= flags } + w.mu.Unlock() err = w.startRead(watchEntry) if err != nil { return err } + w.mu.Lock() if pathname == dir { watchEntry.mask &= ^provisional } else { watchEntry.names[filepath.Base(pathname)] &= ^provisional } + w.mu.Unlock() return nil } @@ -394,8 +404,13 @@ func (w *readDirChangesW) remWatch(pathname string) error { w.mu.Lock() watch := w.watches.get(ino) w.mu.Unlock() + if watch == nil { + windows.CloseHandle(ino.handle) + return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname) + } if recurse && !watch.recurse { + windows.CloseHandle(ino.handle) return fmt.Errorf("can't use \\... with non-recursive watch %q", pathname) } @@ -403,16 +418,19 @@ func (w *readDirChangesW) remWatch(pathname string) error { if err != nil { w.sendError(os.NewSyscallError("CloseHandle", err)) } - if watch == nil { - return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname) - } if pathname == dir { - w.sendEvent(watch.path, "", watch.mask&sysFSIGNORED) + w.mu.Lock() + mask := watch.mask watch.mask = 0 + w.mu.Unlock() + w.sendEvent(watch.path, "", mask&sysFSIGNORED) } else { name := filepath.Base(pathname) - w.sendEvent(filepath.Join(watch.path, name), "", watch.names[name]&sysFSIGNORED) + w.mu.Lock() + mask := watch.names[name] delete(watch.names, name) + w.mu.Unlock() + w.sendEvent(filepath.Join(watch.path, name), "", mask&sysFSIGNORED) } return w.startRead(watch) @@ -420,17 +438,23 @@ func (w *readDirChangesW) remWatch(pathname string) error { // Must run within the I/O thread. func (w *readDirChangesW) deleteWatch(watch *watch) { - for name, mask := range watch.names { - if mask&provisional == 0 { - w.sendEvent(filepath.Join(watch.path, name), "", mask&sysFSIGNORED) + // Snapshot+clear under the lock so concurrent WatchList() readers see a + // consistent state. sendEvent must run outside the lock since it can + // block on the user-facing Events channel. + w.mu.Lock() + names := watch.names + watch.names = make(map[string]uint64) + mask := watch.mask + watch.mask = 0 + w.mu.Unlock() + + for name, m := range names { + if m&provisional == 0 { + w.sendEvent(filepath.Join(watch.path, name), "", m&sysFSIGNORED) } - delete(watch.names, name) } - if watch.mask != 0 { - if watch.mask&provisional == 0 { - w.sendEvent(watch.path, "", watch.mask&sysFSIGNORED) - } - watch.mask = 0 + if mask != 0 && mask&provisional == 0 { + w.sendEvent(watch.path, "", mask&sysFSIGNORED) } } @@ -457,9 +481,8 @@ func (w *readDirChangesW) startRead(watch *watch) error { } // We need to pass the array, rather than the slice. - hdr := (*reflect.SliceHeader)(unsafe.Pointer(&watch.buf)) rdErr := windows.ReadDirectoryChanges(watch.ino.handle, - (*byte)(unsafe.Pointer(hdr.Data)), uint32(hdr.Len), + unsafe.SliceData(watch.buf), uint32(len(watch.buf)), watch.recurse, mask, nil, &watch.ov, 0) if rdErr != nil { err := os.NewSyscallError("ReadDirectoryChanges", rdErr) @@ -565,12 +588,7 @@ func (w *readDirChangesW) readEvents() { // Create a buf that is the size of the path name size := int(raw.FileNameLength / 2) - var buf []uint16 - // TODO: Use unsafe.Slice in Go 1.17; https://stackoverflow.com/questions/51187973 - sh := (*reflect.SliceHeader)(unsafe.Pointer(&buf)) - sh.Data = uintptr(unsafe.Pointer(&raw.FileName)) - sh.Len = size - sh.Cap = size + buf := unsafe.Slice(&raw.FileName, size) name := windows.UTF16ToString(buf) fullname := filepath.Join(watch.path, name) @@ -587,31 +605,35 @@ func (w *readDirChangesW) readEvents() { case windows.FILE_ACTION_RENAMED_OLD_NAME: watch.rename = name case windows.FILE_ACTION_RENAMED_NEW_NAME: - // Update saved path of all sub-watches. + // Update saved path of all sub-watches and rename the + // names entry under the lock so WatchList() can't observe + // a torn state. old := filepath.Join(watch.path, watch.rename) w.mu.Lock() for _, watchMap := range w.watches { for _, ww := range watchMap { - if strings.HasPrefix(ww.path, old) { + if isSameOrDescendantPath(ww.path, old) { ww.path = filepath.Join(fullname, strings.TrimPrefix(ww.path, old)) } } } - w.mu.Unlock() - if watch.names[watch.rename] != 0 { watch.names[name] |= watch.names[watch.rename] delete(watch.names, watch.rename) mask = sysFSMOVESELF } + w.mu.Unlock() } if raw.Action != windows.FILE_ACTION_RENAMED_NEW_NAME { w.sendEvent(fullname, "", watch.names[name]&mask) } if raw.Action == windows.FILE_ACTION_REMOVED { - w.sendEvent(fullname, "", watch.names[name]&sysFSIGNORED) + w.mu.Lock() + ignored := watch.names[name] & sysFSIGNORED delete(watch.names, name) + w.mu.Unlock() + w.sendEvent(fullname, "", ignored) } if watch.rename != "" && raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME { diff --git a/vendor/github.com/fsnotify/fsnotify/fsnotify.go b/vendor/github.com/fsnotify/fsnotify/fsnotify.go index f64be4bf98..38cb4dd481 100644 --- a/vendor/github.com/fsnotify/fsnotify/fsnotify.go +++ b/vendor/github.com/fsnotify/fsnotify/fsnotify.go @@ -51,26 +51,25 @@ import ( // The fs.inotify.max_user_watches sysctl variable specifies the upper limit // for the number of watches per user, and fs.inotify.max_user_instances // specifies the maximum number of inotify instances per user. Every Watcher you -// create is an "instance", and every path you add is a "watch". +// create is an "instance", and every path you add is a "watch". Reaching the +// limit will result in a "no space left on device" or "too many open files" +// error. // // These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and -// /proc/sys/fs/inotify/max_user_instances +// /proc/sys/fs/inotify/max_user_instances. The default values differ per distro +// and available memory. // // To increase them you can use sysctl or write the value to the /proc file: // -// # Default values on Linux 5.18 -// sysctl fs.inotify.max_user_watches=124983 -// sysctl fs.inotify.max_user_instances=128 +// sysctl fs.inotify.max_user_watches=200000 +// sysctl fs.inotify.max_user_instances=256 // // To make the changes persist on reboot edit /etc/sysctl.conf or // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // your distro's documentation): // -// fs.inotify.max_user_watches=124983 -// fs.inotify.max_user_instances=128 -// -// Reaching the limit will result in a "no space left on device" or "too many open -// files" error. +// fs.inotify.max_user_watches=200000 +// fs.inotify.max_user_instances=256 // // # kqueue notes (macOS, BSD) // @@ -93,6 +92,28 @@ import ( // Sometimes it will send events for all files, sometimes it will send no // events, and often only for some files. // +// Recursive watching is not currently enabled through fsnotify's public +// API; the recursive code path is gated and only exercised by fsnotify's +// own tests. The note below describes backend behavior observed when +// recursive watching is enabled internally, and is kept here as a +// reference for maintainers and contributors who encounter it. +// +// When recursive watching is enabled and you watch a directory, you may +// receive a Write event for an intermediate directory whenever a child +// entry inside it is created, renamed, or removed. For example, with a +// recursive watch on /a and a new file /a/b/c, you will receive +// Create /a/b/c and may also receive Write /a/b. +// +// This happens because, on NTFS-backed volumes, modifying the entries of a +// directory updates that directory's last-write time, and the Windows +// backend requests FILE_NOTIFY_CHANGE_LAST_WRITE to support Write events +// on files. The same Write filter therefore picks up the directory's +// metadata update. +// +// Whether the directory Write is actually delivered alongside the child +// events is not guaranteed; it depends on ReadDirectoryChangesW buffering, +// NTFS metadata update timing, and event coalescing. +// // The default ReadDirectoryChangesW() buffer size is 64K, which is the largest // value that is guaranteed to work with SMB filesystems. If you have many // events in quick succession this may not be enough, and you will have to use @@ -129,8 +150,12 @@ type Watcher struct { // want to wait until you've stopped receiving them // (see the dedup example in cmd/fsnotify). // - // Some systems may send Write event for directories - // when the directory content changes. + // Some systems also send Write events for directories + // when the directory contents change. This is the + // case for kqueue, and on Windows for the directory + // that contains a created, renamed, or removed child + // entry. It does not happen on inotify. See the + // per-platform notes on [Watcher]. // // fsnotify.Chmod Attributes were changed. On Linux this is also sent // when a file is removed (or more accurately, when a @@ -179,7 +204,9 @@ const ( Create Op = 1 << iota // The pathname was written to; this does *not* mean the write has finished, - // and a write can be followed by more writes. + // and a write can be followed by more writes. On Windows and kqueue, a + // Write on a directory can also indicate that its contents changed; see + // the per-platform notes on [Watcher]. Write // The path was removed; any watches on it will be removed. Some "remove" @@ -220,7 +247,7 @@ const ( // File opened for reading was closed. // - // Only works on Linux and FreeBSD. + // Only works on Linux. xUnportableCloseRead ) @@ -410,7 +437,6 @@ type ( withOpts struct { bufsize int op Op - noFollow bool sendCreate bool } ) @@ -469,12 +495,6 @@ func withOps(op Op) addOpt { return func(opt *withOpts) { opt.op = op } } -// WithNoFollow disables following symlinks, so the symlinks themselves are -// watched. -func withNoFollow() addOpt { - return func(opt *withOpts) { opt.noFollow = true } -} - // "Internal" option for recursive watches on inotify. func withCreate() addOpt { return func(opt *withOpts) { opt.sendCreate = true } @@ -494,3 +514,13 @@ func recursivePath(path string) (string, bool) { } return path, false } + +type watchFlag uint8 + +const ( + // Added by user with Add(), rather than an internal watch. + flagByUser = watchFlag(0x01) + // Part of recursive watch; as the top-level path added by the user or an + // "internal" watch. + flagRecurse = watchFlag(0x02) +) diff --git a/vendor/github.com/fsnotify/fsnotify/internal/darwin.go b/vendor/github.com/fsnotify/fsnotify/internal/darwin.go index 0b01bc182a..6721aa6096 100644 --- a/vendor/github.com/fsnotify/fsnotify/internal/darwin.go +++ b/vendor/github.com/fsnotify/fsnotify/internal/darwin.go @@ -15,25 +15,6 @@ var ( var maxfiles uint64 -func SetRlimit() { - // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/ - var l syscall.Rlimit - err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l) - if err == nil && l.Cur != l.Max { - l.Cur = l.Max - syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l) - } - maxfiles = l.Cur - - if n, err := syscall.SysctlUint32("kern.maxfiles"); err == nil && uint64(n) < maxfiles { - maxfiles = uint64(n) - } - - if n, err := syscall.SysctlUint32("kern.maxfilesperproc"); err == nil && uint64(n) < maxfiles { - maxfiles = uint64(n) - } -} - func Maxfiles() uint64 { return maxfiles } func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) } diff --git a/vendor/github.com/fsnotify/fsnotify/internal/debug_darwin.go b/vendor/github.com/fsnotify/fsnotify/internal/debug_darwin.go index 928319fb09..7600180742 100644 --- a/vendor/github.com/fsnotify/fsnotify/internal/debug_darwin.go +++ b/vendor/github.com/fsnotify/fsnotify/internal/debug_darwin.go @@ -6,52 +6,10 @@ var names = []struct { n string m uint32 }{ - {"NOTE_ABSOLUTE", unix.NOTE_ABSOLUTE}, {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, - {"NOTE_BACKGROUND", unix.NOTE_BACKGROUND}, - {"NOTE_CHILD", unix.NOTE_CHILD}, - {"NOTE_CRITICAL", unix.NOTE_CRITICAL}, {"NOTE_DELETE", unix.NOTE_DELETE}, - {"NOTE_EXEC", unix.NOTE_EXEC}, - {"NOTE_EXIT", unix.NOTE_EXIT}, - {"NOTE_EXITSTATUS", unix.NOTE_EXITSTATUS}, - {"NOTE_EXIT_CSERROR", unix.NOTE_EXIT_CSERROR}, - {"NOTE_EXIT_DECRYPTFAIL", unix.NOTE_EXIT_DECRYPTFAIL}, - {"NOTE_EXIT_DETAIL", unix.NOTE_EXIT_DETAIL}, - {"NOTE_EXIT_DETAIL_MASK", unix.NOTE_EXIT_DETAIL_MASK}, - {"NOTE_EXIT_MEMORY", unix.NOTE_EXIT_MEMORY}, - {"NOTE_EXIT_REPARENTED", unix.NOTE_EXIT_REPARENTED}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, - {"NOTE_FFAND", unix.NOTE_FFAND}, - {"NOTE_FFCOPY", unix.NOTE_FFCOPY}, - {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK}, - {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK}, - {"NOTE_FFNOP", unix.NOTE_FFNOP}, - {"NOTE_FFOR", unix.NOTE_FFOR}, - {"NOTE_FORK", unix.NOTE_FORK}, - {"NOTE_FUNLOCK", unix.NOTE_FUNLOCK}, - {"NOTE_LEEWAY", unix.NOTE_LEEWAY}, {"NOTE_LINK", unix.NOTE_LINK}, - {"NOTE_LOWAT", unix.NOTE_LOWAT}, - {"NOTE_MACHTIME", unix.NOTE_MACHTIME}, - {"NOTE_MACH_CONTINUOUS_TIME", unix.NOTE_MACH_CONTINUOUS_TIME}, - {"NOTE_NONE", unix.NOTE_NONE}, - {"NOTE_NSECONDS", unix.NOTE_NSECONDS}, - {"NOTE_OOB", unix.NOTE_OOB}, - //{"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, -0x100000 (?!) - {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, - {"NOTE_REAP", unix.NOTE_REAP}, {"NOTE_RENAME", unix.NOTE_RENAME}, - {"NOTE_REVOKE", unix.NOTE_REVOKE}, - {"NOTE_SECONDS", unix.NOTE_SECONDS}, - {"NOTE_SIGNAL", unix.NOTE_SIGNAL}, - {"NOTE_TRACK", unix.NOTE_TRACK}, - {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, - {"NOTE_TRIGGER", unix.NOTE_TRIGGER}, - {"NOTE_USECONDS", unix.NOTE_USECONDS}, - {"NOTE_VM_ERROR", unix.NOTE_VM_ERROR}, - {"NOTE_VM_PRESSURE", unix.NOTE_VM_PRESSURE}, - {"NOTE_VM_PRESSURE_SUDDEN_TERMINATE", unix.NOTE_VM_PRESSURE_SUDDEN_TERMINATE}, - {"NOTE_VM_PRESSURE_TERMINATE", unix.NOTE_VM_PRESSURE_TERMINATE}, {"NOTE_WRITE", unix.NOTE_WRITE}, } diff --git a/vendor/github.com/fsnotify/fsnotify/internal/debug_dragonfly.go b/vendor/github.com/fsnotify/fsnotify/internal/debug_dragonfly.go index 3186b0c349..7600180742 100644 --- a/vendor/github.com/fsnotify/fsnotify/internal/debug_dragonfly.go +++ b/vendor/github.com/fsnotify/fsnotify/internal/debug_dragonfly.go @@ -7,27 +7,9 @@ var names = []struct { m uint32 }{ {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, - {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_DELETE", unix.NOTE_DELETE}, - {"NOTE_EXEC", unix.NOTE_EXEC}, - {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, - {"NOTE_FFAND", unix.NOTE_FFAND}, - {"NOTE_FFCOPY", unix.NOTE_FFCOPY}, - {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK}, - {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK}, - {"NOTE_FFNOP", unix.NOTE_FFNOP}, - {"NOTE_FFOR", unix.NOTE_FFOR}, - {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_LINK", unix.NOTE_LINK}, - {"NOTE_LOWAT", unix.NOTE_LOWAT}, - {"NOTE_OOB", unix.NOTE_OOB}, - {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, - {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_RENAME", unix.NOTE_RENAME}, - {"NOTE_REVOKE", unix.NOTE_REVOKE}, - {"NOTE_TRACK", unix.NOTE_TRACK}, - {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, - {"NOTE_TRIGGER", unix.NOTE_TRIGGER}, {"NOTE_WRITE", unix.NOTE_WRITE}, } diff --git a/vendor/github.com/fsnotify/fsnotify/internal/debug_freebsd.go b/vendor/github.com/fsnotify/fsnotify/internal/debug_freebsd.go index f69fdb930f..b9e45f5551 100644 --- a/vendor/github.com/fsnotify/fsnotify/internal/debug_freebsd.go +++ b/vendor/github.com/fsnotify/fsnotify/internal/debug_freebsd.go @@ -6,37 +6,15 @@ var names = []struct { n string m uint32 }{ - {"NOTE_ABSTIME", unix.NOTE_ABSTIME}, - {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, - {"NOTE_CHILD", unix.NOTE_CHILD}, - {"NOTE_CLOSE", unix.NOTE_CLOSE}, - {"NOTE_CLOSE_WRITE", unix.NOTE_CLOSE_WRITE}, {"NOTE_DELETE", unix.NOTE_DELETE}, - {"NOTE_EXEC", unix.NOTE_EXEC}, - {"NOTE_EXIT", unix.NOTE_EXIT}, + {"NOTE_WRITE", unix.NOTE_WRITE}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, - {"NOTE_FFAND", unix.NOTE_FFAND}, - {"NOTE_FFCOPY", unix.NOTE_FFCOPY}, - {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK}, - {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK}, - {"NOTE_FFNOP", unix.NOTE_FFNOP}, - {"NOTE_FFOR", unix.NOTE_FFOR}, - {"NOTE_FILE_POLL", unix.NOTE_FILE_POLL}, - {"NOTE_FORK", unix.NOTE_FORK}, + {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, {"NOTE_LINK", unix.NOTE_LINK}, - {"NOTE_LOWAT", unix.NOTE_LOWAT}, - {"NOTE_MSECONDS", unix.NOTE_MSECONDS}, - {"NOTE_NSECONDS", unix.NOTE_NSECONDS}, - {"NOTE_OPEN", unix.NOTE_OPEN}, - {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, - {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, - {"NOTE_READ", unix.NOTE_READ}, {"NOTE_RENAME", unix.NOTE_RENAME}, {"NOTE_REVOKE", unix.NOTE_REVOKE}, - {"NOTE_SECONDS", unix.NOTE_SECONDS}, - {"NOTE_TRACK", unix.NOTE_TRACK}, - {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, - {"NOTE_TRIGGER", unix.NOTE_TRIGGER}, - {"NOTE_USECONDS", unix.NOTE_USECONDS}, - {"NOTE_WRITE", unix.NOTE_WRITE}, + {"NOTE_OPEN", unix.NOTE_OPEN}, + {"NOTE_CLOSE", unix.NOTE_CLOSE}, + {"NOTE_CLOSE_WRITE", unix.NOTE_CLOSE_WRITE}, + {"NOTE_READ", unix.NOTE_READ}, } diff --git a/vendor/github.com/fsnotify/fsnotify/internal/debug_kqueue.go b/vendor/github.com/fsnotify/fsnotify/internal/debug_kqueue.go index 607e683bd7..5d8116436d 100644 --- a/vendor/github.com/fsnotify/fsnotify/internal/debug_kqueue.go +++ b/vendor/github.com/fsnotify/fsnotify/internal/debug_kqueue.go @@ -27,6 +27,6 @@ func Debug(name string, kevent *unix.Kevent_t) { if unknown > 0 { l = append(l, fmt.Sprintf("0x%x", unknown)) } - fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %10d:%-60s → %q\n", + fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %10d:%-20s → %q\n", time.Now().Format("15:04:05.000000000"), mask, strings.Join(l, " | "), name) } diff --git a/vendor/github.com/fsnotify/fsnotify/internal/debug_netbsd.go b/vendor/github.com/fsnotify/fsnotify/internal/debug_netbsd.go index e5b3b6f694..7600180742 100644 --- a/vendor/github.com/fsnotify/fsnotify/internal/debug_netbsd.go +++ b/vendor/github.com/fsnotify/fsnotify/internal/debug_netbsd.go @@ -7,19 +7,9 @@ var names = []struct { m uint32 }{ {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, - {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_DELETE", unix.NOTE_DELETE}, - {"NOTE_EXEC", unix.NOTE_EXEC}, - {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, - {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_LINK", unix.NOTE_LINK}, - {"NOTE_LOWAT", unix.NOTE_LOWAT}, - {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, - {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_RENAME", unix.NOTE_RENAME}, - {"NOTE_REVOKE", unix.NOTE_REVOKE}, - {"NOTE_TRACK", unix.NOTE_TRACK}, - {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, {"NOTE_WRITE", unix.NOTE_WRITE}, } diff --git a/vendor/github.com/fsnotify/fsnotify/internal/debug_openbsd.go b/vendor/github.com/fsnotify/fsnotify/internal/debug_openbsd.go index 1dd455bc5a..871766d699 100644 --- a/vendor/github.com/fsnotify/fsnotify/internal/debug_openbsd.go +++ b/vendor/github.com/fsnotify/fsnotify/internal/debug_openbsd.go @@ -7,22 +7,10 @@ var names = []struct { m uint32 }{ {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, - // {"NOTE_CHANGE", unix.NOTE_CHANGE}, // Not on 386? - {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_DELETE", unix.NOTE_DELETE}, - {"NOTE_EOF", unix.NOTE_EOF}, - {"NOTE_EXEC", unix.NOTE_EXEC}, - {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, - {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_LINK", unix.NOTE_LINK}, - {"NOTE_LOWAT", unix.NOTE_LOWAT}, - {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, - {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_RENAME", unix.NOTE_RENAME}, - {"NOTE_REVOKE", unix.NOTE_REVOKE}, - {"NOTE_TRACK", unix.NOTE_TRACK}, - {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, {"NOTE_TRUNCATE", unix.NOTE_TRUNCATE}, {"NOTE_WRITE", unix.NOTE_WRITE}, } diff --git a/vendor/github.com/fsnotify/fsnotify/internal/freebsd.go b/vendor/github.com/fsnotify/fsnotify/internal/freebsd.go index 5ac8b50797..758a249052 100644 --- a/vendor/github.com/fsnotify/fsnotify/internal/freebsd.go +++ b/vendor/github.com/fsnotify/fsnotify/internal/freebsd.go @@ -15,17 +15,6 @@ var ( var maxfiles uint64 -func SetRlimit() { - // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/ - var l syscall.Rlimit - err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l) - if err == nil && l.Cur != l.Max { - l.Cur = l.Max - syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l) - } - maxfiles = uint64(l.Cur) -} - func Maxfiles() uint64 { return maxfiles } func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, uint64(dev)) } diff --git a/vendor/github.com/fsnotify/fsnotify/internal/unix.go b/vendor/github.com/fsnotify/fsnotify/internal/unix.go index b251fb8038..9c66f5d30d 100644 --- a/vendor/github.com/fsnotify/fsnotify/internal/unix.go +++ b/vendor/github.com/fsnotify/fsnotify/internal/unix.go @@ -15,17 +15,6 @@ var ( var maxfiles uint64 -func SetRlimit() { - // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/ - var l syscall.Rlimit - err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l) - if err == nil && l.Cur != l.Max { - l.Cur = l.Max - syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l) - } - maxfiles = uint64(l.Cur) -} - func Maxfiles() uint64 { return maxfiles } func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) } diff --git a/vendor/github.com/fsnotify/fsnotify/internal/unix2.go b/vendor/github.com/fsnotify/fsnotify/internal/unix2.go index 37dfeddc28..b2d89592f7 100644 --- a/vendor/github.com/fsnotify/fsnotify/internal/unix2.go +++ b/vendor/github.com/fsnotify/fsnotify/internal/unix2.go @@ -2,6 +2,24 @@ package internal +import "syscall" + func HasPrivilegesForSymlink() bool { return true } + +// IgnoringEINTR makes a function call and repeats it if it returns an +// EINTR error. This appears to be required even though we install all +// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846. +// Also #20400 and #36644 are issues in which a signal handler is +// installed without setting SA_RESTART. None of these are the common case, +// but there are enough of them that it seems that we can't avoid +// an EINTR loop. +func IgnoringEINTR[T any](fn func() (T, error)) (T, error) { + for { + v, err := fn() + if err != syscall.EINTR { + return v, err + } + } +} diff --git a/vendor/github.com/fsnotify/fsnotify/internal/windows.go b/vendor/github.com/fsnotify/fsnotify/internal/windows.go index 896bc2e5a2..e24d569264 100644 --- a/vendor/github.com/fsnotify/fsnotify/internal/windows.go +++ b/vendor/github.com/fsnotify/fsnotify/internal/windows.go @@ -14,7 +14,6 @@ var ( ErrUnixEACCES = errors.New("dummy") ) -func SetRlimit() {} func Maxfiles() uint64 { return 1<<64 - 1 } func Mkfifo(path string, mode uint32) error { return errors.New("no FIFOs on Windows") } func Mknod(path string, mode uint32, dev int) error { return errors.New("no device nodes on Windows") } diff --git a/vendor/github.com/goccy/go-json/internal/encoder/code.go b/vendor/github.com/goccy/go-json/internal/encoder/code.go index 5b08faefc7..fec45a4b89 100644 --- a/vendor/github.com/goccy/go-json/internal/encoder/code.go +++ b/vendor/github.com/goccy/go-json/internal/encoder/code.go @@ -518,6 +518,7 @@ func (c *StructCode) ToAnonymousOpcode(ctx *compileContext) Opcodes { prevField = firstField codes = codes.Add(fieldCodes...) } + ctx.structTypeToCodes[uintptr(unsafe.Pointer(c.typ))] = codes return codes } diff --git a/vendor/github.com/lestrrat-go/dsig/.goreleaser.yml b/vendor/github.com/lestrrat-go/dsig/.goreleaser.yml new file mode 100644 index 0000000000..b231abdf2e --- /dev/null +++ b/vendor/github.com/lestrrat-go/dsig/.goreleaser.yml @@ -0,0 +1,7 @@ +version: 2 + +builds: + - skip: true + +changelog: + use: github-native diff --git a/vendor/github.com/lestrrat-go/dsig/Changes b/vendor/github.com/lestrrat-go/dsig/Changes index bccce97613..5e7a522cd1 100644 --- a/vendor/github.com/lestrrat-go/dsig/Changes +++ b/vendor/github.com/lestrrat-go/dsig/Changes @@ -1,5 +1,28 @@ Changes ======= +v1.2.1 7 Apr 2026 + * Add `SignDigest()` for signing pre-computed digests. Supported for HMAC, + RSA (PKCS1v15 and PSS), and ECDSA families. EdDSA and Custom return an error. + +v1.2.0 6 Apr 2026 + * Add `VerifyDigest()` for verifying signatures against pre-computed digests. + Supported for HMAC, RSA, and ECDSA families. EdDSA and Custom return an error. + + * Add low-level digest verification functions: `VerifyHMACDigest()`, + `VerifyRSADigest()`, `VerifyECDSADigest()`. + +v1.1.0 2 Apr 2026 + * Add `Custom` algorithm family for registering user-defined sign/verify + implementations. For the `Custom` family, `AlgorithmInfo.Meta` must implement + the `Signer` and/or `Verifier` interfaces. The implementation struct can + carry any additional metadata it needs (hash functions, curves, etc.). + + * Add `UnregisterAlgorithm()` for removing previously registered custom + algorithms. Built-in algorithms are protected and cannot be unregistered. + + * `RegisterAlgorithm()` now rejects re-registration of an already-registered + algorithm name. Use `UnregisterAlgorithm()` first if you need to replace it. + v1.0.0 - 18 Aug 2025 -* Initial release \ No newline at end of file + * Initial release \ No newline at end of file diff --git a/vendor/github.com/lestrrat-go/dsig/README.md b/vendor/github.com/lestrrat-go/dsig/README.md index 37c194579e..b52b998f8f 100644 --- a/vendor/github.com/lestrrat-go/dsig/README.md +++ b/vendor/github.com/lestrrat-go/dsig/README.md @@ -11,7 +11,7 @@ While there are many standards for generating and verifying digital signatures, * EdDSA signatures (Ed25519, Ed448) * HMAC signatures (SHA-256, SHA-384, SHA-512) * Support for crypto.Signer interface -* Allows for dynamic additions of algorithms in limited cases. +* Custom algorithm registration via `Signer`/`Verifier` interfaces # SYNOPSIS diff --git a/vendor/github.com/lestrrat-go/dsig/dsig.go b/vendor/github.com/lestrrat-go/dsig/dsig.go index de6cbdec45..a6b54418a7 100644 --- a/vendor/github.com/lestrrat-go/dsig/dsig.go +++ b/vendor/github.com/lestrrat-go/dsig/dsig.go @@ -14,6 +14,7 @@ import ( "crypto/sha512" "fmt" "hash" + "io" "sync" ) @@ -26,6 +27,7 @@ const ( RSA ECDSA EdDSAFamily + Custom maxFamily ) @@ -40,6 +42,8 @@ func (f Family) String() string { return "ECDSA" case EdDSAFamily: return "EdDSA" + case Custom: + return "Custom" default: return "InvalidFamily" } @@ -73,18 +77,43 @@ type EdDSAFamilyMeta struct { // Reserved for future use } +// Signer is an interface for custom signing implementations. +// For the Custom algorithm family, info.Meta must implement this interface +// to support signing. The implementation struct can carry any additional +// metadata it needs (hash functions, curves, etc.). +type Signer interface { + Sign(key any, payload []byte, rand io.Reader) ([]byte, error) +} + +// Verifier is an interface for custom verification implementations. +// For the Custom algorithm family, info.Meta must implement this interface +// to support verification. The implementation struct can carry any additional +// metadata it needs (hash functions, curves, etc.). +type Verifier interface { + Verify(key any, payload, signature []byte) error +} + var algorithms = make(map[string]AlgorithmInfo) +var builtinAlgorithms = make(map[string]struct{}) var muAlgorithms sync.RWMutex // RegisterAlgorithm registers a new digital signature algorithm with the specified family and metadata. // -// info.Meta should contain extra metadata for some algorithms. Currently HMAC, RSA, -// and ECDSA family of algorithms need their respective metadata (HMACFamilyMeta, -// RSAFamilyMeta, and ECDSAFamilyMeta). Metadata for other families are ignored. +// info.Meta should contain extra metadata for some algorithms. HMAC, RSA, and ECDSA +// families need their respective metadata (HMACFamilyMeta, RSAFamilyMeta, and +// ECDSAFamilyMeta). Metadata for EdDSA is optional. For the Custom family, Meta +// must implement at least one of the Signer or Verifier interfaces. +// +// Re-registration of an already-registered algorithm name is rejected. Use +// UnregisterAlgorithm to remove it first if you need to replace it. func RegisterAlgorithm(name string, info AlgorithmInfo) error { muAlgorithms.Lock() defer muAlgorithms.Unlock() + if _, exists := algorithms[name]; exists { + return fmt.Errorf("algorithm %s is already registered", name) + } + // Validate the metadata matches the family switch info.Family { case HMAC: @@ -101,6 +130,12 @@ func RegisterAlgorithm(name string, info AlgorithmInfo) error { } case EdDSAFamily: // EdDSA metadata is optional for now + case Custom: + _, isSigner := info.Meta.(Signer) + _, isVerifier := info.Meta.(Verifier) + if !isSigner && !isVerifier { + return fmt.Errorf("custom algorithm %s: Meta must implement Signer and/or Verifier", name) + } default: return fmt.Errorf("unsupported algorithm family %s for algorithm %s", info.Family, name) } @@ -109,6 +144,21 @@ func RegisterAlgorithm(name string, info AlgorithmInfo) error { return nil } +// UnregisterAlgorithm removes a previously registered algorithm by name. +// Built-in algorithms cannot be unregistered. +// It is a no-op if the algorithm is not registered. +func UnregisterAlgorithm(name string) error { + muAlgorithms.Lock() + defer muAlgorithms.Unlock() + + if _, ok := builtinAlgorithms[name]; ok { + return fmt.Errorf("algorithm %s is a built-in algorithm and cannot be unregistered", name) + } + + delete(algorithms, name) + return nil +} + // GetAlgorithmInfo retrieves the algorithm information for a given algorithm name. // Returns the info and true if found, zero value and false if not found. func GetAlgorithmInfo(name string) (AlgorithmInfo, bool) { @@ -219,6 +269,7 @@ func init() { if err := RegisterAlgorithm(name, info); err != nil { panic(fmt.Sprintf("failed to register algorithm %s: %v", name, err)) } + builtinAlgorithms[name] = struct{}{} } } diff --git a/vendor/github.com/lestrrat-go/dsig/ecdsa.go b/vendor/github.com/lestrrat-go/dsig/ecdsa.go index a04a266919..4041d9c53d 100644 --- a/vendor/github.com/lestrrat-go/dsig/ecdsa.go +++ b/vendor/github.com/lestrrat-go/dsig/ecdsa.go @@ -176,6 +176,21 @@ func VerifyECDSA(key *ecdsa.PublicKey, payload, signature []byte, h crypto.Hash) return ecdsaVerify(key, payload, h, &r, &s) } +// VerifyECDSADigest verifies an ECDSA signature given a pre-computed digest. +// The caller is responsible for hashing the signing input with the correct +// hash function for the algorithm (e.g. SHA-256 for ES256). This function +// does not validate the digest length. +func VerifyECDSADigest(key *ecdsa.PublicKey, digest, signature []byte) error { + var r, s big.Int + if err := UnpackECDSASignature(signature, key, &r, &s); err != nil { + return fmt.Errorf("dsig.VerifyECDSADigest: %w", err) + } + if !ecdsa.Verify(key, digest, &r, &s) { + return NewVerificationError("invalid ECDSA signature") + } + return nil +} + // VerifyECDSACryptoSigner verifies an ECDSA signature for crypto.Signer implementations. // This function is useful for verifying signatures created by hardware security modules // or other implementations of the crypto.Signer interface. diff --git a/vendor/github.com/lestrrat-go/dsig/hmac.go b/vendor/github.com/lestrrat-go/dsig/hmac.go index 8b2612279d..ad3d86c62a 100644 --- a/vendor/github.com/lestrrat-go/dsig/hmac.go +++ b/vendor/github.com/lestrrat-go/dsig/hmac.go @@ -30,6 +30,14 @@ func SignHMAC(key, payload []byte, hfunc func() hash.Hash) ([]byte, error) { return h.Sum(nil), nil } +// VerifyHMACDigest verifies an HMAC signature given a pre-computed MAC. +func VerifyHMACDigest(computedMAC, signature []byte) error { + if !hmac.Equal(computedMAC, signature) { + return NewVerificationError("invalid HMAC signature") + } + return nil +} + // VerifyHMAC verifies an HMAC signature for the given payload. // This function verifies the signature using the specified key and hash function. // The payload parameter should be the pre-computed signing input (typically header.payload). diff --git a/vendor/github.com/lestrrat-go/dsig/rsa.go b/vendor/github.com/lestrrat-go/dsig/rsa.go index a339fe5b78..b052066944 100644 --- a/vendor/github.com/lestrrat-go/dsig/rsa.go +++ b/vendor/github.com/lestrrat-go/dsig/rsa.go @@ -61,3 +61,17 @@ func VerifyRSA(key *rsa.PublicKey, payload, signature []byte, h crypto.Hash, pss } return rsa.VerifyPKCS1v15(key, h, digest, signature) } + +// VerifyRSADigest verifies an RSA signature given a pre-computed digest. +// If pss is true, RSA-PSS verification is used; otherwise, PKCS#1 v1.5 is used. +func VerifyRSADigest(key *rsa.PublicKey, digest, signature []byte, h crypto.Hash, pss bool) error { + // isValidRSAKey only rejects non-RSA private key types, so this check is + // a no-op for *rsa.PublicKey. Kept for consistency with VerifyRSA. + if !isValidRSAKey(key) { + return fmt.Errorf(`invalid key type %T for RSA algorithm`, key) + } + if pss { + return rsa.VerifyPSS(key, h, digest, signature, &rsa.PSSOptions{Hash: h, SaltLength: rsa.PSSSaltLengthEqualsHash}) + } + return rsa.VerifyPKCS1v15(key, h, digest, signature) +} diff --git a/vendor/github.com/lestrrat-go/dsig/sign.go b/vendor/github.com/lestrrat-go/dsig/sign.go index e2a6bde290..eb57f5eec6 100644 --- a/vendor/github.com/lestrrat-go/dsig/sign.go +++ b/vendor/github.com/lestrrat-go/dsig/sign.go @@ -2,6 +2,8 @@ package dsig import ( "crypto" + "crypto/ecdsa" + "crypto/rand" "crypto/rsa" "fmt" "io" @@ -9,9 +11,6 @@ import ( // Sign generates a digital signature using the specified key and algorithm. // -// This function loads the signer registered in the dsig package _ONLY_. -// It does not support custom signers that the user might have registered. -// // rr is an io.Reader that provides randomness for signing. If rr is nil, it defaults to rand.Reader. // Not all algorithms require this parameter, but it is included for consistency. // 99% of the time, you can pass nil for rr, and it will work fine. @@ -30,6 +29,8 @@ func Sign(key any, alg string, payload []byte, rr io.Reader) ([]byte, error) { return dispatchECDSASign(key, info, payload, rr) case EdDSAFamily: return dispatchEdDSASign(key, info, payload, rr) + case Custom: + return dispatchCustomSign(key, info, payload, rr) default: return nil, fmt.Errorf(`dsig.Sign: unsupported signature family %q`, info.Family) } @@ -98,3 +99,112 @@ func dispatchECDSASign(key any, info AlgorithmInfo, payload []byte, rr io.Reader } return SignECDSA(privkey, payload, meta.Hash, rr) } + +func dispatchCustomSign(key any, info AlgorithmInfo, payload []byte, rr io.Reader) ([]byte, error) { + signer, ok := info.Meta.(Signer) + if !ok { + return nil, fmt.Errorf(`dsig.Sign: algorithm has no signer registered`) + } + return signer.Sign(key, payload, rr) +} + +// SignDigest generates a digital signature from a pre-computed digest. +// +// For RSA/ECDSA, digest is the hash of the signing input and key is the +// private key used for signing. +// +// For HMAC, the digest must be the pre-computed MAC (i.e. the output of +// hmac.New(hashFunc, key) after writing the signing input). The digest IS +// the signature, so it is returned as-is. +// +// EdDSA and Custom families are not supported and return an error. +// +// rr is an io.Reader that provides randomness for signing. If rr is nil, +// it defaults to rand.Reader. +func SignDigest(key any, alg string, digest []byte, rr io.Reader) ([]byte, error) { + info, ok := GetAlgorithmInfo(alg) + if !ok { + return nil, fmt.Errorf(`dsig.SignDigest: unsupported signature algorithm %q`, alg) + } + + switch info.Family { + case HMAC: + // The caller already computed the HMAC (which incorporates the key) + // and passed it as digest. The digest IS the signature. + return digest, nil + case RSA: + return dispatchRSASignDigest(key, info, digest, rr) + case ECDSA: + return dispatchECDSASignDigest(key, info, digest, rr) + case EdDSAFamily: + return nil, fmt.Errorf(`dsig.SignDigest: EdDSA does not support digest-based signing`) + case Custom: + return nil, fmt.Errorf(`dsig.SignDigest: custom algorithms do not support digest-based signing`) + default: + return nil, fmt.Errorf(`dsig.SignDigest: unsupported signature family %q`, info.Family) + } +} + +func dispatchRSASignDigest(key any, info AlgorithmInfo, digest []byte, rr io.Reader) ([]byte, error) { + meta, ok := info.Meta.(RSAFamilyMeta) + if !ok { + return nil, fmt.Errorf(`dsig.SignDigest: invalid RSA metadata`) + } + + if rr == nil { + rr = rand.Reader + } + + cs, isCryptoSigner, err := rsaGetSignerCryptoSignerKey(key) + if err != nil { + return nil, fmt.Errorf(`dsig.SignDigest: %w`, err) + } + if isCryptoSigner { + var opts crypto.SignerOpts = meta.Hash + if meta.PSS { + rsaopts := rsaPSSOptions(meta.Hash) + opts = &rsaopts + } + return cs.Sign(rr, digest, opts) + } + + privkey, ok := key.(*rsa.PrivateKey) + if !ok { + return nil, fmt.Errorf(`dsig.SignDigest: invalid key type %T. *rsa.PrivateKey is required`, key) + } + if meta.PSS { + rsaopts := rsaPSSOptions(meta.Hash) + return rsa.SignPSS(rr, privkey, meta.Hash, digest, &rsaopts) + } + return rsa.SignPKCS1v15(rr, privkey, meta.Hash, digest) +} + +func dispatchECDSASignDigest(key any, info AlgorithmInfo, digest []byte, rr io.Reader) ([]byte, error) { + meta, ok := info.Meta.(ECDSAFamilyMeta) + if !ok { + return nil, fmt.Errorf(`dsig.SignDigest: invalid ECDSA metadata`) + } + + if rr == nil { + rr = rand.Reader + } + + privkey, cs, isCryptoSigner, err := ecdsaGetSignerKey(key) + if err != nil { + return nil, fmt.Errorf(`dsig.SignDigest: %w`, err) + } + if isCryptoSigner { + signed, err := cs.Sign(rr, digest, meta.Hash) + if err != nil { + return nil, fmt.Errorf(`dsig.SignDigest: failed to sign digest using crypto.Signer: %w`, err) + } + return signECDSACryptoSigner(cs, signed) + } + + r, s, err := ecdsa.Sign(rr, privkey, digest) + if err != nil { + return nil, fmt.Errorf(`dsig.SignDigest: failed to sign digest using ecdsa: %w`, err) + } + return PackECDSASignature(r, s, privkey.Curve.Params().BitSize) +} + diff --git a/vendor/github.com/lestrrat-go/dsig/verify.go b/vendor/github.com/lestrrat-go/dsig/verify.go index 86085b0a37..05ffe8e94f 100644 --- a/vendor/github.com/lestrrat-go/dsig/verify.go +++ b/vendor/github.com/lestrrat-go/dsig/verify.go @@ -9,9 +9,6 @@ import ( ) // Verify verifies a digital signature using the specified key and algorithm. -// -// This function loads the verifier registered in the dsig package _ONLY_. -// It does not support custom verifiers that the user might have registered. func Verify(key any, alg string, payload, signature []byte) error { info, ok := GetAlgorithmInfo(alg) if !ok { @@ -27,11 +24,100 @@ func Verify(key any, alg string, payload, signature []byte) error { return dispatchECDSAVerify(key, info, payload, signature) case EdDSAFamily: return dispatchEdDSAVerify(key, info, payload, signature) + case Custom: + return dispatchCustomVerify(key, info, payload, signature) default: return fmt.Errorf(`dsig.Verify: unsupported signature family %q`, info.Family) } } +// VerifyDigest verifies a signature given a pre-computed digest. +// +// For RSA/ECDSA, digest is the hash of the signing input and key is the +// public key used for verification. +// +// For HMAC, digest must be the pre-computed MAC (i.e. the output of +// hmac.New(hashFunc, key) after writing the signing input). The key +// parameter is not used because it is already incorporated into the MAC. +// +// EdDSA and Custom families are not supported and return an error. +func VerifyDigest(key any, alg string, digest, signature []byte) error { + info, ok := GetAlgorithmInfo(alg) + if !ok { + return fmt.Errorf(`dsig.VerifyDigest: unsupported signature algorithm %q`, alg) + } + + switch info.Family { + case HMAC: + // key is not used here: the caller has already computed the HMAC + // (which incorporates the key) and passed it as digest. + return VerifyHMACDigest(digest, signature) + case RSA: + return dispatchRSAVerifyDigest(key, info, digest, signature) + case ECDSA: + return dispatchECDSAVerifyDigest(key, info, digest, signature) + case EdDSAFamily: + return fmt.Errorf(`dsig.VerifyDigest: EdDSA does not support digest-based verification`) + case Custom: + // TODO: a DigestVerifier interface (optional, checked here) would let + // custom algorithms opt in to digest-based verification. + return fmt.Errorf(`dsig.VerifyDigest: custom algorithms do not support digest-based verification`) + default: + return fmt.Errorf(`dsig.VerifyDigest: unsupported signature family %q`, info.Family) + } +} + +func dispatchRSAVerifyDigest(key any, info AlgorithmInfo, digest, signature []byte) error { + meta, ok := info.Meta.(RSAFamilyMeta) + if !ok { + return fmt.Errorf(`dsig.VerifyDigest: invalid RSA metadata`) + } + + var pubkey *rsa.PublicKey + + if cs, ok := key.(crypto.Signer); ok { + cpub := cs.Public() + switch cpub := cpub.(type) { + case rsa.PublicKey: + pubkey = &cpub + case *rsa.PublicKey: + pubkey = cpub + default: + return fmt.Errorf(`dsig.VerifyDigest: failed to retrieve rsa.PublicKey out of crypto.Signer %T`, key) + } + } else { + var ok bool + pubkey, ok = key.(*rsa.PublicKey) + if !ok { + return fmt.Errorf(`dsig.VerifyDigest: failed to retrieve *rsa.PublicKey out of %T`, key) + } + } + + return VerifyRSADigest(pubkey, digest, signature, meta.Hash, meta.PSS) +} + +// Note: the crypto.Signer → *ecdsa.PublicKey extraction below duplicates +// logic in VerifyECDSACryptoSigner. We can't call that function because it +// hashes the payload internally. If the extraction logic changes, update both. +func dispatchECDSAVerifyDigest(key any, info AlgorithmInfo, digest, signature []byte) error { + pubkey, cs, isCryptoSigner, err := ecdsaGetVerifierKey(key) + if err != nil { + return fmt.Errorf(`dsig.VerifyDigest: %w`, err) + } + if isCryptoSigner { + cpub := cs.Public() + switch cpub := cpub.(type) { + case ecdsa.PublicKey: + pubkey = &cpub + case *ecdsa.PublicKey: + pubkey = cpub + default: + return fmt.Errorf(`dsig.VerifyDigest: expected *ecdsa.PublicKey from crypto.Signer, got %T`, cpub) + } + } + return VerifyECDSADigest(pubkey, digest, signature) +} + func dispatchHMACVerify(key any, info AlgorithmInfo, payload, signature []byte) error { meta, ok := info.Meta.(HMACFamilyMeta) if !ok { @@ -110,6 +196,14 @@ func dispatchEdDSAVerify(key any, _ AlgorithmInfo, payload, signature []byte) er return VerifyEdDSA(pubkey, payload, signature) } +func dispatchCustomVerify(key any, info AlgorithmInfo, payload, signature []byte) error { + verifier, ok := info.Meta.(Verifier) + if !ok { + return fmt.Errorf(`dsig.Verify: algorithm has no verifier registered`) + } + return verifier.Verify(key, payload, signature) +} + func ecdsaGetVerifierKey(key any) (*ecdsa.PublicKey, crypto.Signer, bool, error) { cs, isCryptoSigner := key.(crypto.Signer) if isCryptoSigner { diff --git a/vendor/github.com/lestrrat-go/httprc/v3/Changes b/vendor/github.com/lestrrat-go/httprc/v3/Changes index 6a5eb8064a..001c8c5444 100644 --- a/vendor/github.com/lestrrat-go/httprc/v3/Changes +++ b/vendor/github.com/lestrrat-go/httprc/v3/Changes @@ -1,6 +1,22 @@ Changes ======= +v3.0.5 30 Mar 2026 + * Fix periodic check deadlock when number of ready resources exceeds + outgoing channel buffer, which caused circular wait between controller + and worker goroutines (#113, #116) + * Fix proxysink self-deadlock caused by missing mutex unlock on context + cancellation path + +v3.0.4 08 Feb 2026 + * Fix worker goroutine dying on sync refresh failure, which could cause + deadlocks after repeated failures (lestrrat-go/jwx#1551) + * Move ErrNotReady example functions out of client_example_test.go into + separate files to avoid triggering autodoc workflow + +v3.0.3 23 Dec 2025 + * Add ErrNotReady error state to avoid waiting for unstable URLs + v3.0.2 05 Dev 2025 * Code changes mainly due to upgraded linter. * github.com/lestrrat-go/option upgraded to v2 diff --git a/vendor/github.com/lestrrat-go/httprc/v3/backend.go b/vendor/github.com/lestrrat-go/httprc/v3/backend.go index 31f9fc07d3..713de23de2 100644 --- a/vendor/github.com/lestrrat-go/httprc/v3/backend.go +++ b/vendor/github.com/lestrrat-go/httprc/v3/backend.go @@ -118,14 +118,6 @@ func (c *ctrlBackend) handleRequest(ctx context.Context, req any) { } } -func sendWorker(ctx context.Context, ch chan Resource, r Resource) { - r.SetBusy(true) - select { - case <-ctx.Done(): - case ch <- r: - } -} - func sendWorkerSynchronous(ctx context.Context, ch chan synchronousRequest, r synchronousRequest) { r.resource.SetBusy(true) select { @@ -159,26 +151,67 @@ func (c *ctrlBackend) loop(ctx context.Context, readywg, donewg *sync.WaitGroup) readywg.Done() defer c.traceSink.Put(ctx, "httprc controller: stopping main controller loop") defer donewg.Done() + + var pending []Resource for { - c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: waiting for request or tick (tick interval=%s)", c.tickInterval)) - select { - case req := <-c.incoming: - c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: got request %T", req)) - c.handleRequest(ctx, req) - case t := <-c.check.C: - c.periodicCheck(ctx, t) - case <-ctx.Done(): - return + if len(pending) > 0 { + // Dispatch pending items while remaining responsive to incoming + // requests. This prevents a deadlock where periodicCheck blocks + // on c.outgoing while a worker blocks on c.incoming (issue #113). + + // Skip resources that were removed (or replaced) after periodicCheck + // queued them. Without this check, a stale resource could be sent to + // a worker, causing an unnecessary fetch and a subsequent + // adjustIntervalRequest for a resource that is no longer registered. + r := pending[0] + // Compare interface values directly. This is safe because all + // Resource implementations are pointer types (*ResourceBase[T]), + // so the comparison is a pointer identity check. + if cur, ok := c.items[r.URL()]; !ok || cur != r { + c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: skipping pending resource %q (no longer registered or replaced)", r.URL())) + r.SetBusy(false) + pending = pending[1:] + continue + } + + c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: dispatching pending resource %q to worker pool (%d remaining)", pending[0].URL(), len(pending))) + select { + case req := <-c.incoming: + c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: got request %T (while dispatching)", req)) + c.handleRequest(ctx, req) + case c.outgoing <- pending[0]: + pending = pending[1:] + case t := <-c.check.C: + pending = append(pending, c.periodicCheck(ctx, t)...) + case <-ctx.Done(): + return + } + } else { + c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: waiting for request or tick (tick interval=%s)", c.tickInterval)) + select { + case req := <-c.incoming: + c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: got request %T", req)) + c.handleRequest(ctx, req) + case t := <-c.check.C: + pending = c.periodicCheck(ctx, t) + case <-ctx.Done(): + return + } } } } -func (c *ctrlBackend) periodicCheck(ctx context.Context, t time.Time) { +// periodicCheck examines all registered resources and returns those that are +// due for refresh. Items are marked busy here so they won't be selected again +// on the next tick. The caller (loop) is responsible for dispatching them to +// the worker pool, interleaved with incoming request handling, to avoid the +// deadlock described in https://github.com/lestrrat-go/httprc/issues/113. +func (c *ctrlBackend) periodicCheck(ctx context.Context, t time.Time) []Resource { c.traceSink.Put(ctx, "httprc controller: START periodic check") defer c.traceSink.Put(ctx, "httprc controller: END periodic check") var minNext time.Time - var dispatched int minInterval := -1 * time.Second + var toDispatch []Resource for _, item := range c.items { c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: checking resource %q", item.URL())) @@ -196,14 +229,13 @@ func (c *ctrlBackend) periodicCheck(ctx context.Context, t time.Time) { c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: resource %q is busy or not ready yet, skipping", item.URL())) continue } - c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: resource %q is ready, dispatching to worker pool", item.URL())) + c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: resource %q is ready, queuing for dispatch", item.URL())) - dispatched++ - c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: dispatching resource %q to worker pool", item.URL())) - sendWorker(ctx, c.outgoing, item) + item.SetBusy(true) + toDispatch = append(toDispatch, item) } - c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: dispatched %d resources", dispatched)) + c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: queued %d resources for dispatch", len(toDispatch))) // Next check is always at the earliest next check + 1 second. // The extra second makes sure that we are _past_ the actual next check time @@ -223,6 +255,7 @@ func (c *ctrlBackend) periodicCheck(ctx context.Context, t time.Time) { } c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: next check in %s", c.tickInterval)) + return toDispatch } func (c *ctrlBackend) SetTickInterval(d time.Duration) { diff --git a/vendor/github.com/lestrrat-go/httprc/v3/controller.go b/vendor/github.com/lestrrat-go/httprc/v3/controller.go index 1ad9d7b6c6..ffca6a590d 100644 --- a/vendor/github.com/lestrrat-go/httprc/v3/controller.go +++ b/vendor/github.com/lestrrat-go/httprc/v3/controller.go @@ -142,14 +142,23 @@ func (c *controller) Add(ctx context.Context, r Resource, options ...AddOption) resource: r, } c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: sending add request for %q to backend", r.URL())) + // Send to backend and wait for registration confirmation. + // If this succeeds, the resource is in the backend. if _, err := sendBackend[addRequest, struct{}](ctx, c.incoming, req, reply); err != nil { return err } + // IMPORTANT: At this point, the resource has been successfully registered + // in the backend (stored in c.items map). The backend worker will fetch + // this resource periodically. if waitReady { c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: waiting for resource %q to be ready", r.URL())) if err := r.Ready(ctx); err != nil { - return err + // CHANGE: Wrap Ready() errors with errNotReady to indicate that + // registration succeeded but the first fetch hasn't completed. + // Using %w twice creates a multi-error chain (Go 1.20+), allowing + // errors.Is() to check both errNotReady and the underlying error. + return fmt.Errorf("%w: %w", errNotReady, err) } } return nil diff --git a/vendor/github.com/lestrrat-go/httprc/v3/errors.go b/vendor/github.com/lestrrat-go/httprc/v3/errors.go index 1152ba947f..474c790f5d 100644 --- a/vendor/github.com/lestrrat-go/httprc/v3/errors.go +++ b/vendor/github.com/lestrrat-go/httprc/v3/errors.go @@ -55,3 +55,51 @@ var errBlockedByWhitelist = errors.New(`blocked by whitelist`) func ErrBlockedByWhitelist() error { return errBlockedByWhitelist } + +var errNotReady = errors.New(`resource registered but not ready`) + +// ErrNotReady returns a sentinel error indicating that the resource was +// successfully registered with the backend and is being actively managed, +// but the first fetch and transformation has not completed successfully yet. +// +// This error is returned by Add() when: +// - The resource was successfully added to the backend (registration succeeded) +// - WithWaitReady(true) was specified (the default) +// - The Ready() call failed (timeout, transform error, context cancelled, etc.) +// +// When Add() returns this error, the resource IS in the backend's resource map +// and will continue to be fetched periodically in the background according to +// the refresh interval. The application can safely proceed - the resource data +// may become available later when a fetch succeeds. +// +// IMPORTANT: "Not ready" means the first fetch and transformation has not completed +// successfully. The resource may eventually become ready (if the transformation +// succeeds on a subsequent retry), or it may never become ready (if the data is +// permanently invalid or the server is unreachable). The backend will continue +// retrying according to the configured refresh interval. +// +// The underlying error (context deadline, transform failure, etc.) is wrapped +// using Go 1.20+ multiple error wrapping and can be examined with errors.Is() +// or errors.As(). You do not need to manually unwrap the error. +// +// Example: +// +// err := ctrl.Add(ctx, resource) +// if err != nil { +// if errors.Is(err, httprc.ErrNotReady()) { +// // Resource registered, will fetch in background +// log.Print("Resource not ready yet, continuing startup") +// +// // Can also check the underlying cause +// if errors.Is(err, context.DeadlineExceeded) { +// log.Print("Timed out waiting for first fetch") +// } +// return nil +// } +// // Registration failed +// return fmt.Errorf("failed to register resource: %w", err) +// } +// // Resource registered AND ready with data +func ErrNotReady() error { + return errNotReady +} diff --git a/vendor/github.com/lestrrat-go/httprc/v3/proxysink/proxysink.go b/vendor/github.com/lestrrat-go/httprc/v3/proxysink/proxysink.go index f290422d6c..942e654c2e 100644 --- a/vendor/github.com/lestrrat-go/httprc/v3/proxysink/proxysink.go +++ b/vendor/github.com/lestrrat-go/httprc/v3/proxysink/proxysink.go @@ -72,6 +72,7 @@ func (p *Proxy[T]) flushloop(ctx context.Context) { p.mu.Unlock() return } + p.mu.Unlock() default: } diff --git a/vendor/github.com/lestrrat-go/httprc/v3/worker.go b/vendor/github.com/lestrrat-go/httprc/v3/worker.go index d11477dadc..1b74164159 100644 --- a/vendor/github.com/lestrrat-go/httprc/v3/worker.go +++ b/vendor/github.com/lestrrat-go/httprc/v3/worker.go @@ -42,7 +42,7 @@ func (w worker) Run(ctx context.Context, readywg *sync.WaitGroup, donewg *sync.W w.traceSink.Put(ctx, fmt.Sprintf("httprc worker: FAILED to sync %q (synchronous): %s", sr.resource.URL(), err)) sendReply(ctx, sr.reply, struct{}{}, err) sr.resource.SetBusy(false) - return + continue } w.traceSink.Put(ctx, fmt.Sprintf("httprc worker: SUCCESS syncing %q (synchronous)", sr.resource.URL())) sr.resource.SetBusy(false) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/.gitignore b/vendor/github.com/lestrrat-go/jwx/v3/.gitignore index c4c0ebff32..09d70ac9eb 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/.gitignore +++ b/vendor/github.com/lestrrat-go/jwx/v3/.gitignore @@ -37,3 +37,7 @@ out cmd/jwx/jwx bazel-* + +# Go workspace files (local development only) +go.work +go.work.sum diff --git a/vendor/github.com/lestrrat-go/jwx/v3/.golangci.yml b/vendor/github.com/lestrrat-go/jwx/v3/.golangci.yml index 30dc4c519b..5e67fa0d1f 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/.golangci.yml +++ b/vendor/github.com/lestrrat-go/jwx/v3/.golangci.yml @@ -106,6 +106,18 @@ linters: - revive path: jwt/internal/types/ text: "var-naming: avoid meaningless package names" + - linters: + - revive + path: internal/json/ + text: "var-naming: avoid package names" + - linters: + - revive + path: jwe/internal/cipher/ + text: "var-naming: avoid package names that conflict with Go standard library package names" + - linters: + - revive + path: jwk/ecdsa/ + text: "var-naming: avoid package names that conflict with Go standard library package names" - linters: - godoclint path: (^|/)internal/ diff --git a/vendor/github.com/lestrrat-go/jwx/v3/AGENTS.md b/vendor/github.com/lestrrat-go/jwx/v3/AGENTS.md new file mode 100644 index 0000000000..5be5c559bf --- /dev/null +++ b/vendor/github.com/lestrrat-go/jwx/v3/AGENTS.md @@ -0,0 +1,266 @@ +# AGENTS.md + +## For Module Consumers + +If you are writing code that *uses* jwx (not developing jwx itself): + +- **Examples**: See `examples/` directory for runnable usage patterns +- **Documentation**: See `docs/` directory and package READMEs +- **API Reference**: Use `go doc` or https://pkg.go.dev/github.com/lestrrat-go/jwx/v3 + +The rest of this document focuses on developing the jwx library itself. + +--- + +## Go Version + +This project requires **Go 1.25.0** or later. Check `go.mod` for the exact version. + +## Module Path vs Physical Layout + +This repository uses a **flat layout** with vanity import paths. There is no physical `v3/` directory. + +| Branch | Module Path | Physical Root | +|--------|-------------|---------------| +| `develop/v3` | `github.com/lestrrat-go/jwx/v3` | `/` (repo root) | + +`import "github.com/lestrrat-go/jwx/v3/jwt"` → files are at `./jwt/`, not `./v3/jwt/`. + +## Code Generation + +### Immutable Rule + +**NEVER edit files ending in `_gen.go` directly.** These are generated files. Edit the generator sources instead. + +### Generated Files Pattern + +Files matching `*_gen.go` are generated. Examples: +- `jwt/options_gen.go` +- `jwt/token_gen.go` +- `jws/headers_gen.go` +- `jwk/rsa_gen.go` +- `jwa/signature_gen.go` + +### Generator Locations + +| Generator | Location | Input Files | Output | +|-----------|----------|-------------|--------| +| `genoptions` | `tools/cmd/genoptions/` | `{jwa,jwe,jwk,jws,jwt}/options.yaml` | `*/options_gen.go` | +| `genjwt` | `tools/cmd/genjwt/` | `tools/cmd/genjwt/objects.yml` | `jwt/*_gen.go` | +| `genjws` | `tools/cmd/genjws/` | `tools/cmd/genjws/objects.yml` | `jws/*_gen.go` | +| `genjwe` | `tools/cmd/genjwe/` | `tools/cmd/genjwe/objects.yml` | `jwe/*_gen.go` | +| `genjwk` | `tools/cmd/genjwk/` | `tools/cmd/genjwk/objects.yml` | `jwk/*_gen.go` | +| `genjwa` | `tools/cmd/genjwa/` | `tools/cmd/genjwa/objects.yml` | `jwa/*_gen.go` | +| `genreadfile` | `tools/cmd/genreadfile/` | - | ReadFile helpers | + +### Regeneration Commands + +```bash +# Regenerate all code (includes options via `go generate .`) +make generate + +# Regenerate specific package (objects/types only, NOT options) +make generate-jwt +make generate-jws +make generate-jwe +make generate-jwk +make generate-jwa + +# Regenerate options only (options.yaml → options_gen.go for all packages) +go generate . +# or directly: +./tools/cmd/genoptions.sh +``` + +**Important:** `make generate-` does **not** regenerate options. If you +edit an `options.yaml` file, run `make generate` or `go generate .`. + +## Functional Options Pattern + +Options are defined in `{package}/options.yaml` and generated into `{package}/options_gen.go`. + +Example `options.yaml` entry: + +```yaml +options: + - ident: Token + interface: ParseOption + argument_type: Token + comment: | + WithToken specifies the token instance... +``` + +Generates `WithToken(v Token) ParseOption` function. + +## Multi-Module Structure + +This repository contains multiple Go modules. The nested modules use `replace` directives for local development. + +| Module | Path | Purpose | +|--------|------|---------| +| Main | `./go.mod` | Core library | +| Examples | `./examples/go.mod` | Usage examples | +| CLI | `./cmd/jwx/go.mod` | Command-line tool | +| Perf Bench | `./bench/performance/go.mod` | Performance benchmarks | +| Comparison | `./bench/comparison/go.mod` | Library comparison | +| Generators | `./tools/cmd/*/go.mod` | Code generators | + +### Local Development + +The `examples/go.mod` contains: +```go +replace github.com/lestrrat-go/jwx/v3 v3.0.0 => ../ +``` + +No `go.work` file is committed. When working across modules, either: +1. Create a temporary `go.work` file (it is .gitignored) +2. Rely on the `replace` directives already in place + +## Development Commands + +```bash +# Run all tests +make test + +# Run tests with specific build tags +make test-goccy # Use goccy/go-json +make test-es256k # Enable ES256K support +make test-alltags # All optional features + +# Run short/smoke tests +make smoke + +# Generate coverage report +make cover +make viewcover + +# Lint +make lint + +# Format and tidy +make imports +make tidy +``` + +### Test Script Details + +Tests are run via `./tools/test.sh` which iterates over: +- `.` (main module) +- `./examples` +- `./bench/performance` +- `./cmd/jwx` + +## Package Directory Map + +| Package | Responsibility | +|---------|----------------| +| `jwa/` | Algorithm identifiers (e.g., `RS256`, `ES384`, `A128GCM`) | +| `jwk/` | JSON Web Keys - key representation and management | +| `jws/` | JSON Web Signatures - `Sign()` and `Verify()` | +| `jwe/` | JSON Web Encryption - `Encrypt()` and `Decrypt()` | +| `jwt/` | JSON Web Tokens - claims and validation | +| `jwt/openid/` | OpenID Connect ID tokens | +| `transform/` | Token transformation utilities | + +## Relevant RFCs + +- RFC 7515 - JWS (JSON Web Signature) +- RFC 7516 - JWE (JSON Web Encryption) +- RFC 7517 - JWK (JSON Web Key) +- RFC 7518 - JWA (JSON Web Algorithms) +- RFC 7519 - JWT (JSON Web Token) +- OpenID Connect Core 1.0 + +## Error Handling + +Sentinel errors are exposed via functions. Use `errors.Is()`: + +```go +if errors.Is(err, jwt.TokenExpiredError()) { ... } +``` + +| Package | Function | Meaning | +|---------|----------|---------| +| `jwt` | `TokenExpiredError()` | `exp` claim not satisfied | +| `jwt` | `TokenNotYetValidError()` | `nbf` claim not satisfied | +| `jwt` | `InvalidIssuerError()` | `iss` claim not satisfied | +| `jwt` | `InvalidAudienceError()` | `aud` claim not satisfied | +| `jwt` | `ValidateError()` | Generic validation failure | +| `jwt` | `ParseError()` | Parse failed | +| `jws` | `VerificationError()` | Signature verification failed | +| `jwe` | `DecryptError()` | Decryption failed | + +## Testing + +Use `github.com/stretchr/testify/require` for assertions (not `assert`). + +## Build Tags + +| Tag | Effect | +|-----|--------| +| `jwx_goccy` | Use `goccy/go-json` instead of `encoding/json` | +| `jwx_es256k` | Enable secp256k1/ES256K algorithm support | +| `jwx_secp256k1_pem` | Enable PEM encoding for secp256k1 keys | +| `jwx_asmbase64` | Use assembly-optimized base64 | + +## Quick Reference: Common Modifications + +| Task | Edit This | Then Run | +|------|-----------|----------| +| Add/edit any option | `{pkg}/options.yaml` | `make generate` or `go generate .` | +| Add new JWS header field | `tools/cmd/genjws/objects.yml` | `make generate-jws` | +| Add new JWK key field | `tools/cmd/genjwk/objects.yml` | `make generate-jwk` | +| Add new algorithm | `tools/cmd/genjwa/objects.yml` | `make generate-jwa` | +| Modify token fields | `tools/cmd/genjwt/objects.yml` | `make generate-jwt` | + +## File Naming Conventions + +| Pattern | Meaning | +|---------|---------| +| `*_gen.go` | Generated code - DO NOT EDIT | +| `*_test.go` | Test files | +| `*_gen_test.go` | Generated tests - DO NOT EDIT | +| `options.yaml` | Option definitions (input to genoptions) | +| `objects.yml` | Object definitions (input to package-specific generators) | + +## Examples Directory + +Naming convention: `{package}_xxx_example_test.go` +- `jwt_parse_example_test.go` +- `jws_sign_example_test.go` +- `jwx_example_test.go` (cross-package) +- `jwx_readme_example_test.go` (cross-package, used in README) +- `jwx_register_ec_and_key_example_test.go` (cross-package, key registration) + +Examples are included in `docs/` via autodoc markers: +```markdown + + +``` + +## Pre-Read Rules + +Read linked doc BEFORE working in that area. No exceptions. + +| Trigger | Doc | +|---------|-----| +| Looking up package APIs, types, functions | `.claude/docs/packages.md` | +| Running or writing tests, fuzz tests | `.claude/docs/testing.md` | +| Understanding package relationships, imports | `.claude/docs/dependencies.md` | +| Working with errors, error handling patterns | `.claude/docs/error-formatting.md` | +| Code generation, options pattern, extension points, JSON/base64 backends | `.claude/docs/internals.md` | + +## Cache Maintenance + +These docs cache repository state. Still read source before modifying code. + +1. When your changes affect a doc below, update it in the same commit. +2. If you notice any doc is wrong or stale — even on an unrelated task — fix it immediately. + +| Doc | Update trigger | +|-----|----------------| +| `.claude/docs/packages.md` | New/renamed/removed exported functions, types, or packages | +| `.claude/docs/testing.md` | Changes to test infrastructure, build tags, test helpers, fuzz targets | +| `.claude/docs/dependencies.md` | New internal imports between packages, new external dependencies | +| `.claude/docs/error-formatting.md` | New sentinel errors, changes to error wrapping patterns | +| `.claude/docs/internals.md` | Changes to generators, options YAML schema, registration points, multi-module layout | diff --git a/vendor/github.com/lestrrat-go/jwx/v3/Changes b/vendor/github.com/lestrrat-go/jwx/v3/Changes index 4df33b756c..c5eeebf6dd 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/Changes +++ b/vendor/github.com/lestrrat-go/jwx/v3/Changes @@ -4,6 +4,337 @@ Changes v3 has many incompatibilities with v2. To see the full list of differences between v2 and v3, please read the Changes-v3.md file (https://github.com/lestrrat-go/jwx/blob/develop/v3/Changes-v3.md) +v3.1.1 7 May 2026 + * [jws] Coordinated RFC 7797 `b64=false` handling pass: `jws.Verify` + rejects payloads with `b64=false` unless `b64` is also listed in + `crit`; `jws.Sign` auto-declares `b64` in `crit` when emitting + `b64=false`; `Message.MarshalJSON` honors `b64=false` instead of + silently re-encoding; `jws.VerifyCompactFast` refuses any compact + JWS carrying `b64` (the fast path doesn't process extension + headers); and `b64` is now declared as a typed boolean header + field rather than handled ad-hoc. + (#2081, #2087, #2102, #2104, #2106) + + * [jws] Reject malformed general-form JSON-serialized JWS: inputs + with a top-level `header` member as a sibling of `signatures` are + rejected (the spec only permits `header` inside per-signature + objects), as are inputs whose `protected` member is a literal + JSON object instead of a base64url-encoded string. + (#2089, #2108) + + * [jws] `jws.AlgorithmsForKey` failures from unclassifiable keys + are now wrapped in a typed sentinel so callers can branch on + "couldn't categorize this key" without string matching the error + message. (#2110) + + * [jws] Verify error-shape consistency: `VerifyCompactFast` + refusals now match the `jws.VerifyError()` taxonomy used by the + slow path, fan-out verify errors name the loose `WithKeySet` + options that were tried, multi-signature `b64` mismatches name + the offending signature index and conflicting value, and the + compact `b64=false`+payload-contains-`.` error references RFC + 7797 §5.2 and points at `WithDetachedPayload`. + (#2083, #2085, #2114) + + * [jws] Keys fetched via the `jku` header are no longer accepted + for signature verification when the JWK declares `use=enc`. + (#2060) + + * [jws][jwe] `jws.VerifyMessage` and `jwe.DecryptMessage` observe + context cancellation between loop iterations rather than only at + boundaries. Long fan-out verify/decrypt loops now respond to a + cancelled context promptly. (#2112, #2117) + + * [jwe] Reject PBES2 messages whose `p2c` (iteration count) does + not parse cleanly into int64 or violates the configured bound. + The error now names the violated bound (min vs max) instead of + the generic "out of range". (#2119) + + * [jwe] `jwe.WithKey()` validates the alg-vs-key shape at option + construction time rather than during encryption, so misuse + surfaces at the call site instead of inside the encrypt loop. + (#2121) + + * [jwe] Decrypt error-path cleanup: per-key failures from key-set + providers are surfaced via `errors.Join` so each underlying + error remains inspectable; the joined error count is bounded to + keep diagnostics readable; the redundant outer `Decrypt:` prefix + is dropped; and the compression-cap error names the + "decompressed" payload, the option, and the size. + (#2123, #2125, #2127) + + * [jwe] Add `jwe.WithDisabledKeyAlgorithms(...)` as a global + policy hook (`jwe.Configure(...)` or per-call) for refusing + specific key-management algorithms across all `jwe.Decrypt` + calls. (#2129) + + * [jwe] Reject messages whose protected-header `alg` conflicts + with a per-recipient `alg`. The previous behavior silently + preferred the protected-header value. (#2052) + + * [jwk] Stop duplicating JWK fields at the JWKS top level on + parse. A JWKS whose top-level object carries fields with the + same names as JWK members no longer copies those values into + every key in `keys`. (#2133) + + * [jwk] A custom `jwk.KeyParser` returning `(nil, nil)` now + signals "continue to the next parser" rather than "successfully + produced a nil key". Callers no longer end up with a nil + `jwk.Key` from a successful parse when an extension returns the + empty pair. (#2140) + + * [jwk] Stream the JWKS `keys` array with a cap-before-allocate + strategy. Inputs respect `WithMaxKeys` before any unbounded + slice growth, and bounded-size JWKS no longer over-allocate + based on attacker-controlled length hints. (#2137) + + * [jwk] Wrap `jwk.ParseKey` errors with the `jwk.ParseError` + sentinel so callers can branch on parse-vs-other failures with + `errors.Is(err, jwk.ParseError())`. (#2135) + + * [jwk] ECDSA public keys whose X / Y coordinates exceed the + curve's byte length are rejected with a typed error instead of + reaching curve arithmetic with an out-of-range `big.Int`. + (#2050) + + * [jwt] `jwt.ParseRequest` no longer skips the request body when + the request uses chunked transfer encoding. The + Content-Length-based fast path previously bypassed `ParseForm` + for chunked requests even when `WithFormKey` was supplied. + (#2091) + + * [jwt] Pedantic mode (`jwt.WithPedanticParse(true)`) enforces + that nested-envelope JWTs declare `cty=JWT`. Tokens missing + `cty` or carrying a different value are rejected. (#2094) + + * [jwt] `jwt.ParseInsecure` parses the loop-local payload split + from the input rather than the original input bytes. Fixes a + case where `ParseInsecure` could return a token assembled from + a different signature segment than the one being inspected. + (#2097) + + * [jwt] `jwt.WithMaxDeltaIs(...)` and `jwt.WithMinDeltaIs(...)` + reject tokens whose compared claim is missing rather than + treating it as zero. Validation no longer silently succeeds + when the claim isn't present on the token. (#2099) + + * [jwt] `jwt.Parse` / `jwt.ParseRequest` only call `ParseForm` + when `WithFormKey` is supplied. Previously the form body could + be consumed even when the caller did not opt in to form-source + extraction. (#2058) + + * [jwt] Fix `AddressClaim.MarshalJSON` to handle non-printable + bytes correctly. (#2056) + + * [jwa] Unify the signature, key-encryption, and + content-encryption algorithm tables behind a single + registration entry point. Extension algorithms register once + rather than via three parallel APIs. (#2066) + + * [cmd/jwx] Warn before writing a private key to a TTY; reject + `keysize <= 0` for `oct` key generation. (#2071) + +v3.1.0 19 Apr 2026 + * [jwk] Add `jwk.WithRejectDuplicateKID(bool)` — when enabled, `jwk.Parse` / + `jwk.ParseReader` / `jwk.ParseString` return an error if the input JWKS + contains more than one key sharing the same non-empty `kid`. Usable as a + `jwk.Configure()` global or a per-call override. Default behavior + (first-match-wins) is unchanged. + + * [jwk] Add `jwk.WithMaxKeys(int)` — caps the number of keys accepted + by `jwk.Parse` / `jwk.ParseReader` / `jwk.ParseString` in both the + JSON `"keys"` array and the PEM/X.509 block stream (default 1000). + Usable as a `jwk.Configure()` global or a per-call override. + Replaces the hardcoded internal PEM cap of the same value, so the + default behavior is unchanged. Backport of the v4 amplification + cap that mirrors `jws.WithMaxSignatures` and + `jwe.WithMaxRecipients`. + + * [jws] Add `jws.WithDetachedPayloadReader(io.Reader)` — a streaming + variant of `jws.WithDetachedPayload([]byte)` that consumes the + payload from an `io.Reader` instead of a byte slice, so the payload + is never materialized in memory. It is a `jws.Sign()` / `jws.Verify()` + option; the two remain the default entry points. Only HMAC/RSA/ECDSA + algorithms are supported; EdDSA, custom-family algorithms, and + algorithms registered via `jws.RegisterSigner()` / `jws.RegisterVerifier()` + are rejected with a clear error pointing at `jws.WithDetachedPayload()`. + On sign, multiple `jws.WithKey()` options combined with `jws.WithJSON()` + produce a general-form multi-signature JWS (the payload is streamed + once and fanned out to each signer). On verify, only single-signature + JWS input is supported; `jws.WithKeySet()`, `jws.WithKeyProvider()`, + and `jws.WithVerifyAuto()` are not accepted. (#1663) + + * [jws] Add `jws.Base64StreamEncoder` — the stream-capable extension + of `jws.Base64Encoder`. The default encoder and `*base64.Encoding` + values supplied via `jws.WithBase64Encoder()` are auto-wrapped, so + typical callers see no change. Custom encoders only need to + implement this additional interface if they want to be usable with + `jws.WithDetachedPayloadReader()`. (#1663) + + * [jws] Fix `jws.Sign` with `WithDetachedPayload` + `WithJSON` to + omit the `"payload"` member from the output per RFC 7515 + Appendix F. Previously the payload was still emitted, producing + non-detached JSON. (#1663) + + * [jwk] BREAKING: `jwk.PublicSetOf` now returns an error when the input + set contains a symmetric (oct) key. Previously, symmetric keys were + silently passed through — which meant callers following the documented + "publish my public JWKS" pattern could leak HMAC secret material. + Callers who genuinely want the legacy pass-through behavior can opt in + with `jwk.WithAllowSymmetric(true)`. The signature is now variadic + (`PublicSetOf(v Set, options ...PublicSetOption)`), so existing call + sites compile unchanged. The minor version is bumped from v3.0.x → + v3.1.0 to reflect this deliberate behavior change. + + `jwk.PublicKeyOf` on a single symmetric key is unchanged — it still + returns the key as-is, matching its documented behavior. + + * [jws][jwe][jwk] Replace intermediate map[string]any allocation in + MarshalJSON with a pair-slice + sync.Pool pattern, matching the approach + already used in jwt. Eliminates per-call map and key-slice allocations + in the serialization hot path. + + * [jwt][jwe][jws][jwk] Fix inconsistent mutex locking across main data + structures. Named getters on JWK key types, MarshalJSON on JWK keys, + UnmarshalJSON on JWE headers, makePairs/MarshalJSON on JWT tokens, + rawBuffer on JWS headers, and Set/Keys on jwk.Set were missing proper + lock protection. Switch all mutex fields from *sync.RWMutex (pointer) + to sync.RWMutex (value) so go vet -copylocks catches accidental copies, + and convert affected value-receiver methods to pointer receivers. + + * [jwe] Add `WithMaxRecipients(int)` to reject JWE messages with more recipients + than the configured limit. Default is 100. Can be set globally via + `jwe.Settings()` or per-call in `jwe.Decrypt()` / `jwe.Parse()`. (#1633) + + * [jws] Add `WithMaxSignatures(int)` to reject JWS JSON-serialized messages with + more signatures than the configured limit. Default is 100. Can be set globally + via `jws.Settings()` or per-call in `jws.Parse()`. (#1636) + + * [jwk] The default HTTP client used by `jwk.Fetch()` and `jwk.Cache` now + enforces a 30-second timeout, blocks HTTPS-to-HTTP redirect downgrades at + every hop, and limits redirect chains to 5 hops. This mitigates SSRF via + redirect chains and slowloris-style DoS from unresponsive JWKS endpoints. + Callers who provide their own `http.Client` via `jwk.WithHTTPClient()` are + not affected. (#1634, #1637, #1639, #1640) + + * [jwk] Add `jwk.DefaultHTTPClient()` which returns a new `*http.Client` + configured with the library's default protections. Useful for restoring + defaults after calling `jwk.Configure(jwk.WithHTTPClient(...))`. (#1638) + + * [jwk] `WithMaxFetchBodySize(int64)` can now be set globally via + `jwk.Configure()` in addition to per-call. (#1631) + + * [jwk] Add `jwk.WrapHTTPClientDefaults()` to apply the library's default + safety behaviors (timeout, redirect policy) to a caller-provided + `*http.Client`. Existing client settings (Transport, Jar, etc.) are + preserved; CheckRedirect is wrapped rather than overwritten. + + * [jwt] Add `jwt.WithStrictStringClaims(true)` option for `jwt.Parse()` and + `jwt.ReadFile()` to reject JSON `null` for string registered claims + (`iss`, `sub`, `jti`). By default, null is silently accepted as an empty + string. (#1484) + + * [jwa] Add fully-specified EdDSA signature algorithms `Ed25519` and `Ed448` per + RFC 9864. The polymorphic `EdDSA` algorithm is now marked as deprecated. + New Go accessors: `jwa.EdDSAEd25519()` and `jwa.EdDSAEd448()` (function names + are tentative and may change in future releases). + Ed448 signing/verification requires `github.com/cloudflare/circl` because + Go's standard library does not support Ed448. To avoid pulling in this + extra dependency for all users, Ed448 support is provided as a separate + module (`github.com/lestrrat-go/jwx-circl-ed448`). Import it for side + effects to enable Ed448: + import _ "github.com/lestrrat-go/jwx-circl-ed448" + Without this import, Ed448 is registered as an algorithm identifier but + will return an error at sign/verify time. + + * [jwk] Add `jwk/jwkunsafe` package with `NewKey(kty)` and `NewPublicKey(kty)` + functions for creating empty, unpopulated JWK key objects. This is intended + for extension module authors who need to register custom KeyImporter + implementations for new key types. + + * [jws] Add `jws.RegisterAlgorithmForKeyType()` for external modules to register + additional algorithm-to-key-type mappings. + + * [jws/jwsbb] Add `jwsbb.RegisterAlgorithm()` for external modules to + register custom algorithm implementations (e.g. Ed448). + + * [jws] `jws.Verify()` and `jws.VerifyCompactFast()` now validate the "crit" + (Critical) header parameter per RFC 7515 Section 4.1.11. Signatures with an + empty "crit" array, standard JOSE header names in "crit", or "crit"-listed + extensions not present in the protected header are now rejected. To disable + this validation on a per-call basis, pass + `jws.WithCritValidation(false)` to `jws.Verify()`. + + * [jwe] `jwe.Decrypt()` now validates the "crit" (Critical) header parameter + per RFC 7516 Section 4.1.13, matching the jws behavior above. Messages with + an empty "crit" array, standard JOSE header names in "crit", or "crit"-listed + extensions not present in the protected header are now rejected. Declare + extensions with `jwe.WithCritExtension()`, or disable validation on a + per-call basis with `jwe.WithCritValidation(false)`. (#1735) + + * [jwk] BREAKING (extension modules): `jwk.RegisterKeyExporter` now takes a + `jwk.KeyKind` instead of `jwa.KeyType`. Call sites migrate with + `jwk.KeyKind(kty.String())`; for curve-specific exporters, use a compound + identity like `jwk.KeyKind("OKP:Ed448")`. Only affects extension-module + authors registering custom exporters; library users calling `jwk.Export` + are unaffected. + + * [jwk] Fixed inverted rlocker condition in RSA key export. (#1576) + + * [jwe] Fixed X25519 ECDH-ES key agreement to include `apu` and `apv` parameters + in the Concat KDF derivation, matching the ECDSA path. Previously these values + were silently discarded during encryption, weakening the key derivation per + RFC 7518 Section 4.6.2. + + * [jwe] POTENTIALLY BREAKING: `jwe.Decrypt()` now rejects PBES2 messages with + a `p2c` (iteration count) below 1,000 by default. This prevents accepting + tokens with trivially low iteration counts that eliminate PBKDF2 brute-force + protection. To restore the previous behavior or adjust the threshold, call + `jwe.Settings(jwe.WithMinPBES2Count(0))`. + + * [jwk] Fixed a deadlock in `jwk.Cache` that occurred when repeated `Refresh()` calls + failed (e.g. HTTP 500 responses). Each failure killed an httprc worker goroutine, + and after all workers were exhausted, subsequent `Refresh()` calls would block forever. (#1551) + + * [jws] Add `jws.RegisterAlgorithmForCurve()` for external modules to register + algorithm-to-curve mappings. `jws.AlgorithmsForKey()` now filters results + by the key's curve when applicable. (#1620) + + * [jwk] Add `jwk.WithMaxFetchBodySize()` option to limit the response body size + when fetching remote JWK Sets via `jwk.Fetch()` and `jwk.Cache`. (#1622) + + * [jwe] Add `jwe.WithMaxPBES2Count()` and `jwe.WithMinPBES2Count()` as per-call + options to `jwe.Decrypt()`, allowing callers to override the global PBES2 + iteration count limits on a per-decryption basis. (#1623) + + * [jwk] `jwk.X509CertChain()` now correctly returns `false` as the second return + value when the certificate chain is nil. (#1624) + + * [jwk] Fixed a data race in the x509 decoder registry iteration. (#1625) + + * [jwk] `jwk.Parse()` now limits PEM input to 1,000 blocks maximum to prevent + resource exhaustion from inputs containing thousands of small PEM blocks. (#1626) + + * [jwk] RSA JWK validation is now enforced consistently across JSON parse, + JWKS parse, PEM/X.509 parse, and `jwk.Import()`. Keys with moduli smaller + than 2048 bits or unsafe public exponents are now rejected by default. + Compatibility knobs were added to `jwk.Configure()` via + `jwk.WithMinRSAModulusBits(...)` and `jwk.WithMinRSAPublicExponent(...)`. + + * [jwt] `jwt.Validate()` now rejects negative durations passed to + `jwt.WithAcceptableSkew()`. (#1627) + + * [jwt/openid] `openid.Birthdate` now accepts year `0000` as a valid value per + the OpenID Connect Core specification. (#1628) + + * [jwk][jws][jwe] Generated `Set()` methods now deep-copy slice and byte-slice + values so callers cannot mutate internal header or key state after setting + a field. (#1659) + + * [jwk] When `UnmarshalJSON` fails on a private key (RSA, EC, OKP, Symmetric), + all sensitive fields are now zeroed before the error is returned. This + prevents leaking partial key material through half-constructed objects. (#1660) + v3.0.13 12 Jan 2026 * [jwt] The `jwt.WithContext()` option is now properly being passed to `jws.Verify()` from `jwt.Parse()`. diff --git a/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel b/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel index c9bdc9b730..61947500b4 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel +++ b/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel @@ -5,7 +5,7 @@ module( ) bazel_dep(name = "bazel_skylib", version = "1.7.1") -bazel_dep(name = "rules_go", version = "0.55.1") +bazel_dep(name = "rules_go", version = "0.57.0") bazel_dep(name = "gazelle", version = "0.44.0") bazel_dep(name = "aspect_bazel_lib", version = "2.11.0") diff --git a/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel.lock b/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel.lock index 2848e8716d..7d196c3671 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel.lock +++ b/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel.lock @@ -56,12 +56,13 @@ "https://bcr.bazel.build/modules/package_metadata/0.0.2/source.json": "e53a759a72488d2c0576f57491ef2da0cf4aab05ac0997314012495935531b73", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", - "https://bcr.bazel.build/modules/platforms/0.0.11/source.json": "f7e188b79ebedebfe75e9e1d098b8845226c7992b307e28e1496f23112e8fc29", "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615", "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814", "https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d", + "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580", + "https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96", "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c", "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d", @@ -97,8 +98,8 @@ "https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270", "https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd", "https://bcr.bazel.build/modules/rules_go/0.51.0/MODULE.bazel": "b6920f505935bfd69381651c942496d99b16e2a12f3dd5263b90ded16f3b4d0f", - "https://bcr.bazel.build/modules/rules_go/0.55.1/MODULE.bazel": "a57a6fc59a74326c0b440d07cca209edf13c7d1a641e48cfbeab56e79f873609", - "https://bcr.bazel.build/modules/rules_go/0.55.1/source.json": "827a740c8959c9d20616889e7746cde4dcc6ee80d25146943627ccea0736328f", + "https://bcr.bazel.build/modules/rules_go/0.57.0/MODULE.bazel": "bee44028b527cd6d1b7699a2c78714bba237b40ee21f90a83b472c94bc53159d", + "https://bcr.bazel.build/modules/rules_go/0.57.0/source.json": "a782b756d87c68a223a48848eda4b0dac1c5fd1d925d648d7598b68aa1fb6d6d", "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", "https://bcr.bazel.build/modules/rules_java/6.0.0/MODULE.bazel": "8a43b7df601a7ec1af61d79345c17b31ea1fedc6711fd4abfd013ea612978e39", diff --git a/vendor/github.com/lestrrat-go/jwx/v3/Makefile b/vendor/github.com/lestrrat-go/jwx/v3/Makefile index 672c007b29..9f96115057 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/Makefile +++ b/vendor/github.com/lestrrat-go/jwx/v3/Makefile @@ -1,4 +1,4 @@ -.PHONY: generate realclean cover viewcover test lint check_diffs imports tidy jwx +.PHONY: generate realclean cover viewcover test lint check_diffs imports tidy jwx fuzz fuzz-jwt fuzz-jws fuzz-jwe fuzz-jwk generate: @go generate @$(MAKE) generate-jwa generate-jwe generate-jwk generate-jws generate-jwt @@ -94,5 +94,26 @@ imports: tidy: ./scripts/tidy.sh +FUZZTIME ?= 30s + +fuzz: fuzz-jwt fuzz-jws fuzz-jwe fuzz-jwk + +fuzz-jwt: + go test ./jwt/ -run "^$$" -fuzz FuzzParse -fuzztime $(FUZZTIME) + go test ./jwt/ -run "^$$" -fuzz FuzzSignAndParse -fuzztime $(FUZZTIME) + +fuzz-jws: + go test ./jws/ -run "^$$" -fuzz FuzzParse -fuzztime $(FUZZTIME) + go test ./jws/ -run "^$$" -fuzz FuzzSignAndVerify -fuzztime $(FUZZTIME) + +fuzz-jwe: + go test ./jwe/ -run "^$$" -fuzz FuzzParse -fuzztime $(FUZZTIME) + go test ./jwe/ -run "^$$" -fuzz FuzzEncryptAndDecrypt -fuzztime $(FUZZTIME) + +fuzz-jwk: + go test ./jwk/ -run "^$$" -fuzz "^FuzzParseKey$$" -fuzztime $(FUZZTIME) + go test ./jwk/ -run "^$$" -fuzz "^FuzzParse$$" -fuzztime $(FUZZTIME) + go test ./jwk/ -run "^$$" -fuzz FuzzParseKeyRoundtrip -fuzztime $(FUZZTIME) + jwx: @./tools/cmd/install-jwx.sh diff --git a/vendor/github.com/lestrrat-go/jwx/v3/cert/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/cert/BUILD.bazel index f308530bcf..8ec85d00be 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/cert/BUILD.bazel +++ b/vendor/github.com/lestrrat-go/jwx/v3/cert/BUILD.bazel @@ -5,12 +5,15 @@ go_library( srcs = [ "cert.go", "chain.go", + "options.go", + "settings.go", ], importpath = "github.com/lestrrat-go/jwx/v3/cert", visibility = ["//visibility:public"], deps = [ - "//internal/base64", - "//internal/tokens", + "//internal/base64", + "//internal/tokens", + "@com_github_lestrrat_go_option_v2//:option", ], ) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/cert/cert.go b/vendor/github.com/lestrrat-go/jwx/v3/cert/cert.go index efefbcb417..8007c74133 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/cert/cert.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/cert/cert.go @@ -1,6 +1,7 @@ package cert import ( + "bytes" "crypto/x509" stdlibb64 "encoding/base64" "fmt" @@ -31,18 +32,81 @@ func EncodeBase64(der []byte) ([]byte, error) { return dst, nil } -// Parse is a utility function to decode a base64 encoded -// ASN.1 DER format certificate, and to parse the byte sequence. -// The certificate must be in PKIX format, and it must not contain PEM markers +// Parse decodes a base64-encoded ASN.1 DER certificate and validates that it +// parses as X.509. +// +// The certificate must be in PKIX format and it must not contain PEM markers. +// The maximum decoded certificate size is controlled by `cert.Settings()`. func Parse(src []byte) (*x509.Certificate, error) { + src = stripASCIIWhitespace(bytes.TrimSpace(src)) + if err := validateEncodedCertificateSize(src); err != nil { + return nil, err + } + dst, err := base64.Decode(src) if err != nil { return nil, fmt.Errorf(`failed to base64 decode the certificate: %w`, err) } - cert, err := x509.ParseCertificate(dst) + return validateDERCertificate(dst) +} + +func validateDERCertificate(der []byte) (*x509.Certificate, error) { + if err := validateCertificateSize(len(der)); err != nil { + return nil, err + } + + cert, err := x509.ParseCertificate(der) if err != nil { return nil, fmt.Errorf(`failed to parse x509 certificate: %w`, err) } return cert, nil } + +func validateEncodedCertificateSize(src []byte) error { + return validateCertificateSize(decodedCertificateSize(src)) +} + +func decodedCertificateSize(src []byte) int { + n := len(src) + if n == 0 { + return 0 + } + + size := n / 4 * 3 + switch n % 4 { + case 2: + size++ + case 3: + size += 2 + } + + if n%4 == 0 && src[n-1] == '=' { + size-- + if n > 1 && src[n-2] == '=' { + size-- + } + } + + if size < 0 { + return 0 + } + return size +} + +func normalizeAndValidateChainCertificate(src []byte) ([]byte, error) { + normalized := stripASCIIWhitespace(src) + if err := validateEncodedCertificateSize(normalized); err != nil { + return nil, err + } + + der, err := stdlibb64.StdEncoding.DecodeString(string(normalized)) + if err != nil { + return nil, fmt.Errorf(`failed to base64 decode the certificate: %w`, err) + } + + if _, err := validateDERCertificate(der); err != nil { + return nil, err + } + return normalized, nil +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/cert/chain.go b/vendor/github.com/lestrrat-go/jwx/v3/cert/chain.go index 112274669a..30e48296f0 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/cert/chain.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/cert/chain.go @@ -2,8 +2,11 @@ package cert import ( "bytes" + "encoding/base64" "encoding/json" + "encoding/pem" "fmt" + "io" "github.com/lestrrat-go/jwx/v3/internal/tokens" ) @@ -11,8 +14,9 @@ import ( // Chain represents a certificate chain as used in the `x5c` field of // various objects within JOSE. // -// It stores the certificates as a list of base64 encoded []byte -// sequence. By definition these values must PKIX encoded. +// It stores the certificates as a list of base64-encoded byte sequences. Every +// certificate added to or decoded into the chain must parse as X.509 and is +// subject to the global limits configured by `cert.Settings()`. type Chain struct { certificates [][]byte } @@ -24,24 +28,65 @@ func (cc Chain) MarshalJSON() ([]byte, error) { if i > 0 { buf.WriteByte(tokens.Comma) } - buf.WriteByte('"') - buf.Write(cert) - buf.WriteByte('"') + encoded, err := json.Marshal(string(cert)) + if err != nil { + return nil, fmt.Errorf(`failed to encode certificate at index %d: %w`, i, err) + } + buf.Write(encoded) } buf.WriteByte(tokens.CloseSquareBracket) return buf.Bytes(), nil } +// UnmarshalJSON decodes an `x5c` JSON array and validates each entry as a +// base64-encoded X.509 certificate. func (cc *Chain) UnmarshalJSON(data []byte) error { - var tmp []string - if err := json.Unmarshal(data, &tmp); err != nil { + dec := json.NewDecoder(bytes.NewReader(data)) + tok, err := dec.Token() + if err != nil { return fmt.Errorf(`failed to unmarshal certificate chain: %w`, err) } - certs := make([][]byte, len(tmp)) - for i, cert := range tmp { - certs[i] = []byte(cert) + delim, ok := tok.(json.Delim) + if !ok || delim != '[' { + return fmt.Errorf(`failed to unmarshal certificate chain: expected JSON array`) + } + + var certs [][]byte + for dec.More() { + if err := validateChainLength(len(certs) + 1); err != nil { + return fmt.Errorf(`failed to unmarshal certificate chain: %w`, err) + } + + var cert string + if err := dec.Decode(&cert); err != nil { + return fmt.Errorf(`failed to decode certificate at index %d: %w`, len(certs), err) + } + + normalized, err := normalizeAndValidateChainCertificate([]byte(cert)) + if err != nil { + return fmt.Errorf(`failed to decode certificate at index %d: %w`, len(certs), err) + } + certs = append(certs, normalized) + } + + tok, err = dec.Token() + if err != nil { + return fmt.Errorf(`failed to unmarshal certificate chain: %w`, err) + } + + delim, ok = tok.(json.Delim) + if !ok || delim != ']' { + return fmt.Errorf(`failed to unmarshal certificate chain: expected closing array`) + } + + if _, err := dec.Token(); err != io.EOF { + if err != nil { + return fmt.Errorf(`failed to unmarshal certificate chain: %w`, err) + } + return fmt.Errorf(`failed to unmarshal certificate chain: unexpected trailing data`) } + cc.certificates = certs return nil } @@ -62,19 +107,52 @@ func (cc *Chain) Len() int { return len(cc.certificates) } -var pemStart = []byte("----- BEGIN CERTIFICATE -----") -var pemEnd = []byte("----- END CERTIFICATE -----") - func (cc *Chain) AddString(der string) error { return cc.Add([]byte(der)) } +// Add appends a certificate to the chain. +// +// Input may be either a PEM `CERTIFICATE` block or a base64-encoded DER value +// as stored in JOSE `x5c` fields. The certificate is validated as X.509 and is +// subject to the global limits configured by `cert.Settings()`. func (cc *Chain) Add(der []byte) error { - // We're going to be nice and remove marker lines if they - // give it to us - der = bytes.TrimPrefix(der, pemStart) - der = bytes.TrimSuffix(der, pemEnd) + if err := validateChainLength(len(cc.certificates) + 1); err != nil { + return fmt.Errorf(`cert.Chain.Add: %w`, err) + } + der = bytes.TrimSpace(der) - cc.certificates = append(cc.certificates, der) + // Accept a PEM-encoded CERTIFICATE block and convert it to the + // base64(DER) form that x5c requires. + if block, _ := pem.Decode(der); block != nil && block.Type == "CERTIFICATE" { + if _, err := validateDERCertificate(block.Bytes); err != nil { + return fmt.Errorf(`cert.Chain.Add: %w`, err) + } + + encoded := make([]byte, base64.StdEncoding.EncodedLen(len(block.Bytes))) + base64.StdEncoding.Encode(encoded, block.Bytes) + cc.certificates = append(cc.certificates, encoded) + return nil + } + + // Non-PEM input must be base64(DER). Strip any internal whitespace + // (callers commonly pass multi-line base64 literals) and validate. + normalized, err := normalizeAndValidateChainCertificate(der) + if err != nil { + return fmt.Errorf(`cert.Chain.Add: %w`, err) + } + cc.certificates = append(cc.certificates, normalized) return nil } + +func stripASCIIWhitespace(src []byte) []byte { + dst := make([]byte, 0, len(src)) + for _, b := range src { + switch b { + case ' ', '\t', '\r', '\n', '\v', '\f': + continue + } + dst = append(dst, b) + } + return dst +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/cert/options.go b/vendor/github.com/lestrrat-go/jwx/v3/cert/options.go new file mode 100644 index 0000000000..b374ae18cc --- /dev/null +++ b/vendor/github.com/lestrrat-go/jwx/v3/cert/options.go @@ -0,0 +1,34 @@ +package cert + +import "github.com/lestrrat-go/option/v2" + +// GlobalOption describes an option that can be passed to `cert.Settings()`. +type GlobalOption interface { + option.Interface + globalOption() +} + +type globalOption struct { + option.Interface +} + +func (*globalOption) globalOption() {} + +type identMaxChainLength struct{} +type identMaxCertificateSize struct{} + +// WithMaxChainLength specifies the maximum number of certificates allowed in +// a certificate chain handled by `cert.Chain`. +// +// The default is 10. Set to 0 to disable the limit. +func WithMaxChainLength(v int) GlobalOption { + return &globalOption{option.New(identMaxChainLength{}, v)} +} + +// WithMaxCertificateSize specifies the maximum decoded DER size, in bytes, +// accepted by `cert.Parse()` and `cert.Chain` ingestion. +// +// The default is 256 KiB. Set to 0 to disable the limit. +func WithMaxCertificateSize(v int64) GlobalOption { + return &globalOption{option.New(identMaxCertificateSize{}, v)} +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/cert/settings.go b/vendor/github.com/lestrrat-go/jwx/v3/cert/settings.go new file mode 100644 index 0000000000..50c90800de --- /dev/null +++ b/vendor/github.com/lestrrat-go/jwx/v3/cert/settings.go @@ -0,0 +1,76 @@ +package cert + +import ( + "fmt" + "sync/atomic" +) + +const ( + defaultMaxChainLength = 10 + defaultMaxCertificateSize = 256 * 1024 +) + +var maxChainLength atomic.Int64 +var maxCertificateSize atomic.Int64 + +func init() { + maxChainLength.Store(defaultMaxChainLength) + maxCertificateSize.Store(defaultMaxCertificateSize) +} + +// Settings configures process-global validation limits for `cert.Parse()` and +// `cert.Chain` ingestion. +// +// These settings are read atomically, so changing them at runtime is race-free. +// However, concurrent parses may observe a mix of old and new values. Configure +// them once at program startup when possible. +func Settings(options ...GlobalOption) { + for _, opt := range options { + switch opt.Ident() { + case identMaxChainLength{}: + var v int + if err := opt.Value(&v); err != nil { + panic(fmt.Sprintf("cert.Settings: value for option WithMaxChainLength must be an int: %s", err)) + } + if v < 0 { + panic("cert.Settings: WithMaxChainLength must be greater than or equal to zero") + } + maxChainLength.Store(int64(v)) + case identMaxCertificateSize{}: + var v int64 + if err := opt.Value(&v); err != nil { + panic(fmt.Sprintf("cert.Settings: value for option WithMaxCertificateSize must be an int64: %s", err)) + } + if v < 0 { + panic("cert.Settings: WithMaxCertificateSize must be greater than or equal to zero") + } + maxCertificateSize.Store(v) + } + } +} + +func currentMaxChainLength() int64 { + return maxChainLength.Load() +} + +func currentMaxCertificateSize() int64 { + return maxCertificateSize.Load() +} + +func validateChainLength(n int) error { + limit := currentMaxChainLength() + if limit == 0 || int64(n) <= limit { + return nil + } + + return fmt.Errorf(`certificate chain length %d exceeds maximum allowed length of %d`, n, limit) +} + +func validateCertificateSize(n int) error { + limit := currentMaxCertificateSize() + if limit == 0 || int64(n) <= limit { + return nil + } + + return fmt.Errorf(`certificate size %d exceeds maximum allowed size of %d bytes`, n, limit) +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/asmbase64.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/asmbase64.go index 6e83ecc4a5..f06f6f6c74 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/asmbase64.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/asmbase64.go @@ -3,7 +3,9 @@ package base64 import ( + stdbase64 "encoding/base64" "fmt" + "io" "slices" asmbase64 "github.com/segmentio/asm/base64" @@ -25,6 +27,15 @@ func (e asmEncoder) AppendEncode(dst, src []byte) []byte { return dst[:len(dst)+n] } +// NewEncoder satisfies [StreamEncoder]. segmentio/asm's base64 package +// does not provide a streaming encoder, so this falls back to the +// stdlib's RawURLEncoding streaming encoder — output is byte-identical +// for RFC 4648 raw URL encoding, so the signing prefix (produced via +// asm) and the streamed payload remain consistent. +func (e asmEncoder) NewEncoder(w io.Writer) io.WriteCloser { + return stdbase64.NewEncoder(stdbase64.RawURLEncoding, w) +} + type asmDecoder struct{} func (d asmDecoder) Decode(src []byte) ([]byte, error) { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/base64.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/base64.go index 5ed8e35006..356131da19 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/base64.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/base64.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/binary" "fmt" + "io" "sync" ) @@ -19,6 +20,55 @@ type Encoder interface { AppendEncode([]byte, []byte) []byte } +// StreamEncoder is an [Encoder] that can also produce an incremental +// [io.WriteCloser] for encoding a byte stream directly into a downstream +// writer. This is the shape the jws streaming detached-payload path +// needs to avoid materializing the payload in memory. +// +// The stdlib *[encoding/base64.Encoding] satisfies this interface +// automatically via [AsStreamEncoder] (the stdlib exposes NewEncoder as +// a package-level function rather than a method, so a small wrapper is +// applied on lookup). Extension modules providing custom encoders +// should implement [io.WriteCloser]-returning NewEncoder if they want +// their encoder honored by the streaming path. +type StreamEncoder interface { + Encoder + // NewEncoder returns a new [io.WriteCloser] that encodes bytes + // written to it and forwards the encoded output to w. Close must + // be called to flush any partial final block. + NewEncoder(w io.Writer) io.WriteCloser +} + +// AsStreamEncoder reports whether e can be used as a [StreamEncoder] +// and returns the stream-capable view. Callers should error out when +// the second return value is false rather than silently falling back +// to a different encoder, to avoid mixing encodings within a single +// signing operation. +// +// The stdlib [*encoding/base64.Encoding] is supported as a special +// case (its streaming form is a top-level function rather than a +// method, so it does not directly satisfy the interface). +func AsStreamEncoder(e Encoder) (StreamEncoder, bool) { + if s, ok := e.(StreamEncoder); ok { + return s, true + } + if enc, ok := e.(*base64.Encoding); ok { + return stdStreamEncoder{Encoding: enc}, true + } + return nil, false +} + +// stdStreamEncoder wraps the stdlib [*base64.Encoding] so it satisfies +// [StreamEncoder]. It is used as the fallback in [AsStreamEncoder] +// when a caller passes a raw [*base64.Encoding]. +type stdStreamEncoder struct { + *base64.Encoding +} + +func (e stdStreamEncoder) NewEncoder(w io.Writer) io.WriteCloser { + return base64.NewEncoder(e.Encoding, w) +} + var muEncoder sync.RWMutex var encoder Encoder = base64.RawURLEncoding var muDecoder sync.RWMutex @@ -59,6 +109,14 @@ func Encode(src []byte) []byte { return dst } +func AppendEncode(dst, src []byte) []byte { + return getEncoder().AppendEncode(dst, src) +} + +func EncodedLen(n int) int { + return getEncoder().EncodedLen(n) +} + func EncodeToString(src []byte) string { return getEncoder().EncodeToString(src) } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/json/json.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/json/json.go index c1917ef27a..4dec2b806c 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/internal/json/json.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/internal/json/json.go @@ -3,16 +3,16 @@ package json import ( "bytes" "fmt" - "os" + "sync/atomic" "github.com/lestrrat-go/jwx/v3/internal/base64" ) -var useNumber uint32 // TODO: at some point, change to atomic.Bool +var useNumber atomic.Uint32 func UseNumber() bool { - return atomic.LoadUint32(&useNumber) == 1 + return useNumber.Load() == 1 } // Sets the global configuration for json decoding @@ -21,7 +21,7 @@ func DecoderSettings(inUseNumber bool) { if inUseNumber { val = 1 } - atomic.StoreUint32(&useNumber, val) + useNumber.Store(val) } // Unmarshal respects the values specified in DecoderSettings, @@ -45,7 +45,35 @@ func AssignNextBytesToken(dst *[]byte, dec *Decoder) error { return nil } -func ReadNextStringToken(dec *Decoder) (string, error) { +func shouldRejectNullStrings(dc DecodeCtx) bool { + if dc != nil { + if sdc, ok := dc.(StrictStringDecodeCtx); ok { + return sdc.StrictStrings() + } + } + return false +} + +// ReadNextStringToken reads the next JSON token from the decoder and +// returns it as a string. By default, JSON null is silently accepted as "". +// When the given DecodeCtx implements StrictStringDecodeCtx and StrictStrings() +// returns true, null values are rejected. +func ReadNextStringToken(dec *Decoder, dc DecodeCtx) (string, error) { + if shouldRejectNullStrings(dc) { + var val any + if err := dec.Decode(&val); err != nil { + return "", fmt.Errorf(`error reading next value: %w`, err) + } + if val == nil { + return "", fmt.Errorf(`error reading next value: expected string, got null`) + } + s, ok := val.(string) + if !ok { + return "", fmt.Errorf(`error reading next value: expected string, got %T`, val) + } + return s, nil + } + var val string if err := dec.Decode(&val); err != nil { return "", fmt.Errorf(`error reading next value: %w`, err) @@ -53,8 +81,8 @@ func ReadNextStringToken(dec *Decoder) (string, error) { return val, nil } -func AssignNextStringToken(dst **string, dec *Decoder) error { - val, err := ReadNextStringToken(dec) +func AssignNextStringToken(dst **string, dec *Decoder, dc DecodeCtx) error { + val, err := ReadNextStringToken(dec, dc) if err != nil { return err } @@ -106,22 +134,33 @@ type DecodeCtxContainer interface { SetDecodeCtx(DecodeCtx) } +// StrictStringDecodeCtx is an optional interface that DecodeCtx implementations +// can satisfy to control per-call null string rejection. +type StrictStringDecodeCtx interface { + StrictStrings() bool +} + // stock decodeCtx. should cover 80% of the cases type decodeCtx struct { - registry *Registry + registry *Registry + strictStrings bool } +// NewDecodeCtx creates a new DecodeCtx with the given registry. func NewDecodeCtx(r *Registry) DecodeCtx { return &decodeCtx{registry: r} } +// NewDecodeCtxStrictStrings creates a new DecodeCtx with the given registry +// and strict string rejection flag. +func NewDecodeCtxStrictStrings(r *Registry, strict bool) DecodeCtx { + return &decodeCtx{registry: r, strictStrings: strict} +} + func (dc *decodeCtx) Registry() *Registry { return dc.registry } -func Dump(v any) { - enc := NewEncoder(os.Stdout) - enc.SetIndent("", " ") - //nolint:errchkjson - _ = enc.Encode(v) +func (dc *decodeCtx) StrictStrings() bool { + return dc.strictStrings } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/json/stdlib.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/json/stdlib.go index 9e51fa7fe9..f249d8f60a 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/internal/json/stdlib.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/internal/json/stdlib.go @@ -1,6 +1,5 @@ //go:build !jwx_goccy -//nolint:revive package json import ( diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/jwxio/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/internal/jwxio/BUILD.bazel deleted file mode 100644 index c70c4d2871..0000000000 --- a/vendor/github.com/lestrrat-go/jwx/v3/internal/jwxio/BUILD.bazel +++ /dev/null @@ -1,8 +0,0 @@ -load("@rules_go//go:def.bzl", "go_library") - -go_library( - name = "jwxio", - srcs = ["jwxio.go"], - importpath = "github.com/lestrrat-go/jwx/v3/internal/jwxio", - visibility = ["//:__subpackages__"], -) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/jwxio/jwxio.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/jwxio/jwxio.go deleted file mode 100644 index 8396417a9d..0000000000 --- a/vendor/github.com/lestrrat-go/jwx/v3/internal/jwxio/jwxio.go +++ /dev/null @@ -1,29 +0,0 @@ -package jwxio - -import ( - "bytes" - "errors" - "io" - "strings" -) - -var errNonFiniteSource = errors.New(`cannot read from non-finite source`) - -func NonFiniteSourceError() error { - return errNonFiniteSource -} - -// ReadAllFromFiniteSource reads all data from a io.Reader _if_ it comes from a -// finite source. -func ReadAllFromFiniteSource(rdr io.Reader) ([]byte, error) { - switch rdr.(type) { - case *bytes.Reader, *bytes.Buffer, *strings.Reader: - data, err := io.ReadAll(rdr) - if err != nil { - return nil, err - } - return data, nil - default: - return nil, errNonFiniteSource - } -} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/BUILD.bazel index cebc269330..c48330e278 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/BUILD.bazel +++ b/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/BUILD.bazel @@ -3,7 +3,6 @@ load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "pool", srcs = [ - "big_int.go", "byte_slice.go", "bytes_buffer.go", "error_slice.go", diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/big_int.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/big_int.go deleted file mode 100644 index 57c446d4d2..0000000000 --- a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/big_int.go +++ /dev/null @@ -1,19 +0,0 @@ -package pool - -import "math/big" - -var bigIntPool = New[*big.Int](allocBigInt, freeBigInt) - -func allocBigInt() *big.Int { - return &big.Int{} -} - -func freeBigInt(b *big.Int) *big.Int { - b.SetInt64(0) // Reset the value to zero - return b -} - -// BigInt returns a pool of *big.Int instances. -func BigInt() *Pool[*big.Int] { - return bigIntPool -} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/byte_slice.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/byte_slice.go index 46f1028343..857d41ca38 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/byte_slice.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/byte_slice.go @@ -9,9 +9,14 @@ func allocByteSlice() []byte { } func freeByteSlice(b []byte) []byte { + // Defensive: scrub the entire backing array, not just b[:len(b)]. No + // current caller is known to reslice past len(b) and observe stale + // bytes, but a defer Put(buf) that captures buf at len=0 (before a + // subsequent buf = buf[:n]) would otherwise leave plaintext resident + // in the pool's backing storage. + b = b[:cap(b)] clear(b) - b = b[:0] // Reset the slice to zero length - return b + return b[:0] } func ByteSlice() SlicePool[byte] { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/bytes_buffer.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/bytes_buffer.go index a877f73ff8..090c83400d 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/bytes_buffer.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/bytes_buffer.go @@ -9,6 +9,13 @@ func allocBytesBuffer() *bytes.Buffer { } func freeBytesBuffer(b *bytes.Buffer) *bytes.Buffer { + // Zero the backing array before returning to pool — the buffer + // may hold private-key material, plaintext, or HMAC input. + // b.Bytes() shares the internal slice (offset is always 0 in + // our write-only usage); reslicing to cap reaches all residual bytes. + if buf := b.Bytes(); cap(buf) > 0 { + clear(buf[:cap(buf)]) + } b.Reset() return b } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/jwa/BUILD.bazel index cfb7af02a0..6d0af7efb3 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/BUILD.bazel +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "compression_gen.go", "content_encryption_gen.go", + "ed448.go", "elliptic_gen.go", "jwa.go", "key_encryption_gen.go", @@ -29,10 +30,11 @@ go_test( "jwa_test.go", "key_encryption_gen_test.go", "key_type_gen_test.go", + "options_gen_test.go", "signature_gen_test.go", ], + embed = [":jwa"], deps = [ - ":jwa", "@com_github_stretchr_testify//require", "@com_github_lestrrat_go_option_v2//:option", ], diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/compression_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/compression_gen.go index a7a2451afa..42e5006a9c 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/compression_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/compression_gen.go @@ -22,6 +22,9 @@ func init() { algorithms[1] = NewCompressionAlgorithm("") RegisterCompressionAlgorithm(algorithms...) + for _, alg := range algorithms { + builtinCompressionAlgorithm[alg.String()] = struct{}{} + } } // Deflate returns an object representing the "DEF" content compression algorithm value. Using this value specifies that the content should be compressed using DEFLATE (RFC 1951). @@ -88,36 +91,44 @@ func LookupCompressionAlgorithm(name string) (CompressionAlgorithm, bool) { // RegisterCompressionAlgorithm registers a new CompressionAlgorithm. The signature value must be immutable // and safe to be used by multiple goroutines, as it is going to be shared with all other users of this library. +// +// Registration is process-global. Built-in identifiers such as RS256 are +// reserved and cannot be replaced by callers after init has completed; use a +// distinct name for third-party algorithms. func RegisterCompressionAlgorithm(algorithms ...CompressionAlgorithm) { muAllCompressionAlgorithm.Lock() + defer muAllCompressionAlgorithm.Unlock() for _, alg := range algorithms { + if _, ok := builtinCompressionAlgorithm[alg.String()]; ok { + if existing, ok := allCompressionAlgorithm[alg.String()]; ok && existing != alg { + continue + } + continue + } allCompressionAlgorithm[alg.String()] = alg } - muAllCompressionAlgorithm.Unlock() - rebuildCompressionAlgorithm() + rebuildCompressionAlgorithmLocked() } // UnregisterCompressionAlgorithm unregisters a CompressionAlgorithm from its known database. // Non-existent entries, as well as built-in algorithms will silently be ignored. func UnregisterCompressionAlgorithm(algorithms ...CompressionAlgorithm) { muAllCompressionAlgorithm.Lock() + defer muAllCompressionAlgorithm.Unlock() for _, alg := range algorithms { if _, ok := builtinCompressionAlgorithm[alg.String()]; ok { continue } delete(allCompressionAlgorithm, alg.String()) } - muAllCompressionAlgorithm.Unlock() - rebuildCompressionAlgorithm() + rebuildCompressionAlgorithmLocked() } -func rebuildCompressionAlgorithm() { +func rebuildCompressionAlgorithmLocked() { list := make([]CompressionAlgorithm, 0, len(allCompressionAlgorithm)) - muAllCompressionAlgorithm.RLock() for _, v := range allCompressionAlgorithm { list = append(list, v) } - muAllCompressionAlgorithm.RUnlock() sort.Slice(list, func(i, j int) bool { return list[i].String() < list[j].String() }) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/content_encryption_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/content_encryption_gen.go index 8ccc47e462..b04b1c755d 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/content_encryption_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/content_encryption_gen.go @@ -5,18 +5,10 @@ package jwa import ( "encoding/json" "fmt" - "sort" - "sync" "github.com/lestrrat-go/jwx/v3/internal/tokens" ) -var muAllContentEncryptionAlgorithm sync.RWMutex -var allContentEncryptionAlgorithm = map[string]ContentEncryptionAlgorithm{} -var muListContentEncryptionAlgorithm sync.RWMutex -var listContentEncryptionAlgorithm []ContentEncryptionAlgorithm -var builtinContentEncryptionAlgorithm = map[string]struct{}{} - func init() { // builtin values for ContentEncryptionAlgorithm algorithms := make([]ContentEncryptionAlgorithm, 6) @@ -28,6 +20,9 @@ func init() { algorithms[5] = NewContentEncryptionAlgorithm(tokens.A256GCM) RegisterContentEncryptionAlgorithm(algorithms...) + for _, alg := range algorithms { + markBuiltin(alg.String()) + } } // A128CBC_HS256 returns an object representing A128CBC-HS256. Using this value specifies that the content should be encrypted using AES-CBC + HMAC-SHA256 (128). @@ -61,13 +56,11 @@ func A256GCM() ContentEncryptionAlgorithm { } func lookupBuiltinContentEncryptionAlgorithm(name string) ContentEncryptionAlgorithm { - muAllContentEncryptionAlgorithm.RLock() - v, ok := allContentEncryptionAlgorithm[name] - muAllContentEncryptionAlgorithm.RUnlock() + v, ok := lookupAlgorithm(algKindContentEncryption, name) if !ok { panic(fmt.Sprintf(`jwa: ContentEncryptionAlgorithm %q not registered`, name)) } - return v + return v.(ContentEncryptionAlgorithm) } // ContentEncryptionAlgorithm represents the various encryption algorithms as described in https://tools.ietf.org/html/rfc7518#section-5 @@ -106,57 +99,46 @@ func NewContentEncryptionAlgorithm(name string, options ...NewAlgorithmOption) C // LookupContentEncryptionAlgorithm returns the ContentEncryptionAlgorithm object for the given name. func LookupContentEncryptionAlgorithm(name string) (ContentEncryptionAlgorithm, bool) { - muAllContentEncryptionAlgorithm.RLock() - v, ok := allContentEncryptionAlgorithm[name] - muAllContentEncryptionAlgorithm.RUnlock() - return v, ok + if v, ok := lookupAlgorithm(algKindContentEncryption, name); ok { + return v.(ContentEncryptionAlgorithm), true + } + var zero ContentEncryptionAlgorithm + return zero, false } // RegisterContentEncryptionAlgorithm registers a new ContentEncryptionAlgorithm. The signature value must be immutable // and safe to be used by multiple goroutines, as it is going to be shared with all other users of this library. +// +// Registration is process-global. Built-in identifiers such as RS256 are +// reserved and cannot be replaced by callers after init has completed; use a +// distinct name for third-party algorithms. +// +// SignatureAlgorithm, KeyEncryptionAlgorithm, and ContentEncryptionAlgorithm +// share a single algorithm-name namespace so that KeyAlgorithmFrom can +// resolve unambiguously. Registering a name that is already registered as a +// different kind is a silent no-op (the first Register* call wins). func RegisterContentEncryptionAlgorithm(algorithms ...ContentEncryptionAlgorithm) { - muAllContentEncryptionAlgorithm.Lock() for _, alg := range algorithms { - allContentEncryptionAlgorithm[alg.String()] = alg + registerAlgorithm(algKindContentEncryption, alg) } - muAllContentEncryptionAlgorithm.Unlock() - rebuildContentEncryptionAlgorithm() } // UnregisterContentEncryptionAlgorithm unregisters a ContentEncryptionAlgorithm from its known database. // Non-existent entries, as well as built-in algorithms will silently be ignored. func UnregisterContentEncryptionAlgorithm(algorithms ...ContentEncryptionAlgorithm) { - muAllContentEncryptionAlgorithm.Lock() for _, alg := range algorithms { - if _, ok := builtinContentEncryptionAlgorithm[alg.String()]; ok { - continue - } - delete(allContentEncryptionAlgorithm, alg.String()) + unregisterAlgorithm(algKindContentEncryption, alg.String()) } - muAllContentEncryptionAlgorithm.Unlock() - rebuildContentEncryptionAlgorithm() -} - -func rebuildContentEncryptionAlgorithm() { - list := make([]ContentEncryptionAlgorithm, 0, len(allContentEncryptionAlgorithm)) - muAllContentEncryptionAlgorithm.RLock() - for _, v := range allContentEncryptionAlgorithm { - list = append(list, v) - } - muAllContentEncryptionAlgorithm.RUnlock() - sort.Slice(list, func(i, j int) bool { - return list[i].String() < list[j].String() - }) - muListContentEncryptionAlgorithm.Lock() - listContentEncryptionAlgorithm = list - muListContentEncryptionAlgorithm.Unlock() } // ContentEncryptionAlgorithms returns a list of all available values for ContentEncryptionAlgorithm. func ContentEncryptionAlgorithms() []ContentEncryptionAlgorithm { - muListContentEncryptionAlgorithm.RLock() - defer muListContentEncryptionAlgorithm.RUnlock() - return listContentEncryptionAlgorithm + raw := listAlgorithmsByKind(algKindContentEncryption) + out := make([]ContentEncryptionAlgorithm, len(raw)) + for i, alg := range raw { + out[i] = alg.(ContentEncryptionAlgorithm) + } + return out } // MarshalJSON serializes the ContentEncryptionAlgorithm object to a JSON string. diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/ed448.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/ed448.go new file mode 100644 index 0000000000..f28d7296b3 --- /dev/null +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/ed448.go @@ -0,0 +1,14 @@ +package jwa + +// EdDSAEd448 returns an object representing the EdDSA signature algorithm +// using Ed448 (RFC 9864). +// +// Unlike built-in algorithms, Ed448 is not registered by default. Import +// the ed448 module for its side effects to enable Ed448 support: +// +// import _ "github.com/lestrrat-go/jwx-circl-ed448" +// +// The function name is tentative and may change in future releases. +func EdDSAEd448() SignatureAlgorithm { + return NewSignatureAlgorithm("Ed448") +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/elliptic_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/elliptic_gen.go index 2418efde08..b6d7de5490 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/elliptic_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/elliptic_gen.go @@ -27,6 +27,9 @@ func init() { algorithms[6] = NewEllipticCurveAlgorithm("X448") RegisterEllipticCurveAlgorithm(algorithms...) + for _, alg := range algorithms { + builtinEllipticCurveAlgorithm[alg.String()] = struct{}{} + } } // Ed25519 returns an object representing Ed25519 algorithm for EdDSA operations. @@ -125,36 +128,44 @@ func LookupEllipticCurveAlgorithm(name string) (EllipticCurveAlgorithm, bool) { // RegisterEllipticCurveAlgorithm registers a new EllipticCurveAlgorithm. The signature value must be immutable // and safe to be used by multiple goroutines, as it is going to be shared with all other users of this library. +// +// Registration is process-global. Built-in identifiers such as RS256 are +// reserved and cannot be replaced by callers after init has completed; use a +// distinct name for third-party algorithms. func RegisterEllipticCurveAlgorithm(algorithms ...EllipticCurveAlgorithm) { muAllEllipticCurveAlgorithm.Lock() + defer muAllEllipticCurveAlgorithm.Unlock() for _, alg := range algorithms { + if _, ok := builtinEllipticCurveAlgorithm[alg.String()]; ok { + if existing, ok := allEllipticCurveAlgorithm[alg.String()]; ok && existing != alg { + continue + } + continue + } allEllipticCurveAlgorithm[alg.String()] = alg } - muAllEllipticCurveAlgorithm.Unlock() - rebuildEllipticCurveAlgorithm() + rebuildEllipticCurveAlgorithmLocked() } // UnregisterEllipticCurveAlgorithm unregisters a EllipticCurveAlgorithm from its known database. // Non-existent entries, as well as built-in algorithms will silently be ignored. func UnregisterEllipticCurveAlgorithm(algorithms ...EllipticCurveAlgorithm) { muAllEllipticCurveAlgorithm.Lock() + defer muAllEllipticCurveAlgorithm.Unlock() for _, alg := range algorithms { if _, ok := builtinEllipticCurveAlgorithm[alg.String()]; ok { continue } delete(allEllipticCurveAlgorithm, alg.String()) } - muAllEllipticCurveAlgorithm.Unlock() - rebuildEllipticCurveAlgorithm() + rebuildEllipticCurveAlgorithmLocked() } -func rebuildEllipticCurveAlgorithm() { +func rebuildEllipticCurveAlgorithmLocked() { list := make([]EllipticCurveAlgorithm, 0, len(allEllipticCurveAlgorithm)) - muAllEllipticCurveAlgorithm.RLock() for _, v := range allEllipticCurveAlgorithm { list = append(list, v) } - muAllEllipticCurveAlgorithm.RUnlock() sort.Slice(list, func(i, j int) bool { return list[i].String() < list[j].String() }) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/jwa.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/jwa.go index 29ac1dfe76..a363b80e41 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/jwa.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/jwa.go @@ -6,8 +6,12 @@ package jwa import ( "errors" "fmt" + "sort" + "sync" ) +const maxKeyAlgorithmErrorPreview = 64 + // KeyAlgorithm is a workaround for jwk.Key being able to contain different // types of algorithms in its `alg` field. // @@ -30,38 +34,189 @@ func ErrInvalidKeyAlgorithm() error { return errInvalidKeyAlgorithm } +func formatInvalidKeyAlgorithmValue(v string) string { + runes := []rune(v) + if len(runes) <= maxKeyAlgorithmErrorPreview { + return fmt.Sprintf("%q", v) + } + + return fmt.Sprintf("%q", string(runes[:maxKeyAlgorithmErrorPreview])+`...`) +} + +// algorithmKind tags entries in the shared algRegistry so the +// per-kind public Register/Lookup/Unregister/s functions can +// dispatch through one map without losing the typed identity of each +// algorithm. +type algorithmKind uint8 + +const ( + algKindUnknown algorithmKind = iota + algKindSignature + algKindKeyEncryption + algKindContentEncryption +) + +func (k algorithmKind) String() string { + switch k { + case algKindSignature: + return "SignatureAlgorithm" + case algKindKeyEncryption: + return "KeyEncryptionAlgorithm" + case algKindContentEncryption: + return "ContentEncryptionAlgorithm" + default: + return "unknown algorithm kind" + } +} + +type algRegistryEntry struct { + kind algorithmKind + alg KeyAlgorithm + builtin bool +} + +// algRegistry is the single shared namespace for the three +// KeyAlgorithm-implementing kinds. Independent per-kind maps would +// let an extension register the same name as both (say) a +// SignatureAlgorithm and a KeyEncryptionAlgorithm, after which +// KeyAlgorithmFrom("X") would resolve to whichever kind was tried +// first — silently flipping with import order. Funnelling all three +// through one map fixes that ambiguity at registration time. +var ( + muAlgRegistry sync.RWMutex + algRegistry = map[string]algRegistryEntry{} +) + +// registerAlgorithm is the shared backend for the three public +// Register{Signature,KeyEncryption,ContentEncryption}Algorithm +// functions. +// +// Behavior: +// - Re-registering the exact same value (same kind, same alg) is a +// no-op. +// - Cross-kind name reuse is a silent no-op: the first registration +// wins and the second Register* call has no effect. v3's +// pre-existing Register* signature returns no error and v3 does +// not change observable error/panic behavior, so the cross-kind +// case is silently skipped — KeyAlgorithmFrom now resolves +// unambiguously to the first-registered kind. (v4 escalates this +// to a returned error from Register*.) +// - Built-in replacement is a silent no-op, preserving the +// pre-unification per-kind v3 Register* behavior. +// - Same-kind, non-builtin re-registration with a different value +// silently overwrites. This preserves the pre-unification +// behavior of the per-kind Register* functions. +func registerAlgorithm(kind algorithmKind, alg KeyAlgorithm) { + name := alg.String() + muAlgRegistry.Lock() + defer muAlgRegistry.Unlock() + if existing, ok := algRegistry[name]; ok { + if existing.kind == kind && existing.alg == alg { + return + } + if existing.kind != kind { + return + } + if existing.builtin { + return + } + } + algRegistry[name] = algRegistryEntry{kind: kind, alg: alg} +} + +// markBuiltin flips the builtin flag on an already-registered name. +// Called by the per-kind generated init() after the bulk Register* +// pass, preserving the existing two-phase init pattern. +func markBuiltin(name string) { + muAlgRegistry.Lock() + defer muAlgRegistry.Unlock() + if entry, ok := algRegistry[name]; ok { + entry.builtin = true + algRegistry[name] = entry + } +} + +// unregisterAlgorithm is the shared backend for the three public +// Unregister*Algorithm functions. No-op for built-ins, no-op for a +// kind mismatch, no-op for unknown names — same surface contract as +// the pre-unification per-kind Unregister*. +func unregisterAlgorithm(kind algorithmKind, name string) { + muAlgRegistry.Lock() + defer muAlgRegistry.Unlock() + if entry, ok := algRegistry[name]; ok && entry.kind == kind && !entry.builtin { + delete(algRegistry, name) + } +} + +// lookupAlgorithm returns the registered KeyAlgorithm for name iff it +// is registered as the requested kind. Used by the per-kind +// generated Lookup* wrappers. +func lookupAlgorithm(kind algorithmKind, name string) (KeyAlgorithm, bool) { + muAlgRegistry.RLock() + defer muAlgRegistry.RUnlock() + if entry, ok := algRegistry[name]; ok && entry.kind == kind { + return entry.alg, true + } + return nil, false +} + +// listAlgorithmsByKind returns every registered algorithm of the +// given kind, sorted by name. Used by the per-kind generated +// s() functions. +func listAlgorithmsByKind(kind algorithmKind) []KeyAlgorithm { + muAlgRegistry.RLock() + defer muAlgRegistry.RUnlock() + out := make([]KeyAlgorithm, 0, len(algRegistry)) + for _, entry := range algRegistry { + if entry.kind == kind { + out = append(out, entry.alg) + } + } + sort.Slice(out, func(i, j int) bool { return out[i].String() < out[j].String() }) + return out +} + // KeyAlgorithmFrom takes either a string, `jwa.SignatureAlgorithm`, -// `jwa.KeyEncryptionAlgorithm`, or `jwa.ContentEncryptionAlgorithm`. +// `jwa.KeyEncryptionAlgorithm`, or `jwa.ContentEncryptionAlgorithm`, // and returns a `jwa.KeyAlgorithm`. // -// If the value cannot be handled, it returns an `jwa.InvalidKeyAlgorithm` -// object instead of returning an error. This design choice was made to allow -// users to directly pass the return value to functions such as `jws.Sign()` +// String inputs resolve through the shared algorithm registry: the +// returned KeyAlgorithm holds the concrete typed value (Signature, +// KeyEncryption, or ContentEncryption) for whichever kind owns the +// name. Cross-kind name reuse is structurally avoided — the first +// Register* wins and subsequent cross-kind registrations are silent +// no-ops — so KeyAlgorithmFrom no longer needs precedence rules. +// +// Typed inputs whose String() is empty (for example a zero-value +// `var sa jwa.SignatureAlgorithm`) are rejected with +// ErrInvalidKeyAlgorithm. Without this check the typed arms accepted +// names that would never resolve through any registry, surfacing as +// confusing failures far from the call site. func KeyAlgorithmFrom(v any) (KeyAlgorithm, error) { switch v := v.(type) { case SignatureAlgorithm: + if v.String() == "" { + return nil, fmt.Errorf(`invalid key value: zero-value %T: %w`, v, errInvalidKeyAlgorithm) + } return v, nil case KeyEncryptionAlgorithm: + if v.String() == "" { + return nil, fmt.Errorf(`invalid key value: zero-value %T: %w`, v, errInvalidKeyAlgorithm) + } return v, nil case ContentEncryptionAlgorithm: + if v.String() == "" { + return nil, fmt.Errorf(`invalid key value: zero-value %T: %w`, v, errInvalidKeyAlgorithm) + } return v, nil case string: - salg, ok := LookupSignatureAlgorithm(v) - if ok { - return salg, nil + muAlgRegistry.RLock() + entry, ok := algRegistry[v] + muAlgRegistry.RUnlock() + if !ok { + return nil, fmt.Errorf(`invalid key value: %s: %w`, formatInvalidKeyAlgorithmValue(v), errInvalidKeyAlgorithm) } - - kalg, ok := LookupKeyEncryptionAlgorithm(v) - if ok { - return kalg, nil - } - - calg, ok := LookupContentEncryptionAlgorithm(v) - if ok { - return calg, nil - } - - return nil, fmt.Errorf(`invalid key value: %q: %w`, v, errInvalidKeyAlgorithm) + return entry.alg, nil default: return nil, fmt.Errorf(`invalid key type: %T: %w`, v, errInvalidKeyAlgorithm) } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_encryption_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_encryption_gen.go index 716c43cd04..bdb19b0dd3 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_encryption_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_encryption_gen.go @@ -5,18 +5,10 @@ package jwa import ( "encoding/json" "fmt" - "sort" - "sync" "github.com/lestrrat-go/jwx/v3/internal/tokens" ) -var muAllKeyEncryptionAlgorithm sync.RWMutex -var allKeyEncryptionAlgorithm = map[string]KeyEncryptionAlgorithm{} -var muListKeyEncryptionAlgorithm sync.RWMutex -var listKeyEncryptionAlgorithm []KeyEncryptionAlgorithm -var builtinKeyEncryptionAlgorithm = map[string]struct{}{} - func init() { // builtin values for KeyEncryptionAlgorithm algorithms := make([]KeyEncryptionAlgorithm, 19) @@ -41,6 +33,9 @@ func init() { algorithms[18] = NewKeyEncryptionAlgorithm(tokens.RSA_OAEP_512) RegisterKeyEncryptionAlgorithm(algorithms...) + for _, alg := range algorithms { + markBuiltin(alg.String()) + } } // A128GCMKW returns an object representing AES-GCM key wrap (128) key encryption algorithm. @@ -139,13 +134,11 @@ func RSA_OAEP_512() KeyEncryptionAlgorithm { } func lookupBuiltinKeyEncryptionAlgorithm(name string) KeyEncryptionAlgorithm { - muAllKeyEncryptionAlgorithm.RLock() - v, ok := allKeyEncryptionAlgorithm[name] - muAllKeyEncryptionAlgorithm.RUnlock() + v, ok := lookupAlgorithm(algKindKeyEncryption, name) if !ok { panic(fmt.Sprintf(`jwa: KeyEncryptionAlgorithm %q not registered`, name)) } - return v + return v.(KeyEncryptionAlgorithm) } // KeyEncryptionAlgorithm represents the various encryption algorithms as described in https://tools.ietf.org/html/rfc7518#section-4.1 @@ -195,57 +188,46 @@ func NewKeyEncryptionAlgorithm(name string, options ...NewKeyEncryptionAlgorithm // LookupKeyEncryptionAlgorithm returns the KeyEncryptionAlgorithm object for the given name. func LookupKeyEncryptionAlgorithm(name string) (KeyEncryptionAlgorithm, bool) { - muAllKeyEncryptionAlgorithm.RLock() - v, ok := allKeyEncryptionAlgorithm[name] - muAllKeyEncryptionAlgorithm.RUnlock() - return v, ok + if v, ok := lookupAlgorithm(algKindKeyEncryption, name); ok { + return v.(KeyEncryptionAlgorithm), true + } + var zero KeyEncryptionAlgorithm + return zero, false } // RegisterKeyEncryptionAlgorithm registers a new KeyEncryptionAlgorithm. The signature value must be immutable // and safe to be used by multiple goroutines, as it is going to be shared with all other users of this library. +// +// Registration is process-global. Built-in identifiers such as RS256 are +// reserved and cannot be replaced by callers after init has completed; use a +// distinct name for third-party algorithms. +// +// SignatureAlgorithm, KeyEncryptionAlgorithm, and ContentEncryptionAlgorithm +// share a single algorithm-name namespace so that KeyAlgorithmFrom can +// resolve unambiguously. Registering a name that is already registered as a +// different kind is a silent no-op (the first Register* call wins). func RegisterKeyEncryptionAlgorithm(algorithms ...KeyEncryptionAlgorithm) { - muAllKeyEncryptionAlgorithm.Lock() for _, alg := range algorithms { - allKeyEncryptionAlgorithm[alg.String()] = alg + registerAlgorithm(algKindKeyEncryption, alg) } - muAllKeyEncryptionAlgorithm.Unlock() - rebuildKeyEncryptionAlgorithm() } // UnregisterKeyEncryptionAlgorithm unregisters a KeyEncryptionAlgorithm from its known database. // Non-existent entries, as well as built-in algorithms will silently be ignored. func UnregisterKeyEncryptionAlgorithm(algorithms ...KeyEncryptionAlgorithm) { - muAllKeyEncryptionAlgorithm.Lock() for _, alg := range algorithms { - if _, ok := builtinKeyEncryptionAlgorithm[alg.String()]; ok { - continue - } - delete(allKeyEncryptionAlgorithm, alg.String()) + unregisterAlgorithm(algKindKeyEncryption, alg.String()) } - muAllKeyEncryptionAlgorithm.Unlock() - rebuildKeyEncryptionAlgorithm() -} - -func rebuildKeyEncryptionAlgorithm() { - list := make([]KeyEncryptionAlgorithm, 0, len(allKeyEncryptionAlgorithm)) - muAllKeyEncryptionAlgorithm.RLock() - for _, v := range allKeyEncryptionAlgorithm { - list = append(list, v) - } - muAllKeyEncryptionAlgorithm.RUnlock() - sort.Slice(list, func(i, j int) bool { - return list[i].String() < list[j].String() - }) - muListKeyEncryptionAlgorithm.Lock() - listKeyEncryptionAlgorithm = list - muListKeyEncryptionAlgorithm.Unlock() } // KeyEncryptionAlgorithms returns a list of all available values for KeyEncryptionAlgorithm. func KeyEncryptionAlgorithms() []KeyEncryptionAlgorithm { - muListKeyEncryptionAlgorithm.RLock() - defer muListKeyEncryptionAlgorithm.RUnlock() - return listKeyEncryptionAlgorithm + raw := listAlgorithmsByKind(algKindKeyEncryption) + out := make([]KeyEncryptionAlgorithm, len(raw)) + for i, alg := range raw { + out[i] = alg.(KeyEncryptionAlgorithm) + } + return out } // MarshalJSON serializes the KeyEncryptionAlgorithm object to a JSON string. diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_type_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_type_gen.go index 8bc5ebb5f0..db3580a277 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_type_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_type_gen.go @@ -24,6 +24,9 @@ func init() { algorithms[3] = NewKeyType("RSA") RegisterKeyType(algorithms...) + for _, alg := range algorithms { + builtinKeyType[alg.String()] = struct{}{} + } } // EC returns an object representing EC. Elliptic Curve @@ -107,36 +110,44 @@ func LookupKeyType(name string) (KeyType, bool) { // RegisterKeyType registers a new KeyType. The signature value must be immutable // and safe to be used by multiple goroutines, as it is going to be shared with all other users of this library. +// +// Registration is process-global. Built-in identifiers such as RS256 are +// reserved and cannot be replaced by callers after init has completed; use a +// distinct name for third-party algorithms. func RegisterKeyType(algorithms ...KeyType) { muAllKeyType.Lock() + defer muAllKeyType.Unlock() for _, alg := range algorithms { + if _, ok := builtinKeyType[alg.String()]; ok { + if existing, ok := allKeyType[alg.String()]; ok && existing != alg { + continue + } + continue + } allKeyType[alg.String()] = alg } - muAllKeyType.Unlock() - rebuildKeyType() + rebuildKeyTypeLocked() } // UnregisterKeyType unregisters a KeyType from its known database. // Non-existent entries, as well as built-in algorithms will silently be ignored. func UnregisterKeyType(algorithms ...KeyType) { muAllKeyType.Lock() + defer muAllKeyType.Unlock() for _, alg := range algorithms { if _, ok := builtinKeyType[alg.String()]; ok { continue } delete(allKeyType, alg.String()) } - muAllKeyType.Unlock() - rebuildKeyType() + rebuildKeyTypeLocked() } -func rebuildKeyType() { +func rebuildKeyTypeLocked() { list := make([]KeyType, 0, len(allKeyType)) - muAllKeyType.RLock() for _, v := range allKeyType { list = append(list, v) } - muAllKeyType.RUnlock() sort.Slice(list, func(i, j int) bool { return list[i].String() < list[j].String() }) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/signature_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/signature_gen.go index 653d7d56af..c682b8dd65 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/signature_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/signature_gen.go @@ -5,36 +5,32 @@ package jwa import ( "encoding/json" "fmt" - "sort" - "sync" ) -var muAllSignatureAlgorithm sync.RWMutex -var allSignatureAlgorithm = map[string]SignatureAlgorithm{} -var muListSignatureAlgorithm sync.RWMutex -var listSignatureAlgorithm []SignatureAlgorithm -var builtinSignatureAlgorithm = map[string]struct{}{} - func init() { // builtin values for SignatureAlgorithm - algorithms := make([]SignatureAlgorithm, 15) + algorithms := make([]SignatureAlgorithm, 16) algorithms[0] = NewSignatureAlgorithm("ES256") algorithms[1] = NewSignatureAlgorithm("ES256K") algorithms[2] = NewSignatureAlgorithm("ES384") algorithms[3] = NewSignatureAlgorithm("ES512") - algorithms[4] = NewSignatureAlgorithm("EdDSA") - algorithms[5] = NewSignatureAlgorithm("HS256", WithIsSymmetric(true)) - algorithms[6] = NewSignatureAlgorithm("HS384", WithIsSymmetric(true)) - algorithms[7] = NewSignatureAlgorithm("HS512", WithIsSymmetric(true)) - algorithms[8] = NewSignatureAlgorithm("none") - algorithms[9] = NewSignatureAlgorithm("PS256") - algorithms[10] = NewSignatureAlgorithm("PS384") - algorithms[11] = NewSignatureAlgorithm("PS512") - algorithms[12] = NewSignatureAlgorithm("RS256") - algorithms[13] = NewSignatureAlgorithm("RS384") - algorithms[14] = NewSignatureAlgorithm("RS512") + algorithms[4] = NewSignatureAlgorithm("EdDSA", WithDeprecated(true)) + algorithms[5] = NewSignatureAlgorithm("Ed25519") + algorithms[6] = NewSignatureAlgorithm("HS256", WithIsSymmetric(true)) + algorithms[7] = NewSignatureAlgorithm("HS384", WithIsSymmetric(true)) + algorithms[8] = NewSignatureAlgorithm("HS512", WithIsSymmetric(true)) + algorithms[9] = NewSignatureAlgorithm("none") + algorithms[10] = NewSignatureAlgorithm("PS256") + algorithms[11] = NewSignatureAlgorithm("PS384") + algorithms[12] = NewSignatureAlgorithm("PS512") + algorithms[13] = NewSignatureAlgorithm("RS256") + algorithms[14] = NewSignatureAlgorithm("RS384") + algorithms[15] = NewSignatureAlgorithm("RS512") RegisterSignatureAlgorithm(algorithms...) + for _, alg := range algorithms { + markBuiltin(alg.String()) + } } // ES256 returns an object representing ECDSA signature algorithm using P-256 curve and SHA-256. @@ -57,11 +53,16 @@ func ES512() SignatureAlgorithm { return lookupBuiltinSignatureAlgorithm("ES512") } -// EdDSA returns an object representing EdDSA signature algorithms. +// EdDSA returns an object representing EdDSA signature algorithms (deprecated by RFC 9864, use EdDSAEd25519 or EdDSAEd448). func EdDSA() SignatureAlgorithm { return lookupBuiltinSignatureAlgorithm("EdDSA") } +// EdDSAEd25519 returns an object representing EdDSA signature algorithm using Ed25519 (RFC 9864). The function name is tentative and may change in future releases. +func EdDSAEd25519() SignatureAlgorithm { + return lookupBuiltinSignatureAlgorithm("Ed25519") +} + // HS256 returns an object representing HMAC signature algorithm using SHA-256. func HS256() SignatureAlgorithm { return lookupBuiltinSignatureAlgorithm("HS256") @@ -113,13 +114,11 @@ func RS512() SignatureAlgorithm { } func lookupBuiltinSignatureAlgorithm(name string) SignatureAlgorithm { - muAllSignatureAlgorithm.RLock() - v, ok := allSignatureAlgorithm[name] - muAllSignatureAlgorithm.RUnlock() + v, ok := lookupAlgorithm(algKindSignature, name) if !ok { panic(fmt.Sprintf(`jwa: SignatureAlgorithm %q not registered`, name)) } - return v + return v.(SignatureAlgorithm) } // SignatureAlgorithm represents the various signature algorithms as described in https://tools.ietf.org/html/rfc7518#section-3.1 @@ -169,57 +168,46 @@ func NewSignatureAlgorithm(name string, options ...NewSignatureAlgorithmOption) // LookupSignatureAlgorithm returns the SignatureAlgorithm object for the given name. func LookupSignatureAlgorithm(name string) (SignatureAlgorithm, bool) { - muAllSignatureAlgorithm.RLock() - v, ok := allSignatureAlgorithm[name] - muAllSignatureAlgorithm.RUnlock() - return v, ok + if v, ok := lookupAlgorithm(algKindSignature, name); ok { + return v.(SignatureAlgorithm), true + } + var zero SignatureAlgorithm + return zero, false } // RegisterSignatureAlgorithm registers a new SignatureAlgorithm. The signature value must be immutable // and safe to be used by multiple goroutines, as it is going to be shared with all other users of this library. +// +// Registration is process-global. Built-in identifiers such as RS256 are +// reserved and cannot be replaced by callers after init has completed; use a +// distinct name for third-party algorithms. +// +// SignatureAlgorithm, KeyEncryptionAlgorithm, and ContentEncryptionAlgorithm +// share a single algorithm-name namespace so that KeyAlgorithmFrom can +// resolve unambiguously. Registering a name that is already registered as a +// different kind is a silent no-op (the first Register* call wins). func RegisterSignatureAlgorithm(algorithms ...SignatureAlgorithm) { - muAllSignatureAlgorithm.Lock() for _, alg := range algorithms { - allSignatureAlgorithm[alg.String()] = alg + registerAlgorithm(algKindSignature, alg) } - muAllSignatureAlgorithm.Unlock() - rebuildSignatureAlgorithm() } // UnregisterSignatureAlgorithm unregisters a SignatureAlgorithm from its known database. // Non-existent entries, as well as built-in algorithms will silently be ignored. func UnregisterSignatureAlgorithm(algorithms ...SignatureAlgorithm) { - muAllSignatureAlgorithm.Lock() for _, alg := range algorithms { - if _, ok := builtinSignatureAlgorithm[alg.String()]; ok { - continue - } - delete(allSignatureAlgorithm, alg.String()) + unregisterAlgorithm(algKindSignature, alg.String()) } - muAllSignatureAlgorithm.Unlock() - rebuildSignatureAlgorithm() -} - -func rebuildSignatureAlgorithm() { - list := make([]SignatureAlgorithm, 0, len(allSignatureAlgorithm)) - muAllSignatureAlgorithm.RLock() - for _, v := range allSignatureAlgorithm { - list = append(list, v) - } - muAllSignatureAlgorithm.RUnlock() - sort.Slice(list, func(i, j int) bool { - return list[i].String() < list[j].String() - }) - muListSignatureAlgorithm.Lock() - listSignatureAlgorithm = list - muListSignatureAlgorithm.Unlock() } // SignatureAlgorithms returns a list of all available values for SignatureAlgorithm. func SignatureAlgorithms() []SignatureAlgorithm { - muListSignatureAlgorithm.RLock() - defer muListSignatureAlgorithm.RUnlock() - return listSignatureAlgorithm + raw := listAlgorithmsByKind(algKindSignature) + out := make([]SignatureAlgorithm, len(raw)) + for i, alg := range raw { + out[i] = alg.(SignatureAlgorithm) + } + return out } // MarshalJSON serializes the SignatureAlgorithm object to a JSON string. diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/README.md b/vendor/github.com/lestrrat-go/jwx/v3/jwe/README.md index c85d05bbbe..982e7454b2 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/README.md +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/README.md @@ -12,38 +12,38 @@ Examples are located in the examples directory ([jwe_example_test.go](../example Supported key encryption algorithm: -| Algorithm | Supported? | Constant in [jwa](../jwa) | -|:-----------------------------------------|:-----------|:-------------------------| -| RSA-PKCS1v1.5 | YES | jwa.RSA1_5 | -| RSA-OAEP-SHA1 | YES | jwa.RSA_OAEP | -| RSA-OAEP-SHA256 | YES | jwa.RSA_OAEP_256 | -| AES key wrap (128) | YES | jwa.A128KW | -| AES key wrap (192) | YES | jwa.A192KW | -| AES key wrap (256) | YES | jwa.A256KW | -| Direct encryption | YES (1) | jwa.DIRECT | -| ECDH-ES | YES (1) | jwa.ECDH_ES | -| ECDH-ES + AES key wrap (128) | YES | jwa.ECDH_ES_A128KW | -| ECDH-ES + AES key wrap (192) | YES | jwa.ECDH_ES_A192KW | -| ECDH-ES + AES key wrap (256) | YES | jwa.ECDH_ES_A256KW | -| AES-GCM key wrap (128) | YES | jwa.A128GCMKW | -| AES-GCM key wrap (192) | YES | jwa.A192GCMKW | -| AES-GCM key wrap (256) | YES | jwa.A256GCMKW | -| PBES2 + HMAC-SHA256 + AES key wrap (128) | YES | jwa.PBES2_HS256_A128KW | -| PBES2 + HMAC-SHA384 + AES key wrap (192) | YES | jwa.PBES2_HS384_A192KW | -| PBES2 + HMAC-SHA512 + AES key wrap (256) | YES | jwa.PBES2_HS512_A256KW | +| Algorithm | Supported? | Constant in [jwa](../jwa) | Note | +|:-----------------------------------------|:-----------|:-------------------------|:-----| +| RSA-PKCS1v1.5 | YES | jwa.RSA1_5 | Legacy interop only; prefer RSA-OAEP for new code | +| RSA-OAEP-SHA1 | YES | jwa.RSA_OAEP | | +| RSA-OAEP-SHA256 | YES | jwa.RSA_OAEP_256 | | +| AES key wrap (128) | YES | jwa.A128KW | | +| AES key wrap (192) | YES | jwa.A192KW | | +| AES key wrap (256) | YES | jwa.A256KW | | +| Direct encryption | YES (1) | jwa.DIRECT | | +| ECDH-ES | YES (1) | jwa.ECDH_ES | | +| ECDH-ES + AES key wrap (128) | YES | jwa.ECDH_ES_A128KW | | +| ECDH-ES + AES key wrap (192) | YES | jwa.ECDH_ES_A192KW | | +| ECDH-ES + AES key wrap (256) | YES | jwa.ECDH_ES_A256KW | | +| AES-GCM key wrap (128) | YES | jwa.A128GCMKW | | +| AES-GCM key wrap (192) | YES | jwa.A192GCMKW | | +| AES-GCM key wrap (256) | YES | jwa.A256GCMKW | | +| PBES2 + HMAC-SHA256 + AES key wrap (128) | YES | jwa.PBES2_HS256_A128KW | | +| PBES2 + HMAC-SHA384 + AES key wrap (192) | YES | jwa.PBES2_HS384_A192KW | | +| PBES2 + HMAC-SHA512 + AES key wrap (256) | YES | jwa.PBES2_HS512_A256KW | | * Note 1: Single-recipient only Supported content encryption algorithm: -| Algorithm | Supported? | Constant in [jwa](../jwa) | -|:----------------------------|:-----------|:--------------------------| -| AES-CBC + HMAC-SHA256 (128) | YES | jwa.A128CBC_HS256 | -| AES-CBC + HMAC-SHA384 (192) | YES | jwa.A192CBC_HS384 | -| AES-CBC + HMAC-SHA512 (256) | YES | jwa.A256CBC_HS512 | -| AES-GCM (128) | YES | jwa.A128GCM | -| AES-GCM (192) | YES | jwa.A192GCM | -| AES-GCM (256) | YES | jwa.A256GCM | +| Algorithm | Supported? | Constant in [jwa](../jwa) | Required CEK length | +|:----------------------------|:-----------|:--------------------------|:--------------------| +| AES-CBC + HMAC-SHA256 (128) | YES | jwa.A128CBC_HS256 | 32 bytes | +| AES-CBC + HMAC-SHA384 (192) | YES | jwa.A192CBC_HS384 | 48 bytes | +| AES-CBC + HMAC-SHA512 (256) | YES | jwa.A256CBC_HS512 | 64 bytes | +| AES-GCM (128) | YES | jwa.A128GCM | 16 bytes | +| AES-GCM (192) | YES | jwa.A192GCM | 24 bytes | +| AES-GCM (256) | YES | jwa.A256GCM | 32 bytes | # SYNOPSIS @@ -59,7 +59,7 @@ func ExampleEncrypt() { payload := []byte("Lorem Ipsum") - encrypted, err := jwe.Encrypt(payload, jwe.WithKey(jwa.RSA1_5, &privkey.PublicKey), jwe.WithContentEncryption(jwa.A128CBC_HS256)) + encrypted, err := jwe.Encrypt(payload, jwe.WithKey(jwa.RSA_OAEP, &privkey.PublicKey), jwe.WithContentEncryption(jwa.A128CBC_HS256)) if err != nil { log.Printf("failed to encrypt payload: %s", err) return @@ -79,7 +79,7 @@ func ExampleDecrypt() { return } - decrypted, err := jwe.Decrypt(encrypted, jwe.WithKey(jwa.RSA1_5, privkey)) + decrypted, err := jwe.Decrypt(encrypted, jwe.WithKey(jwa.RSA_OAEP, privkey)) if err != nil { log.Printf("failed to decrypt: %s", err) return diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/compress.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/compress.go index a1ed158fb0..2476046a91 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/compress.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/compress.go @@ -19,7 +19,7 @@ func uncompress(src []byte, maxBufferSize int64) ([]byte, error) { n, readErr := r.Read(buf[:]) sofar += int64(n) if sofar > maxBufferSize { - return nil, fmt.Errorf(`compressed payload exceeds maximum allowed size`) + return nil, fmt.Errorf(`decompressed payload exceeds WithMaxDecompressBufferSize=%d (saw %d bytes after a %d-byte read)`, maxBufferSize, sofar, n) } if readErr != nil { // if we have a read error, and it's not EOF, then we need to stop @@ -56,7 +56,6 @@ func compress(plaintext []byte) ([]byte, error) { return nil, fmt.Errorf(`failed to close compression writer: %w`, err) } - ret := make([]byte, buf.Len()) - copy(ret, buf.Bytes()) + ret := bytes.Clone(buf.Bytes()) return ret, nil } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/decrypt.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/decrypt.go index 9429d84b1b..bc6e6f705c 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/decrypt.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/decrypt.go @@ -3,7 +3,6 @@ package jwe import ( "fmt" - "github.com/lestrrat-go/jwx/v3/internal/tokens" "github.com/lestrrat-go/jwx/v3/jwa" "github.com/lestrrat-go/jwx/v3/jwe/internal/content_crypt" "github.com/lestrrat-go/jwx/v3/jwe/jwebb" @@ -140,10 +139,11 @@ func (d *decrypter) Decrypt(recipient Recipient, ciphertext []byte, msg *Message return } - computedAad := d.computedAad - if d.aad != nil { - computedAad = append(append(computedAad, tokens.Period), d.aad...) - } + // When an external aad is present we must NOT append into + // d.computedAad's backing array: it aliases msg.rawProtectedHeaders + // in the caller, and appending would mutate bytes past its length + // in storage still referenced by the Message. + computedAad := concatAAD(d.computedAad, d.aad) plaintext, err = cipher.Decrypt(cek, d.iv, ciphertext, d.tag, computedAad) if err != nil { @@ -158,40 +158,43 @@ func (d *decrypter) Decrypt(recipient Recipient, ciphertext []byte, msg *Message } func (d *decrypter) DecryptKey(recipient Recipient, msg *Message) (cek []byte, err error) { + keyalgStr := d.keyalg.String() + ctalgStr := d.ctalg.String() + recipientKey := recipient.EncryptedKey() if kd, ok := d.privkey.(KeyDecrypter); ok { return kd.DecryptKey(d.keyalg, recipientKey, recipient, msg) } - if jwebb.IsDirect(d.keyalg.String()) { + if jwebb.IsDirect(keyalgStr) { cek, ok := d.privkey.([]byte) if !ok { - return nil, fmt.Errorf("decrypt key: []byte is required as the key for %s (got %T)", d.keyalg, d.privkey) + return nil, fmt.Errorf("decrypt key: []byte is required as the key for %s (got %T)", keyalgStr, d.privkey) } - return jwebb.KeyDecryptDirect(recipientKey, recipientKey, d.keyalg.String(), cek) + return jwebb.KeyDecryptDirect(recipientKey, recipientKey, keyalgStr, cek) } - if jwebb.IsPBES2(d.keyalg.String()) { + if jwebb.IsPBES2(keyalgStr) { password, ok := d.privkey.([]byte) if !ok { - return nil, fmt.Errorf("decrypt key: []byte is required as the password for %s (got %T)", d.keyalg, d.privkey) + return nil, fmt.Errorf("decrypt key: []byte is required as the password for %s (got %T)", keyalgStr, d.privkey) } - salt := []byte(d.keyalg.String()) + salt := []byte(keyalgStr) salt = append(salt, byte(0)) salt = append(salt, d.keysalt...) - return jwebb.KeyDecryptPBES2(recipientKey, recipientKey, d.keyalg.String(), password, salt, d.keycount) + return jwebb.KeyDecryptPBES2(recipientKey, recipientKey, keyalgStr, password, salt, d.keycount) } - if jwebb.IsAESGCMKW(d.keyalg.String()) { + if jwebb.IsAESGCMKW(keyalgStr) { sharedkey, ok := d.privkey.([]byte) if !ok { - return nil, fmt.Errorf("decrypt key: []byte is required as the key for %s (got %T)", d.keyalg, d.privkey) + return nil, fmt.Errorf("decrypt key: []byte is required as the key for %s (got %T)", keyalgStr, d.privkey) } - return jwebb.KeyDecryptAESGCMKW(recipientKey, recipientKey, d.keyalg.String(), sharedkey, d.keyiv, d.keytag) + return jwebb.KeyDecryptAESGCMKW(recipientKey, recipientKey, keyalgStr, sharedkey, d.keyiv, d.keytag) } - if jwebb.IsECDHES(d.keyalg.String()) { - alg, keysize, keywrap, err := jwebb.KeyEncryptionECDHESKeySize(d.keyalg.String(), d.ctalg.String()) + if jwebb.IsECDHES(keyalgStr) { + alg, keysize, keywrap, err := jwebb.KeyEncryptionECDHESKeySize(keyalgStr, ctalgStr) if err != nil { return nil, fmt.Errorf(`failed to determine ECDH-ES key size: %w`, err) } @@ -199,10 +202,10 @@ func (d *decrypter) DecryptKey(recipient Recipient, msg *Message) (cek []byte, e if !keywrap { return jwebb.KeyDecryptECDHES(recipientKey, cek, alg, d.apu, d.apv, d.privkey, d.pubkey, keysize) } - return jwebb.KeyDecryptECDHESKeyWrap(recipientKey, recipientKey, d.keyalg.String(), d.apu, d.apv, d.privkey, d.pubkey, keysize) + return jwebb.KeyDecryptECDHESKeyWrap(recipientKey, recipientKey, keyalgStr, d.apu, d.apv, d.privkey, d.pubkey, keysize) } - if jwebb.IsRSA15(d.keyalg.String()) { + if jwebb.IsRSA15(keyalgStr) { cipher, err := d.ContentCipher() if err != nil { return nil, fmt.Errorf(`failed to fetch content crypt cipher: %w`, err) @@ -211,17 +214,17 @@ func (d *decrypter) DecryptKey(recipient Recipient, msg *Message) (cek []byte, e return jwebb.KeyDecryptRSA15(recipientKey, recipientKey, d.privkey, keysize) } - if jwebb.IsRSAOAEP(d.keyalg.String()) { - return jwebb.KeyDecryptRSAOAEP(recipientKey, recipientKey, d.keyalg.String(), d.privkey) + if jwebb.IsRSAOAEP(keyalgStr) { + return jwebb.KeyDecryptRSAOAEP(recipientKey, recipientKey, keyalgStr, d.privkey) } - if jwebb.IsAESKW(d.keyalg.String()) { + if jwebb.IsAESKW(keyalgStr) { sharedkey, ok := d.privkey.([]byte) if !ok { - return nil, fmt.Errorf("[]byte is required as the key to decrypt %s", d.keyalg.String()) + return nil, fmt.Errorf("[]byte is required as the key to decrypt %s", keyalgStr) } - return jwebb.KeyDecryptAESKW(recipientKey, recipientKey, d.keyalg.String(), sharedkey) + return jwebb.KeyDecryptAESKW(recipientKey, recipientKey, keyalgStr, sharedkey) } - return nil, fmt.Errorf(`unsupported algorithm for key decryption (%s)`, d.keyalg) + return nil, fmt.Errorf(`unsupported algorithm for key decryption (%s)`, keyalgStr) } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/encrypt.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/encrypt.go index e75f342a3d..16de2aa898 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/encrypt.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/encrypt.go @@ -8,9 +8,9 @@ import ( "github.com/lestrrat-go/jwx/v3/internal/keyconv" "github.com/lestrrat-go/jwx/v3/jwa" - "github.com/lestrrat-go/jwx/v3/jwe/internal/content_crypt" "github.com/lestrrat-go/jwx/v3/jwe/internal/keygen" "github.com/lestrrat-go/jwx/v3/jwe/jwebb" + "github.com/lestrrat-go/jwx/v3/jwk" ) // encrypter is responsible for taking various components to encrypt a key. @@ -18,13 +18,13 @@ import ( // //nolint:govet type encrypter struct { - apu []byte - apv []byte - ctalg jwa.ContentEncryptionAlgorithm - keyalg jwa.KeyEncryptionAlgorithm - pubkey any - rawKey any - cipher content_crypt.Cipher + apu []byte + apv []byte + ctalg jwa.ContentEncryptionAlgorithm + keyalg jwa.KeyEncryptionAlgorithm + pubkey any + rawKey any + pbes2Count int } // newEncrypter creates a new Encrypter instance with all required parameters. @@ -34,24 +34,22 @@ type encrypter struct { // *rsa.PublicKey, instead of jwk.Key) // // You should consider this object immutable once created. -func newEncrypter(keyalg jwa.KeyEncryptionAlgorithm, ctalg jwa.ContentEncryptionAlgorithm, pubkey any, rawKey any, apu, apv []byte) (*encrypter, error) { - cipher, err := jwebb.CreateContentCipher(ctalg.String()) - if err != nil { - return nil, fmt.Errorf(`failed to create content cipher: %w`, err) - } - +func newEncrypter(keyalg jwa.KeyEncryptionAlgorithm, ctalg jwa.ContentEncryptionAlgorithm, pubkey any, rawKey any, apu, apv []byte, pbes2Count int) *encrypter { return &encrypter{ - apu: apu, - apv: apv, - ctalg: ctalg, - keyalg: keyalg, - pubkey: pubkey, - rawKey: rawKey, - cipher: cipher, - }, nil + apu: apu, + apv: apv, + ctalg: ctalg, + keyalg: keyalg, + pubkey: pubkey, + rawKey: rawKey, + pbes2Count: pbes2Count, + } } func (e *encrypter) EncryptKey(cek []byte) (keygen.ByteSource, error) { + keyalgStr := e.keyalg.String() + ctalgStr := e.ctalg.String() + if ke, ok := e.pubkey.(KeyEncrypter); ok { encrypted, err := ke.EncryptKey(cek) if err != nil { @@ -60,32 +58,32 @@ func (e *encrypter) EncryptKey(cek []byte) (keygen.ByteSource, error) { return keygen.ByteKey(encrypted), nil } - if jwebb.IsDirect(e.keyalg.String()) { + if jwebb.IsDirect(keyalgStr) { sharedkey, ok := e.rawKey.([]byte) if !ok { - return nil, fmt.Errorf("encrypt key: []byte is required as the key for %s (got %T)", e.keyalg, e.rawKey) + return nil, fmt.Errorf("encrypt key: []byte is required as the key for %s (got %T)", keyalgStr, e.rawKey) } - return jwebb.KeyEncryptDirect(cek, e.keyalg.String(), sharedkey) + return jwebb.KeyEncryptDirect(cek, keyalgStr, sharedkey) } - if jwebb.IsPBES2(e.keyalg.String()) { + if jwebb.IsPBES2(keyalgStr) { password, ok := e.rawKey.([]byte) if !ok { - return nil, fmt.Errorf("encrypt key: []byte is required as the password for %s (got %T)", e.keyalg, e.rawKey) + return nil, fmt.Errorf("encrypt key: []byte is required as the password for %s (got %T)", keyalgStr, e.rawKey) } - return jwebb.KeyEncryptPBES2(cek, e.keyalg.String(), password) + return jwebb.KeyEncryptPBES2(cek, keyalgStr, password, e.pbes2Count) } - if jwebb.IsAESGCMKW(e.keyalg.String()) { + if jwebb.IsAESGCMKW(keyalgStr) { sharedkey, ok := e.rawKey.([]byte) if !ok { - return nil, fmt.Errorf("encrypt key: []byte is required as the key for %s (got %T)", e.keyalg, e.rawKey) + return nil, fmt.Errorf("encrypt key: []byte is required as the key for %s (got %T)", keyalgStr, e.rawKey) } - return jwebb.KeyEncryptAESGCMKW(cek, e.keyalg.String(), sharedkey) + return jwebb.KeyEncryptAESGCMKW(cek, keyalgStr, sharedkey) } - if jwebb.IsECDHES(e.keyalg.String()) { - _, keysize, keywrap, err := jwebb.KeyEncryptionECDHESKeySize(e.keyalg.String(), e.ctalg.String()) + if jwebb.IsECDHES(keyalgStr) { + _, keysize, keywrap, err := jwebb.KeyEncryptionECDHESKeySize(keyalgStr, ctalgStr) if err != nil { return nil, fmt.Errorf(`failed to determine ECDH-ES key size: %w`, err) } @@ -120,9 +118,9 @@ func (e *encrypter) EncryptKey(cek []byte) (keygen.ByteSource, error) { case *ecdh.PublicKey: if key.Curve() == ecdh.X25519() { if !keywrap { - return jwebb.KeyEncryptECDHESX25519(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String()) + return jwebb.KeyEncryptECDHESX25519(cek, keyalgStr, e.apu, e.apv, key, keysize, ctalgStr) } - return jwebb.KeyEncryptECDHESKeyWrapX25519(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String()) + return jwebb.KeyEncryptECDHESKeyWrapX25519(cek, keyalgStr, e.apu, e.apv, key, keysize, ctalgStr) } var ecdsaKey *ecdsa.PublicKey @@ -135,15 +133,15 @@ func (e *encrypter) EncryptKey(cek []byte) (keygen.ByteSource, error) { switch key := keyToUse.(type) { case *ecdsa.PublicKey: if !keywrap { - return jwebb.KeyEncryptECDHESECDSA(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String()) + return jwebb.KeyEncryptECDHESECDSA(cek, keyalgStr, e.apu, e.apv, key, keysize, ctalgStr) } - return jwebb.KeyEncryptECDHESKeyWrapECDSA(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String()) + return jwebb.KeyEncryptECDHESKeyWrapECDSA(cek, keyalgStr, e.apu, e.apv, key, keysize, ctalgStr) default: return nil, fmt.Errorf(`encrypt: unsupported key type for ECDH-ES: %T`, keyToUse) } } - if jwebb.IsRSA15(e.keyalg.String()) { + if jwebb.IsRSA15(keyalgStr) { keyToUse := e.rawKey if keyToUse == nil { keyToUse = e.pubkey @@ -159,10 +157,10 @@ func (e *encrypter) EncryptKey(cek []byte) (keygen.ByteSource, error) { return nil, fmt.Errorf(`encrypt: failed to convert to RSA public key: %w`, err) } - return jwebb.KeyEncryptRSA15(cek, e.keyalg.String(), pubkey) + return jwebb.KeyEncryptRSA15(cek, keyalgStr, pubkey) } - if jwebb.IsRSAOAEP(e.keyalg.String()) { + if jwebb.IsRSAOAEP(keyalgStr) { keyToUse := e.rawKey if keyToUse == nil { keyToUse = e.pubkey @@ -178,16 +176,76 @@ func (e *encrypter) EncryptKey(cek []byte) (keygen.ByteSource, error) { return nil, fmt.Errorf(`encrypt: failed to convert to RSA public key: %w`, err) } - return jwebb.KeyEncryptRSAOAEP(cek, e.keyalg.String(), pubkey) + return jwebb.KeyEncryptRSAOAEP(cek, keyalgStr, pubkey) } - if jwebb.IsAESKW(e.keyalg.String()) { + if jwebb.IsAESKW(keyalgStr) { sharedkey, ok := e.rawKey.([]byte) if !ok { - return nil, fmt.Errorf("[]byte is required as the key to encrypt %s", e.keyalg.String()) + return nil, fmt.Errorf("[]byte is required as the key to encrypt %s", keyalgStr) } - return jwebb.KeyEncryptAESKW(cek, e.keyalg.String(), sharedkey) + return jwebb.KeyEncryptAESKW(cek, keyalgStr, sharedkey) } - return nil, fmt.Errorf(`unsupported algorithm for key encryption (%s)`, e.keyalg) + return nil, fmt.Errorf(`unsupported algorithm for key encryption (%s)`, keyalgStr) +} + +// validateAlgorithmForKey checks that alg is family-compatible with +// key at the WithKey option boundary, surfacing wrong-shape mismatches +// as crisp `jwe.WithKey: ...` errors instead of nested errors deep in +// the dispatcher (e.g. `[]byte is required as the key to encrypt ...` +// from inside the AESKW path). +// +// Permissive carve-outs (return nil, deferring validation): +// +// - jwk.Key wrappers: kty-vs-alg check happens at jwk.Export time. +// - Caller-supplied KeyEncrypter / KeyDecrypter implementations: +// the caller takes responsibility for the key-shape contract. +// - Nil key: legitimate for `dir` (caller provides CEK separately). +// +// All other built-in algorithm families enforce a concrete key-shape +// expectation here. The error is wrapped by the WithKey site so the +// caller sees `jwe.WithKey: ...` consistently. +func validateAlgorithmForKey(alg jwa.KeyEncryptionAlgorithm, key any) error { + if key == nil { + return nil + } + if _, ok := key.(jwk.Key); ok { + return nil + } + if _, ok := key.(KeyEncrypter); ok { + return nil + } + if _, ok := key.(KeyDecrypter); ok { + return nil + } + + algStr := alg.String() + switch { + case jwebb.IsDirect(algStr): + if _, ok := key.([]byte); !ok { + return fmt.Errorf(`algorithm %q requires a []byte key (got %T)`, algStr, key) + } + case jwebb.IsAESKW(algStr) || jwebb.IsAESGCMKW(algStr) || jwebb.IsPBES2(algStr): + if _, ok := key.([]byte); !ok { + return fmt.Errorf(`algorithm %q requires a []byte key (got %T)`, algStr, key) + } + case jwebb.IsRSA15(algStr) || jwebb.IsRSAOAEP(algStr): + switch key.(type) { + case *rsa.PublicKey, rsa.PublicKey, *rsa.PrivateKey, rsa.PrivateKey: + default: + return fmt.Errorf(`algorithm %q requires an RSA key (got %T)`, algStr, key) + } + case jwebb.IsECDHES(algStr): + switch key.(type) { + case *ecdsa.PublicKey, ecdsa.PublicKey, *ecdsa.PrivateKey, ecdsa.PrivateKey, + *ecdh.PublicKey, ecdh.PublicKey, *ecdh.PrivateKey, ecdh.PrivateKey: + default: + return fmt.Errorf(`algorithm %q requires an ECDSA or ECDH key (got %T)`, algStr, key) + } + default: + // Unknown algorithm family: defer to dispatch. + return nil + } + return nil } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/errors.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/errors.go index 89d276fc44..8f65daa4b1 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/errors.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/errors.go @@ -1,6 +1,9 @@ package jwe -import "errors" +import ( + "errors" + "fmt" +) type encryptError struct { error @@ -22,6 +25,10 @@ func EncryptError() error { return errDefaultEncryptError } +func makeEncryptError(prefix string, f string, args ...any) error { + return encryptError{fmt.Errorf(prefix+": "+f, args...)} +} + type decryptError struct { error } @@ -42,6 +49,10 @@ func DecryptError() error { return errDefaultDecryptError } +func makeDecryptError(f string, args ...any) error { + return decryptError{fmt.Errorf("jwe.Decrypt: "+f, args...)} +} + type recipientError struct { error } @@ -68,6 +79,10 @@ func RecipientError() error { return errDefaultRecipientError } +func makeRecipientError(err error) error { + return recipientError{err} +} + type parseError struct { error } @@ -88,3 +103,7 @@ var errDefaultParseError = parseError{errors.New(`parse error`)} func ParseError() error { return errDefaultParseError } + +func makeParseError(prefix string, f string, args ...any) error { + return parseError{fmt.Errorf(prefix+": "+f, args...)} +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/headers_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/headers_gen.go index 7773a5d812..5390c2be45 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/headers_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/headers_gen.go @@ -108,12 +108,11 @@ type stdHeaders struct { x509CertThumbprintS256 *string x509URL *string privateParams map[string]any - mu *sync.RWMutex + mu sync.RWMutex } func NewHeaders() Headers { return &stdHeaders{ - mu: &sync.RWMutex{}, privateParams: map[string]any{}, } } @@ -430,13 +429,23 @@ func (h *stdHeaders) setNoLock(name string, value any) error { switch name { case AgreementPartyUInfoKey: if v, ok := value.([]byte); ok { - h.agreementPartyUInfo = v + if v == nil { + h.agreementPartyUInfo = nil + } else { + h.agreementPartyUInfo = make([]byte, len(v)) + copy(h.agreementPartyUInfo, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, AgreementPartyUInfoKey, value) case AgreementPartyVInfoKey: if v, ok := value.([]byte); ok { - h.agreementPartyVInfo = v + if v == nil { + h.agreementPartyVInfo = nil + } else { + h.agreementPartyVInfo = make([]byte, len(v)) + copy(h.agreementPartyVInfo, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, AgreementPartyVInfoKey, value) @@ -469,7 +478,12 @@ func (h *stdHeaders) setNoLock(name string, value any) error { return fmt.Errorf(`invalid value for %s key: %T`, ContentTypeKey, value) case CriticalKey: if v, ok := value.([]string); ok { - h.critical = v + if v == nil { + h.critical = nil + } else { + h.critical = make([]string, len(v)) + copy(h.critical, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, CriticalKey, value) @@ -579,6 +593,8 @@ func (h *stdHeaders) Remove(key string) error { } func (h *stdHeaders) UnmarshalJSON(buf []byte) error { + h.mu.Lock() + defer h.mu.Unlock() h.agreementPartyUInfo = nil h.agreementPartyVInfo = nil h.algorithm = nil @@ -640,7 +656,7 @@ LOOP: } h.contentEncryption = &decoded case ContentTypeKey: - if err := json.AssignNextStringToken(&h.contentType, dec); err != nil { + if err := json.AssignNextStringToken(&h.contentType, dec, nil); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, ContentTypeKey, err) } case CriticalKey: @@ -670,15 +686,15 @@ LOOP: } h.jwk = key case JWKSetURLKey: - if err := json.AssignNextStringToken(&h.jwkSetURL, dec); err != nil { + if err := json.AssignNextStringToken(&h.jwkSetURL, dec, nil); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, JWKSetURLKey, err) } case KeyIDKey: - if err := json.AssignNextStringToken(&h.keyID, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyID, dec, nil); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err) } case TypeKey: - if err := json.AssignNextStringToken(&h.typ, dec); err != nil { + if err := json.AssignNextStringToken(&h.typ, dec, nil); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, TypeKey, err) } case X509CertChainKey: @@ -688,15 +704,15 @@ LOOP: } h.x509CertChain = &decoded case X509CertThumbprintKey: - if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, nil); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err) } case X509CertThumbprintS256Key: - if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, nil); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err) } case X509URLKey: - if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509URL, dec, nil); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err) } default: @@ -771,108 +787,190 @@ func (h *stdHeaders) Keys() []string { return keys } -func (h stdHeaders) MarshalJSON() ([]byte, error) { - data := make(map[string]any) - keys := make([]string, 0, 16+len(h.privateParams)) +type headerPair struct { + Name string + Value any +} + +var headerPairPool = sync.Pool{ + New: func() any { + return make([]headerPair, 0, 16) + }, +} + +func getHeaderPairList() []headerPair { + return headerPairPool.Get().([]headerPair) +} + +func putHeaderPairList(list []headerPair) { + list = list[:0] + headerPairPool.Put(list) +} + +func (h *stdHeaders) makePairs() ([]headerPair, error) { + pairs := getHeaderPairList() h.mu.RLock() + defer h.mu.RUnlock() if h.agreementPartyUInfo != nil { - data[AgreementPartyUInfoKey] = h.agreementPartyUInfo - keys = append(keys, AgreementPartyUInfoKey) + v, err := json.Marshal(base64.EncodeToString(h.agreementPartyUInfo)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, AgreementPartyUInfoKey, err) + } + pairs = append(pairs, headerPair{Name: AgreementPartyUInfoKey, Value: v}) } if h.agreementPartyVInfo != nil { - data[AgreementPartyVInfoKey] = h.agreementPartyVInfo - keys = append(keys, AgreementPartyVInfoKey) + v, err := json.Marshal(base64.EncodeToString(h.agreementPartyVInfo)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, AgreementPartyVInfoKey, err) + } + pairs = append(pairs, headerPair{Name: AgreementPartyVInfoKey, Value: v}) } if h.algorithm != nil { - data[AlgorithmKey] = *(h.algorithm) - keys = append(keys, AlgorithmKey) + v, err := json.Marshal(*(h.algorithm)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err) + } + pairs = append(pairs, headerPair{Name: AlgorithmKey, Value: v}) } if h.compression != nil { - data[CompressionKey] = *(h.compression) - keys = append(keys, CompressionKey) + v, err := json.Marshal(*(h.compression)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, CompressionKey, err) + } + pairs = append(pairs, headerPair{Name: CompressionKey, Value: v}) } if h.contentEncryption != nil { - data[ContentEncryptionKey] = *(h.contentEncryption) - keys = append(keys, ContentEncryptionKey) + v, err := json.Marshal(*(h.contentEncryption)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, ContentEncryptionKey, err) + } + pairs = append(pairs, headerPair{Name: ContentEncryptionKey, Value: v}) } if h.contentType != nil { - data[ContentTypeKey] = *(h.contentType) - keys = append(keys, ContentTypeKey) + v, err := json.Marshal(*(h.contentType)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, ContentTypeKey, err) + } + pairs = append(pairs, headerPair{Name: ContentTypeKey, Value: v}) } if h.critical != nil { - data[CriticalKey] = h.critical - keys = append(keys, CriticalKey) + v, err := json.Marshal(h.critical) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, CriticalKey, err) + } + pairs = append(pairs, headerPair{Name: CriticalKey, Value: v}) } if h.ephemeralPublicKey != nil { - data[EphemeralPublicKeyKey] = h.ephemeralPublicKey - keys = append(keys, EphemeralPublicKeyKey) + v, err := json.Marshal(h.ephemeralPublicKey) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, EphemeralPublicKeyKey, err) + } + pairs = append(pairs, headerPair{Name: EphemeralPublicKeyKey, Value: v}) } if h.jwk != nil { - data[JWKKey] = h.jwk - keys = append(keys, JWKKey) + v, err := json.Marshal(h.jwk) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, JWKKey, err) + } + pairs = append(pairs, headerPair{Name: JWKKey, Value: v}) } if h.jwkSetURL != nil { - data[JWKSetURLKey] = *(h.jwkSetURL) - keys = append(keys, JWKSetURLKey) + v, err := json.Marshal(*(h.jwkSetURL)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, JWKSetURLKey, err) + } + pairs = append(pairs, headerPair{Name: JWKSetURLKey, Value: v}) } if h.keyID != nil { - data[KeyIDKey] = *(h.keyID) - keys = append(keys, KeyIDKey) + v, err := json.Marshal(*(h.keyID)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err) + } + pairs = append(pairs, headerPair{Name: KeyIDKey, Value: v}) } if h.typ != nil { - data[TypeKey] = *(h.typ) - keys = append(keys, TypeKey) + v, err := json.Marshal(*(h.typ)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, TypeKey, err) + } + pairs = append(pairs, headerPair{Name: TypeKey, Value: v}) } if h.x509CertChain != nil { - data[X509CertChainKey] = h.x509CertChain - keys = append(keys, X509CertChainKey) + v, err := json.Marshal(h.x509CertChain) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err) + } + pairs = append(pairs, headerPair{Name: X509CertChainKey, Value: v}) } if h.x509CertThumbprint != nil { - data[X509CertThumbprintKey] = *(h.x509CertThumbprint) - keys = append(keys, X509CertThumbprintKey) + v, err := json.Marshal(*(h.x509CertThumbprint)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err) + } + pairs = append(pairs, headerPair{Name: X509CertThumbprintKey, Value: v}) } if h.x509CertThumbprintS256 != nil { - data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256) - keys = append(keys, X509CertThumbprintS256Key) + v, err := json.Marshal(*(h.x509CertThumbprintS256)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err) + } + pairs = append(pairs, headerPair{Name: X509CertThumbprintS256Key, Value: v}) } if h.x509URL != nil { - data[X509URLKey] = *(h.x509URL) - keys = append(keys, X509URLKey) + v, err := json.Marshal(*(h.x509URL)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err) + } + pairs = append(pairs, headerPair{Name: X509URLKey, Value: v}) } for k, v := range h.privateParams { - data[k] = v - keys = append(keys, k) + var encoded []byte + switch v := v.(type) { + case []byte: + var err error + encoded, err = json.Marshal(base64.EncodeToString(v)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + default: + var err error + encoded, err = json.Marshal(v) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + } + pairs = append(pairs, headerPair{Name: k, Value: encoded}) } - h.mu.RUnlock() - sort.Strings(keys) + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Name < pairs[j].Name + }) + + return pairs, nil +} + +func (h *stdHeaders) MarshalJSON() ([]byte, error) { buf := pool.BytesBuffer().Get() defer pool.BytesBuffer().Put(buf) - enc := json.NewEncoder(buf) + pairs, err := h.makePairs() + if err != nil { + return nil, fmt.Errorf(`failed to make pairs: %w`, err) + } buf.WriteByte(tokens.OpenCurlyBracket) - for i, k := range keys { + + for i, pair := range pairs { if i > 0 { - buf.WriteRune(tokens.Comma) - } - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(k) - buf.WriteString(`":`) - v := data[k] - switch v := v.(type) { - case []byte: - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(base64.EncodeToString(v)) - buf.WriteRune(tokens.DoubleQuote) - default: - if err := enc.Encode(v); err != nil { - return nil, fmt.Errorf(`failed to encode value for field %s`, k) - } - buf.Truncate(buf.Len() - 1) + buf.WriteByte(tokens.Comma) } + buf.WriteByte('"') + buf.WriteString(pair.Name) + buf.WriteString(`": `) + buf.Write(pair.Value.([]byte)) } buf.WriteByte(tokens.CloseCurlyBracket) ret := make([]byte, buf.Len()) copy(ret, buf.Bytes()) + putHeaderPairList(pairs) return ret, nil } @@ -894,6 +992,6 @@ func (h *stdHeaders) clear() { h.x509CertThumbprint = nil h.x509CertThumbprintS256 = nil h.x509URL = nil - h.privateParams = map[string]any{} + clear(h.privateParams) h.mu.Unlock() } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/interface.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/interface.go index 91ad8cb809..6f0f918de3 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/interface.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/interface.go @@ -39,6 +39,25 @@ type KeyIDer interface { // expose the secret key in memory, for example, when you want to use // hardware security modules (HSMs) to decrypt the key. // +// Library contract for implementers (read carefully): +// +// - The library has already verified that the wire-level `alg` is +// consistent across the protected header and per-recipient header +// (RFC 7516 §7.2.1 disjointness). Your DecryptKey is invoked with +// the alg the library has decided to use for this attempt. +// - The library has NOT validated key-shape-vs-alg compatibility for +// your custom decrypter. You receive the raw recipient and message; +// headers are split between protected (signed/integrity-protected) +// and per-recipient (unprotected). If you read a value from the +// unprotected per-recipient header for a security decision, you +// must enforce its consistency with the protected header yourself. +// - Returning a non-nil error short-circuits this recipient. Returning +// nil bytes with nil error is treated as "decryption failed" by the +// dispatcher (use a non-nil error for clarity). +// - You are responsible for any constant-time considerations relevant +// to your decryption primitive (e.g. RFC 3218 random-CEK fallback +// for RSA-PKCS1v1.5; the library does this for the built-in path). +// // This API is experimental and may change without notice, even // in minor releases. type KeyDecrypter interface { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/aescbc/aescbc.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/aescbc/aescbc.go index 4f08c4936f..da22c82235 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/aescbc/aescbc.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/aescbc/aescbc.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "hash" + "slices" "sync/atomic" "github.com/lestrrat-go/jwx/v3/internal/pool" @@ -23,6 +24,12 @@ const defaultBufSize int64 = 256 * 1024 * 1024 var maxBufSize atomic.Int64 +// errInvalidCiphertext is the single opaque error returned by Hmac.Open for +// every failure mode (pre-MAC structural checks and post-MAC tag mismatch). +// Keeping one value across all paths prevents a structural-vs-cryptographic +// oracle on remote decrypt endpoints. +var errInvalidCiphertext = errors.New("invalid ciphertext") + func init() { SetMaxBufferSize(defaultBufSize) } @@ -108,7 +115,7 @@ type Hmac struct { blockCipher cipher.Block hash func() hash.Hash keysize int - tagsize int + tlen int integrityKey []byte } @@ -125,14 +132,23 @@ func New(key []byte, f BlockCipherFunc) (hmac *Hmac, err error) { return } + // Per RFC 7518 §5.2.2.1, T_LEN is the authentication tag length. For the + // three defined AES-CBC-HMAC variants (A128CBC-HS256, A192CBC-HS384, + // A256CBC-HS512) T_LEN happens to equal MAC_KEY_LEN (== keysize here), + // but we track it independently so a future variant with a different + // T_LEN won't silently mis-truncate the HMAC output. var hfunc func() hash.Hash + var tlen int switch keysize { - case 16: + case 16: // A128CBC-HS256 hfunc = sha256.New - case 24: + tlen = 16 + case 24: // A192CBC-HS384 hfunc = sha512.New384 - case 32: + tlen = 24 + case 32: // A256CBC-HS512 hfunc = sha512.New + tlen = 32 default: return nil, fmt.Errorf("unsupported key size %d", keysize) } @@ -142,11 +158,7 @@ func New(key []byte, f BlockCipherFunc) (hmac *Hmac, err error) { hash: hfunc, integrityKey: ikey, keysize: keysize, - tagsize: keysize, // NonceSize, - // While investigating GH #207, I stumbled upon another problem where - // the computed tags don't match on decrypt. After poking through the - // code using a bunch of debug statements, I've finally found out that - // tagsize = keysize makes the whole thing work. + tlen: tlen, }, nil } @@ -157,7 +169,7 @@ func (c Hmac) NonceSize() int { // Overhead fulfills the crypto.AEAD interface func (c Hmac) Overhead() int { - return c.blockCipher.BlockSize() + c.tagsize + return c.blockCipher.BlockSize() + c.tlen } func (c Hmac) ComputeAuthTag(aad, nonce, ciphertext []byte) ([]byte, error) { @@ -176,20 +188,26 @@ func (c Hmac) ComputeAuthTag(aad, nonce, ciphertext []byte) ([]byte, error) { h.Write(ciphertext) h.Write(buf[:]) s := h.Sum(nil) - return s[:c.tagsize], nil + return s[:c.tlen], nil } func ensureSize(dst []byte, n int) []byte { - // if the dst buffer has enough length just copy the relevant parts to it. - // Otherwise create a new slice that's big enough, and operate on that - // Note: I think go-jose has a bug in that it checks for cap(), but not len(). - ret := dst - if diff := n - len(dst); diff > 0 { - // dst is not big enough - ret = make([]byte, n) - copy(ret, dst) + // Grow dst by n bytes, preserving its current contents as the prefix. + // This matches the crypto.AEAD append contract used by Seal/Open. + if n < 0 { + panic(fmt.Errorf("failed to allocate buffer")) } - return ret + + const maxInt = int64(^uint(0) >> 1) + maxAlloc := min(maxBufSize.Load(), maxInt) + + if int64(len(dst)) > maxAlloc-int64(n) { + panic(fmt.Errorf("failed to allocate buffer")) + } + + retlen := len(dst) + n + dst = slices.Grow(dst, n) + return dst[:retlen] } // Seal fulfills the crypto.AEAD interface @@ -215,9 +233,7 @@ func (c Hmac) Seal(dst, nonce, plaintext, data []byte) []byte { panic(fmt.Errorf("failed to seal on hmac: %v", err)) } - retlen := len(dst) + len(ciphertext) + len(authtag) - - ret := ensureSize(dst, retlen) + ret := ensureSize(dst, len(ciphertext)+len(authtag)) out := ret[len(dst):] n := copy(out, ciphertext) copy(out[n:], authtag) @@ -227,17 +243,32 @@ func (c Hmac) Seal(dst, nonce, plaintext, data []byte) []byte { // Open fulfills the crypto.AEAD interface func (c Hmac) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) { - if len(ciphertext) < c.keysize { - return nil, fmt.Errorf(`invalid ciphertext (too short)`) + // Validate the IV length explicitly instead of letting + // cipher.NewCBCDecrypter panic on a mismatched nonce. The caller in + // jwe/internal/cipher also wraps Open in a defer/recover, and we + // intentionally keep BOTH layers: the explicit check turns a malformed + // IV into a normal error on the happy path (reviewable, testable, no + // stack unwind), while the recover stays as a belt-and-braces guard + // against other panics inside the stdlib CBC path (e.g. future + // invariants we don't currently enforce). Removing either layer would + // mean relying on the other — this way a regression in one is still + // caught by the other. See JWE-005 in the v4 security review. + // All pre-MAC structural failures return the exact same error value + // as the post-MAC failure below. Distinguishing "malformed nonce", + // "ciphertext too short", "ciphertext length not block-aligned", and + // "MAC mismatch" at the caller would leak whether an attacker probe + // is block-aligned vs cryptographically invalid — a structural-vs-MAC + // oracle that composes with other leaks. Keep all four paths opaque. + if len(nonce) != c.blockCipher.BlockSize() { + return nil, errInvalidCiphertext + } + if len(ciphertext) < c.tlen { + return nil, errInvalidCiphertext } - tagOffset := len(ciphertext) - c.tagsize + tagOffset := len(ciphertext) - c.tlen if tagOffset%c.blockCipher.BlockSize() != 0 { - return nil, fmt.Errorf( - "invalid ciphertext (invalid length: %d %% %d != 0)", - tagOffset, - c.blockCipher.BlockSize(), - ) + return nil, errInvalidCiphertext } tag := ciphertext[tagOffset:] ciphertext = ciphertext[:tagOffset] @@ -248,16 +279,15 @@ func (c Hmac) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) { } cbc := cipher.NewCBCDecrypter(c.blockCipher, nonce) - buf := pool.ByteSlice().GetCapacity(tagOffset) + buf := pool.ByteSlice().GetCapacity(tagOffset)[:tagOffset] defer pool.ByteSlice().Put(buf) - buf = buf[:tagOffset] cbc.CryptBlocks(buf, ciphertext) toRemove, good := extractPadding(buf) cmp := subtle.ConstantTimeCompare(expectedTag, tag) & int(good) if cmp != 1 { - return nil, errors.New(`invalid ciphertext`) + return nil, errInvalidCiphertext } plaintext := buf[:len(buf)-toRemove] diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/cipher/cipher.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/cipher/cipher.go index 9b9a40d00d..9595864c69 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/cipher/cipher.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/cipher/cipher.go @@ -128,9 +128,8 @@ func (c AesContentCipher) Encrypt(cek, plaintext, aad []byte) (iv, ciphertxt, ta panic(fmt.Sprintf("tag offset is less than 0 (combined len = %d, tagsize = %d)", len(combined), c.TagSize())) } + ciphertxt = combined[:tagoffset:tagoffset] tag = combined[tagoffset:] - ciphertxt = make([]byte, tagoffset) - copy(ciphertxt, combined[:tagoffset]) return } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/concatkdf/concatkdf.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/concatkdf/concatkdf.go index 3691830a63..18b61afb6d 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/concatkdf/concatkdf.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/concatkdf/concatkdf.go @@ -13,22 +13,25 @@ type KDF struct { hash crypto.Hash } -func ndata(src []byte) []byte { - buf := make([]byte, 4+len(src)) - binary.BigEndian.PutUint32(buf, uint32(len(src))) - copy(buf[4:], src) - return buf -} - func New(hash crypto.Hash, alg, Z, apu, apv, pubinfo, privinfo []byte) *KDF { - algbuf := ndata(alg) - apubuf := ndata(apu) - apvbuf := ndata(apv) + // Write length-prefixed fields directly into a single buffer, + // avoiding intermediate allocations from ndata(). + totalSize := (4 + len(alg)) + (4 + len(apu)) + (4 + len(apv)) + len(pubinfo) + len(privinfo) + concat := make([]byte, totalSize) + + n := 0 + binary.BigEndian.PutUint32(concat[n:], uint32(len(alg))) + n += 4 + n += copy(concat[n:], alg) + + binary.BigEndian.PutUint32(concat[n:], uint32(len(apu))) + n += 4 + n += copy(concat[n:], apu) + + binary.BigEndian.PutUint32(concat[n:], uint32(len(apv))) + n += 4 + n += copy(concat[n:], apv) - concat := make([]byte, len(algbuf)+len(apubuf)+len(apvbuf)+len(pubinfo)+len(privinfo)) - n := copy(concat, algbuf) - n += copy(concat[n:], apubuf) - n += copy(concat[n:], apvbuf) n += copy(concat[n:], pubinfo) copy(concat[n:], privinfo) @@ -42,11 +45,13 @@ func New(hash crypto.Hash, alg, Z, apu, apv, pubinfo, privinfo []byte) *KDF { func (k *KDF) Read(out []byte) (int, error) { var round uint32 = 1 h := k.hash.New() + var roundBuf [4]byte for len(out) > len(k.buf) { h.Reset() - if err := binary.Write(h, binary.BigEndian, round); err != nil { + binary.BigEndian.PutUint32(roundBuf[:], round) + if _, err := h.Write(roundBuf[:]); err != nil { return 0, fmt.Errorf(`failed to write round using kdf: %w`, err) } if _, err := h.Write(k.z); err != nil { @@ -56,7 +61,7 @@ func (k *KDF) Read(out []byte) (int, error) { return 0, fmt.Errorf(`failed to write other info using kdf: %w`, err) } - k.buf = append(k.buf, h.Sum(nil)...) + k.buf = h.Sum(k.buf) round++ } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/keygen/keygen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/keygen/keygen.go index daa7599d9f..34e6b05c30 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/keygen/keygen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/keygen/keygen.go @@ -9,7 +9,6 @@ import ( "fmt" "io" - "github.com/lestrrat-go/jwx/v3/internal/ecutil" "github.com/lestrrat-go/jwx/v3/internal/tokens" "github.com/lestrrat-go/jwx/v3/jwe/internal/concatkdf" "github.com/lestrrat-go/jwx/v3/jwk" @@ -28,9 +27,27 @@ func Random(n int) (ByteSource, error) { return ByteKey(buf), nil } -// Ecdhes generates a new key using ECDH-ES +// Ecdhes generates a new key using ECDH-ES. +// +// The recipient pubkey is converted to *ecdh.PublicKey via stdlib +// (*ecdsa.PublicKey).ECDH() before any cryptographic operation. That +// conversion uses identity matching against the named NIST curves +// (elliptic.P256/P384/P521) and rejects anything else — including a +// caller-controlled or tampered elliptic.Curve and the generic big-int +// CurveParams path. This closes the invalid-curve attack surface that +// the previous deprecated crypto/elliptic.Curve.ScalarMult code path +// exposed when the recipient's *ecdsa.PublicKey.Curve field was +// attacker-influenced. func Ecdhes(alg string, enc string, keysize int, pubkey *ecdsa.PublicKey, apu, apv []byte) (ByteSource, error) { - priv, err := ecdsa.GenerateKey(pubkey.Curve, rand.Reader) + if pubkey == nil || pubkey.X == nil || pubkey.Y == nil { + return nil, fmt.Errorf(`invalid ECDH-ES public key: nil X or Y`) + } + ecdhPub, err := pubkey.ECDH() + if err != nil { + return nil, fmt.Errorf(`failed to convert ECDH-ES public key to *ecdh.PublicKey: %w`, err) + } + + priv, err := ecdhPub.Curve().GenerateKey(rand.Reader) if err != nil { return nil, fmt.Errorf(`failed to generate key for ECDH-ES: %w`, err) } @@ -45,12 +62,11 @@ func Ecdhes(alg string, enc string, keysize int, pubkey *ecdsa.PublicKey, apu, a pubinfo := make([]byte, 4) binary.BigEndian.PutUint32(pubinfo, uint32(keysize)*8) - if !priv.PublicKey.Curve.IsOnCurve(pubkey.X, pubkey.Y) { - return nil, fmt.Errorf(`public key used does not contain a point (X,Y) on the curve`) + zBytes, err := priv.ECDH(ecdhPub) + if err != nil { + return nil, fmt.Errorf(`failed to compute Z: %w`, err) } - z, _ := priv.PublicKey.Curve.ScalarMult(pubkey.X, pubkey.Y, priv.D.Bytes()) - zBytes := ecutil.AllocECPointBuffer(z, priv.PublicKey.Curve) - defer ecutil.ReleaseECPointBuffer(zBytes) + kdf := concatkdf.New(crypto.SHA256, []byte(algorithm), zBytes, apu, apv, pubinfo, []byte{}) kek := make([]byte, keysize) if _, err := kdf.Read(kek); err != nil { @@ -58,13 +74,13 @@ func Ecdhes(alg string, enc string, keysize int, pubkey *ecdsa.PublicKey, apu, a } return ByteWithECPublicKey{ - PublicKey: &priv.PublicKey, + PublicKey: priv.PublicKey(), ByteKey: ByteKey(kek), }, nil } // X25519 generates a new key using ECDH-ES with X25519 -func X25519(alg string, enc string, keysize int, pubkey *ecdh.PublicKey) (ByteSource, error) { +func X25519(alg string, enc string, keysize int, pubkey *ecdh.PublicKey, apu, apv []byte) (ByteSource, error) { priv, err := ecdh.X25519().GenerateKey(rand.Reader) if err != nil { return nil, fmt.Errorf(`failed to generate key for X25519: %w`, err) @@ -84,7 +100,7 @@ func X25519(alg string, enc string, keysize int, pubkey *ecdh.PublicKey) (ByteSo if err != nil { return nil, fmt.Errorf(`failed to compute Z: %w`, err) } - kdf := concatkdf.New(crypto.SHA256, []byte(algorithm), zBytes, []byte{}, []byte{}, pubinfo, []byte{}) + kdf := concatkdf.New(crypto.SHA256, []byte(algorithm), zBytes, apu, apv, pubinfo, []byte{}) kek := make([]byte, keysize) if _, err := kdf.Read(kek); err != nil { return nil, fmt.Errorf(`failed to read kdf: %w`, err) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/io.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/io.go index a5d6aca8a3..5f1a397c68 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/io.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/io.go @@ -15,6 +15,12 @@ func (sysFS) Open(path string) (fs.File, error) { } func ReadFile(path string, options ...ReadFileOption) (*Message, error) { + var parseOptions []ParseOption + for _, option := range options { + if po, ok := option.(ParseOption); ok { + parseOptions = append(parseOptions, po) + } + } var srcFS fs.FS = sysFS{} for _, option := range options { @@ -32,5 +38,5 @@ func ReadFile(path string, options ...ReadFileOption) (*Message, error) { } defer f.Close() - return ParseReader(f) + return ParseReader(f, parseOptions...) } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwe.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwe.go index 5b9c92771a..706efaaa27 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwe.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwe.go @@ -1,6 +1,11 @@ //go:generate ../tools/cmd/genjwe.sh -// Package jwe implements JWE as described in https://tools.ietf.org/html/rfc7516 +// Package jwe implements JWE as described in https://tools.ietf.org/html/rfc7516. +// +// Legacy note: RSA-PKCS1 v1.5 key encryption (`jwa.RSA1_5()`) is supported +// only for interoperability with existing peers. New applications should +// prefer an RSA-OAEP variant such as `jwa.RSA_OAEP_256()` because PKCS#1 v1.5 +// decryption is exposed to Bleichenbacher-style oracle attacks. package jwe // #region imports @@ -11,7 +16,9 @@ import ( "errors" "fmt" "io" - "sync" + "math" + "slices" + "sync/atomic" "github.com/lestrrat-go/blackmagic" "github.com/lestrrat-go/jwx/v3/internal/base64" @@ -28,33 +35,92 @@ import ( // #region globals -var muSettings sync.RWMutex -var maxPBES2Count = 10000 -var maxDecompressBufferSize int64 = 10 * 1024 * 1024 // 10MB +var maxPBES2Count atomic.Int64 +var minPBES2Count atomic.Int64 +var pbes2Count atomic.Int64 +var maxRecipients atomic.Int64 +var maxDecompressBufferSize atomic.Int64 +var disabledKeyAlgs atomic.Pointer[map[string]struct{}] + +func init() { + maxPBES2Count.Store(10000) + minPBES2Count.Store(1000) + pbes2Count.Store(int64(tokens.PBES2DefaultIterations)) + maxRecipients.Store(100) + maxDecompressBufferSize.Store(10 * 1024 * 1024) // 10MB +} func Settings(options ...GlobalOption) { - muSettings.Lock() - defer muSettings.Unlock() for _, option := range options { switch option.Ident() { case identMaxPBES2Count{}: - if err := option.Value(&maxPBES2Count); err != nil { + var v int + if err := option.Value(&v); err != nil { panic(fmt.Sprintf("jwe.Settings: value for option WithMaxPBES2Count must be an int: %s", err)) } + maxPBES2Count.Store(int64(v)) + case identMinPBES2Count{}: + var v int + if err := option.Value(&v); err != nil { + panic(fmt.Sprintf("jwe.Settings: value for option WithMinPBES2Count must be an int: %s", err)) + } + minPBES2Count.Store(int64(v)) + case identPBES2Count{}: + var v int + if err := option.Value(&v); err != nil { + panic(fmt.Sprintf("jwe.Settings: value for option WithPBES2Count must be an int: %s", err)) + } + if v <= 0 { + v = tokens.PBES2DefaultIterations + } + pbes2Count.Store(int64(v)) + case identMaxRecipients{}: + var v int + if err := option.Value(&v); err != nil { + panic(fmt.Sprintf("jwe.Settings: value for option WithMaxRecipients must be an int: %s", err)) + } + maxRecipients.Store(int64(v)) case identMaxDecompressBufferSize{}: - if err := option.Value(&maxDecompressBufferSize); err != nil { + var v int64 + if err := option.Value(&v); err != nil { panic(fmt.Sprintf("jwe.Settings: value for option WithMaxDecompressBufferSize must be an int64: %s", err)) } + maxDecompressBufferSize.Store(v) case identCBCBufferSize{}: var v int64 if err := option.Value(&v); err != nil { panic(fmt.Sprintf("jwe.Settings: value for option WithCBCBufferSize must be an int64: %s", err)) } aescbc.SetMaxBufferSize(v) + case identDisabledKeyAlgorithms{}: + var algs []jwa.KeyEncryptionAlgorithm + if err := option.Value(&algs); err != nil { + panic(fmt.Sprintf("jwe.Settings: value for option WithDisabledKeyAlgorithms must be []jwa.KeyEncryptionAlgorithm: %s", err)) + } + if len(algs) == 0 { + disabledKeyAlgs.Store(nil) + continue + } + m := make(map[string]struct{}, len(algs)) + for _, alg := range algs { + m[alg.String()] = struct{}{} + } + disabledKeyAlgs.Store(&m) } } } +// isKeyAlgorithmDisabled reports whether alg is in the global +// jwe.WithDisabledKeyAlgorithms set. +func isKeyAlgorithmDisabled(alg jwa.KeyEncryptionAlgorithm) bool { + m := disabledKeyAlgs.Load() + if m == nil { + return false + } + _, ok := (*m)[alg.String()] + return ok +} + const ( fmtInvalid = iota fmtCompact @@ -63,18 +129,19 @@ const ( fmtMax ) -var _ = fmtInvalid -var _ = fmtMax - var registry = json.NewRegistry() type recipientBuilder struct { - alg jwa.KeyEncryptionAlgorithm - key any - headers Headers + alg jwa.KeyEncryptionAlgorithm + key any + headers Headers + pbes2Count int } func (b *recipientBuilder) Build(r Recipient, cek []byte, calg jwa.ContentEncryptionAlgorithm, _ *content_crypt.Generic) ([]byte, error) { + if isKeyAlgorithmDisabled(b.alg) { + return nil, fmt.Errorf(`jwe.Encrypt: key encryption algorithm %q is disabled by jwe.WithDisabledKeyAlgorithms`, b.alg) + } // we need the raw key for later use rawKey := b.key @@ -104,7 +171,7 @@ func (b *recipientBuilder) Build(r Recipient, cek []byte, calg jwa.ContentEncryp hdr := b.headers if hdr == nil { - hdr = NewHeaders() + hdr = r.Headers() } if val, ok := hdr.AgreementPartyUInfo(); ok { @@ -116,10 +183,7 @@ func (b *recipientBuilder) Build(r Recipient, cek []byte, calg jwa.ContentEncryp } // Create the encrypter using the new jwebb pattern - enc, err := newEncrypter(b.alg, calg, b.key, rawKey, apu, apv) - if err != nil { - return nil, fmt.Errorf(`jwe.Encrypt: recipientBuilder: failed to create encrypter: %w`, err) - } + enc := newEncrypter(b.alg, calg, b.key, rawKey, apu, apv, b.pbes2Count) _ = r.SetHeaders(hdr) @@ -168,9 +232,9 @@ func (b *recipientBuilder) Build(r Recipient, cek []byte, calg jwa.ContentEncryp // option. // // jwe.Encrypt(payload, jwe.WithKey(alg, key)) -// jwe.Encrypt(payload, jws.WithJSON(), jws.WithKey(alg1, key1), jws.WithKey(alg2, key2)) +// jwe.Encrypt(payload, jwe.WithJSON(), jwe.WithKey(alg1, key1), jwe.WithKey(alg2, key2)) // -// Note that in the second example the `jws.WithJSON()` option is +// Note that in the second example the `jwe.WithJSON()` option is // specified as well. This is because the compact serialization // format does not support multiple recipients, and users must // specifically ask for the JSON serialization format. @@ -178,7 +242,18 @@ func (b *recipientBuilder) Build(r Recipient, cek []byte, calg jwa.ContentEncryp // Read the documentation for `jwe.WithKey()` to learn more about the // possible values that can be used for `alg` and `key`. // -// Look for options that return `jwe.EncryptOption` or `jws.EncryptDecryptOption` +// `jwa.RSA1_5()` is supported only for interoperability with legacy peers. +// New applications should prefer an RSA-OAEP variant such as +// `jwa.RSA_OAEP_256()` because PKCS#1 v1.5 decryption is exposed to +// Bleichenbacher-style oracle attacks. +// If you enable `jwe.WithCompress()`, this library does not enforce a +// producer-side payload size limit before compression. Callers that accept +// untrusted or arbitrarily large plaintext must bound the input size before +// calling `jwe.Encrypt()`. Recipients may also reject compressed messages +// whose decompressed payload exceeds their `jwe.WithMaxDecompressBufferSize()` +// setting. +// +// Look for options that return `jwe.EncryptOption` or `jwe.EncryptDecryptOption` // for a complete list of options that can be passed to this function. // // As of v3.0.12, users can specify `jwe.WithLegacyHeaderMerging()` to @@ -188,11 +263,11 @@ func Encrypt(payload []byte, options ...EncryptOption) ([]byte, error) { ec := encryptContextPool.Get() defer encryptContextPool.Put(ec) if err := ec.ProcessOptions(options); err != nil { - return nil, encryptError{fmt.Errorf(`jwe.Encrypt: failed to process options: %w`, err)} + return nil, makeEncryptError(`jwe.Encrypt`, `failed to process options: %w`, err) } ret, err := ec.EncryptMessage(payload, nil) if err != nil { - return nil, encryptError{fmt.Errorf(`jwe.Encrypt: %w`, err)} + return nil, makeEncryptError(`jwe.Encrypt`, `%w`, err) } return ret, nil } @@ -202,6 +277,32 @@ func Encrypt(payload []byte, options ...EncryptOption) ([]byte, error) { // Encrypt function such that the latter does not accidentally use a static // CEK. // +// Unless `jwe.WithContentEncryption()` is provided, `EncryptStatic` uses +// `jwa.A256GCM()`, which requires a 32-byte CEK. +// +// The CEK used to encrypt the payload must match the selected content +// encryption algorithm: +// +// - `jwa.A128GCM()`: 16 bytes +// - `jwa.A192GCM()`: 24 bytes +// - `jwa.A256GCM()`: 32 bytes +// - `jwa.A128CBC_HS256()`: 32 bytes +// - `jwa.A192CBC_HS384()`: 48 bytes +// - `jwa.A256CBC_HS512()`: 64 bytes +// +// `EncryptStatic` validates the final CEK length before payload encryption +// and returns an error if it does not match the selected `enc` algorithm. +// +// NOTE: when the chosen key-encryption algorithm derives the CEK rather than +// wrapping it — specifically `jwa.DIRECT()` and bare `jwa.ECDH_ES()` (without +// a key-wrap suffix) — the `cek` argument supplied here is ignored for +// content encryption. In those modes the effective CEK is the shared/derived +// key produced by the `jwe.WithKey()` input, and the byte-length check +// described above is enforced against that derived CEK, not against the +// value passed as `cek`. To pin the CEK deterministically, pair +// `EncryptStatic` only with key-wrapping algorithms such as +// `jwa.RSA_OAEP()`, `jwa.A256KW()`, or `jwa.ECDH_ES_A256KW()`. +// // DO NOT attempt to use this function unless you completely understand the // security implications to using static CEKs. You have been warned. // @@ -209,16 +310,16 @@ func Encrypt(payload []byte, options ...EncryptOption) ([]byte, error) { // future changes across minor/micro versions. func EncryptStatic(payload, cek []byte, options ...EncryptOption) ([]byte, error) { if len(cek) <= 0 { - return nil, encryptError{fmt.Errorf(`jwe.EncryptStatic: empty CEK`)} + return nil, makeEncryptError(`jwe.EncryptStatic`, `empty CEK`) } ec := encryptContextPool.Get() defer encryptContextPool.Put(ec) if err := ec.ProcessOptions(options); err != nil { - return nil, encryptError{fmt.Errorf(`jwe.EncryptStatic: failed to process options: %w`, err)} + return nil, makeEncryptError(`jwe.EncryptStatic`, `failed to process options: %w`, err) } ret, err := ec.EncryptMessage(payload, cek) if err != nil { - return nil, encryptError{fmt.Errorf(`jwe.EncryptStatic: %w`, err)} + return nil, makeEncryptError(`jwe.EncryptStatic`, `%w`, err) } return ret, nil } @@ -229,7 +330,12 @@ type decryptContext struct { keyUsed any cek *[]byte dst *Message + maxRecipients int maxDecompressBufferSize int64 + maxPBES2Count int + minPBES2Count int + critValidation bool + criticalExtensions []string //nolint:containedctx ctx context.Context } @@ -247,16 +353,21 @@ func freeDecryptContext(dc *decryptContext) *decryptContext { dc.keyUsed = nil dc.cek = nil dc.dst = nil + dc.maxRecipients = 0 dc.maxDecompressBufferSize = 0 + dc.maxPBES2Count = 0 + dc.minPBES2Count = 0 + dc.critValidation = false + dc.criticalExtensions = dc.criticalExtensions[:0] dc.ctx = context.Background() return dc } func (dc *decryptContext) ProcessOptions(options []DecryptOption) error { - // Set default max decompress buffer size - muSettings.RLock() - dc.maxDecompressBufferSize = maxDecompressBufferSize - muSettings.RUnlock() + dc.maxRecipients = int(maxRecipients.Load()) + dc.maxDecompressBufferSize = maxDecompressBufferSize.Load() + dc.maxPBES2Count = int(maxPBES2Count.Load()) + dc.minPBES2Count = int(minPBES2Count.Load()) for _, option := range options { switch option.Ident() { @@ -283,19 +394,44 @@ func (dc *decryptContext) ProcessOptions(options []DecryptOption) error { if !ok { return fmt.Errorf("jwe.decrypt: WithKey() option must be specified using jwa.KeyEncryptionAlgorithm (got %T)", pair.alg) } + if err := validateAlgorithmForKey(alg, pair.key); err != nil { + return fmt.Errorf("jwe.WithKey: %w", err) + } dc.keyProviders = append(dc.keyProviders, &staticKeyProvider{alg: alg, key: pair.key}) case identCEK{}: if err := option.Value(&dc.cek); err != nil { return fmt.Errorf("jwe.decrypt: WithCEK must be a *[]byte: %w", err) } + case identMaxRecipients{}: + if err := option.Value(&dc.maxRecipients); err != nil { + return fmt.Errorf("jwe.decrypt: WithMaxRecipients must be int: %w", err) + } case identMaxDecompressBufferSize{}: if err := option.Value(&dc.maxDecompressBufferSize); err != nil { return fmt.Errorf("jwe.decrypt: WithMaxDecompressBufferSize must be int64: %w", err) } + case identMaxPBES2Count{}: + if err := option.Value(&dc.maxPBES2Count); err != nil { + return fmt.Errorf("jwe.decrypt: WithMaxPBES2Count must be int: %w", err) + } + case identMinPBES2Count{}: + if err := option.Value(&dc.minPBES2Count); err != nil { + return fmt.Errorf("jwe.decrypt: WithMinPBES2Count must be int: %w", err) + } case identContext{}: if err := option.Value(&dc.ctx); err != nil { return fmt.Errorf("jwe.decrypt: WithContext must be a context.Context: %w", err) } + case identCritValidation{}: + if err := option.Value(&dc.critValidation); err != nil { + return fmt.Errorf("jwe.decrypt: WithCritValidation must be a bool: %w", err) + } + case identCritExtension{}: + var names []string + if err := option.Value(&names); err != nil { + return fmt.Errorf("jwe.decrypt: WithCritExtension must be a string: %w", err) + } + dc.criticalExtensions = append(dc.criticalExtensions, names...) } } @@ -306,20 +442,107 @@ func (dc *decryptContext) ProcessOptions(options []DecryptOption) error { return nil } +// validateCritical checks the "crit" header per RFC 7516 Section 4.1.13 +// (which references RFC 7515 Section 4.1.11). It enforces: +// - the list is non-empty +// - no entry is the empty string +// - no entry duplicates another +// - no entry names a standard JOSE/JWE header parameter +// - every entry appears as a header parameter in the protected header +// - every entry is in the caller-supplied allowedExtensions allowlist +// +// The last check is the central RFC requirement: recipients MUST reject +// any "crit" extension they do not understand, and the only way the +// library knows which extensions the caller understands is via the +// allowlist (populated from jwe.WithCritExtension()). +func validateCritical(protected Headers, allowedExtensions []string) error { + if !protected.Has(CriticalKey) { + return nil + } + + crit, _ := protected.Critical() + if len(crit) == 0 { + return makeDecryptError(`"crit" header must not be empty`) + } + + seen := make(map[string]struct{}, len(crit)) + for _, name := range crit { + if name == "" { + return makeDecryptError(`"crit" header must not contain an empty extension name`) + } + if _, dup := seen[name]; dup { + return makeDecryptError(`"crit" header must not contain duplicate extension %q`, name) + } + seen[name] = struct{}{} + + // RFC 7515 Section 4.1.11: "crit" MUST NOT include names defined + // by the JOSE Header specification itself. + if slices.Contains(stdHeaderNames, name) { + return makeDecryptError(`"crit" header must not contain standard header parameter %q`, name) + } + + // The extension must be present in the protected header. + if !protected.Has(name) { + return makeDecryptError(`"crit" header references extension %q, but it is not present in the protected header`, name) + } + + // The recipient must have declared support for the extension. + if !slices.Contains(allowedExtensions, name) { + return makeDecryptError(`"crit" header references extension %q, but the recipient has not declared support for it (use jwe.WithCritExtension(%q))`, name, name) + } + } + + return nil +} + +// concatAAD returns the AAD value used to seal or open a JWE payload: +// the protected-header segment, optionally followed by ASCII '.' and +// the caller-supplied external aad (RFC 7516 §5.1 step 14 / §5.2 +// step 14). A fresh slice is always allocated so the caller's computed +// and aad slices are never appended into, which matters because +// computedAad often aliases a Message field whose backing array is +// still referenced elsewhere. +func concatAAD(computed, aad []byte) []byte { + if len(aad) == 0 { + return computed + } + out := make([]byte, len(computed)+1+len(aad)) + n := copy(out, computed) + out[n] = tokens.Period + copy(out[n+1:], aad) + return out +} + func (dc *decryptContext) DecryptMessage(buf []byte) ([]byte, error) { - msg, err := parseJSONOrCompact(buf, true) + msg, err := parseJSONOrCompact(buf, true, dc.maxRecipients) if err != nil { - return nil, fmt.Errorf(`failed to parse buffer for Decrypt: %w`, err) + return nil, fmt.Errorf(`jwe.Decrypt: failed to parse buffer: %w`, err) } - // Process things that are common to the message - h, err := msg.protectedHeaders.Clone() - if err != nil { - return nil, fmt.Errorf(`failed to copy protected headers: %w`, err) + // Validate the "crit" header per RFC 7516 Section 4.1.13. The check + // runs against the protected header only — RFC says "crit" MUST live + // there — and short-circuits before any key-decrypt or content-decrypt + // work happens. + if dc.critValidation { + if err := validateCritical(msg.protectedHeaders, dc.criticalExtensions); err != nil { + return nil, err + } } - h, err = h.Merge(msg.unprotectedHeaders) + + // Clone the shared (top-level) protected header as our working copy. + // We deliberately do NOT merge msg.unprotectedHeaders (the shared, + // top-level *unprotected* header) here: it is never covered by the + // AEAD tag, so it must not contribute algorithm parameters. + // + // Per-recipient unprotected headers are a separate case — RFC 7516 + // §5.3 explicitly permits them to carry recipient-specific algorithm + // parameters (alg, epk, p2s, p2c, iv, tag, apu, apv, …), and + // decryptContent merges recipient.Headers() onto this base below. + // That merge is bounded by WithMaxRecipients and, for PBES2, by + // WithMaxPBES2Count (applied per recipient). + h, err := msg.protectedHeaders.Clone() if err != nil { - return nil, fmt.Errorf(`failed to merge headers for message decryption: %w`, err) + return nil, fmt.Errorf(`jwe.Decrypt: failed to copy protected headers: %w`, err) } var aad []byte @@ -335,7 +558,7 @@ func (dc *decryptContext) DecryptMessage(buf []byte) ([]byte, error) { var err error computedAad, err = msg.protectedHeaders.Encode() if err != nil { - return nil, fmt.Errorf(`failed to encode protected headers: %w`, err) + return nil, fmt.Errorf(`jwe.Decrypt: failed to encode protected headers: %w`, err) } } @@ -345,16 +568,22 @@ func (dc *decryptContext) DecryptMessage(buf []byte) ([]byte, error) { if len(recipients) == 0 { r := NewRecipient() if err := r.SetHeaders(msg.protectedHeaders); err != nil { - return nil, fmt.Errorf(`failed to set headers to recipient: %w`, err) + return nil, fmt.Errorf(`jwe.Decrypt: failed to set headers to recipient: %w`, err) } recipients = append(recipients, r) } errs := make([]error, 0, len(recipients)) for _, recipient := range recipients { + // Honor caller's deadline between recipients. Symmetric with + // the per-keyProvider and per-(alg,key) checks in tryRecipient. + if err := dc.ctx.Err(); err != nil { + return nil, makeDecryptError(`%w`, err) + } + decrypted, err := dc.tryRecipient(msg, recipient, h, aad, computedAad) if err != nil { - errs = append(errs, recipientError{err}) + errs = append(errs, makeRecipientError(err)) continue } if dc.dst != nil { @@ -364,19 +593,48 @@ func (dc *decryptContext) DecryptMessage(buf []byte) ([]byte, error) { } return decrypted, nil } - return nil, fmt.Errorf(`failed to decrypt any of the recipients: %w`, errors.Join(errs...)) + // Bound the joined-error count so a hostile JWE with many recipients + // can't produce an unbounded error string. Keep the first + // decryptErrorJoinCap entries verbatim and replace the rest with a + // single "... and N more" sentinel. + return nil, fmt.Errorf(`jwe.Decrypt: failed to decrypt any of the recipients: %w`, joinDecryptErrors(errs)) +} + +// decryptErrorJoinCap caps how many per-recipient constituent errors +// get joined into the final Decrypt error so the resulting err.Error() +// can't grow unboundedly under a hostile multi-recipient JWE. +const decryptErrorJoinCap = 10 + +func joinDecryptErrors(errs []error) error { + if len(errs) <= decryptErrorJoinCap { + return errors.Join(errs...) + } + kept := make([]error, decryptErrorJoinCap, decryptErrorJoinCap+1) + copy(kept, errs[:decryptErrorJoinCap]) + kept = append(kept, fmt.Errorf("... and %d more error(s) suppressed", len(errs)-decryptErrorJoinCap)) + return errors.Join(kept...) } func (dc *decryptContext) tryRecipient(msg *Message, recipient Recipient, protectedHeaders Headers, aad, computedAad []byte) ([]byte, error) { var tried int var lastError error for i, kp := range dc.keyProviders { + // Honor caller's deadline between key providers. + if err := dc.ctx.Err(); err != nil { + return nil, err + } + var sink algKeySink if err := kp.FetchKeys(dc.ctx, &sink, recipient, msg); err != nil { return nil, fmt.Errorf(`key provider %d failed: %w`, i, err) } for _, pair := range sink.list { + // Honor caller's deadline between (alg,key) pairs. + if err := dc.ctx.Err(); err != nil { + return nil, err + } + tried++ // alg is converted here because pair.alg is of type jwa.KeyAlgorithm. // this may seem ugly, but we're trying to avoid declaring separate @@ -403,6 +661,9 @@ func (dc *decryptContext) tryRecipient(msg *Message, recipient Recipient, protec } func (dc *decryptContext) decryptContent(msg *Message, alg jwa.KeyEncryptionAlgorithm, key any, recipient Recipient, protectedHeaders Headers, aad, computedAad []byte) ([]byte, error) { + if isKeyAlgorithmDisabled(alg) { + return nil, makeDecryptError(`key encryption algorithm %q is disabled by jwe.WithDisabledKeyAlgorithms`, alg) + } if jwkKey, ok := key.(jwk.Key); ok { var raw any if err := jwk.Export(jwkKey, &raw); err != nil { @@ -422,9 +683,34 @@ func (dc *decryptContext) decryptContent(msg *Message, alg jwa.KeyEncryptionAlgo Tag(msg.tag). CEK(dc.cek) - // The "alg" header can be in either protected/unprotected headers. - // prefer per-recipient headers (as it might be the case that the algorithm differs - // by each recipient), then look at protected headers. + // RFC 7516 §7.2.1 requires header parameter names to be disjoint + // across the protected, shared-unprotected, and per-recipient + // header locations. For "alg" specifically, allowing protected + // and per-recipient headers to declare conflicting values is an + // algorithm-confusion vector: an attacker who can rewrite the + // per-recipient (unprotected) location can claim a different alg + // than the integrity-protected one, and the alg-match loop below + // would silently break on whichever it sees first. + // + // Compact-form JWE legitimately has the same alg value in both + // places — parseCompact synthesizes a per-recipient header by + // cloning the protected header (minus enc), so a strict-disjoint + // check would reject every compact JWE. We therefore allow the + // duplication when the values agree, and reject only when they + // disagree. + if rh := recipient.Headers(); rh != nil { + if recipAlg, recipHas := rh.Algorithm(); recipHas { + if protectedAlg, protectedHas := protectedHeaders.Algorithm(); protectedHas && protectedAlg != recipAlg { + return nil, makeDecryptError(`malformed JWE — "alg" header value differs between protected (%q) and per-recipient (%q) headers (RFC 7516 §7.2.1)`, protectedAlg, recipAlg) + } + } + } + + // The "alg" header can be in either protected or per-recipient + // headers. With disjointness enforced above, only one location can + // have it, so iteration order does not affect security; we keep + // per-recipient first to match the historical preference for + // recipient-specific algs in multi-recipient JWE. var algMatched bool for _, hdr := range []Headers{recipient.Headers(), protectedHeaders} { v, ok := hdr.Algorithm() @@ -484,7 +770,10 @@ func (dc *decryptContext) decryptContent(msg *Message, alg jwa.KeyEncryptionAlgo } case jwa.A128GCMKW(), jwa.A192GCMKW(), jwa.A256GCMKW(): var ivB64 string - if err := h2.Get(InitializationVectorKey, &ivB64); err == nil { + if h2.Has(InitializationVectorKey) { + if err := h2.Get(InitializationVectorKey, &ivB64); err != nil { + return nil, fmt.Errorf(`field %q is not a string: %w`, InitializationVectorKey, err) + } iv, err := base64.DecodeString(ivB64) if err != nil { return nil, fmt.Errorf(`failed to b64-decode 'iv': %w`, err) @@ -492,7 +781,10 @@ func (dc *decryptContext) decryptContent(msg *Message, alg jwa.KeyEncryptionAlgo dec.KeyInitializationVector(iv) } var tagB64 string - if err := h2.Get(TagKey, &tagB64); err == nil { + if h2.Has(TagKey) { + if err := h2.Get(TagKey, &tagB64); err != nil { + return nil, fmt.Errorf(`field %q is not a string: %w`, TagKey, err) + } tag, err := base64.DecodeString(tagB64) if err != nil { return nil, fmt.Errorf(`failed to b64-decode 'tag': %w`, err) @@ -505,39 +797,58 @@ func (dc *decryptContext) decryptContent(msg *Message, alg jwa.KeyEncryptionAlgo return nil, fmt.Errorf(`failed to get %q field`, SaltKey) } - // check if WithUseNumber is effective, because it will change the - // type of the underlying value (#1140) - var countFlt float64 + // Parse p2c into int64 directly. Float64 cannot represent + // integers above 2^53 exactly; comparing a parsed value + // against a high MaxPBES2Count cap in float-space and then + // casting via int(...) lets out-of-range values silently + // round into the accepted range when callers raise the cap + // past 2^53. int64 keeps the bound check exact. + var count int64 if json.UseNumber() { - var count json.Number - if err := h2.Get(CountKey, &count); err != nil { + var n json.Number + if err := h2.Get(CountKey, &n); err != nil { return nil, fmt.Errorf(`failed to get %q field`, CountKey) } - v, err := count.Float64() + c, err := n.Int64() if err != nil { - return nil, fmt.Errorf("failed to convert 'p2c' to float64: %w", err) + return nil, fmt.Errorf(`invalid 'p2c' value: %q is not a valid integer: %w`, n.String(), err) } - countFlt = v + count = c } else { - var count float64 - if err := h2.Get(CountKey, &count); err != nil { + var v float64 + if err := h2.Get(CountKey, &v); err != nil { return nil, fmt.Errorf(`failed to get %q field`, CountKey) } - countFlt = count + if math.IsNaN(v) || math.IsInf(v, 0) || math.Trunc(v) != v { + return nil, fmt.Errorf(`invalid 'p2c' value: not a positive integer (got %v)`, v) + } + // Use explicit float-domain bounds (2^63 / -2^63) so + // the comparison is platform-independent and does not + // go through math.MaxInt64's implicit conversion. + const ( + int64MaxAsFloat = float64(1 << 63) // 2^63, smallest float > MaxInt64 + int64MinAsFloat = -int64MaxAsFloat // -2^63, exact float = MinInt64 + ) + if v >= int64MaxAsFloat || v < int64MinAsFloat { + return nil, fmt.Errorf(`invalid 'p2c' value: not representable as int64 (got %v)`, v) + } + count = int64(v) } - muSettings.RLock() - maxCount := maxPBES2Count - muSettings.RUnlock() - if countFlt > float64(maxCount) { - return nil, fmt.Errorf("invalid 'p2c' value") + maxCount := dc.maxPBES2Count + minCount := dc.minPBES2Count + if count < int64(minCount) { + return nil, fmt.Errorf(`invalid 'p2c' value: %d is below WithMinPBES2Count=%d (RFC 7518 §4.8.1.2 floor; loosen via jwe.WithMinPBES2Count)`, count, minCount) + } + if count > int64(maxCount) { + return nil, fmt.Errorf(`invalid 'p2c' value: %d exceeds WithMaxPBES2Count=%d (DoS amplification cap; raise via jwe.WithMaxPBES2Count)`, count, maxCount) } salt, err := base64.DecodeString(saltB64) if err != nil { return nil, fmt.Errorf(`failed to b64-decode 'salt': %w`, err) } dec.KeySalt(salt) - dec.KeyCount(int(countFlt)) + dec.KeyCount(int(count)) } plaintext, err := dec.Decrypt(recipient, msg.cipherText, msg) @@ -548,7 +859,7 @@ func (dc *decryptContext) decryptContent(msg *Message, alg jwa.KeyEncryptionAlgo if v, ok := h2.Compression(); ok && v == jwa.Deflate() { buf, err := uncompress(plaintext, dc.maxDecompressBufferSize) if err != nil { - return nil, fmt.Errorf(`jwe.Derypt: failed to uncompress payload: %w`, err) + return nil, fmt.Errorf(`jwe.Decrypt: failed to uncompress payload: %w`, err) } plaintext = buf } @@ -565,6 +876,7 @@ type encryptContext struct { calg jwa.ContentEncryptionAlgorithm compression jwa.CompressionAlgorithm format int + pbes2Count int builders []*recipientBuilder protected Headers legacyHeaderMerging bool @@ -584,6 +896,7 @@ func freeEncryptContext(ec *encryptContext) *encryptContext { ec.calg = jwa.A256GCM() ec.compression = jwa.NoCompress() ec.format = fmtCompact + ec.pbes2Count = 0 ec.builders = ec.builders[:0] ec.protected = nil return ec @@ -591,6 +904,7 @@ func freeEncryptContext(ec *encryptContext) *encryptContext { func (ec *encryptContext) ProcessOptions(options []EncryptOption) error { ec.legacyHeaderMerging = true + ec.pbes2Count = int(pbes2Count.Load()) var mergeProtected bool var useRawCEK bool for _, option := range options { @@ -604,6 +918,9 @@ func (ec *encryptContext) ProcessOptions(options []EncryptOption) error { if !ok { return fmt.Errorf("jwe.encrypt: WithKey() option must be specified using jwa.KeyEncryptionAlgorithm (got %T)", wk.alg) } + if err := validateAlgorithmForKey(v, wk.key); err != nil { + return fmt.Errorf("jwe.WithKey: %w", err) + } if v == jwa.DIRECT() || v == jwa.ECDH_ES() { useRawCEK = true } @@ -612,6 +929,14 @@ func (ec *encryptContext) ProcessOptions(options []EncryptOption) error { key: wk.key, headers: wk.headers, }) + case identPBES2Count{}: + var v int + if err := option.Value(&v); err != nil { + return fmt.Errorf("jwe.encrypt: WithPBES2Count must be int: %w", err) + } + if v > 0 { + ec.pbes2Count = v + } case identContentEncryptionAlgorithm{}: var c jwa.ContentEncryptionAlgorithm if err := option.Value(&c); err != nil { @@ -671,7 +996,7 @@ func (ec *encryptContext) ProcessOptions(options []EncryptOption) error { if useRawCEK { if len(ec.builders) != 1 { - return fmt.Errorf(`multiple recipients for ECDH-ES/DIRECT mode supported`) + return fmt.Errorf(`multiple recipients for ECDH-ES/DIRECT mode are not supported`) } } @@ -714,10 +1039,14 @@ func freeHeaders(h Headers) Headers { var recipientPool = pool.New(NewRecipient, freeRecipient) func freeRecipient(r Recipient) Recipient { + // Return the recipient's headers to headerPool and install a fresh + // instance so the next recipientPool.Get() never hands out a + // pointer the caller may still hold a reference to. This is safe + // because WithPerRecipientHeaders clones the caller-supplied + // Headers, so anything we receive here is already library-owned. if h := r.Headers(); h != nil { - if c, ok := h.(interface{ clear() }); ok { - c.clear() - } + headerPool.Put(h) + _ = r.SetHeaders(headerPool.Get()) } if sr, ok := r.(*stdRecipient); ok { @@ -777,6 +1106,7 @@ func (ec *encryptContext) EncryptMessage(payload []byte, cek []byte) ([]byte, er defer recipientSlicePool.Put(recipients) for i, builder := range ec.builders { + builder.pbes2Count = ec.pbes2Count r := recipientPool.Get() defer recipientPool.Put(r) @@ -794,6 +1124,10 @@ func (ec *encryptContext) EncryptMessage(payload []byte, cek []byte) ([]byte, er } } + if len(cek) != contentcrypt.KeySize() { + return nil, fmt.Errorf(`content encryption key length %d does not match enc %q (expected %d bytes)`, len(cek), ec.calg.String(), contentcrypt.KeySize()) + } + if err := protected.Set(ContentEncryptionKey, ec.calg); err != nil { return nil, fmt.Errorf(`failed to set "enc" in protected header: %w`, err) } @@ -869,6 +1203,13 @@ func (ec *encryptContext) EncryptMessage(payload []byte, cek []byte) ([]byte, er return nil, fmt.Errorf(`failed to encrypt payload: %w`, err) } + // Fast path for compact serialization: assemble directly from + // pre-encoded headers and raw fields, avoiding the full Message + // construction and redundant header re-encoding that Compact() does. + if ec.format == fmtCompact { + return compactSerialize(aad, recipients[0].EncryptedKey(), iv, ciphertext, tag), nil + } + msg := msgPool.Get() defer msgPool.Put(msg) @@ -889,8 +1230,6 @@ func (ec *encryptContext) EncryptMessage(payload []byte, cek []byte) ([]byte, er } switch ec.format { - case fmtCompact: - return Compact(msg) case fmtJSON: return json.Marshal(msg) case fmtJSONPretty: @@ -904,11 +1243,11 @@ func (ec *encryptContext) EncryptMessage(payload []byte, cek []byte) ([]byte, er // payload (e.g. the key encryption algorithm and the corresponding // key to decrypt the JWE message) in its optional arguments. See // the examples and list of options that return a DecryptOption for possible -// values. Upon successful decryptiond returns the decrypted payload. +// values. Upon successful decryption returns the decrypted payload. // // The JWE message can be either compact or full JSON format. // -// When using `jwe.WithKeyEncryptionAlgorithm()`, you can pass a `jwa.KeyAlgorithm` +// When using `jwe.WithKey()`, you can pass a `jwa.KeyAlgorithm` // for convenience: this is mainly to allow you to directly pass the result of `(jwk.Key).Algorithm()`. // However, do note that while `(jwk.Key).Algorithm()` could very well contain key encryption // algorithms, it could also contain other types of values, such as _signature algorithms_. @@ -929,17 +1268,31 @@ func (ec *encryptContext) EncryptMessage(payload []byte, cek []byte) ([]byte, er // // jwe.Settings(jwe.WithMaxDecompressBufferSize(10*1024*1024)) // changes value globally // jwe.Decrypt(..., jwe.WithMaxDecompressBufferSize(250*1024)) // changes just for this call +// +// PBES2 amplification: PBES2 algorithms (PBES2-HS256+A128KW, etc.) +// derive the CEK via PBKDF2 with the iteration count taken from the +// JWE's `p2c` header. An attacker-controlled iteration count multiplied +// by `WithMaxRecipients` is the major CPU-amplification vector on the +// decrypt side. Bound it via `WithMaxPBES2Count` (default 1,000,000) +// and reject too-low counts via `WithMinPBES2Count` (default 1000; +// RFC 7518 §4.8.1.2 floor — note OWASP 2023 recommends ≥600,000 for +// production password-derived key material). Both options accept a +// `Settings()` global or a per-call value. func Decrypt(buf []byte, options ...DecryptOption) ([]byte, error) { dc := decryptContextPool.Get() defer decryptContextPool.Put(dc) if err := dc.ProcessOptions(options); err != nil { - return nil, decryptError{fmt.Errorf(`jwe.Decrypt: failed to process options: %w`, err)} + return nil, makeDecryptError(`failed to process options: %w`, err) } ret, err := dc.DecryptMessage(buf) if err != nil { - return nil, decryptError{fmt.Errorf(`jwe.Decrypt: %w`, err)} + // DecryptMessage already returns errors prefixed with + // "jwe.Decrypt:" — wrap as decryptError without adding a + // second prefix, otherwise multi-recipient errors carry + // the "jwe.Decrypt:" string multiple times. + return nil, decryptError{err} } return ret, nil } @@ -947,18 +1300,18 @@ func Decrypt(buf []byte, options ...DecryptOption) ([]byte, error) { // Parse parses the JWE message into a Message object. The JWE message // can be either compact or full JSON format. // -// Parse() currently does not take any options, but the API accepts it -// in anticipation of future addition. +// Bounding the input size is the caller's responsibility; this function +// trusts the caller-provided buf. See docs/13-input-size.md. func Parse(buf []byte, _ ...ParseOption) (*Message, error) { - return parseJSONOrCompact(buf, false) + return parseJSONOrCompact(buf, false, int(maxRecipients.Load())) } // errors are wrapped within this function, because we call it directly // from Decrypt as well. -func parseJSONOrCompact(buf []byte, storeProtectedHeaders bool) (*Message, error) { +func parseJSONOrCompact(buf []byte, storeProtectedHeaders bool, maxR int) (*Message, error) { buf = bytes.TrimSpace(buf) if len(buf) == 0 { - return nil, parseError{fmt.Errorf(`jwe.Parse: empty buffer`)} + return nil, makeParseError(`jwe.Parse`, `empty buffer`) } var msg *Message @@ -970,29 +1323,38 @@ func parseJSONOrCompact(buf []byte, storeProtectedHeaders bool) (*Message, error } if err != nil { - return nil, parseError{fmt.Errorf(`jwe.Parse: %w`, err)} + return nil, makeParseError(`jwe.Parse`, `%w`, err) + } + + if maxR > 0 && len(msg.recipients) > maxR { + return nil, makeParseError(`jwe.Parse`, `too many recipients in JWE message (%d > %d)`, len(msg.recipients), maxR) } + return msg, nil } // ParseString is the same as Parse, but takes a string. -func ParseString(s string) (*Message, error) { +func ParseString(s string, _ ...ParseOption) (*Message, error) { msg, err := Parse([]byte(s)) if err != nil { - return nil, parseError{fmt.Errorf(`jwe.ParseString: %w`, err)} + return nil, makeParseError(`jwe.ParseString`, `%w`, err) } return msg, nil } // ParseReader is the same as Parse, but takes an io.Reader. -func ParseReader(src io.Reader) (*Message, error) { +// +// Bounding the input size is the caller's responsibility: wrap src with +// [io.LimitReader] or [net/http.MaxBytesReader] before passing it in. See +// docs/13-input-size.md for the rationale. +func ParseReader(src io.Reader, _ ...ParseOption) (*Message, error) { buf, err := io.ReadAll(src) if err != nil { - return nil, parseError{fmt.Errorf(`jwe.ParseReader: failed to read from io.Reader: %w`, err)} + return nil, makeParseError(`jwe.ParseReader`, `failed to read from io.Reader: %w`, err) } msg, err := Parse(buf) if err != nil { - return nil, parseError{fmt.Errorf(`jwe.ParseReader: %w`, err)} + return nil, makeParseError(`jwe.ParseReader`, `%w`, err) } return msg, nil } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_asymmetric.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_asymmetric.go index ac07993176..a8fa825a57 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_asymmetric.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_asymmetric.go @@ -113,8 +113,10 @@ func KeyDecryptRSA15(_, enckey []byte, privkeyif any, keysize int) ([]byte, erro return nil, fmt.Errorf(`jwebb.KeyDecryptRSA15: %w`, err) } - // Perform some input validation. - expectedlen := privkey.PublicKey.N.BitLen() / tokens.BitsPerByte + // Perform some input validation. Use privkey.Size() which applies + // ceiling division on the modulus bit length, avoiding silent truncation + // if N.BitLen() is not a multiple of 8. + expectedlen := privkey.Size() if expectedlen != len(enckey) { // Input size is incorrect, the encrypted payload should always match // the size of the public modulus (e.g. using a 2048 bit key will diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_symmetric.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_symmetric.go index c09e30a34e..cb98801865 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_symmetric.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_symmetric.go @@ -78,8 +78,11 @@ func KeyDecryptAESGCMKW(recipientKey, _ []byte, _ string, sharedkey []byte, iv [ return nil, fmt.Errorf(`failed to create new GCM wrap: %w`, err) } - // Combine recipient key and tag for GCM decryption - ciphertext := recipientKey[:] + // Combine recipient key and tag for GCM decryption. Allocate a fresh + // buffer so we never alias into recipientKey's backing array, which + // is owned by the parsed message. + ciphertext := make([]byte, 0, len(recipientKey)+len(tag)) + ciphertext = append(ciphertext, recipientKey...) ciphertext = append(ciphertext, tag...) jek, err := aesgcm.Open(nil, iv, ciphertext, nil) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_asymmetric.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_asymmetric.go index 6f008173c8..4dd6274863 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_asymmetric.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_asymmetric.go @@ -65,9 +65,9 @@ func generateECDHESKeyECDSA(alg string, calg string, keysize uint32, pubkey *ecd } // generateECDHESKeyX25519 generates the key material for X25519 keys using ECDH-ES -func generateECDHESKeyX25519(alg string, calg string, keysize uint32, pubkey *ecdh.PublicKey) (keygen.ByteWithECPublicKey, error) { +func generateECDHESKeyX25519(alg string, calg string, keysize uint32, pubkey *ecdh.PublicKey, apu, apv []byte) (keygen.ByteWithECPublicKey, error) { // Generate the key directly - kg, err := keygen.X25519(alg, calg, int(keysize), pubkey) + kg, err := keygen.X25519(alg, calg, int(keysize), pubkey, apu, apv) if err != nil { return keygen.ByteWithECPublicKey{}, fmt.Errorf(`failed to generate X25519 key: %w`, err) } @@ -103,8 +103,8 @@ func KeyEncryptECDHESKeyWrapECDSA(cek []byte, alg string, apu, apv []byte, pubke } // KeyEncryptECDHESKeyWrapX25519 encrypts the CEK using ECDH-ES with key wrapping for X25519 keys -func KeyEncryptECDHESKeyWrapX25519(cek []byte, alg string, _ []byte, _ []byte, pubkey *ecdh.PublicKey, keysize uint32, calg string) (keygen.ByteSource, error) { - bwpk, err := generateECDHESKeyX25519(alg, calg, keysize, pubkey) +func KeyEncryptECDHESKeyWrapX25519(cek []byte, alg string, apu []byte, apv []byte, pubkey *ecdh.PublicKey, keysize uint32, calg string) (keygen.ByteSource, error) { + bwpk, err := generateECDHESKeyX25519(alg, calg, keysize, pubkey, apu, apv) if err != nil { return nil, err } @@ -136,8 +136,8 @@ func KeyEncryptECDHESECDSA(_ []byte, alg string, apu, apv []byte, pubkey *ecdsa. } // KeyEncryptECDHESX25519 encrypts using ECDH-ES direct (no key wrapping) for X25519 keys -func KeyEncryptECDHESX25519(_ []byte, alg string, _, _ []byte, pubkey *ecdh.PublicKey, keysize uint32, calg string) (keygen.ByteSource, error) { - bwpk, err := generateECDHESKeyX25519(alg, calg, keysize, pubkey) +func KeyEncryptECDHESX25519(_ []byte, alg string, apu, apv []byte, pubkey *ecdh.PublicKey, keysize uint32, calg string) (keygen.ByteSource, error) { + bwpk, err := generateECDHESKeyX25519(alg, calg, keysize, pubkey, apu, apv) if err != nil { return nil, err } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_symmetric.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_symmetric.go index d489aaba28..e2ee2766fa 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_symmetric.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_symmetric.go @@ -35,8 +35,11 @@ func KeyEncryptDirect(_ []byte, _ string, sharedkey []byte) (keygen.ByteSource, return keygen.ByteKey(sharedkey), nil } -// KeyEncryptPBES2 encrypts the CEK using PBES2 password-based encryption -func KeyEncryptPBES2(cek []byte, alg string, password []byte) (keygen.ByteSource, error) { +// KeyEncryptPBES2 encrypts the CEK using PBES2 password-based encryption. +// count is the PBKDF2 iteration count. If count <= 0, tokens.PBES2DefaultIterations +// is used as a safety fallback; public callers go through jwe.Encrypt / jwe.Settings +// and always provide a positive value via the WithPBES2Count option. +func KeyEncryptPBES2(cek []byte, alg string, password []byte, count int) (keygen.ByteSource, error) { var hashFunc func() hash.Hash var keylen int @@ -54,7 +57,9 @@ func KeyEncryptPBES2(cek []byte, alg string, password []byte) (keygen.ByteSource return nil, fmt.Errorf(`unsupported PBES2 algorithm: %s`, alg) } - count := tokens.PBES2DefaultIterations + if count <= 0 { + count = tokens.PBES2DefaultIterations + } salt := make([]byte, keylen) _, err := io.ReadFull(rand.Reader, salt) if err != nil { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/keywrap.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/keywrap.go index 0792d6cb8e..b48bcec5f3 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/keywrap.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/keywrap.go @@ -13,6 +13,9 @@ import ( var keywrapDefaultIV = []byte{0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6} func Wrap(kek cipher.Block, cek []byte) ([]byte, error) { + if len(cek) < tokens.KeywrapChunkLen { + return nil, fmt.Errorf(`keywrap input must be at least %d bytes`, tokens.KeywrapChunkLen) + } if len(cek)%tokens.KeywrapBlockSize != 0 { return nil, fmt.Errorf(`keywrap input must be %d byte blocks`, tokens.KeywrapBlockSize) } @@ -25,15 +28,11 @@ func Wrap(kek cipher.Block, cek []byte) ([]byte, error) { copy(r[i], cek[i*tokens.KeywrapChunkLen:]) } - buffer := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen * 2) + buffer := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen * 2)[:tokens.KeywrapChunkLen*2] defer pool.ByteSlice().Put(buffer) - // the byte slice has the capacity, but len is 0 - buffer = buffer[:tokens.KeywrapChunkLen*2] - tBytes := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen) + tBytes := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen)[:tokens.KeywrapChunkLen] defer pool.ByteSlice().Put(tBytes) - // the byte slice has the capacity, but len is 0 - tBytes = tBytes[:tokens.KeywrapChunkLen] copy(buffer, keywrapDefaultIV) @@ -60,6 +59,9 @@ func Wrap(kek cipher.Block, cek []byte) ([]byte, error) { } func Unwrap(block cipher.Block, ciphertxt []byte) ([]byte, error) { + if len(ciphertxt) < 2*tokens.KeywrapChunkLen { + return nil, fmt.Errorf(`keyunwrap input must be at least %d bytes`, 2*tokens.KeywrapChunkLen) + } if len(ciphertxt)%tokens.KeywrapChunkLen != 0 { return nil, fmt.Errorf(`keyunwrap input must be %d byte blocks`, tokens.KeywrapChunkLen) } @@ -72,15 +74,11 @@ func Unwrap(block cipher.Block, ciphertxt []byte) ([]byte, error) { copy(r[i], ciphertxt[(i+1)*tokens.KeywrapChunkLen:]) } - buffer := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen * 2) + buffer := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen * 2)[:tokens.KeywrapChunkLen*2] defer pool.ByteSlice().Put(buffer) - // the byte slice has the capacity, but len is 0 - buffer = buffer[:tokens.KeywrapChunkLen*2] - tBytes := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen) + tBytes := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen)[:tokens.KeywrapChunkLen] defer pool.ByteSlice().Put(tBytes) - // the byte slice has the capacity, but len is 0 - tBytes = tBytes[:tokens.KeywrapChunkLen] copy(buffer[:tokens.KeywrapChunkLen], ciphertxt[:tokens.KeywrapChunkLen]) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/key_provider.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/key_provider.go index 05adc04517..81bb5b6ec7 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/key_provider.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/key_provider.go @@ -2,6 +2,7 @@ package jwe import ( "context" + "errors" "fmt" "sync" @@ -106,10 +107,11 @@ type keySetProvider struct { requireKid bool } -func (kp *keySetProvider) selectKey(sink KeySink, key jwk.Key, _ Recipient, _ *Message) error { +func (kp *keySetProvider) selectKey(sink KeySink, key jwk.Key, r Recipient, msg *Message) error { if usage, ok := key.KeyUsage(); ok { if usage != "" && usage != jwk.ForEncryption.String() { - return nil + kid, _ := key.KeyID() + return fmt.Errorf(`key %q has key_use=%q (expected %q for encryption)`, kid, usage, jwk.ForEncryption.String()) } } @@ -123,7 +125,30 @@ func (kp *keySetProvider) selectKey(sink KeySink, key jwk.Key, _ Recipient, _ *M return nil } - return nil + // The JWK has no "alg" — common for IdP-published encryption keys. + // Fall back to the recipient's declared "alg" (per-recipient header, + // then protected header), matching the preference order used when + // jwe.Decrypt verifies the chosen key's algorithm against the message. + // jwe.Decrypt re-checks agreement before use, so trusting the header + // alg here does not widen the attack surface. + for _, hdr := range []Headers{r.Headers(), msg.ProtectedHeaders()} { + if hdr == nil { + continue + } + v, ok := hdr.Algorithm() + if !ok { + continue + } + kalg, ok := jwa.LookupKeyEncryptionAlgorithm(v.String()) + if !ok { + continue + } + sink.Key(kalg, key) + return nil + } + + kid, _ := key.KeyID() + return fmt.Errorf(`key %q in set has no "alg" field and the JWE message has no recoverable "alg" header; declare "alg" on the JWK or use jwe.WithKey(alg, key) directly`, kid) } func (kp *keySetProvider) FetchKeys(_ context.Context, sink KeySink, r Recipient, msg *Message) error { @@ -144,11 +169,22 @@ func (kp *keySetProvider) FetchKeys(_ context.Context, sink KeySink, r Recipient return kp.selectKey(sink, key, r, msg) } + // Collect per-key errors and surface them via errors.Join when + // nothing produced a usable (alg, key) pair. Without this, a + // caller debugging "why didn't my keyset match" got no signal. + var perKeyErrs []error + var emitted bool for i := range kp.set.Len() { key, _ := kp.set.Key(i) - if err := kp.selectKey(sink, key, r, msg); err != nil { + err := kp.selectKey(sink, key, r, msg) + if err != nil { + perKeyErrs = append(perKeyErrs, err) continue } + emitted = true + } + if !emitted && len(perKeyErrs) > 0 { + return fmt.Errorf(`failed to select any usable key from set of %d (no key produced a usable (alg, key) pair): %w`, kp.set.Len(), errors.Join(perKeyErrs...)) } return nil } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/message.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/message.go index 7aad833f26..22c6e6660a 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/message.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/message.go @@ -1,6 +1,7 @@ package jwe import ( + "bytes" "fmt" "sort" "strings" @@ -71,8 +72,7 @@ func (r *stdRecipient) MarshalJSON() ([]byte, error) { buf.WriteString(base64.EncodeToString(r.encryptedKey)) buf.WriteString(`"}`) - ret := make([]byte, buf.Len()) - copy(ret, buf.Bytes()) + ret := bytes.Clone(buf.Bytes()) return ret, nil } @@ -248,8 +248,7 @@ func (m *Message) MarshalJSON() ([]byte, error) { if aad := m.AuthenticatedData(); len(aad) > 0 { aad = base64.Encode(aad) if encodedProtectedHeaders != nil { - tmp := append(encodedProtectedHeaders, tokens.Period) - aad = append(tmp, aad...) + aad = concatAAD(encodedProtectedHeaders, aad) } buf.Reset() @@ -344,8 +343,7 @@ func (m *Message) MarshalJSON() ([]byte, error) { } fmt.Fprintf(buf, `}`) - ret := make([]byte, buf.Len()) - copy(ret, buf.Bytes()) + ret := bytes.Clone(buf.Bytes()) return ret, nil } @@ -422,29 +420,36 @@ func (m *Message) UnmarshalJSON(buf []byte) error { m.authenticatedData = v } - if src := proxy.CipherText; len(src) > 0 { - v, err := base64.DecodeString(src) - if err != nil { - return fmt.Errorf(`failed to decode "ciphertext": %w`, err) - } - m.cipherText = v + // RFC 7516 §7.2: "ciphertext", "iv", and "tag" MUST be present and + // non-empty for any AEAD-protected JWE. Reject missing/empty values + // here so that a zero-length authentication tag cannot reach the + // AEAD verification code path. + if len(proxy.CipherText) == 0 { + return fmt.Errorf(`missing or empty "ciphertext" field`) + } + ctbuf, err := base64.DecodeString(proxy.CipherText) + if err != nil { + return fmt.Errorf(`failed to decode "ciphertext": %w`, err) } + m.cipherText = ctbuf - if src := proxy.InitializationVector; len(src) > 0 { - v, err := base64.DecodeString(src) - if err != nil { - return fmt.Errorf(`failed to decode "iv": %w`, err) - } - m.initializationVector = v + if len(proxy.InitializationVector) == 0 { + return fmt.Errorf(`missing or empty "iv" field`) } + ivbuf, err := base64.DecodeString(proxy.InitializationVector) + if err != nil { + return fmt.Errorf(`failed to decode "iv": %w`, err) + } + m.initializationVector = ivbuf - if src := proxy.Tag; len(src) > 0 { - v, err := base64.DecodeString(src) - if err != nil { - return fmt.Errorf(`failed to decode "tag": %w`, err) - } - m.tag = v + if len(proxy.Tag) == 0 { + return fmt.Errorf(`missing or empty "tag" field`) } + tagbuf, err := base64.DecodeString(proxy.Tag) + if err != nil { + return fmt.Errorf(`failed to decode "tag": %w`, err) + } + m.tag = tagbuf m.protectedHeaders = h if m.storeProtectedHeaders { @@ -554,7 +559,26 @@ func Compact(m *Message, _ ...CompactOption) ([]byte, error) { buf.WriteByte(tokens.Period) buf.Write(tag) - result := make([]byte, buf.Len()) - copy(result, buf.Bytes()) + result := bytes.Clone(buf.Bytes()) return result, nil } + +// compactSerialize assembles a JWE compact serialization from pre-encoded +// protected headers (aad) and raw binary fields. This avoids the redundant +// Clone/Merge/Encode cycle that Compact() performs. +func compactSerialize(aad, encryptedKey, iv, ciphertext, tag []byte) []byte { + size := len(aad) + base64.EncodedLen(len(encryptedKey)) + base64.EncodedLen(len(iv)) + base64.EncodedLen(len(ciphertext)) + base64.EncodedLen(len(tag)) + 4 + buf := make([]byte, 0, size) + + buf = append(buf, aad...) + buf = append(buf, tokens.Period) + buf = base64.AppendEncode(buf, encryptedKey) + buf = append(buf, tokens.Period) + buf = base64.AppendEncode(buf, iv) + buf = append(buf, tokens.Period) + buf = base64.AppendEncode(buf, ciphertext) + buf = append(buf, tokens.Period) + buf = base64.AppendEncode(buf, tag) + + return buf +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.go index 0437ea8733..ab356f5d81 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.go @@ -6,6 +6,66 @@ import ( "github.com/lestrrat-go/option/v2" ) +type identCritExtension struct{} +type identDisabledKeyAlgorithms struct{} + +// WithDisabledKeyAlgorithms returns a process-global option for jwe.Settings() +// that refuses the named key encryption algorithms in both directions. After +// the call returns, jwe.Encrypt() will not produce a recipient using any +// listed algorithm, and jwe.Decrypt() will reject any recipient whose "alg" +// is in the list, before any cryptographic work runs. The check fires per +// recipient: a multi-recipient JWE is rejected as soon as a disabled "alg" +// is seen on any recipient. +// +// The list is replaced (not unioned) on each Settings() call. To clear the +// disabled set, call jwe.Settings(jwe.WithDisabledKeyAlgorithms()) with no +// arguments. +// +// This is a deployment-time policy hook for the canonical "disable RSA1_5" +// case (RFC 8725 §3.1) and similar legacy-algorithm bans. The jwa package +// does not unregister these algorithms — keeping them registered preserves +// header parsing for diagnostic logs, while this option blocks any actual +// crypto use. +func WithDisabledKeyAlgorithms(algorithms ...jwa.KeyEncryptionAlgorithm) GlobalOption { + return &globalOption{option.New(identDisabledKeyAlgorithms{}, algorithms)} +} + +// WithCritExtension declares that the caller understands and will process +// the named "crit" (Critical) header parameter extension(s) per RFC 7516 +// Section 4.1.13 (which references RFC 7515 Section 4.1.11). The option +// is variadic and accumulating: a single call may register any number +// of extension names, and the option may be passed multiple times to add +// more. +// +// This option only takes effect when jwe.WithCritValidation(true) is +// also passed. With validation enabled, jwe.Decrypt() rejects any JWE +// whose protected header lists a "crit" extension that has not been +// declared via this option, satisfying the RFC's requirement that +// recipients MUST reject any "crit" extension they do not understand. +// +// IMPORTANT: declaring an extension here is a promise to the library +// that the caller knows what the extension means and will perform any +// validation, side effect, or policy enforcement the extension requires +// AFTER jwe.Decrypt() returns successfully. The library cannot inspect +// or enforce the semantics of an extension; it only checks that every +// "crit" entry in the message has been declared. If you register an +// extension and then forget to act on its value, you have effectively +// disabled the protection the producer was trying to obtain by listing +// the extension as critical. +// +// Concretely, the post-decrypt code path for a declared extension must: +// +// 1. Read the value of the named header from the decrypted message. +// 2. Apply whatever check or transformation the extension specifies +// (e.g. for an "x-tenant-binding" extension, refuse to act on the +// payload unless the binding matches the current tenant). +// 3. Treat any failure of that check as a decryption failure for +// the application's purposes, even though jwe.Decrypt() returned +// no error. +func WithCritExtension(names ...string) DecryptOption { + return &decryptOption{option.New(identCritExtension{}, names)} +} + // WithProtectedHeaders is used to specify contents of the protected header. // Some fields such as "enc" and "zip" will be overwritten when encryption is // performed. @@ -35,12 +95,22 @@ func (*withKeySuboption) withKeySuboption() {} // WithPerRecipientHeaders is used to pass header values for each recipient. // Note that these headers are by definition _unprotected_. +// +// The supplied Headers is cloned before being stored in the option, so the +// caller retains exclusive ownership of the original instance and the +// library never mutates or pools it. func WithPerRecipientHeaders(hdr Headers) WithKeySuboption { + if hdr != nil { + if cloned, err := hdr.Clone(); err == nil { + hdr = cloned + } + } return &withKeySuboption{option.New(identPerRecipientHeaders{}, hdr)} } // WithKey is used to pass a static algorithm/key pair to either `jwe.Encrypt()` or `jwe.Decrypt()`. -// either a raw key or `jwk.Key` may be passed as `key`. +// Either a raw key or `jwk.Key` may be passed as `key`. If `key` is a `jwk.Key`, +// it must export to one of the raw key types described below. // // The `alg` parameter is the identifier for the key encryption algorithm that should be used. // It is of type `jwa.KeyAlgorithm` but in reality you can only pass `jwa.KeyEncryptionAlgorithm` @@ -48,6 +118,30 @@ func WithPerRecipientHeaders(hdr Headers) WithKeySuboption { // passed to the option. If you specify other algorithm types such as `jwa.SignatureAlgorithm`, // then you will get an error when `jwe.Encrypt()` or `jwe.Decrypt()` is executed. // +// Built-in algorithm/key pairs are: +// +// - `jwa.RSA1_5()` and `jwa.RSA_OAEP*()`: `*rsa.PublicKey` for `jwe.Encrypt()` +// and the matching `*rsa.PrivateKey` for `jwe.Decrypt()` +// - `jwa.A128KW()`, `jwa.A192KW()`, `jwa.A256KW()`, `jwa.A128GCMKW()`, +// `jwa.A192GCMKW()`, and `jwa.A256GCMKW()`: shared symmetric key bytes of +// the size required by the selected algorithm +// - `jwa.DIRECT()`: shared symmetric key bytes used as the CEK. The key length +// must match the selected `enc`, and DIRECT supports only a single recipient +// - `jwa.ECDH_ES()` and `jwa.ECDH_ES_A*KW()`: recipient public key for +// `jwe.Encrypt()` and the matching private key for `jwe.Decrypt()`. Built-in +// support accepts `*ecdsa.PublicKey`, `*ecdsa.PrivateKey`, +// `*ecdh.PublicKey`, and `*ecdh.PrivateKey`; `jwa.ECDH_ES()` also supports +// only a single recipient +// - `jwa.PBES2_*()`: password bytes +// +// `jwa.RSA1_5()` is supported only for interoperability with legacy peers. +// New applications should prefer an RSA-OAEP variant such as +// `jwa.RSA_OAEP_256()` because PKCS#1 v1.5 decryption is exposed to +// Bleichenbacher-style oracle attacks. +// +// Additional algorithms may be added by extension packages, but the key must +// still match the contract for the selected `alg`. +// // Unlike `jwe.WithKeySet()`, the `kid` field does not need to match for the key // to be tried. func WithKey(alg jwa.KeyAlgorithm, key any, options ...WithKeySuboption) EncryptDecryptOption { @@ -68,6 +162,24 @@ func WithKey(alg jwa.KeyAlgorithm, key any, options ...WithKeySuboption) Encrypt })} } +// WithKeySet specifies a JWKS (jwk.Set) to use for decryption. The +// recipient's `kid` header selects a key from the set, and the key's +// `alg` (or, when the JWK lacks `alg`, the recipient's declared `alg`) +// drives the decrypt-time dispatch. +// +// By default WithKeySet requires the JWE to carry a `kid` header that +// matches a key in the set. Pass `WithRequireKid(false)` to fall back +// to trying every key in the set (slower, looser; intended for legacy +// peers that don't emit `kid`). Per-key errors from the set are +// surfaced via `errors.Join` when nothing matched, so a caller +// debugging "why didn't my keyset match" sees the per-key reasons. +// +// Security note: the recipient's per-recipient header is unprotected. +// When the selected JWK has no `alg`, the keyset provider falls back +// to the per-recipient `alg`, then the protected header's `alg`. +// `jwe.Decrypt` re-checks `alg` against the integrity-protected +// protected header before any cryptographic call (RFC 7516 §7.2.1 +// disjointness). func WithKeySet(set jwk.Set, options ...WithKeySetSuboption) DecryptOption { requireKid := true for _, option := range options { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.yaml b/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.yaml index 359d80944d..428e73e2b0 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.yaml +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.yaml @@ -10,6 +10,12 @@ interfaces: methods: - globalOption - decryptOption + - name: GlobalEncryptOption + comment: | + GlobalEncryptOption describes options that changes global settings and for each call of the `jwe.Encrypt` function + methods: + - globalOption + - encryptOption - name: CompactOption comment: | CompactOption describes options that can be passed to `jwe.Compact` @@ -37,6 +43,13 @@ interfaces: - readFileOption comment: | ReadFileOption is a type of `Option` that can be passed to `jwe.Parse` + - name: GlobalParseOption + methods: + - globalOption + - readFileOption + comment: | + GlobalParseOption describes an Option that can be passed to `jwe.Settings()` + and `jwe.ReadFile()`. - name: ReadFileOption comment: | ReadFileOption is a type of `Option` that can be passed to `jwe.ReadFile` @@ -76,6 +89,17 @@ options: a payload using `jwe.Encrypt` (Yes, we know it can only be "" or "DEF", but the way the specification is written it could allow for more options, and therefore this option takes an argument) + + Compression can leak information about the plaintext through message + length, so enable it only when you understand that tradeoff. + + This library does not enforce an encrypt-side plaintext size limit before + compression. Callers that accept untrusted or arbitrarily large payloads + must bound the input size before calling `jwe.Encrypt` with this option. + + Recipients may independently reject compressed messages whose + decompressed payload exceeds their `jwe.WithMaxDecompressBufferSize` + setting. - ident: ContentEncryptionAlgorithm interface: EncryptOption option_name: WithContentEncryption @@ -140,14 +164,77 @@ options: This option is currently considered EXPERIMENTAL, and is subject to future changes across minor/micro versions. - ident: MaxPBES2Count - interface: GlobalOption + interface: GlobalDecryptOption argument_type: int comment: | WithMaxPBES2Count specifies the maximum number of PBES2 iterations to use when decrypting a message. If not specified, the default value of 10,000 is used. - This option has a global effect. + The cap is applied per recipient. RFC 7516 §5.3 allows each + recipient of a JSON-serialized JWE to carry its own "p2c" iteration + count in its per-recipient unprotected header, and jwe.Decrypt + honors that. For a JWE with N recipients, the worst-case PBKDF2 + cost is therefore on the order of N * MaxPBES2Count iterations per + decrypt attempt. When accepting multi-recipient JSON JWEs from + untrusted senders, also clamp jwe.WithMaxRecipients. + + This option can be used for `jwe.Settings()`, which changes the behavior + globally, or for `jwe.Decrypt()`, which changes the behavior for that + specific call. + - ident: MinPBES2Count + interface: GlobalDecryptOption + argument_type: int + comment: | + WithMinPBES2Count specifies the minimum number of PBES2 iterations + to accept when decrypting a message. If not specified, the default + value of 1,000 is used (RFC 7518 §4.8.1.2 floor). Set to 0 to + disable the minimum check (NOT RECOMMENDED for untrusted issuers + — without a floor, recipients accept arbitrarily-weak password- + derived keys and silently spend the producer-chosen amount of + crypto work to verify them). + + Threat model: a malicious or careless issuer signs a PBES2-wrapped + JWE with a very low p2c (e.g. 100) so they spend almost no CPU on + their side, while the recipient happily derives the wrap key with + the same low iteration count. The result is an authenticated + message whose key-derivation strength is well below industry + practice (OWASP 2023 recommends ≥600,000 PBKDF2-SHA256 iterations + for password-derived keys; the RFC floor of 1,000 is far below + that). For receiver-side hardening against under-iteration, raise + WithMinPBES2Count above 1,000. + + This option can be used for `jwe.Settings()`, which changes the behavior + globally, or for `jwe.Decrypt()`, which changes the behavior for that + specific call. + - ident: PBES2Count + interface: GlobalEncryptOption + argument_type: int + comment: | + WithPBES2Count specifies the number of PBKDF2 iterations to use when + encrypting a key with the PBES2 family of algorithms. If not specified, + the default value of 10,000 is used. Modern guidance (OWASP 2023) + recommends 600,000 or more for PBKDF2-HMAC-SHA256. + + This option only affects encryption. Iteration counts on incoming + messages are validated separately on decrypt via WithMinPBES2Count + and WithMaxPBES2Count. + + This option can be used for `jwe.Settings()`, which changes the behavior + globally, or for `jwe.Encrypt()`, which changes the behavior for that + specific call. + - ident: MaxRecipients + interface: GlobalDecryptOption + argument_type: int + comment: | + WithMaxRecipients specifies the maximum number of recipients allowed + in a JWE message. If a JWE message contains more recipients than this + value, parsing or decryption will return an error. The default value + is 100. + + This option can be used for `jwe.Settings()`, which changes the behavior + globally, or for `jwe.Decrypt()`, which changes the behavior for that + specific call. - ident: MaxDecompressBufferSize interface: GlobalDecryptOption argument_type: int64 @@ -157,6 +244,12 @@ options: exceeds this amount when decompressed, jwe.Decrypt will return an error. The default value is 10MB. + A non-positive value rejects every compressed JWE: the cap fires on + the first byte of inflated output, so any "zip"-compressed message + fails with an exceeds-cap error before any payload is delivered. Use + this when the deployment refuses to accept compressed JWEs at all. + Pass an explicit positive cap when compressed payloads are expected. + This option can be used for `jwe.Settings()`, which changes the behavior globally, or for `jwe.Decrypt()`, which changes the behavior for that specific call. @@ -170,6 +263,39 @@ options: In v2, this option was called MaxBufferSize. This option has a global effect. + - ident: CritValidation + interface: DecryptOption + argument_type: bool + comment: | + WithCritValidation enables RFC 7516 Section 4.1.13 (via RFC 7515 + Section 4.1.11) validation of the "crit" (Critical) header parameter + during decryption. The default is false, matching the behavior of + v3.0.13 and earlier (the "crit" header is silently ignored). + + When enabled, jwe.Decrypt() will reject any JWE whose protected + header lists "crit" entries that the recipient has not declared + support for via jwe.WithCritExtension(). It will also reject + structurally invalid "crit" lists: empty arrays, duplicate names, + empty extension names, names of standard JOSE/JWE header parameters, + and names that do not appear as header parameters in the protected + header. + + Per RFC 7516 Section 4.1.13 (referencing RFC 7515 Section 4.1.11), + recipients MUST reject a JWE whose "crit" list names extensions + they do not understand. Enabling this option together with one or + more jwe.WithCritExtension() calls is the only way to satisfy that + requirement with this library. + + IMPORTANT: enabling this option makes the library check that every + "crit" entry has been declared via jwe.WithCritExtension(), but the + library cannot perform the actual extension-specific processing on + your behalf. After jwe.Decrypt() returns successfully, your code + MUST read each declared extension header and apply whatever check + or side effect the extension semantics demand. If you declare an + extension and then forget to act on its value, you have defeated + the protection the producer was trying to obtain by marking that + extension critical. See the documentation on jwe.WithCritExtension + for details. - ident: LegacyHeaderMerging interface: EncryptOption argument_type: bool @@ -207,4 +333,4 @@ options: merged into the protected headers). In future versions, the new behavior will be the default. New users are - encouraged to set this option to `false` now to avoid future issues. \ No newline at end of file + encouraged to set this option to `false` now to avoid future issues. diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/options_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/options_gen.go index 2d28eecb44..54962c5d04 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/options_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/options_gen.go @@ -78,6 +78,21 @@ func (*globalDecryptOption) globalOption() {} func (*globalDecryptOption) decryptOption() {} +// GlobalEncryptOption describes options that changes global settings and for each call of the `jwe.Encrypt` function +type GlobalEncryptOption interface { + Option + globalOption() + encryptOption() +} + +type globalEncryptOption struct { + Option +} + +func (*globalEncryptOption) globalOption() {} + +func (*globalEncryptOption) encryptOption() {} + // GlobalOption describes options that changes global settings for this package type GlobalOption interface { Option @@ -90,6 +105,22 @@ type globalOption struct { func (*globalOption) globalOption() {} +// GlobalParseOption describes an Option that can be passed to `jwe.Settings()` +// and `jwe.ReadFile()`. +type GlobalParseOption interface { + Option + globalOption() + readFileOption() +} + +type globalParseOption struct { + Option +} + +func (*globalParseOption) globalOption() {} + +func (*globalParseOption) readFileOption() {} + // ReadFileOption is a type of `Option` that can be passed to `jwe.Parse` type ParseOption interface { Option @@ -143,6 +174,7 @@ type identCEK struct{} type identCompress struct{} type identContentEncryptionAlgorithm struct{} type identContext struct{} +type identCritValidation struct{} type identFS struct{} type identKey struct{} type identKeyProvider struct{} @@ -150,8 +182,11 @@ type identKeyUsed struct{} type identLegacyHeaderMerging struct{} type identMaxDecompressBufferSize struct{} type identMaxPBES2Count struct{} +type identMaxRecipients struct{} type identMergeProtectedHeaders struct{} type identMessage struct{} +type identMinPBES2Count struct{} +type identPBES2Count struct{} type identPerRecipientHeaders struct{} type identPretty struct{} type identProtectedHeaders struct{} @@ -178,6 +213,10 @@ func (identContext) String() string { return "WithContext" } +func (identCritValidation) String() string { + return "WithCritValidation" +} + func (identFS) String() string { return "WithFS" } @@ -206,6 +245,10 @@ func (identMaxPBES2Count) String() string { return "WithMaxPBES2Count" } +func (identMaxRecipients) String() string { + return "WithMaxRecipients" +} + func (identMergeProtectedHeaders) String() string { return "WithMergeProtectedHeaders" } @@ -214,6 +257,14 @@ func (identMessage) String() string { return "WithMessage" } +func (identMinPBES2Count) String() string { + return "WithMinPBES2Count" +} + +func (identPBES2Count) String() string { + return "WithPBES2Count" +} + func (identPerRecipientHeaders) String() string { return "WithPerRecipientHeaders" } @@ -258,6 +309,17 @@ func WithCEK(v *[]byte) DecryptOption { // a payload using `jwe.Encrypt` (Yes, we know it can only be "" or "DEF", // but the way the specification is written it could allow for more options, // and therefore this option takes an argument) +// +// Compression can leak information about the plaintext through message +// length, so enable it only when you understand that tradeoff. +// +// This library does not enforce an encrypt-side plaintext size limit before +// compression. Callers that accept untrusted or arbitrarily large payloads +// must bound the input size before calling `jwe.Encrypt` with this option. +// +// Recipients may independently reject compressed messages whose +// decompressed payload exceeds their `jwe.WithMaxDecompressBufferSize` +// setting. func WithCompress(v jwa.CompressionAlgorithm) EncryptOption { return &encryptOption{option.New(identCompress{}, v)} } @@ -274,6 +336,39 @@ func WithContext(v context.Context) DecryptOption { return &decryptOption{option.New(identContext{}, v)} } +// WithCritValidation enables RFC 7516 Section 4.1.13 (via RFC 7515 +// Section 4.1.11) validation of the "crit" (Critical) header parameter +// during decryption. The default is false, matching the behavior of +// v3.0.13 and earlier (the "crit" header is silently ignored). +// +// When enabled, jwe.Decrypt() will reject any JWE whose protected +// header lists "crit" entries that the recipient has not declared +// support for via jwe.WithCritExtension(). It will also reject +// structurally invalid "crit" lists: empty arrays, duplicate names, +// empty extension names, names of standard JOSE/JWE header parameters, +// and names that do not appear as header parameters in the protected +// header. +// +// Per RFC 7516 Section 4.1.13 (referencing RFC 7515 Section 4.1.11), +// recipients MUST reject a JWE whose "crit" list names extensions +// they do not understand. Enabling this option together with one or +// more jwe.WithCritExtension() calls is the only way to satisfy that +// requirement with this library. +// +// IMPORTANT: enabling this option makes the library check that every +// "crit" entry has been declared via jwe.WithCritExtension(), but the +// library cannot perform the actual extension-specific processing on +// your behalf. After jwe.Decrypt() returns successfully, your code +// MUST read each declared extension header and apply whatever check +// or side effect the extension semantics demand. If you declare an +// extension and then forget to act on its value, you have defeated +// the protection the producer was trying to obtain by marking that +// extension critical. See the documentation on jwe.WithCritExtension +// for details. +func WithCritValidation(v bool) DecryptOption { + return &decryptOption{option.New(identCritValidation{}, v)} +} + // WithFS specifies the source `fs.FS` object to read the file from. func WithFS(v fs.FS) ReadFileOption { return &readFileOption{option.New(identFS{}, v)} @@ -339,6 +434,12 @@ func WithLegacyHeaderMerging(v bool) EncryptOption { // exceeds this amount when decompressed, jwe.Decrypt will return an error. // The default value is 10MB. // +// A non-positive value rejects every compressed JWE: the cap fires on +// the first byte of inflated output, so any "zip"-compressed message +// fails with an exceeds-cap error before any payload is delivered. Use +// this when the deployment refuses to accept compressed JWEs at all. +// Pass an explicit positive cap when compressed payloads are expected. +// // This option can be used for `jwe.Settings()`, which changes the behavior // globally, or for `jwe.Decrypt()`, which changes the behavior for that // specific call. @@ -350,9 +451,31 @@ func WithMaxDecompressBufferSize(v int64) GlobalDecryptOption { // to use when decrypting a message. If not specified, the default // value of 10,000 is used. // -// This option has a global effect. -func WithMaxPBES2Count(v int) GlobalOption { - return &globalOption{option.New(identMaxPBES2Count{}, v)} +// The cap is applied per recipient. RFC 7516 §5.3 allows each +// recipient of a JSON-serialized JWE to carry its own "p2c" iteration +// count in its per-recipient unprotected header, and jwe.Decrypt +// honors that. For a JWE with N recipients, the worst-case PBKDF2 +// cost is therefore on the order of N * MaxPBES2Count iterations per +// decrypt attempt. When accepting multi-recipient JSON JWEs from +// untrusted senders, also clamp jwe.WithMaxRecipients. +// +// This option can be used for `jwe.Settings()`, which changes the behavior +// globally, or for `jwe.Decrypt()`, which changes the behavior for that +// specific call. +func WithMaxPBES2Count(v int) GlobalDecryptOption { + return &globalDecryptOption{option.New(identMaxPBES2Count{}, v)} +} + +// WithMaxRecipients specifies the maximum number of recipients allowed +// in a JWE message. If a JWE message contains more recipients than this +// value, parsing or decryption will return an error. The default value +// is 100. +// +// This option can be used for `jwe.Settings()`, which changes the behavior +// globally, or for `jwe.Decrypt()`, which changes the behavior for that +// specific call. +func WithMaxRecipients(v int) GlobalDecryptOption { + return &globalDecryptOption{option.New(identMaxRecipients{}, v)} } // WithMergeProtectedHeaders specify that when given multiple headers @@ -369,6 +492,47 @@ func WithMessage(v *Message) DecryptOption { return &decryptOption{option.New(identMessage{}, v)} } +// WithMinPBES2Count specifies the minimum number of PBES2 iterations +// to accept when decrypting a message. If not specified, the default +// value of 1,000 is used (RFC 7518 §4.8.1.2 floor). Set to 0 to +// disable the minimum check (NOT RECOMMENDED for untrusted issuers +// — without a floor, recipients accept arbitrarily-weak password- +// derived keys and silently spend the producer-chosen amount of +// crypto work to verify them). +// +// Threat model: a malicious or careless issuer signs a PBES2-wrapped +// JWE with a very low p2c (e.g. 100) so they spend almost no CPU on +// their side, while the recipient happily derives the wrap key with +// the same low iteration count. The result is an authenticated +// message whose key-derivation strength is well below industry +// practice (OWASP 2023 recommends ≥600,000 PBKDF2-SHA256 iterations +// for password-derived keys; the RFC floor of 1,000 is far below +// that). For receiver-side hardening against under-iteration, raise +// WithMinPBES2Count above 1,000. +// +// This option can be used for `jwe.Settings()`, which changes the behavior +// globally, or for `jwe.Decrypt()`, which changes the behavior for that +// specific call. +func WithMinPBES2Count(v int) GlobalDecryptOption { + return &globalDecryptOption{option.New(identMinPBES2Count{}, v)} +} + +// WithPBES2Count specifies the number of PBKDF2 iterations to use when +// encrypting a key with the PBES2 family of algorithms. If not specified, +// the default value of 10,000 is used. Modern guidance (OWASP 2023) +// recommends 600,000 or more for PBKDF2-HMAC-SHA256. +// +// This option only affects encryption. Iteration counts on incoming +// messages are validated separately on decrypt via WithMinPBES2Count +// and WithMaxPBES2Count. +// +// This option can be used for `jwe.Settings()`, which changes the behavior +// globally, or for `jwe.Encrypt()`, which changes the behavior for that +// specific call. +func WithPBES2Count(v int) GlobalEncryptOption { + return &globalEncryptOption{option.New(identPBES2Count{}, v)} +} + // WithPretty specifies whether the JSON output should be formatted and // indented func WithPretty(v bool) WithJSONSuboption { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/jwk/BUILD.bazel index 8e82e1f009..1bcb93a319 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/BUILD.bazel +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/BUILD.bazel @@ -41,6 +41,7 @@ go_library( "//internal/tokens", "//jwa", "//jwk/ecdsa", + "//jwk/internal/registry", "//jwk/jwkbb", "@com_github_lestrrat_go_blackmagic//:blackmagic", "@com_github_lestrrat_go_httprc_v3//:httprc", diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/cache.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/cache.go index 6d5b00f056..388c776a81 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/cache.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/cache.go @@ -81,14 +81,23 @@ type Cache struct { // conjection with `httprc.NewResource` to create a `httprc.Resource` object // to auto-update `jwk.Set` objects. type Transformer struct { - parseOptions []ParseOption + parseOptions []ParseOption + maxFetchBodySize int64 } func (t Transformer) Transform(_ context.Context, res *http.Response) (Set, error) { - buf, err := io.ReadAll(res.Body) + maxBodySize := t.maxFetchBodySize + if maxBodySize <= 0 { + maxBodySize = maxFetchBodySize.Load() + } + + buf, err := io.ReadAll(io.LimitReader(res.Body, maxBodySize+1)) if err != nil { return nil, fmt.Errorf(`failed to read response body status: %w`, err) } + if int64(len(buf)) > maxBodySize { + return nil, fmt.Errorf(`response body at %q exceeded max size of %d bytes`, res.Request.URL.String(), maxBodySize) + } set, err := Parse(buf, t.parseOptions...) if err != nil { @@ -121,10 +130,17 @@ func NewCache(ctx context.Context, client *httprc.Client) (*Cache, error) { // Register registers a URL to be managed by the cache. URLs must // be registered before issuing `Get` // -// The `Register` method is a thin wrapper around `(httprc.Controller).Add` +// The `Register` method is a thin wrapper around `(httprc.Controller).Add`. +// +// As with `jwk.Fetch`, the default whitelist is `jwk.InsecureWhitelist{}` +// — every URL is allowed. Supply `jwk.WithFetchWhitelist()` when the URL +// originates from untrusted input. See `jwk.Fetch` for the full security +// rationale. func (c *Cache) Register(ctx context.Context, u string, options ...RegisterOption) error { var parseOptions []ParseOption var resourceOptions []httprc.NewResourceOption + var maxFetchBodySize int64 + var hasHTTPClient bool waitReady := true for _, option := range options { switch option := option.(type) { @@ -144,16 +160,29 @@ func (c *Cache) Register(ctx context.Context, u string, options ...RegisterOptio return fmt.Errorf(`failed to retrieve HTTPClient option value: %w`, err) } resourceOptions = append(resourceOptions, httprc.WithHTTPClient(cli)) + hasHTTPClient = true case identWaitReady{}: if err := option.Value(&waitReady); err != nil { return fmt.Errorf(`failed to retrieve WaitReady option value: %w`, err) } + case identMaxFetchBodySize{}: + if err := option.Value(&maxFetchBodySize); err != nil { + return fmt.Errorf(`failed to retrieve MaxFetchBodySize option value: %w`, err) + } } } } + // If no HTTP client was explicitly provided, use the library's default + // client which includes timeout and redirect protections. Without this, + // httprc would fall back to http.DefaultClient which has no such protections. + if !hasHTTPClient { + resourceOptions = append(resourceOptions, httprc.WithHTTPClient(getFetchHTTPClient())) + } + r, err := httprc.NewResource[Set](u, &Transformer{ - parseOptions: parseOptions, + parseOptions: parseOptions, + maxFetchBodySize: maxFetchBodySize, }, resourceOptions...) if err != nil { return fmt.Errorf(`failed to create httprc.Resource: %w`, err) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/convert.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/convert.go index 057f4b02a0..4cf7bb6450 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/convert.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/convert.go @@ -14,7 +14,6 @@ import ( "github.com/lestrrat-go/blackmagic" "github.com/lestrrat-go/jwx/v3/internal/ecutil" - "github.com/lestrrat-go/jwx/v3/jwa" ) // # Converting between Raw Keys and `jwk.Key`s @@ -23,33 +22,72 @@ import ( // A converter that converts from a `jwk.Key` to a raw key is called a KeyExporter. var keyImporters = make(map[reflect.Type]KeyImporter) -var keyExporters = make(map[jwa.KeyType][]KeyExporter) + +// KeyKind identifies a key for exporter dispatch. Built-in key types +// use the key type string (e.g. "RSA", "EC", "OKP", "oct"). Keys that +// implement KeyKinder can return a more specific identity +// (e.g. "OKP:Ed448") to select a curve-specific exporter. +type KeyKind string + +// KeyKinder is implemented by keys that need exporter dispatch +// beyond just their key type. For example, OKP keys return +// "OKP:" so that curve-specific exporters can be registered +// by external modules. +// +// Keys that do not implement this interface are dispatched by +// KeyType().String() alone. +type KeyKinder interface { + KeyKind() KeyKind +} + +var keyExporters = make(map[KeyKind][]KeyExporter) var muKeyImporters sync.RWMutex var muKeyExporters sync.RWMutex -// RegisterKeyImporter registers a KeyImporter for the given raw key. When `jwk.Import()` is called, -// the library will look up the appropriate KeyImporter for the given raw key type (via `reflect`) -// and execute the KeyImporters in succession until either one of them succeeds, or all of them fail. +// RegisterKeyImporter registers a KeyImporter for the given raw key. +// When `jwk.Import()` is called, the library looks up the importer for +// the given raw key type (via `reflect`) and executes it. +// +// Importer dispatch is single-valued per Go type: there is exactly +// one importer registered per `reflect.TypeOf(from)`. Registering a +// second importer for the same raw-key type silently replaces the +// previous entry — including built-in importers for +// `*rsa.PrivateKey`, `*ecdsa.PrivateKey`, and so on. Callers that +// need to guard against accidental overwrites should keep track of +// registrations themselves and avoid double-registration at init() +// time. +// +// This deliberately differs from the stacking behavior of +// [RegisterKeyExporter] (keyed by [KeyKind] strings) and +// [RegisterKeyParser] (an untyped-JSON fallback chain); importer +// dispatch is a single-value map keyed by Go type, with no +// equivalent dimension to try next. v4 turns the overwrite into +// an error; v3 keeps the frozen silent-overwrite behavior for +// backward compatibility. func RegisterKeyImporter(from any, conv KeyImporter) { muKeyImporters.Lock() defer muKeyImporters.Unlock() keyImporters[reflect.TypeOf(from)] = conv } -// RegisterKeyExporter registers a KeyExporter for the given key type. When `key.Raw()` is called, -// the library will look up the appropriate KeyExporter for the given key type and execute the -// KeyExporters in succession until either one of them succeeds, or all of them fail. -func RegisterKeyExporter(kty jwa.KeyType, conv KeyExporter) { +// RegisterKeyExporter registers a KeyExporter for the given key identity. +// When `jwk.Export()` is called, the library first tries exporters registered +// for the key's specific identity (via [KeyKinder]), then falls back to +// exporters registered for the key type alone. +// +// For most key types, pass `KeyKind(kty.String())` (e.g. `KeyKind("RSA")`). +// For curve-specific exporters, use a compound identity like `KeyKind("OKP:Ed448")`. +func RegisterKeyExporter(ident KeyKind, conv KeyExporter) { muKeyExporters.Lock() defer muKeyExporters.Unlock() - convs, ok := keyExporters[kty] + convs, ok := keyExporters[ident] if !ok { convs = []KeyExporter{conv} } else { convs = append([]KeyExporter{conv}, convs...) } - keyExporters[kty] = convs + keyExporters[ident] = convs } // KeyImporter is used to convert from a raw key to a `jwk.Key`. mneumonic: from the PoV of the `jwk.Key`, @@ -376,10 +414,12 @@ func Export(key Key, dst any) error { if rv.Kind() != reflect.Ptr { return fmt.Errorf(`jwk.Export: destination object must be a pointer`) } + muKeyExporters.RLock() - exporters, ok := keyExporters[key.KeyType()] + exporters := findExporters(key) muKeyExporters.RUnlock() - if !ok { + + if len(exporters) == 0 { return fmt.Errorf(`jwk.Export: no exporters registered for key type '%T'`, key) } for _, conv := range exporters { @@ -397,3 +437,16 @@ func Export(key Key, dst any) error { } return fmt.Errorf(`jwk.Export: no suitable exporter found for key type '%T'`, key) } + +// findExporters returns exporters for the key, trying the specific +// KeyKind first, then falling back to the key type. Caller must +// hold muKeyExporters.RLock. +func findExporters(key Key) []KeyExporter { + if ki, ok := key.(KeyKinder); ok { + ident := ki.KeyKind() + if exporters, ok := keyExporters[ident]; ok { + return exporters + } + } + return keyExporters[KeyKind(key.KeyType().String())] +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/doc.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/doc.go index 7df707521b..b4f6e164e2 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/doc.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/doc.go @@ -22,7 +22,7 @@ // // jwkKey, _ := jwk.Import(rsaPrivateKey) // var rawKey *rsa.PRrivateKey -// jwkKey.Raw(&rawKey) +// jwk.Export(jwkKey, &rawKey) // // You can use them to sign/verify/encrypt/decrypt: // @@ -177,9 +177,9 @@ // var hint string // if err := probe.Get("MyHint", &hint); err != nil { // // if it doesn't have the `my_hint` field, it probably means -// // it's not for us, so we return ContinueParseError so that +// // it's not for us, so we return ContinueError so that // // the next parser can pick it up -// return nil, jwk.ContinueParseError() +// return nil, jwk.ContinueError() // } // // // Use hint to determine concrete key type @@ -190,7 +190,7 @@ // ... // } // -// return unmarshaler.Unmarshal(data, key) +// return unmarshaler.UnmarshalKey(data, key) // } // // ## Registering KeyImporter/KeyExporter diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa.go index 8f76d0508e..7d246afdb3 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa.go @@ -20,7 +20,7 @@ func init() { ourecdsa.RegisterCurve(jwa.P384(), elliptic.P384()) ourecdsa.RegisterCurve(jwa.P521(), elliptic.P521()) - RegisterKeyExporter(jwa.EC(), KeyExportFunc(ecdsaJWKToRaw)) + RegisterKeyExporter(KeyKind(jwa.EC().String()), KeyExportFunc(ecdsaJWKToRaw)) } func (k *ecdsaPublicKey) Import(rawKey *ecdsa.PublicKey) error { @@ -35,6 +35,10 @@ func (k *ecdsaPublicKey) Import(rawKey *ecdsa.PublicKey) error { return fmt.Errorf(`invalid ecdsa.PublicKey`) } + if err := validateECDSAPoint(rawKey.Curve, rawKey.X, rawKey.Y); err != nil { + return fmt.Errorf(`jwk: %w`, err) + } + xbuf := ecutil.AllocECPointBuffer(rawKey.X, rawKey.Curve) ybuf := ecutil.AllocECPointBuffer(rawKey.Y, rawKey.Curve) defer ecutil.ReleaseECPointBuffer(xbuf) @@ -68,6 +72,10 @@ func (k *ecdsaPrivateKey) Import(rawKey *ecdsa.PrivateKey) error { return fmt.Errorf(`invalid ecdsa.PrivateKey`) } + if err := validateECDSAPoint(rawKey.Curve, rawKey.PublicKey.X, rawKey.PublicKey.Y); err != nil { + return fmt.Errorf(`jwk: %w`, err) + } + xbuf := ecutil.AllocECPointBuffer(rawKey.PublicKey.X, rawKey.Curve) ybuf := ecutil.AllocECPointBuffer(rawKey.PublicKey.Y, rawKey.Curve) dbuf := ecutil.AllocECPointBuffer(rawKey.D, rawKey.Curve) @@ -101,9 +109,121 @@ func buildECDSAPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf, ybuf []byte) (*ec x.SetBytes(xbuf) y.SetBytes(ybuf) + if err := validateECDSAPoint(crv, &x, &y); err != nil { + return nil, fmt.Errorf(`jwk: %w`, err) + } + return &ecdsa.PublicKey{Curve: crv, X: &x, Y: &y}, nil } +// validateECDSAPoint rejects ECDSA public key coordinates that are not +// safe to use: the identity point (0, 0) and any point that does not lie +// on the named curve. Without these checks, attacker-supplied JWKs can +// smuggle off-curve or small-subgroup points into downstream ECDSA/ECDH +// operations (invalid-curve attacks). See JWK-003. +// +// The implementation is split into two branches for a reason: +// +// 1. For the NIST P-256/P-384/P-521 curves we route through crypto/ecdh. +// Go 1.21 deprecated most of crypto/elliptic's Curve methods — not +// because point-on-curve validation stopped being necessary, but +// because the generic big.Int implementation in crypto/elliptic had +// subtle edge cases and the Go team wanted users off it. The blessed +// replacement for "parse and validate an uncompressed point" on +// stdlib curves is ecdh.Curve.NewPublicKey, which enforces on-curve +// membership and rejects the identity as part of parsing the SEC1 +// 0x04 || X || Y encoding. Using ecdh here means we're using exactly +// the Go team's recommended replacement, and the deprecated stdlib +// elliptic methods are never reached for any NIST-curve input. +// +// 2. For any other curve registered through jwk/ecdsa.RegisterCurve +// (most importantly secp256k1 via the ES256K extension module), +// crypto/ecdh has no entry point — it only supports the four curves +// listed above. The only mechanism available for validating a point +// on a custom curve is the elliptic.Curve interface's IsOnCurve +// method. Calling it here is correct despite the staticcheck +// deprecation notice, for three reasons: +// +// a. The deprecation targets the *stdlib* elliptic.Curve +// implementations (elliptic.P256() etc.). Custom curves such as +// btcec/secp256k1 ship their own IsOnCurve implementation; the +// interface dispatch lands in that implementation, not in the +// deprecated stdlib one. staticcheck cannot see through interface +// dispatch, so the lint scope is suppressed on just this line. +// +// b. The elliptic.Curve interface itself remains part of Go's +// supported API because crypto/ecdsa.Verify and +// crypto/ecdsa.Sign continue to take elliptic.Curve-backed keys. +// Any third-party curve that plugs into crypto/ecdsa is +// contractually required to implement a working IsOnCurve; that +// is the only thing crypto/ecdsa has to validate the public point +// before verification. Calling it from here is the same contract. +// +// c. The remaining alternatives are worse: (i) refusing to validate +// non-stdlib curves at all reintroduces JWK-003 for ES256K users; +// (ii) refusing to *support* non-stdlib curves is a regression +// for ES256K users. A cleaner long-term fix is to extend +// jwk/ecdsa.RegisterCurve so extension modules can register a +// validator function alongside the curve, letting us drop the +// IsOnCurve call entirely. That is a deliberate follow-up, not a +// blocker for this security fix. +func validateECDSAPoint(crv elliptic.Curve, x, y *big.Int) error { + if x.Sign() == 0 && y.Sign() == 0 { + return fmt.Errorf(`invalid ECDSA public key: identity point is not a valid public key`) + } + + // Coordinates must fit in the curve's field. The NIST P-curve + // branch below pads x and y into a fixed-size SEC1 buffer via + // big.Int.FillBytes, which panics on oversized input. Bounding + // here makes the function safe by construction for every caller, + // including jwk.Import handed a hand-built *ecdsa.PublicKey from + // raw bytes. + bits := crv.Params().BitSize + if x.BitLen() > bits { + return fmt.Errorf(`invalid ECDSA public key: x coordinate is %d bits, exceeds curve %q field size of %d bits`, x.BitLen(), crv.Params().Name, bits) + } + if y.BitLen() > bits { + return fmt.Errorf(`invalid ECDSA public key: y coordinate is %d bits, exceeds curve %q field size of %d bits`, y.BitLen(), crv.Params().Name, bits) + } + + if ecdhCrv, ok := stdlibECDHCurve(crv); ok { + size := (crv.Params().BitSize + 7) / 8 + buf := make([]byte, 1+2*size) + buf[0] = 0x04 + x.FillBytes(buf[1 : 1+size]) + y.FillBytes(buf[1+size:]) + if _, err := ecdhCrv.NewPublicKey(buf); err != nil { + return fmt.Errorf(`invalid ECDSA public key: %w`, err) + } + return nil + } + + // Custom-curve fallback. See the block comment on validateECDSAPoint + // for the full justification of calling a deprecated-marked method; + // the short version is that interface dispatch lands in the custom + // curve's own IsOnCurve, not in deprecated stdlib code. + if !crv.IsOnCurve(x, y) { //nolint:staticcheck // see validateECDSAPoint godoc: only path that validates custom curves + return fmt.Errorf(`invalid ECDSA public key: point is not on curve %q`, crv.Params().Name) + } + return nil +} + +// stdlibECDHCurve maps a crypto/elliptic curve to its crypto/ecdh +// counterpart when one exists. Only the NIST P-curves supported by both +// packages are mapped; everything else returns ok=false and falls back +// to the elliptic.Curve path in validateECDSAPoint. +func stdlibECDHCurve(crv elliptic.Curve) (ecdh.Curve, bool) { + switch crv { + case elliptic.P256(): + return ecdh.P256(), true + case elliptic.P384(): + return ecdh.P384(), true + case elliptic.P521(): + return ecdh.P521(), true + } + return nil, false +} + func buildECDHPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf, ybuf []byte) (*ecdh.PublicKey, error) { var ecdhcrv ecdh.Curve switch alg { @@ -170,34 +290,64 @@ func ecdsaJWKToRaw(keyif Key, hint any) (any, error) { } } - locker, ok := k.(rlocker) - if ok { + // rlocker is unexported with unexported methods, so only our + // concrete types implement it. A successful assertion lets us + // type-assert to the concrete struct and read fields directly + // under a single batch lock. This avoids nested RLock (which + // deadlocks when a writer is pending) while preserving an + // atomic snapshot of all fields. + var crv jwa.EllipticCurveAlgorithm + var hasCrv bool + var od, ox, oy []byte + if locker, ok := k.(rlocker); ok { locker.rlock() - defer locker.runlock() + concrete := k.(*ecdsaPrivateKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it + if concrete.crv != nil { + crv = *(concrete.crv) + hasCrv = true + } + od, ox, oy = concrete.d, concrete.x, concrete.y + locker.runlock() + } else { + // External implementation — use self-locking interface getters. + var ok bool + if crv, ok = k.Crv(); !ok { + return nil, fmt.Errorf(`missing "crv" field`) + } + hasCrv = true + if od, ok = k.D(); !ok { + return nil, fmt.Errorf(`missing "d" field`) + } + if ox, ok = k.X(); !ok { + return nil, fmt.Errorf(`missing "x" field`) + } + if oy, ok = k.Y(); !ok { + return nil, fmt.Errorf(`missing "y" field`) + } } - crv, ok := k.Crv() - if !ok { + if !hasCrv { return nil, fmt.Errorf(`missing "crv" field`) } if isECDH { - d, ok := k.D() - if !ok { + if od == nil { return nil, fmt.Errorf(`missing "d" field`) } - return buildECDHPrivateKey(crv, d) + return buildECDHPrivateKey(crv, od) } - x, ok := k.X() - if !ok { + if ox == nil { return nil, fmt.Errorf(`missing "x" field`) } - y, ok := k.Y() - if !ok { + if oy == nil { return nil, fmt.Errorf(`missing "y" field`) } - pubk, err := buildECDSAPublicKey(crv, x, y) + if od == nil { + return nil, fmt.Errorf(`missing "d" field`) + } + + pubk, err := buildECDSAPublicKey(crv, ox, oy) if err != nil { return nil, fmt.Errorf(`failed to build public key: %w`, err) } @@ -205,12 +355,7 @@ func ecdsaJWKToRaw(keyif Key, hint any) (any, error) { var key ecdsa.PrivateKey var d big.Int - origD, ok := k.D() - if !ok { - return nil, fmt.Errorf(`missing "d" field`) - } - - d.SetBytes(origD) + d.SetBytes(od) key.D = &d key.PublicKey = *pubk @@ -231,24 +376,40 @@ func ecdsaJWKToRaw(keyif Key, hint any) (any, error) { } } - locker, ok := k.(rlocker) - if ok { + // See ECDSAPrivateKey case above for explanation of the rlocker pattern. + var crv jwa.EllipticCurveAlgorithm + var hasCrv bool + var x, y []byte + if locker, ok := k.(rlocker); ok { locker.rlock() - defer locker.runlock() + concrete := k.(*ecdsaPublicKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it + if concrete.crv != nil { + crv = *(concrete.crv) + hasCrv = true + } + x, y = concrete.x, concrete.y + locker.runlock() + } else { + var ok bool + if crv, ok = k.Crv(); !ok { + return nil, fmt.Errorf(`missing "crv" field`) + } + hasCrv = true + if x, ok = k.X(); !ok { + return nil, fmt.Errorf(`missing "x" field`) + } + if y, ok = k.Y(); !ok { + return nil, fmt.Errorf(`missing "y" field`) + } } - crv, ok := k.Crv() - if !ok { + if !hasCrv { return nil, fmt.Errorf(`missing "crv" field`) } - - x, ok := k.X() - if !ok { + if x == nil { return nil, fmt.Errorf(`missing "x" field`) } - - y, ok := k.Y() - if !ok { + if y == nil { return nil, fmt.Errorf(`missing "y" field`) } if isECDH { @@ -305,12 +466,12 @@ func ecdsaThumbprint(hash crypto.Hash, crv, x, y string) []byte { // Thumbprint returns the JWK thumbprint using the indicated // hashing algorithm, according to RFC 7638 -func (k ecdsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { +func (k *ecdsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { k.mu.RLock() defer k.mu.RUnlock() var key ecdsa.PublicKey - if err := Export(&k, &key); err != nil { + if err := Export(k, &key); err != nil { return nil, fmt.Errorf(`failed to export ecdsa.PublicKey for thumbprint generation: %w`, err) } @@ -329,12 +490,12 @@ func (k ecdsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { // Thumbprint returns the JWK thumbprint using the indicated // hashing algorithm, according to RFC 7638 -func (k ecdsaPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) { +func (k *ecdsaPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) { k.mu.RLock() defer k.mu.RUnlock() var key ecdsa.PrivateKey - if err := Export(&k, &key); err != nil { + if err := Export(k, &key); err != nil { return nil, fmt.Errorf(`failed to export ecdsa.PrivateKey for thumbprint generation: %w`, err) } @@ -367,12 +528,21 @@ func ecdsaValidateKey(k interface { } keySize := ecutil.CalculateKeySize(crv) - if x, ok := k.X(); !ok || len(x) != keySize { - return fmt.Errorf(`invalid "x" length (%d) for curve %q`, len(x), crv.Params().Name) + xbuf, ok := k.X() + if !ok || len(xbuf) != keySize { + return fmt.Errorf(`invalid "x" length (%d) for curve %q`, len(xbuf), crv.Params().Name) } - if y, ok := k.Y(); !ok || len(y) != keySize { - return fmt.Errorf(`invalid "y" length (%d) for curve %q`, len(y), crv.Params().Name) + ybuf, ok := k.Y() + if !ok || len(ybuf) != keySize { + return fmt.Errorf(`invalid "y" length (%d) for curve %q`, len(ybuf), crv.Params().Name) + } + + var x, y big.Int + x.SetBytes(xbuf) + y.SetBytes(ybuf) + if err := validateECDSAPoint(crv, &x, &y); err != nil { + return err } if checkPrivate { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa/ecdsa.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa/ecdsa.go index 3392483218..ef0338413c 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa/ecdsa.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa/ecdsa.go @@ -45,16 +45,22 @@ func rebuildCurves() { } } -// Algorithms returns the list of registered jwa.EllipticCurveAlgorithms -// that ca be used for ECDSA keys. +// Algorithms returns a snapshot of the registered +// jwa.EllipticCurveAlgorithms that can be used for ECDSA keys. +// +// The returned slice is caller-owned. Modifying it does not affect the +// package registry, and ordering is unspecified. func Algorithms() []jwa.EllipticCurveAlgorithm { muCurves.RLock() defer muCurves.RUnlock() - return algList + return append([]jwa.EllipticCurveAlgorithm(nil), algList...) } func AlgorithmFromCurve(crv elliptic.Curve) (jwa.EllipticCurveAlgorithm, error) { + muCurves.RLock() + defer muCurves.RUnlock() + alg, ok := curveToAlgMap[crv] if !ok { return jwa.InvalidEllipticCurve(), fmt.Errorf(`unknown elliptic curve: %q`, crv) @@ -63,6 +69,9 @@ func AlgorithmFromCurve(crv elliptic.Curve) (jwa.EllipticCurveAlgorithm, error) } func CurveFromAlgorithm(alg jwa.EllipticCurveAlgorithm) (elliptic.Curve, error) { + muCurves.RLock() + defer muCurves.RUnlock() + crv, ok := algToCurveMap[alg] if !ok { return nil, fmt.Errorf(`unknown elliptic curve algorithm: %q`, alg) @@ -71,6 +80,9 @@ func CurveFromAlgorithm(alg jwa.EllipticCurveAlgorithm) (elliptic.Curve, error) } func IsCurveAvailable(alg jwa.EllipticCurveAlgorithm) bool { + muCurves.RLock() + defer muCurves.RUnlock() + _, ok := algToCurveMap[alg] return ok } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa_gen.go index 39536de3d8..a717e24bb9 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa_gen.go @@ -15,6 +15,7 @@ import ( "github.com/lestrrat-go/jwx/v3/internal/pool" "github.com/lestrrat-go/jwx/v3/internal/tokens" "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk/internal/registry" ) const ( @@ -44,7 +45,7 @@ type ecdsaPublicKey struct { x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5 y []byte privateParams map[string]any - mu *sync.RWMutex + mu sync.RWMutex dc json.DecodeCtx } @@ -53,28 +54,29 @@ var _ Key = &ecdsaPublicKey{} func newECDSAPublicKey() *ecdsaPublicKey { return &ecdsaPublicKey{ - mu: &sync.RWMutex{}, privateParams: make(map[string]any), } } -func (h ecdsaPublicKey) KeyType() jwa.KeyType { +func (h *ecdsaPublicKey) KeyType() jwa.KeyType { return jwa.EC() } -func (h ecdsaPublicKey) rlock() { +func (h *ecdsaPublicKey) rlock() { h.mu.RLock() } -func (h ecdsaPublicKey) runlock() { +func (h *ecdsaPublicKey) runlock() { h.mu.RUnlock() } -func (h ecdsaPublicKey) IsPrivate() bool { +func (h *ecdsaPublicKey) IsPrivate() bool { return false } func (h *ecdsaPublicKey) Algorithm() (jwa.KeyAlgorithm, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.algorithm != nil { return *(h.algorithm), true } @@ -82,6 +84,8 @@ func (h *ecdsaPublicKey) Algorithm() (jwa.KeyAlgorithm, bool) { } func (h *ecdsaPublicKey) Crv() (jwa.EllipticCurveAlgorithm, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.crv != nil { return *(h.crv), true } @@ -89,6 +93,8 @@ func (h *ecdsaPublicKey) Crv() (jwa.EllipticCurveAlgorithm, bool) { } func (h *ecdsaPublicKey) KeyID() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyID != nil { return *(h.keyID), true } @@ -96,6 +102,8 @@ func (h *ecdsaPublicKey) KeyID() (string, bool) { } func (h *ecdsaPublicKey) KeyOps() (KeyOperationList, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyOps != nil { return *(h.keyOps), true } @@ -103,6 +111,8 @@ func (h *ecdsaPublicKey) KeyOps() (KeyOperationList, bool) { } func (h *ecdsaPublicKey) KeyUsage() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyUsage != nil { return *(h.keyUsage), true } @@ -110,6 +120,8 @@ func (h *ecdsaPublicKey) KeyUsage() (string, bool) { } func (h *ecdsaPublicKey) X() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x != nil { return h.x, true } @@ -117,10 +129,17 @@ func (h *ecdsaPublicKey) X() ([]byte, bool) { } func (h *ecdsaPublicKey) X509CertChain() (*cert.Chain, bool) { - return h.x509CertChain, true + h.mu.RLock() + defer h.mu.RUnlock() + if h.x509CertChain != nil { + return h.x509CertChain, true + } + return nil, false } func (h *ecdsaPublicKey) X509CertThumbprint() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509CertThumbprint != nil { return *(h.x509CertThumbprint), true } @@ -128,6 +147,8 @@ func (h *ecdsaPublicKey) X509CertThumbprint() (string, bool) { } func (h *ecdsaPublicKey) X509CertThumbprintS256() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509CertThumbprintS256 != nil { return *(h.x509CertThumbprintS256), true } @@ -135,6 +156,8 @@ func (h *ecdsaPublicKey) X509CertThumbprintS256() (string, bool) { } func (h *ecdsaPublicKey) X509URL() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509URL != nil { return *(h.x509URL), true } @@ -142,6 +165,8 @@ func (h *ecdsaPublicKey) X509URL() (string, bool) { } func (h *ecdsaPublicKey) Y() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.y != nil { return h.y, true } @@ -348,7 +373,12 @@ func (h *ecdsaPublicKey) setNoLock(name string, value any) error { } case ECDSAXKey: if v, ok := value.([]byte); ok { - h.x = v + if v == nil { + h.x = nil + } else { + h.x = make([]byte, len(v)) + copy(h.x, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, ECDSAXKey, value) @@ -378,7 +408,12 @@ func (h *ecdsaPublicKey) setNoLock(name string, value any) error { return fmt.Errorf(`invalid value for %s key: %T`, X509URLKey, value) case ECDSAYKey: if v, ok := value.([]byte); ok { - h.y = v + if v == nil { + h.y = nil + } else { + h.y = make([]byte, len(v)) + copy(h.y, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, ECDSAYKey, value) @@ -476,7 +511,7 @@ LOOP: case string: // Objects can only have string keys switch tok { case KeyTypeKey: - val, err := json.ReadNextStringToken(dec) + val, err := json.ReadNextStringToken(dec, h.dc) if err != nil { return fmt.Errorf(`error reading token: %w`, err) } @@ -500,7 +535,7 @@ LOOP: } h.crv = &decoded case KeyIDKey: - if err := json.AssignNextStringToken(&h.keyID, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyID, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err) } case KeyOpsKey: @@ -510,7 +545,7 @@ LOOP: } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } case ECDSAXKey: @@ -524,15 +559,15 @@ LOOP: } h.x509CertChain = &decoded case X509CertThumbprintKey: - if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err) } case X509CertThumbprintS256Key: - if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err) } case X509URLKey: - if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509URL, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err) } case ECDSAYKey: @@ -549,7 +584,7 @@ LOOP: } } } - decoded, err := registry.Decode(dec, tok) + decoded, err := fieldRegistry.Decode(dec, tok) if err == nil { h.setNoLock(tok, decoded) continue @@ -572,88 +607,142 @@ LOOP: return nil } -func (h ecdsaPublicKey) MarshalJSON() ([]byte, error) { - data := make(map[string]any) - fields := make([]string, 0, 11) - data[KeyTypeKey] = jwa.EC() - fields = append(fields, KeyTypeKey) +func (h *ecdsaPublicKey) makePairs() ([]fieldPair, error) { + pairs := getFieldPairList() + h.mu.RLock() + defer h.mu.RUnlock() + { + v, err := json.Marshal(jwa.EC()) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyTypeKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyTypeKey, Value: v}) + } if h.algorithm != nil { - data[AlgorithmKey] = *(h.algorithm) - fields = append(fields, AlgorithmKey) + v, err := json.Marshal(*(h.algorithm)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err) + } + pairs = append(pairs, fieldPair{Name: AlgorithmKey, Value: v}) } if h.crv != nil { - data[ECDSACrvKey] = *(h.crv) - fields = append(fields, ECDSACrvKey) + v, err := json.Marshal(*(h.crv)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, ECDSACrvKey, err) + } + pairs = append(pairs, fieldPair{Name: ECDSACrvKey, Value: v}) } if h.keyID != nil { - data[KeyIDKey] = *(h.keyID) - fields = append(fields, KeyIDKey) + v, err := json.Marshal(*(h.keyID)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyIDKey, Value: v}) } if h.keyOps != nil { - data[KeyOpsKey] = *(h.keyOps) - fields = append(fields, KeyOpsKey) + v, err := json.Marshal(*(h.keyOps)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyOpsKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyOpsKey, Value: v}) } if h.keyUsage != nil { - data[KeyUsageKey] = *(h.keyUsage) - fields = append(fields, KeyUsageKey) + v, err := json.Marshal(*(h.keyUsage)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyUsageKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyUsageKey, Value: v}) } if h.x != nil { - data[ECDSAXKey] = h.x - fields = append(fields, ECDSAXKey) + v, err := json.Marshal(base64.EncodeToString(h.x)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, ECDSAXKey, err) + } + pairs = append(pairs, fieldPair{Name: ECDSAXKey, Value: v}) } if h.x509CertChain != nil { - data[X509CertChainKey] = h.x509CertChain - fields = append(fields, X509CertChainKey) + v, err := json.Marshal(h.x509CertChain) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err) + } + pairs = append(pairs, fieldPair{Name: X509CertChainKey, Value: v}) } if h.x509CertThumbprint != nil { - data[X509CertThumbprintKey] = *(h.x509CertThumbprint) - fields = append(fields, X509CertThumbprintKey) + v, err := json.Marshal(*(h.x509CertThumbprint)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err) + } + pairs = append(pairs, fieldPair{Name: X509CertThumbprintKey, Value: v}) } if h.x509CertThumbprintS256 != nil { - data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256) - fields = append(fields, X509CertThumbprintS256Key) + v, err := json.Marshal(*(h.x509CertThumbprintS256)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err) + } + pairs = append(pairs, fieldPair{Name: X509CertThumbprintS256Key, Value: v}) } if h.x509URL != nil { - data[X509URLKey] = *(h.x509URL) - fields = append(fields, X509URLKey) + v, err := json.Marshal(*(h.x509URL)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err) + } + pairs = append(pairs, fieldPair{Name: X509URLKey, Value: v}) } if h.y != nil { - data[ECDSAYKey] = h.y - fields = append(fields, ECDSAYKey) + v, err := json.Marshal(base64.EncodeToString(h.y)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, ECDSAYKey, err) + } + pairs = append(pairs, fieldPair{Name: ECDSAYKey, Value: v}) } for k, v := range h.privateParams { - data[k] = v - fields = append(fields, k) + var encoded []byte + switch v := v.(type) { + case []byte: + var err error + encoded, err = json.Marshal(base64.EncodeToString(v)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + default: + var err error + encoded, err = json.Marshal(v) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + } + pairs = append(pairs, fieldPair{Name: k, Value: encoded}) } - sort.Strings(fields) + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Name < pairs[j].Name + }) + + return pairs, nil +} + +func (h *ecdsaPublicKey) MarshalJSON() ([]byte, error) { buf := pool.BytesBuffer().Get() defer pool.BytesBuffer().Put(buf) + pairs, err := h.makePairs() + if err != nil { + return nil, fmt.Errorf(`failed to make pairs: %w`, err) + } buf.WriteByte(tokens.OpenCurlyBracket) - enc := json.NewEncoder(buf) - for i, f := range fields { + + for i, pair := range pairs { if i > 0 { - buf.WriteRune(tokens.Comma) - } - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(f) - buf.WriteString(`":`) - v := data[f] - switch v := v.(type) { - case []byte: - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(base64.EncodeToString(v)) - buf.WriteRune(tokens.DoubleQuote) - default: - if err := enc.Encode(v); err != nil { - return nil, fmt.Errorf(`failed to encode value for field %s: %w`, f, err) - } - buf.Truncate(buf.Len() - 1) + buf.WriteByte(tokens.Comma) } + buf.WriteByte('"') + buf.WriteString(pair.Name) + buf.WriteString(`": `) + buf.Write(pair.Value.([]byte)) } buf.WriteByte(tokens.CloseCurlyBracket) ret := make([]byte, buf.Len()) copy(ret, buf.Bytes()) + putFieldPairList(pairs) return ret, nil } @@ -723,7 +812,7 @@ type ecdsaPrivateKey struct { x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5 y []byte privateParams map[string]any - mu *sync.RWMutex + mu sync.RWMutex dc json.DecodeCtx } @@ -732,28 +821,29 @@ var _ Key = &ecdsaPrivateKey{} func newECDSAPrivateKey() *ecdsaPrivateKey { return &ecdsaPrivateKey{ - mu: &sync.RWMutex{}, privateParams: make(map[string]any), } } -func (h ecdsaPrivateKey) KeyType() jwa.KeyType { +func (h *ecdsaPrivateKey) KeyType() jwa.KeyType { return jwa.EC() } -func (h ecdsaPrivateKey) rlock() { +func (h *ecdsaPrivateKey) rlock() { h.mu.RLock() } -func (h ecdsaPrivateKey) runlock() { +func (h *ecdsaPrivateKey) runlock() { h.mu.RUnlock() } -func (h ecdsaPrivateKey) IsPrivate() bool { +func (h *ecdsaPrivateKey) IsPrivate() bool { return true } func (h *ecdsaPrivateKey) Algorithm() (jwa.KeyAlgorithm, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.algorithm != nil { return *(h.algorithm), true } @@ -761,6 +851,8 @@ func (h *ecdsaPrivateKey) Algorithm() (jwa.KeyAlgorithm, bool) { } func (h *ecdsaPrivateKey) Crv() (jwa.EllipticCurveAlgorithm, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.crv != nil { return *(h.crv), true } @@ -768,6 +860,8 @@ func (h *ecdsaPrivateKey) Crv() (jwa.EllipticCurveAlgorithm, bool) { } func (h *ecdsaPrivateKey) D() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.d != nil { return h.d, true } @@ -775,6 +869,8 @@ func (h *ecdsaPrivateKey) D() ([]byte, bool) { } func (h *ecdsaPrivateKey) KeyID() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyID != nil { return *(h.keyID), true } @@ -782,6 +878,8 @@ func (h *ecdsaPrivateKey) KeyID() (string, bool) { } func (h *ecdsaPrivateKey) KeyOps() (KeyOperationList, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyOps != nil { return *(h.keyOps), true } @@ -789,6 +887,8 @@ func (h *ecdsaPrivateKey) KeyOps() (KeyOperationList, bool) { } func (h *ecdsaPrivateKey) KeyUsage() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyUsage != nil { return *(h.keyUsage), true } @@ -796,6 +896,8 @@ func (h *ecdsaPrivateKey) KeyUsage() (string, bool) { } func (h *ecdsaPrivateKey) X() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x != nil { return h.x, true } @@ -803,10 +905,17 @@ func (h *ecdsaPrivateKey) X() ([]byte, bool) { } func (h *ecdsaPrivateKey) X509CertChain() (*cert.Chain, bool) { - return h.x509CertChain, true + h.mu.RLock() + defer h.mu.RUnlock() + if h.x509CertChain != nil { + return h.x509CertChain, true + } + return nil, false } func (h *ecdsaPrivateKey) X509CertThumbprint() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509CertThumbprint != nil { return *(h.x509CertThumbprint), true } @@ -814,6 +923,8 @@ func (h *ecdsaPrivateKey) X509CertThumbprint() (string, bool) { } func (h *ecdsaPrivateKey) X509CertThumbprintS256() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509CertThumbprintS256 != nil { return *(h.x509CertThumbprintS256), true } @@ -821,6 +932,8 @@ func (h *ecdsaPrivateKey) X509CertThumbprintS256() (string, bool) { } func (h *ecdsaPrivateKey) X509URL() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509URL != nil { return *(h.x509URL), true } @@ -828,6 +941,8 @@ func (h *ecdsaPrivateKey) X509URL() (string, bool) { } func (h *ecdsaPrivateKey) Y() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.y != nil { return h.y, true } @@ -1016,7 +1131,12 @@ func (h *ecdsaPrivateKey) setNoLock(name string, value any) error { return fmt.Errorf(`invalid value for %s key: %T`, ECDSACrvKey, value) case ECDSADKey: if v, ok := value.([]byte); ok { - h.d = v + if v == nil { + h.d = nil + } else { + h.d = make([]byte, len(v)) + copy(h.d, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, ECDSADKey, value) @@ -1050,7 +1170,12 @@ func (h *ecdsaPrivateKey) setNoLock(name string, value any) error { } case ECDSAXKey: if v, ok := value.([]byte); ok { - h.x = v + if v == nil { + h.x = nil + } else { + h.x = make([]byte, len(v)) + copy(h.x, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, ECDSAXKey, value) @@ -1080,7 +1205,12 @@ func (h *ecdsaPrivateKey) setNoLock(name string, value any) error { return fmt.Errorf(`invalid value for %s key: %T`, X509URLKey, value) case ECDSAYKey: if v, ok := value.([]byte); ok { - h.y = v + if v == nil { + h.y = nil + } else { + h.y = make([]byte, len(v)) + copy(h.y, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, ECDSAYKey, value) @@ -1147,7 +1277,7 @@ func (k *ecdsaPrivateKey) SetDecodeCtx(dc json.DecodeCtx) { k.dc = dc } -func (h *ecdsaPrivateKey) UnmarshalJSON(buf []byte) error { +func (h *ecdsaPrivateKey) UnmarshalJSON(buf []byte) (retErr error) { h.mu.Lock() defer h.mu.Unlock() h.algorithm = nil @@ -1162,6 +1292,16 @@ func (h *ecdsaPrivateKey) UnmarshalJSON(buf []byte) error { h.x509CertThumbprintS256 = nil h.x509URL = nil h.y = nil + defer func() { + if retErr != nil { + clear(h.d) + h.d = nil + clear(h.x) + h.x = nil + clear(h.y) + h.y = nil + } + }() dec := json.NewDecoder(bytes.NewReader(buf)) LOOP: for { @@ -1181,7 +1321,7 @@ LOOP: case string: // Objects can only have string keys switch tok { case KeyTypeKey: - val, err := json.ReadNextStringToken(dec) + val, err := json.ReadNextStringToken(dec, h.dc) if err != nil { return fmt.Errorf(`error reading token: %w`, err) } @@ -1209,7 +1349,7 @@ LOOP: return fmt.Errorf(`failed to decode value for key %s: %w`, ECDSADKey, err) } case KeyIDKey: - if err := json.AssignNextStringToken(&h.keyID, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyID, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err) } case KeyOpsKey: @@ -1219,7 +1359,7 @@ LOOP: } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } case ECDSAXKey: @@ -1233,15 +1373,15 @@ LOOP: } h.x509CertChain = &decoded case X509CertThumbprintKey: - if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err) } case X509CertThumbprintS256Key: - if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err) } case X509URLKey: - if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509URL, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err) } case ECDSAYKey: @@ -1258,7 +1398,7 @@ LOOP: } } } - decoded, err := registry.Decode(dec, tok) + decoded, err := fieldRegistry.Decode(dec, tok) if err == nil { h.setNoLock(tok, decoded) continue @@ -1284,92 +1424,149 @@ LOOP: return nil } -func (h ecdsaPrivateKey) MarshalJSON() ([]byte, error) { - data := make(map[string]any) - fields := make([]string, 0, 12) - data[KeyTypeKey] = jwa.EC() - fields = append(fields, KeyTypeKey) +func (h *ecdsaPrivateKey) makePairs() ([]fieldPair, error) { + pairs := getFieldPairList() + h.mu.RLock() + defer h.mu.RUnlock() + { + v, err := json.Marshal(jwa.EC()) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyTypeKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyTypeKey, Value: v}) + } if h.algorithm != nil { - data[AlgorithmKey] = *(h.algorithm) - fields = append(fields, AlgorithmKey) + v, err := json.Marshal(*(h.algorithm)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err) + } + pairs = append(pairs, fieldPair{Name: AlgorithmKey, Value: v}) } if h.crv != nil { - data[ECDSACrvKey] = *(h.crv) - fields = append(fields, ECDSACrvKey) + v, err := json.Marshal(*(h.crv)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, ECDSACrvKey, err) + } + pairs = append(pairs, fieldPair{Name: ECDSACrvKey, Value: v}) } if h.d != nil { - data[ECDSADKey] = h.d - fields = append(fields, ECDSADKey) + v, err := json.Marshal(base64.EncodeToString(h.d)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, ECDSADKey, err) + } + pairs = append(pairs, fieldPair{Name: ECDSADKey, Value: v}) } if h.keyID != nil { - data[KeyIDKey] = *(h.keyID) - fields = append(fields, KeyIDKey) + v, err := json.Marshal(*(h.keyID)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyIDKey, Value: v}) } if h.keyOps != nil { - data[KeyOpsKey] = *(h.keyOps) - fields = append(fields, KeyOpsKey) + v, err := json.Marshal(*(h.keyOps)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyOpsKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyOpsKey, Value: v}) } if h.keyUsage != nil { - data[KeyUsageKey] = *(h.keyUsage) - fields = append(fields, KeyUsageKey) + v, err := json.Marshal(*(h.keyUsage)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyUsageKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyUsageKey, Value: v}) } if h.x != nil { - data[ECDSAXKey] = h.x - fields = append(fields, ECDSAXKey) + v, err := json.Marshal(base64.EncodeToString(h.x)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, ECDSAXKey, err) + } + pairs = append(pairs, fieldPair{Name: ECDSAXKey, Value: v}) } if h.x509CertChain != nil { - data[X509CertChainKey] = h.x509CertChain - fields = append(fields, X509CertChainKey) + v, err := json.Marshal(h.x509CertChain) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err) + } + pairs = append(pairs, fieldPair{Name: X509CertChainKey, Value: v}) } if h.x509CertThumbprint != nil { - data[X509CertThumbprintKey] = *(h.x509CertThumbprint) - fields = append(fields, X509CertThumbprintKey) + v, err := json.Marshal(*(h.x509CertThumbprint)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err) + } + pairs = append(pairs, fieldPair{Name: X509CertThumbprintKey, Value: v}) } if h.x509CertThumbprintS256 != nil { - data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256) - fields = append(fields, X509CertThumbprintS256Key) + v, err := json.Marshal(*(h.x509CertThumbprintS256)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err) + } + pairs = append(pairs, fieldPair{Name: X509CertThumbprintS256Key, Value: v}) } if h.x509URL != nil { - data[X509URLKey] = *(h.x509URL) - fields = append(fields, X509URLKey) + v, err := json.Marshal(*(h.x509URL)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err) + } + pairs = append(pairs, fieldPair{Name: X509URLKey, Value: v}) } if h.y != nil { - data[ECDSAYKey] = h.y - fields = append(fields, ECDSAYKey) + v, err := json.Marshal(base64.EncodeToString(h.y)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, ECDSAYKey, err) + } + pairs = append(pairs, fieldPair{Name: ECDSAYKey, Value: v}) } for k, v := range h.privateParams { - data[k] = v - fields = append(fields, k) + var encoded []byte + switch v := v.(type) { + case []byte: + var err error + encoded, err = json.Marshal(base64.EncodeToString(v)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + default: + var err error + encoded, err = json.Marshal(v) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + } + pairs = append(pairs, fieldPair{Name: k, Value: encoded}) } - sort.Strings(fields) + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Name < pairs[j].Name + }) + + return pairs, nil +} + +func (h *ecdsaPrivateKey) MarshalJSON() ([]byte, error) { buf := pool.BytesBuffer().Get() defer pool.BytesBuffer().Put(buf) + pairs, err := h.makePairs() + if err != nil { + return nil, fmt.Errorf(`failed to make pairs: %w`, err) + } buf.WriteByte(tokens.OpenCurlyBracket) - enc := json.NewEncoder(buf) - for i, f := range fields { + + for i, pair := range pairs { if i > 0 { - buf.WriteRune(tokens.Comma) - } - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(f) - buf.WriteString(`":`) - v := data[f] - switch v := v.(type) { - case []byte: - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(base64.EncodeToString(v)) - buf.WriteRune(tokens.DoubleQuote) - default: - if err := enc.Encode(v); err != nil { - return nil, fmt.Errorf(`failed to encode value for field %s: %w`, f, err) - } - buf.Truncate(buf.Len() - 1) + buf.WriteByte(tokens.Comma) } + buf.WriteByte('"') + buf.WriteString(pair.Name) + buf.WriteString(`": `) + buf.Write(pair.Value.([]byte)) } buf.WriteByte(tokens.CloseCurlyBracket) ret := make([]byte, buf.Len()) copy(ret, buf.Bytes()) + putFieldPairList(pairs) return ret, nil } @@ -1430,3 +1627,10 @@ func init() { func ECDSAStandardFieldsFilter() KeyFilter { return ecdsaStandardFields } + +func init() { + registry.Register(jwa.EC().String(), registry.Constructor{ + Public: func() any { return newECDSAPublicKey() }, + Private: func() any { return newECDSAPrivateKey() }, + }) +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/errors.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/errors.go index af7e00d952..e7be20c5f5 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/errors.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/errors.go @@ -72,6 +72,31 @@ func sparseerr(f string, args ...any) error { return bparseerr(`jwk.ParseString`, f, args...) } +func kparseerr(f string, args ...any) error { + return bparseerr(`jwk.ParseKey`, f, args...) +} + +type whitelistError struct { + error +} + +func (e whitelistError) Unwrap() error { + return e.error +} + +func (whitelistError) Is(err error) bool { + _, ok := err.(whitelistError) + return ok +} + +var errDefaultWhitelistError = whitelistError{errors.New(`rejected by whitelist`)} + +// WhitelistError returns an error that can be passed to `errors.Is` to check +// if the error is caused by a URL being rejected by a whitelist. +func WhitelistError() error { + return errDefaultWhitelistError +} + var errDefaultParseError = parseError{errors.New(`parse error`)} func ParseError() error { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/fetch.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/fetch.go index 2c80a369dc..79572edf3f 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/fetch.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/fetch.go @@ -5,8 +5,110 @@ import ( "fmt" "io" "net/http" + "sync" + "sync/atomic" + "time" ) +// defaultMaxFetchBodySize is the initial default maximum number of bytes read +// from an HTTP response body when fetching a JWKS (10 MB). +const defaultMaxFetchBodySize int64 = 10 * 1024 * 1024 + +// defaultFetchTimeout is the default timeout for HTTP requests made by +// jwk.Fetch(). This prevents malicious or unresponsive JWKS endpoints from +// hanging indefinitely (e.g. slowloris-style DoS). +const defaultFetchTimeout = 30 * time.Second + +// defaultMaxRedirects is the maximum number of HTTP redirects the default +// fetch client will follow. This is intentionally lower than Go's default +// of 10 to limit redirect chain abuse. +const defaultMaxRedirects = 5 + +var maxFetchBodySize atomic.Int64 + +var ( + fetchHTTPClientMu sync.RWMutex + fetchHTTPClient HTTPClient +) + +func init() { + maxFetchBodySize.Store(defaultMaxFetchBodySize) + fetchHTTPClient = DefaultHTTPClient() +} + +// DefaultHTTPClient returns a new http.Client configured with the same +// defaults used by jwk.Fetch(): a 30-second timeout, a redirect policy +// that blocks HTTPS-to-HTTP scheme downgrades, and a maximum of 5 redirects. +// +// This is useful for callers who need the library's default protections +// but want to wrap or augment the client (e.g. adding a custom Transport), +// and for restoring defaults after calling jwk.Configure(jwk.WithHTTPClient(...)). +func DefaultHTTPClient() *http.Client { + return WrapHTTPClientDefaults(&http.Client{}) +} + +// WrapHTTPClientDefaults returns a shallow copy of the given http.Client with the +// library's default safety behaviors applied. Existing client settings +// (Transport, Jar, etc.) are preserved. +// +// - Timeout: applied only when the client has no timeout set (zero value). +// - CheckRedirect: if the client already has one, the library's redirect +// policy runs first; if it passes, the original CheckRedirect is called. +// If the client has no CheckRedirect, the library's policy is used directly. +// +// This is useful when you need to bring your own http.Client (e.g. for custom +// TLS configuration) but still want the library's redirect hardening. +func WrapHTTPClientDefaults(client *http.Client) *http.Client { + cloned := *client + if cloned.Timeout == 0 { + cloned.Timeout = defaultFetchTimeout + } + orig := cloned.CheckRedirect + if orig == nil { + cloned.CheckRedirect = defaultCheckRedirect + } else { + cloned.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if err := defaultCheckRedirect(req, via); err != nil { + return err + } + return orig(req, via) + } + } + return &cloned +} + +// defaultCheckRedirect is the CheckRedirect policy for the default HTTP client +// used by jwk.Fetch(). It prevents HTTPS-to-HTTP scheme downgrades and limits +// the total number of redirects. +// +// This does NOT protect against redirects to private/internal IP addresses. +// For full SSRF protection, callers should provide a custom http.Client via +// WithHTTPClient that validates destination IPs in Transport.DialContext. +func defaultCheckRedirect(req *http.Request, via []*http.Request) error { + if len(via) >= defaultMaxRedirects { + return fmt.Errorf("jwk.Fetch: stopped after %d redirects", defaultMaxRedirects) + } + + // Prevent HTTPS → HTTP scheme downgrade at any hop. + // via[len(via)-1] is the immediately previous request in the chain. + if len(via) > 0 && via[len(via)-1].URL.Scheme == "https" && req.URL.Scheme != "https" { + return fmt.Errorf("jwk.Fetch: redirect from HTTPS to non-HTTPS URL %q is not allowed", req.URL.Redacted()) + } + return nil +} + +func getFetchHTTPClient() HTTPClient { + fetchHTTPClientMu.RLock() + defer fetchHTTPClientMu.RUnlock() + return fetchHTTPClient +} + +func setFetchHTTPClient(c HTTPClient) { + fetchHTTPClientMu.Lock() + defer fetchHTTPClientMu.Unlock() + fetchHTTPClient = c +} + // Fetcher is an interface that represents an object that fetches a JWKS. // Currently this is only used in the `jws.WithVerifyAuto` option. // @@ -66,11 +168,28 @@ func (f *CachedFetcher) Fetch(ctx context.Context, u string, _ ...FetchOption) ( // contents of the object with the data at the remote resource, // consider using `jwk.Cache`, which automatically refreshes // jwk.Set objects asynchronously. +// +// # Security +// +// By default, jwk.Fetch does not restrict which URLs may be contacted: the +// URL you pass is fetched as-is, with only the default HTTP client's +// HTTPS-to-HTTP redirect block applied. This is the right default when the +// URL is hard-coded or comes from configuration you control. +// +// It is NOT safe when the URL is attacker-controllable — most commonly a +// `jku` header copied out of an untrusted JWS. In that case you MUST pass +// a jwk.WithFetchWhitelist() option that restricts the reachable URLs via +// jwk.MapWhitelist, jwk.RegexpWhitelist, or a custom Whitelist. +// +// For defense against redirect-to-private-IP and DNS-rebinding attacks, +// combine WithFetchWhitelist with a custom http.Client (see WithHTTPClient) +// whose Transport.DialContext validates resolved addresses. func Fetch(ctx context.Context, u string, options ...FetchOption) (Set, error) { var parseOptions []ParseOption //nolint:revive // I want to keep the type of `wl` as `Whitelist` instead of `InsecureWhitelist` var wl Whitelist = InsecureWhitelist{} - var client HTTPClient = http.DefaultClient + var client = getFetchHTTPClient() + var maxBodySize = maxFetchBodySize.Load() for _, option := range options { if parseOpt, ok := option.(ParseOption); ok { parseOptions = append(parseOptions, parseOpt) @@ -86,11 +205,18 @@ func Fetch(ctx context.Context, u string, options ...FetchOption) (Set, error) { if err := option.Value(&wl); err != nil { return nil, fmt.Errorf(`failed to retrieve fetch whitelist option value: %w`, err) } + case identMaxFetchBodySize{}: + if err := option.Value(&maxBodySize); err != nil { + return nil, fmt.Errorf(`failed to retrieve MaxFetchBodySize option value: %w`, err) + } + if maxBodySize <= 0 { + return nil, fmt.Errorf(`jwk.Fetch: WithMaxFetchBodySize must be greater than zero`) + } } } if !wl.IsAllowed(u) { - return nil, fmt.Errorf(`jwk.Fetch: url %q has been rejected by whitelist`, u) + return nil, whitelistError{fmt.Errorf(`jwk.Fetch: url %q has been rejected by whitelist`, u)} } req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) @@ -108,10 +234,18 @@ func Fetch(ctx context.Context, u string, options ...FetchOption) (Set, error) { return nil, fmt.Errorf(`jwk.Fetch: request returned status %d, expected 200`, res.StatusCode) } - buf, err := io.ReadAll(res.Body) + // LimitReader caps memory at maxBodySize+1; reading +1 byte lets us detect + // oversized responses. We intentionally skip a Content-Length pre-check because + // the header is untrustworthy (server-controlled, absent in chunked transfers). + // Slow-trickle attacks are mitigated by context deadlines and http.Client.Timeout, + // not by header inspection. + buf, err := io.ReadAll(io.LimitReader(res.Body, maxBodySize+1)) if err != nil { return nil, fmt.Errorf(`jwk.Fetch: failed to read response body for %q: %w`, u, err) } + if int64(len(buf)) > maxBodySize { + return nil, fmt.Errorf(`jwk.Fetch: response body for %q exceeded max size of %d bytes`, u, maxBodySize) + } return Parse(buf, parseOptions...) } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface.go index c5a22a43fb..db6b8e31d5 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface.go @@ -41,15 +41,17 @@ const ( // Set represents JWKS object, a collection of jwk.Key objects. // -// Sets can be safely converted to and from JSON using the standard -// `"encoding/json".Marshal` and `"encoding/json".Unmarshal`. However, -// if you do not know if the payload contains a single JWK or a JWK set, -// consider using `jwk.Parse()` to always get a `jwk.Set` out of it. +// Sets can be marshaled and unmarshaled with the standard +// `"encoding/json".Marshal` and `"encoding/json".Unmarshal`. The +// unmarshal path requires JWKS shape (an object with a "keys" field). +// For input that may be either a single bare JWK or a JWKS, use +// [Parse], which dispatches between the two shapes and always returns +// a `jwk.Set`. // -// Since v1.2.12, JWK sets with private parameters can be parsed as well. -// Such private parameters can be accessed via the `Field()` method. -// If a resource contains a single JWK instead of a JWK set, private parameters -// are stored in _both_ the resulting `jwk.Set` object and the `jwk.Key` object . +// JWKS-level extension members (any top-level field other than "keys") +// are preserved as set-level private parameters and are accessible via +// the `Field()` method. Per-key extension members live on the +// individual `jwk.Key` objects, accessible via that key's `Field()`. // //nolint:interfacebloat type Set interface { @@ -119,10 +121,12 @@ type Set interface { } type set struct { - keys []Key - mu sync.RWMutex - dc DecodeCtx - privateParams map[string]any + keys []Key + mu sync.RWMutex + dc DecodeCtx + privateParams map[string]any + maxKeys int // scratch cap consumed by UnmarshalJSON; 0 means use global default + rejectDuplicateKID bool // scratch flag consumed by UnmarshalJSON; false falls back to global } type PublicKeyer interface { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface_gen.go index 4f23d96cb0..1bb2d081bf 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface_gen.go @@ -4,6 +4,7 @@ package jwk import ( "crypto" + "sync" "github.com/lestrrat-go/jwx/v3/cert" "github.com/lestrrat-go/jwx/v3/jwa" @@ -21,6 +22,26 @@ const ( X509CertThumbprintS256Key = "x5t#S256" ) +type fieldPair struct { + Name string + Value any +} + +var fieldPairPool = sync.Pool{ + New: func() any { + return make([]fieldPair, 0, 17) + }, +} + +func getFieldPairList() []fieldPair { + return fieldPairPool.Get().([]fieldPair) +} + +func putFieldPairList(list []fieldPair) { + clear(list) // zero fieldPair entries so pooled values don't retain references across Put/Get + fieldPairPool.Put(list[:0]) +} + // Key defines the minimal interface for each of the // key types. Their use and implementation differ significantly // between each key type, so you should use type assertions diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/internal/registry/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/jwk/internal/registry/BUILD.bazel new file mode 100644 index 0000000000..af55d665ba --- /dev/null +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/internal/registry/BUILD.bazel @@ -0,0 +1,14 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "registry", + srcs = ["registry.go"], + importpath = "github.com/lestrrat-go/jwx/v3/jwk/internal/registry", + visibility = ["//jwk:__subpackages__"], +) + +alias( + name = "go_default_library", + actual = ":registry", + visibility = ["//jwk:__subpackages__"], +) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/internal/registry/registry.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/internal/registry/registry.go new file mode 100644 index 0000000000..9f09d77283 --- /dev/null +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/internal/registry/registry.go @@ -0,0 +1,43 @@ +// Package registry provides an internal registry of JWK key constructors. +// It exists so that both jwk and jwk/jwkunsafe can access the same set of +// constructors without exporting them from the jwk package. +package registry + +import "fmt" + +// Constructor holds factory functions for creating empty JWK keys. +// Public may be nil for key types without a public/private distinction +// (e.g. symmetric keys). +type Constructor struct { + Public func() any // returns jwk.Key + Private func() any // returns jwk.Key +} + +var constructors = map[string]Constructor{} + +// Register adds a constructor for the given key type name. +func Register(kty string, c Constructor) { + constructors[kty] = c +} + +// NewKey creates a new empty private (or symmetric) key for the given key type. +func NewKey(kty string) (any, error) { + c, ok := constructors[kty] + if !ok { + return nil, fmt.Errorf(`registry: unknown key type %q`, kty) + } + return c.Private(), nil +} + +// NewPublicKey creates a new empty public key for the given key type. +// Returns an error for key types that have no public/private distinction. +func NewPublicKey(kty string) (any, error) { + c, ok := constructors[kty] + if !ok { + return nil, fmt.Errorf(`registry: unknown key type %q`, kty) + } + if c.Public == nil { + return nil, fmt.Errorf(`registry: key type %q has no public key variant`, kty) + } + return c.Public(), nil +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwk.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwk.go index 22d4950d8f..ba2db6cb48 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwk.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwk.go @@ -14,12 +14,15 @@ import ( "math/big" "reflect" "slices" + "sync/atomic" "github.com/lestrrat-go/jwx/v3/internal/base64" "github.com/lestrrat-go/jwx/v3/internal/json" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk/jwkbb" ) -var registry = json.NewRegistry() +var fieldRegistry = json.NewRegistry() func bigIntToBytes(n *big.Int) ([]byte, error) { if n == nil { @@ -28,7 +31,22 @@ func bigIntToBytes(n *big.Int) ([]byte, error) { return n.Bytes(), nil } +// maxKeys bounds the number of keys accepted by Parse() from a single +// input. It applies to both the JSON `keys` array and the PEM/X.509 +// block stream: each entry triggers a probe + unmarshal + validation, +// and callers cannot predict that amplification from the raw input +// size alone. Tunable via WithMaxKeys / Configure(WithMaxKeys(...)). +var maxKeys atomic.Int64 + +// rejectDuplicateKID makes Parse/UnmarshalJSON fail when the JWKS +// carries two or more keys with the same non-empty "kid". Default is +// false (RFC 7517 allows duplicates; LookupKeyID returns the first). +// Tunable via WithRejectDuplicateKID / Configure(WithRejectDuplicateKID(...)). +var rejectDuplicateKID atomic.Bool + func init() { + maxKeys.Store(1000) + if err := RegisterProbeField(reflect.StructField{ Name: "Kty", Type: reflect.TypeFor[string](), @@ -41,7 +59,7 @@ func init() { Type: reflect.TypeFor[json.RawMessage](), Tag: `json:"d,omitempty"`, }); err != nil { - panic(fmt.Errorf("failed to register mandatory probe for 'kty' field: %w", err)) + panic(fmt.Errorf("failed to register mandatory probe for 'd' field: %w", err)) } } @@ -67,7 +85,16 @@ func Import(raw any) (Key, error) { return nil, importerr(`failed to convert %T to jwk.Key: no converters were able to convert`, raw) } - return conv.Import(raw) + key, err := conv.Import(raw) + if err != nil { + return nil, err + } + if v, ok := key.(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return nil, importerr(`key validation failed: %w`, err) + } + } + return key, nil } // PublicSetOf returns a new jwk.Set consisting of @@ -77,9 +104,29 @@ func Import(raw any) (Key, error) { // you want to generate the corresponding public versions for the // users to verify with. // -// Be aware that all fields will be copied onto the new public key. It is the caller's -// responsibility to remove any fields, if necessary. -func PublicSetOf(v Set) (Set, error) { +// By default, if the input set contains a symmetric (oct) key, this +// function returns an error: a symmetric key has no public form, and +// its "public" representation would be the secret itself. Publishing +// such a set (e.g. as `/.well-known/jwks.json`) would leak secret +// material. This is a behavior change from earlier v3.0.x releases, +// where symmetric keys were silently passed through. Callers who +// explicitly want the legacy pass-through behavior can opt in with +// `jwk.WithAllowSymmetric(true)`. +// +// Be aware that for asymmetric private keys, all fields will be +// copied onto the new public key. It is the caller's responsibility +// to remove any fields, if necessary. +func PublicSetOf(v Set, options ...PublicSetOption) (Set, error) { + var allowSymmetric bool + for _, option := range options { + switch option.Ident() { + case identAllowSymmetric{}: + if err := option.Value(&allowSymmetric); err != nil { + return nil, fmt.Errorf(`failed to retrieve AllowSymmetric option value: %w`, err) + } + } + } + newSet := NewSet() n := v.Len() @@ -88,6 +135,10 @@ func PublicSetOf(v Set) (Set, error) { if !ok { return nil, fmt.Errorf(`key not found`) } + if k.KeyType() == jwa.OctetSeq() && !allowSymmetric { + kid, _ := k.KeyID() + return nil, fmt.Errorf(`jwk.PublicSetOf: input set contains a symmetric key (kid=%q, index=%d); symmetric keys have no public form and would leak secret material if published. Remove symmetric keys from the set before calling PublicSetOf, or pass jwk.WithAllowSymmetric(true) to opt into legacy pass-through behavior`, kid, i) + } pubKey, err := PublicKeyOf(k) if err != nil { return nil, fmt.Errorf(`failed to get public key of %T: %w`, k, err) @@ -198,6 +249,14 @@ func (ctx *setDecodeCtx) IgnoreParseError() bool { // are performed for certificate expiration, no checks against missing // parameters are performed, etc. func ParseKey(data []byte, options ...ParseOption) (Key, error) { + key, err := doParseKey(data, options...) + if err != nil { + return nil, kparseerr(`%w`, err) + } + return key, nil +} + +func doParseKey(data []byte, options ...ParseOption) (Key, error) { var parsePEM bool var localReg *json.Registry var pemDecoder PEMDecoder @@ -263,6 +322,13 @@ func ParseKey(data []byte, options ...ParseOption) (Key, error) { parser := parsers[i] key, err := parser.ParseKey(probe, &unmarshaler, data) if err == nil { + // A buggy custom parser may return (nil, nil); treat + // that as if it had returned ContinueError so the next + // parser runs instead of handing the caller a nil Key + // they will dereference. + if key == nil { + continue + } return key, nil } @@ -295,6 +361,8 @@ func Parse(src []byte, options ...ParseOption) (Set, error) { var localReg *json.Registry var ignoreParseError bool var pemDecoder PEMDecoder + maxK := int(maxKeys.Load()) + rejectDupKid := rejectDuplicateKID.Load() for _, option := range options { switch option.Ident() { case identPEM{}: @@ -313,6 +381,19 @@ func Parse(src []byte, options ...ParseOption) (Set, error) { if err := option.Value(&ignoreParseError); err != nil { return nil, parseerr(`failed to retrieve IgnoreParseError option value: %w`, err) } + case identMaxKeys{}: + var v int + if err := option.Value(&v); err != nil { + return nil, parseerr(`failed to retrieve MaxKeys option value: %w`, err) + } + if v <= 0 { + return nil, parseerr(`WithMaxKeys must be greater than zero, got %d`, v) + } + maxK = v + case identRejectDuplicateKID{}: + if err := option.Value(&rejectDupKid); err != nil { + return nil, parseerr(`failed to retrieve RejectDuplicateKID option value: %w`, err) + } case identTypedField{}: var pair typedFieldPair // temporary var needed for typed field if err := option.Value(&pair); err != nil { @@ -332,6 +413,7 @@ func Parse(src []byte, options ...ParseOption) (Set, error) { pemDecoder = NewPEMDecoder() } src = bytes.TrimSpace(src) + var keyCount int for len(src) > 0 { raw, rest, err := pemDecoder.Decode(src) if err != nil { @@ -344,8 +426,17 @@ func Parse(src []byte, options ...ParseOption) (Set, error) { if err := s.AddKey(key); err != nil { return nil, parseerr(`failed to add jwk.Key to set: %w`, err) } + keyCount++ + if keyCount > maxK { + return nil, parseerr(`too many keys in PEM input: max %d`, maxK) + } src = bytes.TrimSpace(rest) } + if rejectDupKid { + if kid, dup := firstDuplicateKID(s); dup { + return nil, parseerr(`duplicate "kid" %q in PEM input`, kid) + } + } return s, nil } @@ -362,13 +453,60 @@ func Parse(src []byte, options ...ParseOption) (Set, error) { defer func() { dcKs.SetDecodeCtx(nil) }() } - if err := json.Unmarshal(src, s); err != nil { - return nil, parseerr(`failed to unmarshal JWK set: %w`, err) + // Propagate the resolved cap to Set.UnmarshalJSON. A scratch field + // rather than a ParseOption thread-through keeps json.Unmarshal happy. + if setter, ok := s.(interface{ setMaxKeys(int) }); ok { + setter.setMaxKeys(maxK) + defer setter.setMaxKeys(0) + } + if setter, ok := s.(interface{ setRejectDuplicateKID(bool) }); ok && rejectDupKid { + setter.setRejectDuplicateKID(true) + defer setter.setRejectDuplicateKID(false) + } + + // Dispatch JWK-vs-JWKS up front. Set.UnmarshalJSON requires JWKS + // shape; the bare-JWK convenience lives here. + if jwkbb.HeaderHas(jwkbb.HeaderParse(src), "keys") { + if err := json.Unmarshal(src, s); err != nil { + return nil, parseerr(`failed to unmarshal JWK set: %w`, err) + } + } else { + key, err := ParseKey(src, options...) + if err != nil { + return nil, parseerr(`failed to parse sole key: %w`, err) + } + if err := s.AddKey(key); err != nil { + return nil, parseerr(`failed to add jwk.Key to set: %w`, err) + } + } + + if rejectDupKid { + if kid, dup := firstDuplicateKID(s); dup { + return nil, parseerr(`duplicate "kid" %q`, kid) + } } return s, nil } +// firstDuplicateKID returns the first non-empty kid that appears more +// than once in s, or ("", false) if every non-empty kid is unique. +func firstDuplicateKID(s Set) (string, bool) { + seen := make(map[string]struct{}, s.Len()) + for i := range s.Len() { + key, _ := s.Key(i) + kid, ok := key.KeyID() + if !ok || kid == "" { + continue + } + if _, dup := seen[kid]; dup { + return kid, true + } + seen[kid] = struct{}{} + } + return "", false +} + // ParseReader parses a JWK set from the incoming byte buffer. func ParseReader(src io.Reader, options ...ParseOption) (Set, error) { // meh, there's no way to tell if a stream has "ended" a single @@ -396,22 +534,32 @@ func ParseString(s string, options ...ParseOption) (Set, error) { // AssignKeyID is a convenience function to automatically assign the "kid" // section of the key, if it already doesn't have one. It uses Key.Thumbprint -// method with crypto.SHA256 as the default hashing algorithm +// method with crypto.SHA256 as the default hashing algorithm. +// +// By default, if the key already carries a `kid`, `AssignKeyID` leaves it +// alone and returns nil. Pass `jwk.WithForceAssign(true)` to force +// recomputation (for example, when upgrading to a stronger thumbprint hash +// via `jwk.WithThumbprintHash`). func AssignKeyID(key Key, options ...AssignKeyIDOption) error { - if key.Has(KeyIDKey) { - return nil - } - hash := crypto.SHA256 + var force bool for _, option := range options { switch option.Ident() { case identThumbprintHash{}: if err := option.Value(&hash); err != nil { return fmt.Errorf(`failed to retrieve thumbprint hash option value: %w`, err) } + case identForceAssign{}: + if err := option.Value(&force); err != nil { + return fmt.Errorf(`failed to retrieve force assign option value: %w`, err) + } } } + if !force && key.Has(KeyIDKey) { + return nil + } + h, err := key.Thumbprint(hash) if err != nil { return fmt.Errorf(`failed to generate thumbprint: %w`, err) @@ -574,7 +722,7 @@ type CustomDecodeFunc = json.CustomDecodeFunc // that wraps your desired type (in this case `time.Time`) and implement // MarshalJSON and UnmashalJSON. func RegisterCustomField(name string, object any) { - registry.Register(name, object) + fieldRegistry.Register(name, object) } // Equal compares two keys and returns true if they are equal. The comparison @@ -639,6 +787,11 @@ func IsKeyValidationError(err error) bool { // Configure is used to configure global behavior of the jwk package. func Configure(options ...GlobalOption) { var strictKeyUsagePtr *bool + var maxFetchBodySizePtr *int64 + var maxKeysPtr *int64 + var httpClientPtr *HTTPClient + var minRSAModulusBitsPtr *int64 + var minRSAPublicExponentPtr *int64 for _, option := range options { switch option.Ident() { case identStrictKeyUsage{}: @@ -647,12 +800,77 @@ func Configure(options ...GlobalOption) { continue } strictKeyUsagePtr = &v + case identMaxFetchBodySize{}: + var v int64 + if err := option.Value(&v); err != nil { + continue + } + if v <= 0 { + continue + } + maxFetchBodySizePtr = &v + case identMaxKeys{}: + var v int + if err := option.Value(&v); err != nil { + continue + } + if v <= 0 { + continue + } + v64 := int64(v) + maxKeysPtr = &v64 + case identHTTPClient{}: + var v HTTPClient + if err := option.Value(&v); err != nil { + continue + } + httpClientPtr = &v + case identMinRSAModulusBits{}: + var v int + if err := option.Value(&v); err != nil { + continue + } + v64 := int64(v) + minRSAModulusBitsPtr = &v64 + case identMinRSAPublicExponent{}: + var v int + if err := option.Value(&v); err != nil { + continue + } + v64 := int64(v) + minRSAPublicExponentPtr = &v64 + case identRejectDuplicateKID{}: + var v bool + if err := option.Value(&v); err != nil { + continue + } + rejectDuplicateKID.Store(v) } } if strictKeyUsagePtr != nil { strictKeyUsage.Store(*strictKeyUsagePtr) } + + if maxFetchBodySizePtr != nil { + maxFetchBodySize.Store(*maxFetchBodySizePtr) + } + + if maxKeysPtr != nil { + maxKeys.Store(*maxKeysPtr) + } + + if httpClientPtr != nil { + setFetchHTTPClient(*httpClientPtr) + } + + if minRSAModulusBitsPtr != nil { + rsaMinModulusBits.Store(*minRSAModulusBitsPtr) + } + + if minRSAPublicExponentPtr != nil { + setMinRSAPublicExponent(int(*minRSAPublicExponentPtr)) + } } // These are used when validating keys. diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/BUILD.bazel index 68a4ccdc19..baf286688a 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/BUILD.bazel +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/BUILD.bazel @@ -1,12 +1,25 @@ -load("@rules_go//go:def.bzl", "go_library") +load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "jwkbb", - srcs = ["x509.go"], + srcs = [ + "header.go", + "x509.go", + ], importpath = "github.com/lestrrat-go/jwx/v3/jwk/jwkbb", visibility = ["//visibility:public"], deps = [ "@com_github_lestrrat_go_blackmagic//:blackmagic", + "@com_github_valyala_fastjson//:fastjson", + ], +) + +go_test( + name = "jwkbb_test", + srcs = ["header_test.go"], + deps = [ + ":jwkbb", + "@com_github_stretchr_testify//require", ], ) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/header.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/header.go new file mode 100644 index 0000000000..93a1202017 --- /dev/null +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/header.go @@ -0,0 +1,130 @@ +package jwkbb + +import ( + "fmt" + + "github.com/valyala/fastjson" +) + +type headerNotFoundError struct { + key string +} + +func (e headerNotFoundError) Error() string { + return fmt.Sprintf(`jwkbb: field "%s" not found`, e.key) +} + +func (e headerNotFoundError) Is(target error) bool { + switch target.(type) { + case headerNotFoundError, *headerNotFoundError: + return true + default: + return false + } +} + +// ErrHeaderNotFound returns an error that can be passed to errors.Is +// to check if the error is the result of a field not being found in +// the parsed JSON. +func ErrHeaderNotFound() error { + return headerNotFoundError{} +} + +// Header is an opaque handle to a parsed JWK or JWKS JSON object. +// It exists for fast, allocation-light field probing without paying +// the cost of a full encoding/json unmarshal. +// +// Header instances are NOT safe for concurrent use. Create a new one +// per goroutine. Values returned by HeaderGet* helpers may alias +// memory owned by the Header; do not retain them past the Header's +// lifetime unless the helper explicitly copies (HeaderGetString does; +// HeaderGetStringBytes does not). +// +// This type is experimental and may change or be removed in the future. +type Header interface { + // Sealed so callers can't depend on the underlying fastjson type + // or substitute their own implementation. + jwkbbHeader() +} + +type header struct { + v *fastjson.Value + err error +} + +func (h *header) jwkbbHeader() {} + +// HeaderParse parses a JSON byte slice and returns a Header for fast +// field access. Parse errors are deferred to the first HeaderGet* / +// HeaderHas call. +// +// This function is experimental and may change or be removed in the future. +func HeaderParse(buf []byte) Header { + var p fastjson.Parser + v, err := p.ParseBytes(buf) + if err != nil { + return &header{err: err} + } + return &header{v: v} +} + +func headerGet(h Header, key string) (*fastjson.Value, error) { + //nolint:forcetypeassert + hh := h.(*header) // we _know_ this can't be another type + if hh.err != nil { + return nil, hh.err + } + + v := hh.v.Get(key) + if v == nil { + return nil, headerNotFoundError{key: key} + } + return v, nil +} + +// HeaderHas reports whether the given key exists in the parsed JSON object. +// Returns false on parse errors. +// +// This function is experimental and may change or be removed in the future. +func HeaderHas(h Header, key string) bool { + _, err := headerGet(h, key) + return err == nil +} + +// HeaderGetString returns the string value for the given key as a +// freshly-allocated Go string. The returned value remains valid after +// the Header is garbage collected. +// +// This function is experimental and may change or be removed in the future. +func HeaderGetString(h Header, key string) (string, error) { + v, err := headerGet(h, key) + if err != nil { + return "", err + } + + sb, err := v.StringBytes() + if err != nil { + return "", err + } + + return string(sb), nil +} + +// HeaderGetStringBytes returns the JSON string bytes for the given key +// without copying. +// +// WARNING: the returned slice aliases memory owned by h. It becomes +// invalid as soon as h is reused, re-parsed, or goes out of scope and +// is garbage collected. Do not retain the slice, share it across +// goroutines, or use it after any further call on h. If you need a +// value that outlives h, use [HeaderGetString]. +// +// This function is experimental and may change or be removed in the future. +func HeaderGetStringBytes(h Header, key string) ([]byte, error) { + v, err := headerGet(h, key) + if err != nil { + return nil, err + } + + return v.StringBytes() +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/x509.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/x509.go index 3c827cfa6f..338d471bfb 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/x509.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/x509.go @@ -100,6 +100,9 @@ func DecodeX509(dst any, block *pem.Block) error { } return blackmagic.AssignIfCompatible(dst, key) case CertificateBlockType: + // Only the public key is extracted. Certificate validation (chain, + // expiration, CN/SAN, EKU, etc.) is intentionally not performed here; + // it is an application-level concern. See jwk.ParseKey documentation. cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return fmt.Errorf(`failed to parse certificate: %w`, err) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp.go index 7cbf66c2d8..ddbda60efa 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp.go @@ -7,16 +7,26 @@ import ( "crypto/ed25519" "fmt" "reflect" + "sync" - "github.com/lestrrat-go/blackmagic" "github.com/lestrrat-go/jwx/v3/internal/base64" "github.com/lestrrat-go/jwx/v3/jwa" ) func init() { - RegisterKeyExporter(jwa.OKP(), KeyExportFunc(okpJWKToRaw)) + RegisterKeyExporter(KeyKind(jwa.OKP().String()), KeyExportFunc(okpJWKToRaw)) } +func okpKeyKind(crv func() (jwa.EllipticCurveAlgorithm, bool)) KeyKind { + if c, ok := crv(); ok { + return KeyKind(jwa.OKP().String() + ":" + c.String()) + } + return KeyKind(jwa.OKP().String()) +} + +func (k *okpPublicKey) KeyKind() KeyKind { return okpKeyKind(k.Crv) } +func (k *okpPrivateKey) KeyKind() KeyKind { return okpKeyKind(k.Crv) } + // Mental note: // // Curve25519 refers to a particular curve, and is represented in its Montgomery form. @@ -40,14 +50,32 @@ func (k *okpPublicKey) Import(rawKeyIf any) error { var crv jwa.EllipticCurveAlgorithm switch rawKey := rawKeyIf.(type) { case ed25519.PublicKey: - k.x = rawKey crv = jwa.Ed25519() + if err := validateOKPPublicKeySize(crv, rawKey); err != nil { + return err + } + k.x = rawKey k.crv = &crv case *ecdh.PublicKey: - k.x = rawKey.Bytes() crv = jwa.X25519() + xbuf := rawKey.Bytes() + if err := validateOKPPublicKeySize(crv, xbuf); err != nil { + return err + } + k.x = xbuf k.crv = &crv default: + muOKPRawKeyImporters.RLock() + defer muOKPRawKeyImporters.RUnlock() + for _, fn := range okpRawKeyImporters { + c, x, _, ok := fn(rawKeyIf) + if ok { + k.x = x + crv = c + k.crv = &crv + return nil + } + } return fmt.Errorf(`unknown key type %T`, rawKeyIf) } @@ -61,24 +89,92 @@ func (k *okpPrivateKey) Import(rawKeyIf any) error { var crv jwa.EllipticCurveAlgorithm switch rawKey := rawKeyIf.(type) { case ed25519.PrivateKey: + crv = jwa.Ed25519() + if len(rawKey) != ed25519.PrivateKeySize { + return fmt.Errorf(`ed25519: wrong private key size`) + } k.d = rawKey.Seed() k.x = rawKey.Public().(ed25519.PublicKey) //nolint:forcetypeassert - crv = jwa.Ed25519() k.crv = &crv case *ecdh.PrivateKey: - // k.d = rawKey.Seed() - k.d = rawKey.Bytes() - k.x = rawKey.PublicKey().Bytes() crv = jwa.X25519() + dbuf := rawKey.Bytes() + if err := validateOKPPrivateKeySize(crv, dbuf); err != nil { + return err + } + xbuf := rawKey.PublicKey().Bytes() + if err := validateOKPPublicKeySize(crv, xbuf); err != nil { + return err + } + k.d = dbuf + k.x = xbuf k.crv = &crv default: + muOKPRawKeyImporters.RLock() + defer muOKPRawKeyImporters.RUnlock() + for _, fn := range okpRawKeyImporters { + c, x, d, ok := fn(rawKeyIf) + if ok { + k.x = x + k.d = d + crv = c + k.crv = &crv + return nil + } + } return fmt.Errorf(`unknown key type %T`, rawKeyIf) } return nil } +// OKPRawKeyImporter tries to import a raw key as an OKP key. +// Returns the curve, x, d (nil for public), and true if handled. +type OKPRawKeyImporter func(key any) (crv jwa.EllipticCurveAlgorithm, x, d []byte, ok bool) + +var muOKPRawKeyImporters sync.RWMutex +var okpRawKeyImporters []OKPRawKeyImporter + +// RegisterOKPRawKeyImporter registers a function that can import raw keys as OKP keys. +func RegisterOKPRawKeyImporter(fn OKPRawKeyImporter) { + muOKPRawKeyImporters.Lock() + defer muOKPRawKeyImporters.Unlock() + okpRawKeyImporters = append(okpRawKeyImporters, fn) +} + +func validateOKPPublicKeySize(alg jwa.EllipticCurveAlgorithm, xbuf []byte) error { + switch alg { + case jwa.Ed25519(): + if len(xbuf) != ed25519.PublicKeySize { + return fmt.Errorf(`ed25519: wrong public key size`) + } + case jwa.X25519(): + if len(xbuf) != 32 { + return fmt.Errorf(`x25519: wrong public key size`) + } + } + return nil +} + +func validateOKPPrivateKeySize(alg jwa.EllipticCurveAlgorithm, dbuf []byte) error { + switch alg { + case jwa.Ed25519(): + if len(dbuf) != ed25519.SeedSize { + return fmt.Errorf(`ed25519: wrong private key size`) + } + case jwa.X25519(): + if len(dbuf) != 32 { + return fmt.Errorf(`x25519: wrong private key size`) + } + } + return nil +} + func buildOKPPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf []byte) (any, error) { + if err := validateOKPPublicKeySize(alg, xbuf); err != nil { + return nil, err + } + switch alg { case jwa.Ed25519(): return ed25519.PublicKey(xbuf), nil @@ -93,36 +189,19 @@ func buildOKPPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf []byte) (any, error) } } -// Raw returns the EC-DSA public key represented by this JWK -func (k *okpPublicKey) Raw(v any) error { - k.mu.RLock() - defer k.mu.RUnlock() - - crv, ok := k.Crv() - if !ok { - return fmt.Errorf(`missing "crv" field`) - } - - pubk, err := buildOKPPublicKey(crv, k.x) - if err != nil { - return fmt.Errorf(`jwk.OKPPublicKey: failed to build public key: %w`, err) - } - - if err := blackmagic.AssignIfCompatible(v, pubk); err != nil { - return fmt.Errorf(`jwk.OKPPublicKey: failed to assign to destination variable: %w`, err) - } - return nil -} - func buildOKPPrivateKey(alg jwa.EllipticCurveAlgorithm, xbuf []byte, dbuf []byte) (any, error) { if len(dbuf) == 0 { return nil, fmt.Errorf(`cannot use empty seed`) } + if err := validateOKPPublicKeySize(alg, xbuf); err != nil { + return nil, err + } + if err := validateOKPPrivateKeySize(alg, dbuf); err != nil { + return nil, err + } + switch alg { case jwa.Ed25519(): - if len(dbuf) != ed25519.SeedSize { - return nil, fmt.Errorf(`ed25519: wrong private key size`) - } ret := ed25519.NewKeyFromSeed(dbuf) //nolint:forcetypeassert if !bytes.Equal(xbuf, ret.Public().(ed25519.PublicKey)) { @@ -154,24 +233,46 @@ func okpJWKToRaw(key Key, _ any /* this is unused because this is half baked */) switch key := extracted.(type) { case OKPPrivateKey: - locker, ok := key.(rlocker) - if ok { + // rlocker is unexported with unexported methods, so only our + // concrete types implement it. A successful assertion lets us + // type-assert to the concrete struct and read fields directly + // under a single batch lock. This avoids nested RLock (which + // deadlocks when a writer is pending) while preserving an + // atomic snapshot of all fields. + var crv jwa.EllipticCurveAlgorithm + var hasCrv bool + var x, d []byte + if locker, ok := key.(rlocker); ok { locker.rlock() - defer locker.runlock() + concrete := key.(*okpPrivateKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it + if concrete.crv != nil { + crv = *(concrete.crv) + hasCrv = true + } + x, d = concrete.x, concrete.d + locker.runlock() + } else { + // External implementation — use self-locking interface getters. + var ok bool + if crv, ok = key.Crv(); !ok { + return nil, fmt.Errorf(`missing "crv" field`) + } + hasCrv = true + if x, ok = key.X(); !ok { + return nil, fmt.Errorf(`missing "x" field`) + } + if d, ok = key.D(); !ok { + return nil, fmt.Errorf(`missing "d" field`) + } } - crv, ok := key.Crv() - if !ok { + if !hasCrv { return nil, fmt.Errorf(`missing "crv" field`) } - - x, ok := key.X() - if !ok { + if x == nil { return nil, fmt.Errorf(`missing "x" field`) } - - d, ok := key.D() - if !ok { + if d == nil { return nil, fmt.Errorf(`missing "d" field`) } @@ -181,21 +282,37 @@ func okpJWKToRaw(key Key, _ any /* this is unused because this is half baked */) } return privk, nil case OKPPublicKey: - locker, ok := key.(rlocker) - if ok { + // See OKPPrivateKey case above for explanation of the rlocker pattern. + var crv jwa.EllipticCurveAlgorithm + var hasCrv bool + var x []byte + if locker, ok := key.(rlocker); ok { locker.rlock() - defer locker.runlock() + concrete := key.(*okpPublicKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it + if concrete.crv != nil { + crv = *(concrete.crv) + hasCrv = true + } + x = concrete.x + locker.runlock() + } else { + var ok bool + if crv, ok = key.Crv(); !ok { + return nil, fmt.Errorf(`missing "crv" field`) + } + hasCrv = true + if x, ok = key.X(); !ok { + return nil, fmt.Errorf(`missing "x" field`) + } } - crv, ok := key.Crv() - if !ok { + if !hasCrv { return nil, fmt.Errorf(`missing "crv" field`) } - - x, ok := key.X() - if !ok { + if x == nil { return nil, fmt.Errorf(`missing "x" field`) } + pubk, err := buildOKPPublicKey(crv, x) if err != nil { return nil, fmt.Errorf(`jwk.OKPPublicKey: failed to build public key: %w`, err) @@ -249,7 +366,7 @@ func okpThumbprint(hash crypto.Hash, crv, x string) []byte { // Thumbprint returns the JWK thumbprint using the indicated // hashing algorithm, according to RFC 7638 / 8037 -func (k okpPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { +func (k *okpPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { k.mu.RLock() defer k.mu.RUnlock() @@ -266,7 +383,7 @@ func (k okpPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { // Thumbprint returns the JWK thumbprint using the indicated // hashing algorithm, according to RFC 7638 / 8037 -func (k okpPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) { +func (k *okpPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) { k.mu.RLock() defer k.mu.RUnlock() @@ -286,18 +403,27 @@ func validateOKPKey(key interface { Crv() (jwa.EllipticCurveAlgorithm, bool) X() ([]byte, bool) }) error { - if v, ok := key.Crv(); !ok || v == jwa.InvalidEllipticCurve() { + crv, ok := key.Crv() + if !ok || crv == jwa.InvalidEllipticCurve() { return fmt.Errorf(`invalid curve algorithm`) } - if v, ok := key.X(); !ok || len(v) == 0 { + x, ok := key.X() + if !ok || len(x) == 0 { return fmt.Errorf(`missing "x" field`) } + if err := validateOKPPublicKeySize(crv, x); err != nil { + return err + } if priv, ok := key.(keyWithD); ok { - if d, ok := priv.D(); !ok || len(d) == 0 { + d, ok := priv.D() + if !ok || len(d) == 0 { return fmt.Errorf(`missing "d" field`) } + if err := validateOKPPrivateKeySize(crv, d); err != nil { + return err + } } return nil } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp_gen.go index 0bde986147..3cfac02757 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp_gen.go @@ -15,6 +15,7 @@ import ( "github.com/lestrrat-go/jwx/v3/internal/pool" "github.com/lestrrat-go/jwx/v3/internal/tokens" "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk/internal/registry" ) const ( @@ -41,7 +42,7 @@ type okpPublicKey struct { x509CertThumbprintS256 *string // https://tools.ietf.org/html/rfc7515#section-4.1.8 x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5 privateParams map[string]any - mu *sync.RWMutex + mu sync.RWMutex dc json.DecodeCtx } @@ -50,28 +51,29 @@ var _ Key = &okpPublicKey{} func newOKPPublicKey() *okpPublicKey { return &okpPublicKey{ - mu: &sync.RWMutex{}, privateParams: make(map[string]any), } } -func (h okpPublicKey) KeyType() jwa.KeyType { +func (h *okpPublicKey) KeyType() jwa.KeyType { return jwa.OKP() } -func (h okpPublicKey) rlock() { +func (h *okpPublicKey) rlock() { h.mu.RLock() } -func (h okpPublicKey) runlock() { +func (h *okpPublicKey) runlock() { h.mu.RUnlock() } -func (h okpPublicKey) IsPrivate() bool { +func (h *okpPublicKey) IsPrivate() bool { return false } func (h *okpPublicKey) Algorithm() (jwa.KeyAlgorithm, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.algorithm != nil { return *(h.algorithm), true } @@ -79,6 +81,8 @@ func (h *okpPublicKey) Algorithm() (jwa.KeyAlgorithm, bool) { } func (h *okpPublicKey) Crv() (jwa.EllipticCurveAlgorithm, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.crv != nil { return *(h.crv), true } @@ -86,6 +90,8 @@ func (h *okpPublicKey) Crv() (jwa.EllipticCurveAlgorithm, bool) { } func (h *okpPublicKey) KeyID() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyID != nil { return *(h.keyID), true } @@ -93,6 +99,8 @@ func (h *okpPublicKey) KeyID() (string, bool) { } func (h *okpPublicKey) KeyOps() (KeyOperationList, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyOps != nil { return *(h.keyOps), true } @@ -100,6 +108,8 @@ func (h *okpPublicKey) KeyOps() (KeyOperationList, bool) { } func (h *okpPublicKey) KeyUsage() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyUsage != nil { return *(h.keyUsage), true } @@ -107,6 +117,8 @@ func (h *okpPublicKey) KeyUsage() (string, bool) { } func (h *okpPublicKey) X() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x != nil { return h.x, true } @@ -114,10 +126,17 @@ func (h *okpPublicKey) X() ([]byte, bool) { } func (h *okpPublicKey) X509CertChain() (*cert.Chain, bool) { - return h.x509CertChain, true + h.mu.RLock() + defer h.mu.RUnlock() + if h.x509CertChain != nil { + return h.x509CertChain, true + } + return nil, false } func (h *okpPublicKey) X509CertThumbprint() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509CertThumbprint != nil { return *(h.x509CertThumbprint), true } @@ -125,6 +144,8 @@ func (h *okpPublicKey) X509CertThumbprint() (string, bool) { } func (h *okpPublicKey) X509CertThumbprintS256() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509CertThumbprintS256 != nil { return *(h.x509CertThumbprintS256), true } @@ -132,6 +153,8 @@ func (h *okpPublicKey) X509CertThumbprintS256() (string, bool) { } func (h *okpPublicKey) X509URL() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509URL != nil { return *(h.x509URL), true } @@ -328,7 +351,12 @@ func (h *okpPublicKey) setNoLock(name string, value any) error { } case OKPXKey: if v, ok := value.([]byte); ok { - h.x = v + if v == nil { + h.x = nil + } else { + h.x = make([]byte, len(v)) + copy(h.x, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, OKPXKey, value) @@ -447,7 +475,7 @@ LOOP: case string: // Objects can only have string keys switch tok { case KeyTypeKey: - val, err := json.ReadNextStringToken(dec) + val, err := json.ReadNextStringToken(dec, h.dc) if err != nil { return fmt.Errorf(`error reading token: %w`, err) } @@ -471,7 +499,7 @@ LOOP: } h.crv = &decoded case KeyIDKey: - if err := json.AssignNextStringToken(&h.keyID, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyID, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err) } case KeyOpsKey: @@ -481,7 +509,7 @@ LOOP: } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } case OKPXKey: @@ -495,15 +523,15 @@ LOOP: } h.x509CertChain = &decoded case X509CertThumbprintKey: - if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err) } case X509CertThumbprintS256Key: - if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err) } case X509URLKey: - if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509URL, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err) } default: @@ -516,7 +544,7 @@ LOOP: } } } - decoded, err := registry.Decode(dec, tok) + decoded, err := fieldRegistry.Decode(dec, tok) if err == nil { h.setNoLock(tok, decoded) continue @@ -536,84 +564,135 @@ LOOP: return nil } -func (h okpPublicKey) MarshalJSON() ([]byte, error) { - data := make(map[string]any) - fields := make([]string, 0, 10) - data[KeyTypeKey] = jwa.OKP() - fields = append(fields, KeyTypeKey) +func (h *okpPublicKey) makePairs() ([]fieldPair, error) { + pairs := getFieldPairList() + h.mu.RLock() + defer h.mu.RUnlock() + { + v, err := json.Marshal(jwa.OKP()) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyTypeKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyTypeKey, Value: v}) + } if h.algorithm != nil { - data[AlgorithmKey] = *(h.algorithm) - fields = append(fields, AlgorithmKey) + v, err := json.Marshal(*(h.algorithm)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err) + } + pairs = append(pairs, fieldPair{Name: AlgorithmKey, Value: v}) } if h.crv != nil { - data[OKPCrvKey] = *(h.crv) - fields = append(fields, OKPCrvKey) + v, err := json.Marshal(*(h.crv)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, OKPCrvKey, err) + } + pairs = append(pairs, fieldPair{Name: OKPCrvKey, Value: v}) } if h.keyID != nil { - data[KeyIDKey] = *(h.keyID) - fields = append(fields, KeyIDKey) + v, err := json.Marshal(*(h.keyID)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyIDKey, Value: v}) } if h.keyOps != nil { - data[KeyOpsKey] = *(h.keyOps) - fields = append(fields, KeyOpsKey) + v, err := json.Marshal(*(h.keyOps)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyOpsKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyOpsKey, Value: v}) } if h.keyUsage != nil { - data[KeyUsageKey] = *(h.keyUsage) - fields = append(fields, KeyUsageKey) + v, err := json.Marshal(*(h.keyUsage)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyUsageKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyUsageKey, Value: v}) } if h.x != nil { - data[OKPXKey] = h.x - fields = append(fields, OKPXKey) + v, err := json.Marshal(base64.EncodeToString(h.x)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, OKPXKey, err) + } + pairs = append(pairs, fieldPair{Name: OKPXKey, Value: v}) } if h.x509CertChain != nil { - data[X509CertChainKey] = h.x509CertChain - fields = append(fields, X509CertChainKey) + v, err := json.Marshal(h.x509CertChain) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err) + } + pairs = append(pairs, fieldPair{Name: X509CertChainKey, Value: v}) } if h.x509CertThumbprint != nil { - data[X509CertThumbprintKey] = *(h.x509CertThumbprint) - fields = append(fields, X509CertThumbprintKey) + v, err := json.Marshal(*(h.x509CertThumbprint)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err) + } + pairs = append(pairs, fieldPair{Name: X509CertThumbprintKey, Value: v}) } if h.x509CertThumbprintS256 != nil { - data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256) - fields = append(fields, X509CertThumbprintS256Key) + v, err := json.Marshal(*(h.x509CertThumbprintS256)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err) + } + pairs = append(pairs, fieldPair{Name: X509CertThumbprintS256Key, Value: v}) } if h.x509URL != nil { - data[X509URLKey] = *(h.x509URL) - fields = append(fields, X509URLKey) + v, err := json.Marshal(*(h.x509URL)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err) + } + pairs = append(pairs, fieldPair{Name: X509URLKey, Value: v}) } for k, v := range h.privateParams { - data[k] = v - fields = append(fields, k) + var encoded []byte + switch v := v.(type) { + case []byte: + var err error + encoded, err = json.Marshal(base64.EncodeToString(v)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + default: + var err error + encoded, err = json.Marshal(v) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + } + pairs = append(pairs, fieldPair{Name: k, Value: encoded}) } - sort.Strings(fields) + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Name < pairs[j].Name + }) + + return pairs, nil +} + +func (h *okpPublicKey) MarshalJSON() ([]byte, error) { buf := pool.BytesBuffer().Get() defer pool.BytesBuffer().Put(buf) + pairs, err := h.makePairs() + if err != nil { + return nil, fmt.Errorf(`failed to make pairs: %w`, err) + } buf.WriteByte(tokens.OpenCurlyBracket) - enc := json.NewEncoder(buf) - for i, f := range fields { + + for i, pair := range pairs { if i > 0 { - buf.WriteRune(tokens.Comma) - } - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(f) - buf.WriteString(`":`) - v := data[f] - switch v := v.(type) { - case []byte: - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(base64.EncodeToString(v)) - buf.WriteRune(tokens.DoubleQuote) - default: - if err := enc.Encode(v); err != nil { - return nil, fmt.Errorf(`failed to encode value for field %s: %w`, f, err) - } - buf.Truncate(buf.Len() - 1) + buf.WriteByte(tokens.Comma) } + buf.WriteByte('"') + buf.WriteString(pair.Name) + buf.WriteString(`": `) + buf.Write(pair.Value.([]byte)) } buf.WriteByte(tokens.CloseCurlyBracket) ret := make([]byte, buf.Len()) copy(ret, buf.Bytes()) + putFieldPairList(pairs) return ret, nil } @@ -678,7 +757,7 @@ type okpPrivateKey struct { x509CertThumbprintS256 *string // https://tools.ietf.org/html/rfc7515#section-4.1.8 x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5 privateParams map[string]any - mu *sync.RWMutex + mu sync.RWMutex dc json.DecodeCtx } @@ -687,28 +766,29 @@ var _ Key = &okpPrivateKey{} func newOKPPrivateKey() *okpPrivateKey { return &okpPrivateKey{ - mu: &sync.RWMutex{}, privateParams: make(map[string]any), } } -func (h okpPrivateKey) KeyType() jwa.KeyType { +func (h *okpPrivateKey) KeyType() jwa.KeyType { return jwa.OKP() } -func (h okpPrivateKey) rlock() { +func (h *okpPrivateKey) rlock() { h.mu.RLock() } -func (h okpPrivateKey) runlock() { +func (h *okpPrivateKey) runlock() { h.mu.RUnlock() } -func (h okpPrivateKey) IsPrivate() bool { +func (h *okpPrivateKey) IsPrivate() bool { return true } func (h *okpPrivateKey) Algorithm() (jwa.KeyAlgorithm, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.algorithm != nil { return *(h.algorithm), true } @@ -716,6 +796,8 @@ func (h *okpPrivateKey) Algorithm() (jwa.KeyAlgorithm, bool) { } func (h *okpPrivateKey) Crv() (jwa.EllipticCurveAlgorithm, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.crv != nil { return *(h.crv), true } @@ -723,6 +805,8 @@ func (h *okpPrivateKey) Crv() (jwa.EllipticCurveAlgorithm, bool) { } func (h *okpPrivateKey) D() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.d != nil { return h.d, true } @@ -730,6 +814,8 @@ func (h *okpPrivateKey) D() ([]byte, bool) { } func (h *okpPrivateKey) KeyID() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyID != nil { return *(h.keyID), true } @@ -737,6 +823,8 @@ func (h *okpPrivateKey) KeyID() (string, bool) { } func (h *okpPrivateKey) KeyOps() (KeyOperationList, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyOps != nil { return *(h.keyOps), true } @@ -744,6 +832,8 @@ func (h *okpPrivateKey) KeyOps() (KeyOperationList, bool) { } func (h *okpPrivateKey) KeyUsage() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyUsage != nil { return *(h.keyUsage), true } @@ -751,6 +841,8 @@ func (h *okpPrivateKey) KeyUsage() (string, bool) { } func (h *okpPrivateKey) X() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x != nil { return h.x, true } @@ -758,10 +850,17 @@ func (h *okpPrivateKey) X() ([]byte, bool) { } func (h *okpPrivateKey) X509CertChain() (*cert.Chain, bool) { - return h.x509CertChain, true + h.mu.RLock() + defer h.mu.RUnlock() + if h.x509CertChain != nil { + return h.x509CertChain, true + } + return nil, false } func (h *okpPrivateKey) X509CertThumbprint() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509CertThumbprint != nil { return *(h.x509CertThumbprint), true } @@ -769,6 +868,8 @@ func (h *okpPrivateKey) X509CertThumbprint() (string, bool) { } func (h *okpPrivateKey) X509CertThumbprintS256() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509CertThumbprintS256 != nil { return *(h.x509CertThumbprintS256), true } @@ -776,6 +877,8 @@ func (h *okpPrivateKey) X509CertThumbprintS256() (string, bool) { } func (h *okpPrivateKey) X509URL() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509URL != nil { return *(h.x509URL), true } @@ -954,7 +1057,12 @@ func (h *okpPrivateKey) setNoLock(name string, value any) error { return fmt.Errorf(`invalid value for %s key: %T`, OKPCrvKey, value) case OKPDKey: if v, ok := value.([]byte); ok { - h.d = v + if v == nil { + h.d = nil + } else { + h.d = make([]byte, len(v)) + copy(h.d, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, OKPDKey, value) @@ -988,7 +1096,12 @@ func (h *okpPrivateKey) setNoLock(name string, value any) error { } case OKPXKey: if v, ok := value.([]byte); ok { - h.x = v + if v == nil { + h.x = nil + } else { + h.x = make([]byte, len(v)) + copy(h.x, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, OKPXKey, value) @@ -1077,7 +1190,7 @@ func (k *okpPrivateKey) SetDecodeCtx(dc json.DecodeCtx) { k.dc = dc } -func (h *okpPrivateKey) UnmarshalJSON(buf []byte) error { +func (h *okpPrivateKey) UnmarshalJSON(buf []byte) (retErr error) { h.mu.Lock() defer h.mu.Unlock() h.algorithm = nil @@ -1091,6 +1204,14 @@ func (h *okpPrivateKey) UnmarshalJSON(buf []byte) error { h.x509CertThumbprint = nil h.x509CertThumbprintS256 = nil h.x509URL = nil + defer func() { + if retErr != nil { + clear(h.d) + h.d = nil + clear(h.x) + h.x = nil + } + }() dec := json.NewDecoder(bytes.NewReader(buf)) LOOP: for { @@ -1110,7 +1231,7 @@ LOOP: case string: // Objects can only have string keys switch tok { case KeyTypeKey: - val, err := json.ReadNextStringToken(dec) + val, err := json.ReadNextStringToken(dec, h.dc) if err != nil { return fmt.Errorf(`error reading token: %w`, err) } @@ -1138,7 +1259,7 @@ LOOP: return fmt.Errorf(`failed to decode value for key %s: %w`, OKPDKey, err) } case KeyIDKey: - if err := json.AssignNextStringToken(&h.keyID, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyID, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err) } case KeyOpsKey: @@ -1148,7 +1269,7 @@ LOOP: } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } case OKPXKey: @@ -1162,15 +1283,15 @@ LOOP: } h.x509CertChain = &decoded case X509CertThumbprintKey: - if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err) } case X509CertThumbprintS256Key: - if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err) } case X509URLKey: - if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509URL, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err) } default: @@ -1183,7 +1304,7 @@ LOOP: } } } - decoded, err := registry.Decode(dec, tok) + decoded, err := fieldRegistry.Decode(dec, tok) if err == nil { h.setNoLock(tok, decoded) continue @@ -1206,88 +1327,142 @@ LOOP: return nil } -func (h okpPrivateKey) MarshalJSON() ([]byte, error) { - data := make(map[string]any) - fields := make([]string, 0, 11) - data[KeyTypeKey] = jwa.OKP() - fields = append(fields, KeyTypeKey) +func (h *okpPrivateKey) makePairs() ([]fieldPair, error) { + pairs := getFieldPairList() + h.mu.RLock() + defer h.mu.RUnlock() + { + v, err := json.Marshal(jwa.OKP()) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyTypeKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyTypeKey, Value: v}) + } if h.algorithm != nil { - data[AlgorithmKey] = *(h.algorithm) - fields = append(fields, AlgorithmKey) + v, err := json.Marshal(*(h.algorithm)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err) + } + pairs = append(pairs, fieldPair{Name: AlgorithmKey, Value: v}) } if h.crv != nil { - data[OKPCrvKey] = *(h.crv) - fields = append(fields, OKPCrvKey) + v, err := json.Marshal(*(h.crv)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, OKPCrvKey, err) + } + pairs = append(pairs, fieldPair{Name: OKPCrvKey, Value: v}) } if h.d != nil { - data[OKPDKey] = h.d - fields = append(fields, OKPDKey) + v, err := json.Marshal(base64.EncodeToString(h.d)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, OKPDKey, err) + } + pairs = append(pairs, fieldPair{Name: OKPDKey, Value: v}) } if h.keyID != nil { - data[KeyIDKey] = *(h.keyID) - fields = append(fields, KeyIDKey) + v, err := json.Marshal(*(h.keyID)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyIDKey, Value: v}) } if h.keyOps != nil { - data[KeyOpsKey] = *(h.keyOps) - fields = append(fields, KeyOpsKey) + v, err := json.Marshal(*(h.keyOps)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyOpsKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyOpsKey, Value: v}) } if h.keyUsage != nil { - data[KeyUsageKey] = *(h.keyUsage) - fields = append(fields, KeyUsageKey) + v, err := json.Marshal(*(h.keyUsage)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyUsageKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyUsageKey, Value: v}) } if h.x != nil { - data[OKPXKey] = h.x - fields = append(fields, OKPXKey) + v, err := json.Marshal(base64.EncodeToString(h.x)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, OKPXKey, err) + } + pairs = append(pairs, fieldPair{Name: OKPXKey, Value: v}) } if h.x509CertChain != nil { - data[X509CertChainKey] = h.x509CertChain - fields = append(fields, X509CertChainKey) + v, err := json.Marshal(h.x509CertChain) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err) + } + pairs = append(pairs, fieldPair{Name: X509CertChainKey, Value: v}) } if h.x509CertThumbprint != nil { - data[X509CertThumbprintKey] = *(h.x509CertThumbprint) - fields = append(fields, X509CertThumbprintKey) + v, err := json.Marshal(*(h.x509CertThumbprint)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err) + } + pairs = append(pairs, fieldPair{Name: X509CertThumbprintKey, Value: v}) } if h.x509CertThumbprintS256 != nil { - data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256) - fields = append(fields, X509CertThumbprintS256Key) + v, err := json.Marshal(*(h.x509CertThumbprintS256)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err) + } + pairs = append(pairs, fieldPair{Name: X509CertThumbprintS256Key, Value: v}) } if h.x509URL != nil { - data[X509URLKey] = *(h.x509URL) - fields = append(fields, X509URLKey) + v, err := json.Marshal(*(h.x509URL)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err) + } + pairs = append(pairs, fieldPair{Name: X509URLKey, Value: v}) } for k, v := range h.privateParams { - data[k] = v - fields = append(fields, k) + var encoded []byte + switch v := v.(type) { + case []byte: + var err error + encoded, err = json.Marshal(base64.EncodeToString(v)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + default: + var err error + encoded, err = json.Marshal(v) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + } + pairs = append(pairs, fieldPair{Name: k, Value: encoded}) } - sort.Strings(fields) + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Name < pairs[j].Name + }) + + return pairs, nil +} + +func (h *okpPrivateKey) MarshalJSON() ([]byte, error) { buf := pool.BytesBuffer().Get() defer pool.BytesBuffer().Put(buf) + pairs, err := h.makePairs() + if err != nil { + return nil, fmt.Errorf(`failed to make pairs: %w`, err) + } buf.WriteByte(tokens.OpenCurlyBracket) - enc := json.NewEncoder(buf) - for i, f := range fields { + + for i, pair := range pairs { if i > 0 { - buf.WriteRune(tokens.Comma) - } - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(f) - buf.WriteString(`":`) - v := data[f] - switch v := v.(type) { - case []byte: - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(base64.EncodeToString(v)) - buf.WriteRune(tokens.DoubleQuote) - default: - if err := enc.Encode(v); err != nil { - return nil, fmt.Errorf(`failed to encode value for field %s: %w`, f, err) - } - buf.Truncate(buf.Len() - 1) + buf.WriteByte(tokens.Comma) } + buf.WriteByte('"') + buf.WriteString(pair.Name) + buf.WriteString(`": `) + buf.Write(pair.Value.([]byte)) } buf.WriteByte(tokens.CloseCurlyBracket) ret := make([]byte, buf.Len()) copy(ret, buf.Bytes()) + putFieldPairList(pairs) return ret, nil } @@ -1345,3 +1520,10 @@ func init() { func OKPStandardFieldsFilter() KeyFilter { return okpStandardFields } + +func init() { + registry.Register(jwa.OKP().String(), registry.Constructor{ + Public: func() any { return newOKPPublicKey() }, + Private: func() any { return newOKPPrivateKey() }, + }) +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/options.yaml b/vendor/github.com/lestrrat-go/jwx/v3/jwk/options.yaml index 879dcba158..765f3ea8e9 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/options.yaml +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/options.yaml @@ -45,16 +45,79 @@ interfaces: comment: | GlobalOption is a type of Option that can be passed to the `jwk.Configure()` to change the global configuration of the jwk package. + - name: GlobalParseOption + methods: + - globalOption + - fetchOption + - registerOption + - readFileOption + comment: | + GlobalParseOption describes an Option that can be passed to both + `jwk.Configure()` (to change the default globally) and + `jwk.Parse()` / `jwk.ParseReader()` / `jwk.ParseString()` (to + override per call). + - name: PublicSetOption + comment: | + PublicSetOption is a type of Option that can be passed to `jwk.PublicSetOf()` + - name: GlobalFetchOption + methods: + - globalOption + - fetchOption + - registerOption + - parseOption + comment: | + GlobalFetchOption describes an Option that can be passed to `jwk.Configure()`, + `jwk.Fetch()`, and `(*jwk.Cache).Register()`. options: - ident: HTTPClient - interface: RegisterFetchOption + interface: GlobalFetchOption argument_type: HTTPClient comment: | WithHTTPClient allows users to specify the "net/http".Client object that is used when fetching jwk.Set objects. + + When passed to `jwk.Configure()`, it sets the global default HTTP client + used by `jwk.Fetch()`. By default, `jwk.Fetch()` uses an HTTP client with + a 30-second timeout and a redirect policy that blocks HTTPS-to-HTTP + scheme downgrades (with a maximum of 5 redirects) instead of + `http.DefaultClient` (which has no timeout and allows up to 10 redirects). + + The client is used as-is: the library does NOT automatically apply its + default timeout or redirect policy to a user-supplied client. If you want + to bring your own client (e.g. for custom TLS or proxy settings) while + retaining the library's defaults, wrap it with `jwk.WrapHTTPClientDefaults()` + before passing it to this option. + + For full SSRF protection (blocking redirects to private IPs, DNS + rebinding prevention), provide a custom http.Client with an appropriate + Transport.DialContext that validates resolved IP addresses. + + Users can override the client per-call via `jwk.Fetch()` or per-resource + via `(*jwk.Cache).Register()`. + - ident: MaxFetchBodySize + interface: GlobalFetchOption + argument_type: int64 + comment: | + WithMaxFetchBodySize specifies the maximum number of bytes to read from + an HTTP response body when fetching a JWKS. If the response body exceeds + this size, the fetch returns an error. The default value is 10MB (10485760). + + This option can be passed to `jwk.Configure()` to change the default + globally, or to `jwk.Fetch()` / `(*jwk.Cache).Register()` for a per-call + override. - ident: ThumbprintHash interface: AssignKeyIDOption argument_type: crypto.Hash + - ident: ForceAssign + interface: AssignKeyIDOption + argument_type: bool + comment: | + WithForceAssign forces `jwk.AssignKeyID` to recompute and overwrite + the `kid` header even when the key already has one. The default + behavior preserves any existing `kid`; use this option to upgrade + the thumbprint hash (e.g. with `jwk.WithThumbprintHash`) or to + refresh a `kid` after mutating a key field that invalidates the + cached thumbprint. - ident: LocalRegistry option_name: withLocalRegistry interface: ParseOption @@ -86,9 +149,25 @@ options: interface: FetchOption argument_type: Whitelist comment: | - WithFetchWhitelist specifies the Whitelist object to use when - fetching JWKs from a remote source. This option can be passed - to both `jwk.Fetch()` + WithFetchWhitelist specifies the Whitelist applied to the URL passed + to `jwk.Fetch()` (and `(*jwk.Cache).Register()`). + + The default when this option is not supplied is `jwk.InsecureWhitelist{}`, + which allows every URL. That is the right default for URLs that are + hard-coded in your program or loaded from trusted configuration, and + keeps first-time usage free of boilerplate. + + It is NOT safe when the URL comes from an untrusted source — most + commonly the `jku` header of a JWS handed to you by a peer. For those + call sites you MUST supply a restrictive Whitelist: use + `jwk.NewMapWhitelist()` for a fixed allow-list, `jwk.RegexpWhitelist` + for pattern-based allow-lists, or implement the `jwk.Whitelist` + interface yourself. + + Note that a whitelist only constrains the initial URL. For defense + against redirect-to-private-IP and DNS-rebinding attacks, also supply + a custom `http.Client` via `jwk.WithHTTPClient` whose + `Transport.DialContext` validates resolved addresses. - ident: IgnoreParseError interface: ParseOption argument_type: bool @@ -140,4 +219,80 @@ options: If this option is set to false, then the "use" field can be any value. If this options is set to true, then the "use" field must be one of the registered values, and otherwise an error will be - reported during parsing / assignment to `jwk.KeyUsageType` \ No newline at end of file + reported during parsing / assignment to `jwk.KeyUsageType` + - ident: MinRSAModulusBits + interface: GlobalOption + argument_type: int + comment: | + WithMinRSAModulusBits specifies the minimum RSA modulus size, in bits, + accepted by JWK validation and raw/PEM/X.509 import. + + The default is 2048. Lower this only for legacy interoperability with + older key material. A value of 0 disables the modulus-size floor. + - ident: MinRSAPublicExponent + interface: GlobalOption + argument_type: int + comment: | + WithMinRSAPublicExponent specifies the minimum RSA public exponent + accepted by JWK validation and raw/PEM/X.509 import. + + The default is 3. The exponent must still be odd and fit in a Go `int`. + Lower this only for legacy interoperability. A value of 0 disables the + minimum-exponent floor. + - ident: MaxKeys + interface: GlobalParseOption + argument_type: int + comment: | + WithMaxKeys specifies the maximum number of keys allowed in a JWK + set passed to `jwk.Parse()` / `jwk.ParseReader()` / `jwk.ParseString()`. + If the "keys" array of a JSON-encoded JWKS, or the number of PEM + blocks in a PEM/X.509-encoded input, exceeds this value, parsing + returns an error. The default is 1000. + + This option can be passed to `jwk.Configure()` to change the + default globally, or to `jwk.Parse()` / `jwk.ParseReader()` / + `jwk.ParseString()` for a per-call override. A non-positive + value is rejected. + + The cap defends against amplification: each entry triggers a + probe + unmarshal + validation, each of which allocates. + Bounding raw input bytes remains the caller's responsibility. + - ident: RejectDuplicateKID + interface: GlobalParseOption + argument_type: bool + comment: | + WithRejectDuplicateKID instructs `jwk.Parse()` / + `jwk.ParseReader()` / `jwk.ParseString()` and + `Set.UnmarshalJSON()` to return an error when the JWKS contains + two or more keys with the same non-empty `kid`. Keys without a + kid are not considered. + + Default is false — first-match-wins is retained for + compatibility with RFC 7517 (which permits, but does not + mandate, unique kids) and with existing callers that rely on + `(jwk.Set).LookupKeyID` returning the first entry. Use this + option when your issuer should guarantee kid uniqueness and a + duplicate is a sign of misconfiguration worth surfacing at + parse time rather than at verify time. + + Can be set globally via `jwk.Configure()` or per-call on + `jwk.Parse()` / `jwk.ParseReader()` / `jwk.ParseString()`. + + This does not affect `(*Set).AddKey` — programmatic additions + remain permissive (AddKey dedupes only by pointer identity). + - ident: AllowSymmetric + interface: PublicSetOption + argument_type: bool + comment: | + WithAllowSymmetric controls whether `jwk.PublicSetOf` tolerates + symmetric (oct) keys in the input set. + + By default this option is false: a symmetric key in the input is + an error, because a symmetric key has no public form — its + "public" representation is the secret itself. Passing such a set + through `PublicSetOf` silently and then publishing the result + (e.g. as `/.well-known/jwks.json`) would leak HMAC secret material. + + Pass `WithAllowSymmetric(true)` only if you are certain the + resulting set will not be published. When true, symmetric keys + are passed through unchanged, matching the legacy behavior. diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/options_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/options_gen.go index 99e66c3e7e..e90a9ee8d4 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/options_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/options_gen.go @@ -56,6 +56,28 @@ func (*fetchOption) parseOption() {} func (*fetchOption) registerOption() {} +// GlobalFetchOption describes an Option that can be passed to `jwk.Configure()`, +// `jwk.Fetch()`, and `(*jwk.Cache).Register()`. +type GlobalFetchOption interface { + Option + globalOption() + fetchOption() + registerOption() + parseOption() +} + +type globalFetchOption struct { + Option +} + +func (*globalFetchOption) globalOption() {} + +func (*globalFetchOption) fetchOption() {} + +func (*globalFetchOption) registerOption() {} + +func (*globalFetchOption) parseOption() {} + // GlobalOption is a type of Option that can be passed to the `jwk.Configure()` to // change the global configuration of the jwk package. type GlobalOption interface { @@ -69,6 +91,30 @@ type globalOption struct { func (*globalOption) globalOption() {} +// GlobalParseOption describes an Option that can be passed to both +// `jwk.Configure()` (to change the default globally) and +// `jwk.Parse()` / `jwk.ParseReader()` / `jwk.ParseString()` (to +// override per call). +type GlobalParseOption interface { + Option + globalOption() + fetchOption() + registerOption() + readFileOption() +} + +type globalParseOption struct { + Option +} + +func (*globalParseOption) globalOption() {} + +func (*globalParseOption) fetchOption() {} + +func (*globalParseOption) registerOption() {} + +func (*globalParseOption) readFileOption() {} + // ParseOption is a type of Option that can be passed to `jwk.Parse()` // ParseOption also implements the `ReadFileOption` and `NewCacheOption`, // and thus safely be passed to `jwk.ReadFile` and `(*jwk.Cache).Configure()` @@ -89,6 +135,18 @@ func (*parseOption) registerOption() {} func (*parseOption) readFileOption() {} +// PublicSetOption is a type of Option that can be passed to `jwk.PublicSetOf()` +type PublicSetOption interface { + Option + publicSetOption() +} + +type publicSetOption struct { + Option +} + +func (*publicSetOption) publicSetOption() {} + // ReadFileOption is a type of `Option` that can be passed to `jwk.ReadFile` type ReadFileOption interface { Option @@ -144,18 +202,29 @@ type resourceOption struct { func (*resourceOption) resourceOption() {} +type identAllowSymmetric struct{} type identFS struct{} type identFetchWhitelist struct{} +type identForceAssign struct{} type identHTTPClient struct{} type identIgnoreParseError struct{} type identLocalRegistry struct{} +type identMaxFetchBodySize struct{} +type identMaxKeys struct{} +type identMinRSAModulusBits struct{} +type identMinRSAPublicExponent struct{} type identPEM struct{} type identPEMDecoder struct{} +type identRejectDuplicateKID struct{} type identStrictKeyUsage struct{} type identThumbprintHash struct{} type identWaitReady struct{} type identX509 struct{} +func (identAllowSymmetric) String() string { + return "WithAllowSymmetric" +} + func (identFS) String() string { return "WithFS" } @@ -164,6 +233,10 @@ func (identFetchWhitelist) String() string { return "WithFetchWhitelist" } +func (identForceAssign) String() string { + return "WithForceAssign" +} + func (identHTTPClient) String() string { return "WithHTTPClient" } @@ -176,6 +249,22 @@ func (identLocalRegistry) String() string { return "withLocalRegistry" } +func (identMaxFetchBodySize) String() string { + return "WithMaxFetchBodySize" +} + +func (identMaxKeys) String() string { + return "WithMaxKeys" +} + +func (identMinRSAModulusBits) String() string { + return "WithMinRSAModulusBits" +} + +func (identMinRSAPublicExponent) String() string { + return "WithMinRSAPublicExponent" +} + func (identPEM) String() string { return "WithPEM" } @@ -184,6 +273,10 @@ func (identPEMDecoder) String() string { return "WithPEMDecoder" } +func (identRejectDuplicateKID) String() string { + return "WithRejectDuplicateKID" +} + func (identStrictKeyUsage) String() string { return "WithStrictKeyUsage" } @@ -200,22 +293,83 @@ func (identX509) String() string { return "WithX509" } +// WithAllowSymmetric controls whether `jwk.PublicSetOf` tolerates +// symmetric (oct) keys in the input set. +// +// By default this option is false: a symmetric key in the input is +// an error, because a symmetric key has no public form — its +// "public" representation is the secret itself. Passing such a set +// through `PublicSetOf` silently and then publishing the result +// (e.g. as `/.well-known/jwks.json`) would leak HMAC secret material. +// +// Pass `WithAllowSymmetric(true)` only if you are certain the +// resulting set will not be published. When true, symmetric keys +// are passed through unchanged, matching the legacy behavior. +func WithAllowSymmetric(v bool) PublicSetOption { + return &publicSetOption{option.New(identAllowSymmetric{}, v)} +} + // WithFS specifies the source `fs.FS` object to read the file from. func WithFS(v fs.FS) ReadFileOption { return &readFileOption{option.New(identFS{}, v)} } -// WithFetchWhitelist specifies the Whitelist object to use when -// fetching JWKs from a remote source. This option can be passed -// to both `jwk.Fetch()` +// WithFetchWhitelist specifies the Whitelist applied to the URL passed +// to `jwk.Fetch()` (and `(*jwk.Cache).Register()`). +// +// The default when this option is not supplied is `jwk.InsecureWhitelist{}`, +// which allows every URL. That is the right default for URLs that are +// hard-coded in your program or loaded from trusted configuration, and +// keeps first-time usage free of boilerplate. +// +// It is NOT safe when the URL comes from an untrusted source — most +// commonly the `jku` header of a JWS handed to you by a peer. For those +// call sites you MUST supply a restrictive Whitelist: use +// `jwk.NewMapWhitelist()` for a fixed allow-list, `jwk.RegexpWhitelist` +// for pattern-based allow-lists, or implement the `jwk.Whitelist` +// interface yourself. +// +// Note that a whitelist only constrains the initial URL. For defense +// against redirect-to-private-IP and DNS-rebinding attacks, also supply +// a custom `http.Client` via `jwk.WithHTTPClient` whose +// `Transport.DialContext` validates resolved addresses. func WithFetchWhitelist(v Whitelist) FetchOption { return &fetchOption{option.New(identFetchWhitelist{}, v)} } +// WithForceAssign forces `jwk.AssignKeyID` to recompute and overwrite +// the `kid` header even when the key already has one. The default +// behavior preserves any existing `kid`; use this option to upgrade +// the thumbprint hash (e.g. with `jwk.WithThumbprintHash`) or to +// refresh a `kid` after mutating a key field that invalidates the +// cached thumbprint. +func WithForceAssign(v bool) AssignKeyIDOption { + return &assignKeyIDOption{option.New(identForceAssign{}, v)} +} + // WithHTTPClient allows users to specify the "net/http".Client object that // is used when fetching jwk.Set objects. -func WithHTTPClient(v HTTPClient) RegisterFetchOption { - return ®isterFetchOption{option.New(identHTTPClient{}, v)} +// +// When passed to `jwk.Configure()`, it sets the global default HTTP client +// used by `jwk.Fetch()`. By default, `jwk.Fetch()` uses an HTTP client with +// a 30-second timeout and a redirect policy that blocks HTTPS-to-HTTP +// scheme downgrades (with a maximum of 5 redirects) instead of +// `http.DefaultClient` (which has no timeout and allows up to 10 redirects). +// +// The client is used as-is: the library does NOT automatically apply its +// default timeout or redirect policy to a user-supplied client. If you want +// to bring your own client (e.g. for custom TLS or proxy settings) while +// retaining the library's defaults, wrap it with `jwk.WrapHTTPClientDefaults()` +// before passing it to this option. +// +// For full SSRF protection (blocking redirects to private IPs, DNS +// rebinding prevention), provide a custom http.Client with an appropriate +// Transport.DialContext that validates resolved IP addresses. +// +// Users can override the client per-call via `jwk.Fetch()` or per-resource +// via `(*jwk.Cache).Register()`. +func WithHTTPClient(v HTTPClient) GlobalFetchOption { + return &globalFetchOption{option.New(identHTTPClient{}, v)} } // WithIgnoreParseError is only applicable when used with `jwk.Parse()` @@ -246,6 +400,54 @@ func withLocalRegistry(v *json.Registry) ParseOption { return &parseOption{option.New(identLocalRegistry{}, v)} } +// WithMaxFetchBodySize specifies the maximum number of bytes to read from +// an HTTP response body when fetching a JWKS. If the response body exceeds +// this size, the fetch returns an error. The default value is 10MB (10485760). +// +// This option can be passed to `jwk.Configure()` to change the default +// globally, or to `jwk.Fetch()` / `(*jwk.Cache).Register()` for a per-call +// override. +func WithMaxFetchBodySize(v int64) GlobalFetchOption { + return &globalFetchOption{option.New(identMaxFetchBodySize{}, v)} +} + +// WithMaxKeys specifies the maximum number of keys allowed in a JWK +// set passed to `jwk.Parse()` / `jwk.ParseReader()` / `jwk.ParseString()`. +// If the "keys" array of a JSON-encoded JWKS, or the number of PEM +// blocks in a PEM/X.509-encoded input, exceeds this value, parsing +// returns an error. The default is 1000. +// +// This option can be passed to `jwk.Configure()` to change the +// default globally, or to `jwk.Parse()` / `jwk.ParseReader()` / +// `jwk.ParseString()` for a per-call override. A non-positive +// value is rejected. +// +// The cap defends against amplification: each entry triggers a +// probe + unmarshal + validation, each of which allocates. +// Bounding raw input bytes remains the caller's responsibility. +func WithMaxKeys(v int) GlobalParseOption { + return &globalParseOption{option.New(identMaxKeys{}, v)} +} + +// WithMinRSAModulusBits specifies the minimum RSA modulus size, in bits, +// accepted by JWK validation and raw/PEM/X.509 import. +// +// The default is 2048. Lower this only for legacy interoperability with +// older key material. A value of 0 disables the modulus-size floor. +func WithMinRSAModulusBits(v int) GlobalOption { + return &globalOption{option.New(identMinRSAModulusBits{}, v)} +} + +// WithMinRSAPublicExponent specifies the minimum RSA public exponent +// accepted by JWK validation and raw/PEM/X.509 import. +// +// The default is 3. The exponent must still be odd and fit in a Go `int`. +// Lower this only for legacy interoperability. A value of 0 disables the +// minimum-exponent floor. +func WithMinRSAPublicExponent(v int) GlobalOption { + return &globalOption{option.New(identMinRSAPublicExponent{}, v)} +} + // WithPEM specifies that the input to `Parse()` is a PEM encoded key. // // This option is planned to be deprecated in the future. The plan is to @@ -263,6 +465,29 @@ func WithPEMDecoder(v PEMDecoder) ParseOption { return &parseOption{option.New(identPEMDecoder{}, v)} } +// WithRejectDuplicateKID instructs `jwk.Parse()` / +// `jwk.ParseReader()` / `jwk.ParseString()` and +// `Set.UnmarshalJSON()` to return an error when the JWKS contains +// two or more keys with the same non-empty `kid`. Keys without a +// kid are not considered. +// +// Default is false — first-match-wins is retained for +// compatibility with RFC 7517 (which permits, but does not +// mandate, unique kids) and with existing callers that rely on +// `(jwk.Set).LookupKeyID` returning the first entry. Use this +// option when your issuer should guarantee kid uniqueness and a +// duplicate is a sign of misconfiguration worth surfacing at +// parse time rather than at verify time. +// +// Can be set globally via `jwk.Configure()` or per-call on +// `jwk.Parse()` / `jwk.ParseReader()` / `jwk.ParseString()`. +// +// This does not affect `(*Set).AddKey` — programmatic additions +// remain permissive (AddKey dedupes only by pointer identity). +func WithRejectDuplicateKID(v bool) GlobalParseOption { + return &globalParseOption{option.New(identRejectDuplicateKID{}, v)} +} + // WithStrictKeyUsage specifies if during JWK parsing, the "use" field // should be confined to the values that have been registered via // `jwk.RegisterKeyType()`. By default this option is true, and the diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/parser.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/parser.go index fa8764ef72..2969fd3078 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/parser.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/parser.go @@ -21,7 +21,7 @@ type KeyParser interface { // If your KeyParser decides that the payload is not something // you can parse, and you would like to continue parsing with // the remaining KeyParser instances that are registered, - // return a `jwk.ContinueParseError`. Any other errors will immediately + // return a `jwk.ContinueError()`. Any other errors will immediately // halt the parsing process. // // When unmarshaling JSON, use the unmarshaler object supplied as @@ -89,6 +89,15 @@ func defaultParseKey(probe *KeyProbe, unmarshaler KeyUnmarshaler, data []byte) ( if err := unmarshaler.UnmarshalKey(data, key); err != nil { return nil, fmt.Errorf(`failed to unmarshal JSON into key (%T): %w`, key, err) } + // Enforce the trust boundary: a key that fails its own Validate() must + // never escape Parse/ParseKey. All built-in key types implement this + // interface via NewKeyValidationError, so callers can still use + // jwk.IsKeyValidationError on the wrapped error. + if v, ok := key.(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return nil, fmt.Errorf(`jwk.Parse: key validation failed: %w`, err) + } + } return key, nil } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa.go index ca27681587..ecb74edfc1 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa.go @@ -6,7 +6,9 @@ import ( "encoding/binary" "fmt" "math/big" + "math/bits" "reflect" + "sync/atomic" "github.com/lestrrat-go/jwx/v3/internal/base64" "github.com/lestrrat-go/jwx/v3/internal/pool" @@ -14,7 +16,27 @@ import ( ) func init() { - RegisterKeyExporter(jwa.RSA(), KeyExportFunc(rsaJWKToRaw)) + RegisterKeyExporter(KeyKind(jwa.RSA().String()), KeyExportFunc(rsaJWKToRaw)) +} + +const minRSAModulusBits = 2048 +const minRSAPublicExponent = 3 + +var rsaMinModulusBits = atomic.Int64{} +var rsaMinPublicExponent atomic.Pointer[big.Int] + +func init() { + rsaMinModulusBits.Store(minRSAModulusBits) + setMinRSAPublicExponent(minRSAPublicExponent) +} + +func setMinRSAPublicExponent(v int) { + if v <= 0 { + rsaMinPublicExponent.Store(nil) + return + } + + rsaMinPublicExponent.Store(big.NewInt(int64(v))) } func (k *rsaPrivateKey) Import(rawKey *rsa.PrivateKey) error { @@ -76,6 +98,9 @@ func importRsaPublicKeyByteValues(rawKey *rsa.PublicKey) ([]byte, []byte, error) if err != nil { return nil, nil, fmt.Errorf(`invalid rsa.PublicKey: %w`, err) } + if rawKey.E <= 0 { + return nil, nil, fmt.Errorf(`invalid rsa.PublicKey: invalid rsa public exponent: must be a positive odd integer`) + } data := make([]byte, 8) binary.BigEndian.PutUint64(data, uint64(rawKey.E)) @@ -102,16 +127,51 @@ func (k *rsaPublicKey) Import(rawKey *rsa.PublicKey) error { return nil } -func buildRSAPublicKey(key *rsa.PublicKey, n, e []byte) { - bin := pool.BigInt().Get() - bie := pool.BigInt().Get() - defer pool.BigInt().Put(bie) +func validateRSAModulusAndExponent(n, e []byte) (*big.Int, error) { + n = trimLeadingZeroBytes(n) + if len(n) == 0 { + return nil, fmt.Errorf(`missing "n" value`) + } + + bigN := new(big.Int).SetBytes(n) + minBits := int(rsaMinModulusBits.Load()) + if minBits > 0 && bigN.BitLen() < minBits { + return nil, fmt.Errorf(`rsa modulus too small: got %d bits, need at least %d`, bigN.BitLen(), minBits) + } + + e = trimLeadingZeroBytes(e) + if len(e) == 0 { + return nil, fmt.Errorf(`missing "e" value`) + } + + bigE := new(big.Int).SetBytes(e) + minExponent := rsaMinPublicExponent.Load() + if bigE.Sign() <= 0 || bigE.Bit(0) == 0 { + return nil, fmt.Errorf(`invalid rsa public exponent: must be a positive odd integer`) + } + if minExponent != nil && bigE.Cmp(minExponent) < 0 { + return nil, fmt.Errorf(`invalid rsa public exponent: got %s, need at least %s`, bigE.String(), minExponent.String()) + } - bin.SetBytes(n) - bie.SetBytes(e) + // rsa.PublicKey.E is a Go int. Reject exponents that do not fit on the + // current platform (e.g. GOARCH=386). Without this guard, Int64()/int() + // silently truncates, causing the materialized key to disagree with the + // JSON bytes and breaking RFC 7638 thumbprint uniqueness. + if bigE.BitLen() >= bits.UintSize { + return nil, fmt.Errorf(`rsa public exponent too large for this platform: %d bits (max %d)`, bigE.BitLen(), bits.UintSize-1) + } - key.N = bin - key.E = int(bie.Int64()) + return bigE, nil +} + +func buildRSAPublicKey(key *rsa.PublicKey, n, e []byte) error { + bigE, err := validateRSAModulusAndExponent(n, e) + if err != nil { + return err + } + key.N = new(big.Int).SetBytes(trimLeadingZeroBytes(n)) + key.E = int(bigE.Int64()) + return nil } var rsaConvertibleKeys = []reflect.Type{ @@ -132,26 +192,67 @@ func rsaJWKToRaw(key Key, hint any) (any, error) { return nil, fmt.Errorf(`invalid destination object type %T for private RSA JWK: %w`, hint, ContinueError()) } - locker, ok := key.(rlocker) - if !ok { + // rlocker is unexported with unexported methods, so only our + // concrete types implement it. A successful assertion lets us + // type-assert to the concrete struct and read fields directly + // under a single batch lock. This avoids nested RLock (which + // deadlocks when a writer is pending) while preserving an + // atomic snapshot of all fields. + var od, oq, op, on, oe []byte + var odp, odq, oqi []byte + var hasDp, hasDq, hasQi bool + if locker, ok := key.(rlocker); ok { locker.rlock() - defer locker.runlock() + concrete := key.(*rsaPrivateKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it + od, oq, op, on, oe = concrete.d, concrete.q, concrete.p, concrete.n, concrete.e + if concrete.dp != nil { + odp, hasDp = concrete.dp, true + } + if concrete.dq != nil { + odq, hasDq = concrete.dq, true + } + if concrete.qi != nil { + oqi, hasQi = concrete.qi, true + } + locker.runlock() + } else { + // External implementation — use self-locking interface getters. + var ok bool + if od, ok = key.D(); !ok { + return nil, fmt.Errorf(`missing "d" value`) + } + if oq, ok = key.Q(); !ok { + return nil, fmt.Errorf(`missing "q" value`) + } + if op, ok = key.P(); !ok { + return nil, fmt.Errorf(`missing "p" value`) + } + if on, ok = key.N(); !ok { + return nil, fmt.Errorf(`missing "n" value`) + } + if oe, ok = key.E(); !ok { + return nil, fmt.Errorf(`missing "e" value`) + } + odp, hasDp = key.DP() + odq, hasDq = key.DQ() + oqi, hasQi = key.QI() } - od, ok := key.D() - if !ok { + if od == nil { return nil, fmt.Errorf(`missing "d" value`) } - - oq, ok := key.Q() - if !ok { + if oq == nil { return nil, fmt.Errorf(`missing "q" value`) } - - op, ok := key.P() - if !ok { + if op == nil { return nil, fmt.Errorf(`missing "p" value`) } + if on == nil { + return nil, fmt.Errorf(`missing "n" value`) + } + if oe == nil { + return nil, fmt.Errorf(`missing "e" value`) + } var d, q, p big.Int // note: do not use from sync.Pool @@ -159,36 +260,27 @@ func rsaJWKToRaw(key Key, hint any) (any, error) { q.SetBytes(oq) p.SetBytes(op) - // optional fields var dp, dq, qi *big.Int - if odp, ok := key.DP(); ok { + if hasDp { dp = &big.Int{} // note: do not use from sync.Pool dp.SetBytes(odp) } - if odq, ok := key.DQ(); ok { + if hasDq { dq = &big.Int{} // note: do not use from sync.Pool dq.SetBytes(odq) } - if oqi, ok := key.QI(); ok { + if hasQi { qi = &big.Int{} // note: do not use from sync.Pool qi.SetBytes(oqi) } - n, ok := key.N() - if !ok { - return nil, fmt.Errorf(`missing "n" value`) - } - - e, ok := key.E() - if !ok { - return nil, fmt.Errorf(`missing "e" value`) - } - var privkey rsa.PrivateKey - buildRSAPublicKey(&privkey.PublicKey, n, e) + if err := buildRSAPublicKey(&privkey.PublicKey, on, oe); err != nil { + return nil, fmt.Errorf(`failed to build rsa.PublicKey: %w`, err) + } privkey.D = &d privkey.Primes = []*big.Int{&p, &q} @@ -212,24 +304,34 @@ func rsaJWKToRaw(key Key, hint any) (any, error) { return nil, fmt.Errorf(`invalid destination object type %T for public RSA JWK: %w`, hint, ContinueError()) } - locker, ok := key.(rlocker) - if !ok { + var n, e []byte + // See RSAPrivateKey case above for explanation of the rlocker pattern. + if locker, ok := key.(rlocker); ok { locker.rlock() - defer locker.runlock() + concrete := key.(*rsaPublicKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it + n, e = concrete.n, concrete.e + locker.runlock() + } else { + var ok bool + if n, ok = key.N(); !ok { + return nil, fmt.Errorf(`missing "n" value`) + } + if e, ok = key.E(); !ok { + return nil, fmt.Errorf(`missing "e" value`) + } } - n, ok := key.N() - if !ok { + if n == nil { return nil, fmt.Errorf(`missing "n" value`) } - - e, ok := key.E() - if !ok { + if e == nil { return nil, fmt.Errorf(`missing "e" value`) } var pubkey rsa.PublicKey - buildRSAPublicKey(&pubkey, n, e) + if err := buildRSAPublicKey(&pubkey, n, e); err != nil { + return nil, fmt.Errorf(`failed to build rsa.PublicKey: %w`, err) + } return &pubkey, nil @@ -270,36 +372,46 @@ func (k *rsaPublicKey) PublicKey() (Key, error) { // Thumbprint returns the JWK thumbprint using the indicated // hashing algorithm, according to RFC 7638 -func (k rsaPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) { +func (k *rsaPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) { k.mu.RLock() defer k.mu.RUnlock() - var key rsa.PrivateKey - if err := Export(&k, &key); err != nil { - return nil, fmt.Errorf(`failed to export RSA private key: %w`, err) - } - return rsaThumbprint(hash, &key.PublicKey) + return rsaThumbprint(hash, k.n, k.e) } -func (k rsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { +func (k *rsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { k.mu.RLock() defer k.mu.RUnlock() - var key rsa.PublicKey - if err := Export(&k, &key); err != nil { - return nil, fmt.Errorf(`failed to export RSA public key: %w`, err) + return rsaThumbprint(hash, k.n, k.e) +} + +// trimLeadingZeroBytes strips leading zero bytes. RFC 7638 requires the +// minimal big-endian representation of n/e for canonical JSON. +func trimLeadingZeroBytes(b []byte) []byte { + for len(b) > 0 && b[0] == 0 { + b = b[1:] } - return rsaThumbprint(hash, &key) + return b } -func rsaThumbprint(hash crypto.Hash, key *rsa.PublicKey) ([]byte, error) { +func rsaThumbprint(hash crypto.Hash, n, e []byte) ([]byte, error) { + n = trimLeadingZeroBytes(n) + e = trimLeadingZeroBytes(e) + if len(n) == 0 { + return nil, fmt.Errorf(`failed to compute rsa thumbprint: missing "n" value`) + } + if len(e) == 0 { + return nil, fmt.Errorf(`failed to compute rsa thumbprint: missing "e" value`) + } + buf := pool.BytesBuffer().Get() defer pool.BytesBuffer().Put(buf) buf.WriteString(`{"e":"`) - buf.WriteString(base64.EncodeUint64ToString(uint64(key.E))) + buf.WriteString(base64.EncodeToString(e)) buf.WriteString(`","kty":"RSA","n":"`) - buf.WriteString(base64.EncodeToString(key.N.Bytes())) + buf.WriteString(base64.EncodeToString(n)) buf.WriteString(`"}`) h := hash.New() @@ -322,15 +434,8 @@ func validateRSAKey(key interface { if !ok { return fmt.Errorf(`missing "e" value`) } - - if len(n) == 0 { - // Ideally we would like to check for the actual length, but unlike - // EC keys, we have nothing in the key itself that will tell us - // how many bits this key should have. - return fmt.Errorf(`missing "n" value`) - } - if len(e) == 0 { - return fmt.Errorf(`missing "e" value`) + if _, err := validateRSAModulusAndExponent(n, e); err != nil { + return err } if checkPrivate { if priv, ok := key.(keyWithD); ok { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa_gen.go index 8e2a4f085b..3ef59aec6f 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa_gen.go @@ -15,6 +15,7 @@ import ( "github.com/lestrrat-go/jwx/v3/internal/pool" "github.com/lestrrat-go/jwx/v3/internal/tokens" "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk/internal/registry" ) const ( @@ -46,7 +47,7 @@ type rsaPublicKey struct { x509CertThumbprintS256 *string // https://tools.ietf.org/html/rfc7515#section-4.1.8 x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5 privateParams map[string]any - mu *sync.RWMutex + mu sync.RWMutex dc json.DecodeCtx } @@ -55,28 +56,29 @@ var _ Key = &rsaPublicKey{} func newRSAPublicKey() *rsaPublicKey { return &rsaPublicKey{ - mu: &sync.RWMutex{}, privateParams: make(map[string]any), } } -func (h rsaPublicKey) KeyType() jwa.KeyType { +func (h *rsaPublicKey) KeyType() jwa.KeyType { return jwa.RSA() } -func (h rsaPublicKey) rlock() { +func (h *rsaPublicKey) rlock() { h.mu.RLock() } -func (h rsaPublicKey) runlock() { +func (h *rsaPublicKey) runlock() { h.mu.RUnlock() } -func (h rsaPublicKey) IsPrivate() bool { +func (h *rsaPublicKey) IsPrivate() bool { return false } func (h *rsaPublicKey) Algorithm() (jwa.KeyAlgorithm, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.algorithm != nil { return *(h.algorithm), true } @@ -84,6 +86,8 @@ func (h *rsaPublicKey) Algorithm() (jwa.KeyAlgorithm, bool) { } func (h *rsaPublicKey) E() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.e != nil { return h.e, true } @@ -91,6 +95,8 @@ func (h *rsaPublicKey) E() ([]byte, bool) { } func (h *rsaPublicKey) KeyID() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyID != nil { return *(h.keyID), true } @@ -98,6 +104,8 @@ func (h *rsaPublicKey) KeyID() (string, bool) { } func (h *rsaPublicKey) KeyOps() (KeyOperationList, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyOps != nil { return *(h.keyOps), true } @@ -105,6 +113,8 @@ func (h *rsaPublicKey) KeyOps() (KeyOperationList, bool) { } func (h *rsaPublicKey) KeyUsage() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyUsage != nil { return *(h.keyUsage), true } @@ -112,6 +122,8 @@ func (h *rsaPublicKey) KeyUsage() (string, bool) { } func (h *rsaPublicKey) N() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.n != nil { return h.n, true } @@ -119,10 +131,17 @@ func (h *rsaPublicKey) N() ([]byte, bool) { } func (h *rsaPublicKey) X509CertChain() (*cert.Chain, bool) { - return h.x509CertChain, true + h.mu.RLock() + defer h.mu.RUnlock() + if h.x509CertChain != nil { + return h.x509CertChain, true + } + return nil, false } func (h *rsaPublicKey) X509CertThumbprint() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509CertThumbprint != nil { return *(h.x509CertThumbprint), true } @@ -130,6 +149,8 @@ func (h *rsaPublicKey) X509CertThumbprint() (string, bool) { } func (h *rsaPublicKey) X509CertThumbprintS256() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509CertThumbprintS256 != nil { return *(h.x509CertThumbprintS256), true } @@ -137,6 +158,8 @@ func (h *rsaPublicKey) X509CertThumbprintS256() (string, bool) { } func (h *rsaPublicKey) X509URL() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509URL != nil { return *(h.x509URL), true } @@ -299,7 +322,12 @@ func (h *rsaPublicKey) setNoLock(name string, value any) error { return nil case RSAEKey: if v, ok := value.([]byte); ok { - h.e = v + if v == nil { + h.e = nil + } else { + h.e = make([]byte, len(v)) + copy(h.e, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, RSAEKey, value) @@ -333,7 +361,12 @@ func (h *rsaPublicKey) setNoLock(name string, value any) error { } case RSANKey: if v, ok := value.([]byte); ok { - h.n = v + if v == nil { + h.n = nil + } else { + h.n = make([]byte, len(v)) + copy(h.n, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, RSANKey, value) @@ -452,7 +485,7 @@ LOOP: case string: // Objects can only have string keys switch tok { case KeyTypeKey: - val, err := json.ReadNextStringToken(dec) + val, err := json.ReadNextStringToken(dec, h.dc) if err != nil { return fmt.Errorf(`error reading token: %w`, err) } @@ -474,7 +507,7 @@ LOOP: return fmt.Errorf(`failed to decode value for key %s: %w`, RSAEKey, err) } case KeyIDKey: - if err := json.AssignNextStringToken(&h.keyID, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyID, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err) } case KeyOpsKey: @@ -484,7 +517,7 @@ LOOP: } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } case RSANKey: @@ -498,15 +531,15 @@ LOOP: } h.x509CertChain = &decoded case X509CertThumbprintKey: - if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err) } case X509CertThumbprintS256Key: - if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err) } case X509URLKey: - if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509URL, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err) } default: @@ -519,7 +552,7 @@ LOOP: } } } - decoded, err := registry.Decode(dec, tok) + decoded, err := fieldRegistry.Decode(dec, tok) if err == nil { h.setNoLock(tok, decoded) continue @@ -539,84 +572,135 @@ LOOP: return nil } -func (h rsaPublicKey) MarshalJSON() ([]byte, error) { - data := make(map[string]any) - fields := make([]string, 0, 10) - data[KeyTypeKey] = jwa.RSA() - fields = append(fields, KeyTypeKey) +func (h *rsaPublicKey) makePairs() ([]fieldPair, error) { + pairs := getFieldPairList() + h.mu.RLock() + defer h.mu.RUnlock() + { + v, err := json.Marshal(jwa.RSA()) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyTypeKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyTypeKey, Value: v}) + } if h.algorithm != nil { - data[AlgorithmKey] = *(h.algorithm) - fields = append(fields, AlgorithmKey) + v, err := json.Marshal(*(h.algorithm)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err) + } + pairs = append(pairs, fieldPair{Name: AlgorithmKey, Value: v}) } if h.e != nil { - data[RSAEKey] = h.e - fields = append(fields, RSAEKey) + v, err := json.Marshal(base64.EncodeToString(h.e)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSAEKey, err) + } + pairs = append(pairs, fieldPair{Name: RSAEKey, Value: v}) } if h.keyID != nil { - data[KeyIDKey] = *(h.keyID) - fields = append(fields, KeyIDKey) + v, err := json.Marshal(*(h.keyID)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyIDKey, Value: v}) } if h.keyOps != nil { - data[KeyOpsKey] = *(h.keyOps) - fields = append(fields, KeyOpsKey) + v, err := json.Marshal(*(h.keyOps)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyOpsKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyOpsKey, Value: v}) } if h.keyUsage != nil { - data[KeyUsageKey] = *(h.keyUsage) - fields = append(fields, KeyUsageKey) + v, err := json.Marshal(*(h.keyUsage)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyUsageKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyUsageKey, Value: v}) } if h.n != nil { - data[RSANKey] = h.n - fields = append(fields, RSANKey) + v, err := json.Marshal(base64.EncodeToString(h.n)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSANKey, err) + } + pairs = append(pairs, fieldPair{Name: RSANKey, Value: v}) } if h.x509CertChain != nil { - data[X509CertChainKey] = h.x509CertChain - fields = append(fields, X509CertChainKey) + v, err := json.Marshal(h.x509CertChain) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err) + } + pairs = append(pairs, fieldPair{Name: X509CertChainKey, Value: v}) } if h.x509CertThumbprint != nil { - data[X509CertThumbprintKey] = *(h.x509CertThumbprint) - fields = append(fields, X509CertThumbprintKey) + v, err := json.Marshal(*(h.x509CertThumbprint)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err) + } + pairs = append(pairs, fieldPair{Name: X509CertThumbprintKey, Value: v}) } if h.x509CertThumbprintS256 != nil { - data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256) - fields = append(fields, X509CertThumbprintS256Key) + v, err := json.Marshal(*(h.x509CertThumbprintS256)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err) + } + pairs = append(pairs, fieldPair{Name: X509CertThumbprintS256Key, Value: v}) } if h.x509URL != nil { - data[X509URLKey] = *(h.x509URL) - fields = append(fields, X509URLKey) + v, err := json.Marshal(*(h.x509URL)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err) + } + pairs = append(pairs, fieldPair{Name: X509URLKey, Value: v}) } for k, v := range h.privateParams { - data[k] = v - fields = append(fields, k) + var encoded []byte + switch v := v.(type) { + case []byte: + var err error + encoded, err = json.Marshal(base64.EncodeToString(v)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + default: + var err error + encoded, err = json.Marshal(v) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + } + pairs = append(pairs, fieldPair{Name: k, Value: encoded}) } - sort.Strings(fields) + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Name < pairs[j].Name + }) + + return pairs, nil +} + +func (h *rsaPublicKey) MarshalJSON() ([]byte, error) { buf := pool.BytesBuffer().Get() defer pool.BytesBuffer().Put(buf) + pairs, err := h.makePairs() + if err != nil { + return nil, fmt.Errorf(`failed to make pairs: %w`, err) + } buf.WriteByte(tokens.OpenCurlyBracket) - enc := json.NewEncoder(buf) - for i, f := range fields { + + for i, pair := range pairs { if i > 0 { - buf.WriteRune(tokens.Comma) - } - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(f) - buf.WriteString(`":`) - v := data[f] - switch v := v.(type) { - case []byte: - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(base64.EncodeToString(v)) - buf.WriteRune(tokens.DoubleQuote) - default: - if err := enc.Encode(v); err != nil { - return nil, fmt.Errorf(`failed to encode value for field %s: %w`, f, err) - } - buf.Truncate(buf.Len() - 1) + buf.WriteByte(tokens.Comma) } + buf.WriteByte('"') + buf.WriteString(pair.Name) + buf.WriteString(`": `) + buf.Write(pair.Value.([]byte)) } buf.WriteByte(tokens.CloseCurlyBracket) ret := make([]byte, buf.Len()) copy(ret, buf.Bytes()) + putFieldPairList(pairs) return ret, nil } @@ -691,7 +775,7 @@ type rsaPrivateKey struct { x509CertThumbprintS256 *string // https://tools.ietf.org/html/rfc7515#section-4.1.8 x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5 privateParams map[string]any - mu *sync.RWMutex + mu sync.RWMutex dc json.DecodeCtx } @@ -700,28 +784,29 @@ var _ Key = &rsaPrivateKey{} func newRSAPrivateKey() *rsaPrivateKey { return &rsaPrivateKey{ - mu: &sync.RWMutex{}, privateParams: make(map[string]any), } } -func (h rsaPrivateKey) KeyType() jwa.KeyType { +func (h *rsaPrivateKey) KeyType() jwa.KeyType { return jwa.RSA() } -func (h rsaPrivateKey) rlock() { +func (h *rsaPrivateKey) rlock() { h.mu.RLock() } -func (h rsaPrivateKey) runlock() { +func (h *rsaPrivateKey) runlock() { h.mu.RUnlock() } -func (h rsaPrivateKey) IsPrivate() bool { +func (h *rsaPrivateKey) IsPrivate() bool { return true } func (h *rsaPrivateKey) Algorithm() (jwa.KeyAlgorithm, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.algorithm != nil { return *(h.algorithm), true } @@ -729,6 +814,8 @@ func (h *rsaPrivateKey) Algorithm() (jwa.KeyAlgorithm, bool) { } func (h *rsaPrivateKey) D() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.d != nil { return h.d, true } @@ -736,6 +823,8 @@ func (h *rsaPrivateKey) D() ([]byte, bool) { } func (h *rsaPrivateKey) DP() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.dp != nil { return h.dp, true } @@ -743,6 +832,8 @@ func (h *rsaPrivateKey) DP() ([]byte, bool) { } func (h *rsaPrivateKey) DQ() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.dq != nil { return h.dq, true } @@ -750,6 +841,8 @@ func (h *rsaPrivateKey) DQ() ([]byte, bool) { } func (h *rsaPrivateKey) E() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.e != nil { return h.e, true } @@ -757,6 +850,8 @@ func (h *rsaPrivateKey) E() ([]byte, bool) { } func (h *rsaPrivateKey) KeyID() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyID != nil { return *(h.keyID), true } @@ -764,6 +859,8 @@ func (h *rsaPrivateKey) KeyID() (string, bool) { } func (h *rsaPrivateKey) KeyOps() (KeyOperationList, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyOps != nil { return *(h.keyOps), true } @@ -771,6 +868,8 @@ func (h *rsaPrivateKey) KeyOps() (KeyOperationList, bool) { } func (h *rsaPrivateKey) KeyUsage() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyUsage != nil { return *(h.keyUsage), true } @@ -778,6 +877,8 @@ func (h *rsaPrivateKey) KeyUsage() (string, bool) { } func (h *rsaPrivateKey) N() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.n != nil { return h.n, true } @@ -785,6 +886,8 @@ func (h *rsaPrivateKey) N() ([]byte, bool) { } func (h *rsaPrivateKey) P() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.p != nil { return h.p, true } @@ -792,6 +895,8 @@ func (h *rsaPrivateKey) P() ([]byte, bool) { } func (h *rsaPrivateKey) Q() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.q != nil { return h.q, true } @@ -799,6 +904,8 @@ func (h *rsaPrivateKey) Q() ([]byte, bool) { } func (h *rsaPrivateKey) QI() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.qi != nil { return h.qi, true } @@ -806,10 +913,17 @@ func (h *rsaPrivateKey) QI() ([]byte, bool) { } func (h *rsaPrivateKey) X509CertChain() (*cert.Chain, bool) { - return h.x509CertChain, true + h.mu.RLock() + defer h.mu.RUnlock() + if h.x509CertChain != nil { + return h.x509CertChain, true + } + return nil, false } func (h *rsaPrivateKey) X509CertThumbprint() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509CertThumbprint != nil { return *(h.x509CertThumbprint), true } @@ -817,6 +931,8 @@ func (h *rsaPrivateKey) X509CertThumbprint() (string, bool) { } func (h *rsaPrivateKey) X509CertThumbprintS256() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509CertThumbprintS256 != nil { return *(h.x509CertThumbprintS256), true } @@ -824,6 +940,8 @@ func (h *rsaPrivateKey) X509CertThumbprintS256() (string, bool) { } func (h *rsaPrivateKey) X509URL() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509URL != nil { return *(h.x509URL), true } @@ -1046,25 +1164,45 @@ func (h *rsaPrivateKey) setNoLock(name string, value any) error { return nil case RSADKey: if v, ok := value.([]byte); ok { - h.d = v + if v == nil { + h.d = nil + } else { + h.d = make([]byte, len(v)) + copy(h.d, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, RSADKey, value) case RSADPKey: if v, ok := value.([]byte); ok { - h.dp = v + if v == nil { + h.dp = nil + } else { + h.dp = make([]byte, len(v)) + copy(h.dp, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, RSADPKey, value) case RSADQKey: if v, ok := value.([]byte); ok { - h.dq = v + if v == nil { + h.dq = nil + } else { + h.dq = make([]byte, len(v)) + copy(h.dq, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, RSADQKey, value) case RSAEKey: if v, ok := value.([]byte); ok { - h.e = v + if v == nil { + h.e = nil + } else { + h.e = make([]byte, len(v)) + copy(h.e, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, RSAEKey, value) @@ -1098,25 +1236,45 @@ func (h *rsaPrivateKey) setNoLock(name string, value any) error { } case RSANKey: if v, ok := value.([]byte); ok { - h.n = v + if v == nil { + h.n = nil + } else { + h.n = make([]byte, len(v)) + copy(h.n, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, RSANKey, value) case RSAPKey: if v, ok := value.([]byte); ok { - h.p = v + if v == nil { + h.p = nil + } else { + h.p = make([]byte, len(v)) + copy(h.p, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, RSAPKey, value) case RSAQKey: if v, ok := value.([]byte); ok { - h.q = v + if v == nil { + h.q = nil + } else { + h.q = make([]byte, len(v)) + copy(h.q, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, RSAQKey, value) case RSAQIKey: if v, ok := value.([]byte); ok { - h.qi = v + if v == nil { + h.qi = nil + } else { + h.qi = make([]byte, len(v)) + copy(h.qi, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, RSAQIKey, value) @@ -1215,7 +1373,7 @@ func (k *rsaPrivateKey) SetDecodeCtx(dc json.DecodeCtx) { k.dc = dc } -func (h *rsaPrivateKey) UnmarshalJSON(buf []byte) error { +func (h *rsaPrivateKey) UnmarshalJSON(buf []byte) (retErr error) { h.mu.Lock() defer h.mu.Unlock() h.algorithm = nil @@ -1234,6 +1392,26 @@ func (h *rsaPrivateKey) UnmarshalJSON(buf []byte) error { h.x509CertThumbprint = nil h.x509CertThumbprintS256 = nil h.x509URL = nil + defer func() { + if retErr != nil { + clear(h.d) + h.d = nil + clear(h.dp) + h.dp = nil + clear(h.dq) + h.dq = nil + clear(h.e) + h.e = nil + clear(h.n) + h.n = nil + clear(h.p) + h.p = nil + clear(h.q) + h.q = nil + clear(h.qi) + h.qi = nil + } + }() dec := json.NewDecoder(bytes.NewReader(buf)) LOOP: for { @@ -1253,7 +1431,7 @@ LOOP: case string: // Objects can only have string keys switch tok { case KeyTypeKey: - val, err := json.ReadNextStringToken(dec) + val, err := json.ReadNextStringToken(dec, h.dc) if err != nil { return fmt.Errorf(`error reading token: %w`, err) } @@ -1287,7 +1465,7 @@ LOOP: return fmt.Errorf(`failed to decode value for key %s: %w`, RSAEKey, err) } case KeyIDKey: - if err := json.AssignNextStringToken(&h.keyID, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyID, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err) } case KeyOpsKey: @@ -1297,7 +1475,7 @@ LOOP: } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } case RSANKey: @@ -1323,15 +1501,15 @@ LOOP: } h.x509CertChain = &decoded case X509CertThumbprintKey: - if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err) } case X509CertThumbprintS256Key: - if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err) } case X509URLKey: - if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509URL, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err) } default: @@ -1344,7 +1522,7 @@ LOOP: } } } - decoded, err := registry.Decode(dec, tok) + decoded, err := fieldRegistry.Decode(dec, tok) if err == nil { h.setNoLock(tok, decoded) continue @@ -1367,108 +1545,177 @@ LOOP: return nil } -func (h rsaPrivateKey) MarshalJSON() ([]byte, error) { - data := make(map[string]any) - fields := make([]string, 0, 16) - data[KeyTypeKey] = jwa.RSA() - fields = append(fields, KeyTypeKey) +func (h *rsaPrivateKey) makePairs() ([]fieldPair, error) { + pairs := getFieldPairList() + h.mu.RLock() + defer h.mu.RUnlock() + { + v, err := json.Marshal(jwa.RSA()) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyTypeKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyTypeKey, Value: v}) + } if h.algorithm != nil { - data[AlgorithmKey] = *(h.algorithm) - fields = append(fields, AlgorithmKey) + v, err := json.Marshal(*(h.algorithm)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err) + } + pairs = append(pairs, fieldPair{Name: AlgorithmKey, Value: v}) } if h.d != nil { - data[RSADKey] = h.d - fields = append(fields, RSADKey) + v, err := json.Marshal(base64.EncodeToString(h.d)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSADKey, err) + } + pairs = append(pairs, fieldPair{Name: RSADKey, Value: v}) } if h.dp != nil { - data[RSADPKey] = h.dp - fields = append(fields, RSADPKey) + v, err := json.Marshal(base64.EncodeToString(h.dp)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSADPKey, err) + } + pairs = append(pairs, fieldPair{Name: RSADPKey, Value: v}) } if h.dq != nil { - data[RSADQKey] = h.dq - fields = append(fields, RSADQKey) + v, err := json.Marshal(base64.EncodeToString(h.dq)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSADQKey, err) + } + pairs = append(pairs, fieldPair{Name: RSADQKey, Value: v}) } if h.e != nil { - data[RSAEKey] = h.e - fields = append(fields, RSAEKey) + v, err := json.Marshal(base64.EncodeToString(h.e)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSAEKey, err) + } + pairs = append(pairs, fieldPair{Name: RSAEKey, Value: v}) } if h.keyID != nil { - data[KeyIDKey] = *(h.keyID) - fields = append(fields, KeyIDKey) + v, err := json.Marshal(*(h.keyID)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyIDKey, Value: v}) } if h.keyOps != nil { - data[KeyOpsKey] = *(h.keyOps) - fields = append(fields, KeyOpsKey) + v, err := json.Marshal(*(h.keyOps)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyOpsKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyOpsKey, Value: v}) } if h.keyUsage != nil { - data[KeyUsageKey] = *(h.keyUsage) - fields = append(fields, KeyUsageKey) + v, err := json.Marshal(*(h.keyUsage)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyUsageKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyUsageKey, Value: v}) } if h.n != nil { - data[RSANKey] = h.n - fields = append(fields, RSANKey) + v, err := json.Marshal(base64.EncodeToString(h.n)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSANKey, err) + } + pairs = append(pairs, fieldPair{Name: RSANKey, Value: v}) } if h.p != nil { - data[RSAPKey] = h.p - fields = append(fields, RSAPKey) + v, err := json.Marshal(base64.EncodeToString(h.p)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSAPKey, err) + } + pairs = append(pairs, fieldPair{Name: RSAPKey, Value: v}) } if h.q != nil { - data[RSAQKey] = h.q - fields = append(fields, RSAQKey) + v, err := json.Marshal(base64.EncodeToString(h.q)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSAQKey, err) + } + pairs = append(pairs, fieldPair{Name: RSAQKey, Value: v}) } if h.qi != nil { - data[RSAQIKey] = h.qi - fields = append(fields, RSAQIKey) + v, err := json.Marshal(base64.EncodeToString(h.qi)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSAQIKey, err) + } + pairs = append(pairs, fieldPair{Name: RSAQIKey, Value: v}) } if h.x509CertChain != nil { - data[X509CertChainKey] = h.x509CertChain - fields = append(fields, X509CertChainKey) + v, err := json.Marshal(h.x509CertChain) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err) + } + pairs = append(pairs, fieldPair{Name: X509CertChainKey, Value: v}) } if h.x509CertThumbprint != nil { - data[X509CertThumbprintKey] = *(h.x509CertThumbprint) - fields = append(fields, X509CertThumbprintKey) + v, err := json.Marshal(*(h.x509CertThumbprint)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err) + } + pairs = append(pairs, fieldPair{Name: X509CertThumbprintKey, Value: v}) } if h.x509CertThumbprintS256 != nil { - data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256) - fields = append(fields, X509CertThumbprintS256Key) + v, err := json.Marshal(*(h.x509CertThumbprintS256)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err) + } + pairs = append(pairs, fieldPair{Name: X509CertThumbprintS256Key, Value: v}) } if h.x509URL != nil { - data[X509URLKey] = *(h.x509URL) - fields = append(fields, X509URLKey) + v, err := json.Marshal(*(h.x509URL)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err) + } + pairs = append(pairs, fieldPair{Name: X509URLKey, Value: v}) } for k, v := range h.privateParams { - data[k] = v - fields = append(fields, k) + var encoded []byte + switch v := v.(type) { + case []byte: + var err error + encoded, err = json.Marshal(base64.EncodeToString(v)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + default: + var err error + encoded, err = json.Marshal(v) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + } + pairs = append(pairs, fieldPair{Name: k, Value: encoded}) } - sort.Strings(fields) + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Name < pairs[j].Name + }) + + return pairs, nil +} + +func (h *rsaPrivateKey) MarshalJSON() ([]byte, error) { buf := pool.BytesBuffer().Get() defer pool.BytesBuffer().Put(buf) + pairs, err := h.makePairs() + if err != nil { + return nil, fmt.Errorf(`failed to make pairs: %w`, err) + } buf.WriteByte(tokens.OpenCurlyBracket) - enc := json.NewEncoder(buf) - for i, f := range fields { + + for i, pair := range pairs { if i > 0 { - buf.WriteRune(tokens.Comma) - } - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(f) - buf.WriteString(`":`) - v := data[f] - switch v := v.(type) { - case []byte: - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(base64.EncodeToString(v)) - buf.WriteRune(tokens.DoubleQuote) - default: - if err := enc.Encode(v); err != nil { - return nil, fmt.Errorf(`failed to encode value for field %s: %w`, f, err) - } - buf.Truncate(buf.Len() - 1) + buf.WriteByte(tokens.Comma) } + buf.WriteByte('"') + buf.WriteString(pair.Name) + buf.WriteString(`": `) + buf.Write(pair.Value.([]byte)) } buf.WriteByte(tokens.CloseCurlyBracket) ret := make([]byte, buf.Len()) copy(ret, buf.Bytes()) + putFieldPairList(pairs) return ret, nil } @@ -1541,3 +1788,10 @@ func init() { func RSAStandardFieldsFilter() KeyFilter { return rsaStandardFields } + +func init() { + registry.Register(jwa.RSA().String(), registry.Constructor{ + Public: func() any { return newRSAPublicKey() }, + Private: func() any { return newRSAPrivateKey() }, + }) +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/set.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/set.go index 6f339649a8..6b8c7aa564 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/set.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/set.go @@ -27,8 +27,8 @@ func NewSet() Set { } func (s *set) Set(n string, v any) error { - s.mu.RLock() - defer s.mu.RUnlock() + s.mu.Lock() + defer s.mu.Unlock() if n == keysKey { vl, ok := v.([]Key) @@ -95,8 +95,15 @@ func (s *set) AddKey(key Key) error { s.mu.Lock() defer s.mu.Unlock() - if reflect.ValueOf(key).IsNil() { - panic("nil key") + rv := reflect.ValueOf(key) + if !rv.IsValid() { + return fmt.Errorf(`(jwk.Set).AddKey: nil key`) + } + switch rv.Kind() { + case reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func, reflect.Map, reflect.Slice: + if rv.IsNil() { + return fmt.Errorf(`(jwk.Set).AddKey: nil key`) + } } if i := s.indexNL(key); i > -1 { @@ -144,6 +151,8 @@ func (s *set) Clear() error { } func (s *set) Keys() []string { + s.mu.RLock() + defer s.mu.RUnlock() ret := make([]string, len(s.privateParams)) var i int for k := range s.privateParams { @@ -161,7 +170,8 @@ func (s *set) MarshalJSON() ([]byte, error) { defer pool.BytesBuffer().Put(buf) enc := json.NewEncoder(buf) - fields := []string{keysKey} + fields := make([]string, 0, 1+len(s.privateParams)) + fields = append(fields, keysKey) for k := range s.privateParams { fields = append(fields, k) } @@ -197,6 +207,20 @@ func (s *set) MarshalJSON() ([]byte, error) { return ret, nil } +func (s *set) setMaxKeys(n int) { + s.maxKeys = n +} + +func (s *set) setRejectDuplicateKID(v bool) { + s.rejectDuplicateKID = v +} + +// UnmarshalJSON streams a JWKS document. The "keys" array is read +// element-by-element with the configured cap enforced BEFORE the +// (cap+1)-th element is decoded — an attacker-controlled input length +// cannot force allocation past the cap. This entry point requires +// JWKS shape; bare JWK input is rejected here. Callers that don't +// know the shape ahead of time should use [Parse], which dispatches. func (s *set) UnmarshalJSON(data []byte) error { s.mu.Lock() defer s.mu.Unlock() @@ -213,7 +237,12 @@ func (s *set) UnmarshalJSON(data []byte) error { ignoreParseError = dc.IgnoreParseError() } - var sawKeysField bool + maxK := s.maxKeys + if maxK <= 0 { + maxK = int(maxKeys.Load()) + } + rejectDupKid := s.rejectDuplicateKID || rejectDuplicateKID.Load() + dec := json.NewDecoder(bytes.NewReader(data)) LOOP: for { @@ -224,9 +253,7 @@ LOOP: switch tok := tok.(type) { case json.Delim: - // Assuming we're doing everything correctly, we should ONLY - // get either tokens.OpenCurlyBracket or tokens.CloseCurlyBracket here. - if tok == tokens.CloseCurlyBracket { // End of object + if tok == tokens.CloseCurlyBracket { break LOOP } else if tok != tokens.OpenCurlyBracket { return fmt.Errorf(`expected '%c' but got '%c'`, tokens.OpenCurlyBracket, tok) @@ -234,21 +261,54 @@ LOOP: case string: switch tok { case "keys": - sawKeysField = true - var list []json.RawMessage - if err := dec.Decode(&list); err != nil { + openTok, err := dec.Token() + if err != nil { return fmt.Errorf(`failed to decode "keys": %w`, err) } + openDelim, ok := openTok.(json.Delim) + if !ok || openDelim != tokens.OpenSquareBracket { + return fmt.Errorf(`failed to decode "keys": expected '%c' but got %v`, tokens.OpenSquareBracket, openTok) + } - for i, keysrc := range list { - key, err := ParseKey(keysrc, options...) + var seenKIDs map[string]struct{} + if rejectDupKid { + seenKIDs = make(map[string]struct{}) + } + var i int + for dec.More() { + if i >= maxK { + return fmt.Errorf(`too many keys in "keys" array: max %d`, maxK) + } + var raw json.RawMessage + if err := dec.Decode(&raw); err != nil { + return fmt.Errorf(`failed to decode "keys": %w`, err) + } + key, err := ParseKey(raw, options...) if err != nil { if !ignoreParseError { return fmt.Errorf(`failed to decode key #%d in "keys": %w`, i, err) } + i++ continue } + if seenKIDs != nil { + if kid, ok := key.KeyID(); ok && kid != "" { + if _, dup := seenKIDs[kid]; dup { + return fmt.Errorf(`duplicate "kid" %q in "keys" array`, kid) + } + seenKIDs[kid] = struct{}{} + } + } s.keys = append(s.keys, key) + i++ + } + closeTok, err := dec.Token() + if err != nil { + return fmt.Errorf(`failed to decode "keys": %w`, err) + } + closeDelim, ok := closeTok.(json.Delim) + if !ok || closeDelim != tokens.CloseSquareBracket { + return fmt.Errorf(`failed to decode "keys": expected '%c' but got %v`, tokens.CloseSquareBracket, closeTok) } default: var v any @@ -259,19 +319,6 @@ LOOP: } } } - - // This is really silly, but we can only detect the - // lack of the "keys" field after going through the - // entire object once - // Not checking for len(s.keys) == 0, because it could be - // an empty key set - if !sawKeysField { - key, err := ParseKey(data, options...) - if err != nil { - return fmt.Errorf(`failed to parse sole key in key set`) - } - s.keys = append(s.keys, key) - } return nil } @@ -279,11 +326,7 @@ func (s *set) LookupKeyID(kid string) (Key, bool) { s.mu.RLock() defer s.mu.RUnlock() - for i := range s.Len() { - key, ok := s.Key(i) - if !ok { - return nil, false - } + for _, key := range s.keys { gotkid, ok := key.KeyID() if ok && gotkid == kid { return key, true diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric.go index 7db5e1591a..da4bff433b 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric.go @@ -4,13 +4,14 @@ import ( "crypto" "fmt" "reflect" + "slices" "github.com/lestrrat-go/jwx/v3/internal/base64" "github.com/lestrrat-go/jwx/v3/jwa" ) func init() { - RegisterKeyExporter(jwa.OctetSeq(), KeyExportFunc(octetSeqToRaw)) + RegisterKeyExporter(KeyKind(jwa.OctetSeq().String()), KeyExportFunc(octetSeqToRaw)) } func (k *symmetricKey) Import(rawKey []byte) error { @@ -21,7 +22,7 @@ func (k *symmetricKey) Import(rawKey []byte) error { return fmt.Errorf(`non-empty []byte key required`) } - k.octets = rawKey + k.octets = slices.Clone(rawKey) return nil } @@ -44,14 +45,27 @@ func octetSeqToRaw(key Key, hint any) (any, error) { return nil, fmt.Errorf(`invalid destination object type %T for symmetric key: %w`, hint, ContinueError()) } - locker, ok := key.(rlocker) - if ok { + // rlocker is unexported with unexported methods, so only our + // concrete types implement it. A successful assertion lets us + // type-assert to the concrete struct and read fields directly + // under a single batch lock. This avoids nested RLock (which + // deadlocks when a writer is pending) while preserving an + // atomic snapshot of all fields. + var ooctets []byte + if locker, ok := key.(rlocker); ok { locker.rlock() - defer locker.runlock() + concrete := key.(*symmetricKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it + ooctets = concrete.octets + locker.runlock() + } else { + // External implementation — use self-locking interface getters. + var ok bool + if ooctets, ok = key.Octets(); !ok { + return nil, fmt.Errorf(`jwk.SymmetricKey: missing "k" field`) + } } - ooctets, ok := key.Octets() - if !ok { + if ooctets == nil { return nil, fmt.Errorf(`jwk.SymmetricKey: missing "k" field`) } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric_gen.go index bfd2f8497d..900ed6537b 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric_gen.go @@ -15,6 +15,7 @@ import ( "github.com/lestrrat-go/jwx/v3/internal/pool" "github.com/lestrrat-go/jwx/v3/internal/tokens" "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk/internal/registry" ) const ( @@ -37,7 +38,7 @@ type symmetricKey struct { x509CertThumbprintS256 *string // https://tools.ietf.org/html/rfc7515#section-4.1.8 x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5 privateParams map[string]any - mu *sync.RWMutex + mu sync.RWMutex dc json.DecodeCtx } @@ -46,24 +47,25 @@ var _ Key = &symmetricKey{} func newSymmetricKey() *symmetricKey { return &symmetricKey{ - mu: &sync.RWMutex{}, privateParams: make(map[string]any), } } -func (h symmetricKey) KeyType() jwa.KeyType { +func (h *symmetricKey) KeyType() jwa.KeyType { return jwa.OctetSeq() } -func (h symmetricKey) rlock() { +func (h *symmetricKey) rlock() { h.mu.RLock() } -func (h symmetricKey) runlock() { +func (h *symmetricKey) runlock() { h.mu.RUnlock() } func (h *symmetricKey) Algorithm() (jwa.KeyAlgorithm, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.algorithm != nil { return *(h.algorithm), true } @@ -71,6 +73,8 @@ func (h *symmetricKey) Algorithm() (jwa.KeyAlgorithm, bool) { } func (h *symmetricKey) KeyID() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyID != nil { return *(h.keyID), true } @@ -78,6 +82,8 @@ func (h *symmetricKey) KeyID() (string, bool) { } func (h *symmetricKey) KeyOps() (KeyOperationList, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyOps != nil { return *(h.keyOps), true } @@ -85,6 +91,8 @@ func (h *symmetricKey) KeyOps() (KeyOperationList, bool) { } func (h *symmetricKey) KeyUsage() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.keyUsage != nil { return *(h.keyUsage), true } @@ -92,6 +100,8 @@ func (h *symmetricKey) KeyUsage() (string, bool) { } func (h *symmetricKey) Octets() ([]byte, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.octets != nil { return h.octets, true } @@ -99,10 +109,17 @@ func (h *symmetricKey) Octets() ([]byte, bool) { } func (h *symmetricKey) X509CertChain() (*cert.Chain, bool) { - return h.x509CertChain, true + h.mu.RLock() + defer h.mu.RUnlock() + if h.x509CertChain != nil { + return h.x509CertChain, true + } + return nil, false } func (h *symmetricKey) X509CertThumbprint() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509CertThumbprint != nil { return *(h.x509CertThumbprint), true } @@ -110,6 +127,8 @@ func (h *symmetricKey) X509CertThumbprint() (string, bool) { } func (h *symmetricKey) X509CertThumbprintS256() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509CertThumbprintS256 != nil { return *(h.x509CertThumbprintS256), true } @@ -117,6 +136,8 @@ func (h *symmetricKey) X509CertThumbprintS256() (string, bool) { } func (h *symmetricKey) X509URL() (string, bool) { + h.mu.RLock() + defer h.mu.RUnlock() if h.x509URL != nil { return *(h.x509URL), true } @@ -297,7 +318,12 @@ func (h *symmetricKey) setNoLock(name string, value any) error { } case SymmetricOctetsKey: if v, ok := value.([]byte); ok { - h.octets = v + if v == nil { + h.octets = nil + } else { + h.octets = make([]byte, len(v)) + copy(h.octets, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, SymmetricOctetsKey, value) @@ -382,7 +408,7 @@ func (k *symmetricKey) SetDecodeCtx(dc json.DecodeCtx) { k.dc = dc } -func (h *symmetricKey) UnmarshalJSON(buf []byte) error { +func (h *symmetricKey) UnmarshalJSON(buf []byte) (retErr error) { h.mu.Lock() defer h.mu.Unlock() h.algorithm = nil @@ -394,6 +420,12 @@ func (h *symmetricKey) UnmarshalJSON(buf []byte) error { h.x509CertThumbprint = nil h.x509CertThumbprintS256 = nil h.x509URL = nil + defer func() { + if retErr != nil { + clear(h.octets) + h.octets = nil + } + }() dec := json.NewDecoder(bytes.NewReader(buf)) LOOP: for { @@ -413,7 +445,7 @@ LOOP: case string: // Objects can only have string keys switch tok { case KeyTypeKey: - val, err := json.ReadNextStringToken(dec) + val, err := json.ReadNextStringToken(dec, h.dc) if err != nil { return fmt.Errorf(`error reading token: %w`, err) } @@ -431,7 +463,7 @@ LOOP: } h.algorithm = &alg case KeyIDKey: - if err := json.AssignNextStringToken(&h.keyID, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyID, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err) } case KeyOpsKey: @@ -441,7 +473,7 @@ LOOP: } h.keyOps = &decoded case KeyUsageKey: - if err := json.AssignNextStringToken(&h.keyUsage, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err) } case SymmetricOctetsKey: @@ -455,15 +487,15 @@ LOOP: } h.x509CertChain = &decoded case X509CertThumbprintKey: - if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err) } case X509CertThumbprintS256Key: - if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err) } case X509URLKey: - if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509URL, dec, h.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err) } default: @@ -476,7 +508,7 @@ LOOP: } } } - decoded, err := registry.Decode(dec, tok) + decoded, err := fieldRegistry.Decode(dec, tok) if err == nil { h.setNoLock(tok, decoded) continue @@ -493,80 +525,128 @@ LOOP: return nil } -func (h symmetricKey) MarshalJSON() ([]byte, error) { - data := make(map[string]any) - fields := make([]string, 0, 9) - data[KeyTypeKey] = jwa.OctetSeq() - fields = append(fields, KeyTypeKey) +func (h *symmetricKey) makePairs() ([]fieldPair, error) { + pairs := getFieldPairList() + h.mu.RLock() + defer h.mu.RUnlock() + { + v, err := json.Marshal(jwa.OctetSeq()) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyTypeKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyTypeKey, Value: v}) + } if h.algorithm != nil { - data[AlgorithmKey] = *(h.algorithm) - fields = append(fields, AlgorithmKey) + v, err := json.Marshal(*(h.algorithm)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err) + } + pairs = append(pairs, fieldPair{Name: AlgorithmKey, Value: v}) } if h.keyID != nil { - data[KeyIDKey] = *(h.keyID) - fields = append(fields, KeyIDKey) + v, err := json.Marshal(*(h.keyID)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyIDKey, Value: v}) } if h.keyOps != nil { - data[KeyOpsKey] = *(h.keyOps) - fields = append(fields, KeyOpsKey) + v, err := json.Marshal(*(h.keyOps)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyOpsKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyOpsKey, Value: v}) } if h.keyUsage != nil { - data[KeyUsageKey] = *(h.keyUsage) - fields = append(fields, KeyUsageKey) + v, err := json.Marshal(*(h.keyUsage)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyUsageKey, err) + } + pairs = append(pairs, fieldPair{Name: KeyUsageKey, Value: v}) } if h.octets != nil { - data[SymmetricOctetsKey] = h.octets - fields = append(fields, SymmetricOctetsKey) + v, err := json.Marshal(base64.EncodeToString(h.octets)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, SymmetricOctetsKey, err) + } + pairs = append(pairs, fieldPair{Name: SymmetricOctetsKey, Value: v}) } if h.x509CertChain != nil { - data[X509CertChainKey] = h.x509CertChain - fields = append(fields, X509CertChainKey) + v, err := json.Marshal(h.x509CertChain) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err) + } + pairs = append(pairs, fieldPair{Name: X509CertChainKey, Value: v}) } if h.x509CertThumbprint != nil { - data[X509CertThumbprintKey] = *(h.x509CertThumbprint) - fields = append(fields, X509CertThumbprintKey) + v, err := json.Marshal(*(h.x509CertThumbprint)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err) + } + pairs = append(pairs, fieldPair{Name: X509CertThumbprintKey, Value: v}) } if h.x509CertThumbprintS256 != nil { - data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256) - fields = append(fields, X509CertThumbprintS256Key) + v, err := json.Marshal(*(h.x509CertThumbprintS256)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err) + } + pairs = append(pairs, fieldPair{Name: X509CertThumbprintS256Key, Value: v}) } if h.x509URL != nil { - data[X509URLKey] = *(h.x509URL) - fields = append(fields, X509URLKey) + v, err := json.Marshal(*(h.x509URL)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err) + } + pairs = append(pairs, fieldPair{Name: X509URLKey, Value: v}) } for k, v := range h.privateParams { - data[k] = v - fields = append(fields, k) + var encoded []byte + switch v := v.(type) { + case []byte: + var err error + encoded, err = json.Marshal(base64.EncodeToString(v)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + default: + var err error + encoded, err = json.Marshal(v) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + } + pairs = append(pairs, fieldPair{Name: k, Value: encoded}) } - sort.Strings(fields) + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Name < pairs[j].Name + }) + + return pairs, nil +} + +func (h *symmetricKey) MarshalJSON() ([]byte, error) { buf := pool.BytesBuffer().Get() defer pool.BytesBuffer().Put(buf) + pairs, err := h.makePairs() + if err != nil { + return nil, fmt.Errorf(`failed to make pairs: %w`, err) + } buf.WriteByte(tokens.OpenCurlyBracket) - enc := json.NewEncoder(buf) - for i, f := range fields { + + for i, pair := range pairs { if i > 0 { - buf.WriteRune(tokens.Comma) - } - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(f) - buf.WriteString(`":`) - v := data[f] - switch v := v.(type) { - case []byte: - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(base64.EncodeToString(v)) - buf.WriteRune(tokens.DoubleQuote) - default: - if err := enc.Encode(v); err != nil { - return nil, fmt.Errorf(`failed to encode value for field %s: %w`, f, err) - } - buf.Truncate(buf.Len() - 1) + buf.WriteByte(tokens.Comma) } + buf.WriteByte('"') + buf.WriteString(pair.Name) + buf.WriteString(`": `) + buf.Write(pair.Value.([]byte)) } buf.WriteByte(tokens.CloseCurlyBracket) ret := make([]byte, buf.Len()) copy(ret, buf.Bytes()) + putFieldPairList(pairs) return ret, nil } @@ -618,3 +698,9 @@ func init() { func SymmetricStandardFieldsFilter() KeyFilter { return symmetricStandardFields } + +func init() { + registry.Register(jwa.OctetSeq().String(), registry.Constructor{ + Private: func() any { return newSymmetricKey() }, + }) +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/whitelist.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/whitelist.go index 0b0df701ae..81db97f82e 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/whitelist.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/whitelist.go @@ -5,8 +5,16 @@ import "github.com/lestrrat-go/httprc/v3" type Whitelist = httprc.Whitelist type WhitelistFunc = httprc.WhitelistFunc -// InsecureWhitelist is an alias to httprc.InsecureWhitelist. Use -// functions in the `httprc` package to interact with this type. +// InsecureWhitelist is a Whitelist implementation (aliased to +// httprc.InsecureWhitelist) that allows every URL jwk.Fetch() is asked to +// retrieve. It is the library's default, which keeps first-time usage +// simple: callers with a hard-coded JWKS URL do not have to configure +// anything. +// +// Do NOT use InsecureWhitelist in any code path where the URL originates +// from untrusted input (for example, the `jku` header of a JWS). For +// those paths, construct a MapWhitelist, RegexpWhitelist, or custom +// Whitelist and pass it via jwk.WithFetchWhitelist(). type InsecureWhitelist = httprc.InsecureWhitelist func NewInsecureWhitelist() InsecureWhitelist { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/x509.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/x509.go index f06063c6ed..006ead224d 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/x509.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/x509.go @@ -154,8 +154,15 @@ func (f X509DecodeFunc) DecodeX509(dst any, block *pem.Block) error { return f(dst, block) } -var muX509Decoders sync.Mutex -var x509Decoders = map[any]int{} +// x509Decoders holds every registered decoder keyed by its caller-supplied +// ident. x509DecoderIdents keeps the registration order so decodeX509 tries +// decoders in a stable, deterministic sequence. x509DecoderList is the +// read-optimized snapshot handed out to readers; every mutation replaces it +// with a freshly allocated slice so readers can iterate their captured +// header without any further synchronization. +var muX509Decoders sync.RWMutex +var x509Decoders = map[any]X509Decoder{} +var x509DecoderIdents = []any{} var x509DecoderList = []X509Decoder{} type identDefaultX509Decoder struct{} @@ -180,8 +187,14 @@ func RegisterX509Decoder(ident any, decoder X509Decoder) { return // already registered } - x509Decoders[ident] = len(x509DecoderList) - x509DecoderList = append(x509DecoderList, decoder) + x509Decoders[ident] = decoder + x509DecoderIdents = append(x509DecoderIdents, ident) + // Publish a fresh slice so any reader that snapshotted the previous + // one keeps iterating its own immutable copy. + next := make([]X509Decoder, len(x509DecoderList)+1) + copy(next, x509DecoderList) + next[len(x509DecoderList)] = decoder + x509DecoderList = next } // UnregisterX509Decoder unregisters the X509Decoder identified by the given identifier. @@ -191,26 +204,27 @@ func RegisterX509Decoder(ident any, decoder X509Decoder) { func UnregisterX509Decoder(ident any) { muX509Decoders.Lock() defer muX509Decoders.Unlock() - idx, ok := x509Decoders[ident] - if !ok { + if _, ok := x509Decoders[ident]; !ok { return // not registered } delete(x509Decoders, ident) - l := len(x509DecoderList) - switch idx { - case l - 1: - // if the last element, just truncate the slice - x509DecoderList = x509DecoderList[:l-1] - case 0: - // if the first element, just shift the slice - x509DecoderList = x509DecoderList[1:] - default: - // if the element is in the middle, remove it by slicing - // and appending the two slices together - x509DecoderList = append(x509DecoderList[:idx], x509DecoderList[idx+1:]...) + // Rebuild idents and the reader-facing slice as fresh allocations, + // preserving registration order and filtering the removed ident. + // Mutating the old slices in place would race with readers in + // decodeX509 that iterate a captured snapshot without holding RLock. + nextIdents := make([]any, 0, len(x509DecoderIdents)-1) + nextList := make([]X509Decoder, 0, len(x509DecoderList)-1) + for _, id := range x509DecoderIdents { + if id == ident { + continue + } + nextIdents = append(nextIdents, id) + nextList = append(nextList, x509Decoders[id]) } + x509DecoderIdents = nextIdents + x509DecoderList = nextList } // decodeX509 decodes a PEM encoded ASN.1 DER format into the given destination. @@ -222,8 +236,12 @@ func decodeX509(dst any, src []byte) error { return fmt.Errorf(`failed to decode PEM data`) } + muX509Decoders.RLock() + decoders := x509DecoderList + muX509Decoders.RUnlock() + var errs []error - for _, d := range x509DecoderList { + for _, d := range decoders { if err := d.DecodeX509(dst, block); err != nil { errs = append(errs, err) continue diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/jws/BUILD.bazel index 920d3f87b1..32dbdd1881 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/BUILD.bazel +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/BUILD.bazel @@ -18,6 +18,7 @@ go_library( "signer.go", "sign_context.go", "signature_builder.go", + "streaming_detached.go", "verifier.go", "verify_context.go", ], @@ -28,7 +29,6 @@ go_library( "//internal/base64", "//internal/ecutil", "//internal/json", - "//internal/jwxio", "//internal/tokens", "//internal/keyconv", "//internal/pool", @@ -39,6 +39,7 @@ go_library( "//jws/legacy", "//transform", "@com_github_lestrrat_go_blackmagic//:blackmagic", + "@com_github_lestrrat_go_dsig//:dsig", "@com_github_lestrrat_go_option_v2//:option", ], ) @@ -53,6 +54,7 @@ go_test( "message_test.go", "options_gen_test.go", "signer_test.go", + "streaming_detached_test.go", ], embed = [":jws"], deps = [ diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/errors.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/errors.go index d5e1762a6a..e4445bd547 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/errors.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/errors.go @@ -1,14 +1,81 @@ package jws import ( + "errors" "fmt" ) +// errCritPresent is returned by VerifyCompactFast when the protected +// header carries a "crit" list. The fast path cannot enforce RFC 7515 +// §4.1.11 (it has no WithCritExtension allowlist), so it refuses rather +// than silently accepting. The sentinel is wrapped in verifyError at the +// return site so the resulting error matches BOTH errors.Is(err, +// jws.ErrCritPresent()) (the specific reason) AND errors.Is(err, +// jws.VerifyError()) (the general class), letting callers choose the +// classification granularity that fits their code path. +var errCritPresent = errors.New("VerifyCompactFast: protected header contains \"crit\"; use jws.Verify") + +// ErrCritPresent returns the sentinel error returned by VerifyCompactFast +// when the protected header contains a "crit" list. The error returned +// from VerifyCompactFast also matches jws.VerifyError(), so callers that +// only branch on the general class still classify the refusal correctly. +func ErrCritPresent() error { + return errCritPresent +} + +// errB64Present is returned by VerifyCompactFast when the protected +// header carries a "b64" entry (typically b64=false per RFC 7797). The +// fast path assumes the default b64=true encoding for both the +// signing-input reconstruction and the post-verify payload decode; a +// b64=false message signed under non-conformant rules (b64 not declared +// in "crit") would otherwise verify cryptographically while returning +// a decoded payload that differs from the producer's intent. Refusing +// here defers such messages to jws.Verify, which has the +// WithDetachedPayload and WithCritExtension machinery to handle b64=false +// correctly. As with errCritPresent, the sentinel is wrapped in +// verifyError at the return site so the resulting error matches both +// errors.Is(err, jws.ErrB64Present()) and errors.Is(err, jws.VerifyError()). +var errB64Present = errors.New("VerifyCompactFast: protected header contains \"b64\"; use jws.Verify") + +// ErrB64Present returns the sentinel error returned by VerifyCompactFast +// when the protected header contains a "b64" entry. The error returned +// from VerifyCompactFast also matches jws.VerifyError(), so callers that +// only branch on the general class still classify the refusal correctly. +func ErrB64Present() error { + return errB64Present +} + +// errUnclassifiableKey is the common sentinel for AlgorithmsForKey +// failures: the key shape cannot be matched to any registered key type +// for signing. Three different code paths land here — Import-failed, +// kty-not-registered, and shape-rejected (e.g. ecdh) — but they're all +// the same logical "we can't classify this key" outcome from the +// caller's perspective. Wrap-with-this lets callers branch on +// errors.Is(err, jws.ErrUnclassifiableKey()) instead of pattern-matching +// the three error-message shapes the function previously emitted. +var errUnclassifiableKey = errors.New("jws: key cannot be classified for signing") + +// ErrUnclassifiableKey returns the sentinel that jws.AlgorithmsForKey +// (and indirectly jws.Sign / jws.Verify when option-time validation +// fails) wraps when the supplied key cannot be matched to a registered +// key type. Branching on this sentinel is the right way to ask "is this +// a 'we can't tell what this key is' failure?" — the wrapping error +// also carries the concrete %T or %q diagnostic in its message, so the +// human-readable error stays specific. +func ErrUnclassifiableKey() error { + return errUnclassifiableKey +} + type signError struct { error } -var errDefaultSignError = signerr(`unknown error`) +const ( + prefixJwsSign = `jws.Sign` + prefixJwsCompact = `jws.Compact` +) + +var errDefaultSignError = makeSignError(prefixJwsSign, `unknown error`) // SignError returns an error that can be passed to `errors.Is` to check if the error is a sign error. func SignError() error { @@ -24,8 +91,8 @@ func (signError) Is(err error) bool { return ok } -func signerr(f string, args ...any) error { - return signError{fmt.Errorf(`jws.Sign: `+f, args...)} +func makeSignError(prefix string, f string, args ...any) error { + return signError{fmt.Errorf(prefix+`: `+f, args...)} } // This error is returned when jws.Verify fails, but note that there's another type of @@ -34,7 +101,7 @@ type verifyError struct { error } -var errDefaultVerifyError = verifyerr(`unknown error`) +var errDefaultVerifyError = makeVerifyError(`unknown error`) // VerifyError returns an error that can be passed to `errors.Is` to check if the error is a verify error. func VerifyError() error { @@ -50,7 +117,7 @@ func (verifyError) Is(err error) bool { return ok } -func verifyerr(f string, args ...any) error { +func makeVerifyError(f string, args ...any) error { return verifyError{fmt.Errorf(`jws.Verify: `+f, args...)} } @@ -79,7 +146,7 @@ type parseError struct { error } -var errDefaultParseError = parseerr(`unknown error`) +var errDefaultParseError = makeParseError(`jws.Parse`, `unknown error`) // ParseError returns an error that can be passed to `errors.Is` to check if the error is a parse error. func ParseError() error { @@ -95,18 +162,6 @@ func (parseError) Is(err error) bool { return ok } -func bparseerr(prefix string, f string, args ...any) error { +func makeParseError(prefix string, f string, args ...any) error { return parseError{fmt.Errorf(prefix+": "+f, args...)} } - -func parseerr(f string, args ...any) error { - return bparseerr(`jws.Parse`, f, args...) -} - -func sparseerr(f string, args ...any) error { - return bparseerr(`jws.ParseString`, f, args...) -} - -func rparseerr(f string, args ...any) error { - return bparseerr(`jws.ParseReader`, f, args...) -} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/es256k.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/es256k.go index 28ebd2ea0e..73d746099a 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/es256k.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/es256k.go @@ -8,5 +8,5 @@ import ( func init() { // Register ES256K to EC algorithm family - addAlgorithmForKeyType(jwa.EC(), jwa.ES256K()) + RegisterAlgorithmForKeyType(jwa.EC(), jwa.ES256K()) } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/headers_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/headers_gen.go index 8465eda2b1..0628e626d2 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/headers_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/headers_gen.go @@ -20,6 +20,7 @@ import ( const ( AlgorithmKey = "alg" + B64Key = "b64" ContentTypeKey = "cty" CriticalKey = "crit" JWKKey = "jwk" @@ -41,6 +42,7 @@ const ( // In most cases, you likely want to use the protected headers, as this is part of the signed content. type Headers interface { Algorithm() (jwa.SignatureAlgorithm, bool) + B64() (bool, bool) ContentType() (string, bool) Critical() ([]string, bool) JWK() (jwk.Key, bool) @@ -72,10 +74,11 @@ type Headers interface { } // stdHeaderNames is a list of all standard header names defined in the JWS specification. -var stdHeaderNames = []string{AlgorithmKey, ContentTypeKey, CriticalKey, JWKKey, JWKSetURLKey, KeyIDKey, TypeKey, X509CertChainKey, X509CertThumbprintKey, X509CertThumbprintS256Key, X509URLKey} +var stdHeaderNames = []string{AlgorithmKey, B64Key, ContentTypeKey, CriticalKey, JWKKey, JWKSetURLKey, KeyIDKey, TypeKey, X509CertChainKey, X509CertThumbprintKey, X509CertThumbprintS256Key, X509URLKey} type stdHeaders struct { algorithm *jwa.SignatureAlgorithm // https://tools.ietf.org/html/rfc7515#section-4.1.1 + b64 *bool // https://tools.ietf.org/html/rfc7797#section-3 contentType *string // https://tools.ietf.org/html/rfc7515#section-4.1.10 critical []string // https://tools.ietf.org/html/rfc7515#section-4.1.11 jwk jwk.Key // https://tools.ietf.org/html/rfc7515#section-4.1.3 @@ -87,15 +90,13 @@ type stdHeaders struct { x509CertThumbprintS256 *string // https://tools.ietf.org/html/rfc7515#section-4.1.8 x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5 privateParams map[string]any - mu *sync.RWMutex + mu sync.RWMutex dc DecodeCtx raw []byte // stores the raw version of the header so it can be used later } func NewHeaders() Headers { - return &stdHeaders{ - mu: &sync.RWMutex{}, - } + return &stdHeaders{} } func (h *stdHeaders) Algorithm() (jwa.SignatureAlgorithm, bool) { @@ -107,6 +108,15 @@ func (h *stdHeaders) Algorithm() (jwa.SignatureAlgorithm, bool) { return *(h.algorithm), true } +func (h *stdHeaders) B64() (bool, bool) { + h.mu.RLock() + defer h.mu.RUnlock() + if h.b64 == nil { + return false, false + } + return *(h.b64), true +} + func (h *stdHeaders) ContentType() (string, bool) { h.mu.RLock() defer h.mu.RUnlock() @@ -119,13 +129,13 @@ func (h *stdHeaders) ContentType() (string, bool) { func (h *stdHeaders) Critical() ([]string, bool) { h.mu.RLock() defer h.mu.RUnlock() - return h.critical, true + return h.critical, h.critical != nil } func (h *stdHeaders) JWK() (jwk.Key, bool) { h.mu.RLock() defer h.mu.RUnlock() - return h.jwk, true + return h.jwk, h.jwk != nil } func (h *stdHeaders) JWKSetURL() (string, bool) { @@ -158,7 +168,7 @@ func (h *stdHeaders) Type() (string, bool) { func (h *stdHeaders) X509CertChain() (*cert.Chain, bool) { h.mu.RLock() defer h.mu.RUnlock() - return h.x509CertChain, true + return h.x509CertChain, h.x509CertChain != nil } func (h *stdHeaders) X509CertThumbprint() (string, bool) { @@ -190,6 +200,7 @@ func (h *stdHeaders) X509URL() (string, bool) { func (h *stdHeaders) clear() { h.algorithm = nil + h.b64 = nil h.contentType = nil h.critical = nil h.jwk = nil @@ -217,6 +228,8 @@ func (h *stdHeaders) SetDecodeCtx(dc DecodeCtx) { } func (h *stdHeaders) rawBuffer() []byte { + h.mu.RLock() + defer h.mu.RUnlock() return h.raw } @@ -232,6 +245,8 @@ func (h *stdHeaders) Has(name string) bool { switch name { case AlgorithmKey: return h.algorithm != nil + case B64Key: + return h.b64 != nil case ContentTypeKey: return h.contentType != nil case CriticalKey: @@ -270,6 +285,14 @@ func (h *stdHeaders) Get(name string, dst any) error { return fmt.Errorf(`failed to assign value for field %q: %w`, name, err) } return nil + case B64Key: + if h.b64 == nil { + return fmt.Errorf(`field %q not found`, name) + } + if err := blackmagic.AssignIfCompatible(dst, *(h.b64)); err != nil { + return fmt.Errorf(`failed to assign value for field %q: %w`, name, err) + } + return nil case ContentTypeKey: if h.contentType == nil { return fmt.Errorf(`field %q not found`, name) @@ -383,6 +406,12 @@ func (h *stdHeaders) setNoLock(name string, value any) error { return nil } return fmt.Errorf("expecte jwa.SignatureAlgorithm, received %T", alg) + case B64Key: + if v, ok := value.(bool); ok { + h.b64 = &v + return nil + } + return fmt.Errorf(`invalid value for %s key: %T`, B64Key, value) case ContentTypeKey: if v, ok := value.(string); ok { h.contentType = &v @@ -391,7 +420,12 @@ func (h *stdHeaders) setNoLock(name string, value any) error { return fmt.Errorf(`invalid value for %s key: %T`, ContentTypeKey, value) case CriticalKey: if v, ok := value.([]string); ok { - h.critical = v + if v == nil { + h.critical = nil + } else { + h.critical = make([]string, len(v)) + copy(h.critical, v) + } return nil } return fmt.Errorf(`invalid value for %s key: %T`, CriticalKey, value) @@ -458,6 +492,8 @@ func (h *stdHeaders) Remove(key string) error { switch key { case AlgorithmKey: h.algorithm = nil + case B64Key: + h.b64 = nil case ContentTypeKey: h.contentType = nil case CriticalKey: @@ -512,8 +548,14 @@ LOOP: return fmt.Errorf(`failed to decode value for key %s: %w`, AlgorithmKey, err) } h.algorithm = &decoded + case B64Key: + var decoded bool + if err := dec.Decode(&decoded); err != nil { + return fmt.Errorf(`failed to decode value for key %s: %w`, B64Key, err) + } + h.b64 = &decoded case ContentTypeKey: - if err := json.AssignNextStringToken(&h.contentType, dec); err != nil { + if err := json.AssignNextStringToken(&h.contentType, dec, nil); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, ContentTypeKey, err) } case CriticalKey: @@ -533,15 +575,15 @@ LOOP: } h.jwk = key case JWKSetURLKey: - if err := json.AssignNextStringToken(&h.jwkSetURL, dec); err != nil { + if err := json.AssignNextStringToken(&h.jwkSetURL, dec, nil); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, JWKSetURLKey, err) } case KeyIDKey: - if err := json.AssignNextStringToken(&h.keyID, dec); err != nil { + if err := json.AssignNextStringToken(&h.keyID, dec, nil); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err) } case TypeKey: - if err := json.AssignNextStringToken(&h.typ, dec); err != nil { + if err := json.AssignNextStringToken(&h.typ, dec, nil); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, TypeKey, err) } case X509CertChainKey: @@ -551,15 +593,15 @@ LOOP: } h.x509CertChain = &decoded case X509CertThumbprintKey: - if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, nil); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err) } case X509CertThumbprintS256Key: - if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, nil); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err) } case X509URLKey: - if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil { + if err := json.AssignNextStringToken(&h.x509URL, dec, nil); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err) } default: @@ -580,10 +622,13 @@ LOOP: func (h *stdHeaders) Keys() []string { h.mu.RLock() defer h.mu.RUnlock() - keys := make([]string, 0, 11+len(h.privateParams)) + keys := make([]string, 0, 12+len(h.privateParams)) if h.algorithm != nil { keys = append(keys, AlgorithmKey) } + if h.b64 != nil { + keys = append(keys, B64Key) + } if h.contentType != nil { keys = append(keys, ContentTypeKey) } @@ -620,85 +665,161 @@ func (h *stdHeaders) Keys() []string { return keys } -func (h stdHeaders) MarshalJSON() ([]byte, error) { +type headerPair struct { + Name string + Value any +} + +var headerPairPool = sync.Pool{ + New: func() any { + return make([]headerPair, 0, 12) + }, +} + +func getHeaderPairList() []headerPair { + return headerPairPool.Get().([]headerPair) +} + +func putHeaderPairList(list []headerPair) { + list = list[:0] + headerPairPool.Put(list) +} + +func (h *stdHeaders) makePairs() ([]headerPair, error) { + pairs := getHeaderPairList() h.mu.RLock() - data := make(map[string]any) - keys := make([]string, 0, 11+len(h.privateParams)) + defer h.mu.RUnlock() if h.algorithm != nil { - data[AlgorithmKey] = *(h.algorithm) - keys = append(keys, AlgorithmKey) + v, err := json.Marshal(*(h.algorithm)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err) + } + pairs = append(pairs, headerPair{Name: AlgorithmKey, Value: v}) + } + if h.b64 != nil { + v, err := json.Marshal(*(h.b64)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, B64Key, err) + } + pairs = append(pairs, headerPair{Name: B64Key, Value: v}) } if h.contentType != nil { - data[ContentTypeKey] = *(h.contentType) - keys = append(keys, ContentTypeKey) + v, err := json.Marshal(*(h.contentType)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, ContentTypeKey, err) + } + pairs = append(pairs, headerPair{Name: ContentTypeKey, Value: v}) } if h.critical != nil { - data[CriticalKey] = h.critical - keys = append(keys, CriticalKey) + v, err := json.Marshal(h.critical) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, CriticalKey, err) + } + pairs = append(pairs, headerPair{Name: CriticalKey, Value: v}) } if h.jwk != nil { - data[JWKKey] = h.jwk - keys = append(keys, JWKKey) + v, err := json.Marshal(h.jwk) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, JWKKey, err) + } + pairs = append(pairs, headerPair{Name: JWKKey, Value: v}) } if h.jwkSetURL != nil { - data[JWKSetURLKey] = *(h.jwkSetURL) - keys = append(keys, JWKSetURLKey) + v, err := json.Marshal(*(h.jwkSetURL)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, JWKSetURLKey, err) + } + pairs = append(pairs, headerPair{Name: JWKSetURLKey, Value: v}) } if h.keyID != nil { - data[KeyIDKey] = *(h.keyID) - keys = append(keys, KeyIDKey) + v, err := json.Marshal(*(h.keyID)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err) + } + pairs = append(pairs, headerPair{Name: KeyIDKey, Value: v}) } if h.typ != nil { - data[TypeKey] = *(h.typ) - keys = append(keys, TypeKey) + v, err := json.Marshal(*(h.typ)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, TypeKey, err) + } + pairs = append(pairs, headerPair{Name: TypeKey, Value: v}) } if h.x509CertChain != nil { - data[X509CertChainKey] = h.x509CertChain - keys = append(keys, X509CertChainKey) + v, err := json.Marshal(h.x509CertChain) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err) + } + pairs = append(pairs, headerPair{Name: X509CertChainKey, Value: v}) } if h.x509CertThumbprint != nil { - data[X509CertThumbprintKey] = *(h.x509CertThumbprint) - keys = append(keys, X509CertThumbprintKey) + v, err := json.Marshal(*(h.x509CertThumbprint)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err) + } + pairs = append(pairs, headerPair{Name: X509CertThumbprintKey, Value: v}) } if h.x509CertThumbprintS256 != nil { - data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256) - keys = append(keys, X509CertThumbprintS256Key) + v, err := json.Marshal(*(h.x509CertThumbprintS256)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err) + } + pairs = append(pairs, headerPair{Name: X509CertThumbprintS256Key, Value: v}) } if h.x509URL != nil { - data[X509URLKey] = *(h.x509URL) - keys = append(keys, X509URLKey) + v, err := json.Marshal(*(h.x509URL)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err) + } + pairs = append(pairs, headerPair{Name: X509URLKey, Value: v}) } for k, v := range h.privateParams { - data[k] = v - keys = append(keys, k) + var encoded []byte + switch v := v.(type) { + case []byte: + var err error + encoded, err = json.Marshal(base64.EncodeToString(v)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + default: + var err error + encoded, err = json.Marshal(v) + if err != nil { + return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err) + } + } + pairs = append(pairs, headerPair{Name: k, Value: encoded}) } - h.mu.RUnlock() - sort.Strings(keys) + + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Name < pairs[j].Name + }) + + return pairs, nil +} + +func (h *stdHeaders) MarshalJSON() ([]byte, error) { buf := pool.BytesBuffer().Get() defer pool.BytesBuffer().Put(buf) - enc := json.NewEncoder(buf) + pairs, err := h.makePairs() + if err != nil { + return nil, fmt.Errorf(`failed to make pairs: %w`, err) + } buf.WriteByte(tokens.OpenCurlyBracket) - for i, k := range keys { + + for i, pair := range pairs { if i > 0 { - buf.WriteRune(tokens.Comma) - } - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(k) - buf.WriteString(`":`) - switch v := data[k].(type) { - case []byte: - buf.WriteRune(tokens.DoubleQuote) - buf.WriteString(base64.EncodeToString(v)) - buf.WriteRune(tokens.DoubleQuote) - default: - if err := enc.Encode(v); err != nil { - return nil, fmt.Errorf(`failed to encode value for field %s: %w`, k, err) - } - buf.Truncate(buf.Len() - 1) + buf.WriteByte(tokens.Comma) } + buf.WriteByte('"') + buf.WriteString(pair.Name) + buf.WriteString(`": `) + buf.Write(pair.Value.([]byte)) } buf.WriteByte(tokens.CloseCurlyBracket) ret := make([]byte, buf.Len()) copy(ret, buf.Bytes()) + putHeaderPairList(pairs) return ret, nil } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/interface.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/interface.go index e3ad296844..c87252344d 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/interface.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/interface.go @@ -5,9 +5,16 @@ import ( "github.com/lestrrat-go/jwx/v3/jws/legacy" ) +// Deprecated: Use the legacy package directly. These type aliases will be removed in v4. type Signer = legacy.Signer + +// Deprecated: Use the legacy package directly. These type aliases will be removed in v4. type Verifier = legacy.Verifier + +// Deprecated: Use the legacy package directly. These type aliases will be removed in v4. type HMACSigner = legacy.HMACSigner + +// Deprecated: Use the legacy package directly. These type aliases will be removed in v4. type HMACVerifier = legacy.HMACVerifier // Base64Encoder is an interface that can be used when encoding JWS message @@ -20,6 +27,15 @@ type HMACVerifier = legacy.HMACVerifier // but it uses a base64 encoding with padding. type Base64Encoder = base64.Encoder +// Base64StreamEncoder is the stream-capable extension of +// [Base64Encoder]. Encoders that satisfy this interface can be used +// with the streaming detached-payload path +// ([jws.WithDetachedPayloadReader]). The default encoder +// (`encoding/base64.RawURLEncoding`) satisfies it. Custom encoders +// that do not satisfy it cause [jws.Sign] / [jws.Verify] with +// [jws.WithDetachedPayloadReader] to return an error. +type Base64StreamEncoder = base64.StreamEncoder + type DecodeCtx interface { CollectRaw() bool } @@ -63,11 +79,21 @@ type DecodeCtx interface { // headers and the signatures don't match. // // To sign and verify, use the appropriate `Sign()` and `Verify()` functions. +// +// JSON round-trip note: Message.MarshalJSON collapses any single-signature +// general-form input (one with a top-level "signatures" array of length 1) +// into the flattened form (with top-level "protected"/"signature" fields) +// on output. The conversion is cryptographically lossless — the protected, +// signature, and payload bytes survive — but byte-level identity changes, +// so callers that hash or dedup JWS messages by their JSON encoding will +// not recognize a round-tripped message as equal to its input. type Message struct { - dc DecodeCtx - payload []byte - signatures []*Signature - b64 bool // true if payload should be base64 encoded + dc DecodeCtx + payload []byte + signatures []*Signature + b64 bool // true if payload should be base64 encoded + detached bool // true if the JWS is a detached-payload form: JSON output omits the "payload" member per RFC 7515 Appendix F + maxSignatures int // scratch cap enforced during UnmarshalJSON; 0 means use global default } type Signature struct { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/io.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/io.go index 77a084cfda..894c1195e7 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/io.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/io.go @@ -15,6 +15,12 @@ func (sysFS) Open(path string) (fs.File, error) { } func ReadFile(path string, options ...ReadFileOption) (*Message, error) { + var parseOptions []ParseOption + for _, option := range options { + if po, ok := option.(ParseOption); ok { + parseOptions = append(parseOptions, po) + } + } var srcFS fs.FS = sysFS{} for _, option := range options { @@ -32,5 +38,5 @@ func ReadFile(path string, options ...ReadFileOption) (*Message, error) { } defer f.Close() - return ParseReader(f) + return ParseReader(f, parseOptions...) } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/jws.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/jws.go index f09e40db2d..99bf78581a 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/jws.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/jws.go @@ -26,7 +26,7 @@ package jws import ( - "bufio" + "crypto" "crypto/ecdh" "crypto/ecdsa" "crypto/ed25519" @@ -34,14 +34,14 @@ import ( "errors" "fmt" "io" - "reflect" + "slices" "sync" + "sync/atomic" "unicode" "unicode/utf8" "github.com/lestrrat-go/jwx/v3/internal/base64" "github.com/lestrrat-go/jwx/v3/internal/json" - "github.com/lestrrat-go/jwx/v3/internal/jwxio" "github.com/lestrrat-go/jwx/v3/internal/pool" "github.com/lestrrat-go/jwx/v3/internal/tokens" "github.com/lestrrat-go/jwx/v3/jwa" @@ -51,6 +51,12 @@ import ( var registry = json.NewRegistry() +var maxSignatures atomic.Int64 + +func init() { + maxSignatures.Store(100) +} + var signers = make(map[jwa.SignatureAlgorithm]Signer) var muSigner = &sync.Mutex{} @@ -92,10 +98,6 @@ const ( fmtMax ) -// silence linters -var _ = fmtInvalid -var _ = fmtMax - func validateKeyBeforeUse(key any) error { jwkKey, ok := key.(jwk.Key) if !ok { @@ -143,6 +145,16 @@ func validateKeyBeforeUse(key any) error { // signing process, as you will likely be required to set the `b64` field // when using detached payload. // +// RFC 7797 note: producing an in-band compact JWS with `b64=false` +// (i.e. setting the `b64` protected header to `false` without also +// passing [WithDetachedPayload]) is "NOT RECOMMENDED" per §5.2; strict +// peers commonly reject such messages. The canonical pairing for +// `b64=false` is [WithDetachedPayload] (or [WithDetachedPayloadReader] +// for streaming), which keeps the unencoded payload out of the wire +// format. Sign auto-declares `"b64"` in `crit` whenever `b64=false` +// is set, so the produced JWS is at least RFC 7797 §3 conformant on +// the producer side. +// // Look for options that return `jws.SignOption` or `jws.SignVerifyOption` // for a complete list of options that can be passed to this function. // @@ -154,12 +166,12 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) { sc.payload = payload if err := sc.ProcessOptions(options); err != nil { - return nil, signerr(`failed to process options: %w`, err) + return nil, makeSignError(prefixJwsSign, `failed to process options: %w`, err) } lsigner := len(sc.sigbuilders) if lsigner == 0 { - return nil, signerr(`no signers available. Specify an algorithm and a key using jws.WithKey()`) + return nil, makeSignError(prefixJwsSign, `no signers available. Specify an algorithm and a key using jws.WithKey()`) } // Design note: while we could have easily set format = fmtJSON when @@ -171,7 +183,11 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) { // Therefore, instead of making implicit format conversions, we force the // user to spell it out as `jws.Sign(..., jws.WithJSON(), jws.WithKey(...), jws.WithKey(...))` if sc.format == fmtCompact && lsigner != 1 { - return nil, signerr(`cannot have multiple signers (keys) specified for compact serialization. Use only one jws.WithKey()`) + return nil, makeSignError(prefixJwsSign, `cannot have multiple signers (keys) specified for compact serialization. Use only one jws.WithKey()`) + } + + if sc.payloadReader != nil { + return sc.signStreaming() } // Create a Message object with all the bits and bobs, and we'll @@ -179,7 +195,7 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) { var result Message if err := sc.PopulateMessage(&result); err != nil { - return nil, signerr(`failed to populate message: %w`, err) + return nil, makeSignError(prefixJwsSign, `failed to populate message: %w`, err) } switch sc.format { case fmtJSON: @@ -200,7 +216,7 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) { } return Compact(&result, compactOpts...) default: - return nil, signerr(`invalid serialization format`) + return nil, makeSignError(prefixJwsSign, `invalid serialization format`) } } @@ -230,27 +246,58 @@ var allowNoneWhitelist = jwk.WhitelistFunc(func(string) bool { // when the verification process itself fails (e.g. invalid signature, wrong key), // while the former is returned when any other part of the `jws.Verify()` // function fails. +// +// When `jws.WithDetachedPayloadReader()` is used, the payload is streamed +// from the caller's `io.Reader` and is not extracted from the JWS envelope. +// In that case, the returned `[]byte` is a non-nil zero-length slice on +// success; the verified bytes are whatever the caller read from the Reader. +// Do not treat the returned slice as "the payload is empty" — callers that +// need the payload bytes must retain their own copy. +// +// Context cancellation is governed by [WithContext]. The slow-path verify +// loop checks ctx.Err() between each signature, each key provider, and +// each (alg, key) attempt; jkuProvider passes ctx to its underlying +// jwk.Fetcher; the streaming path checks ctx between payload Reads. +// staticKeyProvider and keySetProvider do not consult ctx inside +// FetchKeys themselves (their backing data is already in memory) — see +// the [WithContext] godoc for the full per-layer breakdown. func Verify(buf []byte, options ...VerifyOption) ([]byte, error) { vc := verifyContextPool.Get() defer verifyContextPool.Put(vc) if err := vc.ProcessOptions(options); err != nil { - return nil, verifyerr(`failed to process options: %w`, err) + return nil, makeVerifyError(`failed to process options: %w`, err) } return vc.VerifyMessage(buf) } -// get the value of b64 header field. -// If the field does not exist, returns true (default) -// Otherwise return the value specified by the header field. +// getB64Value reads the typed "b64" header field and returns its value, +// or RFC 7797's default of true when the field is unset. func getB64Value(hdr Headers) bool { - var b64 bool - if err := hdr.Get("b64", &b64); err != nil { - return true // default + v, ok := hdr.B64() + if !ok { + return true // RFC 7797 default } + return v +} - return b64 +func detectParseFormat(src []byte) int { + for i := 0; i < len(src); { + r := rune(src[i]) + width := 1 + if r >= utf8.RuneSelf { + r, width = utf8.DecodeRune(src[i:]) + } + if !unicode.IsSpace(r) { + if r == tokens.OpenCurlyBracket { + return fmtJSON + } + return fmtCompact + } + i += width + } + return 0 } // Parse parses contents from the given source and creates a jws.Message @@ -261,15 +308,20 @@ func getB64Value(hdr Headers) bool { // will attempt to autodetect the format. If one or the other is specified, // only the specified format will be attempted. // +// Bounding the input size is the caller's responsibility; this function +// trusts the caller-provided src. See docs/13-input-size.md. +// // On error, returns a jws.ParseError. func Parse(src []byte, options ...ParseOption) (*Message, error) { + maxSigs := int(maxSignatures.Load()) + var formats int for _, option := range options { switch option.Ident() { case identSerialization{}: var v int if err := option.Value(&v); err != nil { - return nil, parseerr(`failed to retrieve serialization option value: %w`, err) + return nil, makeParseError(`jws.Parse`, `failed to retrieve serialization option value: %w`, err) } switch v { case fmtJSON: @@ -277,53 +329,46 @@ func Parse(src []byte, options ...ParseOption) (*Message, error) { case fmtCompact: formats |= fmtCompact } + case identMaxSignatures{}: + if err := option.Value(&maxSigs); err != nil { + return nil, makeParseError(`jws.Parse`, `failed to retrieve max signatures option value: %w`, err) + } + if maxSigs <= 0 { + return nil, makeParseError(`jws.Parse`, `WithMaxSignatures must be greater than zero`) + } } } // if format is 0 or both JSON/Compact, auto detect if v := formats & (fmtJSON | fmtCompact); v == 0 || v == fmtJSON|fmtCompact { - CHECKLOOP: - for i := range src { - r := rune(src[i]) - if r >= utf8.RuneSelf { - r, _ = utf8.DecodeRune(src) - } - if !unicode.IsSpace(r) { - if r == tokens.OpenCurlyBracket { - formats = fmtJSON - } else { - formats = fmtCompact - } - break CHECKLOOP - } - } + formats = detectParseFormat(src) } if formats&fmtCompact == fmtCompact { msg, err := parseCompact(src) if err != nil { - return nil, parseerr(`failed to parse compact format: %w`, err) + return nil, makeParseError(`jws.Parse`, `failed to parse compact format: %w`, err) } return msg, nil } else if formats&fmtJSON == fmtJSON { - msg, err := parseJSON(src) + msg, err := parseJSON(src, maxSigs) if err != nil { - return nil, parseerr(`failed to parse JSON format: %w`, err) + return nil, makeParseError(`jws.Parse`, `failed to parse JSON format: %w`, err) } return msg, nil } - return nil, parseerr(`invalid byte sequence`) + return nil, makeParseError(`jws.Parse`, `invalid byte sequence`) } // ParseString parses contents from the given source and creates a jws.Message // struct. The input can be in either compact or full JSON serialization. // // On error, returns a jws.ParseError. -func ParseString(src string) (*Message, error) { - msg, err := Parse([]byte(src)) +func ParseString(src string, options ...ParseOption) (*Message, error) { + msg, err := Parse([]byte(src), options...) if err != nil { - return nil, sparseerr(`failed to parse string: %w`, err) + return nil, makeParseError(`jws.ParseString`, `failed to parse string: %w`, err) } return msg, nil } @@ -331,59 +376,22 @@ func ParseString(src string) (*Message, error) { // ParseReader parses contents from the given source and creates a jws.Message // struct. The input can be in either compact or full JSON serialization. // +// Bounding the input size is the caller's responsibility: wrap src with +// [io.LimitReader] or [net/http.MaxBytesReader] before passing it in. See +// docs/13-input-size.md for the rationale. +// // On error, returns a jws.ParseError. -func ParseReader(src io.Reader) (*Message, error) { - data, err := jwxio.ReadAllFromFiniteSource(src) - if err == nil { - return Parse(data) - } - - if !errors.Is(err, jwxio.NonFiniteSourceError()) { - return nil, rparseerr(`failed to read from finite source: %w`, err) - } - - rdr := bufio.NewReader(src) - var first rune - for { - r, _, err := rdr.ReadRune() - if err != nil { - return nil, rparseerr(`failed to read rune: %w`, err) - } - if !unicode.IsSpace(r) { - first = r - if err := rdr.UnreadRune(); err != nil { - return nil, rparseerr(`failed to unread rune: %w`, err) - } - - break - } - } - - var parser func(io.Reader) (*Message, error) - if first == tokens.OpenCurlyBracket { - parser = parseJSONReader - } else { - parser = parseCompactReader - } - - m, err := parser(rdr) +func ParseReader(src io.Reader, options ...ParseOption) (*Message, error) { + buf, err := io.ReadAll(src) if err != nil { - return nil, rparseerr(`failed to parse reader: %w`, err) - } - - return m, nil -} - -func parseJSONReader(src io.Reader) (result *Message, err error) { - var m Message - if err := json.NewDecoder(src).Decode(&m); err != nil { - return nil, fmt.Errorf(`failed to unmarshal jws message: %w`, err) + return nil, makeParseError(`jws.ParseReader`, `failed to read from io.Reader: %w`, err) } - return &m, nil + return Parse(buf, options...) } -func parseJSON(data []byte) (result *Message, err error) { +func parseJSON(data []byte, maxSigs int) (result *Message, err error) { var m Message + m.maxSignatures = maxSigs if err := json.Unmarshal(data, &m); err != nil { return nil, fmt.Errorf(`failed to unmarshal jws message: %w`, err) } @@ -394,12 +402,12 @@ func parseJSON(data []byte) (result *Message, err error) { // separately: protected headers, payload and signature. // On error, returns a jws.ParseError. // -// This function will be deprecated in v4. It is a low-level API, and -// thus will be available in the `jwsbb` package. +// Deprecated: This is a low-level API that will be removed in v4. +// Use the jwsbb package directly instead. func SplitCompact(src []byte) ([]byte, []byte, []byte, error) { hdr, payload, signature, err := jwsbb.SplitCompact(src) if err != nil { - return nil, nil, nil, parseerr(`%w`, err) + return nil, nil, nil, makeParseError(`jws.Parse`, `%w`, err) } return hdr, payload, signature, nil } @@ -408,12 +416,12 @@ func SplitCompact(src []byte) ([]byte, []byte, []byte, error) { // separately: protected headers, payload and signature. // On error, returns a jws.ParseError. // -// This function will be deprecated in v4. It is a low-level API, and -// thus will be available in the `jwsbb` package. +// Deprecated: This is a low-level API that will be removed in v4. +// Use the jwsbb package directly instead. func SplitCompactString(src string) ([]byte, []byte, []byte, error) { hdr, payload, signature, err := jwsbb.SplitCompactString(src) if err != nil { - return nil, nil, nil, parseerr(`%w`, err) + return nil, nil, nil, makeParseError(`jws.Parse`, `%w`, err) } return hdr, payload, signature, nil } @@ -422,29 +430,20 @@ func SplitCompactString(src string) ([]byte, []byte, []byte, error) { // separately: protected headers, payload and signature. // On error, returns a jws.ParseError. // -// This function will be deprecated in v4. It is a low-level API, and -// thus will be available in the `jwsbb` package. +// Deprecated: This is a low-level API that will be removed in v4. +// Use the jwsbb package directly instead. func SplitCompactReader(rdr io.Reader) ([]byte, []byte, []byte, error) { hdr, payload, signature, err := jwsbb.SplitCompactReader(rdr) if err != nil { - return nil, nil, nil, parseerr(`%w`, err) + return nil, nil, nil, makeParseError(`jws.Parse`, `%w`, err) } return hdr, payload, signature, nil } -// parseCompactReader parses a JWS value serialized via compact serialization. -func parseCompactReader(rdr io.Reader) (m *Message, err error) { - protected, payload, signature, err := SplitCompactReader(rdr) - if err != nil { - return nil, fmt.Errorf(`invalid compact serialization format: %w`, err) - } - return parse(protected, payload, signature) -} - func parseCompact(data []byte) (m *Message, err error) { - protected, payload, signature, err := SplitCompact(data) + protected, payload, signature, err := jwsbb.SplitCompact(data) if err != nil { - return nil, fmt.Errorf(`invalid compact serialization format: %w`, err) + return nil, makeParseError(`jws.Parse`, `invalid compact serialization format: %w`, err) } return parse(protected, payload, signature) } @@ -476,6 +475,12 @@ func parse(protected, payload, signature []byte) (*Message, error) { if err != nil { return nil, fmt.Errorf(`failed to decode signature: %w`, err) } + if len(decodedSignature) == 0 { + alg, ok := hdr.Algorithm() + if !ok || alg != jwa.NoSignature() { + return nil, fmt.Errorf(`empty compact signature requires protected header "alg" to be "none"`) + } + } var msg Message msg.payload = decodedPayload @@ -530,60 +535,259 @@ func RegisterCustomField(name string, object any) { registry.Register(name, object) } +// curver is implemented by jwk.Key types that carry curve information. +type curver interface { + Crv() (jwa.EllipticCurveAlgorithm, bool) +} + // Helpers for signature verification -var rawKeyToKeyType = make(map[reflect.Type]jwa.KeyType) +var muAlgorithmMaps sync.RWMutex var keyTypeToAlgorithms = make(map[jwa.KeyType][]jwa.SignatureAlgorithm) +var algorithmToKeyTypes = make(map[jwa.SignatureAlgorithm][]jwa.KeyType) +var curveToAlgorithms = make(map[jwa.EllipticCurveAlgorithm][]jwa.SignatureAlgorithm) func init() { - rawKeyToKeyType[reflect.TypeFor[[]byte]()] = jwa.OctetSeq() - rawKeyToKeyType[reflect.TypeFor[ed25519.PublicKey]()] = jwa.OKP() - rawKeyToKeyType[reflect.TypeFor[rsa.PublicKey]()] = jwa.RSA() - rawKeyToKeyType[reflect.TypeFor[*rsa.PublicKey]()] = jwa.RSA() - rawKeyToKeyType[reflect.TypeFor[ecdsa.PublicKey]()] = jwa.EC() - rawKeyToKeyType[reflect.TypeFor[*ecdsa.PublicKey]()] = jwa.EC() - - addAlgorithmForKeyType(jwa.OKP(), jwa.EdDSA()) + RegisterAlgorithmForKeyType(jwa.OKP(), jwa.EdDSA()) + RegisterAlgorithmForCurve(jwa.Ed25519(), jwa.EdDSAEd25519()) for _, alg := range []jwa.SignatureAlgorithm{jwa.HS256(), jwa.HS384(), jwa.HS512()} { - addAlgorithmForKeyType(jwa.OctetSeq(), alg) + RegisterAlgorithmForKeyType(jwa.OctetSeq(), alg) } for _, alg := range []jwa.SignatureAlgorithm{jwa.RS256(), jwa.RS384(), jwa.RS512(), jwa.PS256(), jwa.PS384(), jwa.PS512()} { - addAlgorithmForKeyType(jwa.RSA(), alg) + RegisterAlgorithmForKeyType(jwa.RSA(), alg) } for _, alg := range []jwa.SignatureAlgorithm{jwa.ES256(), jwa.ES384(), jwa.ES512()} { - addAlgorithmForKeyType(jwa.EC(), alg) + RegisterAlgorithmForKeyType(jwa.EC(), alg) } } -func addAlgorithmForKeyType(kty jwa.KeyType, alg jwa.SignatureAlgorithm) { +// RegisterAlgorithmForKeyType registers an additional algorithm as valid for +// the given key type. This is used internally by init() and can also be called +// from external modules that provide support for additional algorithms (e.g. Ed448). +func RegisterAlgorithmForKeyType(kty jwa.KeyType, alg jwa.SignatureAlgorithm) { + muAlgorithmMaps.Lock() + defer muAlgorithmMaps.Unlock() keyTypeToAlgorithms[kty] = append(keyTypeToAlgorithms[kty], alg) + if !slices.Contains(algorithmToKeyTypes[alg], kty) { + algorithmToKeyTypes[alg] = append(algorithmToKeyTypes[alg], kty) + } +} + +// RegisterAlgorithmForCurve registers an algorithm as valid for the given +// elliptic curve. When [AlgorithmsForKey] can determine the curve of a key, +// it returns the union of key-type-level algorithms and curve-specific +// algorithms instead of all algorithms for the key type. +// +// This function is append-only and deduplicates entries, so builtin +// registrations cannot be overwritten by external modules. +func RegisterAlgorithmForCurve(crv jwa.EllipticCurveAlgorithm, alg jwa.SignatureAlgorithm) { + muAlgorithmMaps.Lock() + defer muAlgorithmMaps.Unlock() + if slices.Contains(curveToAlgorithms[crv], alg) { + return + } + curveToAlgorithms[crv] = append(curveToAlgorithms[crv], alg) } // AlgorithmsForKey returns the possible signature algorithms that can // be used for a given key. It only takes in consideration keys/algorithms // for verification purposes, as this is the only usage where one may need // dynamically figure out which method to use. +// +// When the key's curve can be determined (via [jwk.Key] Crv() method or +// inferred from the raw Go type), curve-specific algorithms registered via +// [RegisterAlgorithmForCurve] are combined with key-type-level algorithms +// to produce a more precise result. +// +// Accepted key shapes (resolved in order): +// +// 1. [jwk.Key] — kty is read directly; if the implementation also exposes +// Crv(), the curve refines the result. +// 2. Stdlib crypto types: [rsa.PublicKey] / [rsa.PrivateKey] (and pointer +// forms), [ecdsa.PublicKey] / [ecdsa.PrivateKey] (and pointer forms), +// [ed25519.PublicKey], [ed25519.PrivateKey], and [byte] slices for +// symmetric keys. +// 3. [crypto/ecdh.PublicKey] / [crypto/ecdh.PrivateKey] (and pointer +// forms) — explicitly rejected; ECDH keys are key-agreement only. +// Returns an error wrapping [ErrUnclassifiableKey]. +// 4. [crypto.Signer] (e.g. KMS-backed adapters) — resolved once via +// .Public(); the public key is then re-classified through tiers 1–2 +// or the [jwk.Import] fallback below. To prevent infinite recursion, +// a Signer whose .Public() is itself a Signer is left for the +// downstream dispatcher to handle. +// 5. [jwk.Import] fallback — anything else is offered to the import +// registry, allowing extension modules to register their own raw key +// types. +// +// All "we cannot classify this key" failures wrap [ErrUnclassifiableKey], +// so callers can branch with errors.Is rather than pattern-matching error +// strings. The wrapping error keeps the concrete %T or %q diagnostic in +// its message for human readers. func AlgorithmsForKey(key any) ([]jwa.SignatureAlgorithm, error) { var kty jwa.KeyType + var crv jwa.EllipticCurveAlgorithm + var hasCrv bool + switch key := key.(type) { case jwk.Key: kty = key.KeyType() + if ck, ok := key.(curver); ok { + crv, hasCrv = ck.Crv() + } case rsa.PublicKey, *rsa.PublicKey, rsa.PrivateKey, *rsa.PrivateKey: kty = jwa.RSA() case ecdsa.PublicKey, *ecdsa.PublicKey, ecdsa.PrivateKey, *ecdsa.PrivateKey: kty = jwa.EC() - case ed25519.PublicKey, ed25519.PrivateKey, *ecdh.PublicKey, ecdh.PublicKey, *ecdh.PrivateKey, ecdh.PrivateKey: + case ed25519.PublicKey, ed25519.PrivateKey: kty = jwa.OKP() + crv = jwa.Ed25519() + hasCrv = true + case *ecdh.PublicKey, ecdh.PublicKey, *ecdh.PrivateKey, ecdh.PrivateKey: + // ecdh keys are for key agreement (X25519/X448), not signing. + // Reject at the API boundary instead of returning a misleading + // algorithm list that would fail deeper in the signing stack. + return nil, fmt.Errorf(`%w: key type %T cannot be used for signing (ecdh keys are key-agreement only)`, errUnclassifiableKey, key) case []byte: kty = jwa.OctetSeq() default: - return nil, fmt.Errorf(`unknown key type %T`, key) + // For crypto.Signer from external packages (e.g. KMS-backed signers), + // extract the underlying public key type via .Public(). + // Standard library types (*rsa.PrivateKey, etc.) are already handled + // by the concrete cases above. + var signerPubErr error + if signer, ok := key.(crypto.Signer); ok { + pub := signer.Public() + // Guard: only recurse if the public key is not itself a crypto.Signer, + // to prevent infinite recursion from pathological implementations. + if _, isSigner := pub.(crypto.Signer); !isSigner { + algs, err := AlgorithmsForKey(pub) + if err == nil { + return algs, nil + } + // Save the inner classification error so a + // downstream Import-fallback failure can surface + // both diagnostics. A successful Import discards + // signerPubErr — only the eventual failure path + // joins them. + signerPubErr = err + } + } + imported, err := jwk.Import(key) + if err != nil { + outer := fmt.Errorf(`%w: unknown key type %T`, errUnclassifiableKey, key) + if signerPubErr != nil { + return nil, errors.Join(outer, signerPubErr) + } + return nil, outer + } + kty = imported.KeyType() + if ck, ok := imported.(curver); ok { + crv, hasCrv = ck.Crv() + } } - algs, ok := keyTypeToAlgorithms[kty] + muAlgorithmMaps.RLock() + defer muAlgorithmMaps.RUnlock() + + ktyAlgs, ok := keyTypeToAlgorithms[kty] if !ok { - return nil, fmt.Errorf(`unregistered key type %q`, kty) + return nil, fmt.Errorf(`%w: unregistered key type %q`, errUnclassifiableKey, kty) + } + + // If we know the curve and there are curve-specific registrations, + // return only key-type-level algorithms (those not registered under + // any curve) plus curve-specific algorithms for this curve. + if hasCrv { + crvAlgs := curveToAlgorithms[crv] + return filterAlgorithmsForCurve(ktyAlgs, crvAlgs), nil + } + + return ktyAlgs, nil +} + +// filterAlgorithmsForCurve returns the subset of ktyAlgs that are not +// registered under any curve (i.e., generic for the key type) plus the +// curve-specific algorithms from crvAlgs. +func filterAlgorithmsForCurve(ktyAlgs, crvAlgs []jwa.SignatureAlgorithm) []jwa.SignatureAlgorithm { + var result []jwa.SignatureAlgorithm + + // Add key-type-level algorithms that are not claimed by any curve + for _, alg := range ktyAlgs { + if !isRegisteredUnderAnyCurve(alg) { + result = append(result, alg) + } } - return algs, nil + + // Add curve-specific algorithms + result = append(result, crvAlgs...) + return result +} + +func isRegisteredUnderAnyCurve(alg jwa.SignatureAlgorithm) bool { + for _, algs := range curveToAlgorithms { + if slices.Contains(algs, alg) { + return true + } + } + return false +} + +// validateAlgorithmForKey checks that alg is compatible with key. +// Three classification failures are intentionally allowed through: +// (a) a nil key, used by keyless algorithms (see GH910); +// (b) any key handed to an algorithm with a user-registered custom +// Signer2/Verifier2 — custom implementations may accept arbitrary key +// types that AlgorithmsForKey cannot classify; and +// (c) an opaque crypto.Signer whose .Public() is itself a crypto.Signer, +// the one case AlgorithmsForKey refuses to recurse into. +// Every other classification failure is surfaced so callers get a crisp +// option-boundary rejection instead of a deep-stack error. +func validateAlgorithmForKey(alg jwa.SignatureAlgorithm, key any) error { + if key == nil { + return nil + } + algs, err := AlgorithmsForKey(key) + if err != nil { + if hasCustomSigVerifier(alg) { + return nil + } + if signer, ok := key.(crypto.Signer); ok { + if _, isSigner := signer.Public().(crypto.Signer); isSigner { + return nil + } + } + return fmt.Errorf(`jws.WithKey: %w`, err) + } + if !slices.Contains(algs, alg) { + if hasCustomSigVerifier(alg) { + return nil + } + return fmt.Errorf(`jws.WithKey: algorithm %q is not compatible with key type %T`, alg, key) + } + return nil +} + +// hasCustomSigVerifier reports whether a non-default Signer2 or +// Verifier2 has been registered for alg. When this is true, key-type +// validation must be skipped: the custom implementation decides what +// key types it accepts. +func hasCustomSigVerifier(alg jwa.SignatureAlgorithm) bool { + muSigner2DB.RLock() + s, sok := signer2DB[alg] + muSigner2DB.RUnlock() + if sok { + if _, isDefault := s.(defaultSigner); !isDefault { + return true + } + } + muVerifier2DB.RLock() + v, vok := verifier2DB[alg] + muVerifier2DB.RUnlock() + if vok { + if _, isDefault := v.(defaultVerifier); !isDefault { + return true + } + } + return false } // Settings allows you to set global settings for this JWS operations. @@ -594,6 +798,15 @@ func Settings(options ...GlobalOption) { for _, option := range options { switch option.Ident() { case identLegacySigners{}: + case identMaxSignatures{}: + var v int + if err := option.Value(&v); err != nil { + panic(fmt.Sprintf("jws.Settings: value for WithMaxSignatures must be an int: %s", err)) + } + if v <= 0 { + panic("jws.Settings: WithMaxSignatures must be greater than zero") + } + maxSignatures.Store(int64(v)) } } } @@ -619,18 +832,90 @@ func Settings(options ...GlobalOption) { // // Since this function avoids doing many checks that jws.Verify would perform, // you must ensure to perform the necessary checks including ensuring that algorithm is safe to use for your payload yourself. +// +// VerifyCompactFast cross-checks the protected header's "alg" against +// the caller-supplied alg: if the header omits "alg" (required by +// RFC 7515 §4.1.1) or advertises a different value, it returns a +// verification error. This prevents silently verifying a message +// under a different discipline than the one its header advertises. +// +// VerifyCompactFast refuses messages whose protected header carries a +// "crit" list. RFC 7515 §4.1.11 requires every critical extension to be +// understood by the recipient, and the fast path has no WithCritExtension +// allowlist to consult. On crit-present input it returns a sentinel error +// that callers can detect with errors.Is(err, jws.ErrCritPresent()) and +// retry through jws.Verify, which enforces the full validateCritical rule +// set. Applications that may legitimately receive "crit" headers should +// call jws.Verify directly. +// +// VerifyCompactFast assumes the JWS uses the default "b64":true +// (base64url-encoded) payload encoding. Any protected header carrying +// a "b64" entry is refused with jws.ErrB64Present(), regardless of +// whether "crit" also lists it: the fast path's signing-input +// reconstruction and post-verify base64 decode both depend on the +// default encoding, and a non-conformant b64=false producer (one that +// omits "b64" from "crit") would otherwise verify cryptographically +// while returning bytes that differ from the producer's intent. +// Detached-payload callers must use jws.Verify with jws.WithDetachedPayload +// regardless, since VerifyCompactFast has no way to accept a detached +// payload. func VerifyCompactFast(key any, compact []byte, alg jwa.SignatureAlgorithm) ([]byte, error) { + if err := validateAlgorithmForKey(alg, key); err != nil { + return nil, makeVerifyError(`%w`, err) + } + algstr := alg.String() - // Split the serialized JWT into its components + // Split the serialized JWS into its components hdr, payload, encodedSig, err := jwsbb.SplitCompact(compact) if err != nil { - return nil, fmt.Errorf("jwt.verifyFast: failed to split compact: %w", err) + return nil, makeVerifyError("failed to split compact: %w", err) + } + + parsedHdr := jwsbb.HeaderParseCompact(hdr) + + // Refuse crit-bearing messages: the fast path has no WithCritExtension + // allowlist, so accepting them would silently violate RFC 7515 §4.1.11. + // Callers that wrap VerifyCompactFast can detect this via + // errors.Is(err, jws.ErrCritPresent()) and fall through to jws.Verify. + // The sentinel is wrapped in verifyError so the same error also matches + // errors.Is(err, jws.VerifyError()) — fast-path refusals are a verify + // error, just one with a more specific classification available. + if jwsbb.HeaderHas(parsedHdr, CriticalKey) { + return nil, verifyError{errCritPresent} + } + + // Refuse "b64"-bearing messages, regardless of whether "crit" also + // lists it. The signing-input reconstruction and the post-verify + // base64 decode both assume the default b64=true encoding; a + // b64=false JWS that the fast path "verified" would either fail the + // post-verify base64 decode with a misleading error, or — worse — + // return base64-decoded garbage as the payload while the producer's + // raw bytes silently disagree. jws.Verify has the WithDetachedPayload + // / WithCritExtension machinery to handle b64=false correctly. As with + // the crit refusal above, the sentinel is wrapped in verifyError so the + // same error matches both jws.ErrB64Present() and jws.VerifyError(). + if jwsbb.HeaderHas(parsedHdr, "b64") { + return nil, verifyError{errB64Present} + } + + // Cross-check the protected header "alg" against the caller-supplied + // alg. RFC 7515 §4.1.1 makes "alg" mandatory in the protected header + // for compact serialization, and a mismatch between what the message + // advertises and the discipline under which we verify is the sort of + // silent divergence that downstream code (e.g. JWT consumers) should + // not be asked to re-discover on its own. + hdrAlg, err := jwsbb.HeaderGetString(parsedHdr, AlgorithmKey) + if err != nil { + return nil, verifyError{verificationError{fmt.Errorf(`jws.Verify: failed to extract %q from protected header: %w`, AlgorithmKey, err)}} + } + if hdrAlg != algstr { + return nil, verifyError{verificationError{fmt.Errorf(`jws.Verify: protected header %q %q does not match caller-supplied algorithm %q`, AlgorithmKey, hdrAlg, algstr)}} } signature, err := base64.Decode(encodedSig) if err != nil { - return nil, fmt.Errorf("jwt.verifyFast: failed to decode signature: %w", err) + return nil, makeVerifyError("failed to decode signature: %w", err) } // Instead of appending, copy the data from hdr/payload @@ -645,21 +930,21 @@ func VerifyCompactFast(key any, compact []byte, alg jwa.SignatureAlgorithm) ([]b // Verify the signature if verifier2, err := VerifierFor(alg); err == nil { if err := verifier2.Verify(key, verifyBuf, signature); err != nil { - return nil, verifyError{verificationError{fmt.Errorf("jwt.VerifyCompact: signature verification failed for %s: %w", algstr, err)}} + return nil, verifyError{verificationError{fmt.Errorf("signature verification failed for %s: %w", algstr, err)}} } } else { legacyVerifier, err := NewVerifier(alg) if err != nil { - return nil, verifyerr("jwt.VerifyCompact: failed to create verifier for %s: %w", algstr, err) + return nil, makeVerifyError("failed to create verifier for %s: %w", algstr, err) } if err := legacyVerifier.Verify(verifyBuf, signature, key); err != nil { - return nil, verifyError{verificationError{fmt.Errorf("jwt.VerifyCompact: signature verification failed for %s: %w", algstr, err)}} + return nil, verifyError{verificationError{fmt.Errorf("signature verification failed for %s: %w", algstr, err)}} } } decoded, err := base64.Decode(payload) if err != nil { - return nil, verifyerr("jwt.VerifyCompact: failed to decode payload: %w", err) + return nil, makeVerifyError("failed to decode payload: %w", err) } return decoded, nil } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/BUILD.bazel index 0799e81110..54e64265a0 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/BUILD.bazel +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/BUILD.bazel @@ -7,6 +7,7 @@ go_library( "ecdsa.go", "eddsa.go", "format.go", + "header.go", "hmac.go", "jwsbb.go", "rsa.go", @@ -18,12 +19,12 @@ go_library( deps = [ "//internal/base64", "//internal/ecutil", - "//internal/jwxio", "//internal/keyconv", "//internal/pool", "//internal/tokens", "//jws/internal/keytype", "@com_github_lestrrat_go_dsig//:dsig", + "@com_github_valyala_fastjson//:fastjson", ], ) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/format.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/format.go index 430bf625ac..945206c2c9 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/format.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/format.go @@ -6,7 +6,6 @@ import ( "io" "github.com/lestrrat-go/jwx/v3/internal/base64" - "github.com/lestrrat-go/jwx/v3/internal/jwxio" "github.com/lestrrat-go/jwx/v3/internal/tokens" ) @@ -39,6 +38,26 @@ func SignBuffer(buf, hdr, payload []byte, encoder base64.Encoder, encodePayload return buf } +// SigningPrefix returns base64url(hdr) + "." — the portion of the signing +// input that precedes the (possibly base64-encoded) payload. +// +// Parameters: +// - buf: Reusable scratch buffer (can be nil for automatic allocation); +// any prior contents are discarded, matching [SignBuffer]. This is not +// an append-style API. +// - hdr: Raw header bytes (will be base64-encoded) +// - encoder: Base64 encoder to use for encoding the header +func SigningPrefix(buf, hdr []byte, encoder base64.Encoder) []byte { + l := encoder.EncodedLen(len(hdr)) + 1 + if cap(buf) < l { + buf = make([]byte, 0, l) + } + buf = buf[:0] + buf = encoder.AppendEncode(buf, hdr) + buf = append(buf, tokens.Period) + return buf +} + // AppendSignature appends a base64-encoded signature to a JWS signing input buffer. // This completes the compact JWS serialization by adding the final signature component. // The input buffer should contain the signing input (header.payload), and this function @@ -150,9 +169,11 @@ func SplitCompactString(src string) (protected, payload, signature []byte, err e } // SplitCompactReader parses a compact JWS serialization from an io.Reader. -// This function handles both finite and streaming sources efficiently. -// For finite sources, it reads all data at once. For streaming sources, -// it uses a buffer-based approach to find segment boundaries. +// It reads the entire input from rdr and dispatches to [SplitCompact]. +// +// Bounding the input size is the caller's responsibility: wrap rdr with +// [io.LimitReader] or [net/http.MaxBytesReader] before passing it in. See +// docs/13-input-size.md for the rationale. // // Parameters: // - rdr: Reader containing the compact JWS data @@ -165,71 +186,9 @@ func SplitCompactString(src string) (protected, payload, signature []byte, err e // // The function validates that exactly 3 segments are present, separated by periods. func SplitCompactReader(rdr io.Reader) (protected, payload, signature []byte, err error) { - data, err := jwxio.ReadAllFromFiniteSource(rdr) - if err == nil { - return SplitCompact(data) - } - - if !errors.Is(err, jwxio.NonFiniteSourceError()) { + data, err := io.ReadAll(rdr) + if err != nil { return nil, nil, nil, err } - - var periods int - var state int - - buf := make([]byte, 4096) - var sofar []byte - - for { - // read next bytes - n, err := rdr.Read(buf) - // return on unexpected read error - if err != nil && err != io.EOF { - return nil, nil, nil, io.ErrUnexpectedEOF - } - - // append to current buffer - sofar = append(sofar, buf[:n]...) - // loop to capture multiple tokens.Period in current buffer - for loop := true; loop; { - var i = bytes.IndexByte(sofar, tokens.Period) - if i == -1 && err != io.EOF { - // no tokens.Period found -> exit and read next bytes (outer loop) - loop = false - continue - } else if i == -1 && err == io.EOF { - // no tokens.Period found -> process rest and exit - i = len(sofar) - loop = false - } else { - // tokens.Period found - periods++ - } - - // Reaching this point means we have found a tokens.Period or EOF and process the rest of the buffer - switch state { - case 0: - protected = sofar[:i] - state++ - case 1: - payload = sofar[:i] - state++ - case 2: - signature = sofar[:i] - } - // Shorten current buffer - if len(sofar) > i { - sofar = sofar[i+1:] - } - } - // Exit on EOF - if err == io.EOF { - break - } - } - if periods != 2 { - return nil, nil, nil, InvalidNumberOfSegmentsError() - } - - return protected, payload, signature, nil + return SplitCompact(data) } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/header.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/header.go index cac3987ea5..17f5b7b7de 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/header.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/header.go @@ -177,12 +177,16 @@ func HeaderGetInt64(h Header, key string) (int64, error) { return v.Int64() } -// HeaderGetStringBytes returns the byte slice value for the given key from the JWS header. -// An error is returned if the JSON was not valid, if the key does not exist, -// or if the value is not a byte slice. +// HeaderGetStringBytes returns the JSON string bytes for the given key +// from the JWS header, without copying. An error is returned if the JSON +// was not valid, if the key does not exist, or if the value is not a +// JSON string. // -// Because of limitations of the underlying library, you cannot use the return value -// of this function after the parser is garbage collected. +// WARNING: the returned slice aliases memory owned by h. It becomes +// invalid as soon as h is reused, re-parsed, or goes out of scope and is +// garbage collected. Do not retain the slice, share it across +// goroutines, or use it after any further call on h. If you need a value +// that outlives h, use [HeaderGetString], which returns a string copy. // // This function is experimental and may change or be removed in the future. func HeaderGetStringBytes(h Header, key string) ([]byte, error) { @@ -194,6 +198,41 @@ func HeaderGetStringBytes(h Header, key string) ([]byte, error) { return v.StringBytes() } +// HeaderHas returns true if the given key exists in the JWS header. +// +// This function is experimental and may change or be removed in the future. +func HeaderHas(h Header, key string) bool { + _, err := headerGet(h, key) + return err == nil +} + +// HeaderGetStringArray returns a string array for the given key from the JWS header. +// An error is returned if the JSON was not valid, if the key does not exist, +// or if the value is not a JSON array of strings. +// +// This function is experimental and may change or be removed in the future. +func HeaderGetStringArray(h Header, key string) ([]string, error) { + v, err := headerGet(h, key) + if err != nil { + return nil, err + } + + arr, err := v.Array() + if err != nil { + return nil, err + } + + result := make([]string, len(arr)) + for i, item := range arr { + sb, err := item.StringBytes() + if err != nil { + return nil, err + } + result[i] = string(sb) + } + return result, nil +} + // HeaderGetUint returns the uint value for the given key from the JWS header. // An error is returned if the JSON was not valid, if the key does not exist, // or if the value is not a uint. diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/jwsbb.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/jwsbb.go index 6a67ee8f86..0a36ef1877 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/jwsbb.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/jwsbb.go @@ -17,6 +17,10 @@ package jwsbb import ( + "crypto" + "crypto/ed25519" + "fmt" + "github.com/lestrrat-go/dsig" ) @@ -44,21 +48,10 @@ const ( // EdDSA algorithm edDSA = "EdDSA" -) -// Signer is a generic interface that defines the method for signing payloads. -// The type parameter K represents the key type (e.g., []byte for HMAC keys, -// *rsa.PrivateKey for RSA keys, *ecdsa.PrivateKey for ECDSA keys). -type Signer[K any] interface { - Sign(key K, payload []byte) ([]byte, error) -} - -// Verifier is a generic interface that defines the method for verifying signatures. -// The type parameter K represents the key type (e.g., []byte for HMAC keys, -// *rsa.PublicKey for RSA keys, *ecdsa.PublicKey for ECDSA keys). -type Verifier[K any] interface { - Verify(key K, buf []byte, signature []byte) error -} + // Fully-specified EdDSA algorithms (RFC 9864) + edDSAEd25519 = "Ed25519" +) // JWS to dsig algorithm mapping var jwsToDsigAlgorithm = map[string]string{ @@ -85,10 +78,31 @@ var jwsToDsigAlgorithm = map[string]string{ // EdDSA algorithm edDSA: dsig.EdDSA, + + // Fully-specified EdDSA algorithms (RFC 9864) + edDSAEd25519: dsig.EdDSA, } -// getDsigAlgorithm returns the dsig algorithm name for a JWS algorithm -func getDsigAlgorithm(jwsAlg string) (string, bool) { +// GetDsigAlgorithm returns the dsig algorithm name for a JWS algorithm. +func GetDsigAlgorithm(jwsAlg string) (string, bool) { dsigAlg, ok := jwsToDsigAlgorithm[jwsAlg] return dsigAlg, ok } + +// validateEdDSACurve enforces that fully-specified EdDSA algorithms (RFC 9864) +// are only used with the correct key curve. The polymorphic "EdDSA" algorithm +// accepts any EdDSA key without curve checks. The pub argument must be the +// already-extracted public key (after jwk.Key unwrapping / keyconv). +func validateEdDSACurve(jwsAlg string, pub crypto.PublicKey) error { + switch jwsAlg { + case edDSAEd25519: + if _, ok := pub.(ed25519.PublicKey); !ok { + return fmt.Errorf(`algorithm %q requires an Ed25519 key, got %T`, jwsAlg, pub) + } + case edDSA: + // Polymorphic EdDSA: no curve restriction + default: + return fmt.Errorf(`unsupported fully-specified EdDSA algorithm %q`, jwsAlg) + } + return nil +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/sign.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/sign.go index 6f36ab0554..8c0c185c54 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/sign.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/sign.go @@ -21,9 +21,10 @@ import ( // Not all algorithms require this parameter, but it is included for consistency. // 99% of the time, you can pass nil for rr, and it will work fine. func Sign(key any, alg string, payload []byte, rr io.Reader) ([]byte, error) { - dsigAlg, ok := getDsigAlgorithm(alg) + dsigAlg, ok := GetDsigAlgorithm(alg) if !ok { - return nil, fmt.Errorf(`jwsbb.Sign: unsupported signature algorithm %q`, alg) + // For custom algorithms registered with dsig, JWS name = dsig name + dsigAlg = alg } // Get dsig algorithm info to determine key conversion strategy @@ -40,7 +41,9 @@ func Sign(key any, alg string, payload []byte, rr io.Reader) ([]byte, error) { case dsig.ECDSA: return dispatchECDSASign(key, dsigAlg, payload, rr) case dsig.EdDSAFamily: - return dispatchEdDSASign(key, dsigAlg, payload, rr) + return dispatchEdDSASign(key, alg, dsigAlg, payload, rr) + case dsig.Custom: + return dsig.Sign(key, dsigAlg, payload, rr) default: return nil, fmt.Errorf(`jwsbb.Sign: unsupported dsig algorithm family %q`, dsigInfo.Family) } @@ -91,11 +94,17 @@ func dispatchECDSASign(key any, dsigAlg string, payload []byte, rr io.Reader) ([ return dsig.Sign(privkey, dsigAlg, payload, rr) } -func dispatchEdDSASign(key any, dsigAlg string, payload []byte, rr io.Reader) ([]byte, error) { +func dispatchEdDSASign(key any, jwsAlg, dsigAlg string, payload []byte, rr io.Reader) ([]byte, error) { + // Note: Extension algorithms (e.g. Ed448) are registered as dsig.Custom family, + // so they take the dsig.Custom branch in Sign() and never reach this function. + // Try crypto.Signer first (dsig can handle it directly) if signer, ok := key.(crypto.Signer); ok { // Verify it's an EdDSA key - if _, ok := signer.Public().(ed25519.PublicKey); ok { + if pub, ok := signer.Public().(ed25519.PublicKey); ok { + if err := validateEdDSACurve(jwsAlg, pub); err != nil { + return nil, fmt.Errorf(`jwsbb.Sign: %w`, err) + } return dsig.Sign(signer, dsigAlg, payload, rr) } } @@ -106,5 +115,9 @@ func dispatchEdDSASign(key any, dsigAlg string, payload []byte, rr io.Reader) ([ return nil, fmt.Errorf(`jwsbb.Sign: invalid key type %T. ed25519.PrivateKey is required: %w`, key, err) } + if err := validateEdDSACurve(jwsAlg, privkey.Public()); err != nil { + return nil, fmt.Errorf(`jwsbb.Sign: %w`, err) + } + return dsig.Sign(privkey, dsigAlg, payload, rr) } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/verify.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/verify.go index bac3ff487e..f3bfcc6b7f 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/verify.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/verify.go @@ -16,9 +16,10 @@ import ( // This function loads the verifier registered in the jwsbb package _ONLY_. // It does not support custom verifiers that the user might have registered. func Verify(key any, alg string, payload, signature []byte) error { - dsigAlg, ok := getDsigAlgorithm(alg) + dsigAlg, ok := GetDsigAlgorithm(alg) if !ok { - return fmt.Errorf(`jwsbb.Verify: unsupported signature algorithm %q`, alg) + // For custom algorithms registered with dsig, JWS name = dsig name + dsigAlg = alg } // Get dsig algorithm info to determine key conversion strategy @@ -35,7 +36,9 @@ func Verify(key any, alg string, payload, signature []byte) error { case dsig.ECDSA: return dispatchECDSAVerify(key, dsigAlg, payload, signature) case dsig.EdDSAFamily: - return dispatchEdDSAVerify(key, dsigAlg, payload, signature) + return dispatchEdDSAVerify(key, alg, dsigAlg, payload, signature) + case dsig.Custom: + return dsig.Verify(key, dsigAlg, payload, signature) default: return fmt.Errorf(`jwsbb.Verify: unsupported dsig algorithm family %q`, dsigInfo.Family) } @@ -86,11 +89,17 @@ func dispatchECDSAVerify(key any, dsigAlg string, payload, signature []byte) err return dsig.Verify(pubkey, dsigAlg, payload, signature) } -func dispatchEdDSAVerify(key any, dsigAlg string, payload, signature []byte) error { +func dispatchEdDSAVerify(key any, jwsAlg, dsigAlg string, payload, signature []byte) error { + // Note: Extension algorithms (e.g. Ed448) are registered as dsig.Custom family, + // so they take the dsig.Custom branch in Verify() and never reach this function. + // Try crypto.Signer first (dsig can handle it directly) if signer, ok := key.(crypto.Signer); ok { // Verify it's an EdDSA key - if _, ok := signer.Public().(ed25519.PublicKey); ok { + if pub, ok := signer.Public().(ed25519.PublicKey); ok { + if err := validateEdDSACurve(jwsAlg, pub); err != nil { + return fmt.Errorf(`jwsbb.Verify: %w`, err) + } return dsig.Verify(signer, dsigAlg, payload, signature) } } @@ -101,5 +110,9 @@ func dispatchEdDSAVerify(key any, dsigAlg string, payload, signature []byte) err return fmt.Errorf(`jwsbb.Verify: invalid key type %T. ed25519.PublicKey is required: %w`, key, err) } + if err := validateEdDSACurve(jwsAlg, pubkey); err != nil { + return fmt.Errorf(`jwsbb.Verify: %w`, err) + } + return dsig.Verify(pubkey, dsigAlg, payload, signature) } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/key_provider.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/key_provider.go index 84529a1a87..49afd0e19f 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/key_provider.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/key_provider.go @@ -2,8 +2,10 @@ package jws import ( "context" + "errors" "fmt" "net/url" + "slices" "sync" "github.com/lestrrat-go/jwx/v3/jwa" @@ -76,7 +78,7 @@ type KeySink interface { } type algKeyPair struct { - alg jwa.KeyAlgorithm + alg jwa.SignatureAlgorithm key any } @@ -109,116 +111,184 @@ type keySetProvider struct { multipleKeysPerKeyID bool // true if we should attempt to match multiple keys per key ID. if false we assume that only one key exists for a given key ID } -func (kp *keySetProvider) selectKey(sink KeySink, key jwk.Key, sig *Signature, _ *Message) error { +// selectKey examines a single key and, if it is suitable for the given +// signature, adds one or more (algorithm, key) pairs to the sink. +// It returns true if at least one pair was added, false if the key was +// filtered out (e.g. wrong usage, no matching algorithm). +func (kp *keySetProvider) selectKey(sink KeySink, key jwk.Key, sig *Signature, _ *Message) (bool, error) { if usage, ok := key.KeyUsage(); ok { // it's okay if use: "". we'll assume it's "sig" if usage != "" && usage != jwk.ForSignature.String() { - return nil + kid, _ := key.KeyID() + return false, fmt.Errorf(`key with kid %q is marked use=%q, not usable for signature verification (expected %q)`, kid, usage, jwk.ForSignature.String()) } } if v, ok := key.Algorithm(); ok { salg, ok := jwa.LookupSignatureAlgorithm(v.String()) if !ok { - return fmt.Errorf(`invalid signature algorithm %q`, v) + return false, fmt.Errorf(`invalid signature algorithm %q`, v) } sink.Key(salg, key) - return nil + return true, nil } - if kp.inferAlgorithm { - algs, err := AlgorithmsForKey(key) - if err != nil { - return fmt.Errorf(`failed to get a list of signature methods for key type %s: %w`, key.KeyType(), err) - } + if !kp.inferAlgorithm { + return false, nil + } - // bail out if the JWT has a `alg` field, and it doesn't match - if tokAlg, ok := sig.ProtectedHeaders().Algorithm(); ok { - for _, alg := range algs { - if tokAlg == alg { - sink.Key(alg, key) - return nil - } - } - return fmt.Errorf(`algorithm in the message does not match any of the inferred algorithms`) - } + algs, err := AlgorithmsForKey(key) + if err != nil { + return false, fmt.Errorf(`failed to get a list of signature methods for key type %s: %w`, key.KeyType(), err) + } - // Yes, you get to try them all!!!!!!! + // bail out if the JWT has a `alg` field, and it doesn't match + if tokAlg, ok := sig.ProtectedHeaders().Algorithm(); ok { for _, alg := range algs { - sink.Key(alg, key) + if tokAlg == alg { + sink.Key(alg, key) + return true, nil + } } - return nil + return false, fmt.Errorf(`algorithm in the message does not match any of the inferred algorithms`) } - return nil + + // Yes, you get to try them all!!!!!!! + for _, alg := range algs { + sink.Key(alg, key) + } + return len(algs) > 0, nil } func (kp *keySetProvider) FetchKeys(_ context.Context, sink KeySink, sig *Signature, msg *Message) error { if kp.requireKid { wantedKid, ok := sig.ProtectedHeaders().KeyID() if !ok { - // If the kid is NOT specified... kp.useDefault needs to be true, and the - // JWKs must have exactly one key in it - if !kp.useDefault { - return fmt.Errorf(`failed to find matching key: no key ID ("kid") specified in token`) - } else if kp.useDefault && kp.set.Len() > 1 { - return fmt.Errorf(`failed to find matching key: no key ID ("kid") specified in token but multiple keys available in key set`) - } - - // if we got here, then useDefault == true AND there is exactly - // one key in the set. - key, ok := kp.set.Key(0) - if !ok { - return fmt.Errorf(`failed to get key at index 0 (empty JWKS?)`) - } - return kp.selectKey(sink, key, sig, msg) + return kp.fetchDefaultKey(sink, sig, msg) } + return kp.fetchKeysByKid(sink, sig, msg, wantedKid) + } + return kp.fetchAllKeys(sink, sig, msg) +} - // Otherwise we better be able to look up the key. - // <= v2.0.3 backwards compatible case: only match a single key - // whose key ID matches `wantedKid` - if !kp.multipleKeysPerKeyID { - key, ok := kp.set.LookupKeyID(wantedKid) - if !ok { - return fmt.Errorf(`failed to find key with key ID %q in key set`, wantedKid) - } - return kp.selectKey(sink, key, sig, msg) - } +// fetchDefaultKey handles the case where kid is required but the token +// has no kid field. It uses the sole key in the set when useDefault is true. +func (kp *keySetProvider) fetchDefaultKey(sink KeySink, sig *Signature, msg *Message) error { + if !kp.useDefault { + return fmt.Errorf(`failed to find matching key: no key ID ("kid") specified in token`) + } + if kp.set.Len() > 1 { + return fmt.Errorf(`failed to find matching key: no key ID ("kid") specified in token but multiple keys available in key set`) + } - // if multipleKeysPerKeyID is true, we attempt all keys whose key ID matches - // the wantedKey - ok = false - for i := range kp.set.Len() { - key, _ := kp.set.Key(i) - if kid, ok := key.KeyID(); !ok || kid != wantedKid { - continue - } + key, ok := kp.set.Key(0) + if !ok { + return fmt.Errorf(`failed to get key at index 0 (empty JWKS?)`) + } + _, err := kp.selectKey(sink, key, sig, msg) + return err +} - if err := kp.selectKey(sink, key, sig, msg); err != nil { - continue - } - ok = true - // continue processing so that we try all keys with the same key ID - } +// fetchKeysByKid looks up keys by their key ID and adds matching ones to the sink. +func (kp *keySetProvider) fetchKeysByKid(sink KeySink, sig *Signature, msg *Message, wantedKid string) error { + // <= v2.0.3 backwards compatible case: only match a single key + // whose key ID matches `wantedKid` + if !kp.multipleKeysPerKeyID { + key, ok := kp.set.LookupKeyID(wantedKid) if !ok { return fmt.Errorf(`failed to find key with key ID %q in key set`, wantedKid) } - return nil + _, err := kp.selectKey(sink, key, sig, msg) + return err + } + + // multipleKeysPerKeyID: attempt all keys whose key ID matches + found := false + var errs []error + for i := range kp.set.Len() { + key, _ := kp.set.Key(i) + if kid, ok := key.KeyID(); !ok || kid != wantedKid { + continue + } + + added, err := kp.selectKey(sink, key, sig, msg) + if err != nil { + errs = append(errs, fmt.Errorf(`key #%d: %w`, i, err)) + continue + } + if added { + found = true + } + } + if !found { + if len(errs) > 0 { + return fmt.Errorf(`failed to select any key with key ID %q: %w`, wantedKid, errors.Join(errs...)) + } + return fmt.Errorf(`failed to find key with key ID %q in key set`, wantedKid) } + return nil +} - // Otherwise just try all keys +// fetchAllKeys iterates all keys in the set and adds suitable ones to the sink. +// +// When the protected header advertises an `alg`, keys whose type cannot +// produce that algorithm are skipped before reaching selectKey. This +// bounds verification fan-out to N_keys_of_matching_type instead of +// N_keys when `WithRequireKid(false)` is used against a heterogeneous +// JWKS. The skip is semantics-preserving: validateAlgorithmForKey in +// verify_context would reject the incompatible (alg, key) pair before +// running any verifier anyway. +// +// The allowed-KeyType set is looked up once per FetchKeys call via the +// precomputed algorithmToKeyTypes inverse map, so the per-key check is +// a cheap KeyType equality over a tiny slice (typically 1 element). +// When allowedKtys is nil (no header alg, or alg has no registered +// key type), the filter is skipped. +func (kp *keySetProvider) fetchAllKeys(sink KeySink, sig *Signature, msg *Message) error { + var allowedKtys []jwa.KeyType + if hdrAlg, ok := sig.ProtectedHeaders().Algorithm(); ok { + allowedKtys = keyTypesForAlgorithm(hdrAlg) + } + found := false + var errs []error for i := range kp.set.Len() { key, ok := kp.set.Key(i) if !ok { return fmt.Errorf(`failed to get key at index %d`, i) } - if err := kp.selectKey(sink, key, sig, msg); err != nil { + if allowedKtys != nil && !slices.Contains(allowedKtys, key.KeyType()) { continue } + added, err := kp.selectKey(sink, key, sig, msg) + if err != nil { + errs = append(errs, fmt.Errorf(`key #%d: %w`, i, err)) + continue + } + if added { + found = true + } + } + if !found && len(errs) > 0 { + return fmt.Errorf(`no key in the key set was usable: %w`, errors.Join(errs...)) } return nil } +// keyTypesForAlgorithm returns the registered key types that can +// produce the given signature algorithm. The inverse map is maintained +// at registration time so this is an O(1) lookup. Returns nil if no +// key type is registered for alg, which signals callers to skip the +// prefilter. +func keyTypesForAlgorithm(alg jwa.SignatureAlgorithm) []jwa.KeyType { + muAlgorithmMaps.RLock() + defer muAlgorithmMaps.RUnlock() + // Copy so the caller can safely iterate without holding the + // lock; RegisterAlgorithmForKeyType may append concurrently + // after we return. Typical length is 1. + return slices.Clone(algorithmToKeyTypes[alg]) +} + type jkuProvider struct { fetcher jwk.Fetcher options []jwk.FetchOption @@ -256,8 +326,13 @@ func (kp jkuProvider) FetchKeys(ctx context.Context, sink KeySink, sig *Signatur key, ok := set.LookupKeyID(kid) if !ok { - // It is not an error if the key with the kid doesn't exist - return nil + return fmt.Errorf(`jku: key with "kid" %q not found in JWKS fetched from %q`, kid, u) + } + + if usage, ok := key.KeyUsage(); ok { + if usage != "" && usage != jwk.ForSignature.String() { + return fmt.Errorf(`key with kid %q is marked use=%q, not usable for signature verification (expected %q)`, kid, usage, jwk.ForSignature.String()) + } } algs, err := AlgorithmsForKey(key) @@ -266,19 +341,26 @@ func (kp jkuProvider) FetchKeys(ctx context.Context, sink KeySink, sig *Signatur } hdrAlg, ok := sig.ProtectedHeaders().Algorithm() - if ok { - for _, alg := range algs { - // if we have an "alg" field in the JWS, we can only proceed if - // the inferred algorithm matches - if hdrAlg != alg { - continue - } + if !ok { + // The jku provider routes a key by matching both "kid" and + // "alg" against the JWS protected header. With no alg in the + // header there's nothing to pin the signature algorithm to, + // so reject explicitly rather than returning no keys and + // letting the outer verify loop surface a generic "could not + // be verified with any of the keys" message. + return fmt.Errorf(`use of "jku" requires that the protected header contain an "alg" field`) + } - sink.Key(alg, key) - break + for _, alg := range algs { + if hdrAlg != alg { + continue } + + sink.Key(alg, key) + return nil } - return nil + + return fmt.Errorf(`algorithm %q in JWS header does not match any algorithm for key type %s from jku`, hdrAlg, key.KeyType()) } // KeyProviderFunc is a type of KeyProvider that is implemented by diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy.go index 767ad723a3..eeec25c8a5 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy.go @@ -61,15 +61,20 @@ func enableLegacySigners() { } } - if err := RegisterSigner(jwa.EdDSA(), SignerFactoryFn(func() (Signer, error) { - return legacy.NewEdDSASigner(), nil - })); err != nil { - panic(fmt.Sprintf("RegisterSigner failed: %v", err)) - } - if err := RegisterVerifier(jwa.EdDSA(), VerifierFactoryFn(func() (Verifier, error) { - return legacy.NewEdDSAVerifier(), nil - })); err != nil { - panic(fmt.Sprintf("RegisterVerifier failed: %v", err)) + // Ed448 is intentionally excluded: the legacy EdDSA signer/verifier + // only supports ed25519. Ed448 is handled via the defaultSigner/defaultVerifier + // path which routes to jwsbb (requires github.com/lestrrat-go/jwx-circl-ed448). + for _, alg := range []jwa.SignatureAlgorithm{jwa.EdDSA(), jwa.EdDSAEd25519()} { + if err := RegisterSigner(alg, SignerFactoryFn(func() (Signer, error) { + return legacy.NewEdDSASigner(), nil + })); err != nil { + panic(fmt.Sprintf("RegisterSigner failed: %v", err)) + } + if err := RegisterVerifier(alg, VerifierFactoryFn(func() (Verifier, error) { + return legacy.NewEdDSAVerifier(), nil + })); err != nil { + panic(fmt.Sprintf("RegisterVerifier failed: %v", err)) + } } } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy/ecdsa.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy/ecdsa.go index 0b714a44b8..36c816d79f 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy/ecdsa.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy/ecdsa.go @@ -10,7 +10,6 @@ import ( "github.com/lestrrat-go/jwx/v3/internal/ecutil" "github.com/lestrrat-go/jwx/v3/internal/keyconv" - "github.com/lestrrat-go/jwx/v3/internal/pool" "github.com/lestrrat-go/jwx/v3/jwa" "github.com/lestrrat-go/jwx/v3/jws/internal/keytype" ) @@ -179,18 +178,13 @@ func (v *ecdsaVerifier) Verify(payload []byte, signature []byte, key any) error return fmt.Errorf(`public key used does not contain a point (X,Y) on the curve`) } - r := pool.BigInt().Get() - s := pool.BigInt().Get() - defer pool.BigInt().Put(r) - defer pool.BigInt().Put(s) - keySize := ecutil.CalculateKeySize(pubkey.Curve) if len(signature) != keySize*2 { return fmt.Errorf(`invalid signature length for curve %q`, pubkey.Curve.Params().Name) } - r.SetBytes(signature[:keySize]) - s.SetBytes(signature[keySize:]) + r := new(big.Int).SetBytes(signature[:keySize]) + s := new(big.Int).SetBytes(signature[keySize:]) h := v.hash.New() if _, err := h.Write(payload); err != nil { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/message.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/message.go index e113d1438c..02e4590620 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/message.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/message.go @@ -65,14 +65,18 @@ func (s *Signature) UnmarshalJSON(data []byte) error { s.headers = sup.Header if buf := sup.Protected; buf != nil { - src := []byte(*buf) - if !bytes.HasPrefix(src, []byte{tokens.OpenCurlyBracket}) { - decoded, err := base64.Decode(src) - if err != nil { - return fmt.Errorf(`failed to base64 decode protected headers: %w`, err) - } - src = decoded + // RFC 7515 §3 mandates that "protected" be base64url-encoded. + // Earlier code carried a relaxed probe that accepted a literal- + // JSON form (a JSON string whose content begins with "{") and + // skipped base64 decoding — that was asymmetric with the + // flattened branch (which only base64-decodes) and gave callers + // a non-conforming wire form useful for evading byte-exact JWS + // dedup / replay caches. + decoded, err := base64.Decode([]byte(*buf)) + if err != nil { + return fmt.Errorf(`failed to base64 decode protected headers: %w`, err) } + src := decoded prt := NewHeaders() //nolint:forcetypeassert @@ -96,16 +100,14 @@ func (s *Signature) UnmarshalJSON(data []byte) error { } // Sign populates the signature field, with a signature generated by -// given the signer object and payload. +// the given signer object and payload. // // The first return value is the raw signature in binary format. -// The second return value s the full three-segment signature +// The second return value is the full three-segment signature // (e.g. "eyXXXX.XXXXX.XXXX") // -// This method is deprecated, and will be remove in a future release. -// Signature objects in the future will only be used as containers, -// and signing will be done using the `jws.Sign` function, or alternatively -// you could use jwsbb package to craft the signature manually. +// Deprecated: Signature objects will only be used as containers in the future. +// Use [Sign] or the jwsbb package to craft signatures manually. func (s *Signature) Sign(payload []byte, signer Signer, key any) ([]byte, []byte, error) { return s.sign2(payload, signer, key) } @@ -180,7 +182,7 @@ func (s *Signature) sign2(payload []byte, signer interface{ Algorithm() jwa.Sign } else { if !s.detached { if bytes.Contains(payload, []byte{tokens.Period}) { - return nil, nil, fmt.Errorf(`payload must not contain a "."`) + return nil, nil, fmt.Errorf(`compact serialization with b64=false requires payload to contain no "." characters per RFC 7797 §5.2; use jws.WithDetachedPayload to keep the payload out of the wire format`) } } plen = len(payload) @@ -194,8 +196,7 @@ func (s *Signature) sign2(payload []byte, signer interface{ Algorithm() jwa.Sign buf.WriteByte(tokens.Period) buf.WriteString(encoder.EncodeToString(s.signature)) - ret := make([]byte, buf.Len()) - copy(ret, buf.Bytes()) + ret := bytes.Clone(buf.Bytes()) return s.signature, ret, nil } @@ -278,7 +279,7 @@ func (m Message) LookupSignature(kid string) []*Signature { type messageUnmarshalProbe struct { Payload *string `json:"payload"` Signatures []json.RawMessage `json:"signatures,omitempty"` - Header Headers `json:"header,omitempty"` + Header json.RawMessage `json:"header,omitempty"` Protected *string `json:"protected,omitempty"` Signature *string `json:"signature,omitempty"` } @@ -287,19 +288,42 @@ func (m *Message) UnmarshalJSON(buf []byte) error { m.payload = nil m.signatures = nil m.b64 = true + m.detached = false var mup messageUnmarshalProbe - mup.Header = NewHeaders() if err := json.Unmarshal(buf, &mup); err != nil { return fmt.Errorf(`failed to unmarshal into temporary structure: %w`, err) } + // Enforce the signature cap before we decode any signature entry. + // The probe above leaves mup.Signatures as []json.RawMessage, so no + // headers/base64 work has happened yet. Doing the check here prevents + // a large signatures array from allocating O(input) work before Parse + // gets a chance to reject it. + maxSigs := m.maxSignatures + if maxSigs == 0 { + maxSigs = int(maxSignatures.Load()) + } + if maxSigs > 0 && len(mup.Signatures) > maxSigs { + return fmt.Errorf(`too many signatures in JWS message (%d > %d)`, len(mup.Signatures), maxSigs) + } + b64 := true if mup.Signature == nil { // flattened signature is NOT present if len(mup.Signatures) == 0 { return fmt.Errorf(`required field "signatures" not present`) } + // RFC 7515 §7.2.1 places the unprotected JOSE header inside each + // signature entry for the general (multi-signature) form; a + // top-level "header" sibling of "signatures" is not defined. + // Reject rather than silently drop — silent drop hid both typos + // and an attacker-controlled trigger surface for any + // RegisterCustomDecoder side effects on the dropped contents. + if len(mup.Header) > 0 && !bytes.Equal(mup.Header, []byte("null")) { + return fmt.Errorf(`general-form JWS must not contain top-level "header" sibling of "signatures" (RFC 7515 §7.2.1 places the unprotected header inside each signature entry)`) + } + m.signatures = make([]*Signature, 0, len(mup.Signatures)) for i, rawsig := range mup.Signatures { var sig Signature @@ -319,8 +343,8 @@ func (m *Message) UnmarshalJSON(buf []byte) error { b64 = false } } else { - if b64 != getB64Value(sig.protected) { - return fmt.Errorf(`b64 value must be the same for all signatures`) + if got := getB64Value(sig.protected); b64 != got { + return fmt.Errorf(`b64 value must be the same for all signatures; signature #%d declared b64=%t but earlier signature(s) declared b64=%t`, i+1, got, b64) } } @@ -332,7 +356,13 @@ func (m *Message) UnmarshalJSON(buf []byte) error { } var sig Signature - sig.headers = mup.Header + if len(mup.Header) > 0 && !bytes.Equal(mup.Header, []byte("null")) { + hdrs := NewHeaders() + if err := json.Unmarshal(mup.Header, hdrs); err != nil { + return fmt.Errorf(`failed to unmarshal flattened unprotected header: %w`, err) + } + sig.headers = hdrs + } if src := mup.Protected; src != nil { decoded, err := base64.DecodeString(*src) if err != nil { @@ -364,7 +394,11 @@ func (m *Message) UnmarshalJSON(buf []byte) error { b64 = getB64Value(sig.protected) } - if mup.Payload != nil { + if mup.Payload == nil { + // RFC 7515 Appendix F: detached content is signaled in the + // JSON Serialization by omitting the "payload" member. + m.detached = true + } else { if !b64 { // NOT base64 encoded m.payload = []byte(*mup.Payload) } else { @@ -405,30 +439,54 @@ func (m Message) marshalFlattened() ([]byte, error) { wrote = true } - if wrote { - buf.WriteRune(tokens.Comma) + // RFC 7515 Appendix F: detached content is signaled in the JSON + // Serialization by omitting the "payload" member. + if !m.detached { + if wrote { + buf.WriteRune(tokens.Comma) + } + if !getB64Value(sig.protected) { + // RFC 7797 b64=false: emit the raw payload as a JSON string + // rather than re-base64-encoding it. json.Marshal handles + // any necessary escaping for byte sequences that aren't + // JSON-safe as-is. + payloadbuf, err := json.Marshal(string(m.payload)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal "payload" (flattened, b64=false): %w`, err) + } + buf.WriteString(`"payload":`) + buf.Write(payloadbuf) + } else { + buf.WriteString(`"payload":"`) + buf.Write(base64.Encode(m.payload)) + buf.WriteRune('"') + } + wrote = true } - buf.WriteString(`"payload":"`) - buf.WriteString(base64.EncodeToString(m.payload)) - buf.WriteRune('"') if protected := sig.protected; protected != nil { protectedbuf, err := json.Marshal(protected) if err != nil { return nil, fmt.Errorf(`failed to marshal "protected" (flattened format): %w`, err) } - buf.WriteString(`,"protected":"`) - buf.WriteString(base64.EncodeToString(protectedbuf)) + if wrote { + buf.WriteRune(tokens.Comma) + } + buf.WriteString(`"protected":"`) + buf.Write(base64.Encode(protectedbuf)) buf.WriteRune('"') + wrote = true } - buf.WriteString(`,"signature":"`) - buf.WriteString(base64.EncodeToString(sig.signature)) + if wrote { + buf.WriteRune(tokens.Comma) + } + buf.WriteString(`"signature":"`) + buf.Write(base64.Encode(sig.signature)) buf.WriteRune('"') buf.WriteRune(tokens.CloseCurlyBracket) - ret := make([]byte, buf.Len()) - copy(ret, buf.Bytes()) + ret := bytes.Clone(buf.Bytes()) return ret, nil } @@ -436,9 +494,34 @@ func (m Message) marshalFull() ([]byte, error) { buf := pool.BytesBuffer().Get() defer pool.BytesBuffer().Put(buf) - buf.WriteString(`{"payload":"`) - buf.WriteString(base64.EncodeToString(m.payload)) - buf.WriteString(`","signatures":[`) + // RFC 7515 Appendix F: detached content is signaled in the JSON + // Serialization by omitting the "payload" member. + if m.detached { + buf.WriteString(`{"signatures":[`) + } else { + // RFC 7797 b64=false: emit the raw payload as a JSON string + // rather than re-base64-encoding it. The general JWS form has + // one shared payload across signatures; per RFC 7797, all + // signers must agree on the b64 flag, so we consult the first + // signature's protected header. + var b64 = true + if len(m.signatures) > 0 { + b64 = getB64Value(m.signatures[0].protected) + } + if !b64 { + payloadbuf, err := json.Marshal(string(m.payload)) + if err != nil { + return nil, fmt.Errorf(`failed to marshal "payload" (full, b64=false): %w`, err) + } + buf.WriteString(`{"payload":`) + buf.Write(payloadbuf) + buf.WriteString(`,"signatures":[`) + } else { + buf.WriteString(`{"payload":"`) + buf.Write(base64.Encode(m.payload)) + buf.WriteString(`","signatures":[`) + } + } for i, sig := range m.signatures { if i > 0 { buf.WriteRune(tokens.Comma) @@ -465,7 +548,7 @@ func (m Message) marshalFull() ([]byte, error) { buf.WriteRune(tokens.Comma) } buf.WriteString(`"protected":"`) - buf.WriteString(base64.EncodeToString(protectedbuf)) + buf.Write(base64.Encode(protectedbuf)) buf.WriteRune('"') wrote = true } @@ -476,15 +559,14 @@ func (m Message) marshalFull() ([]byte, error) { buf.WriteRune(tokens.Comma) } buf.WriteString(`"signature":"`) - buf.WriteString(base64.EncodeToString(sig.signature)) + buf.Write(base64.Encode(sig.signature)) buf.WriteString(`"`) } buf.WriteString(`}`) } buf.WriteString(`]}`) - ret := make([]byte, buf.Len()) - copy(ret, buf.Bytes()) + ret := bytes.Clone(buf.Bytes()) return ret, nil } @@ -497,7 +579,7 @@ func (m Message) marshalFull() ([]byte, error) { // must be passed to the function. func Compact(msg *Message, options ...CompactOption) ([]byte, error) { if l := len(msg.signatures); l != 1 { - return nil, fmt.Errorf(`jws.Compact: cannot serialize message with %d signatures (must be one)`, l) + return nil, makeSignError(prefixJwsCompact, `cannot serialize message with %d signatures (must be one)`, l) } var detached bool @@ -506,11 +588,11 @@ func Compact(msg *Message, options ...CompactOption) ([]byte, error) { switch option.Ident() { case identDetached{}: if err := option.Value(&detached); err != nil { - return nil, fmt.Errorf(`jws.Compact: failed to retrieve detached option value: %w`, err) + return nil, makeSignError(prefixJwsCompact, `failed to retrieve detached option value: %w`, err) } case identBase64Encoder{}: if err := option.Value(&encoder); err != nil { - return nil, fmt.Errorf(`jws.Compact: failed to retrieve base64 encoder option value: %w`, err) + return nil, makeSignError(prefixJwsCompact, `failed to retrieve base64 encoder option value: %w`, err) } } } @@ -521,7 +603,7 @@ func Compact(msg *Message, options ...CompactOption) ([]byte, error) { hdrbuf, err := json.Marshal(hdrs) if err != nil { - return nil, fmt.Errorf(`jws.Compress: failed to marshal headers: %w`, err) + return nil, makeSignError(prefixJwsCompact, `failed to marshal headers: %w`, err) } buf := pool.BytesBuffer().Get() @@ -536,7 +618,7 @@ func Compact(msg *Message, options ...CompactOption) ([]byte, error) { buf.WriteString(encoded) } else { if bytes.Contains(msg.payload, []byte{tokens.Period}) { - return nil, fmt.Errorf(`jws.Compress: payload must not contain a "."`) + return nil, makeSignError(prefixJwsCompact, `compact serialization with b64=false requires payload to contain no "." characters per RFC 7797 §5.2; use jws.WithDetachedPayload to keep the payload out of the wire format`) } buf.Write(msg.payload) } @@ -544,7 +626,6 @@ func Compact(msg *Message, options ...CompactOption) ([]byte, error) { buf.WriteByte(tokens.Period) buf.WriteString(encoder.EncodeToString(s.signature)) - ret := make([]byte, buf.Len()) - copy(ret, buf.Bytes()) + ret := bytes.Clone(buf.Bytes()) return ret, nil } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/options.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/options.go index 4c217c3483..cc98abc5be 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/options.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/options.go @@ -7,6 +7,42 @@ import ( ) type identInsecureNoSignature struct{} +type identCritExtension struct{} + +// WithCritExtension declares that the caller understands and will process +// the named "crit" (Critical) header parameter extension(s) per RFC 7515 +// Section 4.1.11. The option is variadic and accumulating: a single call +// may register any number of extension names, and the option may be +// passed multiple times to add more. +// +// This option only takes effect when jws.WithCritValidation(true) is +// also passed. With validation enabled, jws.Verify() rejects any JWS +// whose protected header lists a "crit" extension that has not been +// declared via this option, satisfying the RFC's requirement that +// recipients MUST reject any "crit" extension they do not understand. +// +// IMPORTANT: declaring an extension here is a promise to the library +// that the caller knows what the extension means and will perform any +// validation, side effect, or policy enforcement the extension requires +// AFTER jws.Verify() returns successfully. The library cannot inspect +// or enforce the semantics of an extension; it only checks that every +// "crit" entry in the message has been declared. If you register an +// extension and then forget to act on its value, you have effectively +// disabled the protection the producer was trying to obtain by listing +// the extension as critical. +// +// Concretely, the post-verify code path for a declared extension must: +// +// 1. Read the value of the named header from the verified message. +// 2. Apply whatever check or transformation the extension specifies +// (e.g. for an "x-tenant-binding" extension, refuse to act on the +// payload unless the binding matches the current tenant). +// 3. Treat any failure of that check as a verification failure for +// the application's purposes, even though jws.Verify() returned +// no error. +func WithCritExtension(names ...string) VerifyOption { + return &verifyOption{option.New(identCritExtension{}, names)} +} // WithJSON specifies that the result of `jws.Sign()` is serialized in // JSON format. @@ -38,7 +74,10 @@ type withKey struct { public Headers } -// Protected exists as an escape hatch to modify the header values after the fact +// Protected returns the protected headers. If w.protected is nil and v is +// non-nil, v is stored as the protected headers before returning. This allows +// callers to provide a default Headers that is used only when none was +// explicitly configured via WithProtectedHeaders. func (w *withKey) Protected(v Headers) Headers { if w.protected == nil && v != nil { w.protected = v @@ -48,11 +87,15 @@ func (w *withKey) Protected(v Headers) Headers { // WithKey is used to pass a static algorithm/key pair to either `jws.Sign()` or `jws.Verify()`. // +// IMPORTANT: Although `alg` is typed as `jwa.KeyAlgorithm` for compatibility +// with `(jwk.Key).Algorithm()`, JWS only accepts `jwa.SignatureAlgorithm` +// values here. Passing a key-encryption algorithm such as `jwa.A128KW()` to +// `jws.WithKey()` compiles, but `jws.Sign()` / `jws.Verify()` reject it at runtime. +// // The `alg` parameter is the identifier for the signature algorithm that should be used. -// It is of type `jwa.KeyAlgorithm` but in reality you can only pass `jwa.SignatureAlgorithm` -// types. It is this way so that the value in `(jwk.Key).Algorithm()` can be directly -// passed to the option. If you specify other algorithm types such as `jwa.KeyEncryptionAlgorithm`, -// then you will get an error when `jws.Sign()` or `jws.Verify()` is executed. +// It is of type `jwa.KeyAlgorithm` so that the value in `(jwk.Key).Algorithm()` can be +// directly passed to the option, but that is only valid when the JWK is already known to +// be intended for JWS and its `alg` value is a `jwa.SignatureAlgorithm`. // // The `alg` parameter cannot be "none" (jwa.NoSignature) for security reasons. // You will have to use a separate, more explicit option to allow the use of "none" @@ -221,7 +264,10 @@ type withInsecureNoSignature struct { protected Headers } -// Protected exists as an escape hatch to modify the header values after the fact +// Protected returns the protected headers. If w.protected is nil and v is +// non-nil, v is stored as the protected headers before returning. This allows +// callers to provide a default Headers that is used only when none was +// explicitly configured via WithProtectedHeaders. func (w *withInsecureNoSignature) Protected(v Headers) Headers { if w.protected == nil && v != nil { w.protected = v diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/options.yaml b/vendor/github.com/lestrrat-go/jwx/v3/jws/options.yaml index 79dbb72500..fb8c88db62 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/options.yaml +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/options.yaml @@ -54,6 +54,14 @@ interfaces: - verifyOption - parseOption - readFileOption + - name: GlobalParseOption + methods: + - globalOption + - parseOption + - readFileOption + comment: | + GlobalParseOption describes an Option that can be passed to `jws.Settings()`, + `jws.Parse()`, and `jws.ReadFile()`. - name: GlobalOption comment: | GlobalOption can be passed to `jws.Settings()` to set global options for the JWS package. @@ -89,11 +97,88 @@ options: Note that this option does NOT populate the `b64` header, which is sometimes required by other JWS implementations. - + When this option is used for `jws.Sign()`, the first parameter (normally the payload) must be set to `nil`. + + When passed to `jws.Verify()` together with `jws.WithCritValidation(true)`, + the RFC 7797 `"b64"` extension is automatically added to the + caller's allowlist as if `jws.WithCritExtension("b64")` had also + been passed. Detached-payload verification is the canonical + pairing for `b64=false`, and the jws package implements `b64=false` + handling natively, so there is no need to declare it explicitly. + Other crit extensions still require explicit declaration. If you have to verify using this option, you should know exactly how and why this works. + - ident: DetachedPayloadReader + interface: SignVerifyOption + argument_type: io.Reader + comment: | + WithDetachedPayloadReader is the streaming variant of + `jws.WithDetachedPayload()`: the detached payload is consumed from an + `io.Reader` instead of a `[]byte`. Use this when the detached payload + is too large to comfortably materialize in memory. + + For detached payloads that already fit in memory, prefer + `jws.WithDetachedPayload()`; the byte-slice path supports the full + option and algorithm surface, while the Reader path is a narrow + specialist. + + The Reader is consumed exactly once. On signing or verification + failure the Reader cannot be rewound; callers that need retry + semantics must buffer the payload themselves. + + The Reader is accessed from the calling goroutine only; do not share + a single Reader between concurrent `jws.Sign` / `jws.Verify` calls + unless the Reader is itself goroutine-safe and positioned + independently for each call. + + Only the HMAC, RSA (PKCS#1 v1.5 and PSS), and ECDSA algorithm + families are supported by this option; EdDSA and custom-family + algorithms require the full payload in memory and will be rejected + with a clear error. The option is mutually exclusive with + `jws.WithDetachedPayload()` and `jws.WithInsecureNoSignature()`. + Algorithms registered via `jws.RegisterSigner()` / + `jws.RegisterVerifier()` are likewise unreachable through this + path (the streaming code dispatches directly against the dsig + algorithm registry). + + On sign, multiple `jws.WithKey()` options may be combined with + `jws.WithJSON()` to produce a general-form multi-signature JWS; + the payload is streamed once and fanned out to each signer. All + signers must agree on the RFC 7797 `"b64"` flag, since the + produced JWS has a single payload segment on the wire. Compact + serialization still allows exactly one signature. + + On verify, this option supports only single-signature JWS input + (compact or flattened/general-single-signature JSON). To verify + one signature out of a multi-signature JWS, use `jws.Verify()` + with `jws.WithDetachedPayload()` and a buffered copy of the + payload. + + On verify, `jws.Verify()` returns a non-nil zero-length `[]byte` on + success when this option is used, because the payload was streamed + from the caller rather than extracted from the JWS envelope. Do not + read that slice as "the payload is empty" — the verified bytes are + whatever the caller read from the Reader, and callers that need + them must retain their own copy. + + `jws.WithBase64Encoder()` is honored as long as the supplied + encoder implements `jws.Base64StreamEncoder` (i.e. provides a + stream-capable `NewEncoder(io.Writer) io.WriteCloser`). The + default encoder (`encoding/base64.RawURLEncoding`) satisfies + this automatically. Custom encoders that only implement the + basic `jws.Base64Encoder` interface cause `jws.Sign()` / + `jws.Verify()` to return an error when combined with this + option, rather than silently falling back to a different encoder + for the payload. + + Like `jws.WithDetachedPayload()`, on verify this option + implicitly declares RFC 7797 `"b64"` as a recognized `crit` + extension. + + When this option is used with `jws.Sign()`, the first parameter + (normally the payload) must be set to `nil`. - ident: Base64Encoder interface: SignVerifyCompactOption argument_type: Base64Encoder @@ -165,6 +250,17 @@ options: It is highly recommended that you fix your key to contain a proper `alg` header field instead of resorting to using this option, but sometimes it just needs to happen. + + Fan-out and DoS considerations: when combined with `WithRequireKid(false)` + against a large JWKS, verification attempts scale with the number of + keys in the set. If the JWS protected header advertises an `alg` (as + required by RFC 7515 §4.1.1), only keys whose type is compatible with + that algorithm are tried, so the cost is bounded by the number of + type-compatible keys. If the header has no `alg`, every inferred + algorithm is tried against every candidate key, and the cost becomes + `N_keys × N_algs_per_keytype`. Operators exposing verification to + untrusted input should pair this option with `WithMaxSignatures` and + keep their JWKS bounded. - ident: UseDefault interface: WithKeySetSuboption argument_type: bool @@ -199,14 +295,36 @@ options: - ident: Context interface: VerifyOption argument_type: context.Context + comment: | + WithContext attaches a context.Context to the verify call. The + context is observed at three layers: + + - jws.Verify (slow path) checks ctx.Err() between each signature, + each key provider, and each (alg, key) attempt — a deadline + fired between iterations short-circuits the loop with the + context error. + - jkuProvider.FetchKeys passes ctx to jwk.Fetcher.Fetch. + - The streaming detached-payload path checks ctx between Reads + of the caller's payload reader. + + staticKeyProvider and keySetProvider do not themselves consult + ctx inside FetchKeys (the backing data is already in memory); + cancellation observation between key candidates is handled by + Verify's loop checks above. - ident: ProtectedHeaders interface: WithKeySuboption argument_type: Headers comment: | WithProtected is used with `jws.WithKey()` option when used with `jws.Sign()` to specify a protected header to be attached to the JWS signature. - - It has no effect if used when `jws.WithKey()` is passed to `jws.Verify()` + + It has no effect if used when `jws.WithKey()` is passed to `jws.Verify()`. + + kid precedence: if the supplied headers include a "kid" and the + `jwk.Key` passed to `jws.WithKey()` also carries one, the + `jwk.Key`'s kid wins and the header kid is silently overwritten. + Callers that want to keep the header kid must strip it from the + key first (e.g. `key.Remove(jwk.KeyIDKey)`). - ident: PublicHeaders interface: WithKeySuboption argument_type: Headers @@ -228,3 +346,47 @@ options: constant_value: true comment: | WithLegacySigners is a no-op option that exists only for backwards compatibility. + - ident: MaxSignatures + interface: GlobalParseOption + argument_type: int + comment: | + WithMaxSignatures specifies the maximum number of signatures allowed + in a JWS message using JSON serialization. If a JWS message contains + more signatures than this value, parsing will return an error. + The default value is 100. + + This option can be passed to `jws.Settings()` to change the default + globally, or to `jws.Parse()` / `jws.ReadFile()` for a per-call + override. + - ident: CritValidation + interface: VerifyOption + argument_type: bool + comment: | + WithCritValidation enables RFC 7515 Section 4.1.11 validation of the + "crit" (Critical) header parameter during verification. The default + is false, matching the behavior of v3.0.13 and earlier (the "crit" + header is silently ignored). + + When enabled, jws.Verify() will reject any JWS whose protected + header lists "crit" entries that the recipient has not declared + support for via jws.WithCritExtension(). It will also reject + structurally invalid "crit" lists: empty arrays, duplicate names, + empty extension names, names of standard JOSE header parameters, + and names that do not appear as header parameters in the protected + header. + + Per RFC 7515 Section 4.1.11, recipients MUST reject a JWS whose + "crit" list names extensions they do not understand. Enabling this + option together with one or more jws.WithCritExtension() calls is + the only way to satisfy that requirement with this library. + + IMPORTANT: enabling this option makes the library check that every + "crit" entry has been declared via jws.WithCritExtension(), but the + library cannot perform the actual extension-specific processing on + your behalf. After jws.Verify() returns successfully, your code + MUST read each declared extension header and apply whatever check + or side effect the extension semantics demand. If you declare an + extension and then forget to act on its value, you have defeated + the protection the producer was trying to obtain by marking that + extension critical. See the documentation on jws.WithCritExtension + for details. diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/options_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/options_gen.go index 7013e86bd7..10dd96e489 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/options_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/options_gen.go @@ -4,6 +4,7 @@ package jws import ( "context" + "io" "io/fs" "github.com/lestrrat-go/option/v2" @@ -35,6 +36,25 @@ type globalOption struct { func (*globalOption) globalOption() {} +// GlobalParseOption describes an Option that can be passed to `jws.Settings()`, +// `jws.Parse()`, and `jws.ReadFile()`. +type GlobalParseOption interface { + Option + globalOption() + parseOption() + readFileOption() +} + +type globalParseOption struct { + Option +} + +func (*globalParseOption) globalOption() {} + +func (*globalParseOption) parseOption() {} + +func (*globalParseOption) readFileOption() {} + // ReadFileOption is a type of `Option` that can be passed to `jwe.Parse` type ParseOption interface { Option @@ -185,14 +205,17 @@ func (*withKeySuboption) withKeySuboption() {} type identBase64Encoder struct{} type identContext struct{} +type identCritValidation struct{} type identDetached struct{} type identDetachedPayload struct{} +type identDetachedPayloadReader struct{} type identFS struct{} type identInferAlgorithmFromKey struct{} type identKey struct{} type identKeyProvider struct{} type identKeyUsed struct{} type identLegacySigners struct{} +type identMaxSignatures struct{} type identMessage struct{} type identMultipleKeysPerKeyID struct{} type identPretty struct{} @@ -211,6 +234,10 @@ func (identContext) String() string { return "WithContext" } +func (identCritValidation) String() string { + return "WithCritValidation" +} + func (identDetached) String() string { return "WithDetached" } @@ -219,6 +246,10 @@ func (identDetachedPayload) String() string { return "WithDetachedPayload" } +func (identDetachedPayloadReader) String() string { + return "WithDetachedPayloadReader" +} + func (identFS) String() string { return "WithFS" } @@ -243,6 +274,10 @@ func (identLegacySigners) String() string { return "WithLegacySigners" } +func (identMaxSignatures) String() string { + return "WithMaxSignatures" +} + func (identMessage) String() string { return "WithMessage" } @@ -286,10 +321,57 @@ func WithBase64Encoder(v Base64Encoder) SignVerifyCompactOption { return &signVerifyCompactOption{option.New(identBase64Encoder{}, v)} } +// WithContext attaches a context.Context to the verify call. The +// context is observed at three layers: +// +// - jws.Verify (slow path) checks ctx.Err() between each signature, +// each key provider, and each (alg, key) attempt — a deadline +// fired between iterations short-circuits the loop with the +// context error. +// - jkuProvider.FetchKeys passes ctx to jwk.Fetcher.Fetch. +// - The streaming detached-payload path checks ctx between Reads +// of the caller's payload reader. +// +// staticKeyProvider and keySetProvider do not themselves consult +// ctx inside FetchKeys (the backing data is already in memory); +// cancellation observation between key candidates is handled by +// Verify's loop checks above. func WithContext(v context.Context) VerifyOption { return &verifyOption{option.New(identContext{}, v)} } +// WithCritValidation enables RFC 7515 Section 4.1.11 validation of the +// "crit" (Critical) header parameter during verification. The default +// is false, matching the behavior of v3.0.13 and earlier (the "crit" +// header is silently ignored). +// +// When enabled, jws.Verify() will reject any JWS whose protected +// header lists "crit" entries that the recipient has not declared +// support for via jws.WithCritExtension(). It will also reject +// structurally invalid "crit" lists: empty arrays, duplicate names, +// empty extension names, names of standard JOSE header parameters, +// and names that do not appear as header parameters in the protected +// header. +// +// Per RFC 7515 Section 4.1.11, recipients MUST reject a JWS whose +// "crit" list names extensions they do not understand. Enabling this +// option together with one or more jws.WithCritExtension() calls is +// the only way to satisfy that requirement with this library. +// +// IMPORTANT: enabling this option makes the library check that every +// "crit" entry has been declared via jws.WithCritExtension(), but the +// library cannot perform the actual extension-specific processing on +// your behalf. After jws.Verify() returns successfully, your code +// MUST read each declared extension header and apply whatever check +// or side effect the extension semantics demand. If you declare an +// extension and then forget to act on its value, you have defeated +// the protection the producer was trying to obtain by marking that +// extension critical. See the documentation on jws.WithCritExtension +// for details. +func WithCritValidation(v bool) VerifyOption { + return &verifyOption{option.New(identCritValidation{}, v)} +} + // WithDetached specifies that the `jws.Message` should be serialized in // JWS compact serialization with detached payload. The resulting octet // sequence will not contain the payload section. @@ -305,11 +387,88 @@ func WithDetached(v bool) CompactOption { // When this option is used for `jws.Sign()`, the first parameter (normally the payload) // must be set to `nil`. // +// When passed to `jws.Verify()` together with `jws.WithCritValidation(true)`, +// the RFC 7797 `"b64"` extension is automatically added to the +// caller's allowlist as if `jws.WithCritExtension("b64")` had also +// been passed. Detached-payload verification is the canonical +// pairing for `b64=false`, and the jws package implements `b64=false` +// handling natively, so there is no need to declare it explicitly. +// Other crit extensions still require explicit declaration. +// // If you have to verify using this option, you should know exactly how and why this works. func WithDetachedPayload(v []byte) SignVerifyOption { return &signVerifyOption{option.New(identDetachedPayload{}, v)} } +// WithDetachedPayloadReader is the streaming variant of +// `jws.WithDetachedPayload()`: the detached payload is consumed from an +// `io.Reader` instead of a `[]byte`. Use this when the detached payload +// is too large to comfortably materialize in memory. +// +// For detached payloads that already fit in memory, prefer +// `jws.WithDetachedPayload()`; the byte-slice path supports the full +// option and algorithm surface, while the Reader path is a narrow +// specialist. +// +// The Reader is consumed exactly once. On signing or verification +// failure the Reader cannot be rewound; callers that need retry +// semantics must buffer the payload themselves. +// +// The Reader is accessed from the calling goroutine only; do not share +// a single Reader between concurrent `jws.Sign` / `jws.Verify` calls +// unless the Reader is itself goroutine-safe and positioned +// independently for each call. +// +// Only the HMAC, RSA (PKCS#1 v1.5 and PSS), and ECDSA algorithm +// families are supported by this option; EdDSA and custom-family +// algorithms require the full payload in memory and will be rejected +// with a clear error. The option is mutually exclusive with +// `jws.WithDetachedPayload()` and `jws.WithInsecureNoSignature()`. +// Algorithms registered via `jws.RegisterSigner()` / +// `jws.RegisterVerifier()` are likewise unreachable through this +// path (the streaming code dispatches directly against the dsig +// algorithm registry). +// +// On sign, multiple `jws.WithKey()` options may be combined with +// `jws.WithJSON()` to produce a general-form multi-signature JWS; +// the payload is streamed once and fanned out to each signer. All +// signers must agree on the RFC 7797 `"b64"` flag, since the +// produced JWS has a single payload segment on the wire. Compact +// serialization still allows exactly one signature. +// +// On verify, this option supports only single-signature JWS input +// (compact or flattened/general-single-signature JSON). To verify +// one signature out of a multi-signature JWS, use `jws.Verify()` +// with `jws.WithDetachedPayload()` and a buffered copy of the +// payload. +// +// On verify, `jws.Verify()` returns a non-nil zero-length `[]byte` on +// success when this option is used, because the payload was streamed +// from the caller rather than extracted from the JWS envelope. Do not +// read that slice as "the payload is empty" — the verified bytes are +// whatever the caller read from the Reader, and callers that need +// them must retain their own copy. +// +// `jws.WithBase64Encoder()` is honored as long as the supplied +// encoder implements `jws.Base64StreamEncoder` (i.e. provides a +// stream-capable `NewEncoder(io.Writer) io.WriteCloser`). The +// default encoder (`encoding/base64.RawURLEncoding`) satisfies +// this automatically. Custom encoders that only implement the +// basic `jws.Base64Encoder` interface cause `jws.Sign()` / +// `jws.Verify()` to return an error when combined with this +// option, rather than silently falling back to a different encoder +// for the payload. +// +// Like `jws.WithDetachedPayload()`, on verify this option +// implicitly declares RFC 7797 `"b64"` as a recognized `crit` +// extension. +// +// When this option is used with `jws.Sign()`, the first parameter +// (normally the payload) must be set to `nil`. +func WithDetachedPayloadReader(v io.Reader) SignVerifyOption { + return &signVerifyOption{option.New(identDetachedPayloadReader{}, v)} +} + // WithFS specifies the source `fs.FS` object to read the file from. func WithFS(v fs.FS) ReadFileOption { return &readFileOption{option.New(identFS{}, v)} @@ -334,6 +493,17 @@ func WithFS(v fs.FS) ReadFileOption { // It is highly recommended that you fix your key to contain a proper `alg` // header field instead of resorting to using this option, but sometimes // it just needs to happen. +// +// Fan-out and DoS considerations: when combined with `WithRequireKid(false)` +// against a large JWKS, verification attempts scale with the number of +// keys in the set. If the JWS protected header advertises an `alg` (as +// required by RFC 7515 §4.1.1), only keys whose type is compatible with +// that algorithm are tried, so the cost is bounded by the number of +// type-compatible keys. If the header has no `alg`, every inferred +// algorithm is tried against every candidate key, and the cost becomes +// `N_keys × N_algs_per_keytype`. Operators exposing verification to +// untrusted input should pair this option with `WithMaxSignatures` and +// keep their JWKS bounded. func WithInferAlgorithmFromKey(v bool) WithKeySetSuboption { return &withKeySetSuboption{option.New(identInferAlgorithmFromKey{}, v)} } @@ -361,6 +531,18 @@ func WithLegacySigners() GlobalOption { return &globalOption{option.New(identLegacySigners{}, true)} } +// WithMaxSignatures specifies the maximum number of signatures allowed +// in a JWS message using JSON serialization. If a JWS message contains +// more signatures than this value, parsing will return an error. +// The default value is 100. +// +// This option can be passed to `jws.Settings()` to change the default +// globally, or to `jws.Parse()` / `jws.ReadFile()` for a per-call +// override. +func WithMaxSignatures(v int) GlobalParseOption { + return &globalParseOption{option.New(identMaxSignatures{}, v)} +} + // WithMessage can be passed to Verify() to obtain the jws.Message upon // a successful verification. func WithMessage(v *Message) VerifyOption { @@ -385,7 +567,13 @@ func WithPretty(v bool) WithJSONSuboption { // WithProtected is used with `jws.WithKey()` option when used with `jws.Sign()` // to specify a protected header to be attached to the JWS signature. // -// It has no effect if used when `jws.WithKey()` is passed to `jws.Verify()` +// It has no effect if used when `jws.WithKey()` is passed to `jws.Verify()`. +// +// kid precedence: if the supplied headers include a "kid" and the +// `jwk.Key` passed to `jws.WithKey()` also carries one, the +// `jwk.Key`'s kid wins and the header kid is silently overwritten. +// Callers that want to keep the header kid must strip it from the +// key first (e.g. `key.Remove(jwk.KeyIDKey)`). func WithProtectedHeaders(v Headers) WithKeySuboption { return &withKeySuboption{option.New(identProtectedHeaders{}, v)} } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/sign_context.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/sign_context.go index 49abe0abca..a6cbd045e9 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/sign_context.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/sign_context.go @@ -2,6 +2,8 @@ package jws import ( "fmt" + "io" + "strings" "github.com/lestrrat-go/jwx/v3/internal/base64" "github.com/lestrrat-go/jwx/v3/internal/pool" @@ -9,13 +11,14 @@ import ( ) type signContext struct { - format int - detached bool - validateKey bool - payload []byte - encoder Base64Encoder - none *signatureBuilder // special signature builder - sigbuilders []*signatureBuilder + format int + detached bool + validateKey bool + payload []byte + payloadReader io.Reader + encoder Base64Encoder + none *signatureBuilder // special signature builder + sigbuilders []*signatureBuilder } var signContextPool = pool.New[*signContext](allocSignContext, freeSignContext) @@ -39,6 +42,7 @@ func freeSignContext(ctx *signContext) *signContext { ctx.encoder = base64.DefaultEncoder() ctx.none = nil ctx.payload = nil + ctx.payloadReader = nil return ctx } @@ -48,12 +52,12 @@ func (sc *signContext) ProcessOptions(options []SignOption) error { switch option.Ident() { case identSerialization{}: if err := option.Value(&sc.format); err != nil { - return signerr(`failed to retrieve serialization option value: %w`, err) + return makeSignError(prefixJwsSign, `failed to retrieve serialization option value: %w`, err) } case identInsecureNoSignature{}: var data withInsecureNoSignature if err := option.Value(&data); err != nil { - return signerr(`failed to retrieve insecure-no-signature option value: %w`, err) + return makeSignError(prefixJwsSign, `failed to retrieve insecure-no-signature option value: %w`, err) } sb := signatureBuilderPool.Get() sb.alg = jwa.NoSignature() @@ -64,17 +68,21 @@ func (sc *signContext) ProcessOptions(options []SignOption) error { case identKey{}: var data *withKey if err := option.Value(&data); err != nil { - return signerr(`jws.Sign: invalid value for WithKey option: %w`, err) + return makeSignError(prefixJwsSign, `invalid value for WithKey option: %w`, err) } alg, ok := data.alg.(jwa.SignatureAlgorithm) if !ok { - return signerr(`expected algorithm to be of type jwa.SignatureAlgorithm but got (%[1]q, %[1]T)`, data.alg) + return makeSignError(prefixJwsSign, `expected algorithm to be of type jwa.SignatureAlgorithm but got (%[1]q, %[1]T)`, data.alg) } // No, we don't accept "none" here. if alg == jwa.NoSignature() { - return signerr(`"none" (jwa.NoSignature) cannot be used with jws.WithKey`) + return makeSignError(prefixJwsSign, `"none" (jwa.NoSignature) cannot be used with jws.WithKey`) + } + + if err := validateAlgorithmForKey(alg, data.key); err != nil { + return makeSignError(prefixJwsSign, `%w`, err) } sb := signatureBuilderPool.Get() @@ -97,21 +105,40 @@ func (sc *signContext) ProcessOptions(options []SignOption) error { sc.sigbuilders = append(sc.sigbuilders, sb) case identDetachedPayload{}: + if sc.payloadReader != nil { + return makeSignError(prefixJwsSign, `jws.WithDetachedPayload() and jws.WithDetachedPayloadReader() are mutually exclusive`) + } if sc.payload != nil { - return signerr(`payload must be nil when jws.WithDetachedPayload() is specified`) + return makeSignError(prefixJwsSign, `the first argument to jws.Sign() must be nil when jws.WithDetachedPayload() is used`) } if err := option.Value(&sc.payload); err != nil { - return signerr(`failed to retrieve detached payload option value: %w`, err) + return makeSignError(prefixJwsSign, `failed to retrieve detached payload option value: %w`, err) + } + sc.detached = true + case identDetachedPayloadReader{}: + if sc.payloadReader != nil { + return makeSignError(prefixJwsSign, `jws.WithDetachedPayloadReader() specified more than once`) + } + if sc.detached { + return makeSignError(prefixJwsSign, `jws.WithDetachedPayload() and jws.WithDetachedPayloadReader() are mutually exclusive`) + } + if sc.payload != nil { + return makeSignError(prefixJwsSign, `the first argument to jws.Sign() must be nil when jws.WithDetachedPayloadReader() is used`) + } + if err := option.Value(&sc.payloadReader); err != nil { + return makeSignError(prefixJwsSign, `failed to retrieve detached payload reader option value: %w`, err) } sc.detached = true case identValidateKey{}: if err := option.Value(&sc.validateKey); err != nil { - return signerr(`failed to retrieve validate-key option value: %w`, err) + return makeSignError(prefixJwsSign, `failed to retrieve validate-key option value: %w`, err) } case identBase64Encoder{}: if err := option.Value(&sc.encoder); err != nil { - return signerr(`failed to retrieve base64-encoder option value: %w`, err) + return makeSignError(prefixJwsSign, `failed to retrieve base64-encoder option value: %w`, err) } + default: + return makeSignError(prefixJwsSign, `invalid jws.SignOption %q passed`, `With`+strings.TrimPrefix(fmt.Sprintf(`%T`, option.Ident()), `jws.ident`)) } } return nil @@ -119,6 +146,7 @@ func (sc *signContext) ProcessOptions(options []SignOption) error { func (sc *signContext) PopulateMessage(m *Message) error { m.payload = sc.payload + m.detached = sc.detached m.signatures = make([]*Signature, 0, len(sc.sigbuilders)) for i, sb := range sc.sigbuilders { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/signature_builder.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/signature_builder.go index fc09a69367..2963b6e48d 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/signature_builder.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/signature_builder.go @@ -3,6 +3,7 @@ package jws import ( "bytes" "fmt" + "slices" "github.com/lestrrat-go/jwx/v3/internal/json" "github.com/lestrrat-go/jwx/v3/internal/pool" @@ -52,26 +53,51 @@ func freeSignatureBuilder(sb *signatureBuilder) *signatureBuilder { } func (sb *signatureBuilder) Build(sc *signContext, payload []byte) (*Signature, error) { - protected := sb.protected - if protected == nil { + // Clone caller-provided headers before mutating so that re-using the + // same Headers instance across multiple Sign calls does not cause + // cross-contamination of alg/kid. + var protected Headers + if sb.protected != nil { + cloned, err := sb.protected.Clone() + if err != nil { + return nil, makeSignError(prefixJwsSign, `failed to clone protected headers: %w`, err) + } + protected = cloned + } else { protected = NewHeaders() } if err := protected.Set(AlgorithmKey, sb.alg); err != nil { - return nil, signerr(`failed to set "alg" header: %w`, err) + return nil, makeSignError(prefixJwsSign, `failed to set "alg" header: %w`, err) } if key, ok := sb.key.(jwk.Key); ok { if kid, ok := key.KeyID(); ok && kid != "" { if err := protected.Set(KeyIDKey, kid); err != nil { - return nil, signerr(`failed to set "kid" header: %w`, err) + return nil, makeSignError(prefixJwsSign, `failed to set "kid" header: %w`, err) + } + } + } + + // RFC 7797 §3 requires producers that set "b64":false to also list + // "b64" in "crit". Auto-declare it in the protected header so a + // caller who set b64=false but forgot the crit declaration does not + // emit a non-conformant stream that strict verifiers refuse. + // Idempotent: if "b64" is already in crit, the list is unchanged. + // If crit is unset, it is created with just "b64". + if !getB64Value(protected) { + crit, _ := protected.Critical() + if !slices.Contains(crit, "b64") { + crit = append(crit, "b64") + if err := protected.Set(CriticalKey, crit); err != nil { + return nil, makeSignError(prefixJwsSign, `failed to set "crit" header: %w`, err) } } } hdrs, err := mergeHeaders(sb.public, protected) if err != nil { - return nil, signerr(`failed to merge headers: %w`, err) + return nil, makeSignError(prefixJwsSign, `failed to merge headers: %w`, err) } // raw, json format headers @@ -84,7 +110,7 @@ func (sb *signatureBuilder) Build(sc *signContext, payload []byte) (*Signature, b64 := getB64Value(hdrs) if !b64 && !sc.detached { if bytes.IndexByte(payload, tokens.Period) != -1 { - return nil, fmt.Errorf(`payload must not contain a "."`) + return nil, fmt.Errorf(`compact serialization with b64=false requires payload to contain no "." characters per RFC 7797 §5.2; use jws.WithDetachedPayload to keep the payload out of the wire format`) } } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/signer.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/signer.go index 99005e859a..af88f5e4b1 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/signer.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/signer.go @@ -145,10 +145,9 @@ func UnregisterSigner(alg jwa.SignatureAlgorithm) { } // NewSigner creates a signer that signs payloads using the given signature algorithm. -// This function is deprecated, and will either be removed to re-purposed using -// a different signature. // -// When you want to load a Signer object, you should use `SignerFor()` instead. +// Deprecated: Use [SignerFor] instead. This function will be removed or +// repurposed with a different signature in v4. func NewSigner(alg jwa.SignatureAlgorithm) (Signer, error) { s, err := newLegacySigner(alg) if err == nil { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/streaming_detached.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/streaming_detached.go new file mode 100644 index 0000000000..871bb6439d --- /dev/null +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/streaming_detached.go @@ -0,0 +1,525 @@ +package jws + +import ( + "crypto/ecdsa" + "crypto/hmac" + "crypto/rsa" + "fmt" + "hash" + "io" + + "github.com/lestrrat-go/blackmagic" + "github.com/lestrrat-go/dsig" + "github.com/lestrrat-go/jwx/v3/internal/base64" + "github.com/lestrrat-go/jwx/v3/internal/json" + "github.com/lestrrat-go/jwx/v3/internal/keyconv" + "github.com/lestrrat-go/jwx/v3/internal/tokens" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jws/jwsbb" +) + +// This file implements the streaming detached-payload variant of jws.Sign() +// and jws.Verify(), reached via the jws.WithDetachedPayloadReader() option. +// It deliberately bypasses the jws.Signer / jws.Verifier dispatch path and +// talks to dsig directly, because it needs incremental hashing through a +// hash.Hash — the Signer interface takes a fully materialized []byte payload. +// +// Consequences: algorithms registered via jws.RegisterSigner() / +// jws.RegisterVerifier() are unreachable here, as are algorithm families +// that cannot be driven from a digest (EdDSA, custom). + +// streamingSigner carries per-signature state through signStreaming: +// the header prefix already written into hasher, the hasher itself +// (fed with the prefix, ready to consume payload bytes), the resolved +// dsig alg info, and the raw key / unprotected header needed for final +// signing and JSON assembly. +type streamingSigner struct { + dsigInfo streamingAlgorithmInfo + rawKey any + hasher hash.Hash + hdrEncoded string // base64(protected header JSON) + public Headers +} + +// signStreaming is invoked from Sign() when sc.payloadReader is set. It +// assembles the signing input for each configured signer by feeding +// base64(header) "." base64(payload) (or raw payload when b64=false) +// into a hash.Hash, then calls dsig.SignDigest once per signer. When +// more than one signer is registered the payload is streamed once and +// fanned out to each hasher via [io.MultiWriter]; the general JSON +// serialization is used for the output. +func (sc *signContext) signStreaming() ([]byte, error) { + if sc.none != nil { + return nil, makeSignError(prefixJwsSign, `jws.WithInsecureNoSignature() cannot be combined with jws.WithDetachedPayloadReader(); use jws.Sign with jws.WithInsecureNoSignature() if you really need an unsecured in-memory JWS`) + } + + streamEncoder, ok := base64.AsStreamEncoder(sc.encoder) + if !ok { + return nil, makeSignError(prefixJwsSign, `jws.WithDetachedPayloadReader() requires a base64 encoder with a NewEncoder(io.Writer) io.WriteCloser method (interface jws.Base64StreamEncoder). The configured encoder %T does not provide one. Install a stream-capable encoder via jwx.Settings(jwx.WithBase64Encoder(...)) or jws.WithBase64Encoder(...); the default encoding/base64.RawURLEncoding satisfies this automatically.`, sc.encoder) + } + + signers := make([]streamingSigner, 0, len(sc.sigbuilders)) + var b64 bool + var b64Set bool + + for idx, sb := range sc.sigbuilders { + alg := sb.alg + if alg == jwa.NoSignature() { + return nil, makeSignError(prefixJwsSign, `"none" (jwa.NoSignature) cannot be used with jws.WithDetachedPayloadReader()`) + } + + dsigInfo, err := resolveStreamingAlgorithm(alg) + if err != nil { + return nil, makeSignError(prefixJwsSign, `signature %d: %w`, idx, err) + } + + if sc.validateKey { + if err := validateKeyBeforeUse(sb.key); err != nil { + return nil, makeSignError(prefixJwsSign, `failed to validate key for signature %d: %w`, idx, err) + } + } + + rawKey, err := convertStreamingSignKey(sb.key, dsigInfo.Family) + if err != nil { + return nil, makeSignError(prefixJwsSign, `failed to convert key for signature %d: %w`, idx, err) + } + + protected, err := cloneOrNewHeaders(sb.protected) + if err != nil { + return nil, makeSignError(prefixJwsSign, `failed to clone protected headers for signature %d: %w`, idx, err) + } + if err := protected.Set(AlgorithmKey, alg); err != nil { + return nil, makeSignError(prefixJwsSign, `failed to set "alg" header for signature %d: %w`, idx, err) + } + if jwkKey, ok := sb.key.(jwk.Key); ok { + var kid string + if err := jwkKey.Get(jwk.KeyIDKey, &kid); err == nil && kid != "" { + if err := protected.Set(KeyIDKey, kid); err != nil { + return nil, makeSignError(prefixJwsSign, `failed to set "kid" header for signature %d: %w`, idx, err) + } + } + } + + // For compact serialization RFC 7515 requires the unprotected + // header to be merged into the protected header because there + // is no separate slot for it on the wire. JSON serializations + // keep them separate. + signingHeaders := protected + if sc.format == fmtCompact { + signingHeaders, err = mergeHeaders(sb.public, protected) + if err != nil { + return nil, makeSignError(prefixJwsSign, `failed to merge headers for signature %d: %w`, idx, err) + } + } + + // A multi-signature JWS has a single payload segment on the + // wire, so every signer must agree on the RFC 7797 "b64" flag + // or the produced JWS is internally inconsistent. + thisB64 := getB64Value(signingHeaders) + if !b64Set { + b64 = thisB64 + b64Set = true + } else if thisB64 != b64 { + return nil, makeSignError(prefixJwsSign, `signature %d disagrees with earlier signers on the RFC 7797 "b64" flag; all signers must use the same b64 value`, idx) + } + + hdrbuf, err := json.Marshal(signingHeaders) + if err != nil { + return nil, makeSignError(prefixJwsSign, `failed to marshal headers for signature %d: %w`, idx, err) + } + + hasher, err := newStreamingHasher(dsigInfo, rawKey) + if err != nil { + return nil, makeSignError(prefixJwsSign, `failed to create hasher for signature %d: %w`, idx, err) + } + + hdrEncoded := streamEncoder.EncodeToString(hdrbuf) + if _, err := hasher.Write([]byte(hdrEncoded)); err != nil { + return nil, makeSignError(prefixJwsSign, `failed to write signing prefix for signature %d: %w`, idx, err) + } + if _, err := hasher.Write([]byte{tokens.Period}); err != nil { + return nil, makeSignError(prefixJwsSign, `failed to write signing prefix for signature %d: %w`, idx, err) + } + + signers = append(signers, streamingSigner{ + dsigInfo: dsigInfo, + rawKey: rawKey, + hasher: hasher, + hdrEncoded: hdrEncoded, + public: sb.public, + }) + } + + hashers := make([]hash.Hash, len(signers)) + for i := range signers { + hashers[i] = signers[i].hasher + } + if err := streamPayloadIntoHashers(hashers, sc.payloadReader, b64, streamEncoder); err != nil { + return nil, makeSignError(prefixJwsSign, `failed to stream payload: %w`, err) + } + + rawSignatures := make([][]byte, len(signers)) + for i, st := range signers { + sig, err := dsig.SignDigest(st.rawKey, st.dsigInfo.Name, st.hasher.Sum(nil), nil) + if err != nil { + return nil, makeSignError(prefixJwsSign, `failed to sign digest for signature %d: %w`, i, err) + } + rawSignatures[i] = sig + } + + switch sc.format { + case fmtCompact: + // Upstream validation in jws.Sign guarantees compact implies + // exactly one signer, but guard against future drift. + if len(signers) != 1 { + return nil, makeSignError(prefixJwsSign, `compact serialization requires exactly one signature, got %d`, len(signers)) + } + sigEncoded := streamEncoder.EncodeToString(rawSignatures[0]) + buf := make([]byte, 0, len(signers[0].hdrEncoded)+2+len(sigEncoded)) + buf = append(buf, signers[0].hdrEncoded...) + buf = append(buf, tokens.Period, tokens.Period) + buf = append(buf, sigEncoded...) + return buf, nil + case fmtJSON, fmtJSONPretty: + return assembleStreamingDetachedJSON(signers, rawSignatures, streamEncoder, sc.format == fmtJSONPretty) + default: + return nil, makeSignError(prefixJwsSign, `unexpected serialization format %d`, sc.format) + } +} + +// verifyStreaming is invoked from VerifyMessage() when vc.payloadReader is +// set. It parses the JWS envelope through jws.Parse, then re-builds the +// signing input by feeding base64(header) "." base64(payload) into a +// hash.Hash fed from the supplied io.Reader and calls dsig.VerifyDigest. +func (vc *verifyContext) verifyStreaming(buf []byte) ([]byte, error) { + if len(vc.keyProviders) != 1 { + return nil, makeVerifyError(`jws.WithDetachedPayloadReader() requires exactly one jws.WithKey(); jws.WithKeySet(), jws.WithKeyProvider() and jws.WithVerifyAuto() are not supported on the streaming path`) + } + staticKP, ok := vc.keyProviders[0].(*staticKeyProvider) + if !ok { + return nil, makeVerifyError(`jws.WithDetachedPayloadReader() requires exactly one jws.WithKey(); jws.WithKeySet(), jws.WithKeyProvider() and jws.WithVerifyAuto() are not supported on the streaming path`) + } + alg := staticKP.alg + key := staticKP.key + + if alg == jwa.NoSignature() { + return nil, makeVerifyError(`"none" (jwa.NoSignature) cannot be used with jws.WithDetachedPayloadReader(); use jws.Parse if you need to inspect an unsecured JWS`) + } + + dsigInfo, err := resolveStreamingAlgorithm(alg) + if err != nil { + return nil, makeVerifyError(`%w`, err) + } + + streamEncoder, ok := base64.AsStreamEncoder(vc.encoder) + if !ok { + return nil, makeVerifyError(`jws.WithDetachedPayloadReader() requires a base64 encoder with a NewEncoder(io.Writer) io.WriteCloser method (interface jws.Base64StreamEncoder). The configured encoder %T does not provide one. Install a stream-capable encoder via jwx.Settings(jwx.WithBase64Encoder(...)) or jws.WithBase64Encoder(...); the default encoding/base64.RawURLEncoding satisfies this automatically.`, vc.encoder) + } + + msg, err := Parse(buf, vc.parseOptions...) + if err != nil { + return nil, makeVerifyError(`failed to parse jws: %w`, err) + } + defer msg.clearRaw() + + if len(msg.signatures) != 1 { + return nil, makeVerifyError(`jws.WithDetachedPayloadReader() supports only single-signature JWS, got %d`, len(msg.signatures)) + } + if len(msg.payload) != 0 { + return nil, makeVerifyError(`JWS must not have an embedded payload when jws.WithDetachedPayloadReader() is used`) + } + + sig := msg.signatures[0] + + var rawHeaders []byte + if rbp, ok := sig.protected.(interface{ rawBuffer() []byte }); ok { + rawHeaders = rbp.rawBuffer() + } + if rawHeaders == nil { + rawHeaders, err = json.Marshal(sig.protected) + if err != nil { + return nil, makeVerifyError(`failed to marshal "protected": %w`, err) + } + } + + if vc.critValidation { + if err := validateB64InCritIfFalse(sig.protected); err != nil { + return nil, makeVerifyError(`%w`, err) + } + if err := validateCritical(sig.protected, vc.criticalExtensions); err != nil { + return nil, makeVerifyError(`invalid "crit" header: %w`, err) + } + } + + if vc.validateKey { + if err := validateKeyBeforeUse(key); err != nil { + return nil, makeVerifyError(`failed to validate key before verification: %w`, err) + } + } + + rawKey, err := convertStreamingVerifyKey(key, dsigInfo.Family) + if err != nil { + return nil, makeVerifyError(`failed to convert key: %w`, err) + } + + hasher, err := newStreamingHasher(dsigInfo, rawKey) + if err != nil { + return nil, makeVerifyError(`failed to create hasher: %w`, err) + } + hdrEncoded := streamEncoder.EncodeToString(rawHeaders) + if _, err := hasher.Write([]byte(hdrEncoded)); err != nil { + return nil, makeVerifyError(`failed to write signing prefix: %w`, err) + } + if _, err := hasher.Write([]byte{tokens.Period}); err != nil { + return nil, makeVerifyError(`failed to write signing prefix: %w`, err) + } + if err := streamPayloadIntoHashers([]hash.Hash{hasher}, vc.payloadReader, msg.b64, streamEncoder); err != nil { + return nil, makeVerifyError(`failed to stream payload: %w`, err) + } + + if err := dsig.VerifyDigest(rawKey, dsigInfo.Name, hasher.Sum(nil), sig.signature); err != nil { + return nil, makeVerifyError(`failed to verify signature: %w`, verificationError{err}) + } + + if vc.keyUsed != nil { + if err := blackmagic.AssignIfCompatible(vc.keyUsed, key); err != nil { + return nil, makeVerifyError(`failed to assign key to keyUsed: %w`, err) + } + } + if vc.dst != nil { + *vc.dst = *msg + } + // Non-nil zero-length slice is the sentinel: the payload was streamed + // from the caller so there are no bytes to hand back, but returning + // nil would be indistinguishable from "ignored return" and invite + // `len(payload) == 0` silent-logic bugs in callers. + return []byte{}, nil +} + +// streamingAlgorithmInfo carries the resolved dsig metadata plus the dsig +// algorithm name, since dsig.AlgorithmInfo itself does not include it. +type streamingAlgorithmInfo struct { + dsig.AlgorithmInfo + + Name string +} + +// resolveStreamingAlgorithm maps a JWS algorithm to its dsig metadata and +// enforces the family restrictions for the streaming path. It routes through +// jwsbb.GetDsigAlgorithm so algorithms registered by extension modules work +// just like algorithms built in to jws. +func resolveStreamingAlgorithm(alg jwa.SignatureAlgorithm) (streamingAlgorithmInfo, error) { + dsigAlg, ok := jwsbb.GetDsigAlgorithm(alg.String()) + if !ok { + // For custom dsig algorithms registered directly with dsig the JWS + // name may equal the dsig name. + dsigAlg = alg.String() + } + info, ok := dsig.GetAlgorithmInfo(dsigAlg) + if !ok { + return streamingAlgorithmInfo{}, fmt.Errorf(`unsupported algorithm %q; use jws.WithDetachedPayload() if you need the general detached path`, alg) + } + switch info.Family { + case dsig.EdDSAFamily: + return streamingAlgorithmInfo{}, fmt.Errorf(`algorithm %q is incompatible with streaming detached payloads: RFC 8032 EdDSA signs the full message, not a pre-computed digest, so the payload cannot be streamed; use jws.WithDetachedPayload() if the payload fits in memory, or a digest-based algorithm such as HS256, RS256, or ES256`, alg) + case dsig.Custom: + return streamingAlgorithmInfo{}, fmt.Errorf(`algorithm %q is a custom-family algorithm and does not support streaming because the library cannot know whether the algorithm pre-hashes the payload; use jws.WithDetachedPayload() if the payload fits in memory`, alg) + } + return streamingAlgorithmInfo{AlgorithmInfo: info, Name: dsigAlg}, nil +} + +// newStreamingHasher returns a hash.Hash preloaded with the key material +// the family needs (HMAC is keyed; RSA/ECDSA just hash). +func newStreamingHasher(info streamingAlgorithmInfo, key any) (hash.Hash, error) { + switch info.Family { + case dsig.HMAC: + meta, ok := info.Meta.(dsig.HMACFamilyMeta) + if !ok { + return nil, fmt.Errorf(`invalid HMAC metadata`) + } + keyBytes, ok := key.([]byte) + if !ok { + // Route through keyconv so the error matches the non-streaming + // HMAC path (e.g., passing a string secret surfaces + // `keyconv: expected []byte, got string`) instead of the + // terser type-assertion failure. + if err := keyconv.ByteSliceKey(&keyBytes, key); err != nil { + return nil, fmt.Errorf(`failed to convert HMAC key to []byte (streaming path): %w`, err) + } + } + return hmac.New(meta.HashFunc, keyBytes), nil + case dsig.RSA: + meta, ok := info.Meta.(dsig.RSAFamilyMeta) + if !ok { + return nil, fmt.Errorf(`invalid RSA metadata`) + } + return meta.Hash.New(), nil + case dsig.ECDSA: + meta, ok := info.Meta.(dsig.ECDSAFamilyMeta) + if !ok { + return nil, fmt.Errorf(`invalid ECDSA metadata`) + } + return meta.Hash.New(), nil + default: + return nil, fmt.Errorf(`unsupported algorithm family %q for streaming`, info.Family) + } +} + +// streamPayloadIntoHashers copies the payload once and fans it out to +// every hasher via [io.MultiWriter]. When b64=true the bytes are +// routed through per-hasher [io.WriteCloser]s returned by the +// [base64.StreamEncoder] (each encoder keeps an unflushed 3-byte tail, +// so the wrappers cannot be shared). When b64=false the hashers receive +// the payload bytes directly. +func streamPayloadIntoHashers(hashers []hash.Hash, payload io.Reader, encodePayload bool, enc base64.StreamEncoder) error { + if len(hashers) == 0 { + return fmt.Errorf(`no hashers to stream into`) + } + if !encodePayload { + writers := make([]io.Writer, len(hashers)) + for i, h := range hashers { + writers[i] = h + } + if _, err := io.Copy(io.MultiWriter(writers...), payload); err != nil { + return fmt.Errorf(`failed to stream payload: %w`, err) + } + return nil + } + + encoders := make([]io.WriteCloser, len(hashers)) + writers := make([]io.Writer, len(hashers)) + for i, h := range hashers { + encoders[i] = enc.NewEncoder(h) + writers[i] = encoders[i] + } + if _, err := io.Copy(io.MultiWriter(writers...), payload); err != nil { + for _, w := range encoders { + _ = w.Close() + } + return fmt.Errorf(`failed to stream payload through base64 encoder: %w`, err) + } + for i, w := range encoders { + if err := w.Close(); err != nil { + return fmt.Errorf(`failed to close base64 encoder for signature %d: %w`, i, err) + } + } + return nil +} + +type streamingDetachedJSONEntry struct { + Header json.RawMessage `json:"header,omitempty"` + Protected string `json:"protected"` + Signature string `json:"signature"` +} + +type streamingDetachedJSONGeneral struct { + Signatures []streamingDetachedJSONEntry `json:"signatures"` +} + +// assembleStreamingDetachedJSON emits the flattened form for a single +// signature and the general form for multiple. The "payload" member is +// omitted in both cases per RFC 7515 Appendix F. +func assembleStreamingDetachedJSON(signers []streamingSigner, rawSignatures [][]byte, enc base64.StreamEncoder, pretty bool) ([]byte, error) { + entries := make([]streamingDetachedJSONEntry, len(signers)) + for i, st := range signers { + e := streamingDetachedJSONEntry{ + Protected: st.hdrEncoded, + Signature: enc.EncodeToString(rawSignatures[i]), + } + if st.public != nil { + hdrjs, err := json.Marshal(st.public) + if err != nil { + return nil, makeSignError(prefixJwsSign, `failed to marshal unprotected header for signature %d: %w`, i, err) + } + e.Header = hdrjs + } + entries[i] = e + } + + var payload any + if len(entries) == 1 { + payload = entries[0] + } else { + payload = streamingDetachedJSONGeneral{Signatures: entries} + } + + if pretty { + out, err := json.MarshalIndent(payload, "", " ") + if err != nil { + return nil, makeSignError(prefixJwsSign, `failed to marshal JSON output: %w`, err) + } + return out, nil + } + out, err := json.Marshal(payload) + if err != nil { + return nil, makeSignError(prefixJwsSign, `failed to marshal JSON output: %w`, err) + } + return out, nil +} + +// cloneOrNewHeaders returns a defensive copy of hdr, or a fresh empty +// Headers if hdr is nil. The streaming path mutates the protected headers +// to set "alg" / "kid", so we never mutate a caller-supplied value. +func cloneOrNewHeaders(hdr Headers) (Headers, error) { + if hdr == nil { + return NewHeaders(), nil + } + return hdr.Clone() +} + +func convertStreamingSignKey(key any, family dsig.Family) (any, error) { + if _, ok := key.(jwk.Key); !ok { + return key, nil + } + switch family { + case dsig.HMAC: + var rawKey []byte + if err := keyconv.ByteSliceKey(&rawKey, key); err != nil { + return nil, fmt.Errorf(`failed to convert HMAC key: %w`, err) + } + return rawKey, nil + case dsig.RSA: + var rawKey rsa.PrivateKey + if err := keyconv.RSAPrivateKey(&rawKey, key); err != nil { + return nil, fmt.Errorf(`failed to convert RSA key: %w`, err) + } + return &rawKey, nil + case dsig.ECDSA: + var rawKey ecdsa.PrivateKey + if err := keyconv.ECDSAPrivateKey(&rawKey, key); err != nil { + return nil, fmt.Errorf(`failed to convert ECDSA key: %w`, err) + } + return &rawKey, nil + default: + return key, nil + } +} + +func convertStreamingVerifyKey(key any, family dsig.Family) (any, error) { + if _, ok := key.(jwk.Key); !ok { + return key, nil + } + switch family { + case dsig.HMAC: + var rawKey []byte + if err := keyconv.ByteSliceKey(&rawKey, key); err != nil { + return nil, fmt.Errorf(`failed to convert HMAC key: %w`, err) + } + return rawKey, nil + case dsig.RSA: + var rawKey rsa.PublicKey + if err := keyconv.RSAPublicKey(&rawKey, key); err != nil { + return nil, fmt.Errorf(`failed to convert RSA key: %w`, err) + } + return &rawKey, nil + case dsig.ECDSA: + var rawKey ecdsa.PublicKey + if err := keyconv.ECDSAPublicKey(&rawKey, key); err != nil { + return nil, fmt.Errorf(`failed to convert ECDSA key: %w`, err) + } + return &rawKey, nil + default: + return key, nil + } +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/verifier.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/verifier.go index 70b91c2938..c7838b4ab7 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/verifier.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/verifier.go @@ -17,10 +17,7 @@ func (v defaultVerifier) Algorithm() jwa.SignatureAlgorithm { } func (v defaultVerifier) Verify(key any, payload, signature []byte) error { - if err := jwsbb.Verify(key, v.alg.String(), payload, signature); err != nil { - return verifyError{verificationError{err}} - } - return nil + return jwsbb.Verify(key, v.alg.String(), payload, signature) } type Verifier2 interface { @@ -35,10 +32,7 @@ type verifierAdapter struct { } func (v verifierAdapter) Verify(key any, payload, signature []byte) error { - if err := v.v.Verify(payload, signature, key); err != nil { - return verifyError{verificationError{err}} - } - return nil + return v.v.Verify(payload, signature, key) } // VerifierFor returns a Verifier2 for the given signature algorithm. diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/verify_context.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/verify_context.go index b4807d569c..f9c2421f34 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jws/verify_context.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/verify_context.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "io" + "slices" "strings" "github.com/lestrrat-go/blackmagic" @@ -16,13 +18,16 @@ import ( // verifyContext holds the state during JWS verification type verifyContext struct { - parseOptions []ParseOption - dst *Message - detachedPayload []byte - keyProviders []KeyProvider - keyUsed any - validateKey bool - encoder Base64Encoder + parseOptions []ParseOption + dst *Message + detachedPayload []byte + payloadReader io.Reader + keyProviders []KeyProvider + keyUsed any + validateKey bool + critValidation bool + criticalExtensions []string + encoder Base64Encoder //nolint:containedctx ctx context.Context } @@ -40,9 +45,12 @@ func freeVerifyContext(vc *verifyContext) *verifyContext { vc.parseOptions = vc.parseOptions[:0] vc.dst = nil vc.detachedPayload = nil + vc.payloadReader = nil vc.keyProviders = vc.keyProviders[:0] vc.keyUsed = nil vc.validateKey = false + vc.critValidation = false + vc.criticalExtensions = vc.criticalExtensions[:0] vc.encoder = base64.DefaultEncoder() vc.ctx = context.Background() return vc @@ -54,67 +62,119 @@ func (vc *verifyContext) ProcessOptions(options []VerifyOption) error { switch option.Ident() { case identMessage{}: if err := option.Value(&vc.dst); err != nil { - return verifyerr(`invalid value for option WithMessage: %w`, err) + return makeVerifyError(`invalid value for option WithMessage: %w`, err) } case identDetachedPayload{}: + if vc.payloadReader != nil { + return makeVerifyError(`jws.WithDetachedPayload() and jws.WithDetachedPayloadReader() are mutually exclusive`) + } if err := option.Value(&vc.detachedPayload); err != nil { - return verifyerr(`invalid value for option WithDetachedPayload: %w`, err) + return makeVerifyError(`invalid value for option WithDetachedPayload: %w`, err) + } + // RFC 7797 "b64" auto-declaration. Detached-payload + // verification is the canonical use case for b64=false, + // and the jws package implements b64=false handling + // natively, so requiring callers to also pass + // jws.WithCritExtension("b64") is busywork. We declare + // it implicitly here so application code stays focused + // on its own crit extensions. This does not relax any + // other validateCritical check — the b64 header still + // has to appear in the protected header, the crit list + // still has to be non-empty / no duplicates / no + // standard names, etc. Only the "is in the caller's + // allowlist" check is short-circuited for "b64", and + // only when WithDetachedPayload was passed. + vc.criticalExtensions = append(vc.criticalExtensions, "b64") + case identDetachedPayloadReader{}: + if vc.detachedPayload != nil { + return makeVerifyError(`jws.WithDetachedPayload() and jws.WithDetachedPayloadReader() are mutually exclusive`) } + if err := option.Value(&vc.payloadReader); err != nil { + return makeVerifyError(`invalid value for option WithDetachedPayloadReader: %w`, err) + } + // Same RFC 7797 "b64" auto-declaration as for + // identDetachedPayload; streaming is the other canonical + // use case for b64=false. + vc.criticalExtensions = append(vc.criticalExtensions, "b64") case identKey{}: var pair *withKey if err := option.Value(&pair); err != nil { - return verifyerr(`invalid value for option WithKey: %w`, err) + return makeVerifyError(`invalid value for option WithKey: %w`, err) + } + + alg, ok := pair.alg.(jwa.SignatureAlgorithm) + if !ok { + return makeVerifyError(`expected algorithm to be of type jwa.SignatureAlgorithm but got (%[1]q, %[1]T)`, pair.alg) + } + + if err := validateAlgorithmForKey(alg, pair.key); err != nil { + return makeVerifyError(`%w`, err) } + vc.keyProviders = append(vc.keyProviders, &staticKeyProvider{ - alg: pair.alg.(jwa.SignatureAlgorithm), + alg: alg, key: pair.key, }) case identKeyProvider{}: var kp KeyProvider if err := option.Value(&kp); err != nil { - return verifyerr(`failed to retrieve key-provider option value: %w`, err) + return makeVerifyError(`failed to retrieve key-provider option value: %w`, err) } vc.keyProviders = append(vc.keyProviders, kp) case identKeyUsed{}: if err := option.Value(&vc.keyUsed); err != nil { - return verifyerr(`failed to retrieve key-used option value: %w`, err) + return makeVerifyError(`failed to retrieve key-used option value: %w`, err) } case identContext{}: if err := option.Value(&vc.ctx); err != nil { - return verifyerr(`failed to retrieve context option value: %w`, err) + return makeVerifyError(`failed to retrieve context option value: %w`, err) } case identValidateKey{}: if err := option.Value(&vc.validateKey); err != nil { - return verifyerr(`failed to retrieve validate-key option value: %w`, err) + return makeVerifyError(`failed to retrieve validate-key option value: %w`, err) + } + case identCritValidation{}: + if err := option.Value(&vc.critValidation); err != nil { + return makeVerifyError(`failed to retrieve crit-validation option value: %w`, err) } + case identCritExtension{}: + var names []string + if err := option.Value(&names); err != nil { + return makeVerifyError(`failed to retrieve crit-extension option value: %w`, err) + } + vc.criticalExtensions = append(vc.criticalExtensions, names...) case identSerialization{}: vc.parseOptions = append(vc.parseOptions, option.(ParseOption)) case identBase64Encoder{}: if err := option.Value(&vc.encoder); err != nil { - return verifyerr(`failed to retrieve base64-encoder option value: %w`, err) + return makeVerifyError(`failed to retrieve base64-encoder option value: %w`, err) } default: - return verifyerr(`invalid jws.VerifyOption %q passed`, `With`+strings.TrimPrefix(fmt.Sprintf(`%T`, option.Ident()), `jws.ident`)) + return makeVerifyError(`invalid jws.VerifyOption %q passed`, `With`+strings.TrimPrefix(fmt.Sprintf(`%T`, option.Ident()), `jws.ident`)) } } if len(vc.keyProviders) < 1 { - return verifyerr(`no key providers have been provided (see jws.WithKey(), jws.WithKeySet(), jws.WithVerifyAuto(), and jws.WithKeyProvider()`) + return makeVerifyError(`no key providers have been provided (see jws.WithKey(), jws.WithKeySet(), jws.WithVerifyAuto(), and jws.WithKeyProvider()`) } return nil } func (vc *verifyContext) VerifyMessage(buf []byte) ([]byte, error) { + if vc.payloadReader != nil { + return vc.verifyStreaming(buf) + } + msg, err := Parse(buf, vc.parseOptions...) if err != nil { - return nil, verifyerr(`failed to parse jws: %w`, err) + return nil, makeVerifyError(`failed to parse jws: %w`, err) } defer msg.clearRaw() if vc.detachedPayload != nil { if len(msg.payload) != 0 { - return nil, verifyerr(`can't specify detached payload for JWS with payload`) + return nil, makeVerifyError(`can't specify detached payload for JWS with payload`) } msg.payload = vc.detachedPayload @@ -136,6 +196,15 @@ func (vc *verifyContext) VerifyMessage(buf []byte) ([]byte, error) { pool.ErrorSlice().Put(errs) }() for idx, sig := range msg.signatures { + // Honor caller's deadline between signatures. Without this + // check, a hostile JWS with many signatures keeps the loop + // running long after the deadline; only kp.FetchKeys had + // visibility into vc.ctx, and not every key provider observes + // it. Cheap (~1ns) on the success path. + if err := vc.ctx.Err(); err != nil { + return nil, makeVerifyError(`%w`, err) + } + var rawHeaders []byte if rbp, ok := sig.protected.(interface{ rawBuffer() []byte }); ok { if raw := rbp.rawBuffer(); raw != nil { @@ -146,44 +215,81 @@ func (vc *verifyContext) VerifyMessage(buf []byte) ([]byte, error) { if rawHeaders == nil { protected, err := json.Marshal(sig.protected) if err != nil { - return nil, verifyerr(`failed to marshal "protected" for signature #%d: %w`, idx+1, err) + return nil, makeVerifyError(`failed to marshal "protected" for signature #%d: %w`, idx+1, err) } rawHeaders = protected } + if vc.critValidation { + if err := validateB64InCritIfFalse(sig.protected); err != nil { + errs = append(errs, makeVerifyError(`signature #%d: %w`, idx+1, err)) + continue + } + if err := validateCritical(sig.protected, vc.criticalExtensions); err != nil { + errs = append(errs, makeVerifyError(`signature #%d has invalid "crit" header: %w`, idx+1, err)) + continue + } + } + verifyBuf = verifyBuf[:0] verifyBuf = jwsbb.SignBuffer(verifyBuf, rawHeaders, msg.payload, vc.encoder, msg.b64) + keysAttempted := 0 for i, kp := range vc.keyProviders { + // Honor caller's deadline between key providers. + if err := vc.ctx.Err(); err != nil { + return nil, makeVerifyError(`%w`, err) + } + var sink algKeySink if err := kp.FetchKeys(vc.ctx, &sink, sig, msg); err != nil { - return nil, verifyerr(`key provider %d failed: %w`, i, err) + errs = append(errs, makeVerifyError(`signature #%d: key provider %d failed: %w`, idx+1, i, err)) + continue } for _, pair := range sink.list { - // alg is converted here because pair.alg is of type jwa.KeyAlgorithm. - // this may seem ugly, but we're trying to avoid declaring separate - // structs for `alg jwa.KeyEncryptionAlgorithm` and `alg jwa.SignatureAlgorithm` - //nolint:forcetypeassert - alg := pair.alg.(jwa.SignatureAlgorithm) + // Honor caller's deadline between (alg,key) pairs. + // Under WithRequireKid(false) + WithInferAlgorithmFromKey(true) + // + a large JWKS, this inner loop is the dominant + // cost — checking ctx between attempts caps the + // post-deadline crypto work at one operation. + if err := vc.ctx.Err(); err != nil { + return nil, makeVerifyError(`%w`, err) + } + + alg := pair.alg key := pair.key + keysAttempted++ if err := vc.tryKey(verifyBuf, alg, key, msg, sig); err != nil { - errs = append(errs, verifyerr(`failed to verify signature #%d with key %T: %w`, idx+1, key, err)) + errs = append(errs, makeVerifyError(`failed to verify signature #%d with key %T: %w`, idx+1, key, err)) continue } return msg.payload, nil } } - errs = append(errs, verifyerr(`signature #%d could not be verified with any of the keys`, idx+1)) + if keysAttempted == 0 { + errs = append(errs, makeVerifyError(`signature #%d: no matching keys were provided by any key provider`, idx+1)) + } else if looseOpts := vc.namedLooseKeySetOptions(); len(looseOpts) > 0 && keysAttempted > 1 { + // When a loose keySet config widened the candidate set, name + // the option(s) so the operator can see why a single Verify + // call paid N× the cost — the un-attributed message gets + // mis-diagnosed by adding more keys instead of fixing the + // JWS or tightening the config. + errs = append(errs, makeVerifyError( + `signature #%d: tried %d (alg,key) pair(s) but none verified successfully; %s widened the candidate set`, + idx+1, keysAttempted, strings.Join(looseOpts, " and "))) + } else { + errs = append(errs, makeVerifyError(`signature #%d: tried %d key(s) but none verified successfully`, idx+1, keysAttempted)) + } } - return nil, verifyerr(`could not verify message using any of the signatures or keys: %w`, errors.Join(errs...)) + return nil, makeVerifyError(`could not verify message using any of the signatures or keys: %w`, errors.Join(errs...)) } func (vc *verifyContext) tryKey(verifyBuf []byte, alg jwa.SignatureAlgorithm, key any, msg *Message, sig *Signature) error { if vc.validateKey { if err := validateKeyBeforeUse(key); err != nil { - return fmt.Errorf(`failed to validate key before signing: %w`, err) + return fmt.Errorf(`failed to validate key before verification: %w`, err) } } @@ -209,3 +315,130 @@ func (vc *verifyContext) tryKey(verifyBuf []byte, alg jwa.SignatureAlgorithm, ke return nil } + +// validateB64InCritIfFalse enforces RFC 7797 §3: producers that set +// b64=false in the protected header MUST also list "b64" in the protected +// header's "crit" array. The check runs alongside (and before) +// validateCritical so a non-conformant b64=false JWS is rejected up front +// regardless of whether the caller has supplied a crit allowlist via +// jws.WithCritExtension. Without this check, jws.Verify silently honors +// b64=false on the wire and computes its signing input differently from a +// strictly conformant verifier — exactly the cross-implementation +// disagreement RFC 7797 §6 was designed to prevent. VerifyCompactFast +// rejects any b64-bearing message outright via jws.ErrB64Present(); this +// helper is the slow-path mirror that targets only the non-conformant +// shape rather than blanket-refusing b64=false. +func validateB64InCritIfFalse(protected Headers) error { + if getB64Value(protected) { + return nil + } + if !protected.Has(CriticalKey) { + return makeVerifyError(`protected header has "b64":false but no "crit"; RFC 7797 §3 requires producers that set "b64":false to list "b64" in "crit"`) + } + crit, _ := protected.Critical() + if !slices.Contains(crit, "b64") { + return makeVerifyError(`protected header has "b64":false but "crit" does not list "b64"; RFC 7797 §3 requires producers that set "b64":false to list "b64" in "crit"`) + } + return nil +} + +// validateCritical checks the "crit" header per RFC 7515 Section 4.1.11. +// It enforces: +// - the list is non-empty +// - no entry is the empty string +// - no entry duplicates another +// - no entry names a standard JOSE header parameter +// - every entry appears as a header parameter in the protected header +// - every entry is in the caller-supplied allowedExtensions allowlist +// +// The last check is the central RFC requirement: recipients MUST reject +// any "crit" extension they do not understand, and the only way the +// library knows which extensions the caller understands is via the +// allowlist (populated from jws.WithCritExtension()). +// +// As a convenience, the RFC 7797 "b64" extension is auto-declared into +// allowedExtensions whenever the caller passes jws.WithDetachedPayload +// — see the identDetachedPayload case in ProcessOptions. The auto- +// declaration only short-circuits the allowlist check; every other +// rule above still applies to the "b64" entry. +func validateCritical(protected Headers, allowedExtensions []string) error { + if !protected.Has(CriticalKey) { + return nil + } + + crit, _ := protected.Critical() + if len(crit) == 0 { + return makeVerifyError(`"crit" header must not be empty`) + } + + seen := make(map[string]struct{}, len(crit)) + for _, name := range crit { + if name == "" { + return makeVerifyError(`"crit" header must not contain an empty extension name`) + } + if _, dup := seen[name]; dup { + return makeVerifyError(`"crit" header must not contain duplicate extension %q`, name) + } + seen[name] = struct{}{} + + // RFC 7515 Section 4.1.11: "crit" MUST NOT include names defined + // by the JOSE Header specification itself. The "b64" parameter + // is RFC 7797, not RFC 7515 — listing it in "crit" is the + // canonical use of the field per RFC 7797 §3 — so exclude it + // from this check even though it is a typed field on stdHeaders. + if name != B64Key && slices.Contains(stdHeaderNames, name) { + return makeVerifyError(`"crit" header must not contain standard header parameter %q`, name) + } + + // The extension must be present in the protected header. + if !protected.Has(name) { + return makeVerifyError(`"crit" header references extension %q, but it is not present in the protected header`, name) + } + + // The recipient must have declared support for the extension. + if !slices.Contains(allowedExtensions, name) { + if name == B64Key { + // b64=false is the canonical RFC 7797 case. The + // auto-declare only fires for WithDetachedPayload / + // WithDetachedPayloadReader; in-band b64=false still + // requires the caller to opt in explicitly. + return makeVerifyError(`"crit" header references extension "b64", but the recipient has not declared support for it; pass jws.WithCritExtension("b64") to accept in-band b64=false (auto-declare only fires for jws.WithDetachedPayload / jws.WithDetachedPayloadReader)`) + } + return makeVerifyError(`"crit" header references extension %q, but the recipient has not declared support for it (use jws.WithCritExtension(%q))`, name, name) + } + } + + return nil +} + +// namedLooseKeySetOptions inspects the registered key providers and +// returns the human-readable names of the loose-config keySet options +// in effect for this verify call: jws.WithRequireKid(false) and/or +// jws.WithInferAlgorithmFromKey(true). These are the options whose +// presence widens the per-signature (alg,key) candidate set beyond +// the default "kid + alg pin" of one. The names are used in the final +// "could not verify" error so an operator sees which options produced +// the fan-out without grep'ing the source. +func (vc *verifyContext) namedLooseKeySetOptions() []string { + var requireKidFalse, inferAlgorithm bool + for _, kp := range vc.keyProviders { + ksp, ok := kp.(*keySetProvider) + if !ok { + continue + } + if !ksp.requireKid { + requireKidFalse = true + } + if ksp.inferAlgorithm { + inferAlgorithm = true + } + } + var names []string + if requireKidFalse { + names = append(names, "jws.WithRequireKid(false)") + } + if inferAlgorithm { + names = append(names, "jws.WithInferAlgorithmFromKey(true)") + } + return names +} diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/doc.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/doc.go new file mode 100644 index 0000000000..53c696762c --- /dev/null +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/doc.go @@ -0,0 +1,46 @@ +// Package jwt implements JSON Web Tokens as described in RFC 7519. +// +// # Parsing and Verification +// +// Parse verifies signed tokens by default. Pass WithKey, WithKeySet, +// WithKeyProvider, or WithVerifyAuto when the token is signed. A bare +// Parse call returns an error instead of silently accepting an unverified +// token. +// +// To intentionally skip verification, pass WithVerify(false). Use +// ParseInsecure only when the input is already trusted: it disables both +// signature verification and validation, and it rejects key-bearing options +// so that stray verification settings cannot silently be ignored. +// +// # Validation +// +// Parse validates registered time claims by default after decoding. In +// particular, exp, nbf, and iat are checked automatically. Validate can also +// be called directly on an existing Token. Pass WithValidate(false) to skip +// automatic validation for a particular parse. +// +// # Errors +// +// Error checks in v3 use opaque values exposed by helper functions. Use +// errors.Is with the exported helpers to check for a class of failure: +// +// err := jwt.Validate(token) +// if errors.Is(err, jwt.TokenExpiredError()) { /* ... */ } +// if errors.Is(err, jwt.InvalidIssuerError()) { /* ... */ } +// +// # Time Claims +// +// Numeric date claims are decoded into time.Time values. Validation uses +// exact timestamps by default; configure WithAcceptableSkew for clock skew +// and WithTruncation when you need truncated comparisons. +// +// # OpenID Connect and Nested Tokens +// +// Use package github.com/lestrrat-go/jwx/v3/jwt/openid when you want a Token +// implementation with typed OpenID Connect claim accessors. +// +// Parse handles compact JWS JWTs and raw JSON tokens. It does not decrypt +// JWE envelopes for you. To produce nested JWTs, use +// NewSerializer().Sign(...).Encrypt(...).Serialize(...), and decrypt any +// outer JWE before calling Parse. +package jwt diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/fastpath.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/fastpath.go index 43f7c966da..da2f8acdc2 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/fastpath.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/fastpath.go @@ -12,6 +12,36 @@ import ( "github.com/lestrrat-go/jwx/v3/jws/jwsbb" ) +// fastPathKidSafe reports whether kid can be concatenated into a +// hand-built JSON header literal without escaping. Any byte that would +// require a JSON escape (control bytes, `"`, `\`) or any non-ASCII +// byte disqualifies the fast path; such kids fall through to the +// regular jws.Sign path where encoding/json handles escaping. +func fastPathKidSafe(kid string) bool { + for i := range len(kid) { + c := kid[i] + if c < 0x20 || c >= 0x7f || c == '"' || c == '\\' { + return false + } + } + return true +} + +// fastPathAlgSafe reports whether alg can be concatenated into a +// hand-built JSON header literal without escaping. The byte set +// mirrors fastPathKidSafe: any control byte, `"`, `\`, or non-ASCII +// byte disqualifies the value. Unlike kid, an unsafe alg is rejected +// outright by jwt.Sign rather than silently falling through. +func fastPathAlgSafe(alg string) bool { + for i := range len(alg) { + c := alg[i] + if c < 0x20 || c >= 0x7f || c == '"' || c == '\\' { + return false + } + } + return true +} + // signFast reinvents the wheel a bit to avoid the overhead of // going through the entire jws.Sign() machinery. func signFast(t Token, alg jwa.SignatureAlgorithm, key any) ([]byte, error) { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/http.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/http.go index 691c5a0df4..9c02aca7fa 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/http.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/http.go @@ -1,6 +1,7 @@ package jwt import ( + "errors" "fmt" "net/http" "net/url" @@ -19,7 +20,7 @@ func ParseCookie(req *http.Request, name string, options ...ParseOption) (Token, switch option.Ident() { case identCookie{}: if err := option.Value(&dst); err != nil { - return nil, fmt.Errorf(`jws.ParseCookie: value to option WithCookie must be **http.Cookie: %w`, err) + return nil, fmt.Errorf(`jwt.ParseCookie: value to option WithCookie must be **http.Cookie: %w`, err) } } } @@ -30,7 +31,7 @@ func ParseCookie(req *http.Request, name string, options ...ParseOption) (Token, } tok, err := ParseString(cookie.Value, options...) if err != nil { - return nil, fmt.Errorf(`jws.ParseCookie: failed to parse token stored in cookie: %w`, err) + return nil, fmt.Errorf(`jwt.ParseCookie: failed to parse token stored in cookie: %w`, err) } if dst != nil { @@ -41,8 +42,10 @@ func ParseCookie(req *http.Request, name string, options ...ParseOption) (Token, // ParseHeader parses a JWT stored in a http.Header. // -// For the header "Authorization", it will strip the prefix "Bearer " and will -// treat the remaining value as a JWT. +// For the header "Authorization", it will strip the "Bearer" scheme per +// RFC 6750 §2.1 (case-insensitive scheme token; space or tab separator +// required) and treat the remainder as a JWT. If the value does not begin +// with a well-formed "Bearer ", the full value is parsed as-is. func ParseHeader(hdr http.Header, name string, options ...ParseOption) (Token, error) { key := http.CanonicalHeaderKey(name) v := strings.TrimSpace(hdr.Get(key)) @@ -51,9 +54,9 @@ func ParseHeader(hdr http.Header, name string, options ...ParseOption) (Token, e } if key == "Authorization" { - // Authorization header is an exception. We strip the "Bearer " from - // the prefix - v = strings.TrimSpace(strings.TrimPrefix(v, "Bearer")) + if len(v) >= 7 && strings.EqualFold(v[:6], "Bearer") && (v[6] == ' ' || v[6] == '\t') { + v = strings.TrimSpace(v[7:]) + } } tok, err := ParseString(v, options...) @@ -84,19 +87,26 @@ func ParseForm(values url.Values, name string, options ...ParseOption) (Token, e // are specified, you must explicitly re-enable searching for "Authorization" header // if you also want to search for it. // -// # searches for "Authorization" +// // searches for "Authorization" // jwt.ParseRequest(req) // -// # searches for "x-my-token" ONLY. +// // searches for "x-my-token" ONLY. // jwt.ParseRequest(req, jwt.WithHeaderKey("x-my-token")) // -// # searches for "Authorization" AND "x-my-token" +// // searches for "Authorization" AND "x-my-token" // jwt.ParseRequest(req, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey("x-my-token")) // // Cookies are searched using (http.Request).Cookie(). If you have multiple // cookies with the same name, and you want to search for a specific one that // (http.Request).Cookie() would not return, you will need to implement your // own logic to extract the cookie and use jwt.ParseString(). +// +// When (and only when) at least one WithFormKey() option is supplied, +// ParseRequest will call (*http.Request).ParseForm() to read form fields +// from the request body. Callers that accept untrusted requests should +// wrap req.Body with http.MaxBytesReader before calling ParseRequest so +// that an oversized body does not exhaust memory during form parsing. +// Without WithFormKey() the request body is left untouched. func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) { var hdrkeys []string var formkeys []string @@ -107,19 +117,19 @@ func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) { case identHeaderKey{}: var v string if err := option.Value(&v); err != nil { - return nil, fmt.Errorf(`jws.ParseRequest: value to option WithHeaderKey must be string: %w`, err) + return nil, fmt.Errorf(`jwt.ParseRequest: value to option WithHeaderKey must be string: %w`, err) } hdrkeys = append(hdrkeys, v) case identFormKey{}: var v string if err := option.Value(&v); err != nil { - return nil, fmt.Errorf(`jws.ParseRequest: value to option WithFormKey must be string: %w`, err) + return nil, fmt.Errorf(`jwt.ParseRequest: value to option WithFormKey must be string: %w`, err) } formkeys = append(formkeys, v) case identCookieKey{}: var v string if err := option.Value(&v); err != nil { - return nil, fmt.Errorf(`jws.ParseRequest: value to option WithCookieKey must be string: %w`, err) + return nil, fmt.Errorf(`jwt.ParseRequest: value to option WithCookieKey must be string: %w`, err) } cookiekeys = append(cookiekeys, v) default: @@ -165,7 +175,15 @@ func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) { return tok, nil } - if cl := req.ContentLength; cl > 0 { + // Only touch the request body when the caller actually asked us + // to look at form fields. Without this guard ParseRequest would + // call req.ParseForm() on every request — for form-encoded bodies + // that drains the body, leaving downstream handlers with an empty + // io.Reader; for other Content-Types it is still wasted work on + // the URL query. We DO NOT gate on ContentLength: chunked-transfer + // requests have ContentLength == -1, and RFC 6750 §2.2 allows + // form-borne bearer tokens including under chunked encoding. + if len(formkeys) > 0 { if err := req.ParseForm(); err != nil { return nil, fmt.Errorf(`failed to parse form: %w`, err) } @@ -241,21 +259,35 @@ func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) { lmhdrs := len(mhdrs) lmfrms := len(mfrms) lmcookies := len(mcookies) - var errors []any + // Render display text without fmt verbs. A dynamic fmt.Errorf format + // string would be brittle: caller-supplied keys flow through + // strconv.Quote, but strconv.Quote does not escape '%', so a key + // containing '%s' would otherwise turn into a format verb and mangle + // output. Write texts directly and propagate the underlying errors + // via errors.Join so errors.Is / errors.As still traverse them. + var errs []error if lmhdrs > 0 || lmfrms > 0 || lmcookies > 0 { b.WriteString(". Additionally, errors were encountered during attempts to verify using:") if lmhdrs > 0 { b.WriteString(" headers: (") count := 0 - for hdrkey, err := range mhdrs { + // Iterate ordered key slices so rendering is deterministic + // (map iteration would reorder per run). + for _, hdrkey := range hdrkeys { + err, ok := mhdrs[hdrkey] + if !ok { + continue + } if count > 0 { b.WriteString(", ") } b.WriteString("[header key: ") b.WriteString(strconv.Quote(hdrkey)) - b.WriteString(", error: %w]") - errors = append(errors, err) + b.WriteString(", error: ") + b.WriteString(err.Error()) + b.WriteByte(tokens.CloseSquareBracket) + errs = append(errs, err) count++ } b.WriteString(")") @@ -264,32 +296,49 @@ func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) { if lmcookies > 0 { count := 0 b.WriteString(" cookies: (") - for cookiekey, err := range mcookies { + for _, cookiekey := range cookiekeys { + err, ok := mcookies[cookiekey] + if !ok { + continue + } if count > 0 { b.WriteString(", ") } b.WriteString("[cookie key: ") b.WriteString(strconv.Quote(cookiekey)) - b.WriteString(", error: %w]") - errors = append(errors, err) + b.WriteString(", error: ") + b.WriteString(err.Error()) + b.WriteByte(tokens.CloseSquareBracket) + errs = append(errs, err) count++ } + b.WriteString(")") } if lmfrms > 0 { count := 0 b.WriteString(" forms: (") - for formkey, err := range mfrms { + for _, formkey := range formkeys { + err, ok := mfrms[formkey] + if !ok { + continue + } if count > 0 { b.WriteString(", ") } b.WriteString("[form key: ") b.WriteString(strconv.Quote(formkey)) - b.WriteString(", error: %w]") - errors = append(errors, err) + b.WriteString(", error: ") + b.WriteString(err.Error()) + b.WriteByte(tokens.CloseSquareBracket) + errs = append(errs, err) count++ } + b.WriteString(")") } } - return nil, fmt.Errorf(b.String(), errors...) + if len(errs) == 0 { + return nil, errors.New(b.String()) + } + return nil, fmt.Errorf("%s: %w", b.String(), errors.Join(errs...)) } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/errors/errors.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/errors/errors.go index 179763a50d..31bfcd1850 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/errors/errors.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/errors/errors.go @@ -173,11 +173,8 @@ type MissingRequiredClaimError struct { } func (err *MissingRequiredClaimError) Is(target error) bool { - err1, ok := target.(*MissingRequiredClaimError) - if !ok { - return false - } - return err1 == ErrMissingRequiredClaimDefault || err1.claim == err.claim + _, ok := target.(*MissingRequiredClaimError) + return ok } func MissingRequiredClaimErrorf(name string) error { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/types/date.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/types/date.go index 3d40a9ed97..959eaeeff1 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/types/date.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/types/date.go @@ -4,6 +4,7 @@ import ( "fmt" "strconv" "strings" + "sync/atomic" "time" "github.com/lestrrat-go/jwx/v3/internal/json" @@ -15,9 +16,9 @@ const ( MaxPrecision uint32 = 9 // nanosecond level ) -var Pedantic uint32 -var ParsePrecision = DefaultPrecision -var FormatPrecision = DefaultPrecision +var Pedantic atomic.Uint32 +var ParsePrecision atomic.Uint32 +var FormatPrecision atomic.Uint32 // NumericDate represents the date format used in the 'nbf' claim type NumericDate struct { @@ -57,7 +58,7 @@ func parseNumericString(x string) (time.Time, error) { // Only check for the escape hatch if it's the pedantic // flag is off - if Pedantic != 1 { + if Pedantic.Load() != 1 { // This is an escape hatch for non-conformant providers // that gives us RFC3339 instead of epoch time for _, r := range x { @@ -77,12 +78,13 @@ func parseNumericString(x string) (time.Time, error) { var fractional string whole := x + parsePrecision := ParsePrecision.Load() if i := strings.IndexRune(x, tokens.Period); i > 0 { - if ParsePrecision > 0 && len(x) > i+1 { + if parsePrecision > 0 && len(x) > i+1 { fractional = x[i+1:] // everything after the tokens.Period - if int(ParsePrecision) < len(fractional) { + if int(parsePrecision) < len(fractional) { // Remove insignificant digits - fractional = fractional[:int(ParsePrecision)] + fractional = fractional[:int(parsePrecision)] } // Replace missing fractional diits with zeros for len(fractional) < int(MaxPrecision) { @@ -146,7 +148,8 @@ func (n *NumericDate) Accept(v any) error { } func (n NumericDate) String() string { - if FormatPrecision == 0 { + formatPrecision := FormatPrecision.Load() + if formatPrecision == 0 { return strconv.FormatInt(n.Unix(), 10) } @@ -159,7 +162,7 @@ func (n NumericDate) String() string { } slwhole := len(s) - int(MaxPrecision) - s = s[:slwhole] + "." + s[slwhole:slwhole+int(FormatPrecision)] + s = s[:slwhole] + "." + s[slwhole:slwhole+int(formatPrecision)] if s[0] == tokens.Period { s = "0" + s } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/jwt.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/jwt.go index 99b5ef37a6..6229c763cd 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/jwt.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/jwt.go @@ -1,28 +1,34 @@ //go:generate ../tools/cmd/genjwt.sh //go:generate stringer -type=TokenOption -output=token_options_gen.go -// Package jwt implements JSON Web Tokens as described in https://tools.ietf.org/html/rfc7519 package jwt import ( "bytes" + "errors" "fmt" "io" + "sync" "sync/atomic" "time" "github.com/lestrrat-go/jwx/v3" "github.com/lestrrat-go/jwx/v3/internal/json" "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/lestrrat-go/jwx/v3/jws" jwterrs "github.com/lestrrat-go/jwx/v3/jwt/internal/errors" "github.com/lestrrat-go/jwx/v3/jwt/internal/types" ) +var muSettings sync.Mutex var defaultTruncation atomic.Int64 // Settings controls global settings that are specific to JWTs. func Settings(options ...GlobalOption) { + muSettings.Lock() + defer muSettings.Unlock() + var flattenAudience bool var parsePedantic bool var parsePrecision = types.MaxPrecision + 1 // illegal value, so we can detect nothing was set @@ -64,38 +70,29 @@ func Settings(options ...GlobalOption) { } if parsePrecision <= types.MaxPrecision { // remember we set default to max + 1 - v := atomic.LoadUint32(&types.ParsePrecision) - if v != parsePrecision { - atomic.CompareAndSwapUint32(&types.ParsePrecision, v, parsePrecision) - } + types.ParsePrecision.Store(parsePrecision) } if formatPrecision <= types.MaxPrecision { // remember we set default to max + 1 - v := atomic.LoadUint32(&types.FormatPrecision) - if v != formatPrecision { - atomic.CompareAndSwapUint32(&types.FormatPrecision, v, formatPrecision) - } + types.FormatPrecision.Store(formatPrecision) } { - v := atomic.LoadUint32(&types.Pedantic) - if (v == 1) != parsePedantic { - var newVal uint32 - if parsePedantic { - newVal = 1 - } - atomic.CompareAndSwapUint32(&types.Pedantic, v, newVal) + var newVal uint32 + if parsePedantic { + newVal = 1 } + types.Pedantic.Store(newVal) } { - defaultOptionsMu.Lock() + opts := TokenOptionSet(defaultOptions.Load()) if flattenAudience { - defaultOptions.Enable(FlattenAudience) + opts.Enable(FlattenAudience) } else { - defaultOptions.Disable(FlattenAudience) + opts.Disable(FlattenAudience) } - defaultOptionsMu.Unlock() + defaultOptions.Store(opts.Value()) } if truncation >= 0 { @@ -118,27 +115,33 @@ func ParseString(s string, options ...ParseOption) (Token, error) { // The token must be encoded in JWS compact format, or a raw JSON form of JWT // without any signatures. // -// If you need JWE support on top of JWS, you will need to rollout your -// own workaround. +// Signed input is verified by default. Pass `jwt.WithKey()`, +// `jwt.WithKeySet()`, `jwt.WithKeyProvider()`, or +// `jwt.WithVerifyAuto(fetcher, fetchOptions...)` when verification is +// required. A bare `jwt.Parse()` call returns an error; to intentionally +// skip verification, pass `jwt.WithVerify(false)` or use +// `jwt.ParseInsecure()`. // -// If the token is signed, and you want to verify the payload matches the signature, -// you must pass the jwt.WithKey(alg, key) or jwt.WithKeySet(jwk.Set) option. -// If you do not specify these parameters, no verification will be performed. +// `Parse()` also accepts `ValidateOption` values. Validation runs by default +// after parsing, so `jwt.WithValidate(true)` is only needed to override a +// prior `jwt.WithValidate(false)` in the same option set. Pass +// `jwt.WithValidate(false)` if you need to defer validation and call +// `Validate()` yourself later. +// +// To produce nested JWTs, use +// `jwt.NewSerializer().Sign(...).Encrypt(...).Serialize(...)`. `Parse()` does +// not decrypt JWE envelopes; decrypt the outer JWE before calling it. // // During verification, if the JWS headers specify a key ID (`kid`), the // key used for verification must match the specified ID. If you are somehow // using a key without a `kid` (which is highly unlikely if you are working -// with a JWT from a well-know provider), you can work around this by modifying -// the `jwk.Key` and setting the `kid` header. -// -// If you also want to assert the validity of the JWT itself (i.e. expiration -// and such), use the `Validate()` function on the returned token, or pass the -// `WithValidate(true)` option. Validate options can also be passed to -// `Parse` +// with a JWT from a well-known provider), you can work around this by +// modifying the `jwk.Key` and setting its `kid` field. // // This function takes both ParseOption and ValidateOption types: -// ParseOptions control the parsing behavior, and ValidateOptions are -// passed to `Validate()` when `jwt.WithValidate` is specified. +// ParseOptions control parsing and verification behavior, and +// ValidateOptions are passed to `Validate()` when automatic validation is +// enabled. func Parse(s []byte, options ...ParseOption) (Token, error) { tok, err := parseBytes(s, options...) if err != nil { @@ -150,14 +153,19 @@ func Parse(s []byte, options ...ParseOption) (Token, error) { // ParseInsecure is exactly the same as Parse(), but it disables // signature verification and token validation. // -// You cannot override `jwt.WithVerify()` or `jwt.WithValidate()` -// using this function. Providing these options would result in -// an error +// `jwt.WithVerify()` and `jwt.WithValidate()` may not be specified +// because they would conflict with the function's purpose. Likewise, +// the key-bearing options `jwt.WithKey()`, `jwt.WithKeySet()`, +// `jwt.WithKeyProvider()`, and `jwt.WithVerifyAuto()` are rejected so +// that typos like `jwt.ParseInsecure(data, jwt.WithKey(...))` cannot +// silently skip verification. Use `jwt.Parse` when a key is available. func ParseInsecure(s []byte, options ...ParseOption) (Token, error) { for _, option := range options { switch option.Ident() { case identVerify{}, identValidate{}: return nil, jwterrs.ParseErrorf(`jwt.ParseInsecure`, `jwt.WithVerify() and jwt.WithValidate() may not be specified`) + case identKey{}, identKeySet{}, identKeyProvider{}, identVerifyAuto{}: + return nil, jwterrs.ParseErrorf(`jwt.ParseInsecure`, `key-bearing options (jwt.WithKey, jwt.WithKeySet, jwt.WithKeyProvider, jwt.WithVerifyAuto) may not be specified; use jwt.Parse to verify with a key`) } } @@ -169,9 +177,12 @@ func ParseInsecure(s []byte, options ...ParseOption) (Token, error) { return tok, nil } -// ParseReader calls Parse against an io.Reader +// ParseReader calls Parse against an io.Reader. +// +// Bounding the input size is the caller's responsibility: wrap src with +// [io.LimitReader] or [net/http.MaxBytesReader] before passing it in. See +// docs/13-input-size.md for the rationale. func ParseReader(src io.Reader, options ...ParseOption) (Token, error) { - // We're going to need the raw bytes regardless. Read it. data, err := io.ReadAll(src) if err != nil { return nil, jwterrs.ParseErrorf(`jwt.ParseReader`, `failed to read from token data source: %w`, err) @@ -184,15 +195,16 @@ func ParseReader(src io.Reader, options ...ParseOption) (Token, error) { } type parseCtx struct { - token Token - validateOpts []ValidateOption - verifyOpts []jws.VerifyOption - localReg *json.Registry - pedantic bool - skipVerification bool - validate bool - withKeyCount int - withKey *withKey // this is used to detect if we have a WithKey option + token Token + validateOpts []ValidateOption + verifyOpts []jws.VerifyOption + localReg *json.Registry + strictStringClaims *bool // per-call override; nil = use global + pedantic bool + skipVerification bool + validate bool + withKeyCount int + withKey *withKey // this is used to detect if we have a WithKey option } func parseBytes(data []byte, options ...ParseOption) (Token, error) { @@ -262,6 +274,12 @@ func parseBytes(data []byte, options ...ParseOption) (Token, error) { ctx.localReg = json.NewRegistry() } ctx.localReg.Register(pair.Name, pair.Value) + case identStrictStringClaims{}: + var v bool + if err := o.Value(&v); err != nil { + return nil, fmt.Errorf("jwt.parseBytes: value for WithStrictStringClaims must be bool: %w", err) + } + ctx.strictStringClaims = &v } } @@ -306,16 +324,61 @@ func verifyJWS(ctx *parseCtx, payload []byte) ([]byte, int, error) { alg, ok := wk.alg.(jwa.SignatureAlgorithm) if ok && len(wk.options) == 0 { verified, err := jws.VerifyCompactFast(wk.key, payload, alg) - if err != nil { - return nil, _JwsVerifyDone, err + if err == nil { + return verified, peekJWSNestedState(ctx, payload), nil + } + // VerifyCompactFast refuses crit-bearing messages. In v3 + // jws.Verify defaults critValidation=false, so the generic + // fall-through path would still silently accept "crit". + // Force the strict path here: jwt.Parse must not be laxer + // than jws.Verify + WithCritValidation. + if errors.Is(err, jws.ErrCritPresent()) { + verifyOpts := append(ctx.verifyOpts, jws.WithCompact(), jws.WithCritValidation(true)) + verified, err := jws.Verify(payload, verifyOpts...) + if err != nil { + return nil, _JwsVerifyDone, err + } + return verified, peekJWSNestedState(ctx, payload), nil } - return verified, _JwsVerifyDone, nil + return nil, _JwsVerifyDone, err } } verifyOpts := append(ctx.verifyOpts, jws.WithCompact()) verified, err := jws.Verify(payload, verifyOpts...) - return verified, _JwsVerifyDone, err + if err != nil { + return nil, _JwsVerifyDone, err + } + return verified, peekJWSNestedState(ctx, payload), nil +} + +// peekJWSNestedState returns _JwsVerifyExpectNested when pedantic mode is on +// and the verified JWS protected header carries cty=JWT (RFC 7519 §5.2 — the +// payload is itself a Nested JWT; the outer envelope expects another signed/ +// encrypted layer wrapping the JWT, not a raw JWT). Otherwise returns +// _JwsVerifyDone. The signature has already been verified at this point, so +// re-parsing the protected header is safe — it operates on bytes the producer +// signed. +func peekJWSNestedState(ctx *parseCtx, payload []byte) int { + if !ctx.pedantic { + return _JwsVerifyDone + } + msg, err := jws.Parse(payload, jws.WithCompact()) + if err != nil || len(msg.Signatures()) == 0 { + return _JwsVerifyDone + } + hdr := msg.Signatures()[0].ProtectedHeaders() + if hdr == nil { + return _JwsVerifyDone + } + cty, ok := hdr.ContentType() + if !ok { + return _JwsVerifyDone + } + if cty == "JWT" { + return _JwsVerifyExpectNested + } + return _JwsVerifyDone } // verify parameter exists to make sure that we don't accidentally skip @@ -399,8 +462,11 @@ OUTER: } } - // No verification. - m, err := jws.Parse(data, jws.WithCompact()) + // No verification. Parse the LOOP-LOCAL `payload` (not the + // original `data`); for a 2-layer nested JWS, iter 2 must + // see the inner JWS bytes that iter 1 produced, not re- + // parse the outer envelope. + m, err := jws.Parse(payload, jws.WithCompact()) if err != nil { return nil, fmt.Errorf(`invalid jws message: %w`, err) } @@ -415,12 +481,18 @@ OUTER: ctx.token = New() } - if ctx.localReg != nil { + if ctx.localReg != nil || ctx.strictStringClaims != nil { dcToken, ok := ctx.token.(TokenWithDecodeCtx) if !ok { - return nil, fmt.Errorf(`typed claim was requested, but the token (%T) does not support DecodeCtx`, ctx.token) + return nil, fmt.Errorf(`typed claim or strict string claims was requested, but the token (%T) does not support DecodeCtx`, ctx.token) + } + + var strict bool + if ctx.strictStringClaims != nil { + strict = *ctx.strictStringClaims } - dc := json.NewDecodeCtx(ctx.localReg) + + dc := json.NewDecodeCtxStrictStrings(ctx.localReg, strict) dcToken.SetDecodeCtx(dc) defer func() { dcToken.SetDecodeCtx(nil) }() } @@ -473,10 +545,30 @@ func Sign(t Token, options ...SignOption) ([]byte, error) { return nil, fmt.Errorf(`jwt.Sign: invalid algorithm type %T. jwa.SignatureAlgorithm is required`, wk.alg) } + // Reject algorithm names that would require JSON escaping + // in the protected header. Unlike kid (which may be attacker- + // influenced and silently falls through to jws.Sign), an + // unsafe alg is almost certainly a caller bug or an injection + // attempt, so we fail fast rather than emit any signature. + if !fastPathAlgSafe(alg.String()) { + return nil, fmt.Errorf(`jwt.Sign: algorithm %q contains bytes that require JSON escaping`, alg.String()) + } + // Check if option contains anything other than alg/key if len(wk.options) == 0 { - // yay, we have something we can put in the FAST PATH! - return signFast(t, alg, wk.key) + // If the key carries a kid that would require JSON escaping, + // skip the fast path (which concatenates kid raw into the + // protected header) and fall through to jws.Sign. + fastSafe := true + if jwkKey, ok := wk.key.(jwk.Key); ok { + if v, ok := jwkKey.KeyID(); ok && !fastPathKidSafe(v) { + fastSafe = false + } + } + if fastSafe { + // yay, we have something we can put in the FAST PATH! + return signFast(t, alg, wk.key) + } } // fallthrough } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.go index 4a7cfd3e5d..1e263f2e00 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.go @@ -206,9 +206,6 @@ type withKeySet struct { // and you do not mind the verification process having to possibly // attempt using multiple times before succeeding to verify. See // `jws.InferAlgorithmFromKey` option -// -// If you have only one key in the set, and are sure you want to -// use that key, you can use the `jwt.WithDefaultKey` option. func WithKeySet(set jwk.Set, options ...any) ParseOption { return &parseOption{option.New(identKeySet{}, &withKeySet{ set: set, @@ -242,7 +239,10 @@ func WithAudience(s string) ValidateOption { return WithValidator(audienceClaimContainsString(s)) } -// WithClaimValue specifies the expected value for a given claim +// WithClaimValue specifies the expected value for a given claim. +// The stored and expected values are compared with reflect.DeepEqual, +// so slice-, map-, and struct-valued claims are supported in addition +// to scalars. See [ClaimValueIs] for edge-case semantics. func WithClaimValue(name string, v any) ValidateOption { return WithValidator(ClaimValueIs(name, v)) } diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.yaml b/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.yaml index bfcadfac25..fe03bd653e 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.yaml +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.yaml @@ -48,6 +48,14 @@ interfaces: - name: ReadFileOption comment: | ReadFileOption is a type of `Option` that can be passed to `jws.ReadFile` + - name: GlobalParseOption + methods: + - globalOption + - parseOption + - readFileOption + comment: | + GlobalParseOption describes an Option that can be passed to `jwt.Settings()`, + `jwt.Parse()`, and `jwt.ReadFile()`. - name: GlobalValidateOption methods: - globalOption @@ -125,6 +133,18 @@ options: See the documentation for `jwt.TokenOptionSet`, `(jwt.Token).Options`, and `jwt.FlattenAudience` for more details + - ident: StrictStringClaims + interface: ParseOption + argument_type: bool + comment: | + WithStrictStringClaims controls whether JSON null values for string + claims (such as "iss", "sub", "jti") cause a parse error. By default, + null is silently accepted as an empty string (matching Go's standard + JSON decoding behavior). When set to true, null values are rejected + per the RFC type definitions (e.g. StringOrURI). + + This option only affects JWT claims. JWK, JWE, and JWS fields are + not subject to this check and will always accept null as an empty string. - ident: FormKey interface: ParseOption argument_type: string @@ -271,4 +291,4 @@ options: argument_type: jws.Base64Encoder comment: | WithBase64Encoder specifies the base64 encoder to use for signing - tokens and verifying JWS signatures. \ No newline at end of file + tokens and verifying JWS signatures. diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/options_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/options_gen.go index 3a644a6e4c..7ee6dc3427 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/options_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/options_gen.go @@ -39,6 +39,25 @@ type globalOption struct { func (*globalOption) globalOption() {} +// GlobalParseOption describes an Option that can be passed to `jwt.Settings()`, +// `jwt.Parse()`, and `jwt.ReadFile()`. +type GlobalParseOption interface { + Option + globalOption() + parseOption() + readFileOption() +} + +type globalParseOption struct { + Option +} + +func (*globalParseOption) globalOption() {} + +func (*globalParseOption) parseOption() {} + +func (*globalParseOption) readFileOption() {} + // GlobalValidateOption describes an Option that can be passed to `jwt.Settings()` and `jwt.Validate()` type GlobalValidateOption interface { Option @@ -181,6 +200,7 @@ type identNumericDateParsePrecision struct{} type identPedantic struct{} type identResetValidators struct{} type identSignOption struct{} +type identStrictStringClaims struct{} type identToken struct{} type identTruncation struct{} type identValidate struct{} @@ -259,6 +279,10 @@ func (identSignOption) String() string { return "WithSignOption" } +func (identStrictStringClaims) String() string { + return "WithStrictStringClaims" +} + func (identToken) String() string { return "WithToken" } @@ -432,6 +456,18 @@ func WithSignOption(v jws.SignOption) SignOption { return &signOption{option.New(identSignOption{}, v)} } +// WithStrictStringClaims controls whether JSON null values for string +// claims (such as "iss", "sub", "jti") cause a parse error. By default, +// null is silently accepted as an empty string (matching Go's standard +// JSON decoding behavior). When set to true, null values are rejected +// per the RFC type definitions (e.g. StringOrURI). +// +// This option only affects JWT claims. JWK, JWE, and JWS fields are +// not subject to this check and will always accept null as an empty string. +func WithStrictStringClaims(v bool) ParseOption { + return &parseOption{option.New(identStrictStringClaims{}, v)} +} + // WithToken specifies the token instance in which the resulting JWT is stored // when parsing JWT tokens func WithToken(v Token) ParseOption { diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_gen.go index 2361ff5621..7a057742a9 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_gen.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_gen.go @@ -101,7 +101,7 @@ type Token interface { Keys() []string } type stdToken struct { - mu *sync.RWMutex + mu sync.RWMutex dc DecodeCtx // per-object context for decoding options TokenOptionSet // per-object option audience types.StringList // https://tools.ietf.org/html/rfc7519#section-4.1.3 @@ -119,13 +119,14 @@ type stdToken struct { // Convenience accessors are provided for these standard claims func New() Token { return &stdToken{ - mu: &sync.RWMutex{}, privateClaims: make(map[string]any), options: DefaultOptionSet(), } } func (t *stdToken) Options() *TokenOptionSet { + t.mu.RLock() + defer t.mu.RUnlock() return &t.options } @@ -440,11 +441,11 @@ LOOP: } t.issuedAt = &decoded case IssuerKey: - if err := json.AssignNextStringToken(&t.issuer, dec); err != nil { + if err := json.AssignNextStringToken(&t.issuer, dec, t.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, IssuerKey, err) } case JwtIDKey: - if err := json.AssignNextStringToken(&t.jwtID, dec); err != nil { + if err := json.AssignNextStringToken(&t.jwtID, dec, t.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, JwtIDKey, err) } case NotBeforeKey: @@ -454,7 +455,7 @@ LOOP: } t.notBefore = &decoded case SubjectKey: - if err := json.AssignNextStringToken(&t.subject, dec); err != nil { + if err := json.AssignNextStringToken(&t.subject, dec, t.dc); err != nil { return fmt.Errorf(`failed to decode value for key %s: %w`, SubjectKey, err) } default: @@ -548,6 +549,8 @@ func putClaimPairList(list []claimPair) { func (t *stdToken) makePairs() ([]claimPair, error) { pairs := getClaimPairList() + t.mu.RLock() + defer t.mu.RUnlock() if t.audience != nil { buf, err := json.MarshalAudience(t.audience, t.options.IsEnabled(FlattenAudience)) if err != nil { @@ -612,7 +615,7 @@ func (t *stdToken) makePairs() ([]claimPair, error) { return pairs, nil } -func (t stdToken) MarshalJSON() ([]byte, error) { +func (t *stdToken) MarshalJSON() ([]byte, error) { buf := pool.BytesBuffer().Get() defer pool.BytesBuffer().Put(buf) pairs, err := t.makePairs() @@ -625,7 +628,10 @@ func (t stdToken) MarshalJSON() ([]byte, error) { if i > 0 { buf.WriteByte(tokens.Comma) } - fmt.Fprintf(buf, "%q: %s", pair.Name, pair.Value) + buf.WriteByte('"') + buf.WriteString(pair.Name) + buf.WriteString(`": `) + buf.Write(pair.Value.([]byte)) } buf.WriteByte(tokens.CloseCurlyBracket) ret := make([]byte, buf.Len()) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_options.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_options.go index 088c4263be..ef9799e6d4 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_options.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_options.go @@ -1,12 +1,11 @@ package jwt -import "sync" +import "sync/atomic" // TokenOptionSet is a bit flag containing per-token options. type TokenOptionSet uint64 -var defaultOptions TokenOptionSet -var defaultOptionsMu sync.RWMutex +var defaultOptions atomic.Uint64 // TokenOption describes a single token option that can be set on // the per-token option set (TokenOptionSet) @@ -45,7 +44,7 @@ func (o TokenOptionSet) Value() uint64 { // option set. This may differ depending on if/when functions that // change the global state has been called, such as `jwt.Settings` func DefaultOptionSet() TokenOptionSet { - return TokenOptionSet(defaultOptions.Value()) + return TokenOptionSet(defaultOptions.Load()) } // Clear sets all bits to zero, effectively disabling all options diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/validate.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/validate.go index af46868d8b..f398c83bfe 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/validate.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/validate.go @@ -3,6 +3,7 @@ package jwt import ( "context" "fmt" + "reflect" "slices" "strconv" "time" @@ -24,7 +25,7 @@ func isSupportedTimeClaim(c string) error { case ExpirationKey, IssuedAtKey, NotBeforeKey: return nil } - return fmt.Errorf(`unsupported time claim %s`, strconv.Quote(c)) + return jwterrs.ValidateErrorf(`unsupported time claim %s in jwt.WithMaxDelta/jwt.WithMinDelta`, strconv.Quote(c)) } func timeClaim(t Token, clock Clock, c string) time.Time { @@ -51,6 +52,10 @@ func timeClaim(t Token, clock Clock, c string) time.Time { // See the various `WithXXX` functions for optional parameters // that can control the behavior of this method. func Validate(t Token, options ...ValidateOption) error { + if t == nil { + return jwterrs.ValidateErrorf(`jwt.Validate: token is nil`) + } + ctx := context.Background() trunc := getDefaultTruncation() @@ -73,6 +78,9 @@ func Validate(t Token, options ...ValidateOption) error { if err := o.Value(&skew); err != nil { return fmt.Errorf(`jwt.Validate: value for WithAcceptableSkew() option must be time.Duration: %w`, err) } + if skew < 0 { + return fmt.Errorf(`jwt.Validate: WithAcceptableSkew() must not be negative`) + } case identTruncation{}: if err := o.Value(&trunc); err != nil { return fmt.Errorf(`jwt.Validate: value for WithTruncation() option must be time.Duration: %w`, err) @@ -162,10 +170,21 @@ func MinDeltaIs(c1, c2 string, dur time.Duration) Validator { func (iitr *isInTimeRange) Validate(ctx context.Context, t Token) error { clock := ValidationCtxClock(ctx) // MUST be populated skew := ValidationCtxSkew(ctx) // MUST be populated - // We don't check if the claims already exist, because we already did that - // by piggybacking on `required` check. t1 := timeClaim(t, clock, iitr.c1) t2 := timeClaim(t, clock, iitr.c2) + // Defensive: reject zero-value claims before computing delta. The + // auto-IsRequired piggyback in WithValidator type-switches on the + // concrete *isInTimeRange — wrapping this validator (e.g., in + // ValidatorFunc) skips that piggyback, and a missing time claim + // would silently produce a hugely-negative delta that trivially + // satisfies the upper-bound check. Reject the missing claim + // regardless of how the validator was wrapped. + if t1.IsZero() { + return jwterrs.MissingRequiredClaimErrorf(iitr.c1) + } + if t2.IsZero() { + return jwterrs.MissingRequiredClaimErrorf(iitr.c2) + } if iitr.less { // t1 - t2 <= iitr.dur // t1 - t2 < iitr.dur + skew if t1.Sub(t2) > iitr.dur+skew { @@ -210,21 +229,36 @@ func SetValidationCtxSkew(ctx context.Context, dur time.Duration) context.Contex } // ValidationCtxClock returns the Clock object associated with -// the current validation context. This value will always be available -// during validation of tokens. +// the current validation context. When called from within a Validator +// invoked by [Validate], the value is always populated. If the context +// was not initialized by [Validate] (for example, a custom validator +// was invoked with a bare context), a default clock backed by +// [time.Now] is returned instead of panicking. func ValidationCtxClock(ctx context.Context) Clock { - //nolint:forcetypeassert - return ctx.Value(identValidationCtxClock{}).(Clock) + if cl, ok := ctx.Value(identValidationCtxClock{}).(Clock); ok { + return cl + } + return ClockFunc(time.Now) } +// ValidationCtxSkew returns the clock skew associated with the current +// validation context. If the context was not initialized by [Validate], +// zero is returned instead of panicking. func ValidationCtxSkew(ctx context.Context) time.Duration { - //nolint:forcetypeassert - return ctx.Value(identValidationCtxSkew{}).(time.Duration) + if dur, ok := ctx.Value(identValidationCtxSkew{}).(time.Duration); ok { + return dur + } + return 0 } +// ValidationCtxTruncation returns the truncation granularity associated +// with the current validation context. If the context was not initialized +// by [Validate], zero is returned instead of panicking. func ValidationCtxTruncation(ctx context.Context) time.Duration { - //nolint:forcetypeassert - return ctx.Value(identValidationCtxTruncation{}).(time.Duration) + if dur, ok := ctx.Value(identValidationCtxTruncation{}).(time.Duration); ok { + return dur + } + return 0 } // IsExpirationValid is one of the default validators that will be executed. @@ -368,9 +402,11 @@ type claimValueIs struct { } // ClaimValueIs creates a Validator that checks if the value of claim `name` -// matches `value`. The comparison is done using a simple `==` comparison, -// and therefore complex comparisons may fail using this code. If you -// need to do more, use a custom Validator. +// matches `value`. The comparison is done with reflect.DeepEqual, so +// slice-, map-, and struct-valued claims are supported in addition to +// scalars. Function-valued claims follow reflect.DeepEqual semantics +// (equal only when both sides are nil). If you need finer-grained +// matching than DeepEqual provides, use a custom Validator. func ClaimValueIs(name string, value any) Validator { return &claimValueIs{ name: name, @@ -384,7 +420,7 @@ func (cv *claimValueIs) Validate(_ context.Context, t Token) error { if err := t.Get(cv.name, &v); err != nil { return cv.makeErr(`claim %[1]q does not exist or is not a []string: %[2]w`, cv.name, err) } - if v != cv.value { + if !reflect.DeepEqual(v, cv.value) { return cv.makeErr(`claim %[1]q does not have the expected value`, cv.name) } return nil diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwx.go b/vendor/github.com/lestrrat-go/jwx/v3/jwx.go index fc394e5137..441518fd56 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/jwx.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/jwx.go @@ -28,6 +28,12 @@ import ( // DecoderSettings gives you a access to configure the "encoding/json".Decoder // used to decode JSON objects within the jwx framework. +// +// All options accepted here have process-global effect and are intended +// to be applied exactly once at program startup, before any goroutine +// begins parsing JWx payloads. See the godoc on individual JSONOption +// constructors (e.g. [WithUseNumber]) for the concurrency contract of +// each setting. func DecoderSettings(options ...JSONOption) { // XXX We're using this format instead of just passing a single boolean // in case a new option is to be added some time later diff --git a/vendor/github.com/lestrrat-go/jwx/v3/options.go b/vendor/github.com/lestrrat-go/jwx/v3/options.go index b642a199d8..838a1f93e7 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/options.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/options.go @@ -24,6 +24,15 @@ func newJSONOption(n any, v any) JSONOption { // WithUseNumber controls whether the jwx package should unmarshal // JSON objects with the "encoding/json".Decoder.UseNumber feature on. // +// This setting has process-global effect and must be applied once +// at program startup (typically from func init() or early in main()) +// before any goroutine begins parsing JWx payloads. The underlying +// flag is read atomically, so toggling it at runtime is race-free, +// but any in-flight or subsequent decoders will observe a mix of +// float64 and json.Number values in concurrently-decoded custom +// fields — callers that type-assert on those values will break +// non-deterministically. There is no per-call override. +// // Default is false. func WithUseNumber(b bool) JSONOption { return newJSONOption(identUseNumber{}, b) diff --git a/vendor/github.com/lestrrat-go/jwx/v3/transform/map.go b/vendor/github.com/lestrrat-go/jwx/v3/transform/map.go index 4eb80cb99f..392d22241b 100644 --- a/vendor/github.com/lestrrat-go/jwx/v3/transform/map.go +++ b/vendor/github.com/lestrrat-go/jwx/v3/transform/map.go @@ -23,6 +23,11 @@ type Mappable interface { // Many objects in jwe, jwk, jws, and jwt packages including // `jwt.Token`, `jwk.Key`, `jws.Header`, etc. // +// The values stored in `dst` are the exact values returned by `m.Get()`. +// Mutable values such as slices, maps, and pointers may therefore be live +// aliases to the original object's backing storage. AsMap is not a deep-copy +// or snapshot helper. +// // EXPERIMENTAL: This API is experimental and its interface and behavior is // subject to change in future releases. This API is not subject to semver // compatibility guarantees. diff --git a/vendor/github.com/open-policy-agent/opa/capabilities/v1.15.1.json b/vendor/github.com/open-policy-agent/opa/capabilities/v1.15.1.json index 9eb82c2968..30cef5a5d1 100644 --- a/vendor/github.com/open-policy-agent/opa/capabilities/v1.15.1.json +++ b/vendor/github.com/open-policy-agent/opa/capabilities/v1.15.1.json @@ -4913,4 +4913,4 @@ "rego_v1", "template_strings" ] -} +} \ No newline at end of file diff --git a/vendor/github.com/open-policy-agent/opa/capabilities/v1.15.2.json b/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.0.json similarity index 99% rename from vendor/github.com/open-policy-agent/opa/capabilities/v1.15.2.json rename to vendor/github.com/open-policy-agent/opa/capabilities/v1.16.0.json index 9eb82c2968..b47b68d612 100644 --- a/vendor/github.com/open-policy-agent/opa/capabilities/v1.15.2.json +++ b/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.0.json @@ -4700,6 +4700,42 @@ "type": "function" } }, + { + "name": "uri.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "uri.parse", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object" + }, + "type": "function" + } + }, { "name": "urlquery.decode", "decl": { diff --git a/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.1.json b/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.1.json new file mode 100644 index 0000000000..b47b68d612 --- /dev/null +++ b/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.1.json @@ -0,0 +1,4952 @@ +{ + "builtins": [ + { + "name": "abs", + "decl": { + "args": [ + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "all", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "and", + "decl": { + "args": [ + { + "of": { + "type": "any" + }, + "type": "set" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + }, + "infix": "\u0026" + }, + { + "name": "any", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "array.concat", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "array.flatten", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "array.reverse", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "array.slice", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "assign", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": ":=" + }, + { + "name": "base64.decode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "base64.encode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "base64.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "base64url.decode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "base64url.encode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "base64url.encode_no_pad", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "bits.and", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "bits.lsh", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "bits.negate", + "decl": { + "args": [ + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "bits.or", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "bits.rsh", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "bits.xor", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "cast_array", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "cast_boolean", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "cast_null", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "null" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "cast_object", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "cast_set", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "cast_string", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "ceil", + "decl": { + "args": [ + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "concat", + "decl": { + "args": [ + { + "type": "string" + }, + { + "of": [ + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "contains", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "count", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.equal", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.md5", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.sha1", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.sha256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.sha512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.md5", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.parse_private_keys", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "crypto.sha1", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.sha256", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_and_verify_certificates", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_and_verify_certificates_with_options", + "decl": { + "args": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_certificate_request", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_certificates", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_keypair", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_rsa_private_key", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "div", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + }, + "infix": "/" + }, + { + "name": "endswith", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "eq", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "=" + }, + { + "name": "equal", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "==" + }, + { + "name": "floor", + "decl": { + "args": [ + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "format_int", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "glob.match", + "decl": { + "args": [ + { + "type": "string" + }, + { + "of": [ + { + "type": "null" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + } + ], + "type": "any" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "glob.quote_meta", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "graph.reachable", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "graph.reachable_paths", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "of": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "graphql.is_valid", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "graphql.parse", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "graphql.parse_and_verify", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "graphql.parse_query", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "graphql.parse_schema", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "graphql.schema_is_valid", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "gt", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "\u003e" + }, + { + "name": "gte", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "\u003e=" + }, + { + "name": "hex.decode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "hex.encode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "http.send", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "indexof", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "indexof_n", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "dynamic": { + "type": "number" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "internal.member_2", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "in" + }, + { + "name": "internal.member_3", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "in" + }, + { + "name": "internal.print", + "decl": { + "args": [ + { + "dynamic": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "array" + } + ], + "type": "function" + } + }, + { + "name": "internal.template_string", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "internal.test_case", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "function" + } + }, + { + "name": "intersection", + "decl": { + "args": [ + { + "of": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "set" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "io.jwt.decode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "static": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "type": "string" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "io.jwt.decode_verify", + "decl": { + "args": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "array" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "io.jwt.encode_sign", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "type": "string" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "io.jwt.encode_sign_raw", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "io.jwt.verify_eddsa", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_es256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_es384", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_es512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_hs256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_hs384", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_hs512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_ps256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_ps384", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_ps512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_rs256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_rs384", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_rs512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_array", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_boolean", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_null", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_number", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_object", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_set", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_string", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "json.filter", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "array" + }, + { + "of": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "json.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "json.marshal", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "json.marshal_with_options", + "decl": { + "args": [ + { + "type": "any" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "static": [ + { + "key": "indent", + "value": { + "type": "string" + } + }, + { + "key": "prefix", + "value": { + "type": "string" + } + }, + { + "key": "pretty", + "value": { + "type": "boolean" + } + } + ], + "type": "object" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "json.match_schema", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "static": [ + { + "key": "desc", + "value": { + "type": "string" + } + }, + { + "key": "error", + "value": { + "type": "string" + } + }, + { + "key": "field", + "value": { + "type": "string" + } + }, + { + "key": "type", + "value": { + "type": "string" + } + } + ], + "type": "object" + }, + "type": "array" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "json.patch", + "decl": { + "args": [ + { + "type": "any" + }, + { + "dynamic": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "static": [ + { + "key": "op", + "value": { + "type": "string" + } + }, + { + "key": "path", + "value": { + "type": "any" + } + } + ], + "type": "object" + }, + "type": "array" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "json.remove", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "array" + }, + { + "of": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "json.unmarshal", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "json.verify_schema", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "of": [ + { + "type": "null" + }, + { + "type": "string" + } + ], + "type": "any" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "lower", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "lt", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "\u003c" + }, + { + "name": "lte", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "\u003c=" + }, + { + "name": "max", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "min", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "minus", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "number" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "of": [ + { + "type": "number" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + }, + "type": "function" + }, + "infix": "-" + }, + { + "name": "mul", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + }, + "infix": "*" + }, + { + "name": "neq", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "!=" + }, + { + "name": "net.cidr_contains", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "net.cidr_contains_matches", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "of": { + "static": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "type": "array" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "net.cidr_expand", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "of": { + "type": "string" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "net.cidr_intersects", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "net.cidr_is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "net.cidr_merge", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "of": [ + { + "type": "string" + } + ], + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "of": { + "type": "string" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "net.cidr_overlap", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "net.lookup_ip_addr", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "of": { + "type": "string" + }, + "type": "set" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "numbers.range", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "type": "number" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "numbers.range_step", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "type": "number" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "object.filter", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "object.get", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "object.keys", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "object.remove", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "object.subset", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + }, + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "object.union", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "object.union_n", + "decl": { + "args": [ + { + "dynamic": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "opa.runtime", + "decl": { + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "or", + "decl": { + "args": [ + { + "of": { + "type": "any" + }, + "type": "set" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + }, + "infix": "|" + }, + { + "name": "plus", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + }, + "infix": "+" + }, + { + "name": "print", + "decl": { + "type": "function", + "variadic": { + "type": "any" + } + } + }, + { + "name": "product", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "number" + }, + "type": "array" + }, + { + "of": { + "type": "number" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "providers.aws.sign_req", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "rand.intn", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "re_match", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "regex.find_all_string_submatch_n", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "regex.find_n", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "regex.globs_match", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "regex.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "regex.match", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "regex.replace", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "regex.split", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "regex.template_match", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "rego.metadata.chain", + "decl": { + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "rego.metadata.rule", + "decl": { + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "rego.parse_module", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "rem", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + }, + "infix": "%" + }, + { + "name": "replace", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "round", + "decl": { + "args": [ + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "semver.compare", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "semver.is_valid", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "set_diff", + "decl": { + "args": [ + { + "of": { + "type": "any" + }, + "type": "set" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "sort", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "split", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "sprintf", + "decl": { + "args": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "startswith", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "strings.any_prefix_match", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "strings.any_suffix_match", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "strings.count", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "strings.render_template", + "decl": { + "args": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "strings.replace_n", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "strings.reverse", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "substring", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "sum", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "number" + }, + "type": "array" + }, + { + "of": { + "type": "number" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "time.add_date", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "time.clock", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "time.date", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "time.diff", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "time.format", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "time.now_ns", + "decl": { + "result": { + "type": "number" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "time.parse_duration_ns", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "time.parse_ns", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "time.parse_rfc3339_ns", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "time.weekday", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "to_number", + "decl": { + "args": [ + { + "of": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "any" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "trace", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "trim", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "trim_left", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "trim_prefix", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "trim_right", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "trim_space", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "trim_suffix", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "type_name", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "union", + "decl": { + "args": [ + { + "of": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "set" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "units.parse", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "units.parse_bytes", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "upper", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "uri.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "uri.parse", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "urlquery.decode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "urlquery.decode_object", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "dynamic": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "urlquery.encode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "urlquery.encode_object", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "uuid.parse", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "uuid.rfc4122", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "walk", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "static": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "type": "any" + } + ], + "type": "array" + }, + "type": "function" + }, + "relation": true + }, + { + "name": "yaml.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "yaml.marshal", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "yaml.unmarshal", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + } + ], + "wasm_abi_versions": [ + { + "version": 1, + "minor_version": 1 + }, + { + "version": 1, + "minor_version": 2 + } + ], + "features": [ + "keywords_in_refs", + "rego_v1", + "template_strings" + ] +} diff --git a/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.2.json b/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.2.json new file mode 100644 index 0000000000..b47b68d612 --- /dev/null +++ b/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.2.json @@ -0,0 +1,4952 @@ +{ + "builtins": [ + { + "name": "abs", + "decl": { + "args": [ + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "all", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "and", + "decl": { + "args": [ + { + "of": { + "type": "any" + }, + "type": "set" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + }, + "infix": "\u0026" + }, + { + "name": "any", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "array.concat", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "array.flatten", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "array.reverse", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "array.slice", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "assign", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": ":=" + }, + { + "name": "base64.decode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "base64.encode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "base64.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "base64url.decode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "base64url.encode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "base64url.encode_no_pad", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "bits.and", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "bits.lsh", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "bits.negate", + "decl": { + "args": [ + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "bits.or", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "bits.rsh", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "bits.xor", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "cast_array", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "cast_boolean", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "cast_null", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "null" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "cast_object", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "cast_set", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "cast_string", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "ceil", + "decl": { + "args": [ + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "concat", + "decl": { + "args": [ + { + "type": "string" + }, + { + "of": [ + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "contains", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "count", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.equal", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.md5", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.sha1", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.sha256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.sha512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.md5", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.parse_private_keys", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "crypto.sha1", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.sha256", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_and_verify_certificates", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_and_verify_certificates_with_options", + "decl": { + "args": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_certificate_request", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_certificates", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_keypair", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_rsa_private_key", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "div", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + }, + "infix": "/" + }, + { + "name": "endswith", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "eq", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "=" + }, + { + "name": "equal", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "==" + }, + { + "name": "floor", + "decl": { + "args": [ + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "format_int", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "glob.match", + "decl": { + "args": [ + { + "type": "string" + }, + { + "of": [ + { + "type": "null" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + } + ], + "type": "any" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "glob.quote_meta", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "graph.reachable", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "graph.reachable_paths", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "of": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "graphql.is_valid", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "graphql.parse", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "graphql.parse_and_verify", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "graphql.parse_query", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "graphql.parse_schema", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "graphql.schema_is_valid", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "gt", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "\u003e" + }, + { + "name": "gte", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "\u003e=" + }, + { + "name": "hex.decode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "hex.encode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "http.send", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "indexof", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "indexof_n", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "dynamic": { + "type": "number" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "internal.member_2", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "in" + }, + { + "name": "internal.member_3", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "in" + }, + { + "name": "internal.print", + "decl": { + "args": [ + { + "dynamic": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "array" + } + ], + "type": "function" + } + }, + { + "name": "internal.template_string", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "internal.test_case", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "function" + } + }, + { + "name": "intersection", + "decl": { + "args": [ + { + "of": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "set" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "io.jwt.decode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "static": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "type": "string" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "io.jwt.decode_verify", + "decl": { + "args": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "array" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "io.jwt.encode_sign", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "type": "string" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "io.jwt.encode_sign_raw", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "io.jwt.verify_eddsa", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_es256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_es384", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_es512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_hs256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_hs384", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_hs512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_ps256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_ps384", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_ps512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_rs256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_rs384", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_rs512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_array", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_boolean", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_null", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_number", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_object", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_set", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_string", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "json.filter", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "array" + }, + { + "of": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "json.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "json.marshal", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "json.marshal_with_options", + "decl": { + "args": [ + { + "type": "any" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "static": [ + { + "key": "indent", + "value": { + "type": "string" + } + }, + { + "key": "prefix", + "value": { + "type": "string" + } + }, + { + "key": "pretty", + "value": { + "type": "boolean" + } + } + ], + "type": "object" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "json.match_schema", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "static": [ + { + "key": "desc", + "value": { + "type": "string" + } + }, + { + "key": "error", + "value": { + "type": "string" + } + }, + { + "key": "field", + "value": { + "type": "string" + } + }, + { + "key": "type", + "value": { + "type": "string" + } + } + ], + "type": "object" + }, + "type": "array" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "json.patch", + "decl": { + "args": [ + { + "type": "any" + }, + { + "dynamic": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "static": [ + { + "key": "op", + "value": { + "type": "string" + } + }, + { + "key": "path", + "value": { + "type": "any" + } + } + ], + "type": "object" + }, + "type": "array" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "json.remove", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "array" + }, + { + "of": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "json.unmarshal", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "json.verify_schema", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "of": [ + { + "type": "null" + }, + { + "type": "string" + } + ], + "type": "any" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "lower", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "lt", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "\u003c" + }, + { + "name": "lte", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "\u003c=" + }, + { + "name": "max", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "min", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "minus", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "number" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "of": [ + { + "type": "number" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + }, + "type": "function" + }, + "infix": "-" + }, + { + "name": "mul", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + }, + "infix": "*" + }, + { + "name": "neq", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "!=" + }, + { + "name": "net.cidr_contains", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "net.cidr_contains_matches", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "of": { + "static": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "type": "array" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "net.cidr_expand", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "of": { + "type": "string" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "net.cidr_intersects", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "net.cidr_is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "net.cidr_merge", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "of": [ + { + "type": "string" + } + ], + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "of": { + "type": "string" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "net.cidr_overlap", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "net.lookup_ip_addr", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "of": { + "type": "string" + }, + "type": "set" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "numbers.range", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "type": "number" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "numbers.range_step", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "type": "number" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "object.filter", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "object.get", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "object.keys", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "object.remove", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "object.subset", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + }, + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "object.union", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "object.union_n", + "decl": { + "args": [ + { + "dynamic": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "opa.runtime", + "decl": { + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "or", + "decl": { + "args": [ + { + "of": { + "type": "any" + }, + "type": "set" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + }, + "infix": "|" + }, + { + "name": "plus", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + }, + "infix": "+" + }, + { + "name": "print", + "decl": { + "type": "function", + "variadic": { + "type": "any" + } + } + }, + { + "name": "product", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "number" + }, + "type": "array" + }, + { + "of": { + "type": "number" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "providers.aws.sign_req", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "rand.intn", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "re_match", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "regex.find_all_string_submatch_n", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "regex.find_n", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "regex.globs_match", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "regex.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "regex.match", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "regex.replace", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "regex.split", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "regex.template_match", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "rego.metadata.chain", + "decl": { + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "rego.metadata.rule", + "decl": { + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "rego.parse_module", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "rem", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + }, + "infix": "%" + }, + { + "name": "replace", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "round", + "decl": { + "args": [ + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "semver.compare", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "semver.is_valid", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "set_diff", + "decl": { + "args": [ + { + "of": { + "type": "any" + }, + "type": "set" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "sort", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "split", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "sprintf", + "decl": { + "args": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "startswith", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "strings.any_prefix_match", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "strings.any_suffix_match", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "strings.count", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "strings.render_template", + "decl": { + "args": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "strings.replace_n", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "strings.reverse", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "substring", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "sum", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "number" + }, + "type": "array" + }, + { + "of": { + "type": "number" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "time.add_date", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "time.clock", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "time.date", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "time.diff", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "time.format", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "time.now_ns", + "decl": { + "result": { + "type": "number" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "time.parse_duration_ns", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "time.parse_ns", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "time.parse_rfc3339_ns", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "time.weekday", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "to_number", + "decl": { + "args": [ + { + "of": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "any" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "trace", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "trim", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "trim_left", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "trim_prefix", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "trim_right", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "trim_space", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "trim_suffix", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "type_name", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "union", + "decl": { + "args": [ + { + "of": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "set" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "units.parse", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "units.parse_bytes", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "upper", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "uri.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "uri.parse", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "urlquery.decode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "urlquery.decode_object", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "dynamic": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "urlquery.encode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "urlquery.encode_object", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "uuid.parse", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "uuid.rfc4122", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "walk", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "static": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "type": "any" + } + ], + "type": "array" + }, + "type": "function" + }, + "relation": true + }, + { + "name": "yaml.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "yaml.marshal", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "yaml.unmarshal", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + } + ], + "wasm_abi_versions": [ + { + "version": 1, + "minor_version": 1 + }, + { + "version": 1, + "minor_version": 2 + } + ], + "features": [ + "keywords_in_refs", + "rego_v1", + "template_strings" + ] +} diff --git a/vendor/github.com/open-policy-agent/opa/capabilities/v1.17.0.json b/vendor/github.com/open-policy-agent/opa/capabilities/v1.17.0.json new file mode 100644 index 0000000000..07b0eb9507 --- /dev/null +++ b/vendor/github.com/open-policy-agent/opa/capabilities/v1.17.0.json @@ -0,0 +1,4955 @@ +{ + "builtins": [ + { + "name": "abs", + "decl": { + "args": [ + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "all", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "and", + "decl": { + "args": [ + { + "of": { + "type": "any" + }, + "type": "set" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + }, + "infix": "\u0026" + }, + { + "name": "any", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "array.concat", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "array.flatten", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "array.reverse", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "array.slice", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "assign", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": ":=" + }, + { + "name": "base64.decode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "base64.encode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "base64.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "base64url.decode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "base64url.encode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "base64url.encode_no_pad", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "bits.and", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "bits.lsh", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "bits.negate", + "decl": { + "args": [ + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "bits.or", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "bits.rsh", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "bits.xor", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "cast_array", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "cast_boolean", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "cast_null", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "null" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "cast_object", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "cast_set", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "cast_string", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "ceil", + "decl": { + "args": [ + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "concat", + "decl": { + "args": [ + { + "type": "string" + }, + { + "of": [ + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "contains", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "count", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.equal", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.md5", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.sha1", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.sha256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.hmac.sha512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.md5", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.parse_private_keys", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "crypto.sha1", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.sha256", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_and_verify_certificates", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_and_verify_certificates_with_options", + "decl": { + "args": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_certificate_request", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_certificates", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_keypair", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "crypto.x509.parse_rsa_private_key", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "div", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + }, + "infix": "/" + }, + { + "name": "endswith", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "eq", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "=" + }, + { + "name": "equal", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "==" + }, + { + "name": "floor", + "decl": { + "args": [ + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "format_int", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "glob.match", + "decl": { + "args": [ + { + "type": "string" + }, + { + "of": [ + { + "type": "null" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + } + ], + "type": "any" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "glob.quote_meta", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "graph.reachable", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "graph.reachable_paths", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "of": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "graphql.is_valid", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "graphql.parse", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "graphql.parse_and_verify", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "graphql.parse_query", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "graphql.parse_schema", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "graphql.schema_is_valid", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "gt", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "\u003e" + }, + { + "name": "gte", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "\u003e=" + }, + { + "name": "hex.decode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "hex.encode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "http.send", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "indexof", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "indexof_n", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "dynamic": { + "type": "number" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "internal.member_2", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "in" + }, + { + "name": "internal.member_3", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "in" + }, + { + "name": "internal.print", + "decl": { + "args": [ + { + "dynamic": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "array" + } + ], + "type": "function" + } + }, + { + "name": "internal.template_string", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "internal.test_case", + "decl": { + "args": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "function" + } + }, + { + "name": "intersection", + "decl": { + "args": [ + { + "of": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "set" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "io.jwt.decode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "static": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "type": "string" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "io.jwt.decode_verify", + "decl": { + "args": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "array" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "io.jwt.encode_sign", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "type": "string" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "io.jwt.encode_sign_raw", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "io.jwt.verify_eddsa", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_es256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_es384", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_es512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_hs256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_hs384", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_hs512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_ps256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_ps384", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_ps512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_rs256", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_rs384", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "io.jwt.verify_rs512", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_array", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_boolean", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_null", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_number", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_object", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_set", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "is_string", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "json.filter", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "array" + }, + { + "of": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "json.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "json.marshal", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "json.marshal_with_options", + "decl": { + "args": [ + { + "type": "any" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "static": [ + { + "key": "indent", + "value": { + "type": "string" + } + }, + { + "key": "prefix", + "value": { + "type": "string" + } + }, + { + "key": "pretty", + "value": { + "type": "boolean" + } + } + ], + "type": "object" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "json.match_schema", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "dynamic": { + "static": [ + { + "key": "desc", + "value": { + "type": "string" + } + }, + { + "key": "error", + "value": { + "type": "string" + } + }, + { + "key": "field", + "value": { + "type": "string" + } + }, + { + "key": "type", + "value": { + "type": "string" + } + } + ], + "type": "object" + }, + "type": "array" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "json.patch", + "decl": { + "args": [ + { + "type": "any" + }, + { + "dynamic": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "static": [ + { + "key": "op", + "value": { + "type": "string" + } + }, + { + "key": "path", + "value": { + "type": "any" + } + } + ], + "type": "object" + }, + "type": "array" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "json.remove", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "array" + }, + { + "of": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "json.unmarshal", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "json.verify_schema", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "boolean" + }, + { + "of": [ + { + "type": "null" + }, + { + "type": "string" + } + ], + "type": "any" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "lower", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "lt", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "\u003c" + }, + { + "name": "lte", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "\u003c=" + }, + { + "name": "max", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "min", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "minus", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "number" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "of": [ + { + "type": "number" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + }, + "type": "function" + }, + "infix": "-" + }, + { + "name": "mul", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + }, + "infix": "*" + }, + { + "name": "neq", + "decl": { + "args": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "infix": "!=" + }, + { + "name": "net.cidr_contains", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "net.cidr_contains_matches", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "of": { + "static": [ + { + "type": "any" + }, + { + "type": "any" + } + ], + "type": "array" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "net.cidr_expand", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "of": { + "type": "string" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "net.cidr_intersects", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "net.cidr_is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "net.cidr_merge", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "of": [ + { + "type": "string" + } + ], + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "of": { + "type": "string" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "net.cidr_overlap", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "net.lookup_ip_addr", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "of": { + "type": "string" + }, + "type": "set" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "numbers.range", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "type": "number" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "numbers.range_step", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "type": "number" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "object.filter", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "object.get", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "type": "any" + }, + { + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "object.keys", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "object.remove", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "object.subset", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + }, + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "object.union", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "object.union_n", + "decl": { + "args": [ + { + "dynamic": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "opa.runtime", + "decl": { + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "or", + "decl": { + "args": [ + { + "of": { + "type": "any" + }, + "type": "set" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + }, + "infix": "|" + }, + { + "name": "plus", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + }, + "infix": "+" + }, + { + "name": "print", + "decl": { + "type": "function", + "variadic": { + "type": "any" + } + } + }, + { + "name": "product", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "number" + }, + "type": "array" + }, + { + "of": { + "type": "number" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "providers.aws.sign_req", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "key": { + "type": "any" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "rand.intn", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "re_match", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "regex.find_all_string_submatch_n", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "regex.find_n", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "number" + } + ], + "result": { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "regex.globs_match", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "regex.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "regex.match", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "regex.replace", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "regex.split", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "regex.template_match", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "rego.metadata.chain", + "decl": { + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "rego.metadata.rule", + "decl": { + "result": { + "type": "any" + }, + "type": "function" + } + }, + { + "name": "rego.parse_module", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "rem", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + }, + "infix": "%" + }, + { + "name": "replace", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "round", + "decl": { + "args": [ + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "semver.compare", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "semver.is_valid", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "set_diff", + "decl": { + "args": [ + { + "of": { + "type": "any" + }, + "type": "set" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + }, + "deprecated": true + }, + { + "name": "sort", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "of": { + "type": "any" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "split", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + "type": "function" + } + }, + { + "name": "sprintf", + "decl": { + "args": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "any" + }, + "type": "array" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "startswith", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "strings.any_prefix_match", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "strings.any_suffix_match", + "decl": { + "args": [ + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "strings.count", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "strings.render_template", + "decl": { + "args": [ + { + "type": "string" + }, + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "strings.replace_n", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "strings.reverse", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "substring", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "sum", + "decl": { + "args": [ + { + "of": [ + { + "dynamic": { + "type": "number" + }, + "type": "array" + }, + { + "of": { + "type": "number" + }, + "type": "set" + } + ], + "type": "any" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "time.add_date", + "decl": { + "args": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "time.clock", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "time.date", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "time.diff", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + }, + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + } + ], + "result": { + "static": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "type": "array" + }, + "type": "function" + } + }, + { + "name": "time.format", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "time.now_ns", + "decl": { + "result": { + "type": "number" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "time.parse_duration_ns", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "time.parse_ns", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "time.parse_rfc3339_ns", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "time.weekday", + "decl": { + "args": [ + { + "of": [ + { + "type": "number" + }, + { + "static": [ + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "array" + } + ], + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "to_number", + "decl": { + "args": [ + { + "of": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + } + ], + "type": "any" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "trace", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "trim", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "trim_left", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "trim_prefix", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "trim_right", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "trim_space", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "trim_suffix", + "decl": { + "args": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "type_name", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "union", + "decl": { + "args": [ + { + "of": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "set" + } + ], + "result": { + "of": { + "type": "any" + }, + "type": "set" + }, + "type": "function" + } + }, + { + "name": "units.parse", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "units.parse_bytes", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "number" + }, + "type": "function" + } + }, + { + "name": "upper", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "uri.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "uri.parse", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "urlquery.decode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "urlquery.decode_object", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "dynamic": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "urlquery.encode", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "urlquery.encode_object", + "decl": { + "args": [ + { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "of": [ + { + "type": "string" + }, + { + "dynamic": { + "type": "string" + }, + "type": "array" + }, + { + "of": { + "type": "string" + }, + "type": "set" + } + ], + "type": "any" + } + }, + "type": "object" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "uuid.parse", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, + { + "name": "uuid.rfc4122", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "string" + }, + "type": "function" + }, + "nondeterministic": true + }, + { + "name": "walk", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "static": [ + { + "dynamic": { + "type": "any" + }, + "type": "array" + }, + { + "type": "any" + } + ], + "type": "array" + }, + "type": "function" + }, + "relation": true + }, + { + "name": "yaml.is_valid", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "boolean" + }, + "type": "function" + } + }, + { + "name": "yaml.marshal", + "decl": { + "args": [ + { + "type": "any" + } + ], + "result": { + "type": "string" + }, + "type": "function" + } + }, + { + "name": "yaml.unmarshal", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "type": "any" + }, + "type": "function" + } + } + ], + "future_keywords": [ + "not" + ], + "wasm_abi_versions": [ + { + "version": 1, + "minor_version": 1 + }, + { + "version": 1, + "minor_version": 2 + } + ], + "features": [ + "keywords_in_refs", + "rego_v1", + "template_strings" + ] +} diff --git a/vendor/github.com/open-policy-agent/opa/internal/bundle/utils.go b/vendor/github.com/open-policy-agent/opa/internal/bundle/utils.go index 98093b774e..0ba5c33269 100644 --- a/vendor/github.com/open-policy-agent/opa/internal/bundle/utils.go +++ b/vendor/github.com/open-policy-agent/opa/internal/bundle/utils.go @@ -78,7 +78,7 @@ func LoadWasmResolversFromStore(ctx context.Context, store storage.Store, txn st } for _, wmf := range resolversToLoad { - resolver, err := wasm.New(wmf.Entrypoints, wmf.Raw, data) + resolver, err := wasm.NewWithContext(ctx, wmf.Entrypoints, wmf.Raw, data) if err != nil { return nil, fmt.Errorf("failed to initialize wasm module for entrypoints '%s': %s", wmf.Entrypoints, err) } diff --git a/vendor/github.com/open-policy-agent/opa/internal/cidr/merge/merge.go b/vendor/github.com/open-policy-agent/opa/internal/cidr/merge/merge.go index c2392b6775..85695b1292 100644 --- a/vendor/github.com/open-policy-agent/opa/internal/cidr/merge/merge.go +++ b/vendor/github.com/open-policy-agent/opa/internal/cidr/merge/merge.go @@ -293,7 +293,7 @@ func partitionCIDR(targetCIDR net.IPNet, excludeCIDR net.IPNet) ([]*net.IPNet, [ // package expects, as big package doesn't append leading zeroes. if iUpperBytesLen != net.IPv6len { numZeroesToAppend := net.IPv6len - iUpperBytesLen - zeroBytes := make([]byte, numZeroesToAppend) + zeroBytes := make([]byte, numZeroesToAppend, numZeroesToAppend+len(iUpper.Bytes())) iUpperBytes = append(zeroBytes, iUpper.Bytes()...) } else { iUpperBytes = iUpper.Bytes() @@ -303,7 +303,7 @@ func partitionCIDR(targetCIDR net.IPNet, excludeCIDR net.IPNet) ([]*net.IPNet, [ iLowerBytesLen := len(iLower.Bytes()) if iLowerBytesLen != net.IPv6len { numZeroesToAppend := net.IPv6len - iLowerBytesLen - zeroBytes := make([]byte, numZeroesToAppend) + zeroBytes := make([]byte, numZeroesToAppend, numZeroesToAppend+len(iLower.Bytes())) iLowerBytes = append(zeroBytes, iLower.Bytes()...) } else { iLowerBytes = iLower.Bytes() diff --git a/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schema.go b/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schema.go index e8007ee2b6..f23416e0b5 100644 --- a/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schema.go +++ b/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schema.go @@ -56,6 +56,7 @@ type Schema struct { RootSchema *SubSchema Pool *schemaPool ReferencePool *schemaReferencePool + validatePatterns bool } func (d *Schema) parse(document any, draft Draft) error { @@ -516,12 +517,23 @@ func (d *Schema) parseSchema(documentNode any, currentSchema *SubSchema) error { } } - // NOTE: Regex compilation step removed as we don't use "pattern" attribute for - // type checking, and this would cause schemas to fail if they included patterns - // that were valid ECMA regex dialect but not known to Go (i.e. the regexp.Compile - // function), such as patterns with negative lookahead - if _, err := getString(m, KeyPattern); err != nil { + // The "pattern" keyword is only compiled into a regex when + // validatePatterns is set. It is off by default because many real-world + // schemas use ECMA-262 regex features (such as negative lookahead) that + // Go's RE2-based regexp package cannot compile; callers that don't need + // runtime pattern enforcement can leave it off and still load such + // schemas without error. + if pattern, err := getString(m, KeyPattern); err != nil { return err + } else if pattern != nil && d.validatePatterns { + regexpObject, err := regexp.Compile(*pattern) + if err != nil { + return errors.New(formatErrorDescription( + Locale.MustBeValidRegex(), + ErrorDetails{"key": KeyPattern}, + )) + } + currentSchema.pattern = regexpObject } if format, err := getString(m, KeyFormat); err != nil { diff --git a/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schemaLoader.go b/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schemaLoader.go index 88caa65de2..a27113d83f 100644 --- a/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schemaLoader.go +++ b/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schemaLoader.go @@ -23,10 +23,11 @@ import ( // SchemaLoader is used to load schemas type SchemaLoader struct { - pool *schemaPool - AutoDetect bool - Validate bool - Draft Draft + pool *schemaPool + AutoDetect bool + Validate bool + Draft Draft + ValidatePatterns bool } // NewSchemaLoader creates a new NewSchemaLoader @@ -157,6 +158,7 @@ func (sl *SchemaLoader) Compile(rootSchema JSONLoader) (*Schema, error) { d.Pool.jsonLoaderFactory = rootSchema.LoaderFactory() d.DocumentReference = ref d.ReferencePool = newSchemaReferencePool() + d.validatePatterns = sl.ValidatePatterns var doc any if ref.String() != "" { diff --git a/vendor/github.com/open-policy-agent/opa/internal/planner/planner.go b/vendor/github.com/open-policy-agent/opa/internal/planner/planner.go index 445b5d7c2a..f3e76ee854 100644 --- a/vendor/github.com/open-policy-agent/opa/internal/planner/planner.go +++ b/vendor/github.com/open-policy-agent/opa/internal/planner/planner.go @@ -637,12 +637,12 @@ func (p *Planner) planQuery(q ast.Body, index int, iter planiter) error { func (p *Planner) planExpr(e *ast.Expr, iter planiter) error { switch { - case e.Negated: - return p.planNot(e, iter) - case len(e.With) > 0: return p.planWith(e, iter) + case e.IsNegated(): + return p.planNot(e, iter) + case e.IsCall(): return p.planExprCall(e, iter) @@ -661,8 +661,27 @@ func (p *Planner) planNot(e *ast.Expr, iter planiter) error { prev := p.curr p.curr = not.Block - if err := p.planExpr(e.Complement(), func() error { return nil }); err != nil { - return err + if n, ok := e.Terms.(*ast.Not); ok { + cond := p.newLocal() // success condition + + err := p.planQuery(n.Body, 0, func() error { + p.appendStmt(&ir.AssignVarStmt{ + Source: op(ir.Bool(true)), + Target: cond, + }) + return nil + }) + if err != nil { + return err + } + + p.appendStmt(&ir.IsDefinedStmt{ + Source: cond, + }) + } else { + if err := p.planExpr(e.Complement(), func() error { return nil }); err != nil { + return err + } } p.curr = prev diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/annotations.go b/vendor/github.com/open-policy-agent/opa/v1/ast/annotations.go index 603ab5cd77..90af23a81c 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/annotations.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/annotations.go @@ -7,9 +7,13 @@ package ast import ( "encoding/json" "fmt" + "maps" "net/url" + "runtime" "slices" "strings" + "sync" + "weak" "github.com/open-policy-agent/opa/internal/deepcopy" astJSON "github.com/open-policy-agent/opa/v1/ast/json" @@ -36,6 +40,7 @@ type ( Schemas []*SchemaAnnotation `json:"schemas,omitempty"` Compile *CompileAnnotation `json:"compile,omitempty"` Custom map[string]any `json:"custom,omitempty"` + Labels map[string]any `json:"labels,omitempty"` Location *Location `json:"location,omitempty"` comments []*Comment @@ -65,10 +70,11 @@ type ( } AnnotationSet struct { - byRule map[*Rule][]*Annotations - byPackage map[int]*Annotations - byPath *annotationTreeNode - modules []*Module // Modules this set was constructed from + byRule map[*Rule][]*Annotations + byPackage map[int]*Annotations + byPath *annotationTreeNode + modules []*Module // Modules this set was constructed from + mergedLabels sync.Map // map[weak.Pointer[Rule]]*ruleLabelsEntry; lazily populated, entries cleaned up via runtime.AddCleanup when rules are GC'd } annotationTreeNode struct { @@ -172,6 +178,10 @@ func (a *Annotations) Compare(other *Annotations) int { return cmp } + if cmp := util.Compare(a.Labels, other.Labels); cmp != 0 { + return cmp + } + return 0 } @@ -228,6 +238,10 @@ func (a *Annotations) MarshalJSON() ([]byte, error) { data["custom"] = a.Custom } + if len(a.Labels) > 0 { + data["labels"] = a.Labels + } + if astJSON.GetOptions().MarshalOptions.IncludeLocation.Annotations { if a.Location != nil { data["location"] = a.Location @@ -419,6 +433,10 @@ func (a *Annotations) Copy(node Node) *Annotations { cpy.Custom = deepcopy.Map(a.Custom) } + if a.Labels != nil { + cpy.Labels = deepcopy.Map(a.Labels) + } + cpy.node = node return &cpy @@ -513,6 +531,14 @@ func (a *Annotations) toObject() (*Object, *Error) { obj.Insert(InternedTerm("custom"), NewTerm(c)) } + if len(a.Labels) > 0 { + l, err := InterfaceToValue(a.Labels) + if err != nil { + return nil, NewError(CompileErr, a.Location, "invalid labels annotation %s", err.Error()) + } + obj.Insert(InternedTerm("labels"), NewTerm(l)) + } + return &obj, nil } @@ -882,6 +908,17 @@ func (as *AnnotationSet) Chain(rule *Rule) AnnotationsRefSet { ruleAnnots := as.GetRuleScope(rule) + // Fall back to the rule's own attached annotations when the rule's source + // module isn't tracked by this AnnotationSet. This happens for rules + // supplied by an ExternalRuleSource that returns []*Rule directly: their + // source module is never compiled by the outer Compiler, so the set has no + // entries for them at any scope. attachRuleAnnotations (run at parse time) + // populates rule.Annotations with rule-scope and document-scope entries, + // which is the upper bound of what's reachable for such rules anyway. + if len(ruleAnnots) == 0 && len(rule.Annotations) > 0 && !slices.Contains(as.modules, rule.Module) { + ruleAnnots = rule.Annotations + } + if len(ruleAnnots) >= 1 { for _, a := range ruleAnnots { refs = append(refs, NewAnnotationsRef(a)) @@ -923,6 +960,62 @@ func (as *AnnotationSet) Chain(rule *Rule) AnnotationsRefSet { return refs } +// ruleLabelsEntry caches the merged labels and dedup key for a single rule. +type ruleLabelsEntry struct { + labels map[string]any + key string +} + +// MergedLabels returns the inner-scope-wins merged labels for the given rule +// along with a stable string suitable for content-based deduplication. The +// result is computed once per rule and cached on the AnnotationSet; the cache +// entry is dropped automatically when the rule is garbage-collected. +// +// labels is nil when the rule has no labels anywhere in its annotation chain; +// in that case key is the empty string. +func (as *AnnotationSet) MergedLabels(rule *Rule) (labels map[string]any, key string) { + if as == nil { + return nil, "" + } + k := weak.Make(rule) + if v, ok := as.mergedLabels.Load(k); ok { + e := v.(*ruleLabelsEntry) + return e.labels, e.key + } + merged := mergeChainLabels(as.Chain(rule)) + e := &ruleLabelsEntry{labels: merged} + if len(merged) > 0 { + b, _ := json.Marshal(merged) + e.key = string(b) + } + actual, loaded := as.mergedLabels.LoadOrStore(k, e) + if !loaded { + // k is a weak.Pointer (value type) — it does not keep rule alive, so + // the cleanup will fire once the rule becomes unreachable elsewhere. + runtime.AddCleanup(rule, func(k weak.Pointer[Rule]) { as.mergedLabels.Delete(k) }, k) + } + e = actual.(*ruleLabelsEntry) + return e.labels, e.key +} + +// mergeChainLabels folds labels from a rule's annotation chain with inner-wins +// precedence. AnnotationSet.Chain returns entries in inner-to-outer order, so +// we iterate in reverse to fold outer-to-inner. +func mergeChainLabels(chain AnnotationsRefSet) map[string]any { + var merged map[string]any + for i := len(chain) - 1; i >= 0; i-- { + a := chain[i].Annotations + if a == nil || len(a.Labels) == 0 { + continue + } + if merged == nil { + merged = make(map[string]any, len(a.Labels)) + } + maps.Copy(merged, a.Labels) + } + return merged +} + func (ars FlatAnnotationsRefSet) Insert(ar *AnnotationsRef) FlatAnnotationsRefSet { result := make(FlatAnnotationsRefSet, 0, len(ars)+1) diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/builtins.go b/vendor/github.com/open-policy-agent/opa/v1/ast/builtins.go index 7e30a8051c..78bd9a7d87 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/builtins.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/builtins.go @@ -174,6 +174,8 @@ var DefaultBuiltins = [...]*Builtin{ URLQueryEncode, URLQueryEncodeObject, URLQueryDecodeObject, + URIParse, + URIIsValid, YAMLMarshal, YAMLUnmarshal, YAMLIsValid, @@ -2043,6 +2045,33 @@ var URLQueryDecodeObject = &Builtin{ CanSkipBctx: true, } +var URIParse = &Builtin{ + Name: "uri.parse", + Description: "Parses a URI and returns an object containing its components according to RFC 3986. " + + "Empty components are omitted. " + + "In addition to the standard components, `raw_query` is returned for use with `urlquery` builtins, " + + "and `raw_path` is returned to allow detection of path-based exploits using percent-encoded characters.", + Decl: types.NewFunction( + types.Args( + types.Named("uri", types.S).Description("the URI string to parse"), + ), + types.Named("output", types.NewObject(nil, types.NewDynamicProperty(types.S, types.S))).Description("object containing URI components"), + ), + CanSkipBctx: true, +} + +var URIIsValid = &Builtin{ + Name: "uri.is_valid", + Description: "Returns true if the input can be parsed as a URI.", + Decl: types.NewFunction( + types.Args( + types.Named("uri", types.S).Description("the URI string to validate"), + ), + types.Named("result", types.B).Description("true if `uri` is a valid URI, false otherwise"), + ), + CanSkipBctx: true, +} + var YAMLMarshal = &Builtin{ Name: "yaml.marshal", Description: "Serializes the input term to YAML.", @@ -2416,7 +2445,7 @@ var ParseDurationNanos = &Builtin{ Description: "Returns the duration in nanoseconds represented by a string.", Decl: types.NewFunction( types.Args( - types.Named("duration", types.S).Description("a duration like \"3m\"; see the [Go `time` package documentation](https://golang.org/pkg/time/#ParseDuration) for more details"), + types.Named("duration", types.S).Description("a duration like \"3m\"; see the [OPA `Duration Parsing` documentation](https://www.openpolicyagent.org/docs/latest/policy-reference/builtins/time#duration-parsing) for more details"), ), types.Named("ns", types.N).Description("the `duration` in nanoseconds"), ), @@ -3048,7 +3077,7 @@ var GraphQLSchemaIsValid = &Builtin{ // and returns error string for all other inputs. var JSONSchemaVerify = &Builtin{ Name: "json.verify_schema", - Description: "Checks that the input is a valid JSON schema object. The schema can be either a JSON string or an JSON object.", + Description: "Checks that the input is a valid JSON schema object. The schema can be either a JSON string or an JSON object. The `pattern` keyword, if present, is compiled using Go's RE2 regex dialect; schemas relying on ECMA-262 features that RE2 does not support (e.g. negative lookahead) will be rejected.", Decl: types.NewFunction( types.Args( types.Named("schema", types.NewAny(types.S, types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)))). @@ -3068,7 +3097,7 @@ var JSONSchemaVerify = &Builtin{ // and returns non-empty array with error objects otherwise. var JSONMatchSchema = &Builtin{ Name: "json.match_schema", - Description: "Checks that the document matches the JSON schema.", + Description: "Checks that the document matches the JSON schema. The `pattern` keyword is enforced using Go's RE2 regex dialect; schemas relying on ECMA-262 features that RE2 does not support (e.g. negative lookahead) will be rejected.", Decl: types.NewFunction( types.Args( types.Named("document", types.NewAny(types.S, types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)))). diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/capabilities.go b/vendor/github.com/open-policy-agent/opa/v1/ast/capabilities.go index 170f0bf176..0c37dfb1fe 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/capabilities.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/capabilities.go @@ -108,7 +108,8 @@ type WasmABIVersion struct { } type CapabilitiesOptions struct { - regoVersion RegoVersion + regoVersion RegoVersion + experimentalKeywords bool } func newCapabilitiesOptions(opts []CapabilitiesOption) CapabilitiesOptions { @@ -127,6 +128,21 @@ func CapabilitiesRegoVersion(regoVersion RegoVersion) CapabilitiesOption { } } +// CapabilitiesExperimentalKeywords controls whether experimental future +// keywords are included in the returned capabilities. When enabled, the +// future_keywords list will include keywords that are recognized by the +// parser but are not yet ready for general use. +// +// Experimental keywords are subject to change or removal without notice and +// are not covered by OPA's compatibility guarantees. Enable this option only +// when you intentionally want to opt in to in-progress language features +// (for example, when writing tests for those features). +func CapabilitiesExperimentalKeywords(yes bool) CapabilitiesOption { + return func(o *CapabilitiesOptions) { + o.experimentalKeywords = yes + } +} + // CapabilitiesForThisVersion returns the capabilities of this version of OPA. func CapabilitiesForThisVersion(opts ...CapabilitiesOption) *Capabilities { co := newCapabilitiesOptions(opts) @@ -147,6 +163,9 @@ func CapabilitiesForThisVersion(opts ...CapabilitiesOption) *Capabilities { switch co.regoVersion { case RegoV0, RegoV0CompatV1: for kw := range allFutureKeywords { + if _, internal := experimentalFutureKeywords[kw]; internal && !co.experimentalKeywords { + continue + } f.FutureKeywords = append(f.FutureKeywords, kw) } @@ -159,6 +178,9 @@ func CapabilitiesForThisVersion(opts ...CapabilitiesOption) *Capabilities { } default: for kw := range futureKeywords { + if _, internal := experimentalFutureKeywords[kw]; internal && !co.experimentalKeywords { + continue + } f.FutureKeywords = append(f.FutureKeywords, kw) } @@ -277,6 +299,10 @@ func (c *Capabilities) ContainsBuiltin(name string) bool { }) } +func (c *Capabilities) ContainsFutureKeyword(kw string) bool { + return slices.Contains(c.FutureKeywords, kw) +} + // addBuiltinSorted inserts a built-in into c in sorted order. An existing built-in with the same name // will be overwritten. func (c *Capabilities) addBuiltinSorted(bi *Builtin) { diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/check.go b/vendor/github.com/open-policy-agent/opa/v1/ast/check.go index 6e4d8ddd74..810f38b1d3 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/check.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/check.go @@ -308,6 +308,17 @@ func (tc *typeChecker) checkRule(env *TypeEnv, as *AnnotationSet, rule *Rule) { typeK := cpy.GetByValue(rule.Head.Key.Value) if typeK != nil { tpe = types.NewSet(typeK) + if !path.IsGround() { + objPath := path.DynamicSuffix() + path = path.GroundPrefix() + + var err error + tpe, err = nestedObject(cpy, objPath, tpe) + if err != nil { + tc.err(NewError(TypeErr, rule.Head.Location, "%s", err.Error())) + tpe = nil + } + } } } } @@ -419,7 +430,18 @@ func (tc *typeChecker) checkExprBuiltin(env *TypeEnv, expr *Expr) *Error { return newArgError(expr.Location, name, "too few arguments", getArgTypes(env, args), namedFargs) } + pre := getArgTypes(env, args) + for i := range args { + // Check that pre-existing argument types are compatible with the expected types. + // Catching that case here avoids false negatives for builtins like sum([1, a]) where a is known to be a string. + if pre[i] != nil && !types.Nil(pre[i]) && !unifies(pre[i], fargs.Arg(i)) { + return newArgError(expr.Location, name, "invalid argument(s)", pre, namedFargs) + } + + // unify1 infers types for untyped variables and checks resolved parts (constants, refs) inside partially-typed composites. + // The unifies pre-check above is skipped when the argument contains untyped variables, + // so unify1 is still needed to catch those errors. if !unify1(env, args[i], fargs.Arg(i), false) { post := make([]types.Type, len(args)) for i := range args { @@ -509,6 +531,16 @@ func unify2(env *TypeEnv, a *Term, typeA types.Type, b *Term, typeB types.Type) } } + // When one side is a Var with no type and the other has partial type + // info (e.g. a comprehension with some undetermined components), + // assign the known structure to the Var. + if _, ok := a.Value.(Var); ok && typeA == nil && typeB != nil { + return unify1(env, a, typeB, false) + } + if _, ok := b.Value.(Var); ok && typeB == nil && typeA != nil { + return unify1(env, b, typeA, false) + } + return false } @@ -549,12 +581,20 @@ func unify2Object(env *TypeEnv, a *Term, b *Term) bool { return false } +// unify1 walks into a term's structure (arrays, objects, sets, vars), checks +// compatibility against the expected type, and infers types for variables by +// assigning them in env. It uses unifies internally for leaf checks. func unify1(env *TypeEnv, term *Term, tpe types.Type, union bool) bool { switch v := term.Value.(type) { case *Array: switch tpe := tpe.(type) { case *types.Array: return unify1Array(env, v, tpe, union) + case *types.Recursive: + if arr, ok := tpe.Unwrap().(*types.Array); ok { + return unify1Array(env, v, arr, union) + } + return false case types.Any: if types.Compare(tpe, types.A) == 0 { for i := range v.Len() { @@ -573,6 +613,11 @@ func unify1(env *TypeEnv, term *Term, tpe types.Type, union bool) bool { switch tpe := tpe.(type) { case *types.Object: return unify1Object(env, v, tpe, union) + case *types.Recursive: + if obj, ok := tpe.Unwrap().(*types.Object); ok { + return unify1Object(env, v, obj, union) + } + return false case types.Any: if types.Compare(tpe, types.A) == 0 { v.Foreach(func(key, value *Term) { @@ -710,6 +755,9 @@ func (rc *refChecker) Visit(x any) bool { case *Term: NewGenericVisitor(rc.Visit).Walk(terms) return true + case *Not: + NewGenericVisitor(rc.Visit).Walk(terms.Body) + return true } case Ref: if err := rc.checkApply(rc.env, x); err != nil { @@ -839,12 +887,21 @@ func (rc *refChecker) checkRefLeaf(tpe types.Type, ref Ref, idx int) *Error { return rc.checkRefLeaf(types.Values(tpe), ref, idx+1) } +// unifies checks whether two types are compatible with each other. func unifies(a, b types.Type) bool { if a == nil || b == nil { return false } + // Unwrap recursive types so they compare as their underlying type. + if r, ok := a.(*types.Recursive); ok { + a = r.Unwrap() + } + if r, ok := b.(*types.Recursive); ok { + b = r.Unwrap() + } + anyA, ok1 := a.(types.Any) if ok1 { if unifiesAny(anyA, b) { @@ -962,7 +1019,14 @@ func unifiesObjects(a, b *types.Object) bool { func unifiesObjectsStatic(a, b *types.Object) bool { for _, k := range a.Keys() { - if !unifies(a.Select(k), b.Select(k)) { + tpeB := b.Select(k) + if tpeB == nil { + if a.DynamicValue() != nil || b.DynamicValue() != nil { + continue + } + return false + } + if !unifies(a.Select(k), tpeB) { return false } } @@ -1147,6 +1211,9 @@ func getOneOfForType(tpe types.Type) (result []Value) { result = append(result, v) } + case *types.Recursive: + return getOneOfForType(tpe.Unwrap()) + case types.Any: for _, object := range tpe { objRes := getOneOfForType(object) @@ -1174,7 +1241,7 @@ func removeDuplicate(list []Value) []Value { func getArgTypes(env *TypeEnv, args []*Term) []types.Type { pre := make([]types.Type, len(args)) for i := range args { - pre[i] = env.Get(args[i]) + pre[i] = env.GetByValue(args[i].Value) } return pre } @@ -1201,6 +1268,9 @@ var dynamicAnyAny = types.NewDynamicProperty(types.A, types.A) // override takes a type t and returns a type obtained from t where the path represented by ref within it has type o (overriding the original type of that path) func override(ref Ref, t types.Type, o types.Type, rule *Rule) (types.Type, *Error) { var newStaticProps []*types.StaticProperty + if r, ok := t.(*types.Recursive); ok { + t = r.Unwrap() + } obj, ok := t.(*types.Object) if !ok { newType, err := getObjectType(ref, o, rule, dynamicAnyAny) diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/compare.go b/vendor/github.com/open-policy-agent/opa/v1/ast/compare.go index 8cd2bf9dc4..0314905670 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/compare.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/compare.go @@ -75,6 +75,9 @@ func Compare(a, b any) int { } switch a := a.(type) { + case *Not: + b := b.(*Not) + return Compare(a.Body, b.Body) case Null: return 0 case Boolean: @@ -144,6 +147,10 @@ func Compare(a, b any) int { return a.Compare(b.(*SomeDecl)) case *Every: return a.Compare(b.(*Every)) + case *LogicalAnd: + return a.Compare(b.(*LogicalAnd)) + case *LogicalOr: + return a.Compare(b.(*LogicalOr)) case *With: return a.Compare(b.(*With)) case Body: @@ -210,8 +217,14 @@ func sortOrder(x any) int { return 101 case *Every: return 102 + case *LogicalAnd: + return 103 + case *LogicalOr: + return 104 case *With: return 110 + case *Not: + return 111 case *Head: return 120 case Body: diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/compile.go b/vendor/github.com/open-policy-agent/opa/v1/ast/compile.go index a9455145ef..6bc705a232 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/compile.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/compile.go @@ -5,6 +5,7 @@ package ast import ( + "context" "errors" "fmt" "io" @@ -128,6 +129,7 @@ type Compiler struct { localvargen *localVarGenerator moduleLoader ModuleLoader + externalSources *util.HasherMap[Ref, ExternalRuleSource] stages []stage maxErrs int errCount uint32 @@ -135,6 +137,7 @@ type Compiler struct { sorted []string // list of sorted module names pathExists func([]string) (bool, error) pathConflictCheckRoots []string + injectedVirtual func(Ref) bool // optional custom virtual document checker after map[string][]CompilerStageDefinition metrics metrics.Metrics capabilities *Capabilities // user-supplied capabilities @@ -419,6 +422,7 @@ func NewCompiler() *Compiler { Modules: map[string]*Module{}, RewrittenVars: map[Var]Var{}, Required: &Capabilities{}, + externalSources: util.NewHasherMap[Ref, ExternalRuleSource](RefEqual), maxErrs: CompileErrorLimitDefault, mu: &sync.Mutex{}, after: map[string][]CompilerStageDefinition{}, @@ -651,6 +655,14 @@ func (c *Compiler) QueryCompiler() QueryCompiler { return newQueryCompiler(&c0) } +// WithVirtual sets a custom virtual document checker on the compiler. +// The provided function will be called during rule index building to determine +// if additional refs should be considered virtual documents. +func (c *Compiler) WithVirtual(fn func(Ref) bool) *Compiler { + c.injectedVirtual = fn + return c +} + // Compile runs the compilation process on the input modules. The compiled // version of the modules and associated data structures are stored on the // compiler. If the compilation process fails for any reason, the compiler will @@ -894,9 +906,8 @@ func (c *Compiler) GetRulesDynamic(ref Ref) []*Rule { // Without the options, it would be excluded. func (c *Compiler) GetRulesDynamicWithOpts(ref Ref, opts RulesOptions) []*Rule { node := c.RuleTree - set := map[*Rule]struct{}{} - var walk func(node *TreeNode, i int) + var walk func(*TreeNode, int) walk = func(node *TreeNode, i int) { switch { case i >= len(ref): @@ -1041,6 +1052,19 @@ func (c *Compiler) WithModuleLoader(f ModuleLoader) *Compiler { return c } +// WithExternalSource registers an external rule source for the given package +// reference. When rules under this package are queried via RuleIndex, the +// external source will be invoked to fetch all rules for the package. The +// fetched rules are cached so the external source is only called once per +// package. +// +// The package reference should be a fully qualified path (e.g., data.foo.bar). +// All rule queries under this package will be handled by the external source. +func (c *Compiler) WithExternalSource(packageRef Ref, source ExternalRuleSource) *Compiler { + c.externalSources.Put(packageRef, source) + return c +} + // WithDefaultRegoVersion sets the default Rego version to use when a module doesn't specify one; // such as when it's hand-crafted instead of parsed. func (c *Compiler) WithDefaultRegoVersion(regoVersion RegoVersion) *Compiler { @@ -1113,10 +1137,14 @@ func (c *Compiler) counterAdd(name string, n uint64) { func (c *Compiler) buildRuleIndices() { c.RuleTree.DepthFirst(func(node *TreeNode) bool { - if len(node.Values) == 0 { + if len(node.Values) == 0 && node.External == nil { return false } - rules := node.Values + if node.External != nil { + // Skip external sources - they build indices dynamically + return true + } + rules := node.Values // must be len > 0 here hasNonGroundRef := false for _, r := range rules { hasNonGroundRef = !r.Head.Ref().IsGround() @@ -1141,9 +1169,7 @@ func (c *Compiler) buildRuleIndices() { } } - index := newBaseDocEqIndex(func(ref Ref) bool { - return isVirtual(c.RuleTree, ref.GroundPrefix()) - }) + index := newBaseDocEqIndex(c.isVirtual) if index.Build(rules) { node.Index = index } @@ -1196,10 +1222,19 @@ func (c *Compiler) buildRequiredCapabilities() { if len(path) == 2 { if c.moduleIsRegoV1(c.Modules[name]) { for kw := range futureKeywords { + // Don't output experimental keywords for wildcard imports + // TODO: Remove on and/or release + if _, internal := experimentalFutureKeywords[kw]; internal { + continue + } keywords[kw] = struct{}{} } } else { for kw := range allFutureKeywords { + // TODO: Remove on and/or release + if _, internal := experimentalFutureKeywords[kw]; internal { + continue + } keywords[kw] = struct{}{} } } @@ -1302,15 +1337,19 @@ func (c *Compiler) checkRuleConflicts() { return false // go deeper } - kinds := make(map[RuleKind]struct{}, len(node.Values)) + rules := node.Values + if len(rules) == 0 { + return true // ?? right + } + kinds := make(map[RuleKind]struct{}, len(rules)) completeRules := 0 partialRules := 0 - arities := make(map[int]struct{}, len(node.Values)) + arities := make(map[int]struct{}, len(rules)) name := "" var conflicts []Ref defaultRules := make([]*Rule, 0) - for _, rule := range node.Values { + for _, rule := range rules { r := rule ref := r.Ref() name = rw(ref.CopyNonGround()).String() // varRewriter operates in-place @@ -1366,12 +1405,27 @@ func (c *Compiler) checkRuleConflicts() { } } + // Functions cannot exist within a rule's dynamic extent, as there is no valid + // evaluation scenario for this right now: it will return an error or panic. + // NB(sr): This is something we may overcome later -- but for now, it's better + // to return an error than to fail in hard-to-understand ways. + if conflicts == nil && len(node.Children) > 0 { + for _, rule := range node.Values { + if !rule.Ref().IsGround() { + if funcConflicts := node.flattenChildFunctions(); len(funcConflicts) > 0 { + conflicts = funcConflicts + } + break + } + } + } + switch { case conflicts != nil: - return !c.err(NewError(TypeErr, node.Values[0].Loc(), "rule %v conflicts with %v", name, conflicts)) + return !c.err(NewError(TypeErr, rules[0].Loc(), "rule %v conflicts with %v", name, conflicts)) case len(kinds) > 1 || len(arities) > 1 || (completeRules >= 1 && partialRules >= 1): - return !c.err(NewError(TypeErr, node.Values[0].Loc(), "conflicting rules %v found", name)) + return !c.err(NewError(TypeErr, rules[0].Loc(), "conflicting rules %v found", name)) case len(defaultRules) > 1: buf := append(append(append(make([]byte, 0, 64), "multiple default rules "...), name...), " found at "...) @@ -1516,6 +1570,7 @@ func (c *Compiler) checkBodySafety(safe VarSet, b Body) Body { // SafetyCheckVisitorParams defines the AST visitor parameters to use for collecting // variables during the safety check. This has to be exported because it's relied on // by the copy propagation implementation in topdown. +// TODO: deprecate? var SafetyCheckVisitorParams = VarVisitorParams{ SkipRefCallHead: true, SkipClosures: true, @@ -1623,7 +1678,9 @@ type schemaParser struct { } type cachedDef struct { - properties []*types.StaticProperty + typ types.Type + rec *types.Recursive + processing bool } func newSchemaParser() *schemaParser { @@ -1636,7 +1693,7 @@ func (parser *schemaParser) parseSchema(schema any) (types.Type, error) { return parser.parseSchemaWithPropertyKey(schema, "") } -func (parser *schemaParser) parseSchemaWithPropertyKey(schema any, propertyKey string) (types.Type, error) { +func (parser *schemaParser) parseSchemaWithPropertyKey(schema any, propertyKey string) (result types.Type, err error) { subSchema, ok := schema.(*gojsonschema.SubSchema) if !ok { return nil, fmt.Errorf("unexpected schema type %v", subSchema) @@ -1645,11 +1702,35 @@ func (parser *schemaParser) parseSchemaWithPropertyKey(schema any, propertyKey s // Handle referenced schemas, returns directly when a $ref is found if subSchema.RefSchema != nil { if existing, ok := parser.definitionCache[subSchema.Ref.String()]; ok { - return types.NewObject(existing.properties, nil), nil + if existing.processing { + if existing.rec == nil { + existing.rec = types.NewRecursive(subSchema.Ref.String(), nil) + } + return existing.rec, nil + } + return existing.typ, nil } return parser.parseSchemaWithPropertyKey(subSchema.RefSchema, subSchema.Ref.String()) } + // Cache this $ref definition and finalize it via defer when parsing + // completes. This allows recursive $refs to be detected: if a nested + // $ref hits a definition that is still processing, we know it's a cycle. + var def *cachedDef + if propertyKey != "" { + def = &cachedDef{processing: true} + parser.definitionCache[propertyKey] = def + defer func() { + def.processing = false + if result != nil && err == nil { + def.typ = result + if def.rec != nil { + def.rec.SetType(result) + } + } + }() + } + // Handle anyOf if subSchema.AnyOf != nil { var orType types.Type @@ -1685,7 +1766,22 @@ func (parser *schemaParser) parseSchemaWithPropertyKey(schema any, propertyKey s } if subSchema.AllOf != nil { - subSchemaArray := subSchema.AllOf + // Build the list of schemas to merge: resolve $refs and skip pure anyOf + // wrappers that carry no explicit type or structure. Such schemas have an + // "Undefined" type that would cause a spurious type-mismatch in mergeSchemas. + subSchemaArray := make([]*gojsonschema.SubSchema, 0, len(subSchema.AllOf)) + for _, s := range subSchema.AllOf { + for s.RefSchema != nil { + s = s.RefSchema + } + if !s.Types.IsTyped() && s.AnyOf != nil && len(s.PropertiesChildren) == 0 && len(s.ItemsChildren) == 0 { + continue + } + subSchemaArray = append(subSchemaArray, s) + } + if len(subSchemaArray) == 0 { + return types.A, nil + } allOfResult, err := mergeSchemas(subSchemaArray...) if err != nil { return nil, err @@ -1717,28 +1813,15 @@ func (parser *schemaParser) parseSchemaWithPropertyKey(schema any, propertyKey s } else if subSchema.Types.Contains("object") { if len(subSchema.PropertiesChildren) > 0 { - def := &cachedDef{ - properties: make([]*types.StaticProperty, 0, len(subSchema.PropertiesChildren)), - } - for _, pSchema := range subSchema.PropertiesChildren { - def.properties = append(def.properties, types.NewStaticProperty(pSchema.Property, nil)) - } - if propertyKey != "" { - parser.definitionCache[propertyKey] = def - } + properties := make([]*types.StaticProperty, 0, len(subSchema.PropertiesChildren)) for _, pSchema := range subSchema.PropertiesChildren { newtype, err := parser.parseSchema(pSchema) if err != nil { return nil, fmt.Errorf("unexpected schema type %v: %w", pSchema, err) } - for i, prop := range def.properties { - if prop.Key == pSchema.Property { - def.properties[i].Value = newtype - break - } - } + properties = append(properties, types.NewStaticProperty(pSchema.Property, newtype)) } - return types.NewObject(def.properties, nil), nil + return types.NewObject(properties, nil), nil } return types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)), nil @@ -2480,6 +2563,7 @@ func rewriteTemplateString(tsr *templateStringRewriter, safe VarSet, loc *Locati if len(ts.Parts) == 0 { terms = append(terms, NewTerm(InternedEmptyStringValue).SetLocation(loc)) } else { + // Note: we don't care about not exprs here vis := ClearOrNewVarVisitor(nil).WithParams(SafetyCheckVisitorParams) for _, p := range ts.Parts { switch p := p.(type) { @@ -2518,6 +2602,7 @@ func rewriteTemplateString(tsr *templateStringRewriter, safe VarSet, loc *Locati continue } + // Note: we don't care about not exprs here vis = ClearOrNewVarVisitor(vis).WithParams(SafetyCheckVisitorParams) vis.Walk(t) vars := vis.Vars() @@ -2647,6 +2732,22 @@ func rewritePrintCalls(gen *localVarGenerator, getArity func(Ref) int, globals V case *Every: safe.Update(x.KeyValueVars()) modrec, errsrec = rewritePrintCalls(gen, getArity, safe, x.Body) + case *Not: + modrec, errsrec = rewritePrintCalls(gen, getArity, safe, x.Body) + case *LogicalAnd: + var modR bool + var errsR Errors + modrec, errsrec = rewritePrintCalls(gen, getArity, safe, x.Lhs) + modR, errsR = rewritePrintCalls(gen, getArity, safe, x.Rhs) + modrec = modrec || modR + errsrec = append(errsrec, errsR...) + case *LogicalOr: + var modR bool + var errsR Errors + modrec, errsrec = rewritePrintCalls(gen, getArity, safe, x.Lhs) + modR, errsR = rewritePrintCalls(gen, getArity, safe, x.Rhs) + modrec = modrec || modR + errsrec = append(errsrec, errsR...) } if modrec { modified = true @@ -2689,6 +2790,7 @@ func rewritePrintCalls(gen *localVarGenerator, getArity func(Ref) int, globals V } for j := range args { + // Note: we don't care about not exprs here vis = vis.Clear().WithParams(SafetyCheckVisitorParams) vis.Walk(args[j]) vars := vis.Vars() @@ -2736,6 +2838,18 @@ func erasePrintCalls(node any) bool { modrec, x.Body = erasePrintCallsInBody(x.Body) case *Every: modrec, x.Body = erasePrintCallsInBody(x.Body) + case *Not: + modrec, x.Body = erasePrintCallsInBody(x.Body) + case *LogicalAnd: + modL, lhs := erasePrintCallsInBody(x.Lhs) + modR, rhs := erasePrintCallsInBody(x.Rhs) + x.Lhs, x.Rhs = lhs, rhs + modrec = modL || modR + case *LogicalOr: + modL, lhs := erasePrintCallsInBody(x.Lhs) + modR, rhs := erasePrintCallsInBody(x.Rhs) + x.Lhs, x.Rhs = lhs, rhs + modrec = modL || modR } if modrec { modified = true @@ -3026,6 +3140,14 @@ func rewriteRegoMetadataCalls(metadataChainVar *Var, metadataRuleVar *Var, body errs = rewriteRegoMetadataCalls(metadataChainVar, metadataRuleVar, x.Body, rewrittenVars) case *Every: errs = rewriteRegoMetadataCalls(metadataChainVar, metadataRuleVar, x.Body, rewrittenVars) + case *Not: + errs = rewriteRegoMetadataCalls(metadataChainVar, metadataRuleVar, x.Body, rewrittenVars) + case *LogicalAnd: + errs = append(errs, rewriteRegoMetadataCalls(metadataChainVar, metadataRuleVar, x.Lhs, rewrittenVars)...) + errs = append(errs, rewriteRegoMetadataCalls(metadataChainVar, metadataRuleVar, x.Rhs, rewrittenVars)...) + case *LogicalOr: + errs = append(errs, rewriteRegoMetadataCalls(metadataChainVar, metadataRuleVar, x.Lhs, rewrittenVars)...) + errs = append(errs, rewriteRegoMetadataCalls(metadataChainVar, metadataRuleVar, x.Rhs, rewrittenVars)...) } return true }) @@ -3417,6 +3539,17 @@ func (c *Compiler) setModuleTree() { func (c *Compiler) setRuleTree() { c.RuleTree = NewRuleTree(c.ModuleTree) + + // Add tree nodes for external source paths so evaluation knows to look there + c.externalSources.Iter(func(pkgRef Ref, source ExternalRuleSource) bool { + ri, err := source.Init(context.TODO(), pkgRef) + if err != nil { + c.err(NewError(CompileErr, nil, "failed to initialize external rule source for ref %v: %v", pkgRef, err)) + return true + } + c.RuleTree.add(pkgRef, ri) + return false + }) } func (c *Compiler) setGraph() { @@ -4085,6 +4218,7 @@ func (n *ModuleTreeNode) DepthFirst(f func(*ModuleTreeNode) bool) { // rule path. type TreeNode struct { Key Value + External *ExternalIndex Values []*Rule Children map[Value]*TreeNode Sorted []Value @@ -4129,18 +4263,102 @@ func NewRuleTree(mtree *ModuleTreeNode) *TreeNode { return &root } -func (n *TreeNode) add(path Ref, rule *Rule) { +func (n *TreeNode) add(path Ref, val any) { node, tail := n.find(path) if len(tail) > 0 { - sub := treeNodeFromRef(tail, rule) + sub := treeNodeFromRef(path, tail, val) if node.Children == nil { node.Children = make(map[Value]*TreeNode, 1) } node.Children[sub.Key] = sub node.Sorted = append(node.Sorted, sub.Key) - } else if rule != nil { - node.Values = append(node.Values, rule) + } else if val != nil { + switch val := val.(type) { + case *Rule: + node.Values = append(node.Values, val) + case ExternalRuleIndex: + node.External = &ExternalIndex{ + Index: val, + Ref: path, + } + } + } +} + +type ExternalIndex struct { + Index ExternalRuleIndex + Ref Ref +} + +func (ei *ExternalIndex) Tree(ctx context.Context, rt *TreeNode, prefix Ref, input *Term, m metrics.Metrics, reqMD map[string]any, respMD map[string]any) (*TreeNode, ExternalRuleIndex, error) { + resolver := &termResolver{input: input} + + rules, updatedIndex, err := ei.Index.Lookup(ctx, + LookupResolver(resolver), + LookupMetrics(m), + LookupRequestMetadata(reqMD), + LookupResponseMetadata(respMD), + ) + if err != nil { + return nil, nil, err + } + c0 := NewCompiler() + + if o := ei.Index.Opts(); o != nil { + if len(o.SkippedStages) > 0 { + c0.WithSkipStages(o.SkippedStages...) + } + + if len(o.VisibleRefs) > 0 { + visible := o.VisibleRefs + c0.WithVirtual(func(ref Ref) bool { + return slices.ContainsFunc(visible, ref.HasPrefix) && rt.isVirtual(ref) + }) + } + } + + modules := make(map[string]*Module) + for _, rule := range rules { + pkgPathStr := rule.Module.Package.Path.String() + if mod, exists := modules[pkgPathStr]; exists { + mod.Rules = append(mod.Rules, rule) + } else { + modules[pkgPathStr] = &Module{ + Package: &Package{Path: rule.Module.Package.Path}, + Rules: []*Rule{rule}, + } + } } + if m != nil { + t := m.Timer("external_lookup_compile_module") + t.Start() + defer t.Stop() + } + c0.Compile(modules) + if c0.Failed() { + return nil, nil, c0.Errors + } + + node := c0.RuleTree.Find(prefix) + return node, updatedIndex, nil +} + +type termResolver struct { + input *Term +} + +func (r *termResolver) Resolve(ref Ref) (Value, error) { + if ref.HasPrefix(InputRootRef) { + if r.input == nil { + return nil, UnknownValueErr{} + } + v, err := r.input.Value.Find(ref[1:]) + if err != nil { + return nil, UnknownValueErr{} + } + return v, nil + } + return nil, UnknownValueErr{} } // Size returns the number of rules in the tree. @@ -4202,36 +4420,85 @@ func (n *TreeNode) DepthFirst(f func(*TreeNode) bool) { } } -func treeNodeFromRef(ref Ref, rule *Rule) *TreeNode { - depth := len(ref) - 1 - key := ref[depth].Value - node := &TreeNode{ - Key: key, - Children: nil, +func (c *Compiler) isVirtual(ref Ref) bool { + return (c.injectedVirtual != nil && c.injectedVirtual(ref)) || + c.RuleTree.isVirtual(ref.GroundPrefix()) +} + +// isVirtual returns true if the ref is virtual (has rules). +func (n *TreeNode) isVirtual(ref Ref) bool { + node := n + for i := range ref { + child := node.Child(ref[i].Value) + if child == nil { + return false + } else if len(child.Values) > 0 || child.External != nil { + return true + } + node = child + } + return true +} + +func treeNodeFromRef(ref, tail Ref, val any) *TreeNode { + if len(tail) == 0 { + node := &TreeNode{ + Children: make(map[Value]*TreeNode), + } + attachValueToNode(node, ref, val) + return node } - if rule != nil { - node.Values = []*Rule{rule} + + depth := len(tail) - 1 + node := &TreeNode{ + Key: tail[depth].Value, } + attachValueToNode(node, ref, val) - for i := len(ref) - 2; i >= 0; i-- { - key := ref[i].Value + for i := depth - 1; i >= 0; i-- { + childKey := tail[i+1].Value node = &TreeNode{ - Key: key, - Children: map[Value]*TreeNode{ref[i+1].Value: node}, - Sorted: []Value{ref[i+1].Value}, + Key: tail[i].Value, + Children: map[Value]*TreeNode{childKey: node}, + Sorted: []Value{childKey}, } } return node } +func attachValueToNode(node *TreeNode, ref Ref, val any) { + if val == nil { + return + } + switch val := val.(type) { + case *Rule: + node.Values = append(node.Values, val) + case ExternalRuleIndex: + node.External = &ExternalIndex{ + Index: val, + Ref: ref, + } + } +} + // flattenChildren flattens all children's rule refs into a sorted array. func (n *TreeNode) flattenChildren() []Ref { + return n.flattenMatchingChildren(func(_ *Rule) bool { return true }) +} + +// flattenChildFunctions is like flattenChildren but only collects functions (rules with args). +func (n *TreeNode) flattenChildFunctions() []Ref { + return n.flattenMatchingChildren(func(r *Rule) bool { return r.isFunction() }) +} + +func (n *TreeNode) flattenMatchingChildren(f func(*Rule) bool) []Ref { ret := newRefSet() for _, sub := range n.Children { // we only want the children, so don't use n.DepthFirst() right away sub.DepthFirst(func(x *TreeNode) bool { - for _, r := range x.Values { - rule := r - ret.AddPrefix(rule.Ref()) + for _, rule := range x.Values { + if f(rule) { + ret.AddPrefix(rule.Ref()) + } } return false }) @@ -4240,6 +4507,60 @@ func (n *TreeNode) flattenChildren() []Ref { return util.SortedFunc(ret.s, RefCompare) } +// Copy creates a shallow copy of the TreeNode suitable for augmentation. +// Children map is copied recursively. Values slices are initially shared but +// reallocated on modification (e.g., by MergeChild's append operation). +func (n *TreeNode) Copy() *TreeNode { + if n == nil { + return nil + } + + result := &TreeNode{ + Key: n.Key, + External: n.External, + Values: n.Values, + Hide: n.Hide, + Index: n.Index, + } + + if n.Children != nil { + result.Children = make(map[Value]*TreeNode, len(n.Children)) + for k, v := range n.Children { + result.Children[k] = v.Copy() + } + } + + if n.Sorted != nil { + result.Sorted = make([]Value, len(n.Sorted)) + copy(result.Sorted, n.Sorted) + } + + return result +} + +// MergeChild merges another TreeNode into this node's children. +func (n *TreeNode) MergeChild(key Value, other *TreeNode) { + if other == nil { + return + } + + existing := n.Child(key) + if existing == nil { + if n.Children == nil { + n.Children = make(map[Value]*TreeNode) + } + n.Children[key] = other + n.Sorted = append(n.Sorted, key) + return + } + + existing.Values = append(existing.Values, other.Values...) + + for childKey, childNode := range other.Children { + existing.MergeChild(childKey, childNode) + } +} + // Graph represents the graph of dependencies between rules. type Graph struct { adj map[util.T]map[util.T]struct{} @@ -4251,7 +4572,6 @@ type Graph struct { // NewGraph returns a new Graph based on modules. The list function must return // the rules referred to directly by the ref. func NewGraph(modules map[string]*Module, list func(Ref) []*Rule) *Graph { - graph := &Graph{ adj: map[util.T]map[util.T]struct{}{}, radj: map[util.T]map[util.T]struct{}{}, @@ -4492,7 +4812,7 @@ func (vs unsafeVars) Slice() (result []unsafePair) { // If the body cannot be reordered to ensure safety, the second return value // contains a mapping of expressions to unsafe variables in those expressions. func reorderBodyForSafety(builtins map[string]*Builtin, arity func(Ref) int, globals VarSet, body Body) (Body, unsafeVars) { - vis := varVisitorPool.Get().WithParams(SafetyCheckVisitorParams) + vis := varVisitorPool.Get().WithParams(SafetyCheckVisitorParamsWithArity(arity)) vis.WalkBody(body) defer varVisitorPool.Put(vis) @@ -4502,7 +4822,7 @@ func reorderBodyForSafety(builtins map[string]*Builtin, arity func(Ref) int, glo unsafe := make(unsafeVars, len(bodyVars)-len(safe)) for _, e := range body { - vis = vis.Clear().WithParams(SafetyCheckVisitorParams) + vis = vis.Clear().WithParams(SafetyCheckVisitorParamsWithArity(arity)) vis.Walk(e) for v := range vis.Vars() { if _, ok := safe[v]; !ok { @@ -4538,6 +4858,7 @@ func reorderBodyForSafety(builtins map[string]*Builtin, arity func(Ref) int, glo if uv.Equal(ovs) { // special case "closure-self" continue } + // The expression is closing over variables not yet present in reordered body unsafe.Set(e, uv) } @@ -4570,7 +4891,7 @@ func reorderBodyForSafety(builtins map[string]*Builtin, arity func(Ref) int, glo for i, e := range reordered { if i > 0 { - vis = vis.Clear().WithParams(SafetyCheckVisitorParams) + vis = vis.Clear().WithParams(SafetyCheckVisitorParamsWithArity(arity)) vis.Walk(reordered[i-1]) g.Update(vis.Vars()) } @@ -4583,6 +4904,138 @@ func reorderBodyForSafety(builtins map[string]*Builtin, arity func(Ref) int, glo return reordered, unsafe } +// SafetyCheckVisitorParamsWithArity installs a customVisit hook on top of +// SafetyCheckVisitorParams that promotes vars from inside implicit operand +// bodies of *Not, *LogicalAnd, and *LogicalOr into the visiting set. It +// has two consumers, both of which depend on this promotion: +func SafetyCheckVisitorParamsWithArity(arity func(Ref) int) VarVisitorParams { + params := SafetyCheckVisitorParams + params.customVisit = func(vis *VarVisitor, v any) bool { + return promoteUnsafeOperandBodyVars(arity, vis, v) + } + return params +} + +func promoteUnsafeOperandBodyVars(arity func(Ref) int, vis *VarVisitor, v any) bool { + promote := func(body Body) { + for v := range unsafeImplicitBodyVars(body, arity) { + vis.Add(v) + } + } + switch n := v.(type) { + case *Not: + if !n.ExplicitBody { + promote(n.Body) + } + return true + case *LogicalAnd: + if !n.ExplicitLhs { + promote(n.Lhs) + } + if !n.ExplicitRhs { + promote(n.Rhs) + } + return true + case *LogicalOr: + if !n.ExplicitLhs { + promote(n.Lhs) + } + if !n.ExplicitRhs { + promote(n.Rhs) + } + return true + } + return false +} + +// unsafeImplicitBodyVars returns the set of vars from an implicit logical +// operand/not body that are not internally satisfied. The classification +// rule: +// - single-expr body: every visible var is returned (no consumer is +// possible within a single expression). +// - multi-expr body: a var is returned iff it has no binding within the +// body, OR it appears in exactly one body expr (no separate consumer). +func unsafeImplicitBodyVars(body Body, arity func(Ref) int) VarSet { + result := NewVarSet() + if len(body) == 0 { + return result + } + + internalVis := varVisitorPool.Get() + defer varVisitorPool.Put(internalVis) + + // 1. Collect per-expr occurrences. + occurrences := map[Var][]*Expr{} + for _, e := range body { + internalVis.Clear().WithParams(SafetyCheckVisitorParams) + internalVis.Walk(e) + for v := range internalVis.Vars() { + occurrences[v] = append(occurrences[v], e) + } + } + + // Single-expr fast path: there is no other expression to consume a + // binding, so every visible var must come from outside the body. + if len(body) == 1 { + for v := range occurrences { + result.Add(v) + } + return result + } + + // 2. Collect bindings (eq outputs + trailing call-arg outputs). + bindings := map[Var]struct{}{} + for _, e := range body { + terms, ok := e.Terms.([]*Term) + if !ok { + continue + } + + if e.IsEquality() { + for v := range outputVarsForExprEq(e, VarSet{}, VarSet{}) { + bindings[v] = struct{}{} + } + continue + } + + operator, ok := terms[0].Value.(Ref) + if !ok { + continue + } + + ar := arity(operator) + if ar < 0 { + continue + } + + numInputTerms := ar + 1 + if numInputTerms >= len(terms) { + continue + } + + internalVis.Clear().WithParams(VarVisitorParams{ + SkipClosures: true, + SkipSets: true, + SkipObjectKeys: true, + SkipRefHead: true, + }) + internalVis.WalkArgs(terms[numInputTerms:]) + for v := range internalVis.Vars() { + bindings[v] = struct{}{} + } + } + + // 3. Check if each var occurrence is bound by multiple expressions + for v, exprs := range occurrences { + if _, bound := bindings[v]; !bound { + result.Add(v) + } else if len(exprs) == 1 { + result.Add(v) + } + } + return result +} + type bodySafetyTransformer struct { builtins map[string]*Builtin arity func(Ref) int @@ -4635,9 +5088,21 @@ func (xform *bodySafetyTransformer) Visit(x any) bool { return true } case *Expr: - if ev, ok := term.Terms.(*Every); ok { - xform.globals.Update(ev.KeyValueVars()) - ev.Body = xform.reorderComprehensionSafety(NewVarSet(), ev.Body) + switch x := term.Terms.(type) { + case *Every: + xform.globals.Update(x.KeyValueVars()) + x.Body = xform.reorderComprehensionSafety(NewVarSet(), x.Body) + return true + case *Not: + x.Body = xform.reorderComprehensionSafety(NewVarSet(), x.Body) + return true + case *LogicalAnd: + x.Lhs = xform.reorderComprehensionSafety(NewVarSet(), x.Lhs) + x.Rhs = xform.reorderComprehensionSafety(NewVarSet(), x.Rhs) + return true + case *LogicalOr: + x.Lhs = xform.reorderComprehensionSafety(NewVarSet(), x.Lhs) + x.Rhs = xform.reorderComprehensionSafety(NewVarSet(), x.Rhs) return true } } @@ -4645,7 +5110,7 @@ func (xform *bodySafetyTransformer) Visit(x any) bool { } func (xform *bodySafetyTransformer) reorderComprehensionSafety(tv VarSet, body Body) Body { - bv := body.Vars(SafetyCheckVisitorParams) + bv := body.Vars(SafetyCheckVisitorParamsWithArity(xform.arity)) bv.Update(xform.globals) if tv.DiffCount(bv) > 0 { @@ -4682,11 +5147,18 @@ func (xform *bodySafetyTransformer) reorderSetComprehensionSafety(sc *SetCompreh // this expression. func unsafeVarsInClosures(e *Expr, vis *VarVisitor) { WalkClosures(e, func(x any) bool { - if ev, ok := x.(*Every); ok { - vis.WalkBody(ev.Body) - return true + switch x := x.(type) { + case *Every: + vis.WalkBody(x.Body) + case *LogicalAnd: + vis.WalkBody(x.Lhs) + vis.WalkBody(x.Rhs) + case *LogicalOr: + vis.WalkBody(x.Lhs) + vis.WalkBody(x.Rhs) + default: + vis.Walk(x) } - vis.Walk(x) return true }) } @@ -4719,16 +5191,18 @@ func OutputVarsFromExpr(c *Compiler, expr *Expr, safe VarSet) VarSet { func outputVarsForExpr(expr *Expr, arity func(Ref) int, safe VarSet, output VarSet, vis *VarVisitor) VarSet { // Negated expressions must be safe. - if expr.Negated { + if expr.IsNegated() { return VarSet{} } if len(expr.With) > 0 { + // Note: we don't care about not exprs here vis = ClearOrNewVarVisitor(vis).WithParams(SafetyCheckVisitorParams) } // With modifier inputs must be safe. for _, with := range expr.With { + // Note: we don't care about not exprs here vis = vis.Clear().WithParams(SafetyCheckVisitorParams) vis.Walk(with) if vis.Vars().DiffCount(safe) > 0 { @@ -4760,6 +5234,9 @@ func outputVarsForExpr(expr *Expr, arity func(Ref) int, safe VarSet, output VarS return outputVarsForExprCall(expr, ar, safe, terms, vis, output) case *Every: return outputVarsForTerms(terms.Domain, safe, output) + case *LogicalAnd, *LogicalOr: + // and/or expressions do not contribute bindings to the enclosing body. + return VarSet{} default: panic("illegal expression") } @@ -4922,7 +5399,7 @@ func resolveRef(globals map[Var]*usedRef, ignore *declaredVarStack, ref Ref) Ref switch v := x.Value.(type) { case Var: if g, ok := globals[v]; ok && !ignore.Contains(v) { - cpy := g.ref.Copy() + cpy := g.ref.CopyNonGround() for i := range cpy { cpy[i].SetLocation(x.Location) } @@ -5059,6 +5536,27 @@ func resolveRefsInExpr(globals map[Var]*usedRef, ignore *declaredVarStack, expr Body: resolveRefsInBody(globals, ignore, ts.Body), } ignore.Pop() + case *Not: + cpy.Terms = &Not{ + Body: resolveRefsInBody(globals, ignore, ts.Body), + ExplicitBody: ts.ExplicitBody, + } + case *LogicalAnd: + cpy.Terms = &LogicalAnd{ + Lhs: resolveRefsInBody(globals, ignore, ts.Lhs), + Rhs: resolveRefsInBody(globals, ignore, ts.Rhs), + ExplicitLhs: ts.ExplicitLhs, + ExplicitRhs: ts.ExplicitRhs, + Location: ts.Location, + } + case *LogicalOr: + cpy.Terms = &LogicalOr{ + Lhs: resolveRefsInBody(globals, ignore, ts.Lhs), + Rhs: resolveRefsInBody(globals, ignore, ts.Rhs), + ExplicitLhs: ts.ExplicitLhs, + ExplicitRhs: ts.ExplicitRhs, + Location: ts.Location, + } } for _, w := range cpy.With { w.Target = resolveRefsInTerm(globals, ignore, w.Target) @@ -5071,7 +5569,7 @@ func resolveRefsInTerm(globals map[Var]*usedRef, ignore *declaredVarStack, term switch v := term.Value.(type) { case Var: if g, ok := globals[v]; ok && !ignore.Contains(v) { - cpy := g.ref.Copy() + cpy := g.ref.CopyNonGround() for i := range cpy { cpy[i].SetLocation(term.Location) } @@ -5365,6 +5863,10 @@ func rewriteDynamics(f *equalityFactory, body Body) Body { result = rewriteDynamicsCallExpr(f, expr, result) case expr.IsEvery(): result = rewriteDynamicsEveryExpr(f, expr, result) + case expr.IsNot(): + result = rewriteDynamicsNotExpr(f, expr, result) + case expr.IsAnd(), expr.IsOr(): + result = rewriteDynamicsLogicalExpr(f, expr, result) default: result = rewriteDynamicsTermExpr(f, expr, result) } @@ -5400,6 +5902,26 @@ func rewriteDynamicsEveryExpr(f *equalityFactory, expr *Expr, result Body) Body return result } +func rewriteDynamicsNotExpr(f *equalityFactory, expr *Expr, result Body) Body { + n := expr.Terms.(*Not) + n.Body = rewriteDynamics(f, n.Body) + result.Append(expr) + return result +} + +func rewriteDynamicsLogicalExpr(f *equalityFactory, expr *Expr, result Body) Body { + switch t := expr.Terms.(type) { + case *LogicalAnd: + t.Lhs = rewriteDynamics(f, t.Lhs) + t.Rhs = rewriteDynamics(f, t.Rhs) + case *LogicalOr: + t.Lhs = rewriteDynamics(f, t.Lhs) + t.Rhs = rewriteDynamics(f, t.Rhs) + } + result.Append(expr) + return result +} + func rewriteDynamicsTermExpr(f *equalityFactory, expr *Expr, result Body) Body { term := expr.Terms.(*Term) result, expr.Terms = rewriteDynamicsInTerm(expr, f, term, result) @@ -5419,6 +5941,8 @@ func rewriteDynamicsInTerm(original *Expr, f *equalityFactory, term *Term, resul v.Body = rewriteDynamics(f, v.Body) case *ObjectComprehension: v.Body = rewriteDynamics(f, v.Body) + case *Not: + v.Body = rewriteDynamics(f, v.Body) default: result, term = rewriteDynamicsOne(original, f, term, result) } @@ -5606,6 +6130,17 @@ func expandExpr(gen *localVarGenerator, expr *Expr) (result []*Expr) { terms.Body = rewriteExprTermsInBody(gen, terms.Body) result = append(result, extras...) result = append(result, expr) + case *Not: + terms.Body = rewriteExprTermsInBody(gen, terms.Body) + result = append(result, expr) + case *LogicalAnd: + terms.Lhs = rewriteExprTermsInBody(gen, terms.Lhs) + terms.Rhs = rewriteExprTermsInBody(gen, terms.Rhs) + result = append(result, expr) + case *LogicalOr: + terms.Lhs = rewriteExprTermsInBody(gen, terms.Lhs) + terms.Rhs = rewriteExprTermsInBody(gen, terms.Rhs) + result = append(result, expr) } return } @@ -5666,6 +6201,9 @@ func expandExprTerm(gen *localVarGenerator, term *Term) (support []*Expr, output support, value := expandExprTerm(gen, v.Value) v.Value = value v.Body = rewriteExprTermsInBody(gen, appendToBody(v.Body, support...)) + case *Not: + // Note: not strictly needed as long as 'not' can only be a term node, and not a term value + v.Body = rewriteExprTermsInBody(gen, appendToBody(v.Body, support...)) } return } @@ -5913,6 +6451,11 @@ func rewriteDeclaredVarsInBody(g *localVarGenerator, stack *localDeclaredVars, u expr, errs = rewriteSomeDeclStatement(g, stack, body[i], errs, strict) case body[i].IsEvery(): expr, errs = rewriteEveryStatement(g, stack, body[i], errs, strict) + case body[i].IsNot() && body[i].Terms.(*Not).ExplicitBody: + // Only explicit not bodies are allowed to declare vars + expr, errs = rewriteNotStatement(g, stack, body[i], errs, strict) + case body[i].IsAnd() || body[i].IsOr(): + expr, errs = rewriteLogicalStatement(g, stack, body[i], errs, strict) default: expr, errs = rewriteDeclaredVarsInExpr(g, stack, body[i], errs, strict) } @@ -6152,9 +6695,54 @@ func rewriteSomeDeclStatement(g *localVarGenerator, stack *localDeclaredVars, ex return nil, errs } +func rewriteNotStatement(g *localVarGenerator, stack *localDeclaredVars, expr *Expr, errs Errors, strict bool) (*Expr, Errors) { + e := expr.Copy() + not := e.Terms.(*Not) + + stack.Push() + defer stack.Pop() + + used := NewVarSet() + not.Body, errs = rewriteDeclaredVarsInBody(g, stack, used, not.Body, errs, strict) + + return rewriteDeclaredVarsInExpr(g, stack, e, errs, strict) +} + +func rewriteLogicalStatement(g *localVarGenerator, stack *localDeclaredVars, expr *Expr, errs Errors, strict bool) (*Expr, Errors) { + e := expr.Copy() + + switch t := e.Terms.(type) { + case *LogicalAnd: + if t.ExplicitLhs { + t.Lhs, errs = rewriteLogicalOperandBody(g, stack, t.Lhs, errs, strict) + } + if t.ExplicitRhs { + t.Rhs, errs = rewriteLogicalOperandBody(g, stack, t.Rhs, errs, strict) + } + case *LogicalOr: + if t.ExplicitLhs { + t.Lhs, errs = rewriteLogicalOperandBody(g, stack, t.Lhs, errs, strict) + } + if t.ExplicitRhs { + t.Rhs, errs = rewriteLogicalOperandBody(g, stack, t.Rhs, errs, strict) + } + } + + return rewriteDeclaredVarsInExpr(g, stack, e, errs, strict) +} + +func rewriteLogicalOperandBody(g *localVarGenerator, stack *localDeclaredVars, body Body, errs Errors, strict bool) (Body, Errors) { + stack.Push() + defer stack.Pop() + + used := NewVarSet() + return rewriteDeclaredVarsInBody(g, stack, used, body, errs, strict) +} + func rewriteDeclaredVarsInExpr(g *localVarGenerator, stack *localDeclaredVars, expr *Expr, errs Errors, strict bool) (*Expr, Errors) { vis := NewGenericVisitor(func(x any) bool { var stop bool + // Note: we don't include *Not nodes here, as such bodies are allowed to contain assignments; e.g. 'not {x := input.x; f(x)}' switch x := x.(type) { case *Term: stop, errs = rewriteDeclaredVarsInTerm(g, stack, x, errs, strict) @@ -6467,8 +7055,8 @@ func validateWith(c *Compiler, unsafeBuiltinsMap map[string]struct{}, expr *Expr // target is a function. It's probably wrong for arity-0 functions, but those are // and edge case anyways. if child := targetNode.Child(ref[len(ref)-1].Value); child != nil { - for _, v := range child.Values { - if len(v.Head.Args) > 0 { + for _, r := range child.Values { + if len(r.Head.Args) > 0 { if ok, err := validateWithFunctionValue(c.builtins, unsafeBuiltinsMap, c.RuleTree, value); err != nil || ok { return false, err // err may be nil } @@ -6481,8 +7069,8 @@ func validateWith(c *Compiler, unsafeBuiltinsMap map[string]struct{}, expr *Expr if r, ok := value.Value.(Ref); ok { // TODO: check that target ref doesn't exist? if valueNode := c.RuleTree.Find(r); valueNode != nil { - for _, v := range valueNode.Values { - if len(v.Head.Args) > 0 { + for _, r := range valueNode.Values { + if len(r.Head.Args) > 0 { return false, nil } } @@ -6567,19 +7155,6 @@ func isBuiltinRefOrVar(bs map[string]*Builtin, unsafeBuiltinsMap map[string]stru return false, nil } -func isVirtual(node *TreeNode, ref Ref) bool { - for i := range ref { - child := node.Child(ref[i].Value) - if child == nil { - return false - } else if len(child.Values) > 0 { - return true - } - node = child - } - return true -} - func safetyErrorSlice(unsafe unsafeVars, rewritten map[Var]Var) (result Errors) { if len(unsafe) == 0 { return diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/env.go b/vendor/github.com/open-policy-agent/opa/v1/ast/env.go index 3a3e793778..c3a5fcef02 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/env.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/env.go @@ -320,7 +320,11 @@ func (n *typeTreeNode) Insert(path Ref, tpe types.Type, env *TypeEnv) { curr.children.Put(child.key, child) } else if child.value != nil && i+1 < len(path) { // If child has an object value, merge the new value into it. - if o, ok := child.value.(*types.Object); ok { + cv := child.value + if r, ok := cv.(*types.Recursive); ok { + cv = r.Unwrap() + } + if o, ok := cv.(*types.Object); ok { var err error child.value, err = insertIntoObject(o, path[i+1:], tpe, env) if err != nil { @@ -334,15 +338,26 @@ func (n *typeTreeNode) Insert(path Ref, tpe types.Type, env *TypeEnv) { curr.value = mergeTypes(curr.value, tpe) - if _, ok := tpe.(*types.Object); ok && curr.children.Len() > 0 { + _, isObj := tpe.(*types.Object) + if !isObj { + if r, ok := tpe.(*types.Recursive); ok { + _, isObj = r.Unwrap().(*types.Object) + } + } + if isObj && curr.children.Len() > 0 { // merge all leafs into the inserted object + cv := curr.value + if r, ok := cv.(*types.Recursive); ok { + cv = r.Unwrap() + } for p, t := range curr.Leafs() { var err error - curr.value, err = insertIntoObject(curr.value.(*types.Object), *p, t, env) + cv, err = insertIntoObject(cv.(*types.Object), *p, t, env) if err != nil { panic(fmt.Errorf("unreachable, insertIntoObject: %w", err)) } } + curr.value = cv } } @@ -363,6 +378,14 @@ func mergeTypes(a, b types.Type) types.Type { return a } + // Unwrap recursive types so they merge as their underlying type. + if r, ok := a.(*types.Recursive); ok { + a = r.Unwrap() + } + if r, ok := b.(*types.Recursive); ok { + b = r.Unwrap() + } + switch a := a.(type) { case *types.Object: if bObj, ok := b.(*types.Object); ok && len(a.StaticProperties()) == 0 && len(bObj.StaticProperties()) == 0 { diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/external_source.go b/vendor/github.com/open-policy-agent/opa/v1/ast/external_source.go new file mode 100644 index 0000000000..ce3433cce0 --- /dev/null +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/external_source.go @@ -0,0 +1,128 @@ +// Copyright 2026 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + "context" + + "github.com/open-policy-agent/opa/v1/metrics" +) + +type ExternalRuleSource interface { + // Refs returns the package refs that this source provides rules for. + // A source can provide rules for multiple packages. + Refs() []Ref + + // Init returns an initialized [ExternalRuleIndex]. A `Ref` is provided + // so we know which package we're preparing if multiple Refs are external. + Init(context.Context, Ref) (ExternalRuleIndex, error) +} + +// ExternalRuleIndex mirrors RuleIndex.Lookup(), but add a [context.Context] parameter. +type ExternalRuleIndex interface { + // Opts returns the options for the ExternalRuleIndex. Returns nil if no + // options are configured. + Opts() *ExternalSourceOptions + + // Lookup returns rules and optionally an updated ExternalRuleIndex instance. + // The returned ExternalRuleIndex (if non-nil) will be used for subsequent + // Lookup calls within the same evaluation context, allowing plugins to + // maintain per-evaluation state. + // + // Plugins can use two strategies: + // 1. Immutable: Return a new ExternalRuleIndex instance with updated state + // 2. Mutable: Update internal state and return self + // + // If the plugin does not need per-evaluation state, it can return nil for + // the ExternalRuleIndex, and the original instance will continue to be used. + Lookup(context.Context, ...LookupOption) ([]*Rule, ExternalRuleIndex, error) +} + +// ExternalRuleIndexCloser is an optional interface for resource cleanup. +type ExternalRuleIndexCloser interface { + ExternalRuleIndex + Close() error +} + +// ExternalSourceOptions contains options for registering an external rule source. +type ExternalSourceOptions struct { + // VisibleRefs controls which parts of the surrounding rule tree the external + // source can reference during compilation. By default (nil), the source is + // fully isolated and cannot access any surrounding policy. An empty slice + // is equivalent to nil (fully isolated). + // + // To allow access to the entire rule tree, use []Ref{MustParseRef("data")}. + // To allow access to specific subtrees only, list them explicitly, e.g. + // []Ref{MustParseRef("data.helpers")}. The external source can then + // reference rules under those prefixes but nothing else. + VisibleRefs []Ref + + // SkippedStages allows external sources to skip stages in the dynamic compiler + // used with the externally-provided Rego. If, for example, the `[]*Rule` returned + // has already been compiled, we can skip all stages. + // + // For pre-compiled rules, prefer starting from AllStages() and removing only + // the stages you need (e.g. SetModuleTree, SetRuleTree, BuildRuleIndices). + // This is forward-compatible: new compiler stages added in future releases + // will be skipped automatically rather than running unexpectedly. + SkippedStages []StageID +} + +// LookupOption is a functional option for ExternalRuleIndex.Lookup calls. +type LookupOption func(*LookupOptions) + +// LookupOptions contains options for ExternalRuleIndex.Lookup calls. +type LookupOptions struct { + metrics metrics.Metrics + resolver ValueResolver + requestMetadata map[string]any + responseMetadata map[string]any +} + +// Metrics returns the metrics instance from the options, or nil if not set. +func (o *LookupOptions) Metrics() metrics.Metrics { + if o == nil { + return nil + } + return o.metrics +} + +func (o *LookupOptions) Resolver() ValueResolver { + return o.resolver +} + +func (o *LookupOptions) RequestMetadata() map[string]any { + return o.requestMetadata +} + +func (o *LookupOptions) ResponseMetadata() map[string]any { + return o.responseMetadata +} + +// LookupMetrics returns a LookupOption that sets the metrics instance +// for the Lookup call. +func LookupMetrics(m metrics.Metrics) LookupOption { + return func(opts *LookupOptions) { + opts.metrics = m + } +} + +func LookupResolver(r ValueResolver) LookupOption { + return func(opts *LookupOptions) { + opts.resolver = r + } +} + +func LookupRequestMetadata(m map[string]any) LookupOption { + return func(opts *LookupOptions) { + opts.requestMetadata = m + } +} + +func LookupResponseMetadata(m map[string]any) LookupOption { + return func(opts *LookupOptions) { + opts.responseMetadata = m + } +} diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/internal/tokens/tokens.go b/vendor/github.com/open-policy-agent/opa/v1/ast/internal/tokens/tokens.go index 2721c3618b..4a32950e96 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/internal/tokens/tokens.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/internal/tokens/tokens.go @@ -76,6 +76,8 @@ const ( Every Contains If + LogicalAnd + LogicalOr ) var strings = [...]string{ @@ -131,6 +133,8 @@ var strings = [...]string{ Every: "every", Contains: "contains", If: "if", + LogicalAnd: "and", + LogicalOr: "or", } var keywords = map[string]Token{ diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/interning.go b/vendor/github.com/open-policy-agent/opa/v1/ast/interning.go index 4f454beb96..f1f4db59d1 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/interning.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/interning.go @@ -41,9 +41,8 @@ var ( minusOneValue Value = Number("-1") minusOneTerm = NewTerm(minusOneValue) - internedStringTerms = map[string]*Term{ - "": InternedEmptyString, - } + internedStringValues = map[string]Value{"": InternedEmptyStringValue} + internedStringTerms = map[string]*Term{"": InternedEmptyString} internedVarValues = map[string]Value{ "input": Var("input"), @@ -68,11 +67,12 @@ var ( // interned terms are shared globally, and the underlying map is not thread-safe. func InternStringTerm(str ...string) { for _, s := range str { - if _, ok := internedStringTerms[s]; ok { + if _, ok := internedStringValues[s]; ok { continue } - internedStringTerms[s] = &Term{Value: String(s)} + internedStringValues[s] = String(s) + internedStringTerms[s] = &Term{Value: internedStringValues[s]} } } @@ -125,7 +125,7 @@ func HasInternedValue[T internable](v T) bool { // InternedValue returns an interned Value for scalar v, if the value is // interned. If the value is not interned, a new Value is returned. func InternedValue[T internable](v T) Value { - return InternedValueOr(v, internedTermValue) + return InternedValueOr(v, newValue) } // InternedVarValue returns an interned Var Value for the given name. If the @@ -144,6 +144,8 @@ func InternedValueOr[T internable](v T, supplier func(T) Value) Value { switch value := any(v).(type) { case bool: return internedBooleanValue(value) + case string: + return internedStringValue(value) case int: return internedIntNumberValue(value) case int8: @@ -168,6 +170,37 @@ func InternedValueOr[T internable](v T, supplier func(T) Value) Value { return supplier(v) } +func newValue[T internable](v T) Value { + switch value := any(v).(type) { + case bool: + return Boolean(value) + case string: + return String(value) + case int: + return Number(strconv.Itoa(value)) + case int8: + return Number(strconv.Itoa(int(value))) + case int16: + return Number(strconv.Itoa(int(value))) + case int32: + return Number(strconv.Itoa(int(value))) + case int64: + return Number(strconv.Itoa(int(value))) + case uint: + return Number(strconv.Itoa(int(value))) + case uint8: + return Number(strconv.Itoa(int(value))) + case uint16: + return Number(strconv.Itoa(int(value))) + case uint32: + return Number(strconv.Itoa(int(value))) + case uint64: + return Number(strconv.Itoa(int(value))) + default: + panic("unreachable") + } +} + // Interned returns a possibly interned term for the given scalar value. // If the value is not interned, a new term is created for that value. func InternedTerm[T internable](v T) *Term { @@ -267,6 +300,14 @@ func internedBooleanValue(b bool) Value { return InternedBooleanFalseValue } +func internedStringValue(s string) Value { + if v, ok := internedStringValues[s]; ok { + return v + } + + return String(s) +} + // InternedBooleanTerm returns an interned term with the given boolean value. func internedBooleanTerm(b bool) *Term { if b { @@ -300,7 +341,7 @@ func internedIntNumberTerm(i int) *Term { return minusOneTerm } - return &Term{Value: Number(strconv.Itoa(i))} + return &Term{Value: internedIntNumberValue(i)} } // InternedStringTerm returns an interned term with the given string value. If the @@ -314,10 +355,6 @@ func internedStringTerm(s string) *Term { return StringTerm(s) } -func internedTermValue[T internable](v T) Value { - return InternedTerm(v).Value -} - func init() { InternStringTerm( // Numbers @@ -341,6 +378,8 @@ func init() { "data", "input", "result", "keywords", "path", "v1", "error", "partial", // HTTP "code", "message", "status_code", "method", "url", "uri", "body", "raw_body", "headers", "query_params", + // URI + "scheme", "hostname", "port", "raw_path", "raw_query", "fragment", // JWT "enc", "cty", "iss", "exp", "nbf", "aud", "secret", "cert", // Decisions diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/json/json.go b/vendor/github.com/open-policy-agent/opa/v1/ast/json/json.go index 9081fe7039..922520e958 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/json/json.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/json/json.go @@ -54,6 +54,9 @@ type NodeToggle struct { With bool Annotations bool AnnotationsRef bool + Not bool + And bool + Or bool } // configuredJSONOptions synchronizes access to the global JSON options diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/mermaid.go b/vendor/github.com/open-policy-agent/opa/v1/ast/mermaid.go new file mode 100644 index 0000000000..217947bc83 --- /dev/null +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/mermaid.go @@ -0,0 +1,431 @@ +// Copyright 2026 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + "fmt" + "strconv" + "strings" +) + +// TODO: move/rename? + +// mermaidFormatter is implemented by AST nodes that can render themselves as a +// Mermaid flowchart fragment. Each call writes node and edge declarations to b +// and returns the ID of the node it emitted. +type mermaidFormatter interface { + mermaidFormat(b *mermaidBuilder) string +} + +// mermaidBuilder accumulates Mermaid flowchart lines and issues unique node IDs. +type mermaidBuilder struct { + buf strings.Builder + counter int +} + +func (b *mermaidBuilder) newID() string { + b.counter++ + return fmt.Sprintf("n%d", b.counter) +} + +// node writes a Mermaid node declaration and returns its ID. +// Supported shapes: "rect" (default), "round", "hex", "cyl", "stadium", "trap". +func (b *mermaidBuilder) node(shape, label string) string { + id := b.newID() + label = mermaidEscapeLabel(label) + b.buf.WriteString(" ") + b.buf.WriteString(id) + switch shape { + case "round": + b.buf.WriteString("(\"") + b.buf.WriteString(label) + b.buf.WriteString("\")\n") + case "hex": + b.buf.WriteString("{{\"") + b.buf.WriteString(label) + b.buf.WriteString("\"}}\n") + case "cyl": + b.buf.WriteString("[(\"") + b.buf.WriteString(label) + b.buf.WriteString("\")]\n") + case "stadium": + b.buf.WriteString("([\"") + b.buf.WriteString(label) + b.buf.WriteString("\"])\n") + case "trap": + b.buf.WriteString("[/\"") + b.buf.WriteString(label) + b.buf.WriteString("\"/]\n") + default: // "rect" + b.buf.WriteString("[\"") + b.buf.WriteString(label) + b.buf.WriteString("\"]\n") + } + return id +} + +// edge writes a plain directed edge. +func (b *mermaidBuilder) edge(from, to string) { + b.buf.WriteString(" ") + b.buf.WriteString(from) + b.buf.WriteString(" --> ") + b.buf.WriteString(to) + b.buf.WriteString("\n") +} + +// edgeLabeled writes a directed edge with a text label. +func (b *mermaidBuilder) edgeLabeled(from, to, label string) { + b.buf.WriteString(" ") + b.buf.WriteString(from) + b.buf.WriteString(" -->|") + b.buf.WriteString(label) + b.buf.WriteString("| ") + b.buf.WriteString(to) + b.buf.WriteString("\n") +} + +// mermaidEscapeLabel replaces characters that break Mermaid quoted node labels. +func mermaidEscapeLabel(s string) string { + return strings.ReplaceAll(s, `"`, "#quot;") +} + +// mermaidGraph returns a Mermaid flowchart string representing the structure of +// the given module. +func mermaidGraph(module *Module) string { + b := &mermaidBuilder{} + module.mermaidFormat(b) + var out strings.Builder + out.WriteString("flowchart TD\n") + out.WriteString(b.buf.String()) + return out.String() +} + +// --- Module --- + +func (mod *Module) mermaidFormat(b *mermaidBuilder) string { + id := b.node("rect", "Module") + pkgID := mod.Package.mermaidFormat(b) + b.edgeLabeled(id, pkgID, "package") + for _, imp := range mod.Imports { + impID := imp.mermaidFormat(b) + b.edgeLabeled(id, impID, "import") + } + for _, rule := range mod.Rules { + ruleID := rule.mermaidFormat(b) + b.edgeLabeled(id, ruleID, "rule") + } + return id +} + +// --- Package --- + +func (pkg *Package) mermaidFormat(b *mermaidBuilder) string { + return b.node("rect", "Package: "+pkg.Path.String()) +} + +// --- Import --- + +func (imp *Import) mermaidFormat(b *mermaidBuilder) string { + label := "Import: " + imp.Path.String() + if imp.Alias != "" { + label += " as " + string(imp.Alias) + } + return b.node("rect", label) +} + +// --- Rule --- + +func (rule *Rule) mermaidFormat(b *mermaidBuilder) string { + ref := rule.Head.Ref().String() + label := "Rule: " + ref + if rule.Default { + label = "Rule: default " + ref + } + id := b.node("rect", label) + + headID := mermaidFormatHead(rule.Head, b) + b.edge(id, headID) + + if len(rule.Body) > 0 { + bodyID := b.node("rect", "Body") + b.edge(id, bodyID) + for i, expr := range rule.Body { + exprID := mermaidFormatExpr(expr, i, b) + b.edgeLabeled(bodyID, exprID, strconv.Itoa(i)) + } + } + + if rule.Else != nil { + elseID := rule.Else.mermaidFormat(b) + b.edgeLabeled(id, elseID, "else") + } + return id +} + +func mermaidFormatHead(head *Head, b *mermaidBuilder) string { + id := b.node("rect", "Head") + + refId := head.Ref().mermaidFormat(b) + b.edgeLabeled(id, refId, "ref") + + for i, arg := range head.Args { + argID := arg.mermaidFormat(b) + b.edgeLabeled(id, argID, fmt.Sprintf(`"arg[%d]"`, i)) + } + + if head.Key != nil { + keyID := head.Key.mermaidFormat(b) + b.edgeLabeled(id, keyID, "key") + } + + if head.Value != nil { + valID := head.Value.mermaidFormat(b) + b.edgeLabeled(id, valID, "value") + } + + return id +} + +func mermaidFormatExpr(expr *Expr, index int, b *mermaidBuilder) string { + label := expr.String() + + id := b.node("hex", label) + + switch terms := expr.Terms.(type) { + case *Term: + termID := terms.mermaidFormat(b) + b.edge(id, termID) + case []*Term: + // terms[0] is the operator; remaining are arguments. + for i, t := range terms { + tID := t.mermaidFormat(b) + if i == 0 { + b.edgeLabeled(id, tID, "op") + } else { + b.edgeLabeled(id, tID, fmt.Sprintf(`"arg[%d]"`, i-1)) + } + } + case *SomeDecl: + for _, sym := range terms.Symbols { + symID := sym.mermaidFormat(b) + b.edgeLabeled(id, symID, "symbol") + } + case *Every: + everyID := mermaidFormatEvery(terms, b) + b.edge(id, everyID) + case *Not: + notID := terms.mermaidFormat(b) + b.edge(id, notID) + case *LogicalAnd: + andID := mermaidFormatLogical("and", terms.Lhs, terms.Rhs, b) + b.edge(id, andID) + case *LogicalOr: + orID := mermaidFormatLogical("or", terms.Lhs, terms.Rhs, b) + b.edge(id, orID) + } + + for _, w := range expr.With { + withID := mermaidFormatWith(w, b) + b.edgeLabeled(id, withID, "with") + } + return id +} + +func mermaidFormatEvery(every *Every, b *mermaidBuilder) string { + id := b.node("rect", "every") + if every.Key != nil { + keyID := every.Key.mermaidFormat(b) + b.edgeLabeled(id, keyID, "key") + } + valID := every.Value.mermaidFormat(b) + b.edgeLabeled(id, valID, "value") + domainID := every.Domain.mermaidFormat(b) + b.edgeLabeled(id, domainID, "domain") + bodyID := b.node("rect", "Body") + b.edge(id, bodyID) + for i, expr := range every.Body { + exprID := mermaidFormatExpr(expr, i, b) + b.edge(bodyID, exprID) + } + return id +} + +func mermaidFormatLogical(op string, lhs, rhs Body, b *mermaidBuilder) string { + id := b.node("rect", op) + lhsID := b.node("rect", "Lhs") + b.edge(id, lhsID) + for i, expr := range lhs { + exprID := mermaidFormatExpr(expr, i, b) + b.edge(lhsID, exprID) + } + rhsID := b.node("rect", "Rhs") + b.edge(id, rhsID) + for i, expr := range rhs { + exprID := mermaidFormatExpr(expr, i, b) + b.edge(rhsID, exprID) + } + return id +} + +func mermaidFormatWith(w *With, b *mermaidBuilder) string { + id := b.node("rect", "with") + targetID := w.Target.mermaidFormat(b) + b.edgeLabeled(id, targetID, "target") + valID := w.Value.mermaidFormat(b) + b.edgeLabeled(id, valID, "value") + return id +} + +// --- Not --- + +func (not *Not) mermaidFormat(b *mermaidBuilder) string { + id := b.node("stadium", "not") + for i, expr := range not.Body { + exprID := mermaidFormatExpr(expr, i, b) + b.edgeLabeled(id, exprID, strconv.Itoa(i)) + } + return id +} + +// --- Term --- + +// mermaidFormat delegates to the underlying Value. Term itself does not emit a +// node; the Value determines the node shape and label. +func (term *Term) mermaidFormat(b *mermaidBuilder) string { + switch v := term.Value.(type) { + case mermaidFormatter: + return v.mermaidFormat(b) + case Set: + return mermaidFormatSet(v, b) + case Object: + return mermaidFormatObject(v, b) + default: + return b.node("round", term.String()) + } +} + +// --- Scalar Values --- + +func (Null) mermaidFormat(b *mermaidBuilder) string { + return b.node("round", "null") +} + +func (bol Boolean) mermaidFormat(b *mermaidBuilder) string { + return b.node("round", bol.String()) +} + +func (num Number) mermaidFormat(b *mermaidBuilder) string { + return b.node("round", num.String()) +} + +func (str String) mermaidFormat(b *mermaidBuilder) string { + return b.node("round", str.String()) +} + +func (v Var) mermaidFormat(b *mermaidBuilder) string { + return b.node("round", string(v)) +} + +// --- Ref --- + +func (ref Ref) mermaidFormat(b *mermaidBuilder) string { + return b.node("round", "ref: "+ref.String()) +} + +// --- Array --- + +func (arr *Array) mermaidFormat(b *mermaidBuilder) string { + id := b.node("cyl", fmt.Sprintf("array[%d]", arr.Len())) + arr.Foreach(func(t *Term) { + elemID := t.mermaidFormat(b) + b.edge(id, elemID) + }) + return id +} + +// --- Set --- + +func mermaidFormatSet(s Set, b *mermaidBuilder) string { + id := b.node("cyl", fmt.Sprintf("set{%d}", s.Len())) + s.Foreach(func(t *Term) { + elemID := t.mermaidFormat(b) + b.edge(id, elemID) + }) + return id +} + +// --- Object --- + +func mermaidFormatObject(o Object, b *mermaidBuilder) string { + id := b.node("cyl", fmt.Sprintf("object{%d}", o.Len())) + _ = o.Iter(func(k, v *Term) error { + kvID := b.node("rect", "kv") + b.edge(id, kvID) + kID := k.mermaidFormat(b) + b.edgeLabeled(kvID, kID, "key") + vID := v.mermaidFormat(b) + b.edgeLabeled(kvID, vID, "value") + return nil + }) + return id +} + +// --- Comprehensions --- + +func (ac *ArrayComprehension) mermaidFormat(b *mermaidBuilder) string { + id := b.node("stadium", "Array Comprehension") + termID := ac.Term.mermaidFormat(b) + b.edgeLabeled(id, termID, "term") + bodyID := b.node("rect", "Body") + b.edge(id, bodyID) + for i, expr := range ac.Body { + exprID := mermaidFormatExpr(expr, i, b) + b.edgeLabeled(bodyID, exprID, strconv.Itoa(i)) + } + return id +} + +func (oc *ObjectComprehension) mermaidFormat(b *mermaidBuilder) string { + id := b.node("stadium", "Object Comprehension") + kID := oc.Key.mermaidFormat(b) + b.edgeLabeled(id, kID, "key") + vID := oc.Value.mermaidFormat(b) + b.edgeLabeled(id, vID, "value") + bodyID := b.node("rect", "Body") + b.edge(id, bodyID) + for i, expr := range oc.Body { + exprID := mermaidFormatExpr(expr, i, b) + b.edgeLabeled(bodyID, exprID, strconv.Itoa(i)) + } + return id +} + +func (sc *SetComprehension) mermaidFormat(b *mermaidBuilder) string { + id := b.node("stadium", "Set Comprehension") + termID := sc.Term.mermaidFormat(b) + b.edgeLabeled(id, termID, "term") + bodyID := b.node("rect", "Body") + b.edge(id, bodyID) + for i, expr := range sc.Body { + exprID := mermaidFormatExpr(expr, i, b) + b.edgeLabeled(bodyID, exprID, strconv.Itoa(i)) + } + return id +} + +// --- Call --- + +func (c Call) mermaidFormat(b *mermaidBuilder) string { + opLabel := "call" + if len(c) > 0 { + opLabel = "call: " + c[0].String() + } + id := b.node("trap", opLabel) + for i, arg := range c[1:] { + argID := arg.mermaidFormat(b) + b.edgeLabeled(id, argID, fmt.Sprintf(`"arg[%d]"`, i)) + } + return id +} diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/parser.go b/vendor/github.com/open-policy-agent/opa/v1/ast/parser.go index 9e52b89a67..a164cea9ff 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/parser.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/parser.go @@ -148,6 +148,7 @@ type Parser struct { cache parsedTermCache recursionDepth int maxRecursionDepth int + notBodies bool } type parsedTermCacheItem struct { @@ -184,8 +185,7 @@ type ParserOptions struct { FutureKeywords []string SkipRules bool // RegoVersion is the version of Rego to parse for. - RegoVersion RegoVersion - unreleasedKeywords bool // TODO(sr): cleanup + RegoVersion RegoVersion } // EffectiveRegoVersion returns the effective RegoVersion to use for parsing. @@ -256,14 +256,6 @@ func (p *Parser) WithAllFutureKeywords(yes bool) *Parser { return p } -// withUnreleasedKeywords allows using keywords that haven't surfaced -// as future keywords (see above) yet, but have tests that require -// them to be parsed -func (p *Parser) withUnreleasedKeywords(yes bool) *Parser { - p.po.unreleasedKeywords = yes - return p -} - // WithCapabilities sets the capabilities structure on the parser. func (p *Parser) WithCapabilities(c *Capabilities) *Parser { p.po.Capabilities = c @@ -434,9 +426,25 @@ func (p *Parser) Parse() ([]Statement, []*Comment, Errors) { } selected := map[string]tokens.Token{} - if p.po.AllFutureKeywords || p.po.EffectiveRegoVersion() == RegoV1 { + if p.po.AllFutureKeywords { maps.Copy(selected, allowedFutureKeywords) } else { + if p.po.EffectiveRegoVersion() == RegoV1 { + for kw := range futureKeywordsV0 { + tok, ok := allowedFutureKeywords[kw] + if !ok { + return nil, nil, Errors{ + &Error{ + Code: ParseErr, + Message: fmt.Sprintf("unknown future keyword: %v", kw), + Location: nil, + }, + } + } + selected[kw] = tok + } + } + for _, kw := range p.po.FutureKeywords { tok, ok := allowedFutureKeywords[kw] if !ok { @@ -451,10 +459,15 @@ func (p *Parser) Parse() ([]Statement, []*Comment, Errors) { selected[kw] = tok } } + + if _, ok := selected["not"]; ok { + p.notBodies = true + } + p.s.s = p.s.s.WithKeywords(selected) if p.po.EffectiveRegoVersion() == RegoV1 { - for kw, tok := range allowedFutureKeywords { + for kw, tok := range futureKeywordsV0 { p.s.s.AddKeyword(kw, tok) } } @@ -545,7 +558,7 @@ func (p *Parser) parseAnnotations(stmts []Statement) []Statement { } func parseAnnotations(comments []*Comment) (stmts []*Annotations, errs Errors) { - numBlocks := CountFunc(comments, isMetadataComment) + numBlocks := CountFunc(comments, IsMetadataComment) if numBlocks == 0 { return nil, nil } @@ -557,7 +570,7 @@ func parseAnnotations(comments []*Comment) (stmts []*Annotations, errs Errors) { } for i := range comments { - if isMetadataComment(comments[i]) { // scan until end of block + if IsMetadataComment(comments[i]) { // scan until end of block mdp.Reset(comments[i].Location) for i++; i < len(comments) && !blockBuster(comments[i], comments[i-1]); i++ { mdp.Append(comments[i]) @@ -576,12 +589,12 @@ func parseAnnotations(comments []*Comment) (stmts []*Annotations, errs Errors) { return stmts, errs } -func isMetadataComment(c *Comment) bool { +func IsMetadataComment(c *Comment) bool { return c.Location.Col == 1 && bytes.HasPrefix(bytes.TrimSpace(c.Text), metadataBytes) } func blockBuster(curr, prev *Comment) bool { // or endOfBlock, but the name was too good to pass up - return curr.Location.Col != 1 || curr.Location.Row-1 != prev.Location.Row + return curr.Location.Col != 1 || curr.Location.Row-1 != prev.Location.Row || IsMetadataComment(curr) } func (p *Parser) parsePackage() *Package { @@ -1211,6 +1224,32 @@ func (p *Parser) parseLiteral() (expr *Expr) { } }() + // LHS explicit-body operand of an `and`/`or` binary: `{ body } and/or ...`. + // Speculatively parse `{...}`; if followed by an and/or operator, build the + // binary. Otherwise, restore and fall through to regular handling. + if p.s.tok == tokens.LBrace && p.logicalKeywordsActive() { + s := p.save() + bodyLoc := p.s.Loc() + p.scan() + body := p.parseBody(tokens.RBrace) + if body != nil { + p.scan() // consume `}` + if p.s.tok == tokens.LogicalAnd || p.s.tok == tokens.LogicalOr { + outer := p.parseLogicalOrChain(body, true, bodyLoc) + if outer == nil { + return nil + } + if p.s.tok == tokens.With { + if outer.With = p.parseWith(); outer.With == nil { + return nil + } + } + return outer + } + } + p.restore(s) + } + // Check that we're not parsing a ref if p.isAllowedRefKeyword(p.s.tok) { // Scan ahead @@ -1225,17 +1264,18 @@ func (p *Parser) parseLiteral() (expr *Expr) { } } - var negated bool - if p.s.tok == tokens.Not { - s := p.save() - p.scanWS() - tok := p.s.tok - p.restore(s) + negated := isNegatedExpression(p) - if tok != tokens.Dot && tok != tokens.LBrack { - p.scan() - negated = true + if negated && p.notBodies && p.s.tok == tokens.LBrace { + nb := p.parseNotBody() + + if nb != nil && p.s.tok == tokens.With { + if nb.With = p.parseWith(); nb.With == nil { + return nil + } } + + return nb } switch p.s.tok { @@ -1269,11 +1309,14 @@ func (p *Parser) isAllowedRefKeywordStr(s string) bool { } func (p *Parser) parseLiteralExpr(negated bool) *Expr { + startOffset := p.s.loc.Offset + startLoc := p.s.Loc() s := p.save() expr := p.parseExpr() if expr != nil { - expr.Negated = negated + var withLoc *Location if p.s.tok == tokens.With { + withLoc = p.s.Loc() if expr.With = p.parseWith(); expr.With == nil { return nil } @@ -1295,6 +1338,42 @@ func (p *Parser) parseLiteralExpr(negated bool) *Expr { } } } + + if negated && p.notBodies { + // Move 'with' statement to outer not expr + w := expr.With + expr.With = nil + expr = NewExpr(&Not{Body: NewBody(expr), Location: p.s.Loc()}) + expr.With = w + } else { + expr.Negated = negated + } + + if p.s.tok == tokens.LogicalAnd || p.s.tok == tokens.LogicalOr { + if withLoc != nil { + kw := p.s.tok.String() + p.errorf(withLoc, + "`with` modifier is not allowed on operand of `%s`; wrap the operand in `{...}` to scope, or move `with` after the %s expression to apply it to the whole expression", + kw, kw) + return nil + } + + if expr.Location == nil { + startLoc.Text = p.s.Text(startOffset, p.s.lastEnd) + expr.SetLoc(startLoc) + } + + outer := p.parseLogicalOrChain(NewBody(expr), false, expr.Location) + if outer == nil { + return nil + } + if p.s.tok == tokens.With { + if outer.With = p.parseWith(); outer.With == nil { + return nil + } + } + return outer + } } return expr } @@ -1427,6 +1506,184 @@ func (p *Parser) parseSome() *Expr { return NewExpr(decl).SetLocation(decl.Location) } +func (p *Parser) parseNotBody() *Expr { + loc := p.s.Loc() + p.scan() + + body := p.parseBody(tokens.RBrace) + if body == nil { + return nil + } + p.scan() + + not := &Not{Body: body, ExplicitBody: true, Location: loc} + return NewExpr(not).SetLocation(loc) +} + +// logicalKeywordsActive reports whether the scanner currently treats `and` or +// `or` as keywords. +func (p *Parser) logicalKeywordsActive() bool { + return p.s.s.IsKeyword("and") || p.s.s.IsKeyword("or") +} + +// parseLogicalOrChain folds a left-associative chain of `or` operators on top +// of the given lhs, with `and`-chains folded in first because `and` binds +// tighter. The lhs is supplied as a (body, explicit, location) triple so that +// both implicit single-expression operands and explicit `{...}` operands can +// be represented. +func (p *Parser) parseLogicalOrChain(lhsBody Body, lhsExplicit bool, lhsLoc *Location) *Expr { + if p.s.tok != tokens.LogicalAnd && p.s.tok != tokens.LogicalOr { + panic("expected logical and/or operator at p.s.tok") + } + + if !p.enter() { + return nil + } + defer p.leave() + + // Higher precedence first: fold any leading `and`-chain into the lhs. + if p.s.tok == tokens.LogicalAnd { + andExpr := p.parseLogicalAndChain(lhsBody, lhsExplicit, lhsLoc) + if andExpr == nil { + return nil + } + lhsBody = NewBody(andExpr) + lhsExplicit = false + } + + for p.s.tok == tokens.LogicalOr { + p.scan() + + rhsBody, rhsExplicit := p.parseLogicalOperand() + if rhsBody == nil { + return nil + } + + // RHS may extend into a higher-precedence `and`-chain. + if p.s.tok == tokens.LogicalAnd { + rhsLoc := rhsBody[0].Location + andExpr := p.parseLogicalAndChain(rhsBody, rhsExplicit, rhsLoc) + if andExpr == nil { + return nil + } + rhsBody = NewBody(andExpr) + rhsExplicit = false + } + + node := &LogicalOr{ + Lhs: lhsBody, + Rhs: rhsBody, + ExplicitLhs: lhsExplicit, + ExplicitRhs: rhsExplicit, + Location: lhsLoc, + } + wrapper := NewExpr(node).SetLocation(lhsLoc) + lhsBody = NewBody(wrapper) + lhsExplicit = false + } + + return lhsBody[0] +} + +// parseLogicalAndChain folds a left-associative chain of `and` operators on +// top of the given lhs. +func (p *Parser) parseLogicalAndChain(lhsBody Body, lhsExplicit bool, lhsLoc *Location) *Expr { + if p.s.tok != tokens.LogicalAnd { + panic("expected logical and operator at p.s.tok") + } + + if !p.enter() { + return nil + } + defer p.leave() + + for p.s.tok == tokens.LogicalAnd { + p.scan() + + rhsBody, rhsExplicit := p.parseLogicalOperand() + if rhsBody == nil { + return nil + } + + node := &LogicalAnd{ + Lhs: lhsBody, + Rhs: rhsBody, + ExplicitLhs: lhsExplicit, + ExplicitRhs: rhsExplicit, + Location: lhsLoc, + } + wrapper := NewExpr(node).SetLocation(lhsLoc) + lhsBody = NewBody(wrapper) + lhsExplicit = false + } + + return lhsBody[0] +} + +func isNegatedExpression(p *Parser) bool { + if p.s.tok == tokens.Not { + // Distinguish the `not` keyword from a ref like `not.x`. + s := p.save() + p.scanWS() + tok := p.s.tok + p.restore(s) + if tok != tokens.Dot && tok != tokens.LBrack { + p.scan() + return true + } + } + return false +} + +// parseLogicalOperand parses a single operand of an `and`/`or` expression. +// Returns the operand body and whether it was parsed from an explicit `{...}` +// body. Returns (nil, false) on parse error. +func (p *Parser) parseLogicalOperand() (Body, bool) { + if p.s.tok == tokens.LBrace { + loc := p.s.Loc() + p.scan() + body := p.parseBody(tokens.RBrace) + if body == nil { + return nil, false + } + p.scan() + _ = loc + return body, true + } + + negated := isNegatedExpression(p) + + if negated && p.notBodies && p.s.tok == tokens.LBrace { + nb := p.parseNotBody() + if nb == nil { + return nil, false + } + return NewBody(nb), false + } + + startOffset := p.s.loc.Offset + startLoc := p.s.Loc() + expr := p.parseExpr() + if expr == nil { + return nil, false + } + + if expr.Location == nil { + startLoc.Text = p.s.Text(startOffset, p.s.lastEnd) + expr.SetLoc(startLoc) + } + + if negated && p.notBodies { + // Don't attach any existing 'with' statements, they belong to the and/or, not the negated expression. + notNode := &Not{Body: NewBody(expr), Location: expr.Location} + expr = NewExpr(notNode).SetLocation(expr.Location) + } else if negated { + expr.Negated = true + } + + return NewBody(expr), false +} + func (p *Parser) parseEvery() *Expr { qb := &Every{} qb.SetLoc(p.s.Loc()) @@ -1731,6 +1988,7 @@ func (p *Parser) parseTerm() *Term { s0 := p.save() var term *Term + var unaryMinusLoc *Location switch p.s.tok { case tokens.Null: term = NullTerm().SetLocation(p.s.Loc()) @@ -1738,7 +1996,21 @@ func (p *Parser) parseTerm() *Term { term = BooleanTerm(true).SetLocation(p.s.Loc()) case tokens.False: term = BooleanTerm(false).SetLocation(p.s.Loc()) - case tokens.Sub, tokens.Dot, tokens.Number: + case tokens.Sub: + loc := p.s.Loc() + s := p.save() + p.scan() + if p.s.tok == tokens.Ident || p.s.tok == tokens.Contains { + // Unary minus on a reference: -ref → minus(0, ref). + // parseTermFinish below will resolve the full ref (e.g. input.number), + // after which we wrap the result in a minus call. + unaryMinusLoc = loc + term = p.parseVar() + } else { + p.restore(s) + term = p.parseNumber() + } + case tokens.Dot, tokens.Number: term = p.parseNumber() case tokens.String: term = p.parseString() @@ -1768,6 +2040,10 @@ func (p *Parser) parseTerm() *Term { } term = p.parseTermFinish(term, false) + if unaryMinusLoc != nil && term != nil { + zero := IntNumberTerm(0).SetLocation(unaryMinusLoc) + term = p.setLoc(Minus.Call(zero, term), unaryMinusLoc, unaryMinusLoc.Offset, p.s.lastEnd) + } p.parsedTermCachePush(term, s0) return term } @@ -2572,7 +2848,7 @@ func (p *Parser) illegal(note string, a ...any) { tok := p.s.tok.String() tokType := "token" - if _, ok := allFutureKeywords[tok]; ok || tokens.IsKeyword(p.s.tok) { + if tokens.IsKeyword(p.s.tok) || isFutureKeywordToken(p.s.tok) { tokType = "keyword" } @@ -2748,6 +3024,7 @@ type rawAnnotation struct { Schemas []map[string]any `yaml:"schemas"` Compile map[string]any `yaml:"compile"` Custom map[string]any `yaml:"custom"` + Labels map[string]any `yaml:"labels"` } type metadataParser struct { @@ -2898,6 +3175,15 @@ func (b *metadataParser) Parse() (result *Annotations, err error) { } } + if raw.Labels != nil { + result.Labels = make(map[string]any, len(raw.Labels)) + for k, v := range raw.Labels { + if result.Labels[k], err = convertYAMLMapKeyTypes(v, nil); err != nil { + return nil, err + } + } + } + result.Location = b.loc // recreate original text of entire metadata block for location text attribute @@ -3101,7 +3387,11 @@ func convertYAMLMapKeyTypes(x any, path []string) (any, error) { // futureKeywords is the source of truth for future keywords that will // eventually become standard keywords inside of Rego. -var futureKeywords = map[string]tokens.Token{} +var futureKeywords = map[string]tokens.Token{ + "not": tokens.Not, + "and": tokens.LogicalAnd, + "or": tokens.LogicalOr, +} // futureKeywordsV0 is the source of truth for future keywords that were // not yet a standard part of Rego in v0, and required importing. @@ -3114,6 +3404,22 @@ var futureKeywordsV0 = map[string]tokens.Token{ var allFutureKeywords map[string]tokens.Token +// experimentalFutureKeywords are future keywords that exist in the parser but are +// intentionally hidden from the default capabilities advertisement. +// They are only activated when a policy imports them AND the active +// capabilities explicitly list them. +var experimentalFutureKeywords = map[string]struct{}{ + "and": {}, + "or": {}, +} + +var allFutureKeywordTokens map[tokens.Token]struct{} + +func isFutureKeywordToken(tok tokens.Token) bool { + _, ok := allFutureKeywordTokens[tok] + return ok +} + func IsFutureKeyword(s string) bool { return IsFutureKeywordForRegoVersion(s, RegoV1) } @@ -3167,8 +3473,13 @@ func (p *Parser) futureImport(imp *Import, allowedFutureKeywords map[string]toke return } - kwds = []string{keyword} // overwrite + if keyword == "not" { + p.notBodies = true + } else { + kwds = []string{keyword} // overwrite + } } + for _, kw := range kwds { p.s.s.AddKeyword(kw, allowedFutureKeywords[kw]) } @@ -3211,6 +3522,11 @@ func init() { allFutureKeywords = map[string]tokens.Token{} maps.Copy(allFutureKeywords, futureKeywords) maps.Copy(allFutureKeywords, futureKeywordsV0) + + allFutureKeywordTokens = make(map[tokens.Token]struct{}, len(allFutureKeywords)) + for _, tok := range allFutureKeywords { + allFutureKeywordTokens[tok] = struct{}{} + } } // enter increments the recursion depth counter and checks if it exceeds the maximum. diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/parser_ext.go b/vendor/github.com/open-policy-agent/opa/v1/ast/parser_ext.go index ab3de33a1f..8b63182c0f 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/parser_ext.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/parser_ext.go @@ -46,6 +46,16 @@ func MustParseExpr(input string) *Expr { return parsed } +// MustParseExprWithOpts returns a parsed expression. +// If an error occurs during parsing, panic. +func MustParseExprWithOpts(input string, opts ParserOptions) *Expr { + parsed, err := ParseExprWithOpts(input, opts) + if err != nil { + panic(err) + } + return parsed +} + // MustParseImports returns a slice of imports. // If an error occurs during parsing, panic. func MustParseImports(input string) []*Import { @@ -523,6 +533,20 @@ func ParseExpr(input string) (*Expr, error) { return body[0], nil } +// ParseExprWithOpts returns exactly one expression. +// If multiple expressions are parsed, an error is returned. +// It does _not_ set SkipRules: true on its own, but respects whatever ParserOptions it's been given. +func ParseExprWithOpts(input string, popts ParserOptions) (*Expr, error) { + body, err := ParseBodyWithOpts(input, popts) + if err != nil { + return nil, fmt.Errorf("failed to parse expression: %w", err) + } + if len(body) != 1 { + return nil, fmt.Errorf("expected exactly one expression but got: %v", body) + } + return body[0], nil +} + // ParsePackage returns exactly one Package. // If multiple statements are parsed, an error is returned. func ParsePackage(input string) (*Package, error) { @@ -632,8 +656,7 @@ func ParseStatementsWithOpts(filename, input string, popts ParserOptions) ([]Sta WithAllFutureKeywords(popts.AllFutureKeywords). WithCapabilities(popts.Capabilities). WithSkipRules(popts.SkipRules). - WithRegoVersion(popts.RegoVersion). - withUnreleasedKeywords(popts.unreleasedKeywords) + WithRegoVersion(popts.RegoVersion) stmts, comments, errs := parser.Parse() if len(errs) > 0 { diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/policy.go b/vendor/github.com/open-policy-agent/opa/v1/ast/policy.go index 632b5aa6d5..470c6ab6f7 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/policy.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/policy.go @@ -289,6 +289,24 @@ type ( Location *Location `json:"location,omitempty"` } + // LogicalAnd represents a logical conjunction (`lhs and rhs`). + LogicalAnd struct { + Lhs Body `json:"lhs"` + Rhs Body `json:"rhs"` + ExplicitLhs bool `json:"explicit_lhs,omitempty"` + ExplicitRhs bool `json:"explicit_rhs,omitempty"` + Location *Location `json:"location,omitempty"` + } + + // LogicalOr represents a logical disjunction (`lhs or rhs`). + LogicalOr struct { + Lhs Body `json:"lhs"` + Rhs Body `json:"rhs"` + ExplicitLhs bool `json:"explicit_lhs,omitempty"` + ExplicitRhs bool `json:"explicit_rhs,omitempty"` + Location *Location `json:"location,omitempty"` + } + // With represents a modifier on an expression. With struct { Target *Term `json:"target"` @@ -989,11 +1007,7 @@ func (head *Head) HasDynamicRef() bool { // Copy returns a deep copy of a. func (a Args) Copy() Args { - cpy := make(Args, 0, len(a)) - for _, t := range a { - cpy = append(cpy, t.Copy()) - } - return cpy + return termSliceCopy(a) } func (a Args) String() string { @@ -1150,7 +1164,7 @@ func (body Body) Vars(params VarVisitorParams) VarSet { // NewExpr returns a new Expr object. func NewExpr(terms any) *Expr { switch terms.(type) { - case *SomeDecl, *Every, *Term, []*Term: // ok + case *SomeDecl, *Every, *Not, *LogicalAnd, *LogicalOr, *Term, []*Term: // ok default: panic("unreachable") } @@ -1163,7 +1177,14 @@ func NewExpr(terms any) *Expr { } // Complement returns a copy of this expression with the negation flag flipped. +// Note: complementing an expression containing an ast.Not term is invalid, as this will create a double negation; +// ast.Not terms may contain multiple expressions, and can therefore not be un-negated to a single *ast.Expr; use ast.Complement() instead. +// Passing an expression containing an ast.Not with multiple expressions in its body will cause a panic. func (expr *Expr) Complement() *Expr { + if n, ok := expr.Terms.(*Not); ok && len(n.Body) > 1 { + panic(fmt.Errorf("cannot complement %T containing multiple expressions (%s)", n, n)) + } + cpy := *expr cpy.Negated = !cpy.Negated return &cpy @@ -1246,6 +1267,18 @@ func (expr *Expr) Compare(other *Expr) int { if cmp := Compare(t, other.Terms.(*Every)); cmp != 0 { return cmp } + case *Not: + if cmp := Compare(t, other.Terms.(*Not)); cmp != 0 { + return cmp + } + case *LogicalAnd: + if cmp := Compare(t, other.Terms.(*LogicalAnd)); cmp != 0 { + return cmp + } + case *LogicalOr: + if cmp := Compare(t, other.Terms.(*LogicalOr)); cmp != 0 { + return cmp + } } return withSliceCompare(expr.With, other.With) @@ -1261,6 +1294,12 @@ func (expr *Expr) sortOrder() int { return 2 case *Every: return 3 + case *Not: + return 4 + case *LogicalAnd: + return 5 + case *LogicalOr: + return 6 } return -1 } @@ -1293,6 +1332,12 @@ func (expr *Expr) Copy() *Expr { cpy.Terms = ts.Copy() case *Every: cpy.Terms = ts.Copy() + case *Not: + cpy.Terms = ts.Copy() + case *LogicalAnd: + cpy.Terms = ts.Copy() + case *LogicalOr: + cpy.Terms = ts.Copy() } return cpy @@ -1310,6 +1355,10 @@ func (expr *Expr) Hash() int { } case *Term: s += ts.Value.Hash() + case *LogicalAnd: + s += ts.Hash() + case *LogicalOr: + s += ts.Hash() } if expr.Negated { s++ @@ -1356,12 +1405,35 @@ func (expr *Expr) IsEvery() bool { return ok } +// IsNot returns true if this expression is a 'not' expression. +func (expr *Expr) IsNot() bool { + _, ok := expr.Terms.(*Not) + return ok +} + +// IsNegated returns true if Negated or IsNot() returns true for this expression +func (expr *Expr) IsNegated() bool { + return expr.Negated || expr.IsNot() +} + // IsSome returns true if this expression is a 'some' expression. func (expr *Expr) IsSome() bool { _, ok := expr.Terms.(*SomeDecl) return ok } +// IsAnd returns true if this expression is a logical 'and' expression. +func (expr *Expr) IsAnd() bool { + _, ok := expr.Terms.(*LogicalAnd) + return ok +} + +// IsOr returns true if this expression is a logical 'or' expression. +func (expr *Expr) IsOr() bool { + _, ok := expr.Terms.(*LogicalOr) + return ok +} + // Operator returns the name of the function or built-in this expression refers // to. If this expression is not a function call, returns nil. func (expr *Expr) Operator() Ref { @@ -1416,6 +1488,12 @@ func (expr *Expr) IsGround() bool { } case *Term: return ts.IsGround() + case *Not: + return ts.IsGround() + case *LogicalAnd: + return ts.Lhs.IsGround() && ts.Rhs.IsGround() + case *LogicalOr: + return ts.Lhs.IsGround() && ts.Rhs.IsGround() } return true } @@ -1670,6 +1748,182 @@ func (q *Every) MarshalJSON() ([]byte, error) { return json.Marshal(data) } +func (a *LogicalAnd) String() string { + return formatBinaryLogical("and", a.Lhs, a.Rhs, a.ExplicitLhs, a.ExplicitRhs) +} + +func (a *LogicalAnd) Loc() *Location { + return a.Location +} + +func (a *LogicalAnd) SetLoc(l *Location) { + a.Location = l +} + +func (a *LogicalAnd) Copy() *LogicalAnd { + cpy := *a + cpy.Lhs = a.Lhs.Copy() + cpy.Rhs = a.Rhs.Copy() + return &cpy +} + +// Compare returns an integer indicating whether a is less than, equal to, or +// greater than other. The ExplicitLhs/ExplicitRhs fields are ignored, as they +// describe surface syntax rather than semantic content. +func (a *LogicalAnd) Compare(other *LogicalAnd) int { + if cmp := a.Lhs.Compare(other.Lhs); cmp != 0 { + return cmp + } + return a.Rhs.Compare(other.Rhs) +} + +func (a *LogicalAnd) Hash() int { + return a.Lhs.Hash() + a.Rhs.Hash() +} + +func (a *LogicalAnd) MarshalJSON() ([]byte, error) { + data := map[string]any{ + "type": "and", + "lhs": a.Lhs, + "rhs": a.Rhs, + } + if a.ExplicitLhs { + data["explicit_lhs"] = true + } + if a.ExplicitRhs { + data["explicit_rhs"] = true + } + + if astJSON.GetOptions().MarshalOptions.IncludeLocation.And { + if a.Location != nil { + data["location"] = a.Location + } + } + + return json.Marshal(data) +} + +func (a *LogicalAnd) UnmarshalJSON(bs []byte) error { + v := map[string]any{} + if err := util.UnmarshalJSON(bs, &v); err != nil { + return err + } + return unmarshalLogical("and", &a.Lhs, &a.Rhs, &a.ExplicitLhs, &a.ExplicitRhs, v) +} + +func (o *LogicalOr) String() string { + return formatBinaryLogical("or", o.Lhs, o.Rhs, o.ExplicitLhs, o.ExplicitRhs) +} + +func (o *LogicalOr) Loc() *Location { + return o.Location +} + +func (o *LogicalOr) SetLoc(l *Location) { + o.Location = l +} + +func (o *LogicalOr) Copy() *LogicalOr { + cpy := *o + cpy.Lhs = o.Lhs.Copy() + cpy.Rhs = o.Rhs.Copy() + return &cpy +} + +// Compare returns an integer indicating whether o is less than, equal to, or +// greater than other. The ExplicitLhs/ExplicitRhs fields are ignored, as they +// describe surface syntax rather than semantic content. +func (o *LogicalOr) Compare(other *LogicalOr) int { + if cmp := o.Lhs.Compare(other.Lhs); cmp != 0 { + return cmp + } + return o.Rhs.Compare(other.Rhs) +} + +func (o *LogicalOr) Hash() int { + return o.Lhs.Hash() + o.Rhs.Hash() +} + +func (o *LogicalOr) MarshalJSON() ([]byte, error) { + data := map[string]any{ + "type": "or", + "lhs": o.Lhs, + "rhs": o.Rhs, + } + if o.ExplicitLhs { + data["explicit_lhs"] = true + } + if o.ExplicitRhs { + data["explicit_rhs"] = true + } + + if astJSON.GetOptions().MarshalOptions.IncludeLocation.Or { + if o.Location != nil { + data["location"] = o.Location + } + } + + return json.Marshal(data) +} + +func (o *LogicalOr) UnmarshalJSON(bs []byte) error { + v := map[string]any{} + if err := util.UnmarshalJSON(bs, &v); err != nil { + return err + } + return unmarshalLogical("or", &o.Lhs, &o.Rhs, &o.ExplicitLhs, &o.ExplicitRhs, v) +} + +func unmarshalLogical(typeName string, lhs, rhs *Body, explicitLhs, explicitRhs *bool, v map[string]any) error { + lhsRaw, ok := v["lhs"].([]any) + if !ok { + return fmt.Errorf("ast: unable to unmarshal %s, invalid lhs field type: %T (expected list)", typeName, v["lhs"]) + } + l, err := unmarshalBody(lhsRaw) + if err != nil { + return fmt.Errorf("ast: unable to unmarshal %s lhs: %w", typeName, err) + } + *lhs = l + + rhsRaw, ok := v["rhs"].([]any) + if !ok { + return fmt.Errorf("ast: unable to unmarshal %s, invalid rhs field type: %T (expected list)", typeName, v["rhs"]) + } + r, err := unmarshalBody(rhsRaw) + if err != nil { + return fmt.Errorf("ast: unable to unmarshal %s rhs: %w", typeName, err) + } + *rhs = r + + if x, ok := v["explicit_lhs"]; ok { + b, ok := x.(bool) + if !ok { + return fmt.Errorf("ast: unable to unmarshal %s explicit_lhs field with type: %T (expected true or false)", typeName, x) + } + *explicitLhs = b + } + if x, ok := v["explicit_rhs"]; ok { + b, ok := x.(bool) + if !ok { + return fmt.Errorf("ast: unable to unmarshal %s explicit_rhs field with type: %T (expected true or false)", typeName, x) + } + *explicitRhs = b + } + + return nil +} + +func formatBinaryLogical(op string, lhs, rhs Body, explicitLhs, explicitRhs bool) string { + return formatLogicalOperand(lhs, explicitLhs) + " " + op + " " + formatLogicalOperand(rhs, explicitRhs) +} + +func formatLogicalOperand(b Body, explicit bool) string { + if explicit { + return "{ " + b.String() + " }" + } + return b.String() +} + func (w *With) String() string { buf, _ := w.AppendText(make([]byte, 0, w.StringLength())) return util.ByteSliceToString(buf) @@ -1775,6 +2029,8 @@ func Copy(x any) any { return x.Copy() case *Every: return x.Copy() + case *Not: + return x.Copy() case *Term: return x.Copy() case *ArrayComprehension: diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/policy_appenders.go b/vendor/github.com/open-policy-agent/opa/v1/ast/policy_appenders.go index 1c9813aa97..cead974b16 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/policy_appenders.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/policy_appenders.go @@ -325,6 +325,38 @@ func (c *Comment) AppendText(buf []byte) ([]byte, error) { return append(append(buf, '#'), c.Text...), nil } +func (a *LogicalAnd) AppendText(buf []byte) ([]byte, error) { + return appendLogical(buf, "and", a.Lhs, a.Rhs, a.ExplicitLhs, a.ExplicitRhs) +} + +func (o *LogicalOr) AppendText(buf []byte) ([]byte, error) { + return appendLogical(buf, "or", o.Lhs, o.Rhs, o.ExplicitLhs, o.ExplicitRhs) +} + +func appendLogical(buf []byte, op string, lhs, rhs Body, explicitLhs, explicitRhs bool) ([]byte, error) { + var err error + if buf, err = appendLogicalOperand(buf, lhs, explicitLhs); err != nil { + return nil, err + } + buf = append(buf, ' ') + buf = append(buf, op...) + buf = append(buf, ' ') + return appendLogicalOperand(buf, rhs, explicitRhs) +} + +func appendLogicalOperand(buf []byte, b Body, explicit bool) ([]byte, error) { + if !explicit && len(b) == 1 { + return b.AppendText(buf) + } + + buf = append(buf, "{ "...) + var err error + if buf, err = b.AppendText(buf); err != nil { + return nil, err + } + return append(buf, " }"...), nil +} + // RulePath returns the string representation of the rule's path, i.e. its package path followed by the rule head ref. func RulePath(r *Rule) string { if r == nil { diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/pretty.go b/vendor/github.com/open-policy-agent/opa/v1/ast/pretty.go index aa34f37471..1ee32ee555 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/pretty.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/pretty.go @@ -50,6 +50,8 @@ func (pp *prettyPrinter) Before(x any) bool { pp.writeIndent("%v %v", TypeName(x), strings.Join(extras, " ")) case Null, Boolean, Number, String, Var: pp.writeValue(x) + case *Not: + pp.writeType(x) default: pp.writeType(x) } diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/string_length.go b/vendor/github.com/open-policy-agent/opa/v1/ast/string_length.go index fe53227d24..ccdc7dc7a4 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/string_length.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/string_length.go @@ -125,6 +125,19 @@ func (c Call) StringLength() int { return c[0].StringLength() + 2 + TermSliceStringLength(c[1:], 2) } +// comprehensionTermStringLength returns the string length of a term when +// rendered inside a comprehension head, where infix operators are rendered +// in infix notation wrapped in parens. +func comprehensionTermStringLength(t *Term) int { + if call, ok := t.Value.(Call); ok && len(call) == 3 { + if bi, found := BuiltinMap[call[0].String()]; found && bi.Infix != "" && bi.Infix != "in" { + // "(left op right)" = left + " " + op + " " + right + "()" + return comprehensionTermStringLength(call[1]) + len(bi.Infix) + comprehensionTermStringLength(call[2]) + 4 + } + } + return t.StringLength() +} + func (r Ref) StringLength() (n int) { rlen := len(r) if rlen == 0 { @@ -165,16 +178,16 @@ func (v Var) StringLength() int { } func (s *SetComprehension) StringLength() int { - return s.Term.StringLength() + s.Body.StringLength() + 5 // {} and " | " + return comprehensionTermStringLength(s.Term) + s.Body.StringLength() + 5 // {} and " | " } func (a *ArrayComprehension) StringLength() int { - return a.Term.StringLength() + a.Body.StringLength() + 5 // [] and " | " + return comprehensionTermStringLength(a.Term) + a.Body.StringLength() + 5 // [] and " | " } func (o *ObjectComprehension) StringLength() (n int) { - n += o.Key.StringLength() - n += o.Value.StringLength() + n += comprehensionTermStringLength(o.Key) + n += comprehensionTermStringLength(o.Value) n += o.Body.StringLength() return n + 7 // "{}"", " | ", and ": " } @@ -349,3 +362,31 @@ func (s *SomeDecl) StringLength() int { func (c *Comment) StringLength() int { return 1 + len(c.Text) // '#' + text } + +func (not *Not) StringLength() int { + if !not.ExplicitBody && len(not.Body) == 1 { + // "not ..." + return 4 + not.Body.StringLength() + } + // "not {...}" + return 6 + not.Body.StringLength() +} + +func (a *LogicalAnd) StringLength() int { + return logicalOperandStringLength(a.Lhs, a.ExplicitLhs) + + 5 + // " and " + logicalOperandStringLength(a.Rhs, a.ExplicitRhs) +} + +func (o *LogicalOr) StringLength() int { + return logicalOperandStringLength(o.Lhs, o.ExplicitLhs) + + 4 + // " or " + logicalOperandStringLength(o.Rhs, o.ExplicitRhs) +} + +func logicalOperandStringLength(b Body, explicit bool) int { + if !explicit && len(b) == 1 { + return b.StringLength() + } + return b.StringLength() + 4 // "{ " + body + " }" +} diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/strings.go b/vendor/github.com/open-policy-agent/opa/v1/ast/strings.go index 72ec03f8cc..8bb7e7ddfd 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/strings.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/strings.go @@ -50,6 +50,8 @@ func ValueName(x Value) string { return "setcomprehension" case *TemplateString: return "templatestring" + case *Not: + return "not" } return TypeName(x) diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/term.go b/vendor/github.com/open-policy-agent/opa/v1/ast/term.go index 436b22eb2a..fb1e7aeae5 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/term.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/term.go @@ -562,6 +562,154 @@ func IsScalar(v Value) bool { return false } +// Not is both a Term and a Node +type Not struct { + Body Body `json:"body"` + ExplicitBody bool `json:"explicit_body,omitempty"` + Location *Location `json:"location,omitempty"` +} + +func NewNot(exprs ...*Expr) *Not { + return &Not{ + Body: NewBody(exprs...), + } +} + +func NotTerm(exprs ...*Expr) *Term { + return NewTerm(NewNot(exprs...)) +} + +func NotExpr(exprs ...*Expr) *Expr { + return NewExpr(&Not{ + Body: NewBody(exprs...), + }) +} + +func Complement(expr *Expr) []*Expr { + if expr.Negated { + // Legacy negation + return []*Expr{expr.Complement()} + } + + if n, ok := expr.Terms.(*Not); ok { + b := make([]*Expr, 0, len(n.Body)) + for _, e := range n.Body { + cpy := *e + + for _, w := range expr.With { + cpy.With = append(cpy.With, w.Copy()) + } + + b = append(b, &cpy) + } + return b + } + + return []*Expr{NotExpr(expr)} +} + +// Copy returns a deep copy of n. +func (n *Not) Copy() *Not { + cpy := *n + cpy.Body = n.Body.Copy() + return &cpy +} + +func (n *Not) Loc() *Location { + return n.Location +} + +func (n *Not) SetLoc(l *Location) { + n.Location = l +} + +func (n *Not) Equal(other Value) bool { + return n.Compare(other) == 0 +} + +func (n *Not) Compare(other Value) int { + switch o := other.(type) { + case *Not: + // We don't consider the ExplicitBody field, as it has no effect on expression negation + return n.Body.Compare(o.Body) + default: + return -1 + } +} + +func (n *Not) Find(path Ref) (Value, error) { + if len(path) == 0 { + return n, nil + } + return nil, errFindNotFound +} + +func (n *Not) Hash() int { + return 1 + n.Body.Hash() +} + +func (n *Not) IsGround() bool { + return n.Body.IsGround() +} + +func (n *Not) String() string { + if !n.ExplicitBody && len(n.Body) == 1 { + return "not " + n.Body.String() + } + + return "not {" + n.Body.String() + "}" +} + +func (n *Not) MarshalJSON() ([]byte, error) { + data := map[string]any{ + "type": "not", + "body": n.Body, + "explicit_body": n.ExplicitBody, + } + + if astJSON.GetOptions().MarshalOptions.IncludeLocation.Not { + if n.Location != nil { + data["location"] = n.Location + } + } + + return json.Marshal(data) +} + +func (n *Not) UnmarshalJSON(bs []byte) error { + v := map[string]any{} + if err := util.UnmarshalJSON(bs, &v); err != nil { + return err + } + + return unmarshalNot(n, v) +} + +func unmarshalNot(n *Not, v map[string]any) error { + var eb bool + if x, ok := v["explicit_body"]; ok { + eb, ok = x.(bool) + if !ok { + return fmt.Errorf("ast: unable to unmarshal explicit_body field with type: %T (expected true or false)", v["explicit_body"]) + } + } + + b, ok := v["body"].([]any) + if !ok { + return fmt.Errorf("ast: unable to unmarshal not, invalid body field type: %T (expected list)", v["body"]) + } + + body, err := unmarshalBody(b) + if err != nil { + return fmt.Errorf("ast: unable to unmarshal not body: %w", err) + } + + n.ExplicitBody = eb + n.Body = body + + return nil +} + // Null represents the null value defined by JSON. type Null struct{} @@ -1173,10 +1321,10 @@ func (ref Ref) Copy() Ref { return termSliceCopy(ref) } -// CopyNonGround returns a new ref with deep copies of the non-ground parts and shallow -// copies of the ground parts. This is a *much* cheaper operation than Copy for operations -// that only intend to modify (e.g. plug) the non-ground parts. The head element of the ref -// is always shallow copied. +// CopyNonGround returns a new ref with shallow copies of ground parts and deep +// copies of non-ground parts. The head element is always shallow copied. This is +// cheaper than Copy for operations that only modify non-ground parts (e.g. plugging) +// or metadata (e.g. Location). func (ref Ref) CopyNonGround() Ref { cpy := make(Ref, len(ref)) cpy[0] = ref[0] @@ -1729,17 +1877,26 @@ type set struct { // Copy returns a deep copy of s. func (s *set) Copy() Set { + n := len(s.keys) cpy := &set{ hash: s.hash, ground: s.ground, sortGuard: sync.Once{}, - elems: make(map[int]*Term, len(s.elems)), - keys: make([]*Term, 0, len(s.keys)), + elems: make(map[int]*Term, n), + keys: make([]*Term, n), } - for hash := range s.elems { - cpy.elems[hash] = s.elems[hash].Copy() - cpy.keys = append(cpy.keys, cpy.elems[hash]) + if n > 0 { + // Batch-allocate all Term structs in a single contiguous block. + buf := make([]Term, n) + i := 0 + for hash, elem := range s.elems { + buf[i] = *elem + deepCopyTermValue(&buf[i]) + cpy.elems[hash] = &buf[i] + cpy.keys[i] = &buf[i] + i++ + } } return cpy @@ -2364,10 +2521,41 @@ func (obj *object) IsGround() bool { // Copy returns a deep copy of obj. func (obj *object) Copy() Object { - cpy, _ := obj.Map(func(k, v *Term) (*Term, *Term, error) { - return k.Copy(), v.Copy(), nil - }) - cpy.(*object).hash = obj.hash + n := len(obj.keys) + cpy := &object{ + elems: make(map[int]*objectElem, n), + sortGuard: sync.Once{}, + hash: obj.hash, + ground: obj.ground, + } + + if n == 0 { + return cpy + } + + // Batch-allocate all objectElems, keys, and values in contiguous blocks + // (3 allocations instead of 3N). + elems := make([]objectElem, n) + keys := make([]Term, n) + vals := make([]Term, n) + cpy.keys = make([]*objectElem, n) + + for i, srcElem := range obj.keys { + keys[i] = *srcElem.key + deepCopyTermValue(&keys[i]) + vals[i] = *srcElem.value + deepCopyTermValue(&vals[i]) + + elems[i] = objectElem{key: &keys[i], value: &vals[i]} + cpy.keys[i] = &elems[i] + + hash := keys[i].Hash() + if head, ok := cpy.elems[hash]; ok { + elems[i].next = head + } + cpy.elems[hash] = &elems[i] + } + return cpy } @@ -2931,10 +3119,45 @@ func (c Call) String() string { return util.ByteSliceToString(buf) } +// deepCopyTermValue deep copies the Value of term in-place. +// Scalar values (Null, Boolean, Number, String, Var) are already +// copied by struct assignment, so only container types need work. +func deepCopyTermValue(term *Term) { + switch v := term.Value.(type) { + case Null, Boolean, Number, String, Var: + // Already copied by *term = *src struct assignment. + case Ref: + term.Value = v.Copy() + case *Array: + term.Value = v.Copy() + case Set: + term.Value = v.Copy() + case *object: + term.Value = v.Copy() + case *ArrayComprehension: + term.Value = v.Copy() + case *ObjectComprehension: + term.Value = v.Copy() + case *SetComprehension: + term.Value = v.Copy() + case *TemplateString: + term.Value = v.Copy() + case Call: + term.Value = v.Copy() + } +} + func termSliceCopy(a []*Term) []*Term { - cpy := make([]*Term, len(a)) - for i := range a { - cpy[i] = a[i].Copy() + n := len(a) + if n == 0 { + return make([]*Term, 0) + } + // Batch allocate all Term structs in a single contiguous slice (2 allocs + // instead of N+1) using the same pattern as util.NewPtrSlice. + cpy := util.NewPtrSlice[Term](n) + for i := range n { + *cpy[i] = *a[i] // copy Term struct (Value + Location) + deepCopyTermValue(cpy[i]) // deep copy container Values in-place } return cpy } @@ -3019,11 +3242,32 @@ func unmarshalExpr(expr *Expr, v map[string]any) error { } switch ts := v["terms"].(type) { case map[string]any: - t, err := unmarshalTerm(ts) - if err != nil { - return err + switch tt, _ := ts["type"].(string); tt { + case "not": + n := &Not{} + if err := unmarshalNot(n, ts); err != nil { + return err + } + expr.Terms = n + case "and": + a := &LogicalAnd{} + if err := unmarshalLogical("and", &a.Lhs, &a.Rhs, &a.ExplicitLhs, &a.ExplicitRhs, ts); err != nil { + return err + } + expr.Terms = a + case "or": + o := &LogicalOr{} + if err := unmarshalLogical("or", &o.Lhs, &o.Rhs, &o.ExplicitLhs, &o.ExplicitRhs, ts); err != nil { + return err + } + expr.Terms = o + default: + t, err := unmarshalTerm(ts) + if err != nil { + return err + } + expr.Terms = t } - expr.Terms = t case []any: terms, err := unmarshalTermSlice(ts) if err != nil { diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/term_appenders.go b/vendor/github.com/open-policy-agent/opa/v1/ast/term_appenders.go index 93c0803910..63c3973b3f 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/term_appenders.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/term_appenders.go @@ -2,6 +2,7 @@ package ast import ( "encoding" + "fmt" "strconv" "strings" @@ -228,7 +229,7 @@ func (r Ref) AppendText(buf []byte) ([]byte, error) { func (sc *SetComprehension) AppendText(buf []byte) ([]byte, error) { buf = append(buf, '{') var err error - if buf, err = sc.Term.AppendText(buf); err != nil { + if buf, err = appendComprehensionTerm(buf, sc.Term); err != nil { return nil, err } if buf, err = sc.Body.AppendText(append(buf, " | "...)); err != nil { @@ -240,7 +241,7 @@ func (sc *SetComprehension) AppendText(buf []byte) ([]byte, error) { func (ac *ArrayComprehension) AppendText(buf []byte) ([]byte, error) { buf = append(buf, '[') var err error - if buf, err = ac.Term.AppendText(buf); err != nil { + if buf, err = appendComprehensionTerm(buf, ac.Term); err != nil { return nil, err } if buf, err = ac.Body.AppendText(append(buf, " | "...)); err != nil { @@ -252,11 +253,11 @@ func (ac *ArrayComprehension) AppendText(buf []byte) ([]byte, error) { func (oc *ObjectComprehension) AppendText(buf []byte) ([]byte, error) { buf = append(buf, '{') var err error - if buf, err = oc.Key.AppendText(buf); err != nil { + if buf, err = appendComprehensionTerm(buf, oc.Key); err != nil { return nil, err } buf = append(buf, ": "...) - if buf, err = oc.Value.AppendText(buf); err != nil { + if buf, err = appendComprehensionTerm(buf, oc.Value); err != nil { return nil, err } if buf, err = oc.Body.AppendText(append(buf, " | "...)); err != nil { @@ -264,3 +265,38 @@ func (oc *ObjectComprehension) AppendText(buf []byte) ([]byte, error) { } return append(buf, '}'), nil } + +// appendComprehensionTerm writes a term, rendering infix operator calls +// in infix notation wrapped in parens. This prevents ambiguity with the +// comprehension "|" separator and produces more readable output. +func appendComprehensionTerm(buf []byte, term *Term) ([]byte, error) { + if call, ok := term.Value.(Call); ok && len(call) == 3 { + if bi, found := BuiltinMap[call[0].String()]; found && bi.Infix != "" && bi.Infix != "in" { + buf = append(buf, '(') + var err error + if buf, err = appendComprehensionTerm(buf, call[1]); err != nil { + return nil, err + } + buf = fmt.Appendf(buf, " %s ", bi.Infix) + if buf, err = appendComprehensionTerm(buf, call[2]); err != nil { + return nil, err + } + return append(buf, ')'), nil + } + } + return term.AppendText(buf) +} + +func (not *Not) AppendText(buf []byte) ([]byte, error) { + if !not.ExplicitBody && len(not.Body) == 1 { + buf = append(buf, "not "...) + return not.Body.AppendText(buf) + } + + buf = append(buf, "not {"...) + var err error + if buf, err = not.Body.AppendText(buf); err != nil { + return nil, err + } + return append(buf, '}'), nil +} diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/transform.go b/vendor/github.com/open-policy-agent/opa/v1/ast/transform.go index a71bc0a77c..7c0c54e3d7 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/transform.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/transform.go @@ -194,6 +194,29 @@ func Transform(t Transformer, x any) (any, error) { return nil, err } y.Terms = ts + case *Not: + ts.Body, err = transformBody(t, ts.Body) + if err != nil { + return nil, err + } + case *LogicalAnd: + ts.Lhs, err = transformBody(t, ts.Lhs) + if err != nil { + return nil, err + } + ts.Rhs, err = transformBody(t, ts.Rhs) + if err != nil { + return nil, err + } + case *LogicalOr: + ts.Lhs, err = transformBody(t, ts.Lhs) + if err != nil { + return nil, err + } + ts.Rhs, err = transformBody(t, ts.Rhs) + if err != nil { + return nil, err + } } for i, w := range y.With { w, err := Transform(t, w) diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/treenode_dump.go b/vendor/github.com/open-policy-agent/opa/v1/ast/treenode_dump.go new file mode 100644 index 0000000000..f22367bc81 --- /dev/null +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/treenode_dump.go @@ -0,0 +1,56 @@ +package ast + +import ( + "fmt" + "sort" + "strings" +) + +// Dump returns a string representation of the tree structure rooted at this node. +func (n *TreeNode) Dump() string { + var sb strings.Builder + n.dumpRecursive(&sb, "", "") + return sb.String() +} + +func (n *TreeNode) dumpRecursive(sb *strings.Builder, prefix, childPrefix string) { + sb.WriteString(prefix) + fmt.Fprintf(sb, "%v", n.Key) + + if n.Hide { + sb.WriteString(" [hidden]") + } + if n.External != nil { + fmt.Fprintf(sb, " ext:%v", n.External.Ref) + } + if len(n.Values) > 0 { + fmt.Fprintf(sb, " rules:%d", len(n.Values)) + } + sb.WriteString("\n") + + if len(n.Children) == 0 { + return + } + + keys := make([]Value, 0, len(n.Children)) + for k := range n.Children { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { + return Compare(keys[i], keys[j]) < 0 + }) + + for i, key := range keys { + child := n.Children[key] + isLast := i == len(keys)-1 + var newPrefix, newChildPrefix string + if isLast { + newPrefix = childPrefix + "└── " + newChildPrefix = childPrefix + " " + } else { + newPrefix = childPrefix + "├── " + newChildPrefix = childPrefix + "│ " + } + child.dumpRecursive(sb, newPrefix, newChildPrefix) + } +} diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/version_index.json b/vendor/github.com/open-policy-agent/opa/v1/ast/version_index.json index bd0df82ad6..4e5442604c 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/version_index.json +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/version_index.json @@ -970,6 +970,16 @@ "Minor": 17, "Patch": 0 }, + "uri.is_valid": { + "Major": 1, + "Minor": 16, + "Patch": 0 + }, + "uri.parse": { + "Major": 1, + "Minor": 16, + "Patch": 0 + }, "urlquery.decode": { "Major": 0, "Minor": 17, @@ -1074,6 +1084,11 @@ "Major": 0, "Minor": 34, "Patch": 0 + }, + "not": { + "Major": 1, + "Minor": 17, + "Patch": 0 } } } diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/visit.go b/vendor/github.com/open-policy-agent/opa/v1/ast/visit.go index d7725f5a51..c054b92b29 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ast/visit.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ast/visit.go @@ -48,6 +48,7 @@ type ( SkipWithTarget bool SkipSets bool SkipTemplateStrings bool + customVisit func(vis *VarVisitor, v any) bool } // Visitor defines the interface for iterating AST elements. The Visit function @@ -161,7 +162,7 @@ func walk(v Visitor, x any) { } case *Expr: switch ts := x.Terms.(type) { - case *Term, *SomeDecl, *Every: + case *Term, *SomeDecl, *Every, *Not, *LogicalAnd, *LogicalOr: Walk(w, ts) case []*Term: for i := range ts { @@ -214,6 +215,14 @@ func walk(v Visitor, x any) { Walk(w, x.Value) Walk(w, x.Domain) Walk(w, x.Body) + case *Not: + Walk(w, x.Body) + case *LogicalAnd: + Walk(w, x.Lhs) + Walk(w, x.Rhs) + case *LogicalOr: + Walk(w, x.Lhs) + Walk(w, x.Rhs) case *SomeDecl: for i := range x.Symbols { Walk(w, x.Symbols[i]) @@ -236,7 +245,7 @@ func WalkVars(x any, f func(Var) bool) { func WalkClosures(x any, f func(any) bool) { vis := NewGenericVisitor(func(x any) bool { switch x := x.(type) { - case *ArrayComprehension, *ObjectComprehension, *SetComprehension, *Every: + case *ArrayComprehension, *ObjectComprehension, *SetComprehension, *Every, *Not, *LogicalAnd, *LogicalOr: return f(x) } return false @@ -396,7 +405,7 @@ func (tv *typeVisitor[T]) walk(x any, visit func(x T) bool) { } case *Expr: switch ts := x.Terms.(type) { - case *Term, *SomeDecl, *Every: + case *Term, *SomeDecl, *Every, *Not, *LogicalAnd, *LogicalOr: tv.walk(ts, visit) case []*Term: for i := range ts { @@ -463,6 +472,14 @@ func (tv *typeVisitor[T]) walk(x any, visit func(x T) bool) { for i := range x.Parts { tv.walk(x.Parts[i], visit) } + case *Not: + tv.walk(x.Body, visit) + case *LogicalAnd: + tv.walkBody(x.Lhs, visit) + tv.walkBody(x.Rhs, visit) + case *LogicalOr: + tv.walkBody(x.Lhs, visit) + tv.walkBody(x.Rhs, visit) } } @@ -533,7 +550,7 @@ func (vis *GenericVisitor) Walk(x any) { } case *Expr: switch ts := x.Terms.(type) { - case *Term, *SomeDecl, *Every: + case *Term, *SomeDecl, *Every, *Not, *LogicalAnd, *LogicalOr: vis.Walk(ts) case []*Term: for i := range ts { @@ -600,6 +617,14 @@ func (vis *GenericVisitor) Walk(x any) { for i := range x.Parts { vis.Walk(x.Parts[i]) } + case *Not: + vis.Walk(x.Body) + case *LogicalAnd: + vis.Walk(x.Lhs) + vis.Walk(x.Rhs) + case *LogicalOr: + vis.Walk(x.Lhs) + vis.Walk(x.Rhs) } } @@ -668,7 +693,7 @@ func (vis *BeforeAfterVisitor) Walk(x any) { } case *Expr: switch ts := x.Terms.(type) { - case *Term, *SomeDecl, *Every: + case *Term, *SomeDecl, *Every, *Not, *LogicalAnd, *LogicalOr: vis.Walk(ts) case []*Term: for i := range ts { @@ -731,6 +756,14 @@ func (vis *BeforeAfterVisitor) Walk(x any) { for i := range x.Symbols { vis.Walk(x.Symbols[i]) } + case *Not: + vis.Walk(x.Body) + case *LogicalAnd: + vis.Walk(x.Lhs) + vis.Walk(x.Rhs) + case *LogicalOr: + vis.Walk(x.Lhs) + vis.Walk(x.Rhs) } } @@ -807,7 +840,7 @@ func (vis *VarVisitor) visit(v any) bool { } if vis.params.SkipClosures { switch v := v.(type) { - case *ArrayComprehension, *ObjectComprehension, *SetComprehension, *TemplateString: + case *ArrayComprehension, *ObjectComprehension, *SetComprehension, *TemplateString, *Not, *LogicalAnd, *LogicalOr: return true case *Expr: if ev, ok := v.Terms.(*Every); ok { @@ -886,6 +919,12 @@ func (vis *VarVisitor) visit(v any) bool { // Walk iterates the AST by calling the function f on the [VarVisitor] before recursing. // Contrary to the deprecated [Walk] function, this does not require allocating the visitor from heap. func (vis *VarVisitor) Walk(x any) { + if vis.params.customVisit != nil { + if vis.params.customVisit(vis, x) { + return + } + } + if vis.visit(x) { return } @@ -927,7 +966,7 @@ func (vis *VarVisitor) Walk(x any) { vis.WalkArgs(x) case *Expr: switch ts := x.Terms.(type) { - case *Term, *SomeDecl, *Every: + case *Term, *SomeDecl, *Every, *Not, *LogicalAnd, *LogicalOr: vis.Walk(ts) case []*Term: for i := range ts { @@ -992,6 +1031,14 @@ func (vis *VarVisitor) Walk(x any) { for i := range x.Parts { vis.Walk(x.Parts[i]) } + case *Not: + vis.Walk(x.Body) + case *LogicalAnd: + vis.WalkBody(x.Lhs) + vis.WalkBody(x.Rhs) + case *LogicalOr: + vis.WalkBody(x.Lhs) + vis.WalkBody(x.Rhs) } } diff --git a/vendor/github.com/open-policy-agent/opa/v1/bundle/bundle.go b/vendor/github.com/open-policy-agent/opa/v1/bundle/bundle.go index bf00e96ca2..80bad2090b 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/bundle/bundle.go +++ b/vendor/github.com/open-policy-agent/opa/v1/bundle/bundle.go @@ -1115,6 +1115,10 @@ func (b *Bundle) FormatModulesWithOptions(opts BundleFormatOptions) error { Capabilities: opts.Capabilities, } + if fmtOpts.Capabilities == nil { + fmtOpts.Capabilities = ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(fmtOpts.RegoVersion)) + } + if module.Parsed != nil { fmtOpts.ParserOptions = &ast.ParserOptions{ RegoVersion: module.Parsed.RegoVersion(), @@ -1122,10 +1126,9 @@ func (b *Bundle) FormatModulesWithOptions(opts BundleFormatOptions) error { if opts.PreserveModuleRegoVersion { fmtOpts.RegoVersion = module.Parsed.RegoVersion() } - } - - if fmtOpts.Capabilities == nil { - fmtOpts.Capabilities = ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(fmtOpts.RegoVersion)) + if fmtOpts.ParserOptions.RegoVersion == fmtOpts.RegoVersion { + fmtOpts.ParserOptions.Capabilities = fmtOpts.Capabilities + } } if module.Raw == nil { diff --git a/vendor/github.com/open-policy-agent/opa/v1/bundle/manifest.schema.json b/vendor/github.com/open-policy-agent/opa/v1/bundle/manifest.schema.json new file mode 100644 index 0000000000..ff060aeea4 --- /dev/null +++ b/vendor/github.com/open-policy-agent/opa/v1/bundle/manifest.schema.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://openpolicyagent.org/schemas/bundle/v1/manifest.schema.json", + "title": "OPA Bundle Manifest", + "description": "JSON Schema for the bundle `.manifest` file produced by `opa build`. Generated from v1/bundle/bundle.go.", + "$ref": "#/$defs/Manifest", + "$defs": { + "Manifest": { + "type": "object", + "properties": { + "file_rego_versions": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "metadata": { + "type": "object" + }, + "rego_version": { + "type": "integer" + }, + "revision": { + "type": "string" + }, + "roots": { + "type": "array", + "items": { + "type": "string" + } + }, + "wasm": { + "type": "array", + "items": { + "$ref": "#/$defs/WasmResolver" + } + } + }, + "required": [ + "revision" + ] + }, + "WasmResolver": { + "type": "object", + "properties": { + "annotations": { + "type": "array", + "items": { + "type": "object", + "description": "Rego annotations; opaque in this schema. See the OPA documentation for the full annotation shape." + } + }, + "entrypoint": { + "type": "string" + }, + "module": { + "type": "string" + } + }, + "additionalProperties": false + } + } +} diff --git a/vendor/github.com/open-policy-agent/opa/v1/bundle/store.go b/vendor/github.com/open-policy-agent/opa/v1/bundle/store.go index f6c13c75fc..29efe208d2 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/bundle/store.go +++ b/vendor/github.com/open-policy-agent/opa/v1/bundle/store.go @@ -12,7 +12,6 @@ import ( "fmt" "maps" "path/filepath" - "slices" "sort" "strings" "sync" @@ -359,8 +358,9 @@ type ActivateOpts struct { TxnCtx *storage.Context Compiler *ast.Compiler Metrics metrics.Metrics - Bundles map[string]*Bundle // Optional - ExtraModules map[string]*ast.Module // Optional + Bundles map[string]*Bundle // Optional + ExtraModules map[string]*ast.Module // Optional + ExternalSources *util.HasherMap[ast.Ref, ast.ExternalRuleSource] // Optional AuthorizationDecisionRef ast.Ref ParserOptions ast.ParserOptions Plugin string @@ -424,7 +424,6 @@ func Deactivate(opts *DeactivateOpts) error { } func activateBundles(opts *ActivateOpts) error { - // Build collections of bundle names, modules, and roots to erase erase := map[string]struct{}{} names := map[string]struct{}{} @@ -476,9 +475,7 @@ func activateBundles(opts *ActivateOpts) error { // Validate data in bundle does not contain paths outside the bundle's roots. for _, b := range snapshotBundles { - if b.lazyLoadingMode { - for _, item := range b.Raw { path := filepath.ToSlash(item.Path) @@ -535,7 +532,7 @@ func activateBundles(opts *ActivateOpts) error { maps.Copy(remainingAndExtra, remaining) maps.Copy(remainingAndExtra, opts.ExtraModules) - err = compileModules(opts.Compiler, opts.Metrics, snapshotBundles, remainingAndExtra, opts.legacy, opts.AuthorizationDecisionRef) + err = compileModules(opts.Compiler, opts.Metrics, snapshotBundles, remainingAndExtra, opts.legacy, opts.AuthorizationDecisionRef, opts.ExternalSources) if err != nil { return err } @@ -612,7 +609,6 @@ func doDFS(obj map[string]json.RawMessage, path string, roots []string) error { } func activateDeltaBundles(opts *ActivateOpts, bundles map[string]*Bundle) error { - // Check that the manifest roots and wasm resolvers in the delta bundle // match with those currently in the store for name, b := range bundles { @@ -685,7 +681,6 @@ func valueToManifest(v any) (Manifest, error) { // erase bundles by name and roots. This will clear all policies and data at its roots and remove its // manifest from storage. func eraseBundles(ctx context.Context, store storage.Store, txn storage.Transaction, parserOpts ast.ParserOptions, names map[string]struct{}, roots map[string]struct{}) (map[string]*ast.Module, error) { - if err := eraseData(ctx, store, txn, roots); err != nil { return nil, err } @@ -774,7 +769,6 @@ func readModuleInfoFromStore(ctx context.Context, store storage.Store, txn stora } func erasePolicies(ctx context.Context, store storage.Store, txn storage.Transaction, parserOpts ast.ParserOptions, roots map[string]struct{}) (map[string]*ast.Module, []string, error) { - ids, err := store.ListPolicies(ctx, txn) if err != nil { return nil, nil, err @@ -861,8 +855,8 @@ func writeEtagToStore(opts *ActivateOpts, name, etag string) error { } func writeModuleRegoVersionToStore(ctx context.Context, store storage.Store, txn storage.Transaction, b *Bundle, - mf ModuleFile, storagePath string, runtimeRegoVersion ast.RegoVersion) error { - + mf ModuleFile, storagePath string, runtimeRegoVersion ast.RegoVersion, +) error { var regoVersion ast.RegoVersion if mf.Parsed != nil { regoVersion = mf.Parsed.RegoVersion() @@ -965,11 +959,18 @@ func writeData(ctx context.Context, store storage.Store, txn storage.Transaction return nil } -func compileModules(compiler *ast.Compiler, m metrics.Metrics, bundles map[string]*Bundle, extraModules map[string]*ast.Module, legacy bool, authorizationDecisionRef ast.Ref) error { - +func compileModules(compiler *ast.Compiler, m metrics.Metrics, bundles map[string]*Bundle, extraModules map[string]*ast.Module, legacy bool, authorizationDecisionRef ast.Ref, externalSources *util.HasherMap[ast.Ref, ast.ExternalRuleSource]) error { m.Timer(metrics.RegoModuleCompile).Start() defer m.Timer(metrics.RegoModuleCompile).Stop() + // Apply external sources before compilation + if externalSources != nil { + externalSources.Iter(func(ref ast.Ref, source ast.ExternalRuleSource) bool { + compiler = compiler.WithExternalSource(ref, source) + return false + }) + } + modules := make(map[string]*ast.Module, len(compiler.Modules)+len(extraModules)+len(bundles)) // preserve any modules already on the compiler @@ -1000,11 +1001,18 @@ func compileModules(compiler *ast.Compiler, m metrics.Metrics, bundles map[strin return iCompiler.VerifyAuthorizationPolicySchema(compiler, authorizationDecisionRef) } -func writeModules(ctx context.Context, store storage.Store, txn storage.Transaction, compiler *ast.Compiler, m metrics.Metrics, bundles map[string]*Bundle, extraModules map[string]*ast.Module, legacy bool) error { - +func writeModules(ctx context.Context, store storage.Store, txn storage.Transaction, compiler *ast.Compiler, m metrics.Metrics, bundles map[string]*Bundle, extraModules map[string]*ast.Module, legacy bool, externalSources *util.HasherMap[ast.Ref, ast.ExternalRuleSource]) error { m.Timer(metrics.RegoModuleCompile).Start() defer m.Timer(metrics.RegoModuleCompile).Stop() + // Apply external sources before compilation + if externalSources != nil { + externalSources.Iter(func(ref ast.Ref, source ast.ExternalRuleSource) bool { + compiler = compiler.WithExternalSource(ref, source) + return false + }) + } + modules := map[string]*ast.Module{} // preserve any modules already on the compiler @@ -1066,67 +1074,165 @@ func lookup(path storage.Path, data map[string]any) (any, bool) { return value, ok } +// rootEntry is a (bundle, root) pair, used for the +// sort-and-scan algorithm in hasRootsOverlap. +type rootEntry struct { + bundle string + canonical string // cannonicalized bundle root path +} + +// displayRoot is used for cleaner error messages. +func (e rootEntry) displayRoot() string { + return strings.Trim(e.canonical, "/") +} + +// canonicalRoot normalizes a raw bundle root so that string prefix +// matching correctly handles path segments. +// Example: "/a/b/" is a prefix of "/a/b/c/" but not of "/a/bc/". +func canonicalRoot(raw string) string { + trimmed := strings.Trim(raw, "/") + if trimmed == "" { + return "/" + } + return "/" + trimmed + "/" +} + +// appendRootEntries appends one rootEntry per root in the `roots` slice +// to the `entries` slice, returning the updated entries list. Bundles +// declaring an empty root are tracked in `bundlesWithEmptyRoots` for the +// empty-root error-message special case. +func appendRootEntries(entries []rootEntry, bundlesWithEmptyRoots map[string]bool, name string, roots []string) []rootEntry { + for _, raw := range roots { + if raw == "" { + bundlesWithEmptyRoots[name] = true + } + entries = append(entries, rootEntry{ + bundle: name, + canonical: canonicalRoot(raw), + }) + } + return entries +} + +// hasRootsOverlap verifies that the roots declared by the bundles being +// activated do not collide with each other or with any bundle already +// present in the store. +// +// Intuition: We sort all paths lexicographically, then scan forward for +// prefix matches. Prefix matches == collisions. This works because +// shorter path prefixes will always be sorted ahead of any colliding +// longer paths. +// +// We use one loop to scan for exact matches. This forms a group of known +// conflicting paths. We then scan forward from that group until we hit +// a path which does not have our root path as the prefix. We then generate +// the conflict sets for all of the paths in the group against each other, +// and then against any conflicting longer paths. +// +// All conflicts among the post-activation root set are reported. +// Existing bundles in the store are assumed conflict-free with each +// other — an invariant maintained because each was checked against the +// store at its own activation time. Replacing a bundle takes its new +// roots only; the store's stale view of that bundle is discarded +// during setup so it can't produce phantom conflicts against the +// replacement's own new roots. func hasRootsOverlap(ctx context.Context, store storage.Store, txn storage.Transaction, newBundles map[string]*Bundle) error { storeBundles, err := ReadBundleNamesFromStore(ctx, store, txn) if suppressNotFound(err) != nil { return err } - allRoots := map[string][]string{} + // Flatten every root (existing + new) into one entry list. A bundle + // being re-activated takes its roots from newBundles only; the + // store's stale view of that bundle is skipped. bundlesWithEmptyRoots := map[string]bool{} + entries := make([]rootEntry, 0, len(storeBundles)+2*len(newBundles)) - // Build a map of roots for existing bundles already in the system for _, name := range storeBundles { + if _, replaced := newBundles[name]; replaced { + continue + } roots, err := ReadBundleRootsFromStore(ctx, store, txn, name) if suppressNotFound(err) != nil { return err } - allRoots[name] = roots - if slices.Contains(roots, "") { - bundlesWithEmptyRoots[name] = true - } + entries = appendRootEntries(entries, bundlesWithEmptyRoots, name, roots) } - - // Add in any bundles that are being activated, overwrite existing roots - // with new ones where bundles are in both groups. for name, bundle := range newBundles { - allRoots[name] = *bundle.Manifest.Roots - if slices.Contains(*bundle.Manifest.Roots, "") { - bundlesWithEmptyRoots[name] = true - } + entries = appendRootEntries(entries, bundlesWithEmptyRoots, name, *bundle.Manifest.Roots) } - // Now check for each new bundle if it conflicts with any of the others + // Sort the bundle roots list. + sort.Slice(entries, func(i, j int) bool { + if entries[i].canonical != entries[j].canonical { + return entries[i].canonical < entries[j].canonical + } + return entries[i].bundle < entries[j].bundle + }) + collidingBundles := map[string]bool{} conflictSet := map[string]bool{} - for name, bundle := range newBundles { - for otherBundle, otherRoots := range allRoots { - if name == otherBundle { - // Skip the current bundle being checked - continue + groupBundles := map[string]bool{} // reused across iterations via clear() + + // Scan through the sorted list of bundle root paths iteratively, + // identifying conflict groups for later reporting at the end. + // If there's a cluster of exact path matches, we can advance the + // groupStart index to the end of that group after we've accounted + // for the conflicts. + for groupStart := 0; groupStart < len(entries); { + // Identify the group of entries sharing the + // exact same canonical root. + groupEnd := groupStart + 1 + for groupEnd < len(entries) && entries[groupEnd].canonical == entries[groupStart].canonical { + groupEnd++ + } + + clear(groupBundles) // Reuse the conflict group map. + for k := groupStart; k < groupEnd; k++ { + groupBundles[entries[k].bundle] = true + } + + // Same-root conflict: more than one bundle shares this root. + if len(groupBundles) > 1 { + for k := groupStart; k < groupEnd; k++ { + collidingBundles[entries[k].bundle] = true } + conflictSet[fmt.Sprintf("root %s is in multiple bundles", entries[groupStart].displayRoot())] = true + } - // Compare the "new" roots with other existing (or a different bundles new roots) - for _, newRoot := range *bundle.Manifest.Roots { - for _, otherRoot := range otherRoots { - if !RootPathsOverlap(newRoot, otherRoot) { - continue - } + groupCanonical := entries[groupStart].canonical + groupDisplay := entries[groupStart].displayRoot() - collidingBundles[name] = true - collidingBundles[otherBundle] = true + // Descendant conflicts: forward scan while the group's canonical + // root is a prefix of the next entry's canonical root. + for d := groupEnd; d < len(entries); d++ { + // Break if next entry isn't a conflict. + if !strings.HasPrefix(entries[d].canonical, groupCanonical) { + break + } - // Different message required if the roots are same - if newRoot == otherRoot { - conflictSet[fmt.Sprintf("root %s is in multiple bundles", newRoot)] = true - } else { - paths := []string{newRoot, otherRoot} - sort.Strings(paths) - conflictSet[fmt.Sprintf("%s overlaps %s", paths[0], paths[1])] = true - } + // Pair the descendant against every group entry from a different bundle. + sawCrossBundleConflict := false + for k := groupStart; k < groupEnd; k++ { + if entries[k].bundle == entries[d].bundle { + continue } + collidingBundles[entries[k].bundle] = true + sawCrossBundleConflict = true + } + + // Only record a conflict if the descendant overlaps with an + // ancestor declared by a different bundle. A single bundle + // is allowed to declare overlapping roots in its own manifest. + if sawCrossBundleConflict { + collidingBundles[entries[d].bundle] = true + paths := []string{groupDisplay, entries[d].displayRoot()} + sort.Strings(paths) + conflictSet[fmt.Sprintf("%s overlaps %s", paths[0], paths[1])] = true } } + + groupStart = groupEnd } if len(collidingBundles) == 0 { @@ -1192,8 +1298,10 @@ func applyPatches(ctx context.Context, store storage.Store, txn storage.Transact // LegacyManifestStoragePath is the older unnamed bundle path for manifests to be stored. // // Deprecated: Use ManifestStoragePath and named bundles instead. -var legacyManifestStoragePath = storage.MustParsePath("/system/bundle/manifest") -var legacyRevisionStoragePath = append(legacyManifestStoragePath, "revision") +var ( + legacyManifestStoragePath = storage.MustParsePath("/system/bundle/manifest") + legacyRevisionStoragePath = append(legacyManifestStoragePath, "revision") +) // LegacyWriteManifestToStore will write the bundle manifest to the older single (unnamed) bundle manifest location. // diff --git a/vendor/github.com/open-policy-agent/opa/v1/format/format.go b/vendor/github.com/open-policy-agent/opa/v1/format/format.go index c14a6ce798..351ae0a600 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/format/format.go +++ b/vendor/github.com/open-policy-agent/opa/v1/format/format.go @@ -226,6 +226,8 @@ func AstWithOpts(x any, opts Opts) ([]byte, error) { extraFutureKeywordImports["in"] = struct{}{} case n.IsEvery(): extraFutureKeywordImports["every"] = struct{}{} + case n.IsNot(): + extraFutureKeywordImports["not"] = struct{}{} } case *ast.Import: @@ -278,6 +280,7 @@ func AstWithOpts(x any, opts Opts) ([]byte, error) { } regoV1Imported := slices.ContainsFunc(x.Imports, isRegoV1Compatible) + if regoVersion == ast.RegoV0CompatV1 || regoVersion == ast.RegoV1 || regoV1Imported { if !opts.DropV0Imports && !regoV1Imported { for _, kw := range o.futureKeywords { @@ -286,11 +289,18 @@ func AstWithOpts(x any, opts Opts) ([]byte, error) { } else { x.Imports = future.FilterFutureImports(x.Imports) } + + for kw := range extraFutureKeywordImports { + if ast.IsFutureKeywordForRegoVersion(kw, ast.RegoV1) { + x.Imports = ensureFutureKeywordImport(x.Imports, kw) + } + } } else { for kw := range extraFutureKeywordImports { x.Imports = ensureFutureKeywordImport(x.Imports, kw) } } + err := w.writeModule(x) if err != nil { w.errs = append(w.errs, ast.NewError(ast.FormatErr, &ast.Location{}, "%s", err.Error())) @@ -519,6 +529,7 @@ func (w *writer) writePackage(pkg *ast.Package, comments []*ast.Comment) ([]*ast } func (w *writer) writeComments(comments []*ast.Comment) error { + var inMetadataBlock bool for i := range comments { if i > 0 { l, err := locCmp(comments[i], comments[i-1]) @@ -527,9 +538,20 @@ func (w *writer) writeComments(comments []*ast.Comment) error { } if l > 1 { w.blankLine() + inMetadataBlock = false + } else if l == 1 { + // if next comment is a metadata header and previous comment + // was part of a metadata block, add a blank line to separate them + if inMetadataBlock && ast.IsMetadataComment(comments[i]) { + w.blankLine() + } } } + if ast.IsMetadataComment(comments[i]) { + inMetadataBlock = true + } + w.writeLine(comments[i].String()) } @@ -915,7 +937,12 @@ func (w *writer) writeExpr(expr *ast.Expr, comments []*ast.Comment) ([]*ast.Comm return nil, err } case *ast.Every: - comments, err = w.writeEvery(t, comments) + comments, err = w.writeEvery(t, expr.Loc(), comments) + if err != nil { + return nil, err + } + case *ast.Not: + comments, err = w.writeNot(t, expr.Loc(), comments) if err != nil { return nil, err } @@ -931,32 +958,39 @@ func (w *writer) writeExpr(expr *ast.Expr, comments []*ast.Comment) ([]*ast.Comm } } - var indented, down bool - for i, with := range expr.With { - if i == 0 || with.Location.Row == expr.With[i-1].Location.Row { // we're on the same line - comments, err = w.writeWith(with, comments, false) - if err != nil { - return nil, err - } - } else { // we're on a new line - if !indented { - indented = true + if len(expr.With) == 0 { + return comments, nil + } - w.up() - down = true - } - w.endLine() - w.startLine() - comments, err = w.writeWith(with, comments, true) - if err != nil { - return nil, err - } + withs := expr.With + lastRow := expr.Location.Row + + // Print on same row if already there, otherwise increase indent a print remaining + if withs[0].Location.Row == lastRow { + if comments, err = w.writeWith(withs[0], comments, false); err != nil { + return comments, err } + lastRow, withs = withs[0].Location.Row, withs[1:] } - if down { - if err := w.down(); err != nil { - return nil, err + if len(withs) > 0 { + var indented bool + + for _, with := range withs { + indent := with.Location.Row > lastRow + if indent { + if !indented { + w.up() + defer w.down() //nolint:errcheck + indented = true + } + w.endLine() + w.startLine() + lastRow = with.Location.Row + } + if comments, err = w.writeWith(with, comments, indent); err != nil { + return comments, err + } } } @@ -1004,9 +1038,13 @@ func (w *writer) writeSomeDecl(decl *ast.SomeDecl, comments []*ast.Comment) ([]* return comments, nil } -func (w *writer) writeEvery(every *ast.Every, comments []*ast.Comment) ([]*ast.Comment, error) { +func (w *writer) writeEvery(every *ast.Every, loc *ast.Location, comments []*ast.Comment) ([]*ast.Comment, error) { + if loc == nil { + loc = every.Loc() + } + var err error - comments, err = w.insertComments(comments, every.Location) + comments, err = w.insertComments(comments, loc) if err != nil { return nil, err } @@ -1028,7 +1066,7 @@ func (w *writer) writeEvery(every *ast.Every, comments []*ast.Comment) ([]*ast.C return nil, err } w.write(" {") - comments, err = w.writeComprehensionBody('{', '}', every.Body, every.Loc(), every.Loc(), comments) + comments, err = w.writeComprehensionBody('{', '}', every.Body, loc, loc, comments) if err != nil { // the unexpected comment error is passed up to be handled by writeHead if !errors.As(err, &unexpectedCommentError{}) { @@ -1044,6 +1082,45 @@ func (w *writer) writeEvery(every *ast.Every, comments []*ast.Comment) ([]*ast.C return comments, nil } +func (w *writer) writeNot(not *ast.Not, loc *ast.Location, comments []*ast.Comment) ([]*ast.Comment, error) { + if loc == nil { + loc = not.Loc() + } + + var err error + comments, err = w.insertComments(comments, loc) + if err != nil { + return nil, err + } + + w.write("not ") + + if not.ExplicitBody || len(not.Body) > 1 { + w.write("{") + comments, err = w.writeComprehensionBody('{', '}', not.Body, loc, loc, comments) + if err != nil { + if !errors.As(err, &unexpectedCommentError{}) { + return nil, err + } + } + + if len(not.Body) == 1 && + not.Body[0].Location.Row == loc.Row { + w.write(" ") + } + w.write("}") + } else { + comments, err = w.writeExpr(not.Body[0], comments) + if err != nil { + if !errors.As(err, &unexpectedCommentError{}) { + return nil, err + } + } + } + + return comments, nil +} + func (w *writer) writeFunctionCall(expr *ast.Expr, comments []*ast.Comment) ([]*ast.Comment, error) { terms := expr.Terms.([]*ast.Term) @@ -1135,7 +1212,7 @@ func (w *writer) writeWith(with *ast.With, comments []*ast.Comment, indented boo w.write(" as ") comments, err = w.writeTerm(with.Value, comments) if err != nil { - return nil, err + return comments, err } return comments, nil } @@ -1158,6 +1235,7 @@ func (w *writer) writeTerm(term *ast.Term, comments []*ast.Comment) ([]*ast.Comm } currentLen := w.buf.Len() + currentLevel := w.level currentComments := saveComments(comments) defer commentsSlicePool.Put(currentComments) @@ -1165,6 +1243,16 @@ func (w *writer) writeTerm(term *ast.Term, comments []*ast.Comment) ([]*ast.Comm if err != nil { if errors.As(err, &unexpectedCommentError{}) { w.buf.Truncate(currentLen) + w.level = currentLevel + + // If beforeEnd refers to a comment within the source text range, clear it + // This prevents the comment from being written twice + if w.beforeEnd != nil && len(term.Location.Text) > 0 { + endRow := term.Location.Row + bytes.Count(term.Location.Text, []byte{'\n'}) + if w.beforeEnd.Location.Row >= term.Location.Row && w.beforeEnd.Location.Row <= endRow { + w.beforeEnd = nil + } + } comments, uErr := w.writeUnformatted(term.Location, *currentComments) if uErr != nil { @@ -1750,6 +1838,9 @@ func (w *writer) writeImport(imp *ast.Import) error { // We don't want to wrap future.keywords imports in parens, so we create a new writer that doesn't w2 := writer{ buf: bytes.Buffer{}, + fmtOpts: fmtOpts{ + allowKeywordsInRefs: true, + }, } _, err := w2.writeRef(path, nil) if err != nil { @@ -1777,6 +1868,20 @@ func (w *writer) writeIterable(elements []any, last *ast.Location, close *ast.Lo if err != nil { return nil, err } + + // If there are comments within the single line, don't collapse it and keep it as-is + // Return an error so that writeTerm will write the original formatting + if len(lines) == 1 { + for _, c := range comments { + if c.Location.Row > last.Row && c.Location.Row < close.Row { + return comments, unexpectedCommentError{ + newComment: truncatedString(c.String(), 100), + newCommentRow: c.Location.Row, + } + } + } + } + if len(lines) > 1 { w.delayBeforeEnd() w.startMultilineSeq() @@ -2295,17 +2400,35 @@ func ensureFutureKeywordImport(imps []*ast.Import, kw string) []*ast.Import { } } imp := &ast.Import{ - // NOTE: This is a hack to not error on the ref containing a keyword already present in v1. - // A cleaner solution would be to instead allow refs to contain keyword terms. - // E.g. in v1, `import future.keywords["in"]` is valid, but `import future.keywords.in` is not - // as it contains a reserved keyword. - Path: ast.MustParseTerm("future.keywords[\"" + kw + "\"]"), - //Path: ast.MustParseTerm("future.keywords." + kw), - } - imp.Location = defaultLocation(imp) + Path: ast.MustParseTerm("future.keywords." + kw), + } + imp.Location = nextImportLoc(imps, imp) return append(imps, imp) } +func nextImportLoc(imps []*ast.Import, node ast.Node) *ast.Location { + maxRow := 0 + for _, imp := range imps { + if imp.Loc() == nil { + continue + } + if isFutureKeywordsImport(imp) || isRegoV1Compatible(imp) { + if imp.Loc().Row > maxRow { + maxRow = imp.Loc().Row + } + } + } + if maxRow == 0 { + return defaultLocation(node) + } + return ast.NewLocation([]byte(node.String()), defaultLocationFile, maxRow+1, 1) +} + +func isFutureKeywordsImport(imp *ast.Import) bool { + path := imp.Path.Value.(ast.Ref) + return len(path) >= 2 && ast.FutureRootDocument.Equal(path[0]) +} + func ensureRegoV1Import(imps []*ast.Import) []*ast.Import { return ensureImport(imps, ast.RegoV1CompatibleRef) } @@ -2331,7 +2454,7 @@ func ensureImport(imps []*ast.Import, path ast.Ref) []*ast.Import { imp := &ast.Import{ Path: ast.NewTerm(path), } - imp.Location = defaultLocation(imp) + imp.Location = nextImportLoc(imps, imp) return append(imps, imp) } diff --git a/vendor/github.com/open-policy-agent/opa/v1/ir/marshal.go b/vendor/github.com/open-policy-agent/opa/v1/ir/marshal.go index f792e2c1b6..f395fb2b62 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/ir/marshal.go +++ b/vendor/github.com/open-policy-agent/opa/v1/ir/marshal.go @@ -103,6 +103,53 @@ type typedOperand struct { Value Val `json:"value"` } +// MarshalJSON for MakeNumberRefStmt emits both "index" (the canonical key, +// matching the casing of every other field in the IR) and "Index" (the +// historical key, kept for backwards compatibility with consumers that +// hard-code the original spelling). The "Index" key is deprecated and will +// be removed in a future major release; new consumers should read "index". +func (m *MakeNumberRefStmt) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + File int `json:"file"` + Col int `json:"col"` + Row int `json:"row"` + Index int `json:"index"` + IndexLegacy int `json:"Index"` // deprecated; remove in next major + Target Local `json:"target"` + }{ + File: m.File, + Col: m.Col, + Row: m.Row, + Index: m.Index, + IndexLegacy: m.Index, + Target: m.Target, + }) +} + +// UnmarshalJSON for MakeNumberRefStmt accepts either the canonical "index" +// key or the deprecated "Index" key. When both are present, "index" wins. +func (m *MakeNumberRefStmt) UnmarshalJSON(bs []byte) error { + var raw struct { + File int `json:"file"` + Col int `json:"col"` + Row int `json:"row"` + Index *int `json:"index"` + IndexLegacy *int `json:"Index"` + Target Local `json:"target"` + } + if err := json.Unmarshal(bs, &raw); err != nil { + return err + } + m.File, m.Col, m.Row, m.Target = raw.File, raw.Col, raw.Row, raw.Target + switch { + case raw.Index != nil: + m.Index = *raw.Index + case raw.IndexLegacy != nil: + m.Index = *raw.IndexLegacy + } + return nil +} + var stmtFactories = map[string]func() Stmt{ "ReturnLocalStmt": func() Stmt { return &ReturnLocalStmt{} }, "CallStmt": func() Stmt { return &CallStmt{} }, @@ -145,3 +192,25 @@ var valFactories = map[string]func() Val{ "string_index": func() Val { var x StringIndex; return &x }, "local": func() Val { var x Local; return &x }, } + +// StmtKinds returns a fresh zero-value instance of every registered Stmt +// kind, keyed by the discriminator string used in the JSON form. Useful for +// tools (schema generators, linters, transformers) that need to walk the +// IR's polymorphic Stmt universe without depending on package internals. +func StmtKinds() map[string]Stmt { + out := make(map[string]Stmt, len(stmtFactories)) + for k, f := range stmtFactories { + out[k] = f() + } + return out +} + +// ValKinds returns a fresh zero-value instance of every registered Val kind, +// keyed by the discriminator string. See StmtKinds for usage notes. +func ValKinds() map[string]Val { + out := make(map[string]Val, len(valFactories)) + for k, f := range valFactories { + out[k] = f() + } + return out +} diff --git a/vendor/github.com/open-policy-agent/opa/v1/ir/plan.schema.json b/vendor/github.com/open-policy-agent/opa/v1/ir/plan.schema.json new file mode 100644 index 0000000000..e4af18ba23 --- /dev/null +++ b/vendor/github.com/open-policy-agent/opa/v1/ir/plan.schema.json @@ -0,0 +1,1782 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://openpolicyagent.org/schemas/ir/v1/plan.schema.json", + "title": "OPA IR Plan", + "description": "JSON Schema for the IR plan produced by `opa build -t plan`. Generated from v1/ir/ir.go.", + "$ref": "#/$defs/Policy", + "$defs": { + "ArrayAppendStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "array": { + "type": "integer" + }, + "value": { + "$ref": "#/$defs/Operand" + } + }, + "required": [ + "array", + "col", + "file", + "row", + "value" + ], + "additionalProperties": false + }, + "AssignIntStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "target": { + "type": "integer" + }, + "value": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "row", + "target", + "value" + ], + "additionalProperties": false + }, + "AssignVarOnceStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "source": { + "$ref": "#/$defs/Operand" + }, + "target": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "row", + "source", + "target" + ], + "additionalProperties": false + }, + "AssignVarStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "source": { + "$ref": "#/$defs/Operand" + }, + "target": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "row", + "source", + "target" + ], + "additionalProperties": false + }, + "Block": { + "type": "object", + "properties": { + "stmts": { + "type": "array", + "items": { + "$ref": "#/$defs/Stmt" + } + } + }, + "required": [ + "stmts" + ], + "additionalProperties": false + }, + "BlockStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "blocks": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/Block" + } + } + }, + "required": [ + "blocks", + "col", + "file", + "row" + ], + "additionalProperties": false + }, + "BreakStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "index": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "index", + "row" + ], + "additionalProperties": false + }, + "BuiltinFunc": { + "type": "object", + "properties": { + "decl": { + "type": [ + "object", + "null" + ], + "description": "BuiltinFunc declaration; opaque in this schema." + }, + "name": { + "type": "string" + } + }, + "required": [ + "decl", + "name" + ], + "additionalProperties": false + }, + "CallDynamicStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "args": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer" + } + }, + "path": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/Operand" + } + }, + "result": { + "type": "integer" + } + }, + "required": [ + "args", + "col", + "file", + "path", + "result", + "row" + ], + "additionalProperties": false + }, + "CallStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "args": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/Operand" + } + }, + "func": { + "type": "string" + }, + "result": { + "type": "integer" + } + }, + "required": [ + "args", + "col", + "file", + "func", + "result", + "row" + ], + "additionalProperties": false + }, + "DotStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "key": { + "$ref": "#/$defs/Operand" + }, + "source": { + "$ref": "#/$defs/Operand" + }, + "target": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "key", + "row", + "source", + "target" + ], + "additionalProperties": false + }, + "EqualStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "a": { + "$ref": "#/$defs/Operand" + }, + "b": { + "$ref": "#/$defs/Operand" + } + }, + "required": [ + "a", + "b", + "col", + "file", + "row" + ], + "additionalProperties": false + }, + "Func": { + "type": "object", + "properties": { + "blocks": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/Block" + } + }, + "name": { + "type": "string" + }, + "params": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer" + } + }, + "path": { + "type": "array", + "items": { + "type": "string" + } + }, + "return": { + "type": "integer" + } + }, + "required": [ + "blocks", + "name", + "params", + "return" + ], + "additionalProperties": false + }, + "Funcs": { + "type": "object", + "properties": { + "funcs": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/Func" + } + } + }, + "required": [ + "funcs" + ], + "additionalProperties": false + }, + "IsArrayStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "source": { + "$ref": "#/$defs/Operand" + } + }, + "required": [ + "col", + "file", + "row", + "source" + ], + "additionalProperties": false + }, + "IsDefinedStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "source": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "row", + "source" + ], + "additionalProperties": false + }, + "IsObjectStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "source": { + "$ref": "#/$defs/Operand" + } + }, + "required": [ + "col", + "file", + "row", + "source" + ], + "additionalProperties": false + }, + "IsSetStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "source": { + "$ref": "#/$defs/Operand" + } + }, + "required": [ + "col", + "file", + "row", + "source" + ], + "additionalProperties": false + }, + "IsUndefinedStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "source": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "row", + "source" + ], + "additionalProperties": false + }, + "LenStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "source": { + "$ref": "#/$defs/Operand" + }, + "target": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "row", + "source", + "target" + ], + "additionalProperties": false + }, + "MakeArrayStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "capacity": { + "type": "integer" + }, + "target": { + "type": "integer" + } + }, + "required": [ + "capacity", + "col", + "file", + "row", + "target" + ], + "additionalProperties": false + }, + "MakeNullStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "target": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "row", + "target" + ], + "additionalProperties": false + }, + "MakeNumberIntStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "target": { + "type": "integer" + }, + "value": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "row", + "target", + "value" + ], + "additionalProperties": false + }, + "MakeNumberRefStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "index": { + "type": "integer" + }, + "Index": { + "type": "integer", + "deprecated": true, + "description": "Deprecated alias for `index`. Both keys are emitted by current OPA versions for backwards compatibility; will be removed in a future major release. Read `index` instead." + }, + "target": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "index", + "row", + "target" + ], + "additionalProperties": false + }, + "MakeObjectStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "target": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "row", + "target" + ], + "additionalProperties": false + }, + "MakeSetStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "target": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "row", + "target" + ], + "additionalProperties": false + }, + "NopStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "row" + ], + "additionalProperties": false + }, + "NotEqualStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "a": { + "$ref": "#/$defs/Operand" + }, + "b": { + "$ref": "#/$defs/Operand" + } + }, + "required": [ + "a", + "b", + "col", + "file", + "row" + ], + "additionalProperties": false + }, + "NotStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "block": { + "oneOf": [ + { + "$ref": "#/$defs/Block" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "block", + "col", + "file", + "row" + ], + "additionalProperties": false + }, + "ObjectInsertOnceStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "key": { + "$ref": "#/$defs/Operand" + }, + "object": { + "type": "integer" + }, + "value": { + "$ref": "#/$defs/Operand" + } + }, + "required": [ + "col", + "file", + "key", + "object", + "row", + "value" + ], + "additionalProperties": false + }, + "ObjectInsertStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "key": { + "$ref": "#/$defs/Operand" + }, + "object": { + "type": "integer" + }, + "value": { + "$ref": "#/$defs/Operand" + } + }, + "required": [ + "col", + "file", + "key", + "object", + "row", + "value" + ], + "additionalProperties": false + }, + "ObjectMergeStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "a": { + "type": "integer" + }, + "b": { + "type": "integer" + }, + "target": { + "type": "integer" + } + }, + "required": [ + "a", + "b", + "col", + "file", + "row", + "target" + ], + "additionalProperties": false + }, + "Operand": { + "$ref": "#/$defs/Val" + }, + "Plan": { + "type": "object", + "properties": { + "blocks": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/Block" + } + }, + "name": { + "type": "string" + } + }, + "required": [ + "blocks", + "name" + ], + "additionalProperties": false + }, + "Plans": { + "type": "object", + "properties": { + "plans": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/Plan" + } + } + }, + "required": [ + "plans" + ], + "additionalProperties": false + }, + "Policy": { + "type": "object", + "properties": { + "funcs": { + "$ref": "#/$defs/Funcs" + }, + "plans": { + "$ref": "#/$defs/Plans" + }, + "static": { + "$ref": "#/$defs/Static" + } + }, + "additionalProperties": false + }, + "ResetLocalStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "target": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "row", + "target" + ], + "additionalProperties": false + }, + "ResultSetAddStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "value": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "row", + "value" + ], + "additionalProperties": false + }, + "ReturnLocalStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "source": { + "type": "integer" + } + }, + "required": [ + "col", + "file", + "row", + "source" + ], + "additionalProperties": false + }, + "ScanStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "block": { + "oneOf": [ + { + "$ref": "#/$defs/Block" + }, + { + "type": "null" + } + ] + }, + "key": { + "type": "integer" + }, + "source": { + "type": "integer" + }, + "value": { + "type": "integer" + } + }, + "required": [ + "block", + "col", + "file", + "key", + "row", + "source", + "value" + ], + "additionalProperties": false + }, + "SetAddStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "set": { + "type": "integer" + }, + "value": { + "$ref": "#/$defs/Operand" + } + }, + "required": [ + "col", + "file", + "row", + "set", + "value" + ], + "additionalProperties": false + }, + "Static": { + "type": "object", + "properties": { + "builtin_funcs": { + "type": "array", + "items": { + "$ref": "#/$defs/BuiltinFunc" + } + }, + "files": { + "type": "array", + "items": { + "$ref": "#/$defs/StringConst" + } + }, + "strings": { + "type": "array", + "items": { + "$ref": "#/$defs/StringConst" + } + } + }, + "additionalProperties": false + }, + "Stmt": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "ArrayAppendStmt" + }, + "stmt": { + "$ref": "#/$defs/ArrayAppendStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "AssignIntStmt" + }, + "stmt": { + "$ref": "#/$defs/AssignIntStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "AssignVarOnceStmt" + }, + "stmt": { + "$ref": "#/$defs/AssignVarOnceStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "AssignVarStmt" + }, + "stmt": { + "$ref": "#/$defs/AssignVarStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "BlockStmt" + }, + "stmt": { + "$ref": "#/$defs/BlockStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "BreakStmt" + }, + "stmt": { + "$ref": "#/$defs/BreakStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "CallDynamicStmt" + }, + "stmt": { + "$ref": "#/$defs/CallDynamicStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "CallStmt" + }, + "stmt": { + "$ref": "#/$defs/CallStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "DotStmt" + }, + "stmt": { + "$ref": "#/$defs/DotStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "EqualStmt" + }, + "stmt": { + "$ref": "#/$defs/EqualStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "IsArrayStmt" + }, + "stmt": { + "$ref": "#/$defs/IsArrayStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "IsDefinedStmt" + }, + "stmt": { + "$ref": "#/$defs/IsDefinedStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "IsObjectStmt" + }, + "stmt": { + "$ref": "#/$defs/IsObjectStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "IsSetStmt" + }, + "stmt": { + "$ref": "#/$defs/IsSetStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "IsUndefinedStmt" + }, + "stmt": { + "$ref": "#/$defs/IsUndefinedStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "LenStmt" + }, + "stmt": { + "$ref": "#/$defs/LenStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "MakeArrayStmt" + }, + "stmt": { + "$ref": "#/$defs/MakeArrayStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "MakeNullStmt" + }, + "stmt": { + "$ref": "#/$defs/MakeNullStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "MakeNumberIntStmt" + }, + "stmt": { + "$ref": "#/$defs/MakeNumberIntStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "MakeNumberRefStmt" + }, + "stmt": { + "$ref": "#/$defs/MakeNumberRefStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "MakeObjectStmt" + }, + "stmt": { + "$ref": "#/$defs/MakeObjectStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "MakeSetStmt" + }, + "stmt": { + "$ref": "#/$defs/MakeSetStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "NopStmt" + }, + "stmt": { + "$ref": "#/$defs/NopStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "NotEqualStmt" + }, + "stmt": { + "$ref": "#/$defs/NotEqualStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "NotStmt" + }, + "stmt": { + "$ref": "#/$defs/NotStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "ObjectInsertOnceStmt" + }, + "stmt": { + "$ref": "#/$defs/ObjectInsertOnceStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "ObjectInsertStmt" + }, + "stmt": { + "$ref": "#/$defs/ObjectInsertStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "ObjectMergeStmt" + }, + "stmt": { + "$ref": "#/$defs/ObjectMergeStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "ResetLocalStmt" + }, + "stmt": { + "$ref": "#/$defs/ResetLocalStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "ResultSetAddStmt" + }, + "stmt": { + "$ref": "#/$defs/ResultSetAddStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "ReturnLocalStmt" + }, + "stmt": { + "$ref": "#/$defs/ReturnLocalStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "ScanStmt" + }, + "stmt": { + "$ref": "#/$defs/ScanStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "SetAddStmt" + }, + "stmt": { + "$ref": "#/$defs/SetAddStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "WithStmt" + }, + "stmt": { + "$ref": "#/$defs/WithStmt" + } + }, + "required": [ + "type", + "stmt" + ], + "additionalProperties": false + } + ] + }, + "StringConst": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "required": [ + "value" + ], + "additionalProperties": false + }, + "Val": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "bool" + }, + "value": { + "type": "boolean" + } + }, + "required": [ + "type", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "local" + }, + "value": { + "type": "integer" + } + }, + "required": [ + "type", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "string_index" + }, + "value": { + "type": "integer" + } + }, + "required": [ + "type", + "value" + ], + "additionalProperties": false + } + ] + }, + "WithStmt": { + "type": "object", + "properties": { + "col": { + "type": "integer" + }, + "file": { + "type": "integer" + }, + "row": { + "type": "integer" + }, + "block": { + "oneOf": [ + { + "$ref": "#/$defs/Block" + }, + { + "type": "null" + } + ] + }, + "local": { + "type": "integer" + }, + "path": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer" + } + }, + "value": { + "$ref": "#/$defs/Operand" + } + }, + "required": [ + "block", + "col", + "file", + "local", + "path", + "row", + "value" + ], + "additionalProperties": false + } + } +} diff --git a/vendor/github.com/open-policy-agent/opa/v1/logging/buffered_logger.go b/vendor/github.com/open-policy-agent/opa/v1/logging/buffered_logger.go index 1fbd560035..3aaaac0973 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/logging/buffered_logger.go +++ b/vendor/github.com/open-policy-agent/opa/v1/logging/buffered_logger.go @@ -16,12 +16,15 @@ type logEntry struct { // BufferedLogger captures log entries in memory until Flush is called, // at which point it replays all buffered entries to the target logger. -// After Flush() is called, the BufferedLogger should not be used anymore. +// After Flush, subsequent log calls are forwarded to the flush target. +// This ensures that code holding a cached reference to the BufferedLogger +// (or a WithFields derivative) continues to work correctly. type BufferedLogger struct { mu sync.Mutex buffer []logEntry maxEntries int currentLevel Level + target Logger } // NewBufferedLogger creates a new buffered logger that will buffer up to maxEntries. @@ -42,20 +45,38 @@ func (b *BufferedLogger) addToBuffer(level Level, format string, args []any, fie message = fmt.Sprintf(format, args...) } - entry := logEntry{ - level: level, - message: message, - fields: fields, - time: time.Now(), - } - b.mu.Lock() - defer b.mu.Unlock() + if b.target != nil { + target := b.target + b.mu.Unlock() + + logger := target + if len(fields) > 0 { + logger = target.WithFields(fields) + } + switch level { + case Debug: + logger.Debug("%s", message) + case Info: + logger.Info("%s", message) + case Warn: + logger.Warn("%s", message) + case Error: + logger.Error("%s", message) + } + return + } if len(b.buffer) >= b.maxEntries { b.buffer = b.buffer[1:] } - b.buffer = append(b.buffer, entry) + b.buffer = append(b.buffer, logEntry{ + level: level, + message: message, + fields: fields, + time: time.Now(), + }) + b.mu.Unlock() } func (*BufferedLogger) logToTarget(target Logger, entry logEntry) { @@ -105,6 +126,9 @@ func (b *BufferedLogger) WithFields(fields map[string]any) Logger { func (b *BufferedLogger) GetLevel() Level { b.mu.Lock() defer b.mu.Unlock() + if b.target != nil { + return b.target.GetLevel() + } return b.currentLevel } @@ -113,6 +137,9 @@ func (b *BufferedLogger) SetLevel(level Level) { b.mu.Lock() defer b.mu.Unlock() b.currentLevel = level + if b.target != nil { + b.target.SetLevel(level) + } } // Close discards all buffered entries without flushing them. @@ -124,14 +151,15 @@ func (b *BufferedLogger) Close() { } // Flush replays all buffered entries to the target logger. -// After calling Flush, the BufferedLogger should not be used anymore. -// The caller should switch to using the target logger directly. +// After Flush, subsequent log calls on this BufferedLogger (and any +// previously obtained WithFields loggers) are forwarded to the target. func (b *BufferedLogger) Flush(targetLogger Logger) { if targetLogger == nil { return } b.mu.Lock() + b.target = targetLogger targetLogger.SetLevel(b.currentLevel) entries := b.buffer b.buffer = nil diff --git a/vendor/github.com/open-policy-agent/opa/v1/rego/rego.go b/vendor/github.com/open-policy-agent/opa/v1/rego/rego.go index a69dba1bb8..6f2a824cb5 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/rego/rego.go +++ b/vendor/github.com/open-policy-agent/opa/v1/rego/rego.go @@ -100,6 +100,7 @@ type EvalContext struct { parsedInput ast.Value metrics metrics.Metrics txn storage.Transaction + generateJSON func(*ast.Term, *EvalContext) (any, error) instrument bool instrumentation *topdown.Instrumentation partialNamespace string @@ -125,6 +126,9 @@ type EvalContext struct { baseCache topdown.BaseCache tracing tracing.Options externalCancel topdown.Cancel // Note(philip): If non-nil, the cancellation is handled outside of this package. + requestMetadata map[string]any + responseMetadata map[string]any + evaluated *topdown.EvaluatedRuleTracker } func (e *EvalContext) RawInput() *any { @@ -224,6 +228,15 @@ func EvalTransaction(txn storage.Transaction) EvalOption { } } +// EvalGenerateJSON sets the AST to JSON converter for an evaluation. When set, this +// option takes precedence over [GenerateJSON] set on the Rego object from e.g. a prepared +// query, allowing individual evaluations to customize how the result is transformed. +func EvalGenerateJSON(f func(*ast.Term, *EvalContext) (any, error)) EvalOption { + return func(e *EvalContext) { + e.generateJSON = f + } +} + // EvalInstrument enables or disables instrumenting for a Prepared Query's evaluation func EvalInstrument(instrument bool) EvalOption { return func(e *EvalContext) { @@ -408,6 +421,31 @@ func EvalExternalCancel(ec topdown.Cancel) EvalOption { } } +// EvalRequestMetadata sets arbitrary metadata from the caller that can be +// passed through to the evaluation. This allows wrapping projects to attach +// custom metadata to queries. +func EvalRequestMetadata(m map[string]any) EvalOption { + return func(e *EvalContext) { + e.requestMetadata = m + } +} + +// EvalResponseMetadata sets a map that wrapping projects can populate during +// evaluation to include additional fields in the API response. +func EvalResponseMetadata(m map[string]any) EvalOption { + return func(e *EvalContext) { + e.responseMetadata = m + } +} + +// EvalEvaluatedRuleTracker sets a tracker to record rule identifiers that +// were successfully evaluated during query evaluation. +func EvalEvaluatedRuleTracker(t *topdown.EvaluatedRuleTracker) EvalOption { + return func(e *EvalContext) { + e.evaluated = t + } +} + func (pq preparedQuery) Modules() map[string]*ast.Module { mods := make(map[string]*ast.Module) @@ -654,6 +692,7 @@ type Rego struct { strictBuiltinErrors bool builtinErrorList *[]topdown.Error resolvers []refResolver + externalSources []ast.ExternalRuleSource schemaSet *ast.SchemaSet target string // target type (wasm, rego, etc.) opa opa.EvalEngine @@ -667,6 +706,7 @@ type Rego struct { compilerHook func(*ast.Compiler) evalMode *ast.CompilerEvalMode filter filter.LoaderFilter + evaluated *topdown.EvaluatedRuleTracker } func (r *Rego) RegoVersion() ast.RegoVersion { @@ -1268,6 +1308,15 @@ func Resolver(ref ast.Ref, r resolver.Resolver) func(r *Rego) { } } +// ExternalSource adds an external rule source that provides rules dynamically. +// The source declares which package refs it handles via its Refs() method. +// A single source can provide rules for multiple packages. +func ExternalSource(source ast.ExternalRuleSource) func(r *Rego) { + return func(rego *Rego) { + rego.externalSources = append(rego.externalSources, source) + } +} + // Schemas sets the schemaSet func Schemas(x *ast.SchemaSet) func(r *Rego) { return func(r *Rego) { @@ -1291,7 +1340,10 @@ func Target(t string) func(r *Rego) { } } -// GenerateJSON sets the AST to JSON converter for the results. +// GenerateJSON sets the AST to JSON converter to use for results. This will have any evaluationn on a Rego +// object use the provided function for conversion. Use [EvalGenerateJSON] if you want to set a converter +// function for individual evaluations, which will take precedence over GenerateJSON set on the Rego object, +// (i.e. by this function) for the scope of that evaluation. func GenerateJSON(f func(*ast.Term, *EvalContext) (any, error)) func(r *Rego) { return func(r *Rego) { r.generateJSON = f @@ -1350,6 +1402,14 @@ func EvalMode(mode ast.CompilerEvalMode) func(r *Rego) { } } +// EvaluatedRuleTracker returns an option that sets a tracker to record rule +// identifiers that were successfully evaluated during query evaluation. +func EvaluatedRuleTracker(t *topdown.EvaluatedRuleTracker) func(r *Rego) { + return func(r *Rego) { + r.evaluated = t + } +} + // New returns a new Rego object. func New(options ...func(r *Rego)) *Rego { r := &Rego{ @@ -2123,6 +2183,18 @@ func parserOptionsFromRegoVersionImport(imports []*ast.Import, popts ast.ParserO } func (r *Rego) compileModules(ctx context.Context, txn storage.Transaction, m metrics.Metrics) error { + if len(r.externalSources) > 0 && r.target != "" && r.target != targetRego { + return fmt.Errorf("external rule sources are not supported with target %q: only the default (rego) target is supported", r.target) + } + + // Apply external sources to the compiler before compilation + for i := range r.externalSources { + source := r.externalSources[i] + for _, ref := range source.Refs() { + r.compiler.WithExternalSource(ref, source) + } + } + // Only compile again if there are new modules. if len(r.bundles) > 0 || len(r.parsedModules) > 0 { @@ -2271,7 +2343,15 @@ func (r *Rego) eval(ctx context.Context, ectx *EvalContext) (ResultSet, error) { WithPrintHook(ectx.printHook). WithDistributedTracingOpts(r.distributedTracingOpts). WithVirtualCache(ectx.virtualCache). - WithBaseCache(ectx.baseCache) + WithBaseCache(ectx.baseCache). + WithRequestMetadata(ectx.requestMetadata). + WithResponseMetadata(ectx.responseMetadata) + + if ectx.evaluated != nil { + q = q.WithEvaluatedRuleTracker(ectx.evaluated) + } else { + q = q.WithEvaluatedRuleTracker(r.evaluated) + } if !ectx.time.IsZero() { q = q.WithTime(ectx.time) @@ -2395,6 +2475,11 @@ func (r *Rego) valueToQueryResult(res ast.Value, ectx *EvalContext) (ResultSet, func (r *Rego) generateResult(qr topdown.QueryResult, ectx *EvalContext) (Result, error) { rewritten := ectx.compiledQuery.compiler.RewrittenVars() + generateJSON := r.generateJSON + if ectx.generateJSON != nil { + generateJSON = ectx.generateJSON + } + result := newResult() for k, term := range qr { if rw, ok := rewritten[k]; ok { @@ -2404,7 +2489,7 @@ func (r *Rego) generateResult(qr topdown.QueryResult, ectx *EvalContext) (Result continue } - v, err := r.generateJSON(term, ectx) + v, err := generateJSON(term, ectx) if err != nil { return result, err } @@ -2418,7 +2503,7 @@ func (r *Rego) generateResult(qr topdown.QueryResult, ectx *EvalContext) (Result } if k, ok := r.capture[expr]; ok { - v, err := r.generateJSON(qr[k], ectx) + v, err := generateJSON(qr[k], ectx) if err != nil { return result, err } @@ -2565,7 +2650,9 @@ func (r *Rego) partial(ctx context.Context, ectx *EvalContext) (*PartialQueries, WithInterQueryBuiltinValueCache(ectx.interQueryBuiltinValueCache). WithStrictBuiltinErrors(ectx.strictBuiltinErrors). WithSeed(ectx.seed). - WithPrintHook(ectx.printHook) + WithPrintHook(ectx.printHook). + WithRequestMetadata(ectx.requestMetadata). + WithResponseMetadata(ectx.responseMetadata) if !ectx.time.IsZero() { q = q.WithTime(ectx.time) @@ -2616,31 +2703,31 @@ func (r *Rego) partial(ctx context.Context, ectx *EvalContext) (*PartialQueries, for i, mod := range support { // We can't apply the RegoV0CompatV1 version to the support module if it contains rules or vars that // conflict with future keywords. - applyRegoVersion := true + applyCompatRegoVersion := true ast.WalkRules(mod, func(r *ast.Rule) bool { name := r.Head.Name if name == "" && len(r.Head.Reference) > 0 { name = r.Head.Reference[0].Value.(ast.Var) } - if ast.IsFutureKeywordForRegoVersion(name.String(), ast.RegoV0) { - applyRegoVersion = false + if ast.IsFutureKeywordForRegoVersion(name.String(), ast.RegoV0) && !ast.IsFutureKeywordForRegoVersion(name.String(), ast.RegoV1) { + applyCompatRegoVersion = false return true } return false }) - if applyRegoVersion { + if applyCompatRegoVersion { ast.WalkVars(mod, func(v ast.Var) bool { - if ast.IsFutureKeywordForRegoVersion(v.String(), ast.RegoV0) { - applyRegoVersion = false + if ast.IsFutureKeywordForRegoVersion(v.String(), ast.RegoV0) && !ast.IsFutureKeywordForRegoVersion(v.String(), ast.RegoV1) { + applyCompatRegoVersion = false return true } return false }) } - if applyRegoVersion { + if applyCompatRegoVersion { support[i].SetRegoVersion(ast.RegoV0CompatV1) } else { support[i].SetRegoVersion(r.regoVersion) diff --git a/vendor/github.com/open-policy-agent/opa/v1/resolver/wasm/wasm.go b/vendor/github.com/open-policy-agent/opa/v1/resolver/wasm/wasm.go index 884e4ca7cc..25c01300a8 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/resolver/wasm/wasm.go +++ b/vendor/github.com/open-policy-agent/opa/v1/resolver/wasm/wasm.go @@ -16,8 +16,16 @@ import ( ) // New creates a new Resolver instance which is using the Wasm module -// policy for the given entrypoint ref. +// policy for the given entrypoint ref. This method creates a new +// background context. If you need to pass an existing context use +// NewWithContext instead. func New(entrypoints []ast.Ref, policy []byte, data any) (*Resolver, error) { + return NewWithContext(context.Background(), entrypoints, policy, data) +} + +// NewWithContext creates a new Resolver instance which is using the Wasm module +// policy for the given entrypoint ref. This method accepts a context. +func NewWithContext(ctx context.Context, entrypoints []ast.Ref, policy []byte, data any) (*Resolver, error) { e, err := opa.LookupEngine("wasm") if err != nil { return nil, err @@ -37,7 +45,7 @@ func New(entrypoints []ast.Ref, policy []byte, data any) (*Resolver, error) { // only the configured ones will be used when Eval() is // called. entrypointRefToID := ast.NewValueMap() - epIDs, err := o.Entrypoints(context.Background()) + epIDs, err := o.Entrypoints(ctx) if err != nil { return nil, err } @@ -137,7 +145,6 @@ func (r *Resolver) RemoveDataPath(ctx context.Context, path []string) error { } func getResult(evalResult *opa.Result) (ast.Value, error) { - parsed, err := ast.ParseTerm(string(evalResult.Result)) if err != nil { return nil, fmt.Errorf("failed to parse wasm result: %s", err) diff --git a/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/inmem.go b/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/inmem.go index 9fa145a051..8aa1fc9e42 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/inmem.go +++ b/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/inmem.go @@ -98,6 +98,21 @@ func NewFromReaderWithOpts(r io.Reader, opts ...Opt) storage.Store { return NewFromObjectWithOpts(data, opts...) } +// NewFromASTObject returns a new in-memory store from the supplied AST object, with +// [OptReturnASTValuesOnRead] enabled. This allows avoiding the overhead of an extra AST +// -> map[string]any -> AST round trip for callers whose data already exists in AST form. +// Note that data passed is **not** copied and it is the responsibility of the caller to +// ensure either that ownership of the data is transferred fully to the store, or when +// that's not possible, that a deep copy of the original data is passed. +func NewFromASTObject(data ast.Object) storage.Store { + return &store{ + data: data, + triggers: map[*handle]storage.TriggerConfig{}, + policies: map[string][]byte{}, + returnASTValuesOnRead: true, + } +} + type store struct { rmu sync.RWMutex // reader-writer lock wmu sync.Mutex // writer lock @@ -304,6 +319,14 @@ func (db *store) Register(_ context.Context, txn storage.Transaction, config sto return h, nil } +func (db *store) MakeDir(_ context.Context, txn storage.Transaction, path storage.Path) error { + underlying, err := db.underlying(txn) + if err != nil { + return err + } + return underlying.makeDir(path) +} + func (db *store) Read(_ context.Context, txn storage.Transaction, path storage.Path) (any, error) { underlying, err := db.underlying(txn) if err != nil { diff --git a/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/txn.go b/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/txn.go index e76bccd013..99f0bca31c 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/txn.go +++ b/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/txn.go @@ -297,6 +297,75 @@ func (txn *transaction) Read(path storage.Path) (any, error) { return cpy, nil } +func (txn *transaction) makeDir(path storage.Path) error { + if len(path) == 0 { + return nil + } + + exists, isObj, err := txn.isObject(path) + if err != nil { + return err + } + + if exists { + if isObj { + return nil + } + return &storage.Error{ + Code: storage.WriteConflictErr, + Message: path.String(), + } + } + + if err := txn.makeDir(path[:len(path)-1]); err != nil { + return err + } + + return txn.Write(storage.AddOp, path, map[string]any{}) +} + +// isObject checks whether the given path exists and points to an object, +// without deep-copying the subtree the way transaction.Read would. +func (txn *transaction) isObject(path storage.Path) (exists bool, isObj bool, err error) { + if !txn.write || txn.updates == nil { + return isObjectNode(pointer(txn.db.data, path)) + } + + for curr := txn.updates.Front(); curr != nil; curr = curr.Next() { + upd := curr.Value.(dataUpdate) + + if path.HasPrefix(upd.Path()) { + if upd.Remove() { + return false, false, nil + } + return isObjectNode(pointer(upd.Value(), path[len(upd.Path()):])) + } + + // A child of this path has been written, so this path is an object. + if upd.Path().HasPrefix(path) { + return true, true, nil + } + } + + return isObjectNode(pointer(txn.db.data, path)) +} + +func isObjectNode(node any, err error) (bool, bool, error) { + if err != nil { + if storage.IsNotFound(err) { + return false, false, nil + } + return false, false, err + } + + switch node.(type) { + case map[string]any, ast.Object: + return true, true, nil + default: + return true, false, nil + } +} + func (txn *transaction) ListPolicies() (ids []string) { for id := range txn.db.policies { if _, ok := txn.policies[id]; !ok { diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/binary.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/binary.go index 05050dbf7d..97cc2fef3e 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/topdown/binary.go +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/binary.go @@ -10,7 +10,6 @@ import ( ) func builtinBinaryAnd(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { - s1, err := builtins.SetOperand(operands[0].Value, 1) if err != nil { return err @@ -21,6 +20,10 @@ func builtinBinaryAnd(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Ter return err } + if s1.Len() == 0 || s2.Len() == 0 { + return iter(ast.InternedEmptySet) + } + i := s1.Intersect(s2) if i.Len() == 0 { return iter(ast.InternedEmptySet) @@ -30,7 +33,6 @@ func builtinBinaryAnd(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Ter } func builtinBinaryOr(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { - s1, err := builtins.SetOperand(operands[0].Value, 1) if err != nil { return err @@ -41,6 +43,14 @@ func builtinBinaryOr(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term return err } + if s1.Len() == 0 { + return iter(operands[1]) + } + + if s2.Len() == 0 { + return iter(operands[0]) + } + return iter(ast.NewTerm(s1.Union(s2))) } diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/builtins.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/builtins.go index e0b893d477..b5a8a6714d 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/topdown/builtins.go +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/builtins.go @@ -54,6 +54,8 @@ type ( PrintHook print.Hook // provides callback function to use for printing RoundTripper CustomizeRoundTripper // customize transport to use for HTTP requests DistributedTracingOpts tracing.Options // options to be used by distributed tracing. + RequestMetadata map[string]any // metadata from the caller, for use by wrapping projects + ResponseMetadata map[string]any // metadata for the response, populated by wrapping projects rand *rand.Rand // randomization source for non-security-sensitive operations Capabilities *ast.Capabilities } diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/copypropagation/copypropagation.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/copypropagation/copypropagation.go index 607855632d..ae30723dfb 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/topdown/copypropagation/copypropagation.go +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/copypropagation/copypropagation.go @@ -96,16 +96,24 @@ func (p *CopyPropagator) Apply(query ast.Body) ast.Body { removedEqs := ast.NewValueMap() for _, expr := range query { - pctx := &plugContext{ removedEqs: removedEqs, uf: uf, - negated: expr.Negated, + negated: expr.IsNegated(), headvars: headvars, } expr = p.plugBindings(pctx, expr) + // Recurse into not-bodies after plugging outer bindings. + if n, ok := expr.Terms.(*ast.Not); ok { + innerLive := p.computeNotBodyLivevars(uf, removedEqs, n.Body) + innerCp := New(innerLive). + WithEnsureNonEmptyBody(true). + WithCompiler(p.compiler) + n.Body = innerCp.Apply(n.Body) + } + if p.updateBindings(pctx, expr) { result.Append(expr) } @@ -158,7 +166,7 @@ func (p *CopyPropagator) Apply(query ast.Body) ast.Body { safe.Update(ast.ReservedVars) safe.Update(p.livevars) safe.Update(ast.OutputVarsFromBody(p.compiler, result, safe)) - unsafe := result.Vars(ast.SafetyCheckVisitorParams).Diff(safe) + unsafe := result.Vars(ast.SafetyCheckVisitorParamsWithArity(p.compiler.GetArity)).Diff(safe) for _, b := range sortbindings(removedEqs) { removedEq := ast.Equality.Expr(ast.NewTerm(b.k), ast.NewTerm(b.v)) @@ -365,6 +373,25 @@ func (p *CopyPropagator) updateBindingsEqAsymmetric(a, b *ast.Term) (ast.Var, as return "", nil, true } +// computeNotBodyLivevars returns the set of variables that must be treated as +// live when recursing into a Not body. A variable is live if it appears in the +// Not body and is bound or visible in the outer scope. +func (p *CopyPropagator) computeNotBodyLivevars(uf *unionFind, removedEqs *ast.ValueMap, body ast.Body) ast.VarSet { + bodyVars := body.Vars(ast.SafetyCheckVisitorParams) + + innerLive := ast.NewVarSet() + for v := range bodyVars { + if p.livevars.Contains(v) { + innerLive.Add(v) + } else if _, ok := uf.Find(v); ok { + innerLive.Add(v) + } else if removedEqs.Get(v) != nil { + innerLive.Add(v) + } + } + return innerLive +} + type plugContext struct { removedEqs *ast.ValueMap uf *unionFind diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/duration.peg b/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/duration.peg new file mode 100644 index 0000000000..339fe3bbe7 --- /dev/null +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/duration.peg @@ -0,0 +1,34 @@ +{ +package durationparser +} + +Duration <- sign:Sign? segments:Segment+ EOF { + signStr := "" + if sign != nil { + signStr = sign.(string) + } + raw := segments.([]any) + segs := make([]Segment, len(raw)) + for i, s := range raw { + segs[i] = s.(Segment) + } + return Result{Sign: signStr, Segments: segs}, nil +} + +Sign <- [-+] { + return string(c.text), nil +} + +Segment <- digits:Digits unit:Unit { + return Segment{Digits: digits.(string), Unit: unit.(string)}, nil +} + +Digits <- [0-9.]+ { + return string(c.text), nil +} + +Unit <- ("ms" / "us" / "µs" / "ns" / [a-z]) { + return string(c.text), nil +} + +EOF <- !. diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/duration_parser.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/duration_parser.go new file mode 100644 index 0000000000..0bef955f2d --- /dev/null +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/duration_parser.go @@ -0,0 +1,1549 @@ +// Code generated by pigeon; DO NOT EDIT. + +package durationparser + +import ( + "bytes" + "errors" + "fmt" + "io" + "math" + "os" + "sort" + "strconv" + "strings" + "sync" + "unicode" + "unicode/utf8" +) + +var g = &grammar{ + rules: []*rule{ + { + name: "Duration", + pos: position{line: 5, col: 1, offset: 28}, + expr: &actionExpr{ + pos: position{line: 5, col: 13, offset: 40}, + run: (*parser).callonDuration1, + expr: &seqExpr{ + pos: position{line: 5, col: 13, offset: 40}, + exprs: []any{ + &labeledExpr{ + pos: position{line: 5, col: 13, offset: 40}, + label: "sign", + expr: &zeroOrOneExpr{ + pos: position{line: 5, col: 18, offset: 45}, + expr: &ruleRefExpr{ + pos: position{line: 5, col: 18, offset: 45}, + name: "Sign", + }, + }, + }, + &labeledExpr{ + pos: position{line: 5, col: 24, offset: 51}, + label: "segments", + expr: &oneOrMoreExpr{ + pos: position{line: 5, col: 33, offset: 60}, + expr: &ruleRefExpr{ + pos: position{line: 5, col: 33, offset: 60}, + name: "Segment", + }, + }, + }, + &ruleRefExpr{ + pos: position{line: 5, col: 42, offset: 69}, + name: "EOF", + }, + }, + }, + }, + }, + { + name: "Sign", + pos: position{line: 18, col: 1, offset: 303}, + expr: &actionExpr{ + pos: position{line: 18, col: 9, offset: 311}, + run: (*parser).callonSign1, + expr: &charClassMatcher{ + pos: position{line: 18, col: 9, offset: 311}, + val: "[-+]", + chars: []rune{'-', '+'}, + ignoreCase: false, + inverted: false, + }, + }, + }, + { + name: "Segment", + pos: position{line: 22, col: 1, offset: 349}, + expr: &actionExpr{ + pos: position{line: 22, col: 12, offset: 360}, + run: (*parser).callonSegment1, + expr: &seqExpr{ + pos: position{line: 22, col: 12, offset: 360}, + exprs: []any{ + &labeledExpr{ + pos: position{line: 22, col: 12, offset: 360}, + label: "digits", + expr: &ruleRefExpr{ + pos: position{line: 22, col: 19, offset: 367}, + name: "Digits", + }, + }, + &labeledExpr{ + pos: position{line: 22, col: 26, offset: 374}, + label: "unit", + expr: &ruleRefExpr{ + pos: position{line: 22, col: 31, offset: 379}, + name: "Unit", + }, + }, + }, + }, + }, + }, + { + name: "Digits", + pos: position{line: 26, col: 1, offset: 456}, + expr: &actionExpr{ + pos: position{line: 26, col: 11, offset: 466}, + run: (*parser).callonDigits1, + expr: &oneOrMoreExpr{ + pos: position{line: 26, col: 11, offset: 466}, + expr: &charClassMatcher{ + pos: position{line: 26, col: 11, offset: 466}, + val: "[0-9.]", + chars: []rune{'.'}, + ranges: []rune{'0', '9'}, + ignoreCase: false, + inverted: false, + }, + }, + }, + }, + { + name: "Unit", + pos: position{line: 30, col: 1, offset: 507}, + expr: &actionExpr{ + pos: position{line: 30, col: 9, offset: 515}, + run: (*parser).callonUnit1, + expr: &choiceExpr{ + pos: position{line: 30, col: 10, offset: 516}, + alternatives: []any{ + &litMatcher{ + pos: position{line: 30, col: 10, offset: 516}, + val: "ms", + ignoreCase: false, + want: "\"ms\"", + }, + &litMatcher{ + pos: position{line: 30, col: 17, offset: 523}, + val: "us", + ignoreCase: false, + want: "\"us\"", + }, + &litMatcher{ + pos: position{line: 30, col: 24, offset: 530}, + val: "µs", + ignoreCase: false, + want: "\"µs\"", + }, + &litMatcher{ + pos: position{line: 30, col: 31, offset: 538}, + val: "ns", + ignoreCase: false, + want: "\"ns\"", + }, + &charClassMatcher{ + pos: position{line: 30, col: 38, offset: 545}, + val: "[a-z]", + ranges: []rune{'a', 'z'}, + ignoreCase: false, + inverted: false, + }, + }, + }, + }, + }, + { + name: "EOF", + pos: position{line: 34, col: 1, offset: 585}, + expr: ¬Expr{ + pos: position{line: 34, col: 8, offset: 592}, + expr: &anyMatcher{ + line: 34, col: 9, offset: 593, + }, + }, + }, + }, +} + +func (c *current) onDuration1(sign, segments any) (any, error) { + signStr := "" + if sign != nil { + signStr = sign.(string) + } + raw := segments.([]any) + segs := make([]Segment, len(raw)) + for i, s := range raw { + segs[i] = s.(Segment) + } + return Result{Sign: signStr, Segments: segs}, nil +} + +func (p *parser) callonDuration1() (any, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onDuration1(stack["sign"], stack["segments"]) +} + +func (c *current) onSign1() (any, error) { + return string(c.text), nil +} + +func (p *parser) callonSign1() (any, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onSign1() +} + +func (c *current) onSegment1(digits, unit any) (any, error) { + return Segment{Digits: digits.(string), Unit: unit.(string)}, nil +} + +func (p *parser) callonSegment1() (any, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onSegment1(stack["digits"], stack["unit"]) +} + +func (c *current) onDigits1() (any, error) { + return string(c.text), nil +} + +func (p *parser) callonDigits1() (any, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onDigits1() +} + +func (c *current) onUnit1() (any, error) { + return string(c.text), nil +} + +func (p *parser) callonUnit1() (any, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onUnit1() +} + +var ( + // errNoRule is returned when the grammar to parse has no rule. + errNoRule = errors.New("grammar has no rule") + + // errInvalidEntrypoint is returned when the specified entrypoint rule + // does not exit. + errInvalidEntrypoint = errors.New("invalid entrypoint") + + // errInvalidEncoding is returned when the source is not properly + // utf8-encoded. + errInvalidEncoding = errors.New("invalid encoding") + + // errMaxExprCnt is used to signal that the maximum number of + // expressions have been parsed. + errMaxExprCnt = errors.New("max number of expressions parsed") +) + +// Option is a function that can set an option on the parser. It returns +// the previous setting as an Option. +type Option func(*parser) Option + +// MaxExpressions creates an Option to stop parsing after the provided +// number of expressions have been parsed, if the value is 0 then the parser will +// parse for as many steps as needed (possibly an infinite number). +// +// The default for maxExprCnt is 0. +func MaxExpressions(maxExprCnt uint64) Option { + return func(p *parser) Option { + oldMaxExprCnt := p.maxExprCnt + p.maxExprCnt = maxExprCnt + return MaxExpressions(oldMaxExprCnt) + } +} + +// Entrypoint creates an Option to set the rule name to use as entrypoint. +// The rule name must have been specified in the -alternate-entrypoints +// if generating the parser with the -optimize-grammar flag, otherwise +// it may have been optimized out. Passing an empty string sets the +// entrypoint to the first rule in the grammar. +// +// The default is to start parsing at the first rule in the grammar. +func Entrypoint(ruleName string) Option { + return func(p *parser) Option { + oldEntrypoint := p.entrypoint + p.entrypoint = ruleName + if ruleName == "" { + p.entrypoint = g.rules[0].name + } + return Entrypoint(oldEntrypoint) + } +} + +// Statistics adds a user provided Stats struct to the parser to allow +// the user to process the results after the parsing has finished. +// Also the key for the "no match" counter is set. +// +// Example usage: +// +// input := "input" +// stats := Stats{} +// _, err := Parse("input-file", []byte(input), Statistics(&stats, "no match")) +// if err != nil { +// log.Panicln(err) +// } +// b, err := json.MarshalIndent(stats.ChoiceAltCnt, "", " ") +// if err != nil { +// log.Panicln(err) +// } +// fmt.Println(string(b)) +func Statistics(stats *Stats, choiceNoMatch string) Option { + return func(p *parser) Option { + oldStats := p.Stats + p.Stats = stats + oldChoiceNoMatch := p.choiceNoMatch + p.choiceNoMatch = choiceNoMatch + if p.Stats.ChoiceAltCnt == nil { + p.Stats.ChoiceAltCnt = make(map[string]map[string]int) + } + return Statistics(oldStats, oldChoiceNoMatch) + } +} + +// Debug creates an Option to set the debug flag to b. When set to true, +// debugging information is printed to stdout while parsing. +// +// The default is false. +func Debug(b bool) Option { + return func(p *parser) Option { + old := p.debug + p.debug = b + return Debug(old) + } +} + +// Memoize creates an Option to set the memoize flag to b. When set to true, +// the parser will cache all results so each expression is evaluated only +// once. This guarantees linear parsing time even for pathological cases, +// at the expense of more memory and slower times for typical cases. +// +// The default is false. +func Memoize(b bool) Option { + return func(p *parser) Option { + old := p.memoize + p.memoize = b + return Memoize(old) + } +} + +// AllowInvalidUTF8 creates an Option to allow invalid UTF-8 bytes. +// Every invalid UTF-8 byte is treated as a utf8.RuneError (U+FFFD) +// by character class matchers and is matched by the any matcher. +// The returned matched value, c.text and c.offset are NOT affected. +// +// The default is false. +func AllowInvalidUTF8(b bool) Option { + return func(p *parser) Option { + old := p.allowInvalidUTF8 + p.allowInvalidUTF8 = b + return AllowInvalidUTF8(old) + } +} + +// Recover creates an Option to set the recover flag to b. When set to +// true, this causes the parser to recover from panics and convert it +// to an error. Setting it to false can be useful while debugging to +// access the full stack trace. +// +// The default is true. +func Recover(b bool) Option { + return func(p *parser) Option { + old := p.recover + p.recover = b + return Recover(old) + } +} + +// GlobalStore creates an Option to set a key to a certain value in +// the globalStore. +func GlobalStore(key string, value any) Option { + return func(p *parser) Option { + old := p.cur.globalStore[key] + p.cur.globalStore[key] = value + return GlobalStore(key, old) + } +} + +// InitState creates an Option to set a key to a certain value in +// the global "state" store. +func InitState(key string, value any) Option { + return func(p *parser) Option { + old := p.cur.state[key] + p.cur.state[key] = value + return InitState(key, old) + } +} + +// ParseFile parses the file identified by filename. +func ParseFile(filename string, opts ...Option) (i any, err error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer func() { + if closeErr := f.Close(); closeErr != nil { + err = closeErr + } + }() + return ParseReader(filename, f, opts...) +} + +// ParseReader parses the data from r using filename as information in the +// error messages. +func ParseReader(filename string, r io.Reader, opts ...Option) (any, error) { + b, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + return Parse(filename, b, opts...) +} + +// Parse parses the data from b using filename as information in the +// error messages. +func Parse(filename string, b []byte, opts ...Option) (any, error) { + return newParser(filename, b, opts...).parse(g) +} + +// position records a position in the text. +type position struct { + line, col, offset int +} + +func (p position) String() string { + return strconv.Itoa(p.line) + ":" + strconv.Itoa(p.col) + " [" + strconv.Itoa(p.offset) + "]" +} + +// savepoint stores all state required to go back to this point in the +// parser. +type savepoint struct { + position + rn rune + w int +} + +type current struct { + pos position // start position of the match + text []byte // raw text of the match + + // state is a store for arbitrary key,value pairs that the user wants to be + // tied to the backtracking of the parser. + // This is always rolled back if a parsing rule fails. + state storeDict + + // globalStore is a general store for the user to store arbitrary key-value + // pairs that they need to manage and that they do not want tied to the + // backtracking of the parser. This is only modified by the user and never + // rolled back by the parser. It is always up to the user to keep this in a + // consistent state. + globalStore storeDict +} + +type storeDict map[string]any + +// the AST types... + +type grammar struct { + pos position + rules []*rule +} + +type rule struct { + pos position + name string + displayName string + expr any +} + +type choiceExpr struct { + pos position + alternatives []any +} + +type actionExpr struct { + pos position + expr any + run func(*parser) (any, error) +} + +type recoveryExpr struct { + pos position + expr any + recoverExpr any + failureLabel []string +} + +type seqExpr struct { + pos position + exprs []any +} + +type throwExpr struct { + pos position + label string +} + +type labeledExpr struct { + pos position + label string + expr any +} + +type expr struct { + pos position + expr any +} + +type ( + andExpr expr + notExpr expr + zeroOrOneExpr expr + zeroOrMoreExpr expr + oneOrMoreExpr expr +) + +type ruleRefExpr struct { + pos position + name string +} + +type stateCodeExpr struct { + pos position + run func(*parser) error +} + +type andCodeExpr struct { + pos position + run func(*parser) (bool, error) +} + +type notCodeExpr struct { + pos position + run func(*parser) (bool, error) +} + +type litMatcher struct { + pos position + val string + ignoreCase bool + want string +} + +type charClassMatcher struct { + pos position + val string + basicLatinChars [128]bool + chars []rune + ranges []rune + classes []*unicode.RangeTable + ignoreCase bool + inverted bool +} + +type anyMatcher position + +// errList cumulates the errors found by the parser. +type errList []error + +func (e *errList) add(err error) { + *e = append(*e, err) +} + +func (e errList) err() error { + if len(e) == 0 { + return nil + } + e.dedupe() + return e +} + +func (e *errList) dedupe() { + var cleaned []error + set := make(map[string]bool) + for _, err := range *e { + if msg := err.Error(); !set[msg] { + set[msg] = true + cleaned = append(cleaned, err) + } + } + *e = cleaned +} + +func (e errList) Error() string { + switch len(e) { + case 0: + return "" + case 1: + return e[0].Error() + default: + var buf bytes.Buffer + + for i, err := range e { + if i > 0 { + buf.WriteRune('\n') + } + buf.WriteString(err.Error()) + } + return buf.String() + } +} + +// parserError wraps an error with a prefix indicating the rule in which +// the error occurred. The original error is stored in the Inner field. +type parserError struct { + Inner error + pos position + prefix string + expected []string +} + +// Error returns the error message. +func (p *parserError) Error() string { + return p.prefix + ": " + p.Inner.Error() +} + +// newParser creates a parser with the specified input source and options. +func newParser(filename string, b []byte, opts ...Option) *parser { + stats := Stats{ + ChoiceAltCnt: make(map[string]map[string]int), + } + + p := &parser{ + filename: filename, + errs: new(errList), + data: b, + pt: savepoint{position: position{line: 1}}, + recover: true, + cur: current{ + state: make(storeDict), + globalStore: make(storeDict), + }, + maxFailPos: position{col: 1, line: 1}, + maxFailExpected: make([]string, 0, 20), + Stats: &stats, + // start rule is rule [0] unless an alternate entrypoint is specified + entrypoint: g.rules[0].name, + } + p.setOptions(opts) + + if p.maxExprCnt == 0 { + p.maxExprCnt = math.MaxUint64 + } + + return p +} + +// setOptions applies the options to the parser. +func (p *parser) setOptions(opts []Option) { + for _, opt := range opts { + opt(p) + } +} + +type resultTuple struct { + v any + b bool + end savepoint +} + +const choiceNoMatch = -1 + +// Stats stores some statistics, gathered during parsing +type Stats struct { + // ExprCnt counts the number of expressions processed during parsing + // This value is compared to the maximum number of expressions allowed + // (set by the MaxExpressions option). + ExprCnt uint64 + + // ChoiceAltCnt is used to count for each ordered choice expression, + // which alternative is used how may times. + // These numbers allow to optimize the order of the ordered choice expression + // to increase the performance of the parser + // + // The outer key of ChoiceAltCnt is composed of the name of the rule as well + // as the line and the column of the ordered choice. + // The inner key of ChoiceAltCnt is the number (one-based) of the matching alternative. + // For each alternative the number of matches are counted. If an ordered choice does not + // match, a special counter is incremented. The name of this counter is set with + // the parser option Statistics. + // For an alternative to be included in ChoiceAltCnt, it has to match at least once. + ChoiceAltCnt map[string]map[string]int +} + +type parser struct { + filename string + pt savepoint + cur current + + data []byte + errs *errList + + depth int + recover bool + debug bool + + memoize bool + // memoization table for the packrat algorithm: + // map[offset in source] map[expression or rule] {value, match} + memo map[int]map[any]resultTuple + + // rules table, maps the rule identifier to the rule node + rules map[string]*rule + // variables stack, map of label to value + vstack []map[string]any + // rule stack, allows identification of the current rule in errors + rstack []*rule + + // parse fail + maxFailPos position + maxFailExpected []string + maxFailInvertExpected bool + + // max number of expressions to be parsed + maxExprCnt uint64 + // entrypoint for the parser + entrypoint string + + allowInvalidUTF8 bool + + *Stats + + choiceNoMatch string + // recovery expression stack, keeps track of the currently available recovery expression, these are traversed in reverse + recoveryStack []map[string]any +} + +// push a variable set on the vstack. +func (p *parser) pushV() { + if cap(p.vstack) == len(p.vstack) { + // create new empty slot in the stack + p.vstack = append(p.vstack, nil) + } else { + // slice to 1 more + p.vstack = p.vstack[:len(p.vstack)+1] + } + + // get the last args set + m := p.vstack[len(p.vstack)-1] + if m != nil && len(m) == 0 { + // empty map, all good + return + } + + m = make(map[string]any) + p.vstack[len(p.vstack)-1] = m +} + +// pop a variable set from the vstack. +func (p *parser) popV() { + // if the map is not empty, clear it + m := p.vstack[len(p.vstack)-1] + if len(m) > 0 { + // GC that map + p.vstack[len(p.vstack)-1] = nil + } + p.vstack = p.vstack[:len(p.vstack)-1] +} + +// push a recovery expression with its labels to the recoveryStack +func (p *parser) pushRecovery(labels []string, expr any) { + if cap(p.recoveryStack) == len(p.recoveryStack) { + // create new empty slot in the stack + p.recoveryStack = append(p.recoveryStack, nil) + } else { + // slice to 1 more + p.recoveryStack = p.recoveryStack[:len(p.recoveryStack)+1] + } + + m := make(map[string]any, len(labels)) + for _, fl := range labels { + m[fl] = expr + } + p.recoveryStack[len(p.recoveryStack)-1] = m +} + +// pop a recovery expression from the recoveryStack +func (p *parser) popRecovery() { + // GC that map + p.recoveryStack[len(p.recoveryStack)-1] = nil + + p.recoveryStack = p.recoveryStack[:len(p.recoveryStack)-1] +} + +func (p *parser) print(prefix, s string) string { + if !p.debug { + return s + } + + fmt.Printf("%s %d:%d:%d: %s [%#U]\n", + prefix, p.pt.line, p.pt.col, p.pt.offset, s, p.pt.rn) + return s +} + +func (p *parser) printIndent(mark string, s string) string { + return p.print(strings.Repeat(" ", p.depth)+mark, s) +} + +func (p *parser) in(s string) string { + res := p.printIndent(">", s) + p.depth++ + return res +} + +func (p *parser) out(s string) string { + p.depth-- + return p.printIndent("<", s) +} + +func (p *parser) addErr(err error) { + p.addErrAt(err, p.pt.position, []string{}) +} + +func (p *parser) addErrAt(err error, pos position, expected []string) { + var buf bytes.Buffer + if p.filename != "" { + buf.WriteString(p.filename) + } + if buf.Len() > 0 { + buf.WriteString(":") + } + buf.WriteString(fmt.Sprintf("%d:%d (%d)", pos.line, pos.col, pos.offset)) + if len(p.rstack) > 0 { + if buf.Len() > 0 { + buf.WriteString(": ") + } + rule := p.rstack[len(p.rstack)-1] + if rule.displayName != "" { + buf.WriteString("rule " + rule.displayName) + } else { + buf.WriteString("rule " + rule.name) + } + } + pe := &parserError{Inner: err, pos: pos, prefix: buf.String(), expected: expected} + p.errs.add(pe) +} + +func (p *parser) failAt(fail bool, pos position, want string) { + // process fail if parsing fails and not inverted or parsing succeeds and invert is set + if fail == p.maxFailInvertExpected { + if pos.offset < p.maxFailPos.offset { + return + } + + if pos.offset > p.maxFailPos.offset { + p.maxFailPos = pos + p.maxFailExpected = p.maxFailExpected[:0] + } + + if p.maxFailInvertExpected { + want = "!" + want + } + p.maxFailExpected = append(p.maxFailExpected, want) + } +} + +// read advances the parser to the next rune. +func (p *parser) read() { + p.pt.offset += p.pt.w + rn, n := utf8.DecodeRune(p.data[p.pt.offset:]) + p.pt.rn = rn + p.pt.w = n + p.pt.col++ + if rn == '\n' { + p.pt.line++ + p.pt.col = 0 + } + + if rn == utf8.RuneError && n == 1 { // see utf8.DecodeRune + if !p.allowInvalidUTF8 { + p.addErr(errInvalidEncoding) + } + } +} + +// restore parser position to the savepoint pt. +func (p *parser) restore(pt savepoint) { + if p.debug { + defer p.out(p.in("restore")) + } + if pt.offset == p.pt.offset { + return + } + p.pt = pt +} + +// Cloner is implemented by any value that has a Clone method, which returns a +// copy of the value. This is mainly used for types which are not passed by +// value (e.g map, slice, chan) or structs that contain such types. +// +// This is used in conjunction with the global state feature to create proper +// copies of the state to allow the parser to properly restore the state in +// the case of backtracking. +type Cloner interface { + Clone() any +} + +var statePool = &sync.Pool{ + New: func() any { return make(storeDict) }, +} + +func (sd storeDict) Discard() { + for k := range sd { + delete(sd, k) + } + statePool.Put(sd) +} + +// clone and return parser current state. +func (p *parser) cloneState() storeDict { + if p.debug { + defer p.out(p.in("cloneState")) + } + + state := statePool.Get().(storeDict) + for k, v := range p.cur.state { + if c, ok := v.(Cloner); ok { + state[k] = c.Clone() + } else { + state[k] = v + } + } + return state +} + +// restore parser current state to the state storeDict. +// every restoreState should applied only one time for every cloned state +func (p *parser) restoreState(state storeDict) { + if p.debug { + defer p.out(p.in("restoreState")) + } + p.cur.state.Discard() + p.cur.state = state +} + +// get the slice of bytes from the savepoint start to the current position. +func (p *parser) sliceFrom(start savepoint) []byte { + return p.data[start.position.offset:p.pt.position.offset] +} + +func (p *parser) getMemoized(node any) (resultTuple, bool) { + if len(p.memo) == 0 { + return resultTuple{}, false + } + m := p.memo[p.pt.offset] + if len(m) == 0 { + return resultTuple{}, false + } + res, ok := m[node] + return res, ok +} + +func (p *parser) setMemoized(pt savepoint, node any, tuple resultTuple) { + if p.memo == nil { + p.memo = make(map[int]map[any]resultTuple) + } + m := p.memo[pt.offset] + if m == nil { + m = make(map[any]resultTuple) + p.memo[pt.offset] = m + } + m[node] = tuple +} + +func (p *parser) buildRulesTable(g *grammar) { + p.rules = make(map[string]*rule, len(g.rules)) + for _, r := range g.rules { + p.rules[r.name] = r + } +} + +func (p *parser) parse(g *grammar) (val any, err error) { + if len(g.rules) == 0 { + p.addErr(errNoRule) + return nil, p.errs.err() + } + + // TODO : not super critical but this could be generated + p.buildRulesTable(g) + + if p.recover { + // panic can be used in action code to stop parsing immediately + // and return the panic as an error. + defer func() { + if e := recover(); e != nil { + if p.debug { + defer p.out(p.in("panic handler")) + } + val = nil + switch e := e.(type) { + case error: + p.addErr(e) + default: + p.addErr(fmt.Errorf("%v", e)) + } + err = p.errs.err() + } + }() + } + + startRule, ok := p.rules[p.entrypoint] + if !ok { + p.addErr(errInvalidEntrypoint) + return nil, p.errs.err() + } + + p.read() // advance to first rune + val, ok = p.parseRuleWrap(startRule) + if !ok { + if len(*p.errs) == 0 { + // If parsing fails, but no errors have been recorded, the expected values + // for the farthest parser position are returned as error. + maxFailExpectedMap := make(map[string]struct{}, len(p.maxFailExpected)) + for _, v := range p.maxFailExpected { + maxFailExpectedMap[v] = struct{}{} + } + expected := make([]string, 0, len(maxFailExpectedMap)) + eof := false + if _, ok := maxFailExpectedMap["!."]; ok { + delete(maxFailExpectedMap, "!.") + eof = true + } + for k := range maxFailExpectedMap { + expected = append(expected, k) + } + sort.Strings(expected) + if eof { + expected = append(expected, "EOF") + } + p.addErrAt(errors.New("no match found, expected: "+listJoin(expected, ", ", "or")), p.maxFailPos, expected) + } + + return nil, p.errs.err() + } + return val, p.errs.err() +} + +func listJoin(list []string, sep string, lastSep string) string { + switch len(list) { + case 0: + return "" + case 1: + return list[0] + default: + return strings.Join(list[:len(list)-1], sep) + " " + lastSep + " " + list[len(list)-1] + } +} + +func (p *parser) parseRuleMemoize(rule *rule) (any, bool) { + res, ok := p.getMemoized(rule) + if ok { + p.restore(res.end) + return res.v, res.b + } + + startMark := p.pt + val, ok := p.parseRule(rule) + p.setMemoized(startMark, rule, resultTuple{val, ok, p.pt}) + + return val, ok +} + +func (p *parser) parseRuleWrap(rule *rule) (any, bool) { + if p.debug { + defer p.out(p.in("parseRule " + rule.name)) + } + var ( + val any + ok bool + startMark = p.pt + ) + + if p.memoize { + val, ok = p.parseRuleMemoize(rule) + } else { + val, ok = p.parseRule(rule) + } + + if ok && p.debug { + p.printIndent("MATCH", string(p.sliceFrom(startMark))) + } + return val, ok +} + +func (p *parser) parseRule(rule *rule) (any, bool) { + p.rstack = append(p.rstack, rule) + p.pushV() + val, ok := p.parseExprWrap(rule.expr) + p.popV() + p.rstack = p.rstack[:len(p.rstack)-1] + return val, ok +} + +func (p *parser) parseExprWrap(expr any) (any, bool) { + var pt savepoint + + if p.memoize { + res, ok := p.getMemoized(expr) + if ok { + p.restore(res.end) + return res.v, res.b + } + pt = p.pt + } + + val, ok := p.parseExpr(expr) + + if p.memoize { + p.setMemoized(pt, expr, resultTuple{val, ok, p.pt}) + } + return val, ok +} + +func (p *parser) parseExpr(expr any) (any, bool) { + p.ExprCnt++ + if p.ExprCnt > p.maxExprCnt { + panic(errMaxExprCnt) + } + + var val any + var ok bool + switch expr := expr.(type) { + case *actionExpr: + val, ok = p.parseActionExpr(expr) + case *andCodeExpr: + val, ok = p.parseAndCodeExpr(expr) + case *andExpr: + val, ok = p.parseAndExpr(expr) + case *anyMatcher: + val, ok = p.parseAnyMatcher(expr) + case *charClassMatcher: + val, ok = p.parseCharClassMatcher(expr) + case *choiceExpr: + val, ok = p.parseChoiceExpr(expr) + case *labeledExpr: + val, ok = p.parseLabeledExpr(expr) + case *litMatcher: + val, ok = p.parseLitMatcher(expr) + case *notCodeExpr: + val, ok = p.parseNotCodeExpr(expr) + case *notExpr: + val, ok = p.parseNotExpr(expr) + case *oneOrMoreExpr: + val, ok = p.parseOneOrMoreExpr(expr) + case *recoveryExpr: + val, ok = p.parseRecoveryExpr(expr) + case *ruleRefExpr: + val, ok = p.parseRuleRefExpr(expr) + case *seqExpr: + val, ok = p.parseSeqExpr(expr) + case *stateCodeExpr: + val, ok = p.parseStateCodeExpr(expr) + case *throwExpr: + val, ok = p.parseThrowExpr(expr) + case *zeroOrMoreExpr: + val, ok = p.parseZeroOrMoreExpr(expr) + case *zeroOrOneExpr: + val, ok = p.parseZeroOrOneExpr(expr) + default: + panic(fmt.Sprintf("unknown expression type %T", expr)) + } + return val, ok +} + +func (p *parser) parseActionExpr(act *actionExpr) (any, bool) { + if p.debug { + defer p.out(p.in("parseActionExpr")) + } + + start := p.pt + val, ok := p.parseExprWrap(act.expr) + if ok { + p.cur.pos = start.position + p.cur.text = p.sliceFrom(start) + state := p.cloneState() + actVal, err := act.run(p) + if err != nil { + p.addErrAt(err, start.position, []string{}) + } + p.restoreState(state) + + val = actVal + } + if ok && p.debug { + p.printIndent("MATCH", string(p.sliceFrom(start))) + } + return val, ok +} + +func (p *parser) parseAndCodeExpr(and *andCodeExpr) (any, bool) { + if p.debug { + defer p.out(p.in("parseAndCodeExpr")) + } + + state := p.cloneState() + + ok, err := and.run(p) + if err != nil { + p.addErr(err) + } + p.restoreState(state) + + return nil, ok +} + +func (p *parser) parseAndExpr(and *andExpr) (any, bool) { + if p.debug { + defer p.out(p.in("parseAndExpr")) + } + + pt := p.pt + state := p.cloneState() + p.pushV() + _, ok := p.parseExprWrap(and.expr) + p.popV() + p.restoreState(state) + p.restore(pt) + + return nil, ok +} + +func (p *parser) parseAnyMatcher(any *anyMatcher) (any, bool) { + if p.debug { + defer p.out(p.in("parseAnyMatcher")) + } + + if p.pt.rn == utf8.RuneError && p.pt.w == 0 { + // EOF - see utf8.DecodeRune + p.failAt(false, p.pt.position, ".") + return nil, false + } + start := p.pt + p.read() + p.failAt(true, start.position, ".") + return p.sliceFrom(start), true +} + +func (p *parser) parseCharClassMatcher(chr *charClassMatcher) (any, bool) { + if p.debug { + defer p.out(p.in("parseCharClassMatcher")) + } + + cur := p.pt.rn + start := p.pt + + // can't match EOF + if cur == utf8.RuneError && p.pt.w == 0 { // see utf8.DecodeRune + p.failAt(false, start.position, chr.val) + return nil, false + } + + if chr.ignoreCase { + cur = unicode.ToLower(cur) + } + + // try to match in the list of available chars + for _, rn := range chr.chars { + if rn == cur { + if chr.inverted { + p.failAt(false, start.position, chr.val) + return nil, false + } + p.read() + p.failAt(true, start.position, chr.val) + return p.sliceFrom(start), true + } + } + + // try to match in the list of ranges + for i := 0; i < len(chr.ranges); i += 2 { + if cur >= chr.ranges[i] && cur <= chr.ranges[i+1] { + if chr.inverted { + p.failAt(false, start.position, chr.val) + return nil, false + } + p.read() + p.failAt(true, start.position, chr.val) + return p.sliceFrom(start), true + } + } + + // try to match in the list of Unicode classes + for _, cl := range chr.classes { + if unicode.Is(cl, cur) { + if chr.inverted { + p.failAt(false, start.position, chr.val) + return nil, false + } + p.read() + p.failAt(true, start.position, chr.val) + return p.sliceFrom(start), true + } + } + + if chr.inverted { + p.read() + p.failAt(true, start.position, chr.val) + return p.sliceFrom(start), true + } + p.failAt(false, start.position, chr.val) + return nil, false +} + +func (p *parser) incChoiceAltCnt(ch *choiceExpr, altI int) { + choiceIdent := fmt.Sprintf("%s %d:%d", p.rstack[len(p.rstack)-1].name, ch.pos.line, ch.pos.col) + m := p.ChoiceAltCnt[choiceIdent] + if m == nil { + m = make(map[string]int) + p.ChoiceAltCnt[choiceIdent] = m + } + // We increment altI by 1, so the keys do not start at 0 + alt := strconv.Itoa(altI + 1) + if altI == choiceNoMatch { + alt = p.choiceNoMatch + } + m[alt]++ +} + +func (p *parser) parseChoiceExpr(ch *choiceExpr) (any, bool) { + if p.debug { + defer p.out(p.in("parseChoiceExpr")) + } + + for altI, alt := range ch.alternatives { + // dummy assignment to prevent compile error if optimized + _ = altI + + state := p.cloneState() + + p.pushV() + val, ok := p.parseExprWrap(alt) + p.popV() + if ok { + p.incChoiceAltCnt(ch, altI) + return val, ok + } + p.restoreState(state) + } + p.incChoiceAltCnt(ch, choiceNoMatch) + return nil, false +} + +func (p *parser) parseLabeledExpr(lab *labeledExpr) (any, bool) { + if p.debug { + defer p.out(p.in("parseLabeledExpr")) + } + + p.pushV() + val, ok := p.parseExprWrap(lab.expr) + p.popV() + if ok && lab.label != "" { + m := p.vstack[len(p.vstack)-1] + m[lab.label] = val + } + return val, ok +} + +func (p *parser) parseLitMatcher(lit *litMatcher) (any, bool) { + if p.debug { + defer p.out(p.in("parseLitMatcher")) + } + + start := p.pt + for _, want := range lit.val { + cur := p.pt.rn + if lit.ignoreCase { + cur = unicode.ToLower(cur) + } + if cur != want { + p.failAt(false, start.position, lit.want) + p.restore(start) + return nil, false + } + p.read() + } + p.failAt(true, start.position, lit.want) + return p.sliceFrom(start), true +} + +func (p *parser) parseNotCodeExpr(not *notCodeExpr) (any, bool) { + if p.debug { + defer p.out(p.in("parseNotCodeExpr")) + } + + state := p.cloneState() + + ok, err := not.run(p) + if err != nil { + p.addErr(err) + } + p.restoreState(state) + + return nil, !ok +} + +func (p *parser) parseNotExpr(not *notExpr) (any, bool) { + if p.debug { + defer p.out(p.in("parseNotExpr")) + } + + pt := p.pt + state := p.cloneState() + p.pushV() + p.maxFailInvertExpected = !p.maxFailInvertExpected + _, ok := p.parseExprWrap(not.expr) + p.maxFailInvertExpected = !p.maxFailInvertExpected + p.popV() + p.restoreState(state) + p.restore(pt) + + return nil, !ok +} + +func (p *parser) parseOneOrMoreExpr(expr *oneOrMoreExpr) (any, bool) { + if p.debug { + defer p.out(p.in("parseOneOrMoreExpr")) + } + + var vals []any + + for { + p.pushV() + val, ok := p.parseExprWrap(expr.expr) + p.popV() + if !ok { + if len(vals) == 0 { + // did not match once, no match + return nil, false + } + return vals, true + } + vals = append(vals, val) + } +} + +func (p *parser) parseRecoveryExpr(recover *recoveryExpr) (any, bool) { + if p.debug { + defer p.out(p.in("parseRecoveryExpr (" + strings.Join(recover.failureLabel, ",") + ")")) + } + + p.pushRecovery(recover.failureLabel, recover.recoverExpr) + val, ok := p.parseExprWrap(recover.expr) + p.popRecovery() + + return val, ok +} + +func (p *parser) parseRuleRefExpr(ref *ruleRefExpr) (any, bool) { + if p.debug { + defer p.out(p.in("parseRuleRefExpr " + ref.name)) + } + + if ref.name == "" { + panic(fmt.Sprintf("%s: invalid rule: missing name", ref.pos)) + } + + rule := p.rules[ref.name] + if rule == nil { + p.addErr(fmt.Errorf("undefined rule: %s", ref.name)) + return nil, false + } + return p.parseRuleWrap(rule) +} + +func (p *parser) parseSeqExpr(seq *seqExpr) (any, bool) { + if p.debug { + defer p.out(p.in("parseSeqExpr")) + } + + vals := make([]any, 0, len(seq.exprs)) + + pt := p.pt + state := p.cloneState() + for _, expr := range seq.exprs { + val, ok := p.parseExprWrap(expr) + if !ok { + p.restoreState(state) + p.restore(pt) + return nil, false + } + vals = append(vals, val) + } + return vals, true +} + +func (p *parser) parseStateCodeExpr(state *stateCodeExpr) (any, bool) { + if p.debug { + defer p.out(p.in("parseStateCodeExpr")) + } + + err := state.run(p) + if err != nil { + p.addErr(err) + } + return nil, true +} + +func (p *parser) parseThrowExpr(expr *throwExpr) (any, bool) { + if p.debug { + defer p.out(p.in("parseThrowExpr")) + } + + for i := len(p.recoveryStack) - 1; i >= 0; i-- { + if recoverExpr, ok := p.recoveryStack[i][expr.label]; ok { + if val, ok := p.parseExprWrap(recoverExpr); ok { + return val, ok + } + } + } + + return nil, false +} + +func (p *parser) parseZeroOrMoreExpr(expr *zeroOrMoreExpr) (any, bool) { + if p.debug { + defer p.out(p.in("parseZeroOrMoreExpr")) + } + + var vals []any + + for { + p.pushV() + val, ok := p.parseExprWrap(expr.expr) + p.popV() + if !ok { + return vals, true + } + vals = append(vals, val) + } +} + +func (p *parser) parseZeroOrOneExpr(expr *zeroOrOneExpr) (any, bool) { + if p.debug { + defer p.out(p.in("parseZeroOrOneExpr")) + } + + p.pushV() + val, _ := p.parseExprWrap(expr.expr) + p.popV() + // whether it matched or not, consider it a match + return val, true +} diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/types.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/types.go new file mode 100644 index 0000000000..1d5b2b6b8c --- /dev/null +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/types.go @@ -0,0 +1,13 @@ +package durationparser + +// Result holds the parsed components of a duration string. +type Result struct { + Sign string // "" or "-" or "+" + Segments []Segment +} + +// Segment holds a single parsed segment (e.g. Digits="1.5", Unit="d"). +type Segment struct { + Digits string + Unit string +} diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/eval.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/eval.go index 6f93ba530e..87b94d1646 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/topdown/eval.go +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/eval.go @@ -92,8 +92,9 @@ type eval struct { input *ast.Term data *ast.Term external *resolverTrie + externalTreeStack *externalTreeStack targetStack *refStack - traceLastLocation *ast.Location // Last location of a trace event. + traceLastLocation *ast.Location instr *Instrumentation builtins map[string]*Builtin builtinCache builtins.Cache @@ -108,6 +109,7 @@ type eval struct { runtime *ast.Term builtinErrors *builtinErrors roundTripper CustomizeRoundTripper + evaluated *EvaluatedRuleTracker genvarprefix string query ast.Body tracers []QueryTracer @@ -124,6 +126,8 @@ type eval struct { findOne bool strictObjects bool defined bool + requestMetadata map[string]any + responseMetadata map[string]any } type ( @@ -231,7 +235,6 @@ func (e *eval) closure(query ast.Body, cpy *eval) { } // childWithBindingSizeHint creates a child evaluator with bindings pre-sized for the expected number of variables. -// This reduces memory waste when evaluating functions or rules with known argument counts. func (e *eval) childWithBindingSizeHint(query ast.Body, cpy *eval, sizeHint int) { *cpy = *e cpy.index = 0 @@ -265,12 +268,15 @@ func (e *eval) unknown(x any, b *bindings) bool { x = ast.NewTerm(v) } - return saveRequired(e.compiler, e.inliningControl, true, e.saveSet, b, x, false) + return saveRequired(e.compiler.RuleTree, e.externalTreeStack, e.inliningControl, true, e.saveSet, b, x, false) } // exactly like `unknown` above` but without the cost of `any` boxing when arg is known to be a ref func (e *eval) unknownRef(ref ast.Ref, b *bindings) bool { - return e.partial() && saveRequired(e.compiler, e.inliningControl, true, e.saveSet, b, ast.NewTerm(ref), false) + if !e.partial() { + return false + } + return saveRequired(e.compiler.RuleTree, e.externalTreeStack, e.inliningControl, true, e.saveSet, b, ast.NewTerm(ref), false) } func (e *eval) traceEnter(x ast.Node) { @@ -517,8 +523,8 @@ func (e *eval) evalStep(iter evalIterator) error { } case *ast.Every: eval := evalEvery{ - Every: terms, e: e, + every: terms, expr: expr, } err = eval.eval(func() error { @@ -528,6 +534,19 @@ func (e *eval) evalStep(iter evalIterator) error { return err }) + case *ast.Not: + en := evalNot{ + e: e, + not: terms, + expr: expr, + } + err = en.eval(func(e *eval) error { + defined = true + err := iter(e) + e.traceRedo(expr) + return err + }) + default: // guard-rail for adding extra (Expr).Terms types return fmt.Errorf("got %T terms: %[1]v", terms) } @@ -571,14 +590,24 @@ func (e *eval) evalStep(iter evalIterator) error { }) case *ast.Every: eval := evalEvery{ - Every: terms, e: e, + every: terms, expr: expr, } err = eval.eval(func() error { return iter(e) }) + case *ast.Not: + en := evalNot{ + e: e, + not: terms, + expr: expr, + } + err = en.eval(func(e *eval) error { + return iter(e) + }) + default: // guard-rail for adding extra (Expr).Terms types return fmt.Errorf("got %T terms: %[1]v", terms) } @@ -604,7 +633,7 @@ func (e *eval) evalNot(iter evalIterator) error { expr := e.query[e.index] if e.unknown(expr, e.bindings) { - return e.evalNotPartial(iter) + return e.setupAndEvalNotPartial(iter) } negation := ast.NewBody(expr.ComplementNoWith()) @@ -730,26 +759,33 @@ func (e *eval) evalWith(iter evalIterator) error { } } - oldInput, oldData := e.evalWithPush(input, data, functionMocks, targets, disable) + oldInput, oldData, pushedFrame := e.evalWithPush(input, data, functionMocks, targets, disable) err = e.evalStep(func(e *eval) error { - e.evalWithPop(oldInput, oldData) + e.evalWithPop(oldInput, oldData, pushedFrame) err := e.next(iter) - oldInput, oldData = e.evalWithPush(input, data, functionMocks, targets, disable) + oldInput, oldData, pushedFrame = e.evalWithPush(input, data, functionMocks, targets, disable) return err }) - e.evalWithPop(oldInput, oldData) + e.evalWithPop(oldInput, oldData, pushedFrame) return err } -func (e *eval) evalWithPush(input, data *ast.Term, functionMocks [][2]*ast.Term, targets, disable []ast.Ref) (*ast.Term, *ast.Term) { +func (e *eval) evalWithPush(input, data *ast.Term, functionMocks [][2]*ast.Term, targets, disable []ast.Ref) (*ast.Term, *ast.Term, bool) { var oldInput *ast.Term + var pushedFrame bool if input != nil { oldInput = e.input e.input = input + + // When input changes, push a new frame for external tree caching + if e.externalTreeStack != nil { + e.externalTreeStack.PushFrame() + pushedFrame = true + } } var oldData *ast.Term @@ -779,29 +815,62 @@ func (e *eval) evalWithPush(input, data *ast.Term, functionMocks [][2]*ast.Term, e.functionMocks.PutPairs(functionMocks) - return oldInput, oldData + return oldInput, oldData, pushedFrame } -func (e *eval) evalWithPop(input, data *ast.Term) { +func (e *eval) evalWithPop(input, data *ast.Term, popFrame bool) { // NOTE(ae) no nil checks here as we assume evalWithPush always called first e.inliningControl.PopDisable() e.targetStack.Pop() e.virtualCache.Pop() e.comprehensionCache.Pop() e.functionMocks.PopPairs() + + // When input is restored, pop the external tree frame + if popFrame { + e.externalTreeStack.PopFrame() + } + e.data = data e.input = input } -func (e *eval) evalNotPartial(iter evalIterator) error { +func (e *eval) setupAndEvalNotPartial(iter evalIterator) error { // Prepare query normally. expr := e.query[e.index] - negation := expr.ComplementNoWith() + unNegate := func(expr *ast.Expr) ast.Body { + return ast.NewBody(expr.ComplementNoWith()) + } + + complement := func(expr *ast.Expr) []*ast.Expr { + if expr.IsNot() { + return ast.Complement(expr) + } + return []*ast.Expr{expr.Complement()} + } + + supportTerms := func(terms any) any { + return terms + } + + return e.evalNotPartial(expr, unNegate, complement, supportTerms, iter) +} + +// unNegateFn returns the "unwrapped" non-negated expressions (1..*) of a negated expressions +type unNegateFn func(expr *ast.Expr) ast.Body + +// complementFn returns the set of expressions (1..*) that is the complement of an expression +type complementFn func(*ast.Expr) []*ast.Expr + +// supportTermsFn transforms the given terms to a support reference/call to the form required by the negated expression's ast.Expr.Terms field. +type supportTermsFn func(terms any) any + +func (e *eval) evalNotPartial(expr *ast.Expr, unNegateFn unNegateFn, complementFn complementFn, supportTermsFn supportTermsFn, iter evalIterator) error { child := evalPool.Get() defer evalPool.Put(child) - e.closure(ast.NewBody(negation), child) + e.closure(unNegateFn(expr), child) // Unknowns is the set of variables that are marked as unknown. The variables // are namespaced with the query ID that they originate in. This ensures that @@ -853,7 +922,7 @@ func (e *eval) evalNotPartial(iter evalIterator) error { // the unknowns as safe because vars in the save set will either be known to // the caller or made safe by an expression on the save stack. if !canInlineNegation(unknowns, savedQueries) { - return e.evalNotPartialSupport(child.queryID, expr, unknowns, savedQueries, iter) + return e.evalNotPartialSupport(child.queryID, expr, supportTermsFn, unknowns, savedQueries, iter) } // If we can inline the result, we have to generate the cross product of the @@ -864,14 +933,14 @@ func (e *eval) evalNotPartial(iter evalIterator) error { // Becomes: // // (!A && !C) || (!A && !D) || (!B && !C) || (!B && !D) - return complementedCartesianProduct(savedQueries, 0, nil, func(q ast.Body) error { + return complementedCartesianProduct(savedQueries, 0, nil, complementFn, func(q ast.Body) error { return e.saveInlinedNegatedExprs(q, func() error { return iter(e) }) }) } -func (e *eval) evalNotPartialSupport(negationID uint64, expr *ast.Expr, unknowns ast.VarSet, queries []ast.Body, iter evalIterator) error { +func (e *eval) evalNotPartialSupport(negationID uint64, expr *ast.Expr, supportTermsFn supportTermsFn, unknowns ast.VarSet, queries []ast.Body, iter evalIterator) error { // Prepare support rule head. supportName := fmt.Sprintf("__not%d_%d_%d__", e.queryID, e.index, negationID) @@ -915,9 +984,9 @@ func (e *eval) evalNotPartialSupport(negationID uint64, expr *ast.Expr, unknowns terms := make([]*ast.Term, len(args)+1) terms[0] = term copy(terms[1:], args) - cpy.Terms = terms + cpy.Terms = supportTermsFn(terms) } else { - cpy.Terms = term + cpy.Terms = supportTermsFn(term) } return e.saveInlinedNegatedExprs([]*ast.Expr{cpy}, func() error { @@ -955,15 +1024,19 @@ func (e *eval) evalCall(terms []*ast.Term, iter unifyIterator) error { var ir *ast.IndexResult var err error + index := e.ruleIndex(ref) if e.partial() { - ir, err = e.getRules(ref, nil) + ir, err = e.getRules(ref, nil, index) } else { - ir, err = e.getRules(ref, terms[1:]) + ir, err = e.getRules(ref, terms[1:], index) } defer ast.IndexResultPool.Put(ir) if err != nil { return err } + if ir == nil { + return nil + } eval := evalFuncPool.Get() defer evalFuncPool.Put(eval) @@ -1027,6 +1100,8 @@ func (e *eval) evalCall(terms []*ast.Term, iter unifyIterator) error { DistributedTracingOpts: e.tracingOpts, Capabilities: capabilities, RoundTripper: e.roundTripper, + RequestMetadata: e.requestMetadata, + ResponseMetadata: e.responseMetadata, } } @@ -1697,11 +1772,10 @@ func (e *eval) saveInlinedNegatedExprs(exprs []*ast.Expr, iter unifyIterator) er return err } -func (e *eval) getRules(ref ast.Ref, args []*ast.Term) (*ast.IndexResult, error) { +func (e *eval) getRules(ref ast.Ref, args []*ast.Term, index ast.RuleIndex) (*ast.IndexResult, error) { e.instr.startTimer(evalOpRuleIndex) defer e.instr.stopTimer(evalOpRuleIndex) - index := e.ruleIndex(ref) if index == nil { return nil, nil } @@ -1715,6 +1789,7 @@ func (e *eval) getRules(ref ast.Ref, args []*ast.Term) (*ast.IndexResult, error) var result *ast.IndexResult var err error + resolver.e = e if e.indexing { resolver.args = args @@ -1745,18 +1820,13 @@ func (e *eval) getRules(ref ast.Ref, args []*ast.Term) (*ast.IndexResult, error) // Copy ref here as ref otherwise always escapes to the heap, // whether tracing is enabled or not. - r := ref.Copy() + r := ref.CopyNonGround() e.traceIndex(e.query[e.index], msg.String(), &r) } return result, err } -// ruleIndex performs a lookup for a RuleIndex in the compiler's RuleTree. -func (e *eval) ruleIndex(ref ast.Ref) ast.RuleIndex { - return e.compiler.RuleIndex(ref) -} - func (e *eval) Resolve(ref ast.Ref) (ast.Value, error) { return (&evalResolver{e: e}).Resolve(ref) } @@ -1969,7 +2039,7 @@ func (e *eval) getDeclArgsLen(x *ast.Expr) (int, error) { return bi.Decl.Arity(), nil } - ir, err := e.getRules(operator, nil) + ir, err := e.getRules(operator, nil, e.ruleIndex(operator)) defer ast.IndexResultPool.Put(ir) if err != nil { return -1, err @@ -2303,6 +2373,7 @@ func (e *evalFunc) evalOneRule(iter unifyIterator, rule *ast.Rule, args []*ast.T err := child.biunifyTerms(e.terms[1:], args, e.e.bindings, child.bindings, func() error { return child.eval(func(child *eval) error { child.traceExit(rule) + e.e.evaluated.Record(rule) // Partial evaluation must save an expression that tests the output value if the output value // was not captured to handle the case where the output value may be `false`. @@ -2509,10 +2580,48 @@ func (e evalTree) next(iter unifyIterator, plugged *ast.Term) error { cpy.plugged[e.pos] = plugged cpy.pos++ + // Track whether we pushed an external tree that needs cleanup + pushedExternalTree := false if !e.e.targetStack.Prefixed(cpy.plugged[:cpy.pos]) { if e.node != nil { node = e.node.Child(plugged.Value) - if node != nil && len(node.Values) > 0 { + + // Handle external sources transparently + if node != nil && node.External != nil { + externalRef := node.External.Ref + externalIndex := node.External.Index + + // Initialize externalTreeStack if needed + if e.e.externalTreeStack == nil { + e.e.externalTreeStack = newExternalTreeStack(e.e) + } + + // Check cache first + cachedNode, _, found := e.e.externalTreeStack.findCached(externalRef) + if found { + node = cachedNode + } else { + // Call Tree() and cache the result + e.e.instr.startTimer(evalOpExternalRuleSource) + tree, updatedIndex, err := node.External.Tree(e.e.ctx, e.e.compiler.RuleTree, externalRef, e.e.input, e.e.metrics, e.e.requestMetadata, e.e.responseMetadata) + e.e.instr.stopTimer(evalOpExternalRuleSource) + if err != nil { + return err + } + if tree != nil { + if updatedIndex != nil { + externalIndex = updatedIndex + } + e.e.externalTreeStack.Push(externalRef, tree, externalIndex, e.e.input) + node = tree + pushedExternalTree = true + } + } + } + + hasRules := node != nil && len(node.Values) > 0 + + if hasRules { r := evalVirtual{ e: e.e, ref: e.ref, @@ -2523,13 +2632,21 @@ func (e evalTree) next(iter unifyIterator, plugged *ast.Term) error { rbindings: e.rbindings, } r.plugged[e.pos] = plugged - return r.eval(iter) + err := r.eval(iter) + if pushedExternalTree { + e.e.externalTreeStack.Pop() + } + return err } } } cpy.node = node - return cpy.eval(iter) + err := cpy.eval(iter) + if pushedExternalTree { + e.e.externalTreeStack.Pop() + } + return err } // enumerateNext is a helper to avoid closure allocation in enumerate loops. @@ -2705,12 +2822,16 @@ type evalVirtual struct { func (e evalVirtual) eval(iter unifyIterator) error { - ir, err := e.e.getRules(e.plugged[:e.pos+1], nil) + ir, err := e.e.getRules(e.plugged[:e.pos+1], nil, e.e.ruleIndex(e.plugged[:e.pos+1])) defer ast.IndexResultPool.Put(ir) if err != nil { return err } + if ir == nil { + return nil + } + // Partial evaluation of ordered rules is not supported currently. Save the // expression and continue. This could be revisited in the future. if len(ir.Else) > 0 && e.e.unknownRef(e.ref, e.bindings) { @@ -2925,6 +3046,7 @@ func (e evalVirtualPartial) evalAllRulesNoCache(rules []*ast.Rule) (*ast.Term, e child.traceEnter(rule) err := child.eval(func(*eval) error { child.traceExit(rule) + e.e.evaluated.Record(rule) var err error result, _, err = e.reduce(rule, child.bindings, result, &visitedRefs) if err != nil { @@ -3397,7 +3519,7 @@ func (q vcKeyScope) AppendText(buf []byte) ([]byte, error) { // reduce removes vars from the tail of the ref. func (q vcKeyScope) reduce() vcKeyScope { - ref := q.Ref.Copy() + ref := q.Ref.CopyNonGround() var i int for i = len(q.Ref) - 1; i >= 0; i-- { if _, ok := q.Ref[i].Value.(ast.Var); !ok { @@ -3647,6 +3769,7 @@ func (e evalVirtualComplete) evalValueRule(iter unifyIterator, rule *ast.Rule, p var result *ast.Term err := child.eval(func(child *eval) error { child.traceExit(rule) + e.e.evaluated.Record(rule) result = child.bindings.Plug(rule.Head.Value) @@ -3990,19 +4113,19 @@ func (e evalTerm) save(iter unifyIterator) error { } type evalEvery struct { - *ast.Every - e *eval - expr *ast.Expr + e *eval + every *ast.Every + expr *ast.Expr } func (e evalEvery) eval(iter unifyIterator) error { // unknowns in domain or body: save the expression, PE its body // partial() check to avoid e.Body -> Node boxing allocation - if e.e.partial() && (e.e.unknown(e.Domain, e.e.bindings) || e.e.unknown(e.Body, e.e.bindings)) { + if e.e.partial() && (e.e.unknown(e.every.Domain, e.e.bindings) || e.e.unknown(e.every.Body, e.e.bindings)) { return e.save(iter) } - if pd := e.e.bindings.Plug(e.Domain); pd != nil { + if pd := e.e.bindings.Plug(e.every.Domain); pd != nil { if !isIterableValue(pd.Value) { e.e.traceFail(e.expr) return nil @@ -4011,9 +4134,9 @@ func (e evalEvery) eval(iter unifyIterator) error { generator := ast.NewBody( ast.Equality.Expr( - ast.RefTerm(e.Domain, e.Key).SetLocation(e.Domain.Location), - e.Value, - ).SetLocation(e.Domain.Location), + ast.RefTerm(e.every.Domain, e.every.Key).SetLocation(e.every.Domain.Location), + e.every.Value, + ).SetLocation(e.every.Domain.Location), ) domain := evalPool.Get() @@ -4035,18 +4158,18 @@ func (e evalEvery) eval(iter unifyIterator) error { body := evalPool.Get() defer evalPool.Put(body) - child.closure(e.Body, body) + child.closure(e.every.Body, body) body.findOne = true if e.e.traceEnabled { - body.traceEnter(e.Body) + body.traceEnter(e.every.Body) } done := false err := body.eval(func(*eval) error { if e.e.traceEnabled { - body.traceExit(e.Body) - body.traceRedo(e.Body) + body.traceExit(e.every.Body) + body.traceRedo(e.every.Body) } done = true @@ -4111,6 +4234,107 @@ func (e *evalEvery) plug(expr *ast.Expr) *ast.Expr { return cpy } +type evalNot struct { + e *eval + not *ast.Not + expr *ast.Expr +} + +func (e evalNot) eval(iter evalIterator) error { + if e.e.partial() && e.e.unknown(e.not.Body, e.e.bindings) { + return e.evalPartial(iter) + } + + child := evalPool.Get() + defer evalPool.Put(child) + + e.e.closure(e.not.Body, child) + + if e.e.traceEnabled { + child.traceEnter(e.not.Body) + } + + if err := child.eval(func(*eval) error { + if e.e.traceEnabled { + child.traceExit(e.not.Body) + child.traceRedo(e.not.Body) + } + child.defined = true + + return nil + }); err != nil { + return err + } + + if !child.defined { + return iter(e.e) + } + + return nil +} + +func (e evalNot) evalPartial(iter evalIterator) error { + if e.not.ExplicitBody && !e.e.inliningControl.shallow { + // we don't need to calculate a cartesian product for explicit not-bodies, as each 'not' acts as a closure for PE:d child expressions. + + child := evalPool.Get() + defer evalPool.Put(child) + + e.e.closure(e.not.Body, child) + + var savedQueries []ast.Body + e.e.saveStack.PushQuery(nil) + + _ = child.eval(func(*eval) error { + query := e.e.saveStack.Peek() + plugged := query.Plug(e.e.caller.bindings) + + // Note: we could possibly apply copy propagation here, but that might fold calls into a nested structure that would require a type-checker update. + + // Skip this rule body if it fails to type-check. + // Type-checking failure means the rule body will never succeed. + if !e.e.compiler.PassesTypeCheck(plugged) { + return nil + } + + savedQueries = append(savedQueries, plugged) + return nil + }) // cannot return error + + e.e.saveStack.PopQuery() + + // If partial evaluation produced no results, the expression is always undefined + // so it does not have to be saved. + if len(savedQueries) == 0 { + return iter(e.e) + } + + q := make([]*ast.Expr, 0, len(savedQueries)) + for i := range savedQueries { + q = append(q, ast.NewExpr(&ast.Not{ + Body: savedQueries[i].Copy(), + ExplicitBody: true, + })) + } + + return e.e.saveInlinedNegatedExprs(q, func() error { + return iter(e.e) + }) + } + + expr := e.e.query[e.e.index] + + unNegate := func(expr *ast.Expr) ast.Body { + return e.not.Body + } + + supportTerms := func(terms any) any { + return ast.NewNot(ast.NewExpr(terms)) + } + + return e.e.evalNotPartial(expr, unNegate, ast.Complement, supportTerms, iter) +} + func (e *eval) comprehensionIndex(term *ast.Term) *ast.ComprehensionIndex { if e.queryCompiler != nil { return e.queryCompiler.ComprehensionIndex(term) @@ -4120,7 +4344,7 @@ func (e *eval) comprehensionIndex(term *ast.Term) *ast.ComprehensionIndex { func (e *eval) namespaceRef(ref ast.Ref) ast.Ref { if e.skipSaveNamespace { - return ref.Copy() + return ref.CopyNonGround() } return ref.Insert(e.saveNamespace, 1) } @@ -4202,7 +4426,7 @@ func canInlineNegation(safe ast.VarSet, queries []ast.Body) bool { // in the future, we can handle more cases. return false } - if !expr.Negated { + if !expr.IsNegated() { // Positive expressions containing variables cannot be trivially negated // because they become unsafe (e.g., "x = 1" negated is "not x = 1" making x // unsafe.) We check if the vars in the expr are already safe. @@ -4264,6 +4488,15 @@ func containsNestedRefOrCall(vis *nestedCheckVisitor, expr *ast.Expr) bool { return false } + if n, ok := expr.Terms.(*ast.Not); ok { + for _, nExpr := range n.Body { + if containsNestedRefOrCall(vis, nExpr) { + return true + } + } + return false + } + return containsNestedRefOrCallInTerm(vis, expr.Terms.(*ast.Term)) } @@ -4286,16 +4519,17 @@ func containsNestedRefOrCallInTerm(vis *nestedCheckVisitor, term *ast.Term) bool } } -func complementedCartesianProduct(queries []ast.Body, idx int, curr ast.Body, iter func(ast.Body) error) error { +func complementedCartesianProduct(queries []ast.Body, idx int, curr ast.Body, complement func(expr *ast.Expr) []*ast.Expr, iter func(ast.Body) error) error { if idx == len(queries) { return iter(curr) } for _, expr := range queries[idx] { - curr = append(curr, expr.Complement()) - if err := complementedCartesianProduct(queries, idx+1, curr, iter); err != nil { + mark := len(curr) + curr = append(curr, complement(expr)...) + if err := complementedCartesianProduct(queries, idx+1, curr, complement, iter); err != nil { return err } - curr = curr[:len(curr)-1] + curr = curr[:mark] } return nil } @@ -4426,3 +4660,180 @@ func (e *eval) updateSavedMocks(withs []*ast.With) []*ast.With { } return ret } + +// simpleTreeNode provides minimal tree structure for navigation +type simpleTreeNode struct { + tree *ast.TreeNode + children map[ast.Value]*simpleTreeNode +} + +func newSimpleTreeNode() *simpleTreeNode { + return &simpleTreeNode{ + children: make(map[ast.Value]*simpleTreeNode), + } +} + +// externalTreeStack caches external rule trees and tracks frames for input changes. +// It maintains both a flat cache for lookups and a tree structure for navigation. +type externalTreeStack struct { + eval *eval + entries []externalTreeEntry // flat list of cached entries + frames []int // frame markers for input changes (indices into entries) + root *simpleTreeNode // tree structure for navigation +} + +type externalTreeEntry struct { + ref ast.Ref + input *ast.Term + tree *ast.TreeNode + index ast.ExternalRuleIndex +} + +func newExternalTreeStack(e *eval) *externalTreeStack { + return &externalTreeStack{ + eval: e, + entries: make([]externalTreeEntry, 0, 4), + frames: make([]int, 0, 4), + } +} + +// findCached checks if we already have a cached tree for this ref. +// Frame tracking ensures any cached entry has the correct input. +func (s *externalTreeStack) findCached(ref ast.Ref) (*ast.TreeNode, ast.ExternalRuleIndex, bool) { + // Determine search boundary: only search within current frame if one exists + startIdx := 0 + if len(s.frames) > 0 { + startIdx = s.frames[len(s.frames)-1] + } + + // Search from most recent to the frame boundary + for i := len(s.entries) - 1; i >= startIdx; i-- { + entry := &s.entries[i] + if entry.ref.Equal(ref) { + return entry.tree, entry.index, true + } + } + return nil, nil, false +} + +func (s *externalTreeStack) Push(ref ast.Ref, tree *ast.TreeNode, index ast.ExternalRuleIndex, input *ast.Term) { + // Add entry to cache (we never have duplicates in the same frame) + s.entries = append(s.entries, externalTreeEntry{ + ref: ref, + input: input, + tree: tree, + index: index, + }) + + // Update root tree structure + if s.root == nil { + s.root = newSimpleTreeNode() + } + node := s.root + for _, term := range ref { + key := term.Value + if node.children[key] == nil { + node.children[key] = newSimpleTreeNode() + } + node = node.children[key] + } + node.tree = tree +} + +// Pop removes the most recent entry from the stack and closes its index. +// If a frame is active and entries are at or below the frame boundary, +// Pop is a no-op — PopFrame already cleaned up (or will clean up) those entries. +func (s *externalTreeStack) Pop() { + if len(s.entries) == 0 { + return + } + + // Don't pop at or below the current frame boundary. + if len(s.frames) > 0 && len(s.entries) <= s.frames[len(s.frames)-1] { + return + } + + // Close the external index if it supports closing + lastEntry := &s.entries[len(s.entries)-1] + if closer, ok := lastEntry.index.(ast.ExternalRuleIndexCloser); ok { + _ = closer.Close() + } + + // Remove the most recent entry + s.entries = s.entries[:len(s.entries)-1] + + // Rebuild tree from remaining entries + s.root = newSimpleTreeNode() + for i := range s.entries { + entry := &s.entries[i] + node := s.root + for _, term := range entry.ref { + key := term.Value + if node.children[key] == nil { + node.children[key] = newSimpleTreeNode() + } + node = node.children[key] + } + node.tree = entry.tree + } +} + +// PushFrame marks the current stack position for input changes +func (s *externalTreeStack) PushFrame() { + s.frames = append(s.frames, len(s.entries)) +} + +// PopFrame restores the cache to the marked position +func (s *externalTreeStack) PopFrame() { + if len(s.frames) == 0 { + return + } + + // Get the frame marker and truncate entries + targetSize := s.frames[len(s.frames)-1] + s.frames = s.frames[:len(s.frames)-1] + + // Close indices of entries being removed + for i := len(s.entries) - 1; i >= targetSize; i-- { + if closer, ok := s.entries[i].index.(ast.ExternalRuleIndexCloser); ok { + _ = closer.Close() + } + } + + // Rebuild tree from remaining entries + s.root = newSimpleTreeNode() + for i := range targetSize { + entry := &s.entries[i] + node := s.root + for _, term := range entry.ref { + key := term.Value + if node.children[key] == nil { + node.children[key] = newSimpleTreeNode() + } + node = node.children[key] + } + node.tree = entry.tree + } + + s.entries = s.entries[:targetSize] +} + +// ruleIndex performs a shadowed lookup for a RuleIndex, checking external trees first. +// It searches through the pushStack (most recent to oldest), navigating the tree structure +// to find matching rules, then falls back to the compiler's static RuleTree. +func (e *eval) ruleIndex(ref ast.Ref) ast.RuleIndex { + if e.externalTreeStack != nil && len(e.externalTreeStack.entries) > 0 { + // Search from most recent to oldest + for i := len(e.externalTreeStack.entries) - 1; i >= 0; i-- { + entry := &e.externalTreeStack.entries[i] + if ref.HasPrefix(entry.ref) { + // Look for the relative ref in the cached tree + relativeRef := ref[len(entry.ref):] + if found := entry.tree.Find(relativeRef); found != nil { + return found.Index + } + } + } + } + return e.compiler.RuleIndex(ref) +} diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/evaluated.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/evaluated.go new file mode 100644 index 0000000000..c92be6fa93 --- /dev/null +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/evaluated.go @@ -0,0 +1,50 @@ +// Copyright 2026 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package topdown + +import ( + "github.com/open-policy-agent/opa/v1/ast" +) + +// EvaluatedRuleTracker records labels from annotations during evaluation. +// For each successfully evaluated rule, labels from the rule's annotation +// chain (subpackages, package, document, rule) are merged into a single map +// with inner-scope-wins precedence. The merged maps are deduplicated across +// rules so that identical label sets collapse to a single entry. +// +// An AnnotationSet must be set via WithAnnotationSet for the tracker to +// resolve chains; without it, Record is a no-op. +type EvaluatedRuleTracker struct { + Labels []map[string]any + seen map[string]struct{} + as *ast.AnnotationSet +} + +// WithAnnotationSet configures the AnnotationSet used to resolve each +// evaluated rule's annotation chain. Typically wired from the compiler. +func (t *EvaluatedRuleTracker) WithAnnotationSet(as *ast.AnnotationSet) *EvaluatedRuleTracker { + if t != nil { + t.as = as + } + return t +} + +func (t *EvaluatedRuleTracker) Record(rule *ast.Rule) { + if t == nil || t.as == nil { + return + } + labels, key := t.as.MergedLabels(rule) + if len(labels) == 0 { + return + } + if t.seen == nil { + t.seen = make(map[string]struct{}) + } + if _, dup := t.seen[key]; dup { + return + } + t.seen[key] = struct{}{} + t.Labels = append(t.Labels, labels) +} diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/instrumentation.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/instrumentation.go index 93da1d0022..3fc7e0138e 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/topdown/instrumentation.go +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/instrumentation.go @@ -19,6 +19,7 @@ const ( evalOpComprehensionCacheBuild = "eval_op_comprehension_cache_build" evalOpComprehensionCacheHit = "eval_op_comprehension_cache_hit" evalOpComprehensionCacheMiss = "eval_op_comprehension_cache_miss" + evalOpExternalRuleSource = "eval_op_external_rule_source" partialOpSaveUnify = "partial_op_save_unify" partialOpSaveSetContains = "partial_op_save_set_contains" partialOpSaveSetContainsRec = "partial_op_save_set_contains_rec" diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/jsonschema.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/jsonschema.go index e48719d145..0236e5be09 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/topdown/jsonschema.go +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/jsonschema.go @@ -47,6 +47,19 @@ func newResultTerm(valid bool, data *ast.Term) *ast.Term { return ast.ArrayTerm(ast.InternedTerm(valid), data) } +// newPatternValidatingSchemaLoader returns a SchemaLoader configured to +// compile and enforce the "pattern" keyword. This is the variant used by the +// json.verify_schema and json.match_schema built-ins, where runtime pattern +// validation is expected. It is intentionally not shared with the compile-time +// type-checking path, where pattern validation is disabled to tolerate +// schemas containing ECMA-262 regex features that Go's RE2 dialect can't +// compile. +func newPatternValidatingSchemaLoader() *gojsonschema.SchemaLoader { + sl := gojsonschema.NewSchemaLoader() + sl.ValidatePatterns = true + return sl +} + // builtinJSONSchemaVerify accepts 1 argument which can be string or object and checks if it is valid JSON schema. // Returns array [false, ] with error string at index 1, or [true, ""] with empty string at index 1 otherwise. func builtinJSONSchemaVerify(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { @@ -57,7 +70,7 @@ func builtinJSONSchemaVerify(_ BuiltinContext, operands []*ast.Term, iter func(* } // Check that schema is correct and parses without errors. - if _, err = gojsonschema.NewSchema(loader); err != nil { + if _, err = newPatternValidatingSchemaLoader().Compile(loader); err != nil { return iter(newResultTerm(false, ast.StringTerm("jsonschema: "+err.Error()))) } @@ -93,7 +106,7 @@ func builtinJSONMatchSchema(bctx BuiltinContext, operands []*ast.Term, iter func return err } - schema, err = gojsonschema.NewSchema(schemaLoader) + schema, err = newPatternValidatingSchemaLoader().Compile(schemaLoader) if err != nil { return err } diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_bytes.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_bytes.go index cd36b87b17..f912d276c2 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_bytes.go +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_bytes.go @@ -5,6 +5,7 @@ package topdown import ( + "errors" "fmt" "math/big" "strings" @@ -43,8 +44,13 @@ var ( errBytesValueNoAmount = parseNumBytesError("no byte amount provided") errBytesValueNumConv = parseNumBytesError("could not parse byte amount to a number") errBytesValueIncludesSpaces = parseNumBytesError("spaces not allowed in resource strings") + errBytesExponentTooLarge = parseNumBytesError("exponent too large") ) +// maxExponentDigits limits the number of digits allowed in the exponent of +// scientific notation input. +const maxExponentDigits = 6 + func builtinNumBytes(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { var m big.Float @@ -59,7 +65,10 @@ func builtinNumBytes(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term return errBytesValueIncludesSpaces } - num, unit := extractNumAndUnit(s) + num, unit, err := extractNumAndUnit(s) + if err != nil { + return errBytesExponentTooLarge + } if num == "" { return errBytesValueNoAmount } @@ -115,7 +124,9 @@ func formatString(s ast.String) string { // Splits the string into a number string à la "10" or "10.2" and a unit // string à la "gb" or "MiB" or "foo". Either can be an empty string // (error handling is provided elsewhere). -func extractNumAndUnit(s string) (string, string) { +// Returns an error if the exponent in scientific notation exceeds +// maxExponentDigits digits. +func extractNumAndUnit(s string) (string, string, error) { isNum := func(r rune) bool { return unicode.IsDigit(r) || r == '.' } @@ -138,18 +149,28 @@ func extractNumAndUnit(s string) (string, string) { if idx+1 < len(s) && (s[idx+1] == '+' || s[idx+1] == '-') { idx++ } + + // Count the digits in the exponent and reject if too large. + expStart := idx + 1 + expEnd := expStart + for expEnd < len(s) && unicode.IsDigit(rune(s[expEnd])) { + expEnd++ + } + if expEnd-expStart > maxExponentDigits { + return "", "", errors.New("exponent too large") + } } } if firstNonNumIdx == -1 { // only digits, '.', or valid scientific notation - return s, "" + return s, "", nil } if firstNonNumIdx == 0 { // only units (starts with non-digit) - return "", s + return "", s, nil } // Return the number and the rest as the unit - return s[:firstNonNumIdx], s[firstNonNumIdx:] + return s[:firstNonNumIdx], s[firstNonNumIdx:], nil } func init() { diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_units.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_units.go index 44aec86299..770eea9878 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_units.go +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_units.go @@ -34,9 +34,10 @@ func errUnitNotRecognized(unit string) error { } var ( - errNoAmount = parseUnitsError("no amount provided") - errNumConv = parseUnitsError("could not parse amount to a number") - errIncludesSpaces = parseUnitsError("spaces not allowed in resource strings") + errNoAmount = parseUnitsError("no amount provided") + errNumConv = parseUnitsError("could not parse amount to a number") + errIncludesSpaces = parseUnitsError("spaces not allowed in resource strings") + errUnitsExponentTooLarge = parseUnitsError("exponent too large") ) // Accepts both normal SI and binary SI units. @@ -56,7 +57,10 @@ func builtinUnits(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) e return errIncludesSpaces } - num, unit := extractNumAndUnit(s) + num, unit, err := extractNumAndUnit(s) + if err != nil { + return errUnitsExponentTooLarge + } if num == "" { return errNoAmount } diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/query.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/query.go index a65f8a312c..85b3ed9e93 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/topdown/query.go +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/query.go @@ -63,6 +63,9 @@ type Query struct { tracingOpts tracing.Options virtualCache VirtualCache baseCache BaseCache + requestMetadata map[string]any + responseMetadata map[string]any + evaluated *EvaluatedRuleTracker } // Builtin represents a built-in function that queries can call. @@ -333,6 +336,28 @@ func (q *Query) WithNondeterministicBuiltins(yes bool) *Query { return q } +// WithRequestMetadata sets arbitrary metadata from the caller that can be +// used by wrapping projects. The data is stored but not directly used by +// OPA's evaluation engine. +func (q *Query) WithRequestMetadata(m map[string]any) *Query { + q.requestMetadata = m + return q +} + +// WithResponseMetadata sets a map that wrapping projects can populate during +// evaluation to include additional fields in the API response. +func (q *Query) WithResponseMetadata(m map[string]any) *Query { + q.responseMetadata = m + return q +} + +// WithEvaluatedRuleTracker sets a tracker to record rule identifiers that were +// successfully evaluated. +func (q *Query) WithEvaluatedRuleTracker(t *EvaluatedRuleTracker) *Query { + q.evaluated = t + return q +} + // PartialRun executes partial evaluation on the query with respect to unknown // values. Partial evaluation attempts to evaluate as much of the query as // possible without requiring values for the unknowns set on the query. The @@ -344,6 +369,9 @@ func (q *Query) PartialRun(ctx context.Context) (partials []ast.Body, support [] if q.partialNamespace == "" { q.partialNamespace = "partial" // lazily initialize partial namespace } + if q.evaluated != nil && q.compiler != nil { + q.evaluated.WithAnnotationSet(q.compiler.GetAnnotationSet()) + } if q.seed == nil { q.seed = rand.Reader } @@ -407,13 +435,16 @@ func (q *Query) PartialRun(ctx context.Context) (partials []ast.Body, support [] shallow: q.shallowInlining, nondeterministicBuiltins: q.nondeterministicBuiltins, }, - genvarprefix: q.genvarprefix, - runtime: q.runtime, - indexing: q.indexing, - earlyExit: q.earlyExit, - builtinErrors: &builtinErrors{}, - printHook: q.printHook, - strictObjects: q.strictObjects, + genvarprefix: q.genvarprefix, + runtime: q.runtime, + indexing: q.indexing, + earlyExit: q.earlyExit, + builtinErrors: &builtinErrors{}, + printHook: q.printHook, + strictObjects: q.strictObjects, + requestMetadata: q.requestMetadata, + responseMetadata: q.responseMetadata, + evaluated: q.evaluated, } if len(q.disableInlining) > 0 { @@ -540,6 +571,10 @@ func (q *Query) Iter(ctx context.Context, iter func(QueryResult) error) error { } } + if q.evaluated != nil && q.compiler != nil { + q.evaluated.WithAnnotationSet(q.compiler.GetAnnotationSet()) + } + if q.seed == nil { q.seed = rand.Reader } @@ -602,6 +637,12 @@ func (q *Query) Iter(ctx context.Context, iter func(QueryResult) error) error { tracingOpts: q.tracingOpts, strictObjects: q.strictObjects, roundTripper: q.roundTripper, + requestMetadata: q.requestMetadata, + responseMetadata: q.responseMetadata, + evaluated: q.evaluated, + } + if e.requestMetadata == nil { + e.requestMetadata = map[string]any{} } e.caller = e q.metrics.Timer(metrics.RegoQueryEval).Start() diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/reachable.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/reachable.go index 1c31019db9..683c31c6c6 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/topdown/reachable.go +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/reachable.go @@ -74,39 +74,31 @@ func builtinReachable(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Ter // pathBuilder is called recursively to build a Set of paths that are reachable from the root func pathBuilder(graph ast.Object, root *ast.Term, path []*ast.Term, edgeRslt ast.Set, reached ast.Set) { - paths := []*ast.Term{} - - if edges := graph.Get(root); edges != nil { - path = append(path, root) - - if numberOfEdges(edges) >= 1 { - - foreachVertex(edges, func(neighbor *ast.Term) { + edges := graph.Get(root) + if edges == nil { + // Node not in graph — commit path without this node. + edgeRslt.Add(ast.ArrayTerm(path...)) + return + } - if reached.Contains(neighbor) { - // If we've already reached this node, return current path (avoid infinite recursion) - paths = append(paths, path...) - edgeRslt.Add(ast.ArrayTerm(paths...)) - } else { - reached.Add(root) - pathBuilder(graph, neighbor, path, edgeRslt, reached) + path = append(path, root) - } + if numberOfEdges(edges) == 0 { + edgeRslt.Add(ast.ArrayTerm(path...)) + return + } - }) + reached = reached.Copy() + reached.Add(root) + foreachVertex(edges, func(neighbor *ast.Term) { + if reached.Contains(neighbor) { + // Cycle detected — commit current path. + edgeRslt.Add(ast.ArrayTerm(path...)) } else { - paths = append(paths, path...) - edgeRslt.Add(ast.ArrayTerm(paths...)) - + pathBuilder(graph, neighbor, append([]*ast.Term(nil), path...), edgeRslt, reached) } - } else { - // Node is nonexistent (not in graph). Commit the current path (without adding this root) - paths = append(paths, path...) - edgeRslt.Add(ast.ArrayTerm(paths...)) - - } - + }) } func builtinReachablePaths(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/save.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/save.go index 47bf7521b4..7a922b5f7b 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/topdown/save.go +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/save.go @@ -357,7 +357,7 @@ func splitPackageAndRule(path ast.Ref) (ast.Ref, ast.Ref) { // being saved. This check allows the evaluator to evaluate statements // completely during partial evaluation as long as they do not depend on any // kind of unknown value or statements that would generate saves. -func saveRequired(c *ast.Compiler, ic *inliningControl, icIgnoreInternal bool, ss *saveSet, b *bindings, x any, rec bool) bool { +func saveRequired(compilerTree *ast.TreeNode, extStack *externalTreeStack, ic *inliningControl, icIgnoreInternal bool, ss *saveSet, b *bindings, x any, rec bool) bool { var found bool @@ -389,8 +389,9 @@ func saveRequired(c *ast.Compiler, ic *inliningControl, icIgnoreInternal bool, s } else if ic.Disabled(v.ConstantPrefix(), icIgnoreInternal) { found = true } else { - for _, rule := range c.GetRulesDynamicWithOpts(v, ast.RulesOptions{IncludeHiddenModules: false}) { - if saveRequired(c, ic, icIgnoreInternal, ss, b, rule, true) { + rules := getRulesDynamic(compilerTree, extStack, v, ast.RulesOptions{IncludeHiddenModules: false}) + for _, rule := range rules { + if saveRequired(compilerTree, extStack, ic, icIgnoreInternal, ss, b, rule, true) { found = true break } @@ -406,6 +407,76 @@ func saveRequired(c *ast.Compiler, ic *inliningControl, icIgnoreInternal bool, s return found } +// getRulesDynamic looks up rules in both the compiler tree and external sources. +func getRulesDynamic(compilerTree *ast.TreeNode, extStack *externalTreeStack, ref ast.Ref, opts ast.RulesOptions) []*ast.Rule { + var rules []*ast.Rule + + // Check external trees + if extStack != nil { + for i := range extStack.entries { + entry := &extStack.entries[i] + if entry.tree != nil && ref.HasPrefix(entry.ref) { + // Navigate into the external tree using the remaining path + remaining := ref[len(entry.ref):] + rules = append(rules, getRulesFromTree(entry.tree, remaining, opts)...) + } + } + } + + // Then check compiler tree + rules = append(rules, getRulesFromTree(compilerTree, ref, opts)...) + + return rules +} + +// getRulesFromTree walks a tree to find all rules matching the given ref. +func getRulesFromTree(node *ast.TreeNode, ref ast.Ref, opts ast.RulesOptions) []*ast.Rule { + set := map[*ast.Rule]struct{}{} + var walk func(*ast.TreeNode, int) + walk = func(nav *ast.TreeNode, i int) { + switch { + case i >= len(ref): + nav.DepthFirst(func(descendant *ast.TreeNode) bool { + for _, rule := range descendant.Values { + set[rule] = struct{}{} + } + if opts.IncludeHiddenModules { + return false + } + return descendant.Hide + }) + + case i == 0 || ast.IsConstant(ref[i].Value): + if child := nav.Child(ref[i].Value); child != nil { + for _, rule := range child.Values { + set[rule] = struct{}{} + } + walk(child, i+1) + } else { + return + } + + default: + for _, child := range nav.Children { + if child.Hide && !opts.IncludeHiddenModules { + continue + } + for _, rule := range child.Values { + set[rule] = struct{}{} + } + walk(child, i+1) + } + } + } + + walk(node, 0) + rules := make([]*ast.Rule, 0, len(set)) + for rule := range set { + rules = append(rules, rule) + } + return rules +} + func ignoreExprDuringPartial(expr *ast.Expr) bool { if !expr.IsCall() { return false diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/time.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/time.go index 16eae3e0bd..7171d5c0b2 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/topdown/time.go +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/time.go @@ -7,15 +7,18 @@ package topdown import ( "encoding/json" "errors" + "fmt" "math" "math/big" "strconv" + "strings" "sync" "time" _ "time/tzdata" // this is needed to have LoadLocation when no filesystem tzdata is available "github.com/open-policy-agent/opa/v1/ast" "github.com/open-policy-agent/opa/v1/topdown/builtins" + "github.com/open-policy-agent/opa/v1/topdown/durationparser" ) var tzCache map[string]*time.Location @@ -27,6 +30,84 @@ var minDateAllowedForNsConversion = time.Unix(0, math.MinInt64) // 2262-04-11T23:47:16.854775807-00:00 var maxDateAllowedForNsConversion = time.Unix(0, math.MaxInt64) +var durationCoefficients = map[string]int{ + "d": 24, + "w": 7 * 24, + "y": 365 * 24, +} + +// parseExtendedDuration parses a duration string that may contain extended +// units (d, w, y) mixed with standard Go duration units (h, m, s, ms, us, ns). +// Extended unit segments are rewritten to equivalent hours (e.g. "1d2h30m" → "24h2h30m") +func parseExtendedDuration(s string) (int64, error) { + if s == "" { + return 0, fmt.Errorf("time: invalid duration %q", s) + } + + if !strings.ContainsAny(s, "dwy") { + v, err := time.ParseDuration(s) + if err != nil { + return 0, err + } + return int64(v), nil + } + + result, err := durationparser.Parse("", []byte(s)) + if err != nil { + return 0, fmt.Errorf("time: invalid duration %q", s) + } + + rewritten, err := rewriteDuration(result.(durationparser.Result)) + if err != nil { + return 0, fmt.Errorf("time: invalid duration %q", s) + } + + v, err := time.ParseDuration(rewritten) + if err != nil { + // Replace the rewritten duration in the error with the original. + msg := err.Error() + if i := strings.LastIndex(msg, " "); i != -1 { + msg = msg[:i] + } + return 0, fmt.Errorf("%s %q", msg, s) + } + + return int64(v), nil +} + +// rewriteDuration converts parsed duration segments, replacing extended +// units (d, w, y) with their equivalent in hours. +func rewriteDuration(dr durationparser.Result) (string, error) { + var b strings.Builder + if dr.Sign != "" { + b.WriteString(dr.Sign) + } + for _, seg := range dr.Segments { + s, err := rewriteSegment(seg) + if err != nil { + return "", err + } + b.WriteString(s) + } + return b.String(), nil +} + +// rewriteSegment rewrites a single segment like {Digits:"1", Unit:"d"} to "24h". +// Segments with standard units are returned unchanged. +func rewriteSegment(seg durationparser.Segment) (string, error) { + coeff, ok := durationCoefficients[seg.Unit] + if !ok { + return seg.Digits + seg.Unit, nil + } + + val, err := strconv.ParseFloat(seg.Digits, 64) + if err != nil { + return "", fmt.Errorf("time: invalid duration: bad value %q for unit %q", seg.Digits, seg.Unit) + } + hours := val * float64(coeff) + return strconv.FormatFloat(hours, 'f', -1, 64) + "h", nil +} + func toSafeUnixNano(t time.Time, iter func(*ast.Term) error) error { if t.Before(minDateAllowedForNsConversion) || t.After(maxDateAllowedForNsConversion) { return errors.New("time outside of valid range") @@ -77,16 +158,19 @@ func builtinTimeParseRFC3339Nanos(_ BuiltinContext, operands []*ast.Term, iter f return toSafeUnixNano(result, iter) } + func builtinParseDurationNanos(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { duration, err := builtins.StringOperand(operands[0].Value, 1) if err != nil { return err } - value, err := time.ParseDuration(string(duration)) + + ns, err := parseExtendedDuration(string(duration)) if err != nil { return err } - return iter(ast.NumberTerm(int64ToJSONNumber(int64(value)))) + + return iter(ast.NumberTerm(int64ToJSONNumber(ns))) } // Represent exposed constants for formatting from the stdlib time pkg diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/tokens.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/tokens.go index bb2c9e1c1c..622f376c77 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/topdown/tokens.go +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/tokens.go @@ -1289,6 +1289,8 @@ func createTokenCacheKey(serializedJwt ast.Value, publicKey ast.Value) ast.Value } func init() { + jwk.Configure(jwk.WithMinRSAPublicExponent(0), jwk.WithMinRSAModulusBits(0)) + // By default, the JWT cache is disabled. disabled := true var tokenCache = cache.NamedValueCacheConfig{ diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/uri.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/uri.go new file mode 100644 index 0000000000..2b97c3e680 --- /dev/null +++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/uri.go @@ -0,0 +1,74 @@ +// Copyright 2026 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package topdown + +import ( + "cmp" + "net/url" + + "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/topdown/builtins" +) + +func builtinURIParse(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + str, err := builtins.StringOperand(operands[0].Value, 1) + if err != nil { + return err + } + + parsed, err := url.Parse(string(str)) + if err != nil { + return err + } + + obj := ast.NewObject() + + if parsed.Scheme != "" { + obj.Insert(ast.InternedTerm("scheme"), ast.StringTerm(parsed.Scheme)) + } + if hostname := parsed.Hostname(); hostname != "" { + obj.Insert(ast.InternedTerm("hostname"), ast.StringTerm(hostname)) + } + if port := parsed.Port(); port != "" { + obj.Insert(ast.InternedTerm("port"), ast.StringTerm(port)) + } + if parsed.Path != "" { + obj.Insert(ast.InternedTerm("path"), ast.StringTerm(parsed.Path)) + // raw_path is always set when path is present, so that users can + // rely on it being available for custom path normalization. + obj.Insert(ast.InternedTerm("raw_path"), ast.StringTerm(cmp.Or(parsed.RawPath, parsed.Path))) + } + if parsed.RawQuery != "" { + // raw_query can be piped into urlquery.decode_object() for structured access + obj.Insert(ast.InternedTerm("raw_query"), ast.StringTerm(parsed.RawQuery)) + } + if parsed.Fragment != "" { + obj.Insert(ast.InternedTerm("fragment"), ast.StringTerm(parsed.Fragment)) + } + + return iter(ast.NewTerm(obj)) +} + +func builtinURIIsValid(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + str, err := builtins.StringOperand(operands[0].Value, 1) + if err != nil { + return iter(ast.InternedTerm(false)) + } + + // Empty strings are technically valid relative references per RFC 3986, + // but are rejected here to avoid unexpected results for policy use cases. + if len(str) == 0 { + return iter(ast.InternedTerm(false)) + } + + _, err = url.Parse(string(str)) + + return iter(ast.InternedTerm(err == nil)) +} + +func init() { + RegisterBuiltinFunc(ast.URIParse.Name, builtinURIParse) + RegisterBuiltinFunc(ast.URIIsValid.Name, builtinURIIsValid) +} diff --git a/vendor/github.com/open-policy-agent/opa/v1/types/decode.go b/vendor/github.com/open-policy-agent/opa/v1/types/decode.go index 367b64bffb..f2515d5df1 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/types/decode.go +++ b/vendor/github.com/open-policy-agent/opa/v1/types/decode.go @@ -12,15 +12,16 @@ import ( ) const ( - typeNull = "null" - typeBoolean = "boolean" - typeNumber = "number" - typeString = "string" - typeArray = "array" - typeSet = "set" - typeObject = "object" - typeAny = "any" - typeFunction = "function" + typeNull = "null" + typeBoolean = "boolean" + typeNumber = "number" + typeString = "string" + typeArray = "array" + typeSet = "set" + typeObject = "object" + typeAny = "any" + typeFunction = "function" + typeRecursive = "recursive" ) // Unmarshal deserializes bs and returns the resulting type. diff --git a/vendor/github.com/open-policy-agent/opa/v1/types/types.go b/vendor/github.com/open-policy-agent/opa/v1/types/types.go index fc1db120a0..aff4fde39e 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/types/types.go +++ b/vendor/github.com/open-policy-agent/opa/v1/types/types.go @@ -48,15 +48,16 @@ type Type interface { json.Marshaler } -func (Null) typeMarker() string { return typeNull } -func (Boolean) typeMarker() string { return typeBoolean } -func (Number) typeMarker() string { return typeNumber } -func (String) typeMarker() string { return typeString } -func (*Array) typeMarker() string { return typeArray } -func (*Object) typeMarker() string { return typeObject } -func (*Set) typeMarker() string { return typeSet } -func (Any) typeMarker() string { return typeAny } -func (Function) typeMarker() string { return typeFunction } +func (Null) typeMarker() string { return typeNull } +func (Boolean) typeMarker() string { return typeBoolean } +func (Number) typeMarker() string { return typeNumber } +func (String) typeMarker() string { return typeString } +func (*Array) typeMarker() string { return typeArray } +func (*Object) typeMarker() string { return typeObject } +func (*Set) typeMarker() string { return typeSet } +func (Any) typeMarker() string { return typeAny } +func (Function) typeMarker() string { return typeFunction } +func (*Recursive) typeMarker() string { return typeRecursive } // Null represents the null type. type Null struct{} @@ -123,6 +124,13 @@ func unwrap(t Type) Type { } } +func unwrapRecursive(t Type) Type { + if r, ok := t.(*Recursive); ok { + return r.typ + } + return t +} + func (Null) String() string { return typeNull } @@ -508,6 +516,36 @@ func mergeObjects(a, b *Object) *Object { return NewObject(staticProps, dynamicProps) } +// Recursive is a Type that contains a pointer back to itself. +// This is for representing recursive JSON Schema definitions. +type Recursive struct { + name string // the $ref key (e.g. "#/$defs/foo") + typ Type // the referenced type (can be *Object, *Array, or Any) +} + +// NewRecursive returns a new Recursive type that wraps typ under the given name. +func NewRecursive(name string, typ Type) *Recursive { + return &Recursive{name: name, typ: typ} +} + +func (t *Recursive) String() string { + return "recursive(" + t.name + ")" +} + +func (t *Recursive) Unwrap() Type { + return t.typ +} + +func (t *Recursive) SetType(typ Type) { + t.typ = typ +} + +func (t *Recursive) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]any{ + "name": t.name, + }) +} + // Any represents a dynamic type. type Any []Type @@ -845,7 +883,7 @@ func (a FuncArgs) Arg(x int) Type { // Compare returns -1, 0, 1 based on comparison between a and b. func Compare(a, b Type) int { - a, b = unwrap(a), unwrap(b) + a, b = unwrapRecursive(unwrap(a)), unwrapRecursive(unwrap(b)) x := typeOrder(a) y := typeOrder(b) if x > y { @@ -859,6 +897,9 @@ func Compare(a, b Type) int { case *Array: arrA := a.(*Array) arrB := b.(*Array) + if arrA == arrB { + return 0 + } if arrA.dynamic != nil && arrB.dynamic == nil { return 1 } else if arrB.dynamic != nil && arrA.dynamic == nil { @@ -873,6 +914,9 @@ func Compare(a, b Type) int { case *Object: objA := a.(*Object) objB := b.(*Object) + if objA == objB { + return 0 + } if objA.dynamic != nil && objB.dynamic == nil { return 1 } else if objB.dynamic != nil && objA.dynamic == nil { @@ -985,7 +1029,7 @@ func Or(a, b Type) Type { // Select returns a property or item of a. func Select(a Type, x any) Type { - switch a := unwrap(a).(type) { + switch a := unwrapRecursive(unwrap(a)).(type) { case *Array: n, ok := x.(json.Number) if !ok { @@ -1028,7 +1072,7 @@ func Select(a Type, x any) Type { // keys are always number types, for objects the keys are always string types, // and for sets the keys are always the type of the set element. func Keys(a Type) Type { - switch a := unwrap(a).(type) { + switch a := unwrapRecursive(unwrap(a)).(type) { case *Array: return N case *Object: @@ -1058,7 +1102,7 @@ func Keys(a Type) Type { // Values returns the type of values that can be enumerated for a. func Values(a Type) Type { - switch a := unwrap(a).(type) { + switch a := unwrapRecursive(unwrap(a)).(type) { case *Array: var tpe Type for i := range a.static { @@ -1091,32 +1135,52 @@ func Values(a Type) Type { // Nil returns true if a's type is unknown. func Nil(a Type) bool { - switch a := unwrap(a).(type) { - case nil: + return nilRec(a, nil) +} + +func nilRec(a Type, seen map[Type]struct{}) bool { + a = unwrapRecursive(unwrap(a)) + if a == nil { return true + } + switch a := a.(type) { case *Function: - if slices.ContainsFunc(a.args, Nil) { + if slices.ContainsFunc(a.args, func(t Type) bool { return nilRec(t, seen) }) { return true } - return Nil(a.result) + return nilRec(a.result, seen) case *Array: - if slices.ContainsFunc(a.static, Nil) { + if _, ok := seen[a]; ok { + return false + } + if seen == nil { + seen = make(map[Type]struct{}) + } + seen[a] = struct{}{} + if slices.ContainsFunc(a.static, func(t Type) bool { return nilRec(t, seen) }) { return true } if a.dynamic != nil { - return Nil(a.dynamic) + return nilRec(a.dynamic, seen) } case *Object: + if _, ok := seen[a]; ok { + return false + } + if seen == nil { + seen = make(map[Type]struct{}) + } + seen[a] = struct{}{} for i := range a.static { - if Nil(a.static[i].Value) { + if nilRec(a.static[i].Value, seen) { return true } } if a.dynamic != nil { - return Nil(a.dynamic.Key) || Nil(a.dynamic.Value) + return nilRec(a.dynamic.Key, seen) || nilRec(a.dynamic.Value, seen) } case *Set: - return Nil(a.of) + return nilRec(a.of, seen) } return false } @@ -1179,7 +1243,7 @@ func typeSliceCompare(a, b []Type) int { } func typeOrder(x Type) int { - switch unwrap(x).(type) { + switch unwrapRecursive(unwrap(x)).(type) { case Null: return 0 case Boolean: diff --git a/vendor/github.com/open-policy-agent/opa/v1/util/compare.go b/vendor/github.com/open-policy-agent/opa/v1/util/compare.go index df78f64755..a930db21c5 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/util/compare.go +++ b/vendor/github.com/open-policy-agent/opa/v1/util/compare.go @@ -10,6 +10,28 @@ import ( "math/big" ) +const ( + nilSort = iota + boolSort + numberSort + stringSort + arraySort + objectSort +) + +// SliceLenCompare is a convenience function for comparing / sorting +// slices by their length using the various slices.SortX functions. +func SliceLenCompare[T any, S ~[]T](a, b S) int { + aLen, bLen := len(a), len(b) + if aLen == bLen { + return 0 + } else if aLen < bLen { + return -1 + } + + return 1 +} + // Compare returns 0 if a equals b, -1 if a is less than b, and 1 if b is than a. // // For comparison between values of different types, the following ordering is used: @@ -125,15 +147,6 @@ func Compare(a, b any) int { panic(fmt.Sprintf("illegal arguments of type %T and type %T", a, b)) } -const ( - nilSort = iota - boolSort = iota - numberSort = iota - stringSort = iota - arraySort = iota - objectSort = iota -) - func compareJSONNumber(a, b json.Number) int { bigA, ok := new(big.Float).SetString(string(a)) if !ok { diff --git a/vendor/github.com/open-policy-agent/opa/v1/util/slices.go b/vendor/github.com/open-policy-agent/opa/v1/util/slices.go new file mode 100644 index 0000000000..57163a5649 --- /dev/null +++ b/vendor/github.com/open-policy-agent/opa/v1/util/slices.go @@ -0,0 +1,16 @@ +// Copyright 2026 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package util + +func Map[T any, U any](s []T, f func(T) U) []U { + if s == nil { + return nil + } + r := make([]U, len(s)) + for i, v := range s { + r[i] = f(v) + } + return r +} diff --git a/vendor/github.com/open-policy-agent/opa/v1/version/version.go b/vendor/github.com/open-policy-agent/opa/v1/version/version.go index 7e71b3b7a9..43b3d8c0a7 100644 --- a/vendor/github.com/open-policy-agent/opa/v1/version/version.go +++ b/vendor/github.com/open-policy-agent/opa/v1/version/version.go @@ -10,7 +10,7 @@ import ( "runtime/debug" ) -var Version = "1.15.2" +var Version = "1.17.0" // GoVersion is the version of Go this was built with var GoVersion = runtime.Version() diff --git a/vendor/github.com/prometheus/procfs/.golangci.yml b/vendor/github.com/prometheus/procfs/.golangci.yml index 3c3bf910fd..eac920ba80 100644 --- a/vendor/github.com/prometheus/procfs/.golangci.yml +++ b/vendor/github.com/prometheus/procfs/.golangci.yml @@ -1,7 +1,9 @@ version: "2" linters: enable: + - errorlint - forbidigo + - gocritic - godot - misspell - revive @@ -11,6 +13,20 @@ linters: forbid: - pattern: ^fmt\.Print.*$ msg: Do not commit print statements. + gocritic: + enable-all: true + disabled-checks: + - commentFormatting + - commentedOutCode + - deferInLoop + - filepathJoin + - hugeParam + - importShadow + - paramTypeCombine + - rangeValCopy + - tooManyResultsChecker + - unnamedResult + - whyNoLint godot: exclude: # Ignore "See: URL". @@ -18,17 +34,21 @@ linters: capital: true misspell: locale: US + revive: + rules: + - name: var-naming + # TODO(SuperQ): See: https://github.com/prometheus/prometheus/issues/17766 + arguments: + - [] + - [] + - - skip-package-name-checks: true exclusions: - generated: lax presets: - comments - common-false-positives - legacy - std-error-handling - paths: - - third_party$ - - builtin$ - - examples$ + warn-unused: true formatters: enable: - gofmt @@ -37,9 +57,3 @@ formatters: goimports: local-prefixes: - github.com/prometheus/procfs - exclusions: - generated: lax - paths: - - third_party$ - - builtin$ - - examples$ diff --git a/vendor/github.com/prometheus/procfs/Makefile b/vendor/github.com/prometheus/procfs/Makefile index 7edfe4d093..bce50a19c5 100644 --- a/vendor/github.com/prometheus/procfs/Makefile +++ b/vendor/github.com/prometheus/procfs/Makefile @@ -1,4 +1,4 @@ -# Copyright 2018 The Prometheus Authors +# Copyright The Prometheus Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/Makefile.common b/vendor/github.com/prometheus/procfs/Makefile.common index 4de21512ff..cce3ef1d16 100644 --- a/vendor/github.com/prometheus/procfs/Makefile.common +++ b/vendor/github.com/prometheus/procfs/Makefile.common @@ -1,4 +1,4 @@ -# Copyright 2018 The Prometheus Authors +# Copyright The Prometheus Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -55,13 +55,13 @@ ifneq ($(shell command -v gotestsum 2> /dev/null),) endif endif -PROMU_VERSION ?= 0.17.0 +PROMU_VERSION ?= 0.18.0 PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz SKIP_GOLANGCI_LINT := GOLANGCI_LINT := GOLANGCI_LINT_OPTS ?= -GOLANGCI_LINT_VERSION ?= v2.1.5 +GOLANGCI_LINT_VERSION ?= v2.10.1 GOLANGCI_FMT_OPTS ?= # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. # windows isn't included here because of the path separator being different. @@ -82,11 +82,50 @@ endif PREFIX ?= $(shell pwd) BIN_DIR ?= $(shell pwd) DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) -DOCKERFILE_PATH ?= ./Dockerfile DOCKERBUILD_CONTEXT ?= ./ DOCKER_REPO ?= prom +# Check if deprecated DOCKERFILE_PATH is set +ifdef DOCKERFILE_PATH +$(error DOCKERFILE_PATH is deprecated. Use DOCKERFILE_VARIANTS ?= $(DOCKERFILE_PATH) in the Makefile) +endif + DOCKER_ARCHS ?= amd64 +DOCKERFILE_VARIANTS ?= Dockerfile $(wildcard Dockerfile.*) + +# Function to extract variant from Dockerfile label. +# Returns the variant name from io.prometheus.image.variant label, or "default" if not found. +define dockerfile_variant +$(strip $(or $(shell sed -n 's/.*io\.prometheus\.image\.variant="\([^"]*\)".*/\1/p' $(1)),default)) +endef + +# Check for duplicate variant names (including default for Dockerfiles without labels). +DOCKERFILE_VARIANT_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df))) +DOCKERFILE_VARIANT_NAMES_SORTED := $(sort $(DOCKERFILE_VARIANT_NAMES)) +ifneq ($(words $(DOCKERFILE_VARIANT_NAMES)),$(words $(DOCKERFILE_VARIANT_NAMES_SORTED))) +$(error Duplicate variant names found. Each Dockerfile must have a unique io.prometheus.image.variant label, and only one can be without a label (default)) +endif + +# Build variant:dockerfile pairs for shell iteration. +DOCKERFILE_VARIANTS_WITH_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df)):$(df)) + +# Shell helper to check whether a dockerfile/arch pair is excluded. +define dockerfile_arch_is_excluded +case " $(DOCKERFILE_ARCH_EXCLUSIONS) " in \ + *" $$dockerfile:$(1) "*) true ;; \ + *) false ;; \ +esac +endef + +# Shell helper to check whether a registry/arch pair is excluded. +# Extracts registry from DOCKER_REPO (e.g., quay.io/prometheus -> quay.io) +define registry_arch_is_excluded +registry=$$(echo "$(DOCKER_REPO)" | cut -d'/' -f1); \ +case " $(DOCKER_REGISTRY_ARCH_EXCLUSIONS) " in \ + *" $$registry:$(1) "*) true ;; \ + *) false ;; \ +esac +endef BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS)) PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) @@ -112,7 +151,7 @@ common-all: precheck style check_license lint yamllint unused build test .PHONY: common-style common-style: @echo ">> checking code style" - @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ + @fmtRes=$$($(GOFMT) -d $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -name '*.go' -print)); \ if [ -n "$${fmtRes}" ]; then \ echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ echo "Please ensure you are using $$($(GO) version) for formatting code."; \ @@ -122,13 +161,19 @@ common-style: .PHONY: common-check_license common-check_license: @echo ">> checking license header" - @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \ + @licRes=$$(for file in $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -type f -iname '*.go' -print) ; do \ awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ done); \ if [ -n "$${licRes}" ]; then \ echo "license header checking failed:"; echo "$${licRes}"; \ exit 1; \ fi + @echo ">> checking for copyright years 2026 or later" + @futureYearRes=$$(git grep -E 'Copyright (202[6-9]|20[3-9][0-9])' -- '*.go' ':!:vendor/*' || true); \ + if [ -n "$${futureYearRes}" ]; then \ + echo "Files with copyright year 2026 or later found (should use 'Copyright The Prometheus Authors'):"; echo "$${futureYearRes}"; \ + exit 1; \ + fi .PHONY: common-deps common-deps: @@ -139,7 +184,7 @@ common-deps: update-go-deps: @echo ">> updating Go dependencies" @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ - $(GO) get -d $$m; \ + $(GO) get $$m; \ done $(GO) mod tidy @@ -220,28 +265,194 @@ common-docker-repo-name: .PHONY: common-docker $(BUILD_DOCKER_ARCHS) common-docker: $(BUILD_DOCKER_ARCHS) $(BUILD_DOCKER_ARCHS): common-docker-%: - docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ - -f $(DOCKERFILE_PATH) \ - --build-arg ARCH="$*" \ - --build-arg OS="linux" \ - $(DOCKERBUILD_CONTEXT) + @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ + dockerfile=$${variant#*:}; \ + variant_name=$${variant%%:*}; \ + if $(call dockerfile_arch_is_excluded,$*); then \ + echo "Skipping $$variant_name variant for linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ + continue; \ + fi; \ + distroless_arch="$*"; \ + if [ "$*" = "armv7" ]; then \ + distroless_arch="arm"; \ + fi; \ + if [ "$$dockerfile" = "Dockerfile" ]; then \ + echo "Building default variant ($$variant_name) for linux-$* using $$dockerfile"; \ + docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ + -f $$dockerfile \ + --build-arg ARCH="$*" \ + --build-arg OS="linux" \ + --build-arg DISTROLESS_ARCH="$$distroless_arch" \ + $(DOCKERBUILD_CONTEXT); \ + if [ "$$variant_name" != "default" ]; then \ + echo "Tagging default variant with $$variant_name suffix"; \ + docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ + "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \ + fi; \ + else \ + echo "Building $$variant_name variant for linux-$* using $$dockerfile"; \ + docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" \ + -f $$dockerfile \ + --build-arg ARCH="$*" \ + --build-arg OS="linux" \ + --build-arg DISTROLESS_ARCH="$$distroless_arch" \ + $(DOCKERBUILD_CONTEXT); \ + fi; \ + done .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) common-docker-publish: $(PUBLISH_DOCKER_ARCHS) $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: - docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" + @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ + dockerfile=$${variant#*:}; \ + variant_name=$${variant%%:*}; \ + if $(call dockerfile_arch_is_excluded,$*); then \ + echo "Skipping push for $$variant_name variant on linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ + continue; \ + fi; \ + if $(call registry_arch_is_excluded,$*); then \ + echo "Skipping push for $$variant_name variant on linux-$* to $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ + continue; \ + fi; \ + if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ + echo "Pushing $$variant_name variant for linux-$*"; \ + docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \ + fi; \ + if [ "$$dockerfile" = "Dockerfile" ]; then \ + echo "Pushing default variant ($$variant_name) for linux-$*"; \ + docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)"; \ + fi; \ + if [ "$(DOCKER_IMAGE_TAG)" = "latest" ]; then \ + if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ + echo "Pushing $$variant_name variant version tags for linux-$*"; \ + docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \ + fi; \ + if [ "$$dockerfile" = "Dockerfile" ]; then \ + echo "Pushing default variant version tag for linux-$*"; \ + docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"; \ + fi; \ + fi; \ + done DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION))) .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) common-docker-tag-latest: $(TAG_DOCKER_ARCHS) $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: - docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest" - docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)" + @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ + dockerfile=$${variant#*:}; \ + variant_name=$${variant%%:*}; \ + if $(call dockerfile_arch_is_excluded,$*); then \ + echo "Skipping tag for $$variant_name variant on linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ + continue; \ + fi; \ + if $(call registry_arch_is_excluded,$*); then \ + echo "Skipping tag for $$variant_name variant on linux-$* for $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ + continue; \ + fi; \ + if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ + echo "Tagging $$variant_name variant for linux-$* as latest"; \ + docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest-$$variant_name"; \ + docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \ + fi; \ + if [ "$$dockerfile" = "Dockerfile" ]; then \ + echo "Tagging default variant ($$variant_name) for linux-$* as latest"; \ + docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest"; \ + docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"; \ + fi; \ + done .PHONY: common-docker-manifest common-docker-manifest: - DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(SANITIZED_DOCKER_IMAGE_TAG)) - DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" + @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ + dockerfile=$${variant#*:}; \ + variant_name=$${variant%%:*}; \ + if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ + echo "Creating manifest for $$variant_name variant"; \ + refs=""; \ + for arch in $(DOCKER_ARCHS); do \ + if $(call dockerfile_arch_is_excluded,$$arch); then \ + echo " Skipping $$arch for $$variant_name (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ + continue; \ + fi; \ + if $(call registry_arch_is_excluded,$$arch); then \ + echo " Skipping $$arch for $$variant_name on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ + continue; \ + fi; \ + refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \ + done; \ + if [ -z "$$refs" ]; then \ + echo "Skipping manifest for $$variant_name variant (no supported architectures)"; \ + continue; \ + fi; \ + DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" $$refs; \ + DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \ + fi; \ + if [ "$$dockerfile" = "Dockerfile" ]; then \ + echo "Creating default variant ($$variant_name) manifest"; \ + refs=""; \ + for arch in $(DOCKER_ARCHS); do \ + if $(call dockerfile_arch_is_excluded,$$arch); then \ + echo " Skipping $$arch for default variant (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ + continue; \ + fi; \ + if $(call registry_arch_is_excluded,$$arch); then \ + echo " Skipping $$arch for default variant on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ + continue; \ + fi; \ + refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)"; \ + done; \ + if [ -z "$$refs" ]; then \ + echo "Skipping default variant manifest (no supported architectures)"; \ + continue; \ + fi; \ + DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $$refs; \ + DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)"; \ + fi; \ + if [ "$(DOCKER_IMAGE_TAG)" = "latest" ]; then \ + if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ + echo "Creating manifest for $$variant_name variant version tag"; \ + refs=""; \ + for arch in $(DOCKER_ARCHS); do \ + if $(call dockerfile_arch_is_excluded,$$arch); then \ + echo " Skipping $$arch for $$variant_name version tag (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ + continue; \ + fi; \ + if $(call registry_arch_is_excluded,$$arch); then \ + echo " Skipping $$arch for $$variant_name version tag on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ + continue; \ + fi; \ + refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \ + done; \ + if [ -z "$$refs" ]; then \ + echo "Skipping version-tag manifest for $$variant_name variant (no supported architectures)"; \ + continue; \ + fi; \ + DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name" $$refs; \ + DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \ + fi; \ + if [ "$$dockerfile" = "Dockerfile" ]; then \ + echo "Creating default variant version tag manifest"; \ + refs=""; \ + for arch in $(DOCKER_ARCHS); do \ + if $(call dockerfile_arch_is_excluded,$$arch); then \ + echo " Skipping $$arch for default variant version tag (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ + continue; \ + fi; \ + if $(call registry_arch_is_excluded,$$arch); then \ + echo " Skipping $$arch for default variant version tag on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ + continue; \ + fi; \ + refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)"; \ + done; \ + if [ -z "$$refs" ]; then \ + echo "Skipping default variant version-tag manifest (no supported architectures)"; \ + continue; \ + fi; \ + DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)" $$refs; \ + DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)"; \ + fi; \ + fi; \ + done .PHONY: promu promu: $(PROMU) @@ -266,6 +477,10 @@ $(GOLANGCI_LINT): | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) endif +.PHONY: common-print-golangci-lint-version +common-print-golangci-lint-version: + @echo $(GOLANGCI_LINT_VERSION) + .PHONY: precheck precheck:: diff --git a/vendor/github.com/prometheus/procfs/arp.go b/vendor/github.com/prometheus/procfs/arp.go index 2e53344151..716bdef109 100644 --- a/vendor/github.com/prometheus/procfs/arp.go +++ b/vendor/github.com/prometheus/procfs/arp.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -73,15 +73,16 @@ func parseARPEntries(data []byte) ([]ARPEntry, error) { columns := strings.Fields(line) width := len(columns) - if width == expectedHeaderWidth || width == 0 { + switch width { + case expectedHeaderWidth, 0: continue - } else if width == expectedDataWidth { + case expectedDataWidth: entry, err := parseARPEntry(columns) if err != nil { return []ARPEntry{}, fmt.Errorf("%w: Failed to parse ARP entry: %v: %w", ErrFileParse, entry, err) } entries = append(entries, entry) - } else { + default: return []ARPEntry{}, fmt.Errorf("%w: %d columns found, but expected %d: %w", ErrFileParse, width, expectedDataWidth, err) } diff --git a/vendor/github.com/prometheus/procfs/buddyinfo.go b/vendor/github.com/prometheus/procfs/buddyinfo.go index 8380750090..53243e6875 100644 --- a/vendor/github.com/prometheus/procfs/buddyinfo.go +++ b/vendor/github.com/prometheus/procfs/buddyinfo.go @@ -1,4 +1,4 @@ -// Copyright 2017 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -64,14 +64,12 @@ func parseBuddyInfo(r io.Reader) ([]BuddyInfo, error) { if bucketCount == -1 { bucketCount = arraySize - } else { - if bucketCount != arraySize { - return nil, fmt.Errorf("%w: mismatch in number of buddyinfo buckets, previous count %d, new count %d", ErrFileParse, bucketCount, arraySize) - } + } else if bucketCount != arraySize { + return nil, fmt.Errorf("%w: mismatch in number of buddyinfo buckets, previous count %d, new count %d", ErrFileParse, bucketCount, arraySize) } sizes := make([]float64, arraySize) - for i := 0; i < arraySize; i++ { + for i := range arraySize { sizes[i], err = strconv.ParseFloat(parts[i+4], 64) if err != nil { return nil, fmt.Errorf("%w: Invalid valid in buddyinfo: %f: %w", ErrFileParse, sizes[i], err) diff --git a/vendor/github.com/prometheus/procfs/cmdline.go b/vendor/github.com/prometheus/procfs/cmdline.go index bf4f3b48c0..4f1cac1f0a 100644 --- a/vendor/github.com/prometheus/procfs/cmdline.go +++ b/vendor/github.com/prometheus/procfs/cmdline.go @@ -1,4 +1,4 @@ -// Copyright 2021 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/cpuinfo.go b/vendor/github.com/prometheus/procfs/cpuinfo.go index f0950bb495..4b23d8d6b5 100644 --- a/vendor/github.com/prometheus/procfs/cpuinfo.go +++ b/vendor/github.com/prometheus/procfs/cpuinfo.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,7 +12,6 @@ // limitations under the License. //go:build linux -// +build linux package procfs @@ -502,7 +501,7 @@ func parseCPUInfoRISCV(info []byte) ([]CPUInfo, error) { return cpuinfo, nil } -func parseCPUInfoDummy(_ []byte) ([]CPUInfo, error) { // nolint:unused,deadcode +func parseCPUInfoDummy(_ []byte) ([]CPUInfo, error) { //nolint:unused return nil, errors.New("not implemented") } diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_armx.go b/vendor/github.com/prometheus/procfs/cpuinfo_armx.go index 64cfd534c1..b09035ff38 100644 --- a/vendor/github.com/prometheus/procfs/cpuinfo_armx.go +++ b/vendor/github.com/prometheus/procfs/cpuinfo_armx.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,8 +12,6 @@ // limitations under the License. //go:build linux && (arm || arm64) -// +build linux -// +build arm arm64 package procfs diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_loong64.go b/vendor/github.com/prometheus/procfs/cpuinfo_loong64.go index d88442f0ed..7bb20211f9 100644 --- a/vendor/github.com/prometheus/procfs/cpuinfo_loong64.go +++ b/vendor/github.com/prometheus/procfs/cpuinfo_loong64.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,7 +12,6 @@ // limitations under the License. //go:build linux -// +build linux package procfs diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_mipsx.go b/vendor/github.com/prometheus/procfs/cpuinfo_mipsx.go index c11207f3ab..fd75d0f79d 100644 --- a/vendor/github.com/prometheus/procfs/cpuinfo_mipsx.go +++ b/vendor/github.com/prometheus/procfs/cpuinfo_mipsx.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,8 +12,6 @@ // limitations under the License. //go:build linux && (mips || mipsle || mips64 || mips64le) -// +build linux -// +build mips mipsle mips64 mips64le package procfs diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_others.go b/vendor/github.com/prometheus/procfs/cpuinfo_others.go index a6b2b3127c..3d36ba0e6b 100644 --- a/vendor/github.com/prometheus/procfs/cpuinfo_others.go +++ b/vendor/github.com/prometheus/procfs/cpuinfo_others.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,7 +12,6 @@ // limitations under the License. //go:build linux && !386 && !amd64 && !arm && !arm64 && !loong64 && !mips && !mips64 && !mips64le && !mipsle && !ppc64 && !ppc64le && !riscv64 && !s390x -// +build linux,!386,!amd64,!arm,!arm64,!loong64,!mips,!mips64,!mips64le,!mipsle,!ppc64,!ppc64le,!riscv64,!s390x package procfs diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_ppcx.go b/vendor/github.com/prometheus/procfs/cpuinfo_ppcx.go index 003bc2ad4a..b3425051ef 100644 --- a/vendor/github.com/prometheus/procfs/cpuinfo_ppcx.go +++ b/vendor/github.com/prometheus/procfs/cpuinfo_ppcx.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,8 +12,6 @@ // limitations under the License. //go:build linux && (ppc64 || ppc64le) -// +build linux -// +build ppc64 ppc64le package procfs diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_riscvx.go b/vendor/github.com/prometheus/procfs/cpuinfo_riscvx.go index 1c9b7313b6..72598230c3 100644 --- a/vendor/github.com/prometheus/procfs/cpuinfo_riscvx.go +++ b/vendor/github.com/prometheus/procfs/cpuinfo_riscvx.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,8 +12,6 @@ // limitations under the License. //go:build linux && (riscv || riscv64) -// +build linux -// +build riscv riscv64 package procfs diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_s390x.go b/vendor/github.com/prometheus/procfs/cpuinfo_s390x.go index fa3686bc00..50a8239cbc 100644 --- a/vendor/github.com/prometheus/procfs/cpuinfo_s390x.go +++ b/vendor/github.com/prometheus/procfs/cpuinfo_s390x.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,7 +12,6 @@ // limitations under the License. //go:build linux -// +build linux package procfs diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_x86.go b/vendor/github.com/prometheus/procfs/cpuinfo_x86.go index a0ef55562e..00edb30a5c 100644 --- a/vendor/github.com/prometheus/procfs/cpuinfo_x86.go +++ b/vendor/github.com/prometheus/procfs/cpuinfo_x86.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,8 +12,6 @@ // limitations under the License. //go:build linux && (386 || amd64) -// +build linux -// +build 386 amd64 package procfs diff --git a/vendor/github.com/prometheus/procfs/crypto.go b/vendor/github.com/prometheus/procfs/crypto.go index 5f2a37a78b..e4a5876eaf 100644 --- a/vendor/github.com/prometheus/procfs/crypto.go +++ b/vendor/github.com/prometheus/procfs/crypto.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/doc.go b/vendor/github.com/prometheus/procfs/doc.go index f9d961e441..26bfea071b 100644 --- a/vendor/github.com/prometheus/procfs/doc.go +++ b/vendor/github.com/prometheus/procfs/doc.go @@ -1,4 +1,4 @@ -// Copyright 2014 Prometheus Team +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/fs.go b/vendor/github.com/prometheus/procfs/fs.go index 9bdaccc7c8..8f27912a13 100644 --- a/vendor/github.com/prometheus/procfs/fs.go +++ b/vendor/github.com/prometheus/procfs/fs.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/fs_statfs_notype.go b/vendor/github.com/prometheus/procfs/fs_statfs_notype.go index 1b5bdbdf84..0bef25bdd9 100644 --- a/vendor/github.com/prometheus/procfs/fs_statfs_notype.go +++ b/vendor/github.com/prometheus/procfs/fs_statfs_notype.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,7 +12,6 @@ // limitations under the License. //go:build !freebsd && !linux -// +build !freebsd,!linux package procfs diff --git a/vendor/github.com/prometheus/procfs/fs_statfs_type.go b/vendor/github.com/prometheus/procfs/fs_statfs_type.go index 80df79c319..d183330390 100644 --- a/vendor/github.com/prometheus/procfs/fs_statfs_type.go +++ b/vendor/github.com/prometheus/procfs/fs_statfs_type.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,7 +12,6 @@ // limitations under the License. //go:build freebsd || linux -// +build freebsd linux package procfs diff --git a/vendor/github.com/prometheus/procfs/fscache.go b/vendor/github.com/prometheus/procfs/fscache.go index 7db8633077..9dde857073 100644 --- a/vendor/github.com/prometheus/procfs/fscache.go +++ b/vendor/github.com/prometheus/procfs/fscache.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -388,20 +388,21 @@ func parseFscacheinfo(r io.Reader) (*Fscacheinfo, error) { } } case "CacheOp:": - if strings.Split(fields[1], "=")[0] == "alo" { + switch strings.Split(fields[1], "=")[0] { + case "alo": err := setFSCacheFields(fields[1:], &m.CacheopAllocationsinProgress, &m.CacheopLookupObjectInProgress, &m.CacheopLookupCompleteInPorgress, &m.CacheopGrabObjectInProgress) if err != nil { return &m, err } - } else if strings.Split(fields[1], "=")[0] == "inv" { + case "inv": err := setFSCacheFields(fields[1:], &m.CacheopInvalidations, &m.CacheopUpdateObjectInProgress, &m.CacheopDropObjectInProgress, &m.CacheopPutObjectInProgress, &m.CacheopAttributeChangeInProgress, &m.CacheopSyncCacheInProgress) if err != nil { return &m, err } - } else { + default: err := setFSCacheFields(fields[1:], &m.CacheopReadOrAllocPageInProgress, &m.CacheopReadOrAllocPagesInProgress, &m.CacheopAllocatePageInProgress, &m.CacheopAllocatePagesInProgress, &m.CacheopWritePagesInProgress, &m.CacheopUncachePagesInProgress, &m.CacheopDissociatePagesInProgress) diff --git a/vendor/github.com/prometheus/procfs/internal/fs/fs.go b/vendor/github.com/prometheus/procfs/internal/fs/fs.go index 3a43e83915..e7ccad66b2 100644 --- a/vendor/github.com/prometheus/procfs/internal/fs/fs.go +++ b/vendor/github.com/prometheus/procfs/internal/fs/fs.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/internal/util/parse.go b/vendor/github.com/prometheus/procfs/internal/util/parse.go index 5a7d2df06a..30c5872019 100644 --- a/vendor/github.com/prometheus/procfs/internal/util/parse.go +++ b/vendor/github.com/prometheus/procfs/internal/util/parse.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/internal/util/readfile.go b/vendor/github.com/prometheus/procfs/internal/util/readfile.go index 71b7a70ebd..0e41f71af1 100644 --- a/vendor/github.com/prometheus/procfs/internal/util/readfile.go +++ b/vendor/github.com/prometheus/procfs/internal/util/readfile.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/internal/util/sysreadfile.go b/vendor/github.com/prometheus/procfs/internal/util/sysreadfile.go index d5404a6d72..f6a4a4de62 100644 --- a/vendor/github.com/prometheus/procfs/internal/util/sysreadfile.go +++ b/vendor/github.com/prometheus/procfs/internal/util/sysreadfile.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,8 +12,6 @@ // limitations under the License. //go:build (linux || darwin) && !appengine -// +build linux darwin -// +build !appengine package util diff --git a/vendor/github.com/prometheus/procfs/internal/util/sysreadfile_compat.go b/vendor/github.com/prometheus/procfs/internal/util/sysreadfile_compat.go index 1d86f5e63f..c80e082cb9 100644 --- a/vendor/github.com/prometheus/procfs/internal/util/sysreadfile_compat.go +++ b/vendor/github.com/prometheus/procfs/internal/util/sysreadfile_compat.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,7 +12,6 @@ // limitations under the License. //go:build (linux && appengine) || (!linux && !darwin) -// +build linux,appengine !linux,!darwin package util diff --git a/vendor/github.com/prometheus/procfs/internal/util/valueparser.go b/vendor/github.com/prometheus/procfs/internal/util/valueparser.go index fe2355d3c6..e0ed671ea0 100644 --- a/vendor/github.com/prometheus/procfs/internal/util/valueparser.go +++ b/vendor/github.com/prometheus/procfs/internal/util/valueparser.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/ipvs.go b/vendor/github.com/prometheus/procfs/ipvs.go index bc3a20c932..5374da9fa8 100644 --- a/vendor/github.com/prometheus/procfs/ipvs.go +++ b/vendor/github.com/prometheus/procfs/ipvs.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/kernel_hung.go b/vendor/github.com/prometheus/procfs/kernel_hung.go new file mode 100644 index 0000000000..0c7a69f99f --- /dev/null +++ b/vendor/github.com/prometheus/procfs/kernel_hung.go @@ -0,0 +1,44 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows + +package procfs + +import ( + "os" + "strconv" + "strings" +) + +// KernelHung contains information about to the kernel's hung_task_detect_count number. +type KernelHung struct { + // Indicates the total number of tasks that have been detected as hung since the system boot. + // This file shows up if `CONFIG_DETECT_HUNG_TASK` is enabled. + HungTaskDetectCount *uint64 +} + +// KernelHung returns values from /proc/sys/kernel/hung_task_detect_count. +func (fs FS) KernelHung() (KernelHung, error) { + data, err := os.ReadFile(fs.proc.Path("sys", "kernel", "hung_task_detect_count")) + if err != nil { + return KernelHung{}, err + } + val, err := strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64) + if err != nil { + return KernelHung{}, err + } + return KernelHung{ + HungTaskDetectCount: &val, + }, nil +} diff --git a/vendor/github.com/prometheus/procfs/kernel_random.go b/vendor/github.com/prometheus/procfs/kernel_random.go index db88566bdf..e7c5b8cf2b 100644 --- a/vendor/github.com/prometheus/procfs/kernel_random.go +++ b/vendor/github.com/prometheus/procfs/kernel_random.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,7 +12,6 @@ // limitations under the License. //go:build !windows -// +build !windows package procfs diff --git a/vendor/github.com/prometheus/procfs/loadavg.go b/vendor/github.com/prometheus/procfs/loadavg.go index 332e76c17f..c8c78a65ed 100644 --- a/vendor/github.com/prometheus/procfs/loadavg.go +++ b/vendor/github.com/prometheus/procfs/loadavg.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/mdstat.go b/vendor/github.com/prometheus/procfs/mdstat.go index 1fd4381b22..d66eeda82a 100644 --- a/vendor/github.com/prometheus/procfs/mdstat.go +++ b/vendor/github.com/prometheus/procfs/mdstat.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -27,13 +27,34 @@ var ( recoveryLinePctRE = regexp.MustCompile(`= (.+)%`) recoveryLineFinishRE = regexp.MustCompile(`finish=(.+)min`) recoveryLineSpeedRE = regexp.MustCompile(`speed=(.+)[A-Z]`) - componentDeviceRE = regexp.MustCompile(`(.*)\[\d+\]`) + componentDeviceRE = regexp.MustCompile(`(.*)\[(\d+)\](\([SF]+\))?`) + personalitiesPrefix = "Personalities : " ) +type MDStatComponent struct { + // Name of the component device. + Name string + // DescriptorIndex number of component device, e.g. the order in the superblock. + DescriptorIndex int32 + // Flags per Linux drivers/md/md.[ch] as of v6.12-rc1 + // Subset that are exposed in mdstat + WriteMostly bool + Journal bool + Faulty bool // "Faulty" is what kernel source uses for "(F)" + Spare bool + Replacement bool + // Some additional flags that are NOT exposed in procfs today; they may + // be available via sysfs. + // In_sync, Bitmap_sync, Blocked, WriteErrorSeen, FaultRecorded, + // BlockedBadBlocks, WantReplacement, Candidate, ... +} + // MDStat holds info parsed from /proc/mdstat. type MDStat struct { // Name of the device. Name string + // raid type of the device. + Type string // activity-state of the device. ActivityState string // Number of active disks. @@ -58,8 +79,8 @@ type MDStat struct { BlocksSyncedFinishTime float64 // current sync speed (in Kilobytes/sec) BlocksSyncedSpeed float64 - // Name of md component devices - Devices []string + // component devices + Devices []MDStatComponent } // MDStat parses an mdstat-file (/proc/mdstat) and returns a slice of @@ -80,28 +101,52 @@ func (fs FS) MDStat() ([]MDStat, error) { // parseMDStat parses data from mdstat file (/proc/mdstat) and returns a slice of // structs containing the relevant info. func parseMDStat(mdStatData []byte) ([]MDStat, error) { + // TODO: + // - parse global hotspares from the "unused devices" line. mdStats := []MDStat{} lines := strings.Split(string(mdStatData), "\n") + knownRaidTypes := make(map[string]bool) for i, line := range lines { if strings.TrimSpace(line) == "" || line[0] == ' ' || - strings.HasPrefix(line, "Personalities") || strings.HasPrefix(line, "unused") { continue } + // Personalities : [linear] [multipath] [raid0] [raid1] [raid6] [raid5] [raid4] [raid10] + if len(knownRaidTypes) == 0 && strings.HasPrefix(line, personalitiesPrefix) { + personalities := strings.Fields(line[len(personalitiesPrefix):]) + for _, word := range personalities { + word := word[1 : len(word)-1] + knownRaidTypes[word] = true + } + continue + } deviceFields := strings.Fields(line) if len(deviceFields) < 3 { return nil, fmt.Errorf("%w: Expected 3+ lines, got %q", ErrFileParse, line) } mdName := deviceFields[0] // mdx - state := deviceFields[2] // active or inactive + state := deviceFields[2] // active, inactive, broken + + mdType := "unknown" // raid1, raid5, etc. + var deviceStartIndex int + if len(deviceFields) > 3 { // mdType may be in the 3rd or 4th field + if isRaidType(deviceFields[3], knownRaidTypes) { + mdType = deviceFields[3] + deviceStartIndex = 4 + } else if len(deviceFields) > 4 && isRaidType(deviceFields[4], knownRaidTypes) { + // if the 3rd field is (...), the 4th field is the mdType + mdType = deviceFields[4] + deviceStartIndex = 5 + } + } if len(lines) <= i+3 { return nil, fmt.Errorf("%w: Too few lines for md device: %q", ErrFileParse, mdName) } - // Failed disks have the suffix (F) & Spare disks have the suffix (S). + // Failed (Faulty) disks have the suffix (F) & Spare disks have the suffix (S). fail := int64(strings.Count(line, "(F)")) spare := int64(strings.Count(line, "(S)")) active, total, down, size, err := evalStatusLine(lines[i], lines[i+1]) @@ -129,13 +174,14 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) { // Append recovery and resyncing state info. if recovering || resyncing || checking || reshaping { - if recovering { + switch { + case recovering: state = "recovering" - } else if reshaping { + case reshaping: state = "reshaping" - } else if checking { + case checking: state = "checking" - } else { + default: state = "resyncing" } @@ -151,8 +197,14 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) { } } + devices, err := evalComponentDevices(deviceFields[deviceStartIndex:]) + if err != nil { + return nil, fmt.Errorf("error parsing components in md device %q: %w", mdName, err) + } + mdStats = append(mdStats, MDStat{ Name: mdName, + Type: mdType, ActivityState: state, DisksActive: active, DisksFailed: fail, @@ -165,14 +217,24 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) { BlocksSyncedPct: pct, BlocksSyncedFinishTime: finish, BlocksSyncedSpeed: speed, - Devices: evalComponentDevices(deviceFields), + Devices: devices, }) } return mdStats, nil } +// check if a string's format is like the mdType +// Rule 1: mdType should not be like (...) +// Rule 2: mdType should not be like sda[0] +// . +func isRaidType(mdType string, knownRaidTypes map[string]bool) bool { + _, ok := knownRaidTypes[mdType] + return !strings.ContainsAny(mdType, "([") && ok +} + func evalStatusLine(deviceLine, statusLine string) (active, total, down, size int64, err error) { + // e.g. 523968 blocks super 1.2 [4/4] [UUUU] statusFields := strings.Fields(statusLine) if len(statusFields) < 1 { return 0, 0, 0, 0, fmt.Errorf("%w: Unexpected statusline %q: %w", ErrFileParse, statusLine, err) @@ -263,17 +325,29 @@ func evalRecoveryLine(recoveryLine string) (blocksSynced int64, blocksToBeSynced return blocksSynced, blocksToBeSynced, pct, finish, speed, nil } -func evalComponentDevices(deviceFields []string) []string { - mdComponentDevices := make([]string, 0) - if len(deviceFields) > 3 { - for _, field := range deviceFields[4:] { - match := componentDeviceRE.FindStringSubmatch(field) - if match == nil { - continue - } - mdComponentDevices = append(mdComponentDevices, match[1]) +func evalComponentDevices(deviceFields []string) ([]MDStatComponent, error) { + mdComponentDevices := make([]MDStatComponent, 0) + for _, field := range deviceFields { + match := componentDeviceRE.FindStringSubmatch(field) + if match == nil { + continue + } + descriptorIndex, err := strconv.ParseInt(match[2], 10, 32) + if err != nil { + return mdComponentDevices, fmt.Errorf("error parsing int from device %q: %w", match[2], err) } + mdComponentDevices = append(mdComponentDevices, MDStatComponent{ + Name: match[1], + DescriptorIndex: int32(descriptorIndex), + // match may contain one or more of these + // https://github.com/torvalds/linux/blob/7ec462100ef9142344ddbf86f2c3008b97acddbe/drivers/md/md.c#L8376-L8392 + Faulty: strings.Contains(match[3], "(F)"), + Spare: strings.Contains(match[3], "(S)"), + Journal: strings.Contains(match[3], "(J)"), + Replacement: strings.Contains(match[3], "(R)"), + WriteMostly: strings.Contains(match[3], "(W)"), + }) } - return mdComponentDevices + return mdComponentDevices, nil } diff --git a/vendor/github.com/prometheus/procfs/meminfo.go b/vendor/github.com/prometheus/procfs/meminfo.go index 937e1f9606..3420383187 100644 --- a/vendor/github.com/prometheus/procfs/meminfo.go +++ b/vendor/github.com/prometheus/procfs/meminfo.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -307,7 +307,7 @@ func parseMemInfo(r io.Reader) (*Meminfo, error) { m.ZswapBytes = &valBytes case "Zswapped:": m.Zswapped = &val - m.ZswapBytes = &valBytes + m.ZswappedBytes = &valBytes case "Dirty:": m.Dirty = &val m.DirtyBytes = &valBytes diff --git a/vendor/github.com/prometheus/procfs/mountinfo.go b/vendor/github.com/prometheus/procfs/mountinfo.go index a704c5e735..9414a12f42 100644 --- a/vendor/github.com/prometheus/procfs/mountinfo.go +++ b/vendor/github.com/prometheus/procfs/mountinfo.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -147,8 +147,7 @@ func mountOptionsParseOptionalFields(o []string) (map[string]string, error) { // mountOptionsParser parses the mount options, superblock options. func mountOptionsParser(mountOptions string) map[string]string { opts := make(map[string]string) - options := strings.Split(mountOptions, ",") - for _, opt := range options { + for opt := range strings.SplitSeq(mountOptions, ",") { splitOption := strings.Split(opt, "=") if len(splitOption) < 2 { key := splitOption[0] @@ -178,3 +177,21 @@ func GetProcMounts(pid int) ([]*MountInfo, error) { } return parseMountInfo(data) } + +// GetMounts retrieves mountinfo information from `/proc/self/mountinfo`. +func (fs FS) GetMounts() ([]*MountInfo, error) { + data, err := util.ReadFileNoStat(fs.proc.Path("self/mountinfo")) + if err != nil { + return nil, err + } + return parseMountInfo(data) +} + +// GetProcMounts retrieves mountinfo information from a processes' `/proc//mountinfo`. +func (fs FS) GetProcMounts(pid int) ([]*MountInfo, error) { + data, err := util.ReadFileNoStat(fs.proc.Path(fmt.Sprintf("%d/mountinfo", pid))) + if err != nil { + return nil, err + } + return parseMountInfo(data) +} diff --git a/vendor/github.com/prometheus/procfs/mountstats.go b/vendor/github.com/prometheus/procfs/mountstats.go index 50caa73274..e503cb3a6c 100644 --- a/vendor/github.com/prometheus/procfs/mountstats.go +++ b/vendor/github.com/prometheus/procfs/mountstats.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -383,7 +383,7 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e if stats.Opts == nil { stats.Opts = map[string]string{} } - for _, opt := range strings.Split(ss[1], ",") { + for opt := range strings.SplitSeq(ss[1], ",") { split := strings.Split(opt, "=") if len(split) == 2 { stats.Opts[split[0]] = split[1] diff --git a/vendor/github.com/prometheus/procfs/net_conntrackstat.go b/vendor/github.com/prometheus/procfs/net_conntrackstat.go index 316df5fbb7..e9ca357079 100644 --- a/vendor/github.com/prometheus/procfs/net_conntrackstat.go +++ b/vendor/github.com/prometheus/procfs/net_conntrackstat.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/net_dev.go b/vendor/github.com/prometheus/procfs/net_dev.go index e66208aa05..7b3e1d61c9 100644 --- a/vendor/github.com/prometheus/procfs/net_dev.go +++ b/vendor/github.com/prometheus/procfs/net_dev.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/net_dev_snmp6.go b/vendor/github.com/prometheus/procfs/net_dev_snmp6.go index f50b38e352..2a0f60f29f 100644 --- a/vendor/github.com/prometheus/procfs/net_dev_snmp6.go +++ b/vendor/github.com/prometheus/procfs/net_dev_snmp6.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -18,6 +18,7 @@ import ( "errors" "io" "os" + "path/filepath" "strconv" "strings" ) @@ -56,7 +57,9 @@ func newNetDevSNMP6(dir string) (NetDevSNMP6, error) { } for _, iFaceFile := range ifaceFiles { - f, err := os.Open(dir + "/" + iFaceFile.Name()) + filePath := filepath.Join(dir, iFaceFile.Name()) + + f, err := os.Open(filePath) if err != nil { return netDevSNMP6, err } diff --git a/vendor/github.com/prometheus/procfs/net_ip_socket.go b/vendor/github.com/prometheus/procfs/net_ip_socket.go index 19e3378f72..9291f8cd4c 100644 --- a/vendor/github.com/prometheus/procfs/net_ip_socket.go +++ b/vendor/github.com/prometheus/procfs/net_ip_socket.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/net_protocols.go b/vendor/github.com/prometheus/procfs/net_protocols.go index 8d4b1ac05b..eaa996cbcf 100644 --- a/vendor/github.com/prometheus/procfs/net_protocols.go +++ b/vendor/github.com/prometheus/procfs/net_protocols.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -169,7 +169,7 @@ func (pc *NetProtocolCapabilities) parseCapabilities(capabilities []string) erro &pc.EnterMemoryPressure, } - for i := 0; i < len(capabilities); i++ { + for i := range capabilities { switch capabilities[i] { case "y": *capabilityFields[i] = true diff --git a/vendor/github.com/prometheus/procfs/net_route.go b/vendor/github.com/prometheus/procfs/net_route.go index deb7029fe1..fa3812d9d0 100644 --- a/vendor/github.com/prometheus/procfs/net_route.go +++ b/vendor/github.com/prometheus/procfs/net_route.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/net_sockstat.go b/vendor/github.com/prometheus/procfs/net_sockstat.go index fae62b13d9..8b221ebfff 100644 --- a/vendor/github.com/prometheus/procfs/net_sockstat.go +++ b/vendor/github.com/prometheus/procfs/net_sockstat.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -139,9 +139,6 @@ func parseSockstatKVs(kvs []string) (map[string]int, error) { func parseSockstatProtocol(kvs map[string]int) NetSockstatProtocol { var nsp NetSockstatProtocol for k, v := range kvs { - // Capture the range variable to ensure we get unique pointers for - // each of the optional fields. - v := v switch k { case "inuse": nsp.InUse = v diff --git a/vendor/github.com/prometheus/procfs/net_softnet.go b/vendor/github.com/prometheus/procfs/net_softnet.go index 71c8059f4d..4a2dfa18fd 100644 --- a/vendor/github.com/prometheus/procfs/net_softnet.go +++ b/vendor/github.com/prometheus/procfs/net_softnet.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/net_tcp.go b/vendor/github.com/prometheus/procfs/net_tcp.go index 0396d72015..2c7f9bc7c3 100644 --- a/vendor/github.com/prometheus/procfs/net_tcp.go +++ b/vendor/github.com/prometheus/procfs/net_tcp.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -25,6 +25,7 @@ type ( // NetTCP returns the IPv4 kernel/networking statistics for TCP datagrams // read from /proc/net/tcp. +// // Deprecated: Use github.com/mdlayher/netlink#Conn (with syscall.AF_INET) instead. func (fs FS) NetTCP() (NetTCP, error) { return newNetTCP(fs.proc.Path("net/tcp")) @@ -32,6 +33,7 @@ func (fs FS) NetTCP() (NetTCP, error) { // NetTCP6 returns the IPv6 kernel/networking statistics for TCP datagrams // read from /proc/net/tcp6. +// // Deprecated: Use github.com/mdlayher/netlink#Conn (with syscall.AF_INET6) instead. func (fs FS) NetTCP6() (NetTCP, error) { return newNetTCP(fs.proc.Path("net/tcp6")) @@ -39,6 +41,7 @@ func (fs FS) NetTCP6() (NetTCP, error) { // NetTCPSummary returns already computed statistics like the total queue lengths // for TCP datagrams read from /proc/net/tcp. +// // Deprecated: Use github.com/mdlayher/netlink#Conn (with syscall.AF_INET) instead. func (fs FS) NetTCPSummary() (*NetTCPSummary, error) { return newNetTCPSummary(fs.proc.Path("net/tcp")) @@ -46,6 +49,7 @@ func (fs FS) NetTCPSummary() (*NetTCPSummary, error) { // NetTCP6Summary returns already computed statistics like the total queue lengths // for TCP datagrams read from /proc/net/tcp6. +// // Deprecated: Use github.com/mdlayher/netlink#Conn (with syscall.AF_INET6) instead. func (fs FS) NetTCP6Summary() (*NetTCPSummary, error) { return newNetTCPSummary(fs.proc.Path("net/tcp6")) diff --git a/vendor/github.com/prometheus/procfs/net_tls_stat.go b/vendor/github.com/prometheus/procfs/net_tls_stat.go index 13994c1782..b1b3f6a6a2 100644 --- a/vendor/github.com/prometheus/procfs/net_tls_stat.go +++ b/vendor/github.com/prometheus/procfs/net_tls_stat.go @@ -1,4 +1,4 @@ -// Copyright 2023 Prometheus Team +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/net_udp.go b/vendor/github.com/prometheus/procfs/net_udp.go index 9ac3daf2d4..8a32779102 100644 --- a/vendor/github.com/prometheus/procfs/net_udp.go +++ b/vendor/github.com/prometheus/procfs/net_udp.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/net_unix.go b/vendor/github.com/prometheus/procfs/net_unix.go index d7e0cacb4c..e4d6359236 100644 --- a/vendor/github.com/prometheus/procfs/net_unix.go +++ b/vendor/github.com/prometheus/procfs/net_unix.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/net_wireless.go b/vendor/github.com/prometheus/procfs/net_wireless.go index 7c597bc870..69d0794451 100644 --- a/vendor/github.com/prometheus/procfs/net_wireless.go +++ b/vendor/github.com/prometheus/procfs/net_wireless.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/net_xfrm.go b/vendor/github.com/prometheus/procfs/net_xfrm.go index 932ef20468..5a9f497d19 100644 --- a/vendor/github.com/prometheus/procfs/net_xfrm.go +++ b/vendor/github.com/prometheus/procfs/net_xfrm.go @@ -1,4 +1,4 @@ -// Copyright 2017 Prometheus Team +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/netstat.go b/vendor/github.com/prometheus/procfs/netstat.go index 742dff453b..dbdae47392 100644 --- a/vendor/github.com/prometheus/procfs/netstat.go +++ b/vendor/github.com/prometheus/procfs/netstat.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/nfnetlink_queue.go b/vendor/github.com/prometheus/procfs/nfnetlink_queue.go new file mode 100644 index 0000000000..b0a73b11e9 --- /dev/null +++ b/vendor/github.com/prometheus/procfs/nfnetlink_queue.go @@ -0,0 +1,85 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package procfs + +import ( + "bufio" + "bytes" + "fmt" + + "github.com/prometheus/procfs/internal/util" +) + +const nfNetLinkQueueFormat = "%d %d %d %d %d %d %d %d %d" + +// NFNetLinkQueue contains general information about netfilter queues found in /proc/net/netfilter/nfnetlink_queue. +type NFNetLinkQueue struct { + // id of the queue + QueueID uint + // pid of process handling the queue + PeerPID uint + // number of packets waiting for a decision + QueueTotal uint + // indicate how userspace receive packets + CopyMode uint + // size of copy + CopyRange uint + // number of items dropped by the kernel because too many packets were waiting a decision. + // It queue_total is superior to queue_max_len (1024 per default) the packets are dropped. + QueueDropped uint + // number of packets dropped by userspace (due to kernel send failure on the netlink socket) + QueueUserDropped uint + // sequence number of packets queued. It gives a correct approximation of the number of queued packets. + SequenceID uint + // internal value (number of entity using the queue) + Use uint +} + +// NFNetLinkQueue returns information about current state of netfilter queues. +func (fs FS) NFNetLinkQueue() ([]NFNetLinkQueue, error) { + data, err := util.ReadFileNoStat(fs.proc.Path("net/netfilter/nfnetlink_queue")) + if err != nil { + return nil, err + } + + queue := []NFNetLinkQueue{} + if len(data) == 0 { + return queue, nil + } + + scanner := bufio.NewScanner(bytes.NewReader(data)) + for scanner.Scan() { + line := scanner.Text() + nFNetLinkQueue, err := parseNFNetLinkQueueLine(line) + if err != nil { + return nil, err + } + queue = append(queue, *nFNetLinkQueue) + } + return queue, nil +} + +// parseNFNetLinkQueueLine parses each line of the /proc/net/netfilter/nfnetlink_queue file. +func parseNFNetLinkQueueLine(line string) (*NFNetLinkQueue, error) { + nFNetLinkQueue := NFNetLinkQueue{} + _, err := fmt.Sscanf( + line, nfNetLinkQueueFormat, + &nFNetLinkQueue.QueueID, &nFNetLinkQueue.PeerPID, &nFNetLinkQueue.QueueTotal, &nFNetLinkQueue.CopyMode, + &nFNetLinkQueue.CopyRange, &nFNetLinkQueue.QueueDropped, &nFNetLinkQueue.QueueUserDropped, &nFNetLinkQueue.SequenceID, &nFNetLinkQueue.Use, + ) + if err != nil { + return nil, err + } + return &nFNetLinkQueue, nil +} diff --git a/vendor/github.com/prometheus/procfs/proc.go b/vendor/github.com/prometheus/procfs/proc.go index 368187fa88..39c14aa55e 100644 --- a/vendor/github.com/prometheus/procfs/proc.go +++ b/vendor/github.com/prometheus/procfs/proc.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -49,7 +49,7 @@ func (p Procs) Less(i, j int) bool { return p[i].PID < p[j].PID } // Self returns a process for the current process read via /proc/self. func Self() (Proc, error) { fs, err := NewFS(DefaultMountPoint) - if err != nil || errors.Unwrap(err) == ErrMountPoint { + if err != nil || errors.Is(err, ErrMountPoint) { return Proc{}, err } return fs.Self() diff --git a/vendor/github.com/prometheus/procfs/proc_cgroup.go b/vendor/github.com/prometheus/procfs/proc_cgroup.go index 4a64347c03..535c08d6fc 100644 --- a/vendor/github.com/prometheus/procfs/proc_cgroup.go +++ b/vendor/github.com/prometheus/procfs/proc_cgroup.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/proc_cgroups.go b/vendor/github.com/prometheus/procfs/proc_cgroups.go index 5dd4938999..0b275c3b1f 100644 --- a/vendor/github.com/prometheus/procfs/proc_cgroups.go +++ b/vendor/github.com/prometheus/procfs/proc_cgroups.go @@ -1,4 +1,4 @@ -// Copyright 2021 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -40,13 +40,13 @@ type CgroupSummary struct { // parseCgroupSummary parses each line of the /proc/cgroup file // Line format is `subsys_name hierarchy num_cgroups enabled`. -func parseCgroupSummaryString(CgroupSummaryStr string) (*CgroupSummary, error) { +func parseCgroupSummaryString(cgroupSummaryStr string) (*CgroupSummary, error) { var err error - fields := strings.Fields(CgroupSummaryStr) + fields := strings.Fields(cgroupSummaryStr) // require at least 4 fields if len(fields) < 4 { - return nil, fmt.Errorf("%w: 4+ fields required, found %d fields in cgroup info string: %s", ErrFileParse, len(fields), CgroupSummaryStr) + return nil, fmt.Errorf("%w: 4+ fields required, found %d fields in cgroup info string: %s", ErrFileParse, len(fields), cgroupSummaryStr) } CgroupSummary := &CgroupSummary{ diff --git a/vendor/github.com/prometheus/procfs/proc_environ.go b/vendor/github.com/prometheus/procfs/proc_environ.go index 57a89895d6..5b941de047 100644 --- a/vendor/github.com/prometheus/procfs/proc_environ.go +++ b/vendor/github.com/prometheus/procfs/proc_environ.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/proc_fdinfo.go b/vendor/github.com/prometheus/procfs/proc_fdinfo.go index fa761b3529..fa57761dbe 100644 --- a/vendor/github.com/prometheus/procfs/proc_fdinfo.go +++ b/vendor/github.com/prometheus/procfs/proc_fdinfo.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -60,15 +60,16 @@ func (p Proc) FDInfo(fd string) (*ProcFDInfo, error) { scanner := bufio.NewScanner(bytes.NewReader(data)) for scanner.Scan() { text = scanner.Text() - if rPos.MatchString(text) { + switch { + case rPos.MatchString(text): pos = rPos.FindStringSubmatch(text)[1] - } else if rFlags.MatchString(text) { + case rFlags.MatchString(text): flags = rFlags.FindStringSubmatch(text)[1] - } else if rMntID.MatchString(text) { + case rMntID.MatchString(text): mntid = rMntID.FindStringSubmatch(text)[1] - } else if rIno.MatchString(text) { + case rIno.MatchString(text): ino = rIno.FindStringSubmatch(text)[1] - } else if rInotify.MatchString(text) { + case rInotify.MatchString(text): newInotify, err := parseInotifyInfo(text) if err != nil { return nil, err diff --git a/vendor/github.com/prometheus/procfs/proc_interrupts.go b/vendor/github.com/prometheus/procfs/proc_interrupts.go index 86b4b45246..643b500d5d 100644 --- a/vendor/github.com/prometheus/procfs/proc_interrupts.go +++ b/vendor/github.com/prometheus/procfs/proc_interrupts.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -42,7 +42,7 @@ type Interrupts map[string]Interrupt // Interrupts creates a new instance from a given Proc instance. func (p Proc) Interrupts() (Interrupts, error) { - data, err := util.ReadFileNoStat(p.path("interrupts")) + data, err := util.ReadFileNoStat(p.fs.proc.Path("interrupts")) if err != nil { return nil, err } diff --git a/vendor/github.com/prometheus/procfs/proc_io.go b/vendor/github.com/prometheus/procfs/proc_io.go index d15b66ddb6..dd8086ba2e 100644 --- a/vendor/github.com/prometheus/procfs/proc_io.go +++ b/vendor/github.com/prometheus/procfs/proc_io.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/proc_limits.go b/vendor/github.com/prometheus/procfs/proc_limits.go index 9530b14bc6..4b7d337847 100644 --- a/vendor/github.com/prometheus/procfs/proc_limits.go +++ b/vendor/github.com/prometheus/procfs/proc_limits.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -19,6 +19,7 @@ import ( "os" "regexp" "strconv" + "strings" ) // ProcLimits represents the soft limits for each of the process's resource @@ -74,7 +75,7 @@ const ( ) var ( - limitsMatch = regexp.MustCompile(`(Max \w+\s{0,1}?\w*\s{0,1}\w*)\s{2,}(\w+)\s+(\w+)`) + limitsMatch = regexp.MustCompile(`(Max \w+\s??\w*\s?\w*)\s{2,}(\w+)\s+(\w+)`) ) // NewLimits returns the current soft limits of the process. @@ -106,7 +107,7 @@ func (p Proc) Limits() (ProcLimits, error) { return ProcLimits{}, fmt.Errorf("%w: couldn't parse %q line %q", ErrFileParse, f.Name(), s.Text()) } - switch fields[1] { + switch strings.TrimSpace(fields[1]) { case "Max cpu time": l.CPUTime, err = parseUint(fields[2]) case "Max file size": diff --git a/vendor/github.com/prometheus/procfs/proc_maps.go b/vendor/github.com/prometheus/procfs/proc_maps.go index 7e75c286b5..08b89a6eb9 100644 --- a/vendor/github.com/prometheus/procfs/proc_maps.go +++ b/vendor/github.com/prometheus/procfs/proc_maps.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,8 +12,6 @@ // limitations under the License. //go:build (aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris) && !js -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris -// +build !js package procfs diff --git a/vendor/github.com/prometheus/procfs/proc_netstat.go b/vendor/github.com/prometheus/procfs/proc_netstat.go index 4248c1716e..7f94cc8914 100644 --- a/vendor/github.com/prometheus/procfs/proc_netstat.go +++ b/vendor/github.com/prometheus/procfs/proc_netstat.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/proc_ns.go b/vendor/github.com/prometheus/procfs/proc_ns.go index 0f8f847f95..5fc0eb9e2f 100644 --- a/vendor/github.com/prometheus/procfs/proc_ns.go +++ b/vendor/github.com/prometheus/procfs/proc_ns.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/proc_psi.go b/vendor/github.com/prometheus/procfs/proc_psi.go index ccd35f153a..cc2c5de873 100644 --- a/vendor/github.com/prometheus/procfs/proc_psi.go +++ b/vendor/github.com/prometheus/procfs/proc_psi.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/proc_smaps.go b/vendor/github.com/prometheus/procfs/proc_smaps.go index 9a297afcf8..f637309b3d 100644 --- a/vendor/github.com/prometheus/procfs/proc_smaps.go +++ b/vendor/github.com/prometheus/procfs/proc_smaps.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,7 +12,6 @@ // limitations under the License. //go:build !windows -// +build !windows package procfs diff --git a/vendor/github.com/prometheus/procfs/proc_snmp.go b/vendor/github.com/prometheus/procfs/proc_snmp.go index 4bdc90b07e..8d9a9bcd67 100644 --- a/vendor/github.com/prometheus/procfs/proc_snmp.go +++ b/vendor/github.com/prometheus/procfs/proc_snmp.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/proc_snmp6.go b/vendor/github.com/prometheus/procfs/proc_snmp6.go index fb7fd3995b..841fef4649 100644 --- a/vendor/github.com/prometheus/procfs/proc_snmp6.go +++ b/vendor/github.com/prometheus/procfs/proc_snmp6.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/proc_stat.go b/vendor/github.com/prometheus/procfs/proc_stat.go index 3328556bdc..02e3f9e316 100644 --- a/vendor/github.com/prometheus/procfs/proc_stat.go +++ b/vendor/github.com/prometheus/procfs/proc_stat.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/proc_statm.go b/vendor/github.com/prometheus/procfs/proc_statm.go index ed57984243..6bcc97ec9c 100644 --- a/vendor/github.com/prometheus/procfs/proc_statm.go +++ b/vendor/github.com/prometheus/procfs/proc_statm.go @@ -1,4 +1,4 @@ -// Copyright 2025 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -45,6 +45,7 @@ type ProcStatm struct { } // NewStatm returns the current status information of the process. +// // Deprecated: Use p.Statm() instead. func (p Proc) NewStatm() (ProcStatm, error) { return p.Statm() @@ -80,7 +81,7 @@ func (p Proc) Statm() (ProcStatm, error) { func parseStatm(data []byte) ([]uint64, error) { var statmSlice []uint64 statmItems := strings.Fields(string(data)) - for i := 0; i < len(statmItems); i++ { + for i := range statmItems { statmItem, err := strconv.ParseUint(statmItems[i], 10, 64) if err != nil { return nil, err diff --git a/vendor/github.com/prometheus/procfs/proc_status.go b/vendor/github.com/prometheus/procfs/proc_status.go index dd8aa56885..12d65581c8 100644 --- a/vendor/github.com/prometheus/procfs/proc_status.go +++ b/vendor/github.com/prometheus/procfs/proc_status.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -16,7 +16,7 @@ package procfs import ( "bytes" "math/bits" - "sort" + "slices" "strconv" "strings" @@ -83,6 +83,19 @@ type ProcStatus struct { // CpusAllowedList: List of cpu cores processes are allowed to run on. CpusAllowedList []uint64 + + // CapInh is the bitmap of inheritable capabilities + // + // See: https://www.kernel.org/doc/man-pages/online/pages/man7/capabilities.7.html + CapInh uint64 + // CapPrm is the bitmap of permitted capabilities + CapPrm uint64 + // CapEff is the bitmap of effective capabilities + CapEff uint64 + // CapBnd is the bitmap of bounding capabilities + CapBnd uint64 + // CapAmb is the bitmap of ambient capabilities + CapAmb uint64 } // NewStatus returns the current status information of the process. @@ -94,8 +107,7 @@ func (p Proc) NewStatus() (ProcStatus, error) { s := ProcStatus{PID: p.PID} - lines := strings.Split(string(data), "\n") - for _, line := range lines { + for line := range strings.SplitSeq(string(data), "\n") { if !bytes.Contains([]byte(line), []byte(":")) { continue } @@ -191,6 +203,36 @@ func (s *ProcStatus) fillStatus(k string, vString string, vUint uint64, vUintByt s.NonVoluntaryCtxtSwitches = vUint case "Cpus_allowed_list": s.CpusAllowedList = calcCpusAllowedList(vString) + case "CapInh": + var err error + s.CapInh, err = strconv.ParseUint(vString, 16, 64) + if err != nil { + return err + } + case "CapPrm": + var err error + s.CapPrm, err = strconv.ParseUint(vString, 16, 64) + if err != nil { + return err + } + case "CapEff": + var err error + s.CapEff, err = strconv.ParseUint(vString, 16, 64) + if err != nil { + return err + } + case "CapBnd": + var err error + s.CapBnd, err = strconv.ParseUint(vString, 16, 64) + if err != nil { + return err + } + case "CapAmb": + var err error + s.CapAmb, err = strconv.ParseUint(vString, 16, 64) + if err != nil { + return err + } } return nil @@ -222,7 +264,7 @@ func calcCpusAllowedList(cpuString string) []uint64 { } - sort.Slice(g, func(i, j int) bool { return g[i] < g[j] }) + slices.Sort(g) return g } diff --git a/vendor/github.com/prometheus/procfs/proc_sys.go b/vendor/github.com/prometheus/procfs/proc_sys.go index 3810d1ac99..52658a4d52 100644 --- a/vendor/github.com/prometheus/procfs/proc_sys.go +++ b/vendor/github.com/prometheus/procfs/proc_sys.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/schedstat.go b/vendor/github.com/prometheus/procfs/schedstat.go index 5f7f32dc83..fafd8dff74 100644 --- a/vendor/github.com/prometheus/procfs/schedstat.go +++ b/vendor/github.com/prometheus/procfs/schedstat.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/slab.go b/vendor/github.com/prometheus/procfs/slab.go index 8611c90177..32a04678ad 100644 --- a/vendor/github.com/prometheus/procfs/slab.go +++ b/vendor/github.com/prometheus/procfs/slab.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/softirqs.go b/vendor/github.com/prometheus/procfs/softirqs.go index 403e6ae708..47b73a7297 100644 --- a/vendor/github.com/prometheus/procfs/softirqs.go +++ b/vendor/github.com/prometheus/procfs/softirqs.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/stat.go b/vendor/github.com/prometheus/procfs/stat.go index e36b41c18a..593ad0f62f 100644 --- a/vendor/github.com/prometheus/procfs/stat.go +++ b/vendor/github.com/prometheus/procfs/stat.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -16,6 +16,7 @@ package procfs import ( "bufio" "bytes" + "errors" "fmt" "io" "strconv" @@ -92,7 +93,7 @@ func parseCPUStat(line string) (CPUStat, int64, error) { &cpuStat.Iowait, &cpuStat.IRQ, &cpuStat.SoftIRQ, &cpuStat.Steal, &cpuStat.Guest, &cpuStat.GuestNice) - if err != nil && err != io.EOF { + if err != nil && !errors.Is(err, io.EOF) { return CPUStat{}, -1, fmt.Errorf("%w: couldn't parse %q (cpu): %w", ErrFileParse, line, err) } if count == 0 { diff --git a/vendor/github.com/prometheus/procfs/swaps.go b/vendor/github.com/prometheus/procfs/swaps.go index 65fec834bf..ee17bf4888 100644 --- a/vendor/github.com/prometheus/procfs/swaps.go +++ b/vendor/github.com/prometheus/procfs/swaps.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/thread.go b/vendor/github.com/prometheus/procfs/thread.go index 80e0e947be..0cfbb54184 100644 --- a/vendor/github.com/prometheus/procfs/thread.go +++ b/vendor/github.com/prometheus/procfs/thread.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/vendor/github.com/prometheus/procfs/vm.go b/vendor/github.com/prometheus/procfs/vm.go index 51c49d89e8..52180c03e2 100644 --- a/vendor/github.com/prometheus/procfs/vm.go +++ b/vendor/github.com/prometheus/procfs/vm.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,7 +12,6 @@ // limitations under the License. //go:build !windows -// +build !windows package procfs diff --git a/vendor/github.com/prometheus/procfs/zoneinfo.go b/vendor/github.com/prometheus/procfs/zoneinfo.go index e54d94b090..63d1898bc8 100644 --- a/vendor/github.com/prometheus/procfs/zoneinfo.go +++ b/vendor/github.com/prometheus/procfs/zoneinfo.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Prometheus Authors +// Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -12,7 +12,6 @@ // limitations under the License. //go:build !windows -// +build !windows package procfs @@ -88,11 +87,9 @@ func parseZoneinfo(zoneinfoData []byte) ([]Zoneinfo, error) { zoneinfo := []Zoneinfo{} - zoneinfoBlocks := bytes.Split(zoneinfoData, []byte("\nNode")) - for _, block := range zoneinfoBlocks { + for block := range bytes.SplitSeq(zoneinfoData, []byte("\nNode")) { var zoneinfoElement Zoneinfo - lines := strings.Split(string(block), "\n") - for _, line := range lines { + for line := range strings.SplitSeq(string(block), "\n") { if nodeZone := nodeZoneRE.FindStringSubmatch(line); nodeZone != nil { zoneinfoElement.Node = nodeZone[1] diff --git a/vendor/github.com/valyala/fastjson/parser.go b/vendor/github.com/valyala/fastjson/parser.go index 885e1841ef..f9f5ce4989 100644 --- a/vendor/github.com/valyala/fastjson/parser.go +++ b/vendor/github.com/valyala/fastjson/parser.go @@ -32,7 +32,7 @@ func (p *Parser) Parse(s string) (*Value, error) { p.b = append(p.b[:0], s...) p.c.reset() - v, tail, err := parseValue(b2s(p.b), &p.c, 0) + v, tail, err := p.c.parseValue(b2s(p.b), 0) if err != nil { return nil, fmt.Errorf("cannot parse JSON: %s; unparsed tail: %q", err, startEndString(tail)) } @@ -57,7 +57,11 @@ type cache struct { } func (c *cache) reset() { - c.vs = c.vs[:0] + vs := c.vs + for i := range vs { + vs[i].reset() + } + c.vs = vs[:0] } func (c *cache) getValue() *Value { @@ -66,7 +70,6 @@ func (c *cache) getValue() *Value { } else { c.vs = append(c.vs, Value{}) } - // Do not reset the value, since the caller must properly init it. return &c.vs[len(c.vs)-1] } @@ -98,7 +101,7 @@ type kv struct { // MaxDepth is the maximum depth for nested JSON. const MaxDepth = 300 -func parseValue(s string, c *cache, depth int) (*Value, string, error) { +func (c *cache) parseValue(s string, depth int) (*Value, string, error) { if len(s) == 0 { return nil, s, fmt.Errorf("cannot parse empty string") } @@ -108,14 +111,14 @@ func parseValue(s string, c *cache, depth int) (*Value, string, error) { } if s[0] == '{' { - v, tail, err := parseObject(s[1:], c, depth) + v, tail, err := c.parseObject(s[1:], depth) if err != nil { return nil, tail, fmt.Errorf("cannot parse object: %s", err) } return v, tail, nil } if s[0] == '[' { - v, tail, err := parseArray(s[1:], c, depth) + v, tail, err := c.parseArray(s[1:], depth) if err != nil { return nil, tail, fmt.Errorf("cannot parse array: %s", err) } @@ -167,7 +170,7 @@ func parseValue(s string, c *cache, depth int) (*Value, string, error) { return v, tail, nil } -func parseArray(s string, c *cache, depth int) (*Value, string, error) { +func (c *cache) parseArray(s string, depth int) (*Value, string, error) { s = skipWS(s) if len(s) == 0 { return nil, s, fmt.Errorf("missing ']'") @@ -188,7 +191,7 @@ func parseArray(s string, c *cache, depth int) (*Value, string, error) { var err error s = skipWS(s) - v, s, err = parseValue(s, c, depth) + v, s, err = c.parseValue(s, depth) if err != nil { return nil, s, fmt.Errorf("cannot parse array value: %s", err) } @@ -210,7 +213,7 @@ func parseArray(s string, c *cache, depth int) (*Value, string, error) { } } -func parseObject(s string, c *cache, depth int) (*Value, string, error) { +func (c *cache) parseObject(s string, depth int) (*Value, string, error) { s = skipWS(s) if len(s) == 0 { return nil, s, fmt.Errorf("missing '}'") @@ -247,7 +250,7 @@ func parseObject(s string, c *cache, depth int) (*Value, string, error) { // Parse value s = skipWS(s) - kv.v, s, err = parseValue(s, c, depth) + kv.v, s, err = c.parseValue(s, depth) if err != nil { return nil, s, fmt.Errorf("cannot parse object value: %s", err) } @@ -283,7 +286,7 @@ func hasSpecialChars(s string) bool { if strings.IndexByte(s, '"') >= 0 || strings.IndexByte(s, '\\') >= 0 { return true } - for i := 0; i < len(s); i++ { + for i := range len(s) { if s[i] < 0x20 { return true } @@ -375,7 +378,7 @@ func unescapeStringBestEffort(s string) string { // parseRawKey is similar to parseRawString, but is optimized // for small-sized keys without escape sequences. func parseRawKey(s string) (string, string, error) { - for i := 0; i < len(s); i++ { + for i := range len(s) { if s[i] == '"' { // Fast path. return s[:i], s[i+1:], nil @@ -424,7 +427,7 @@ func parseRawNumber(s string) (string, string, error) { // The caller must ensure len(s) > 0 // Find the end of the number. - for i := 0; i < len(s); i++ { + for i := range len(s) { ch := s[i] if (ch >= '0' && ch <= '9') || ch == '.' || ch == '-' || ch == 'e' || ch == 'E' || ch == '+' { continue @@ -455,14 +458,19 @@ type Object struct { } func (o *Object) reset() { + // o.kvs entries can point to external byte slices. Clear these references, so GC could free memory. + clear(o.kvs) o.kvs = o.kvs[:0] + o.keysUnescaped = false } // MarshalTo appends marshaled o to dst and returns the result. func (o *Object) MarshalTo(dst []byte) []byte { dst = append(dst, '{') - for i, kv := range o.kvs { + kvs := o.kvs + for i := range kvs { + kv := &kvs[i] if o.keysUnescaped { dst = escapeString(dst, kv.k) } else { @@ -525,7 +533,9 @@ func (o *Object) Len() int { func (o *Object) Get(key string) *Value { if !o.keysUnescaped && strings.IndexByte(key, '\\') < 0 { // Fast path - try searching for the key without object keys unescaping. - for _, kv := range o.kvs { + kvs := o.kvs + for i := range kvs { + kv := &kvs[i] if kv.k == key { return kv.v } @@ -535,7 +545,9 @@ func (o *Object) Get(key string) *Value { // Slow path - unescape object keys. o.unescapeKeys() - for _, kv := range o.kvs { + kvs := o.kvs + for i := range kvs { + kv := &kvs[i] if kv.k == key { return kv.v } @@ -554,7 +566,9 @@ func (o *Object) Visit(f func(key []byte, v *Value)) { o.unescapeKeys() - for _, kv := range o.kvs { + kvs := o.kvs + for i := range kvs { + kv := &kvs[i] f(s2b(kv.k), kv.v) } } @@ -572,6 +586,16 @@ type Value struct { t Type } +func (v *Value) reset() { + v.o.reset() + + clear(v.a) + v.a = v.a[:0] + + v.s = "" + v.t = 0 +} + // MarshalTo appends marshaled v to dst and returns the result. func (v *Value) MarshalTo(dst []byte) []byte { switch v.t { diff --git a/vendor/github.com/valyala/fastjson/scanner.go b/vendor/github.com/valyala/fastjson/scanner.go index 89b38816f0..5db3f14edb 100644 --- a/vendor/github.com/valyala/fastjson/scanner.go +++ b/vendor/github.com/valyala/fastjson/scanner.go @@ -65,7 +65,7 @@ func (sc *Scanner) Next() bool { } sc.c.reset() - v, tail, err := parseValue(sc.s, &sc.c, 0) + v, tail, err := sc.c.parseValue(sc.s, 0) if err != nil { sc.err = err return false diff --git a/vendor/github.com/valyala/fastjson/update.go b/vendor/github.com/valyala/fastjson/update.go index 795b7fbd6d..0b64e384df 100644 --- a/vendor/github.com/valyala/fastjson/update.go +++ b/vendor/github.com/valyala/fastjson/update.go @@ -12,7 +12,9 @@ func (o *Object) Del(key string) { } if !o.keysUnescaped && strings.IndexByte(key, '\\') < 0 { // Fast path - try searching for the key without object keys unescaping. - for i, kv := range o.kvs { + kvs := o.kvs + for i := range kvs { + kv := &kvs[i] if kv.k == key { o.kvs = append(o.kvs[:i], o.kvs[i+1:]...) return @@ -23,7 +25,9 @@ func (o *Object) Del(key string) { // Slow path - unescape object keys before item search. o.unescapeKeys() - for i, kv := range o.kvs { + kvs := o.kvs + for i := range kvs { + kv := &kvs[i] if kv.k == key { o.kvs = append(o.kvs[:i], o.kvs[i+1:]...) return @@ -62,8 +66,9 @@ func (o *Object) Set(key string, value *Value) { o.unescapeKeys() // Try substituting already existing entry with the given key. - for i := range o.kvs { - kv := &o.kvs[i] + kvs := o.kvs + for i := range kvs { + kv := &kvs[i] if kv.k == key { kv.v = value return diff --git a/vendor/github.com/valyala/fastjson/validate.go b/vendor/github.com/valyala/fastjson/validate.go index 196f1c3dc6..8f1173cf61 100644 --- a/vendor/github.com/valyala/fastjson/validate.go +++ b/vendor/github.com/valyala/fastjson/validate.go @@ -51,7 +51,7 @@ func validateValue(s string) (string, error) { return tail, fmt.Errorf("cannot parse string: %s", err) } // Scan the string for control chars. - for i := 0; i < len(sv); i++ { + for i := range len(sv) { if sv[i] < 0x20 { return tail, fmt.Errorf("string cannot contain control char 0x%02X", sv[i]) } @@ -142,7 +142,7 @@ func validateObject(s string) (string, error) { return s, fmt.Errorf("cannot parse object key: %s", err) } // Scan the key for control chars. - for i := 0; i < len(key); i++ { + for i := range len(key) { if key[i] < 0x20 { return s, fmt.Errorf("object key cannot contain control char 0x%02X", key[i]) } @@ -177,7 +177,7 @@ func validateObject(s string) (string, error) { // validateKey is similar to validateString, but is optimized // for typical object keys, which are quite small and have no escape sequences. func validateKey(s string) (string, string, error) { - for i := 0; i < len(s); i++ { + for i := range len(s) { if s[i] == '"' { // Fast path - the key doesn't contain escape sequences. return s[:i], s[i+1:], nil diff --git a/vendor/github.com/vektah/gqlparser/v2/parser/schema.go b/vendor/github.com/vektah/gqlparser/v2/parser/schema.go index 804f02c9f8..a004a0f6c0 100644 --- a/vendor/github.com/vektah/gqlparser/v2/parser/schema.go +++ b/vendor/github.com/vektah/gqlparser/v2/parser/schema.go @@ -231,7 +231,6 @@ func (p *parser) parseFieldsDefinition() (FieldList, *CommentGroup) { func (p *parser) parseFieldDefinition() *FieldDefinition { var def FieldDefinition - def.Position = p.peekPos() desc := p.parseDescription() if desc.text != "" { @@ -241,6 +240,7 @@ func (p *parser) parseFieldDefinition() *FieldDefinition { p.peek() // peek to set p.comment def.AfterDescriptionComment = p.comment + def.Position = p.peekPos() def.Name = p.parseName() def.Arguments = p.parseArgumentDefs() p.expect(lexer.Colon) @@ -260,7 +260,6 @@ func (p *parser) parseArgumentDefs() ArgumentDefinitionList { func (p *parser) parseArgumentDef() *ArgumentDefinition { var def ArgumentDefinition - def.Position = p.peekPos() desc := p.parseDescription() if desc.text != "" { @@ -270,6 +269,7 @@ func (p *parser) parseArgumentDef() *ArgumentDefinition { p.peek() // peek to set p.comment def.AfterDescriptionComment = p.comment + def.Position = p.peekPos() def.Name = p.parseName() p.expect(lexer.Colon) def.Type = p.parseTypeReference() @@ -282,7 +282,6 @@ func (p *parser) parseArgumentDef() *ArgumentDefinition { func (p *parser) parseInputValueDef() *FieldDefinition { var def FieldDefinition - def.Position = p.peekPos() desc := p.parseDescription() if desc.text != "" { @@ -292,6 +291,7 @@ func (p *parser) parseInputValueDef() *FieldDefinition { p.peek() // peek to set p.comment def.AfterDescriptionComment = p.comment + def.Position = p.peekPos() def.Name = p.parseName() p.expect(lexer.Colon) def.Type = p.parseTypeReference() @@ -372,7 +372,6 @@ func (p *parser) parseEnumValuesDefinition() (EnumValueList, *CommentGroup) { func (p *parser) parseEnumValueDefinition() *EnumValueDefinition { var def EnumValueDefinition - def.Position = p.peekPos() desc := p.parseDescription() if desc.text != "" { def.BeforeDescriptionComment = desc.comment @@ -381,7 +380,7 @@ func (p *parser) parseEnumValueDefinition() *EnumValueDefinition { p.peek() // peek to set p.comment def.AfterDescriptionComment = p.comment - + def.Position = p.peekPos() def.Name = p.parseName() def.Directives = p.parseDirectives(true) diff --git a/vendor/github.com/vektah/gqlparser/v2/validator/rules/scalar_leafs.go b/vendor/github.com/vektah/gqlparser/v2/validator/rules/scalar_leafs.go index 95fd2298f4..5979c29980 100644 --- a/vendor/github.com/vektah/gqlparser/v2/validator/rules/scalar_leafs.go +++ b/vendor/github.com/vektah/gqlparser/v2/validator/rules/scalar_leafs.go @@ -6,31 +6,39 @@ import ( . "github.com/vektah/gqlparser/v2/validator/core" ) -var ScalarLeafsRule = Rule{ - Name: "ScalarLeafs", - RuleFunc: func(observers *Events, addError AddErrFunc) { - observers.OnField(func(walker *Walker, field *ast.Field) { - if field.Definition == nil { - return - } +func ruleFuncScalarLeafs(observers *Events, addError AddErrFunc, disableSuggestion bool) { + observers.OnField(func(walker *Walker, field *ast.Field) { + if field.Definition == nil { + return + } - fieldType := walker.Schema.Types[field.Definition.Type.Name()] - if fieldType == nil { - return - } + fieldType := walker.Schema.Types[field.Definition.Type.Name()] + if fieldType == nil { + return + } - if fieldType.IsLeafType() && len(field.SelectionSet) > 0 { + if fieldType.IsLeafType() && len(field.SelectionSet) > 0 { + addError( + Message( + `Field "%s" must not have a selection since type "%s" has no subfields.`, + field.Name, + fieldType.Name, + ), + At(field.Position), + ) + } + + if !fieldType.IsLeafType() && len(field.SelectionSet) == 0 { + if disableSuggestion { addError( Message( - `Field "%s" must not have a selection since type "%s" has no subfields.`, + `Field "%s" of type "%s" must have a selection of subfields.`, field.Name, - fieldType.Name, + field.Definition.Type.String(), ), At(field.Position), ) - } - - if !fieldType.IsLeafType() && len(field.SelectionSet) == 0 { + } else { addError( Message( `Field "%s" of type "%s" must have a selection of subfields.`, @@ -41,6 +49,20 @@ var ScalarLeafsRule = Rule{ At(field.Position), ) } - }) + } + }) +} + +var ScalarLeafsRule = Rule{ + Name: "ScalarLeafs", + RuleFunc: func(observers *Events, addError AddErrFunc) { + ruleFuncScalarLeafs(observers, addError, false) + }, +} + +var ScalarLeafsRuleWithoutSuggestions = Rule{ + Name: "ScalarLeafsWithoutSuggestions", + RuleFunc: func(observers *Events, addError AddErrFunc) { + ruleFuncScalarLeafs(observers, addError, true) }, } diff --git a/vendor/github.com/vektah/gqlparser/v2/validator/rules/unique_directives_per_location.go b/vendor/github.com/vektah/gqlparser/v2/validator/rules/unique_directives_per_location.go index a1a0101671..b801ede378 100644 --- a/vendor/github.com/vektah/gqlparser/v2/validator/rules/unique_directives_per_location.go +++ b/vendor/github.com/vektah/gqlparser/v2/validator/rules/unique_directives_per_location.go @@ -13,7 +13,7 @@ var UniqueDirectivesPerLocationRule = Rule{ seen := map[string]bool{} for _, dir := range directives { - if dir.Name != "repeatable" && seen[dir.Name] { + if (dir.Definition == nil || !dir.Definition.IsRepeatable) && seen[dir.Name] { addError( Message( `The directive "@%s" can only be used once at this location.`, diff --git a/vendor/github.com/vektah/gqlparser/v2/validator/rules/values_of_correct_type.go b/vendor/github.com/vektah/gqlparser/v2/validator/rules/values_of_correct_type.go index 5126245e54..95a06099ec 100644 --- a/vendor/github.com/vektah/gqlparser/v2/validator/rules/values_of_correct_type.go +++ b/vendor/github.com/vektah/gqlparser/v2/validator/rules/values_of_correct_type.go @@ -188,20 +188,8 @@ func ruleFuncValuesOfCorrectType(observers *Events, addError AddErrFunc, disable return } - isVariable := fieldValue.Kind == ast.Variable - if isVariable { - variableName := fieldValue.VariableDefinition.Variable - isNullableVariable := !fieldValue.VariableDefinition.Type.NonNull - if isNullableVariable { - addError( - Message( - `Variable "%s" must be non-nullable to be used for OneOf Input Object "%s".`, - variableName, - value.Definition.Name, - ), - At(fieldValue.Position), - ) - } + if fieldValue.Kind == ast.Variable { + return } }() } diff --git a/vendor/github.com/vektah/gqlparser/v2/validator/rules/variables_in_allowed_position.go b/vendor/github.com/vektah/gqlparser/v2/validator/rules/variables_in_allowed_position.go index d3d36a293f..a10cb9edf2 100644 --- a/vendor/github.com/vektah/gqlparser/v2/validator/rules/variables_in_allowed_position.go +++ b/vendor/github.com/vektah/gqlparser/v2/validator/rules/variables_in_allowed_position.go @@ -44,5 +44,34 @@ var VariablesInAllowedPositionRule = Rule{ ) } }) + + observers.OnValue(func(walker *Walker, value *ast.Value) { + if value.Kind != ast.ObjectValue || value.Definition == nil { + return + } + if value.Definition.Directives.ForName("oneOf") == nil { + return + } + + for _, child := range value.Children { + fieldValue := child.Value + if fieldValue == nil || fieldValue.Kind != ast.Variable || + fieldValue.VariableDefinition == nil { + continue + } + if !fieldValue.VariableDefinition.Type.NonNull { + addError( + Message( + `Variable "%s" is of type "%s" but must be non-nullable to be used for OneOf Input Object "%s".`, + fieldValue, + fieldValue.VariableDefinition.Type.String(), + value.Definition.Name, + ), + At(fieldValue.VariableDefinition.Position), + At(fieldValue.Position), + ) + } + } + }) }, } diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/server.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/server.go index 6dcf1b5b52..83c6ae2465 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/server.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/server.go @@ -364,7 +364,9 @@ func (n HTTPServer) MetricAttributes(server string, req *http.Request, statusCod if statusCode > 0 { num++ } - + if route == "" && req.Pattern != "" { + route = httpRoute(req.Pattern) + } if route != "" { num++ } diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go index 1d90fc264d..835ec5aa7e 100644 --- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go +++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go @@ -4,4 +4,4 @@ package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" // Version is the current release version of the otelhttp instrumentation. -const Version = "0.67.0" +const Version = "0.68.0" diff --git a/vendor/modules.txt b/vendor/modules.txt index ffa1ce959b..1165fecfac 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -354,7 +354,7 @@ github.com/davidbyttow/govips/v2/vips # github.com/deckarep/golang-set v1.8.0 ## explicit; go 1.17 github.com/deckarep/golang-set -# github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 +# github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 ## explicit; go 1.17 github.com/decred/dcrd/dcrec/secp256k1/v4 # github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f @@ -423,8 +423,8 @@ github.com/fatih/color # github.com/felixge/httpsnoop v1.0.4 ## explicit; go 1.13 github.com/felixge/httpsnoop -# github.com/fsnotify/fsnotify v1.9.0 -## explicit; go 1.17 +# github.com/fsnotify/fsnotify v1.10.1 +## explicit; go 1.23 github.com/fsnotify/fsnotify github.com/fsnotify/fsnotify/internal # github.com/gabriel-vasile/mimetype v1.4.13 @@ -654,7 +654,7 @@ github.com/gobwas/pool/pbytes ## explicit; go 1.15 github.com/gobwas/ws github.com/gobwas/ws/wsutil -# github.com/goccy/go-json v0.10.5 +# github.com/goccy/go-json v0.10.6 ## explicit; go 1.19 github.com/goccy/go-json github.com/goccy/go-json/internal/decoder @@ -905,7 +905,7 @@ github.com/leonelquinteros/gotext/plurals # github.com/lestrrat-go/blackmagic v1.0.4 ## explicit; go 1.23 github.com/lestrrat-go/blackmagic -# github.com/lestrrat-go/dsig v1.0.0 +# github.com/lestrrat-go/dsig v1.2.1 ## explicit; go 1.23.0 github.com/lestrrat-go/dsig github.com/lestrrat-go/dsig/internal/ecutil @@ -915,20 +915,19 @@ github.com/lestrrat-go/dsig-secp256k1 # github.com/lestrrat-go/httpcc v1.0.1 ## explicit; go 1.16 github.com/lestrrat-go/httpcc -# github.com/lestrrat-go/httprc/v3 v3.0.2 +# github.com/lestrrat-go/httprc/v3 v3.0.5 ## explicit; go 1.23.0 github.com/lestrrat-go/httprc/v3 github.com/lestrrat-go/httprc/v3/errsink github.com/lestrrat-go/httprc/v3/proxysink github.com/lestrrat-go/httprc/v3/tracesink -# github.com/lestrrat-go/jwx/v3 v3.0.13 -## explicit; go 1.24.0 +# github.com/lestrrat-go/jwx/v3 v3.1.1 +## explicit; go 1.25.0 github.com/lestrrat-go/jwx/v3 github.com/lestrrat-go/jwx/v3/cert github.com/lestrrat-go/jwx/v3/internal/base64 github.com/lestrrat-go/jwx/v3/internal/ecutil github.com/lestrrat-go/jwx/v3/internal/json -github.com/lestrrat-go/jwx/v3/internal/jwxio github.com/lestrrat-go/jwx/v3/internal/keyconv github.com/lestrrat-go/jwx/v3/internal/pool github.com/lestrrat-go/jwx/v3/internal/tokens @@ -942,6 +941,7 @@ github.com/lestrrat-go/jwx/v3/jwe/internal/keygen github.com/lestrrat-go/jwx/v3/jwe/jwebb github.com/lestrrat-go/jwx/v3/jwk github.com/lestrrat-go/jwx/v3/jwk/ecdsa +github.com/lestrrat-go/jwx/v3/jwk/internal/registry github.com/lestrrat-go/jwx/v3/jwk/jwkbb github.com/lestrrat-go/jwx/v3/jws github.com/lestrrat-go/jwx/v3/jws/internal/keytype @@ -1278,7 +1278,7 @@ github.com/onsi/gomega/matchers/support/goraph/edge github.com/onsi/gomega/matchers/support/goraph/node github.com/onsi/gomega/matchers/support/goraph/util github.com/onsi/gomega/types -# github.com/open-policy-agent/opa v1.15.2 +# github.com/open-policy-agent/opa v1.17.0 ## explicit; go 1.25.0 github.com/open-policy-agent/opa/ast github.com/open-policy-agent/opa/ast/json @@ -1351,6 +1351,7 @@ github.com/open-policy-agent/opa/v1/topdown github.com/open-policy-agent/opa/v1/topdown/builtins github.com/open-policy-agent/opa/v1/topdown/cache github.com/open-policy-agent/opa/v1/topdown/copypropagation +github.com/open-policy-agent/opa/v1/topdown/durationparser github.com/open-policy-agent/opa/v1/topdown/print github.com/open-policy-agent/opa/v1/tracing github.com/open-policy-agent/opa/v1/types @@ -1839,8 +1840,8 @@ github.com/prometheus/common/expfmt github.com/prometheus/common/helpers/templates github.com/prometheus/common/model github.com/prometheus/common/promslog -# github.com/prometheus/procfs v0.17.0 -## explicit; go 1.23.0 +# github.com/prometheus/procfs v0.20.1 +## explicit; go 1.25.0 github.com/prometheus/procfs github.com/prometheus/procfs/internal/fs github.com/prometheus/procfs/internal/util @@ -2131,11 +2132,11 @@ github.com/unrolled/secure/cspbuilder # github.com/urfave/cli/v2 v2.27.7 ## explicit; go 1.18 github.com/urfave/cli/v2 -# github.com/valyala/fastjson v1.6.7 -## explicit; go 1.12 +# github.com/valyala/fastjson v1.6.10 +## explicit; go 1.24 github.com/valyala/fastjson github.com/valyala/fastjson/fastfloat -# github.com/vektah/gqlparser/v2 v2.5.32 +# github.com/vektah/gqlparser/v2 v2.5.33 ## explicit; go 1.22 github.com/vektah/gqlparser/v2/ast github.com/vektah/gqlparser/v2/gqlerror @@ -2311,7 +2312,7 @@ go.opentelemetry.io/auto/sdk/internal/telemetry ## explicit; go 1.25.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/internal -# go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 +# go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 ## explicit; go 1.25.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request @@ -2406,7 +2407,7 @@ go.uber.org/zap/internal/pool go.uber.org/zap/internal/stacktrace go.uber.org/zap/zapcore go.uber.org/zap/zapgrpc -# go.yaml.in/yaml/v2 v2.4.3 +# go.yaml.in/yaml/v2 v2.4.4 ## explicit; go 1.15 go.yaml.in/yaml/v2 # go.yaml.in/yaml/v3 v3.0.4