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