Skip to content

Commit 2ccf2d0

Browse files
committed
Add support for ECDSA P-384
1 parent 9164415 commit 2ccf2d0

File tree

10 files changed

+1624
-74
lines changed

10 files changed

+1624
-74
lines changed

cmd/publisher/auth/common.go

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ package auth
33
import (
44
"bytes"
55
"context"
6+
"crypto/ecdsa"
67
"crypto/ed25519"
8+
"crypto/elliptic"
9+
"crypto/rand"
10+
"crypto/sha512"
711
"encoding/hex"
812
"encoding/json"
913
"fmt"
@@ -12,12 +16,24 @@ import (
1216
"time"
1317
)
1418

19+
type CryptoAlgorithm string
20+
21+
const (
22+
AlgorithmEd25519 CryptoAlgorithm = "ed25519"
23+
24+
// ECDSA with NIST P-384 curve
25+
// public key is in compressed format
26+
// signature is in R || S format
27+
AlgorithmECDSAP384 CryptoAlgorithm = "ecdsap384"
28+
)
29+
1530
// CryptoProvider provides common functionality for DNS and HTTP authentication
1631
type CryptoProvider struct {
17-
registryURL string
18-
domain string
19-
hexSeed string
20-
authMethod string
32+
registryURL string
33+
domain string
34+
privateKey string
35+
cryptoAlgorithm CryptoAlgorithm
36+
authMethod string
2137
}
2238

2339
// GetToken retrieves the registry JWT token using cryptographic authentication
@@ -26,38 +42,65 @@ func (c *CryptoProvider) GetToken(ctx context.Context) (string, error) {
2642
return "", fmt.Errorf("%s domain is required", c.authMethod)
2743
}
2844

29-
if c.hexSeed == "" {
30-
return "", fmt.Errorf("%s private key (hex seed) is required", c.authMethod)
45+
if c.privateKey == "" {
46+
return "", fmt.Errorf("%s private key (hex) is required", c.authMethod)
3147
}
3248

33-
// Decode hex seed to private key
34-
seedBytes, err := hex.DecodeString(c.hexSeed)
49+
// Decode private key from hex
50+
privateKeyBytes, err := hex.DecodeString(c.privateKey)
3551
if err != nil {
36-
return "", fmt.Errorf("invalid hex seed format: %w", err)
52+
return "", fmt.Errorf("invalid hex private key format: %w", err)
3753
}
3854

39-
if len(seedBytes) != ed25519.SeedSize {
40-
return "", fmt.Errorf("invalid seed length: expected %d bytes, got %d", ed25519.SeedSize, len(seedBytes))
41-
}
42-
43-
privateKey := ed25519.NewKeyFromSeed(seedBytes)
44-
4555
// Generate current timestamp
4656
timestamp := time.Now().UTC().Format(time.RFC3339)
47-
48-
// Sign the timestamp
49-
signature := ed25519.Sign(privateKey, []byte(timestamp))
50-
signedTimestamp := hex.EncodeToString(signature)
57+
signedTimestamp, err := c.signMessage(privateKeyBytes, []byte(timestamp))
58+
if err != nil {
59+
return "", fmt.Errorf("failed to sign timestamp: %w", err)
60+
}
61+
signedTimestampHex := hex.EncodeToString(signedTimestamp)
5162

5263
// Exchange signature for registry token
53-
registryToken, err := c.exchangeTokenForRegistry(ctx, c.domain, timestamp, signedTimestamp)
64+
registryToken, err := c.exchangeTokenForRegistry(ctx, c.domain, timestamp, signedTimestampHex)
5465
if err != nil {
5566
return "", fmt.Errorf("failed to exchange %s signature: %w", c.authMethod, err)
5667
}
5768

5869
return registryToken, nil
5970
}
6071

72+
func (c *CryptoProvider) signMessage(privateKeyBytes []byte, message []byte) ([]byte, error) {
73+
switch c.cryptoAlgorithm {
74+
case AlgorithmEd25519:
75+
if len(privateKeyBytes) != ed25519.SeedSize {
76+
return nil, fmt.Errorf("invalid seed length: expected %d bytes, got %d", ed25519.SeedSize, len(privateKeyBytes))
77+
}
78+
79+
privateKey := ed25519.NewKeyFromSeed(privateKeyBytes)
80+
signature := ed25519.Sign(privateKey, message)
81+
return signature, nil
82+
case AlgorithmECDSAP384:
83+
if len(privateKeyBytes) != 48 {
84+
return nil, fmt.Errorf("invalid seed length for ECDSA P-384: expected 48 bytes, got %d", len(privateKeyBytes))
85+
}
86+
87+
digest := sha512.Sum384(message)
88+
curve := elliptic.P384()
89+
privateKey, err := ecdsa.ParseRawPrivateKey(curve, privateKeyBytes)
90+
if err != nil {
91+
return nil, fmt.Errorf("failed to parse ECDSA private key: %w", err)
92+
}
93+
r, s, err := ecdsa.Sign(rand.Reader, privateKey, digest[:])
94+
if err != nil {
95+
return nil, fmt.Errorf("failed to sign message: %w", err)
96+
}
97+
signature := append(r.Bytes(), s.Bytes()...)
98+
return signature, nil
99+
default:
100+
return nil, fmt.Errorf("unsupported crypto algorithm: %s", c.cryptoAlgorithm)
101+
}
102+
}
103+
61104
// NeedsLogin always returns false for cryptographic auth since no interactive login is needed
62105
func (c *CryptoProvider) NeedsLogin() bool {
63106
return false

cmd/publisher/auth/dns.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ type DNSProvider struct {
55
}
66

77
// NewDNSProvider creates a new DNS-based auth provider
8-
func NewDNSProvider(registryURL, domain, hexSeed string) Provider {
8+
func NewDNSProvider(registryURL, domain, privateKey string, cryptoAlgorithm CryptoAlgorithm) Provider {
99
return &DNSProvider{
1010
CryptoProvider: &CryptoProvider{
11-
registryURL: registryURL,
12-
domain: domain,
13-
hexSeed: hexSeed,
14-
authMethod: "dns",
11+
registryURL: registryURL,
12+
domain: domain,
13+
privateKey: privateKey,
14+
cryptoAlgorithm: cryptoAlgorithm,
15+
authMethod: "dns",
1516
},
1617
}
1718
}

cmd/publisher/auth/http.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ type HTTPProvider struct {
55
}
66

77
// NewHTTPProvider creates a new HTTP-based auth provider
8-
func NewHTTPProvider(registryURL, domain, hexSeed string) Provider {
8+
func NewHTTPProvider(registryURL, domain, privateKey string, cryptoAlgorithm CryptoAlgorithm) Provider {
99
return &HTTPProvider{
1010
CryptoProvider: &CryptoProvider{
11-
registryURL: registryURL,
12-
domain: domain,
13-
hexSeed: hexSeed,
14-
authMethod: "http",
11+
registryURL: registryURL,
12+
domain: domain,
13+
privateKey: privateKey,
14+
cryptoAlgorithm: cryptoAlgorithm,
15+
authMethod: "http",
1516
},
1617
}
1718
}

cmd/publisher/commands/login.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ const (
1717
TokenFileName = ".mcp_publisher_token" //nolint:gosec // Not a credential, just a filename
1818
)
1919

20+
type CryptoAlgorithm auth.CryptoAlgorithm
21+
22+
func (c *CryptoAlgorithm) String() string {
23+
return string(*c)
24+
}
25+
26+
func (c *CryptoAlgorithm) Set(v string) error {
27+
switch v {
28+
case string(auth.AlgorithmEd25519), string(auth.AlgorithmECDSAP384):
29+
*c = CryptoAlgorithm(v)
30+
return nil
31+
}
32+
return fmt.Errorf("invalid algorithm: %q (allowed: ed25519, ecdsap384)", v)
33+
}
34+
2035
func LoginCommand(args []string) error {
2136
if len(args) < 1 {
2237
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 {
2843
loginFlags := flag.NewFlagSet("login", flag.ExitOnError)
2944
var domain string
3045
var privateKey string
46+
var cryptoAlgorithm = CryptoAlgorithm(auth.AlgorithmEd25519)
3147
var registryURL string
3248

3349
loginFlags.StringVar(&registryURL, "registry", DefaultRegistryURL, "Registry URL")
3450

3551
if method == "dns" || method == "http" {
3652
loginFlags.StringVar(&domain, "domain", "", "Domain name")
37-
loginFlags.StringVar(&privateKey, "private-key", "", "Private key (64-char hex)")
53+
loginFlags.StringVar(&privateKey, "private-key", "", "Private key (hex)")
54+
loginFlags.Var(&cryptoAlgorithm, "algorithm", "Cryptographic algorithm (ed25519, ecdsap384)")
3855
}
3956

4057
if err := loginFlags.Parse(args[1:]); err != nil {
@@ -52,12 +69,12 @@ func LoginCommand(args []string) error {
5269
if domain == "" || privateKey == "" {
5370
return errors.New("dns authentication requires --domain and --private-key")
5471
}
55-
authProvider = auth.NewDNSProvider(registryURL, domain, privateKey)
72+
authProvider = auth.NewDNSProvider(registryURL, domain, privateKey, auth.CryptoAlgorithm(cryptoAlgorithm))
5673
case "http":
5774
if domain == "" || privateKey == "" {
5875
return errors.New("http authentication requires --domain and --private-key")
5976
}
60-
authProvider = auth.NewHTTPProvider(registryURL, domain, privateKey)
77+
authProvider = auth.NewHTTPProvider(registryURL, domain, privateKey, auth.CryptoAlgorithm(cryptoAlgorithm))
6178
case "none":
6279
authProvider = auth.NewNoneProvider(registryURL)
6380
default:

0 commit comments

Comments
 (0)