From 314f336ec5e6d025c21e2cde4d18bbd0ca3d647a Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Thu, 16 Oct 2025 16:56:58 -0400 Subject: [PATCH 1/8] Add Azure Key Vault and Google KMS support Design doc is in this issue comment: https://github.com/modelcontextprotocol/registry/issues/482#issuecomment-3437117171 --- cmd/publisher/README.md | 6 + cmd/publisher/auth/azurekeyvault/common.go | 85 +++++++++++ cmd/publisher/auth/common.go | 112 ++++++++++----- cmd/publisher/auth/dns.go | 11 +- cmd/publisher/auth/googlekms/common.go | 147 +++++++++++++++++++ cmd/publisher/auth/http.go | 11 +- cmd/publisher/commands/login.go | 158 ++++++++++++++++----- docs/reference/cli/commands.md | 60 ++++++++ go.mod | 36 ++++- go.sum | 79 +++++++++-- 10 files changed, 614 insertions(+), 91 deletions(-) create mode 100644 cmd/publisher/auth/azurekeyvault/common.go create mode 100644 cmd/publisher/auth/googlekms/common.go diff --git a/cmd/publisher/README.md b/cmd/publisher/README.md index 274cab82..30762b21 100644 --- a/cmd/publisher/README.md +++ b/cmd/publisher/README.md @@ -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 diff --git a/cmd/publisher/auth/azurekeyvault/common.go b/cmd/publisher/auth/azurekeyvault/common.go new file mode 100644 index 00000000..e8600949 --- /dev/null +++ b/cmd/publisher/auth/azurekeyvault/common.go @@ -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 ×tamp, signResp.Result, nil +} diff --git a/cmd/publisher/auth/common.go b/cmd/publisher/auth/common.go index 94b0cbb5..2dacaaad 100644 --- a/cmd/publisher/auth/common.go +++ b/cmd/publisher/auth/common.go @@ -8,11 +8,14 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/sha512" + "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" "net/http" + "os" + "strings" "time" ) @@ -29,39 +32,52 @@ 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) +} + +func GetTimestamp() string { + return time.Now().UTC().Format(time.RFC3339) +} - if c.privateKey == "" { - return "", fmt.Errorf("%s private key (hex) is required", c.authMethod) +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) } @@ -69,35 +85,50 @@ func (c *CryptoProvider) GetToken(ctx context.Context) (string, error) { 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 ×tamp, 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 := ecdsa.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 ×tamp, signature, nil default: - return nil, fmt.Errorf("unsupported crypto algorithm: %s", c.cryptoAlgorithm) + return nil, nil, fmt.Errorf("unsupported crypto algorithm: %s", c.cryptoAlgorithm) } } @@ -111,6 +142,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 == "" { @@ -130,7 +178,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) diff --git a/cmd/publisher/auth/dns.go b/cmd/publisher/auth/dns.go index f14d7341..af81a1c5 100644 --- a/cmd/publisher/auth/dns.go +++ b/cmd/publisher/auth/dns.go @@ -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", }, } } diff --git a/cmd/publisher/auth/googlekms/common.go b/cmd/publisher/auth/googlekms/common.go new file mode 100644 index 00000000..f5538051 --- /dev/null +++ b/cmd/publisher/auth/googlekms/common.go @@ -0,0 +1,147 @@ +package googlekms + +import ( + "context" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/sha512" + "crypto/x509" + "encoding/asn1" + "encoding/pem" + "errors" + "fmt" + "math/big" + "os" + + kms "cloud.google.com/go/kms/apiv1" + "cloud.google.com/go/kms/apiv1/kmspb" + "github.com/modelcontextprotocol/registry/cmd/publisher/auth" +) + +// GetSignatureProvider validates inputs and returns a GoogleKMSSigner implementing auth.Signer. +func GetSignatureProvider(resourceName string) (auth.Signer, error) { + if resourceName == "" { + return nil, fmt.Errorf("--resource is required, e.g. projects/my-project/locations/global/keyRings/fellowship/cryptoKeys/bilbo/cryptoKeyVersions/1") + } + + return &Signer{ + resource: resourceName, + }, nil +} + +type Signer struct { + resource string +} + +type ecdsaSignature struct { + R, S *big.Int +} + +func derToRS(der []byte, curve elliptic.Curve) ([]byte, error) { + var sig ecdsaSignature + if _, err := asn1.Unmarshal(der, &sig); err != nil { + return nil, fmt.Errorf("invalid DER ECDSA signature: %w", err) + } + if sig.R == nil || sig.S == nil || sig.R.Sign() <= 0 || sig.S.Sign() <= 0 { + return nil, fmt.Errorf("invalid ECDSA signature components") + } + + size := (curve.Params().BitSize + 7) / 8 + rBytes := sig.R.Bytes() + sBytes := sig.S.Bytes() + if len(rBytes) > size || len(sBytes) > size { + return nil, fmt.Errorf("ECDSA signature component too large") + } + + out := make([]byte, 2*size) + copy(out[size-len(rBytes):size], rBytes) + copy(out[2*size-len(sBytes):], sBytes) + return out, nil +} + +func (g *Signer) GetSignedTimestamp(ctx context.Context) (*string, []byte, error) { + client, err := kms.NewKeyManagementClient(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to create KMS client: %w", err) + } + defer client.Close() + + // Fetch public key (PEM) so we can output expected proof record. + algo, err := g.showPublicKeyAndGetAlgorithm(ctx, client) + if err != nil { + return nil, nil, err + } + + timestamp := auth.GetTimestamp() + + fmt.Fprintln(os.Stdout, "Executing the sign request...") + switch algo { + case auth.AlgorithmEd25519: + signReq := &kmspb.AsymmetricSignRequest{ + Name: g.resource, + Data: []byte(timestamp), + } + signResp, err := client.AsymmetricSign(ctx, signReq) + if err != nil { + return nil, nil, fmt.Errorf("failed to sign with KMS: %w", err) + } + + return ×tamp, signResp.Signature, nil + case auth.AlgorithmECDSAP384: + digest := sha512.Sum384([]byte(timestamp)) + signReq := &kmspb.AsymmetricSignRequest{ + Name: g.resource, + Digest: &kmspb.Digest{Digest: &kmspb.Digest_Sha384{Sha384: digest[:]}}, + } + signResp, err := client.AsymmetricSign(ctx, signReq) + if err != nil { + return nil, nil, fmt.Errorf("failed to sign with KMS: %w", err) + } + + sigBytes, err := derToRS(signResp.Signature, elliptic.P384()) + if err != nil { + return nil, nil, fmt.Errorf("failed to convert DER signature: %w", err) + } + + return ×tamp, sigBytes, nil + } + + return nil, nil, fmt.Errorf("unsupported algorithm: %s", algo) +} + +func (g *Signer) showPublicKeyAndGetAlgorithm(ctx context.Context, client *kms.KeyManagementClient) (auth.CryptoAlgorithm, error) { + pubResp, err := client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{Name: g.resource}) + if err != nil { + return "", fmt.Errorf("failed to get public key: %w", err) + } + + block, _ := pem.Decode([]byte(pubResp.Pem)) + if block == nil { + return "", errors.New("failed to decode PEM public key from KMS") + } + + parsed, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return "", fmt.Errorf("failed to parse public key: %w", err) + } + + switch pubResp.Algorithm { //nolint:exhaustive + case kmspb.CryptoKeyVersion_EC_SIGN_ED25519: + pk, ok := parsed.(ed25519.PublicKey) + if !ok { + return "", errors.New("KMS reported ED25519 but parsed key is different type") + } + auth.PrintEd25519KeyInfo(pk) + return auth.AlgorithmEd25519, nil + case kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384: + pk, ok := parsed.(*ecdsa.PublicKey) + if !ok || pk.Curve != elliptic.P384() { + return "", errors.New("KMS reported P-384 but parsed key mismatch") + } + auth.PrintEcdsaP384KeyInfo(*pk) + return auth.AlgorithmECDSAP384, nil + default: + return "", fmt.Errorf("unsupported KMS key algorithm: %s", pubResp.Algorithm.String()) + } +} diff --git a/cmd/publisher/auth/http.go b/cmd/publisher/auth/http.go index 788c72d8..2d44781b 100644 --- a/cmd/publisher/auth/http.go +++ b/cmd/publisher/auth/http.go @@ -5,14 +5,13 @@ type HTTPProvider struct { } // NewHTTPProvider creates a new HTTP-based auth provider -func NewHTTPProvider(registryURL, domain, privateKey string, cryptoAlgorithm CryptoAlgorithm) Provider { +func NewHTTPProvider(registryURL, domain string, signer *Signer) Provider { return &HTTPProvider{ CryptoProvider: &CryptoProvider{ - registryURL: registryURL, - domain: domain, - privateKey: privateKey, - cryptoAlgorithm: cryptoAlgorithm, - authMethod: "http", + registryURL: registryURL, + domain: domain, + signer: *signer, + authMethod: "http", }, } } diff --git a/cmd/publisher/commands/login.go b/cmd/publisher/commands/login.go index 940b9f91..ddeba236 100644 --- a/cmd/publisher/commands/login.go +++ b/cmd/publisher/commands/login.go @@ -10,6 +10,8 @@ import ( "path/filepath" "github.com/modelcontextprotocol/registry/cmd/publisher/auth" + "github.com/modelcontextprotocol/registry/cmd/publisher/auth/azurekeyvault" + "github.com/modelcontextprotocol/registry/cmd/publisher/auth/googlekms" ) const ( @@ -19,6 +21,27 @@ const ( type CryptoAlgorithm auth.CryptoAlgorithm +type SignerType string + +type LoginFlags struct { + Domain string + PrivateKey string + RegistryURL string + KvVault string + KvKeyName string + KmsResource string + CryptoAlgorithm CryptoAlgorithm + SignerType SignerType + ArgOffset int +} + +const ( + InProcessSignerType SignerType = "in-process" + AzureKeyVaultSignerType SignerType = "azure-key-vault" + GoogleKMSSignerType SignerType = "google-kms" + NoSignerType SignerType = "none" +) + func (c *CryptoAlgorithm) String() string { return string(*c) } @@ -32,56 +55,123 @@ func (c *CryptoAlgorithm) Set(v string) error { 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 \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)") - } - - method := args[0] - - // Parse remaining flags based on method +func parseLoginFlags(method string, args []string) (LoginFlags, error) { + var flags LoginFlags 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") + flags.CryptoAlgorithm = CryptoAlgorithm(auth.AlgorithmEd25519) + flags.SignerType = NoSignerType + flags.ArgOffset = 1 + loginFlags.StringVar(&flags.RegistryURL, "registry", DefaultRegistryURL, "Registry URL") if method == "dns" || method == "http" { - loginFlags.StringVar(&domain, "domain", "", "Domain name") - loginFlags.StringVar(&privateKey, "private-key", "", "Private key (hex)") - loginFlags.Var(&cryptoAlgorithm, "algorithm", "Cryptographic algorithm (ed25519, ecdsap384)") + loginFlags.StringVar(&flags.Domain, "domain", "", "Domain name") + if len(args) > 1 { + switch args[1] { + case string(AzureKeyVaultSignerType): + flags.SignerType = AzureKeyVaultSignerType + loginFlags.StringVar(&flags.KvVault, "vault", "", "The name of the Azure Key Vault resource") + loginFlags.StringVar(&flags.KvKeyName, "key", "", "Name of the signing key in the Azure Key Vault") + flags.ArgOffset = 2 + case string(GoogleKMSSignerType): + flags.SignerType = GoogleKMSSignerType + loginFlags.StringVar(&flags.KmsResource, "resource", "", "Google Cloud KMS resource name (e.g. projects/lotr/locations/global/keyRings/fellowship/cryptoKeys/frodo/cryptoKeyVersions/1)") + flags.ArgOffset = 2 + } + } + if flags.SignerType == NoSignerType { + flags.SignerType = InProcessSignerType + loginFlags.StringVar(&flags.PrivateKey, "private-key", "", "Private key (hex)") + loginFlags.Var(&flags.CryptoAlgorithm, "algorithm", "Cryptographic algorithm (ed25519, ecdsap384)") + } } + err := loginFlags.Parse(args[flags.ArgOffset:]) + return flags, err +} - if err := loginFlags.Parse(args[1:]); err != nil { - return err +func createSigner(flags LoginFlags) (auth.Signer, error) { + switch flags.SignerType { + case AzureKeyVaultSignerType: + return azurekeyvault.GetSignatureProvider(flags.KvVault, flags.KvKeyName) + case GoogleKMSSignerType: + return googlekms.GetSignatureProvider(flags.KmsResource) + case InProcessSignerType: + return auth.NewInProcessSigner(flags.PrivateKey, auth.CryptoAlgorithm(flags.CryptoAlgorithm)) + case NoSignerType: + return nil, errors.New("no signing provider specified") + default: + return nil, errors.New("unknown signing provider specified") } +} - // Create auth provider based on method - var authProvider auth.Provider +func createAuthProvider(method, registryURL, domain string, signer auth.Signer) (auth.Provider, error) { switch method { case "github": - authProvider = auth.NewGitHubATProvider(true, registryURL) + return auth.NewGitHubATProvider(true, registryURL), nil case "github-oidc": - authProvider = auth.NewGitHubOIDCProvider(registryURL) + return auth.NewGitHubOIDCProvider(registryURL), nil case "dns": - if domain == "" || privateKey == "" { - return errors.New("dns authentication requires --domain and --private-key") + if domain == "" { + return nil, errors.New("dns authentication requires --domain") } - authProvider = auth.NewDNSProvider(registryURL, domain, privateKey, auth.CryptoAlgorithm(cryptoAlgorithm)) + return auth.NewDNSProvider(registryURL, domain, &signer), nil case "http": - if domain == "" || privateKey == "" { - return errors.New("http authentication requires --domain and --private-key") + if domain == "" { + return nil, errors.New("http authentication requires --domain") } - authProvider = auth.NewHTTPProvider(registryURL, domain, privateKey, auth.CryptoAlgorithm(cryptoAlgorithm)) + return auth.NewHTTPProvider(registryURL, domain, &signer), nil case "none": - authProvider = auth.NewNoneProvider(registryURL) + return auth.NewNoneProvider(registryURL), nil default: - return fmt.Errorf("unknown authentication method: %s\nFor a list of available methods, run: mcp-publisher login", method) + return nil, fmt.Errorf("unknown authentication method: %s\nFor a list of available methods, run: mcp-publisher login", method) + } +} + +func LoginCommand(args []string) error { + if len(args) < 1 { + return errors.New(`authentication method required + +Usage: mcp-publisher login [] + +Methods: + github Interactive GitHub authentication + github-oidc GitHub Actions OIDC authentication + dns DNS-based authentication (requires --domain) + http HTTP-based authentication (requires --domain) + none Anonymous authentication (for testing) + +Signing providers: + azure-key-vault Sign using Azure Key Vault + google-kms Sign using Google Cloud KMS + +The dns and http methods require a --private-key for in-process signing. For +out-of-process signing, use one of the supported signing providers. Signing is +needed for an authentication challenge with the registry. + +The github and github-oidc methods do not support signing providers and +authenticate using the GitHub as an identity provider. + + `) + } + + method := args[0] + flags, err := parseLoginFlags(method, args) + if err != nil { + return err + } + + var signer auth.Signer + if flags.SignerType != NoSignerType { + signer, err = createSigner(flags) + if err != nil { + return err + } + } + + authProvider, err := createAuthProvider(method, flags.RegistryURL, flags.Domain, signer) + if err != nil { + return err } - // Perform login ctx := context.Background() _, _ = fmt.Fprintf(os.Stdout, "Logging in with %s...\n", method) @@ -89,13 +179,11 @@ func LoginCommand(args []string) error { return fmt.Errorf("login failed: %w", err) } - // Get and save token token, err := authProvider.GetToken(ctx) if err != nil { return fmt.Errorf("failed to get token: %w", err) } - // Save token to file homeDir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to get home directory: %w", err) @@ -105,7 +193,7 @@ func LoginCommand(args []string) error { tokenData := map[string]string{ "token": token, "method": method, - "registry": registryURL, + "registry": flags.RegistryURL, } jsonData, err := json.Marshal(tokenData) diff --git a/docs/reference/cli/commands.md b/docs/reference/cli/commands.md index 18d4c5c9..a460d1af 100644 --- a/docs/reference/cli/commands.md +++ b/docs/reference/cli/commands.md @@ -81,6 +81,7 @@ mcp-publisher login dns --domain=example.com --private-key=HEX_KEY [--registry=U - Verifies domain ownership via DNS TXT record - Grants access to `com.example.*` namespaces - Requires Ed25519 private key (64-character hex) or ECDSA P-384 private key (96-character hex) + - The private key can stored in a cloud signing provider like Google KMS or Azure Key Vault. **Setup:** (for Ed25519, recommended) ```bash @@ -112,6 +113,62 @@ openssl ec -in key.pem -text -noout -conv_form compressed | grep -A4 "pub:" | ta openssl ec -in -noout -text | grep -A4 "priv:" | tail -n +2 | tr -d ' :\n' ``` +**Setup:** (for Google KMS signing) + +This requires the [gcloud CLI](https://cloud.google.com/sdk/docs/install). + +```bash +# log in and set default project +gcloud auth login +gcloud config set project myproject + +# Create a keyring in your project +gcloud kms keyrings create mykeyring --location global + +# Create an Ed25519 signing key +gcloud kms keys create mykey --default-algorithm=ec-sign-ed25519 --purpose=asymmetric-signing --keyring=mykeyring --location=global + +# Enable Application Default Credentials (ADC) so the publisher tool can sign +gcloud auth application-default login + +# Attempt login to show the public key +mcp-publisher login dns google-kms --domain=example.com --resource=projects/myproject/locations/global/keyRings/mykering/cryptoKeys/mykey/cryptoKeyVersions/1 + +# Copy the "Expected proof record" and add the TXT record +# example.com. IN TXT "v=MCPv1; k=ed25519; p=PUBLIC_KEY" + +# Re-run the login command +mcp-publisher login dns google-kms --domain=example.com --resource=projects/myproject/locations/global/keyRings/mykering/cryptoKeys/mykey/cryptoKeyVersions/1 +``` + +**Setup:** (for Azure Key Vault signing) + +This requires the [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest). + +```bash +# log in and set default subscrption +az login +az account set --subscription "My Subscription (name or ID)" + +# Create a resource group +az group create --location westus --resource-group MyResourceGroup + +# Create a Key Vault +az keyvault create --name MyKeyVault --location westus --resource-group MyResourceGroup + +# Create an ECDCA P-384 signing key +az keyvault key create --name MyKey --vault-name MyKeyVault --curve P-384 + +# Attempt login to show the public key +mcp-publisher login dns azure-key-vault --domain=example.com --vault MyKeyVault --key MyKey + +# Copy the "Expected proof record" and add the TXT record +# example.com. IN TXT "v=MCPv1; k=ecdsa0384; p=PUBLIC_KEY" + +# Re-run the login command +mcp-publisher login dns azure-key-vault --domain=example.com --vault MyKeyVault --key MyKey +``` + #### HTTP Verification ```bash mcp-publisher login http --domain=example.com --private-key=HEX_KEY [--registry=URL] @@ -119,6 +176,7 @@ mcp-publisher login http --domain=example.com --private-key=HEX_KEY [--registry= - Verifies domain ownership via HTTPS endpoint - Grants access to `com.example.*` namespaces - Requires Ed25519 private key (64-character hex) or ECDSA P-384 private key (96-character hex) + - The private key can stored in a cloud signing provider like Google KMS or Azure Key Vault. **Setup:** (for Ed25519, recommended) ```bash @@ -140,6 +198,8 @@ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -out key.pem # Content: v=MCPv1; k=ecdsap384; p=PUBLIC_KEY ``` +Cloud signing is also supported for HTTP authentication, similar to the DNS examples above. Just swap out the `dns` positional argument for `http`. + #### Anonymous (Testing) ```bash mcp-publisher login none [--registry=URL] diff --git a/go.mod b/go.mod index b8a7394b..c4ddd82a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module github.com/modelcontextprotocol/registry go 1.24.6 require ( + cloud.google.com/go/kms v1.23.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 github.com/caarlos0/env/v11 v11.3.1 github.com/coreos/go-oidc/v3 v3.16.0 github.com/danielgtaylor/huma/v2 v2.34.1 @@ -23,31 +26,56 @@ require ( ) require ( + cloud.google.com/go v0.120.0 // indirect + cloud.google.com/go/auth v0.16.4 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.8.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/longrunning v0.6.7 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/otlptranslator v0.0.2 // indirect github.com/prometheus/procfs v0.17.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/crypto v0.41.0 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/net v0.44.0 // indirect golang.org/x/oauth2 v0.31.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/time v0.12.0 // indirect + google.golang.org/api v0.247.0 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect + google.golang.org/grpc v1.74.2 // indirect google.golang.org/protobuf v1.36.8 // indirect ) diff --git a/go.sum b/go.sum index dc295c07..69990f51 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,33 @@ +cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= +cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= +cloud.google.com/go/auth v0.16.4 h1:fXOAIQmkApVvcIn7Pc2+5J8QTMVbUGLscnSVNl11su8= +cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= +cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/kms v1.23.0 h1:WaqAZsUptyHwOo9II8rFC1Kd2I+yvNsNP2IJ14H2sUw= +cloud.google.com/go/kms v1.23.0/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 h1:wL5IEG5zb7BVv1Kv0Xm92orq+5hB5Nipn3B5tn4Rqfk= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= @@ -13,6 +43,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -22,10 +54,18 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -36,6 +76,8 @@ github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -48,6 +90,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= @@ -71,6 +115,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/contrib/instrumentation/runtime v0.63.0 h1:PeBoRj6af6xMI7qCupwFvTbbnd49V7n5YpG6pg8iDYQ= go.opentelemetry.io/contrib/instrumentation/runtime v0.63.0/go.mod h1:ingqBCtMCe8I4vpz/UVzCW6sxoqgZB37nao91mLQ3Bw= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= @@ -89,18 +137,33 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +google.golang.org/api v0.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc= +google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 7c0bc3dd5b2c9f8174e1fcd3f8cf009d119e01e3 Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Fri, 24 Oct 2025 14:44:35 -0400 Subject: [PATCH 2/8] Implement parseRawPrivateKey for go 1.24 --- cmd/publisher/auth/common.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/cmd/publisher/auth/common.go b/cmd/publisher/auth/common.go index 2dacaaad..81a89012 100644 --- a/cmd/publisher/auth/common.go +++ b/cmd/publisher/auth/common.go @@ -13,6 +13,7 @@ import ( "encoding/json" "fmt" "io" + "math/big" "net/http" "os" "strings" @@ -114,7 +115,7 @@ func (c *InProcessSigner) GetSignedTimestamp(_ context.Context) (*string, []byte digest := sha512.Sum384([]byte(timestamp)) curve := elliptic.P384() - privateKey, err := ecdsa.ParseRawPrivateKey(curve, c.privateKey) + privateKey, err := parseRawPrivateKey(curve, c.privateKey) if err != nil { return nil, nil, fmt.Errorf("failed to parse ECDSA private key: %w", err) } @@ -132,6 +133,27 @@ func (c *InProcessSigner) GetSignedTimestamp(_ context.Context) (*string, []byte } } +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 func (c *CryptoProvider) NeedsLogin() bool { return false From 617be6216c655f0aea8ba99f5aa0d8c6d89de5b2 Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Mon, 27 Oct 2025 13:28:39 -0400 Subject: [PATCH 3/8] Update docs/reference/cli/commands.md Co-authored-by: adam jones --- docs/reference/cli/commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/cli/commands.md b/docs/reference/cli/commands.md index a460d1af..67f9e9f8 100644 --- a/docs/reference/cli/commands.md +++ b/docs/reference/cli/commands.md @@ -163,7 +163,7 @@ az keyvault key create --name MyKey --vault-name MyKeyVault --curve P-384 mcp-publisher login dns azure-key-vault --domain=example.com --vault MyKeyVault --key MyKey # Copy the "Expected proof record" and add the TXT record -# example.com. IN TXT "v=MCPv1; k=ecdsa0384; p=PUBLIC_KEY" +# example.com. IN TXT "v=MCPv1; k=ecdsap384; p=PUBLIC_KEY" # Re-run the login command mcp-publisher login dns azure-key-vault --domain=example.com --vault MyKeyVault --key MyKey From 30895be74665f88d996e9bbc2f5cd84db900ce6a Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Mon, 27 Oct 2025 13:28:48 -0400 Subject: [PATCH 4/8] Update docs/reference/cli/commands.md Co-authored-by: adam jones --- docs/reference/cli/commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/cli/commands.md b/docs/reference/cli/commands.md index 67f9e9f8..a0d21af6 100644 --- a/docs/reference/cli/commands.md +++ b/docs/reference/cli/commands.md @@ -146,7 +146,7 @@ mcp-publisher login dns google-kms --domain=example.com --resource=projects/mypr This requires the [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest). ```bash -# log in and set default subscrption +# log in and set default subscription az login az account set --subscription "My Subscription (name or ID)" From d97eb80d14a589181f9d2124a3d47604ed6eef53 Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Mon, 27 Oct 2025 13:29:23 -0400 Subject: [PATCH 5/8] Update docs/reference/cli/commands.md Co-authored-by: adam jones --- docs/reference/cli/commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/cli/commands.md b/docs/reference/cli/commands.md index a0d21af6..f9abb1b9 100644 --- a/docs/reference/cli/commands.md +++ b/docs/reference/cli/commands.md @@ -176,7 +176,7 @@ mcp-publisher login http --domain=example.com --private-key=HEX_KEY [--registry= - Verifies domain ownership via HTTPS endpoint - Grants access to `com.example.*` namespaces - Requires Ed25519 private key (64-character hex) or ECDSA P-384 private key (96-character hex) - - The private key can stored in a cloud signing provider like Google KMS or Azure Key Vault. + - The private key can be stored in a cloud signing provider like Google KMS or Azure Key Vault. **Setup:** (for Ed25519, recommended) ```bash From 7407198286e51c5213eef34e35369b6c66e9176f Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Mon, 27 Oct 2025 13:29:36 -0400 Subject: [PATCH 6/8] Update docs/reference/cli/commands.md Co-authored-by: adam jones --- docs/reference/cli/commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/cli/commands.md b/docs/reference/cli/commands.md index f9abb1b9..c9c6c429 100644 --- a/docs/reference/cli/commands.md +++ b/docs/reference/cli/commands.md @@ -81,7 +81,7 @@ mcp-publisher login dns --domain=example.com --private-key=HEX_KEY [--registry=U - Verifies domain ownership via DNS TXT record - Grants access to `com.example.*` namespaces - Requires Ed25519 private key (64-character hex) or ECDSA P-384 private key (96-character hex) - - The private key can stored in a cloud signing provider like Google KMS or Azure Key Vault. + - The private key can be stored in a cloud signing provider like Google KMS or Azure Key Vault. **Setup:** (for Ed25519, recommended) ```bash From e6b6eaecc739f6291f77eaec222386b5ba6fc854 Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Mon, 27 Oct 2025 15:37:06 -0400 Subject: [PATCH 7/8] Address comments --- cmd/publisher/README.md | 4 ++-- cmd/publisher/auth/common.go | 3 +-- cmd/publisher/commands/login.go | 24 ++++++++++++++++++++++++ docs/guides/publishing/publish-server.md | 2 +- docs/reference/cli/commands.md | 6 +++--- 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/cmd/publisher/README.md b/cmd/publisher/README.md index 30762b21..5f7111c1 100644 --- a/cmd/publisher/README.md +++ b/cmd/publisher/README.md @@ -33,9 +33,9 @@ make dev-compose # Start local registry - **`none`** - No auth (testing only) ### Signing Providers -Optional, for `dns` and `http` to sign out of process without direct access to the private key. +Optional: enables `dns` and `http` methods to sign out-of-process without direct access to the private key. -- **`google-kms`** - Google KMS signing +- **`google-kms`** - Google KMS signing - **`azure-key-vault`** - Azure Key Vault signing ## Key Files diff --git a/cmd/publisher/auth/common.go b/cmd/publisher/auth/common.go index dcabc2d4..2ffdb7d0 100644 --- a/cmd/publisher/auth/common.go +++ b/cmd/publisher/auth/common.go @@ -16,7 +16,6 @@ import ( "math/big" "net/http" "os" - "strings" "time" ) @@ -218,7 +217,7 @@ func (c *CryptoProvider) exchangeTokenForRegistry(ctx context.Context, domain, t } // Make the token exchange request - exchangeURL := fmt.Sprintf("%s/v0/auth/%s", strings.TrimSuffix(c.registryURL, "/"), c.authMethod) + exchangeURL := fmt.Sprintf("%s/v0/auth/%s", 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) diff --git a/cmd/publisher/commands/login.go b/cmd/publisher/commands/login.go index ddeba236..0b36bc07 100644 --- a/cmd/publisher/commands/login.go +++ b/cmd/publisher/commands/login.go @@ -8,6 +8,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/modelcontextprotocol/registry/cmd/publisher/auth" "github.com/modelcontextprotocol/registry/cmd/publisher/auth/azurekeyvault" @@ -85,6 +86,10 @@ func parseLoginFlags(method string, args []string) (LoginFlags, error) { } } err := loginFlags.Parse(args[flags.ArgOffset:]) + if err == nil { + flags.RegistryURL = strings.TrimRight(flags.RegistryURL, "/") + } + return flags, err } @@ -150,6 +155,25 @@ needed for an authentication challenge with the registry. The github and github-oidc methods do not support signing providers and authenticate using the GitHub as an identity provider. +Examples: + + # Interactive GitHub login, using device code flow + mcp-publisher login github + + # Sign in using a specific Ed25519 private key for DNS authentication + mcp-publisher login dns -algorithm ed25519 -domain example.com -private-key <64 hex chars> + + # Sign in using a specific ECDSA P-384 private key for DNS authentication + mcp-publisher login dns -algorithm ecdsap384 -domain example.com -private-key <96 hex chars> + + # Sign in with gcloud CLI, use Google Cloud KMS for signing in DNS authentication + gcloud auth application-default login + mcp-publisher login dns google-kms -domain example.com -resource projects/lotr/locations/global/keyRings/fellowship/cryptoKeys/frodo/cryptoKeyVersions/1 + + # Sign in with az CLI, use Azure Key Vault for signing in HTTP authentication + az login + mcp-publisher login http azure-key-vault -domain example.com -vault myvault -key mysigningkey + `) } diff --git a/docs/guides/publishing/publish-server.md b/docs/guides/publishing/publish-server.md index b1ab79e9..75658fa4 100644 --- a/docs/guides/publishing/publish-server.md +++ b/docs/guides/publishing/publish-server.md @@ -468,7 +468,7 @@ echo "yourcompany.com. IN TXT \"v=MCPv1; k=ed25519; p=$(openssl pkey -in key.pem mcp-publisher login dns --domain yourcompany.com --private-key $(openssl pkey -in key.pem -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n') ``` -The ECDSA P-384 crypto algorithm is also supported, along with HTTP-based authenication. See the [publisher CLI commands reference](../../reference/cli/commands.md) for more details. +The ECDSA P-384 algorithm is also supported, along with HTTP-based authentication. See the [publisher CLI commands reference](../../reference/cli/commands.md) for more details. ## Step 5: Publish Your Server diff --git a/docs/reference/cli/commands.md b/docs/reference/cli/commands.md index c9c6c429..71d8c850 100644 --- a/docs/reference/cli/commands.md +++ b/docs/reference/cli/commands.md @@ -132,13 +132,13 @@ gcloud kms keys create mykey --default-algorithm=ec-sign-ed25519 --purpose=asymm gcloud auth application-default login # Attempt login to show the public key -mcp-publisher login dns google-kms --domain=example.com --resource=projects/myproject/locations/global/keyRings/mykering/cryptoKeys/mykey/cryptoKeyVersions/1 +mcp-publisher login dns google-kms --domain=example.com --resource=projects/myproject/locations/global/keyRings/mykeyring/cryptoKeys/mykey/cryptoKeyVersions/1 # Copy the "Expected proof record" and add the TXT record # example.com. IN TXT "v=MCPv1; k=ed25519; p=PUBLIC_KEY" # Re-run the login command -mcp-publisher login dns google-kms --domain=example.com --resource=projects/myproject/locations/global/keyRings/mykering/cryptoKeys/mykey/cryptoKeyVersions/1 +mcp-publisher login dns google-kms --domain=example.com --resource=projects/myproject/locations/global/keyRings/mykeyring/cryptoKeys/mykey/cryptoKeyVersions/1 ``` **Setup:** (for Azure Key Vault signing) @@ -156,7 +156,7 @@ az group create --location westus --resource-group MyResourceGroup # Create a Key Vault az keyvault create --name MyKeyVault --location westus --resource-group MyResourceGroup -# Create an ECDCA P-384 signing key +# Create an ECDSA P-384 signing key az keyvault key create --name MyKey --vault-name MyKeyVault --curve P-384 # Attempt login to show the public key From 8eb04cff85af5bfe99b97e6b91845ff10070c66b Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Mon, 27 Oct 2025 16:59:13 -0400 Subject: [PATCH 8/8] Improve error message --- cmd/publisher/auth/azurekeyvault/common.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/publisher/auth/azurekeyvault/common.go b/cmd/publisher/auth/azurekeyvault/common.go index e8600949..d1e95013 100644 --- a/cmd/publisher/auth/azurekeyvault/common.go +++ b/cmd/publisher/auth/azurekeyvault/common.go @@ -54,11 +54,11 @@ func (d Signer) GetSignedTimestamp(ctx context.Context) (*string, []byte, error) } 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) + return nil, nil, fmt.Errorf("unsupported key type: kty: %s (only EC or EC-HSM 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) + return nil, nil, fmt.Errorf("unsupported curve: %s (only P-384 is supported)", *keyResp.Key.Crv) } fmt.Fprintln(os.Stdout, "Successfully read the public key from Key Vault.")