Initial QSfera import
This commit is contained in:
+76
@@ -0,0 +1,76 @@
|
||||
load("@rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "jws",
|
||||
srcs = [
|
||||
"errors.go",
|
||||
"filter.go",
|
||||
"headers.go",
|
||||
"headers_gen.go",
|
||||
"interface.go",
|
||||
"io.go",
|
||||
"jws.go",
|
||||
"key_provider.go",
|
||||
"legacy.go",
|
||||
"message.go",
|
||||
"options.go",
|
||||
"options_gen.go",
|
||||
"signer.go",
|
||||
"sign_context.go",
|
||||
"signature_builder.go",
|
||||
"verifier.go",
|
||||
"verify_context.go",
|
||||
],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/jws",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//cert",
|
||||
"//internal/base64",
|
||||
"//internal/ecutil",
|
||||
"//internal/json",
|
||||
"//internal/jwxio",
|
||||
"//internal/tokens",
|
||||
"//internal/keyconv",
|
||||
"//internal/pool",
|
||||
"//jwa",
|
||||
"//jwk",
|
||||
"//jws/internal/keytype",
|
||||
"//jws/jwsbb",
|
||||
"//jws/legacy",
|
||||
"//transform",
|
||||
"@com_github_lestrrat_go_blackmagic//:blackmagic",
|
||||
"@com_github_lestrrat_go_option_v2//:option",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "jws_test",
|
||||
srcs = [
|
||||
"es256k_test.go",
|
||||
"filter_test.go",
|
||||
"headers_test.go",
|
||||
"jws_test.go",
|
||||
"message_test.go",
|
||||
"options_gen_test.go",
|
||||
"signer_test.go",
|
||||
],
|
||||
embed = [":jws"],
|
||||
deps = [
|
||||
"//cert",
|
||||
"//internal/base64",
|
||||
"//internal/ecutil",
|
||||
"//internal/json",
|
||||
"//internal/jwxtest",
|
||||
"//jwa",
|
||||
"//jwk",
|
||||
"//jwt",
|
||||
"@com_github_lestrrat_go_httprc_v3//:httprc",
|
||||
"@com_github_stretchr_testify//require",
|
||||
],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":jws",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
# JWS [](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3/jws)
|
||||
|
||||
Package jws implements JWS as described in [RFC7515](https://tools.ietf.org/html/rfc7515) and [RFC7797](https://tools.ietf.org/html/rfc7797)
|
||||
|
||||
* Parse and generate compact or JSON serializations
|
||||
* Sign and verify arbitrary payload
|
||||
* Use any of the keys supported in [github.com/lestrrat-go/jwx/v3/jwk](../jwk)
|
||||
* Add arbitrary fields in the JWS object
|
||||
* Ability to add/replace existing signature methods
|
||||
* Respect "b64" settings for RFC7797
|
||||
|
||||
How-to style documentation can be found in the [docs directory](../docs).
|
||||
|
||||
Examples are located in the examples directory ([jws_example_test.go](../examples/jws_example_test.go))
|
||||
|
||||
Supported signature algorithms:
|
||||
|
||||
| Algorithm | Supported? | Constant in [jwa](../jwa) |
|
||||
|:----------------------------------------|:-----------|:-------------------------|
|
||||
| HMAC using SHA-256 | YES | jwa.HS256 |
|
||||
| HMAC using SHA-384 | YES | jwa.HS384 |
|
||||
| HMAC using SHA-512 | YES | jwa.HS512 |
|
||||
| RSASSA-PKCS-v1.5 using SHA-256 | YES | jwa.RS256 |
|
||||
| RSASSA-PKCS-v1.5 using SHA-384 | YES | jwa.RS384 |
|
||||
| RSASSA-PKCS-v1.5 using SHA-512 | YES | jwa.RS512 |
|
||||
| ECDSA using P-256 and SHA-256 | YES | jwa.ES256 |
|
||||
| ECDSA using P-384 and SHA-384 | YES | jwa.ES384 |
|
||||
| ECDSA using P-521 and SHA-512 | YES | jwa.ES512 |
|
||||
| ECDSA using secp256k1 and SHA-256 (2) | YES | jwa.ES256K |
|
||||
| RSASSA-PSS using SHA256 and MGF1-SHA256 | YES | jwa.PS256 |
|
||||
| RSASSA-PSS using SHA384 and MGF1-SHA384 | YES | jwa.PS384 |
|
||||
| RSASSA-PSS using SHA512 and MGF1-SHA512 | YES | jwa.PS512 |
|
||||
| EdDSA (1) | YES | jwa.EdDSA |
|
||||
|
||||
* Note 1: Experimental
|
||||
* Note 2: Experimental, and must be toggled using `-tags jwx_es256k` build tag
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
## Sign and verify arbitrary data
|
||||
|
||||
```go
|
||||
import(
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"log"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jws"
|
||||
)
|
||||
|
||||
func main() {
|
||||
privkey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
log.Printf("failed to generate private key: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := jws.Sign([]byte("Lorem ipsum"), jws.WithKey(jwa.RS256, privkey))
|
||||
if err != nil {
|
||||
log.Printf("failed to created JWS message: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// When you receive a JWS message, you can verify the signature
|
||||
// and grab the payload sent in the message in one go:
|
||||
verified, err := jws.Verify(buf, jws.WithKey(jwa.RS256, &privkey.PublicKey))
|
||||
if err != nil {
|
||||
log.Printf("failed to verify message: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("signed message verified! -> %s", verified)
|
||||
}
|
||||
```
|
||||
|
||||
## Programmatically manipulate `jws.Message`
|
||||
|
||||
```go
|
||||
func ExampleMessage() {
|
||||
// initialization for the following variables have been omitted.
|
||||
// please see jws_example_test.go for details
|
||||
var decodedPayload, decodedSig1, decodedSig2 []byte
|
||||
var public1, protected1, public2, protected2 jws.Header
|
||||
|
||||
// Construct a message. DO NOT use values that are base64 encoded
|
||||
m := jws.NewMessage().
|
||||
SetPayload(decodedPayload).
|
||||
AppendSignature(
|
||||
jws.NewSignature().
|
||||
SetSignature(decodedSig1).
|
||||
SetProtectedHeaders(public1).
|
||||
SetPublicHeaders(protected1),
|
||||
).
|
||||
AppendSignature(
|
||||
jws.NewSignature().
|
||||
SetSignature(decodedSig2).
|
||||
SetProtectedHeaders(public2).
|
||||
SetPublicHeaders(protected2),
|
||||
)
|
||||
|
||||
buf, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
fmt.Printf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = buf
|
||||
}
|
||||
```
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package jws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type signError struct {
|
||||
error
|
||||
}
|
||||
|
||||
var errDefaultSignError = signerr(`unknown error`)
|
||||
|
||||
// SignError returns an error that can be passed to `errors.Is` to check if the error is a sign error.
|
||||
func SignError() error {
|
||||
return errDefaultSignError
|
||||
}
|
||||
|
||||
func (e signError) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
|
||||
func (signError) Is(err error) bool {
|
||||
_, ok := err.(signError)
|
||||
return ok
|
||||
}
|
||||
|
||||
func signerr(f string, args ...any) error {
|
||||
return signError{fmt.Errorf(`jws.Sign: `+f, args...)}
|
||||
}
|
||||
|
||||
// This error is returned when jws.Verify fails, but note that there's another type of
|
||||
// message that can be returned by jws.Verify, which is `errVerification`.
|
||||
type verifyError struct {
|
||||
error
|
||||
}
|
||||
|
||||
var errDefaultVerifyError = verifyerr(`unknown error`)
|
||||
|
||||
// VerifyError returns an error that can be passed to `errors.Is` to check if the error is a verify error.
|
||||
func VerifyError() error {
|
||||
return errDefaultVerifyError
|
||||
}
|
||||
|
||||
func (e verifyError) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
|
||||
func (verifyError) Is(err error) bool {
|
||||
_, ok := err.(verifyError)
|
||||
return ok
|
||||
}
|
||||
|
||||
func verifyerr(f string, args ...any) error {
|
||||
return verifyError{fmt.Errorf(`jws.Verify: `+f, args...)}
|
||||
}
|
||||
|
||||
// verificationError is returned when the actual _verification_ of the key/payload fails.
|
||||
type verificationError struct {
|
||||
error
|
||||
}
|
||||
|
||||
var errDefaultVerificationError = verificationError{fmt.Errorf(`unknown verification error`)}
|
||||
|
||||
// VerificationError returns an error that can be passed to `errors.Is` to check if the error is a verification error.
|
||||
func VerificationError() error {
|
||||
return errDefaultVerificationError
|
||||
}
|
||||
|
||||
func (e verificationError) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
|
||||
func (verificationError) Is(err error) bool {
|
||||
_, ok := err.(verificationError)
|
||||
return ok
|
||||
}
|
||||
|
||||
type parseError struct {
|
||||
error
|
||||
}
|
||||
|
||||
var errDefaultParseError = parseerr(`unknown error`)
|
||||
|
||||
// ParseError returns an error that can be passed to `errors.Is` to check if the error is a parse error.
|
||||
func ParseError() error {
|
||||
return errDefaultParseError
|
||||
}
|
||||
|
||||
func (e parseError) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
|
||||
func (parseError) Is(err error) bool {
|
||||
_, ok := err.(parseError)
|
||||
return ok
|
||||
}
|
||||
|
||||
func bparseerr(prefix string, f string, args ...any) error {
|
||||
return parseError{fmt.Errorf(prefix+": "+f, args...)}
|
||||
}
|
||||
|
||||
func parseerr(f string, args ...any) error {
|
||||
return bparseerr(`jws.Parse`, f, args...)
|
||||
}
|
||||
|
||||
func sparseerr(f string, args ...any) error {
|
||||
return bparseerr(`jws.ParseString`, f, args...)
|
||||
}
|
||||
|
||||
func rparseerr(f string, args ...any) error {
|
||||
return bparseerr(`jws.ParseReader`, f, args...)
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
//go:build jwx_es256k
|
||||
|
||||
package jws
|
||||
|
||||
import (
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register ES256K to EC algorithm family
|
||||
addAlgorithmForKeyType(jwa.EC(), jwa.ES256K())
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package jws
|
||||
|
||||
import (
|
||||
"github.com/lestrrat-go/jwx/v3/transform"
|
||||
)
|
||||
|
||||
// HeaderFilter is an interface that allows users to filter JWS header fields.
|
||||
// It provides two methods: Filter and Reject; Filter returns a new header with only
|
||||
// the fields that match the filter criteria, while Reject returns a new header with
|
||||
// only the fields that DO NOT match the filter.
|
||||
//
|
||||
// EXPERIMENTAL: This API is experimental and its interface and behavior is
|
||||
// subject to change in future releases. This API is not subject to semver
|
||||
// compatibility guarantees.
|
||||
type HeaderFilter interface {
|
||||
Filter(header Headers) (Headers, error)
|
||||
Reject(header Headers) (Headers, error)
|
||||
}
|
||||
|
||||
// StandardHeadersFilter returns a HeaderFilter that filters out standard JWS header fields.
|
||||
//
|
||||
// You can use this filter to create headers that either only have standard fields
|
||||
// or only custom fields.
|
||||
//
|
||||
// If you need to configure the filter more precisely, consider
|
||||
// using the HeaderNameFilter directly.
|
||||
func StandardHeadersFilter() HeaderFilter {
|
||||
return stdHeadersFilter
|
||||
}
|
||||
|
||||
var stdHeadersFilter = NewHeaderNameFilter(stdHeaderNames...)
|
||||
|
||||
// NewHeaderNameFilter creates a new HeaderNameFilter with the specified field names.
|
||||
func NewHeaderNameFilter(names ...string) HeaderFilter {
|
||||
return transform.NewNameBasedFilter[Headers](names...)
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package jws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (h *stdHeaders) Copy(dst Headers) error {
|
||||
for _, k := range h.Keys() {
|
||||
var v any
|
||||
if err := h.Get(k, &v); err != nil {
|
||||
return fmt.Errorf(`failed to get header %q: %w`, k, err)
|
||||
}
|
||||
if err := dst.Set(k, v); err != nil {
|
||||
return fmt.Errorf(`failed to set header %q: %w`, k, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeHeaders merges two headers, and works even if the first Header
|
||||
// object is nil. This is not exported because ATM it felt like this
|
||||
// function is not frequently used, and MergeHeaders seemed a clunky name
|
||||
func mergeHeaders(h1, h2 Headers) (Headers, error) {
|
||||
h3 := NewHeaders()
|
||||
|
||||
if h1 != nil {
|
||||
if err := h1.Copy(h3); err != nil {
|
||||
return nil, fmt.Errorf(`failed to copy headers from first Header: %w`, err)
|
||||
}
|
||||
}
|
||||
|
||||
if h2 != nil {
|
||||
if err := h2.Copy(h3); err != nil {
|
||||
return nil, fmt.Errorf(`failed to copy headers from second Header: %w`, err)
|
||||
}
|
||||
}
|
||||
|
||||
return h3, nil
|
||||
}
|
||||
|
||||
func (h *stdHeaders) Merge(h2 Headers) (Headers, error) {
|
||||
return mergeHeaders(h, h2)
|
||||
}
|
||||
|
||||
// Clone creates a deep copy of the header
|
||||
func (h *stdHeaders) Clone() (Headers, error) {
|
||||
dst := NewHeaders()
|
||||
if err := h.Copy(dst); err != nil {
|
||||
return nil, fmt.Errorf(`failed to copy header: %w`, err)
|
||||
}
|
||||
return dst, nil
|
||||
}
|
||||
+704
@@ -0,0 +1,704 @@
|
||||
// Code generated by tools/cmd/genjws/main.go. DO NOT EDIT.
|
||||
|
||||
package jws
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/lestrrat-go/blackmagic"
|
||||
"github.com/lestrrat-go/jwx/v3/cert"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/base64"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/json"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/pool"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/tokens"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
)
|
||||
|
||||
const (
|
||||
AlgorithmKey = "alg"
|
||||
ContentTypeKey = "cty"
|
||||
CriticalKey = "crit"
|
||||
JWKKey = "jwk"
|
||||
JWKSetURLKey = "jku"
|
||||
KeyIDKey = "kid"
|
||||
TypeKey = "typ"
|
||||
X509CertChainKey = "x5c"
|
||||
X509CertThumbprintKey = "x5t"
|
||||
X509CertThumbprintS256Key = "x5t#S256"
|
||||
X509URLKey = "x5u"
|
||||
)
|
||||
|
||||
// Headers describe a standard JWS Header set. It is part of the JWS message
|
||||
// and is used to represet both Public or Protected headers, which in turn
|
||||
// can be found in each Signature object. If you are not sure how this works,
|
||||
// it is strongly recommended that you read RFC7515, especially the section
|
||||
// that describes the full JSON serialization format of JWS messages.
|
||||
//
|
||||
// In most cases, you likely want to use the protected headers, as this is part of the signed content.
|
||||
type Headers interface {
|
||||
Algorithm() (jwa.SignatureAlgorithm, bool)
|
||||
ContentType() (string, bool)
|
||||
Critical() ([]string, bool)
|
||||
JWK() (jwk.Key, bool)
|
||||
JWKSetURL() (string, bool)
|
||||
KeyID() (string, bool)
|
||||
Type() (string, bool)
|
||||
X509CertChain() (*cert.Chain, bool)
|
||||
X509CertThumbprint() (string, bool)
|
||||
X509CertThumbprintS256() (string, bool)
|
||||
X509URL() (string, bool)
|
||||
Copy(Headers) error
|
||||
Merge(Headers) (Headers, error)
|
||||
Clone() (Headers, error)
|
||||
// Get is used to extract the value of any field, including non-standard fields, out of the header.
|
||||
//
|
||||
// The first argument is the name of the field. The second argument is a pointer
|
||||
// to a variable that will receive the value of the field. The method returns
|
||||
// an error if the field does not exist, or if the value cannot be assigned to
|
||||
// the destination variable. Note that a field is considered to "exist" even if
|
||||
// the value is empty-ish (e.g. 0, false, ""), as long as it is explicitly set.
|
||||
Get(string, any) error
|
||||
Set(string, any) error
|
||||
Remove(string) error
|
||||
// Has returns true if the specified header has a value, even if
|
||||
// the value is empty-ish (e.g. 0, false, "") as long as it has been
|
||||
// explicitly set.
|
||||
Has(string) bool
|
||||
Keys() []string
|
||||
}
|
||||
|
||||
// stdHeaderNames is a list of all standard header names defined in the JWS specification.
|
||||
var stdHeaderNames = []string{AlgorithmKey, ContentTypeKey, CriticalKey, JWKKey, JWKSetURLKey, KeyIDKey, TypeKey, X509CertChainKey, X509CertThumbprintKey, X509CertThumbprintS256Key, X509URLKey}
|
||||
|
||||
type stdHeaders struct {
|
||||
algorithm *jwa.SignatureAlgorithm // https://tools.ietf.org/html/rfc7515#section-4.1.1
|
||||
contentType *string // https://tools.ietf.org/html/rfc7515#section-4.1.10
|
||||
critical []string // https://tools.ietf.org/html/rfc7515#section-4.1.11
|
||||
jwk jwk.Key // https://tools.ietf.org/html/rfc7515#section-4.1.3
|
||||
jwkSetURL *string // https://tools.ietf.org/html/rfc7515#section-4.1.2
|
||||
keyID *string // https://tools.ietf.org/html/rfc7515#section-4.1.4
|
||||
typ *string // https://tools.ietf.org/html/rfc7515#section-4.1.9
|
||||
x509CertChain *cert.Chain // https://tools.ietf.org/html/rfc7515#section-4.1.6
|
||||
x509CertThumbprint *string // https://tools.ietf.org/html/rfc7515#section-4.1.7
|
||||
x509CertThumbprintS256 *string // https://tools.ietf.org/html/rfc7515#section-4.1.8
|
||||
x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5
|
||||
privateParams map[string]any
|
||||
mu *sync.RWMutex
|
||||
dc DecodeCtx
|
||||
raw []byte // stores the raw version of the header so it can be used later
|
||||
}
|
||||
|
||||
func NewHeaders() Headers {
|
||||
return &stdHeaders{
|
||||
mu: &sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *stdHeaders) Algorithm() (jwa.SignatureAlgorithm, bool) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
if h.algorithm == nil {
|
||||
return jwa.EmptySignatureAlgorithm(), false
|
||||
}
|
||||
return *(h.algorithm), true
|
||||
}
|
||||
|
||||
func (h *stdHeaders) ContentType() (string, bool) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
if h.contentType == nil {
|
||||
return "", false
|
||||
}
|
||||
return *(h.contentType), true
|
||||
}
|
||||
|
||||
func (h *stdHeaders) Critical() ([]string, bool) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
return h.critical, true
|
||||
}
|
||||
|
||||
func (h *stdHeaders) JWK() (jwk.Key, bool) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
return h.jwk, true
|
||||
}
|
||||
|
||||
func (h *stdHeaders) JWKSetURL() (string, bool) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
if h.jwkSetURL == nil {
|
||||
return "", false
|
||||
}
|
||||
return *(h.jwkSetURL), true
|
||||
}
|
||||
|
||||
func (h *stdHeaders) KeyID() (string, bool) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
if h.keyID == nil {
|
||||
return "", false
|
||||
}
|
||||
return *(h.keyID), true
|
||||
}
|
||||
|
||||
func (h *stdHeaders) Type() (string, bool) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
if h.typ == nil {
|
||||
return "", false
|
||||
}
|
||||
return *(h.typ), true
|
||||
}
|
||||
|
||||
func (h *stdHeaders) X509CertChain() (*cert.Chain, bool) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
return h.x509CertChain, true
|
||||
}
|
||||
|
||||
func (h *stdHeaders) X509CertThumbprint() (string, bool) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
if h.x509CertThumbprint == nil {
|
||||
return "", false
|
||||
}
|
||||
return *(h.x509CertThumbprint), true
|
||||
}
|
||||
|
||||
func (h *stdHeaders) X509CertThumbprintS256() (string, bool) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
if h.x509CertThumbprintS256 == nil {
|
||||
return "", false
|
||||
}
|
||||
return *(h.x509CertThumbprintS256), true
|
||||
}
|
||||
|
||||
func (h *stdHeaders) X509URL() (string, bool) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
if h.x509URL == nil {
|
||||
return "", false
|
||||
}
|
||||
return *(h.x509URL), true
|
||||
}
|
||||
|
||||
func (h *stdHeaders) clear() {
|
||||
h.algorithm = nil
|
||||
h.contentType = nil
|
||||
h.critical = nil
|
||||
h.jwk = nil
|
||||
h.jwkSetURL = nil
|
||||
h.keyID = nil
|
||||
h.typ = nil
|
||||
h.x509CertChain = nil
|
||||
h.x509CertThumbprint = nil
|
||||
h.x509CertThumbprintS256 = nil
|
||||
h.x509URL = nil
|
||||
h.privateParams = nil
|
||||
h.raw = nil
|
||||
}
|
||||
|
||||
func (h *stdHeaders) DecodeCtx() DecodeCtx {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
return h.dc
|
||||
}
|
||||
|
||||
func (h *stdHeaders) SetDecodeCtx(dc DecodeCtx) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.dc = dc
|
||||
}
|
||||
|
||||
func (h *stdHeaders) rawBuffer() []byte {
|
||||
return h.raw
|
||||
}
|
||||
|
||||
func (h *stdHeaders) PrivateParams() map[string]any {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
return h.privateParams
|
||||
}
|
||||
|
||||
func (h *stdHeaders) Has(name string) bool {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
switch name {
|
||||
case AlgorithmKey:
|
||||
return h.algorithm != nil
|
||||
case ContentTypeKey:
|
||||
return h.contentType != nil
|
||||
case CriticalKey:
|
||||
return h.critical != nil
|
||||
case JWKKey:
|
||||
return h.jwk != nil
|
||||
case JWKSetURLKey:
|
||||
return h.jwkSetURL != nil
|
||||
case KeyIDKey:
|
||||
return h.keyID != nil
|
||||
case TypeKey:
|
||||
return h.typ != nil
|
||||
case X509CertChainKey:
|
||||
return h.x509CertChain != nil
|
||||
case X509CertThumbprintKey:
|
||||
return h.x509CertThumbprint != nil
|
||||
case X509CertThumbprintS256Key:
|
||||
return h.x509CertThumbprintS256 != nil
|
||||
case X509URLKey:
|
||||
return h.x509URL != nil
|
||||
default:
|
||||
_, ok := h.privateParams[name]
|
||||
return ok
|
||||
}
|
||||
}
|
||||
|
||||
func (h *stdHeaders) Get(name string, dst any) error {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
switch name {
|
||||
case AlgorithmKey:
|
||||
if h.algorithm == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, *(h.algorithm)); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case ContentTypeKey:
|
||||
if h.contentType == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, *(h.contentType)); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case CriticalKey:
|
||||
if h.critical == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst,
|
||||
h.critical); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case JWKKey:
|
||||
if h.jwk == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst,
|
||||
h.jwk); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case JWKSetURLKey:
|
||||
if h.jwkSetURL == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, *(h.jwkSetURL)); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case KeyIDKey:
|
||||
if h.keyID == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, *(h.keyID)); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case TypeKey:
|
||||
if h.typ == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, *(h.typ)); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case X509CertChainKey:
|
||||
if h.x509CertChain == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst,
|
||||
h.x509CertChain); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case X509CertThumbprintKey:
|
||||
if h.x509CertThumbprint == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, *(h.x509CertThumbprint)); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case X509CertThumbprintS256Key:
|
||||
if h.x509CertThumbprintS256 == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, *(h.x509CertThumbprintS256)); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case X509URLKey:
|
||||
if h.x509URL == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, *(h.x509URL)); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
v, ok := h.privateParams[name]
|
||||
if !ok {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, v); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *stdHeaders) Set(name string, value any) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
return h.setNoLock(name, value)
|
||||
}
|
||||
|
||||
func (h *stdHeaders) setNoLock(name string, value any) error {
|
||||
switch name {
|
||||
case AlgorithmKey:
|
||||
alg, err := jwa.KeyAlgorithmFrom(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid value for %s key: %w", AlgorithmKey, err)
|
||||
}
|
||||
if salg, ok := alg.(jwa.SignatureAlgorithm); ok {
|
||||
h.algorithm = &salg
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("expecte jwa.SignatureAlgorithm, received %T", alg)
|
||||
case ContentTypeKey:
|
||||
if v, ok := value.(string); ok {
|
||||
h.contentType = &v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, ContentTypeKey, value)
|
||||
case CriticalKey:
|
||||
if v, ok := value.([]string); ok {
|
||||
h.critical = v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, CriticalKey, value)
|
||||
case JWKKey:
|
||||
if v, ok := value.(jwk.Key); ok {
|
||||
h.jwk = v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, JWKKey, value)
|
||||
case JWKSetURLKey:
|
||||
if v, ok := value.(string); ok {
|
||||
h.jwkSetURL = &v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, JWKSetURLKey, value)
|
||||
case KeyIDKey:
|
||||
if v, ok := value.(string); ok {
|
||||
h.keyID = &v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, KeyIDKey, value)
|
||||
case TypeKey:
|
||||
if v, ok := value.(string); ok {
|
||||
h.typ = &v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, TypeKey, value)
|
||||
case X509CertChainKey:
|
||||
if v, ok := value.(*cert.Chain); ok {
|
||||
h.x509CertChain = v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, X509CertChainKey, value)
|
||||
case X509CertThumbprintKey:
|
||||
if v, ok := value.(string); ok {
|
||||
h.x509CertThumbprint = &v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, X509CertThumbprintKey, value)
|
||||
case X509CertThumbprintS256Key:
|
||||
if v, ok := value.(string); ok {
|
||||
h.x509CertThumbprintS256 = &v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, X509CertThumbprintS256Key, value)
|
||||
case X509URLKey:
|
||||
if v, ok := value.(string); ok {
|
||||
h.x509URL = &v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, X509URLKey, value)
|
||||
default:
|
||||
if h.privateParams == nil {
|
||||
h.privateParams = map[string]any{}
|
||||
}
|
||||
h.privateParams[name] = value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *stdHeaders) Remove(key string) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
switch key {
|
||||
case AlgorithmKey:
|
||||
h.algorithm = nil
|
||||
case ContentTypeKey:
|
||||
h.contentType = nil
|
||||
case CriticalKey:
|
||||
h.critical = nil
|
||||
case JWKKey:
|
||||
h.jwk = nil
|
||||
case JWKSetURLKey:
|
||||
h.jwkSetURL = nil
|
||||
case KeyIDKey:
|
||||
h.keyID = nil
|
||||
case TypeKey:
|
||||
h.typ = nil
|
||||
case X509CertChainKey:
|
||||
h.x509CertChain = nil
|
||||
case X509CertThumbprintKey:
|
||||
h.x509CertThumbprint = nil
|
||||
case X509CertThumbprintS256Key:
|
||||
h.x509CertThumbprintS256 = nil
|
||||
case X509URLKey:
|
||||
h.x509URL = nil
|
||||
default:
|
||||
delete(h.privateParams, key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *stdHeaders) UnmarshalJSON(buf []byte) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.clear()
|
||||
dec := json.NewDecoder(bytes.NewReader(buf))
|
||||
LOOP:
|
||||
for {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return fmt.Errorf(`error reading token: %w`, err)
|
||||
}
|
||||
switch tok := tok.(type) {
|
||||
case json.Delim:
|
||||
// Assuming we're doing everything correctly, we should ONLY
|
||||
// get either tokens.OpenCurlyBracket or tokens.CloseCurlyBracket here.
|
||||
if tok == tokens.CloseCurlyBracket { // End of object
|
||||
break LOOP
|
||||
} else if tok != tokens.OpenCurlyBracket {
|
||||
return fmt.Errorf(`expected '%c' but got '%c'`, tokens.OpenCurlyBracket, tok)
|
||||
}
|
||||
case string: // Objects can only have string keys
|
||||
switch tok {
|
||||
case AlgorithmKey:
|
||||
var decoded jwa.SignatureAlgorithm
|
||||
if err := dec.Decode(&decoded); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, AlgorithmKey, err)
|
||||
}
|
||||
h.algorithm = &decoded
|
||||
case ContentTypeKey:
|
||||
if err := json.AssignNextStringToken(&h.contentType, dec); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, ContentTypeKey, err)
|
||||
}
|
||||
case CriticalKey:
|
||||
var decoded []string
|
||||
if err := dec.Decode(&decoded); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, CriticalKey, err)
|
||||
}
|
||||
h.critical = decoded
|
||||
case JWKKey:
|
||||
var buf json.RawMessage
|
||||
if err := dec.Decode(&buf); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, JWKKey, err)
|
||||
}
|
||||
key, err := jwk.ParseKey(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to parse JWK for key %s: %w`, JWKKey, err)
|
||||
}
|
||||
h.jwk = key
|
||||
case JWKSetURLKey:
|
||||
if err := json.AssignNextStringToken(&h.jwkSetURL, dec); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, JWKSetURLKey, err)
|
||||
}
|
||||
case KeyIDKey:
|
||||
if err := json.AssignNextStringToken(&h.keyID, dec); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err)
|
||||
}
|
||||
case TypeKey:
|
||||
if err := json.AssignNextStringToken(&h.typ, dec); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, TypeKey, err)
|
||||
}
|
||||
case X509CertChainKey:
|
||||
var decoded cert.Chain
|
||||
if err := dec.Decode(&decoded); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertChainKey, err)
|
||||
}
|
||||
h.x509CertChain = &decoded
|
||||
case X509CertThumbprintKey:
|
||||
if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err)
|
||||
}
|
||||
case X509CertThumbprintS256Key:
|
||||
if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err)
|
||||
}
|
||||
case X509URLKey:
|
||||
if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err)
|
||||
}
|
||||
default:
|
||||
decoded, err := registry.Decode(dec, tok)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.setNoLock(tok, decoded)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf(`invalid token %T`, tok)
|
||||
}
|
||||
}
|
||||
h.raw = buf
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *stdHeaders) Keys() []string {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
keys := make([]string, 0, 11+len(h.privateParams))
|
||||
if h.algorithm != nil {
|
||||
keys = append(keys, AlgorithmKey)
|
||||
}
|
||||
if h.contentType != nil {
|
||||
keys = append(keys, ContentTypeKey)
|
||||
}
|
||||
if h.critical != nil {
|
||||
keys = append(keys, CriticalKey)
|
||||
}
|
||||
if h.jwk != nil {
|
||||
keys = append(keys, JWKKey)
|
||||
}
|
||||
if h.jwkSetURL != nil {
|
||||
keys = append(keys, JWKSetURLKey)
|
||||
}
|
||||
if h.keyID != nil {
|
||||
keys = append(keys, KeyIDKey)
|
||||
}
|
||||
if h.typ != nil {
|
||||
keys = append(keys, TypeKey)
|
||||
}
|
||||
if h.x509CertChain != nil {
|
||||
keys = append(keys, X509CertChainKey)
|
||||
}
|
||||
if h.x509CertThumbprint != nil {
|
||||
keys = append(keys, X509CertThumbprintKey)
|
||||
}
|
||||
if h.x509CertThumbprintS256 != nil {
|
||||
keys = append(keys, X509CertThumbprintS256Key)
|
||||
}
|
||||
if h.x509URL != nil {
|
||||
keys = append(keys, X509URLKey)
|
||||
}
|
||||
for k := range h.privateParams {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (h stdHeaders) MarshalJSON() ([]byte, error) {
|
||||
h.mu.RLock()
|
||||
data := make(map[string]any)
|
||||
keys := make([]string, 0, 11+len(h.privateParams))
|
||||
if h.algorithm != nil {
|
||||
data[AlgorithmKey] = *(h.algorithm)
|
||||
keys = append(keys, AlgorithmKey)
|
||||
}
|
||||
if h.contentType != nil {
|
||||
data[ContentTypeKey] = *(h.contentType)
|
||||
keys = append(keys, ContentTypeKey)
|
||||
}
|
||||
if h.critical != nil {
|
||||
data[CriticalKey] = h.critical
|
||||
keys = append(keys, CriticalKey)
|
||||
}
|
||||
if h.jwk != nil {
|
||||
data[JWKKey] = h.jwk
|
||||
keys = append(keys, JWKKey)
|
||||
}
|
||||
if h.jwkSetURL != nil {
|
||||
data[JWKSetURLKey] = *(h.jwkSetURL)
|
||||
keys = append(keys, JWKSetURLKey)
|
||||
}
|
||||
if h.keyID != nil {
|
||||
data[KeyIDKey] = *(h.keyID)
|
||||
keys = append(keys, KeyIDKey)
|
||||
}
|
||||
if h.typ != nil {
|
||||
data[TypeKey] = *(h.typ)
|
||||
keys = append(keys, TypeKey)
|
||||
}
|
||||
if h.x509CertChain != nil {
|
||||
data[X509CertChainKey] = h.x509CertChain
|
||||
keys = append(keys, X509CertChainKey)
|
||||
}
|
||||
if h.x509CertThumbprint != nil {
|
||||
data[X509CertThumbprintKey] = *(h.x509CertThumbprint)
|
||||
keys = append(keys, X509CertThumbprintKey)
|
||||
}
|
||||
if h.x509CertThumbprintS256 != nil {
|
||||
data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256)
|
||||
keys = append(keys, X509CertThumbprintS256Key)
|
||||
}
|
||||
if h.x509URL != nil {
|
||||
data[X509URLKey] = *(h.x509URL)
|
||||
keys = append(keys, X509URLKey)
|
||||
}
|
||||
for k, v := range h.privateParams {
|
||||
data[k] = v
|
||||
keys = append(keys, k)
|
||||
}
|
||||
h.mu.RUnlock()
|
||||
sort.Strings(keys)
|
||||
buf := pool.BytesBuffer().Get()
|
||||
defer pool.BytesBuffer().Put(buf)
|
||||
enc := json.NewEncoder(buf)
|
||||
buf.WriteByte(tokens.OpenCurlyBracket)
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
buf.WriteRune(tokens.Comma)
|
||||
}
|
||||
buf.WriteRune(tokens.DoubleQuote)
|
||||
buf.WriteString(k)
|
||||
buf.WriteString(`":`)
|
||||
switch v := data[k].(type) {
|
||||
case []byte:
|
||||
buf.WriteRune(tokens.DoubleQuote)
|
||||
buf.WriteString(base64.EncodeToString(v))
|
||||
buf.WriteRune(tokens.DoubleQuote)
|
||||
default:
|
||||
if err := enc.Encode(v); err != nil {
|
||||
return nil, fmt.Errorf(`failed to encode value for field %s: %w`, k, err)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
}
|
||||
}
|
||||
buf.WriteByte(tokens.CloseCurlyBracket)
|
||||
ret := make([]byte, buf.Len())
|
||||
copy(ret, buf.Bytes())
|
||||
return ret, nil
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package jws
|
||||
|
||||
import (
|
||||
"github.com/lestrrat-go/jwx/v3/internal/base64"
|
||||
"github.com/lestrrat-go/jwx/v3/jws/legacy"
|
||||
)
|
||||
|
||||
type Signer = legacy.Signer
|
||||
type Verifier = legacy.Verifier
|
||||
type HMACSigner = legacy.HMACSigner
|
||||
type HMACVerifier = legacy.HMACVerifier
|
||||
|
||||
// Base64Encoder is an interface that can be used when encoding JWS message
|
||||
// components to base64. This is useful when you want to use a non-standard
|
||||
// base64 encoder while generating or verifying signatures. By default JWS
|
||||
// uses raw url base64 encoding (without padding), but there are apparently
|
||||
// some cases where you may want to use a base64 encoders that uses padding.
|
||||
//
|
||||
// For example, apparently AWS ALB User Claims is provided in JWT format,
|
||||
// but it uses a base64 encoding with padding.
|
||||
type Base64Encoder = base64.Encoder
|
||||
|
||||
type DecodeCtx interface {
|
||||
CollectRaw() bool
|
||||
}
|
||||
|
||||
// Message represents a full JWS encoded message. Flattened serialization
|
||||
// is not supported as a struct, but rather it's represented as a
|
||||
// Message struct with only one `signature` element.
|
||||
//
|
||||
// Do not expect to use the Message object to verify or construct a
|
||||
// signed payload with. You should only use this when you want to actually
|
||||
// programmatically view the contents of the full JWS payload.
|
||||
//
|
||||
// As of this version, there is one big incompatibility when using Message
|
||||
// objects to convert between compact and JSON representations.
|
||||
// The protected header is sometimes encoded differently from the original
|
||||
// message and the JSON serialization that we use in Go.
|
||||
//
|
||||
// For example, the protected header `eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9`
|
||||
// decodes to
|
||||
//
|
||||
// {"typ":"JWT",
|
||||
// "alg":"HS256"}
|
||||
//
|
||||
// However, when we parse this into a message, we create a jws.Header object,
|
||||
// which, when we marshal into a JSON object again, becomes
|
||||
//
|
||||
// {"typ":"JWT","alg":"HS256"}
|
||||
//
|
||||
// Notice that serialization lacks a line break and a space between `"JWT",`
|
||||
// and `"alg"`. This causes a problem when verifying the signatures AFTER
|
||||
// a compact JWS message has been unmarshaled into a jws.Message.
|
||||
//
|
||||
// jws.Verify() doesn't go through this step, and therefore this does not
|
||||
// manifest itself. However, you may see this discrepancy when you manually
|
||||
// go through these conversions, and/or use the `jwx` tool like so:
|
||||
//
|
||||
// jwx jws parse message.jws | jwx jws verify --key somekey.jwk --stdin
|
||||
//
|
||||
// In this scenario, the first `jwx jws parse` outputs a parsed jws.Message
|
||||
// which is marshaled into JSON. At this point the message's protected
|
||||
// headers and the signatures don't match.
|
||||
//
|
||||
// To sign and verify, use the appropriate `Sign()` and `Verify()` functions.
|
||||
type Message struct {
|
||||
dc DecodeCtx
|
||||
payload []byte
|
||||
signatures []*Signature
|
||||
b64 bool // true if payload should be base64 encoded
|
||||
}
|
||||
|
||||
type Signature struct {
|
||||
encoder Base64Encoder
|
||||
dc DecodeCtx
|
||||
headers Headers // Unprotected Headers
|
||||
protected Headers // Protected Headers
|
||||
signature []byte // Signature
|
||||
detached bool
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
load("@rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "keytype",
|
||||
srcs = ["keytype.go"],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/jws/internal/keytype",
|
||||
visibility = ["//jws:__subpackages__"],
|
||||
deps = [
|
||||
"//jwk",
|
||||
],
|
||||
)
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package keytype
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
)
|
||||
|
||||
// Because the keys defined in github.com/lestrrat-go/jwx/jwk may also implement
|
||||
// crypto.Signer, it would be possible for to mix up key types when signing/verifying
|
||||
// for example, when we specify jws.WithKey(jwa.RSA256, cryptoSigner), the cryptoSigner
|
||||
// can be for RSA, or any other type that implements crypto.Signer... even if it's for the
|
||||
// wrong algorithm.
|
||||
//
|
||||
// These functions are there to differentiate between the valid KNOWN key types.
|
||||
// For any other key type that is outside of the Go std library and our own code,
|
||||
// we must rely on the user to be vigilant.
|
||||
//
|
||||
// Notes: symmetric keys are obviously not part of this. for v2 OKP keys,
|
||||
// x25519 does not implement Sign()
|
||||
func IsValidRSAKey(key any) bool {
|
||||
switch key.(type) {
|
||||
case
|
||||
ecdsa.PrivateKey, *ecdsa.PrivateKey,
|
||||
ed25519.PrivateKey,
|
||||
jwk.ECDSAPrivateKey, jwk.OKPPrivateKey:
|
||||
// these are NOT ok
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func IsValidECDSAKey(key any) bool {
|
||||
switch key.(type) {
|
||||
case
|
||||
ed25519.PrivateKey,
|
||||
rsa.PrivateKey, *rsa.PrivateKey,
|
||||
jwk.RSAPrivateKey, jwk.OKPPrivateKey:
|
||||
// these are NOT ok
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func IsValidEDDSAKey(key any) bool {
|
||||
switch key.(type) {
|
||||
case
|
||||
ecdsa.PrivateKey, *ecdsa.PrivateKey,
|
||||
rsa.PrivateKey, *rsa.PrivateKey,
|
||||
jwk.RSAPrivateKey, jwk.ECDSAPrivateKey:
|
||||
// these are NOT ok
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Code generated by tools/cmd/genreadfile/main.go. DO NOT EDIT.
|
||||
|
||||
package jws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
type sysFS struct{}
|
||||
|
||||
func (sysFS) Open(path string) (fs.File, error) {
|
||||
return os.Open(path)
|
||||
}
|
||||
|
||||
func ReadFile(path string, options ...ReadFileOption) (*Message, error) {
|
||||
|
||||
var srcFS fs.FS = sysFS{}
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identFS{}:
|
||||
if err := option.Value(&srcFS); err != nil {
|
||||
return nil, fmt.Errorf("failed to set fs.FS: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f, err := srcFS.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
return ParseReader(f)
|
||||
}
|
||||
+665
@@ -0,0 +1,665 @@
|
||||
//go:generate ../tools/cmd/genjws.sh
|
||||
|
||||
// Package jws implements the digital signature on JSON based data
|
||||
// structures as described in https://tools.ietf.org/html/rfc7515
|
||||
//
|
||||
// If you do not care about the details, the only things that you
|
||||
// would need to use are the following functions:
|
||||
//
|
||||
// jws.Sign(payload, jws.WithKey(algorithm, key))
|
||||
// jws.Verify(serialized, jws.WithKey(algorithm, key))
|
||||
//
|
||||
// To sign, simply use `jws.Sign`. `payload` is a []byte buffer that
|
||||
// contains whatever data you want to sign. `alg` is one of the
|
||||
// jwa.SignatureAlgorithm constants from package jwa. For RSA and
|
||||
// ECDSA family of algorithms, you will need to prepare a private key.
|
||||
// For HMAC family, you just need a []byte value. The `jws.Sign`
|
||||
// function will return the encoded JWS message on success.
|
||||
//
|
||||
// To verify, use `jws.Verify`. It will parse the `encodedjws` buffer
|
||||
// and verify the result using `algorithm` and `key`. Upon successful
|
||||
// verification, the original payload is returned, so you can work on it.
|
||||
//
|
||||
// As a sidenote, consider using github.com/lestrrat-go/htmsig if you
|
||||
// looking for HTTP Message Signatures (RFC9421) -- it uses the same
|
||||
// underlying signing/verification mechanisms as this module.
|
||||
package jws
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/ecdh"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sync"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/base64"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/json"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/jwxio"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/pool"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/tokens"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
"github.com/lestrrat-go/jwx/v3/jws/jwsbb"
|
||||
)
|
||||
|
||||
var registry = json.NewRegistry()
|
||||
|
||||
var signers = make(map[jwa.SignatureAlgorithm]Signer)
|
||||
var muSigner = &sync.Mutex{}
|
||||
|
||||
func removeSigner(alg jwa.SignatureAlgorithm) {
|
||||
muSigner.Lock()
|
||||
defer muSigner.Unlock()
|
||||
delete(signers, alg)
|
||||
}
|
||||
|
||||
type defaultSigner struct {
|
||||
alg jwa.SignatureAlgorithm
|
||||
}
|
||||
|
||||
func (s defaultSigner) Algorithm() jwa.SignatureAlgorithm {
|
||||
return s.alg
|
||||
}
|
||||
|
||||
func (s defaultSigner) Sign(key any, payload []byte) ([]byte, error) {
|
||||
return jwsbb.Sign(key, s.alg.String(), payload, nil)
|
||||
}
|
||||
|
||||
type signerAdapter struct {
|
||||
signer Signer
|
||||
}
|
||||
|
||||
func (s signerAdapter) Algorithm() jwa.SignatureAlgorithm {
|
||||
return s.signer.Algorithm()
|
||||
}
|
||||
|
||||
func (s signerAdapter) Sign(key any, payload []byte) ([]byte, error) {
|
||||
return s.signer.Sign(payload, key)
|
||||
}
|
||||
|
||||
const (
|
||||
fmtInvalid = 1 << iota
|
||||
fmtCompact
|
||||
fmtJSON
|
||||
fmtJSONPretty
|
||||
fmtMax
|
||||
)
|
||||
|
||||
// silence linters
|
||||
var _ = fmtInvalid
|
||||
var _ = fmtMax
|
||||
|
||||
func validateKeyBeforeUse(key any) error {
|
||||
jwkKey, ok := key.(jwk.Key)
|
||||
if !ok {
|
||||
converted, err := jwk.Import(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`could not convert key of type %T to jwk.Key for validation: %w`, key, err)
|
||||
}
|
||||
jwkKey = converted
|
||||
}
|
||||
return jwkKey.Validate()
|
||||
}
|
||||
|
||||
// Sign generates a JWS message for the given payload and returns
|
||||
// it in serialized form, which can be in either compact or
|
||||
// JSON format. Default is compact.
|
||||
//
|
||||
// You must pass at least one key to `jws.Sign()` by using `jws.WithKey()`
|
||||
// option.
|
||||
//
|
||||
// jws.Sign(payload, jws.WithKey(alg, key))
|
||||
// jws.Sign(payload, jws.WithJSON(), jws.WithKey(alg1, key1), jws.WithKey(alg2, key2))
|
||||
//
|
||||
// Note that in the second example the `jws.WithJSON()` option is
|
||||
// specified as well. This is because the compact serialization
|
||||
// format does not support multiple signatures, and users must
|
||||
// specifically ask for the JSON serialization format.
|
||||
//
|
||||
// Read the documentation for `jws.WithKey()` to learn more about the
|
||||
// possible values that can be used for `alg` and `key`.
|
||||
//
|
||||
// You may create JWS messages with the "none" (jwa.NoSignature) algorithm
|
||||
// if you use the `jws.WithInsecureNoSignature()` option. This option
|
||||
// can be combined with one or more signature keys, as well as the
|
||||
// `jws.WithJSON()` option to generate multiple signatures (though
|
||||
// the usefulness of such constructs is highly debatable)
|
||||
//
|
||||
// Note that this library does not allow you to successfully call `jws.Verify()` on
|
||||
// signatures with the "none" algorithm. To parse these, use `jws.Parse()` instead.
|
||||
//
|
||||
// If you want to use a detached payload, use `jws.WithDetachedPayload()` as
|
||||
// one of the options. When you use this option, you must always set the
|
||||
// first parameter (`payload`) to `nil`, or the function will return an error
|
||||
//
|
||||
// You may also want to look at how to pass protected headers to the
|
||||
// signing process, as you will likely be required to set the `b64` field
|
||||
// when using detached payload.
|
||||
//
|
||||
// Look for options that return `jws.SignOption` or `jws.SignVerifyOption`
|
||||
// for a complete list of options that can be passed to this function.
|
||||
//
|
||||
// You can use `errors.Is` with `jws.SignError()` to check if an error is from this function.
|
||||
func Sign(payload []byte, options ...SignOption) ([]byte, error) {
|
||||
sc := signContextPool.Get()
|
||||
defer signContextPool.Put(sc)
|
||||
|
||||
sc.payload = payload
|
||||
|
||||
if err := sc.ProcessOptions(options); err != nil {
|
||||
return nil, signerr(`failed to process options: %w`, err)
|
||||
}
|
||||
|
||||
lsigner := len(sc.sigbuilders)
|
||||
if lsigner == 0 {
|
||||
return nil, signerr(`no signers available. Specify an algorithm and a key using jws.WithKey()`)
|
||||
}
|
||||
|
||||
// Design note: while we could have easily set format = fmtJSON when
|
||||
// lsigner > 1, I believe the decision to change serialization formats
|
||||
// must be explicitly stated by the caller. Otherwise, I'm pretty sure
|
||||
// there would be people filing issues saying "I get JSON when I expected
|
||||
// compact serialization".
|
||||
//
|
||||
// Therefore, instead of making implicit format conversions, we force the
|
||||
// user to spell it out as `jws.Sign(..., jws.WithJSON(), jws.WithKey(...), jws.WithKey(...))`
|
||||
if sc.format == fmtCompact && lsigner != 1 {
|
||||
return nil, signerr(`cannot have multiple signers (keys) specified for compact serialization. Use only one jws.WithKey()`)
|
||||
}
|
||||
|
||||
// Create a Message object with all the bits and bobs, and we'll
|
||||
// serialize it in the end
|
||||
var result Message
|
||||
|
||||
if err := sc.PopulateMessage(&result); err != nil {
|
||||
return nil, signerr(`failed to populate message: %w`, err)
|
||||
}
|
||||
switch sc.format {
|
||||
case fmtJSON:
|
||||
return json.Marshal(result)
|
||||
case fmtJSONPretty:
|
||||
return json.MarshalIndent(result, "", " ")
|
||||
case fmtCompact:
|
||||
// Take the only signature object, and convert it into a Compact
|
||||
// serialization format
|
||||
var compactOpts []CompactOption
|
||||
if sc.detached {
|
||||
compactOpts = append(compactOpts, WithDetached(true))
|
||||
}
|
||||
for _, option := range options {
|
||||
if copt, ok := option.(CompactOption); ok {
|
||||
compactOpts = append(compactOpts, copt)
|
||||
}
|
||||
}
|
||||
return Compact(&result, compactOpts...)
|
||||
default:
|
||||
return nil, signerr(`invalid serialization format`)
|
||||
}
|
||||
}
|
||||
|
||||
var allowNoneWhitelist = jwk.WhitelistFunc(func(string) bool {
|
||||
return false
|
||||
})
|
||||
|
||||
// Verify checks if the given JWS message is verifiable using `alg` and `key`.
|
||||
// `key` may be a "raw" key (e.g. rsa.PublicKey) or a jwk.Key
|
||||
//
|
||||
// If the verification is successful, `err` is nil, and the content of the
|
||||
// payload that was signed is returned. If you need more fine-grained
|
||||
// control of the verification process, manually generate a
|
||||
// `Verifier` in `verify` subpackage, and call `Verify` method on it.
|
||||
// If you need to access signatures and JOSE headers in a JWS message,
|
||||
// use `Parse` function to get `Message` object.
|
||||
//
|
||||
// Because the use of "none" (jwa.NoSignature) algorithm is strongly discouraged,
|
||||
// this function DOES NOT consider it a success when `{"alg":"none"}` is
|
||||
// encountered in the message (it would also be counterintuitive when the code says
|
||||
// it _verified_ something when in fact it did no such thing). If you want to
|
||||
// accept messages with "none" signature algorithm, use `jws.Parse` to get the
|
||||
// raw JWS message.
|
||||
//
|
||||
// The error returned by this function is of type can be checked against
|
||||
// `jws.VerifyError()` and `jws.VerificationError()`. The latter is returned
|
||||
// when the verification process itself fails (e.g. invalid signature, wrong key),
|
||||
// while the former is returned when any other part of the `jws.Verify()`
|
||||
// function fails.
|
||||
func Verify(buf []byte, options ...VerifyOption) ([]byte, error) {
|
||||
vc := verifyContextPool.Get()
|
||||
defer verifyContextPool.Put(vc)
|
||||
|
||||
if err := vc.ProcessOptions(options); err != nil {
|
||||
return nil, verifyerr(`failed to process options: %w`, err)
|
||||
}
|
||||
|
||||
return vc.VerifyMessage(buf)
|
||||
}
|
||||
|
||||
// get the value of b64 header field.
|
||||
// If the field does not exist, returns true (default)
|
||||
// Otherwise return the value specified by the header field.
|
||||
func getB64Value(hdr Headers) bool {
|
||||
var b64 bool
|
||||
if err := hdr.Get("b64", &b64); err != nil {
|
||||
return true // default
|
||||
}
|
||||
|
||||
return b64
|
||||
}
|
||||
|
||||
// Parse parses contents from the given source and creates a jws.Message
|
||||
// struct. By default the input can be in either compact or full JSON serialization.
|
||||
//
|
||||
// You may pass `jws.WithJSON()` and/or `jws.WithCompact()` to specify
|
||||
// explicitly which format to use. If neither or both is specified, the function
|
||||
// will attempt to autodetect the format. If one or the other is specified,
|
||||
// only the specified format will be attempted.
|
||||
//
|
||||
// On error, returns a jws.ParseError.
|
||||
func Parse(src []byte, options ...ParseOption) (*Message, error) {
|
||||
var formats int
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identSerialization{}:
|
||||
var v int
|
||||
if err := option.Value(&v); err != nil {
|
||||
return nil, parseerr(`failed to retrieve serialization option value: %w`, err)
|
||||
}
|
||||
switch v {
|
||||
case fmtJSON:
|
||||
formats |= fmtJSON
|
||||
case fmtCompact:
|
||||
formats |= fmtCompact
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if format is 0 or both JSON/Compact, auto detect
|
||||
if v := formats & (fmtJSON | fmtCompact); v == 0 || v == fmtJSON|fmtCompact {
|
||||
CHECKLOOP:
|
||||
for i := range src {
|
||||
r := rune(src[i])
|
||||
if r >= utf8.RuneSelf {
|
||||
r, _ = utf8.DecodeRune(src)
|
||||
}
|
||||
if !unicode.IsSpace(r) {
|
||||
if r == tokens.OpenCurlyBracket {
|
||||
formats = fmtJSON
|
||||
} else {
|
||||
formats = fmtCompact
|
||||
}
|
||||
break CHECKLOOP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if formats&fmtCompact == fmtCompact {
|
||||
msg, err := parseCompact(src)
|
||||
if err != nil {
|
||||
return nil, parseerr(`failed to parse compact format: %w`, err)
|
||||
}
|
||||
return msg, nil
|
||||
} else if formats&fmtJSON == fmtJSON {
|
||||
msg, err := parseJSON(src)
|
||||
if err != nil {
|
||||
return nil, parseerr(`failed to parse JSON format: %w`, err)
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
return nil, parseerr(`invalid byte sequence`)
|
||||
}
|
||||
|
||||
// ParseString parses contents from the given source and creates a jws.Message
|
||||
// struct. The input can be in either compact or full JSON serialization.
|
||||
//
|
||||
// On error, returns a jws.ParseError.
|
||||
func ParseString(src string) (*Message, error) {
|
||||
msg, err := Parse([]byte(src))
|
||||
if err != nil {
|
||||
return nil, sparseerr(`failed to parse string: %w`, err)
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// ParseReader parses contents from the given source and creates a jws.Message
|
||||
// struct. The input can be in either compact or full JSON serialization.
|
||||
//
|
||||
// On error, returns a jws.ParseError.
|
||||
func ParseReader(src io.Reader) (*Message, error) {
|
||||
data, err := jwxio.ReadAllFromFiniteSource(src)
|
||||
if err == nil {
|
||||
return Parse(data)
|
||||
}
|
||||
|
||||
if !errors.Is(err, jwxio.NonFiniteSourceError()) {
|
||||
return nil, rparseerr(`failed to read from finite source: %w`, err)
|
||||
}
|
||||
|
||||
rdr := bufio.NewReader(src)
|
||||
var first rune
|
||||
for {
|
||||
r, _, err := rdr.ReadRune()
|
||||
if err != nil {
|
||||
return nil, rparseerr(`failed to read rune: %w`, err)
|
||||
}
|
||||
if !unicode.IsSpace(r) {
|
||||
first = r
|
||||
if err := rdr.UnreadRune(); err != nil {
|
||||
return nil, rparseerr(`failed to unread rune: %w`, err)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var parser func(io.Reader) (*Message, error)
|
||||
if first == tokens.OpenCurlyBracket {
|
||||
parser = parseJSONReader
|
||||
} else {
|
||||
parser = parseCompactReader
|
||||
}
|
||||
|
||||
m, err := parser(rdr)
|
||||
if err != nil {
|
||||
return nil, rparseerr(`failed to parse reader: %w`, err)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func parseJSONReader(src io.Reader) (result *Message, err error) {
|
||||
var m Message
|
||||
if err := json.NewDecoder(src).Decode(&m); err != nil {
|
||||
return nil, fmt.Errorf(`failed to unmarshal jws message: %w`, err)
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func parseJSON(data []byte) (result *Message, err error) {
|
||||
var m Message
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return nil, fmt.Errorf(`failed to unmarshal jws message: %w`, err)
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// SplitCompact splits a JWS in compact format and returns its three parts
|
||||
// separately: protected headers, payload and signature.
|
||||
// On error, returns a jws.ParseError.
|
||||
//
|
||||
// This function will be deprecated in v4. It is a low-level API, and
|
||||
// thus will be available in the `jwsbb` package.
|
||||
func SplitCompact(src []byte) ([]byte, []byte, []byte, error) {
|
||||
hdr, payload, signature, err := jwsbb.SplitCompact(src)
|
||||
if err != nil {
|
||||
return nil, nil, nil, parseerr(`%w`, err)
|
||||
}
|
||||
return hdr, payload, signature, nil
|
||||
}
|
||||
|
||||
// SplitCompactString splits a JWT and returns its three parts
|
||||
// separately: protected headers, payload and signature.
|
||||
// On error, returns a jws.ParseError.
|
||||
//
|
||||
// This function will be deprecated in v4. It is a low-level API, and
|
||||
// thus will be available in the `jwsbb` package.
|
||||
func SplitCompactString(src string) ([]byte, []byte, []byte, error) {
|
||||
hdr, payload, signature, err := jwsbb.SplitCompactString(src)
|
||||
if err != nil {
|
||||
return nil, nil, nil, parseerr(`%w`, err)
|
||||
}
|
||||
return hdr, payload, signature, nil
|
||||
}
|
||||
|
||||
// SplitCompactReader splits a JWT and returns its three parts
|
||||
// separately: protected headers, payload and signature.
|
||||
// On error, returns a jws.ParseError.
|
||||
//
|
||||
// This function will be deprecated in v4. It is a low-level API, and
|
||||
// thus will be available in the `jwsbb` package.
|
||||
func SplitCompactReader(rdr io.Reader) ([]byte, []byte, []byte, error) {
|
||||
hdr, payload, signature, err := jwsbb.SplitCompactReader(rdr)
|
||||
if err != nil {
|
||||
return nil, nil, nil, parseerr(`%w`, err)
|
||||
}
|
||||
return hdr, payload, signature, nil
|
||||
}
|
||||
|
||||
// parseCompactReader parses a JWS value serialized via compact serialization.
|
||||
func parseCompactReader(rdr io.Reader) (m *Message, err error) {
|
||||
protected, payload, signature, err := SplitCompactReader(rdr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`invalid compact serialization format: %w`, err)
|
||||
}
|
||||
return parse(protected, payload, signature)
|
||||
}
|
||||
|
||||
func parseCompact(data []byte) (m *Message, err error) {
|
||||
protected, payload, signature, err := SplitCompact(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`invalid compact serialization format: %w`, err)
|
||||
}
|
||||
return parse(protected, payload, signature)
|
||||
}
|
||||
|
||||
func parse(protected, payload, signature []byte) (*Message, error) {
|
||||
decodedHeader, err := base64.Decode(protected)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to decode protected headers: %w`, err)
|
||||
}
|
||||
|
||||
hdr := NewHeaders()
|
||||
if err := json.Unmarshal(decodedHeader, hdr); err != nil {
|
||||
return nil, fmt.Errorf(`failed to parse JOSE headers: %w`, err)
|
||||
}
|
||||
|
||||
var decodedPayload []byte
|
||||
b64 := getB64Value(hdr)
|
||||
if !b64 {
|
||||
decodedPayload = payload
|
||||
} else {
|
||||
v, err := base64.Decode(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to decode payload: %w`, err)
|
||||
}
|
||||
decodedPayload = v
|
||||
}
|
||||
|
||||
decodedSignature, err := base64.Decode(signature)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to decode signature: %w`, err)
|
||||
}
|
||||
|
||||
var msg Message
|
||||
msg.payload = decodedPayload
|
||||
msg.signatures = append(msg.signatures, &Signature{
|
||||
protected: hdr,
|
||||
signature: decodedSignature,
|
||||
})
|
||||
msg.b64 = b64
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
type CustomDecoder = json.CustomDecoder
|
||||
type CustomDecodeFunc = json.CustomDecodeFunc
|
||||
|
||||
// RegisterCustomField allows users to specify that a private field
|
||||
// be decoded as an instance of the specified type. This option has
|
||||
// a global effect.
|
||||
//
|
||||
// For example, suppose you have a custom field `x-birthday`, which
|
||||
// you want to represent as a string formatted in RFC3339 in JSON,
|
||||
// but want it back as `time.Time`.
|
||||
//
|
||||
// In such case you would register a custom field as follows
|
||||
//
|
||||
// jws.RegisterCustomField(`x-birthday`, time.Time{})
|
||||
//
|
||||
// Then you can use a `time.Time` variable to extract the value
|
||||
// of `x-birthday` field, instead of having to use `any`
|
||||
// and later convert it to `time.Time`
|
||||
//
|
||||
// var bday time.Time
|
||||
// _ = hdr.Get(`x-birthday`, &bday)
|
||||
//
|
||||
// If you need a more fine-tuned control over the decoding process,
|
||||
// you can register a `CustomDecoder`. For example, below shows
|
||||
// how to register a decoder that can parse RFC1123 format string:
|
||||
//
|
||||
// jws.RegisterCustomField(`x-birthday`, jws.CustomDecodeFunc(func(data []byte) (any, error) {
|
||||
// return time.Parse(time.RFC1123, string(data))
|
||||
// }))
|
||||
//
|
||||
// Please note that use of custom fields can be problematic if you
|
||||
// are using a library that does not implement MarshalJSON/UnmarshalJSON
|
||||
// and you try to roundtrip from an object to JSON, and then back to an object.
|
||||
// For example, in the above example, you can _parse_ time values formatted
|
||||
// in the format specified in RFC822, but when you convert an object into
|
||||
// JSON, it will be formatted in RFC3339, because that's what `time.Time`
|
||||
// likes to do. To avoid this, it's always better to use a custom type
|
||||
// that wraps your desired type (in this case `time.Time`) and implement
|
||||
// MarshalJSON and UnmashalJSON.
|
||||
func RegisterCustomField(name string, object any) {
|
||||
registry.Register(name, object)
|
||||
}
|
||||
|
||||
// Helpers for signature verification
|
||||
var rawKeyToKeyType = make(map[reflect.Type]jwa.KeyType)
|
||||
var keyTypeToAlgorithms = make(map[jwa.KeyType][]jwa.SignatureAlgorithm)
|
||||
|
||||
func init() {
|
||||
rawKeyToKeyType[reflect.TypeFor[[]byte]()] = jwa.OctetSeq()
|
||||
rawKeyToKeyType[reflect.TypeFor[ed25519.PublicKey]()] = jwa.OKP()
|
||||
rawKeyToKeyType[reflect.TypeFor[rsa.PublicKey]()] = jwa.RSA()
|
||||
rawKeyToKeyType[reflect.TypeFor[*rsa.PublicKey]()] = jwa.RSA()
|
||||
rawKeyToKeyType[reflect.TypeFor[ecdsa.PublicKey]()] = jwa.EC()
|
||||
rawKeyToKeyType[reflect.TypeFor[*ecdsa.PublicKey]()] = jwa.EC()
|
||||
|
||||
addAlgorithmForKeyType(jwa.OKP(), jwa.EdDSA())
|
||||
for _, alg := range []jwa.SignatureAlgorithm{jwa.HS256(), jwa.HS384(), jwa.HS512()} {
|
||||
addAlgorithmForKeyType(jwa.OctetSeq(), alg)
|
||||
}
|
||||
for _, alg := range []jwa.SignatureAlgorithm{jwa.RS256(), jwa.RS384(), jwa.RS512(), jwa.PS256(), jwa.PS384(), jwa.PS512()} {
|
||||
addAlgorithmForKeyType(jwa.RSA(), alg)
|
||||
}
|
||||
for _, alg := range []jwa.SignatureAlgorithm{jwa.ES256(), jwa.ES384(), jwa.ES512()} {
|
||||
addAlgorithmForKeyType(jwa.EC(), alg)
|
||||
}
|
||||
}
|
||||
|
||||
func addAlgorithmForKeyType(kty jwa.KeyType, alg jwa.SignatureAlgorithm) {
|
||||
keyTypeToAlgorithms[kty] = append(keyTypeToAlgorithms[kty], alg)
|
||||
}
|
||||
|
||||
// AlgorithmsForKey returns the possible signature algorithms that can
|
||||
// be used for a given key. It only takes in consideration keys/algorithms
|
||||
// for verification purposes, as this is the only usage where one may need
|
||||
// dynamically figure out which method to use.
|
||||
func AlgorithmsForKey(key any) ([]jwa.SignatureAlgorithm, error) {
|
||||
var kty jwa.KeyType
|
||||
switch key := key.(type) {
|
||||
case jwk.Key:
|
||||
kty = key.KeyType()
|
||||
case rsa.PublicKey, *rsa.PublicKey, rsa.PrivateKey, *rsa.PrivateKey:
|
||||
kty = jwa.RSA()
|
||||
case ecdsa.PublicKey, *ecdsa.PublicKey, ecdsa.PrivateKey, *ecdsa.PrivateKey:
|
||||
kty = jwa.EC()
|
||||
case ed25519.PublicKey, ed25519.PrivateKey, *ecdh.PublicKey, ecdh.PublicKey, *ecdh.PrivateKey, ecdh.PrivateKey:
|
||||
kty = jwa.OKP()
|
||||
case []byte:
|
||||
kty = jwa.OctetSeq()
|
||||
default:
|
||||
return nil, fmt.Errorf(`unknown key type %T`, key)
|
||||
}
|
||||
|
||||
algs, ok := keyTypeToAlgorithms[kty]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`unregistered key type %q`, kty)
|
||||
}
|
||||
return algs, nil
|
||||
}
|
||||
|
||||
// Settings allows you to set global settings for this JWS operations.
|
||||
//
|
||||
// Currently, the only setting available is `jws.WithLegacySigners()`,
|
||||
// which for various reason is now a no-op.
|
||||
func Settings(options ...GlobalOption) {
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identLegacySigners{}:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyCompactFast is a fast path verification function for JWS messages
|
||||
// in compact serialization format.
|
||||
//
|
||||
// This function is considered experimental, and may change or be removed
|
||||
// in the future.
|
||||
//
|
||||
// VerifyCompactFast performs signature verification on a JWS compact
|
||||
// serialization without fully parsing the message into a jws.Message object.
|
||||
// This makes it more efficient for cases where you only need to verify
|
||||
// the signature and extract the payload, without needing access to headers
|
||||
// or other JWS metadata.
|
||||
//
|
||||
// Returns the original payload that was signed if verification succeeds.
|
||||
//
|
||||
// Unlike jws.Verify(), this function requires you to specify the
|
||||
// algorithm explicitly rather than extracting it from the JWS headers.
|
||||
// This can be useful for performance-critical applications where the
|
||||
// algorithm is known in advance.
|
||||
//
|
||||
// Since this function avoids doing many checks that jws.Verify would perform,
|
||||
// you must ensure to perform the necessary checks including ensuring that algorithm is safe to use for your payload yourself.
|
||||
func VerifyCompactFast(key any, compact []byte, alg jwa.SignatureAlgorithm) ([]byte, error) {
|
||||
algstr := alg.String()
|
||||
|
||||
// Split the serialized JWT into its components
|
||||
hdr, payload, encodedSig, err := jwsbb.SplitCompact(compact)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("jwt.verifyFast: failed to split compact: %w", err)
|
||||
}
|
||||
|
||||
signature, err := base64.Decode(encodedSig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("jwt.verifyFast: failed to decode signature: %w", err)
|
||||
}
|
||||
|
||||
// Instead of appending, copy the data from hdr/payload
|
||||
lvb := len(hdr) + 1 + len(payload)
|
||||
verifyBuf := pool.ByteSlice().GetCapacity(lvb)
|
||||
verifyBuf = verifyBuf[:lvb]
|
||||
copy(verifyBuf, hdr)
|
||||
verifyBuf[len(hdr)] = tokens.Period
|
||||
copy(verifyBuf[len(hdr)+1:], payload)
|
||||
defer pool.ByteSlice().Put(verifyBuf)
|
||||
|
||||
// Verify the signature
|
||||
if verifier2, err := VerifierFor(alg); err == nil {
|
||||
if err := verifier2.Verify(key, verifyBuf, signature); err != nil {
|
||||
return nil, verifyError{verificationError{fmt.Errorf("jwt.VerifyCompact: signature verification failed for %s: %w", algstr, err)}}
|
||||
}
|
||||
} else {
|
||||
legacyVerifier, err := NewVerifier(alg)
|
||||
if err != nil {
|
||||
return nil, verifyerr("jwt.VerifyCompact: failed to create verifier for %s: %w", algstr, err)
|
||||
}
|
||||
if err := legacyVerifier.Verify(verifyBuf, signature, key); err != nil {
|
||||
return nil, verifyError{verificationError{fmt.Errorf("jwt.VerifyCompact: signature verification failed for %s: %w", algstr, err)}}
|
||||
}
|
||||
}
|
||||
|
||||
decoded, err := base64.Decode(payload)
|
||||
if err != nil {
|
||||
return nil, verifyerr("jwt.VerifyCompact: failed to decode payload: %w", err)
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
load("@rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "jwsbb",
|
||||
srcs = [
|
||||
"crypto_signer.go",
|
||||
"ecdsa.go",
|
||||
"eddsa.go",
|
||||
"format.go",
|
||||
"hmac.go",
|
||||
"jwsbb.go",
|
||||
"rsa.go",
|
||||
"sign.go",
|
||||
"verify.go",
|
||||
],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/jws/jwsbb",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//internal/base64",
|
||||
"//internal/ecutil",
|
||||
"//internal/jwxio",
|
||||
"//internal/keyconv",
|
||||
"//internal/pool",
|
||||
"//internal/tokens",
|
||||
"//jws/internal/keytype",
|
||||
"@com_github_lestrrat_go_dsig//:dsig",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "jwsbb_test",
|
||||
srcs = ["jwsbb_test.go"],
|
||||
embed = [":jwsbb"],
|
||||
deps = [
|
||||
"//internal/base64",
|
||||
"@com_github_stretchr_testify//require",
|
||||
],
|
||||
)
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package jwsbb
|
||||
|
||||
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("jwsbb.SignCryptoSignerRaw: signer is nil")
|
||||
}
|
||||
return cryptosign(signer, raw, h, opts, rr)
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
package jwsbb
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"encoding/asn1"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
|
||||
"github.com/lestrrat-go/dsig"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/ecutil"
|
||||
)
|
||||
|
||||
// ecdsaHashToDsigAlgorithm maps ECDSA hash functions to dsig algorithm constants
|
||||
func ecdsaHashToDsigAlgorithm(h crypto.Hash) (string, error) {
|
||||
switch h {
|
||||
case crypto.SHA256:
|
||||
return dsig.ECDSAWithP256AndSHA256, nil
|
||||
case crypto.SHA384:
|
||||
return dsig.ECDSAWithP384AndSHA384, nil
|
||||
case crypto.SHA512:
|
||||
return dsig.ECDSAWithP521AndSHA512, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported ECDSA hash function: %v", h)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// This function is now a thin wrapper around dsig.SignECDSA. For new projects, you should
|
||||
// consider using dsig instead of this function.
|
||||
func SignECDSA(key *ecdsa.PrivateKey, payload []byte, h crypto.Hash, rr io.Reader) ([]byte, error) {
|
||||
dsigAlg, err := ecdsaHashToDsigAlgorithm(h)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("jwsbb.SignECDSA: %w", err)
|
||||
}
|
||||
|
||||
return dsig.Sign(key, dsigAlg, payload, rr)
|
||||
}
|
||||
|
||||
// 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 fmt.Errorf("jwsbb.ECDSAVerifier: 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).
|
||||
//
|
||||
// This function is now a thin wrapper around dsig.VerifyECDSA. For new projects, you should
|
||||
// consider using dsig instead of this function.
|
||||
func VerifyECDSA(key *ecdsa.PublicKey, payload, signature []byte, h crypto.Hash) error {
|
||||
dsigAlg, err := ecdsaHashToDsigAlgorithm(h)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jwsbb.VerifyECDSA: %w", err)
|
||||
}
|
||||
|
||||
return dsig.Verify(key, dsigAlg, payload, signature)
|
||||
}
|
||||
|
||||
// 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(`jwsbb.VerifyECDSACryptoSigner: expected *ecdsa.PublicKey, got %T`, cpub)
|
||||
}
|
||||
|
||||
var r, s big.Int
|
||||
if err := UnpackECDSASignature(signature, pubkey, &r, &s); err != nil {
|
||||
return fmt.Errorf("jwsbb.ECDSAVerifier: failed to unpack ASN.1 encoded ECDSA signature: %w", err)
|
||||
}
|
||||
|
||||
return ecdsaVerify(pubkey, payload, h, &r, &s)
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package jwsbb
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
|
||||
"github.com/lestrrat-go/dsig"
|
||||
)
|
||||
|
||||
// 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.
|
||||
//
|
||||
// This function is now a thin wrapper around dsig.SignEdDSA. For new projects, you should
|
||||
// consider using dsig instead of this function.
|
||||
func SignEdDSA(key ed25519.PrivateKey, payload []byte) ([]byte, error) {
|
||||
// Use dsig.Sign with EdDSA algorithm constant
|
||||
return dsig.Sign(key, dsig.EdDSA, 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.
|
||||
//
|
||||
// This function is now a thin wrapper around dsig.VerifyEdDSA. For new projects, you should
|
||||
// consider using dsig instead of this function.
|
||||
func VerifyEdDSA(key ed25519.PublicKey, payload, signature []byte) error {
|
||||
// Use dsig.Verify with EdDSA algorithm constant
|
||||
return dsig.Verify(key, dsig.EdDSA, payload, signature)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
//go:build jwx_es256k
|
||||
|
||||
package jwsbb
|
||||
|
||||
import (
|
||||
dsigsecp256k1 "github.com/lestrrat-go/dsig-secp256k1"
|
||||
)
|
||||
|
||||
const es256k = "ES256K"
|
||||
|
||||
func init() {
|
||||
// Add ES256K mapping when this build tag is enabled
|
||||
jwsToDsigAlgorithm[es256k] = dsigsecp256k1.ECDSAWithSecp256k1AndSHA256
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
package jwsbb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/base64"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/jwxio"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/tokens"
|
||||
)
|
||||
|
||||
// SignBuffer combines the base64-encoded header and payload into a single byte slice
|
||||
// for signing purposes. This creates the signing input according to JWS specification (RFC 7515).
|
||||
// The result should be passed to signature generation functions.
|
||||
//
|
||||
// Parameters:
|
||||
// - buf: Reusable buffer (can be nil for automatic allocation)
|
||||
// - hdr: Raw header bytes (will be base64-encoded)
|
||||
// - payload: Raw payload bytes (encoded based on encodePayload flag)
|
||||
// - encoder: Base64 encoder to use for encoding components
|
||||
// - encodePayload: If true, payload is base64-encoded; if false, payload is used as-is
|
||||
//
|
||||
// Returns the constructed signing input in the format: base64(header).base64(payload) or base64(header).payload
|
||||
func SignBuffer(buf, hdr, payload []byte, encoder base64.Encoder, encodePayload bool) []byte {
|
||||
l := encoder.EncodedLen(len(hdr)+len(payload)) + 1
|
||||
if cap(buf) < l {
|
||||
buf = make([]byte, 0, l)
|
||||
}
|
||||
buf = buf[:0]
|
||||
buf = encoder.AppendEncode(buf, hdr)
|
||||
buf = append(buf, tokens.Period)
|
||||
if encodePayload {
|
||||
buf = encoder.AppendEncode(buf, payload)
|
||||
} else {
|
||||
buf = append(buf, payload...)
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// AppendSignature appends a base64-encoded signature to a JWS signing input buffer.
|
||||
// This completes the compact JWS serialization by adding the final signature component.
|
||||
// The input buffer should contain the signing input (header.payload), and this function
|
||||
// adds the period separator and base64-encoded signature.
|
||||
//
|
||||
// Parameters:
|
||||
// - buf: Buffer containing the signing input (typically from SignBuffer)
|
||||
// - signature: Raw signature bytes (will be base64-encoded)
|
||||
// - encoder: Base64 encoder to use for encoding the signature
|
||||
//
|
||||
// Returns the complete compact JWS in the format: base64(header).base64(payload).base64(signature)
|
||||
func AppendSignature(buf, signature []byte, encoder base64.Encoder) []byte {
|
||||
l := len(buf) + len(signature) + 1
|
||||
if cap(buf) < l {
|
||||
buf = make([]byte, 0, l)
|
||||
}
|
||||
buf = append(buf, tokens.Period)
|
||||
buf = encoder.AppendEncode(buf, signature)
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// JoinCompact creates a complete compact JWS serialization from individual components.
|
||||
// This is a one-step function that combines header, payload, and signature into the final JWS format.
|
||||
// It includes safety checks to prevent excessive memory allocation.
|
||||
//
|
||||
// Parameters:
|
||||
// - buf: Reusable buffer (can be nil for automatic allocation)
|
||||
// - hdr: Raw header bytes (will be base64-encoded)
|
||||
// - payload: Raw payload bytes (encoded based on encodePayload flag)
|
||||
// - signature: Raw signature bytes (will be base64-encoded)
|
||||
// - encoder: Base64 encoder to use for encoding all components
|
||||
// - encodePayload: If true, payload is base64-encoded; if false, payload is used as-is
|
||||
//
|
||||
// Returns the complete compact JWS or an error if the total size exceeds safety limits (1GB).
|
||||
func JoinCompact(buf, hdr, payload, signature []byte, encoder base64.Encoder, encodePayload bool) ([]byte, error) {
|
||||
const MaxBufferSize = 1 << 30 // 1 GB
|
||||
totalSize := len(hdr) + len(payload) + len(signature) + 2
|
||||
if totalSize > MaxBufferSize {
|
||||
return nil, errors.New("input sizes exceed maximum allowable buffer size")
|
||||
}
|
||||
if cap(buf) < totalSize {
|
||||
buf = make([]byte, 0, totalSize)
|
||||
}
|
||||
buf = buf[:0]
|
||||
buf = encoder.AppendEncode(buf, hdr)
|
||||
buf = append(buf, tokens.Period)
|
||||
if encodePayload {
|
||||
buf = encoder.AppendEncode(buf, payload)
|
||||
} else {
|
||||
buf = append(buf, payload...)
|
||||
}
|
||||
buf = append(buf, tokens.Period)
|
||||
buf = encoder.AppendEncode(buf, signature)
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
var compactDelim = []byte{tokens.Period}
|
||||
|
||||
var errInvalidNumberOfSegments = errors.New(`jwsbb: invalid number of segments`)
|
||||
|
||||
// InvalidNumberOfSegmentsError returns the standard error for invalid JWS segment count.
|
||||
// A valid compact JWS must have exactly 3 segments separated by periods: header.payload.signature
|
||||
func InvalidNumberOfSegmentsError() error {
|
||||
return errInvalidNumberOfSegments
|
||||
}
|
||||
|
||||
// SplitCompact parses a compact JWS serialization into its three components.
|
||||
// This function validates that the input has exactly 3 segments separated by periods
|
||||
// and returns the base64-encoded components without decoding them.
|
||||
//
|
||||
// Parameters:
|
||||
// - src: Complete compact JWS string as bytes
|
||||
//
|
||||
// Returns:
|
||||
// - protected: Base64-encoded protected header
|
||||
// - payload: Base64-encoded payload (or raw payload if b64=false was used)
|
||||
// - signature: Base64-encoded signature
|
||||
// - err: Error if the format is invalid or segment count is wrong
|
||||
func SplitCompact(src []byte) (protected, payload, signature []byte, err error) {
|
||||
var s []byte
|
||||
var ok bool
|
||||
|
||||
protected, s, ok = bytes.Cut(src, compactDelim)
|
||||
if !ok { // no period found
|
||||
return nil, nil, nil, InvalidNumberOfSegmentsError()
|
||||
}
|
||||
payload, s, ok = bytes.Cut(s, compactDelim)
|
||||
if !ok { // only one period found
|
||||
return nil, nil, nil, InvalidNumberOfSegmentsError()
|
||||
}
|
||||
signature, _, ok = bytes.Cut(s, compactDelim)
|
||||
if ok { // three periods found
|
||||
return nil, nil, nil, InvalidNumberOfSegmentsError()
|
||||
}
|
||||
return protected, payload, signature, nil
|
||||
}
|
||||
|
||||
// SplitCompactString is a convenience wrapper around SplitCompact for string inputs.
|
||||
// It converts the string to bytes and parses the compact JWS serialization.
|
||||
//
|
||||
// Parameters:
|
||||
// - src: Complete compact JWS as a string
|
||||
//
|
||||
// Returns the same components as SplitCompact: protected header, payload, signature, and error.
|
||||
func SplitCompactString(src string) (protected, payload, signature []byte, err error) {
|
||||
return SplitCompact([]byte(src))
|
||||
}
|
||||
|
||||
// SplitCompactReader parses a compact JWS serialization from an io.Reader.
|
||||
// This function handles both finite and streaming sources efficiently.
|
||||
// For finite sources, it reads all data at once. For streaming sources,
|
||||
// it uses a buffer-based approach to find segment boundaries.
|
||||
//
|
||||
// Parameters:
|
||||
// - rdr: Reader containing the compact JWS data
|
||||
//
|
||||
// Returns:
|
||||
// - protected: Base64-encoded protected header
|
||||
// - payload: Base64-encoded payload (or raw payload if b64=false was used)
|
||||
// - signature: Base64-encoded signature
|
||||
// - err: Error if reading fails or the format is invalid
|
||||
//
|
||||
// The function validates that exactly 3 segments are present, separated by periods.
|
||||
func SplitCompactReader(rdr io.Reader) (protected, payload, signature []byte, err error) {
|
||||
data, err := jwxio.ReadAllFromFiniteSource(rdr)
|
||||
if err == nil {
|
||||
return SplitCompact(data)
|
||||
}
|
||||
|
||||
if !errors.Is(err, jwxio.NonFiniteSourceError()) {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
var periods int
|
||||
var state int
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
var sofar []byte
|
||||
|
||||
for {
|
||||
// read next bytes
|
||||
n, err := rdr.Read(buf)
|
||||
// return on unexpected read error
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, nil, nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
// append to current buffer
|
||||
sofar = append(sofar, buf[:n]...)
|
||||
// loop to capture multiple tokens.Period in current buffer
|
||||
for loop := true; loop; {
|
||||
var i = bytes.IndexByte(sofar, tokens.Period)
|
||||
if i == -1 && err != io.EOF {
|
||||
// no tokens.Period found -> exit and read next bytes (outer loop)
|
||||
loop = false
|
||||
continue
|
||||
} else if i == -1 && err == io.EOF {
|
||||
// no tokens.Period found -> process rest and exit
|
||||
i = len(sofar)
|
||||
loop = false
|
||||
} else {
|
||||
// tokens.Period found
|
||||
periods++
|
||||
}
|
||||
|
||||
// Reaching this point means we have found a tokens.Period or EOF and process the rest of the buffer
|
||||
switch state {
|
||||
case 0:
|
||||
protected = sofar[:i]
|
||||
state++
|
||||
case 1:
|
||||
payload = sofar[:i]
|
||||
state++
|
||||
case 2:
|
||||
signature = sofar[:i]
|
||||
}
|
||||
// Shorten current buffer
|
||||
if len(sofar) > i {
|
||||
sofar = sofar[i+1:]
|
||||
}
|
||||
}
|
||||
// Exit on EOF
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
if periods != 2 {
|
||||
return nil, nil, nil, InvalidNumberOfSegmentsError()
|
||||
}
|
||||
|
||||
return protected, payload, signature, nil
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
package jwsbb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/base64"
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
type headerNotFoundError struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func (e headerNotFoundError) Error() string {
|
||||
return fmt.Sprintf(`jwsbb: header "%s" not found`, e.key)
|
||||
}
|
||||
|
||||
func (e headerNotFoundError) Is(target error) bool {
|
||||
switch target.(type) {
|
||||
case headerNotFoundError, *headerNotFoundError:
|
||||
// If the target is a headerNotFoundError or a pointer to it, we
|
||||
// consider it a match
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ErrHeaderNotFound returns an error that can be passed to `errors.Is` to check if the error is
|
||||
// the result of the field not being found
|
||||
func ErrHeaderNotFound() error {
|
||||
return headerNotFoundError{}
|
||||
}
|
||||
|
||||
// ErrFieldNotFound is an alias for ErrHeaderNotFound, and is deprecated. It was a misnomer.
|
||||
// It will be removed in a future release.
|
||||
func ErrFieldNotFound() error {
|
||||
return ErrHeaderNotFound()
|
||||
}
|
||||
|
||||
// Header is an object that allows you to access the JWS header in a quick and
|
||||
// dirty way. It does not verify anything, it does not know anything about what
|
||||
// each header field means, and it does not care about the JWS specification.
|
||||
// But when you need to access the JWS header for that one field that you
|
||||
// need, this is the object you want to use.
|
||||
//
|
||||
// As of this writing, HeaderParser cannot be used from concurrent goroutines.
|
||||
// You will need to create a new instance for each goroutine that needs to parse a JWS header.
|
||||
// Also, in general values obtained from this object should only be used
|
||||
// while the Header object is still in scope.
|
||||
//
|
||||
// This type is experimental and may change or be removed in the future.
|
||||
type Header interface {
|
||||
// I'm hiding this behind an interface so that users won't accidentally
|
||||
// rely on the underlying json handler implementation, nor the concrete
|
||||
// type name that jwsbb provides, as we may choose a different one in the future.
|
||||
jwsbbHeader()
|
||||
}
|
||||
|
||||
type header struct {
|
||||
v *fastjson.Value
|
||||
err error
|
||||
}
|
||||
|
||||
func (h *header) jwsbbHeader() {}
|
||||
|
||||
// HeaderParseCompact parses a JWS header from a compact serialization format.
|
||||
// You will need to call HeaderGet* functions to extract the values from the header.
|
||||
//
|
||||
// This function is experimental and may change or be removed in the future.
|
||||
func HeaderParseCompact(buf []byte) Header {
|
||||
decoded, err := base64.Decode(buf)
|
||||
if err != nil {
|
||||
return &header{err: err}
|
||||
}
|
||||
return HeaderParse(decoded)
|
||||
}
|
||||
|
||||
// HeaderParse parses a JWS header from a byte slice containing the decoded JSON.
|
||||
// You will need to call HeaderGet* functions to extract the values from the header.
|
||||
//
|
||||
// Unlike HeaderParseCompact, this function does not perform any base64 decoding.
|
||||
// This function is experimental and may change or be removed in the future.
|
||||
func HeaderParse(decoded []byte) Header {
|
||||
var p fastjson.Parser
|
||||
v, err := p.ParseBytes(decoded)
|
||||
if err != nil {
|
||||
return &header{err: err}
|
||||
}
|
||||
return &header{
|
||||
v: v,
|
||||
}
|
||||
}
|
||||
|
||||
func headerGet(h Header, key string) (*fastjson.Value, error) {
|
||||
//nolint:forcetypeassert
|
||||
hh := h.(*header) // we _know_ this can't be another type
|
||||
if hh.err != nil {
|
||||
return nil, hh.err
|
||||
}
|
||||
|
||||
v := hh.v.Get(key)
|
||||
if v == nil {
|
||||
return nil, headerNotFoundError{key: key}
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// HeaderGetString returns the string value for the given key from the JWS header.
|
||||
// An error is returned if the JSON was not valid, if the key does not exist,
|
||||
// or if the value is not a string.
|
||||
//
|
||||
// This function is experimental and may change or be removed in the future.
|
||||
func HeaderGetString(h Header, key string) (string, error) {
|
||||
v, err := headerGet(h, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sb, err := v.StringBytes()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(sb), nil
|
||||
}
|
||||
|
||||
// HeaderGetBool returns the boolean value for the given key from the JWS header.
|
||||
// An error is returned if the JSON was not valid, if the key does not exist,
|
||||
// or if the value is not a boolean.
|
||||
//
|
||||
// This function is experimental and may change or be removed in the future.
|
||||
func HeaderGetBool(h Header, key string) (bool, error) {
|
||||
v, err := headerGet(h, key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return v.Bool()
|
||||
}
|
||||
|
||||
// HeaderGetFloat64 returns the float64 value for the given key from the JWS header.
|
||||
// An error is returned if the JSON was not valid, if the key does not exist,
|
||||
// or if the value is not a float64.
|
||||
//
|
||||
// This function is experimental and may change or be removed in the future.
|
||||
func HeaderGetFloat64(h Header, key string) (float64, error) {
|
||||
v, err := headerGet(h, key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return v.Float64()
|
||||
}
|
||||
|
||||
// HeaderGetInt returns the int value for the given key from the JWS header.
|
||||
// An error is returned if the JSON was not valid, if the key does not exist,
|
||||
// or if the value is not an int.
|
||||
//
|
||||
// This function is experimental and may change or be removed in the future.
|
||||
func HeaderGetInt(h Header, key string) (int, error) {
|
||||
v, err := headerGet(h, key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return v.Int()
|
||||
}
|
||||
|
||||
// HeaderGetInt64 returns the int64 value for the given key from the JWS header.
|
||||
// An error is returned if the JSON was not valid, if the key does not exist,
|
||||
// or if the value is not an int64.
|
||||
//
|
||||
// This function is experimental and may change or be removed in the future.
|
||||
func HeaderGetInt64(h Header, key string) (int64, error) {
|
||||
v, err := headerGet(h, key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return v.Int64()
|
||||
}
|
||||
|
||||
// HeaderGetStringBytes returns the byte slice value for the given key from the JWS header.
|
||||
// An error is returned if the JSON was not valid, if the key does not exist,
|
||||
// or if the value is not a byte slice.
|
||||
//
|
||||
// Because of limitations of the underlying library, you cannot use the return value
|
||||
// of this function after the parser is garbage collected.
|
||||
//
|
||||
// This function is experimental and may change or be removed in the future.
|
||||
func HeaderGetStringBytes(h Header, key string) ([]byte, error) {
|
||||
v, err := headerGet(h, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v.StringBytes()
|
||||
}
|
||||
|
||||
// HeaderGetUint returns the uint value for the given key from the JWS header.
|
||||
// An error is returned if the JSON was not valid, if the key does not exist,
|
||||
// or if the value is not a uint.
|
||||
//
|
||||
// This function is experimental and may change or be removed in the future.
|
||||
func HeaderGetUint(h Header, key string) (uint, error) {
|
||||
v, err := headerGet(h, key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return v.Uint()
|
||||
}
|
||||
|
||||
// HeaderGetUint64 returns the uint64 value for the given key from the JWS header.
|
||||
// An error is returned if the JSON was not valid, if the key does not exist,
|
||||
// or if the value is not a uint64.
|
||||
//
|
||||
// This function is experimental and may change or be removed in the future.
|
||||
func HeaderGetUint64(h Header, key string) (uint64, error) {
|
||||
v, err := headerGet(h, key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return v.Uint64()
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package jwsbb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash"
|
||||
|
||||
"github.com/lestrrat-go/dsig"
|
||||
)
|
||||
|
||||
// hmacHashToDsigAlgorithm maps HMAC hash function sizes to dsig algorithm constants
|
||||
func hmacHashToDsigAlgorithm(hfunc func() hash.Hash) (string, error) {
|
||||
h := hfunc()
|
||||
switch h.Size() {
|
||||
case 32: // SHA256
|
||||
return dsig.HMACWithSHA256, nil
|
||||
case 48: // SHA384
|
||||
return dsig.HMACWithSHA384, nil
|
||||
case 64: // SHA512
|
||||
return dsig.HMACWithSHA512, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported HMAC hash function: size=%d", h.Size())
|
||||
}
|
||||
}
|
||||
|
||||
// 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).
|
||||
//
|
||||
// This function is now a thin wrapper around dsig.SignHMAC. For new projects, you should
|
||||
// consider using dsig instead of this function.
|
||||
func SignHMAC(key, payload []byte, hfunc func() hash.Hash) ([]byte, error) {
|
||||
dsigAlg, err := hmacHashToDsigAlgorithm(hfunc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("jwsbb.SignHMAC: %w", err)
|
||||
}
|
||||
|
||||
return dsig.Sign(key, dsigAlg, payload, 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).
|
||||
//
|
||||
// This function is now a thin wrapper around dsig.VerifyHMAC. For new projects, you should
|
||||
// consider using dsig instead of this function.
|
||||
func VerifyHMAC(key, payload, signature []byte, hfunc func() hash.Hash) error {
|
||||
dsigAlg, err := hmacHashToDsigAlgorithm(hfunc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jwsbb.VerifyHMAC: %w", err)
|
||||
}
|
||||
|
||||
return dsig.Verify(key, dsigAlg, payload, signature)
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
// Package jwsbb provides the building blocks (hence the name "bb") for JWS operations.
|
||||
// It should be thought of as a low-level API, almost akin to internal packages
|
||||
// that should not be used directly by users of the jwx package. However, these exist
|
||||
// to provide a more efficient way to perform JWS operations without the overhead of
|
||||
// the higher-level jws package to power-users who know what they are doing.
|
||||
//
|
||||
// This package is currently considered EXPERIMENTAL, and the API may change
|
||||
// without notice. It is not recommended to use this package unless you are
|
||||
// fully aware of the implications of using it.
|
||||
//
|
||||
// All bb packages in jwx follow the same 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 public jwx packages (they are standalone, except for internal packages).
|
||||
//
|
||||
// This implementation uses github.com/lestrrat-go/dsig as the underlying signature provider.
|
||||
package jwsbb
|
||||
|
||||
import (
|
||||
"github.com/lestrrat-go/dsig"
|
||||
)
|
||||
|
||||
// JWS algorithm name constants
|
||||
const (
|
||||
// HMAC algorithms
|
||||
hs256 = "HS256"
|
||||
hs384 = "HS384"
|
||||
hs512 = "HS512"
|
||||
|
||||
// RSA PKCS#1 v1.5 algorithms
|
||||
rs256 = "RS256"
|
||||
rs384 = "RS384"
|
||||
rs512 = "RS512"
|
||||
|
||||
// RSA PSS algorithms
|
||||
ps256 = "PS256"
|
||||
ps384 = "PS384"
|
||||
ps512 = "PS512"
|
||||
|
||||
// ECDSA algorithms
|
||||
es256 = "ES256"
|
||||
es384 = "ES384"
|
||||
es512 = "ES512"
|
||||
|
||||
// EdDSA algorithm
|
||||
edDSA = "EdDSA"
|
||||
)
|
||||
|
||||
// Signer is a generic interface that defines the method for signing payloads.
|
||||
// The type parameter K represents the key type (e.g., []byte for HMAC keys,
|
||||
// *rsa.PrivateKey for RSA keys, *ecdsa.PrivateKey for ECDSA keys).
|
||||
type Signer[K any] interface {
|
||||
Sign(key K, payload []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// Verifier is a generic interface that defines the method for verifying signatures.
|
||||
// The type parameter K represents the key type (e.g., []byte for HMAC keys,
|
||||
// *rsa.PublicKey for RSA keys, *ecdsa.PublicKey for ECDSA keys).
|
||||
type Verifier[K any] interface {
|
||||
Verify(key K, buf []byte, signature []byte) error
|
||||
}
|
||||
|
||||
// JWS to dsig algorithm mapping
|
||||
var jwsToDsigAlgorithm = map[string]string{
|
||||
// HMAC algorithms
|
||||
hs256: dsig.HMACWithSHA256,
|
||||
hs384: dsig.HMACWithSHA384,
|
||||
hs512: dsig.HMACWithSHA512,
|
||||
|
||||
// RSA PKCS#1 v1.5 algorithms
|
||||
rs256: dsig.RSAPKCS1v15WithSHA256,
|
||||
rs384: dsig.RSAPKCS1v15WithSHA384,
|
||||
rs512: dsig.RSAPKCS1v15WithSHA512,
|
||||
|
||||
// RSA PSS algorithms
|
||||
ps256: dsig.RSAPSSWithSHA256,
|
||||
ps384: dsig.RSAPSSWithSHA384,
|
||||
ps512: dsig.RSAPSSWithSHA512,
|
||||
|
||||
// ECDSA algorithms
|
||||
es256: dsig.ECDSAWithP256AndSHA256,
|
||||
es384: dsig.ECDSAWithP384AndSHA384,
|
||||
es512: dsig.ECDSAWithP521AndSHA512,
|
||||
// Note: ES256K requires external dependency and is handled separately
|
||||
|
||||
// EdDSA algorithm
|
||||
edDSA: dsig.EdDSA,
|
||||
}
|
||||
|
||||
// getDsigAlgorithm returns the dsig algorithm name for a JWS algorithm
|
||||
func getDsigAlgorithm(jwsAlg string) (string, bool) {
|
||||
dsigAlg, ok := jwsToDsigAlgorithm[jwsAlg]
|
||||
return dsigAlg, ok
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package jwsbb
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/lestrrat-go/dsig"
|
||||
)
|
||||
|
||||
// rsaHashToDsigAlgorithm maps RSA hash functions to dsig algorithm constants
|
||||
func rsaHashToDsigAlgorithm(h crypto.Hash, pss bool) (string, error) {
|
||||
if pss {
|
||||
switch h {
|
||||
case crypto.SHA256:
|
||||
return dsig.RSAPSSWithSHA256, nil
|
||||
case crypto.SHA384:
|
||||
return dsig.RSAPSSWithSHA384, nil
|
||||
case crypto.SHA512:
|
||||
return dsig.RSAPSSWithSHA512, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported hash algorithm for RSA-PSS: %v", h)
|
||||
}
|
||||
} else {
|
||||
switch h {
|
||||
case crypto.SHA256:
|
||||
return dsig.RSAPKCS1v15WithSHA256, nil
|
||||
case crypto.SHA384:
|
||||
return dsig.RSAPKCS1v15WithSHA384, nil
|
||||
case crypto.SHA512:
|
||||
return dsig.RSAPKCS1v15WithSHA512, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported hash algorithm for RSA PKCS#1 v1.5: %v", h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// This function is now a thin wrapper around dsig.SignRSA. For new projects, you should
|
||||
// consider using dsig instead of this function.
|
||||
func SignRSA(key *rsa.PrivateKey, payload []byte, h crypto.Hash, pss bool, rr io.Reader) ([]byte, error) {
|
||||
dsigAlg, err := rsaHashToDsigAlgorithm(h, pss)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("jwsbb.SignRSA: %w", err)
|
||||
}
|
||||
|
||||
return dsig.Sign(key, dsigAlg, payload, 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.
|
||||
//
|
||||
// This function is now a thin wrapper around dsig.VerifyRSA. For new projects, you should
|
||||
// consider using dsig instead of this function.
|
||||
func VerifyRSA(key *rsa.PublicKey, payload, signature []byte, h crypto.Hash, pss bool) error {
|
||||
dsigAlg, err := rsaHashToDsigAlgorithm(h, pss)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jwsbb.VerifyRSA: %w", err)
|
||||
}
|
||||
|
||||
return dsig.Verify(key, dsigAlg, payload, signature)
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package jwsbb
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/lestrrat-go/dsig"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/keyconv"
|
||||
)
|
||||
|
||||
// Sign generates a JWS signature using the specified key and algorithm.
|
||||
//
|
||||
// This function loads the signer registered in the jwsbb 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) {
|
||||
dsigAlg, ok := getDsigAlgorithm(alg)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`jwsbb.Sign: unsupported signature algorithm %q`, alg)
|
||||
}
|
||||
|
||||
// Get dsig algorithm info to determine key conversion strategy
|
||||
dsigInfo, ok := dsig.GetAlgorithmInfo(dsigAlg)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`jwsbb.Sign: dsig algorithm %q not registered`, dsigAlg)
|
||||
}
|
||||
|
||||
switch dsigInfo.Family {
|
||||
case dsig.HMAC:
|
||||
return dispatchHMACSign(key, dsigAlg, payload)
|
||||
case dsig.RSA:
|
||||
return dispatchRSASign(key, dsigAlg, payload, rr)
|
||||
case dsig.ECDSA:
|
||||
return dispatchECDSASign(key, dsigAlg, payload, rr)
|
||||
case dsig.EdDSAFamily:
|
||||
return dispatchEdDSASign(key, dsigAlg, payload, rr)
|
||||
default:
|
||||
return nil, fmt.Errorf(`jwsbb.Sign: unsupported dsig algorithm family %q`, dsigInfo.Family)
|
||||
}
|
||||
}
|
||||
|
||||
func dispatchHMACSign(key any, dsigAlg string, payload []byte) ([]byte, error) {
|
||||
var hmackey []byte
|
||||
if err := keyconv.ByteSliceKey(&hmackey, key); err != nil {
|
||||
return nil, fmt.Errorf(`jwsbb.Sign: invalid key type %T. []byte is required: %w`, key, err)
|
||||
}
|
||||
|
||||
return dsig.Sign(hmackey, dsigAlg, payload, nil)
|
||||
}
|
||||
|
||||
func dispatchRSASign(key any, dsigAlg string, payload []byte, rr io.Reader) ([]byte, error) {
|
||||
// Try crypto.Signer first (dsig can handle it directly)
|
||||
if signer, ok := key.(crypto.Signer); ok {
|
||||
// Verify it's an RSA key
|
||||
if _, ok := signer.Public().(*rsa.PublicKey); ok {
|
||||
return dsig.Sign(signer, dsigAlg, payload, rr)
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to concrete key types
|
||||
var privkey *rsa.PrivateKey
|
||||
if err := keyconv.RSAPrivateKey(&privkey, key); err != nil {
|
||||
return nil, fmt.Errorf(`jwsbb.Sign: invalid key type %T. *rsa.PrivateKey is required: %w`, key, err)
|
||||
}
|
||||
|
||||
return dsig.Sign(privkey, dsigAlg, payload, rr)
|
||||
}
|
||||
|
||||
func dispatchECDSASign(key any, dsigAlg string, payload []byte, rr io.Reader) ([]byte, error) {
|
||||
// Try crypto.Signer first (dsig can handle it directly)
|
||||
if signer, ok := key.(crypto.Signer); ok {
|
||||
// Verify it's an ECDSA key
|
||||
if _, ok := signer.Public().(*ecdsa.PublicKey); ok {
|
||||
return dsig.Sign(signer, dsigAlg, payload, rr)
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to concrete key types
|
||||
var privkey *ecdsa.PrivateKey
|
||||
if err := keyconv.ECDSAPrivateKey(&privkey, key); err != nil {
|
||||
return nil, fmt.Errorf(`jwsbb.Sign: invalid key type %T. *ecdsa.PrivateKey is required: %w`, key, err)
|
||||
}
|
||||
|
||||
return dsig.Sign(privkey, dsigAlg, payload, rr)
|
||||
}
|
||||
|
||||
func dispatchEdDSASign(key any, dsigAlg string, payload []byte, rr io.Reader) ([]byte, error) {
|
||||
// Try crypto.Signer first (dsig can handle it directly)
|
||||
if signer, ok := key.(crypto.Signer); ok {
|
||||
// Verify it's an EdDSA key
|
||||
if _, ok := signer.Public().(ed25519.PublicKey); ok {
|
||||
return dsig.Sign(signer, dsigAlg, payload, rr)
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to concrete key types
|
||||
var privkey ed25519.PrivateKey
|
||||
if err := keyconv.Ed25519PrivateKey(&privkey, key); err != nil {
|
||||
return nil, fmt.Errorf(`jwsbb.Sign: invalid key type %T. ed25519.PrivateKey is required: %w`, key, err)
|
||||
}
|
||||
|
||||
return dsig.Sign(privkey, dsigAlg, payload, rr)
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package jwsbb
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
|
||||
"github.com/lestrrat-go/dsig"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/keyconv"
|
||||
)
|
||||
|
||||
// Verify verifies a JWS signature using the specified key and algorithm.
|
||||
//
|
||||
// This function loads the verifier registered in the jwsbb package _ONLY_.
|
||||
// It does not support custom verifiers that the user might have registered.
|
||||
func Verify(key any, alg string, payload, signature []byte) error {
|
||||
dsigAlg, ok := getDsigAlgorithm(alg)
|
||||
if !ok {
|
||||
return fmt.Errorf(`jwsbb.Verify: unsupported signature algorithm %q`, alg)
|
||||
}
|
||||
|
||||
// Get dsig algorithm info to determine key conversion strategy
|
||||
dsigInfo, ok := dsig.GetAlgorithmInfo(dsigAlg)
|
||||
if !ok {
|
||||
return fmt.Errorf(`jwsbb.Verify: dsig algorithm %q not registered`, dsigAlg)
|
||||
}
|
||||
|
||||
switch dsigInfo.Family {
|
||||
case dsig.HMAC:
|
||||
return dispatchHMACVerify(key, dsigAlg, payload, signature)
|
||||
case dsig.RSA:
|
||||
return dispatchRSAVerify(key, dsigAlg, payload, signature)
|
||||
case dsig.ECDSA:
|
||||
return dispatchECDSAVerify(key, dsigAlg, payload, signature)
|
||||
case dsig.EdDSAFamily:
|
||||
return dispatchEdDSAVerify(key, dsigAlg, payload, signature)
|
||||
default:
|
||||
return fmt.Errorf(`jwsbb.Verify: unsupported dsig algorithm family %q`, dsigInfo.Family)
|
||||
}
|
||||
}
|
||||
|
||||
func dispatchHMACVerify(key any, dsigAlg string, payload, signature []byte) error {
|
||||
var hmackey []byte
|
||||
if err := keyconv.ByteSliceKey(&hmackey, key); err != nil {
|
||||
return fmt.Errorf(`jwsbb.Verify: invalid key type %T. []byte is required: %w`, key, err)
|
||||
}
|
||||
|
||||
return dsig.Verify(hmackey, dsigAlg, payload, signature)
|
||||
}
|
||||
|
||||
func dispatchRSAVerify(key any, dsigAlg string, payload, signature []byte) error {
|
||||
// Try crypto.Signer first (dsig can handle it directly)
|
||||
if signer, ok := key.(crypto.Signer); ok {
|
||||
// Verify it's an RSA key
|
||||
if _, ok := signer.Public().(*rsa.PublicKey); ok {
|
||||
return dsig.Verify(signer, dsigAlg, payload, signature)
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to concrete key types
|
||||
var pubkey *rsa.PublicKey
|
||||
if err := keyconv.RSAPublicKey(&pubkey, key); err != nil {
|
||||
return fmt.Errorf(`jwsbb.Verify: invalid key type %T. *rsa.PublicKey is required: %w`, key, err)
|
||||
}
|
||||
|
||||
return dsig.Verify(pubkey, dsigAlg, payload, signature)
|
||||
}
|
||||
|
||||
func dispatchECDSAVerify(key any, dsigAlg string, payload, signature []byte) error {
|
||||
// Try crypto.Signer first (dsig can handle it directly)
|
||||
if signer, ok := key.(crypto.Signer); ok {
|
||||
// Verify it's an ECDSA key
|
||||
if _, ok := signer.Public().(*ecdsa.PublicKey); ok {
|
||||
return dsig.Verify(signer, dsigAlg, payload, signature)
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to concrete key types
|
||||
var pubkey *ecdsa.PublicKey
|
||||
if err := keyconv.ECDSAPublicKey(&pubkey, key); err != nil {
|
||||
return fmt.Errorf(`jwsbb.Verify: invalid key type %T. *ecdsa.PublicKey is required: %w`, key, err)
|
||||
}
|
||||
|
||||
return dsig.Verify(pubkey, dsigAlg, payload, signature)
|
||||
}
|
||||
|
||||
func dispatchEdDSAVerify(key any, dsigAlg string, payload, signature []byte) error {
|
||||
// Try crypto.Signer first (dsig can handle it directly)
|
||||
if signer, ok := key.(crypto.Signer); ok {
|
||||
// Verify it's an EdDSA key
|
||||
if _, ok := signer.Public().(ed25519.PublicKey); ok {
|
||||
return dsig.Verify(signer, dsigAlg, payload, signature)
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to concrete key types
|
||||
var pubkey ed25519.PublicKey
|
||||
if err := keyconv.Ed25519PublicKey(&pubkey, key); err != nil {
|
||||
return fmt.Errorf(`jwsbb.Verify: invalid key type %T. ed25519.PublicKey is required: %w`, key, err)
|
||||
}
|
||||
|
||||
return dsig.Verify(pubkey, dsigAlg, payload, signature)
|
||||
}
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
package jws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
)
|
||||
|
||||
// KeyProvider is responsible for providing key(s) to sign or verify a payload.
|
||||
// Multiple `jws.KeyProvider`s can be passed to `jws.Verify()` or `jws.Sign()`
|
||||
//
|
||||
// `jws.Sign()` can only accept static key providers via `jws.WithKey()`,
|
||||
// while `jws.Verify()` can accept `jws.WithKey()`, `jws.WithKeySet()`,
|
||||
// `jws.WithVerifyAuto()`, and `jws.WithKeyProvider()`.
|
||||
//
|
||||
// Understanding how this works is crucial to learn how this package works.
|
||||
//
|
||||
// `jws.Sign()` is straightforward: signatures are created for each
|
||||
// provided key.
|
||||
//
|
||||
// `jws.Verify()` is a bit more involved, because there are cases you
|
||||
// will want to compute/deduce/guess the keys that you would like to
|
||||
// use for verification.
|
||||
//
|
||||
// The first thing that `jws.Verify()` does is to collect the
|
||||
// KeyProviders from the option list that the user provided (presented in pseudocode):
|
||||
//
|
||||
// keyProviders := filterKeyProviders(options)
|
||||
//
|
||||
// Then, remember that a JWS message may contain multiple signatures in the
|
||||
// message. For each signature, we call on the KeyProviders to give us
|
||||
// the key(s) to use on this signature:
|
||||
//
|
||||
// for sig in msg.Signatures {
|
||||
// for kp in keyProviders {
|
||||
// kp.FetchKeys(ctx, sink, sig, msg)
|
||||
// ...
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// The `sink` argument passed to the KeyProvider is a temporary storage
|
||||
// for the keys (either a jwk.Key or a "raw" key). The `KeyProvider`
|
||||
// is responsible for sending keys into the `sink`.
|
||||
//
|
||||
// When called, the `KeyProvider` created by `jws.WithKey()` sends the same key,
|
||||
// `jws.WithKeySet()` sends keys that matches a particular `kid` and `alg`,
|
||||
// `jws.WithVerifyAuto()` fetches a JWK from the `jku` URL,
|
||||
// and finally `jws.WithKeyProvider()` allows you to execute arbitrary
|
||||
// logic to provide keys. If you are providing a custom `KeyProvider`,
|
||||
// you should execute the necessary checks or retrieval of keys, and
|
||||
// then send the key(s) to the sink:
|
||||
//
|
||||
// sink.Key(alg, key)
|
||||
//
|
||||
// These keys are then retrieved and tried for each signature, until
|
||||
// a match is found:
|
||||
//
|
||||
// keys := sink.Keys()
|
||||
// for key in keys {
|
||||
// if givenSignature == makeSignature(key, payload, ...)) {
|
||||
// return OK
|
||||
// }
|
||||
// }
|
||||
type KeyProvider interface {
|
||||
FetchKeys(context.Context, KeySink, *Signature, *Message) error
|
||||
}
|
||||
|
||||
// KeySink is a data storage where `jws.KeyProvider` objects should
|
||||
// send their keys to.
|
||||
type KeySink interface {
|
||||
Key(jwa.SignatureAlgorithm, any)
|
||||
}
|
||||
|
||||
type algKeyPair struct {
|
||||
alg jwa.KeyAlgorithm
|
||||
key any
|
||||
}
|
||||
|
||||
type algKeySink struct {
|
||||
mu sync.Mutex
|
||||
list []algKeyPair
|
||||
}
|
||||
|
||||
func (s *algKeySink) Key(alg jwa.SignatureAlgorithm, key any) {
|
||||
s.mu.Lock()
|
||||
s.list = append(s.list, algKeyPair{alg, key})
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
type staticKeyProvider struct {
|
||||
alg jwa.SignatureAlgorithm
|
||||
key any
|
||||
}
|
||||
|
||||
func (kp *staticKeyProvider) FetchKeys(_ context.Context, sink KeySink, _ *Signature, _ *Message) error {
|
||||
sink.Key(kp.alg, kp.key)
|
||||
return nil
|
||||
}
|
||||
|
||||
type keySetProvider struct {
|
||||
set jwk.Set
|
||||
requireKid bool // true if `kid` must be specified
|
||||
useDefault bool // true if the first key should be used iff there's exactly one key in set
|
||||
inferAlgorithm bool // true if the algorithm should be inferred from key type
|
||||
multipleKeysPerKeyID bool // true if we should attempt to match multiple keys per key ID. if false we assume that only one key exists for a given key ID
|
||||
}
|
||||
|
||||
func (kp *keySetProvider) selectKey(sink KeySink, key jwk.Key, sig *Signature, _ *Message) error {
|
||||
if usage, ok := key.KeyUsage(); ok {
|
||||
// it's okay if use: "". we'll assume it's "sig"
|
||||
if usage != "" && usage != jwk.ForSignature.String() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if v, ok := key.Algorithm(); ok {
|
||||
salg, ok := jwa.LookupSignatureAlgorithm(v.String())
|
||||
if !ok {
|
||||
return fmt.Errorf(`invalid signature algorithm %q`, v)
|
||||
}
|
||||
|
||||
sink.Key(salg, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
if kp.inferAlgorithm {
|
||||
algs, err := AlgorithmsForKey(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to get a list of signature methods for key type %s: %w`, key.KeyType(), err)
|
||||
}
|
||||
|
||||
// bail out if the JWT has a `alg` field, and it doesn't match
|
||||
if tokAlg, ok := sig.ProtectedHeaders().Algorithm(); ok {
|
||||
for _, alg := range algs {
|
||||
if tokAlg == alg {
|
||||
sink.Key(alg, key)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf(`algorithm in the message does not match any of the inferred algorithms`)
|
||||
}
|
||||
|
||||
// Yes, you get to try them all!!!!!!!
|
||||
for _, alg := range algs {
|
||||
sink.Key(alg, key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kp *keySetProvider) FetchKeys(_ context.Context, sink KeySink, sig *Signature, msg *Message) error {
|
||||
if kp.requireKid {
|
||||
wantedKid, ok := sig.ProtectedHeaders().KeyID()
|
||||
if !ok {
|
||||
// If the kid is NOT specified... kp.useDefault needs to be true, and the
|
||||
// JWKs must have exactly one key in it
|
||||
if !kp.useDefault {
|
||||
return fmt.Errorf(`failed to find matching key: no key ID ("kid") specified in token`)
|
||||
} else if kp.useDefault && kp.set.Len() > 1 {
|
||||
return fmt.Errorf(`failed to find matching key: no key ID ("kid") specified in token but multiple keys available in key set`)
|
||||
}
|
||||
|
||||
// if we got here, then useDefault == true AND there is exactly
|
||||
// one key in the set.
|
||||
key, ok := kp.set.Key(0)
|
||||
if !ok {
|
||||
return fmt.Errorf(`failed to get key at index 0 (empty JWKS?)`)
|
||||
}
|
||||
return kp.selectKey(sink, key, sig, msg)
|
||||
}
|
||||
|
||||
// Otherwise we better be able to look up the key.
|
||||
// <= v2.0.3 backwards compatible case: only match a single key
|
||||
// whose key ID matches `wantedKid`
|
||||
if !kp.multipleKeysPerKeyID {
|
||||
key, ok := kp.set.LookupKeyID(wantedKid)
|
||||
if !ok {
|
||||
return fmt.Errorf(`failed to find key with key ID %q in key set`, wantedKid)
|
||||
}
|
||||
return kp.selectKey(sink, key, sig, msg)
|
||||
}
|
||||
|
||||
// if multipleKeysPerKeyID is true, we attempt all keys whose key ID matches
|
||||
// the wantedKey
|
||||
ok = false
|
||||
for i := range kp.set.Len() {
|
||||
key, _ := kp.set.Key(i)
|
||||
if kid, ok := key.KeyID(); !ok || kid != wantedKid {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := kp.selectKey(sink, key, sig, msg); err != nil {
|
||||
continue
|
||||
}
|
||||
ok = true
|
||||
// continue processing so that we try all keys with the same key ID
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf(`failed to find key with key ID %q in key set`, wantedKid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Otherwise just try all keys
|
||||
for i := range kp.set.Len() {
|
||||
key, ok := kp.set.Key(i)
|
||||
if !ok {
|
||||
return fmt.Errorf(`failed to get key at index %d`, i)
|
||||
}
|
||||
if err := kp.selectKey(sink, key, sig, msg); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type jkuProvider struct {
|
||||
fetcher jwk.Fetcher
|
||||
options []jwk.FetchOption
|
||||
}
|
||||
|
||||
func (kp jkuProvider) FetchKeys(ctx context.Context, sink KeySink, sig *Signature, _ *Message) error {
|
||||
if kp.fetcher == nil {
|
||||
kp.fetcher = jwk.FetchFunc(jwk.Fetch)
|
||||
}
|
||||
|
||||
kid, ok := sig.ProtectedHeaders().KeyID()
|
||||
if !ok {
|
||||
return fmt.Errorf(`use of "jku" requires that the payload contain a "kid" field in the protected header`)
|
||||
}
|
||||
|
||||
// errors here can't be reliably passed to the consumers.
|
||||
// it's unfortunate, but if you need this control, you are
|
||||
// going to have to write your own fetcher
|
||||
u, ok := sig.ProtectedHeaders().JWKSetURL()
|
||||
if !ok || u == "" {
|
||||
return fmt.Errorf(`use of "jku" field specified, but the field is empty`)
|
||||
}
|
||||
uo, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to parse "jku": %w`, err)
|
||||
}
|
||||
if uo.Scheme != "https" {
|
||||
return fmt.Errorf(`url in "jku" must be HTTPS`)
|
||||
}
|
||||
|
||||
set, err := kp.fetcher.Fetch(ctx, u, kp.options...)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to fetch %q: %w`, u, err)
|
||||
}
|
||||
|
||||
key, ok := set.LookupKeyID(kid)
|
||||
if !ok {
|
||||
// It is not an error if the key with the kid doesn't exist
|
||||
return nil
|
||||
}
|
||||
|
||||
algs, err := AlgorithmsForKey(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to get a list of signature methods for key type %s: %w`, key.KeyType(), err)
|
||||
}
|
||||
|
||||
hdrAlg, ok := sig.ProtectedHeaders().Algorithm()
|
||||
if ok {
|
||||
for _, alg := range algs {
|
||||
// if we have an "alg" field in the JWS, we can only proceed if
|
||||
// the inferred algorithm matches
|
||||
if hdrAlg != alg {
|
||||
continue
|
||||
}
|
||||
|
||||
sink.Key(alg, key)
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyProviderFunc is a type of KeyProvider that is implemented by
|
||||
// a single function. You can use this to create ad-hoc `KeyProvider`
|
||||
// instances.
|
||||
type KeyProviderFunc func(context.Context, KeySink, *Signature, *Message) error
|
||||
|
||||
func (kp KeyProviderFunc) FetchKeys(ctx context.Context, sink KeySink, sig *Signature, msg *Message) error {
|
||||
return kp(ctx, sink, sig, msg)
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package jws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jws/legacy"
|
||||
)
|
||||
|
||||
var enableLegacySignersOnce = &sync.Once{}
|
||||
|
||||
func enableLegacySigners() {
|
||||
for _, alg := range []jwa.SignatureAlgorithm{jwa.HS256(), jwa.HS384(), jwa.HS512()} {
|
||||
if err := RegisterSigner(alg, func(alg jwa.SignatureAlgorithm) SignerFactory {
|
||||
return SignerFactoryFn(func() (Signer, error) {
|
||||
return legacy.NewHMACSigner(alg), nil
|
||||
})
|
||||
}(alg)); err != nil {
|
||||
panic(fmt.Sprintf("RegisterSigner failed: %v", err))
|
||||
}
|
||||
if err := RegisterVerifier(alg, func(alg jwa.SignatureAlgorithm) VerifierFactory {
|
||||
return VerifierFactoryFn(func() (Verifier, error) {
|
||||
return legacy.NewHMACVerifier(alg), nil
|
||||
})
|
||||
}(alg)); err != nil {
|
||||
panic(fmt.Sprintf("RegisterVerifier failed: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
for _, alg := range []jwa.SignatureAlgorithm{jwa.RS256(), jwa.RS384(), jwa.RS512(), jwa.PS256(), jwa.PS384(), jwa.PS512()} {
|
||||
if err := RegisterSigner(alg, func(alg jwa.SignatureAlgorithm) SignerFactory {
|
||||
return SignerFactoryFn(func() (Signer, error) {
|
||||
return legacy.NewRSASigner(alg), nil
|
||||
})
|
||||
}(alg)); err != nil {
|
||||
panic(fmt.Sprintf("RegisterSigner failed: %v", err))
|
||||
}
|
||||
if err := RegisterVerifier(alg, func(alg jwa.SignatureAlgorithm) VerifierFactory {
|
||||
return VerifierFactoryFn(func() (Verifier, error) {
|
||||
return legacy.NewRSAVerifier(alg), nil
|
||||
})
|
||||
}(alg)); err != nil {
|
||||
panic(fmt.Sprintf("RegisterVerifier failed: %v", err))
|
||||
}
|
||||
}
|
||||
for _, alg := range []jwa.SignatureAlgorithm{jwa.ES256(), jwa.ES384(), jwa.ES512(), jwa.ES256K()} {
|
||||
if err := RegisterSigner(alg, func(alg jwa.SignatureAlgorithm) SignerFactory {
|
||||
return SignerFactoryFn(func() (Signer, error) {
|
||||
return legacy.NewECDSASigner(alg), nil
|
||||
})
|
||||
}(alg)); err != nil {
|
||||
panic(fmt.Sprintf("RegisterSigner failed: %v", err))
|
||||
}
|
||||
if err := RegisterVerifier(alg, func(alg jwa.SignatureAlgorithm) VerifierFactory {
|
||||
return VerifierFactoryFn(func() (Verifier, error) {
|
||||
return legacy.NewECDSAVerifier(alg), nil
|
||||
})
|
||||
}(alg)); err != nil {
|
||||
panic(fmt.Sprintf("RegisterVerifier failed: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := RegisterSigner(jwa.EdDSA(), SignerFactoryFn(func() (Signer, error) {
|
||||
return legacy.NewEdDSASigner(), nil
|
||||
})); err != nil {
|
||||
panic(fmt.Sprintf("RegisterSigner failed: %v", err))
|
||||
}
|
||||
if err := RegisterVerifier(jwa.EdDSA(), VerifierFactoryFn(func() (Verifier, error) {
|
||||
return legacy.NewEdDSAVerifier(), nil
|
||||
})); err != nil {
|
||||
panic(fmt.Sprintf("RegisterVerifier failed: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
func legacySignerFor(alg jwa.SignatureAlgorithm) (Signer, error) {
|
||||
muSigner.Lock()
|
||||
s, ok := signers[alg]
|
||||
if !ok {
|
||||
v, err := newLegacySigner(alg)
|
||||
if err != nil {
|
||||
muSigner.Unlock()
|
||||
return nil, fmt.Errorf(`failed to create payload signer: %w`, err)
|
||||
}
|
||||
signers[alg] = v
|
||||
s = v
|
||||
}
|
||||
muSigner.Unlock()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
load("@rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "legacy",
|
||||
srcs = [
|
||||
"ecdsa.go",
|
||||
"eddsa.go",
|
||||
"hmac.go",
|
||||
"legacy.go",
|
||||
"rsa.go",
|
||||
],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/jws/legacy",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//internal/ecutil",
|
||||
"//internal/keyconv",
|
||||
"//internal/pool",
|
||||
"//jwa",
|
||||
"//jws/internal/keytype",
|
||||
],
|
||||
)
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"encoding/asn1"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/ecutil"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/keyconv"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/pool"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jws/internal/keytype"
|
||||
)
|
||||
|
||||
var ecdsaSigners = make(map[jwa.SignatureAlgorithm]*ecdsaSigner)
|
||||
var ecdsaVerifiers = make(map[jwa.SignatureAlgorithm]*ecdsaVerifier)
|
||||
|
||||
func init() {
|
||||
algs := map[jwa.SignatureAlgorithm]crypto.Hash{
|
||||
jwa.ES256(): crypto.SHA256,
|
||||
jwa.ES384(): crypto.SHA384,
|
||||
jwa.ES512(): crypto.SHA512,
|
||||
jwa.ES256K(): crypto.SHA256,
|
||||
}
|
||||
for alg, hash := range algs {
|
||||
ecdsaSigners[alg] = &ecdsaSigner{
|
||||
alg: alg,
|
||||
hash: hash,
|
||||
}
|
||||
ecdsaVerifiers[alg] = &ecdsaVerifier{
|
||||
alg: alg,
|
||||
hash: hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewECDSASigner(alg jwa.SignatureAlgorithm) Signer {
|
||||
return ecdsaSigners[alg]
|
||||
}
|
||||
|
||||
// ecdsaSigners are immutable.
|
||||
type ecdsaSigner struct {
|
||||
alg jwa.SignatureAlgorithm
|
||||
hash crypto.Hash
|
||||
}
|
||||
|
||||
func (es ecdsaSigner) Algorithm() jwa.SignatureAlgorithm {
|
||||
return es.alg
|
||||
}
|
||||
|
||||
func (es *ecdsaSigner) Sign(payload []byte, key any) ([]byte, error) {
|
||||
if key == nil {
|
||||
return nil, fmt.Errorf(`missing private key while signing payload`)
|
||||
}
|
||||
|
||||
h := es.hash.New()
|
||||
if _, err := h.Write(payload); err != nil {
|
||||
return nil, fmt.Errorf(`failed to write payload using ecdsa: %w`, err)
|
||||
}
|
||||
|
||||
signer, ok := key.(crypto.Signer)
|
||||
if ok {
|
||||
if !keytype.IsValidECDSAKey(key) {
|
||||
return nil, fmt.Errorf(`cannot use key of type %T to generate ECDSA based signatures`, key)
|
||||
}
|
||||
switch key.(type) {
|
||||
case ecdsa.PrivateKey, *ecdsa.PrivateKey:
|
||||
// if it's a ecdsa.PrivateKey, it's more efficient to
|
||||
// go through the non-crypto.Signer route. Set ok to false
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
|
||||
var r, s *big.Int
|
||||
var curveBits int
|
||||
if ok {
|
||||
signed, err := signer.Sign(rand.Reader, h.Sum(nil), es.hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var p struct {
|
||||
R *big.Int
|
||||
S *big.Int
|
||||
}
|
||||
if _, err := asn1.Unmarshal(signed, &p); err != nil {
|
||||
return nil, fmt.Errorf(`failed to unmarshal ASN1 encoded signature: %w`, err)
|
||||
}
|
||||
|
||||
// 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 should be cached some where.
|
||||
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
|
||||
|
||||
r = p.R
|
||||
s = p.S
|
||||
} else {
|
||||
var privkey ecdsa.PrivateKey
|
||||
if err := keyconv.ECDSAPrivateKey(&privkey, key); err != nil {
|
||||
return nil, fmt.Errorf(`failed to retrieve ecdsa.PrivateKey out of %T: %w`, key, err)
|
||||
}
|
||||
curveBits = privkey.Curve.Params().BitSize
|
||||
rtmp, stmp, err := ecdsa.Sign(rand.Reader, &privkey, h.Sum(nil))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to sign payload using ecdsa: %w`, err)
|
||||
}
|
||||
r = rtmp
|
||||
s = stmp
|
||||
}
|
||||
|
||||
keyBytes := curveBits / 8
|
||||
// Curve bits do not need to be a multiple of 8.
|
||||
if curveBits%8 > 0 {
|
||||
keyBytes++
|
||||
}
|
||||
|
||||
rBytes := r.Bytes()
|
||||
rBytesPadded := make([]byte, keyBytes)
|
||||
copy(rBytesPadded[keyBytes-len(rBytes):], rBytes)
|
||||
|
||||
sBytes := s.Bytes()
|
||||
sBytesPadded := make([]byte, keyBytes)
|
||||
copy(sBytesPadded[keyBytes-len(sBytes):], sBytes)
|
||||
|
||||
out := append(rBytesPadded, sBytesPadded...)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ecdsaVerifiers are immutable.
|
||||
type ecdsaVerifier struct {
|
||||
alg jwa.SignatureAlgorithm
|
||||
hash crypto.Hash
|
||||
}
|
||||
|
||||
func NewECDSAVerifier(alg jwa.SignatureAlgorithm) Verifier {
|
||||
return ecdsaVerifiers[alg]
|
||||
}
|
||||
|
||||
func (v ecdsaVerifier) Algorithm() jwa.SignatureAlgorithm {
|
||||
return v.alg
|
||||
}
|
||||
|
||||
func (v *ecdsaVerifier) Verify(payload []byte, signature []byte, key any) error {
|
||||
if key == nil {
|
||||
return fmt.Errorf(`missing public key while verifying payload`)
|
||||
}
|
||||
|
||||
var pubkey ecdsa.PublicKey
|
||||
if cs, ok := key.(crypto.Signer); ok {
|
||||
cpub := cs.Public()
|
||||
switch cpub := cpub.(type) {
|
||||
case ecdsa.PublicKey:
|
||||
pubkey = cpub
|
||||
case *ecdsa.PublicKey:
|
||||
pubkey = *cpub
|
||||
default:
|
||||
return fmt.Errorf(`failed to retrieve ecdsa.PublicKey out of crypto.Signer %T`, key)
|
||||
}
|
||||
} else {
|
||||
if err := keyconv.ECDSAPublicKey(&pubkey, key); err != nil {
|
||||
return fmt.Errorf(`failed to retrieve ecdsa.PublicKey out of %T: %w`, key, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !pubkey.Curve.IsOnCurve(pubkey.X, pubkey.Y) {
|
||||
return fmt.Errorf(`public key used does not contain a point (X,Y) on the curve`)
|
||||
}
|
||||
|
||||
r := pool.BigInt().Get()
|
||||
s := pool.BigInt().Get()
|
||||
defer pool.BigInt().Put(r)
|
||||
defer pool.BigInt().Put(s)
|
||||
|
||||
keySize := ecutil.CalculateKeySize(pubkey.Curve)
|
||||
if len(signature) != keySize*2 {
|
||||
return fmt.Errorf(`invalid signature length for curve %q`, pubkey.Curve.Params().Name)
|
||||
}
|
||||
|
||||
r.SetBytes(signature[:keySize])
|
||||
s.SetBytes(signature[keySize:])
|
||||
|
||||
h := v.hash.New()
|
||||
if _, err := h.Write(payload); err != nil {
|
||||
return fmt.Errorf(`failed to write payload using ecdsa: %w`, err)
|
||||
}
|
||||
|
||||
if !ecdsa.Verify(&pubkey, h.Sum(nil), r, s) {
|
||||
return fmt.Errorf(`failed to verify signature using ecdsa`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/keyconv"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jws/internal/keytype"
|
||||
)
|
||||
|
||||
type eddsaSigner struct{}
|
||||
|
||||
func NewEdDSASigner() Signer {
|
||||
return &eddsaSigner{}
|
||||
}
|
||||
|
||||
func (s eddsaSigner) Algorithm() jwa.SignatureAlgorithm {
|
||||
return jwa.EdDSA()
|
||||
}
|
||||
|
||||
func (s eddsaSigner) Sign(payload []byte, key any) ([]byte, error) {
|
||||
if key == nil {
|
||||
return nil, fmt.Errorf(`missing private key while signing payload`)
|
||||
}
|
||||
|
||||
// The ed25519.PrivateKey object implements crypto.Signer, so we should
|
||||
// simply accept a crypto.Signer here.
|
||||
signer, ok := key.(crypto.Signer)
|
||||
if ok {
|
||||
if !keytype.IsValidEDDSAKey(key) {
|
||||
return nil, fmt.Errorf(`cannot use key of type %T to generate EdDSA based signatures`, key)
|
||||
}
|
||||
} else {
|
||||
// This fallback exists for cases when jwk.Key was passed, or
|
||||
// users gave us a pointer instead of non-pointer, etc.
|
||||
var privkey ed25519.PrivateKey
|
||||
if err := keyconv.Ed25519PrivateKey(&privkey, key); err != nil {
|
||||
return nil, fmt.Errorf(`failed to retrieve ed25519.PrivateKey out of %T: %w`, key, err)
|
||||
}
|
||||
signer = privkey
|
||||
}
|
||||
|
||||
return signer.Sign(rand.Reader, payload, crypto.Hash(0))
|
||||
}
|
||||
|
||||
type eddsaVerifier struct{}
|
||||
|
||||
func NewEdDSAVerifier() Verifier {
|
||||
return &eddsaVerifier{}
|
||||
}
|
||||
|
||||
func (v eddsaVerifier) Verify(payload, signature []byte, key any) (err error) {
|
||||
if key == nil {
|
||||
return fmt.Errorf(`missing public key while verifying payload`)
|
||||
}
|
||||
|
||||
var pubkey ed25519.PublicKey
|
||||
signer, ok := key.(crypto.Signer)
|
||||
if ok {
|
||||
v := signer.Public()
|
||||
pubkey, ok = v.(ed25519.PublicKey)
|
||||
if !ok {
|
||||
return fmt.Errorf(`expected crypto.Signer.Public() to return ed25519.PublicKey, but got %T`, v)
|
||||
}
|
||||
} else {
|
||||
if err := keyconv.Ed25519PublicKey(&pubkey, key); err != nil {
|
||||
return fmt.Errorf(`failed to retrieve ed25519.PublicKey out of %T: %w`, key, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !ed25519.Verify(pubkey, payload, signature) {
|
||||
return fmt.Errorf(`failed to match EdDSA signature`)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"hash"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/keyconv"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
)
|
||||
|
||||
func init() {
|
||||
algs := map[jwa.SignatureAlgorithm]func() hash.Hash{
|
||||
jwa.HS256(): sha256.New,
|
||||
jwa.HS384(): sha512.New384,
|
||||
jwa.HS512(): sha512.New,
|
||||
}
|
||||
|
||||
for alg, h := range algs {
|
||||
hmacSignFuncs[alg] = makeHMACSignFunc(h)
|
||||
}
|
||||
}
|
||||
|
||||
// HMACSigner uses crypto/hmac to sign the payloads.
|
||||
// This is for legacy support only.
|
||||
type HMACSigner struct {
|
||||
alg jwa.SignatureAlgorithm
|
||||
sign hmacSignFunc
|
||||
}
|
||||
|
||||
type HMACVerifier struct {
|
||||
signer Signer
|
||||
}
|
||||
|
||||
type hmacSignFunc func(payload []byte, key []byte) ([]byte, error)
|
||||
|
||||
var hmacSignFuncs = make(map[jwa.SignatureAlgorithm]hmacSignFunc)
|
||||
|
||||
func NewHMACSigner(alg jwa.SignatureAlgorithm) Signer {
|
||||
return &HMACSigner{
|
||||
alg: alg,
|
||||
sign: hmacSignFuncs[alg], // we know this will succeed
|
||||
}
|
||||
}
|
||||
|
||||
func makeHMACSignFunc(hfunc func() hash.Hash) hmacSignFunc {
|
||||
return func(payload []byte, key []byte) ([]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
|
||||
}
|
||||
}
|
||||
|
||||
func (s HMACSigner) Algorithm() jwa.SignatureAlgorithm {
|
||||
return s.alg
|
||||
}
|
||||
|
||||
func (s HMACSigner) Sign(payload []byte, key any) ([]byte, error) {
|
||||
var hmackey []byte
|
||||
if err := keyconv.ByteSliceKey(&hmackey, key); err != nil {
|
||||
return nil, fmt.Errorf(`invalid key type %T. []byte is required: %w`, key, err)
|
||||
}
|
||||
|
||||
if len(hmackey) == 0 {
|
||||
return nil, fmt.Errorf(`missing key while signing payload`)
|
||||
}
|
||||
|
||||
return s.sign(payload, hmackey)
|
||||
}
|
||||
|
||||
func NewHMACVerifier(alg jwa.SignatureAlgorithm) Verifier {
|
||||
s := NewHMACSigner(alg)
|
||||
return &HMACVerifier{signer: s}
|
||||
}
|
||||
|
||||
func (v HMACVerifier) Verify(payload, signature []byte, key any) (err error) {
|
||||
expected, err := v.signer.Sign(payload, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to generated signature: %w`, err)
|
||||
}
|
||||
|
||||
if !hmac.Equal(signature, expected) {
|
||||
return fmt.Errorf(`failed to match hmac signature`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Package legacy provides support for legacy implementation of JWS signing and verification.
|
||||
// Types, functions, and variables in this package are exported only for legacy support,
|
||||
// and should not be relied upon for new code.
|
||||
//
|
||||
// This package will be available until v3 is sunset, but it will be removed in v4
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
)
|
||||
|
||||
// Signer generates the signature for a given payload.
|
||||
// This is for legacy support only.
|
||||
type Signer interface {
|
||||
// Sign creates a signature for the given payload.
|
||||
// The second argument is the key used for signing the payload, and is usually
|
||||
// the private key type associated with the signature method. For example,
|
||||
// for `jwa.RSXXX` and `jwa.PSXXX` types, you need to pass the
|
||||
// `*"crypto/rsa".PrivateKey` type.
|
||||
// Check the documentation for each signer for details
|
||||
Sign([]byte, any) ([]byte, error)
|
||||
|
||||
Algorithm() jwa.SignatureAlgorithm
|
||||
}
|
||||
|
||||
// Verifier is for legacy support only.
|
||||
type Verifier interface {
|
||||
// Verify checks whether the payload and signature are valid for
|
||||
// the given key.
|
||||
// `key` is the key used for verifying the payload, and is usually
|
||||
// the public key associated with the signature method. For example,
|
||||
// for `jwa.RSXXX` and `jwa.PSXXX` types, you need to pass the
|
||||
// `*"crypto/rsa".PublicKey` type.
|
||||
// Check the documentation for each verifier for details
|
||||
Verify(payload []byte, signature []byte, key any) error
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/keyconv"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jws/internal/keytype"
|
||||
)
|
||||
|
||||
var rsaSigners = make(map[jwa.SignatureAlgorithm]*rsaSigner)
|
||||
var rsaVerifiers = make(map[jwa.SignatureAlgorithm]*rsaVerifier)
|
||||
|
||||
func init() {
|
||||
data := map[jwa.SignatureAlgorithm]struct {
|
||||
Hash crypto.Hash
|
||||
PSS bool
|
||||
}{
|
||||
jwa.RS256(): {
|
||||
Hash: crypto.SHA256,
|
||||
},
|
||||
jwa.RS384(): {
|
||||
Hash: crypto.SHA384,
|
||||
},
|
||||
jwa.RS512(): {
|
||||
Hash: crypto.SHA512,
|
||||
},
|
||||
jwa.PS256(): {
|
||||
Hash: crypto.SHA256,
|
||||
PSS: true,
|
||||
},
|
||||
jwa.PS384(): {
|
||||
Hash: crypto.SHA384,
|
||||
PSS: true,
|
||||
},
|
||||
jwa.PS512(): {
|
||||
Hash: crypto.SHA512,
|
||||
PSS: true,
|
||||
},
|
||||
}
|
||||
|
||||
for alg, item := range data {
|
||||
rsaSigners[alg] = &rsaSigner{
|
||||
alg: alg,
|
||||
hash: item.Hash,
|
||||
pss: item.PSS,
|
||||
}
|
||||
rsaVerifiers[alg] = &rsaVerifier{
|
||||
alg: alg,
|
||||
hash: item.Hash,
|
||||
pss: item.PSS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type rsaSigner struct {
|
||||
alg jwa.SignatureAlgorithm
|
||||
hash crypto.Hash
|
||||
pss bool
|
||||
}
|
||||
|
||||
func NewRSASigner(alg jwa.SignatureAlgorithm) Signer {
|
||||
return rsaSigners[alg]
|
||||
}
|
||||
|
||||
func (rs *rsaSigner) Algorithm() jwa.SignatureAlgorithm {
|
||||
return rs.alg
|
||||
}
|
||||
|
||||
func (rs *rsaSigner) Sign(payload []byte, key any) ([]byte, error) {
|
||||
if key == nil {
|
||||
return nil, fmt.Errorf(`missing private key while signing payload`)
|
||||
}
|
||||
|
||||
signer, ok := key.(crypto.Signer)
|
||||
if ok {
|
||||
if !keytype.IsValidRSAKey(key) {
|
||||
return nil, fmt.Errorf(`cannot use key of type %T to generate RSA based signatures`, key)
|
||||
}
|
||||
} else {
|
||||
var privkey rsa.PrivateKey
|
||||
if err := keyconv.RSAPrivateKey(&privkey, key); err != nil {
|
||||
return nil, fmt.Errorf(`failed to retrieve rsa.PrivateKey out of %T: %w`, key, err)
|
||||
}
|
||||
signer = &privkey
|
||||
}
|
||||
|
||||
h := rs.hash.New()
|
||||
if _, err := h.Write(payload); err != nil {
|
||||
return nil, fmt.Errorf(`failed to write payload to hash: %w`, err)
|
||||
}
|
||||
if rs.pss {
|
||||
return signer.Sign(rand.Reader, h.Sum(nil), &rsa.PSSOptions{
|
||||
Hash: rs.hash,
|
||||
SaltLength: rsa.PSSSaltLengthEqualsHash,
|
||||
})
|
||||
}
|
||||
return signer.Sign(rand.Reader, h.Sum(nil), rs.hash)
|
||||
}
|
||||
|
||||
type rsaVerifier struct {
|
||||
alg jwa.SignatureAlgorithm
|
||||
hash crypto.Hash
|
||||
pss bool
|
||||
}
|
||||
|
||||
func NewRSAVerifier(alg jwa.SignatureAlgorithm) Verifier {
|
||||
return rsaVerifiers[alg]
|
||||
}
|
||||
|
||||
func (rv *rsaVerifier) Verify(payload, signature []byte, key any) error {
|
||||
if key == nil {
|
||||
return fmt.Errorf(`missing public key while verifying payload`)
|
||||
}
|
||||
|
||||
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(`failed to retrieve rsa.PublicKey out of crypto.Signer %T`, key)
|
||||
}
|
||||
} else {
|
||||
if err := keyconv.RSAPublicKey(&pubkey, key); err != nil {
|
||||
return fmt.Errorf(`failed to retrieve rsa.PublicKey out of %T: %w`, key, err)
|
||||
}
|
||||
}
|
||||
|
||||
h := rv.hash.New()
|
||||
if _, err := h.Write(payload); err != nil {
|
||||
return fmt.Errorf(`failed to write payload to hash: %w`, err)
|
||||
}
|
||||
|
||||
if rv.pss {
|
||||
return rsa.VerifyPSS(&pubkey, rv.hash, h.Sum(nil), signature, nil)
|
||||
}
|
||||
return rsa.VerifyPKCS1v15(&pubkey, rv.hash, h.Sum(nil), signature)
|
||||
}
|
||||
+550
@@ -0,0 +1,550 @@
|
||||
package jws
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/base64"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/json"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/pool"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/tokens"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
)
|
||||
|
||||
func NewSignature() *Signature {
|
||||
return &Signature{}
|
||||
}
|
||||
|
||||
func (s *Signature) DecodeCtx() DecodeCtx {
|
||||
return s.dc
|
||||
}
|
||||
|
||||
func (s *Signature) SetDecodeCtx(dc DecodeCtx) {
|
||||
s.dc = dc
|
||||
}
|
||||
|
||||
func (s Signature) PublicHeaders() Headers {
|
||||
return s.headers
|
||||
}
|
||||
|
||||
func (s *Signature) SetPublicHeaders(v Headers) *Signature {
|
||||
s.headers = v
|
||||
return s
|
||||
}
|
||||
|
||||
func (s Signature) ProtectedHeaders() Headers {
|
||||
return s.protected
|
||||
}
|
||||
|
||||
func (s *Signature) SetProtectedHeaders(v Headers) *Signature {
|
||||
s.protected = v
|
||||
return s
|
||||
}
|
||||
|
||||
func (s Signature) Signature() []byte {
|
||||
return s.signature
|
||||
}
|
||||
|
||||
func (s *Signature) SetSignature(v []byte) *Signature {
|
||||
s.signature = v
|
||||
return s
|
||||
}
|
||||
|
||||
type signatureUnmarshalProbe struct {
|
||||
Header Headers `json:"header,omitempty"`
|
||||
Protected *string `json:"protected,omitempty"`
|
||||
Signature *string `json:"signature,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Signature) UnmarshalJSON(data []byte) error {
|
||||
var sup signatureUnmarshalProbe
|
||||
sup.Header = NewHeaders()
|
||||
if err := json.Unmarshal(data, &sup); err != nil {
|
||||
return fmt.Errorf(`failed to unmarshal signature into temporary struct: %w`, err)
|
||||
}
|
||||
|
||||
s.headers = sup.Header
|
||||
if buf := sup.Protected; buf != nil {
|
||||
src := []byte(*buf)
|
||||
if !bytes.HasPrefix(src, []byte{tokens.OpenCurlyBracket}) {
|
||||
decoded, err := base64.Decode(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to base64 decode protected headers: %w`, err)
|
||||
}
|
||||
src = decoded
|
||||
}
|
||||
|
||||
prt := NewHeaders()
|
||||
//nolint:forcetypeassert
|
||||
prt.(*stdHeaders).SetDecodeCtx(s.DecodeCtx())
|
||||
if err := json.Unmarshal(src, prt); err != nil {
|
||||
return fmt.Errorf(`failed to unmarshal protected headers: %w`, err)
|
||||
}
|
||||
//nolint:forcetypeassert
|
||||
prt.(*stdHeaders).SetDecodeCtx(nil)
|
||||
s.protected = prt
|
||||
}
|
||||
|
||||
if sup.Signature != nil {
|
||||
decoded, err := base64.DecodeString(*sup.Signature)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to base decode signature: %w`, err)
|
||||
}
|
||||
s.signature = decoded
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign populates the signature field, with a signature generated by
|
||||
// given the signer object and payload.
|
||||
//
|
||||
// The first return value is the raw signature in binary format.
|
||||
// The second return value s the full three-segment signature
|
||||
// (e.g. "eyXXXX.XXXXX.XXXX")
|
||||
//
|
||||
// This method is deprecated, and will be remove in a future release.
|
||||
// Signature objects in the future will only be used as containers,
|
||||
// and signing will be done using the `jws.Sign` function, or alternatively
|
||||
// you could use jwsbb package to craft the signature manually.
|
||||
func (s *Signature) Sign(payload []byte, signer Signer, key any) ([]byte, []byte, error) {
|
||||
return s.sign2(payload, signer, key)
|
||||
}
|
||||
|
||||
func (s *Signature) sign2(payload []byte, signer interface{ Algorithm() jwa.SignatureAlgorithm }, key any) ([]byte, []byte, error) {
|
||||
// Create a signatureBuilder to use the shared signing logic
|
||||
sb := signatureBuilderPool.Get()
|
||||
defer signatureBuilderPool.Put(sb)
|
||||
|
||||
sb.alg = signer.Algorithm()
|
||||
sb.key = key
|
||||
sb.protected = s.protected
|
||||
sb.public = s.headers
|
||||
|
||||
// Set up the appropriate signer interface
|
||||
switch typedSigner := signer.(type) {
|
||||
case Signer2:
|
||||
sb.signer2 = typedSigner
|
||||
case Signer:
|
||||
sb.signer = typedSigner
|
||||
default:
|
||||
return nil, nil, fmt.Errorf(`invalid signer type: %T`, signer)
|
||||
}
|
||||
|
||||
// Create a minimal sign context
|
||||
sc := signContextPool.Get()
|
||||
defer signContextPool.Put(sc)
|
||||
|
||||
sc.detached = s.detached
|
||||
|
||||
encoder := s.encoder
|
||||
if encoder == nil {
|
||||
encoder = base64.DefaultEncoder()
|
||||
}
|
||||
sc.encoder = encoder
|
||||
|
||||
// Build the signature using signatureBuilder
|
||||
sig, err := sb.Build(sc, payload)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf(`failed to build signature: %w`, err)
|
||||
}
|
||||
|
||||
// Copy the signature result back to this signature instance
|
||||
s.signature = sig.signature
|
||||
s.protected = sig.protected
|
||||
s.headers = sig.headers
|
||||
|
||||
// Build the complete JWS token for the return value
|
||||
buf := pool.BytesBuffer().Get()
|
||||
defer pool.BytesBuffer().Put(buf)
|
||||
|
||||
// Marshal the merged headers for the final output
|
||||
hdrs, err := mergeHeaders(s.headers, s.protected)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf(`failed to merge headers: %w`, err)
|
||||
}
|
||||
|
||||
hdrbuf, err := json.Marshal(hdrs)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf(`failed to marshal headers: %w`, err)
|
||||
}
|
||||
|
||||
buf.WriteString(encoder.EncodeToString(hdrbuf))
|
||||
buf.WriteByte(tokens.Period)
|
||||
|
||||
var plen int
|
||||
b64 := getB64Value(hdrs)
|
||||
if b64 {
|
||||
encoded := encoder.EncodeToString(payload)
|
||||
plen = len(encoded)
|
||||
buf.WriteString(encoded)
|
||||
} else {
|
||||
if !s.detached {
|
||||
if bytes.Contains(payload, []byte{tokens.Period}) {
|
||||
return nil, nil, fmt.Errorf(`payload must not contain a "."`)
|
||||
}
|
||||
}
|
||||
plen = len(payload)
|
||||
buf.Write(payload)
|
||||
}
|
||||
|
||||
// Handle detached payload
|
||||
if s.detached {
|
||||
buf.Truncate(buf.Len() - plen)
|
||||
}
|
||||
|
||||
buf.WriteByte(tokens.Period)
|
||||
buf.WriteString(encoder.EncodeToString(s.signature))
|
||||
ret := make([]byte, buf.Len())
|
||||
copy(ret, buf.Bytes())
|
||||
|
||||
return s.signature, ret, nil
|
||||
}
|
||||
|
||||
func NewMessage() *Message {
|
||||
return &Message{}
|
||||
}
|
||||
|
||||
// Clears the internal raw buffer that was accumulated during
|
||||
// the verify phase
|
||||
func (m *Message) clearRaw() {
|
||||
for _, sig := range m.signatures {
|
||||
if protected := sig.protected; protected != nil {
|
||||
if cr, ok := protected.(*stdHeaders); ok {
|
||||
cr.raw = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) SetDecodeCtx(dc DecodeCtx) {
|
||||
m.dc = dc
|
||||
}
|
||||
|
||||
func (m *Message) DecodeCtx() DecodeCtx {
|
||||
return m.dc
|
||||
}
|
||||
|
||||
// Payload returns the decoded payload
|
||||
func (m Message) Payload() []byte {
|
||||
return m.payload
|
||||
}
|
||||
|
||||
func (m *Message) SetPayload(v []byte) *Message {
|
||||
m.payload = v
|
||||
return m
|
||||
}
|
||||
|
||||
func (m Message) Signatures() []*Signature {
|
||||
return m.signatures
|
||||
}
|
||||
|
||||
func (m *Message) AppendSignature(v *Signature) *Message {
|
||||
m.signatures = append(m.signatures, v)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Message) ClearSignatures() *Message {
|
||||
m.signatures = nil
|
||||
return m
|
||||
}
|
||||
|
||||
// LookupSignature looks up a particular signature entry using
|
||||
// the `kid` value
|
||||
func (m Message) LookupSignature(kid string) []*Signature {
|
||||
var sigs []*Signature
|
||||
for _, sig := range m.signatures {
|
||||
if hdr := sig.PublicHeaders(); hdr != nil {
|
||||
hdrKeyID, ok := hdr.KeyID()
|
||||
if ok && hdrKeyID == kid {
|
||||
sigs = append(sigs, sig)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if hdr := sig.ProtectedHeaders(); hdr != nil {
|
||||
hdrKeyID, ok := hdr.KeyID()
|
||||
if ok && hdrKeyID == kid {
|
||||
sigs = append(sigs, sig)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return sigs
|
||||
}
|
||||
|
||||
// This struct is used to first probe for the structure of the
|
||||
// incoming JSON object. We then decide how to parse it
|
||||
// from the fields that are populated.
|
||||
type messageUnmarshalProbe struct {
|
||||
Payload *string `json:"payload"`
|
||||
Signatures []json.RawMessage `json:"signatures,omitempty"`
|
||||
Header Headers `json:"header,omitempty"`
|
||||
Protected *string `json:"protected,omitempty"`
|
||||
Signature *string `json:"signature,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Message) UnmarshalJSON(buf []byte) error {
|
||||
m.payload = nil
|
||||
m.signatures = nil
|
||||
m.b64 = true
|
||||
|
||||
var mup messageUnmarshalProbe
|
||||
mup.Header = NewHeaders()
|
||||
if err := json.Unmarshal(buf, &mup); err != nil {
|
||||
return fmt.Errorf(`failed to unmarshal into temporary structure: %w`, err)
|
||||
}
|
||||
|
||||
b64 := true
|
||||
if mup.Signature == nil { // flattened signature is NOT present
|
||||
if len(mup.Signatures) == 0 {
|
||||
return fmt.Errorf(`required field "signatures" not present`)
|
||||
}
|
||||
|
||||
m.signatures = make([]*Signature, 0, len(mup.Signatures))
|
||||
for i, rawsig := range mup.Signatures {
|
||||
var sig Signature
|
||||
sig.SetDecodeCtx(m.DecodeCtx())
|
||||
if err := json.Unmarshal(rawsig, &sig); err != nil {
|
||||
return fmt.Errorf(`failed to unmarshal signature #%d: %w`, i+1, err)
|
||||
}
|
||||
sig.SetDecodeCtx(nil)
|
||||
|
||||
if sig.protected == nil {
|
||||
// Instead of barfing on a nil protected header, use an empty header
|
||||
sig.protected = NewHeaders()
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
if !getB64Value(sig.protected) {
|
||||
b64 = false
|
||||
}
|
||||
} else {
|
||||
if b64 != getB64Value(sig.protected) {
|
||||
return fmt.Errorf(`b64 value must be the same for all signatures`)
|
||||
}
|
||||
}
|
||||
|
||||
m.signatures = append(m.signatures, &sig)
|
||||
}
|
||||
} else { // .signature is present, it's a flattened structure
|
||||
if len(mup.Signatures) != 0 {
|
||||
return fmt.Errorf(`invalid format ("signatures" and "signature" keys cannot both be present)`)
|
||||
}
|
||||
|
||||
var sig Signature
|
||||
sig.headers = mup.Header
|
||||
if src := mup.Protected; src != nil {
|
||||
decoded, err := base64.DecodeString(*src)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to base64 decode flattened protected headers: %w`, err)
|
||||
}
|
||||
prt := NewHeaders()
|
||||
//nolint:forcetypeassert
|
||||
prt.(*stdHeaders).SetDecodeCtx(m.DecodeCtx())
|
||||
if err := json.Unmarshal(decoded, prt); err != nil {
|
||||
return fmt.Errorf(`failed to unmarshal flattened protected headers: %w`, err)
|
||||
}
|
||||
//nolint:forcetypeassert
|
||||
prt.(*stdHeaders).SetDecodeCtx(nil)
|
||||
sig.protected = prt
|
||||
}
|
||||
|
||||
if sig.protected == nil {
|
||||
// Instead of barfing on a nil protected header, use an empty header
|
||||
sig.protected = NewHeaders()
|
||||
}
|
||||
|
||||
decoded, err := base64.DecodeString(*mup.Signature)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to base64 decode flattened signature: %w`, err)
|
||||
}
|
||||
sig.signature = decoded
|
||||
|
||||
m.signatures = []*Signature{&sig}
|
||||
b64 = getB64Value(sig.protected)
|
||||
}
|
||||
|
||||
if mup.Payload != nil {
|
||||
if !b64 { // NOT base64 encoded
|
||||
m.payload = []byte(*mup.Payload)
|
||||
} else {
|
||||
decoded, err := base64.DecodeString(*mup.Payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to base64 decode payload: %w`, err)
|
||||
}
|
||||
m.payload = decoded
|
||||
}
|
||||
}
|
||||
m.b64 = b64
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Message) MarshalJSON() ([]byte, error) {
|
||||
if len(m.signatures) == 1 {
|
||||
return m.marshalFlattened()
|
||||
}
|
||||
return m.marshalFull()
|
||||
}
|
||||
|
||||
func (m Message) marshalFlattened() ([]byte, error) {
|
||||
buf := pool.BytesBuffer().Get()
|
||||
defer pool.BytesBuffer().Put(buf)
|
||||
|
||||
sig := m.signatures[0]
|
||||
|
||||
buf.WriteRune(tokens.OpenCurlyBracket)
|
||||
var wrote bool
|
||||
|
||||
if hdr := sig.headers; hdr != nil {
|
||||
hdrjs, err := json.Marshal(hdr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to marshal "header" (flattened format): %w`, err)
|
||||
}
|
||||
buf.WriteString(`"header":`)
|
||||
buf.Write(hdrjs)
|
||||
wrote = true
|
||||
}
|
||||
|
||||
if wrote {
|
||||
buf.WriteRune(tokens.Comma)
|
||||
}
|
||||
buf.WriteString(`"payload":"`)
|
||||
buf.WriteString(base64.EncodeToString(m.payload))
|
||||
buf.WriteRune('"')
|
||||
|
||||
if protected := sig.protected; protected != nil {
|
||||
protectedbuf, err := json.Marshal(protected)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to marshal "protected" (flattened format): %w`, err)
|
||||
}
|
||||
buf.WriteString(`,"protected":"`)
|
||||
buf.WriteString(base64.EncodeToString(protectedbuf))
|
||||
buf.WriteRune('"')
|
||||
}
|
||||
|
||||
buf.WriteString(`,"signature":"`)
|
||||
buf.WriteString(base64.EncodeToString(sig.signature))
|
||||
buf.WriteRune('"')
|
||||
buf.WriteRune(tokens.CloseCurlyBracket)
|
||||
|
||||
ret := make([]byte, buf.Len())
|
||||
copy(ret, buf.Bytes())
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (m Message) marshalFull() ([]byte, error) {
|
||||
buf := pool.BytesBuffer().Get()
|
||||
defer pool.BytesBuffer().Put(buf)
|
||||
|
||||
buf.WriteString(`{"payload":"`)
|
||||
buf.WriteString(base64.EncodeToString(m.payload))
|
||||
buf.WriteString(`","signatures":[`)
|
||||
for i, sig := range m.signatures {
|
||||
if i > 0 {
|
||||
buf.WriteRune(tokens.Comma)
|
||||
}
|
||||
|
||||
buf.WriteRune(tokens.OpenCurlyBracket)
|
||||
var wrote bool
|
||||
if hdr := sig.headers; hdr != nil {
|
||||
hdrbuf, err := json.Marshal(hdr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to marshal "header" for signature #%d: %w`, i+1, err)
|
||||
}
|
||||
buf.WriteString(`"header":`)
|
||||
buf.Write(hdrbuf)
|
||||
wrote = true
|
||||
}
|
||||
|
||||
if protected := sig.protected; protected != nil {
|
||||
protectedbuf, err := json.Marshal(protected)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to marshal "protected" for signature #%d: %w`, i+1, err)
|
||||
}
|
||||
if wrote {
|
||||
buf.WriteRune(tokens.Comma)
|
||||
}
|
||||
buf.WriteString(`"protected":"`)
|
||||
buf.WriteString(base64.EncodeToString(protectedbuf))
|
||||
buf.WriteRune('"')
|
||||
wrote = true
|
||||
}
|
||||
|
||||
if len(sig.signature) > 0 {
|
||||
// If InsecureNoSignature is enabled, signature may not exist
|
||||
if wrote {
|
||||
buf.WriteRune(tokens.Comma)
|
||||
}
|
||||
buf.WriteString(`"signature":"`)
|
||||
buf.WriteString(base64.EncodeToString(sig.signature))
|
||||
buf.WriteString(`"`)
|
||||
}
|
||||
buf.WriteString(`}`)
|
||||
}
|
||||
buf.WriteString(`]}`)
|
||||
|
||||
ret := make([]byte, buf.Len())
|
||||
copy(ret, buf.Bytes())
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Compact generates a JWS message in compact serialization format from
|
||||
// `*jws.Message` object. The object contain exactly one signature, or
|
||||
// an error is returned.
|
||||
//
|
||||
// If using a detached payload, the payload must already be stored in
|
||||
// the `*jws.Message` object, and the `jws.WithDetached()` option
|
||||
// must be passed to the function.
|
||||
func Compact(msg *Message, options ...CompactOption) ([]byte, error) {
|
||||
if l := len(msg.signatures); l != 1 {
|
||||
return nil, fmt.Errorf(`jws.Compact: cannot serialize message with %d signatures (must be one)`, l)
|
||||
}
|
||||
|
||||
var detached bool
|
||||
var encoder Base64Encoder = base64.DefaultEncoder()
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identDetached{}:
|
||||
if err := option.Value(&detached); err != nil {
|
||||
return nil, fmt.Errorf(`jws.Compact: failed to retrieve detached option value: %w`, err)
|
||||
}
|
||||
case identBase64Encoder{}:
|
||||
if err := option.Value(&encoder); err != nil {
|
||||
return nil, fmt.Errorf(`jws.Compact: failed to retrieve base64 encoder option value: %w`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s := msg.signatures[0]
|
||||
// XXX check if this is correct
|
||||
hdrs := s.ProtectedHeaders()
|
||||
|
||||
hdrbuf, err := json.Marshal(hdrs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`jws.Compress: failed to marshal headers: %w`, err)
|
||||
}
|
||||
|
||||
buf := pool.BytesBuffer().Get()
|
||||
defer pool.BytesBuffer().Put(buf)
|
||||
|
||||
buf.WriteString(encoder.EncodeToString(hdrbuf))
|
||||
buf.WriteByte(tokens.Period)
|
||||
|
||||
if !detached {
|
||||
if getB64Value(hdrs) {
|
||||
encoded := encoder.EncodeToString(msg.payload)
|
||||
buf.WriteString(encoded)
|
||||
} else {
|
||||
if bytes.Contains(msg.payload, []byte{tokens.Period}) {
|
||||
return nil, fmt.Errorf(`jws.Compress: payload must not contain a "."`)
|
||||
}
|
||||
buf.Write(msg.payload)
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteByte(tokens.Period)
|
||||
buf.WriteString(encoder.EncodeToString(s.signature))
|
||||
ret := make([]byte, buf.Len())
|
||||
copy(ret, buf.Bytes())
|
||||
return ret, nil
|
||||
}
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
package jws
|
||||
|
||||
import (
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
"github.com/lestrrat-go/option/v2"
|
||||
)
|
||||
|
||||
type identInsecureNoSignature struct{}
|
||||
|
||||
// WithJSON specifies that the result of `jws.Sign()` is serialized in
|
||||
// JSON format.
|
||||
//
|
||||
// If you pass multiple keys to `jws.Sign()`, it will fail unless
|
||||
// you also pass this option.
|
||||
func WithJSON(options ...WithJSONSuboption) SignVerifyParseOption {
|
||||
var pretty bool
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identPretty{}:
|
||||
if err := option.Value(&pretty); err != nil {
|
||||
panic(`jws.WithJSON() option must be of type bool`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
format := fmtJSON
|
||||
if pretty {
|
||||
format = fmtJSONPretty
|
||||
}
|
||||
return &signVerifyParseOption{option.New(identSerialization{}, format)}
|
||||
}
|
||||
|
||||
type withKey struct {
|
||||
alg jwa.KeyAlgorithm
|
||||
key any
|
||||
protected Headers
|
||||
public Headers
|
||||
}
|
||||
|
||||
// Protected exists as an escape hatch to modify the header values after the fact
|
||||
func (w *withKey) Protected(v Headers) Headers {
|
||||
if w.protected == nil && v != nil {
|
||||
w.protected = v
|
||||
}
|
||||
return w.protected
|
||||
}
|
||||
|
||||
// WithKey is used to pass a static algorithm/key pair to either `jws.Sign()` or `jws.Verify()`.
|
||||
//
|
||||
// The `alg` parameter is the identifier for the signature algorithm that should be used.
|
||||
// It is of type `jwa.KeyAlgorithm` but in reality you can only pass `jwa.SignatureAlgorithm`
|
||||
// types. It is this way so that the value in `(jwk.Key).Algorithm()` can be directly
|
||||
// passed to the option. If you specify other algorithm types such as `jwa.KeyEncryptionAlgorithm`,
|
||||
// then you will get an error when `jws.Sign()` or `jws.Verify()` is executed.
|
||||
//
|
||||
// The `alg` parameter cannot be "none" (jwa.NoSignature) for security reasons.
|
||||
// You will have to use a separate, more explicit option to allow the use of "none"
|
||||
// algorithm (WithInsecureNoSignature).
|
||||
//
|
||||
// The algorithm specified in the `alg` parameter MUST be able to support
|
||||
// the type of key you provided, otherwise an error is returned.
|
||||
//
|
||||
// Any of the following is accepted for the `key` parameter:
|
||||
// * A "raw" key (e.g. rsa.PrivateKey, ecdsa.PrivateKey, etc)
|
||||
// * A crypto.Signer
|
||||
// * A jwk.Key
|
||||
//
|
||||
// Note that due to technical reasons, this library is NOT able to differentiate
|
||||
// between a valid/invalid key for given algorithm if the key implements crypto.Signer
|
||||
// and the key is from an external library. For example, while we can tell that it is
|
||||
// invalid to use `jwk.WithKey(jwa.RSA256, ecdsaPrivateKey)` because the key is
|
||||
// presumably from `crypto/ecdsa` or this library, if you use a KMS wrapper
|
||||
// that implements crypto.Signer that is outside of the go standard library or this
|
||||
// library, we will not be able to properly catch the misuse of such keys --
|
||||
// the output will happily generate an ECDSA signature even in the presence of
|
||||
// `jwa.RSA256`
|
||||
//
|
||||
// A `crypto.Signer` is used when the private part of a key is
|
||||
// kept in an inaccessible location, such as hardware.
|
||||
// `crypto.Signer` is currently supported for RSA, ECDSA, and EdDSA
|
||||
// family of algorithms. You may consider using `github.com/jwx-go/crypto-signer`
|
||||
// if you would like to use keys stored in GCP/AWS KMS services.
|
||||
//
|
||||
// If the key is a jwk.Key and the key contains a key ID (`kid` field),
|
||||
// then it is added to the protected header generated by the signature.
|
||||
//
|
||||
// `jws.WithKey()` can further accept suboptions to change signing behavior
|
||||
// when used with `jws.Sign()`. `jws.WithProtected()` and `jws.WithPublic()`
|
||||
// can be passed to specify JWS headers that should be used whe signing.
|
||||
//
|
||||
// If the protected headers contain "b64" field, then the boolean value for the field
|
||||
// is respected when serializing. That is, if you specify a header with
|
||||
// `{"b64": false}`, then the payload is not base64 encoded.
|
||||
//
|
||||
// These suboptions are ignored when the `jws.WithKey()` option is used with `jws.Verify()`.
|
||||
func WithKey(alg jwa.KeyAlgorithm, key any, options ...WithKeySuboption) SignVerifyOption {
|
||||
// Implementation note: this option is shared between Sign() and
|
||||
// Verify(). As such we don't create a KeyProvider here because
|
||||
// if used in Sign() we would be doing something else.
|
||||
var protected, public Headers
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identProtectedHeaders{}:
|
||||
if err := option.Value(&protected); err != nil {
|
||||
panic(`jws.WithKey() option must be of type Headers`)
|
||||
}
|
||||
case identPublicHeaders{}:
|
||||
if err := option.Value(&public); err != nil {
|
||||
panic(`jws.WithKey() option must be of type Headers`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &signVerifyOption{
|
||||
option.New(identKey{}, &withKey{
|
||||
alg: alg,
|
||||
key: key,
|
||||
protected: protected,
|
||||
public: public,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// WithKeySet specifies a JWKS (jwk.Set) to use for verification.
|
||||
//
|
||||
// Because a JWKS can contain multiple keys and this library cannot tell
|
||||
// which one of the keys should be used for verification, we by default
|
||||
// require that both `alg` and `kid` fields in the JWS _and_ the
|
||||
// key match before a key is considered to be used.
|
||||
//
|
||||
// There are ways to override this behavior, but they must be explicitly
|
||||
// specified by the caller.
|
||||
//
|
||||
// To work with keys/JWS messages not having a `kid` field, you may specify
|
||||
// the suboption `WithKeySetRequired` via `jws.WithKey(key, jws.WithRequireKid(false))`.
|
||||
// This will allow the library to proceed without having to match the `kid` field.
|
||||
//
|
||||
// However, it will still check if the `alg` fields in the JWS message and the key(s)
|
||||
// match. If you must work with JWS messages that do not have an `alg` field,
|
||||
// you will need to use `jws.WithKeySet(key, jws.WithInferAlgorithm(true))`.
|
||||
//
|
||||
// See the documentation for `WithInferAlgorithm()` for more details.
|
||||
func WithKeySet(set jwk.Set, options ...WithKeySetSuboption) VerifyOption {
|
||||
requireKid := true
|
||||
var useDefault, inferAlgorithm, multipleKeysPerKeyID bool
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identRequireKid{}:
|
||||
if err := option.Value(&requireKid); err != nil {
|
||||
panic(`jws.WithKeySet() option must be of type bool`)
|
||||
}
|
||||
case identUseDefault{}:
|
||||
if err := option.Value(&useDefault); err != nil {
|
||||
panic(`jws.WithKeySet() option must be of type bool`)
|
||||
}
|
||||
case identMultipleKeysPerKeyID{}:
|
||||
if err := option.Value(&multipleKeysPerKeyID); err != nil {
|
||||
panic(`jws.WithKeySet() option must be of type bool`)
|
||||
}
|
||||
case identInferAlgorithmFromKey{}:
|
||||
if err := option.Value(&inferAlgorithm); err != nil {
|
||||
panic(`jws.WithKeySet() option must be of type bool`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return WithKeyProvider(&keySetProvider{
|
||||
set: set,
|
||||
requireKid: requireKid,
|
||||
useDefault: useDefault,
|
||||
multipleKeysPerKeyID: multipleKeysPerKeyID,
|
||||
inferAlgorithm: inferAlgorithm,
|
||||
})
|
||||
}
|
||||
|
||||
// WithVerifyAuto enables automatic verification of the signature using the JWKS specified in
|
||||
// the `jku` header. Note that by default this option will _reject_ any jku
|
||||
// provided by the JWS message. Read on for details.
|
||||
//
|
||||
// The JWKS is retrieved by the `jwk.Fetcher` specified in the first argument.
|
||||
// If the fetcher object is nil, the default fetcher, which is the `jwk.Fetch()`
|
||||
// function (wrapped in the `jwk.FetchFunc` type) is used.
|
||||
//
|
||||
// The remaining arguments are passed to the `(jwk.Fetcher).Fetch` method
|
||||
// when the JWKS is retrieved.
|
||||
//
|
||||
// jws.WithVerifyAuto(nil) // uses jwk.Fetch
|
||||
// jws.WithVerifyAuto(jwk.NewCachedFetcher(...)) // uses cached fetcher
|
||||
// jws.WithVerifyAuto(myFetcher) // use your custom fetcher
|
||||
//
|
||||
// By default a whitelist that disallows all URLs is added to the options
|
||||
// passed to the fetcher. You must explicitly specify a whitelist that allows
|
||||
// the URLs you trust. This default behavior is provided because by design
|
||||
// of the JWS specification it is the/ caller's responsibility to verify if
|
||||
// the URL specified in the `jku` header can be trusted -- thus by default
|
||||
// we trust nothing.
|
||||
//
|
||||
// Users are free to specify an open whitelist if they so choose, but this must
|
||||
// be explicitly done:
|
||||
//
|
||||
// jws.WithVerifyAuto(nil, jwk.WithFetchWhitelist(jwk.InsecureWhitelist()))
|
||||
//
|
||||
// You can also use `jwk.CachedFetcher` to use cached JWKS objects, but do note
|
||||
// that this object is not really designed to accommodate a large set of
|
||||
// arbitrary URLs. Use `jwk.CachedFetcher` as the first argument if you only
|
||||
// have a small set of URLs that you trust. For anything more complex, you should
|
||||
// implement your own `jwk.Fetcher` object.
|
||||
func WithVerifyAuto(f jwk.Fetcher, options ...jwk.FetchOption) VerifyOption {
|
||||
// the option MUST start with a "disallow no whitelist" to force
|
||||
// users provide a whitelist
|
||||
options = append(append([]jwk.FetchOption(nil), jwk.WithFetchWhitelist(allowNoneWhitelist)), options...)
|
||||
|
||||
return WithKeyProvider(jkuProvider{
|
||||
fetcher: f,
|
||||
options: options,
|
||||
})
|
||||
}
|
||||
|
||||
type withInsecureNoSignature struct {
|
||||
protected Headers
|
||||
}
|
||||
|
||||
// Protected exists as an escape hatch to modify the header values after the fact
|
||||
func (w *withInsecureNoSignature) Protected(v Headers) Headers {
|
||||
if w.protected == nil && v != nil {
|
||||
w.protected = v
|
||||
}
|
||||
return w.protected
|
||||
}
|
||||
|
||||
// WithInsecureNoSignature creates an option that allows the user to use the
|
||||
// "none" signature algorithm.
|
||||
//
|
||||
// Please note that this is insecure, and should never be used in production
|
||||
// (this is exactly why specifying "none"/jwa.NoSignature to `jws.WithKey()`
|
||||
// results in an error when `jws.Sign()` is called -- we do not allow using
|
||||
// "none" by accident)
|
||||
//
|
||||
// TODO: create specific suboption set for this option
|
||||
func WithInsecureNoSignature(options ...WithKeySuboption) SignOption {
|
||||
var protected Headers
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identProtectedHeaders{}:
|
||||
if err := option.Value(&protected); err != nil {
|
||||
panic(`jws.WithInsecureNoSignature() option must be of type Headers`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &signOption{
|
||||
option.New(identInsecureNoSignature{},
|
||||
&withInsecureNoSignature{
|
||||
protected: protected,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
package_name: jws
|
||||
output: jws/options_gen.go
|
||||
interfaces:
|
||||
- name: CompactOption
|
||||
comment: |
|
||||
CompactOption describes options that can be passed to `jws.Compact`
|
||||
- name: VerifyOption
|
||||
comment: |
|
||||
VerifyOption describes options that can be passed to `jws.Verify`
|
||||
methods:
|
||||
- verifyOption
|
||||
- parseOption
|
||||
- name: SignOption
|
||||
comment: |
|
||||
SignOption describes options that can be passed to `jws.Sign`
|
||||
- name: SignVerifyOption
|
||||
methods:
|
||||
- signOption
|
||||
- verifyOption
|
||||
- parseOption
|
||||
comment: |
|
||||
SignVerifyOption describes options that can be passed to either `jws.Verify` or `jws.Sign`
|
||||
- name: SignVerifyCompactOption
|
||||
methods:
|
||||
- signOption
|
||||
- verifyOption
|
||||
- compactOption
|
||||
- parseOption
|
||||
comment: |
|
||||
SignVerifyCompactOption describes options that can be passed to either `jws.Verify`,
|
||||
`jws.Sign`, or `jws.Compact`
|
||||
- name: WithJSONSuboption
|
||||
concrete_type: withJSONSuboption
|
||||
comment: |
|
||||
JSONSuboption describes suboptions that can be passed to the `jws.WithJSON()` option.
|
||||
- name: WithKeySuboption
|
||||
comment: |
|
||||
WithKeySuboption describes option types that can be passed to the `jws.WithKey()`
|
||||
option.
|
||||
- name: WithKeySetSuboption
|
||||
comment: |
|
||||
WithKeySetSuboption is a suboption passed to the `jws.WithKeySet()` option
|
||||
- name: ParseOption
|
||||
methods:
|
||||
- readFileOption
|
||||
comment: |
|
||||
ReadFileOption is a type of `Option` that can be passed to `jwe.Parse`
|
||||
- name: ReadFileOption
|
||||
comment: |
|
||||
ReadFileOption is a type of `Option` that can be passed to `jws.ReadFile`
|
||||
- name: SignVerifyParseOption
|
||||
methods:
|
||||
- signOption
|
||||
- verifyOption
|
||||
- parseOption
|
||||
- readFileOption
|
||||
- name: GlobalOption
|
||||
comment: |
|
||||
GlobalOption can be passed to `jws.Settings()` to set global options for the JWS package.
|
||||
options:
|
||||
- ident: Key
|
||||
skip_option: true
|
||||
- ident: Serialization
|
||||
skip_option: true
|
||||
- ident: Serialization
|
||||
option_name: WithCompact
|
||||
interface: SignVerifyParseOption
|
||||
constant_value: fmtCompact
|
||||
comment: |
|
||||
WithCompact specifies that the result of `jws.Sign()` is serialized in
|
||||
compact format.
|
||||
|
||||
By default `jws.Sign()` will opt to use compact format, so you usually
|
||||
do not need to specify this option other than to be explicit about it
|
||||
- ident: Detached
|
||||
interface: CompactOption
|
||||
argument_type: bool
|
||||
comment: |
|
||||
WithDetached specifies that the `jws.Message` should be serialized in
|
||||
JWS compact serialization with detached payload. The resulting octet
|
||||
sequence will not contain the payload section.
|
||||
|
||||
- ident: DetachedPayload
|
||||
interface: SignVerifyOption
|
||||
argument_type: '[]byte'
|
||||
comment: |
|
||||
WithDetachedPayload can be used to both sign or verify a JWS message with a
|
||||
detached payload.
|
||||
Note that this option does NOT populate the `b64` header, which is sometimes
|
||||
required by other JWS implementations.
|
||||
|
||||
|
||||
When this option is used for `jws.Sign()`, the first parameter (normally the payload)
|
||||
must be set to `nil`.
|
||||
|
||||
If you have to verify using this option, you should know exactly how and why this works.
|
||||
- ident: Base64Encoder
|
||||
interface: SignVerifyCompactOption
|
||||
argument_type: Base64Encoder
|
||||
comment: |
|
||||
WithBase64Encoder specifies the base64 encoder to be used while signing or
|
||||
verifying the JWS message. By default, the raw URL base64 encoding (no padding)
|
||||
is used.
|
||||
- ident: Message
|
||||
interface: VerifyOption
|
||||
argument_type: '*Message'
|
||||
comment: |
|
||||
WithMessage can be passed to Verify() to obtain the jws.Message upon
|
||||
a successful verification.
|
||||
- ident: KeyUsed
|
||||
interface: VerifyOption
|
||||
argument_type: 'any'
|
||||
comment: |
|
||||
WithKeyUsed allows you to specify the `jws.Verify()` function to
|
||||
return the key used for verification. This may be useful when
|
||||
you specify multiple key sources or if you pass a `jwk.Set`
|
||||
and you want to know which key was successful at verifying the
|
||||
signature.
|
||||
|
||||
`v` must be a pointer to an empty `any`. Do not use
|
||||
`jwk.Key` here unless you are 100% sure that all keys that you
|
||||
have provided are instances of `jwk.Key` (remember that the
|
||||
jwx API allows users to specify a raw key such as *rsa.PublicKey)
|
||||
- ident: ValidateKey
|
||||
interface: SignVerifyOption
|
||||
argument_type: bool
|
||||
comment: |
|
||||
WithValidateKey specifies whether the key used for signing or verification
|
||||
should be validated before using. Note that this means calling
|
||||
`key.Validate()` on the key, which in turn means that your key
|
||||
must be a `jwk.Key` instance, or a key that can be converted to
|
||||
a `jwk.Key` by calling `jwk.Import()`. This means that your
|
||||
custom hardware-backed keys will probably not work.
|
||||
|
||||
You can directly call `key.Validate()` yourself if you need to
|
||||
mix keys that cannot be converted to `jwk.Key`.
|
||||
|
||||
Please also note that use of this option will also result in
|
||||
one extra conversion of raw keys to a `jwk.Key` instance. If you
|
||||
care about shaving off as much as possible, consider using a
|
||||
pre-validated key instead of using this option to validate
|
||||
the key on-demand each time.
|
||||
|
||||
By default, the key is not validated.
|
||||
- ident: InferAlgorithmFromKey
|
||||
interface: WithKeySetSuboption
|
||||
argument_type: bool
|
||||
comment: |
|
||||
WithInferAlgorithmFromKey specifies whether the JWS signing algorithm name
|
||||
should be inferred by looking at the provided key, in case the JWS
|
||||
message or the key does not have a proper `alg` header.
|
||||
|
||||
When this option is set to true, a list of algorithm(s) that is compatible
|
||||
with the key type will be enumerated, and _ALL_ of them will be tried
|
||||
against the key/message pair. If any of them succeeds, the verification
|
||||
will be considered successful.
|
||||
|
||||
Compared to providing explicit `alg` from the key this is slower, and
|
||||
verification may fail to verify if somehow our heuristics are wrong
|
||||
or outdated.
|
||||
|
||||
Also, automatic detection of signature verification methods are always
|
||||
more vulnerable for potential attack vectors.
|
||||
|
||||
It is highly recommended that you fix your key to contain a proper `alg`
|
||||
header field instead of resorting to using this option, but sometimes
|
||||
it just needs to happen.
|
||||
- ident: UseDefault
|
||||
interface: WithKeySetSuboption
|
||||
argument_type: bool
|
||||
comment: |
|
||||
WithUseDefault specifies that if and only if a jwk.Key contains
|
||||
exactly one jwk.Key, that key should be used.
|
||||
- ident: RequireKid
|
||||
interface: WithKeySetSuboption
|
||||
argument_type: bool
|
||||
comment: |
|
||||
WithRequiredKid specifies whether the keys in the jwk.Set should
|
||||
only be matched if the target JWS message's Key ID and the Key ID
|
||||
in the given key matches.
|
||||
- ident: MultipleKeysPerKeyID
|
||||
interface: WithKeySetSuboption
|
||||
argument_type: bool
|
||||
comment: |
|
||||
WithMultipleKeysPerKeyID specifies if we should expect multiple keys
|
||||
to match against a key ID. By default it is assumed that key IDs are
|
||||
unique, i.e. for a given key ID, the key set only contains a single
|
||||
key that has the matching ID. When this option is set to true,
|
||||
multiple keys that match the same key ID in the set can be tried.
|
||||
- ident: Pretty
|
||||
interface: WithJSONSuboption
|
||||
argument_type: bool
|
||||
comment: |
|
||||
WithPretty specifies whether the JSON output should be formatted and
|
||||
indented
|
||||
- ident: KeyProvider
|
||||
interface: VerifyOption
|
||||
argument_type: KeyProvider
|
||||
- ident: Context
|
||||
interface: VerifyOption
|
||||
argument_type: context.Context
|
||||
- ident: ProtectedHeaders
|
||||
interface: WithKeySuboption
|
||||
argument_type: Headers
|
||||
comment: |
|
||||
WithProtected is used with `jws.WithKey()` option when used with `jws.Sign()`
|
||||
to specify a protected header to be attached to the JWS signature.
|
||||
|
||||
It has no effect if used when `jws.WithKey()` is passed to `jws.Verify()`
|
||||
- ident: PublicHeaders
|
||||
interface: WithKeySuboption
|
||||
argument_type: Headers
|
||||
comment: |
|
||||
WithPublic is used with `jws.WithKey()` option when used with `jws.Sign()`
|
||||
to specify a public header to be attached to the JWS signature.
|
||||
|
||||
It has no effect if used when `jws.WithKey()` is passed to `jws.Verify()`
|
||||
|
||||
`jws.Sign()` will result in an error if `jws.WithPublic()` is used
|
||||
and the serialization format is compact serialization.
|
||||
- ident: FS
|
||||
interface: ReadFileOption
|
||||
argument_type: fs.FS
|
||||
comment: |
|
||||
WithFS specifies the source `fs.FS` object to read the file from.
|
||||
- ident: LegacySigners
|
||||
interface: GlobalOption
|
||||
constant_value: true
|
||||
comment: |
|
||||
WithLegacySigners is a no-op option that exists only for backwards compatibility.
|
||||
+445
@@ -0,0 +1,445 @@
|
||||
// Code generated by tools/cmd/genoptions/main.go. DO NOT EDIT.
|
||||
|
||||
package jws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
|
||||
"github.com/lestrrat-go/option/v2"
|
||||
)
|
||||
|
||||
type Option = option.Interface
|
||||
|
||||
// CompactOption describes options that can be passed to `jws.Compact`
|
||||
type CompactOption interface {
|
||||
Option
|
||||
compactOption()
|
||||
}
|
||||
|
||||
type compactOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*compactOption) compactOption() {}
|
||||
|
||||
// GlobalOption can be passed to `jws.Settings()` to set global options for the JWS package.
|
||||
type GlobalOption interface {
|
||||
Option
|
||||
globalOption()
|
||||
}
|
||||
|
||||
type globalOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*globalOption) globalOption() {}
|
||||
|
||||
// ReadFileOption is a type of `Option` that can be passed to `jwe.Parse`
|
||||
type ParseOption interface {
|
||||
Option
|
||||
readFileOption()
|
||||
}
|
||||
|
||||
type parseOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*parseOption) readFileOption() {}
|
||||
|
||||
// ReadFileOption is a type of `Option` that can be passed to `jws.ReadFile`
|
||||
type ReadFileOption interface {
|
||||
Option
|
||||
readFileOption()
|
||||
}
|
||||
|
||||
type readFileOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*readFileOption) readFileOption() {}
|
||||
|
||||
// SignOption describes options that can be passed to `jws.Sign`
|
||||
type SignOption interface {
|
||||
Option
|
||||
signOption()
|
||||
}
|
||||
|
||||
type signOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*signOption) signOption() {}
|
||||
|
||||
// SignVerifyCompactOption describes options that can be passed to either `jws.Verify`,
|
||||
// `jws.Sign`, or `jws.Compact`
|
||||
type SignVerifyCompactOption interface {
|
||||
Option
|
||||
signOption()
|
||||
verifyOption()
|
||||
compactOption()
|
||||
parseOption()
|
||||
}
|
||||
|
||||
type signVerifyCompactOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*signVerifyCompactOption) signOption() {}
|
||||
|
||||
func (*signVerifyCompactOption) verifyOption() {}
|
||||
|
||||
func (*signVerifyCompactOption) compactOption() {}
|
||||
|
||||
func (*signVerifyCompactOption) parseOption() {}
|
||||
|
||||
// SignVerifyOption describes options that can be passed to either `jws.Verify` or `jws.Sign`
|
||||
type SignVerifyOption interface {
|
||||
Option
|
||||
signOption()
|
||||
verifyOption()
|
||||
parseOption()
|
||||
}
|
||||
|
||||
type signVerifyOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*signVerifyOption) signOption() {}
|
||||
|
||||
func (*signVerifyOption) verifyOption() {}
|
||||
|
||||
func (*signVerifyOption) parseOption() {}
|
||||
|
||||
type SignVerifyParseOption interface {
|
||||
Option
|
||||
signOption()
|
||||
verifyOption()
|
||||
parseOption()
|
||||
readFileOption()
|
||||
}
|
||||
|
||||
type signVerifyParseOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*signVerifyParseOption) signOption() {}
|
||||
|
||||
func (*signVerifyParseOption) verifyOption() {}
|
||||
|
||||
func (*signVerifyParseOption) parseOption() {}
|
||||
|
||||
func (*signVerifyParseOption) readFileOption() {}
|
||||
|
||||
// VerifyOption describes options that can be passed to `jws.Verify`
|
||||
type VerifyOption interface {
|
||||
Option
|
||||
verifyOption()
|
||||
parseOption()
|
||||
}
|
||||
|
||||
type verifyOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*verifyOption) verifyOption() {}
|
||||
|
||||
func (*verifyOption) parseOption() {}
|
||||
|
||||
// JSONSuboption describes suboptions that can be passed to the `jws.WithJSON()` option.
|
||||
type WithJSONSuboption interface {
|
||||
Option
|
||||
withJSONSuboption()
|
||||
}
|
||||
|
||||
type withJSONSuboption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*withJSONSuboption) withJSONSuboption() {}
|
||||
|
||||
// WithKeySetSuboption is a suboption passed to the `jws.WithKeySet()` option
|
||||
type WithKeySetSuboption interface {
|
||||
Option
|
||||
withKeySetSuboption()
|
||||
}
|
||||
|
||||
type withKeySetSuboption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*withKeySetSuboption) withKeySetSuboption() {}
|
||||
|
||||
// WithKeySuboption describes option types that can be passed to the `jws.WithKey()`
|
||||
// option.
|
||||
type WithKeySuboption interface {
|
||||
Option
|
||||
withKeySuboption()
|
||||
}
|
||||
|
||||
type withKeySuboption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*withKeySuboption) withKeySuboption() {}
|
||||
|
||||
type identBase64Encoder struct{}
|
||||
type identContext struct{}
|
||||
type identDetached struct{}
|
||||
type identDetachedPayload struct{}
|
||||
type identFS struct{}
|
||||
type identInferAlgorithmFromKey struct{}
|
||||
type identKey struct{}
|
||||
type identKeyProvider struct{}
|
||||
type identKeyUsed struct{}
|
||||
type identLegacySigners struct{}
|
||||
type identMessage struct{}
|
||||
type identMultipleKeysPerKeyID struct{}
|
||||
type identPretty struct{}
|
||||
type identProtectedHeaders struct{}
|
||||
type identPublicHeaders struct{}
|
||||
type identRequireKid struct{}
|
||||
type identSerialization struct{}
|
||||
type identUseDefault struct{}
|
||||
type identValidateKey struct{}
|
||||
|
||||
func (identBase64Encoder) String() string {
|
||||
return "WithBase64Encoder"
|
||||
}
|
||||
|
||||
func (identContext) String() string {
|
||||
return "WithContext"
|
||||
}
|
||||
|
||||
func (identDetached) String() string {
|
||||
return "WithDetached"
|
||||
}
|
||||
|
||||
func (identDetachedPayload) String() string {
|
||||
return "WithDetachedPayload"
|
||||
}
|
||||
|
||||
func (identFS) String() string {
|
||||
return "WithFS"
|
||||
}
|
||||
|
||||
func (identInferAlgorithmFromKey) String() string {
|
||||
return "WithInferAlgorithmFromKey"
|
||||
}
|
||||
|
||||
func (identKey) String() string {
|
||||
return "WithKey"
|
||||
}
|
||||
|
||||
func (identKeyProvider) String() string {
|
||||
return "WithKeyProvider"
|
||||
}
|
||||
|
||||
func (identKeyUsed) String() string {
|
||||
return "WithKeyUsed"
|
||||
}
|
||||
|
||||
func (identLegacySigners) String() string {
|
||||
return "WithLegacySigners"
|
||||
}
|
||||
|
||||
func (identMessage) String() string {
|
||||
return "WithMessage"
|
||||
}
|
||||
|
||||
func (identMultipleKeysPerKeyID) String() string {
|
||||
return "WithMultipleKeysPerKeyID"
|
||||
}
|
||||
|
||||
func (identPretty) String() string {
|
||||
return "WithPretty"
|
||||
}
|
||||
|
||||
func (identProtectedHeaders) String() string {
|
||||
return "WithProtectedHeaders"
|
||||
}
|
||||
|
||||
func (identPublicHeaders) String() string {
|
||||
return "WithPublicHeaders"
|
||||
}
|
||||
|
||||
func (identRequireKid) String() string {
|
||||
return "WithRequireKid"
|
||||
}
|
||||
|
||||
func (identSerialization) String() string {
|
||||
return "WithSerialization"
|
||||
}
|
||||
|
||||
func (identUseDefault) String() string {
|
||||
return "WithUseDefault"
|
||||
}
|
||||
|
||||
func (identValidateKey) String() string {
|
||||
return "WithValidateKey"
|
||||
}
|
||||
|
||||
// WithBase64Encoder specifies the base64 encoder to be used while signing or
|
||||
// verifying the JWS message. By default, the raw URL base64 encoding (no padding)
|
||||
// is used.
|
||||
func WithBase64Encoder(v Base64Encoder) SignVerifyCompactOption {
|
||||
return &signVerifyCompactOption{option.New(identBase64Encoder{}, v)}
|
||||
}
|
||||
|
||||
func WithContext(v context.Context) VerifyOption {
|
||||
return &verifyOption{option.New(identContext{}, v)}
|
||||
}
|
||||
|
||||
// WithDetached specifies that the `jws.Message` should be serialized in
|
||||
// JWS compact serialization with detached payload. The resulting octet
|
||||
// sequence will not contain the payload section.
|
||||
func WithDetached(v bool) CompactOption {
|
||||
return &compactOption{option.New(identDetached{}, v)}
|
||||
}
|
||||
|
||||
// WithDetachedPayload can be used to both sign or verify a JWS message with a
|
||||
// detached payload.
|
||||
// Note that this option does NOT populate the `b64` header, which is sometimes
|
||||
// required by other JWS implementations.
|
||||
//
|
||||
// When this option is used for `jws.Sign()`, the first parameter (normally the payload)
|
||||
// must be set to `nil`.
|
||||
//
|
||||
// If you have to verify using this option, you should know exactly how and why this works.
|
||||
func WithDetachedPayload(v []byte) SignVerifyOption {
|
||||
return &signVerifyOption{option.New(identDetachedPayload{}, v)}
|
||||
}
|
||||
|
||||
// WithFS specifies the source `fs.FS` object to read the file from.
|
||||
func WithFS(v fs.FS) ReadFileOption {
|
||||
return &readFileOption{option.New(identFS{}, v)}
|
||||
}
|
||||
|
||||
// WithInferAlgorithmFromKey specifies whether the JWS signing algorithm name
|
||||
// should be inferred by looking at the provided key, in case the JWS
|
||||
// message or the key does not have a proper `alg` header.
|
||||
//
|
||||
// When this option is set to true, a list of algorithm(s) that is compatible
|
||||
// with the key type will be enumerated, and _ALL_ of them will be tried
|
||||
// against the key/message pair. If any of them succeeds, the verification
|
||||
// will be considered successful.
|
||||
//
|
||||
// Compared to providing explicit `alg` from the key this is slower, and
|
||||
// verification may fail to verify if somehow our heuristics are wrong
|
||||
// or outdated.
|
||||
//
|
||||
// Also, automatic detection of signature verification methods are always
|
||||
// more vulnerable for potential attack vectors.
|
||||
//
|
||||
// It is highly recommended that you fix your key to contain a proper `alg`
|
||||
// header field instead of resorting to using this option, but sometimes
|
||||
// it just needs to happen.
|
||||
func WithInferAlgorithmFromKey(v bool) WithKeySetSuboption {
|
||||
return &withKeySetSuboption{option.New(identInferAlgorithmFromKey{}, v)}
|
||||
}
|
||||
|
||||
func WithKeyProvider(v KeyProvider) VerifyOption {
|
||||
return &verifyOption{option.New(identKeyProvider{}, v)}
|
||||
}
|
||||
|
||||
// WithKeyUsed allows you to specify the `jws.Verify()` function to
|
||||
// return the key used for verification. This may be useful when
|
||||
// you specify multiple key sources or if you pass a `jwk.Set`
|
||||
// and you want to know which key was successful at verifying the
|
||||
// signature.
|
||||
//
|
||||
// `v` must be a pointer to an empty `any`. Do not use
|
||||
// `jwk.Key` here unless you are 100% sure that all keys that you
|
||||
// have provided are instances of `jwk.Key` (remember that the
|
||||
// jwx API allows users to specify a raw key such as *rsa.PublicKey)
|
||||
func WithKeyUsed(v any) VerifyOption {
|
||||
return &verifyOption{option.New(identKeyUsed{}, v)}
|
||||
}
|
||||
|
||||
// WithLegacySigners is a no-op option that exists only for backwards compatibility.
|
||||
func WithLegacySigners() GlobalOption {
|
||||
return &globalOption{option.New(identLegacySigners{}, true)}
|
||||
}
|
||||
|
||||
// WithMessage can be passed to Verify() to obtain the jws.Message upon
|
||||
// a successful verification.
|
||||
func WithMessage(v *Message) VerifyOption {
|
||||
return &verifyOption{option.New(identMessage{}, v)}
|
||||
}
|
||||
|
||||
// WithMultipleKeysPerKeyID specifies if we should expect multiple keys
|
||||
// to match against a key ID. By default it is assumed that key IDs are
|
||||
// unique, i.e. for a given key ID, the key set only contains a single
|
||||
// key that has the matching ID. When this option is set to true,
|
||||
// multiple keys that match the same key ID in the set can be tried.
|
||||
func WithMultipleKeysPerKeyID(v bool) WithKeySetSuboption {
|
||||
return &withKeySetSuboption{option.New(identMultipleKeysPerKeyID{}, v)}
|
||||
}
|
||||
|
||||
// WithPretty specifies whether the JSON output should be formatted and
|
||||
// indented
|
||||
func WithPretty(v bool) WithJSONSuboption {
|
||||
return &withJSONSuboption{option.New(identPretty{}, v)}
|
||||
}
|
||||
|
||||
// WithProtected is used with `jws.WithKey()` option when used with `jws.Sign()`
|
||||
// to specify a protected header to be attached to the JWS signature.
|
||||
//
|
||||
// It has no effect if used when `jws.WithKey()` is passed to `jws.Verify()`
|
||||
func WithProtectedHeaders(v Headers) WithKeySuboption {
|
||||
return &withKeySuboption{option.New(identProtectedHeaders{}, v)}
|
||||
}
|
||||
|
||||
// WithPublic is used with `jws.WithKey()` option when used with `jws.Sign()`
|
||||
// to specify a public header to be attached to the JWS signature.
|
||||
//
|
||||
// It has no effect if used when `jws.WithKey()` is passed to `jws.Verify()`
|
||||
//
|
||||
// `jws.Sign()` will result in an error if `jws.WithPublic()` is used
|
||||
// and the serialization format is compact serialization.
|
||||
func WithPublicHeaders(v Headers) WithKeySuboption {
|
||||
return &withKeySuboption{option.New(identPublicHeaders{}, v)}
|
||||
}
|
||||
|
||||
// WithRequiredKid specifies whether the keys in the jwk.Set should
|
||||
// only be matched if the target JWS message's Key ID and the Key ID
|
||||
// in the given key matches.
|
||||
func WithRequireKid(v bool) WithKeySetSuboption {
|
||||
return &withKeySetSuboption{option.New(identRequireKid{}, v)}
|
||||
}
|
||||
|
||||
// WithCompact specifies that the result of `jws.Sign()` is serialized in
|
||||
// compact format.
|
||||
//
|
||||
// By default `jws.Sign()` will opt to use compact format, so you usually
|
||||
// do not need to specify this option other than to be explicit about it
|
||||
func WithCompact() SignVerifyParseOption {
|
||||
return &signVerifyParseOption{option.New(identSerialization{}, fmtCompact)}
|
||||
}
|
||||
|
||||
// WithUseDefault specifies that if and only if a jwk.Key contains
|
||||
// exactly one jwk.Key, that key should be used.
|
||||
func WithUseDefault(v bool) WithKeySetSuboption {
|
||||
return &withKeySetSuboption{option.New(identUseDefault{}, v)}
|
||||
}
|
||||
|
||||
// WithValidateKey specifies whether the key used for signing or verification
|
||||
// should be validated before using. Note that this means calling
|
||||
// `key.Validate()` on the key, which in turn means that your key
|
||||
// must be a `jwk.Key` instance, or a key that can be converted to
|
||||
// a `jwk.Key` by calling `jwk.Import()`. This means that your
|
||||
// custom hardware-backed keys will probably not work.
|
||||
//
|
||||
// You can directly call `key.Validate()` yourself if you need to
|
||||
// mix keys that cannot be converted to `jwk.Key`.
|
||||
//
|
||||
// Please also note that use of this option will also result in
|
||||
// one extra conversion of raw keys to a `jwk.Key` instance. If you
|
||||
// care about shaving off as much as possible, consider using a
|
||||
// pre-validated key instead of using this option to validate
|
||||
// the key on-demand each time.
|
||||
//
|
||||
// By default, the key is not validated.
|
||||
func WithValidateKey(v bool) SignVerifyOption {
|
||||
return &signVerifyOption{option.New(identValidateKey{}, v)}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package jws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/base64"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/pool"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
)
|
||||
|
||||
type signContext struct {
|
||||
format int
|
||||
detached bool
|
||||
validateKey bool
|
||||
payload []byte
|
||||
encoder Base64Encoder
|
||||
none *signatureBuilder // special signature builder
|
||||
sigbuilders []*signatureBuilder
|
||||
}
|
||||
|
||||
var signContextPool = pool.New[*signContext](allocSignContext, freeSignContext)
|
||||
|
||||
func allocSignContext() *signContext {
|
||||
return &signContext{
|
||||
format: fmtCompact,
|
||||
sigbuilders: make([]*signatureBuilder, 0, 1),
|
||||
encoder: base64.DefaultEncoder(),
|
||||
}
|
||||
}
|
||||
|
||||
func freeSignContext(ctx *signContext) *signContext {
|
||||
ctx.format = fmtCompact
|
||||
for _, sb := range ctx.sigbuilders {
|
||||
signatureBuilderPool.Put(sb)
|
||||
}
|
||||
ctx.sigbuilders = ctx.sigbuilders[:0]
|
||||
ctx.detached = false
|
||||
ctx.validateKey = false
|
||||
ctx.encoder = base64.DefaultEncoder()
|
||||
ctx.none = nil
|
||||
ctx.payload = nil
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (sc *signContext) ProcessOptions(options []SignOption) error {
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identSerialization{}:
|
||||
if err := option.Value(&sc.format); err != nil {
|
||||
return signerr(`failed to retrieve serialization option value: %w`, err)
|
||||
}
|
||||
case identInsecureNoSignature{}:
|
||||
var data withInsecureNoSignature
|
||||
if err := option.Value(&data); err != nil {
|
||||
return signerr(`failed to retrieve insecure-no-signature option value: %w`, err)
|
||||
}
|
||||
sb := signatureBuilderPool.Get()
|
||||
sb.alg = jwa.NoSignature()
|
||||
sb.protected = data.protected
|
||||
sb.signer = noneSigner{}
|
||||
sc.none = sb
|
||||
sc.sigbuilders = append(sc.sigbuilders, sb)
|
||||
case identKey{}:
|
||||
var data *withKey
|
||||
if err := option.Value(&data); err != nil {
|
||||
return signerr(`jws.Sign: invalid value for WithKey option: %w`, err)
|
||||
}
|
||||
|
||||
alg, ok := data.alg.(jwa.SignatureAlgorithm)
|
||||
if !ok {
|
||||
return signerr(`expected algorithm to be of type jwa.SignatureAlgorithm but got (%[1]q, %[1]T)`, data.alg)
|
||||
}
|
||||
|
||||
// No, we don't accept "none" here.
|
||||
if alg == jwa.NoSignature() {
|
||||
return signerr(`"none" (jwa.NoSignature) cannot be used with jws.WithKey`)
|
||||
}
|
||||
|
||||
sb := signatureBuilderPool.Get()
|
||||
sb.alg = alg
|
||||
sb.protected = data.protected
|
||||
sb.key = data.key
|
||||
sb.public = data.public
|
||||
|
||||
s2, err := SignerFor(alg)
|
||||
if err == nil {
|
||||
sb.signer2 = s2
|
||||
} else {
|
||||
s1, err := legacySignerFor(alg)
|
||||
if err != nil {
|
||||
sb.signer2 = defaultSigner{alg: alg}
|
||||
} else {
|
||||
sb.signer = s1
|
||||
}
|
||||
}
|
||||
|
||||
sc.sigbuilders = append(sc.sigbuilders, sb)
|
||||
case identDetachedPayload{}:
|
||||
if sc.payload != nil {
|
||||
return signerr(`payload must be nil when jws.WithDetachedPayload() is specified`)
|
||||
}
|
||||
if err := option.Value(&sc.payload); err != nil {
|
||||
return signerr(`failed to retrieve detached payload option value: %w`, err)
|
||||
}
|
||||
sc.detached = true
|
||||
case identValidateKey{}:
|
||||
if err := option.Value(&sc.validateKey); err != nil {
|
||||
return signerr(`failed to retrieve validate-key option value: %w`, err)
|
||||
}
|
||||
case identBase64Encoder{}:
|
||||
if err := option.Value(&sc.encoder); err != nil {
|
||||
return signerr(`failed to retrieve base64-encoder option value: %w`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sc *signContext) PopulateMessage(m *Message) error {
|
||||
m.payload = sc.payload
|
||||
m.signatures = make([]*Signature, 0, len(sc.sigbuilders))
|
||||
|
||||
for i, sb := range sc.sigbuilders {
|
||||
// Create signature for each builders
|
||||
if sc.validateKey {
|
||||
if err := validateKeyBeforeUse(sb.key); err != nil {
|
||||
return fmt.Errorf(`failed to validate key for signature %d: %w`, i, err)
|
||||
}
|
||||
}
|
||||
|
||||
sig, err := sb.Build(sc, m.payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to build signature %d: %w`, i, err)
|
||||
}
|
||||
|
||||
m.signatures = append(m.signatures, sig)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package jws
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/json"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/pool"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/tokens"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
"github.com/lestrrat-go/jwx/v3/jws/jwsbb"
|
||||
)
|
||||
|
||||
var signatureBuilderPool = pool.New[*signatureBuilder](allocSignatureBuilder, freeSignatureBuilder)
|
||||
|
||||
// signatureBuilder is a transient object that is used to build
|
||||
// a single JWS signature.
|
||||
//
|
||||
// In a multi-signature JWS message, each message is paired with
|
||||
// the following:
|
||||
// - a signer (the object that takes a buffer and key and generates a signature)
|
||||
// - a key (the key that is used to sign the payload)
|
||||
// - protected headers (the headers that are protected by the signature)
|
||||
// - public headers (the headers that are not protected by the signature)
|
||||
//
|
||||
// This object stores all of this information in one place.
|
||||
//
|
||||
// This object does NOT take care of any synchronization, because it is
|
||||
// meant to be used in a single-threaded context.
|
||||
type signatureBuilder struct {
|
||||
alg jwa.SignatureAlgorithm
|
||||
signer Signer
|
||||
signer2 Signer2
|
||||
key any
|
||||
protected Headers
|
||||
public Headers
|
||||
}
|
||||
|
||||
func allocSignatureBuilder() *signatureBuilder {
|
||||
return &signatureBuilder{}
|
||||
}
|
||||
|
||||
func freeSignatureBuilder(sb *signatureBuilder) *signatureBuilder {
|
||||
sb.alg = jwa.EmptySignatureAlgorithm()
|
||||
sb.signer = nil
|
||||
sb.signer2 = nil
|
||||
sb.key = nil
|
||||
sb.protected = nil
|
||||
sb.public = nil
|
||||
return sb
|
||||
}
|
||||
|
||||
func (sb *signatureBuilder) Build(sc *signContext, payload []byte) (*Signature, error) {
|
||||
protected := sb.protected
|
||||
if protected == nil {
|
||||
protected = NewHeaders()
|
||||
}
|
||||
|
||||
if err := protected.Set(AlgorithmKey, sb.alg); err != nil {
|
||||
return nil, signerr(`failed to set "alg" header: %w`, err)
|
||||
}
|
||||
|
||||
if key, ok := sb.key.(jwk.Key); ok {
|
||||
if kid, ok := key.KeyID(); ok && kid != "" {
|
||||
if err := protected.Set(KeyIDKey, kid); err != nil {
|
||||
return nil, signerr(`failed to set "kid" header: %w`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hdrs, err := mergeHeaders(sb.public, protected)
|
||||
if err != nil {
|
||||
return nil, signerr(`failed to merge headers: %w`, err)
|
||||
}
|
||||
|
||||
// raw, json format headers
|
||||
hdrbuf, err := json.Marshal(hdrs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to marshal headers: %w`, err)
|
||||
}
|
||||
|
||||
// check if we need to base64 encode the payload
|
||||
b64 := getB64Value(hdrs)
|
||||
if !b64 && !sc.detached {
|
||||
if bytes.IndexByte(payload, tokens.Period) != -1 {
|
||||
return nil, fmt.Errorf(`payload must not contain a "."`)
|
||||
}
|
||||
}
|
||||
|
||||
combined := jwsbb.SignBuffer(nil, hdrbuf, payload, sc.encoder, b64)
|
||||
|
||||
var sig Signature
|
||||
sig.protected = protected
|
||||
sig.headers = sb.public
|
||||
|
||||
if sb.signer2 != nil {
|
||||
signature, err := sb.signer2.Sign(sb.key, combined)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to sign payload: %w`, err)
|
||||
}
|
||||
sig.signature = signature
|
||||
return &sig, nil
|
||||
}
|
||||
|
||||
if sb.signer == nil {
|
||||
panic("can't get here")
|
||||
}
|
||||
|
||||
signature, err := sb.signer.Sign(combined, sb.key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to sign payload: %w`, err)
|
||||
}
|
||||
|
||||
sig.signature = signature
|
||||
|
||||
return &sig, nil
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
package jws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
)
|
||||
|
||||
// Signer2 is an interface that represents a per-signature algorithm signing
|
||||
// operation.
|
||||
type Signer2 interface {
|
||||
Algorithm() jwa.SignatureAlgorithm
|
||||
|
||||
// Sign takes a key and a payload, and returns the signature for the payload.
|
||||
// The key type is restricted by the signature algorithm that this
|
||||
// signer is associated with.
|
||||
//
|
||||
// (Note to users of legacy Signer interface: the method signature
|
||||
// is different from the legacy Signer interface)
|
||||
Sign(key any, payload []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
var muSigner2DB sync.RWMutex
|
||||
var signer2DB = make(map[jwa.SignatureAlgorithm]Signer2)
|
||||
|
||||
type SignerFactory interface {
|
||||
Create() (Signer, error)
|
||||
}
|
||||
type SignerFactoryFn func() (Signer, error)
|
||||
|
||||
func (fn SignerFactoryFn) Create() (Signer, error) {
|
||||
return fn()
|
||||
}
|
||||
|
||||
func init() {
|
||||
// register the signers using jwsbb. These will be used by default.
|
||||
for _, alg := range jwa.SignatureAlgorithms() {
|
||||
if alg == jwa.NoSignature() {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := RegisterSigner(alg, defaultSigner{alg: alg}); err != nil {
|
||||
panic(fmt.Sprintf("RegisterSigner failed: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SignerFor returns a Signer2 for the given signature algorithm.
|
||||
//
|
||||
// Currently, this function will never fail. It will always return a
|
||||
// valid Signer2 object. The heuristic is as follows:
|
||||
// 1. If a Signer2 is registered for the given algorithm, it will return that.
|
||||
// 2. If a legacy Signer(Factory) is registered for the given algorithm, it will
|
||||
// return a Signer2 that wraps the legacy Signer.
|
||||
// 3. If no Signer2 or legacy Signer(Factory) is registered, it will return a
|
||||
// default signer that uses jwsbb.Sign.
|
||||
//
|
||||
// 1 and 2 will take care of 99% of the cases. The only time 3 will happen is
|
||||
// when you are using a custom algorithm that is not supported out of the box.
|
||||
//
|
||||
// jwsbb.Sign knows how to handle a static set of algorithms, so if the
|
||||
// algorithm is not supported, it will return an error when you call
|
||||
// `Sign` on the default signer.
|
||||
func SignerFor(alg jwa.SignatureAlgorithm) (Signer2, error) {
|
||||
muSigner2DB.RLock()
|
||||
defer muSigner2DB.RUnlock()
|
||||
|
||||
signer, ok := signer2DB[alg]
|
||||
if ok {
|
||||
return signer, nil
|
||||
}
|
||||
|
||||
s1, err := legacySignerFor(alg)
|
||||
if err == nil {
|
||||
return signerAdapter{signer: s1}, nil
|
||||
}
|
||||
|
||||
return defaultSigner{alg: alg}, nil
|
||||
}
|
||||
|
||||
var muSignerDB sync.RWMutex
|
||||
var signerDB = make(map[jwa.SignatureAlgorithm]SignerFactory)
|
||||
|
||||
// RegisterSigner is used to register a signer for the given
|
||||
// algorithm.
|
||||
//
|
||||
// Please note that this function is intended to be passed a
|
||||
// signer object as its second argument, but due to historical
|
||||
// reasons the function signature is defined as taking `any` type.
|
||||
//
|
||||
// You should create a signer object that implements the `Signer2`
|
||||
// interface to register a signer, unless you have legacy code that
|
||||
// plugged into the `SignerFactory` interface.
|
||||
//
|
||||
// Unlike the `UnregisterSigner` function, this function automatically
|
||||
// calls `jwa.RegisterSignatureAlgorithm` to register the algorithm
|
||||
// in this module's algorithm database.
|
||||
//
|
||||
// For backwards compatibility, this function also accepts
|
||||
// `SignerFactory` implementations, but this usage is deprecated.
|
||||
// You should use `Signer2` implementations instead.
|
||||
//
|
||||
// If you want to completely remove an algorithm, you must call
|
||||
// `jwa.UnregisterSignatureAlgorithm` yourself after calling
|
||||
// `UnregisterSigner`.
|
||||
func RegisterSigner(alg jwa.SignatureAlgorithm, f any) error {
|
||||
jwa.RegisterSignatureAlgorithm(alg)
|
||||
switch s := f.(type) {
|
||||
case Signer2:
|
||||
muSigner2DB.Lock()
|
||||
signer2DB[alg] = s
|
||||
muSigner2DB.Unlock()
|
||||
case SignerFactory:
|
||||
muSignerDB.Lock()
|
||||
signerDB[alg] = s
|
||||
muSignerDB.Unlock()
|
||||
default:
|
||||
return fmt.Errorf(`jws.RegisterSigner: unsupported type %T for algorithm %q`, f, alg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnregisterSigner removes the signer factory associated with
|
||||
// the given algorithm, as well as the signer instance created
|
||||
// by the factory.
|
||||
//
|
||||
// Note that when you call this function, the algorithm itself is
|
||||
// not automatically unregistered from this module's algorithm database.
|
||||
// This is because the algorithm may still be required for verification or
|
||||
// some other operation (however unlikely, it is still possible).
|
||||
// Therefore, in order to completely remove the algorithm, you must
|
||||
// call `jwa.UnregisterSignatureAlgorithm` yourself.
|
||||
func UnregisterSigner(alg jwa.SignatureAlgorithm) {
|
||||
muSigner2DB.Lock()
|
||||
delete(signer2DB, alg)
|
||||
muSigner2DB.Unlock()
|
||||
|
||||
muSignerDB.Lock()
|
||||
delete(signerDB, alg)
|
||||
muSignerDB.Unlock()
|
||||
// Remove previous signer
|
||||
removeSigner(alg)
|
||||
}
|
||||
|
||||
// NewSigner creates a signer that signs payloads using the given signature algorithm.
|
||||
// This function is deprecated, and will either be removed to re-purposed using
|
||||
// a different signature.
|
||||
//
|
||||
// When you want to load a Signer object, you should use `SignerFor()` instead.
|
||||
func NewSigner(alg jwa.SignatureAlgorithm) (Signer, error) {
|
||||
s, err := newLegacySigner(alg)
|
||||
if err == nil {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(err.Error(), `jws.NewSigner: unsupported signature algorithm`) {
|
||||
// When newLegacySigner fails, automatically trigger to enable signers
|
||||
enableLegacySignersOnce.Do(enableLegacySigners)
|
||||
return newLegacySigner(alg)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func newLegacySigner(alg jwa.SignatureAlgorithm) (Signer, error) {
|
||||
muSignerDB.RLock()
|
||||
f, ok := signerDB[alg]
|
||||
muSignerDB.RUnlock()
|
||||
|
||||
if ok {
|
||||
return f.Create()
|
||||
}
|
||||
return nil, fmt.Errorf(`jws.NewSigner: unsupported signature algorithm "%s"`, alg)
|
||||
}
|
||||
|
||||
type noneSigner struct{}
|
||||
|
||||
func (noneSigner) Algorithm() jwa.SignatureAlgorithm {
|
||||
return jwa.NoSignature()
|
||||
}
|
||||
|
||||
func (noneSigner) Sign([]byte, any) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package jws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jws/jwsbb"
|
||||
)
|
||||
|
||||
type defaultVerifier struct {
|
||||
alg jwa.SignatureAlgorithm
|
||||
}
|
||||
|
||||
func (v defaultVerifier) Algorithm() jwa.SignatureAlgorithm {
|
||||
return v.alg
|
||||
}
|
||||
|
||||
func (v defaultVerifier) Verify(key any, payload, signature []byte) error {
|
||||
if err := jwsbb.Verify(key, v.alg.String(), payload, signature); err != nil {
|
||||
return verifyError{verificationError{err}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Verifier2 interface {
|
||||
Verify(key any, payload, signature []byte) error
|
||||
}
|
||||
|
||||
var muVerifier2DB sync.RWMutex
|
||||
var verifier2DB = make(map[jwa.SignatureAlgorithm]Verifier2)
|
||||
|
||||
type verifierAdapter struct {
|
||||
v Verifier
|
||||
}
|
||||
|
||||
func (v verifierAdapter) Verify(key any, payload, signature []byte) error {
|
||||
if err := v.v.Verify(payload, signature, key); err != nil {
|
||||
return verifyError{verificationError{err}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifierFor returns a Verifier2 for the given signature algorithm.
|
||||
//
|
||||
// Currently, this function will never fail. It will always return a
|
||||
// valid Verifier2 object. The heuristic is as follows:
|
||||
// 1. If a Verifier2 is registered for the given algorithm, it will return that.
|
||||
// 2. If a legacy Verifier(Factory) is registered for the given algorithm, it will
|
||||
// return a Verifier2 that wraps the legacy Verifier.
|
||||
// 3. If no Verifier2 or legacy Verifier(Factory) is registered, it will return a
|
||||
// default verifier that uses jwsbb.Verify.
|
||||
//
|
||||
// jwsbb.Verify knows how to handle a static set of algorithms, so if the
|
||||
// algorithm is not supported, it will return an error when you call
|
||||
// `Verify` on the default verifier.
|
||||
func VerifierFor(alg jwa.SignatureAlgorithm) (Verifier2, error) {
|
||||
muVerifier2DB.RLock()
|
||||
defer muVerifier2DB.RUnlock()
|
||||
|
||||
v2, ok := verifier2DB[alg]
|
||||
if ok {
|
||||
return v2, nil
|
||||
}
|
||||
|
||||
v1, err := NewVerifier(alg)
|
||||
if err == nil {
|
||||
return verifierAdapter{v: v1}, nil
|
||||
}
|
||||
|
||||
return defaultVerifier{alg: alg}, nil
|
||||
}
|
||||
|
||||
type VerifierFactory interface {
|
||||
Create() (Verifier, error)
|
||||
}
|
||||
type VerifierFactoryFn func() (Verifier, error)
|
||||
|
||||
func (fn VerifierFactoryFn) Create() (Verifier, error) {
|
||||
return fn()
|
||||
}
|
||||
|
||||
var muVerifierDB sync.RWMutex
|
||||
var verifierDB = make(map[jwa.SignatureAlgorithm]VerifierFactory)
|
||||
|
||||
// RegisterVerifier is used to register a verifier for the given
|
||||
// algorithm.
|
||||
//
|
||||
// Please note that this function is intended to be passed a
|
||||
// verifier object as its second argument, but due to historical
|
||||
// reasons the function signature is defined as taking `any` type.
|
||||
//
|
||||
// You should create a signer object that implements the `Verifier2`
|
||||
// interface to register a signer, unless you have legacy code that
|
||||
// plugged into the `SignerFactory` interface.
|
||||
//
|
||||
// Unlike the `UnregisterVerifier` function, this function automatically
|
||||
// calls `jwa.RegisterSignatureAlgorithm` to register the algorithm
|
||||
// in this module's algorithm database.
|
||||
func RegisterVerifier(alg jwa.SignatureAlgorithm, f any) error {
|
||||
jwa.RegisterSignatureAlgorithm(alg)
|
||||
switch v := f.(type) {
|
||||
case Verifier2:
|
||||
muVerifier2DB.Lock()
|
||||
verifier2DB[alg] = v
|
||||
muVerifier2DB.Unlock()
|
||||
|
||||
muVerifierDB.Lock()
|
||||
delete(verifierDB, alg)
|
||||
muVerifierDB.Unlock()
|
||||
case VerifierFactory:
|
||||
muVerifierDB.Lock()
|
||||
verifierDB[alg] = v
|
||||
muVerifierDB.Unlock()
|
||||
|
||||
muVerifier2DB.Lock()
|
||||
delete(verifier2DB, alg)
|
||||
muVerifier2DB.Unlock()
|
||||
default:
|
||||
return fmt.Errorf(`jws.RegisterVerifier: unsupported type %T for algorithm %q`, f, alg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnregisterVerifier removes the signer factory associated with
|
||||
// the given algorithm.
|
||||
//
|
||||
// Note that when you call this function, the algorithm itself is
|
||||
// not automatically unregistered from this module's algorithm database.
|
||||
// This is because the algorithm may still be required for signing or
|
||||
// some other operation (however unlikely, it is still possible).
|
||||
// Therefore, in order to completely remove the algorithm, you must
|
||||
// call `jwa.UnregisterSignatureAlgorithm` yourself.
|
||||
func UnregisterVerifier(alg jwa.SignatureAlgorithm) {
|
||||
muVerifier2DB.Lock()
|
||||
delete(verifier2DB, alg)
|
||||
muVerifier2DB.Unlock()
|
||||
|
||||
muVerifierDB.Lock()
|
||||
delete(verifierDB, alg)
|
||||
muVerifierDB.Unlock()
|
||||
}
|
||||
|
||||
// NewVerifier creates a verifier that signs payloads using the given signature algorithm.
|
||||
func NewVerifier(alg jwa.SignatureAlgorithm) (Verifier, error) {
|
||||
muVerifierDB.RLock()
|
||||
f, ok := verifierDB[alg]
|
||||
muVerifierDB.RUnlock()
|
||||
|
||||
if ok {
|
||||
return f.Create()
|
||||
}
|
||||
return nil, fmt.Errorf(`jws.NewVerifier: unsupported signature algorithm "%s"`, alg)
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
package jws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/lestrrat-go/blackmagic"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/base64"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/json"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/pool"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jws/jwsbb"
|
||||
)
|
||||
|
||||
// verifyContext holds the state during JWS verification
|
||||
type verifyContext struct {
|
||||
parseOptions []ParseOption
|
||||
dst *Message
|
||||
detachedPayload []byte
|
||||
keyProviders []KeyProvider
|
||||
keyUsed any
|
||||
validateKey bool
|
||||
encoder Base64Encoder
|
||||
//nolint:containedctx
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
var verifyContextPool = pool.New[*verifyContext](allocVerifyContext, freeVerifyContext)
|
||||
|
||||
func allocVerifyContext() *verifyContext {
|
||||
return &verifyContext{
|
||||
encoder: base64.DefaultEncoder(),
|
||||
ctx: context.Background(),
|
||||
}
|
||||
}
|
||||
|
||||
func freeVerifyContext(vc *verifyContext) *verifyContext {
|
||||
vc.parseOptions = vc.parseOptions[:0]
|
||||
vc.dst = nil
|
||||
vc.detachedPayload = nil
|
||||
vc.keyProviders = vc.keyProviders[:0]
|
||||
vc.keyUsed = nil
|
||||
vc.validateKey = false
|
||||
vc.encoder = base64.DefaultEncoder()
|
||||
vc.ctx = context.Background()
|
||||
return vc
|
||||
}
|
||||
|
||||
func (vc *verifyContext) ProcessOptions(options []VerifyOption) error {
|
||||
//nolint:forcetypeassert
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identMessage{}:
|
||||
if err := option.Value(&vc.dst); err != nil {
|
||||
return verifyerr(`invalid value for option WithMessage: %w`, err)
|
||||
}
|
||||
case identDetachedPayload{}:
|
||||
if err := option.Value(&vc.detachedPayload); err != nil {
|
||||
return verifyerr(`invalid value for option WithDetachedPayload: %w`, err)
|
||||
}
|
||||
case identKey{}:
|
||||
var pair *withKey
|
||||
if err := option.Value(&pair); err != nil {
|
||||
return verifyerr(`invalid value for option WithKey: %w`, err)
|
||||
}
|
||||
vc.keyProviders = append(vc.keyProviders, &staticKeyProvider{
|
||||
alg: pair.alg.(jwa.SignatureAlgorithm),
|
||||
key: pair.key,
|
||||
})
|
||||
case identKeyProvider{}:
|
||||
var kp KeyProvider
|
||||
if err := option.Value(&kp); err != nil {
|
||||
return verifyerr(`failed to retrieve key-provider option value: %w`, err)
|
||||
}
|
||||
vc.keyProviders = append(vc.keyProviders, kp)
|
||||
case identKeyUsed{}:
|
||||
if err := option.Value(&vc.keyUsed); err != nil {
|
||||
return verifyerr(`failed to retrieve key-used option value: %w`, err)
|
||||
}
|
||||
case identContext{}:
|
||||
if err := option.Value(&vc.ctx); err != nil {
|
||||
return verifyerr(`failed to retrieve context option value: %w`, err)
|
||||
}
|
||||
case identValidateKey{}:
|
||||
if err := option.Value(&vc.validateKey); err != nil {
|
||||
return verifyerr(`failed to retrieve validate-key option value: %w`, err)
|
||||
}
|
||||
case identSerialization{}:
|
||||
vc.parseOptions = append(vc.parseOptions, option.(ParseOption))
|
||||
case identBase64Encoder{}:
|
||||
if err := option.Value(&vc.encoder); err != nil {
|
||||
return verifyerr(`failed to retrieve base64-encoder option value: %w`, err)
|
||||
}
|
||||
default:
|
||||
return verifyerr(`invalid jws.VerifyOption %q passed`, `With`+strings.TrimPrefix(fmt.Sprintf(`%T`, option.Ident()), `jws.ident`))
|
||||
}
|
||||
}
|
||||
|
||||
if len(vc.keyProviders) < 1 {
|
||||
return verifyerr(`no key providers have been provided (see jws.WithKey(), jws.WithKeySet(), jws.WithVerifyAuto(), and jws.WithKeyProvider()`)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vc *verifyContext) VerifyMessage(buf []byte) ([]byte, error) {
|
||||
msg, err := Parse(buf, vc.parseOptions...)
|
||||
if err != nil {
|
||||
return nil, verifyerr(`failed to parse jws: %w`, err)
|
||||
}
|
||||
defer msg.clearRaw()
|
||||
|
||||
if vc.detachedPayload != nil {
|
||||
if len(msg.payload) != 0 {
|
||||
return nil, verifyerr(`can't specify detached payload for JWS with payload`)
|
||||
}
|
||||
|
||||
msg.payload = vc.detachedPayload
|
||||
}
|
||||
|
||||
verifyBuf := pool.ByteSlice().Get()
|
||||
|
||||
// Because deferred functions bind to the current value of the variable,
|
||||
// we can't just use `defer pool.ByteSlice().Put(verifyBuf)` here.
|
||||
// Instead, we use a closure to reference the _variable_.
|
||||
// it would be better if we could call it directly, but there are
|
||||
// too many place we may return from this function
|
||||
defer func() {
|
||||
pool.ByteSlice().Put(verifyBuf)
|
||||
}()
|
||||
|
||||
errs := pool.ErrorSlice().Get()
|
||||
defer func() {
|
||||
pool.ErrorSlice().Put(errs)
|
||||
}()
|
||||
for idx, sig := range msg.signatures {
|
||||
var rawHeaders []byte
|
||||
if rbp, ok := sig.protected.(interface{ rawBuffer() []byte }); ok {
|
||||
if raw := rbp.rawBuffer(); raw != nil {
|
||||
rawHeaders = raw
|
||||
}
|
||||
}
|
||||
|
||||
if rawHeaders == nil {
|
||||
protected, err := json.Marshal(sig.protected)
|
||||
if err != nil {
|
||||
return nil, verifyerr(`failed to marshal "protected" for signature #%d: %w`, idx+1, err)
|
||||
}
|
||||
rawHeaders = protected
|
||||
}
|
||||
|
||||
verifyBuf = verifyBuf[:0]
|
||||
verifyBuf = jwsbb.SignBuffer(verifyBuf, rawHeaders, msg.payload, vc.encoder, msg.b64)
|
||||
for i, kp := range vc.keyProviders {
|
||||
var sink algKeySink
|
||||
if err := kp.FetchKeys(vc.ctx, &sink, sig, msg); err != nil {
|
||||
return nil, verifyerr(`key provider %d failed: %w`, i, err)
|
||||
}
|
||||
|
||||
for _, pair := range sink.list {
|
||||
// alg is converted here because pair.alg is of type jwa.KeyAlgorithm.
|
||||
// this may seem ugly, but we're trying to avoid declaring separate
|
||||
// structs for `alg jwa.KeyEncryptionAlgorithm` and `alg jwa.SignatureAlgorithm`
|
||||
//nolint:forcetypeassert
|
||||
alg := pair.alg.(jwa.SignatureAlgorithm)
|
||||
key := pair.key
|
||||
|
||||
if err := vc.tryKey(verifyBuf, alg, key, msg, sig); err != nil {
|
||||
errs = append(errs, verifyerr(`failed to verify signature #%d with key %T: %w`, idx+1, key, err))
|
||||
continue
|
||||
}
|
||||
|
||||
return msg.payload, nil
|
||||
}
|
||||
}
|
||||
errs = append(errs, verifyerr(`signature #%d could not be verified with any of the keys`, idx+1))
|
||||
}
|
||||
return nil, verifyerr(`could not verify message using any of the signatures or keys: %w`, errors.Join(errs...))
|
||||
}
|
||||
|
||||
func (vc *verifyContext) tryKey(verifyBuf []byte, alg jwa.SignatureAlgorithm, key any, msg *Message, sig *Signature) error {
|
||||
if vc.validateKey {
|
||||
if err := validateKeyBeforeUse(key); err != nil {
|
||||
return fmt.Errorf(`failed to validate key before signing: %w`, err)
|
||||
}
|
||||
}
|
||||
|
||||
verifier, err := VerifierFor(alg)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to get verifier for algorithm %q: %w`, alg, err)
|
||||
}
|
||||
|
||||
if err := verifier.Verify(key, verifyBuf, sig.signature); err != nil {
|
||||
return verificationError{err}
|
||||
}
|
||||
|
||||
// Verification succeeded
|
||||
if vc.keyUsed != nil {
|
||||
if err := blackmagic.AssignIfCompatible(vc.keyUsed, key); err != nil {
|
||||
return fmt.Errorf(`failed to assign used key (%T) to %T: %w`, key, vc.keyUsed, err)
|
||||
}
|
||||
}
|
||||
|
||||
if vc.dst != nil {
|
||||
*(vc.dst) = *msg
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user