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.
129 lines
3.5 KiB
Go
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
|
|
}
|