k_card/component3/phone.go

69 lines
1.7 KiB
Go

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
}