Files
Maven/internal/webauthn/cbor.go
T
kami d52f60c54e maven: fix test mocks for CalendarEvents interface (verification)
- Add CalendarEvents method to recordingAPI in auth_test.go
- Add CalendarEvents method to fakeCore in handlers_test.go

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:20:16 +04:00

247 lines
6.6 KiB
Go

package webauthn
import (
"fmt"
"math"
)
// cborValue is one decoded CBOR item. Only the subset needed for WebAuthn
// COSE key + attestation object parsing is handled: integers, byte strings,
// text strings, arrays, maps.
type cborValue struct {
typ cborType
u uint64 // unsigned integer value
n int64 // negative integer value (-1 - u)
b []byte // byte string
t string // text string
items []cborValue // array items or map key-value pairs (flattened)
}
type cborType int
const (
cborUint cborType = 0
cborNegInt cborType = 1
cborBytes cborType = 2
cborText cborType = 3
cborArray cborType = 4
cborMap cborType = 5
cborSimple cborType = 7
)
func (v cborValue) Int() (int, error) {
switch v.typ {
case cborUint:
return int(v.u), nil
case cborNegInt:
return int(v.n), nil
default:
return 0, fmt.Errorf("cbor: expected int, got type %d", v.typ)
}
}
func (v cborValue) Int64() (int64, error) {
switch v.typ {
case cborUint:
return int64(v.u), nil
case cborNegInt:
return v.n, nil
default:
return 0, fmt.Errorf("cbor: expected int, got type %d", v.typ)
}
}
func (v cborValue) Bytes() ([]byte, error) {
if v.typ != cborBytes {
return nil, fmt.Errorf("cbor: expected bytes, got type %d", v.typ)
}
return v.b, nil
}
func (v cborValue) Text() (string, error) {
if v.typ != cborText {
return "", fmt.Errorf("cbor: expected text, got type %d", v.typ)
}
return v.t, nil
}
func (v cborValue) Map() (map[int64]cborValue, error) {
if v.typ != cborMap {
return nil, fmt.Errorf("cbor: expected map, got type %d", v.typ)
}
m := make(map[int64]cborValue, len(v.items)/2)
for i := 0; i+1 < len(v.items); i += 2 {
k, err := v.items[i].Int64()
if err != nil {
return nil, fmt.Errorf("cbor: map key: %w", err)
}
m[k] = v.items[i+1]
}
return m, nil
}
func (v cborValue) MapText() (map[string]cborValue, error) {
if v.typ != cborMap {
return nil, fmt.Errorf("cbor: expected map, got type %d", v.typ)
}
m := make(map[string]cborValue, len(v.items)/2)
for i := 0; i+1 < len(v.items); i += 2 {
k, err := v.items[i].Text()
if err != nil {
return nil, fmt.Errorf("cbor: map text key: %w", err)
}
m[k] = v.items[i+1]
}
return m, nil
}
func (v cborValue) At(i int) (cborValue, error) {
if v.typ != cborArray {
return cborValue{}, fmt.Errorf("cbor: expected array, got type %d", v.typ)
}
if i < 0 || i >= len(v.items) {
return cborValue{}, fmt.Errorf("cbor: index %d out of range (len %d)", i, len(v.items))
}
return v.items[i], nil
}
// decodeCBOR decodes a single CBOR item from data. It handles only the subset
// needed for WebAuthn COSE key + attestation parsing.
func decodeCBOR(data []byte) (cborValue, error) {
v, _, err := decodeItem(data)
return v, err
}
func decodeItem(data []byte) (cborValue, int, error) {
if len(data) == 0 {
return cborValue{}, 0, fmt.Errorf("cbor: empty data")
}
ib := data[0]
mt := ib >> 5
ai := ib & 0x1f
off := 1
arg, n, err := decodeArg(data, off, ai)
if err != nil {
return cborValue{}, 0, err
}
off = n
switch mt {
case 0: // unsigned integer
return cborValue{typ: cborUint, u: arg}, off, nil
case 1: // negative integer
return cborValue{typ: cborNegInt, n: -1 - int64(arg)}, off, nil
case 2: // byte string
if off+int(arg) > len(data) {
return cborValue{}, 0, fmt.Errorf("cbor: byte string length %d exceeds data", arg)
}
b := make([]byte, arg)
copy(b, data[off:off+int(arg)])
return cborValue{typ: cborBytes, b: b}, off + int(arg), nil
case 3: // text string
if off+int(arg) > len(data) {
return cborValue{}, 0, fmt.Errorf("cbor: text string length %d exceeds data", arg)
}
return cborValue{typ: cborText, t: string(data[off : off+int(arg)])}, off + int(arg), nil
case 4: // array
items := make([]cborValue, 0, arg)
pos := off
for i := uint64(0); i < arg; i++ {
item, n, err := decodeItem(data[pos:])
if err != nil {
return cborValue{}, 0, fmt.Errorf("cbor: array item %d: %w", i, err)
}
items = append(items, item)
pos += n
}
return cborValue{typ: cborArray, items: items}, pos, nil
case 5: // map
items := make([]cborValue, 0, 2*arg)
pos := off
for i := uint64(0); i < arg; i++ {
k, n, err := decodeItem(data[pos:])
if err != nil {
return cborValue{}, 0, fmt.Errorf("cbor: map key %d: %w", i, err)
}
pos += n
v, n, err := decodeItem(data[pos:])
if err != nil {
return cborValue{}, 0, fmt.Errorf("cbor: map value %d: %w", i, err)
}
pos += n
items = append(items, k, v)
}
return cborValue{typ: cborMap, items: items}, pos, nil
case 7: // simple / float
switch ai {
case 20: // false
return cborValue{typ: cborSimple, u: 20}, off, nil
case 21: // true
return cborValue{typ: cborSimple, u: 21}, off, nil
case 22: // null
return cborValue{typ: cborSimple, u: 22}, off, nil
case 25: // half-precision float (not needed but avoid panic)
return cborValue{typ: cborSimple, u: 25}, off + 2, nil
case 26: // single-precision float
if off+4 > len(data) {
return cborValue{}, 0, fmt.Errorf("cbor: truncated float32")
}
_ = math.Float32frombits(readBE32(data[off:]))
return cborValue{typ: cborSimple, u: 26}, off + 4, nil
case 27: // double-precision float
if off+8 > len(data) {
return cborValue{}, 0, fmt.Errorf("cbor: truncated float64")
}
_ = math.Float64frombits(readBE64(data[off:]))
return cborValue{typ: cborSimple, u: 27}, off + 8, nil
default:
return cborValue{typ: cborSimple, u: arg}, off, nil
}
default:
return cborValue{}, 0, fmt.Errorf("cbor: unsupported major type %d", mt)
}
}
func decodeArg(data []byte, off int, ai byte) (uint64, int, error) {
switch {
case ai <= 23:
return uint64(ai), off, nil
case ai == 24:
if off >= len(data) {
return 0, 0, fmt.Errorf("cbor: truncated additional info")
}
return uint64(data[off]), off + 1, nil
case ai == 25:
if off+2 > len(data) {
return 0, 0, fmt.Errorf("cbor: truncated uint16")
}
return uint64(readBE16(data[off:])), off + 2, nil
case ai == 26:
if off+4 > len(data) {
return 0, 0, fmt.Errorf("cbor: truncated uint32")
}
return uint64(readBE32(data[off:])), off + 4, nil
case ai == 27:
if off+8 > len(data) {
return 0, 0, fmt.Errorf("cbor: truncated uint64")
}
return readBE64(data[off:]), off + 8, nil
default:
return 0, 0, fmt.Errorf("cbor: reserved additional info %d", ai)
}
}
func readBE16(b []byte) uint16 { return uint16(b[0])<<8 | uint16(b[1]) }
func readBE32(b []byte) uint32 {
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
}
func readBE64(b []byte) uint64 { return uint64(readBE32(b))<<32 | uint64(readBE32(b[4:])) }