Add support for ECDSA P-384 (#685)
## Motivation and Context See https://github.com/modelcontextprotocol/registry/issues/482#issuecomment-3436993157 for full details. This adds ECDSA P-384 (a popular NIST approved curve) in addition to Ed25519. ## How Has This Been Tested? I have tested with a local deployment against `mcp.joelverhagen.com`. Also, I have added significant tests. ## Breaking Changes None. (hopefully) ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context <!-- Add any other context, implementation notes, or design decisions --> @domdomegg and I spoke a bit about this on Discord: https://discord.com/channels/1358869848138059966/1369487942862504016/1423710918180606052 --------- Co-authored-by: adam jones <domdomegg+git@gmail.com>
This commit is contained in:
parent
0ef5b90ed7
commit
6c0f085708
4 changed files with 1389 additions and 74 deletions
|
|
@ -3,7 +3,11 @@ package auth
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
|
@ -12,12 +16,24 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
type CryptoAlgorithm string
|
||||
|
||||
const (
|
||||
AlgorithmEd25519 CryptoAlgorithm = "ed25519"
|
||||
|
||||
// ECDSA with NIST P-384 curve
|
||||
// public key is in compressed format
|
||||
// signature is in R || S format
|
||||
AlgorithmECDSAP384 CryptoAlgorithm = "ecdsap384"
|
||||
)
|
||||
|
||||
// CryptoProvider provides common functionality for DNS and HTTP authentication
|
||||
type CryptoProvider struct {
|
||||
registryURL string
|
||||
domain string
|
||||
hexSeed string
|
||||
authMethod string
|
||||
registryURL string
|
||||
domain string
|
||||
privateKey string
|
||||
cryptoAlgorithm CryptoAlgorithm
|
||||
authMethod string
|
||||
}
|
||||
|
||||
// GetToken retrieves the registry JWT token using cryptographic authentication
|
||||
|
|
@ -26,31 +42,26 @@ func (c *CryptoProvider) GetToken(ctx context.Context) (string, error) {
|
|||
return "", fmt.Errorf("%s domain is required", c.authMethod)
|
||||
}
|
||||
|
||||
if c.hexSeed == "" {
|
||||
return "", fmt.Errorf("%s private key (hex seed) is required", c.authMethod)
|
||||
if c.privateKey == "" {
|
||||
return "", fmt.Errorf("%s private key (hex) is required", c.authMethod)
|
||||
}
|
||||
|
||||
// Decode hex seed to private key
|
||||
seedBytes, err := hex.DecodeString(c.hexSeed)
|
||||
// Decode private key from hex
|
||||
privateKeyBytes, err := hex.DecodeString(c.privateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid hex seed format: %w", err)
|
||||
return "", fmt.Errorf("invalid hex private key format: %w", err)
|
||||
}
|
||||
|
||||
if len(seedBytes) != ed25519.SeedSize {
|
||||
return "", fmt.Errorf("invalid seed length: expected %d bytes, got %d", ed25519.SeedSize, len(seedBytes))
|
||||
}
|
||||
|
||||
privateKey := ed25519.NewKeyFromSeed(seedBytes)
|
||||
|
||||
// Generate current timestamp
|
||||
timestamp := time.Now().UTC().Format(time.RFC3339)
|
||||
|
||||
// Sign the timestamp
|
||||
signature := ed25519.Sign(privateKey, []byte(timestamp))
|
||||
signedTimestamp := hex.EncodeToString(signature)
|
||||
signedTimestamp, err := c.signMessage(privateKeyBytes, []byte(timestamp))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign timestamp: %w", err)
|
||||
}
|
||||
signedTimestampHex := hex.EncodeToString(signedTimestamp)
|
||||
|
||||
// Exchange signature for registry token
|
||||
registryToken, err := c.exchangeTokenForRegistry(ctx, c.domain, timestamp, signedTimestamp)
|
||||
registryToken, err := c.exchangeTokenForRegistry(ctx, c.domain, timestamp, signedTimestampHex)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to exchange %s signature: %w", c.authMethod, err)
|
||||
}
|
||||
|
|
@ -58,6 +69,38 @@ func (c *CryptoProvider) GetToken(ctx context.Context) (string, error) {
|
|||
return registryToken, nil
|
||||
}
|
||||
|
||||
func (c *CryptoProvider) signMessage(privateKeyBytes []byte, message []byte) ([]byte, error) {
|
||||
switch c.cryptoAlgorithm {
|
||||
case AlgorithmEd25519:
|
||||
if len(privateKeyBytes) != ed25519.SeedSize {
|
||||
return nil, fmt.Errorf("invalid seed length: expected %d bytes, got %d", ed25519.SeedSize, len(privateKeyBytes))
|
||||
}
|
||||
|
||||
privateKey := ed25519.NewKeyFromSeed(privateKeyBytes)
|
||||
signature := ed25519.Sign(privateKey, message)
|
||||
return signature, nil
|
||||
case AlgorithmECDSAP384:
|
||||
if len(privateKeyBytes) != 48 {
|
||||
return nil, fmt.Errorf("invalid seed length for ECDSA P-384: expected 48 bytes, got %d", len(privateKeyBytes))
|
||||
}
|
||||
|
||||
digest := sha512.Sum384(message)
|
||||
curve := elliptic.P384()
|
||||
privateKey, err := ecdsa.ParseRawPrivateKey(curve, privateKeyBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse ECDSA private key: %w", err)
|
||||
}
|
||||
r, s, err := ecdsa.Sign(rand.Reader, privateKey, digest[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign message: %w", err)
|
||||
}
|
||||
signature := append(r.Bytes(), s.Bytes()...)
|
||||
return signature, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported crypto algorithm: %s", c.cryptoAlgorithm)
|
||||
}
|
||||
}
|
||||
|
||||
// NeedsLogin always returns false for cryptographic auth since no interactive login is needed
|
||||
func (c *CryptoProvider) NeedsLogin() bool {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ type DNSProvider struct {
|
|||
}
|
||||
|
||||
// NewDNSProvider creates a new DNS-based auth provider
|
||||
func NewDNSProvider(registryURL, domain, hexSeed string) Provider {
|
||||
func NewDNSProvider(registryURL, domain, privateKey string, cryptoAlgorithm CryptoAlgorithm) Provider {
|
||||
return &DNSProvider{
|
||||
CryptoProvider: &CryptoProvider{
|
||||
registryURL: registryURL,
|
||||
domain: domain,
|
||||
hexSeed: hexSeed,
|
||||
authMethod: "dns",
|
||||
registryURL: registryURL,
|
||||
domain: domain,
|
||||
privateKey: privateKey,
|
||||
cryptoAlgorithm: cryptoAlgorithm,
|
||||
authMethod: "dns",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ type HTTPProvider struct {
|
|||
}
|
||||
|
||||
// NewHTTPProvider creates a new HTTP-based auth provider
|
||||
func NewHTTPProvider(registryURL, domain, hexSeed string) Provider {
|
||||
func NewHTTPProvider(registryURL, domain, privateKey string, cryptoAlgorithm CryptoAlgorithm) Provider {
|
||||
return &HTTPProvider{
|
||||
CryptoProvider: &CryptoProvider{
|
||||
registryURL: registryURL,
|
||||
domain: domain,
|
||||
hexSeed: hexSeed,
|
||||
authMethod: "http",
|
||||
registryURL: registryURL,
|
||||
domain: domain,
|
||||
privateKey: privateKey,
|
||||
cryptoAlgorithm: cryptoAlgorithm,
|
||||
authMethod: "http",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,21 @@ const (
|
|||
TokenFileName = ".mcp_publisher_token" //nolint:gosec // Not a credential, just a filename
|
||||
)
|
||||
|
||||
type CryptoAlgorithm auth.CryptoAlgorithm
|
||||
|
||||
func (c *CryptoAlgorithm) String() string {
|
||||
return string(*c)
|
||||
}
|
||||
|
||||
func (c *CryptoAlgorithm) Set(v string) error {
|
||||
switch v {
|
||||
case string(auth.AlgorithmEd25519), string(auth.AlgorithmECDSAP384):
|
||||
*c = CryptoAlgorithm(v)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid algorithm: %q (allowed: ed25519, ecdsap384)", v)
|
||||
}
|
||||
|
||||
func LoginCommand(args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errors.New("authentication method required\n\nUsage: mcp-publisher login <method>\n\nMethods:\n github Interactive GitHub authentication\n github-oidc GitHub Actions OIDC authentication\n dns DNS-based authentication (requires --domain and --private-key)\n http HTTP-based authentication (requires --domain and --private-key)\n none Anonymous authentication (for testing)")
|
||||
|
|
@ -28,13 +43,15 @@ func LoginCommand(args []string) error {
|
|||
loginFlags := flag.NewFlagSet("login", flag.ExitOnError)
|
||||
var domain string
|
||||
var privateKey string
|
||||
var cryptoAlgorithm = CryptoAlgorithm(auth.AlgorithmEd25519)
|
||||
var registryURL string
|
||||
|
||||
loginFlags.StringVar(®istryURL, "registry", DefaultRegistryURL, "Registry URL")
|
||||
|
||||
if method == "dns" || method == "http" {
|
||||
loginFlags.StringVar(&domain, "domain", "", "Domain name")
|
||||
loginFlags.StringVar(&privateKey, "private-key", "", "Private key (64-char hex)")
|
||||
loginFlags.StringVar(&privateKey, "private-key", "", "Private key (hex)")
|
||||
loginFlags.Var(&cryptoAlgorithm, "algorithm", "Cryptographic algorithm (ed25519, ecdsap384)")
|
||||
}
|
||||
|
||||
if err := loginFlags.Parse(args[1:]); err != nil {
|
||||
|
|
@ -52,12 +69,12 @@ func LoginCommand(args []string) error {
|
|||
if domain == "" || privateKey == "" {
|
||||
return errors.New("dns authentication requires --domain and --private-key")
|
||||
}
|
||||
authProvider = auth.NewDNSProvider(registryURL, domain, privateKey)
|
||||
authProvider = auth.NewDNSProvider(registryURL, domain, privateKey, auth.CryptoAlgorithm(cryptoAlgorithm))
|
||||
case "http":
|
||||
if domain == "" || privateKey == "" {
|
||||
return errors.New("http authentication requires --domain and --private-key")
|
||||
}
|
||||
authProvider = auth.NewHTTPProvider(registryURL, domain, privateKey)
|
||||
authProvider = auth.NewHTTPProvider(registryURL, domain, privateKey, auth.CryptoAlgorithm(cryptoAlgorithm))
|
||||
case "none":
|
||||
authProvider = auth.NewNoneProvider(registryURL)
|
||||
default:
|
||||
|
|
|
|||
Loading…
Reference in a new issue