package infini import ( "bytes" "context" "crypto/ed25519" "crypto/rand" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" "fmt" "net/http" "net/url" "strconv" "strings" "time" ) func edLoadPrivateKey(data []byte) (ed25519.PrivateKey, error) { block, rest := pem.Decode(bytes.TrimSpace(data)) if block == nil || block.Type != "PRIVATE KEY" || len(block.Headers) != 0 || len(bytes.TrimSpace(rest)) != 0 { return nil, fmt.Errorf("expected one unencrypted PKCS#8 PEM block: BEGIN PRIVATE KEY") } parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("invalid PKCS#8 private key: %w", err) } key, ok := parsed.(ed25519.PrivateKey) if !ok { return nil, fmt.Errorf("private key must be Ed25519") } return key, nil } func edSignRequest(req *http.Request, body []byte, keyID string, key ed25519.PrivateKey, now time.Time) (string, error) { nonceBytes := make([]byte, 16) if _, err := rand.Read(nonceBytes); err != nil { return "", err } nonce := base64.RawURLEncoding.EncodeToString(nonceBytes) timestamp := strconv.FormatInt(now.Unix(), 10) hash := sha256.Sum256(body) digest := "SHA-256=" + base64.StdEncoding.EncodeToString(hash[:]) // Sign the exact escaped path and raw query sent on the wire, with a final LF. payload := strings.Join([]string{"INFINI-OPENAPI-V2", keyID, req.Method, req.URL.RequestURI(), timestamp, nonce, digest}, "\n") + "\n" signature := base64.StdEncoding.EncodeToString(ed25519.Sign(key, []byte(payload))) req.Header.Set("X-API-Key-ID", keyID) req.Header.Set("X-API-Timestamp", timestamp) req.Header.Set("X-API-Nonce", nonce) req.Header.Set("Digest", digest) req.Header.Set("Authorization", `Ed25519 signature="`+signature+`"`) req.Header.Set("Content-Type", "application/json") return payload, nil } type Client struct { origin string keyID string key ed25519.PrivateKey http *http.Client } func NewClient(origin, keyID string, privateKeyPEM []byte) (*Client, error) { base, err := url.Parse(origin) if err != nil || base.Scheme != "https" || base.Host == "" || base.User != nil || base.RawQuery != "" || base.Fragment != "" || (base.Path != "" && base.Path != "/") { return nil, fmt.Errorf("expected an HTTPS API origin") } if len(keyID) != 32 { return nil, fmt.Errorf("expected a 32-character Base58 Key ID") } for _, c := range keyID { if !strings.ContainsRune("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", c) { return nil, fmt.Errorf("expected a Base58 Key ID") } } key, err := edLoadPrivateKey(privateKeyPEM) if err != nil { return nil, err } return &Client{origin: strings.TrimRight(origin, "/"), keyID: keyID, key: key, http: &http.Client{Timeout: 15 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}}, nil } // Do sends the exact body bytes. Caller closes resp.Body and checks HTTP and business status. func (c *Client) Do(ctx context.Context, method, target string, body []byte) (*http.Response, error) { if !strings.HasPrefix(target, "/v2/") || strings.Contains(target, "#") { return nil, fmt.Errorf("expected an escaped /v2/ path and raw query") } for _, ch := range target { if ch <= 32 || ch >= 127 { return nil, fmt.Errorf("target must be escaped ASCII") } } req, err := http.NewRequestWithContext(ctx, strings.ToUpper(method), c.origin+target, bytes.NewReader(body)) if err != nil { return nil, err } if req.URL.RequestURI() != target { return nil, fmt.Errorf("request target changed") } if _, err = edSignRequest(req, body, c.keyID, c.key, time.Now()); err != nil { return nil, err } return c.http.Do(req) }