Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/publisher/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ make dev-compose # Start local registry
- **`http`** - Domain verification via HTTPS endpoints
- **`none`** - No auth (testing only)

### Signing Providers
Optional, for `dns` and `http` to sign out of process without direct access to the private key.

- **`google-kms`** - Google KMS signing
- **`azure-key-vault`** - Azure Key Vault signing

## Key Files

- **`main.go`** - CLI setup and command routing
Expand Down
85 changes: 85 additions & 0 deletions cmd/publisher/auth/azurekeyvault/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package azurekeyvault

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha512"
"fmt"
"math/big"
"os"

"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys"
"github.com/modelcontextprotocol/registry/cmd/publisher/auth"
)

func GetSignatureProvider(vaultName, keyName string) (auth.Signer, error) {
if vaultName == "" {
return nil, fmt.Errorf("--vault option (vault name) is required")
}

if keyName == "" {
return nil, fmt.Errorf("--key option (key name) is required")
}

return Signer{
vaultName: vaultName,
keyName: keyName,
}, nil
}

type Signer struct {
vaultName string
keyName string
}

func (d Signer) GetSignedTimestamp(ctx context.Context) (*string, []byte, error) {
fmt.Fprintf(os.Stdout, "Signing using Azure Key Vault %s and key %s\n", d.vaultName, d.keyName)

cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return nil, nil, fmt.Errorf("authentication to Azure failed: %w", err)
}

vaultURL := fmt.Sprintf("https://%s.vault.azure.net/", d.vaultName)
client, err := azkeys.NewClient(vaultURL, cred, nil)
if err != nil {
return nil, nil, fmt.Errorf("failed to create Key Vault client: %w", err)
}

keyResp, err := client.GetKey(ctx, d.keyName, "", nil)
if err != nil {
return nil, nil, fmt.Errorf("failed to retrieve key for public parameters: %w", err)
}

if *keyResp.Key.Kty != azkeys.KeyTypeEC && *keyResp.Key.Kty != azkeys.KeyTypeECHSM {
return nil, nil, fmt.Errorf("unsupported key type: kty=%v (only EC keys are supported)", keyResp.Key.Kty)
}

if *keyResp.Key.Crv != azkeys.CurveNameP384 {
return nil, nil, fmt.Errorf("unsupported curve: crv=%v (only P-384 is supported)", keyResp.Key.Crv)
}

fmt.Fprintln(os.Stdout, "Successfully read the public key from Key Vault.")
auth.PrintEcdsaP384KeyInfo(ecdsa.PublicKey{
Curve: elliptic.P384(),
X: new(big.Int).SetBytes(keyResp.Key.X),
Y: new(big.Int).SetBytes(keyResp.Key.Y),
})

timestamp := auth.GetTimestamp()
digest := sha512.Sum384([]byte(timestamp))
alg := azkeys.SignatureAlgorithmES384
fmt.Fprintln(os.Stdout, "Executing the sign request...")
signResp, err := client.Sign(ctx, d.keyName, "", azkeys.SignParameters{
Algorithm: &alg,
Value: digest[:],
}, nil)

if err != nil {
return nil, nil, fmt.Errorf("failed to sign message: %w", err)
}

return &timestamp, signResp.Result, nil
}
134 changes: 102 additions & 32 deletions cmd/publisher/auth/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ import (
"crypto/elliptic"
"crypto/rand"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"os"
"strings"
"time"
)

Expand All @@ -29,76 +33,125 @@ const (

// CryptoProvider provides common functionality for DNS and HTTP authentication
type CryptoProvider struct {
registryURL string
domain string
privateKey string
cryptoAlgorithm CryptoAlgorithm
authMethod string
registryURL string
domain string
signer Signer
authMethod string
}

// GetToken retrieves the registry JWT token using cryptographic authentication
func (c *CryptoProvider) GetToken(ctx context.Context) (string, error) {
if c.domain == "" {
return "", fmt.Errorf("%s domain is required", c.authMethod)
}
type Signer interface {
GetSignedTimestamp(ctx context.Context) (*string, []byte, error)
}

if c.privateKey == "" {
return "", fmt.Errorf("%s private key (hex) is required", c.authMethod)
func GetTimestamp() string {
return time.Now().UTC().Format(time.RFC3339)
}

func NewInProcessSigner(privateKey string, algorithm CryptoAlgorithm) (Signer, error) {
if privateKey == "" {
return nil, fmt.Errorf("%s private key (hex) is required", algorithm)
}

// Decode private key from hex
privateKeyBytes, err := hex.DecodeString(c.privateKey)
privateKeyBytes, err := hex.DecodeString(privateKey)
if err != nil {
return "", fmt.Errorf("invalid hex private key format: %w", err)
return nil, fmt.Errorf("invalid hex private key format: %w", err)
}

return &InProcessSigner{
privateKey: privateKeyBytes,
cryptoAlgorithm: algorithm,
}, nil
}

// GetToken retrieves the registry JWT token using cryptographic authentication
func (c *CryptoProvider) GetToken(ctx context.Context) (string, error) {
if c.domain == "" {
return "", fmt.Errorf("%s domain is required", c.authMethod)
}

// Generate current timestamp
timestamp := time.Now().UTC().Format(time.RFC3339)
signedTimestamp, err := c.signMessage(privateKeyBytes, []byte(timestamp))
timestamp, signedTimestamp, err := c.signer.GetSignedTimestamp(ctx)
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, signedTimestampHex)
registryToken, err := c.exchangeTokenForRegistry(ctx, c.domain, *timestamp, signedTimestampHex)
if err != nil {
return "", fmt.Errorf("failed to exchange %s signature: %w", c.authMethod, err)
}

return registryToken, nil
}

func (c *CryptoProvider) signMessage(privateKeyBytes []byte, message []byte) ([]byte, error) {
type InProcessSigner struct {
privateKey []byte
cryptoAlgorithm CryptoAlgorithm
}

func (c *InProcessSigner) GetSignedTimestamp(_ context.Context) (*string, []byte, error) {
fmt.Fprintf(os.Stdout, "Signing in process using key algorithm %s\n", c.cryptoAlgorithm)

timestamp := GetTimestamp()

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))
if len(c.privateKey) != ed25519.SeedSize {
return nil, nil, fmt.Errorf("invalid seed length: expected %d bytes, got %d", ed25519.SeedSize, len(c.privateKey))
}

privateKey := ed25519.NewKeyFromSeed(privateKeyBytes)
signature := ed25519.Sign(privateKey, message)
return signature, nil
privateKey := ed25519.NewKeyFromSeed(c.privateKey)

PrintEd25519KeyInfo(privateKey.Public().(ed25519.PublicKey))

signature := ed25519.Sign(privateKey, []byte(timestamp))
return &timestamp, 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))
if len(c.privateKey) != 48 {
return nil, nil, fmt.Errorf("invalid seed length for ECDSA P-384: expected 48 bytes, got %d", len(c.privateKey))
}

