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 }