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:])) }