Enable Google KMS and Azure Key Vault for publisher login tool (#696)
<!-- Provide a brief summary of your changes --> ## Motivation and Context This allows the private key used for DNS or HTTP based authentication to be stored securely in a cloud key management system. These services often provide HSM storage which makes it very hard to leak the private key. The full context is described in my design document here: https://github.com/modelcontextprotocol/registry/issues/482#issuecomment-3437117171 ## How Has This Been Tested? I have tested it against PROD using an Ed25519 key stored in Google KMS. I have added unit tests. I have tested both ECDSA P-384 and Ed25519 with Az KV and Google KMS against a locally running server. ## Breaking Changes None intended. ## 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 --> --------- Co-authored-by: adam jones <domdomegg+git@gmail.com>
This commit is contained in:
parent
3d375e6544
commit
88c2e1be16
7 changed files with 641 additions and 91 deletions
|
|
@ -32,6 +32,12 @@ make dev-compose # Start local registry
|
|||
- **`http`** - Domain verification via HTTPS endpoints
|
||||
- **`none`** - No auth (testing only)
|
||||
|
||||
### Signing Providers
|
||||
Optional: enables `dns` and `http` methods 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
|
||||
|
|
|
|||
85
cmd/publisher/auth/azurekeyvault/common.go
Normal file
85
cmd/publisher/auth/azurekeyvault/common.go
Normal file
|
|
@ -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: %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: %s (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
|
||||
}
|
||||
|
|
@ -8,12 +8,14 @@ import (
|
|||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -30,11 +32,35 @@ 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
|
||||
}
|
||||
|
||||
type Signer interface {
|
||||
GetSignedTimestamp(ctx context.Context) (*string, []byte, error)
|
||||
}
|
||||
|
||||
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(privateKey)
|
||||
if err != nil {
|
||||
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
|
||||
|
|
@ -43,26 +69,15 @@ func (c *CryptoProvider) GetToken(ctx context.Context) (string, error) {
|
|||
return "", fmt.Errorf("%s domain is required", c.authMethod)
|
||||
}
|
||||
|
||||
if c.privateKey == "" {
|
||||
return "", fmt.Errorf("%s private key (hex) is required", c.authMethod)
|
||||
}
|
||||
|
||||
// Decode private key from hex
|
||||
privateKeyBytes, err := hex.DecodeString(c.privateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid hex private key format: %w", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
|
@ -70,38 +85,52 @@ 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()
|
||||
|
||||
// Parse the raw private key (compatible with Go 1.24)
|
||||
privateKey, err := 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 ×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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +141,11 @@ func parseRawPrivateKey(curve elliptic.Curve, privateKeyBytes []byte) (*ecdsa.Pr
|
|||
return nil, fmt.Errorf("nil curve")
|
||||
}
|
||||
|
||||
expectedBytes := (curve.Params().N.BitLen() + 7) / 8
|
||||
if len(privateKeyBytes) != expectedBytes {
|
||||
return nil, fmt.Errorf("invalid private key length: expected %d bytes, got %d", expectedBytes, len(privateKeyBytes))
|
||||
}
|
||||
|
||||
// Only standard NIST curves supported
|
||||
switch curve {
|
||||
case elliptic.P224(), elliptic.P256(), elliptic.P384(), elliptic.P521():
|
||||
|
|
@ -147,6 +181,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 == "" {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
147
cmd/publisher/auth/googlekms/common.go
Normal file
147
cmd/publisher/auth/googlekms/common.go
Normal file
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,11 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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 +22,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 +56,146 @@ func (c *CryptoAlgorithm) Set(v string) error {
|
|||
return fmt.Errorf("invalid algorithm: %q (allowed: ed25519, ecdsap384)", v)
|
||||
}
|
||||
|
||||
func parseLoginFlags(method string, args []string) (LoginFlags, error) {
|
||||
var flags LoginFlags
|
||||
loginFlags := flag.NewFlagSet("login", flag.ExitOnError)
|
||||
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(&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:])
|
||||
if err == nil {
|
||||
flags.RegistryURL = strings.TrimRight(flags.RegistryURL, "/")
|
||||
}
|
||||
|
||||
return flags, 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")
|
||||
}
|
||||
}
|
||||
|
||||
func createAuthProvider(method, registryURL, domain string, signer auth.Signer) (auth.Provider, error) {
|
||||
switch method {
|
||||
case "github":
|
||||
return auth.NewGitHubATProvider(true, registryURL), nil
|
||||
case "github-oidc":
|
||||
return auth.NewGitHubOIDCProvider(registryURL), nil
|
||||
case "dns":
|
||||
if domain == "" {
|
||||
return nil, errors.New("dns authentication requires --domain")
|
||||
}
|
||||
return auth.NewDNSProvider(registryURL, domain, &signer), nil
|
||||
case "http":
|
||||
if domain == "" {
|
||||
return nil, errors.New("http authentication requires --domain")
|
||||
}
|
||||
return auth.NewHTTPProvider(registryURL, domain, &signer), nil
|
||||
case "none":
|
||||
return auth.NewNoneProvider(registryURL), nil
|
||||
default:
|
||||
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\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)")
|
||||
return errors.New(`authentication method required
|
||||
|
||||
Usage: mcp-publisher login <method> [<signing provider>]
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
`)
|
||||
}
|
||||
|
||||
method := args[0]
|
||||
|
||||
// Parse remaining flags based on method
|
||||
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 (hex)")
|
||||
loginFlags.Var(&cryptoAlgorithm, "algorithm", "Cryptographic algorithm (ed25519, ecdsap384)")
|
||||
}
|
||||
|
||||
if err := loginFlags.Parse(args[1:]); err != nil {
|
||||
flags, err := parseLoginFlags(method, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create auth provider based on method
|
||||
var authProvider auth.Provider
|
||||
switch method {
|
||||
case "github":
|
||||
authProvider = auth.NewGitHubATProvider(true, registryURL)
|
||||
case "github-oidc":
|
||||
authProvider = auth.NewGitHubOIDCProvider(registryURL)
|
||||
case "dns":
|
||||
if domain == "" || privateKey == "" {
|
||||
return errors.New("dns authentication requires --domain and --private-key")
|
||||
var signer auth.Signer
|
||||
if flags.SignerType != NoSignerType {
|
||||
signer, err = createSigner(flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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, auth.CryptoAlgorithm(cryptoAlgorithm))
|
||||
case "none":
|
||||
authProvider = auth.NewNoneProvider(registryURL)
|
||||
default:
|
||||
return fmt.Errorf("unknown authentication method: %s\nFor a list of available methods, run: mcp-publisher login", method)
|
||||
}
|
||||
|
||||
// Perform login
|
||||
authProvider, err := createAuthProvider(method, flags.RegistryURL, flags.Domain, signer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
_, _ = fmt.Fprintf(os.Stdout, "Logging in with %s...\n", method)
|
||||
|
||||
|
|
@ -89,13 +203,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 +217,7 @@ func LoginCommand(args []string) error {
|
|||
tokenData := map[string]string{
|
||||
"token": token,
|
||||
"method": method,
|
||||
"registry": registryURL,
|
||||
"registry": flags.RegistryURL,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(tokenData)
|
||||
|
|
|
|||
Loading…
Reference in a new issue