blob: c51a74cd30b07b2ceaecd185a5568ef64e075612 [file] [edit]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package hmac implements CEL extension functions for Hash-based Message Authentication Code (HMAC) verification and computation.
package hmac
import (
"crypto"
"crypto/hmac"
_ "crypto/md5"
_ "crypto/sha1"
_ "crypto/sha256"
_ "crypto/sha512"
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
"cel.dev/cel-go/cel"
"cel.dev/cel-go/common/types"
"cel.dev/cel-go/common/types/ref"
)
// Library returns a cel.EnvOption to configure extended functions for HMAC signature verification and computation.
func Library(options ...Option) cel.EnvOption {
l := &hmacLib{
version: ^uint32(0),
customAlgorithms: make(map[string]crypto.Hash),
}
for _, o := range options {
l = o(l)
}
if len(l.customAlgorithms) == 0 {
l = CommonAlgorithms()(l)
}
return cel.Lib(l)
}
// Option declares a functional operator for configuring HMAC extension library behavior.
type Option func(*hmacLib) *hmacLib
// Version sets the library version for HMAC extensions.
func Version(version uint32) Option {
return func(l *hmacLib) *hmacLib {
l.version = version
return l
}
}
// MaxPrefixLength sets the maximum signature prefix length to parse during verification.
// Defaults to 20.
func MaxPrefixLength(limit int) Option {
return func(l *hmacLib) *hmacLib {
l.maxPrefixLength = limit
return l
}
}
// Algorithm registers a crypto.Hash algorithm with optional aliases
// (e.g. Algorithm(crypto.SHA256, "HS256")),
// exposing constant declarations (e.g., hmac.SHA256, hmac.HS256) in CEL and enabling it for HMAC operations.
func Algorithm(h crypto.Hash, aliases ...string) Option {
return func(l *hmacLib) *hmacLib {
if l.customAlgorithms == nil {
l.customAlgorithms = make(map[string]crypto.Hash)
}
name := h.String()
normName := normalizeAlgName(name)
l.customAlgorithms[normName] = h
l.customAlgorithms[name] = h
for _, alias := range aliases {
l.customAlgorithms[normalizeAlgName(alias)] = h
l.customAlgorithms[alias] = h
}
if normName != "" {
l.addConstant("hmac."+normName, normName)
}
for _, alias := range aliases {
constAlias := normalizeAlgName(alias)
if constAlias != "" {
l.addConstant("hmac."+constAlias, normName)
}
}
return l
}
}
// CommonAlgorithms registers the most common HMAC hash algorithms (SHA256, SHA384, SHA512, SHA224, SHA512/256, SHA512/224)
// along with their JOSE/JWT aliases (HS256, HS384, HS512, HS224, HS512/256, HS512/224) using Algorithm options by proxy.
func CommonAlgorithms() Option {
return func(l *hmacLib) *hmacLib {
opts := []Option{
Algorithm(crypto.SHA256, "HS256"),
Algorithm(crypto.SHA384, "HS384"),
Algorithm(crypto.SHA512, "HS512"),
Algorithm(crypto.SHA224, "HS224"),
Algorithm(crypto.SHA512_256, "HS512_256"),
Algorithm(crypto.SHA512_224, "HS512_224"),
}
for _, opt := range opts {
l = opt(l)
}
return l
}
}
type celConstant struct {
name string
val string
}
type hmacLib struct {
version uint32
maxPrefixLength int
customAlgorithms map[string]crypto.Hash
constants []celConstant
}
func (l *hmacLib) addConstant(name, val string) {
for _, c := range l.constants {
if c.name == name {
return
}
}
l.constants = append(l.constants, celConstant{name: name, val: val})
}
// LibraryName returns the CEL library identifier string.
func (*hmacLib) LibraryName() string {
return "cel.lib.ext.security.hmac"
}
// CompileOptions returns environment options for declaring CEL functions and constants.
func (l *hmacLib) CompileOptions() []cel.EnvOption {
var opts []cel.EnvOption
for _, c := range l.constants {
opts = append(opts, cel.Constant(c.name, cel.StringType, types.String(c.val)))
}
opts = append(opts,
cel.Function("hmac.verify",
cel.Overload("hmac_verify_bytes_bytes_bytes_string",
[]*cel.Type{cel.BytesType, cel.BytesType, cel.BytesType, cel.StringType},
cel.BoolType,
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
msg := args[0].(types.Bytes)
sig := args[1].(types.Bytes)
secret := args[2].(types.Bytes)
alg := args[3].(types.String)
return types.Bool(l.verifyBytes(msg, sig, secret, string(alg)))
}),
),
cel.Overload("hmac_verify_string_string_string_string",
[]*cel.Type{cel.StringType, cel.StringType, cel.StringType, cel.StringType},
cel.BoolType,
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
msg := args[0].(types.String)
sig := args[1].(types.String)
secret := args[2].(types.String)
alg := args[3].(types.String)
return types.Bool(l.verifyString(string(msg), string(sig), string(secret), string(alg)))
}),
),
),
cel.Function("hmac.compute",
cel.Overload("hmac_compute_bytes_bytes_string",
[]*cel.Type{cel.BytesType, cel.BytesType, cel.StringType},
cel.BytesType,
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
msg := args[0].(types.Bytes)
secret := args[1].(types.Bytes)
alg := args[2].(types.String)
mac, err := l.compute(msg, secret, string(alg))
if err != nil {
return types.ValOrErr(args[0], "%v", err)
}
return types.Bytes(mac)
}),
),
cel.Overload("hmac_compute_string_string_string",
[]*cel.Type{cel.StringType, cel.StringType, cel.StringType},
cel.BytesType,
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
msg := args[0].(types.String)
secret := args[1].(types.String)
alg := args[2].(types.String)
mac, err := l.compute([]byte(string(msg)), []byte(string(secret)), string(alg))
if err != nil {
return types.ValOrErr(args[0], "%v", err)
}
return types.Bytes(mac)
}),
),
),
)
return opts
}
// ProgramOptions returns program options for HMAC extensions.
func (l *hmacLib) ProgramOptions() []cel.ProgramOption {
return nil
}
func (l *hmacLib) compute(msg, secret []byte, alg string) ([]byte, error) {
hType, err := l.resolveHash(alg)
if err != nil {
return nil, err
}
return computeHMAC(msg, secret, hType)
}
func (l *hmacLib) verifyBytes(msg, sig, secret []byte, alg string) bool {
hType, err := l.resolveHash(alg)
if err != nil {
return false
}
expectedMAC, err := computeHMAC(msg, secret, hType)
if err != nil {
return false
}
return hmac.Equal(expectedMAC, sig)
}
func (l *hmacLib) verifyString(msgStr, sigStr, secretStr, alg string) bool {
sigStr = strings.TrimSpace(sigStr)
detectedAlg, cleanSig := l.parseSignaturePrefix(sigStr)
effectiveAlg := alg
if detectedAlg != "" {
effectiveAlg = detectedAlg
}
hType, err := l.resolveHash(effectiveAlg)
if err != nil {
return false
}
expectedMAC, err := computeHMAC([]byte(msgStr), []byte(secretStr), hType)
if err != nil {
return false
}
// Try hex decoding
if hexBytes, err := hex.DecodeString(cleanSig); err == nil && len(hexBytes) == len(expectedMAC) {
if hmac.Equal(expectedMAC, hexBytes) {
return true
}
}
// Try base64 standard decoding
if b64Bytes, err := decodeBase64StdSegment(cleanSig); err == nil && len(b64Bytes) == len(expectedMAC) {
if hmac.Equal(expectedMAC, b64Bytes) {
return true
}
}
// Try base64 URL decoding
if b64URLBytes, err := decodeBase64URLSegment(cleanSig); err == nil && len(b64URLBytes) == len(expectedMAC) {
if hmac.Equal(expectedMAC, b64URLBytes) {
return true
}
}
// Fallback raw string comparison
return hmac.Equal(expectedMAC, []byte(cleanSig))
}
func (l *hmacLib) parseSignaturePrefix(sig string) (string, string) {
sig = strings.TrimSpace(sig)
limit := l.maxPrefixLength
if limit <= 0 {
limit = 20
}
if idx := strings.Index(sig, "="); idx > 0 && idx < limit {
prefix := strings.TrimSpace(sig[:idx])
rest := strings.TrimSpace(sig[idx+1:])
normPrefix := normalizeAlgName(prefix)
if _, ok := l.customAlgorithms[normPrefix]; ok {
for name := range l.customAlgorithms {
if normalizeAlgName(name) == normPrefix {
return name, rest
}
}
}
if _, ok := l.customAlgorithms[prefix]; ok {
return prefix, rest
}
if strings.EqualFold(prefix, "v1") || strings.EqualFold(prefix, "v0") {
return "", rest
}
}
return "", sig
}
func normalizeAlgName(alg string) string {
s := strings.TrimSpace(alg)
s = strings.ReplaceAll(s, "-", "_")
s = strings.ReplaceAll(s, "/", "_")
s = strings.ToUpper(s)
if after, ok := strings.CutPrefix(s, "SHA_"); ok {
s = "SHA" + after
}
return s
}
func (l *hmacLib) resolveHash(alg string) (crypto.Hash, error) {
norm := normalizeAlgName(alg)
for name, h := range l.customAlgorithms {
if strings.EqualFold(alg, name) || norm == normalizeAlgName(name) {
return h, nil
}
}
return 0, fmt.Errorf("unsupported HMAC hash algorithm: %q", alg)
}
func computeHMAC(msg, secret []byte, hType crypto.Hash) ([]byte, error) {
if !hType.Available() {
return nil, fmt.Errorf("hash algorithm %v is not available", hType)
}
mac := hmac.New(hType.New, secret)
mac.Write(msg)
return mac.Sum(nil), nil
}
// decodeBase64URLSegment decodes a URL-safe base64 string with or without padding.
func decodeBase64URLSegment(seg string) ([]byte, error) {
seg = strings.TrimSpace(seg)
if data, err := base64.RawURLEncoding.DecodeString(seg); err == nil {
return data, nil
}
return base64.URLEncoding.DecodeString(seg)
}
// decodeBase64StdSegment decodes a standard base64 string with or without padding.
func decodeBase64StdSegment(seg string) ([]byte, error) {
seg = strings.TrimSpace(seg)
if data, err := base64.RawStdEncoding.DecodeString(seg); err == nil {
return data, nil
}
return base64.StdEncoding.DecodeString(seg)
}