build(deps): bump github.com/open-policy-agent/opa from 1.15.2 to 1.17.1

Bumps [github.com/open-policy-agent/opa](https://github.com/open-policy-agent/opa) from 1.15.2 to 1.17.1.
- [Release notes](https://github.com/open-policy-agent/opa/releases)
- [Changelog](https://github.com/open-policy-agent/opa/blob/v1.17.1/CHANGELOG.md)
- [Commits](https://github.com/open-policy-agent/opa/compare/v1.15.2...v1.17.1)

---
updated-dependencies:
- dependency-name: github.com/open-policy-agent/opa
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2026-06-16 08:40:16 +00:00
committed by GitHub
parent bb715fa4d4
commit 42987b038b
323 changed files with 38280 additions and 3241 deletions

24
go.mod
View File

@@ -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.1
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.4-0.20260615073558-209c2cd3b52b
@@ -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.44.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
@@ -227,7 +227,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
@@ -263,11 +263,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
@@ -327,7 +327,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
@@ -365,8 +365,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
@@ -386,7 +386,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

52
go.sum
View File

@@ -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=
@@ -940,8 +940,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.1 h1:wO0MOux/VCqY41aVAD6Toe1p3A7O7DlRZ1RHmYSpoS8=
github.com/open-policy-agent/opa v1.17.1/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=
@@ -1051,8 +1051,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=
@@ -1229,12 +1229,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=
@@ -1300,8 +1300,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=
@@ -1337,8 +1337,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=

View File

@@ -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
}

View File

@@ -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 ./...

View File

@@ -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
----------------

View File

@@ -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

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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)
)

View File

@@ -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) }

View File

@@ -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},
}

View File

@@ -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},
}

View File

@@ -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},
}

View File

@@ -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)
}

View File

@@ -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},
}

View File

@@ -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},
}

View File

@@ -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)) }

View File

@@ -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) }

View File

@@ -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
}
}
}

View File

@@ -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") }

View File

@@ -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
}

7
vendor/github.com/lestrrat-go/dsig/.goreleaser.yml generated vendored Normal file
View File

@@ -0,0 +1,7 @@
version: 2
builds:
- skip: true
changelog:
use: github-native

View File

@@ -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
* Initial release

View File

@@ -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

View File

@@ -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{}{}
}
}

View File

@@ -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.

View File

@@ -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).

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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
}

View File

@@ -72,6 +72,7 @@ func (p *Proxy[T]) flushloop(ctx context.Context) {
p.mu.Unlock()
return
}
p.mu.Unlock()
default:
}

View File

@@ -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)

View File

@@ -37,3 +37,7 @@ out
cmd/jwx/jwx
bazel-*
# Go workspace files (local development only)
go.work
go.work.sum

View File

@@ -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/

266
vendor/github.com/lestrrat-go/jwx/v3/AGENTS.md generated vendored Normal file
View File

@@ -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-<pkg>` 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
<!-- INCLUDE(examples/jwt_parse_example_test.go) -->
<!-- END INCLUDE -->
```
## 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 |

View File

@@ -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()`.

View File

@@ -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")

View File

@@ -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",

View File

@@ -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

View File

@@ -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",
],
)

View File

@@ -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
}

View File

@@ -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
}

34
vendor/github.com/lestrrat-go/jwx/v3/cert/options.go generated vendored Normal file
View File

@@ -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)}
}

76
vendor/github.com/lestrrat-go/jwx/v3/cert/settings.go generated vendored Normal file
View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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)
}

View File

@@ -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)
}
// stock decodeCtx. should cover 80% of the cases
type decodeCtx struct {
registry *Registry
// 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
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
}

View File

@@ -1,6 +1,5 @@
//go:build !jwx_goccy
//nolint:revive
package json
import (

View File

@@ -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__"],
)

View File

@@ -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
}
}

View File

@@ -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",

View File

@@ -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
}

View File

@@ -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] {

View File

@@ -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
}

View File

@@ -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",
],

View File

@@ -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()
})

View File

@@ -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.

14
vendor/github.com/lestrrat-go/jwx/v3/jwa/ed448.go generated vendored Normal file
View File

@@ -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")
}

View File

@@ -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()
})

View File

@@ -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/<Kind>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
// <Kind>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)
}

View File

@@ -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.

View File

@@ -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()
})

View File

@@ -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.

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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...)}
}

View File

@@ -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)
}
h.mu.RUnlock()
sort.Strings(keys)
buf := pool.BytesBuffer().Get()
defer pool.BytesBuffer().Put(buf)
enc := json.NewEncoder(buf)
buf.WriteByte(tokens.OpenCurlyBracket)
for i, k := range keys {
if i > 0 {
buf.WriteRune(tokens.Comma)
}
buf.WriteRune(tokens.DoubleQuote)
buf.WriteString(k)
buf.WriteString(`":`)
v := data[k]
var encoded []byte
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)
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)
}
buf.Truncate(buf.Len() - 1)
}
pairs = append(pairs, headerPair{Name: k, Value: encoded})
}
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)
pairs, err := h.makePairs()
if err != nil {
return nil, fmt.Errorf(`failed to make pairs: %w`, err)
}
buf.WriteByte(tokens.OpenCurlyBracket)
for i, pair := range pairs {
if i > 0 {
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()
}

View File

@@ -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 {

View File

@@ -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]

View File

@@ -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
}

View File

@@ -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++
}

View File

@@ -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)

View File

@@ -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...)
}

View File

@@ -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
}
func (dc *decryptContext) DecryptMessage(buf []byte) ([]byte, error) {
msg, err := parseJSONOrCompact(buf, true)
if err != nil {
return nil, fmt.Errorf(`failed to parse buffer for Decrypt: %w`, err)
// 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
}
// Process things that are common to the message
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, dc.maxRecipients)
if err != nil {
return nil, fmt.Errorf(`jwe.Decrypt: failed to parse buffer: %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
}
}
// 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 copy protected headers: %w`, err)
}
h, err = h.Merge(msg.unprotectedHeaders)
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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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])

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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.
encouraged to set this option to `false` now to avoid future issues.

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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)

View File

@@ -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:<curve>" 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())]
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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)
}
sort.Strings(fields)
buf := pool.BytesBuffer().Get()
defer pool.BytesBuffer().Put(buf)
buf.WriteByte(tokens.OpenCurlyBracket)
enc := json.NewEncoder(buf)
for i, f := range fields {
if i > 0 {
buf.WriteRune(tokens.Comma)
}
buf.WriteRune(tokens.DoubleQuote)
buf.WriteString(f)
buf.WriteString(`":`)
v := data[f]
var encoded []byte
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)
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)
}
buf.Truncate(buf.Len() - 1)
}
pairs = append(pairs, fieldPair{Name: k, Value: encoded})
}
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)
for i, pair := range pairs {
if i > 0 {
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)
}
sort.Strings(fields)
buf := pool.BytesBuffer().Get()
defer pool.BytesBuffer().Put(buf)
buf.WriteByte(tokens.OpenCurlyBracket)
enc := json.NewEncoder(buf)
for i, f := range fields {
if i > 0 {
buf.WriteRune(tokens.Comma)
}
buf.WriteRune(tokens.DoubleQuote)
buf.WriteString(f)
buf.WriteString(`":`)
v := data[f]
var encoded []byte
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)
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)
}
buf.Truncate(buf.Len() - 1)
}
pairs = append(pairs, fieldPair{Name: k, Value: encoded})
}
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)
for i, pair := range pairs {
if i > 0 {
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() },
})
}

Some files were not shown because too many files have changed in this diff Show More