Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+76
View File
@@ -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
View File
@@ -0,0 +1,111 @@
# JWS [![Go Reference](https://pkg.go.dev/badge/github.com/lestrrat-go/jwx/v3/jws.svg)](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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
@@ -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",
],
)
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}