Initial QSfera import
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 lestrrat-go
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# blackmagic
|
||||
|
||||
Reflect-based black magic. YMMV, and use with caution
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package blackmagic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type errInvalidValue struct{}
|
||||
|
||||
func (*errInvalidValue) Error() string {
|
||||
return "invalid value (probably an untyped nil)"
|
||||
}
|
||||
|
||||
// InvalidValueError is a sentinel error that can be used to
|
||||
// indicate that a value is invalid. This can happen when the
|
||||
// source value is an untyped nil, and we have no further information
|
||||
// about the type of the value, obstructing the assignment.
|
||||
func InvalidValueError() error {
|
||||
return &errInvalidValue{}
|
||||
}
|
||||
|
||||
// AssignField is a convenience function to assign a value to
|
||||
// an optional struct field. In Go, an optional struct field is
|
||||
// usually denoted by a pointer to T instead of T:
|
||||
//
|
||||
// type Object struct {
|
||||
// Optional *T
|
||||
// }
|
||||
//
|
||||
// This gets a bit cumbersome when you want to assign literals
|
||||
// or you do not want to worry about taking the address of a
|
||||
// variable.
|
||||
//
|
||||
// Object.Optional = &"foo" // doesn't compile!
|
||||
//
|
||||
// Instead you can use this function to do it in one line:
|
||||
//
|
||||
// blackmagic.AssignOptionalField(&Object.Optionl, "foo")
|
||||
func AssignOptionalField(dst, src interface{}) error {
|
||||
dstRV := reflect.ValueOf(dst)
|
||||
srcRV := reflect.ValueOf(src)
|
||||
if dstRV.Kind() != reflect.Pointer || dstRV.Elem().Kind() != reflect.Pointer {
|
||||
return fmt.Errorf(`dst must be a pointer to a field that is turn a pointer of src (%T)`, src)
|
||||
}
|
||||
|
||||
if !dstRV.Elem().CanSet() {
|
||||
return fmt.Errorf(`dst (%T) is not assignable`, dstRV.Elem().Interface())
|
||||
}
|
||||
if !reflect.PointerTo(srcRV.Type()).AssignableTo(dstRV.Elem().Type()) {
|
||||
return fmt.Errorf(`cannot assign src (%T) to dst (%T)`, src, dst)
|
||||
}
|
||||
|
||||
ptr := reflect.New(srcRV.Type())
|
||||
ptr.Elem().Set(srcRV)
|
||||
dstRV.Elem().Set(ptr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssignIfCompatible is a convenience function to safely
|
||||
// assign arbitrary values. dst must be a pointer to an
|
||||
// empty interface, or it must be a pointer to a compatible
|
||||
// variable type that can hold src.
|
||||
func AssignIfCompatible(dst, src interface{}) error {
|
||||
orv := reflect.ValueOf(src) // save this value for error reporting
|
||||
result := orv
|
||||
|
||||
// src can be a pointer or a slice, and the code will slightly change
|
||||
// depending on this
|
||||
var srcIsPtr bool
|
||||
var srcIsSlice bool
|
||||
switch result.Kind() {
|
||||
case reflect.Ptr:
|
||||
srcIsPtr = true
|
||||
case reflect.Slice:
|
||||
srcIsSlice = true
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(dst)
|
||||
if rv.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf(`destination argument to AssignIfCompatible() must be a pointer: %T`, dst)
|
||||
}
|
||||
|
||||
actualDst := rv
|
||||
for {
|
||||
if !actualDst.IsValid() {
|
||||
return fmt.Errorf(`could not find a valid destination for AssignIfCompatible() (%T)`, dst)
|
||||
}
|
||||
if actualDst.CanSet() {
|
||||
break
|
||||
}
|
||||
actualDst = actualDst.Elem()
|
||||
}
|
||||
|
||||
switch actualDst.Kind() {
|
||||
case reflect.Interface:
|
||||
// If it's an interface, we can just assign the pointer to the interface{}
|
||||
default:
|
||||
// If it's a pointer to the struct we're looking for, we need to set
|
||||
// the de-referenced struct
|
||||
if !srcIsSlice && srcIsPtr {
|
||||
result = result.Elem()
|
||||
}
|
||||
}
|
||||
|
||||
if !result.IsValid() {
|
||||
// At this point there's nothing we can do. return an error
|
||||
return fmt.Errorf(`source value is invalid (%T): %w`, src, InvalidValueError())
|
||||
}
|
||||
|
||||
if actualDst.Kind() == reflect.Ptr {
|
||||
actualDst.Set(result.Addr())
|
||||
return nil
|
||||
}
|
||||
|
||||
if !result.Type().AssignableTo(actualDst.Type()) {
|
||||
return fmt.Errorf(`argument to AssignIfCompatible() must be compatible with %T (was %T)`, orv.Interface(), dst)
|
||||
}
|
||||
|
||||
if !actualDst.CanSet() {
|
||||
return fmt.Errorf(`argument to AssignIfCompatible() must be settable`)
|
||||
}
|
||||
actualDst.Set(result)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Code coverage profiles and other test artifacts
|
||||
*.out
|
||||
coverage.*
|
||||
*.coverprofile
|
||||
profile.cov
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
# Editor/IDE
|
||||
# .idea/
|
||||
# .vscode/
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
Changes
|
||||
=======
|
||||
|
||||
v1.0.0 18 Aug 2025
|
||||
* Initial release
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 lestrrat-go
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package dsigsecp256k1
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lestrrat-go/dsig"
|
||||
)
|
||||
|
||||
const ECDSAWithSecp256k1AndSHA256 = "ECDSA_WITH_SECP256K1_AND_SHA256"
|
||||
|
||||
// init adds secp256k1 support when the dsig_secp256k1 build tag is used.
|
||||
func init() {
|
||||
// Register ES256K (secp256k1 + SHA256) support using the new API
|
||||
err := dsig.RegisterAlgorithm(ECDSAWithSecp256k1AndSHA256, dsig.AlgorithmInfo{
|
||||
Family: dsig.ECDSA,
|
||||
Meta: dsig.ECDSAFamilyMeta{
|
||||
Hash: crypto.SHA256,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic("failed to register secp256k1 algorithm: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// secp256k1Curve returns the secp256k1 curve.
|
||||
func Curve() *secp256k1.KoblitzCurve {
|
||||
return secp256k1.S256()
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Code coverage profiles and other test artifacts
|
||||
*.out
|
||||
coverage.*
|
||||
*.coverprofile
|
||||
profile.cov
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
# Editor/IDE
|
||||
# .idea/
|
||||
# .vscode/
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
Changes
|
||||
=======
|
||||
|
||||
v1.0.0 - 18 Aug 2025
|
||||
* Initial release
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 lestrrat-go
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
# github.com/lestrrat-go/dsig [](https://github.com/lestrrat-go/dsig/actions/workflows/ci.yml) [](https://pkg.go.dev/github.com/lestrrat-go/dsig) [](https://codecov.io/github/lestrrat-go/dsig?branch=v1)
|
||||
|
||||
Go module providing low-level digital signature operations.
|
||||
|
||||
While there are many standards for generating and verifying digital signatures, the core operations are virtually the same. This module implements the core functionality of digital signature generation / verifications in a framework agnostic way.
|
||||
|
||||
# Features
|
||||
|
||||
* RSA signatures (PKCS1v15 and PSS)
|
||||
* ECDSA signatures (P-256, P-384, P-521)
|
||||
* 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.
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
<!-- INCLUDE(examples/dsig_readme_example_test.go) -->
|
||||
```go
|
||||
package examples_test
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
|
||||
"github.com/lestrrat-go/dsig"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
payload := []byte("hello world")
|
||||
|
||||
// RSA signing and verification
|
||||
{
|
||||
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to generate RSA key: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Sign with RSA-PSS SHA256
|
||||
signature, err := dsig.Sign(privKey, dsig.RSAPSSWithSHA256, payload, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to sign with RSA: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify with RSA-PSS SHA256
|
||||
err = dsig.Verify(&privKey.PublicKey, dsig.RSAPSSWithSHA256, payload, signature)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to verify RSA signature: %s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ECDSA signing and verification
|
||||
{
|
||||
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to generate ECDSA key: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Sign with ECDSA P-256 SHA256
|
||||
signature, err := dsig.Sign(privKey, dsig.ECDSAWithP256AndSHA256, payload, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to sign with ECDSA: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify with ECDSA P-256 SHA256
|
||||
err = dsig.Verify(&privKey.PublicKey, dsig.ECDSAWithP256AndSHA256, payload, signature)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to verify ECDSA signature: %s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// EdDSA signing and verification
|
||||
{
|
||||
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to generate Ed25519 key: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Sign with EdDSA
|
||||
signature, err := dsig.Sign(privKey, dsig.EdDSA, payload, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to sign with EdDSA: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify with EdDSA
|
||||
err = dsig.Verify(pubKey, dsig.EdDSA, payload, signature)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to verify EdDSA signature: %s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// HMAC signing and verification
|
||||
{
|
||||
key := []byte("secret-key")
|
||||
|
||||
// Sign with HMAC SHA256
|
||||
signature, err := dsig.Sign(key, dsig.HMACWithSHA256, payload, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to sign with HMAC: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify with HMAC SHA256
|
||||
err = dsig.Verify(key, dsig.HMACWithSHA256, payload, signature)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to verify HMAC signature: %s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
// OUTPUT:
|
||||
}
|
||||
```
|
||||
source: [examples/dsig_readme_example_test.go](https://github.com/lestrrat-go/dsig/blob/v1/examples/dsig_readme_example_test.go)
|
||||
<!-- END INCLUDE -->
|
||||
|
||||
# Supported Algorithms
|
||||
|
||||
| Constant | Algorithm | Key Type |
|
||||
|----------|-----------|----------|
|
||||
| `HMACWithSHA256` | HMAC using SHA-256 | []byte |
|
||||
| `HMACWithSHA384` | HMAC using SHA-384 | []byte |
|
||||
| `HMACWithSHA512` | HMAC using SHA-512 | []byte |
|
||||
| `RSAPKCS1v15WithSHA256` | RSA PKCS#1 v1.5 using SHA-256 | *rsa.PrivateKey / *rsa.PublicKey |
|
||||
| `RSAPKCS1v15WithSHA384` | RSA PKCS#1 v1.5 using SHA-384 | *rsa.PrivateKey / *rsa.PublicKey |
|
||||
| `RSAPKCS1v15WithSHA512` | RSA PKCS#1 v1.5 using SHA-512 | *rsa.PrivateKey / *rsa.PublicKey |
|
||||
| `RSAPSSWithSHA256` | RSA PSS using SHA-256 | *rsa.PrivateKey / *rsa.PublicKey |
|
||||
| `RSAPSSWithSHA384` | RSA PSS using SHA-384 | *rsa.PrivateKey / *rsa.PublicKey |
|
||||
| `RSAPSSWithSHA512` | RSA PSS using SHA-512 | *rsa.PrivateKey / *rsa.PublicKey |
|
||||
| `ECDSAWithP256AndSHA256` | ECDSA using P-256 and SHA-256 | *ecdsa.PrivateKey / *ecdsa.PublicKey |
|
||||
| `ECDSAWithP384AndSHA384` | ECDSA using P-384 and SHA-384 | *ecdsa.PrivateKey / *ecdsa.PublicKey |
|
||||
| `ECDSAWithP521AndSHA512` | ECDSA using P-521 and SHA-512 | *ecdsa.PrivateKey / *ecdsa.PublicKey |
|
||||
| `EdDSA` | EdDSA using Ed25519 or Ed448 | ed25519.PrivateKey / ed25519.PublicKey |
|
||||
|
||||
# Description
|
||||
|
||||
This library provides low-level digital signature operations. It does minimal parameter validation for performance, uses strongly typed APIs, and has minimal dependencies.
|
||||
|
||||
# Contributions
|
||||
|
||||
## Issues
|
||||
|
||||
For bug reports and feature requests, please include failing tests when possible.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
Please include tests that exercise your changes.
|
||||
|
||||
# Related Libraries
|
||||
|
||||
* [github.com/lestrrat-go/jwx](https://github.com/lestrrat-go/jwx) - JOSE (JWA/JWE/JWK/JWS/JWT) implementation
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package dsig
|
||||
|
||||
// This file defines verbose algorithm name constants that can be mapped to by
|
||||
// different standards (RFC7518, FIDO, etc.) for interoperability.
|
||||
//
|
||||
// The algorithm names are intentionally verbose to avoid any ambiguity about
|
||||
// the exact cryptographic operations being performed.
|
||||
|
||||
const (
|
||||
// HMAC signature algorithms
|
||||
// These use Hash-based Message Authentication Code with specified hash functions
|
||||
HMACWithSHA256 = "HMAC_WITH_SHA256"
|
||||
HMACWithSHA384 = "HMAC_WITH_SHA384"
|
||||
HMACWithSHA512 = "HMAC_WITH_SHA512"
|
||||
|
||||
// RSA signature algorithms with PKCS#1 v1.5 padding
|
||||
// These use RSA signatures with PKCS#1 v1.5 padding and specified hash functions
|
||||
RSAPKCS1v15WithSHA256 = "RSA_PKCS1v15_WITH_SHA256"
|
||||
RSAPKCS1v15WithSHA384 = "RSA_PKCS1v15_WITH_SHA384"
|
||||
RSAPKCS1v15WithSHA512 = "RSA_PKCS1v15_WITH_SHA512"
|
||||
|
||||
// RSA signature algorithms with PSS padding
|
||||
// These use RSA signatures with Probabilistic Signature Scheme (PSS) padding
|
||||
RSAPSSWithSHA256 = "RSA_PSS_WITH_SHA256"
|
||||
RSAPSSWithSHA384 = "RSA_PSS_WITH_SHA384"
|
||||
RSAPSSWithSHA512 = "RSA_PSS_WITH_SHA512"
|
||||
|
||||
// ECDSA signature algorithms
|
||||
// These use Elliptic Curve Digital Signature Algorithm with specified curves and hash functions
|
||||
ECDSAWithP256AndSHA256 = "ECDSA_WITH_P256_AND_SHA256"
|
||||
ECDSAWithP384AndSHA384 = "ECDSA_WITH_P384_AND_SHA384"
|
||||
ECDSAWithP521AndSHA512 = "ECDSA_WITH_P521_AND_SHA512"
|
||||
|
||||
// EdDSA signature algorithms
|
||||
// These use Edwards-curve Digital Signature Algorithm (supports Ed25519 and Ed448)
|
||||
EdDSA = "EDDSA"
|
||||
)
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package dsig
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// cryptosign is a low-level function that signs a payload using a crypto.Signer.
|
||||
// If hash is crypto.Hash(0), the payload is signed directly without hashing.
|
||||
// Otherwise, the payload is hashed using the specified hash function before signing.
|
||||
//
|
||||
// rr is an io.Reader that provides randomness for signing. If rr is nil, it defaults to rand.Reader.
|
||||
func cryptosign(signer crypto.Signer, payload []byte, hash crypto.Hash, opts crypto.SignerOpts, rr io.Reader) ([]byte, error) {
|
||||
if rr == nil {
|
||||
rr = rand.Reader
|
||||
}
|
||||
|
||||
var digest []byte
|
||||
if hash == crypto.Hash(0) {
|
||||
digest = payload
|
||||
} else {
|
||||
h := hash.New()
|
||||
if _, err := h.Write(payload); err != nil {
|
||||
return nil, fmt.Errorf(`failed to write payload to hash: %w`, err)
|
||||
}
|
||||
digest = h.Sum(nil)
|
||||
}
|
||||
return signer.Sign(rr, digest, opts)
|
||||
}
|
||||
|
||||
// SignCryptoSigner generates a signature using a crypto.Signer interface.
|
||||
// This function can be used for hardware security modules, smart cards,
|
||||
// and other implementations of the crypto.Signer interface.
|
||||
//
|
||||
// rr is an io.Reader that provides randomness for signing. If rr is nil, it defaults to rand.Reader.
|
||||
//
|
||||
// Returns the signature bytes or an error if signing fails.
|
||||
func SignCryptoSigner(signer crypto.Signer, raw []byte, h crypto.Hash, opts crypto.SignerOpts, rr io.Reader) ([]byte, error) {
|
||||
if signer == nil {
|
||||
return nil, fmt.Errorf("dsig.SignCryptoSigner: signer is nil")
|
||||
}
|
||||
return cryptosign(signer, raw, h, opts, rr)
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
// Package dsig provides digital signature operations for Go.
|
||||
// It contains low-level signature generation and verification tools that
|
||||
// can be used by other signing libraries
|
||||
//
|
||||
// The package follows these design principles:
|
||||
// 1. Does minimal checking of input parameters (for performance); callers need to ensure that the parameters are valid.
|
||||
// 2. All exported functions are strongly typed (i.e. they do not take `any` types unless they absolutely have to).
|
||||
// 3. Does not rely on other high-level packages (standalone, except for internal packages).
|
||||
package dsig
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"hash"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Family represents the cryptographic algorithm family
|
||||
type Family int
|
||||
|
||||
const (
|
||||
InvalidFamily Family = iota
|
||||
HMAC
|
||||
RSA
|
||||
ECDSA
|
||||
EdDSAFamily
|
||||
maxFamily
|
||||
)
|
||||
|
||||
// String returns the string representation of the Family
|
||||
func (f Family) String() string {
|
||||
switch f {
|
||||
case HMAC:
|
||||
return "HMAC"
|
||||
case RSA:
|
||||
return "RSA"
|
||||
case ECDSA:
|
||||
return "ECDSA"
|
||||
case EdDSAFamily:
|
||||
return "EdDSA"
|
||||
default:
|
||||
return "InvalidFamily"
|
||||
}
|
||||
}
|
||||
|
||||
// AlgorithmInfo contains metadata about a digital signature algorithm
|
||||
type AlgorithmInfo struct {
|
||||
Family Family // The cryptographic family (HMAC, RSA, ECDSA, EdDSA)
|
||||
Meta any // Family-specific metadata
|
||||
}
|
||||
|
||||
// HMACFamilyMeta contains metadata specific to HMAC algorithms
|
||||
type HMACFamilyMeta struct {
|
||||
HashFunc func() hash.Hash // Hash function constructor
|
||||
}
|
||||
|
||||
// RSAFamilyMeta contains metadata specific to RSA algorithms
|
||||
type RSAFamilyMeta struct {
|
||||
Hash crypto.Hash // Hash algorithm
|
||||
PSS bool // Whether to use PSS padding (false = PKCS#1 v1.5)
|
||||
}
|
||||
|
||||
// ECDSAFamilyMeta contains metadata specific to ECDSA algorithms
|
||||
type ECDSAFamilyMeta struct {
|
||||
Hash crypto.Hash // Hash algorithm
|
||||
}
|
||||
|
||||
// EdDSAFamilyMeta contains metadata specific to EdDSA algorithms
|
||||
// Currently EdDSA doesn't need specific metadata, but this provides extensibility
|
||||
type EdDSAFamilyMeta struct {
|
||||
// Reserved for future use
|
||||
}
|
||||
|
||||
var algorithms = make(map[string]AlgorithmInfo)
|
||||
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.
|
||||
func RegisterAlgorithm(name string, info AlgorithmInfo) error {
|
||||
muAlgorithms.Lock()
|
||||
defer muAlgorithms.Unlock()
|
||||
|
||||
// Validate the metadata matches the family
|
||||
switch info.Family {
|
||||
case HMAC:
|
||||
if _, ok := info.Meta.(HMACFamilyMeta); !ok {
|
||||
return fmt.Errorf("invalid HMAC metadata for algorithm %s", name)
|
||||
}
|
||||
case RSA:
|
||||
if _, ok := info.Meta.(RSAFamilyMeta); !ok {
|
||||
return fmt.Errorf("invalid RSA metadata for algorithm %s", name)
|
||||
}
|
||||
case ECDSA:
|
||||
if _, ok := info.Meta.(ECDSAFamilyMeta); !ok {
|
||||
return fmt.Errorf("invalid ECDSA metadata for algorithm %s", name)
|
||||
}
|
||||
case EdDSAFamily:
|
||||
// EdDSA metadata is optional for now
|
||||
default:
|
||||
return fmt.Errorf("unsupported algorithm family %s for algorithm %s", info.Family, name)
|
||||
}
|
||||
|
||||
algorithms[name] = info
|
||||
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) {
|
||||
muAlgorithms.RLock()
|
||||
defer muAlgorithms.RUnlock()
|
||||
|
||||
info, ok := algorithms[name]
|
||||
return info, ok
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register all standard algorithms with their metadata
|
||||
toRegister := map[string]AlgorithmInfo{
|
||||
// HMAC algorithms
|
||||
HMACWithSHA256: {
|
||||
Family: HMAC,
|
||||
Meta: HMACFamilyMeta{
|
||||
HashFunc: sha256.New,
|
||||
},
|
||||
},
|
||||
HMACWithSHA384: {
|
||||
Family: HMAC,
|
||||
Meta: HMACFamilyMeta{
|
||||
HashFunc: sha512.New384,
|
||||
},
|
||||
},
|
||||
HMACWithSHA512: {
|
||||
Family: HMAC,
|
||||
Meta: HMACFamilyMeta{
|
||||
HashFunc: sha512.New,
|
||||
},
|
||||
},
|
||||
|
||||
// RSA PKCS#1 v1.5 algorithms
|
||||
RSAPKCS1v15WithSHA256: {
|
||||
Family: RSA,
|
||||
Meta: RSAFamilyMeta{
|
||||
Hash: crypto.SHA256,
|
||||
PSS: false,
|
||||
},
|
||||
},
|
||||
RSAPKCS1v15WithSHA384: {
|
||||
Family: RSA,
|
||||
Meta: RSAFamilyMeta{
|
||||
Hash: crypto.SHA384,
|
||||
PSS: false,
|
||||
},
|
||||
},
|
||||
RSAPKCS1v15WithSHA512: {
|
||||
Family: RSA,
|
||||
Meta: RSAFamilyMeta{
|
||||
Hash: crypto.SHA512,
|
||||
PSS: false,
|
||||
},
|
||||
},
|
||||
|
||||
// RSA PSS algorithms
|
||||
RSAPSSWithSHA256: {
|
||||
Family: RSA,
|
||||
Meta: RSAFamilyMeta{
|
||||
Hash: crypto.SHA256,
|
||||
PSS: true,
|
||||
},
|
||||
},
|
||||
RSAPSSWithSHA384: {
|
||||
Family: RSA,
|
||||
Meta: RSAFamilyMeta{
|
||||
Hash: crypto.SHA384,
|
||||
PSS: true,
|
||||
},
|
||||
},
|
||||
RSAPSSWithSHA512: {
|
||||
Family: RSA,
|
||||
Meta: RSAFamilyMeta{
|
||||
Hash: crypto.SHA512,
|
||||
PSS: true,
|
||||
},
|
||||
},
|
||||
|
||||
// ECDSA algorithms
|
||||
ECDSAWithP256AndSHA256: {
|
||||
Family: ECDSA,
|
||||
Meta: ECDSAFamilyMeta{
|
||||
Hash: crypto.SHA256,
|
||||
},
|
||||
},
|
||||
ECDSAWithP384AndSHA384: {
|
||||
Family: ECDSA,
|
||||
Meta: ECDSAFamilyMeta{
|
||||
Hash: crypto.SHA384,
|
||||
},
|
||||
},
|
||||
ECDSAWithP521AndSHA512: {
|
||||
Family: ECDSA,
|
||||
Meta: ECDSAFamilyMeta{
|
||||
Hash: crypto.SHA512,
|
||||
},
|
||||
},
|
||||
|
||||
// EdDSA algorithm
|
||||
EdDSA: {
|
||||
Family: EdDSAFamily,
|
||||
Meta: EdDSAFamilyMeta{},
|
||||
},
|
||||
}
|
||||
|
||||
for name, info := range toRegister {
|
||||
if err := RegisterAlgorithm(name, info); err != nil {
|
||||
panic(fmt.Sprintf("failed to register algorithm %s: %v", name, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
package dsig
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"encoding/asn1"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
|
||||
"github.com/lestrrat-go/dsig/internal/ecutil"
|
||||
)
|
||||
|
||||
|
||||
func ecdsaGetSignerKey(key any) (*ecdsa.PrivateKey, crypto.Signer, bool, error) {
|
||||
cs, isCryptoSigner := key.(crypto.Signer)
|
||||
if isCryptoSigner {
|
||||
if !isValidECDSAKey(key) {
|
||||
return nil, nil, false, fmt.Errorf(`invalid key type %T for ECDSA algorithm`, key)
|
||||
}
|
||||
|
||||
switch key.(type) {
|
||||
case ecdsa.PrivateKey, *ecdsa.PrivateKey:
|
||||
// if it's ecdsa.PrivateKey, it's more efficient to
|
||||
// go through the non-crypto.Signer route. Set isCryptoSigner to false
|
||||
isCryptoSigner = false
|
||||
}
|
||||
}
|
||||
|
||||
if isCryptoSigner {
|
||||
return nil, cs, true, nil
|
||||
}
|
||||
|
||||
privkey, ok := key.(*ecdsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, nil, false, fmt.Errorf(`invalid key type %T. *ecdsa.PrivateKey is required`, key)
|
||||
}
|
||||
return privkey, nil, false, nil
|
||||
}
|
||||
|
||||
// UnpackASN1ECDSASignature unpacks an ASN.1 encoded ECDSA signature into r and s values.
|
||||
// This is typically used when working with crypto.Signer interfaces that return ASN.1 encoded signatures.
|
||||
func UnpackASN1ECDSASignature(signed []byte, r, s *big.Int) error {
|
||||
// Okay, this is silly, but hear me out. When we use the
|
||||
// crypto.Signer interface, the PrivateKey is hidden.
|
||||
// But we need some information about the key (its bit size).
|
||||
//
|
||||
// So while silly, we're going to have to make another call
|
||||
// here and fetch the Public key.
|
||||
// (This probably means that this information should be cached somewhere)
|
||||
var p struct {
|
||||
R *big.Int // TODO: get this from a pool?
|
||||
S *big.Int
|
||||
}
|
||||
if _, err := asn1.Unmarshal(signed, &p); err != nil {
|
||||
return fmt.Errorf(`failed to unmarshal ASN1 encoded signature: %w`, err)
|
||||
}
|
||||
|
||||
r.Set(p.R)
|
||||
s.Set(p.S)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnpackECDSASignature unpacks a JWS-format ECDSA signature into r and s values.
|
||||
// The signature should be in the format specified by RFC 7515 (r||s as fixed-length byte arrays).
|
||||
func UnpackECDSASignature(signature []byte, pubkey *ecdsa.PublicKey, r, s *big.Int) error {
|
||||
keySize := ecutil.CalculateKeySize(pubkey.Curve)
|
||||
if len(signature) != keySize*2 {
|
||||
return fmt.Errorf(`invalid signature length for curve %q`, pubkey.Curve.Params().Name)
|
||||
}
|
||||
|
||||
r.SetBytes(signature[:keySize])
|
||||
s.SetBytes(signature[keySize:])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PackECDSASignature packs the r and s values from an ECDSA signature into a JWS-format byte slice.
|
||||
// The output format follows RFC 7515: r||s as fixed-length byte arrays.
|
||||
func PackECDSASignature(r *big.Int, sbig *big.Int, curveBits int) ([]byte, error) {
|
||||
keyBytes := curveBits / 8
|
||||
if curveBits%8 > 0 {
|
||||
keyBytes++
|
||||
}
|
||||
|
||||
// Serialize r and s into fixed-length bytes
|
||||
rBytes := r.Bytes()
|
||||
rBytesPadded := make([]byte, keyBytes)
|
||||
copy(rBytesPadded[keyBytes-len(rBytes):], rBytes)
|
||||
|
||||
sBytes := sbig.Bytes()
|
||||
sBytesPadded := make([]byte, keyBytes)
|
||||
copy(sBytesPadded[keyBytes-len(sBytes):], sBytes)
|
||||
|
||||
// Output as r||s
|
||||
return append(rBytesPadded, sBytesPadded...), nil
|
||||
}
|
||||
|
||||
// SignECDSA generates an ECDSA signature for the given payload using the specified private key and hash.
|
||||
// The raw parameter should be the pre-computed signing input (typically header.payload).
|
||||
//
|
||||
// rr is an io.Reader that provides randomness for signing. if rr is nil, it defaults to rand.Reader.
|
||||
func SignECDSA(key *ecdsa.PrivateKey, payload []byte, h crypto.Hash, rr io.Reader) ([]byte, error) {
|
||||
if !isValidECDSAKey(key) {
|
||||
return nil, fmt.Errorf(`invalid key type %T for ECDSA algorithm`, key)
|
||||
}
|
||||
hh := h.New()
|
||||
if _, err := hh.Write(payload); err != nil {
|
||||
return nil, fmt.Errorf(`failed to write payload using ecdsa: %w`, err)
|
||||
}
|
||||
digest := hh.Sum(nil)
|
||||
|
||||
if rr == nil {
|
||||
rr = rand.Reader
|
||||
}
|
||||
|
||||
// Sign and get r, s values
|
||||
r, s, err := ecdsa.Sign(rr, key, digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to sign payload using ecdsa: %w`, err)
|
||||
}
|
||||
|
||||
return PackECDSASignature(r, s, key.Curve.Params().BitSize)
|
||||
}
|
||||
|
||||
// SignECDSACryptoSigner generates an ECDSA signature using a crypto.Signer interface.
|
||||
// This function works with hardware security modules and other crypto.Signer implementations.
|
||||
// The signature is converted from ASN.1 format to JWS format (r||s).
|
||||
//
|
||||
// rr is an io.Reader that provides randomness for signing. If rr is nil, it defaults to rand.Reader.
|
||||
func SignECDSACryptoSigner(signer crypto.Signer, raw []byte, h crypto.Hash, rr io.Reader) ([]byte, error) {
|
||||
signed, err := SignCryptoSigner(signer, raw, h, h, rr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to sign payload using crypto.Signer: %w`, err)
|
||||
}
|
||||
|
||||
return signECDSACryptoSigner(signer, signed)
|
||||
}
|
||||
|
||||
func signECDSACryptoSigner(signer crypto.Signer, signed []byte) ([]byte, error) {
|
||||
cpub := signer.Public()
|
||||
pubkey, ok := cpub.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`expected *ecdsa.PublicKey, got %T`, pubkey)
|
||||
}
|
||||
curveBits := pubkey.Curve.Params().BitSize
|
||||
|
||||
var r, s big.Int
|
||||
if err := UnpackASN1ECDSASignature(signed, &r, &s); err != nil {
|
||||
return nil, fmt.Errorf(`failed to unpack ASN1 encoded signature: %w`, err)
|
||||
}
|
||||
|
||||
return PackECDSASignature(&r, &s, curveBits)
|
||||
}
|
||||
|
||||
func ecdsaVerify(key *ecdsa.PublicKey, buf []byte, h crypto.Hash, r, s *big.Int) error {
|
||||
hasher := h.New()
|
||||
hasher.Write(buf)
|
||||
digest := hasher.Sum(nil)
|
||||
if !ecdsa.Verify(key, digest, r, s) {
|
||||
return NewVerificationError("invalid ECDSA signature")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyECDSA verifies an ECDSA signature for the given payload.
|
||||
// This function verifies the signature using the specified public key and hash algorithm.
|
||||
// The payload parameter should be the pre-computed signing input (typically header.payload).
|
||||
func VerifyECDSA(key *ecdsa.PublicKey, payload, signature []byte, h crypto.Hash) error {
|
||||
var r, s big.Int
|
||||
if err := UnpackECDSASignature(signature, key, &r, &s); err != nil {
|
||||
return fmt.Errorf("dsig.VerifyECDSA: failed to unpack ECDSA signature: %w", err)
|
||||
}
|
||||
|
||||
return ecdsaVerify(key, payload, h, &r, &s)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// The payload parameter should be the pre-computed signing input (typically header.payload).
|
||||
func VerifyECDSACryptoSigner(signer crypto.Signer, payload, signature []byte, h crypto.Hash) error {
|
||||
var pubkey *ecdsa.PublicKey
|
||||
switch cpub := signer.Public(); cpub := cpub.(type) {
|
||||
case ecdsa.PublicKey:
|
||||
pubkey = &cpub
|
||||
case *ecdsa.PublicKey:
|
||||
pubkey = cpub
|
||||
default:
|
||||
return fmt.Errorf(`dsig.VerifyECDSACryptoSigner: expected *ecdsa.PublicKey, got %T`, cpub)
|
||||
}
|
||||
|
||||
var r, s big.Int
|
||||
if err := UnpackECDSASignature(signature, pubkey, &r, &s); err != nil {
|
||||
return fmt.Errorf("dsig.VerifyECDSACryptoSigner: failed to unpack ASN.1 encoded ECDSA signature: %w", err)
|
||||
}
|
||||
|
||||
return ecdsaVerify(pubkey, payload, h, &r, &s)
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package dsig
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func eddsaGetSigner(key any) (crypto.Signer, error) {
|
||||
// The ed25519.PrivateKey object implements crypto.Signer, so we should
|
||||
// simply accept a crypto.Signer here.
|
||||
signer, ok := key.(crypto.Signer)
|
||||
if ok {
|
||||
if !isValidEDDSAKey(key) {
|
||||
return nil, fmt.Errorf(`invalid key type %T for EdDSA algorithm`, key)
|
||||
}
|
||||
return signer, nil
|
||||
}
|
||||
|
||||
// This fallback exists for cases when users give us a pointer instead of non-pointer, etc.
|
||||
privkey, ok := key.(ed25519.PrivateKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`failed to retrieve ed25519.PrivateKey out of %T`, key)
|
||||
}
|
||||
return privkey, nil
|
||||
}
|
||||
|
||||
// SignEdDSA generates an EdDSA (Ed25519) signature for the given payload.
|
||||
// The raw parameter should be the pre-computed signing input (typically header.payload).
|
||||
// EdDSA is deterministic and doesn't require additional hashing of the input.
|
||||
func SignEdDSA(key ed25519.PrivateKey, payload []byte) ([]byte, error) {
|
||||
return ed25519.Sign(key, payload), nil
|
||||
}
|
||||
|
||||
// VerifyEdDSA verifies an EdDSA (Ed25519) signature for the given payload.
|
||||
// This function verifies the signature using Ed25519 verification algorithm.
|
||||
// The payload parameter should be the pre-computed signing input (typically header.payload).
|
||||
// EdDSA is deterministic and provides strong security guarantees without requiring hash function selection.
|
||||
func VerifyEdDSA(key ed25519.PublicKey, payload, signature []byte) error {
|
||||
if !ed25519.Verify(key, payload, signature) {
|
||||
return fmt.Errorf("invalid EdDSA signature")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package dsig
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"fmt"
|
||||
"hash"
|
||||
)
|
||||
|
||||
func toHMACKey(dst *[]byte, key any) error {
|
||||
keyBytes, ok := key.([]byte)
|
||||
if !ok {
|
||||
return fmt.Errorf(`dsig.toHMACKey: invalid key type %T. []byte is required`, key)
|
||||
}
|
||||
|
||||
if len(keyBytes) == 0 {
|
||||
return fmt.Errorf(`dsig.toHMACKey: missing key while signing payload`)
|
||||
}
|
||||
|
||||
*dst = keyBytes
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignHMAC generates an HMAC signature for the given payload using the specified hash function and key.
|
||||
// The raw parameter should be the pre-computed signing input (typically header.payload).
|
||||
func SignHMAC(key, payload []byte, hfunc func() hash.Hash) ([]byte, error) {
|
||||
h := hmac.New(hfunc, key)
|
||||
if _, err := h.Write(payload); err != nil {
|
||||
return nil, fmt.Errorf(`failed to write payload using hmac: %w`, err)
|
||||
}
|
||||
return h.Sum(nil), 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).
|
||||
func VerifyHMAC(key, payload, signature []byte, hfunc func() hash.Hash) error {
|
||||
expected, err := SignHMAC(key, payload, hfunc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sign payload for verification: %w", err)
|
||||
}
|
||||
if !hmac.Equal(signature, expected) {
|
||||
return NewVerificationError("invalid HMAC signature")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
// Package ecutil defines tools that help with elliptic curve related
|
||||
// computation
|
||||
package ecutil
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"math/big"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// size of buffer that needs to be allocated for EC521 curve
|
||||
ec521BufferSize = 66 // (521 / 8) + 1
|
||||
)
|
||||
|
||||
var ecpointBufferPool = sync.Pool{
|
||||
New: func() any {
|
||||
// In most cases the curve bit size will be less than this length
|
||||
// so allocate the maximum, and keep reusing
|
||||
buf := make([]byte, 0, ec521BufferSize)
|
||||
return &buf
|
||||
},
|
||||
}
|
||||
|
||||
func getCrvFixedBuffer(size int) []byte {
|
||||
//nolint:forcetypeassert
|
||||
buf := *(ecpointBufferPool.Get().(*[]byte))
|
||||
if size > ec521BufferSize && cap(buf) < size {
|
||||
buf = append(buf, make([]byte, size-cap(buf))...)
|
||||
}
|
||||
return buf[:size]
|
||||
}
|
||||
|
||||
// ReleaseECPointBuffer releases the []byte buffer allocated.
|
||||
func ReleaseECPointBuffer(buf []byte) {
|
||||
buf = buf[:cap(buf)]
|
||||
buf[0] = 0x0
|
||||
for i := 1; i < len(buf); i *= 2 {
|
||||
copy(buf[i:], buf[:i])
|
||||
}
|
||||
buf = buf[:0]
|
||||
ecpointBufferPool.Put(&buf)
|
||||
}
|
||||
|
||||
func CalculateKeySize(crv elliptic.Curve) int {
|
||||
// We need to create a buffer that fits the entire curve.
|
||||
// If the curve size is 66, that fits in 9 bytes. If the curve
|
||||
// size is 64, it fits in 8 bytes.
|
||||
bits := crv.Params().BitSize
|
||||
|
||||
// For most common cases we know before hand what the byte length
|
||||
// is going to be. optimize
|
||||
var inBytes int
|
||||
switch bits {
|
||||
case 224, 256, 384: // TODO: use constant?
|
||||
inBytes = bits / 8
|
||||
case 521:
|
||||
inBytes = ec521BufferSize
|
||||
default:
|
||||
inBytes = bits / 8
|
||||
if (bits % 8) != 0 {
|
||||
inBytes++
|
||||
}
|
||||
}
|
||||
|
||||
return inBytes
|
||||
}
|
||||
|
||||
// AllocECPointBuffer allocates a buffer for the given point in the given
|
||||
// curve. This buffer should be released using the ReleaseECPointBuffer
|
||||
// function.
|
||||
func AllocECPointBuffer(v *big.Int, crv elliptic.Curve) []byte {
|
||||
buf := getCrvFixedBuffer(CalculateKeySize(crv))
|
||||
v.FillBytes(buf)
|
||||
return buf
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package dsig
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
func rsaGetSignerCryptoSignerKey(key any) (crypto.Signer, bool, error) {
|
||||
if !isValidRSAKey(key) {
|
||||
return nil, false, fmt.Errorf(`invalid key type %T for RSA algorithm`, key)
|
||||
}
|
||||
cs, isCryptoSigner := key.(crypto.Signer)
|
||||
if isCryptoSigner {
|
||||
return cs, true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// rsaPSSOptions returns the PSS options for RSA-PSS signatures with the specified hash.
|
||||
// The salt length is set to equal the hash length as per RFC 7518.
|
||||
func rsaPSSOptions(h crypto.Hash) rsa.PSSOptions {
|
||||
return rsa.PSSOptions{
|
||||
Hash: h,
|
||||
SaltLength: rsa.PSSSaltLengthEqualsHash,
|
||||
}
|
||||
}
|
||||
|
||||
// SignRSA generates an RSA signature for the given payload using the specified private key and options.
|
||||
// The raw parameter should be the pre-computed signing input (typically header.payload).
|
||||
// If pss is true, RSA-PSS is used; otherwise, PKCS#1 v1.5 is used.
|
||||
//
|
||||
// The rr parameter is an optional io.Reader that can be used to provide randomness for signing.
|
||||
// If rr is nil, it defaults to rand.Reader.
|
||||
func SignRSA(key *rsa.PrivateKey, payload []byte, h crypto.Hash, pss bool, rr io.Reader) ([]byte, error) {
|
||||
if !isValidRSAKey(key) {
|
||||
return nil, fmt.Errorf(`invalid key type %T for RSA algorithm`, key)
|
||||
}
|
||||
var opts crypto.SignerOpts = h
|
||||
if pss {
|
||||
rsaopts := rsaPSSOptions(h)
|
||||
opts = &rsaopts
|
||||
}
|
||||
return cryptosign(key, payload, h, opts, rr)
|
||||
}
|
||||
|
||||
// VerifyRSA verifies an RSA signature for the given payload and header.
|
||||
// This function constructs the signing input by encoding the header and payload according to JWS specification,
|
||||
// then verifies the signature using the specified public key and hash algorithm.
|
||||
// If pss is true, RSA-PSS verification is used; otherwise, PKCS#1 v1.5 verification is used.
|
||||
func VerifyRSA(key *rsa.PublicKey, payload, signature []byte, h crypto.Hash, pss bool) error {
|
||||
if !isValidRSAKey(key) {
|
||||
return fmt.Errorf(`invalid key type %T for RSA algorithm`, key)
|
||||
}
|
||||
hasher := h.New()
|
||||
hasher.Write(payload)
|
||||
digest := hasher.Sum(nil)
|
||||
if pss {
|
||||
return rsa.VerifyPSS(key, h, digest, signature, &rsa.PSSOptions{Hash: h, SaltLength: rsa.PSSSaltLengthEqualsHash})
|
||||
}
|
||||
return rsa.VerifyPKCS1v15(key, h, digest, signature)
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package dsig
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// 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.
|
||||
func Sign(key any, alg string, payload []byte, rr io.Reader) ([]byte, error) {
|
||||
info, ok := GetAlgorithmInfo(alg)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`dsig.Sign: unsupported signature algorithm %q`, alg)
|
||||
}
|
||||
|
||||
switch info.Family {
|
||||
case HMAC:
|
||||
return dispatchHMACSign(key, info, payload)
|
||||
case RSA:
|
||||
return dispatchRSASign(key, info, payload, rr)
|
||||
case ECDSA:
|
||||
return dispatchECDSASign(key, info, payload, rr)
|
||||
case EdDSAFamily:
|
||||
return dispatchEdDSASign(key, info, payload, rr)
|
||||
default:
|
||||
return nil, fmt.Errorf(`dsig.Sign: unsupported signature family %q`, info.Family)
|
||||
}
|
||||
}
|
||||
|
||||
func dispatchHMACSign(key any, info AlgorithmInfo, payload []byte) ([]byte, error) {
|
||||
meta, ok := info.Meta.(HMACFamilyMeta)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`dsig.Sign: invalid HMAC metadata`)
|
||||
}
|
||||
|
||||
var hmackey []byte
|
||||
if err := toHMACKey(&hmackey, key); err != nil {
|
||||
return nil, fmt.Errorf(`dsig.Sign: %w`, err)
|
||||
}
|
||||
return SignHMAC(hmackey, payload, meta.HashFunc)
|
||||
}
|
||||
|
||||
func dispatchRSASign(key any, info AlgorithmInfo, payload []byte, rr io.Reader) ([]byte, error) {
|
||||
meta, ok := info.Meta.(RSAFamilyMeta)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`dsig.Sign: invalid RSA metadata`)
|
||||
}
|
||||
|
||||
cs, isCryptoSigner, err := rsaGetSignerCryptoSignerKey(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`dsig.Sign: %w`, err)
|
||||
}
|
||||
if isCryptoSigner {
|
||||
var options crypto.SignerOpts = meta.Hash
|
||||
if meta.PSS {
|
||||
rsaopts := rsaPSSOptions(meta.Hash)
|
||||
options = &rsaopts
|
||||
}
|
||||
return SignCryptoSigner(cs, payload, meta.Hash, options, rr)
|
||||
}
|
||||
|
||||
privkey, ok := key.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`dsig.Sign: invalid key type %T. *rsa.PrivateKey is required`, key)
|
||||
}
|
||||
return SignRSA(privkey, payload, meta.Hash, meta.PSS, rr)
|
||||
}
|
||||
|
||||
func dispatchEdDSASign(key any, _ AlgorithmInfo, payload []byte, rr io.Reader) ([]byte, error) {
|
||||
signer, err := eddsaGetSigner(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`dsig.Sign: %w`, err)
|
||||
}
|
||||
|
||||
return SignCryptoSigner(signer, payload, crypto.Hash(0), crypto.Hash(0), rr)
|
||||
}
|
||||
|
||||
func dispatchECDSASign(key any, info AlgorithmInfo, payload []byte, rr io.Reader) ([]byte, error) {
|
||||
meta, ok := info.Meta.(ECDSAFamilyMeta)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`dsig.Sign: invalid ECDSA metadata`)
|
||||
}
|
||||
|
||||
privkey, cs, isCryptoSigner, err := ecdsaGetSignerKey(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`dsig.Sign: %w`, err)
|
||||
}
|
||||
if isCryptoSigner {
|
||||
return SignECDSACryptoSigner(cs, payload, meta.Hash, rr)
|
||||
}
|
||||
return SignECDSA(privkey, payload, meta.Hash, rr)
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package dsig
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
)
|
||||
|
||||
// isValidRSAKey validates that the provided key type is appropriate for RSA algorithms.
|
||||
// It returns false if the key is clearly incompatible (e.g., ECDSA or EdDSA keys).
|
||||
func isValidRSAKey(key any) bool {
|
||||
switch key.(type) {
|
||||
case
|
||||
ecdsa.PrivateKey, *ecdsa.PrivateKey,
|
||||
ed25519.PrivateKey:
|
||||
// these are NOT ok for RSA algorithms
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isValidECDSAKey validates that the provided key type is appropriate for ECDSA algorithms.
|
||||
// It returns false if the key is clearly incompatible (e.g., RSA or EdDSA keys).
|
||||
func isValidECDSAKey(key any) bool {
|
||||
switch key.(type) {
|
||||
case
|
||||
ed25519.PrivateKey,
|
||||
rsa.PrivateKey, *rsa.PrivateKey:
|
||||
// these are NOT ok for ECDSA algorithms
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isValidEDDSAKey validates that the provided key type is appropriate for EdDSA algorithms.
|
||||
// It returns false if the key is clearly incompatible (e.g., RSA or ECDSA keys).
|
||||
func isValidEDDSAKey(key any) bool {
|
||||
switch key.(type) {
|
||||
case
|
||||
ecdsa.PrivateKey, *ecdsa.PrivateKey,
|
||||
rsa.PrivateKey, *rsa.PrivateKey:
|
||||
// these are NOT ok for EdDSA algorithms
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// VerificationError represents an error that occurred during signature verification.
|
||||
type VerificationError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *VerificationError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
// NewVerificationError creates a new verification error with the given message.
|
||||
func NewVerificationError(message string) error {
|
||||
return &VerificationError{message: message}
|
||||
}
|
||||
|
||||
// IsVerificationError checks if the given error is a verification error.
|
||||
func IsVerificationError(err error) bool {
|
||||
_, ok := err.(*VerificationError)
|
||||
return ok
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package dsig
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
return fmt.Errorf(`dsig.Verify: unsupported signature algorithm %q`, alg)
|
||||
}
|
||||
|
||||
switch info.Family {
|
||||
case HMAC:
|
||||
return dispatchHMACVerify(key, info, payload, signature)
|
||||
case RSA:
|
||||
return dispatchRSAVerify(key, info, payload, signature)
|
||||
case ECDSA:
|
||||
return dispatchECDSAVerify(key, info, payload, signature)
|
||||
case EdDSAFamily:
|
||||
return dispatchEdDSAVerify(key, info, payload, signature)
|
||||
default:
|
||||
return fmt.Errorf(`dsig.Verify: unsupported signature family %q`, info.Family)
|
||||
}
|
||||
}
|
||||
|
||||
func dispatchHMACVerify(key any, info AlgorithmInfo, payload, signature []byte) error {
|
||||
meta, ok := info.Meta.(HMACFamilyMeta)
|
||||
if !ok {
|
||||
return fmt.Errorf(`dsig.Verify: invalid HMAC metadata`)
|
||||
}
|
||||
|
||||
var hmackey []byte
|
||||
if err := toHMACKey(&hmackey, key); err != nil {
|
||||
return fmt.Errorf(`dsig.Verify: %w`, err)
|
||||
}
|
||||
return VerifyHMAC(hmackey, payload, signature, meta.HashFunc)
|
||||
}
|
||||
|
||||
func dispatchRSAVerify(key any, info AlgorithmInfo, payload, signature []byte) error {
|
||||
meta, ok := info.Meta.(RSAFamilyMeta)
|
||||
if !ok {
|
||||
return fmt.Errorf(`dsig.Verify: 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.Verify: 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.Verify: failed to retrieve *rsa.PublicKey out of %T`, key)
|
||||
}
|
||||
}
|
||||
|
||||
return VerifyRSA(pubkey, payload, signature, meta.Hash, meta.PSS)
|
||||
}
|
||||
|
||||
func dispatchECDSAVerify(key any, info AlgorithmInfo, payload, signature []byte) error {
|
||||
meta, ok := info.Meta.(ECDSAFamilyMeta)
|
||||
if !ok {
|
||||
return fmt.Errorf(`dsig.Verify: invalid ECDSA metadata`)
|
||||
}
|
||||
|
||||
pubkey, cs, isCryptoSigner, err := ecdsaGetVerifierKey(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`dsig.Verify: %w`, err)
|
||||
}
|
||||
if isCryptoSigner {
|
||||
return VerifyECDSACryptoSigner(cs, payload, signature, meta.Hash)
|
||||
}
|
||||
return VerifyECDSA(pubkey, payload, signature, meta.Hash)
|
||||
}
|
||||
|
||||
func dispatchEdDSAVerify(key any, _ AlgorithmInfo, payload, signature []byte) error {
|
||||
var pubkey ed25519.PublicKey
|
||||
signer, ok := key.(crypto.Signer)
|
||||
if ok {
|
||||
v := signer.Public()
|
||||
pubkey, ok = v.(ed25519.PublicKey)
|
||||
if !ok {
|
||||
return fmt.Errorf(`dsig.Verify: expected crypto.Signer.Public() to return ed25519.PublicKey, but got %T`, v)
|
||||
}
|
||||
} else {
|
||||
var ok bool
|
||||
pubkey, ok = key.(ed25519.PublicKey)
|
||||
if !ok {
|
||||
return fmt.Errorf(`dsig.Verify: failed to retrieve ed25519.PublicKey out of %T`, key)
|
||||
}
|
||||
}
|
||||
|
||||
return VerifyEdDSA(pubkey, payload, signature)
|
||||
}
|
||||
|
||||
func ecdsaGetVerifierKey(key any) (*ecdsa.PublicKey, crypto.Signer, bool, error) {
|
||||
cs, isCryptoSigner := key.(crypto.Signer)
|
||||
if isCryptoSigner {
|
||||
switch key.(type) {
|
||||
case ecdsa.PublicKey, *ecdsa.PublicKey:
|
||||
// if it's ecdsa.PublicKey, it's more efficient to
|
||||
// go through the non-crypto.Signer route. Set isCryptoSigner to false
|
||||
isCryptoSigner = false
|
||||
}
|
||||
}
|
||||
|
||||
if isCryptoSigner {
|
||||
return nil, cs, true, nil
|
||||
}
|
||||
|
||||
pubkey, ok := key.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, nil, false, fmt.Errorf(`invalid key type %T. *ecdsa.PublicKey is required`, key)
|
||||
}
|
||||
|
||||
return pubkey, nil, false, nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 lestrrat-go
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
httpcc
|
||||
======
|
||||
|
||||
Parses HTTP/1.1 Cache-Control header, and returns a struct that is convenient
|
||||
for the end-user to do what they will with.
|
||||
|
||||
# Parsing the HTTP Request
|
||||
|
||||
```go
|
||||
dir, err := httpcc.ParseRequest(req.Header.Get(`Cache-Control`))
|
||||
// dir.MaxAge() uint64, bool
|
||||
// dir.MaxStale() uint64, bool
|
||||
// dir.MinFresh() uint64, bool
|
||||
// dir.NoCache() bool
|
||||
// dir.NoStore() bool
|
||||
// dir.NoTransform() bool
|
||||
// dir.OnlyIfCached() bool
|
||||
// dir.Extensions() map[string]string
|
||||
```
|
||||
|
||||
# Parsing the HTTP Response
|
||||
|
||||
```go
|
||||
directives, err := httpcc.ParseResponse(res.Header.Get(`Cache-Control`))
|
||||
// dir.MaxAge() uint64, bool
|
||||
// dir.MustRevalidate() bool
|
||||
// dir.NoCache() []string
|
||||
// dir.NoStore() bool
|
||||
// dir.NoTransform() bool
|
||||
// dir.Public() bool
|
||||
// dir.Private() bool
|
||||
// dir.SMaxAge() uint64, bool
|
||||
// dir.Extensions() map[string]string
|
||||
```
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package httpcc
|
||||
|
||||
type RequestDirective struct {
|
||||
maxAge *uint64
|
||||
maxStale *uint64
|
||||
minFresh *uint64
|
||||
noCache bool
|
||||
noStore bool
|
||||
noTransform bool
|
||||
onlyIfCached bool
|
||||
extensions map[string]string
|
||||
}
|
||||
|
||||
func (d *RequestDirective) MaxAge() (uint64, bool) {
|
||||
if v := d.maxAge; v != nil {
|
||||
return *v, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (d *RequestDirective) MaxStale() (uint64, bool) {
|
||||
if v := d.maxStale; v != nil {
|
||||
return *v, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (d *RequestDirective) MinFresh() (uint64, bool) {
|
||||
if v := d.minFresh; v != nil {
|
||||
return *v, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (d *RequestDirective) NoCache() bool {
|
||||
return d.noCache
|
||||
}
|
||||
|
||||
func (d *RequestDirective) NoStore() bool {
|
||||
return d.noStore
|
||||
}
|
||||
|
||||
func (d *RequestDirective) NoTransform() bool {
|
||||
return d.noTransform
|
||||
}
|
||||
|
||||
func (d *RequestDirective) OnlyIfCached() bool {
|
||||
return d.onlyIfCached
|
||||
}
|
||||
|
||||
func (d *RequestDirective) Extensions() map[string]string {
|
||||
return d.extensions
|
||||
}
|
||||
|
||||
func (d *RequestDirective) Extension(s string) string {
|
||||
return d.extensions[s]
|
||||
}
|
||||
|
||||
type ResponseDirective struct {
|
||||
maxAge *uint64
|
||||
noCache []string
|
||||
noStore bool
|
||||
noTransform bool
|
||||
public bool
|
||||
private []string
|
||||
proxyRevalidate bool
|
||||
sMaxAge *uint64
|
||||
extensions map[string]string
|
||||
}
|
||||
|
||||
func (d *ResponseDirective) MaxAge() (uint64, bool) {
|
||||
if v := d.maxAge; v != nil {
|
||||
return *v, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (d *ResponseDirective) NoCache() []string {
|
||||
return d.noCache
|
||||
}
|
||||
|
||||
func (d *ResponseDirective) NoStore() bool {
|
||||
return d.noStore
|
||||
}
|
||||
|
||||
func (d *ResponseDirective) NoTransform() bool {
|
||||
return d.noTransform
|
||||
}
|
||||
|
||||
func (d *ResponseDirective) Public() bool {
|
||||
return d.public
|
||||
}
|
||||
|
||||
func (d *ResponseDirective) Private() []string {
|
||||
return d.private
|
||||
}
|
||||
|
||||
func (d *ResponseDirective) ProxyRevalidate() bool {
|
||||
return d.proxyRevalidate
|
||||
}
|
||||
|
||||
func (d *ResponseDirective) SMaxAge() (uint64, bool) {
|
||||
if v := d.sMaxAge; v != nil {
|
||||
return *v, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (d *ResponseDirective) Extensions() map[string]string {
|
||||
return d.extensions
|
||||
}
|
||||
|
||||
func (d *ResponseDirective) Extension(s string) string {
|
||||
return d.extensions[s]
|
||||
}
|
||||
|
||||
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
package httpcc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
// Request Cache-Control directives
|
||||
MaxAge = "max-age" // used in response as well
|
||||
MaxStale = "max-stale"
|
||||
MinFresh = "min-fresh"
|
||||
NoCache = "no-cache" // used in response as well
|
||||
NoStore = "no-store" // used in response as well
|
||||
NoTransform = "no-transform" // used in response as well
|
||||
OnlyIfCached = "only-if-cached"
|
||||
|
||||
// Response Cache-Control directive
|
||||
MustRevalidate = "must-revalidate"
|
||||
Public = "public"
|
||||
Private = "private"
|
||||
ProxyRevalidate = "proxy-revalidate"
|
||||
SMaxAge = "s-maxage"
|
||||
)
|
||||
|
||||
type TokenPair struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
type TokenValuePolicy int
|
||||
|
||||
const (
|
||||
NoArgument TokenValuePolicy = iota
|
||||
TokenOnly
|
||||
QuotedStringOnly
|
||||
AnyTokenValue
|
||||
)
|
||||
|
||||
type directiveValidator interface {
|
||||
Validate(string) TokenValuePolicy
|
||||
}
|
||||
type directiveValidatorFn func(string) TokenValuePolicy
|
||||
|
||||
func (fn directiveValidatorFn) Validate(ccd string) TokenValuePolicy {
|
||||
return fn(ccd)
|
||||
}
|
||||
|
||||
func responseDirectiveValidator(s string) TokenValuePolicy {
|
||||
switch s {
|
||||
case MustRevalidate, NoStore, NoTransform, Public, ProxyRevalidate:
|
||||
return NoArgument
|
||||
case NoCache, Private:
|
||||
return QuotedStringOnly
|
||||
case MaxAge, SMaxAge:
|
||||
return TokenOnly
|
||||
default:
|
||||
return AnyTokenValue
|
||||
}
|
||||
}
|
||||
|
||||
func requestDirectiveValidator(s string) TokenValuePolicy {
|
||||
switch s {
|
||||
case MaxAge, MaxStale, MinFresh:
|
||||
return TokenOnly
|
||||
case NoCache, NoStore, NoTransform, OnlyIfCached:
|
||||
return NoArgument
|
||||
default:
|
||||
return AnyTokenValue
|
||||
}
|
||||
}
|
||||
|
||||
// ParseRequestDirective parses a single token.
|
||||
func ParseRequestDirective(s string) (*TokenPair, error) {
|
||||
return parseDirective(s, directiveValidatorFn(requestDirectiveValidator))
|
||||
}
|
||||
|
||||
func ParseResponseDirective(s string) (*TokenPair, error) {
|
||||
return parseDirective(s, directiveValidatorFn(responseDirectiveValidator))
|
||||
}
|
||||
|
||||
func parseDirective(s string, ccd directiveValidator) (*TokenPair, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
i := strings.IndexByte(s, '=')
|
||||
if i == -1 {
|
||||
return &TokenPair{Name: s}, nil
|
||||
}
|
||||
|
||||
pair := &TokenPair{Name: strings.TrimSpace(s[:i])}
|
||||
|
||||
if len(s) <= i {
|
||||
// `key=` feels like it's a parse error, but it's HTTP...
|
||||
// for now, return as if nothing happened.
|
||||
return pair, nil
|
||||
}
|
||||
|
||||
v := strings.TrimSpace(s[i+1:])
|
||||
switch ccd.Validate(pair.Name) {
|
||||
case TokenOnly:
|
||||
if v[0] == '"' {
|
||||
return nil, fmt.Errorf(`invalid value for %s (quoted string not allowed)`, pair.Name)
|
||||
}
|
||||
case QuotedStringOnly: // quoted-string only
|
||||
if v[0] != '"' {
|
||||
return nil, fmt.Errorf(`invalid value for %s (bare token not allowed)`, pair.Name)
|
||||
}
|
||||
tmp, err := strconv.Unquote(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`malformed quoted string in token`)
|
||||
}
|
||||
v = tmp
|
||||
case AnyTokenValue:
|
||||
if v[0] == '"' {
|
||||
tmp, err := strconv.Unquote(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`malformed quoted string in token`)
|
||||
}
|
||||
v = tmp
|
||||
}
|
||||
case NoArgument:
|
||||
if len(v) > 0 {
|
||||
return nil, fmt.Errorf(`received argument to directive %s`, pair.Name)
|
||||
}
|
||||
}
|
||||
|
||||
pair.Value = v
|
||||
return pair, nil
|
||||
}
|
||||
|
||||
func ParseResponseDirectives(s string) ([]*TokenPair, error) {
|
||||
return parseDirectives(s, ParseResponseDirective)
|
||||
}
|
||||
|
||||
func ParseRequestDirectives(s string) ([]*TokenPair, error) {
|
||||
return parseDirectives(s, ParseRequestDirective)
|
||||
}
|
||||
|
||||
func parseDirectives(s string, p func(string) (*TokenPair, error)) ([]*TokenPair, error) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(s))
|
||||
scanner.Split(scanCommaSeparatedWords)
|
||||
|
||||
var tokens []*TokenPair
|
||||
for scanner.Scan() {
|
||||
tok, err := p(scanner.Text())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to parse token #%d: %w`, len(tokens)+1, err)
|
||||
}
|
||||
tokens = append(tokens, tok)
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
// isSpace reports whether the character is a Unicode white space character.
|
||||
// We avoid dependency on the unicode package, but check validity of the implementation
|
||||
// in the tests.
|
||||
func isSpace(r rune) bool {
|
||||
if r <= '\u00FF' {
|
||||
// Obvious ASCII ones: \t through \r plus space. Plus two Latin-1 oddballs.
|
||||
switch r {
|
||||
case ' ', '\t', '\n', '\v', '\f', '\r':
|
||||
return true
|
||||
case '\u0085', '\u00A0':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
// High-valued ones.
|
||||
if '\u2000' <= r && r <= '\u200a' {
|
||||
return true
|
||||
}
|
||||
switch r {
|
||||
case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func scanCommaSeparatedWords(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
// Skip leading spaces.
|
||||
start := 0
|
||||
for width := 0; start < len(data); start += width {
|
||||
var r rune
|
||||
r, width = utf8.DecodeRune(data[start:])
|
||||
if !isSpace(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Scan until we find a comma. Keep track of consecutive whitespaces
|
||||
// so we remove them from the end result
|
||||
var ws int
|
||||
for width, i := 0, start; i < len(data); i += width {
|
||||
var r rune
|
||||
r, width = utf8.DecodeRune(data[i:])
|
||||
switch {
|
||||
case isSpace(r):
|
||||
ws++
|
||||
case r == ',':
|
||||
return i + width, data[start : i-ws], nil
|
||||
default:
|
||||
ws = 0
|
||||
}
|
||||
}
|
||||
|
||||
// If we're at EOF, we have a final, non-empty, non-terminated word. Return it.
|
||||
if atEOF && len(data) > start {
|
||||
return len(data), data[start : len(data)-ws], nil
|
||||
}
|
||||
|
||||
// Request more data.
|
||||
return start, nil, nil
|
||||
}
|
||||
|
||||
// ParseRequest parses the content of `Cache-Control` header of an HTTP Request.
|
||||
func ParseRequest(v string) (*RequestDirective, error) {
|
||||
var dir RequestDirective
|
||||
tokens, err := ParseRequestDirectives(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to parse tokens: %w`, err)
|
||||
}
|
||||
|
||||
for _, token := range tokens {
|
||||
name := strings.ToLower(token.Name)
|
||||
switch name {
|
||||
case MaxAge:
|
||||
iv, err := strconv.ParseUint(token.Value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to parse max-age: %w`, err)
|
||||
}
|
||||
dir.maxAge = &iv
|
||||
case MaxStale:
|
||||
iv, err := strconv.ParseUint(token.Value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to parse max-stale: %w`, err)
|
||||
}
|
||||
dir.maxStale = &iv
|
||||
case MinFresh:
|
||||
iv, err := strconv.ParseUint(token.Value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to parse min-fresh: %w`, err)
|
||||
}
|
||||
dir.minFresh = &iv
|
||||
case NoCache:
|
||||
dir.noCache = true
|
||||
case NoStore:
|
||||
dir.noStore = true
|
||||
case NoTransform:
|
||||
dir.noTransform = true
|
||||
case OnlyIfCached:
|
||||
dir.onlyIfCached = true
|
||||
default:
|
||||
dir.extensions[token.Name] = token.Value
|
||||
}
|
||||
}
|
||||
return &dir, nil
|
||||
}
|
||||
|
||||
// ParseResponse parses the content of `Cache-Control` header of an HTTP Response.
|
||||
func ParseResponse(v string) (*ResponseDirective, error) {
|
||||
tokens, err := ParseResponseDirectives(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to parse tokens: %w`, err)
|
||||
}
|
||||
|
||||
var dir ResponseDirective
|
||||
dir.extensions = make(map[string]string)
|
||||
for _, token := range tokens {
|
||||
name := strings.ToLower(token.Name)
|
||||
switch name {
|
||||
case MaxAge:
|
||||
iv, err := strconv.ParseUint(token.Value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to parse max-age: %w`, err)
|
||||
}
|
||||
dir.maxAge = &iv
|
||||
case NoCache:
|
||||
scanner := bufio.NewScanner(strings.NewReader(token.Value))
|
||||
scanner.Split(scanCommaSeparatedWords)
|
||||
for scanner.Scan() {
|
||||
dir.noCache = append(dir.noCache, scanner.Text())
|
||||
}
|
||||
case NoStore:
|
||||
dir.noStore = true
|
||||
case NoTransform:
|
||||
dir.noTransform = true
|
||||
case Public:
|
||||
dir.public = true
|
||||
case Private:
|
||||
scanner := bufio.NewScanner(strings.NewReader(token.Value))
|
||||
scanner.Split(scanCommaSeparatedWords)
|
||||
for scanner.Scan() {
|
||||
dir.private = append(dir.private, scanner.Text())
|
||||
}
|
||||
case ProxyRevalidate:
|
||||
dir.proxyRevalidate = true
|
||||
case SMaxAge:
|
||||
iv, err := strconv.ParseUint(token.Value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to parse s-maxage: %w`, err)
|
||||
}
|
||||
dir.sMaxAge = &iv
|
||||
default:
|
||||
dir.extensions[token.Name] = token.Value
|
||||
}
|
||||
}
|
||||
return &dir, nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
version: "2"
|
||||
linters:
|
||||
default: all
|
||||
disable:
|
||||
- cyclop
|
||||
- depguard
|
||||
- dupl
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- forbidigo
|
||||
- funcorder
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- godox
|
||||
- gosec
|
||||
- gosmopolitan
|
||||
- govet
|
||||
- inamedparam
|
||||
- ireturn
|
||||
- lll
|
||||
- maintidx
|
||||
- makezero
|
||||
- mnd
|
||||
- nakedret
|
||||
- nestif
|
||||
- nlreturn
|
||||
- noinlineerr
|
||||
- nonamedreturns
|
||||
- paralleltest
|
||||
- tagliatelle
|
||||
- testpackage
|
||||
- thelper
|
||||
- varnamelen
|
||||
- wrapcheck
|
||||
- wsl
|
||||
- wsl_v5
|
||||
settings:
|
||||
govet:
|
||||
disable:
|
||||
- shadow
|
||||
- fieldalignment
|
||||
enable-all: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- staticcheck
|
||||
path: /*.go
|
||||
text: 'ST1003: should not use underscores in package names'
|
||||
- linters:
|
||||
- revive
|
||||
path: /*.go
|
||||
text: don't use an underscore in package name
|
||||
- linters:
|
||||
- contextcheck
|
||||
- exhaustruct
|
||||
path: /*.go
|
||||
- linters:
|
||||
- errcheck
|
||||
path: /main.go
|
||||
- linters:
|
||||
- errcheck
|
||||
- errchkjson
|
||||
- forcetypeassert
|
||||
path: /*_test.go
|
||||
- linters:
|
||||
- forbidigo
|
||||
path: /*_example_test.go
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
Changes
|
||||
=======
|
||||
|
||||
v3.0.2 05 Dev 2025
|
||||
* Code changes mainly due to upgraded linter.
|
||||
* github.com/lestrrat-go/option upgraded to v2
|
||||
|
||||
v3.0.1 18 Aug 2025
|
||||
* Refresh() no longer requires the resource to be ready.
|
||||
|
||||
v3.0.0 5 Jun 2025
|
||||
[Breaking Changes]
|
||||
* The entire API has been re-imagined for Go versions that allow typed parameters
|
||||
|
||||
v2.0.0 19 Feb 2024
|
||||
[Breaking Changes]
|
||||
* `Fetcher` type is no longer available. You probably want to provide
|
||||
a customg HTTP client instead via httprc.WithHTTPClient()).
|
||||
*
|
||||
|
||||
v1.0.4 19 Jul 2022
|
||||
* Fix sloppy API breakage
|
||||
|
||||
v1.0.3 19 Jul 2022
|
||||
* Fix queue insertion in the middle of the queue (#7)
|
||||
|
||||
v1.0.2 13 Jun 2022
|
||||
* Properly release a lock when the fetch fails (#5)
|
||||
|
||||
v1.0.1 29 Mar 2022
|
||||
* Bump dependency for github.com/lestrrat-go/httpcc to v1.0.1
|
||||
|
||||
v1.0.0 29 Mar 2022
|
||||
* Initial release, refactored out of github.com/lestrrat-go/jwx
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 lestrrat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
# github.com/lestrrat-go/httprc/v3  [](https://pkg.go.dev/github.com/lestrrat-go/httprc/v3)
|
||||
|
||||
`httprc` is a HTTP "Refresh" Cache. Its aim is to cache a remote resource that
|
||||
can be fetched via HTTP, but keep the cached content up-to-date based on periodic
|
||||
refreshing.
|
||||
|
||||
# Client
|
||||
|
||||
A `httprc.Client` object is comprised of 3 parts: The user-facing controller API,
|
||||
the main controller loop, and set of workers that perform the actual fetching.
|
||||
|
||||
The user-facing controller API is the object returned when you call `(httprc.Client).Start`.
|
||||
|
||||
```go
|
||||
ctrl, _ := client.Start(ctx)
|
||||
```
|
||||
|
||||
# Controller API
|
||||
|
||||
The controller API gives you access to the controller backend that runs asynchronously.
|
||||
All methods take a `context.Context` object because they potentially block. You should
|
||||
be careful to use `context.WithTimeout` to properly set a timeout if you cannot tolerate
|
||||
a blocking operation.
|
||||
|
||||
# Main Controller Loop
|
||||
|
||||
The main controller loop is run asynchronously to the controller API. It is single threaded,
|
||||
and it has two reponsibilities.
|
||||
|
||||
The first is to receive commands from the controller API,
|
||||
and appropriately modify the state of the goroutine, i.e. modify the list of resources
|
||||
it is watching, performing forced refreshes, etc.
|
||||
|
||||
The other is to periodically wake up and go through the list of resources and re-fetch
|
||||
ones that are past their TTL (in reality, each resource carry a "next-check" time, not
|
||||
a TTL). The main controller loop itself does nothing more: it just kicks these checks periodically.
|
||||
|
||||
The interval between fetches is changed dynamically based on either the metadata carried
|
||||
with the HTTP responses, such as `Cache-Control` and `Expires` headers, or a constant
|
||||
interval set by the user for a given resource. Between these values, the main controller loop
|
||||
will pick the shortest interval (but no less than 1 second) and checks if resources
|
||||
need updating based on that value.
|
||||
|
||||
For example, if a resource A has an expiry of 10 minutes and if resource has an expiry of 5
|
||||
minutes, the main controller loop will attempt to wake up roughly every 5 minutes to check
|
||||
on the resources.
|
||||
|
||||
When the controller loop detects that a resource needs to be checked for freshness,
|
||||
it will send the resource to the worker pool to be synced.
|
||||
|
||||
# Interval calculation
|
||||
|
||||
After the resource is synced, the next fetch is scheduled. The interval to the next
|
||||
fetch is calculated either by using constant intervals, or by heuristics using values
|
||||
from the `http.Response` object.
|
||||
|
||||
If the constant interval is specified, no extra calculation is performed. If you specify
|
||||
a constant interval of 15 minutes, the resource will be checked every 15 minutes. This is
|
||||
predictable and reliable, but not necessarily efficient.
|
||||
|
||||
If you do not specify a constant interval, the HTTP response is analyzed for
|
||||
values in `Cache-Control` and `Expires` headers. These values will be compared against
|
||||
a maximum and minimum interval values, which default to 30 days and 15 minutes, respectively.
|
||||
If the values obtained from the headers fall within that range, the value from the header is
|
||||
used. If the value is larger than the maximum, the maximum is used. If the value is lower
|
||||
than the minimum, the minimum is used.
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
<!-- INCLUDE(client_example_test.go) -->
|
||||
```go
|
||||
package httprc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/httprc/v3"
|
||||
)
|
||||
|
||||
func ExampleClient() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
type HelloWorld struct {
|
||||
Hello string `json:"hello"`
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]string{"hello": "world"})
|
||||
}))
|
||||
|
||||
options := []httprc.NewClientOption{
|
||||
// By default the client will allow all URLs (which is what the option
|
||||
// below is explicitly specifying). If you want to restrict what URLs
|
||||
// are allowed, you can specify another whitelist.
|
||||
//
|
||||
// httprc.WithWhitelist(httprc.NewInsecureWhitelist()),
|
||||
}
|
||||
// If you would like to handle errors from asynchronous workers, you can specify a error sink.
|
||||
// This is disabled in this example because the trace logs are dynamic
|
||||
// and thus would interfere with the runnable example test.
|
||||
// options = append(options, httprc.WithErrorSink(errsink.NewSlog(slog.New(slog.NewJSONHandler(os.Stdout, nil)))))
|
||||
|
||||
// If you would like to see the trace logs, you can specify a trace sink.
|
||||
// This is disabled in this example because the trace logs are dynamic
|
||||
// and thus would interfere with the runnable example test.
|
||||
// options = append(options, httprc.WithTraceSink(tracesink.NewSlog(slog.New(slog.NewJSONHandler(os.Stdout, nil)))))
|
||||
|
||||
// Create a new client
|
||||
cl := httprc.NewClient(options...)
|
||||
|
||||
// Start the client, and obtain a Controller object
|
||||
ctrl, err := cl.Start(ctx)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
// The following is required if you want to make sure that there are no
|
||||
// dangling goroutines hanging around when you exit. For example, if you
|
||||
// are running tests to check for goroutine leaks, you should call this
|
||||
// function before the end of your test.
|
||||
defer ctrl.Shutdown(time.Second)
|
||||
|
||||
// Create a new resource that is synchronized every so often
|
||||
//
|
||||
// By default the client will attempt to fetch the resource once
|
||||
// as soon as it can, and then if no other metadata is provided,
|
||||
// it will fetch the resource every 15 minutes.
|
||||
//
|
||||
// If the resource responds with a Cache-Control/Expires header,
|
||||
// the client will attempt to respect that, and will try to fetch
|
||||
// the resource again based on the values obatained from the headers.
|
||||
r, err := httprc.NewResource[HelloWorld](srv.URL, httprc.JSONTransformer[HelloWorld]())
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Add the resource to the controller, so that it starts fetching.
|
||||
// By default, a call to `Add()` will block until the first fetch
|
||||
// succeeds, via an implicit call to `r.Ready()`
|
||||
// You can change this behavior if you specify the `WithWaitReady(false)`
|
||||
// option.
|
||||
ctrl.Add(ctx, r)
|
||||
|
||||
// if you specified `httprc.WithWaitReady(false)` option, the fetch will happen
|
||||
// "soon", but you're not guaranteed that it will happen before the next
|
||||
// call to `Lookup()`. If you want to make sure that the resource is ready,
|
||||
// you can call `Ready()` like so:
|
||||
/*
|
||||
{
|
||||
tctx, tcancel := context.WithTimeout(ctx, time.Second)
|
||||
defer tcancel()
|
||||
if err := r.Ready(tctx); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
*/
|
||||
m := r.Resource()
|
||||
fmt.Println(m.Hello)
|
||||
// OUTPUT:
|
||||
// world
|
||||
}
|
||||
```
|
||||
source: [client_example_test.go](https://github.com/lestrrat-go/httprc/blob/refs/heads/v3/client_example_test.go)
|
||||
<!-- END INCLUDE -->
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
package httprc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *ctrlBackend) adjustInterval(ctx context.Context, req adjustIntervalRequest) {
|
||||
interval := roundupToSeconds(time.Until(req.resource.Next()))
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: got adjust request (current tick interval=%s, next for %q=%s)", c.tickInterval, req.resource.URL(), interval))
|
||||
|
||||
if interval < time.Second {
|
||||
interval = time.Second
|
||||
}
|
||||
|
||||
if c.tickInterval < interval {
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: no adjusting required (time to next check %s > current tick interval %s)", interval, c.tickInterval))
|
||||
} else {
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: adjusting tick interval to %s", interval))
|
||||
c.tickInterval = interval
|
||||
c.check.Reset(interval)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ctrlBackend) addResource(ctx context.Context, req addRequest) {
|
||||
r := req.resource
|
||||
if _, ok := c.items[r.URL()]; ok {
|
||||
// Already exists
|
||||
sendReply(ctx, req.reply, struct{}{}, errResourceAlreadyExists)
|
||||
return
|
||||
}
|
||||
c.items[r.URL()] = r
|
||||
|
||||
if r.MaxInterval() == 0 {
|
||||
r.SetMaxInterval(c.defaultMaxInterval)
|
||||
}
|
||||
|
||||
if r.MinInterval() == 0 {
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: set minimum interval to %s", c.defaultMinInterval))
|
||||
r.SetMinInterval(c.defaultMinInterval)
|
||||
}
|
||||
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: added resource %q", r.URL()))
|
||||
sendReply(ctx, req.reply, struct{}{}, nil)
|
||||
c.SetTickInterval(time.Nanosecond)
|
||||
}
|
||||
|
||||
func (c *ctrlBackend) rmResource(ctx context.Context, req rmRequest) {
|
||||
u := req.u
|
||||
if _, ok := c.items[u]; !ok {
|
||||
sendReply(ctx, req.reply, struct{}{}, errResourceNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
delete(c.items, u)
|
||||
|
||||
minInterval := oneDay
|
||||
for _, item := range c.items {
|
||||
if d := item.MinInterval(); d < minInterval {
|
||||
minInterval = d
|
||||
}
|
||||
}
|
||||
|
||||
close(req.reply)
|
||||
c.check.Reset(minInterval)
|
||||
}
|
||||
|
||||
func (c *ctrlBackend) refreshResource(ctx context.Context, req refreshRequest) {
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: [refresh] START %q", req.u))
|
||||
defer c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: [refresh] END %q", req.u))
|
||||
u := req.u
|
||||
|
||||
r, ok := c.items[u]
|
||||
if !ok {
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: [refresh] %s is not registered", req.u))
|
||||
sendReply(ctx, req.reply, struct{}{}, errResourceNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Note: We don't wait for r.Ready() here because refresh should work
|
||||
// regardless of whether the resource has been fetched before. This allows
|
||||
// refresh to work with resources registered using WithWaitReady(false).
|
||||
|
||||
r.SetNext(time.Unix(0, 0))
|
||||
sendWorkerSynchronous(ctx, c.syncoutgoing, synchronousRequest{
|
||||
resource: r,
|
||||
reply: req.reply,
|
||||
})
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: [refresh] sync request for %s sent to worker pool", req.u))
|
||||
}
|
||||
|
||||
func (c *ctrlBackend) lookupResource(ctx context.Context, req lookupRequest) {
|
||||
u := req.u
|
||||
r, ok := c.items[u]
|
||||
if !ok {
|
||||
sendReply(ctx, req.reply, nil, errResourceNotFound)
|
||||
return
|
||||
}
|
||||
sendReply(ctx, req.reply, r, nil)
|
||||
}
|
||||
|
||||
func (c *ctrlBackend) handleRequest(ctx context.Context, req any) {
|
||||
switch req := req.(type) {
|
||||
case adjustIntervalRequest:
|
||||
c.adjustInterval(ctx, req)
|
||||
case addRequest:
|
||||
c.addResource(ctx, req)
|
||||
case rmRequest:
|
||||
c.rmResource(ctx, req)
|
||||
case refreshRequest:
|
||||
c.refreshResource(ctx, req)
|
||||
case lookupRequest:
|
||||
c.lookupResource(ctx, req)
|
||||
default:
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: unknown request type %T", req))
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
case <-ctx.Done():
|
||||
case ch <- r:
|
||||
}
|
||||
}
|
||||
|
||||
func sendReply[T any](ctx context.Context, ch chan backendResponse[T], v T, err error) {
|
||||
defer close(ch)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case ch <- backendResponse[T]{payload: v, err: err}:
|
||||
}
|
||||
}
|
||||
|
||||
type ctrlBackend struct {
|
||||
items map[string]Resource
|
||||
outgoing chan Resource
|
||||
syncoutgoing chan synchronousRequest
|
||||
incoming chan any // incoming requests to the controller
|
||||
traceSink TraceSink
|
||||
tickInterval time.Duration
|
||||
check *time.Ticker
|
||||
defaultMaxInterval time.Duration
|
||||
defaultMinInterval time.Duration
|
||||
}
|
||||
|
||||
func (c *ctrlBackend) loop(ctx context.Context, readywg, donewg *sync.WaitGroup) {
|
||||
c.traceSink.Put(ctx, "httprc controller: starting main controller loop")
|
||||
readywg.Done()
|
||||
defer c.traceSink.Put(ctx, "httprc controller: stopping main controller loop")
|
||||
defer donewg.Done()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ctrlBackend) periodicCheck(ctx context.Context, t time.Time) {
|
||||
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
|
||||
for _, item := range c.items {
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: checking resource %q", item.URL()))
|
||||
|
||||
next := item.Next()
|
||||
if minNext.IsZero() || next.Before(minNext) {
|
||||
minNext = next
|
||||
}
|
||||
|
||||
if interval := item.MinInterval(); minInterval < 0 || interval < minInterval {
|
||||
minInterval = interval
|
||||
}
|
||||
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: resource %q isBusy=%t, next(%s).After(%s)=%t", item.URL(), item.IsBusy(), next, t, next.After(t)))
|
||||
if item.IsBusy() || next.After(t) {
|
||||
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()))
|
||||
|
||||
dispatched++
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: dispatching resource %q to worker pool", item.URL()))
|
||||
sendWorker(ctx, c.outgoing, item)
|
||||
}
|
||||
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: dispatched %d resources", dispatched))
|
||||
|
||||
// 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
|
||||
// so we can send the resource to the worker pool
|
||||
if interval := time.Until(minNext); interval > 0 {
|
||||
c.SetTickInterval(roundupToSeconds(interval) + time.Second)
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: resetting check intervanl to %s", c.tickInterval))
|
||||
} else {
|
||||
// if we got here, either we have no resources, or all resources are busy.
|
||||
// In this state, it's possible that the interval is less than 1 second,
|
||||
// because we previously set it to a small value for an immediate refresh.
|
||||
// in this case, we want to reset it to a sane value
|
||||
if c.tickInterval < time.Second {
|
||||
c.SetTickInterval(minInterval)
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: resetting check intervanl to %s after forced refresh", c.tickInterval))
|
||||
}
|
||||
}
|
||||
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: next check in %s", c.tickInterval))
|
||||
}
|
||||
|
||||
func (c *ctrlBackend) SetTickInterval(d time.Duration) {
|
||||
// TODO synchronize
|
||||
if d <= 0 {
|
||||
d = time.Second // ensure positive interval
|
||||
}
|
||||
c.tickInterval = d
|
||||
c.check.Reset(d)
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
package httprc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/httprc/v3/errsink"
|
||||
"github.com/lestrrat-go/httprc/v3/proxysink"
|
||||
"github.com/lestrrat-go/httprc/v3/tracesink"
|
||||
)
|
||||
|
||||
// setupSink creates and starts a proxy for the given sink if it's not a Nop sink
|
||||
// Returns the sink to use and a cancel function that should be chained with the original cancel
|
||||
func setupSink[T any, S proxysink.Backend[T], NopType any](ctx context.Context, sink S, wg *sync.WaitGroup) (S, context.CancelFunc) {
|
||||
if _, ok := any(sink).(NopType); ok {
|
||||
return sink, func() {}
|
||||
}
|
||||
|
||||
proxy := proxysink.New[T](sink)
|
||||
wg.Add(1)
|
||||
go func(ctx context.Context, wg *sync.WaitGroup, proxy *proxysink.Proxy[T]) {
|
||||
defer wg.Done()
|
||||
proxy.Run(ctx)
|
||||
}(ctx, wg, proxy)
|
||||
|
||||
// proxy can be converted to one of the sink subtypes
|
||||
s, ok := any(proxy).(S)
|
||||
if !ok {
|
||||
panic("type assertion failed: proxy cannot be converted to type S")
|
||||
}
|
||||
return s, proxy.Close
|
||||
}
|
||||
|
||||
// Client is the main entry point for the httprc package.
|
||||
type Client struct {
|
||||
mu sync.Mutex
|
||||
httpcl HTTPClient
|
||||
numWorkers int
|
||||
running bool
|
||||
errSink ErrorSink
|
||||
traceSink TraceSink
|
||||
wl Whitelist
|
||||
defaultMaxInterval time.Duration
|
||||
defaultMinInterval time.Duration
|
||||
}
|
||||
|
||||
// NewClient creates a new `httprc.Client` object.
|
||||
//
|
||||
// By default ALL urls are allowed. This may not be suitable for you if
|
||||
// are using this in a production environment. You are encouraged to specify
|
||||
// a whitelist using the `WithWhitelist` option.
|
||||
//
|
||||
// NOTE: In future versions, this function signature should be changed to
|
||||
// return an error to properly handle option parsing failures.
|
||||
func NewClient(options ...NewClientOption) *Client {
|
||||
//nolint:staticcheck
|
||||
var errSink ErrorSink = errsink.NewNop()
|
||||
//nolint:staticcheck
|
||||
var traceSink TraceSink = tracesink.NewNop()
|
||||
var wl Whitelist = InsecureWhitelist{}
|
||||
var httpcl HTTPClient = http.DefaultClient
|
||||
|
||||
defaultMinInterval := DefaultMinInterval
|
||||
defaultMaxInterval := DefaultMaxInterval
|
||||
|
||||
numWorkers := DefaultWorkers
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identHTTPClient{}:
|
||||
_ = option.Value(&httpcl)
|
||||
case identWorkers{}:
|
||||
_ = option.Value(&numWorkers)
|
||||
case identErrorSink{}:
|
||||
_ = option.Value(&errSink)
|
||||
case identTraceSink{}:
|
||||
_ = option.Value(&traceSink)
|
||||
case identWhitelist{}:
|
||||
_ = option.Value(&wl)
|
||||
}
|
||||
}
|
||||
|
||||
if numWorkers <= 0 {
|
||||
numWorkers = 1
|
||||
}
|
||||
return &Client{
|
||||
httpcl: httpcl,
|
||||
numWorkers: numWorkers,
|
||||
errSink: errSink,
|
||||
traceSink: traceSink,
|
||||
wl: wl,
|
||||
|
||||
defaultMinInterval: defaultMinInterval,
|
||||
defaultMaxInterval: defaultMaxInterval,
|
||||
}
|
||||
}
|
||||
|
||||
// Start sets the client into motion. It will start a number of worker goroutines,
|
||||
// and return a Controller object that you can use to control the execution of
|
||||
// the client.
|
||||
//
|
||||
// If you attempt to call Start more than once, it will return an error.
|
||||
func (c *Client) Start(octx context.Context) (Controller, error) {
|
||||
c.mu.Lock()
|
||||
if c.running {
|
||||
c.mu.Unlock()
|
||||
return nil, errAlreadyRunning
|
||||
}
|
||||
c.running = true
|
||||
c.mu.Unlock()
|
||||
|
||||
// DON'T CANCEL THIS IN THIS METHOD! It's the responsibility of the
|
||||
// controller to cancel this context.
|
||||
ctx, cancel := context.WithCancel(octx)
|
||||
|
||||
var donewg sync.WaitGroup
|
||||
|
||||
// start proxy goroutines that will accept sink requests
|
||||
// and forward them to the appropriate sink
|
||||
errSink, errCancel := setupSink[error, ErrorSink, errsink.Nop](ctx, c.errSink, &donewg)
|
||||
traceSink, traceCancel := setupSink[string, TraceSink, tracesink.Nop](ctx, c.traceSink, &donewg)
|
||||
|
||||
// Chain the cancel functions
|
||||
ocancel := cancel
|
||||
cancel = func() {
|
||||
ocancel()
|
||||
errCancel()
|
||||
traceCancel()
|
||||
}
|
||||
|
||||
chbuf := c.numWorkers + 1
|
||||
incoming := make(chan any, chbuf)
|
||||
outgoing := make(chan Resource, chbuf)
|
||||
syncoutgoing := make(chan synchronousRequest, chbuf)
|
||||
|
||||
var readywg sync.WaitGroup
|
||||
readywg.Add(c.numWorkers)
|
||||
donewg.Add(c.numWorkers)
|
||||
for range c.numWorkers {
|
||||
wrk := worker{
|
||||
incoming: incoming,
|
||||
next: outgoing,
|
||||
nextsync: syncoutgoing,
|
||||
errSink: errSink,
|
||||
traceSink: traceSink,
|
||||
httpcl: c.httpcl,
|
||||
}
|
||||
go wrk.Run(ctx, &readywg, &donewg)
|
||||
}
|
||||
|
||||
tickInterval := oneDay
|
||||
ctrl := &controller{
|
||||
cancel: cancel,
|
||||
incoming: incoming,
|
||||
shutdown: make(chan struct{}),
|
||||
traceSink: traceSink,
|
||||
wl: c.wl,
|
||||
}
|
||||
|
||||
backend := &ctrlBackend{
|
||||
items: make(map[string]Resource),
|
||||
outgoing: outgoing,
|
||||
syncoutgoing: syncoutgoing,
|
||||
incoming: incoming,
|
||||
traceSink: traceSink,
|
||||
tickInterval: tickInterval,
|
||||
check: time.NewTicker(tickInterval),
|
||||
|
||||
defaultMinInterval: c.defaultMinInterval,
|
||||
defaultMaxInterval: c.defaultMaxInterval,
|
||||
}
|
||||
donewg.Add(1)
|
||||
readywg.Add(1)
|
||||
go backend.loop(ctx, &readywg, &donewg)
|
||||
|
||||
go func(wg *sync.WaitGroup, ch chan struct{}) {
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
}(&donewg, ctrl.shutdown)
|
||||
|
||||
readywg.Wait()
|
||||
|
||||
return ctrl, nil
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package httprc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Controller interface {
|
||||
// Add adds a new `http.Resource` to the controller. If the resource already exists,
|
||||
// it will return an error.
|
||||
Add(context.Context, Resource, ...AddOption) error
|
||||
|
||||
// Lookup a `httprc.Resource` by its URL. If the resource does not exist, it
|
||||
// will return an error.
|
||||
Lookup(context.Context, string) (Resource, error)
|
||||
|
||||
// Remove a `httprc.Resource` from the controller by its URL. If the resource does
|
||||
// not exist, it will return an error.
|
||||
Remove(context.Context, string) error
|
||||
|
||||
// Refresh forces a resource to be refreshed immediately. If the resource does
|
||||
// not exist, or if the refresh fails, it will return an error.
|
||||
Refresh(context.Context, string) error
|
||||
|
||||
ShutdownContext(context.Context) error
|
||||
Shutdown(time.Duration) error
|
||||
}
|
||||
|
||||
type controller struct {
|
||||
cancel context.CancelFunc
|
||||
incoming chan any // incoming requests to the controller
|
||||
shutdown chan struct{}
|
||||
traceSink TraceSink
|
||||
wl Whitelist
|
||||
}
|
||||
|
||||
// Shutdown is a convenience function that calls ShutdownContext with a
|
||||
// context that has a timeout of `timeout`.
|
||||
func (c *controller) Shutdown(timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
return c.ShutdownContext(ctx)
|
||||
}
|
||||
|
||||
// ShutdownContext stops the client and all associated goroutines, and waits for them
|
||||
// to finish. If the context is canceled, the function will return immediately:
|
||||
// there fore you should not use the context you used to start the client (because
|
||||
// presumably it's already canceled).
|
||||
//
|
||||
// Waiting for the client shutdown will also ensure that all sinks are properly
|
||||
// flushed.
|
||||
func (c *controller) ShutdownContext(ctx context.Context) error {
|
||||
c.cancel()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-c.shutdown:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type ctrlRequest[T any] struct {
|
||||
reply chan T
|
||||
resource Resource
|
||||
u string
|
||||
}
|
||||
type addRequest ctrlRequest[backendResponse[struct{}]]
|
||||
type rmRequest ctrlRequest[backendResponse[struct{}]]
|
||||
type refreshRequest ctrlRequest[backendResponse[struct{}]]
|
||||
type lookupRequest ctrlRequest[backendResponse[Resource]]
|
||||
type synchronousRequest ctrlRequest[backendResponse[struct{}]]
|
||||
type adjustIntervalRequest struct {
|
||||
resource Resource
|
||||
}
|
||||
|
||||
type backendResponse[T any] struct {
|
||||
payload T
|
||||
err error
|
||||
}
|
||||
|
||||
func sendBackend[TReq any, TB any](ctx context.Context, backendCh chan any, v TReq, replyCh chan backendResponse[TB]) (TB, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case backendCh <- v:
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
var zero TB
|
||||
return zero, ctx.Err()
|
||||
case res := <-replyCh:
|
||||
return res.payload, res.err
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup returns a resource by its URL. If the resource does not exist, it
|
||||
// will return an error.
|
||||
//
|
||||
// Unfortunately, due to the way typed parameters are handled in Go, we can only
|
||||
// return a Resource object (and not a ResourceBase[T] object). This means that
|
||||
// you will either need to use the `Resource.Get()` method or use a type
|
||||
// assertion to obtain a `ResourceBase[T]` to get to the actual object you are
|
||||
// looking for
|
||||
func (c *controller) Lookup(ctx context.Context, u string) (Resource, error) {
|
||||
reply := make(chan backendResponse[Resource], 1)
|
||||
req := lookupRequest{
|
||||
reply: reply,
|
||||
u: u,
|
||||
}
|
||||
return sendBackend[lookupRequest, Resource](ctx, c.incoming, req, reply)
|
||||
}
|
||||
|
||||
// Add adds a new resource to the controller. If the resource already
|
||||
// exists, it will return an error.
|
||||
//
|
||||
// By default this function will automatically wait for the resource to be
|
||||
// fetched once (by calling `r.Ready()`). Note that the `r.Ready()` call will NOT
|
||||
// timeout unless you configure your context object with `context.WithTimeout`.
|
||||
// To disable waiting, you can specify the `WithWaitReady(false)` option.
|
||||
func (c *controller) Add(ctx context.Context, r Resource, options ...AddOption) error {
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: START Add(%q)", r.URL()))
|
||||
defer c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: END Add(%q)", r.URL()))
|
||||
waitReady := true
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identWaitReady{}:
|
||||
if err := option.Value(&waitReady); err != nil {
|
||||
return fmt.Errorf(`httprc.Controller.Add: failed to parse WaitReady option: %w`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !c.wl.IsAllowed(r.URL()) {
|
||||
return fmt.Errorf(`httprc.Controller.AddResource: cannot add %q: %w`, r.URL(), errBlockedByWhitelist)
|
||||
}
|
||||
|
||||
reply := make(chan backendResponse[struct{}], 1)
|
||||
req := addRequest{
|
||||
reply: reply,
|
||||
resource: r,
|
||||
}
|
||||
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: sending add request for %q to backend", r.URL()))
|
||||
if _, err := sendBackend[addRequest, struct{}](ctx, c.incoming, req, reply); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a resource from the controller. If the resource does
|
||||
// not exist, it will return an error.
|
||||
func (c *controller) Remove(ctx context.Context, u string) error {
|
||||
reply := make(chan backendResponse[struct{}], 1)
|
||||
req := rmRequest{
|
||||
reply: reply,
|
||||
u: u,
|
||||
}
|
||||
if _, err := sendBackend[rmRequest, struct{}](ctx, c.incoming, req, reply); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh forces a resource to be refreshed immediately. If the resource does
|
||||
// not exist, or if the refresh fails, it will return an error.
|
||||
//
|
||||
// This function is synchronous, and will block until the resource has been refreshed.
|
||||
func (c *controller) Refresh(ctx context.Context, u string) error {
|
||||
reply := make(chan backendResponse[struct{}], 1)
|
||||
req := refreshRequest{
|
||||
reply: reply,
|
||||
u: u,
|
||||
}
|
||||
|
||||
if _, err := sendBackend[refreshRequest, struct{}](ctx, c.incoming, req, reply); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package httprc
|
||||
|
||||
import "errors"
|
||||
|
||||
var errResourceAlreadyExists = errors.New(`resource already exists`)
|
||||
|
||||
func ErrResourceAlreadyExists() error {
|
||||
return errResourceAlreadyExists
|
||||
}
|
||||
|
||||
var errAlreadyRunning = errors.New(`client is already running`)
|
||||
|
||||
func ErrAlreadyRunning() error {
|
||||
return errAlreadyRunning
|
||||
}
|
||||
|
||||
var errResourceNotFound = errors.New(`resource not found`)
|
||||
|
||||
func ErrResourceNotFound() error {
|
||||
return errResourceNotFound
|
||||
}
|
||||
|
||||
var errTransformerRequired = errors.New(`transformer is required`)
|
||||
|
||||
func ErrTransformerRequired() error {
|
||||
return errTransformerRequired
|
||||
}
|
||||
|
||||
var errURLCannotBeEmpty = errors.New(`URL cannot be empty`)
|
||||
|
||||
func ErrURLCannotBeEmpty() error {
|
||||
return errURLCannotBeEmpty
|
||||
}
|
||||
|
||||
var errUnexpectedStatusCode = errors.New(`unexpected status code`)
|
||||
|
||||
func ErrUnexpectedStatusCode() error {
|
||||
return errUnexpectedStatusCode
|
||||
}
|
||||
|
||||
var errTransformerFailed = errors.New(`failed to transform response body`)
|
||||
|
||||
func ErrTransformerFailed() error {
|
||||
return errTransformerFailed
|
||||
}
|
||||
|
||||
var errRecoveredFromPanic = errors.New(`recovered from panic`)
|
||||
|
||||
func ErrRecoveredFromPanic() error {
|
||||
return errRecoveredFromPanic
|
||||
}
|
||||
|
||||
var errBlockedByWhitelist = errors.New(`blocked by whitelist`)
|
||||
|
||||
func ErrBlockedByWhitelist() error {
|
||||
return errBlockedByWhitelist
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package errsink
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Put(context.Context, error)
|
||||
}
|
||||
|
||||
// Nop is an ErrorSink that does nothing. It does not require
|
||||
// any initialization, so the zero value can be used.
|
||||
type Nop struct{}
|
||||
|
||||
// NewNop returns a new NopErrorSink object. The constructor
|
||||
// is provided for consistency.
|
||||
func NewNop() Interface {
|
||||
return Nop{}
|
||||
}
|
||||
|
||||
// Put for NopErrorSink does nothing.
|
||||
func (Nop) Put(context.Context, error) {}
|
||||
|
||||
type SlogLogger interface {
|
||||
Log(context.Context, slog.Level, string, ...any)
|
||||
}
|
||||
|
||||
type slogSink struct {
|
||||
logger SlogLogger
|
||||
}
|
||||
|
||||
// NewSlog returns a new ErrorSink that logs errors using the provided slog.Logger
|
||||
func NewSlog(l SlogLogger) Interface {
|
||||
return &slogSink{
|
||||
logger: l,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *slogSink) Put(ctx context.Context, v error) {
|
||||
s.logger.Log(ctx, slog.LevelError, v.Error())
|
||||
}
|
||||
|
||||
// FuncSink is an ErrorSink that calls a function with the error.
|
||||
type FuncSink struct {
|
||||
fn func(context.Context, error)
|
||||
}
|
||||
|
||||
// NewFunc returns a new FuncSink that calls the provided function with errors.
|
||||
func NewFunc(fn func(context.Context, error)) Interface {
|
||||
return &FuncSink{fn: fn}
|
||||
}
|
||||
|
||||
// Put calls the function with the error.
|
||||
func (f *FuncSink) Put(ctx context.Context, err error) {
|
||||
if f.fn != nil {
|
||||
f.fn(ctx, err)
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package httprc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/httprc/v3/errsink"
|
||||
"github.com/lestrrat-go/httprc/v3/tracesink"
|
||||
)
|
||||
|
||||
// Buffer size constants
|
||||
const (
|
||||
// ReadBufferSize is the default buffer size for reading HTTP responses (10MB)
|
||||
ReadBufferSize = 1024 * 1024 * 10
|
||||
// MaxBufferSize is the maximum allowed buffer size (1GB)
|
||||
MaxBufferSize = 1024 * 1024 * 1000
|
||||
)
|
||||
|
||||
// Client worker constants
|
||||
const (
|
||||
// DefaultWorkers is the default number of worker goroutines
|
||||
DefaultWorkers = 5
|
||||
)
|
||||
|
||||
// Interval constants
|
||||
const (
|
||||
// DefaultMaxInterval is the default maximum interval between fetches (30 days)
|
||||
DefaultMaxInterval = 24 * time.Hour * 30
|
||||
// DefaultMinInterval is the default minimum interval between fetches (15 minutes)
|
||||
DefaultMinInterval = 15 * time.Minute
|
||||
// oneDay is used internally for time calculations
|
||||
oneDay = 24 * time.Hour
|
||||
)
|
||||
|
||||
// utility to round up intervals to the nearest second
|
||||
func roundupToSeconds(d time.Duration) time.Duration {
|
||||
if diff := d % time.Second; diff > 0 {
|
||||
return d + time.Second - diff
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// ErrorSink is an interface that abstracts a sink for errors.
|
||||
type ErrorSink = errsink.Interface
|
||||
|
||||
type TraceSink = tracesink.Interface
|
||||
|
||||
// HTTPClient is an interface that abstracts a "net/http".Client, so that
|
||||
// users can provide their own implementation of the HTTP client, if need be.
|
||||
type HTTPClient interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// Transformer is used to convert the body of an HTTP response into an appropriate
|
||||
// object of type T.
|
||||
type Transformer[T any] interface {
|
||||
Transform(context.Context, *http.Response) (T, error)
|
||||
}
|
||||
|
||||
// TransformFunc is a function type that implements the Transformer interface.
|
||||
type TransformFunc[T any] func(context.Context, *http.Response) (T, error)
|
||||
|
||||
func (f TransformFunc[T]) Transform(ctx context.Context, res *http.Response) (T, error) {
|
||||
return f(ctx, res)
|
||||
}
|
||||
|
||||
// Resource is a single resource that can be retrieved via HTTP, and (possibly) transformed
|
||||
// into an arbitrary object type.
|
||||
//
|
||||
// Realistically, there is no need for third-parties to implement this interface. This exists
|
||||
// to provide a way to aggregate `httprc.ResourceBase` objects with different specialized types
|
||||
// into a single collection.
|
||||
//
|
||||
// See ResourceBase for details
|
||||
type Resource interface { //nolint:interfacebloat
|
||||
Get(any) error
|
||||
Next() time.Time
|
||||
SetNext(time.Time)
|
||||
URL() string
|
||||
Sync(context.Context) error
|
||||
ConstantInterval() time.Duration
|
||||
MaxInterval() time.Duration
|
||||
SetMaxInterval(time.Duration)
|
||||
MinInterval() time.Duration
|
||||
SetMinInterval(time.Duration)
|
||||
IsBusy() bool
|
||||
SetBusy(bool)
|
||||
Ready(context.Context) error
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package httprc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/option/v2"
|
||||
)
|
||||
|
||||
type NewClientOption interface {
|
||||
option.Interface
|
||||
newClientOption()
|
||||
}
|
||||
|
||||
type newClientOption struct {
|
||||
option.Interface
|
||||
}
|
||||
|
||||
func (newClientOption) newClientOption() {}
|
||||
|
||||
type identWorkers struct{}
|
||||
|
||||
// WithWorkers specifies the number of concurrent workers to use for the client.
|
||||
// If n is less than or equal to 0, the client will use a single worker.
|
||||
func WithWorkers(n int) NewClientOption {
|
||||
return newClientOption{option.New(identWorkers{}, n)}
|
||||
}
|
||||
|
||||
type identErrorSink struct{}
|
||||
|
||||
// WithErrorSink specifies the error sink to use for the client.
|
||||
// If not specified, the client will use a NopErrorSink.
|
||||
func WithErrorSink(sink ErrorSink) NewClientOption {
|
||||
return newClientOption{option.New(identErrorSink{}, sink)}
|
||||
}
|
||||
|
||||
type identTraceSink struct{}
|
||||
|
||||
// WithTraceSink specifies the trace sink to use for the client.
|
||||
// If not specified, the client will use a NopTraceSink.
|
||||
func WithTraceSink(sink TraceSink) NewClientOption {
|
||||
return newClientOption{option.New(identTraceSink{}, sink)}
|
||||
}
|
||||
|
||||
type identWhitelist struct{}
|
||||
|
||||
// WithWhitelist specifies the whitelist to use for the client.
|
||||
// If not specified, the client will use a BlockAllWhitelist.
|
||||
func WithWhitelist(wl Whitelist) NewClientOption {
|
||||
return newClientOption{option.New(identWhitelist{}, wl)}
|
||||
}
|
||||
|
||||
type NewResourceOption interface {
|
||||
option.Interface
|
||||
newResourceOption()
|
||||
}
|
||||
|
||||
type newResourceOption struct {
|
||||
option.Interface
|
||||
}
|
||||
|
||||
func (newResourceOption) newResourceOption() {}
|
||||
|
||||
type NewClientResourceOption interface {
|
||||
option.Interface
|
||||
newResourceOption()
|
||||
newClientOption()
|
||||
}
|
||||
|
||||
type newClientResourceOption struct {
|
||||
option.Interface
|
||||
}
|
||||
|
||||
func (newClientResourceOption) newResourceOption() {}
|
||||
func (newClientResourceOption) newClientOption() {}
|
||||
|
||||
type identHTTPClient struct{}
|
||||
|
||||
// WithHTTPClient specifies the HTTP client to use for the client.
|
||||
// If not specified, the client will use http.DefaultClient.
|
||||
//
|
||||
// This option can be passed to NewClient or NewResource.
|
||||
func WithHTTPClient(cl HTTPClient) NewClientResourceOption {
|
||||
return newClientResourceOption{option.New(identHTTPClient{}, cl)}
|
||||
}
|
||||
|
||||
type identMinimumInterval struct{}
|
||||
|
||||
// WithMinInterval specifies the minimum interval between fetches.
|
||||
//
|
||||
// This option affects the dynamic calculation of the interval between fetches.
|
||||
// If the value calculated from the http.Response is less than the this value,
|
||||
// the client will use this value instead.
|
||||
func WithMinInterval(d time.Duration) NewResourceOption {
|
||||
return newResourceOption{option.New(identMinimumInterval{}, d)}
|
||||
}
|
||||
|
||||
type identMaximumInterval struct{}
|
||||
|
||||
// WithMaxInterval specifies the maximum interval between fetches.
|
||||
//
|
||||
// This option affects the dynamic calculation of the interval between fetches.
|
||||
// If the value calculated from the http.Response is greater than the this value,
|
||||
// the client will use this value instead.
|
||||
func WithMaxInterval(d time.Duration) NewResourceOption {
|
||||
return newResourceOption{option.New(identMaximumInterval{}, d)}
|
||||
}
|
||||
|
||||
type identConstantInterval struct{}
|
||||
|
||||
// WithConstantInterval specifies the interval between fetches. When you
|
||||
// specify this option, the client will fetch the resource at the specified
|
||||
// intervals, regardless of the response's Cache-Control or Expires headers.
|
||||
//
|
||||
// By default this option is disabled.
|
||||
func WithConstantInterval(d time.Duration) NewResourceOption {
|
||||
return newResourceOption{option.New(identConstantInterval{}, d)}
|
||||
}
|
||||
|
||||
type AddOption interface {
|
||||
option.Interface
|
||||
newAddOption()
|
||||
}
|
||||
|
||||
type addOption struct {
|
||||
option.Interface
|
||||
}
|
||||
|
||||
func (addOption) newAddOption() {}
|
||||
|
||||
type identWaitReady struct{}
|
||||
|
||||
// WithWaitReady specifies whether the client should wait for the resource to be
|
||||
// ready before returning from the Add method.
|
||||
//
|
||||
// By default, the client will wait for the resource to be ready before returning.
|
||||
// If you specify this option with a value of false, the client will not wait for
|
||||
// the resource to be fully registered, which is usually not what you want.
|
||||
// This option exists to accommodate for cases where you for some reason want to
|
||||
// add a resource to the controller, but want to do something else before
|
||||
// you wait for it. Make sure to call `r.Ready()` later on to ensure that
|
||||
// the resource is ready before you try to access it.
|
||||
func WithWaitReady(b bool) AddOption {
|
||||
return addOption{option.New(identWaitReady{}, b)}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package proxysink
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Backend[T any] interface {
|
||||
Put(context.Context, T)
|
||||
}
|
||||
|
||||
// Proxy is used to send values through a channel. This is used to
|
||||
// serialize calls to underlying sinks.
|
||||
type Proxy[T any] struct {
|
||||
mu *sync.Mutex
|
||||
cancel context.CancelFunc
|
||||
ch chan T
|
||||
cond *sync.Cond
|
||||
pending []T
|
||||
backend Backend[T]
|
||||
closed bool
|
||||
}
|
||||
|
||||
func New[T any](b Backend[T]) *Proxy[T] {
|
||||
mu := &sync.Mutex{}
|
||||
return &Proxy[T]{
|
||||
ch: make(chan T, 1),
|
||||
mu: mu,
|
||||
cond: sync.NewCond(mu),
|
||||
backend: b,
|
||||
cancel: func() {},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy[T]) Run(ctx context.Context) {
|
||||
defer p.cond.Broadcast()
|
||||
|
||||
p.mu.Lock()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
p.cancel = cancel
|
||||
p.mu.Unlock()
|
||||
|
||||
go p.controlloop(ctx)
|
||||
go p.flushloop(ctx)
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
func (p *Proxy[T]) controlloop(ctx context.Context) {
|
||||
defer p.cond.Broadcast()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case r := <-p.ch:
|
||||
p.mu.Lock()
|
||||
p.pending = append(p.pending, r)
|
||||
p.mu.Unlock()
|
||||
}
|
||||
p.cond.Broadcast()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy[T]) flushloop(ctx context.Context) {
|
||||
const defaultPendingSize = 10
|
||||
pending := make([]T, defaultPendingSize)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
p.mu.Lock()
|
||||
if len(p.pending) <= 0 {
|
||||
p.mu.Unlock()
|
||||
return
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
for len(p.pending) <= 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
p.mu.Unlock()
|
||||
return
|
||||
default:
|
||||
p.cond.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
// extract all pending values, and clear the shared slice
|
||||
if cap(pending) < len(p.pending) {
|
||||
pending = make([]T, len(p.pending))
|
||||
} else {
|
||||
pending = pending[:len(p.pending)]
|
||||
}
|
||||
copy(pending, p.pending)
|
||||
if cap(p.pending) > defaultPendingSize {
|
||||
p.pending = make([]T, 0, defaultPendingSize)
|
||||
} else {
|
||||
p.pending = p.pending[:0]
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
for _, v := range pending {
|
||||
// send to sink serially
|
||||
p.backend.Put(ctx, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy[T]) Put(ctx context.Context, v T) {
|
||||
p.mu.Lock()
|
||||
if p.closed {
|
||||
p.mu.Unlock()
|
||||
return
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case p.ch <- v:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy[T]) Close() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if !p.closed {
|
||||
p.closed = true
|
||||
}
|
||||
p.cancel()
|
||||
p.cond.Broadcast()
|
||||
}
|
||||
+366
@@ -0,0 +1,366 @@
|
||||
package httprc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/blackmagic"
|
||||
"github.com/lestrrat-go/httpcc"
|
||||
"github.com/lestrrat-go/httprc/v3/tracesink"
|
||||
)
|
||||
|
||||
// ResourceBase is a generic Resource type
|
||||
type ResourceBase[T any] struct {
|
||||
u string
|
||||
ready chan struct{} // closed when the resource is ready (i.e. after first successful fetch)
|
||||
once sync.Once
|
||||
httpcl HTTPClient
|
||||
t Transformer[T]
|
||||
r atomic.Value
|
||||
next atomic.Value
|
||||
interval time.Duration
|
||||
minInterval atomic.Int64
|
||||
maxInterval atomic.Int64
|
||||
busy atomic.Bool
|
||||
}
|
||||
|
||||
// NewResource creates a new Resource object which after fetching the
|
||||
// resource from the URL, will transform the response body using the
|
||||
// provided Transformer to an object of type T.
|
||||
//
|
||||
// This function will return an error if the URL is not a valid URL
|
||||
// (i.e. it cannot be parsed by url.Parse), or if the transformer is nil.
|
||||
func NewResource[T any](s string, transformer Transformer[T], options ...NewResourceOption) (*ResourceBase[T], error) {
|
||||
var httpcl HTTPClient
|
||||
var interval time.Duration
|
||||
minInterval := DefaultMinInterval
|
||||
maxInterval := DefaultMaxInterval
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identHTTPClient{}:
|
||||
if err := option.Value(&httpcl); err != nil {
|
||||
return nil, fmt.Errorf(`httprc.NewResource: failed to parse HTTPClient option: %w`, err)
|
||||
}
|
||||
case identMinimumInterval{}:
|
||||
if err := option.Value(&minInterval); err != nil {
|
||||
return nil, fmt.Errorf(`httprc.NewResource: failed to parse MinimumInterval option: %w`, err)
|
||||
}
|
||||
case identMaximumInterval{}:
|
||||
if err := option.Value(&maxInterval); err != nil {
|
||||
return nil, fmt.Errorf(`httprc.NewResource: failed to parse MaximumInterval option: %w`, err)
|
||||
}
|
||||
case identConstantInterval{}:
|
||||
if err := option.Value(&interval); err != nil {
|
||||
return nil, fmt.Errorf(`httprc.NewResource: failed to parse ConstantInterval option: %w`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if transformer == nil {
|
||||
return nil, fmt.Errorf(`httprc.NewResource: %w`, errTransformerRequired)
|
||||
}
|
||||
|
||||
if s == "" {
|
||||
return nil, fmt.Errorf(`httprc.NewResource: %w`, errURLCannotBeEmpty)
|
||||
}
|
||||
|
||||
if _, err := url.Parse(s); err != nil {
|
||||
return nil, fmt.Errorf(`httprc.NewResource: %w`, err)
|
||||
}
|
||||
r := &ResourceBase[T]{
|
||||
u: s,
|
||||
httpcl: httpcl,
|
||||
t: transformer,
|
||||
interval: interval,
|
||||
ready: make(chan struct{}),
|
||||
}
|
||||
if httpcl != nil {
|
||||
r.httpcl = httpcl
|
||||
}
|
||||
r.minInterval.Store(int64(minInterval))
|
||||
r.maxInterval.Store(int64(maxInterval))
|
||||
r.SetNext(time.Unix(0, 0)) // initially, it should be fetched immediately
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// URL returns the URL of the resource.
|
||||
func (r *ResourceBase[T]) URL() string {
|
||||
return r.u
|
||||
}
|
||||
|
||||
// Ready returns an empty error when the resource is ready. If the context
|
||||
// is canceled before the resource is ready, it will return the error from
|
||||
// the context.
|
||||
func (r *ResourceBase[T]) Ready(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-r.ready:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Get assigns the value of the resource to the provided pointer.
|
||||
// If using the `httprc.ResourceBase[T]` type directly, you can use the `Resource()`
|
||||
// method to get the resource directly.
|
||||
//
|
||||
// This method exists because parametric types cannot be assigned to a single object type
|
||||
// that return different return values of the specialized type. i.e. for resources
|
||||
// `ResourceBase[A]` and `ResourceBase[B]`, we cannot have a single interface that can
|
||||
// be assigned to the same interface type `X` that expects a `Resource()` method that
|
||||
// returns `A` or `B` depending on the type of the resource. When accessing the
|
||||
// resource through the `httprc.Resource` interface, use this method to obtain the
|
||||
// stored value.
|
||||
func (r *ResourceBase[T]) Get(dst any) error {
|
||||
return blackmagic.AssignIfCompatible(dst, r.Resource())
|
||||
}
|
||||
|
||||
// Resource returns the last fetched resource. If the resource has not been
|
||||
// fetched yet, this will return the zero value of type T.
|
||||
//
|
||||
// If you would rather wait until the resource is fetched, you can use the
|
||||
// `Ready()` method to wait until the resource is ready (i.e. fetched at least once).
|
||||
func (r *ResourceBase[T]) Resource() T {
|
||||
v := r.r.Load()
|
||||
switch v := v.(type) {
|
||||
case T:
|
||||
return v
|
||||
default:
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ResourceBase[T]) Next() time.Time {
|
||||
//nolint:forcetypeassert
|
||||
return r.next.Load().(time.Time)
|
||||
}
|
||||
|
||||
func (r *ResourceBase[T]) SetNext(v time.Time) {
|
||||
r.next.Store(v)
|
||||
}
|
||||
|
||||
func (r *ResourceBase[T]) ConstantInterval() time.Duration {
|
||||
return r.interval
|
||||
}
|
||||
|
||||
func (r *ResourceBase[T]) MaxInterval() time.Duration {
|
||||
return time.Duration(r.maxInterval.Load())
|
||||
}
|
||||
|
||||
func (r *ResourceBase[T]) MinInterval() time.Duration {
|
||||
return time.Duration(r.minInterval.Load())
|
||||
}
|
||||
|
||||
func (r *ResourceBase[T]) SetMaxInterval(v time.Duration) {
|
||||
r.maxInterval.Store(int64(v))
|
||||
}
|
||||
|
||||
func (r *ResourceBase[T]) SetMinInterval(v time.Duration) {
|
||||
r.minInterval.Store(int64(v))
|
||||
}
|
||||
|
||||
func (r *ResourceBase[T]) SetBusy(v bool) {
|
||||
r.busy.Store(v)
|
||||
}
|
||||
|
||||
func (r *ResourceBase[T]) IsBusy() bool {
|
||||
return r.busy.Load()
|
||||
}
|
||||
|
||||
// limitedBody is a wrapper around an io.Reader that will only read up to
|
||||
// MaxBufferSize bytes. This is provided to prevent the user from accidentally
|
||||
// reading a huge response body into memory
|
||||
type limitedBody struct {
|
||||
rdr io.Reader
|
||||
close func() error
|
||||
}
|
||||
|
||||
func (l *limitedBody) Read(p []byte) (n int, err error) {
|
||||
return l.rdr.Read(p)
|
||||
}
|
||||
|
||||
func (l *limitedBody) Close() error {
|
||||
return l.close()
|
||||
}
|
||||
|
||||
type traceSinkKey struct{}
|
||||
|
||||
func withTraceSink(ctx context.Context, sink TraceSink) context.Context {
|
||||
return context.WithValue(ctx, traceSinkKey{}, sink)
|
||||
}
|
||||
|
||||
func traceSinkFromContext(ctx context.Context) TraceSink {
|
||||
if v := ctx.Value(traceSinkKey{}); v != nil {
|
||||
//nolint:forcetypeassert
|
||||
return v.(TraceSink)
|
||||
}
|
||||
return tracesink.Nop{}
|
||||
}
|
||||
|
||||
type httpClientKey struct{}
|
||||
|
||||
func withHTTPClient(ctx context.Context, cl HTTPClient) context.Context {
|
||||
return context.WithValue(ctx, httpClientKey{}, cl)
|
||||
}
|
||||
|
||||
func httpClientFromContext(ctx context.Context) HTTPClient {
|
||||
if v := ctx.Value(httpClientKey{}); v != nil {
|
||||
//nolint:forcetypeassert
|
||||
return v.(HTTPClient)
|
||||
}
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
||||
func (r *ResourceBase[T]) Sync(ctx context.Context) error {
|
||||
traceSink := traceSinkFromContext(ctx)
|
||||
httpcl := r.httpcl
|
||||
if httpcl == nil {
|
||||
httpcl = httpClientFromContext(ctx)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, r.u, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`httprc.Resource.Sync: failed to create request: %w`, err)
|
||||
}
|
||||
|
||||
traceSink.Put(ctx, fmt.Sprintf("httprc.Resource.Sync: fetching %q", r.u))
|
||||
res, err := httpcl.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`httprc.Resource.Sync: failed to execute HTTP request: %w`, err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
next := r.calculateNextRefreshTime(ctx, res)
|
||||
traceSink.Put(ctx, fmt.Sprintf("httprc.Resource.Sync: next refresh time for %q is %v", r.u, next))
|
||||
r.SetNext(next)
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf(`httprc.Resource.Sync: %w (status code=%d, url=%q)`, errUnexpectedStatusCode, res.StatusCode, r.u)
|
||||
}
|
||||
|
||||
// replace the body of the response with a limited reader that
|
||||
// will only read up to MaxBufferSize bytes
|
||||
res.Body = &limitedBody{
|
||||
rdr: &io.LimitedReader{R: res.Body, N: MaxBufferSize},
|
||||
close: res.Body.Close,
|
||||
}
|
||||
traceSink.Put(ctx, fmt.Sprintf("httprc.Resource.Sync: transforming %q", r.u))
|
||||
v, err := r.transform(ctx, res)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`httprc.Resource.Sync: %w: %w`, errTransformerFailed, err)
|
||||
}
|
||||
|
||||
traceSink.Put(ctx, fmt.Sprintf("httprc.Resource.Sync: storing new value for %q", r.u))
|
||||
r.r.Store(v)
|
||||
r.once.Do(func() { close(r.ready) })
|
||||
traceSink.Put(ctx, fmt.Sprintf("httprc.Resource.Sync: stored value for %q", r.u))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ResourceBase[T]) transform(ctx context.Context, res *http.Response) (ret T, gerr error) {
|
||||
// Protect the call to Transform with a defer/recover block, so that even
|
||||
// if the Transform method panics, we can recover from it and return an error
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
gerr = fmt.Errorf(`httprc.Resource.transform: %w: %v`, errRecoveredFromPanic, recovered)
|
||||
}
|
||||
}()
|
||||
return r.t.Transform(ctx, res)
|
||||
}
|
||||
|
||||
func (r *ResourceBase[T]) determineNextFetchInterval(ctx context.Context, name string, fromHeader, minValue, maxValue time.Duration) time.Duration {
|
||||
traceSink := traceSinkFromContext(ctx)
|
||||
|
||||
if fromHeader > maxValue {
|
||||
traceSink.Put(ctx, fmt.Sprintf("httprc.Resource.Sync: %s %s > maximum interval, using maximum interval %s", r.URL(), name, maxValue))
|
||||
return maxValue
|
||||
}
|
||||
|
||||
if fromHeader < minValue {
|
||||
traceSink.Put(ctx, fmt.Sprintf("httprc.Resource.Sync: %s %s < minimum interval, using minimum interval %s", r.URL(), name, minValue))
|
||||
return minValue
|
||||
}
|
||||
|
||||
traceSink.Put(ctx, fmt.Sprintf("httprc.Resource.Sync: %s Using %s (%s)", r.URL(), name, fromHeader))
|
||||
return fromHeader
|
||||
}
|
||||
|
||||
func (r *ResourceBase[T]) calculateNextRefreshTime(ctx context.Context, res *http.Response) time.Time {
|
||||
traceSink := traceSinkFromContext(ctx)
|
||||
now := time.Now()
|
||||
|
||||
// If constant interval is set, use that regardless of what the
|
||||
// response headers say.
|
||||
if interval := r.ConstantInterval(); interval > 0 {
|
||||
traceSink.Put(ctx, fmt.Sprintf("httprc.Resource.Sync: %s Explicit interval set, using value %s", r.URL(), interval))
|
||||
return now.Add(interval)
|
||||
}
|
||||
|
||||
if interval := r.extractCacheControlMaxAge(ctx, res); interval > 0 {
|
||||
return now.Add(interval)
|
||||
}
|
||||
|
||||
if interval := r.extractExpiresInterval(ctx, res); interval > 0 {
|
||||
return now.Add(interval)
|
||||
}
|
||||
|
||||
traceSink.Put(ctx, fmt.Sprintf("httprc.Resource.Sync: %s No cache-control/expires headers found, using minimum interval", r.URL()))
|
||||
return now.Add(r.MinInterval())
|
||||
}
|
||||
|
||||
func (r *ResourceBase[T]) extractCacheControlMaxAge(ctx context.Context, res *http.Response) time.Duration {
|
||||
traceSink := traceSinkFromContext(ctx)
|
||||
|
||||
v := res.Header.Get(`Cache-Control`)
|
||||
if v == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
dir, err := httpcc.ParseResponse(v)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
maxAge, ok := dir.MaxAge()
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
|
||||
traceSink.Put(ctx, fmt.Sprintf("httprc.Resource.Sync: %s Cache-Control=max-age directive set (%d)", r.URL(), maxAge))
|
||||
return r.determineNextFetchInterval(
|
||||
ctx,
|
||||
"max-age",
|
||||
time.Duration(maxAge)*time.Second,
|
||||
r.MinInterval(),
|
||||
r.MaxInterval(),
|
||||
)
|
||||
}
|
||||
|
||||
func (r *ResourceBase[T]) extractExpiresInterval(ctx context.Context, res *http.Response) time.Duration {
|
||||
traceSink := traceSinkFromContext(ctx)
|
||||
|
||||
v := res.Header.Get(`Expires`)
|
||||
if v == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
expires, err := http.ParseTime(v)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
traceSink.Put(ctx, fmt.Sprintf("httprc.Resource.Sync: %s Expires header set (%s)", r.URL(), expires))
|
||||
return r.determineNextFetchInterval(
|
||||
ctx,
|
||||
"expires",
|
||||
time.Until(expires),
|
||||
r.MinInterval(),
|
||||
r.MaxInterval(),
|
||||
)
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package tracesink
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Put(context.Context, string)
|
||||
}
|
||||
|
||||
// Nop is an ErrorSink that does nothing. It does not require
|
||||
// any initialization, so the zero value can be used.
|
||||
type Nop struct{}
|
||||
|
||||
// NewNop returns a new NopTraceSink object. The constructor
|
||||
// is provided for consistency.
|
||||
func NewNop() Interface {
|
||||
return Nop{}
|
||||
}
|
||||
|
||||
// Put for NopTraceSink does nothing.
|
||||
func (Nop) Put(context.Context, string) {}
|
||||
|
||||
type slogSink struct {
|
||||
level slog.Level
|
||||
logger SlogLogger
|
||||
}
|
||||
|
||||
type SlogLogger interface {
|
||||
Log(context.Context, slog.Level, string, ...any)
|
||||
}
|
||||
|
||||
// NewSlog returns a new ErrorSink that logs errors using the provided slog.Logger
|
||||
func NewSlog(l SlogLogger) Interface {
|
||||
return &slogSink{
|
||||
level: slog.LevelInfo,
|
||||
logger: l,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *slogSink) Put(ctx context.Context, v string) {
|
||||
s.logger.Log(ctx, s.level, v)
|
||||
}
|
||||
|
||||
// Func is a TraceSink that calls a function with the trace message.
|
||||
type Func func(context.Context, string)
|
||||
|
||||
// Put calls the function with the trace message.
|
||||
func (f Func) Put(ctx context.Context, msg string) {
|
||||
f(ctx, msg)
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package httprc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type bytesTransformer struct{}
|
||||
|
||||
// BytesTransformer returns a Transformer that reads the entire response body
|
||||
// as a byte slice. This is the default Transformer used by httprc.Client
|
||||
func BytesTransformer() Transformer[[]byte] {
|
||||
return bytesTransformer{}
|
||||
}
|
||||
|
||||
func (bytesTransformer) Transform(_ context.Context, res *http.Response) ([]byte, error) {
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
|
||||
type jsonTransformer[T any] struct{}
|
||||
|
||||
// JSONTransformer returns a Transformer that decodes the response body as JSON
|
||||
// into the provided type T.
|
||||
func JSONTransformer[T any]() Transformer[T] {
|
||||
return jsonTransformer[T]{}
|
||||
}
|
||||
|
||||
func (jsonTransformer[T]) Transform(_ context.Context, res *http.Response) (T, error) {
|
||||
var v T
|
||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
var zero T
|
||||
return zero, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package httprc
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Whitelist is an interface that allows you to determine if a given URL is allowed
|
||||
// or not. Implementations of this interface can be used to restrict the URLs that
|
||||
// the client can access.
|
||||
//
|
||||
// By default all URLs are allowed, but this may not be ideal in production environments
|
||||
// for security reasons.
|
||||
//
|
||||
// This exists because you might use this module to store resources provided by
|
||||
// user of your application, in which case you cannot necessarily trust that the
|
||||
// URLs are safe.
|
||||
//
|
||||
// You will HAVE to provide some sort of whitelist.
|
||||
type Whitelist interface {
|
||||
IsAllowed(string) bool
|
||||
}
|
||||
|
||||
// WhitelistFunc is a function type that implements the Whitelist interface.
|
||||
type WhitelistFunc func(string) bool
|
||||
|
||||
func (f WhitelistFunc) IsAllowed(u string) bool { return f(u) }
|
||||
|
||||
// BlockAllWhitelist is a Whitelist implementation that blocks all URLs.
|
||||
type BlockAllWhitelist struct{}
|
||||
|
||||
// NewBlockAllWhitelist creates a new BlockAllWhitelist instance. It is safe to
|
||||
// use the zero value of this type; this constructor is provided for consistency.
|
||||
func NewBlockAllWhitelist() BlockAllWhitelist { return BlockAllWhitelist{} }
|
||||
|
||||
func (BlockAllWhitelist) IsAllowed(_ string) bool { return false }
|
||||
|
||||
// InsecureWhitelist is a Whitelist implementation that allows all URLs. Be careful
|
||||
// when using this in your production code: make sure you do not blindly register
|
||||
// URLs from untrusted sources.
|
||||
type InsecureWhitelist struct{}
|
||||
|
||||
// NewInsecureWhitelist creates a new InsecureWhitelist instance. It is safe to
|
||||
// use the zero value of this type; this constructor is provided for consistency.
|
||||
func NewInsecureWhitelist() InsecureWhitelist { return InsecureWhitelist{} }
|
||||
|
||||
func (InsecureWhitelist) IsAllowed(_ string) bool { return true }
|
||||
|
||||
// RegexpWhitelist is a jwk.Whitelist object comprised of a list of *regexp.Regexp
|
||||
// objects. All entries in the list are tried until one matches. If none of the
|
||||
// *regexp.Regexp objects match, then the URL is deemed unallowed.
|
||||
type RegexpWhitelist struct {
|
||||
mu sync.RWMutex
|
||||
patterns []*regexp.Regexp
|
||||
}
|
||||
|
||||
// NewRegexpWhitelist creates a new RegexpWhitelist instance. It is safe to use the
|
||||
// zero value of this type; this constructor is provided for consistency.
|
||||
func NewRegexpWhitelist() *RegexpWhitelist {
|
||||
return &RegexpWhitelist{}
|
||||
}
|
||||
|
||||
// Add adds a new regular expression to the list of expressions to match against.
|
||||
func (w *RegexpWhitelist) Add(pat *regexp.Regexp) *RegexpWhitelist {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
w.patterns = append(w.patterns, pat)
|
||||
return w
|
||||
}
|
||||
|
||||
// IsAllowed returns true if any of the patterns in the whitelist
|
||||
// returns true.
|
||||
func (w *RegexpWhitelist) IsAllowed(u string) bool {
|
||||
w.mu.RLock()
|
||||
patterns := w.patterns
|
||||
w.mu.RUnlock()
|
||||
for _, pat := range patterns {
|
||||
if pat.MatchString(u) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MapWhitelist is a jwk.Whitelist object comprised of a map of strings.
|
||||
// If the URL exists in the map, then the URL is allowed to be fetched.
|
||||
type MapWhitelist interface {
|
||||
Whitelist
|
||||
Add(string) MapWhitelist
|
||||
}
|
||||
|
||||
type mapWhitelist struct {
|
||||
mu sync.RWMutex
|
||||
store map[string]struct{}
|
||||
}
|
||||
|
||||
func NewMapWhitelist() MapWhitelist {
|
||||
return &mapWhitelist{store: make(map[string]struct{})}
|
||||
}
|
||||
|
||||
func (w *mapWhitelist) Add(pat string) MapWhitelist {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
w.store[pat] = struct{}{}
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *mapWhitelist) IsAllowed(u string) bool {
|
||||
w.mu.RLock()
|
||||
_, b := w.store[u]
|
||||
w.mu.RUnlock()
|
||||
return b
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package httprc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type worker struct {
|
||||
httpcl HTTPClient
|
||||
incoming chan any
|
||||
next <-chan Resource
|
||||
nextsync <-chan synchronousRequest
|
||||
errSink ErrorSink
|
||||
traceSink TraceSink
|
||||
}
|
||||
|
||||
func (w worker) Run(ctx context.Context, readywg *sync.WaitGroup, donewg *sync.WaitGroup) {
|
||||
w.traceSink.Put(ctx, "httprc worker: START worker loop")
|
||||
defer w.traceSink.Put(ctx, "httprc worker: END worker loop")
|
||||
defer donewg.Done()
|
||||
ctx = withTraceSink(ctx, w.traceSink)
|
||||
ctx = withHTTPClient(ctx, w.httpcl)
|
||||
|
||||
readywg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
w.traceSink.Put(ctx, "httprc worker: stopping worker loop")
|
||||
return
|
||||
case r := <-w.next:
|
||||
w.traceSink.Put(ctx, fmt.Sprintf("httprc worker: syncing %q (async)", r.URL()))
|
||||
if err := r.Sync(ctx); err != nil {
|
||||
w.errSink.Put(ctx, err)
|
||||
}
|
||||
r.SetBusy(false)
|
||||
|
||||
w.sendAdjustIntervalRequest(ctx, r)
|
||||
case sr := <-w.nextsync:
|
||||
w.traceSink.Put(ctx, fmt.Sprintf("httprc worker: syncing %q (synchronous)", sr.resource.URL()))
|
||||
if err := sr.resource.Sync(ctx); err != nil {
|
||||
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
|
||||
}
|
||||
w.traceSink.Put(ctx, fmt.Sprintf("httprc worker: SUCCESS syncing %q (synchronous)", sr.resource.URL()))
|
||||
sr.resource.SetBusy(false)
|
||||
sendReply(ctx, sr.reply, struct{}{}, nil)
|
||||
w.sendAdjustIntervalRequest(ctx, sr.resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w worker) sendAdjustIntervalRequest(ctx context.Context, r Resource) {
|
||||
w.traceSink.Put(ctx, "httprc worker: Sending interval adjustment request for "+r.URL())
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case w.incoming <- adjustIntervalRequest{resource: r}:
|
||||
}
|
||||
w.traceSink.Put(ctx, "httprc worker: Sent interval adjustment request for "+r.URL())
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
cmd
|
||||
bench
|
||||
examples
|
||||
tools
|
||||
+1
@@ -0,0 +1 @@
|
||||
import %workspace%/.aspect/bazelrc/bazel7.bazelrc
|
||||
+1
@@ -0,0 +1 @@
|
||||
8.3.1
|
||||
@@ -0,0 +1,39 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
*~
|
||||
|
||||
coverage.out
|
||||
|
||||
# I redirect my test output to files named "out" way too often
|
||||
out
|
||||
|
||||
cmd/jwx/jwx
|
||||
|
||||
bazel-*
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
version: "2"
|
||||
linters:
|
||||
default: all
|
||||
disable:
|
||||
- cyclop
|
||||
- depguard
|
||||
- dupl
|
||||
- err113
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- funcorder
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- godox
|
||||
- gosec
|
||||
- gosmopolitan
|
||||
- govet
|
||||
- inamedparam
|
||||
- ireturn
|
||||
- lll
|
||||
- maintidx
|
||||
- makezero
|
||||
- mnd
|
||||
- nakedret
|
||||
- nestif
|
||||
- nlreturn
|
||||
- noinlineerr
|
||||
- nonamedreturns
|
||||
- paralleltest
|
||||
- perfsprint
|
||||
- staticcheck
|
||||
- recvcheck
|
||||
- tagliatelle
|
||||
- testifylint
|
||||
- testpackage
|
||||
- thelper
|
||||
- varnamelen
|
||||
- wrapcheck
|
||||
- wsl
|
||||
- wsl_v5
|
||||
settings:
|
||||
govet:
|
||||
disable:
|
||||
- shadow
|
||||
- fieldalignment
|
||||
enable-all: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- staticcheck
|
||||
path: /*.go
|
||||
text: 'ST1003: should not use underscores in package names'
|
||||
- linters:
|
||||
- revive
|
||||
path: /*.go
|
||||
text: don't use an underscore in package name
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: SA1019
|
||||
- linters:
|
||||
- contextcheck
|
||||
- exhaustruct
|
||||
path: /*.go
|
||||
- linters:
|
||||
- errcheck
|
||||
path: /main.go
|
||||
- linters:
|
||||
- errcheck
|
||||
path: internal/codegen/codegen.go
|
||||
- linters:
|
||||
- errcheck
|
||||
- errchkjson
|
||||
- forcetypeassert
|
||||
path: internal/jwxtest/jwxtest.go
|
||||
- linters:
|
||||
- errcheck
|
||||
- errchkjson
|
||||
- forcetypeassert
|
||||
path: /*_test.go
|
||||
- linters:
|
||||
- forbidigo
|
||||
path: /*_example_test.go
|
||||
- linters:
|
||||
- forbidigo
|
||||
path: cmd/jwx/jwx.go
|
||||
- linters:
|
||||
- revive
|
||||
path: /*_test.go
|
||||
text: 'var-naming: '
|
||||
- linters:
|
||||
- revive
|
||||
path: internal/tokens/jwe_tokens.go
|
||||
text: "don't use ALL_CAPS in Go names"
|
||||
- linters:
|
||||
- revive
|
||||
path: jwt/internal/types/
|
||||
text: "var-naming: avoid meaningless package names"
|
||||
- linters:
|
||||
- godoclint
|
||||
path: (^|/)internal/
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
load("@rules_go//go:def.bzl", "go_library", "go_test")
|
||||
load("@gazelle//:def.bzl", "gazelle")
|
||||
|
||||
# gazelle:prefix github.com/lestrrat-go/jwx/v3
|
||||
# gazelle:go_naming_convention import_alias
|
||||
|
||||
gazelle(name = "gazelle")
|
||||
|
||||
go_library(
|
||||
name = "jwx",
|
||||
srcs = [
|
||||
"format.go",
|
||||
"formatkind_string_gen.go",
|
||||
"jwx.go",
|
||||
"options.go",
|
||||
],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//internal/json",
|
||||
"//internal/tokens",
|
||||
"@com_github_lestrrat_go_option_v2//:option",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "jwx_test",
|
||||
srcs = ["jwx_test.go"],
|
||||
deps = [
|
||||
":jwx",
|
||||
"//internal/jose",
|
||||
"//internal/json",
|
||||
"//internal/jwxtest",
|
||||
"//jwa",
|
||||
"//jwe",
|
||||
"//jwk",
|
||||
"//jwk/ecdsa",
|
||||
"//jws",
|
||||
"@com_github_stretchr_testify//require",
|
||||
],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":jwx",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
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.0.13 12 Jan 2026
|
||||
* [jwt] The `jwt.WithContext()` option is now properly being passed to `jws.Verify()` from
|
||||
`jwt.Parse()`.
|
||||
* [jwx] github.com/lestrrat-go/httprc/v3 has been upgraded to remove dependency on
|
||||
github.com/lestrrat-go/option (v1)
|
||||
* [jwk] `jwk.Clone()` has been fixed to properly work with private fields.
|
||||
|
||||
v3.0.12 20 Oct 2025
|
||||
* [jwe] As part of the next change, now per-recipient headers that are empty
|
||||
are no longer serialized in flattened JSON serialization.
|
||||
|
||||
* [jwe] Introduce `jwe.WithLegacyHeaderMerging(bool)` option to control header
|
||||
merging behavior in during JWE encryption. This only applies to flattened
|
||||
JSON serialization.
|
||||
|
||||
Previously, when using flattened JSON serialization (i.e. you specified
|
||||
JSON serialization via `jwe.WithJSON()` and only supplied one key), per-recipient
|
||||
headers were merged into the protected headers during encryption, and then
|
||||
were left to be included in the final serialization as-is. This caused duplicate
|
||||
headers to be present in both the protected headers and the per-recipient headers.
|
||||
|
||||
Since there may be users who rely on this behavior already, instead of changing the
|
||||
default behavior to fix this duplication, a new option to `jwe.Encrypt()` was added
|
||||
to allow clearing the per-recipient headers after merging to leave the `"headers"`
|
||||
field empty. This in effect makes the flattened JSON serialization more similar to
|
||||
the compact serialization, where there are no per-recipient headers present, and
|
||||
leaves the headers disjoint.
|
||||
|
||||
Note that in compact mode, there are no per-recipient headers and thus the
|
||||
headers need to be merged regardless. In full JSON serialization, we never
|
||||
merge the headers, so it is left up to the user to keep the headers disjoint.
|
||||
|
||||
* [jws] Calling the deprecated `jws.NewSigner()` function for the first time will cause
|
||||
legacy signers to be loaded automatically. Previously, you had to explicitly
|
||||
call `jws.Settings(jws.WithLegacySigners(true))` to enable legacy signers.
|
||||
|
||||
We incorrectly assumed that users would not be using `jws.NewSigner()`, and thus
|
||||
disabled legacy signers by default. However, it turned out that some users
|
||||
were using `jws.NewSigner()` in their code, which lead to breakages in
|
||||
existing code. In hindsight we should have known that any API made public before will
|
||||
be used by _somebody_.
|
||||
|
||||
As a side effect, jws.Settings(jws.WithLegacySigners(...)) is now a no-op.
|
||||
|
||||
However, please do note that jws.Signer (and similar) objects were always intended to be
|
||||
used for _registering_ new signing/verifying algorithms, and not for end users to actually
|
||||
use them directly. If you are using them for other purposes, please consider changing
|
||||
your code, as it is more than likely that we will somehow deprecate/remove/discouraged
|
||||
their use in the future.
|
||||
|
||||
v3.0.11 14 Sep 2025
|
||||
* [jwk] Add `(jwk.Cache).Shutdown()` method that delegates to the httprc controller
|
||||
object, to shutdown the cache.
|
||||
* [jwk] Change timing of `res.Body.Close()` call
|
||||
* [jwe] Previously, ecdh.PrivateKey/ecdh.PublicKey were not properly handled
|
||||
when used for encryption, which has been fixed.
|
||||
* [jws/jwsbb] (EXPERIMENTAL/BREAKS COMPATIBILITY) Convert most functions into
|
||||
thin wrappers around functions from github.com/lestrrat-go/dsig package.
|
||||
As a related change, HAMCHashFuncFor/RSAHashFuncFor/ECDSAHashFuncFor/RSAPSSOptions
|
||||
have been removed or unexported.
|
||||
Users of this module should be using jwsbb.Sign() and jwsbb.Verify() instead of
|
||||
algorithm specific jwsbb.SignRSA()/jwsbb.VerifyRSA() and such. If you feel the
|
||||
need to use these functions, you should use github.com/lestrrat-go/dsig directly.
|
||||
|
||||
v3.0.10 04 Aug 2025
|
||||
* [jws/jwsbb] Add `jwsbb.ErrHeaderNotFound()` to return the same error type as when
|
||||
a non-existent header is requested. via `HeaderGetXXX()` functions. Previously, this
|
||||
function was called `jwsbb.ErrFieldNotFound()`, but it was a misnomer.
|
||||
* [jws/jwsbb] Fix a bug where error return values from `HeaderGetXXX()` functions
|
||||
could not be matched against `jwsbb.ErrHeaderNotFound()` using `errors.Is()`.
|
||||
|
||||
v3.0.9 31 Jul 2025
|
||||
* [jws/jwsbb] `HeaderGetXXX()` functions now return errors when
|
||||
the requested header is not found, or if the value cannot be
|
||||
converted to the requested type.
|
||||
|
||||
* [jwt] `(jwt.Token).Get` methods now return specific types of errors depending
|
||||
on if a) the specified claim was not present, or b) the specified claim could
|
||||
not be assigned to the destination variable.
|
||||
|
||||
You can distinguish these by using `errors.Is` against `jwt.ClaimNotFoundError()`
|
||||
or `jwt.ClaimAssignmentFailedError()`
|
||||
|
||||
v3.0.8 27 Jun 2025
|
||||
* [jwe/jwebb] (EXPERIMENTAL) Add low-level functions for JWE operations.
|
||||
* [jws/jwsbb] (EXPERIMENTAL/BREAKS COMPATIBILITY) Add io.Reader parameter
|
||||
so your choice of source of randomness can be passed. Defaults to crypto/rand.Reader.
|
||||
Function signatures around jwsbb.Sign() now accept an addition `rr io.Reader`,
|
||||
which can be nil for 99% of use cases.
|
||||
* [jws/jwsbb] Add HeaderParse([]byte), where it is expected that the header
|
||||
is already in its base64 decoded format.
|
||||
* misc: replace `interface{}` with `any`
|
||||
|
||||
v3.0.7 16 Jun 2025
|
||||
* [jws/jwsbb] (EXPERIMENTAL) Add low-level fast access to JWS headers in compact
|
||||
serialization form.
|
||||
* [jws] Fix error reporting when no key matched for a signature.
|
||||
* [jws] Refactor jws signer setup.
|
||||
* Known algorithms are now implemented completely in the jws/jwsbb package.
|
||||
* VerifierFor and SignerFor now always succeed, and will also return a Signer2
|
||||
or Verifier2 that wraps the legacy Signer or Verifier if one is registered.
|
||||
|
||||
v3.0.6 13 Jun 2025
|
||||
* This release contains various performance improvements all over the code.
|
||||
No, this time for real. In particular, the most common case for signing
|
||||
a JWT with a key is approx 70% more efficient based on the number of allocations.
|
||||
|
||||
Please read the entry for the (retracted) v3.0.4 for what else I have to
|
||||
say about performance improvements
|
||||
|
||||
* [jwt] Added fast-path for token signing and verification. The fast path
|
||||
is triggered if you only pass `jwt.Sign()` and `jwt.Parse()` one options each
|
||||
(`jwt.WithKey()`), with no suboptions.
|
||||
|
||||
* [jws] Major refactoring around basic operations:
|
||||
|
||||
* How to work with Signer/Verifier have completely changed. Please take
|
||||
a look at examples/jws_custom_signer_verifier_example_test.go for how
|
||||
to do it the new way. The old way still works, but it WILL be removed
|
||||
when v4 arrives.
|
||||
* Related to the above, old code has been moved to `jws/legacy`.
|
||||
|
||||
* A new package `jws/jwsbb` has been added. `bb` stands for building blocks.
|
||||
This package separates out the low-level JWS operations into its own
|
||||
package. So if you are looking for just the signing of a payload with
|
||||
a key, this is it.
|
||||
|
||||
`jws/jwsbb` is currently considered to be EXPERIMENTAL.
|
||||
|
||||
v3.0.5 11 Jun 2025
|
||||
* Retract v3.0.4
|
||||
* Code for v3.0.3 is the same as v3.0.3
|
||||
|
||||
v3.0.4 09 Jun 2025
|
||||
* This release contains various performance improvements all over the code.
|
||||
|
||||
Because of the direction that this library is taking, we have always been
|
||||
more focused on correctness and usability/flexibility over performance.
|
||||
|
||||
It just so happens that I had a moment of inspiration and decided to see
|
||||
just how good our AI-based coding agents are in this sort of analysis-heavy tasks.
|
||||
|
||||
Long story short, the AI was fairly good at identifying suspicious code with
|
||||
an okay accuracy, but completely failed to make any meaningful changes to the
|
||||
code in a way that both did not break the code _and_ improved performance.
|
||||
I am sure that they will get better in the near future, but for now,
|
||||
I had to do the changes myself. I should clarify to their defence that
|
||||
the AI was very helpful in writing cumbersome benchmark code for me.
|
||||
|
||||
The end result is that we have anywhere from 10 to 30% performance improvements
|
||||
in various parts of the code that we touched, based on number of allocations.
|
||||
We believe that this would be a significant improvement for many users.
|
||||
|
||||
For further improvements, we can see that there would be a clear benefit to
|
||||
writing optimized code path that is designed to serve the most common cases.
|
||||
For example, for the case of signing JWTs with a single key, we could provide
|
||||
a path that skips a lot of extra processing (we kind of did that in this change,
|
||||
but we _could_ go ever harder in this direction). However, it is a trade-off between
|
||||
maintainability and performance, and as I am currently the sole maintainer of
|
||||
this library for the time being, I only plan to pursue such a route where it
|
||||
requires minimal effort on my part.
|
||||
|
||||
If you are interested in helping out in this area, I hereby thank you in advance.
|
||||
However, please be perfectly clear that unlike other types of changes, for performance
|
||||
related changes, the balance between the performance gains and maintainability is
|
||||
top priority. If you have good ideas and code, they will always be welcome, but
|
||||
please be prepared to justify your changes.
|
||||
|
||||
Finally, thank you for using this library!
|
||||
|
||||
v3.0.3 06 Jun 2025
|
||||
* Update some dependencies
|
||||
* [jwe] Change some error messages to contain more context information
|
||||
|
||||
v3.0.2 03 Jun 2025
|
||||
* [transform] (EXPERIMENTAL) Add utility function `transform.AsMap` to convert a
|
||||
Mappable object to a map[string]interface{}. This is useful for converting
|
||||
objects such as `jws.Header`, `jwk.Key`, `jwt.Token`, etc. to a map that can
|
||||
be used with other libraries that expect a map.
|
||||
* [jwt] (EXPERIMENTAL) Added token filtering functionality through the TokenFilter interface.
|
||||
* [jwt/openid] (EXPERIMENTAL) Added StandardClaimsFilter() for filtering standard OpenID claims.
|
||||
* [jws] (EXPERIMENTAL) Added header filtering functionality through the HeaderFilter interface.
|
||||
* [jwe] (EXPERIMENTAL) Added header filtering functionality through the HeaderFilter interface.
|
||||
* [jwk] (EXPERIMENTAL) Added key filtering functionality through the KeyFilter interface.
|
||||
* [jwk] `jwk.Export` previously did not recognize third-party objects that implemented `jwk.Key`,
|
||||
as it was detecting what to do by checking if the object was one of our own unexported
|
||||
types. This caused some problems for consumers of this library that wanted to extend the
|
||||
features of the keys.
|
||||
|
||||
Now `jwk.Export` checks types against interface types such as `jwk.RSAPrivateKey`, `jwk.ECDSAPrivateKey`, etc.
|
||||
It also uses some reflect blackmagic to detect if the given object implements the `jwk.Key` interface
|
||||
via embedding, so you should be able to embed a `jwk.Key` to another object to act as if it
|
||||
is a legitimate `jwk.Key`, as far as `jwk.Export` is concerned.
|
||||
|
||||
v3.0.1 29 Apr 2025
|
||||
* [jwe] Fixed a long standing bug that could lead to degraded encryption or failure to
|
||||
decrypt JWE messages when a very specific combination of inputs were used for
|
||||
JWE operations.
|
||||
|
||||
This problem only manifested itself when the following conditions in content encryption or decryption
|
||||
were met:
|
||||
- Content encryption was specified to use DIRECT mode.
|
||||
- Contentn encryption algorithm is specified as A256CBC_HS512
|
||||
- The key was erronously constructed with a 32-byte content encryption key (CEK)
|
||||
|
||||
In this case, the user would be passing a mis-constructed key of 32-bytes instead
|
||||
of the intended 64-bytes. In all other cases, this construction would cause
|
||||
an error because `crypto/aes.NewCipher` would return an error when a key with length
|
||||
not matching 16, 24, and 32 bytes is used. However, due to use using a the provided
|
||||
32-bytes as half CEK and half the hash, the `crypto/aes.NewCipher` was passed
|
||||
a 16-byte key, which is fine for AES-128. So internally `crypto/aes.NewCipher` would
|
||||
choose to use AES-128 instead of AES-256, and happily continue. Note that no other
|
||||
key lengths such as 48 and 128 would have worked. It had to be exactly 32.
|
||||
|
||||
This does indeed result in a downgraded encryption, but we believe it is unlikely that this would cause a problem in the real world,
|
||||
as you would have to very specifically choose to use DIRECT mode, choose
|
||||
the specific content encryption algorithm, AND also use the wrong key size of
|
||||
exactly 32 bytes.
|
||||
|
||||
However, in abandunce of caution, we recommend that you upgrade to v3.0.1 or later,
|
||||
or v2.1.6 or later if you are still on v2 series.
|
||||
|
||||
* [jws] Improve performance of jws.SplitCompact and jws.SplitCompactString
|
||||
* [jwe] Improve performance of jwe.Parse
|
||||
|
||||
v3.0.0 1 Apr 2025
|
||||
* Release initial v3.0.0 series. Code is identical to v3.0.0-beta2, except
|
||||
for minor documentation changes.
|
||||
|
||||
Please note that v1 will no longer be maintained.
|
||||
|
||||
Going forward v2 will receive security updates but will no longer receive
|
||||
feature updates. Users are encouraged to migrate to v3. There is no hard-set
|
||||
guarantee as to how long v2 will be supported, but if/when v4 comes out,
|
||||
v2 support will be terminated then.
|
||||
|
||||
v3.0.0-beta2 30 Mar 2025
|
||||
* [jwk] Fix a bug where `jwk.Set`'s `Keys()` method did not return the proper
|
||||
non-standard fields. (#1322)
|
||||
* [jws][jwt] Implement `WithBase64Encoder()` options to pass base64 encoders
|
||||
to use during signing/verifying signatures. This useful when the token
|
||||
provider generates JWTs that don't follow the specification and uses base64
|
||||
encoding other than raw url encoding (no padding), such as, apparently,
|
||||
AWS ALB. (#1324, #1328)
|
||||
|
||||
v3.0.0-beta1 15 Mar 2025
|
||||
* [jwt] Token validation no longer truncates time based fields by default.
|
||||
To restore old behavior, you can either change the global settings by
|
||||
calling `jwt.Settings(jwt.WithTruncation(time.Second))`, or you can
|
||||
change it by each invocation by using `jwt.Validate(..., jwt.WithTruncation(time.Second))`
|
||||
|
||||
v3.0.0-alpha3 13 Mar 2025
|
||||
* [jwk] Importing/Exporting from jwk.Key with P256/P386/P521 curves to
|
||||
ecdh.PrivateKey/ecdh.PublicKey should now work. Previously these keys were not properly
|
||||
recognized by the exporter/importer. Note that keys that use X25519 and P256/P384/P521
|
||||
behave differently: X25519 keys can only be exported to/imported from OKP keys,
|
||||
while P256/P384/P521 can be exported to either ecdsa or ecdh keys.
|
||||
|
||||
v3.0.0-alpha2 25 Feb 2025
|
||||
* Update to work with go1.24
|
||||
* Update tests to work with latest latchset/jose
|
||||
* Fix build pipeline to work with latest golangci-lint
|
||||
* Require go1.23
|
||||
|
||||
v3.0.0-alpha1 01 Nov 2024
|
||||
* Initial release of v3 line.
|
||||
+390
@@ -0,0 +1,390 @@
|
||||
# Incompatible Changes from v1 to v2
|
||||
|
||||
These are changes that are incompatible with the v1.x.x version.
|
||||
|
||||
* [tl;dr](#tldr) - If you don't feel like reading the details -- but you will read the details, right?
|
||||
* [Detailed List of Changes](#detailed-list-of-changes) - A comprehensive list of changes from v1 to v2
|
||||
|
||||
# tl;dr
|
||||
|
||||
## JWT
|
||||
|
||||
```go
|
||||
// most basic
|
||||
jwt.Parse(serialized, jwt.WithKey(alg, key)) // NOTE: verification and validation are ENABLED by default!
|
||||
jwt.Sign(token, jwt.WithKey(alg,key))
|
||||
|
||||
// with a jwk.Set
|
||||
jwt.Parse(serialized, jwt.WithKeySet(set))
|
||||
|
||||
// UseDefault/InferAlgorithm with JWKS
|
||||
jwt.Parse(serialized, jwt.WithKeySet(set,
|
||||
jws.WithUseDefault(true), jws.WithInferAlgorithm(true))
|
||||
|
||||
// Use `jku`
|
||||
jwt.Parse(serialized, jwt.WithVerifyAuto(...))
|
||||
|
||||
// Any other custom key provisioning (using functions in this
|
||||
// example, but can be anything that fulfills jws.KeyProvider)
|
||||
jwt.Parse(serialized, jwt.WithKeyProvider(jws.KeyProviderFunc(...)))
|
||||
```
|
||||
|
||||
## JWK
|
||||
|
||||
```go
|
||||
// jwk.New() was confusing. Renamed to fit the actual implementation
|
||||
key, err := jwk.FromRaw(rawKey)
|
||||
|
||||
// Algorithm() now returns jwa.KeyAlgorithm type. `jws.Sign()`
|
||||
// and other function that receive JWK algorithm names accept
|
||||
// this new type, so you can use the same key and do the following
|
||||
// (previously you needed to type assert)
|
||||
jws.Sign(payload, jws.WithKey(key.Algorithm(), key))
|
||||
|
||||
// If you need the specific type, type assert
|
||||
key.Algorithm().(jwa.SignatureAlgorithm)
|
||||
|
||||
// jwk.AutoRefresh is no more. Use jwk.Cache
|
||||
cache := jwk.NewCache(ctx, options...)
|
||||
|
||||
// Certificate chains are no longer jwk.CertificateChain type, but
|
||||
// *(github.com/lestrrat-go/jwx/cert).Chain
|
||||
cc := key.X509CertChain() // this is *cert.Chain now
|
||||
```
|
||||
|
||||
## JWS
|
||||
|
||||
```go
|
||||
// basic
|
||||
jws.Sign(payload, jws.WithKey(alg, key))
|
||||
jws.Sign(payload, jws.WithKey(alg, key), jws.WithKey(alg, key), jws.WithJSON(true))
|
||||
jws.Verify(signed, jws.WithKey(alg, key))
|
||||
|
||||
// other ways to pass the key
|
||||
jws.Sign(payload, jws.WithKeySet(jwks))
|
||||
jws.Sign(payload, jws.WithKeyProvider(kp))
|
||||
|
||||
// retrieve the key that succeeded in verifying
|
||||
var keyUsed interface{}
|
||||
jws.Verify(signed, jws.WithKeySet(jwks), jws.WithKeyUsed(&keyUsed))
|
||||
```
|
||||
|
||||
## JWE
|
||||
|
||||
```go
|
||||
// basic
|
||||
jwe.Encrypt(payload, jwe.WithKey(alg, key)) // other defaults are inferred
|
||||
jwe.Encrypt(payload, jwe.WithKey(alg, key), jwe.WithKey(alg, key), jwe.WithJSON(true))
|
||||
jwe.Decrypt(encrypted, jwe.WithKey(alg, key))
|
||||
|
||||
// other ways to pass the key
|
||||
jwe.Encrypt(payload, jwe.WithKeySet(jwks))
|
||||
jwe.Encrypt(payload, jwe.WithKeyProvider(kp))
|
||||
|
||||
// retrieve the key that succeeded in decrypting
|
||||
var keyUsed interface{}
|
||||
jwe.Verify(signed, jwe.WithKeySet(jwks), jwe.WithKeyUsed(&keyUsed))
|
||||
```
|
||||
|
||||
# Detailed List of Changes
|
||||
|
||||
## Module
|
||||
|
||||
* Module now requires go 1.16
|
||||
|
||||
* Use of github.com/pkg/errors is no more. If you were relying on behavior
|
||||
that depends on the errors being an instance of github.com/pkg/errors
|
||||
then you need to change your code
|
||||
|
||||
* File-generation tools have been moved out of internal/ directories.
|
||||
These files pre-dates Go modules, and they were in internal/ in order
|
||||
to avoid being listed in the `go doc` -- however, now that we can
|
||||
make them separate modules this is no longer necessary.
|
||||
|
||||
* New package `cert` has been added to handle `x5c` certificate
|
||||
chains, and to work with certificates
|
||||
* cert.Chain to store base64 encoded ASN.1 DER format certificates
|
||||
* cert.EncodeBase64 to encode ASN.1 DER format certificate using base64
|
||||
* cert.Create to create a base64 encoded ASN.1 DER format certificates
|
||||
* cert.Parse to parse base64 encoded ASN.1 DER format certificates
|
||||
|
||||
## JWE
|
||||
|
||||
* `jwe.Compact()`'s signature has changed to
|
||||
`jwe.Compact(*jwe.Message, ...jwe.CompactOption)`
|
||||
|
||||
* `jwe.JSON()` has been removed. You can generate JSON serialization
|
||||
using `jwe.Encrypt(jwe.WitJSON())` or `json.Marshal(jwe.Message)`
|
||||
|
||||
* `(jwe.Message).Decrypt()` has been removed. Since formatting of the
|
||||
original serialized message matters (including whitespace), using a parsed
|
||||
object was inherently confusing.
|
||||
|
||||
* `jwe.Encrypt()` can now generate JWE messages in either compact or JSON
|
||||
forms. By default, the compact form is used. JSON format can be
|
||||
enabled by using the `jwe.WithJSON` option.
|
||||
|
||||
* `jwe.Encrypt()` can now accept multiple keys by passing multiple
|
||||
`jwe.WithKey()` options. This can be used with `jwe.WithJSON` to
|
||||
create JWE messages with multiple recipients.
|
||||
|
||||
* `jwe.DecryptEncryptOption()` has been renamed to `jwe.EncryptDecryptOption()`.
|
||||
This is so that it is more uniform with `jws` equivalent of `jws.SignVerifyOption()`
|
||||
where the producer (`Sign`) comes before the consumer (`Verify`) in the naming
|
||||
|
||||
* `jwe.WithCompact` and `jwe.WithJSON` options have been added
|
||||
to control the serialization format.
|
||||
|
||||
* jwe.Decrypt()'s method signature has been changed to `jwt.Decrypt([]byte, ...jwe.DecryptOption) ([]byte, error)`.
|
||||
These options can be stacked. Therefore, you could configure the
|
||||
verification process to attempt a static key pair, a JWKS, and only
|
||||
try other forms if the first two fails, for example.
|
||||
|
||||
- For static key pair, use `jwe.WithKey()`
|
||||
- For static JWKS, use `jwe.WithKeySet()` (NOTE: InferAlgorithmFromKey like in `jws` package is NOT supported)
|
||||
- For custom, possibly dynamic key provisioning, use `jwe.WithKeyProvider()`
|
||||
|
||||
* jwe.Decrypter has been unexported. Users did not need this.
|
||||
|
||||
* jwe.WithKeyProvider() has been added to specify arbitrary
|
||||
code to specify which keys to try.
|
||||
|
||||
* jwe.KeyProvider interface has been added
|
||||
|
||||
* jwe.KeyProviderFunc has been added
|
||||
|
||||
* `WithPostParser()` has been removed. You can achieve the same effect
|
||||
by using `jwe.WithKeyProvider()`. Because this was the only consumer for
|
||||
`jwe.DecryptCtx`, this type has been removed as well.
|
||||
|
||||
* `x5c` field type has been changed to `*cert.Chain` instead of `[]string`
|
||||
|
||||
* Method signature for `jwe.Parse()` has been changed to include options,
|
||||
but options are currently not used
|
||||
|
||||
* `jwe.ReadFile` now supports the option `jwe.WithFS` which allows you to
|
||||
read data from arbitrary `fs.FS` objects
|
||||
|
||||
* jwe.WithKeyUsed has been added to allow users to retrieve
|
||||
the key used for decryption. This is useful in cases you provided
|
||||
multiple keys and you want to know which one was successful
|
||||
|
||||
## JWK
|
||||
|
||||
* `jwk.New()` has been renamed to `jwk.FromRaw()`, which hopefully will
|
||||
make it easier for the users what the input should be.
|
||||
|
||||
* `jwk.Set` has many interface changes:
|
||||
* Changed methods to match jwk.Key and its semantics:
|
||||
* Field is now Get() (returns values for arbitrary fields other than keys). Fetching a key is done via Key()
|
||||
* Remove() now removes arbitrary fields, not keys. to remove keys, use RemoveKey()
|
||||
* Iterate has been added to iterate through all non-key fields.
|
||||
* Add is now AddKey(Key) string, and returns an error when the same key is added
|
||||
* Get is now Key(int) (Key, bool)
|
||||
* Remove is now RemoveKey(Key) error
|
||||
* Iterate is now Keys(context.Context) KeyIterator
|
||||
* Clear is now Clear() error
|
||||
|
||||
* `jwk.CachedSet` has been added. You can create a `jwk.Set` that is backed by
|
||||
`jwk.Cache` so you can do this:
|
||||
|
||||
```go
|
||||
cache := jkw.NewCache(ctx)
|
||||
cachedSet := jwk.NewCachedSet(cache, jwksURI)
|
||||
|
||||
// cachedSet is always the refreshed, cached version from jwk.Cache
|
||||
jws.Verify(signed, jws.WithKeySet(cachedSet))
|
||||
```
|
||||
|
||||
* `jwk.NewRSAPRivateKey()`, `jwk.NewECDSAPrivateKey()`, etc have been removed.
|
||||
There is no longer any way to create concrete types of `jwk.Key`
|
||||
|
||||
* `jwk.Key` type no longer supports direct unmarshaling via `json.Unmarshal()`,
|
||||
because you can no longer instantiate concrete `jwk.Key` types. You will need to
|
||||
use `jwk.ParseKey()`. See the documentation for ways to parse JWKs.
|
||||
|
||||
* `(jwk.Key).Algorithm()` is now of `jwk.KeyAlgorithm` type. This field used
|
||||
to be `string` and therefore could not be passed directly to `jwt.Sign()`
|
||||
`jws.Sign()`, `jwe.Encrypt()`, et al. This is no longer the case, and
|
||||
now you can pass it directly. See
|
||||
https://github.com/lestrrat-go/jwx/blob/v2/docs/99-faq.md#why-is-jwkkeyalgorithm-and-jwakeyalgorithm-so-confusing
|
||||
for more details
|
||||
|
||||
* `jwk.Fetcher` and `jwk.FetchFunc` has been added.
|
||||
They represent something that can fetch a `jwk.Set`
|
||||
|
||||
* `jwk.CertificateChain` has been removed, use `*cert.Chain`
|
||||
* `x5c` field type has been changed to `*cert.Chain` instead of `[]*x509.Certificate`
|
||||
|
||||
* `jwk.ReadFile` now supports the option `jwk.WithFS` which allows you to
|
||||
read data from arbitrary `fs.FS` objects
|
||||
|
||||
* Added `jwk.PostFetcher`, `jwk.PostFetchFunc`, and `jwk.WithPostFetch` to
|
||||
allow users to get at the `jwk.Set` that was fetched in `jwk.Cache`.
|
||||
This will make it possible for users to supply extra information and edit
|
||||
`jwk.Set` after it has been fetched and parsed, but before it is cached.
|
||||
You could, for example, modify the `alg` field so that it's easier to
|
||||
work with when you use it in `jws.Verify` later.
|
||||
|
||||
* Reworked `jwk.AutoRefresh` in terms of `github.com/lestrrat-go/httprc`
|
||||
and renamed it `jwk.Cache`.
|
||||
|
||||
Major difference between `jwk.AutoRefresh` and `jwk.Cache` is that while
|
||||
former used one `time.Timer` per resource, the latter uses a static timer
|
||||
(based on `jwk.WithRefreshWindow()` value, default 15 minutes) that periodically
|
||||
refreshes all resources that were due to be refreshed within that time frame.
|
||||
|
||||
This method may cause your updates to happen slightly later, but uses significantly
|
||||
less resources and is less prone to clogging.
|
||||
|
||||
* Reimplemented `jwk.Fetch` in terms of `github.com/lestrrat-go/httprc`.
|
||||
|
||||
* Previously `jwk.Fetch` and `jwk.AutoRefresh` respected backoff options,
|
||||
but this has been removed. This is to avoid unwanted clogging of the fetch workers
|
||||
which is the default processing mode in `github.com/lestrrat-go/httprc`.
|
||||
|
||||
If you are using backoffs, you need to control your inputs more carefully so as
|
||||
not to clog your fetch queue, and therefore you should be writing custom code that
|
||||
suits your needs
|
||||
|
||||
## JWS
|
||||
|
||||
* `jws.Sign()` can now generate JWS messages in either compact or JSON
|
||||
forms. By default, the compact form is used. JSON format can be
|
||||
enabled by using the `jws.WithJSON` option.
|
||||
|
||||
* `jws.Sign()` can now accept multiple keys by passing multiple
|
||||
`jws.WithKey()` options. This can be used with `jws.WithJSON` to
|
||||
create JWS messages with multiple signatures.
|
||||
|
||||
* `jws.WithCompact` and `jws.WithJSON` options have been added
|
||||
to control the serialization format.
|
||||
|
||||
* jws.Verify()'s method signature has been changed to `jwt.Verify([]byte, ...jws.VerifyOption) ([]byte, error)`.
|
||||
These options can be stacked. Therefore, you could configure the
|
||||
verification process to attempt a static key pair, a JWKS, and only
|
||||
try other forms if the first two fails, for example.
|
||||
|
||||
- For static key pair, use `jws.WithKey()`
|
||||
- For static JWKS, use `jws.WithKeySet()`
|
||||
- For enabling verification using `jku`, use `jws.WithVerifyAuto()`
|
||||
- For custom, possibly dynamic key provisioning, use `jws.WithKeyProvider()`
|
||||
|
||||
* jws.WithVerify() has been removed.
|
||||
|
||||
* jws.WithKey() has been added to specify an algorithm + key to
|
||||
verify the payload with.
|
||||
|
||||
* jws.WithKeySet() has been added to specify a JWKS to be used for
|
||||
verification. By default `kid` AND `alg` must match between the signature
|
||||
and the key.
|
||||
|
||||
The option can take further suboptions:
|
||||
|
||||
```go
|
||||
jws.Parse(serialized,
|
||||
jws.WithKeySet(set,
|
||||
// by default `kid` is required. set false to disable.
|
||||
jws.WithRequireKid(false),
|
||||
// optionally skip matching kid if there's exactly one key in set
|
||||
jws.WithUseDefault(true),
|
||||
// infer algorithm name from key type
|
||||
jws.WithInferAlgorithm(true),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
* `jws.VerifuAuto` has been removed in favor of using
|
||||
`jws.WithVerifyAuto` option with `jws.Verify()`
|
||||
|
||||
* `jws.WithVerifyAuto` has been added to enable verification
|
||||
using `jku`.
|
||||
|
||||
The first argument must be a jwk.Fetcher object, but can be
|
||||
set to `nil` to use the default implementation which is `jwk.Fetch`
|
||||
|
||||
The rest of the arguments are treated as options passed to the
|
||||
`(jwk.Fetcher).Fetch()` function.
|
||||
|
||||
* Remove `jws.WithPayloadSigner()`. This should be completely replaceable
|
||||
using `jws.WithKey()`
|
||||
|
||||
* jws.WithKeyProvider() has been added to specify arbitrary
|
||||
code to specify which keys to try.
|
||||
|
||||
* jws.KeyProvider interface has been added
|
||||
|
||||
* jws.KeyProviderFunc has been added
|
||||
|
||||
* jws.WithKeyUsed has been added to allow users to retrieve
|
||||
the key used for verification. This is useful in cases you provided
|
||||
multiple keys and you want to know which one was successful
|
||||
|
||||
* `x5c` field type has been changed to `*cert.Chain` instead of `[]string`
|
||||
|
||||
* `jws.ReadFile` now supports the option `jws.WithFS` which allows you to
|
||||
read data from arbitrary `fs.FS` objects
|
||||
|
||||
## JWT
|
||||
|
||||
* `jwt.Parse` now verifies the signature and validates the token
|
||||
by default. You must disable it explicitly using `jwt.WithValidate(false)`
|
||||
and/or `jwt.WithVerify(false)` if you only want to parse the JWT message.
|
||||
|
||||
If you don't want either, a convenience function `jwt.ParseInsecure`
|
||||
has been added.
|
||||
|
||||
* `jwt.Parse` can only parse raw JWT (JSON) or JWS (JSON or Compact).
|
||||
It no longer accepts JWE messages.
|
||||
|
||||
* `jwt.WithDecrypt` has been removed
|
||||
|
||||
* `jwt.WithJweHeaders` has been removed
|
||||
|
||||
* `jwt.WithVerify()` has been renamed to `jwt.WithKey()`. The option can
|
||||
be used for signing, encryption, and parsing.
|
||||
|
||||
* `jwt.Validator` has been changed to return `jwt.ValidationError`.
|
||||
If you provide a custom validator, you should wrap the error with
|
||||
`jwt.NewValidationError()`
|
||||
|
||||
* `jwt.UseDefault()` has been removed. You should use `jws.WithUseDefault()`
|
||||
as a suboption in the `jwt.WithKeySet()` option.
|
||||
|
||||
```go
|
||||
jwt.Parse(serialized, jwt.WithKeySet(set, jws.WithUseDefault(true)))
|
||||
```
|
||||
|
||||
* `jwt.InferAlgorithmFromKey()` has been removed. You should use
|
||||
`jws.WithInferAlgorithmFromKey()` as a suboption in the `jwt.WithKeySet()` option.
|
||||
|
||||
```go
|
||||
jwt.Parse(serialized, jwt.WithKeySet(set, jws.WithInferAlgorithmFromKey(true)))
|
||||
```
|
||||
|
||||
* jwt.WithKeySetProvider has been removed. Use `jwt.WithKeyProvider()`
|
||||
instead. If jwt.WithKeyProvider seems a bit complicated, use a combination of
|
||||
JWS parse, no-verify/validate JWT parse, and an extra JWS verify:
|
||||
|
||||
```go
|
||||
msg, _ := jws.Parse(signed)
|
||||
token, _ := jwt.Parse(msg.Payload(), jwt.WithVerify(false), jwt.WithValidate(false))
|
||||
// Get information out of token, for example, `iss`
|
||||
switch token.Issuer() {
|
||||
case ...:
|
||||
jws.Verify(signed, jwt.WithKey(...))
|
||||
}
|
||||
```
|
||||
|
||||
* `jwt.WithHeaders` and `jwt.WithJwsHeaders` have been removed.
|
||||
You should be able to use the new `jwt.WithKey` option to pass headers
|
||||
|
||||
* `jwt.WithSignOption` and `jwt.WithEncryptOption` have been added as
|
||||
escape hatches for options that are declared in `jws` and `jwe` packages
|
||||
but not in `jwt`
|
||||
|
||||
* `jwt.ReadFile` now supports the option `jwt.WithFS` which allows you to
|
||||
read data from arbitrary `fs.FS` objects
|
||||
|
||||
* `jwt.Sign()` has been changed so that it works more like the new `jws.Sign()`
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
# Incompatible Changes from v2 to v3
|
||||
|
||||
These are changes that are incompatible with the v2.x.x version.
|
||||
|
||||
# Detailed list of changes
|
||||
|
||||
## Module
|
||||
|
||||
* This module now requires Go 1.23
|
||||
|
||||
* All `xxx.Get()` methods have been changed from `Get(string) (interface{}, error)` to
|
||||
`Get(string, interface{}) error`, where the second argument should be a pointer
|
||||
to the storage destination of the field.
|
||||
|
||||
* All convenience accessors (e.g. `(jwt.Token).Subject`) now return `(T, bool)` instead of
|
||||
`T`. If you want an accessor that returns a single value, consider using `Get()`
|
||||
|
||||
* Most major errors can now be differentiated using `errors.Is`
|
||||
|
||||
## JWA
|
||||
|
||||
* All string constants have been renamed to equivalent functions that return a struct.
|
||||
You should rewrite `jwa.RS256` as `jwa.RS256()` and so forth.
|
||||
|
||||
* By default, only known algorithm names are accepted. For example, in our JWK tests,
|
||||
there are tests that deal with "ECMR" algorithm, but this will now fail by default.
|
||||
If you want this algorithm to succeed parsing, you need to call `jwa.RegisterXXXX`
|
||||
functions before using them.
|
||||
|
||||
* Previously, unmarshaling unquoted strings used to work (e.g. `var s = "RS256"`),
|
||||
but now they must conform to the JSON standard and be quoted (e.g. `var s = strconv.Quote("RS256")`)
|
||||
|
||||
## JWT
|
||||
|
||||
* All convenience accessors (e.g. `Subject`) now return `(T, bool)` instead of
|
||||
just `T`. If you want a single return value accessor, use `Get(dst) error` instead.
|
||||
|
||||
* Validation used to work for `iat`, `nbf`, `exp` fields where these fields were
|
||||
set to the explicit time.Time{} zero value, but now the _presence_ of these fields matter.
|
||||
|
||||
* Validation of fields related to time used to be truncated to one second accuracy,
|
||||
but no longer does so. To restore old behavior, you can either change the global settings by
|
||||
calling `jwt.Settings(jwt.WithTruncation(time.Second))`, or you can
|
||||
change it by each invocation by using `jwt.Validate(..., jwt.WithTruncation(time.Second))`
|
||||
|
||||
* Error names have been renamed. For example `jwt.ErrInvalidJWT` has been renamed to
|
||||
`jwt.UnknownPayloadTypeError` to better reflect what the error means. For other errors,
|
||||
`func ErrXXXX()` have generally been renamed to `func XXXError()`
|
||||
|
||||
* Validation errors are now wrapped. While `Validate()` returns a `ValidateError()` type,
|
||||
it can also be matched against more specific error types such as `TokenExpierdError()`
|
||||
using `errors.Is`
|
||||
|
||||
* `jwt.ErrMissingRequiredClaim` has been removed
|
||||
|
||||
## JWS
|
||||
|
||||
* Iterators have been completely removed.
|
||||
* As a side effect of removing iterators, some methods such as `Copy()` lost the
|
||||
`context.Context` argument
|
||||
|
||||
* All convenience accessors (e.g. `Algorithm`) now return `(T, bool)` instead of
|
||||
just `T`. If you want a single return value accessor, use `Get(dst) error` instead.
|
||||
|
||||
* Errors from `jws.Sign` and `jws.Verify`, as well as `jws.Parse` (and friends)
|
||||
can now be differentiated by using `errors.Is`. All `jws.IsXXXXError` functions
|
||||
have been removed.
|
||||
|
||||
## JWE
|
||||
|
||||
* Iterators have been completely removed.
|
||||
* As a side effect of removing iterators, some methods such as `Copy()` lost the
|
||||
`context.Context` argument
|
||||
|
||||
* All convenience accessors (e.g. `Algorithm`) now return `(T, bool)` instead of
|
||||
just `T`. If you want a single return value accessor, use `Get(dst) error` instead.
|
||||
|
||||
* Errors from `jwe.Decrypt` and `jwe.Encrypt`, as well as `jwe.Parse` (and friends)
|
||||
can now be differentiated by using `errors.Is`. All `jwe.IsXXXXrror` functions
|
||||
have been removed.
|
||||
|
||||
## JWK
|
||||
|
||||
* All convenience accessors (e.g. `Algorithm`, `Crv`) now return `(T, bool)` instead
|
||||
of just `T`, except `KeyType`, which _always_ returns a valid value. If you want a
|
||||
single return value accessor, use `Get(dst) error` instead.
|
||||
|
||||
* `jwk.KeyUsageType` can now be configured so that it's possible to assign values
|
||||
other than "sig" and "enc" via `jwk.RegisterKeyUsage()`. Furthermore, strict
|
||||
checks can be turned on/off against these registered values
|
||||
|
||||
* `jwk.Cache` has been completely re-worked based on github.com/lestrrat-go/httprc/v3.
|
||||
In particular, the default whitelist mode has changed from "block everything" to
|
||||
"allow everything".
|
||||
|
||||
* 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
|
||||
`(crypto/ecdh).PublicKey`. Similarly, internals have been reworked to use `crypto/ecdh`
|
||||
|
||||
* Parsing has completely been reworked. It is now possible to add your own `jwk.KeyParser`
|
||||
to generate a custom `jwk.Key` that this library may not natively support. Also see
|
||||
`jwk.RegisterKeyParser()`
|
||||
|
||||
* `jwk.KeyProbe` has been added to aid probing the JSON message. This is used to
|
||||
guess the type of key described in the JSON message before deciding which concrete
|
||||
type to instantiate, and aids implementing your own `jwk.KeyParser`. Also see
|
||||
`jwk.RegisterKeyProbe()`
|
||||
|
||||
* Conversion between raw keys and `jwk.Key` can be customized using `jwk.KeyImporter` and `jwk.KeyExporter`.
|
||||
Also see `jwk.RegisterKeyImporter()` and `jwk.RegisterKeyExporter()`
|
||||
|
||||
* Added `jwk/ecdsa` to keep track of which curves are available for ECDSA keys.
|
||||
|
||||
* `(jwk.Key).Raw()` has been deprecated. Use `jwk.Export()` instead to convert `jwk.Key`
|
||||
objects into their "raw" versions (e.g. `*rsa.PrivateKey`, `*ecdsa.PrivateKey`, etc).
|
||||
This is to allow third parties to register custom key types that this library does not
|
||||
natively support: Whereas a method must be bound to an object, and thus does not necessarily
|
||||
have a way to hook into a global settings (i.e. custom exporter/importer) for arbitrary
|
||||
key types, if the entrypoint is a function it's much easier and cleaner to for third-parties
|
||||
to take advantage and hook into the mechanisms.
|
||||
|
||||
* `jwk.FromRaw()` has been derepcated. Use `jwk.Import()` instead to convert "raw"
|
||||
keys (e.g. `*rsa.PrivateKEy`, `*Ecdsa.PrivateKey`, etc) int `jwk.Key`s.
|
||||
|
||||
* `(jwk.Key).FromRaw()` has been deprecated. The method `(jwk.Key).Import()` still exist for
|
||||
built-in types, but it is no longer part of any public API (`interface{}`).
|
||||
|
||||
* `jwk.Fetch` is marked as a simple wrapper around `net/http` and `jwk.Parse`.
|
||||
|
||||
* `jwk.SetGlobalFetcher` has been deprecated.
|
||||
|
||||
* `jwk.Fetcher` has been clearly marked as something that has limited
|
||||
usage for `jws.WithVerifyAuto`
|
||||
|
||||
* `jwk.Key` with P256/P386/P521 curves can be exporrted to `ecdh.PrivateKey`/`ecdh.PublicKey`
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 lestrrat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
module(
|
||||
name = "com_github_lestrrat_go_jwx_v3",
|
||||
version = "3.0.0",
|
||||
repo_name = "com_github_lestrrat_go_jwx_v2",
|
||||
)
|
||||
|
||||
bazel_dep(name = "bazel_skylib", version = "1.7.1")
|
||||
bazel_dep(name = "rules_go", version = "0.55.1")
|
||||
bazel_dep(name = "gazelle", version = "0.44.0")
|
||||
bazel_dep(name = "aspect_bazel_lib", version = "2.11.0")
|
||||
|
||||
# Go SDK setup from go.mod
|
||||
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
|
||||
go_sdk.from_file(go_mod = "//:go.mod")
|
||||
|
||||
# Go dependencies from go.mod
|
||||
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
|
||||
go_deps.from_file(go_mod = "//:go.mod")
|
||||
|
||||
# Use repositories for external Go dependencies
|
||||
use_repo(
|
||||
go_deps,
|
||||
"com_github_decred_dcrd_dcrec_secp256k1_v4",
|
||||
"com_github_goccy_go_json",
|
||||
"com_github_lestrrat_go_blackmagic",
|
||||
"com_github_lestrrat_go_dsig",
|
||||
"com_github_lestrrat_go_dsig_secp256k1",
|
||||
"com_github_lestrrat_go_httprc_v3",
|
||||
"com_github_lestrrat_go_option_v2",
|
||||
"com_github_segmentio_asm",
|
||||
"com_github_stretchr_testify",
|
||||
"com_github_valyala_fastjson",
|
||||
"org_golang_x_crypto",
|
||||
)
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"lockFileVersion": 18,
|
||||
"registryFileHashes": {
|
||||
"https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.11.0/MODULE.bazel": "cb1ba9f9999ed0bc08600c221f532c1ddd8d217686b32ba7d45b0713b5131452",
|
||||
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.11.0/source.json": "92494d5aa43b96665397dd13ee16023097470fa85e276b93674d62a244de47ee",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.30.0/source.json": "b07e17f067fe4f69f90b03b36ef1e08fe0d1f3cac254c1241a1818773e3423bc",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/source.json": "f121b43eeefc7c29efbd51b83d08631e2347297c95aac9764a701f2a6a2bb953",
|
||||
"https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84",
|
||||
"https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.32.0/MODULE.bazel": "b499f58a5d0d3537f3cf5b76d8ada18242f64ec474d8391247438bf04f58c7b8",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.33.0/MODULE.bazel": "a13a0f279b462b784fb8dd52a4074526c4a2afe70e114c7d09066097a46b3350",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.44.0/MODULE.bazel": "fd3177ca0938da57a1e416cad3f39b9c4334defbc717e89aba9d9ddbbb0341da",
|
||||
"https://bcr.bazel.build/modules/gazelle/0.44.0/source.json": "7fb65ef9c1ce470d099ca27fd478673d9d64c844af28d0d472b0874c7d590cb6",
|
||||
"https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
|
||||
"https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
|
||||
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
|
||||
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/source.json": "41e9e129f80d8c8bf103a7acc337b76e54fad1214ac0a7084bf24f4cd924b8b4",
|
||||
"https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f",
|
||||
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075",
|
||||
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d",
|
||||
"https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902",
|
||||
"https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92",
|
||||
"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/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",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.0-rc2.bcr.1/MODULE.bazel": "52f4126f63a2f0bbf36b99c2a87648f08467a4eaf92ba726bc7d6a500bbf770c",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.0/source.json": "b857f93c796750eef95f0d61ee378f3420d00ee1dd38627b27193aa482f4f981",
|
||||
"https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0",
|
||||
"https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573",
|
||||
"https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858",
|
||||
"https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e",
|
||||
"https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/source.json": "be4789e951dd5301282729fe3d4938995dc4c1a81c2ff150afc9f1b0504c6022",
|
||||
"https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206",
|
||||
"https://bcr.bazel.build/modules/re2/2023-09-01/source.json": "e044ce89c2883cd957a2969a43e79f7752f9656f6b20050b62f90ede21ec6eb4",
|
||||
"https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8",
|
||||
"https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.14/MODULE.bazel": "5e343a3aac88b8d7af3b1b6d2093b55c347b8eefc2e7d1442f7a02dc8fea48ac",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.1.1/source.json": "d61627377bd7dd1da4652063e368d9366fc9a73920bfa396798ad92172cf645c",
|
||||
"https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6",
|
||||
"https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8",
|
||||
"https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/source.json": "c8b1e2c717646f1702290959a3302a178fb639d987ab61d548105019f11e527e",
|
||||
"https://bcr.bazel.build/modules/rules_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8",
|
||||
"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_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",
|
||||
"https://bcr.bazel.build/modules/rules_java/6.3.0/MODULE.bazel": "a97c7678c19f236a956ad260d59c86e10a463badb7eb2eda787490f4c969b963",
|
||||
"https://bcr.bazel.build/modules/rules_java/6.4.0/MODULE.bazel": "e986a9fe25aeaa84ac17ca093ef13a4637f6107375f64667a15999f77db6c8f6",
|
||||
"https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.12.0/MODULE.bazel": "8e6590b961f2defdfc2811c089c75716cb2f06c8a4edeb9a8d85eaa64ee2a761",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.12.0/source.json": "cbd5d55d9d38d4008a7d00bee5b5a5a4b6031fcd4a56515c9accbcd42c7be2ba",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/5.3/MODULE.bazel": "bf93870767689637164657731849fb887ad086739bd5d360d90007a581d5527d",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.1/MODULE.bazel": "75b5fec090dbd46cf9b7d8ea08cf84a0472d92ba3585b476f44c326eda8059c4",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/source.json": "6f5f5a5a4419ae4e37c35a5bb0a6ae657ed40b7abc5a5189111b47fcebe43197",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.0/MODULE.bazel": "ef85697305025e5a61f395d4eaede272a5393cee479ace6686dba707de804d59",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5",
|
||||
"https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0",
|
||||
"https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d",
|
||||
"https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c",
|
||||
"https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a",
|
||||
"https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
|
||||
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
|
||||
"https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f",
|
||||
"https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73",
|
||||
"https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2",
|
||||
"https://bcr.bazel.build/modules/rules_proto/7.0.2/source.json": "1e5e7260ae32ef4f2b52fd1d0de8d03b606a44c91b694d2f1afb1d3b28a48ce1",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.40.0/source.json": "939d4bd2e3110f27bfb360292986bb79fd8dcefb874358ccd6cdaa7bda029320",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.3.0/source.json": "c55ed591aa5009401ddf80ded9762ac32c358d2517ee7820be981e2de9756cf3",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.5.6/MODULE.bazel": "c43dabc564990eeab55e25ed61c07a1aadafe9ece96a4efabb3f8bf9063b71ef",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.6.2/MODULE.bazel": "7060193196395f5dd668eda046ccbeacebfd98efc77fed418dbe2b82ffaa39fd",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.1/source.json": "b6500ffcd7b48cd72c29bb67bcac781e12701cc0d6d55d266a652583cfcdab01",
|
||||
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
|
||||
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
|
||||
"https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
|
||||
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca",
|
||||
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806",
|
||||
"https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198"
|
||||
},
|
||||
"selectedYankedVersions": {},
|
||||
"moduleExtensions": {
|
||||
"@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "hUTp2w+RUVdL7ma5esCXZJAFnX7vLbVfLd7FwnQI6bU=",
|
||||
"usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
"generatedRepoSpecs": {
|
||||
"com_github_jetbrains_kotlin_git": {
|
||||
"repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_compiler_git_repository",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip"
|
||||
],
|
||||
"sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88"
|
||||
}
|
||||
},
|
||||
"com_github_jetbrains_kotlin": {
|
||||
"repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_capabilities_repository",
|
||||
"attributes": {
|
||||
"git_repository_name": "com_github_jetbrains_kotlin_git",
|
||||
"compiler_version": "1.9.23"
|
||||
}
|
||||
},
|
||||
"com_github_google_ksp": {
|
||||
"repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:ksp.bzl%ksp_compiler_plugin_repository",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip"
|
||||
],
|
||||
"sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d",
|
||||
"strip_version": "1.9.23-1.0.20"
|
||||
}
|
||||
},
|
||||
"com_github_pinterest_ktlint": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_file",
|
||||
"attributes": {
|
||||
"sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985",
|
||||
"urls": [
|
||||
"https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint"
|
||||
],
|
||||
"executable": true
|
||||
}
|
||||
},
|
||||
"rules_android": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806",
|
||||
"strip_prefix": "rules_android-0.1.1",
|
||||
"urls": [
|
||||
"https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"recordedRepoMappingEntries": [
|
||||
[
|
||||
"rules_kotlin+",
|
||||
"bazel_tools",
|
||||
"bazel_tools"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
.PHONY: generate realclean cover viewcover test lint check_diffs imports tidy jwx
|
||||
generate:
|
||||
@go generate
|
||||
@$(MAKE) generate-jwa generate-jwe generate-jwk generate-jws generate-jwt
|
||||
@./tools/cmd/gofmt.sh
|
||||
|
||||
generate-%:
|
||||
@go generate $(shell pwd -P)/$(patsubst generate-%,%,$@)
|
||||
|
||||
realclean:
|
||||
rm coverage.out
|
||||
|
||||
test-cmd:
|
||||
env TESTOPTS="$(TESTOPTS)" ./tools/test.sh
|
||||
|
||||
test:
|
||||
$(MAKE) test-stdlib TESTOPTS=
|
||||
|
||||
test-stdlib:
|
||||
$(MAKE) test-cmd TESTOPTS=
|
||||
|
||||
test-goccy:
|
||||
$(MAKE) test-cmd TESTOPTS="-tags jwx_goccy"
|
||||
|
||||
test-es256k:
|
||||
$(MAKE) test-cmd TESTOPTS="-tags jwx_es256k"
|
||||
|
||||
test-secp256k1-pem:
|
||||
$(MAKE) test-cmd TESTOPTS="-tags jwx_es256k,jwx_secp256k1_pem"
|
||||
|
||||
test-asmbase64:
|
||||
$(MAKE) test-cmd TESTOPTS="-tags jwx_asmbase64"
|
||||
|
||||
test-alltags:
|
||||
$(MAKE) test-cmd TESTOPTS="-tags jwx_asmbase64,jwx_goccy,jwx_es256k,jwx_secp256k1_pem"
|
||||
|
||||
cover-cmd:
|
||||
env MODE=cover ./tools/test.sh
|
||||
|
||||
cover:
|
||||
$(MAKE) cover-stdlib
|
||||
|
||||
cover-stdlib:
|
||||
$(MAKE) cover-cmd TESTOPTS=
|
||||
|
||||
cover-goccy:
|
||||
$(MAKE) cover-cmd TESTOPTS="-tags jwx_goccy"
|
||||
|
||||
cover-es256k:
|
||||
$(MAKE) cover-cmd TESTOPTS="-tags jwx_es256k"
|
||||
|
||||
cover-secp256k1-pem:
|
||||
$(MAKE) cover-cmd TESTOPTS="-tags jwx_es256k,jwx_secp256k1"
|
||||
|
||||
cover-asmbase64:
|
||||
$(MAKE) cover-cmd TESTOPTS="-tags jwx_asmbase64"
|
||||
|
||||
cover-alltags:
|
||||
$(MAKE) cover-cmd TESTOPTS="-tags jwx_asmbase64,jwx_goccy,jwx_es256k,jwx_secp256k1_pem"
|
||||
|
||||
smoke-cmd:
|
||||
env MODE=short ./tools/test.sh
|
||||
|
||||
smoke:
|
||||
$(MAKE) smoke-stdlib
|
||||
|
||||
smoke-stdlib:
|
||||
$(MAKE) smoke-cmd TESTOPTS=
|
||||
|
||||
smoke-goccy:
|
||||
$(MAKE) smoke-cmd TESTOPTS="-tags jwx_goccy"
|
||||
|
||||
smoke-es256k:
|
||||
$(MAKE) smoke-cmd TESTOPTS="-tags jwx_es256k"
|
||||
|
||||
smoke-secp256k1-pem:
|
||||
$(MAKE) smoke-cmd TESTOPTS="-tags jwx_es256k,jwx_secp256k1_pem"
|
||||
|
||||
smoke-alltags:
|
||||
$(MAKE) smoke-cmd TESTOPTS="-tags jwx_goccy,jwx_es256k,jwx_secp256k1_pem"
|
||||
|
||||
viewcover:
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
lint:
|
||||
golangci-lint run ./...
|
||||
|
||||
check_diffs:
|
||||
./scripts/check-diff.sh
|
||||
|
||||
imports:
|
||||
goimports -w ./
|
||||
|
||||
tidy:
|
||||
./scripts/tidy.sh
|
||||
|
||||
jwx:
|
||||
@./tools/cmd/install-jwx.sh
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
# github.com/lestrrat-go/jwx/v3 [](https://github.com/lestrrat-go/jwx/actions/workflows/ci.yml) [](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3) [](https://codecov.io/github/lestrrat-go/jwx?branch=v3)
|
||||
|
||||
Go module implementing various JWx (JWA/JWE/JWK/JWS/JWT, otherwise known as JOSE) technologies.
|
||||
|
||||
If you are using this module in your product or your company, please add your product and/or company name in the [Wiki](https://github.com/lestrrat-go/jwx/wiki/Users)! It really helps keeping up our motivation.
|
||||
|
||||
# Features
|
||||
|
||||
* Complete coverage of JWA/JWE/JWK/JWS/JWT, not just JWT+minimum tool set.
|
||||
* Supports JWS messages with multiple signatures, both compact and JSON serialization
|
||||
* Supports JWS with detached payload
|
||||
* Supports JWS with unencoded payload (RFC7797)
|
||||
* Supports JWE messages with multiple recipients, both compact and JSON serialization
|
||||
* Most operations work with either JWK or raw keys e.g. *rsa.PrivateKey, *ecdsa.PrivateKey, etc).
|
||||
* Opinionated, but very uniform API. Everything is symmetric, and follows a standard convention
|
||||
* jws.Parse/Verify/Sign
|
||||
* jwe.Parse/Encrypt/Decrypt
|
||||
* Arguments are organized as explicit required parameters and optional WithXXXX() style options.
|
||||
* Extra utilities
|
||||
* `jwk.Cache` to always keep a JWKS up-to-date
|
||||
* [bazel](https://bazel.build)-ready
|
||||
|
||||
Some more in-depth discussion on why you might want to use this library over others
|
||||
can be found in the [Description section](#description)
|
||||
|
||||
If you are using v0 or v1, you are strongly encouraged to migrate to using v3
|
||||
(the version that comes with the README you are reading).
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
<!-- INCLUDE(examples/jwx_readme_example_test.go) -->
|
||||
```go
|
||||
package examples_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jwe"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
"github.com/lestrrat-go/jwx/v3/jws"
|
||||
"github.com/lestrrat-go/jwx/v3/jwt"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
// Parse, serialize, slice and dice JWKs!
|
||||
privkey, err := jwk.ParseKey(jsonRSAPrivateKey)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to parse JWK: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
pubkey, err := jwk.PublicKeyOf(privkey)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to get public key: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Work with JWTs!
|
||||
{
|
||||
// Build a JWT!
|
||||
tok, err := jwt.NewBuilder().
|
||||
Issuer(`github.com/lestrrat-go/jwx`).
|
||||
IssuedAt(time.Now()).
|
||||
Build()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to build token: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Sign a JWT!
|
||||
signed, err := jwt.Sign(tok, jwt.WithKey(jwa.RS256(), privkey))
|
||||
if err != nil {
|
||||
fmt.Printf("failed to sign token: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify a JWT!
|
||||
{
|
||||
verifiedToken, err := jwt.Parse(signed, jwt.WithKey(jwa.RS256(), pubkey))
|
||||
if err != nil {
|
||||
fmt.Printf("failed to verify JWS: %s\n", err)
|
||||
return
|
||||
}
|
||||
_ = verifiedToken
|
||||
}
|
||||
|
||||
// Work with *http.Request!
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodGet, `https://github.com/lestrrat-go/jwx`, nil)
|
||||
req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, signed))
|
||||
|
||||
verifiedToken, err := jwt.ParseRequest(req, jwt.WithKey(jwa.RS256(), pubkey))
|
||||
if err != nil {
|
||||
fmt.Printf("failed to verify token from HTTP request: %s\n", err)
|
||||
return
|
||||
}
|
||||
_ = verifiedToken
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt and Decrypt arbitrary payload with JWE!
|
||||
{
|
||||
encrypted, err := jwe.Encrypt(payloadLoremIpsum, jwe.WithKey(jwa.RSA_OAEP(), jwkRSAPublicKey))
|
||||
if err != nil {
|
||||
fmt.Printf("failed to encrypt payload: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
decrypted, err := jwe.Decrypt(encrypted, jwe.WithKey(jwa.RSA_OAEP(), jwkRSAPrivateKey))
|
||||
if err != nil {
|
||||
fmt.Printf("failed to decrypt payload: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(decrypted, payloadLoremIpsum) {
|
||||
fmt.Printf("verified payload did not match\n")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Sign and Verify arbitrary payload with JWS!
|
||||
{
|
||||
signed, err := jws.Sign(payloadLoremIpsum, jws.WithKey(jwa.RS256(), jwkRSAPrivateKey))
|
||||
if err != nil {
|
||||
fmt.Printf("failed to sign payload: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
verified, err := jws.Verify(signed, jws.WithKey(jwa.RS256(), jwkRSAPublicKey))
|
||||
if err != nil {
|
||||
fmt.Printf("failed to verify payload: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(verified, payloadLoremIpsum) {
|
||||
fmt.Printf("verified payload did not match\n")
|
||||
return
|
||||
}
|
||||
}
|
||||
// OUTPUT:
|
||||
}
|
||||
```
|
||||
source: [examples/jwx_readme_example_test.go](https://github.com/lestrrat-go/jwx/blob/v3/examples/jwx_readme_example_test.go)
|
||||
<!-- END INCLUDE -->
|
||||
|
||||
# How-to Documentation
|
||||
|
||||
* [API documentation](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3)
|
||||
* [How-to style documentation](./docs)
|
||||
* [Runnable Examples](./examples)
|
||||
|
||||
# Description
|
||||
|
||||
This Go module implements JWA, JWE, JWK, JWS, and JWT. Please see the following table for the list of
|
||||
available packages:
|
||||
|
||||
| Package name | Notes |
|
||||
|-----------------------------------------------------------|-------------------------------------------------|
|
||||
| [jwt](https://github.com/lestrrat-go/jwx/tree/v3/jwt) | [RFC 7519](https://tools.ietf.org/html/rfc7519) |
|
||||
| [jwk](https://github.com/lestrrat-go/jwx/tree/v3/jwk) | [RFC 7517](https://tools.ietf.org/html/rfc7517) + [RFC 7638](https://tools.ietf.org/html/rfc7638) |
|
||||
| [jwa](https://github.com/lestrrat-go/jwx/tree/v3/jwa) | [RFC 7518](https://tools.ietf.org/html/rfc7518) |
|
||||
| [jws](https://github.com/lestrrat-go/jwx/tree/v3/jws) | [RFC 7515](https://tools.ietf.org/html/rfc7515) + [RFC 7797](https://tools.ietf.org/html/rfc7797) |
|
||||
| [jwe](https://github.com/lestrrat-go/jwx/tree/v3/jwe) | [RFC 7516](https://tools.ietf.org/html/rfc7516) |
|
||||
## History
|
||||
|
||||
My goal was to write a server that heavily uses JWK and JWT. At first glance
|
||||
the libraries that already exist seemed sufficient, but soon I realized that
|
||||
|
||||
1. To completely implement the protocols, I needed the entire JWT, JWK, JWS, JWE (and JWA, by necessity).
|
||||
2. Most of the libraries that existed only deal with a subset of the various JWx specifications that were necessary to implement their specific needs
|
||||
|
||||
For example, a certain library looks like it had most of JWS, JWE, JWK covered, but then it lacked the ability to include private claims in its JWT responses. Another library had support of all the private claims, but completely lacked in its flexibility to generate various different response formats.
|
||||
|
||||
Because I was writing the server side (and the client side for testing), I needed the *entire* JOSE toolset to properly implement my server, **and** they needed to be *flexible* enough to fulfill the entire spec that I was writing.
|
||||
|
||||
So here's `github.com/lestrrat-go/jwx/v3`. This library is extensible, customizable, and hopefully well organized to the point that it is easy for you to slice and dice it.
|
||||
|
||||
## Why would I use this library?
|
||||
|
||||
There are several other major Go modules that handle JWT and related data formats,
|
||||
so why should you use this library?
|
||||
|
||||
From a purely functional perspective, the only major difference is this:
|
||||
Whereas most other projects only deal with what they seem necessary to handle
|
||||
JWTs, this module handles the **_entire_** spectrum of JWS, JWE, JWK, and JWT.
|
||||
|
||||
That is, if you need to not only parse JWTs, but also to control JWKs, or
|
||||
if you need to handle payloads that are NOT JWTs, you should probably consider
|
||||
using this module. You should also note that JWT is built _on top_ of those
|
||||
other technologies. You simply cannot have a complete JWT package without
|
||||
implementing the entirety of JWS/JWE/JWK, which this library does.
|
||||
|
||||
Next, from an implementation perspective, this module differs significantly
|
||||
from others in that it tries very hard to expose only the APIs, and not the
|
||||
internal data. For example, individual JWT claims are not accessible through
|
||||
struct field lookups. You need to use one of the getter methods.
|
||||
|
||||
This is because this library takes the stance that the end user is fully capable
|
||||
and even willing to shoot themselves on the foot when presented with a lax
|
||||
API. By making sure that users do not have access to open structs, we can protect
|
||||
users from doing silly things like creating _incomplete_ structs, or access the
|
||||
structs concurrently without any protection. This structure also allows
|
||||
us to put extra smarts in the structs, such as doing the right thing when
|
||||
you want to parse / write custom fields (this module does not require the user
|
||||
to specify alternate structs to parse objects with custom fields)
|
||||
|
||||
In the end I think it comes down to your usage pattern, and priorities.
|
||||
Some general guidelines that come to mind are:
|
||||
|
||||
* If you want a single library to handle everything JWx, such as using JWE, JWK, JWS, handling [auto-refreshing JWKs](https://github.com/lestrrat-go/jwx/blob/v3/docs/04-jwk.md#auto-refreshing-remote-keys), use this module.
|
||||
* If you want to honor all possible custom fields transparently, use this module.
|
||||
* If you want a standardized clean API, use this module.
|
||||
|
||||
Otherwise, feel free to choose something else.
|
||||
|
||||
# Contributions
|
||||
|
||||
## Issues
|
||||
|
||||
For bug reports and feature requests, please try to follow the issue templates as much as possible.
|
||||
For either bug reports or feature requests, failing tests are even better.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
Please make sure to include tests that exercise the changes you made.
|
||||
|
||||
If you are editing auto-generated files (those files with the `_gen.go` suffix, please make sure that you do the following:
|
||||
|
||||
1. Edit the generator, not the generated files (e.g. internal/cmd/genreadfile/main.go)
|
||||
2. Run `make generate` (or `go generate`) to generate the new code
|
||||
3. Commit _both_ the generator _and_ the generated files
|
||||
|
||||
## Discussions / Usage
|
||||
|
||||
Please try [discussions](https://github.com/lestrrat-go/jwx/tree/v3/discussions) first.
|
||||
|
||||
# Related Modules
|
||||
|
||||
* [github.com/lestrrat-go/echo-middleware-jwx](https://github.com/lestrrat-go/echo-middleware-jwx) - Sample Echo middleware
|
||||
* [github.com/jwx-go/crypto-signer/gcp](https://github.com/jwx-go/crypto-signer/tree/main/gcp) - GCP KMS wrapper that implements [`crypto.Signer`](https://pkg.go.dev/crypto#Signer)
|
||||
* [github.com/jwx-go/crypto-signer/aws](https://github.com/jwx-go/crypto-signer/tree/main/aws) - AWS KMS wrapper that implements [`crypto.Signer`](https://pkg.go.dev/crypto#Signer)
|
||||
|
||||
# Credits
|
||||
|
||||
* Initial work on this library was generously sponsored by HDE Inc (https://www.hde.co.jp)
|
||||
* Lots of code, especially JWE was initially taken from go-jose library (https://github.com/square/go-jose)
|
||||
* Lots of individual contributors have helped this project over the years. Thank each and everyone of you very much.
|
||||
|
||||
# Quid pro quo
|
||||
|
||||
If you use this software to build products in a for-profit organization, we ask you to _consider_
|
||||
contributing back to FOSS in the following manner:
|
||||
|
||||
* For every 100 employees (direct hires) of your organization, please consider contributing minimum of $1 every year to either this project, **or** another FOSS projects that this project uses. For example, for 100 employees, we ask you contribute $100 yearly; for 10,000 employees, we ask you contribute $10,000 yearly.
|
||||
* If possible, please make this information public. You do not need to disclose the amount you are contributing, but please make the information that you are contributing to particular FOSS projects public. For this project, please consider writing your name on the [Wiki](https://github.com/lestrrat-go/jwx/wiki/Users)
|
||||
|
||||
This is _NOT_ a licensing term: you are still free to use this software according to the license it
|
||||
comes with. This clause is only a plea for people to acknowledge the work from FOSS developers whose
|
||||
work you rely on each and everyday.
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Most recent two major versions will receive security updates
|
||||
|
||||
| Version | Supported |
|
||||
| -------- | ------------------ |
|
||||
| v3.x.x | :white_check_mark: |
|
||||
| v2.x.x | :white_check_mark: |
|
||||
| < v2.0.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you think you found a vulnerability, please report it via [GitHub Security Advisory](https://github.com/lestrrat-go/jwx/security/advisories/new).
|
||||
Please include explicit steps to reproduce the security issue.
|
||||
|
||||
We will do our best to respond in a timely manner, but please also be aware that this project is maintained by a very limited number of people. Please help us with test code and such.
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# Empty WORKSPACE file for bzlmod compatibility
|
||||
# All dependencies are now managed in MODULE.bazel
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
load("@rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "cert",
|
||||
srcs = [
|
||||
"cert.go",
|
||||
"chain.go",
|
||||
],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/cert",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//internal/base64",
|
||||
"//internal/tokens",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "cert_test",
|
||||
srcs = [
|
||||
"cert_test.go",
|
||||
"chain_test.go",
|
||||
],
|
||||
deps = [
|
||||
":cert",
|
||||
"//internal/jwxtest",
|
||||
"@com_github_stretchr_testify//require",
|
||||
],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":cert",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
stdlibb64 "encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/base64"
|
||||
)
|
||||
|
||||
// Create is a wrapper around x509.CreateCertificate, but it additionally
|
||||
// encodes it in base64 so that it can be easily added to `x5c` fields
|
||||
func Create(rand io.Reader, template, parent *x509.Certificate, pub, priv any) ([]byte, error) {
|
||||
der, err := x509.CreateCertificate(rand, template, parent, pub, priv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to create x509 certificate: %w`, err)
|
||||
}
|
||||
return EncodeBase64(der)
|
||||
}
|
||||
|
||||
// EncodeBase64 is a utility function to encode ASN.1 DER certificates
|
||||
// using base64 encoding. This operation is normally done by `pem.Encode`
|
||||
// but since PEM would include the markers (`-----BEGIN`, and the like)
|
||||
// while `x5c` fields do not need this, this function can be used to
|
||||
// shave off a few lines
|
||||
func EncodeBase64(der []byte) ([]byte, error) {
|
||||
enc := stdlibb64.StdEncoding
|
||||
dst := make([]byte, enc.EncodedLen(len(der)))
|
||||
enc.Encode(dst, der)
|
||||
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
|
||||
func Parse(src []byte) (*x509.Certificate, error) {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to parse x509 certificate: %w`, err)
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/tokens"
|
||||
)
|
||||
|
||||
// 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.
|
||||
type Chain struct {
|
||||
certificates [][]byte
|
||||
}
|
||||
|
||||
func (cc Chain) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte(tokens.OpenSquareBracket)
|
||||
for i, cert := range cc.certificates {
|
||||
if i > 0 {
|
||||
buf.WriteByte(tokens.Comma)
|
||||
}
|
||||
buf.WriteByte('"')
|
||||
buf.Write(cert)
|
||||
buf.WriteByte('"')
|
||||
}
|
||||
buf.WriteByte(tokens.CloseSquareBracket)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (cc *Chain) UnmarshalJSON(data []byte) error {
|
||||
var tmp []string
|
||||
if err := json.Unmarshal(data, &tmp); 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)
|
||||
}
|
||||
cc.certificates = certs
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the n-th ASN.1 DER + base64 encoded certificate
|
||||
// stored. `false` will be returned in the second argument if
|
||||
// the corresponding index is out of range.
|
||||
func (cc *Chain) Get(index int) ([]byte, bool) {
|
||||
if index < 0 || index >= len(cc.certificates) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return cc.certificates[index], true
|
||||
}
|
||||
|
||||
// Len returns the number of certificates stored in this Chain
|
||||
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))
|
||||
}
|
||||
|
||||
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)
|
||||
der = bytes.TrimSpace(der)
|
||||
cc.certificates = append(cc.certificates, der)
|
||||
return nil
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
codecov:
|
||||
allow_coverage_offsets: true
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package jwx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/tokens"
|
||||
)
|
||||
|
||||
type FormatKind int
|
||||
|
||||
// These constants describe the result from guessing the format
|
||||
// of the incoming buffer.
|
||||
const (
|
||||
// InvalidFormat is returned when the format of the incoming buffer
|
||||
// has been deemed conclusively invalid
|
||||
InvalidFormat FormatKind = iota
|
||||
// UnknownFormat is returned when GuessFormat was not able to conclusively
|
||||
// determine the format of the
|
||||
UnknownFormat
|
||||
JWE
|
||||
JWS
|
||||
JWK
|
||||
JWKS
|
||||
JWT
|
||||
)
|
||||
|
||||
type formatHint struct {
|
||||
Payload json.RawMessage `json:"payload"` // Only in JWS
|
||||
Signatures json.RawMessage `json:"signatures"` // Only in JWS
|
||||
Ciphertext json.RawMessage `json:"ciphertext"` // Only in JWE
|
||||
KeyType json.RawMessage `json:"kty"` // Only in JWK
|
||||
Keys json.RawMessage `json:"keys"` // Only in JWKS
|
||||
Audience json.RawMessage `json:"aud"` // Only in JWT
|
||||
}
|
||||
|
||||
// GuessFormat is used to guess the format the given payload is in
|
||||
// using heuristics. See the type FormatKind for a full list of
|
||||
// possible types.
|
||||
//
|
||||
// This may be useful in determining your next action when you may
|
||||
// encounter a payload that could either be a JWE, JWS, or a plain JWT.
|
||||
//
|
||||
// Because JWTs are almost always JWS signed, you may be thrown off
|
||||
// if you pass what you think is a JWT payload to this function.
|
||||
// If the function is in the "Compact" format, it means it's a JWS
|
||||
// signed message, and its payload is the JWT. Therefore this function
|
||||
// will return JWS, not JWT.
|
||||
//
|
||||
// This function requires an extra parsing of the payload, and therefore
|
||||
// may be inefficient if you call it every time before parsing.
|
||||
func GuessFormat(payload []byte) FormatKind {
|
||||
// The check against kty, keys, and aud are something this library
|
||||
// made up. for the distinctions between JWE and JWS, we used
|
||||
// https://datatracker.ietf.org/doc/html/rfc7516#section-9.
|
||||
//
|
||||
// The above RFC described several ways to distinguish between
|
||||
// a JWE and JWS JSON, but we're only using one of them
|
||||
|
||||
payload = bytes.TrimSpace(payload)
|
||||
if len(payload) <= 0 {
|
||||
return UnknownFormat
|
||||
}
|
||||
|
||||
if payload[0] != tokens.OpenCurlyBracket {
|
||||
// Compact format. It's probably a JWS or JWE
|
||||
sep := []byte{tokens.Period} // I want to const this :/
|
||||
|
||||
// Note: this counts the number of occurrences of the
|
||||
// separator, but the RFC talks about the number of segments.
|
||||
// number of tokens.Period == segments - 1, so that's why we have 2 and 4 here
|
||||
switch count := bytes.Count(payload, sep); count {
|
||||
case 2:
|
||||
return JWS
|
||||
case 4:
|
||||
return JWE
|
||||
default:
|
||||
return InvalidFormat
|
||||
}
|
||||
}
|
||||
|
||||
// If we got here, we probably have JSON.
|
||||
var h formatHint
|
||||
if err := json.Unmarshal(payload, &h); err != nil {
|
||||
return UnknownFormat
|
||||
}
|
||||
|
||||
if h.Audience != nil {
|
||||
return JWT
|
||||
}
|
||||
if h.KeyType != nil {
|
||||
return JWK
|
||||
}
|
||||
if h.Keys != nil {
|
||||
return JWKS
|
||||
}
|
||||
if h.Ciphertext != nil {
|
||||
return JWE
|
||||
}
|
||||
if h.Signatures != nil && h.Payload != nil {
|
||||
return JWS
|
||||
}
|
||||
return UnknownFormat
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Code generated by "stringer -type=FormatKind"; DO NOT EDIT.
|
||||
|
||||
package jwx
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[InvalidFormat-0]
|
||||
_ = x[UnknownFormat-1]
|
||||
_ = x[JWE-2]
|
||||
_ = x[JWS-3]
|
||||
_ = x[JWK-4]
|
||||
_ = x[JWKS-5]
|
||||
_ = x[JWT-6]
|
||||
}
|
||||
|
||||
const _FormatKind_name = "InvalidFormatUnknownFormatJWEJWSJWKJWKSJWT"
|
||||
|
||||
var _FormatKind_index = [...]uint8{0, 13, 26, 29, 32, 35, 39, 42}
|
||||
|
||||
func (i FormatKind) String() string {
|
||||
idx := int(i) - 0
|
||||
if i < 0 || idx >= len(_FormatKind_index)-1 {
|
||||
return "FormatKind(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _FormatKind_name[_FormatKind_index[idx]:_FormatKind_index[idx+1]]
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
load("@rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "base64",
|
||||
srcs = ["base64.go"],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/internal/base64",
|
||||
visibility = ["//:__subpackages__"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "base64_test",
|
||||
srcs = ["base64_test.go"],
|
||||
embed = [":base64"],
|
||||
deps = ["@com_github_stretchr_testify//require"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":base64",
|
||||
visibility = ["//:__subpackages__"],
|
||||
)
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
//go:build jwx_asmbase64
|
||||
|
||||
package base64
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
asmbase64 "github.com/segmentio/asm/base64"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SetEncoder(asmEncoder{asmbase64.RawURLEncoding})
|
||||
SetDecoder(asmDecoder{})
|
||||
}
|
||||
|
||||
type asmEncoder struct {
|
||||
*asmbase64.Encoding
|
||||
}
|
||||
|
||||
func (e asmEncoder) AppendEncode(dst, src []byte) []byte {
|
||||
n := e.Encoding.EncodedLen(len(src))
|
||||
dst = slices.Grow(dst, n)
|
||||
e.Encoding.Encode(dst[len(dst):][:n], src)
|
||||
return dst[:len(dst)+n]
|
||||
}
|
||||
|
||||
type asmDecoder struct{}
|
||||
|
||||
func (d asmDecoder) Decode(src []byte) ([]byte, error) {
|
||||
var enc *asmbase64.Encoding
|
||||
switch Guess(src) {
|
||||
case Std:
|
||||
enc = asmbase64.StdEncoding
|
||||
case RawStd:
|
||||
enc = asmbase64.RawStdEncoding
|
||||
case URL:
|
||||
enc = asmbase64.URLEncoding
|
||||
case RawURL:
|
||||
enc = asmbase64.RawURLEncoding
|
||||
default:
|
||||
return nil, fmt.Errorf(`invalid encoding`)
|
||||
}
|
||||
|
||||
dst := make([]byte, enc.DecodedLen(len(src)))
|
||||
n, err := enc.Decode(dst, src)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to decode source: %w`, err)
|
||||
}
|
||||
return dst[:n], nil
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package base64
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Decoder interface {
|
||||
Decode([]byte) ([]byte, error)
|
||||
}
|
||||
|
||||
type Encoder interface {
|
||||
Encode([]byte, []byte)
|
||||
EncodedLen(int) int
|
||||
EncodeToString([]byte) string
|
||||
AppendEncode([]byte, []byte) []byte
|
||||
}
|
||||
|
||||
var muEncoder sync.RWMutex
|
||||
var encoder Encoder = base64.RawURLEncoding
|
||||
var muDecoder sync.RWMutex
|
||||
var decoder Decoder = defaultDecoder{}
|
||||
|
||||
func SetEncoder(enc Encoder) {
|
||||
muEncoder.Lock()
|
||||
defer muEncoder.Unlock()
|
||||
encoder = enc
|
||||
}
|
||||
|
||||
func getEncoder() Encoder {
|
||||
muEncoder.RLock()
|
||||
defer muEncoder.RUnlock()
|
||||
return encoder
|
||||
}
|
||||
|
||||
func DefaultEncoder() Encoder {
|
||||
return getEncoder()
|
||||
}
|
||||
|
||||
func SetDecoder(dec Decoder) {
|
||||
muDecoder.Lock()
|
||||
defer muDecoder.Unlock()
|
||||
decoder = dec
|
||||
}
|
||||
|
||||
func getDecoder() Decoder {
|
||||
muDecoder.RLock()
|
||||
defer muDecoder.RUnlock()
|
||||
return decoder
|
||||
}
|
||||
|
||||
func Encode(src []byte) []byte {
|
||||
encoder := getEncoder()
|
||||
dst := make([]byte, encoder.EncodedLen(len(src)))
|
||||
encoder.Encode(dst, src)
|
||||
return dst
|
||||
}
|
||||
|
||||
func EncodeToString(src []byte) string {
|
||||
return getEncoder().EncodeToString(src)
|
||||
}
|
||||
|
||||
func EncodeUint64ToString(v uint64) string {
|
||||
data := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(data, v)
|
||||
|
||||
i := 0
|
||||
for ; i < len(data); i++ {
|
||||
if data[i] != 0x0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return EncodeToString(data[i:])
|
||||
}
|
||||
|
||||
const (
|
||||
InvalidEncoding = iota
|
||||
Std
|
||||
URL
|
||||
RawStd
|
||||
RawURL
|
||||
)
|
||||
|
||||
func Guess(src []byte) int {
|
||||
var isRaw = !bytes.HasSuffix(src, []byte{'='})
|
||||
var isURL = !bytes.ContainsAny(src, "+/")
|
||||
switch {
|
||||
case isRaw && isURL:
|
||||
return RawURL
|
||||
case isURL:
|
||||
return URL
|
||||
case isRaw:
|
||||
return RawStd
|
||||
default:
|
||||
return Std
|
||||
}
|
||||
}
|
||||
|
||||
// defaultDecoder is a Decoder that detects the encoding of the source and
|
||||
// decodes it accordingly. This shouldn't really be required per the spec, but
|
||||
// it exist because we have seen in the wild JWTs that are encoded using
|
||||
// various versions of the base64 encoding.
|
||||
type defaultDecoder struct{}
|
||||
|
||||
func (defaultDecoder) Decode(src []byte) ([]byte, error) {
|
||||
var enc *base64.Encoding
|
||||
|
||||
switch Guess(src) {
|
||||
case RawURL:
|
||||
enc = base64.RawURLEncoding
|
||||
case URL:
|
||||
enc = base64.URLEncoding
|
||||
case RawStd:
|
||||
enc = base64.RawStdEncoding
|
||||
case Std:
|
||||
enc = base64.StdEncoding
|
||||
default:
|
||||
return nil, fmt.Errorf(`invalid encoding`)
|
||||
}
|
||||
|
||||
dst := make([]byte, enc.DecodedLen(len(src)))
|
||||
n, err := enc.Decode(dst, src)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to decode source: %w`, err)
|
||||
}
|
||||
return dst[:n], nil
|
||||
}
|
||||
|
||||
func Decode(src []byte) ([]byte, error) {
|
||||
return getDecoder().Decode(src)
|
||||
}
|
||||
|
||||
func DecodeString(src string) ([]byte, error) {
|
||||
return getDecoder().Decode([]byte(src))
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
load("@rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "ecutil",
|
||||
srcs = ["ecutil.go"],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/internal/ecutil",
|
||||
visibility = ["//:__subpackages__"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":ecutil",
|
||||
visibility = ["//:__subpackages__"],
|
||||
)
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
// Package ecutil defines tools that help with elliptic curve related
|
||||
// computation
|
||||
package ecutil
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"math/big"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// size of buffer that needs to be allocated for EC521 curve
|
||||
ec521BufferSize = 66 // (521 / 8) + 1
|
||||
)
|
||||
|
||||
var ecpointBufferPool = sync.Pool{
|
||||
New: func() any {
|
||||
// In most cases the curve bit size will be less than this length
|
||||
// so allocate the maximum, and keep reusing
|
||||
buf := make([]byte, 0, ec521BufferSize)
|
||||
return &buf
|
||||
},
|
||||
}
|
||||
|
||||
func getCrvFixedBuffer(size int) []byte {
|
||||
//nolint:forcetypeassert
|
||||
buf := *(ecpointBufferPool.Get().(*[]byte))
|
||||
if size > ec521BufferSize && cap(buf) < size {
|
||||
buf = append(buf, make([]byte, size-cap(buf))...)
|
||||
}
|
||||
return buf[:size]
|
||||
}
|
||||
|
||||
// ReleaseECPointBuffer releases the []byte buffer allocated.
|
||||
func ReleaseECPointBuffer(buf []byte) {
|
||||
buf = buf[:cap(buf)]
|
||||
buf[0] = 0x0
|
||||
for i := 1; i < len(buf); i *= 2 {
|
||||
copy(buf[i:], buf[:i])
|
||||
}
|
||||
buf = buf[:0]
|
||||
ecpointBufferPool.Put(&buf)
|
||||
}
|
||||
|
||||
func CalculateKeySize(crv elliptic.Curve) int {
|
||||
// We need to create a buffer that fits the entire curve.
|
||||
// If the curve size is 66, that fits in 9 bytes. If the curve
|
||||
// size is 64, it fits in 8 bytes.
|
||||
bits := crv.Params().BitSize
|
||||
|
||||
// For most common cases we know before hand what the byte length
|
||||
// is going to be. optimize
|
||||
var inBytes int
|
||||
switch bits {
|
||||
case 224, 256, 384: // TODO: use constant?
|
||||
inBytes = bits / 8
|
||||
case 521:
|
||||
inBytes = ec521BufferSize
|
||||
default:
|
||||
inBytes = bits / 8
|
||||
if (bits % 8) != 0 {
|
||||
inBytes++
|
||||
}
|
||||
}
|
||||
|
||||
return inBytes
|
||||
}
|
||||
|
||||
// AllocECPointBuffer allocates a buffer for the given point in the given
|
||||
// curve. This buffer should be released using the ReleaseECPointBuffer
|
||||
// function.
|
||||
func AllocECPointBuffer(v *big.Int, crv elliptic.Curve) []byte {
|
||||
buf := getCrvFixedBuffer(CalculateKeySize(crv))
|
||||
v.FillBytes(buf)
|
||||
return buf
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
load("@rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "json",
|
||||
srcs = [
|
||||
"json.go",
|
||||
"registry.go",
|
||||
"stdlib.go",
|
||||
],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/internal/json",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = ["//internal/base64"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":json",
|
||||
visibility = ["//:__subpackages__"],
|
||||
)
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
//go:build jwx_goccy
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
type Decoder = json.Decoder
|
||||
type Delim = json.Delim
|
||||
type Encoder = json.Encoder
|
||||
type Marshaler = json.Marshaler
|
||||
type Number = json.Number
|
||||
type RawMessage = json.RawMessage
|
||||
type Unmarshaler = json.Unmarshaler
|
||||
|
||||
func Engine() string {
|
||||
return "github.com/goccy/go-json"
|
||||
}
|
||||
|
||||
// NewDecoder respects the values specified in DecoderSettings,
|
||||
// and creates a Decoder that has certain features turned on/off
|
||||
func NewDecoder(r io.Reader) *json.Decoder {
|
||||
dec := json.NewDecoder(r)
|
||||
|
||||
if UseNumber() {
|
||||
dec.UseNumber()
|
||||
}
|
||||
|
||||
return dec
|
||||
}
|
||||
|
||||
// NewEncoder is just a proxy for "encoding/json".NewEncoder
|
||||
func NewEncoder(w io.Writer) *json.Encoder {
|
||||
return json.NewEncoder(w)
|
||||
}
|
||||
|
||||
// Marshal is just a proxy for "encoding/json".Marshal
|
||||
func Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
// MarshalIndent is just a proxy for "encoding/json".MarshalIndent
|
||||
func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
return json.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
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
|
||||
|
||||
func UseNumber() bool {
|
||||
return atomic.LoadUint32(&useNumber) == 1
|
||||
}
|
||||
|
||||
// Sets the global configuration for json decoding
|
||||
func DecoderSettings(inUseNumber bool) {
|
||||
var val uint32
|
||||
if inUseNumber {
|
||||
val = 1
|
||||
}
|
||||
atomic.StoreUint32(&useNumber, val)
|
||||
}
|
||||
|
||||
// Unmarshal respects the values specified in DecoderSettings,
|
||||
// and uses a Decoder that has certain features turned on/off
|
||||
func Unmarshal(b []byte, v any) error {
|
||||
dec := NewDecoder(bytes.NewReader(b))
|
||||
return dec.Decode(v)
|
||||
}
|
||||
|
||||
func AssignNextBytesToken(dst *[]byte, dec *Decoder) error {
|
||||
var val string
|
||||
if err := dec.Decode(&val); err != nil {
|
||||
return fmt.Errorf(`error reading next value: %w`, err)
|
||||
}
|
||||
|
||||
buf, err := base64.DecodeString(val)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`expected base64 encoded []byte (%T)`, val)
|
||||
}
|
||||
*dst = buf
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadNextStringToken(dec *Decoder) (string, error) {
|
||||
var val string
|
||||
if err := dec.Decode(&val); err != nil {
|
||||
return "", fmt.Errorf(`error reading next value: %w`, err)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func AssignNextStringToken(dst **string, dec *Decoder) error {
|
||||
val, err := ReadNextStringToken(dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*dst = &val
|
||||
return nil
|
||||
}
|
||||
|
||||
// FlattenAudience is a flag to specify if we should flatten the "aud"
|
||||
// entry to a string when there's only one entry.
|
||||
// In jwx < 1.1.8 we just dumped everything as an array of strings,
|
||||
// but apparently AWS Cognito doesn't handle this well.
|
||||
//
|
||||
// So now we have the ability to dump "aud" as a string if there's
|
||||
// only one entry, but we need to retain the old behavior so that
|
||||
// we don't accidentally break somebody else's code. (e.g. messing
|
||||
// up how signatures are calculated)
|
||||
var FlattenAudience uint32
|
||||
|
||||
func MarshalAudience(aud []string, flatten bool) ([]byte, error) {
|
||||
var val any
|
||||
if len(aud) == 1 && flatten {
|
||||
val = aud[0]
|
||||
} else {
|
||||
val = aud
|
||||
}
|
||||
return Marshal(val)
|
||||
}
|
||||
|
||||
func EncodeAudience(enc *Encoder, aud []string, flatten bool) error {
|
||||
var val any
|
||||
if len(aud) == 1 && flatten {
|
||||
val = aud[0]
|
||||
} else {
|
||||
val = aud
|
||||
}
|
||||
return enc.Encode(val)
|
||||
}
|
||||
|
||||
// DecodeCtx is an interface for objects that needs that extra something
|
||||
// when decoding JSON into an object.
|
||||
type DecodeCtx interface {
|
||||
Registry() *Registry
|
||||
}
|
||||
|
||||
// DecodeCtxContainer is used to differentiate objects that can carry extra
|
||||
// decoding hints and those who can't.
|
||||
type DecodeCtxContainer interface {
|
||||
DecodeCtx() DecodeCtx
|
||||
SetDecodeCtx(DecodeCtx)
|
||||
}
|
||||
|
||||
// stock decodeCtx. should cover 80% of the cases
|
||||
type decodeCtx struct {
|
||||
registry *Registry
|
||||
}
|
||||
|
||||
func NewDecodeCtx(r *Registry) DecodeCtx {
|
||||
return &decodeCtx{registry: r}
|
||||
}
|
||||
|
||||
func (dc *decodeCtx) Registry() *Registry {
|
||||
return dc.registry
|
||||
}
|
||||
|
||||
func Dump(v any) {
|
||||
enc := NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
//nolint:errchkjson
|
||||
_ = enc.Encode(v)
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// CustomDecoder is the interface we expect from RegisterCustomField in jws, jwe, jwk, and jwt packages.
|
||||
type CustomDecoder interface {
|
||||
// Decode takes a JSON encoded byte slice and returns the desired
|
||||
// decoded value,which will be used as the value for that field
|
||||
// registered through RegisterCustomField
|
||||
Decode([]byte) (any, error)
|
||||
}
|
||||
|
||||
// CustomDecodeFunc is a stateless, function-based implementation of CustomDecoder
|
||||
type CustomDecodeFunc func([]byte) (any, error)
|
||||
|
||||
func (fn CustomDecodeFunc) Decode(data []byte) (any, error) {
|
||||
return fn(data)
|
||||
}
|
||||
|
||||
type objectTypeDecoder struct {
|
||||
typ reflect.Type
|
||||
name string
|
||||
}
|
||||
|
||||
func (dec *objectTypeDecoder) Decode(data []byte) (any, error) {
|
||||
ptr := reflect.New(dec.typ).Interface()
|
||||
if err := Unmarshal(data, ptr); err != nil {
|
||||
return nil, fmt.Errorf(`failed to decode field %s: %w`, dec.name, err)
|
||||
}
|
||||
return reflect.ValueOf(ptr).Elem().Interface(), nil
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
mu *sync.RWMutex
|
||||
ctrs map[string]CustomDecoder
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
mu: &sync.RWMutex{},
|
||||
ctrs: make(map[string]CustomDecoder),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) Register(name string, object any) {
|
||||
if object == nil {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
delete(r.ctrs, name)
|
||||
return
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if ctr, ok := object.(CustomDecoder); ok {
|
||||
r.ctrs[name] = ctr
|
||||
} else {
|
||||
r.ctrs[name] = &objectTypeDecoder{
|
||||
typ: reflect.TypeOf(object),
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) Decode(dec *Decoder, name string) (any, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
if ctr, ok := r.ctrs[name]; ok {
|
||||
var raw RawMessage
|
||||
if err := dec.Decode(&raw); err != nil {
|
||||
return nil, fmt.Errorf(`failed to decode field %s: %w`, name, err)
|
||||
}
|
||||
v, err := ctr.Decode([]byte(raw))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to decode field %s: %w`, name, err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
var decoded any
|
||||
if err := dec.Decode(&decoded); err != nil {
|
||||
return nil, fmt.Errorf(`failed to decode field %s: %w`, name, err)
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
//go:build !jwx_goccy
|
||||
|
||||
//nolint:revive
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Decoder = json.Decoder
|
||||
type Delim = json.Delim
|
||||
type Encoder = json.Encoder
|
||||
type Marshaler = json.Marshaler
|
||||
type Number = json.Number
|
||||
type RawMessage = json.RawMessage
|
||||
type Unmarshaler = json.Unmarshaler
|
||||
|
||||
func Engine() string {
|
||||
return "encoding/json"
|
||||
}
|
||||
|
||||
// NewDecoder respects the values specified in DecoderSettings,
|
||||
// and creates a Decoder that has certain features turned on/off
|
||||
func NewDecoder(r io.Reader) *json.Decoder {
|
||||
dec := json.NewDecoder(r)
|
||||
|
||||
if UseNumber() {
|
||||
dec.UseNumber()
|
||||
}
|
||||
|
||||
return dec
|
||||
}
|
||||
|
||||
func NewEncoder(w io.Writer) *json.Encoder {
|
||||
return json.NewEncoder(w)
|
||||
}
|
||||
|
||||
// Marshal is just a proxy for "encoding/json".Marshal
|
||||
func Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
// MarshalIndent is just a proxy for "encoding/json".MarshalIndent
|
||||
func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
return json.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
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__"],
|
||||
)
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
load("@rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "keyconv",
|
||||
srcs = ["keyconv.go"],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/internal/keyconv",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//jwk",
|
||||
"@com_github_lestrrat_go_blackmagic//:blackmagic",
|
||||
"@org_golang_x_crypto//ed25519",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "keyconv_test",
|
||||
srcs = ["keyconv_test.go"],
|
||||
deps = [
|
||||
":keyconv",
|
||||
"//internal/jwxtest",
|
||||
"//jwa",
|
||||
"//jwk",
|
||||
"@com_github_stretchr_testify//require",
|
||||
],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":keyconv",
|
||||
visibility = ["//:__subpackages__"],
|
||||
)
|
||||
+354
@@ -0,0 +1,354 @@
|
||||
package keyconv
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdh"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/lestrrat-go/blackmagic"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
)
|
||||
|
||||
// RSAPrivateKey assigns src to dst.
|
||||
// `dst` should be a pointer to a rsa.PrivateKey.
|
||||
// `src` may be rsa.PrivateKey, *rsa.PrivateKey, or a jwk.Key
|
||||
func RSAPrivateKey(dst, src any) error {
|
||||
if jwkKey, ok := src.(jwk.Key); ok {
|
||||
var raw rsa.PrivateKey
|
||||
if err := jwk.Export(jwkKey, &raw); err != nil {
|
||||
return fmt.Errorf(`failed to produce rsa.PrivateKey from %T: %w`, src, err)
|
||||
}
|
||||
src = &raw
|
||||
}
|
||||
|
||||
var ptr *rsa.PrivateKey
|
||||
switch src := src.(type) {
|
||||
case rsa.PrivateKey:
|
||||
ptr = &src
|
||||
case *rsa.PrivateKey:
|
||||
ptr = src
|
||||
default:
|
||||
return fmt.Errorf(`keyconv: expected rsa.PrivateKey or *rsa.PrivateKey, got %T`, src)
|
||||
}
|
||||
|
||||
return blackmagic.AssignIfCompatible(dst, ptr)
|
||||
}
|
||||
|
||||
// RSAPublicKey assigns src to dst
|
||||
// `dst` should be a pointer to a non-zero rsa.PublicKey.
|
||||
// `src` may be rsa.PublicKey, *rsa.PublicKey, or a jwk.Key
|
||||
func RSAPublicKey(dst, src any) error {
|
||||
if jwkKey, ok := src.(jwk.Key); ok {
|
||||
pk, err := jwk.PublicRawKeyOf(jwkKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`keyconv: failed to produce public key from %T: %w`, src, err)
|
||||
}
|
||||
src = pk
|
||||
}
|
||||
|
||||
var ptr *rsa.PublicKey
|
||||
switch src := src.(type) {
|
||||
case rsa.PrivateKey:
|
||||
ptr = &src.PublicKey
|
||||
case *rsa.PrivateKey:
|
||||
ptr = &src.PublicKey
|
||||
case rsa.PublicKey:
|
||||
ptr = &src
|
||||
case *rsa.PublicKey:
|
||||
ptr = src
|
||||
default:
|
||||
return fmt.Errorf(`keyconv: expected rsa.PublicKey/rsa.PrivateKey or *rsa.PublicKey/*rsa.PrivateKey, got %T`, src)
|
||||
}
|
||||
|
||||
return blackmagic.AssignIfCompatible(dst, ptr)
|
||||
}
|
||||
|
||||
// ECDSAPrivateKey assigns src to dst, converting its type from a
|
||||
// non-pointer to a pointer
|
||||
func ECDSAPrivateKey(dst, src any) error {
|
||||
if jwkKey, ok := src.(jwk.Key); ok {
|
||||
var raw ecdsa.PrivateKey
|
||||
if err := jwk.Export(jwkKey, &raw); err != nil {
|
||||
return fmt.Errorf(`keyconv: failed to produce ecdsa.PrivateKey from %T: %w`, src, err)
|
||||
}
|
||||
src = &raw
|
||||
}
|
||||
|
||||
var ptr *ecdsa.PrivateKey
|
||||
switch src := src.(type) {
|
||||
case ecdsa.PrivateKey:
|
||||
ptr = &src
|
||||
case *ecdsa.PrivateKey:
|
||||
ptr = src
|
||||
default:
|
||||
return fmt.Errorf(`keyconv: expected ecdsa.PrivateKey or *ecdsa.PrivateKey, got %T`, src)
|
||||
}
|
||||
return blackmagic.AssignIfCompatible(dst, ptr)
|
||||
}
|
||||
|
||||
// ECDSAPublicKey assigns src to dst, converting its type from a
|
||||
// non-pointer to a pointer
|
||||
func ECDSAPublicKey(dst, src any) error {
|
||||
if jwkKey, ok := src.(jwk.Key); ok {
|
||||
pk, err := jwk.PublicRawKeyOf(jwkKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`keyconv: failed to produce public key from %T: %w`, src, err)
|
||||
}
|
||||
src = pk
|
||||
}
|
||||
|
||||
var ptr *ecdsa.PublicKey
|
||||
switch src := src.(type) {
|
||||
case ecdsa.PrivateKey:
|
||||
ptr = &src.PublicKey
|
||||
case *ecdsa.PrivateKey:
|
||||
ptr = &src.PublicKey
|
||||
case ecdsa.PublicKey:
|
||||
ptr = &src
|
||||
case *ecdsa.PublicKey:
|
||||
ptr = src
|
||||
default:
|
||||
return fmt.Errorf(`keyconv: expected ecdsa.PublicKey/ecdsa.PrivateKey or *ecdsa.PublicKey/*ecdsa.PrivateKey, got %T`, src)
|
||||
}
|
||||
return blackmagic.AssignIfCompatible(dst, ptr)
|
||||
}
|
||||
|
||||
func ByteSliceKey(dst, src any) error {
|
||||
if jwkKey, ok := src.(jwk.Key); ok {
|
||||
var raw []byte
|
||||
if err := jwk.Export(jwkKey, &raw); err != nil {
|
||||
return fmt.Errorf(`keyconv: failed to produce []byte from %T: %w`, src, err)
|
||||
}
|
||||
src = raw
|
||||
}
|
||||
|
||||
if _, ok := src.([]byte); !ok {
|
||||
return fmt.Errorf(`keyconv: expected []byte, got %T`, src)
|
||||
}
|
||||
return blackmagic.AssignIfCompatible(dst, src)
|
||||
}
|
||||
|
||||
func Ed25519PrivateKey(dst, src any) error {
|
||||
if jwkKey, ok := src.(jwk.Key); ok {
|
||||
var raw ed25519.PrivateKey
|
||||
if err := jwk.Export(jwkKey, &raw); err != nil {
|
||||
return fmt.Errorf(`failed to produce ed25519.PrivateKey from %T: %w`, src, err)
|
||||
}
|
||||
src = &raw
|
||||
}
|
||||
|
||||
var ptr *ed25519.PrivateKey
|
||||
switch src := src.(type) {
|
||||
case ed25519.PrivateKey:
|
||||
ptr = &src
|
||||
case *ed25519.PrivateKey:
|
||||
ptr = src
|
||||
default:
|
||||
return fmt.Errorf(`expected ed25519.PrivateKey or *ed25519.PrivateKey, got %T`, src)
|
||||
}
|
||||
return blackmagic.AssignIfCompatible(dst, ptr)
|
||||
}
|
||||
|
||||
func Ed25519PublicKey(dst, src any) error {
|
||||
if jwkKey, ok := src.(jwk.Key); ok {
|
||||
pk, err := jwk.PublicRawKeyOf(jwkKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`keyconv: failed to produce public key from %T: %w`, src, err)
|
||||
}
|
||||
src = pk
|
||||
}
|
||||
|
||||
switch key := src.(type) {
|
||||
case ed25519.PrivateKey:
|
||||
src = key.Public()
|
||||
case *ed25519.PrivateKey:
|
||||
src = key.Public()
|
||||
}
|
||||
|
||||
var ptr *ed25519.PublicKey
|
||||
switch src := src.(type) {
|
||||
case ed25519.PublicKey:
|
||||
ptr = &src
|
||||
case *ed25519.PublicKey:
|
||||
ptr = src
|
||||
case *crypto.PublicKey:
|
||||
tmp, ok := (*src).(ed25519.PublicKey)
|
||||
if !ok {
|
||||
return fmt.Errorf(`failed to retrieve ed25519.PublicKey out of *crypto.PublicKey`)
|
||||
}
|
||||
ptr = &tmp
|
||||
case crypto.PublicKey:
|
||||
tmp, ok := src.(ed25519.PublicKey)
|
||||
if !ok {
|
||||
return fmt.Errorf(`failed to retrieve ed25519.PublicKey out of crypto.PublicKey`)
|
||||
}
|
||||
ptr = &tmp
|
||||
default:
|
||||
return fmt.Errorf(`expected ed25519.PublicKey or *ed25519.PublicKey, got %T`, src)
|
||||
}
|
||||
return blackmagic.AssignIfCompatible(dst, ptr)
|
||||
}
|
||||
|
||||
type privECDHer interface {
|
||||
ECDH() (*ecdh.PrivateKey, error)
|
||||
}
|
||||
|
||||
func ECDHPrivateKey(dst, src any) error {
|
||||
var privECDH *ecdh.PrivateKey
|
||||
if jwkKey, ok := src.(jwk.Key); ok {
|
||||
var rawECDH ecdh.PrivateKey
|
||||
if err := jwk.Export(jwkKey, &rawECDH); err == nil {
|
||||
privECDH = &rawECDH
|
||||
} else {
|
||||
// If we cannot export the key as an ecdh.PrivateKey, we try to export it as an ecdsa.PrivateKey
|
||||
var rawECDSA ecdsa.PrivateKey
|
||||
if err := jwk.Export(jwkKey, &rawECDSA); err != nil {
|
||||
return fmt.Errorf(`keyconv: failed to produce ecdh.PrivateKey or ecdsa.PrivateKey from %T: %w`, src, err)
|
||||
}
|
||||
src = &rawECDSA
|
||||
}
|
||||
}
|
||||
|
||||
switch src := src.(type) {
|
||||
case ecdh.PrivateKey:
|
||||
privECDH = &src
|
||||
case *ecdh.PrivateKey:
|
||||
privECDH = src
|
||||
case privECDHer:
|
||||
priv, err := src.ECDH()
|
||||
if err != nil {
|
||||
return fmt.Errorf(`keyconv: failed to convert ecdsa.PrivateKey to ecdh.PrivateKey: %w`, err)
|
||||
}
|
||||
privECDH = priv
|
||||
}
|
||||
|
||||
return blackmagic.AssignIfCompatible(dst, privECDH)
|
||||
}
|
||||
|
||||
type pubECDHer interface {
|
||||
ECDH() (*ecdh.PublicKey, error)
|
||||
}
|
||||
|
||||
func ECDHPublicKey(dst, src any) error {
|
||||
var pubECDH *ecdh.PublicKey
|
||||
if jwkKey, ok := src.(jwk.Key); ok {
|
||||
var rawECDH ecdh.PublicKey
|
||||
if err := jwk.Export(jwkKey, &rawECDH); err == nil {
|
||||
pubECDH = &rawECDH
|
||||
} else {
|
||||
// If we cannot export the key as an ecdh.PublicKey, we try to export it as an ecdsa.PublicKey
|
||||
var rawECDSA ecdsa.PublicKey
|
||||
if err := jwk.Export(jwkKey, &rawECDSA); err != nil {
|
||||
return fmt.Errorf(`keyconv: failed to produce ecdh.PublicKey or ecdsa.PublicKey from %T: %w`, src, err)
|
||||
}
|
||||
src = &rawECDSA
|
||||
}
|
||||
}
|
||||
|
||||
switch src := src.(type) {
|
||||
case ecdh.PublicKey:
|
||||
pubECDH = &src
|
||||
case *ecdh.PublicKey:
|
||||
pubECDH = src
|
||||
case pubECDHer:
|
||||
pub, err := src.ECDH()
|
||||
if err != nil {
|
||||
return fmt.Errorf(`keyconv: failed to convert ecdsa.PublicKey to ecdh.PublicKey: %w`, err)
|
||||
}
|
||||
pubECDH = pub
|
||||
}
|
||||
|
||||
return blackmagic.AssignIfCompatible(dst, pubECDH)
|
||||
}
|
||||
|
||||
// ecdhCurveToElliptic maps ECDH curves to elliptic curves
|
||||
func ecdhCurveToElliptic(ecdhCurve ecdh.Curve) (elliptic.Curve, error) {
|
||||
switch ecdhCurve {
|
||||
case ecdh.P256():
|
||||
return elliptic.P256(), nil
|
||||
case ecdh.P384():
|
||||
return elliptic.P384(), nil
|
||||
case ecdh.P521():
|
||||
return elliptic.P521(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf(`keyconv: unsupported ECDH curve: %v`, ecdhCurve)
|
||||
}
|
||||
}
|
||||
|
||||
// ecdhPublicKeyToECDSA converts an ECDH public key to an ECDSA public key
|
||||
func ecdhPublicKeyToECDSA(ecdhPubKey *ecdh.PublicKey) (*ecdsa.PublicKey, error) {
|
||||
curve, err := ecdhCurveToElliptic(ecdhPubKey.Curve())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubBytes := ecdhPubKey.Bytes()
|
||||
|
||||
// Parse the uncompressed point format (0x04 prefix + X + Y coordinates)
|
||||
if len(pubBytes) == 0 || pubBytes[0] != 0x04 {
|
||||
return nil, fmt.Errorf(`keyconv: invalid ECDH public key format`)
|
||||
}
|
||||
|
||||
keyLen := (len(pubBytes) - 1) / 2
|
||||
if len(pubBytes) != 1+2*keyLen {
|
||||
return nil, fmt.Errorf(`keyconv: invalid ECDH public key length`)
|
||||
}
|
||||
|
||||
x := new(big.Int).SetBytes(pubBytes[1 : 1+keyLen])
|
||||
y := new(big.Int).SetBytes(pubBytes[1+keyLen:])
|
||||
|
||||
return &ecdsa.PublicKey{
|
||||
Curve: curve,
|
||||
X: x,
|
||||
Y: y,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ECDHToECDSA(dst, src any) error {
|
||||
// convert ecdh.PublicKey to ecdsa.PublicKey, ecdh.PrivateKey to ecdsa.PrivateKey
|
||||
|
||||
// First, handle value types by converting to pointers
|
||||
switch s := src.(type) {
|
||||
case ecdh.PrivateKey:
|
||||
src = &s
|
||||
case ecdh.PublicKey:
|
||||
src = &s
|
||||
}
|
||||
|
||||
var privBytes []byte
|
||||
var pubkey *ecdh.PublicKey
|
||||
// Now handle the actual conversion with pointer types
|
||||
switch src := src.(type) {
|
||||
case *ecdh.PrivateKey:
|
||||
pubkey = src.PublicKey()
|
||||
privBytes = src.Bytes()
|
||||
case *ecdh.PublicKey:
|
||||
pubkey = src
|
||||
default:
|
||||
return fmt.Errorf(`keyconv: expected ecdh.PrivateKey, *ecdh.PrivateKey, ecdh.PublicKey, or *ecdh.PublicKey, got %T`, src)
|
||||
}
|
||||
|
||||
// convert the public key
|
||||
ecdsaPubKey, err := ecdhPublicKeyToECDSA(pubkey)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`keyconv.ECDHToECDSA: failed to convert ECDH public key to ECDSA public key: %w`, err)
|
||||
}
|
||||
|
||||
// return if we were being asked to convert *ecdh.PublicKey
|
||||
if privBytes == nil {
|
||||
return blackmagic.AssignIfCompatible(dst, ecdsaPubKey)
|
||||
}
|
||||
|
||||
// Then create the private key with the public key embedded
|
||||
ecdsaPrivKey := &ecdsa.PrivateKey{
|
||||
D: new(big.Int).SetBytes(privBytes),
|
||||
PublicKey: *ecdsaPubKey,
|
||||
}
|
||||
|
||||
return blackmagic.AssignIfCompatible(dst, ecdsaPrivKey)
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
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",
|
||||
"key_to_error_map.go",
|
||||
"pool.go",
|
||||
],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/internal/pool",
|
||||
visibility = ["//:__subpackages__"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":pool",
|
||||
visibility = ["//:__subpackages__"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "pool_test",
|
||||
srcs = [
|
||||
"byte_slice_test.go",
|
||||
],
|
||||
deps = [
|
||||
":pool",
|
||||
"@com_github_stretchr_testify//require",
|
||||
],
|
||||
)
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
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
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package pool
|
||||
|
||||
var byteSlicePool = SlicePool[byte]{
|
||||
pool: New[[]byte](allocByteSlice, freeByteSlice),
|
||||
}
|
||||
|
||||
func allocByteSlice() []byte {
|
||||
return make([]byte, 0, 64) // Default capacity of 64 bytes
|
||||
}
|
||||
|
||||
func freeByteSlice(b []byte) []byte {
|
||||
clear(b)
|
||||
b = b[:0] // Reset the slice to zero length
|
||||
return b
|
||||
}
|
||||
|
||||
func ByteSlice() SlicePool[byte] {
|
||||
return byteSlicePool
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package pool
|
||||
|
||||
import "bytes"
|
||||
|
||||
var bytesBufferPool = New[*bytes.Buffer](allocBytesBuffer, freeBytesBuffer)
|
||||
|
||||
func allocBytesBuffer() *bytes.Buffer {
|
||||
return &bytes.Buffer{}
|
||||
}
|
||||
|
||||
func freeBytesBuffer(b *bytes.Buffer) *bytes.Buffer {
|
||||
b.Reset()
|
||||
return b
|
||||
}
|
||||
|
||||
func BytesBuffer() *Pool[*bytes.Buffer] {
|
||||
return bytesBufferPool
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package pool
|
||||
|
||||
var errorSlicePool = New[[]error](allocErrorSlice, freeErrorSlice)
|
||||
|
||||
func allocErrorSlice() []error {
|
||||
return make([]error, 0, 1)
|
||||
}
|
||||
|
||||
func freeErrorSlice(s []error) []error {
|
||||
// Reset the slice to its zero value
|
||||
return s[:0]
|
||||
}
|
||||
|
||||
func ErrorSlice() *Pool[[]error] {
|
||||
return errorSlicePool
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package pool
|
||||
|
||||
var keyToErrorMapPool = New[map[string]error](allocKeyToErrorMap, freeKeyToErrorMap)
|
||||
|
||||
func allocKeyToErrorMap() map[string]error {
|
||||
return make(map[string]error)
|
||||
}
|
||||
|
||||
func freeKeyToErrorMap(m map[string]error) map[string]error {
|
||||
for k := range m {
|
||||
delete(m, k) // Clear the map
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// KeyToErrorMap returns a pool of map[string]error instances.
|
||||
func KeyToErrorMap() *Pool[map[string]error] {
|
||||
return keyToErrorMapPool
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Pool[T any] struct {
|
||||
pool sync.Pool
|
||||
destructor func(T) T
|
||||
}
|
||||
|
||||
// New creates a new Pool instance for the type T.
|
||||
// The allocator function is used to create new instances of T when the pool is empty.
|
||||
// The destructor function is used to clean up instances of T before they are returned to the pool.
|
||||
// The destructor should reset the state of T to a clean state, so it can be reused, and
|
||||
// return the modified instance of T. This is required for cases when you reset operations
|
||||
// can modify the underlying data structure, such as slices or maps.
|
||||
func New[T any](allocator func() T, destructor func(T) T) *Pool[T] {
|
||||
return &Pool[T]{
|
||||
pool: sync.Pool{
|
||||
New: func() any {
|
||||
return allocator()
|
||||
},
|
||||
},
|
||||
destructor: destructor,
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves an item of type T from the pool.
|
||||
func (p *Pool[T]) Get() T {
|
||||
//nolint:forcetypeassert
|
||||
return p.pool.Get().(T)
|
||||
}
|
||||
|
||||
// Put returns an item of type T to the pool.
|
||||
// The item is first processed by the destructor function to ensure it is in a clean state.
|
||||
func (p *Pool[T]) Put(item T) {
|
||||
p.pool.Put(p.destructor(item))
|
||||
}
|
||||
|
||||
// SlicePool is a specialized pool for slices of type T. It is identical to Pool[T] but
|
||||
// provides additional functionality to get slices with a specific capacity.
|
||||
type SlicePool[T any] struct {
|
||||
pool *Pool[[]T]
|
||||
}
|
||||
|
||||
func NewSlicePool[T any](allocator func() []T, destructor func([]T) []T) SlicePool[T] {
|
||||
return SlicePool[T]{
|
||||
pool: New(allocator, destructor),
|
||||
}
|
||||
}
|
||||
|
||||
func (p SlicePool[T]) Get() []T {
|
||||
return p.pool.Get()
|
||||
}
|
||||
|
||||
func (p SlicePool[T]) GetCapacity(capacity int) []T {
|
||||
if capacity <= 0 {
|
||||
return p.Get()
|
||||
}
|
||||
s := p.Get()
|
||||
if cap(s) < capacity {
|
||||
s = make([]T, 0, capacity)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (p SlicePool[T]) Put(s []T) {
|
||||
p.pool.Put(s)
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
load("@rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "tokens",
|
||||
srcs = [
|
||||
"jwe_tokens.go",
|
||||
"tokens.go",
|
||||
],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/internal/tokens",
|
||||
visibility = ["//:__subpackages__"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":tokens",
|
||||
visibility = ["//:__subpackages__"],
|
||||
)
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package tokens
|
||||
|
||||
// JWE Key Encryption Algorithms
|
||||
const (
|
||||
// RSA algorithms
|
||||
RSA1_5 = "RSA1_5"
|
||||
RSA_OAEP = "RSA-OAEP"
|
||||
RSA_OAEP_256 = "RSA-OAEP-256"
|
||||
RSA_OAEP_384 = "RSA-OAEP-384"
|
||||
RSA_OAEP_512 = "RSA-OAEP-512"
|
||||
|
||||
// AES Key Wrap algorithms
|
||||
A128KW = "A128KW"
|
||||
A192KW = "A192KW"
|
||||
A256KW = "A256KW"
|
||||
|
||||
// AES GCM Key Wrap algorithms
|
||||
A128GCMKW = "A128GCMKW"
|
||||
A192GCMKW = "A192GCMKW"
|
||||
A256GCMKW = "A256GCMKW"
|
||||
|
||||
// ECDH-ES algorithms
|
||||
ECDH_ES = "ECDH-ES"
|
||||
ECDH_ES_A128KW = "ECDH-ES+A128KW"
|
||||
ECDH_ES_A192KW = "ECDH-ES+A192KW"
|
||||
ECDH_ES_A256KW = "ECDH-ES+A256KW"
|
||||
|
||||
// PBES2 algorithms
|
||||
PBES2_HS256_A128KW = "PBES2-HS256+A128KW"
|
||||
PBES2_HS384_A192KW = "PBES2-HS384+A192KW"
|
||||
PBES2_HS512_A256KW = "PBES2-HS512+A256KW"
|
||||
|
||||
// Direct key agreement
|
||||
DIRECT = "dir"
|
||||
)
|
||||
|
||||
// JWE Content Encryption Algorithms
|
||||
const (
|
||||
// AES GCM algorithms
|
||||
A128GCM = "A128GCM"
|
||||
A192GCM = "A192GCM"
|
||||
A256GCM = "A256GCM"
|
||||
|
||||
// AES CBC + HMAC algorithms
|
||||
A128CBC_HS256 = "A128CBC-HS256"
|
||||
A192CBC_HS384 = "A192CBC-HS384"
|
||||
A256CBC_HS512 = "A256CBC-HS512"
|
||||
)
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package tokens
|
||||
|
||||
const (
|
||||
CloseCurlyBracket = '}'
|
||||
CloseSquareBracket = ']'
|
||||
Colon = ':'
|
||||
Comma = ','
|
||||
DoubleQuote = '"'
|
||||
OpenCurlyBracket = '{'
|
||||
OpenSquareBracket = '['
|
||||
Period = '.'
|
||||
)
|
||||
|
||||
// Cryptographic key sizes
|
||||
const (
|
||||
KeySize16 = 16
|
||||
KeySize24 = 24
|
||||
KeySize32 = 32
|
||||
KeySize48 = 48 // A192CBC_HS384 key size
|
||||
KeySize64 = 64 // A256CBC_HS512 key size
|
||||
)
|
||||
|
||||
// Bit/byte conversion factors
|
||||
const (
|
||||
BitsPerByte = 8
|
||||
BytesPerBit = 1.0 / 8
|
||||
)
|
||||
|
||||
// Key wrapping constants
|
||||
const (
|
||||
KeywrapChunkLen = 8
|
||||
KeywrapRounds = 6 // RFC 3394 key wrap rounds
|
||||
KeywrapBlockSize = 8 // Key wrap block size in bytes
|
||||
)
|
||||
|
||||
// AES-GCM constants
|
||||
const (
|
||||
GCMIVSize = 12 // GCM IV size in bytes (96 bits)
|
||||
GCMTagSize = 16 // GCM tag size in bytes (128 bits)
|
||||
)
|
||||
|
||||
// PBES2 constants
|
||||
const (
|
||||
PBES2DefaultIterations = 10000 // Default PBKDF2 iteration count
|
||||
PBES2NullByteSeparator = 0 // Null byte separator for PBES2
|
||||
)
|
||||
|
||||
// RSA key generation constants
|
||||
const (
|
||||
RSAKeyGenMultiplier = 2 // RSA key generation size multiplier
|
||||
)
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
load("@rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "jwa",
|
||||
srcs = [
|
||||
"compression_gen.go",
|
||||
"content_encryption_gen.go",
|
||||
"elliptic_gen.go",
|
||||
"jwa.go",
|
||||
"key_encryption_gen.go",
|
||||
"key_type_gen.go",
|
||||
"options_gen.go",
|
||||
"signature_gen.go",
|
||||
],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/jwa",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//internal/tokens",
|
||||
"@com_github_lestrrat_go_option_v2//:option",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "jwa_test",
|
||||
srcs = [
|
||||
"compression_gen_test.go",
|
||||
"content_encryption_gen_test.go",
|
||||
"elliptic_gen_test.go",
|
||||
"jwa_test.go",
|
||||
"key_encryption_gen_test.go",
|
||||
"key_type_gen_test.go",
|
||||
"signature_gen_test.go",
|
||||
],
|
||||
deps = [
|
||||
":jwa",
|
||||
"@com_github_stretchr_testify//require",
|
||||
"@com_github_lestrrat_go_option_v2//:option",
|
||||
],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":jwa",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# JWA [](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3/jwa)
|
||||
|
||||
Package [github.com/lestrrat-go/jwx/v3/jwa](./jwa) defines the various algorithm described in [RFC7518](https://tools.ietf.org/html/rfc7518)
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
// Code generated by tools/cmd/genjwa/main.go. DO NOT EDIT.
|
||||
|
||||
package jwa
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var muAllCompressionAlgorithm sync.RWMutex
|
||||
var allCompressionAlgorithm = map[string]CompressionAlgorithm{}
|
||||
var muListCompressionAlgorithm sync.RWMutex
|
||||
var listCompressionAlgorithm []CompressionAlgorithm
|
||||
var builtinCompressionAlgorithm = map[string]struct{}{}
|
||||
|
||||
func init() {
|
||||
// builtin values for CompressionAlgorithm
|
||||
algorithms := make([]CompressionAlgorithm, 2)
|
||||
algorithms[0] = NewCompressionAlgorithm("DEF")
|
||||
algorithms[1] = NewCompressionAlgorithm("")
|
||||
|
||||
RegisterCompressionAlgorithm(algorithms...)
|
||||
}
|
||||
|
||||
// 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).
|
||||
func Deflate() CompressionAlgorithm {
|
||||
return lookupBuiltinCompressionAlgorithm("DEF")
|
||||
}
|
||||
|
||||
// NoCompress returns an object representing an empty compression algorithm value. Using this value specifies that the content should not be compressed.
|
||||
func NoCompress() CompressionAlgorithm {
|
||||
return lookupBuiltinCompressionAlgorithm("")
|
||||
}
|
||||
|
||||
func lookupBuiltinCompressionAlgorithm(name string) CompressionAlgorithm {
|
||||
muAllCompressionAlgorithm.RLock()
|
||||
v, ok := allCompressionAlgorithm[name]
|
||||
muAllCompressionAlgorithm.RUnlock()
|
||||
if !ok {
|
||||
panic(fmt.Sprintf(`jwa: CompressionAlgorithm %q not registered`, name))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// CompressionAlgorithm represents the compression algorithms as described in https://tools.ietf.org/html/rfc7518#section-7.3
|
||||
type CompressionAlgorithm struct {
|
||||
name string
|
||||
deprecated bool
|
||||
}
|
||||
|
||||
func (s CompressionAlgorithm) String() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
// IsDeprecated returns true if the CompressionAlgorithm object is deprecated.
|
||||
func (s CompressionAlgorithm) IsDeprecated() bool {
|
||||
return s.deprecated
|
||||
}
|
||||
|
||||
// EmptyCompressionAlgorithm returns an empty CompressionAlgorithm object, used as a zero value.
|
||||
func EmptyCompressionAlgorithm() CompressionAlgorithm {
|
||||
return CompressionAlgorithm{}
|
||||
}
|
||||
|
||||
// NewCompressionAlgorithm creates a new CompressionAlgorithm object with the given name.
|
||||
func NewCompressionAlgorithm(name string, options ...NewAlgorithmOption) CompressionAlgorithm {
|
||||
var deprecated bool
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identDeprecated{}:
|
||||
if err := option.Value(&deprecated); err != nil {
|
||||
panic("jwa.NewCompressionAlgorithm: WithDeprecated option must be a boolean")
|
||||
}
|
||||
}
|
||||
}
|
||||
return CompressionAlgorithm{name: name, deprecated: deprecated}
|
||||
}
|
||||
|
||||
// LookupCompressionAlgorithm returns the CompressionAlgorithm object for the given name.
|
||||
func LookupCompressionAlgorithm(name string) (CompressionAlgorithm, bool) {
|
||||
muAllCompressionAlgorithm.RLock()
|
||||
v, ok := allCompressionAlgorithm[name]
|
||||
muAllCompressionAlgorithm.RUnlock()
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// 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.
|
||||
func RegisterCompressionAlgorithm(algorithms ...CompressionAlgorithm) {
|
||||
muAllCompressionAlgorithm.Lock()
|
||||
for _, alg := range algorithms {
|
||||
allCompressionAlgorithm[alg.String()] = alg
|
||||
}
|
||||
muAllCompressionAlgorithm.Unlock()
|
||||
rebuildCompressionAlgorithm()
|
||||
}
|
||||
|
||||
// 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()
|
||||
for _, alg := range algorithms {
|
||||
if _, ok := builtinCompressionAlgorithm[alg.String()]; ok {
|
||||
continue
|
||||
}
|
||||
delete(allCompressionAlgorithm, alg.String())
|
||||
}
|
||||
muAllCompressionAlgorithm.Unlock()
|
||||
rebuildCompressionAlgorithm()
|
||||
}
|
||||
|
||||
func rebuildCompressionAlgorithm() {
|
||||
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()
|
||||
})
|
||||
muListCompressionAlgorithm.Lock()
|
||||
listCompressionAlgorithm = list
|
||||
muListCompressionAlgorithm.Unlock()
|
||||
}
|
||||
|
||||
// CompressionAlgorithms returns a list of all available values for CompressionAlgorithm.
|
||||
func CompressionAlgorithms() []CompressionAlgorithm {
|
||||
muListCompressionAlgorithm.RLock()
|
||||
defer muListCompressionAlgorithm.RUnlock()
|
||||
return listCompressionAlgorithm
|
||||
}
|
||||
|
||||
// MarshalJSON serializes the CompressionAlgorithm object to a JSON string.
|
||||
func (s CompressionAlgorithm) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON deserializes the JSON string to a CompressionAlgorithm object.
|
||||
func (s *CompressionAlgorithm) UnmarshalJSON(data []byte) error {
|
||||
var name string
|
||||
if err := json.Unmarshal(data, &name); err != nil {
|
||||
return fmt.Errorf(`failed to unmarshal CompressionAlgorithm: %w`, err)
|
||||
}
|
||||
v, ok := LookupCompressionAlgorithm(name)
|
||||
if !ok {
|
||||
return fmt.Errorf(`unknown CompressionAlgorithm: %q`, name)
|
||||
}
|
||||
*s = v
|
||||
return nil
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
// Code generated by tools/cmd/genjwa/main.go. DO NOT EDIT.
|
||||
|
||||
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)
|
||||
algorithms[0] = NewContentEncryptionAlgorithm(tokens.A128CBC_HS256)
|
||||
algorithms[1] = NewContentEncryptionAlgorithm(tokens.A128GCM)
|
||||
algorithms[2] = NewContentEncryptionAlgorithm(tokens.A192CBC_HS384)
|
||||
algorithms[3] = NewContentEncryptionAlgorithm(tokens.A192GCM)
|
||||
algorithms[4] = NewContentEncryptionAlgorithm(tokens.A256CBC_HS512)
|
||||
algorithms[5] = NewContentEncryptionAlgorithm(tokens.A256GCM)
|
||||
|
||||
RegisterContentEncryptionAlgorithm(algorithms...)
|
||||
}
|
||||
|
||||
// A128CBC_HS256 returns an object representing A128CBC-HS256. Using this value specifies that the content should be encrypted using AES-CBC + HMAC-SHA256 (128).
|
||||
func A128CBC_HS256() ContentEncryptionAlgorithm {
|
||||
return lookupBuiltinContentEncryptionAlgorithm(tokens.A128CBC_HS256)
|
||||
}
|
||||
|
||||
// A128GCM returns an object representing A128GCM. Using this value specifies that the content should be encrypted using AES-GCM (128).
|
||||
func A128GCM() ContentEncryptionAlgorithm {
|
||||
return lookupBuiltinContentEncryptionAlgorithm(tokens.A128GCM)
|
||||
}
|
||||
|
||||
// A192CBC_HS384 returns an object representing A192CBC-HS384. Using this value specifies that the content should be encrypted using AES-CBC + HMAC-SHA384 (192).
|
||||
func A192CBC_HS384() ContentEncryptionAlgorithm {
|
||||
return lookupBuiltinContentEncryptionAlgorithm(tokens.A192CBC_HS384)
|
||||
}
|
||||
|
||||
// A192GCM returns an object representing A192GCM. Using this value specifies that the content should be encrypted using AES-GCM (192).
|
||||
func A192GCM() ContentEncryptionAlgorithm {
|
||||
return lookupBuiltinContentEncryptionAlgorithm(tokens.A192GCM)
|
||||
}
|
||||
|
||||
// A256CBC_HS512 returns an object representing A256CBC-HS512. Using this value specifies that the content should be encrypted using AES-CBC + HMAC-SHA512 (256).
|
||||
func A256CBC_HS512() ContentEncryptionAlgorithm {
|
||||
return lookupBuiltinContentEncryptionAlgorithm(tokens.A256CBC_HS512)
|
||||
}
|
||||
|
||||
// A256GCM returns an object representing A256GCM. Using this value specifies that the content should be encrypted using AES-GCM (256).
|
||||
func A256GCM() ContentEncryptionAlgorithm {
|
||||
return lookupBuiltinContentEncryptionAlgorithm(tokens.A256GCM)
|
||||
}
|
||||
|
||||
func lookupBuiltinContentEncryptionAlgorithm(name string) ContentEncryptionAlgorithm {
|
||||
muAllContentEncryptionAlgorithm.RLock()
|
||||
v, ok := allContentEncryptionAlgorithm[name]
|
||||
muAllContentEncryptionAlgorithm.RUnlock()
|
||||
if !ok {
|
||||
panic(fmt.Sprintf(`jwa: ContentEncryptionAlgorithm %q not registered`, name))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ContentEncryptionAlgorithm represents the various encryption algorithms as described in https://tools.ietf.org/html/rfc7518#section-5
|
||||
type ContentEncryptionAlgorithm struct {
|
||||
name string
|
||||
deprecated bool
|
||||
}
|
||||
|
||||
func (s ContentEncryptionAlgorithm) String() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
// IsDeprecated returns true if the ContentEncryptionAlgorithm object is deprecated.
|
||||
func (s ContentEncryptionAlgorithm) IsDeprecated() bool {
|
||||
return s.deprecated
|
||||
}
|
||||
|
||||
// EmptyContentEncryptionAlgorithm returns an empty ContentEncryptionAlgorithm object, used as a zero value.
|
||||
func EmptyContentEncryptionAlgorithm() ContentEncryptionAlgorithm {
|
||||
return ContentEncryptionAlgorithm{}
|
||||
}
|
||||
|
||||
// NewContentEncryptionAlgorithm creates a new ContentEncryptionAlgorithm object with the given name.
|
||||
func NewContentEncryptionAlgorithm(name string, options ...NewAlgorithmOption) ContentEncryptionAlgorithm {
|
||||
var deprecated bool
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identDeprecated{}:
|
||||
if err := option.Value(&deprecated); err != nil {
|
||||
panic("jwa.NewContentEncryptionAlgorithm: WithDeprecated option must be a boolean")
|
||||
}
|
||||
}
|
||||
}
|
||||
return ContentEncryptionAlgorithm{name: name, deprecated: deprecated}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func RegisterContentEncryptionAlgorithm(algorithms ...ContentEncryptionAlgorithm) {
|
||||
muAllContentEncryptionAlgorithm.Lock()
|
||||
for _, alg := range algorithms {
|
||||
allContentEncryptionAlgorithm[alg.String()] = 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())
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// MarshalJSON serializes the ContentEncryptionAlgorithm object to a JSON string.
|
||||
func (s ContentEncryptionAlgorithm) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON deserializes the JSON string to a ContentEncryptionAlgorithm object.
|
||||
func (s *ContentEncryptionAlgorithm) UnmarshalJSON(data []byte) error {
|
||||
var name string
|
||||
if err := json.Unmarshal(data, &name); err != nil {
|
||||
return fmt.Errorf(`failed to unmarshal ContentEncryptionAlgorithm: %w`, err)
|
||||
}
|
||||
v, ok := LookupContentEncryptionAlgorithm(name)
|
||||
if !ok {
|
||||
return fmt.Errorf(`unknown ContentEncryptionAlgorithm: %q`, name)
|
||||
}
|
||||
*s = v
|
||||
return nil
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
// Code generated by tools/cmd/genjwa/main.go. DO NOT EDIT.
|
||||
|
||||
package jwa
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var muAllEllipticCurveAlgorithm sync.RWMutex
|
||||
var allEllipticCurveAlgorithm = map[string]EllipticCurveAlgorithm{}
|
||||
var muListEllipticCurveAlgorithm sync.RWMutex
|
||||
var listEllipticCurveAlgorithm []EllipticCurveAlgorithm
|
||||
var builtinEllipticCurveAlgorithm = map[string]struct{}{}
|
||||
|
||||
func init() {
|
||||
// builtin values for EllipticCurveAlgorithm
|
||||
algorithms := make([]EllipticCurveAlgorithm, 7)
|
||||
algorithms[0] = NewEllipticCurveAlgorithm("Ed25519")
|
||||
algorithms[1] = NewEllipticCurveAlgorithm("Ed448")
|
||||
algorithms[2] = NewEllipticCurveAlgorithm("P-256")
|
||||
algorithms[3] = NewEllipticCurveAlgorithm("P-384")
|
||||
algorithms[4] = NewEllipticCurveAlgorithm("P-521")
|
||||
algorithms[5] = NewEllipticCurveAlgorithm("X25519")
|
||||
algorithms[6] = NewEllipticCurveAlgorithm("X448")
|
||||
|
||||
RegisterEllipticCurveAlgorithm(algorithms...)
|
||||
}
|
||||
|
||||
// Ed25519 returns an object representing Ed25519 algorithm for EdDSA operations.
|
||||
func Ed25519() EllipticCurveAlgorithm {
|
||||
return lookupBuiltinEllipticCurveAlgorithm("Ed25519")
|
||||
}
|
||||
|
||||
// Ed448 returns an object representing Ed448 algorithm for EdDSA operations.
|
||||
func Ed448() EllipticCurveAlgorithm {
|
||||
return lookupBuiltinEllipticCurveAlgorithm("Ed448")
|
||||
}
|
||||
|
||||
var invalidEllipticCurve = NewEllipticCurveAlgorithm("P-invalid")
|
||||
|
||||
// InvalidEllipticCurve returns an object representing an invalid elliptic curve.
|
||||
func InvalidEllipticCurve() EllipticCurveAlgorithm {
|
||||
return invalidEllipticCurve
|
||||
}
|
||||
|
||||
// P256 returns an object representing P-256 algorithm for ECDSA operations.
|
||||
func P256() EllipticCurveAlgorithm {
|
||||
return lookupBuiltinEllipticCurveAlgorithm("P-256")
|
||||
}
|
||||
|
||||
// P384 returns an object representing P-384 algorithm for ECDSA operations.
|
||||
func P384() EllipticCurveAlgorithm {
|
||||
return lookupBuiltinEllipticCurveAlgorithm("P-384")
|
||||
}
|
||||
|
||||
// P521 returns an object representing P-521 algorithm for ECDSA operations.
|
||||
func P521() EllipticCurveAlgorithm {
|
||||
return lookupBuiltinEllipticCurveAlgorithm("P-521")
|
||||
}
|
||||
|
||||
// X25519 returns an object representing X25519 algorithm for ECDH operations.
|
||||
func X25519() EllipticCurveAlgorithm {
|
||||
return lookupBuiltinEllipticCurveAlgorithm("X25519")
|
||||
}
|
||||
|
||||
// X448 returns an object representing X448 algorithm for ECDH operations.
|
||||
func X448() EllipticCurveAlgorithm {
|
||||
return lookupBuiltinEllipticCurveAlgorithm("X448")
|
||||
}
|
||||
|
||||
func lookupBuiltinEllipticCurveAlgorithm(name string) EllipticCurveAlgorithm {
|
||||
muAllEllipticCurveAlgorithm.RLock()
|
||||
v, ok := allEllipticCurveAlgorithm[name]
|
||||
muAllEllipticCurveAlgorithm.RUnlock()
|
||||
if !ok {
|
||||
panic(fmt.Sprintf(`jwa: EllipticCurveAlgorithm %q not registered`, name))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// EllipticCurveAlgorithm represents the algorithms used for EC keys
|
||||
type EllipticCurveAlgorithm struct {
|
||||
name string
|
||||
deprecated bool
|
||||
}
|
||||
|
||||
func (s EllipticCurveAlgorithm) String() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
// IsDeprecated returns true if the EllipticCurveAlgorithm object is deprecated.
|
||||
func (s EllipticCurveAlgorithm) IsDeprecated() bool {
|
||||
return s.deprecated
|
||||
}
|
||||
|
||||
// EmptyEllipticCurveAlgorithm returns an empty EllipticCurveAlgorithm object, used as a zero value.
|
||||
func EmptyEllipticCurveAlgorithm() EllipticCurveAlgorithm {
|
||||
return EllipticCurveAlgorithm{}
|
||||
}
|
||||
|
||||
// NewEllipticCurveAlgorithm creates a new EllipticCurveAlgorithm object with the given name.
|
||||
func NewEllipticCurveAlgorithm(name string, options ...NewAlgorithmOption) EllipticCurveAlgorithm {
|
||||
var deprecated bool
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identDeprecated{}:
|
||||
if err := option.Value(&deprecated); err != nil {
|
||||
panic("jwa.NewEllipticCurveAlgorithm: WithDeprecated option must be a boolean")
|
||||
}
|
||||
}
|
||||
}
|
||||
return EllipticCurveAlgorithm{name: name, deprecated: deprecated}
|
||||
}
|
||||
|
||||
// LookupEllipticCurveAlgorithm returns the EllipticCurveAlgorithm object for the given name.
|
||||
func LookupEllipticCurveAlgorithm(name string) (EllipticCurveAlgorithm, bool) {
|
||||
muAllEllipticCurveAlgorithm.RLock()
|
||||
v, ok := allEllipticCurveAlgorithm[name]
|
||||
muAllEllipticCurveAlgorithm.RUnlock()
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// 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.
|
||||
func RegisterEllipticCurveAlgorithm(algorithms ...EllipticCurveAlgorithm) {
|
||||
muAllEllipticCurveAlgorithm.Lock()
|
||||
for _, alg := range algorithms {
|
||||
allEllipticCurveAlgorithm[alg.String()] = alg
|
||||
}
|
||||
muAllEllipticCurveAlgorithm.Unlock()
|
||||
rebuildEllipticCurveAlgorithm()
|
||||
}
|
||||
|
||||
// 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()
|
||||
for _, alg := range algorithms {
|
||||
if _, ok := builtinEllipticCurveAlgorithm[alg.String()]; ok {
|
||||
continue
|
||||
}
|
||||
delete(allEllipticCurveAlgorithm, alg.String())
|
||||
}
|
||||
muAllEllipticCurveAlgorithm.Unlock()
|
||||
rebuildEllipticCurveAlgorithm()
|
||||
}
|
||||
|
||||
func rebuildEllipticCurveAlgorithm() {
|
||||
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()
|
||||
})
|
||||
muListEllipticCurveAlgorithm.Lock()
|
||||
listEllipticCurveAlgorithm = list
|
||||
muListEllipticCurveAlgorithm.Unlock()
|
||||
}
|
||||
|
||||
// EllipticCurveAlgorithms returns a list of all available values for EllipticCurveAlgorithm.
|
||||
func EllipticCurveAlgorithms() []EllipticCurveAlgorithm {
|
||||
muListEllipticCurveAlgorithm.RLock()
|
||||
defer muListEllipticCurveAlgorithm.RUnlock()
|
||||
return listEllipticCurveAlgorithm
|
||||
}
|
||||
|
||||
// MarshalJSON serializes the EllipticCurveAlgorithm object to a JSON string.
|
||||
func (s EllipticCurveAlgorithm) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON deserializes the JSON string to a EllipticCurveAlgorithm object.
|
||||
func (s *EllipticCurveAlgorithm) UnmarshalJSON(data []byte) error {
|
||||
var name string
|
||||
if err := json.Unmarshal(data, &name); err != nil {
|
||||
return fmt.Errorf(`failed to unmarshal EllipticCurveAlgorithm: %w`, err)
|
||||
}
|
||||
v, ok := LookupEllipticCurveAlgorithm(name)
|
||||
if !ok {
|
||||
return fmt.Errorf(`unknown EllipticCurveAlgorithm: %q`, name)
|
||||
}
|
||||
*s = v
|
||||
return nil
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
//go:generate ../tools/cmd/genjwa.sh
|
||||
|
||||
// Package jwa defines the various algorithm described in https://tools.ietf.org/html/rfc7518
|
||||
package jwa
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// KeyAlgorithm is a workaround for jwk.Key being able to contain different
|
||||
// types of algorithms in its `alg` field.
|
||||
//
|
||||
// Previously the storage for the `alg` field was represented as a string,
|
||||
// but this caused some users to wonder why the field was not typed appropriately
|
||||
// like other fields.
|
||||
//
|
||||
// Ideally we would like to keep track of Signature Algorithms and
|
||||
// Key Encryption Algorithms separately, and force the APIs to
|
||||
// type-check at compile time, but this allows users to pass a value from a
|
||||
// jwk.Key directly
|
||||
type KeyAlgorithm interface {
|
||||
String() string
|
||||
IsDeprecated() bool
|
||||
}
|
||||
|
||||
var errInvalidKeyAlgorithm = errors.New(`invalid key algorithm`)
|
||||
|
||||
func ErrInvalidKeyAlgorithm() error {
|
||||
return errInvalidKeyAlgorithm
|
||||
}
|
||||
|
||||
// KeyAlgorithmFrom takes either a string, `jwa.SignatureAlgorithm`,
|
||||
// `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()`
|
||||
func KeyAlgorithmFrom(v any) (KeyAlgorithm, error) {
|
||||
switch v := v.(type) {
|
||||
case SignatureAlgorithm:
|
||||
return v, nil
|
||||
case KeyEncryptionAlgorithm:
|
||||
return v, nil
|
||||
case ContentEncryptionAlgorithm:
|
||||
return v, nil
|
||||
case string:
|
||||
salg, ok := LookupSignatureAlgorithm(v)
|
||||
if ok {
|
||||
return salg, nil
|
||||
}
|
||||
|
||||
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)
|
||||
default:
|
||||
return nil, fmt.Errorf(`invalid key type: %T: %w`, v, errInvalidKeyAlgorithm)
|
||||
}
|
||||
}
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
// Code generated by tools/cmd/genjwa/main.go. DO NOT EDIT.
|
||||
|
||||
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)
|
||||
algorithms[0] = NewKeyEncryptionAlgorithm(tokens.A128GCMKW, WithIsSymmetric(true))
|
||||
algorithms[1] = NewKeyEncryptionAlgorithm(tokens.A128KW, WithIsSymmetric(true))
|
||||
algorithms[2] = NewKeyEncryptionAlgorithm(tokens.A192GCMKW, WithIsSymmetric(true))
|
||||
algorithms[3] = NewKeyEncryptionAlgorithm(tokens.A192KW, WithIsSymmetric(true))
|
||||
algorithms[4] = NewKeyEncryptionAlgorithm(tokens.A256GCMKW, WithIsSymmetric(true))
|
||||
algorithms[5] = NewKeyEncryptionAlgorithm(tokens.A256KW, WithIsSymmetric(true))
|
||||
algorithms[6] = NewKeyEncryptionAlgorithm(tokens.DIRECT, WithIsSymmetric(true))
|
||||
algorithms[7] = NewKeyEncryptionAlgorithm(tokens.ECDH_ES)
|
||||
algorithms[8] = NewKeyEncryptionAlgorithm(tokens.ECDH_ES_A128KW)
|
||||
algorithms[9] = NewKeyEncryptionAlgorithm(tokens.ECDH_ES_A192KW)
|
||||
algorithms[10] = NewKeyEncryptionAlgorithm(tokens.ECDH_ES_A256KW)
|
||||
algorithms[11] = NewKeyEncryptionAlgorithm(tokens.PBES2_HS256_A128KW, WithIsSymmetric(true))
|
||||
algorithms[12] = NewKeyEncryptionAlgorithm(tokens.PBES2_HS384_A192KW, WithIsSymmetric(true))
|
||||
algorithms[13] = NewKeyEncryptionAlgorithm(tokens.PBES2_HS512_A256KW, WithIsSymmetric(true))
|
||||
algorithms[14] = NewKeyEncryptionAlgorithm(tokens.RSA1_5, WithDeprecated(true))
|
||||
algorithms[15] = NewKeyEncryptionAlgorithm(tokens.RSA_OAEP)
|
||||
algorithms[16] = NewKeyEncryptionAlgorithm(tokens.RSA_OAEP_256)
|
||||
algorithms[17] = NewKeyEncryptionAlgorithm(tokens.RSA_OAEP_384)
|
||||
algorithms[18] = NewKeyEncryptionAlgorithm(tokens.RSA_OAEP_512)
|
||||
|
||||
RegisterKeyEncryptionAlgorithm(algorithms...)
|
||||
}
|
||||
|
||||
// A128GCMKW returns an object representing AES-GCM key wrap (128) key encryption algorithm.
|
||||
func A128GCMKW() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.A128GCMKW)
|
||||
}
|
||||
|
||||
// A128KW returns an object representing AES key wrap (128) key encryption algorithm.
|
||||
func A128KW() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.A128KW)
|
||||
}
|
||||
|
||||
// A192GCMKW returns an object representing AES-GCM key wrap (192) key encryption algorithm.
|
||||
func A192GCMKW() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.A192GCMKW)
|
||||
}
|
||||
|
||||
// A192KW returns an object representing AES key wrap (192) key encryption algorithm.
|
||||
func A192KW() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.A192KW)
|
||||
}
|
||||
|
||||
// A256GCMKW returns an object representing AES-GCM key wrap (256) key encryption algorithm.
|
||||
func A256GCMKW() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.A256GCMKW)
|
||||
}
|
||||
|
||||
// A256KW returns an object representing AES key wrap (256) key encryption algorithm.
|
||||
func A256KW() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.A256KW)
|
||||
}
|
||||
|
||||
// DIRECT returns an object representing Direct key encryption algorithm.
|
||||
func DIRECT() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.DIRECT)
|
||||
}
|
||||
|
||||
// ECDH_ES returns an object representing ECDH-ES key encryption algorithm.
|
||||
func ECDH_ES() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.ECDH_ES)
|
||||
}
|
||||
|
||||
// ECDH_ES_A128KW returns an object representing ECDH-ES + AES key wrap (128) key encryption algorithm.
|
||||
func ECDH_ES_A128KW() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.ECDH_ES_A128KW)
|
||||
}
|
||||
|
||||
// ECDH_ES_A192KW returns an object representing ECDH-ES + AES key wrap (192) key encryption algorithm.
|
||||
func ECDH_ES_A192KW() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.ECDH_ES_A192KW)
|
||||
}
|
||||
|
||||
// ECDH_ES_A256KW returns an object representing ECDH-ES + AES key wrap (256) key encryption algorithm.
|
||||
func ECDH_ES_A256KW() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.ECDH_ES_A256KW)
|
||||
}
|
||||
|
||||
// PBES2_HS256_A128KW returns an object representing PBES2 + HMAC-SHA256 + AES key wrap (128) key encryption algorithm.
|
||||
func PBES2_HS256_A128KW() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.PBES2_HS256_A128KW)
|
||||
}
|
||||
|
||||
// PBES2_HS384_A192KW returns an object representing PBES2 + HMAC-SHA384 + AES key wrap (192) key encryption algorithm.
|
||||
func PBES2_HS384_A192KW() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.PBES2_HS384_A192KW)
|
||||
}
|
||||
|
||||
// PBES2_HS512_A256KW returns an object representing PBES2 + HMAC-SHA512 + AES key wrap (256) key encryption algorithm.
|
||||
func PBES2_HS512_A256KW() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.PBES2_HS512_A256KW)
|
||||
}
|
||||
|
||||
// RSA1_5 returns an object representing RSA-PKCS1v1.5 key encryption algorithm.
|
||||
func RSA1_5() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.RSA1_5)
|
||||
}
|
||||
|
||||
// RSA_OAEP returns an object representing RSA-OAEP-SHA1 key encryption algorithm.
|
||||
func RSA_OAEP() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.RSA_OAEP)
|
||||
}
|
||||
|
||||
// RSA_OAEP_256 returns an object representing RSA-OAEP-SHA256 key encryption algorithm.
|
||||
func RSA_OAEP_256() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.RSA_OAEP_256)
|
||||
}
|
||||
|
||||
// RSA_OAEP_384 returns an object representing RSA-OAEP-SHA384 key encryption algorithm.
|
||||
func RSA_OAEP_384() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.RSA_OAEP_384)
|
||||
}
|
||||
|
||||
// RSA_OAEP_512 returns an object representing RSA-OAEP-SHA512 key encryption algorithm.
|
||||
func RSA_OAEP_512() KeyEncryptionAlgorithm {
|
||||
return lookupBuiltinKeyEncryptionAlgorithm(tokens.RSA_OAEP_512)
|
||||
}
|
||||
|
||||
func lookupBuiltinKeyEncryptionAlgorithm(name string) KeyEncryptionAlgorithm {
|
||||
muAllKeyEncryptionAlgorithm.RLock()
|
||||
v, ok := allKeyEncryptionAlgorithm[name]
|
||||
muAllKeyEncryptionAlgorithm.RUnlock()
|
||||
if !ok {
|
||||
panic(fmt.Sprintf(`jwa: KeyEncryptionAlgorithm %q not registered`, name))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// KeyEncryptionAlgorithm represents the various encryption algorithms as described in https://tools.ietf.org/html/rfc7518#section-4.1
|
||||
type KeyEncryptionAlgorithm struct {
|
||||
name string
|
||||
deprecated bool
|
||||
isSymmetric bool
|
||||
}
|
||||
|
||||
func (s KeyEncryptionAlgorithm) String() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
// IsDeprecated returns true if the KeyEncryptionAlgorithm object is deprecated.
|
||||
func (s KeyEncryptionAlgorithm) IsDeprecated() bool {
|
||||
return s.deprecated
|
||||
}
|
||||
|
||||
// IsSymmetric returns true if the KeyEncryptionAlgorithm object is symmetric. Symmetric algorithms use the same key for both encryption and decryption.
|
||||
func (s KeyEncryptionAlgorithm) IsSymmetric() bool {
|
||||
return s.isSymmetric
|
||||
}
|
||||
|
||||
// EmptyKeyEncryptionAlgorithm returns an empty KeyEncryptionAlgorithm object, used as a zero value.
|
||||
func EmptyKeyEncryptionAlgorithm() KeyEncryptionAlgorithm {
|
||||
return KeyEncryptionAlgorithm{}
|
||||
}
|
||||
|
||||
// NewKeyEncryptionAlgorithm creates a new KeyEncryptionAlgorithm object with the given name.
|
||||
func NewKeyEncryptionAlgorithm(name string, options ...NewKeyEncryptionAlgorithmOption) KeyEncryptionAlgorithm {
|
||||
var deprecated bool
|
||||
var isSymmetric bool
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identIsSymmetric{}:
|
||||
if err := option.Value(&isSymmetric); err != nil {
|
||||
panic("jwa.NewKeyEncryptionAlgorithm: WithIsSymmetric option must be a boolean")
|
||||
}
|
||||
case identDeprecated{}:
|
||||
if err := option.Value(&deprecated); err != nil {
|
||||
panic("jwa.NewKeyEncryptionAlgorithm: WithDeprecated option must be a boolean")
|
||||
}
|
||||
}
|
||||
}
|
||||
return KeyEncryptionAlgorithm{name: name, deprecated: deprecated, isSymmetric: isSymmetric}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func RegisterKeyEncryptionAlgorithm(algorithms ...KeyEncryptionAlgorithm) {
|
||||
muAllKeyEncryptionAlgorithm.Lock()
|
||||
for _, alg := range algorithms {
|
||||
allKeyEncryptionAlgorithm[alg.String()] = 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())
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// MarshalJSON serializes the KeyEncryptionAlgorithm object to a JSON string.
|
||||
func (s KeyEncryptionAlgorithm) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON deserializes the JSON string to a KeyEncryptionAlgorithm object.
|
||||
func (s *KeyEncryptionAlgorithm) UnmarshalJSON(data []byte) error {
|
||||
var name string
|
||||
if err := json.Unmarshal(data, &name); err != nil {
|
||||
return fmt.Errorf(`failed to unmarshal KeyEncryptionAlgorithm: %w`, err)
|
||||
}
|
||||
v, ok := LookupKeyEncryptionAlgorithm(name)
|
||||
if !ok {
|
||||
return fmt.Errorf(`unknown KeyEncryptionAlgorithm: %q`, name)
|
||||
}
|
||||
*s = v
|
||||
return nil
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
// Code generated by tools/cmd/genjwa/main.go. DO NOT EDIT.
|
||||
|
||||
package jwa
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var muAllKeyType sync.RWMutex
|
||||
var allKeyType = map[string]KeyType{}
|
||||
var muListKeyType sync.RWMutex
|
||||
var listKeyType []KeyType
|
||||
var builtinKeyType = map[string]struct{}{}
|
||||
|
||||
func init() {
|
||||
// builtin values for KeyType
|
||||
algorithms := make([]KeyType, 4)
|
||||
algorithms[0] = NewKeyType("EC")
|
||||
algorithms[1] = NewKeyType("OKP")
|
||||
algorithms[2] = NewKeyType("oct")
|
||||
algorithms[3] = NewKeyType("RSA")
|
||||
|
||||
RegisterKeyType(algorithms...)
|
||||
}
|
||||
|
||||
// EC returns an object representing EC. Elliptic Curve
|
||||
func EC() KeyType {
|
||||
return lookupBuiltinKeyType("EC")
|
||||
}
|
||||
|
||||
var invalidKeyType = NewKeyType("")
|
||||
|
||||
// InvalidKeyType returns an object representing invalid key type. Invalid KeyType
|
||||
func InvalidKeyType() KeyType {
|
||||
return invalidKeyType
|
||||
}
|
||||
|
||||
// OKP returns an object representing OKP. Octet string key pairs
|
||||
func OKP() KeyType {
|
||||
return lookupBuiltinKeyType("OKP")
|
||||
}
|
||||
|
||||
// OctetSeq returns an object representing oct. Octet sequence (used to represent symmetric keys)
|
||||
func OctetSeq() KeyType {
|
||||
return lookupBuiltinKeyType("oct")
|
||||
}
|
||||
|
||||
// RSA returns an object representing RSA. RSA
|
||||
func RSA() KeyType {
|
||||
return lookupBuiltinKeyType("RSA")
|
||||
}
|
||||
|
||||
func lookupBuiltinKeyType(name string) KeyType {
|
||||
muAllKeyType.RLock()
|
||||
v, ok := allKeyType[name]
|
||||
muAllKeyType.RUnlock()
|
||||
if !ok {
|
||||
panic(fmt.Sprintf(`jwa: KeyType %q not registered`, name))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// KeyType represents the key type ("kty") that are supported
|
||||
type KeyType struct {
|
||||
name string
|
||||
deprecated bool
|
||||
}
|
||||
|
||||
func (s KeyType) String() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
// IsDeprecated returns true if the KeyType object is deprecated.
|
||||
func (s KeyType) IsDeprecated() bool {
|
||||
return s.deprecated
|
||||
}
|
||||
|
||||
// EmptyKeyType returns an empty KeyType object, used as a zero value.
|
||||
func EmptyKeyType() KeyType {
|
||||
return KeyType{}
|
||||
}
|
||||
|
||||
// NewKeyType creates a new KeyType object with the given name.
|
||||
func NewKeyType(name string, options ...NewAlgorithmOption) KeyType {
|
||||
var deprecated bool
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identDeprecated{}:
|
||||
if err := option.Value(&deprecated); err != nil {
|
||||
panic("jwa.NewKeyType: WithDeprecated option must be a boolean")
|
||||
}
|
||||
}
|
||||
}
|
||||
return KeyType{name: name, deprecated: deprecated}
|
||||
}
|
||||
|
||||
// LookupKeyType returns the KeyType object for the given name.
|
||||
func LookupKeyType(name string) (KeyType, bool) {
|
||||
muAllKeyType.RLock()
|
||||
v, ok := allKeyType[name]
|
||||
muAllKeyType.RUnlock()
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// 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.
|
||||
func RegisterKeyType(algorithms ...KeyType) {
|
||||
muAllKeyType.Lock()
|
||||
for _, alg := range algorithms {
|
||||
allKeyType[alg.String()] = alg
|
||||
}
|
||||
muAllKeyType.Unlock()
|
||||
rebuildKeyType()
|
||||
}
|
||||
|
||||
// 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()
|
||||
for _, alg := range algorithms {
|
||||
if _, ok := builtinKeyType[alg.String()]; ok {
|
||||
continue
|
||||
}
|
||||
delete(allKeyType, alg.String())
|
||||
}
|
||||
muAllKeyType.Unlock()
|
||||
rebuildKeyType()
|
||||
}
|
||||
|
||||
func rebuildKeyType() {
|
||||
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()
|
||||
})
|
||||
muListKeyType.Lock()
|
||||
listKeyType = list
|
||||
muListKeyType.Unlock()
|
||||
}
|
||||
|
||||
// KeyTypes returns a list of all available values for KeyType.
|
||||
func KeyTypes() []KeyType {
|
||||
muListKeyType.RLock()
|
||||
defer muListKeyType.RUnlock()
|
||||
return listKeyType
|
||||
}
|
||||
|
||||
// MarshalJSON serializes the KeyType object to a JSON string.
|
||||
func (s KeyType) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON deserializes the JSON string to a KeyType object.
|
||||
func (s *KeyType) UnmarshalJSON(data []byte) error {
|
||||
var name string
|
||||
if err := json.Unmarshal(data, &name); err != nil {
|
||||
return fmt.Errorf(`failed to unmarshal KeyType: %w`, err)
|
||||
}
|
||||
v, ok := LookupKeyType(name)
|
||||
if !ok {
|
||||
return fmt.Errorf(`unknown KeyType: %q`, name)
|
||||
}
|
||||
*s = v
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user