From e32b448582b42991156e4c00ea7fdb5597dfc246 Mon Sep 17 00:00:00 2001 From: lestrrat <49281+lestrrat@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:57:30 +0900 Subject: [PATCH] V3 modernize infra (#1180) * Modernize workflows * Appease linter * Fix Raw -> Export * Fix smoke test go version * Remove support for PEM encoding/decoding secp256k1 keys * Add missing file * Fix Bazel --- .github/workflows/assign-issue.yml | 2 +- .github/workflows/assign-pr.yml | 2 +- .github/workflows/autodoc.yml | 2 +- .github/workflows/ci.yml | 23 +- .github/workflows/codeql.yml | 2 +- .github/workflows/dependabot.yml | 2 +- .github/workflows/lint.yml | 11 +- .github/workflows/smoke.yml | 10 +- .github/workflows/stale.yml | 2 +- .golangci.yml | 3 + Changes-v3.md | 6 +- jwe/jwe_test.go | 18 +- jwk/BUILD.bazel | 5 +- jwk/es256k_go1.20_test.go | 44 --- jwk/internal/x509/BUILD.bazel | 14 - jwk/internal/x509/x509.go | 31 -- jwk/internal/x509/x509_nosecp256k1.go | 28 -- jwk/internal/x509/x509_sepc256k1.go | 479 -------------------------- jwk/jwk.go | 148 +------- jwk/jwk_test.go | 26 +- jwk/options.yaml | 6 + jwk/options_gen.go | 11 + jwk/options_gen_test.go | 1 + jwk/x509.go | 160 +++++++++ jws/BUILD.bazel | 1 - jws/jws.go | 1 + jwt/jwt_test.go | 6 +- jwt/validate.go | 1 + jwx_test.go | 2 +- 29 files changed, 249 insertions(+), 798 deletions(-) delete mode 100644 jwk/es256k_go1.20_test.go delete mode 100644 jwk/internal/x509/BUILD.bazel delete mode 100644 jwk/internal/x509/x509.go delete mode 100644 jwk/internal/x509/x509_nosecp256k1.go delete mode 100644 jwk/internal/x509/x509_sepc256k1.go create mode 100644 jwk/x509.go diff --git a/.github/workflows/assign-issue.yml b/.github/workflows/assign-issue.yml index c0f1a5534..2a2de0602 100644 --- a/.github/workflows/assign-issue.yml +++ b/.github/workflows/assign-issue.yml @@ -8,6 +8,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Auto-assign issue' - uses: pozil/auto-assign-issue@v2 + uses: pozil/auto-assign-issue@c5bca5027e680b9e8411b826d16947afd8c76b32 # v2.0.0 with: assignees: lestrrat diff --git a/.github/workflows/assign-pr.yml b/.github/workflows/assign-pr.yml index 3dcb2ed63..7afe436ac 100644 --- a/.github/workflows/assign-pr.yml +++ b/.github/workflows/assign-pr.yml @@ -7,6 +7,6 @@ jobs: add-reviews: runs-on: ubuntu-latest steps: - - uses: kentaro-m/auto-assign-action@v2.0.0 + - uses: kentaro-m/auto-assign-action@f4648c0a9fdb753479e9e75fc251f507ce17bb7e # v2.0.0 with: configuration-path: .github/auto-assign-pr.yml diff --git a/.github/workflows/autodoc.yml b/.github/workflows/autodoc.yml index 2e4843168..79f0d4313 100644 --- a/.github/workflows/autodoc.yml +++ b/.github/workflows/autodoc.yml @@ -14,7 +14,7 @@ jobs: if: github.event.pull_request.merged == true steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Process markdown files run: | find . -name '*.md' | xargs perl tools/autodoc.pl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c56f59867..88684e141 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,13 +11,13 @@ jobs: strategy: matrix: go_tags: [ 'stdlib', 'goccy', 'es256k', 'secp256k1-pem', 'asmbase64', 'alltags'] - go: [ '1.22', '1.21', '1.20' ] + go: [ '1.23', '1.22', '1.21' ] name: "Test [ Go ${{ matrix.go }} / Tags ${{ matrix.go_tags }} ]" steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Cache Go modules - uses: actions/cache@v4 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: | ~/go/pkg/mod @@ -27,20 +27,10 @@ jobs: restore-keys: | ${{ runner.os }}-go- - name: Install Go stable version - if: matrix.go != 'tip' - uses: actions/setup-go@v5 + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version: ${{ matrix.go }} check-latest: true - - name: Install Go tip - if: matrix.go == 'tip' - run: | - git clone --depth=1 https://go.googlesource.com/go $HOME/gotip - cd $HOME/gotip/src - ./make.bash - echo "::set-env name=GOROOT::$HOME/gotip" - echo "::add-path::$HOME/gotip/bin" - echo "::add-path::$(go env GOPATH)/bin" - name: Install stringer run: go install golang.org/x/tools/cmd/stringer@latest - name: Install tparse @@ -52,11 +42,6 @@ jobs: run: make tidy - name: Test with coverage run: make cover-${{ matrix.go_tags }} - - name: Upload code coverage to codecov - if: matrix.go == '1.19' - uses: codecov/codecov-action@v4 - with: - file: ./coverage.out - uses: bazelbuild/setup-bazelisk@v3 - run: bazel run //:gazelle-update-repos - name: Check difference between generation code and commit code diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 489422b92..16580666c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -40,7 +40,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 12b44f305..7125642dc 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Install tparse run: go install github.com/mfridman/tparse@v0.12.2 - run: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5278cad4f..60df228ba 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,14 +5,11 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: - go-version: "1.20" - check-latest: true - - uses: golangci/golangci-lint-action@v6 - with: - version: v1.59 + go-version-file: "go.mod" + - uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0 - name: Run go vet run: | go vet ./... diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index c5892cbf1..2f6a9354d 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -14,16 +14,16 @@ jobs: strategy: matrix: go_tags: [ 'stdlib', 'goccy', 'es256k', 'alltags' ] - go: [ '1.22', '1.21', '1.20' ] + go: [ '1.22', '1.21' ] name: "Smoke [ Go ${{ matrix.go }} / Tags ${{ matrix.go_tags }} ]" steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Check documentation generator run: | find . -name '*.md' | xargs env AUTODOC_DRYRUN=1 perl tools/autodoc.pl - name: Cache Go modules - uses: actions/cache@v4 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.20 with: path: | ~/go/pkg/mod @@ -33,7 +33,7 @@ jobs: restore-keys: | ${{ runner.os }}-go- - name: Install Go stable version - uses: actions/setup-go@v5 + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version: ${{ matrix.go }} check-latest: true @@ -50,6 +50,6 @@ jobs: run: make tidy - name: Run smoke tests run: make smoke-${{ matrix.go_tags }} - - uses: bazelbuild/setup-bazelisk@v3 + - uses: bazelbuild/setup-bazelisk@b39c379c82683a5f25d34f0d062761f62693e0b2 # v3.0.0 - run: bazel build //... diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f2bee296c..1b8e5f5dd 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 with: stale-issue-message: 'This issue is stale because it has been open 14 days with no activity. Remove stale label or comment or this will be closed in 7 days.' stale-pr-message: 'This PR is stale because it has been open 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.' diff --git a/.golangci.yml b/.golangci.yml index f7f07deeb..b6cc945a2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -60,6 +60,9 @@ issues: text: "don't use an underscore in package name" linters: - revive + - linters: + - staticcheck + text: 'SA1019' - path: /*.go linters: - contextcheck diff --git a/Changes-v3.md b/Changes-v3.md index 68f5c8aa3..e95d27eff 100644 --- a/Changes-v3.md +++ b/Changes-v3.md @@ -28,6 +28,10 @@ These are changes that are incompatible with the v2.x.x version. ## JWK +* Experimental secp256k1 encoding/decoding for PEM encoded ASN.1 DER Format + has been removed. Instead, `jwk.PEMDecoder` and `jwk.PEMEncoder` have been + added to support those who want to perform non-standard PEM encoding/decoding + * Iterators have been completely removed. * `jwk/x25519` has been removed. To use X25519 keys, use `(crypto/ecdh).PrivateKey` and @@ -53,4 +57,4 @@ These are changes that are incompatible with the v2.x.x version. has been upgraded, and thus we no longer have a pool of workers that need to be controlled. * `jwk.Fetcher` has been clearly marked as something that has limited - usage for `jws.WithVerifyAuto` \ No newline at end of file + usage for `jws.WithVerifyAuto` diff --git a/jwe/jwe_test.go b/jwe/jwe_test.go index 26303dc77..af976f003 100644 --- a/jwe/jwe_test.go +++ b/jwe/jwe_test.go @@ -318,12 +318,12 @@ func TestRoundtrip_RSAES_OAEP_AES_GCM(t *testing.T) { 110, 97, 116, 105, 111, 110, 46, } - max := 100 + iterations := 100 if testing.Short() { - max = 1 + iterations = 1 } - for i := 0; i < max; i++ { + for i := 0; i < iterations; i++ { encrypted, err := jwe.Encrypt(plaintext, jwe.WithKey(jwa.RSA_OAEP, &rsaPrivKey.PublicKey)) if !assert.NoError(t, err, "Encrypt should succeed") { return @@ -346,12 +346,12 @@ func TestRoundtrip_RSA1_5_A128CBC_HS256(t *testing.T) { 112, 114, 111, 115, 112, 101, 114, 46, } - max := 100 + iterations := 100 if testing.Short() { - max = 1 + iterations = 1 } - for i := 0; i < max; i++ { + for i := 0; i < iterations; i++ { encrypted, err := jwe.Encrypt(plaintext, jwe.WithKey(jwa.RSA1_5, &rsaPrivKey.PublicKey), jwe.WithContentEncryption(jwa.A128CBC_HS256)) if !assert.NoError(t, err, "Encrypt is successful") { return @@ -379,12 +379,12 @@ func TestEncode_A128KW_A128CBC_HS256(t *testing.T) { 25, 172, 32, 130, 225, 114, 26, 181, 138, 106, 254, 192, 95, 133, 74, 82, } - max := 100 + iterations := 100 if testing.Short() { - max = 1 + iterations = 1 } - for i := 0; i < max; i++ { + for i := 0; i < iterations; i++ { encrypted, err := jwe.Encrypt(plaintext, jwe.WithKey(jwa.A128KW, sharedkey), jwe.WithContentEncryption(jwa.A128CBC_HS256)) if !assert.NoError(t, err, "Encrypt is successful") { return diff --git a/jwk/BUILD.bazel b/jwk/BUILD.bazel index f69c15994..1cb0b0563 100644 --- a/jwk/BUILD.bazel +++ b/jwk/BUILD.bazel @@ -25,6 +25,7 @@ go_library( "symmetric_gen.go", "usage.go", "whitelist.go", + "x509.go", ], importpath = "github.com/lestrrat-go/jwx/v3/jwk", visibility = ["//visibility:public"], @@ -35,8 +36,6 @@ go_library( "//internal/json", "//internal/pool", "//jwa", - "//x25519", - "//jwk/internal/x509", "//jwk/ecdsa", "@com_github_lestrrat_go_blackmagic//:blackmagic", "@com_github_lestrrat_go_httprc_v2//:httprc", @@ -66,8 +65,6 @@ go_test( "//jwa", "//jwk/ecdsa", "//jws", - "//x25519", - "//jwk/internal/x509", "@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//require", ], diff --git a/jwk/es256k_go1.20_test.go b/jwk/es256k_go1.20_test.go deleted file mode 100644 index 116b4be64..000000000 --- a/jwk/es256k_go1.20_test.go +++ /dev/null @@ -1,44 +0,0 @@ -//go:build jwx_es256k && jwx_secp256k1_pem && go1.20 - -package jwk_test - -import ( - "fmt" - "testing" - - "github.com/decred/dcrd/dcrec/secp256k1/v4" - "github.com/lestrrat-go/jwx/v3/jwk" - "github.com/stretchr/testify/require" -) - -func TestES256KPem(t *testing.T) { - raw, err := secp256k1.GeneratePrivateKey() - require.NoError(t, err, `GeneratePrivateKey should succeed`) - - testcases := []interface{}{raw.ToECDSA(), raw.PubKey().ToECDSA()} - for _, tc := range testcases { - t.Run(fmt.Sprintf("Marshal %T", tc), func(t *testing.T) { - key, err := jwk.FromRaw(tc) - require.NoError(t, err, `FromRaw should succeed`) - - pem, err := jwk.Pem(key) - require.NoError(t, err, `Pem should succeed`) - require.NotEmpty(t, pem, `Pem should not be empty`) - - parsed, err := jwk.Parse(pem, jwk.WithPEM(true)) - require.NoError(t, err, `Parse should succeed`) - _ = parsed - }) - } - - t.Run("ParsePKCS8PrivateKey", func(t *testing.T) { - const src = `-----BEGIN PRIVATE KEY----- -MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQggS9t6iYyj9JSL+btkMEq -pMYitWV4X+/Jg9zu3L8Ob5ShRANCAAT/YrxWHfw3e8lfDncJLLkPRbdby0L4qT95 -vyWU5lPpSwRbEAfSFR1E5RD9irkN1mCY8D1ko1PAlmHVB78pNzq4 ------END PRIVATE KEY-----` - key, err := jwk.Parse([]byte(src), jwk.WithPEM(true)) - require.NoError(t, err, `Parse should succeed`) - require.NotNil(t, key, `key should not be nil`) - }) -} diff --git a/jwk/internal/x509/BUILD.bazel b/jwk/internal/x509/BUILD.bazel deleted file mode 100644 index 6444808f8..000000000 --- a/jwk/internal/x509/BUILD.bazel +++ /dev/null @@ -1,14 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") - -go_library( - name = "x509", - srcs = [ "x509.go", "x509_nosecp256k1.go", "x509_sepc256k1.go" ], - importpath = "github.com/lestrrat-go/jwx/v3/jwk/internal/x509", - visibility = ["//:__subpackages__"], -) - -alias( - name = "go_default_library", - actual = ":x509", - visibility = ["//jwe:__subpackages__"], -) diff --git a/jwk/internal/x509/x509.go b/jwk/internal/x509/x509.go deleted file mode 100644 index 383203d76..000000000 --- a/jwk/internal/x509/x509.go +++ /dev/null @@ -1,31 +0,0 @@ -package x509 - -import ( - "crypto/rsa" - "crypto/x509" -) - -// In this x509 package we provide a proxy for crypto/x509 methods, -// so that we can easily swap out the ParseECPrivateKey method with -// our version of it that recognizes the secp256k1 curve... -// _IF_ the jwx_es256k build tag is set. - -func MarshalPKCS1PrivateKey(priv *rsa.PrivateKey) []byte { - return x509.MarshalPKCS1PrivateKey(priv) -} - -func MarshalPKCS8PrivateKey(priv interface{}) ([]byte, error) { - return x509.MarshalPKCS8PrivateKey(priv) -} - -func ParsePKCS1PrivateKey(der []byte) (*rsa.PrivateKey, error) { - return x509.ParsePKCS1PrivateKey(der) -} - -func ParsePKCS1PublicKey(der []byte) (*rsa.PublicKey, error) { - return x509.ParsePKCS1PublicKey(der) -} - -func ParseCertificate(der []byte) (*x509.Certificate, error) { - return x509.ParseCertificate(der) -} diff --git a/jwk/internal/x509/x509_nosecp256k1.go b/jwk/internal/x509/x509_nosecp256k1.go deleted file mode 100644 index efcd6cfe1..000000000 --- a/jwk/internal/x509/x509_nosecp256k1.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build !jwx_es256k || !jwx_secp256k1_pem || !go1.20 - -package x509 - -import ( - "crypto/ecdsa" - "crypto/x509" -) - -func MarshalECPrivateKey(priv *ecdsa.PrivateKey) ([]byte, error) { - return x509.MarshalECPrivateKey(priv) -} - -func ParseECPrivateKey(der []byte) (*ecdsa.PrivateKey, error) { - return x509.ParseECPrivateKey(der) -} - -func MarshalPKIXPublicKey(pub any) ([]byte, error) { - return x509.MarshalPKIXPublicKey(pub) -} - -func ParsePKIXPublicKey(der []byte) (any, error) { - return x509.ParsePKIXPublicKey(der) -} - -func ParsePKCS8PrivateKey(der []byte) (interface{}, error) { - return x509.ParsePKCS8PrivateKey(der) -} diff --git a/jwk/internal/x509/x509_sepc256k1.go b/jwk/internal/x509/x509_sepc256k1.go deleted file mode 100644 index 248ab558e..000000000 --- a/jwk/internal/x509/x509_sepc256k1.go +++ /dev/null @@ -1,479 +0,0 @@ -//go:build jwx_es256k && jwx_secp256k1_pem && go1.20 - -package x509 - -import ( - "bytes" - "crypto/dsa" - "crypto/ecdh" - "crypto/ecdsa" - "crypto/ed25519" - "crypto/elliptic" - "crypto/rsa" - "crypto/x509/pkix" - "encoding/asn1" - "errors" - "fmt" - "math/big" - - "github.com/decred/dcrd/dcrec/secp256k1/v4" - "golang.org/x/crypto/cryptobyte" - cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1" -) - -// See src/crypto/x509/sec1.go for original code -const ecPrivKeyVersion = 1 - -// See src/crypto/x509/x509.go for original code -type publicKeyInfo struct { - Raw asn1.RawContent - Algorithm pkix.AlgorithmIdentifier - PublicKey asn1.BitString -} - -// See src/crypto/x509/pkcs1.go for original code -type pkcs1PrivateKey struct { - Version int - N *big.Int - E int - D *big.Int - P *big.Int - Q *big.Int - // We ignore these values, if present, because rsa will calculate them. - Dp *big.Int `asn1:"optional"` - Dq *big.Int `asn1:"optional"` - Qinv *big.Int `asn1:"optional"` - - AdditionalPrimes []pkcs1AdditionalRSAPrime `asn1:"optional,omitempty"` -} - -// See src/crypto/x509/pkcs1.go for original code -type pkcs1AdditionalRSAPrime struct { - Prime *big.Int - - // We ignore these values because rsa will calculate them. - Exp *big.Int - Coeff *big.Int -} - -// See src/crypto/x509/pkcs1.go for original code -type pkcs1PublicKey struct { - N *big.Int - E int -} - -// See src/crypto/x509/pkcs8.go for original code -type pkcs8 struct { - Version int - Algo pkix.AlgorithmIdentifier - PrivateKey []byte - // optional attributes omitted. -} - -// See src/crypto/x509/x509.go for original code -type pkixPublicKey struct { - Algo pkix.AlgorithmIdentifier - BitString asn1.BitString -} - -type ecPrivateKey struct { - Version int - PrivateKey []byte - NamedCurveOID asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"` - PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"` -} - -var ( - // See src/crypto/x509/x509.go for original code - oidNamedCurveP224 = asn1.ObjectIdentifier{1, 3, 132, 0, 33} - oidNamedCurveP256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7} - oidNamedCurveP384 = asn1.ObjectIdentifier{1, 3, 132, 0, 34} - oidNamedCurveP521 = asn1.ObjectIdentifier{1, 3, 132, 0, 35} - oidPublicKeyRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} - oidPublicKeyDSA = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 1} - oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1} - oidPublicKeyX25519 = asn1.ObjectIdentifier{1, 3, 101, 110} - oidPublicKeyEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112} - // Added for this package - oidNamedCurveSecp256K1 = asn1.ObjectIdentifier{1, 3, 132, 0, 10} -) - -// See src/crypto/x509/x509.go for original code -func namedCurveFromOID(oid asn1.ObjectIdentifier) elliptic.Curve { - switch { - case oid.Equal(oidNamedCurveP224): - return elliptic.P224() - case oid.Equal(oidNamedCurveP256): - return elliptic.P256() - case oid.Equal(oidNamedCurveP384): - return elliptic.P384() - case oid.Equal(oidNamedCurveP521): - return elliptic.P521() - case oid.Equal(oidNamedCurveSecp256K1): - // Added for this package - return secp256k1.S256() - } - return nil -} - -// See src/crypto/x509/x509.go for original code -func oidFromNamedCurve(curve elliptic.Curve) (asn1.ObjectIdentifier, bool) { - switch curve { - case elliptic.P224(): - return oidNamedCurveP224, true - case elliptic.P256(): - return oidNamedCurveP256, true - case elliptic.P384(): - return oidNamedCurveP384, true - case elliptic.P521(): - return oidNamedCurveP521, true - case secp256k1.S256(): - return oidNamedCurveSecp256K1, true - } - - return nil, false -} - -// See crypto/x509/x509.go for original code -func oidFromECDHCurve(curve ecdh.Curve) (asn1.ObjectIdentifier, bool) { - switch curve { - case ecdh.X25519(): - return oidPublicKeyX25519, true - case ecdh.P256(): - return oidNamedCurveP256, true - case ecdh.P384(): - return oidNamedCurveP384, true - case ecdh.P521(): - return oidNamedCurveP521, true - } - - return nil, false -} - -func MarshalECPrivateKey(key *ecdsa.PrivateKey) ([]byte, error) { - // See src/crypto/x509/sec1.go for original code - oid, ok := oidFromNamedCurve(key.Curve) - if !ok { - return nil, errors.New("x509: unknown elliptic curve") - } - privateKey := make([]byte, (key.Curve.Params().N.BitLen()+7)/8) - return asn1.Marshal(ecPrivateKey{ - Version: 1, - PrivateKey: key.D.FillBytes(privateKey), - NamedCurveOID: oid, - PublicKey: asn1.BitString{Bytes: elliptic.Marshal(key.Curve, key.X, key.Y)}, - }) -} - -func ParseECPrivateKey(der []byte) (*ecdsa.PrivateKey, error) { - return parseECPrivateKey(nil, der) -} - -func parseECPrivateKey(namedCurveOID *asn1.ObjectIdentifier, der []byte) (*ecdsa.PrivateKey, error) { - var privKey ecPrivateKey - if _, err := asn1.Unmarshal(der, &privKey); err != nil { - if _, err := asn1.Unmarshal(der, &pkcs8{}); err == nil { - return nil, errors.New("x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format)") - } - if _, err := asn1.Unmarshal(der, &pkcs1PrivateKey{}); err == nil { - return nil, errors.New("x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)") - } - return nil, errors.New("x509: failed to parse EC private key: " + err.Error()) - } - if privKey.Version != ecPrivKeyVersion { - return nil, fmt.Errorf("x509: unknown EC private key version %d", privKey.Version) - } - - var curve elliptic.Curve - if namedCurveOID != nil { - curve = namedCurveFromOID(*namedCurveOID) - } else { - curve = namedCurveFromOID(privKey.NamedCurveOID) - } - if curve == nil { - return nil, errors.New("x509: unknown elliptic curve") - } - - k := new(big.Int).SetBytes(privKey.PrivateKey) - curveOrder := curve.Params().N - if k.Cmp(curveOrder) >= 0 { - return nil, errors.New("x509: invalid elliptic curve private key value") - } - priv := new(ecdsa.PrivateKey) - priv.Curve = curve - priv.D = k - - privateKey := make([]byte, (curveOrder.BitLen()+7)/8) - - // Some private keys have leading zero padding. This is invalid - // according to [SEC1], but this code will ignore it. - for len(privKey.PrivateKey) > len(privateKey) { - if privKey.PrivateKey[0] != 0 { - return nil, errors.New("x509: invalid private key length") - } - privKey.PrivateKey = privKey.PrivateKey[1:] - } - - // Some private keys remove all leading zeros, this is also invalid - // according to [SEC1] but since OpenSSL used to do this, we ignore - // this too. - copy(privateKey[len(privateKey)-len(privKey.PrivateKey):], privKey.PrivateKey) - priv.X, priv.Y = curve.ScalarBaseMult(privateKey) - - return priv, nil -} - -// See src/crypto/x509/x509.go for original code -func marshalPublicKey(pub any) (publicKeyBytes []byte, publicKeyAlgorithm pkix.AlgorithmIdentifier, err error) { - switch pub := pub.(type) { - case *rsa.PublicKey: - publicKeyBytes, err = asn1.Marshal(pkcs1PublicKey{ - N: pub.N, - E: pub.E, - }) - if err != nil { - return nil, pkix.AlgorithmIdentifier{}, err - } - publicKeyAlgorithm.Algorithm = oidPublicKeyRSA - // This is a NULL parameters value which is required by - // RFC 3279, Section 2.3.1. - publicKeyAlgorithm.Parameters = asn1.NullRawValue - case *ecdsa.PublicKey: - oid, ok := oidFromNamedCurve(pub.Curve) - if !ok { - return nil, pkix.AlgorithmIdentifier{}, errors.New("x509: unsupported elliptic curve") - } - if !pub.Curve.IsOnCurve(pub.X, pub.Y) { - return nil, pkix.AlgorithmIdentifier{}, errors.New("x509: invalid elliptic curve public key") - } - publicKeyBytes = elliptic.Marshal(pub.Curve, pub.X, pub.Y) - publicKeyAlgorithm.Algorithm = oidPublicKeyECDSA - var paramBytes []byte - paramBytes, err = asn1.Marshal(oid) - if err != nil { - return - } - publicKeyAlgorithm.Parameters.FullBytes = paramBytes - case ed25519.PublicKey: - publicKeyBytes = pub - publicKeyAlgorithm.Algorithm = oidPublicKeyEd25519 - case *ecdh.PublicKey: - publicKeyBytes = pub.Bytes() - if pub.Curve() == ecdh.X25519() { - publicKeyAlgorithm.Algorithm = oidPublicKeyX25519 - } else { - oid, ok := oidFromECDHCurve(pub.Curve()) - if !ok { - return nil, pkix.AlgorithmIdentifier{}, errors.New("x509: unsupported elliptic curve") - } - publicKeyAlgorithm.Algorithm = oidPublicKeyECDSA - var paramBytes []byte - paramBytes, err = asn1.Marshal(oid) - if err != nil { - return - } - publicKeyAlgorithm.Parameters.FullBytes = paramBytes - } - default: - return nil, pkix.AlgorithmIdentifier{}, fmt.Errorf("x509: unsupported public key type: %T", pub) - } - - return publicKeyBytes, publicKeyAlgorithm, nil -} - -// See src/crypto/x509/x509.go for original code -func MarshalPKIXPublicKey(pub any) ([]byte, error) { - var publicKeyBytes []byte - var publicKeyAlgorithm pkix.AlgorithmIdentifier - var err error - - if publicKeyBytes, publicKeyAlgorithm, err = marshalPublicKey(pub); err != nil { - return nil, err - } - - pkix := pkixPublicKey{ - Algo: publicKeyAlgorithm, - BitString: asn1.BitString{ - Bytes: publicKeyBytes, - BitLength: 8 * len(publicKeyBytes), - }, - } - - ret, _ := asn1.Marshal(pkix) - return ret, nil -} - -func ParsePKIXPublicKey(derBytes []byte) (pub any, err error) { - var pki publicKeyInfo - if rest, err := asn1.Unmarshal(derBytes, &pki); err != nil { - if _, err := asn1.Unmarshal(derBytes, &pkcs1PublicKey{}); err == nil { - return nil, errors.New("x509: failed to parse public key (use ParsePKCS1PublicKey instead for this key format)") - } - return nil, err - } else if len(rest) != 0 { - return nil, errors.New("x509: trailing data after ASN.1 of public-key") - } - return parsePublicKey(&pki) -} - -func parsePublicKey(keyData *publicKeyInfo) (any, error) { - oid := keyData.Algorithm.Algorithm - params := keyData.Algorithm.Parameters - der := cryptobyte.String(keyData.PublicKey.RightAlign()) - switch { - case oid.Equal(oidPublicKeyRSA): - // RSA public keys must have a NULL in the parameters. - // See RFC 3279, Section 2.3.1. - if !bytes.Equal(params.FullBytes, asn1.NullBytes) { - return nil, errors.New("x509: RSA key missing NULL parameters") - } - - p := &pkcs1PublicKey{N: new(big.Int)} - if !der.ReadASN1(&der, cryptobyte_asn1.SEQUENCE) { - return nil, errors.New("x509: invalid RSA public key") - } - if !der.ReadASN1Integer(p.N) { - return nil, errors.New("x509: invalid RSA modulus") - } - if !der.ReadASN1Integer(&p.E) { - return nil, errors.New("x509: invalid RSA public exponent") - } - - if p.N.Sign() <= 0 { - return nil, errors.New("x509: RSA modulus is not a positive number") - } - if p.E <= 0 { - return nil, errors.New("x509: RSA public exponent is not a positive number") - } - - pub := &rsa.PublicKey{ - E: p.E, - N: p.N, - } - return pub, nil - case oid.Equal(oidPublicKeyECDSA): - paramsDer := cryptobyte.String(params.FullBytes) - namedCurveOID := new(asn1.ObjectIdentifier) - if !paramsDer.ReadASN1ObjectIdentifier(namedCurveOID) { - return nil, errors.New("x509: invalid ECDSA parameters") - } - namedCurve := namedCurveFromOID(*namedCurveOID) - if namedCurve == nil { - return nil, errors.New("x509: unsupported elliptic curve") - } - x, y := elliptic.Unmarshal(namedCurve, der) - if x == nil { - return nil, errors.New("x509: failed to unmarshal elliptic curve point") - } - pub := &ecdsa.PublicKey{ - Curve: namedCurve, - X: x, - Y: y, - } - return pub, nil - case oid.Equal(oidPublicKeyEd25519): - // RFC 8410, Section 3 - // > For all of the OIDs, the parameters MUST be absent. - if len(params.FullBytes) != 0 { - return nil, errors.New("x509: Ed25519 key encoded with illegal parameters") - } - if len(der) != ed25519.PublicKeySize { - return nil, errors.New("x509: wrong Ed25519 public key size") - } - return ed25519.PublicKey(der), nil - case oid.Equal(oidPublicKeyX25519): - // RFC 8410, Section 3 - // > For all of the OIDs, the parameters MUST be absent. - if len(params.FullBytes) != 0 { - return nil, errors.New("x509: X25519 key encoded with illegal parameters") - } - return ecdh.X25519().NewPublicKey(der) - case oid.Equal(oidPublicKeyDSA): - y := new(big.Int) - if !der.ReadASN1Integer(y) { - return nil, errors.New("x509: invalid DSA public key") - } - pub := &dsa.PublicKey{ - Y: y, - Parameters: dsa.Parameters{ - P: new(big.Int), - Q: new(big.Int), - G: new(big.Int), - }, - } - paramsDer := cryptobyte.String(params.FullBytes) - if !paramsDer.ReadASN1(¶msDer, cryptobyte_asn1.SEQUENCE) || - !paramsDer.ReadASN1Integer(pub.Parameters.P) || - !paramsDer.ReadASN1Integer(pub.Parameters.Q) || - !paramsDer.ReadASN1Integer(pub.Parameters.G) { - return nil, errors.New("x509: invalid DSA parameters") - } - if pub.Y.Sign() <= 0 || pub.Parameters.P.Sign() <= 0 || - pub.Parameters.Q.Sign() <= 0 || pub.Parameters.G.Sign() <= 0 { - return nil, errors.New("x509: zero or negative DSA parameter") - } - return pub, nil - default: - return nil, errors.New("x509: unknown public key algorithm") - } -} - -// See src/crypto/x509/pkcs8.go for original code -func ParsePKCS8PrivateKey(der []byte) (key any, err error) { - var privKey pkcs8 - if _, err := asn1.Unmarshal(der, &privKey); err != nil { - if _, err := asn1.Unmarshal(der, &ecPrivateKey{}); err == nil { - return nil, errors.New("x509: failed to parse private key (use ParseECPrivateKey instead for this key format)") - } - if _, err := asn1.Unmarshal(der, &pkcs1PrivateKey{}); err == nil { - return nil, errors.New("x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)") - } - return nil, err - } - switch { - case privKey.Algo.Algorithm.Equal(oidPublicKeyRSA): - key, err = ParsePKCS1PrivateKey(privKey.PrivateKey) - if err != nil { - return nil, errors.New("x509: failed to parse RSA private key embedded in PKCS#8: " + err.Error()) - } - return key, nil - - case privKey.Algo.Algorithm.Equal(oidPublicKeyECDSA): - bytes := privKey.Algo.Parameters.FullBytes - namedCurveOID := new(asn1.ObjectIdentifier) - if _, err := asn1.Unmarshal(bytes, namedCurveOID); err != nil { - namedCurveOID = nil - } - key, err = parseECPrivateKey(namedCurveOID, privKey.PrivateKey) - if err != nil { - return nil, errors.New("x509: failed to parse EC private key embedded in PKCS#8: " + err.Error()) - } - return key, nil - - case privKey.Algo.Algorithm.Equal(oidPublicKeyEd25519): - if l := len(privKey.Algo.Parameters.FullBytes); l != 0 { - return nil, errors.New("x509: invalid Ed25519 private key parameters") - } - var curvePrivateKey []byte - if _, err := asn1.Unmarshal(privKey.PrivateKey, &curvePrivateKey); err != nil { - return nil, fmt.Errorf("x509: invalid Ed25519 private key: %v", err) - } - if l := len(curvePrivateKey); l != ed25519.SeedSize { - return nil, fmt.Errorf("x509: invalid Ed25519 private key length: %d", l) - } - return ed25519.NewKeyFromSeed(curvePrivateKey), nil - - case privKey.Algo.Algorithm.Equal(oidPublicKeyX25519): - if l := len(privKey.Algo.Parameters.FullBytes); l != 0 { - return nil, errors.New("x509: invalid X25519 private key parameters") - } - var curvePrivateKey []byte - if _, err := asn1.Unmarshal(privKey.PrivateKey, &curvePrivateKey); err != nil { - return nil, fmt.Errorf("x509: invalid X25519 private key: %v", err) - } - return ecdh.X25519().NewPrivateKey(curvePrivateKey) - - default: - return nil, fmt.Errorf("x509: PKCS#8 wrapping contained private key with unknown algorithm: %v", privKey.Algo.Algorithm) - } -} diff --git a/jwk/jwk.go b/jwk/jwk.go index 149cea50d..59c0c3c4f 100644 --- a/jwk/jwk.go +++ b/jwk/jwk.go @@ -6,8 +6,6 @@ import ( "bytes" "crypto" "crypto/ecdsa" - "crypto/ed25519" - "crypto/rsa" "crypto/x509" "encoding/pem" "errors" @@ -180,134 +178,6 @@ func PublicRawKeyOf(v interface{}) (interface{}, error) { return raw, nil } -const ( - pmPrivateKey = `PRIVATE KEY` - pmPublicKey = `PUBLIC KEY` - pmECPrivateKey = `EC PRIVATE KEY` - pmRSAPublicKey = `RSA PUBLIC KEY` - pmRSAPrivateKey = `RSA PRIVATE KEY` -) - -// EncodeX509 encodes the key into a byte sequence in ASN.1 DER format -// suitable for to be PEM encoded. The key can be a jwk.Key or a raw key -// instance, but it must be one of the types supported by `x509` package. -// -// This function will try to do the right thing depending on the key type -// (i.e. switch between `x509.MarshalPKCS1PrivateKey` and `x509.MarshalECPrivateKey`), -// but for public keys, it will always use `x509.MarshalPKIXPublicKey`. -// Please manually perform the encoding if you need more fine-grained control -// -// The first return value is the name that can be used for `(pem.Block).Type`. -// The second return value is the encoded byte sequence. -func EncodeX509(v interface{}) (string, []byte, error) { - // we can't import jwk, so just use the interface - if key, ok := v.(Key); ok { - var raw interface{} - if err := Export(key, &raw); err != nil { - return "", nil, fmt.Errorf(`failed to get raw key out of %T: %w`, key, err) - } - - v = raw - } - - // Try to convert it into a certificate - switch v := v.(type) { - case *rsa.PrivateKey: - return pmRSAPrivateKey, x509.MarshalPKCS1PrivateKey(v), nil - case *ecdsa.PrivateKey: - marshaled, err := x509.MarshalECPrivateKey(v) - if err != nil { - return "", nil, err - } - return pmECPrivateKey, marshaled, nil - case ed25519.PrivateKey: - marshaled, err := x509.MarshalPKCS8PrivateKey(v) - if err != nil { - return "", nil, err - } - return pmPrivateKey, marshaled, nil - case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: - marshaled, err := x509.MarshalPKIXPublicKey(v) - if err != nil { - return "", nil, err - } - return pmPublicKey, marshaled, nil - default: - return "", nil, fmt.Errorf(`unsupported type %T for ASN.1 DER encoding`, v) - } -} - -// EncodePEM encodes the key into a PEM encoded ASN.1 DER format. -// The key can be a jwk.Key or a raw key instance, but it must be one of -// the types supported by `x509` package. -// -// Internally, it uses the same routine as `jwk.EncodeX509()`, and therefore -// the same caveats apply -func EncodePEM(v interface{}) ([]byte, error) { - typ, marshaled, err := EncodeX509(v) - if err != nil { - return nil, fmt.Errorf(`failed to encode key in x509: %w`, err) - } - - block := &pem.Block{ - Type: typ, - Bytes: marshaled, - } - return pem.EncodeToMemory(block), nil -} - -// DecodePEM decodes a key in PEM encoded ASN.1 DER format. -// and returns a raw key -func DecodePEM(src []byte) (interface{}, []byte, error) { - block, rest := pem.Decode(src) - if block == nil { - return nil, nil, fmt.Errorf(`failed to decode PEM data`) - } - - switch block.Type { - // Handle the semi-obvious cases - case pmRSAPrivateKey: - key, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil, nil, fmt.Errorf(`failed to parse PKCS1 private key: %w`, err) - } - return key, rest, nil - case pmRSAPublicKey: - key, err := x509.ParsePKCS1PublicKey(block.Bytes) - if err != nil { - return nil, nil, fmt.Errorf(`failed to parse PKCS1 public key: %w`, err) - } - return key, rest, nil - case pmECPrivateKey: - key, err := x509.ParseECPrivateKey(block.Bytes) - if err != nil { - return nil, nil, fmt.Errorf(`failed to parse EC private key: %w`, err) - } - return key, rest, nil - case pmPublicKey: - // XXX *could* return dsa.PublicKey - key, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return nil, nil, fmt.Errorf(`failed to parse PKIX public key: %w`, err) - } - return key, rest, nil - case pmPrivateKey: - key, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - return nil, nil, fmt.Errorf(`failed to parse PKCS8 private key: %w`, err) - } - return key, rest, nil - case "CERTIFICATE": - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, nil, fmt.Errorf(`failed to parse certificate: %w`, err) - } - return cert.PublicKey, rest, nil - default: - return nil, nil, fmt.Errorf(`invalid PEM block type %s`, block.Type) - } -} - // ParseRawKey is a combination of ParseKey and Raw. It parses a single JWK key, // and assigns the "raw" key to the given parameter. The key must either be // a pointer to an empty interface, or a pointer to the actual raw key type @@ -348,11 +218,14 @@ func (ctx *setDecodeCtx) IgnoreParseError() bool { func ParseKey(data []byte, options ...ParseOption) (Key, error) { var parsePEM bool var localReg *json.Registry + var pemDecoder PEMDecoder for _, option := range options { //nolint:forcetypeassert switch option.Ident() { case identPEM{}: parsePEM = option.Value().(bool) + case identPEMDecoder{}: + pemDecoder = option.Value().(PEMDecoder) case identLocalRegistry{}: // in reality you can only pass either withLocalRegistry or // WithTypedField, but since withLocalRegistry is used only by us, @@ -370,7 +243,10 @@ func ParseKey(data []byte, options ...ParseOption) (Key, error) { } if parsePEM { - raw, _, err := DecodePEM(data) + if pemDecoder == nil { + pemDecoder = NewPEMDecoder() + } + raw, _, err := pemDecoder.Decode(data) if err != nil { return nil, fmt.Errorf(`failed to parse PEM encoded key: %w`, err) } @@ -423,11 +299,14 @@ func Parse(src []byte, options ...ParseOption) (Set, error) { var parsePEM bool var localReg *json.Registry var ignoreParseError bool + var pemDecoder PEMDecoder for _, option := range options { //nolint:forcetypeassert switch option.Ident() { case identPEM{}: parsePEM = option.Value().(bool) + case identPEMDecoder{}: + pemDecoder = option.Value().(PEMDecoder) case identIgnoreParseError{}: ignoreParseError = option.Value().(bool) case identTypedField{}: @@ -442,9 +321,12 @@ func Parse(src []byte, options ...ParseOption) (Set, error) { s := NewSet() if parsePEM { + if pemDecoder == nil { + pemDecoder = NewPEMDecoder() + } src = bytes.TrimSpace(src) for len(src) > 0 { - raw, rest, err := DecodePEM(src) + raw, rest, err := pemDecoder.Decode(src) if err != nil { return nil, fmt.Errorf(`failed to parse PEM encoded key: %w`, err) } @@ -603,7 +485,7 @@ func asnEncode(key Key) (string, []byte, error) { switch key := key.(type) { case ECDSAPrivateKey: var rawkey ecdsa.PrivateKey - if err := key.Raw(&rawkey); err != nil { + if err := Export(key, &rawkey); err != nil { return "", nil, fmt.Errorf(`failed to get raw key from jwk.Key: %w`, err) } buf, err := x509.MarshalECPrivateKey(&rawkey) diff --git a/jwk/jwk_test.go b/jwk/jwk_test.go index 0a6dfe721..fbf8a02f2 100644 --- a/jwk/jwk_test.go +++ b/jwk/jwk_test.go @@ -857,35 +857,35 @@ func TestPublicKeyOf(t *testing.T) { }{ { Key: rsakey, - PublicKeyType: reflect.PtrTo(reflect.TypeOf(rsakey.PublicKey)), + PublicKeyType: reflect.PointerTo(reflect.TypeOf(rsakey.PublicKey)), }, { Key: *rsakey, - PublicKeyType: reflect.PtrTo(reflect.TypeOf(rsakey.PublicKey)), + PublicKeyType: reflect.PointerTo(reflect.TypeOf(rsakey.PublicKey)), }, { Key: rsakey.PublicKey, - PublicKeyType: reflect.PtrTo(reflect.TypeOf(rsakey.PublicKey)), + PublicKeyType: reflect.PointerTo(reflect.TypeOf(rsakey.PublicKey)), }, { Key: &rsakey.PublicKey, - PublicKeyType: reflect.PtrTo(reflect.TypeOf(rsakey.PublicKey)), + PublicKeyType: reflect.PointerTo(reflect.TypeOf(rsakey.PublicKey)), }, { Key: ecdsakey, - PublicKeyType: reflect.PtrTo(reflect.TypeOf(ecdsakey.PublicKey)), + PublicKeyType: reflect.PointerTo(reflect.TypeOf(ecdsakey.PublicKey)), }, { Key: *ecdsakey, - PublicKeyType: reflect.PtrTo(reflect.TypeOf(ecdsakey.PublicKey)), + PublicKeyType: reflect.PointerTo(reflect.TypeOf(ecdsakey.PublicKey)), }, { Key: ecdsakey.PublicKey, - PublicKeyType: reflect.PtrTo(reflect.TypeOf(ecdsakey.PublicKey)), + PublicKeyType: reflect.PointerTo(reflect.TypeOf(ecdsakey.PublicKey)), }, { Key: &ecdsakey.PublicKey, - PublicKeyType: reflect.PtrTo(reflect.TypeOf(ecdsakey.PublicKey)), + PublicKeyType: reflect.PointerTo(reflect.TypeOf(ecdsakey.PublicKey)), }, { Key: octets, @@ -1669,9 +1669,9 @@ func TestTypedFields(t *testing.T) { func TestGH412(t *testing.T) { base := jwk.NewSet() - const max = 5 + const iterations = 5 kids := make(map[string]struct{}) - for i := 0; i < max; i++ { + for i := 0; i < iterations; i++ { k, err := jwxtest.GenerateRsaJwk() if !assert.NoError(t, err, `jwxttest.GenerateRsaJwk() should succeed`) { return @@ -1683,7 +1683,7 @@ func TestGH412(t *testing.T) { kids[kid] = struct{}{} } - for i := 0; i < max; i++ { + for i := 0; i < iterations; i++ { idx := i currentKid := "key-" + strconv.Itoa(i) t.Run(fmt.Sprintf("Remove at position %d", i), func(t *testing.T) { @@ -1692,7 +1692,7 @@ func TestGH412(t *testing.T) { return } - if !assert.Equal(t, max, set.Len(), `set.Len should be %d`, max) { + if !assert.Equal(t, iterations, set.Len(), `set.Len should be %d`, iterations) { return } @@ -1706,7 +1706,7 @@ func TestGH412(t *testing.T) { } t.Logf("deleted key %s", k.KeyID()) - if !assert.Equal(t, max-1, set.Len(), `set.Len should be %d`, max-1) { + if !assert.Equal(t, iterations-1, set.Len(), `set.Len should be %d`, iterations-1) { return } diff --git a/jwk/options.yaml b/jwk/options.yaml index 7687be34a..c678ba0d0 100644 --- a/jwk/options.yaml +++ b/jwk/options.yaml @@ -82,6 +82,12 @@ options: interface: ParseOption argument_type: bool comment: WithPEM specifies that the input to `Parse()` is a PEM encoded key. + - ident: PEMDecoder + interface: ParseOption + argument_type: PEMDecoder + comment: | + WithPEMDecoder specifies the PEMDecoder object to use when decoding + PEM encoded keys. This option can be passed to `jwk.Parse()` - ident: FetchWhitelist interface: FetchOption argument_type: Whitelist diff --git a/jwk/options_gen.go b/jwk/options_gen.go index aff89e607..0cfb1ee1e 100644 --- a/jwk/options_gen.go +++ b/jwk/options_gen.go @@ -109,6 +109,7 @@ type identIgnoreParseError struct{} type identLocalRegistry struct{} type identMinRefreshInterval struct{} type identPEM struct{} +type identPEMDecoder struct{} type identPostFetcher struct{} type identRefreshInterval struct{} type identRefreshWindow struct{} @@ -146,6 +147,10 @@ func (identPEM) String() string { return "WithPEM" } +func (identPEMDecoder) String() string { + return "WithPEMDecoder" +} + func (identPostFetcher) String() string { return "WithPostFetcher" } @@ -244,6 +249,12 @@ func WithPEM(v bool) ParseOption { return &parseOption{option.New(identPEM{}, v)} } +// WithPEMDecoder specifies the PEMDecoder object to use when decoding +// PEM encoded keys. This option can be passed to `jwk.Parse()` +func WithPEMDecoder(v PEMDecoder) ParseOption { + return &parseOption{option.New(identPEMDecoder{}, v)} +} + // WithPostFetcher specifies the PostFetcher object to be used on the // jwk.Set object obtained in `jwk.Cache`. This option can be used // to, for example, modify the jwk.Set to give it key IDs or algorithm diff --git a/jwk/options_gen_test.go b/jwk/options_gen_test.go index 2c7df3685..2131cf30c 100644 --- a/jwk/options_gen_test.go +++ b/jwk/options_gen_test.go @@ -17,6 +17,7 @@ func TestOptionIdent(t *testing.T) { require.Equal(t, "withLocalRegistry", identLocalRegistry{}.String()) require.Equal(t, "WithMinRefreshInterval", identMinRefreshInterval{}.String()) require.Equal(t, "WithPEM", identPEM{}.String()) + require.Equal(t, "WithPEMDecoder", identPEMDecoder{}.String()) require.Equal(t, "WithPostFetcher", identPostFetcher{}.String()) require.Equal(t, "WithRefreshInterval", identRefreshInterval{}.String()) require.Equal(t, "WithRefreshWindow", identRefreshWindow{}.String()) diff --git a/jwk/x509.go b/jwk/x509.go new file mode 100644 index 000000000..0839c2136 --- /dev/null +++ b/jwk/x509.go @@ -0,0 +1,160 @@ +package jwk + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" +) + +// PEMDecoder is an interface to describe an object that can decode +// a key from PEM encoded ASN.1 DER format. +// +// A PEMDecoder can be specified as an option to `jwk.Parse()` or `jwk.ParseKey()` +// along with the `jwk.WithPEM()` option. +type PEMDecoder interface { + Decode([]byte) (interface{}, []byte, error) +} + +// PEMEncoder is an interface to describe an object that can encode +// a key into PEM encoded ASN.1 DER format. +// +// `jwk.Key` instances do not implement a way to encode themselves into +// PEM format. Normally you can just use `jwk.EncodePEM()` to do this, but +// this interface allows you to generalize the encoding process by +// abstracting the `jwk.EncodePEM()` function using `jwk.PEMEncodeFunc` +// along with alternate implementations, should you need them. +type PEMEncoder interface { + Encode(interface{}) (string, []byte, error) +} + +type PEMEncodeFunc func(interface{}) (string, []byte, error) + +func (f PEMEncodeFunc) Encode(v interface{}) (string, []byte, error) { + return f(v) +} + +func encodeX509(v interface{}) (string, []byte, error) { + // we can't import jwk, so just use the interface + if key, ok := v.(Key); ok { + var raw interface{} + if err := Export(key, &raw); err != nil { + return "", nil, fmt.Errorf(`failed to get raw key out of %T: %w`, key, err) + } + + v = raw + } + + // Try to convert it into a certificate + switch v := v.(type) { + case *rsa.PrivateKey: + return pmRSAPrivateKey, x509.MarshalPKCS1PrivateKey(v), nil + case *ecdsa.PrivateKey: + marshaled, err := x509.MarshalECPrivateKey(v) + if err != nil { + return "", nil, err + } + return pmECPrivateKey, marshaled, nil + case ed25519.PrivateKey: + marshaled, err := x509.MarshalPKCS8PrivateKey(v) + if err != nil { + return "", nil, err + } + return pmPrivateKey, marshaled, nil + case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: + marshaled, err := x509.MarshalPKIXPublicKey(v) + if err != nil { + return "", nil, err + } + return pmPublicKey, marshaled, nil + default: + return "", nil, fmt.Errorf(`unsupported type %T for ASN.1 DER encoding`, v) + } +} + +// EncodePEM encodes the key into a PEM encoded ASN.1 DER format. +// The key can be a jwk.Key or a raw key instance, but it must be one of +// the types supported by `x509` package. +// +// Internally, it uses the same routine as `jwk.EncodeX509()`, and therefore +// the same caveats apply +func EncodePEM(v interface{}) ([]byte, error) { + typ, marshaled, err := encodeX509(v) + if err != nil { + return nil, fmt.Errorf(`failed to encode key in x509: %w`, err) + } + + block := &pem.Block{ + Type: typ, + Bytes: marshaled, + } + return pem.EncodeToMemory(block), nil +} + +const ( + pmPrivateKey = `PRIVATE KEY` + pmPublicKey = `PUBLIC KEY` + pmECPrivateKey = `EC PRIVATE KEY` + pmRSAPublicKey = `RSA PUBLIC KEY` + pmRSAPrivateKey = `RSA PRIVATE KEY` +) + +func NewPEMDecoder() PEMDecoder { + return pemDecoder{} +} + +type pemDecoder struct{} + +// DecodePEM decodes a key in PEM encoded ASN.1 DER format. +// and returns a raw key +func (pemDecoder) Decode(src []byte) (interface{}, []byte, error) { + block, rest := pem.Decode(src) + if block == nil { + return nil, nil, fmt.Errorf(`failed to decode PEM data`) + } + + switch block.Type { + // Handle the semi-obvious cases + case pmRSAPrivateKey: + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, nil, fmt.Errorf(`failed to parse PKCS1 private key: %w`, err) + } + return key, rest, nil + case pmRSAPublicKey: + key, err := x509.ParsePKCS1PublicKey(block.Bytes) + if err != nil { + return nil, nil, fmt.Errorf(`failed to parse PKCS1 public key: %w`, err) + } + return key, rest, nil + case pmECPrivateKey: + key, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, nil, fmt.Errorf(`failed to parse EC private key: %w`, err) + } + return key, rest, nil + case pmPublicKey: + // XXX *could* return dsa.PublicKey + key, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, nil, fmt.Errorf(`failed to parse PKIX public key: %w`, err) + } + return key, rest, nil + case pmPrivateKey: + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, nil, fmt.Errorf(`failed to parse PKCS8 private key: %w`, err) + } + return key, rest, nil + case "CERTIFICATE": + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, nil, fmt.Errorf(`failed to parse certificate: %w`, err) + } + return cert.PublicKey, rest, nil + default: + return nil, nil, fmt.Errorf(`invalid PEM block type %s`, block.Type) + } +} diff --git a/jws/BUILD.bazel b/jws/BUILD.bazel index d12a66108..587bc8ca8 100644 --- a/jws/BUILD.bazel +++ b/jws/BUILD.bazel @@ -25,7 +25,6 @@ go_library( "//cert", "//internal/base64", "//internal/ecutil", - "//internal/iter", "//internal/json", "//internal/keyconv", "//internal/pool", diff --git a/jws/jws.go b/jws/jws.go index 8a3018831..0928a5c44 100644 --- a/jws/jws.go +++ b/jws/jws.go @@ -345,6 +345,7 @@ func Verify(buf []byte, options ...VerifyOption) ([]byte, error) { case identKeyUsed{}: keyUsed = option.Value() case identContext{}: + //nolint:fatcontext ctx = option.Value().(context.Context) case identValidateKey{}: validateKey = option.Value().(bool) diff --git a/jwt/jwt_test.go b/jwt/jwt_test.go index 1e8d8f786..f1ff8991c 100644 --- a/jwt/jwt_test.go +++ b/jwt/jwt_test.go @@ -593,10 +593,10 @@ func TestGH52(t *testing.T) { if !assert.NoError(t, err) { return } - const max = 100 + const iterations = 100 var wg sync.WaitGroup - wg.Add(max) - for i := 0; i < max; i++ { + wg.Add(iterations) + for i := 0; i < iterations; i++ { // Do not use t.Run here as it will clutter up the outpuA go func(t *testing.T, priv *ecdsa.PrivateKey, i int) { defer wg.Done() diff --git a/jwt/validate.go b/jwt/validate.go index 86a08073f..e0f0c10ab 100644 --- a/jwt/validate.go +++ b/jwt/validate.go @@ -65,6 +65,7 @@ func Validate(t Token, options ...ValidateOption) error { case identTruncation{}: trunc = o.Value().(time.Duration) case identContext{}: + //nolint:fatcontext ctx = o.Value().(context.Context) case identResetValidators{}: resetValidators = o.Value().(bool) diff --git a/jwx_test.go b/jwx_test.go index 0a6101a19..b33d46694 100644 --- a/jwx_test.go +++ b/jwx_test.go @@ -128,7 +128,7 @@ func TestJoseCompatibility(t *testing.T) { Name: "RSA Private Key with Private Parameters", Raw: rsa.PrivateKey{}, Template: `{"alg": "RS256", "x-jwx": 1234}`, - VerifyKey: func(ctx context.Context, t *testing.T, key jwk.Key) bool { + VerifyKey: func(_ context.Context, t *testing.T, key jwk.Key) bool { var v float64 require.NoError(t, key.Get(`x-jwx`, &v), `key.Get should succeed`) require.Equal(t, float64(1234), v, `private parameters should match`)