naco-api/internal/server/auth.go
Nacorid c965eb51d3
refactor(server): type CDP private key as crypto.Signer
Store the CDP auth key as a crypto.Signer interface so ECDSA and Ed25519 keys
share one signing path, and detect raw base64 Ed25519 keys inline instead of a
separate helper.
2026-09-13 22:24:43 +00:00

129 lines
3.5 KiB
Go

package server
import (
"context"
"crypto"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
x402http "github.com/x402-foundation/x402/go/v2/http"
)
type CDPAuth struct {
keyName string
privateKey crypto.Signer
signingMethod jwt.SigningMethod
}
func NewCDPAuth(keyName, privateKeyPEM string) (*CDPAuth, error) {
if keyName == "" {
return nil, errors.New("key name is required")
}
if privateKeyPEM == "" {
return nil, errors.New("private key is required")
}
// Forgejo/GitHub secrets store multiline PEM as single line with literal \n.
// Convert to real newlines before decoding; idempotent if already multiline.
privateKeyPEM = strings.ReplaceAll(privateKeyPEM, "\\n", "\n")
// Check if it's a base64-encoded Ed25519 key (64 bytes).
decoded, err := base64.StdEncoding.DecodeString(privateKeyPEM)
if err == nil && len(decoded) == 64 {
return &CDPAuth{
keyName: keyName,
privateKey: ed25519.PrivateKey(decoded),
signingMethod: jwt.SigningMethodEdDSA,
}, nil
}
// Otherwise, try PEM-decoded EC key
block, _ := pem.Decode([]byte(privateKeyPEM))
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
// Try to parse as EC private key first
privKey, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse EC private key: %w", err)
}
return &CDPAuth{
keyName: keyName,
privateKey: privKey,
signingMethod: jwt.SigningMethodES256,
}, nil
}
func (a *CDPAuth) generateBearerToken(method, path string) (string, error) {
nonceBytes := make([]byte, 16)
if _, err := rand.Read(nonceBytes); err != nil {
return "", fmt.Errorf("failed to generate nonce: %w", err)
}
nonce := hex.EncodeToString(nonceBytes)
uri := fmt.Sprintf("%s api.cdp.coinbase.com%s", method, path)
now := time.Now()
claims := jwt.MapClaims{
"sub": a.keyName,
"iss": "cdp",
"nbf": now.Unix(),
"iat": now.Unix(),
"exp": now.Add(120 * time.Second).Unix(),
"uris": []string{uri},
}
token := jwt.NewWithClaims(a.signingMethod, claims)
token.Header["kid"] = a.keyName
token.Header["nonce"] = nonce
var signedToken string
var err error
switch a.signingMethod {
case jwt.SigningMethodES256, jwt.SigningMethodEdDSA:
signedToken, err = token.SignedString(a.privateKey)
default:
return "", fmt.Errorf("unsupported signing method: %v", a.signingMethod)
}
if err != nil {
return "", fmt.Errorf("failed to sign token: %w", err)
}
return signedToken, nil
}
func (a *CDPAuth) GetAuthHeaders(ctx context.Context) (x402http.AuthHeaders, error) {
verify, err := a.generateBearerToken("POST", "/platform/v2/x402/verify")
if err != nil {
return x402http.AuthHeaders{}, fmt.Errorf("failed to generate verify token: %w", err)
}
settle, err := a.generateBearerToken("POST", "/platform/v2/x402/settle")
if err != nil {
return x402http.AuthHeaders{}, fmt.Errorf("failed to generate settle token: %w", err)
}
supported, err := a.generateBearerToken("GET", "/platform/v2/x402/supported")
if err != nil {
return x402http.AuthHeaders{}, fmt.Errorf("failed to generate supported token: %w", err)
}
return x402http.AuthHeaders{
Verify: map[string]string{"Authorization": "Bearer " + verify},
Settle: map[string]string{"Authorization": "Bearer " + settle},
Supported: map[string]string{"Authorization": "Bearer " + supported},
}, nil
}
func (a *CDPAuth) GetSigningMethod() jwt.SigningMethod {
return a.signingMethod
}