digest := sha512.Sum384(message)
digest := sha512.Sum384([]byte(timestamp))
curve := elliptic.P384()
privateKey, err := ecdsa.ParseRawPrivateKey(curve, privateKeyBytes)
privateKey, err := parseRawPrivateKey(curve, c.privateKey)
if err != nil {
return nil, fmt.Errorf("failed to parse ECDSA private key: %w", err)
return nil, nil, fmt.Errorf("failed to parse ECDSA private key: %w", err)
}

PrintEcdsaP384KeyInfo(privateKey.PublicKey)

r, s, err := ecdsa.Sign(rand.Reader, privateKey, digest[:])
if err != nil {
return nil, fmt.Errorf("failed to sign message: %w", err)
return nil, nil, fmt.Errorf("failed to sign message: %w", err)
}
signature := append(r.Bytes(), s.Bytes()...)
return signature, nil
return &timestamp, signature, nil
default:
return nil, fmt.Errorf("unsupported crypto algorithm: %s", c.cryptoAlgorithm)
return nil, nil, fmt.Errorf("unsupported crypto algorithm: %s", c.cryptoAlgorithm)
}
}

func parseRawPrivateKey(curve elliptic.Curve, bytes []byte) (*ecdsa.PrivateKey, error) {
// ecdsa.ParseRawPrivateKey is not available until Go 1.25.
// This is the equivalent implementation.
expectedBytes := (curve.Params().N.BitLen() + 7) / 8
if len(bytes) != expectedBytes {
return nil, fmt.Errorf("invalid private key length: expected %d bytes, got %d", expectedBytes, len(bytes))
}

d := new(big.Int).SetBytes(bytes)
x, y := curve.ScalarBaseMult(bytes)
privateKey := &ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: curve,
X: x,
Y: y,
},
D: d,
}
return privateKey, nil
}

// NeedsLogin always returns false for cryptographic auth since no interactive login is needed
Expand All @@ -111,6 +164,23 @@ func (c *CryptoProvider) Login(_ context.Context) error {
return nil
}

func PrintEd25519KeyInfo(pubKey ed25519.PublicKey) {
pubKeyString := base64.StdEncoding.EncodeToString(pubKey)
fmt.Fprint(os.Stdout, "Expected proof record:\n")
fmt.Fprintf(os.Stdout, "v=MCPv1; k=ed25519; p=%s\n", pubKeyString)
}

func PrintEcdsaP384KeyInfo(pubKey ecdsa.PublicKey) {
printEcdsaKeyInfo("ecdsap384", pubKey)
}

func printEcdsaKeyInfo(k string, pubKey ecdsa.PublicKey) {
compressed := elliptic.MarshalCompressed(pubKey.Curve, pubKey.X, pubKey.Y)
pubKeyString := base64.StdEncoding.EncodeToString(compressed)
fmt.Fprint(os.Stdout, "Expected proof record:\n")
fmt.Fprintf(os.Stdout, "v=MCPv1; k=%s; p=%s\n", k, pubKeyString)
}

// exchangeTokenForRegistry exchanges signature for a registry JWT token
func (c *CryptoProvider) exchangeTokenForRegistry(ctx context.Context, domain, timestamp, signedTimestamp string) (string, error) {
if c.registryURL == "" {
Expand All @@ -130,7 +200,7 @@ func (c *CryptoProvider) exchangeTokenForRegistry(ctx context.Context, domain, t
}

// Make the token exchange request
exchangeURL := fmt.Sprintf("%s/v0/auth/%s", c.registryURL, c.authMethod)
exchangeURL := fmt.Sprintf("%s/v0/auth/%s", strings.TrimSuffix(c.registryURL, "/"), c.authMethod)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, exchangeURL, bytes.NewBuffer(jsonData))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
Expand Down
11 changes: 5 additions & 6 deletions cmd/publisher/auth/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ type DNSProvider struct {
}

// NewDNSProvider creates a new DNS-based auth provider
func NewDNSProvider(registryURL, domain, privateKey string, cryptoAlgorithm CryptoAlgorithm) Provider {
func NewDNSProvider(registryURL, domain string, signer *Signer) Provider {
return &DNSProvider{
CryptoProvider: &CryptoProvider{
registryURL: registryURL,
domain: domain,
privateKey: privateKey,
cryptoAlgorithm: cryptoAlgorithm,
authMethod: "dns",
registryURL: registryURL,
domain: domain,
signer: *signer,
authMethod: "dns",
},
}
}
Expand Down
Loading