6239eca243
Completes the three in-flight open items and fixes the away-fallthrough bug. Item 7 — passkey step-up (WebAuthn): - internal/webauthn: ES256/P-256 register + assert with real ecdsa signature verification, minimal CBOR/COSE decode, PasskeySession (L2→L3 on assert, decays after TTL). Drop the RS256 offer we can't verify (register-ok/ assert-fail trap). Verify rpIdHash + UP/UV flags in FinishAssertion — UV is the step-up gesture. Round-trip test with negative cases (tampered sig, missing UV, wrong origin). - cmd/mavweb: /auth/passkey enroll+assert page (the only surface that can do a WebAuthn gesture) + the four begin/finish endpoints. Without this the daemon's PasskeySession swap leaves /tools enable permanently blocked. - daemon wires PasskeySession as the auth Session + srv.StepUp; policy gates MethodAssertStepUp at AuthRead. Item 5 — tools page: DisableTool through store/ipc/client/wire; /tools grows a disable action and a link to the passkey page. Lifecycle test. Item 6 — note RAG: PhraseQuery on the phraser (LLM-composed answer over top-k notes, raw-notes fallback); IntentQuery routes through it. Stub returns a deterministic summary. Item 2 — away-fallthrough: on ErrVoiceNoSession the dispatcher now reroutes through the AWAY table (sev3→ntfy, sev4→telegram-repeat-til-ack, sev≤2→drop) instead of silently dropping / mis-routing to the present-list remainder. Covers DispatchNudge + DispatchReminder. 4 tests. Also: re-add ProposeTool to CoreAPI (dropped in a comment rewrite), fix missing imports + a duplicate block left mid-edit, drop dead AssertStepUpFunc, gitignore /mavcaldav. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
245 lines
6.6 KiB
Go
245 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:])) }
|