package main import ( "bytes" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "net/http" ) // PhoneClient fetches a per-request FIDO2 assertion token from Component 2. // There is no session caching — each call triggers a card interaction. type PhoneClient struct { baseURL string } func NewPhoneClient(baseURL string) *PhoneClient { return &PhoneClient{baseURL: baseURL} } // GetTokenForRequest calls /auth/get-token with url+method+nonce, triggering // a fresh FIDO2 assertion on the card. Returns the self-contained assertion // bundle that the endpoint can verify independently. func (c *PhoneClient) GetTokenForRequest(rawURL, method string) (string, error) { nonce, err := randomHex(16) if err != nil { return "", fmt.Errorf("nonce: %w", err) } body, _ := json.Marshal(map[string]string{ "url": rawURL, "method": method, "nonce": nonce, }) resp, err := http.Post(c.baseURL+"/auth/get-token", "application/json", bytes.NewReader(body)) if err != nil { return "", fmt.Errorf("phone unreachable (%s): %w", c.baseURL, err) } defer resp.Body.Close() raw, _ := io.ReadAll(resp.Body) var result struct { Ok bool `json:"ok"` Token string `json:"token"` Error string `json:"error"` } if err := json.Unmarshal(raw, &result); err != nil { return "", fmt.Errorf("parse token response: %w (body: %s)", err, raw) } if !result.Ok { return "", fmt.Errorf("auth failed: %s", result.Error) } if result.Token == "" { return "", fmt.Errorf("phone returned empty token") } return result.Token, nil } func randomHex(n int) (string, error) { b := make([]byte, n) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil }