Skip to content

Commit 314f336

Browse files
committed
Add Azure Key Vault and Google KMS support
Design doc is in this issue comment: #482 (comment)
1 parent 6c0f085 commit 314f336

File tree

10 files changed

+614
-91
lines changed

10 files changed

+614
-91
lines changed

cmd/publisher/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ make dev-compose # Start local registry
3232
- **`http`** - Domain verification via HTTPS endpoints
3333
- **`none`** - No auth (testing only)
3434

35+
### Signing Providers
36+
Optional, for `dns` and `http` to sign out of process without direct access to the private key.
37+
38+
- **`google-kms`** - Google KMS signing
39+
- **`azure-key-vault`** - Azure Key Vault signing
40+
3541
## Key Files
3642

3743
- **`main.go`** - CLI setup and command routing
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package azurekeyvault
2+
3+
import (
4+
"context"
5+
"crypto/ecdsa"
6+
"crypto/elliptic"
7+
"crypto/sha512"
8+
"fmt"
9+
"math/big"
10+
"os"
11+
12+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
13+
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys"
14+
"github.com/modelcontextprotocol/registry/cmd/publisher/auth"
15+
)
16+
17+
func GetSignatureProvider(vaultName, keyName string) (auth.Signer, error) {
18+
if vaultName == "" {
19+
return nil, fmt.Errorf("--vault option (vault name) is required")
20+
}
21+
22+
if keyName == "" {
23+
return nil, fmt.Errorf("--key option (key name) is required")
24+
}
25+
26+
return Signer{
27+
vaultName: vaultName,
28+
keyName: keyName,
29+
}, nil
30+
}
31+
32+
type Signer struct {
33+
vaultName string
34+
keyName string
35+
}
36+
37+
func (d Signer) GetSignedTimestamp(ctx context.Context) (*string, []byte, error) {
38+
fmt.Fprintf(os.Stdout, "Signing using Azure Key Vault %s and key %s\n", d.vaultName, d.keyName)
39+
40+
cred, err := azidentity.NewDefaultAzureCredential(nil)
41+
if err != nil {
42+
return nil, nil, fmt.Errorf("authentication to Azure failed: %w", err)
43+
}
44+
45+
vaultURL := fmt.Sprintf("https://%s.vault.azure.net/", d.vaultName)
46+
client, err := azkeys.NewClient(vaultURL, cred, nil)
47+
if err != nil {
48+
return nil, nil, fmt.Errorf("failed to create Key Vault client: %w", err)
49+
}
50+
51+
keyResp, err := client.GetKey(ctx, d.keyName, "", nil)
52+
if err != nil {
53+
return nil, nil, fmt.Errorf("failed to retrieve key for public parameters: %w", err)
54+
}
55+
56+
if *keyResp.Key.Kty != azkeys.KeyTypeEC && *keyResp.Key.Kty != azkeys.KeyTypeECHSM {
57+
return nil, nil, fmt.Errorf("unsupported key type: kty=%v (only EC keys are supported)", keyResp.Key.Kty)
58+
}
59+
60+
if *keyResp.Key.Crv != azkeys.CurveNameP384 {
61+
return nil, nil, fmt.Errorf("unsupported curve: crv=%v (only P-384 is supported)", keyResp.Key.Crv)
62+
}
63+
64+
fmt.Fprintln(os.Stdout, "Successfully read the public key from Key Vault.")
65+
auth.PrintEcdsaP384KeyInfo(ecdsa.PublicKey{
66+
Curve: elliptic.P384(),
67+
X: new(big.Int).SetBytes(keyResp.Key.X),
68+
Y: new(big.Int).SetBytes(keyResp.Key.Y),
69+
})
70+
71+
timestamp := auth.GetTimestamp()
72+
digest := sha512.Sum384([]byte(timestamp))
73+
alg := azkeys.SignatureAlgorithmES384
74+
fmt.Fprintln(os.Stdout, "Executing the sign request...")
75+
signResp, err := client.Sign(ctx, d.keyName, "", azkeys.SignParameters{
76+
Algorithm: &alg,
77+
Value: digest[:],
78+
}, nil)
79+
80+
if err != nil {
81+
return nil, nil, fmt.Errorf("failed to sign message: %w", err)
82+
}
83+
84+
return &timestamp, signResp.Result, nil
85+
}

cmd/publisher/auth/common.go

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ import (
88
"crypto/elliptic"
99
"crypto/rand"
1010
"crypto/sha512"
11+
"encoding/base64"
1112
"encoding/hex"
1213
"encoding/json"
1314
"fmt"
1415
"io"
1516
"net/http"
17+
"os"
18+
"strings"
1619
"time"
1720
)
1821

@@ -29,75 +32,103 @@ const (
2932

3033
// CryptoProvider provides common functionality for DNS and HTTP authentication
3134
type CryptoProvider struct {
32-
registryURL string
33-
domain string
34-
privateKey string
35-
cryptoAlgorithm CryptoAlgorithm
36-
authMethod string
35+
registryURL string
36+
domain string
37+
signer Signer
38+
authMethod string
3739
}
3840

39-
// GetToken retrieves the registry JWT token using cryptographic authentication
40-
func (c *CryptoProvider) GetToken(ctx context.Context) (string, error) {
41-
if c.domain == "" {
42-
return "", fmt.Errorf("%s domain is required", c.authMethod)
43-
}
41+
type Signer interface {
42+
GetSignedTimestamp(ctx context.Context) (*string, []byte, error)
43+
}
44+
45+
func GetTimestamp() string {
46+
return time.Now().UTC().Format(time.RFC3339)
47+
}
4448

45-
if c.privateKey == "" {
46-
return "", fmt.Errorf("%s private key (hex) is required", c.authMethod)
49+
func NewInProcessSigner(privateKey string, algorithm CryptoAlgorithm) (Signer, error) {
50+
if privateKey == "" {
51+
return nil, fmt.Errorf("%s private key (hex) is required", algorithm)
4752
}
4853

4954
// Decode private key from hex
50-
privateKeyBytes, err := hex.DecodeString(c.privateKey)
55+
privateKeyBytes, err := hex.DecodeString(privateKey)
5156
if err != nil {
52-
return "", fmt.Errorf("invalid hex private key format: %w", err)
57+
return nil, fmt.Errorf("invalid hex private key format: %w", err)
58+
}
59+
60+
return &InProcessSigner{
61+
privateKey: privateKeyBytes,
62+
cryptoAlgorithm: algorithm,
63+
}, nil
64+
}
65+
66+
// GetToken retrieves the registry JWT token using cryptographic authentication
67+
func (c *CryptoProvider) GetToken(ctx context.Context) (string, error) {
68+
if c.domain == "" {
69+
return "", fmt.Errorf("%s domain is required", c.authMethod)
5370
}
5471

5572
// Generate current timestamp
56-
timestamp := time.Now().UTC().Format(time.RFC3339)
57-
signedTimestamp, err := c.signMessage(privateKeyBytes, []byte(timestamp))
73+
timestamp, signedTimestamp, err := c.signer.GetSignedTimestamp(ctx)
5874
if err != nil {
5975
return "", fmt.Errorf("failed to sign timestamp: %w", err)
6076
}
6177
signedTimestampHex := hex.EncodeToString(signedTimestamp)
6278

6379
// Exchange signature for registry token
64-
registryToken, err := c.exchangeTokenForRegistry(ctx, c.domain, timestamp, signedTimestampHex)
80+
registryToken, err := c.exchangeTokenForRegistry(ctx, c.domain, *timestamp, signedTimestampHex)
6581
if err != nil {
6682
return "", fmt.Errorf("failed to exchange %s signature: %w", c.authMethod, err)
6783
}
6884

6985
return registryToken, nil
7086
}
7187

72-
func (c *CryptoProvider) signMessage(privateKeyBytes []byte, message []byte) ([]byte, error) {
88+
type InProcessSigner struct {
89+
privateKey []byte
90+
cryptoAlgorithm CryptoAlgorithm
91+
}
92+
93+
func (c *InProcessSigner) GetSignedTimestamp(_ context.Context) (*string, []byte, error) {
94+
fmt.Fprintf(os.Stdout, "Signing in process using key algorithm %s\n", c.cryptoAlgorithm)
95+
96+
timestamp := GetTimestamp()
97+
7398
switch c.cryptoAlgorithm {
7499
case AlgorithmEd25519:
75-
if len(privateKeyBytes) != ed25519.SeedSize {
76-
return nil, fmt.Errorf("invalid seed length: expected %d bytes, got %d", ed25519.SeedSize, len(privateKeyBytes))
100+
if len(c.privateKey) != ed25519.SeedSize {
101+
return nil, nil, fmt.Errorf("invalid seed length: expected %d bytes, got %d", ed25519.SeedSize, len(c.privateKey))
77102
}
78103

79-
privateKey := ed25519.NewKeyFromSeed(privateKeyBytes)
80-
signature := ed25519.Sign(privateKey, message)
81-
return signature, nil
104+
privateKey := ed25519.NewKeyFromSeed(c.privateKey)
105+
106+
PrintEd25519KeyInfo(privateKey.Public().(ed25519.PublicKey))
107+
108+
signature := ed25519.Sign(privateKey, []byte(timestamp))
109+
return &timestamp, signature, nil
82110
case AlgorithmECDSAP384:
83-
if len(privateKeyBytes) != 48 {
84-
return nil, fmt.Errorf("invalid seed length for ECDSA P-384: expected 48 bytes, got %d", len(privateKeyBytes))
111+
if len(c.privateKey) != 48 {
112+
return nil, nil, fmt.Errorf("invalid seed length for ECDSA P-384: expected 48 bytes, got %d", len(c.privateKey))
85113
}
86114

87-
digest := sha512.Sum384(message)
115+
digest := sha512.Sum384([]byte(timestamp))
88116
curve := elliptic.P384()
89-
privateKey, err := ecdsa.ParseRawPrivateKey(curve, privateKeyBytes)
117+
privateKey, err := ecdsa.ParseRawPrivateKey(curve, c.privateKey)
90118
if err != nil {
91-
return nil, fmt.Errorf("failed to parse ECDSA private key: %w", err)
119+
return nil, nil, fmt.Errorf("failed to parse ECDSA private key: %w", err)
92120
}
121+
122+
PrintEcdsaP384KeyInfo(privateKey.PublicKey)
123+
93124
r, s, err := ecdsa.Sign(rand.Reader, privateKey, digest[:])
94125
if err != nil {
95-
return nil, fmt.Errorf("failed to sign message: %w", err)
126+
return nil, nil, fmt.Errorf("failed to sign message: %w", err)
96127
}
97128
signature := append(r.Bytes(), s.Bytes()...)
98-
return signature, nil
129+
return &timestamp, signature, nil
99130
default:
100-
return nil, fmt.Errorf("unsupported crypto algorithm: %s", c.cryptoAlgorithm)
131+
return nil, nil, fmt.Errorf("unsupported crypto algorithm: %s", c.cryptoAlgorithm)
101132
}
102133
}
103134

@@ -111,6 +142,23 @@ func (c *CryptoProvider) Login(_ context.Context) error {
111142
return nil
112143
}
113144

145+
func PrintEd25519KeyInfo(pubKey ed25519.PublicKey) {
146+
pubKeyString := base64.StdEncoding.EncodeToString(pubKey)
147+
fmt.Fprint(os.Stdout, "Expected proof record:\n")
148+
fmt.Fprintf(os.Stdout, "v=MCPv1; k=ed25519; p=%s\n", pubKeyString)
149+
}
150+
151+
func PrintEcdsaP384KeyInfo(pubKey ecdsa.PublicKey) {
152+
printEcdsaKeyInfo("ecdsap384", pubKey)
153+
}
154+
155+
func printEcdsaKeyInfo(k string, pubKey ecdsa.PublicKey) {
156+
compressed := elliptic.MarshalCompressed(pubKey.Curve, pubKey.X, pubKey.Y)
157+
pubKeyString := base64.StdEncoding.EncodeToString(compressed)
158+
fmt.Fprint(os.Stdout, "Expected proof record:\n")
159+
fmt.Fprintf(os.Stdout, "v=MCPv1; k=%s; p=%s\n", k, pubKeyString)
160+
}
161+
114162
// exchangeTokenForRegistry exchanges signature for a registry JWT token
115163
func (c *CryptoProvider) exchangeTokenForRegistry(ctx context.Context, domain, timestamp, signedTimestamp string) (string, error) {
116164
if c.registryURL == "" {
@@ -130,7 +178,7 @@ func (c *CryptoProvider) exchangeTokenForRegistry(ctx context.Context, domain, t
130178
}
131179

132180
// Make the token exchange request
133-
exchangeURL := fmt.Sprintf("%s/v0/auth/%s", c.registryURL, c.authMethod)
181+
exchangeURL := fmt.Sprintf("%s/v0/auth/%s", strings.TrimSuffix(c.registryURL, "/"), c.authMethod)
134182
req, err := http.NewRequestWithContext(ctx, http.MethodPost, exchangeURL, bytes.NewBuffer(jsonData))
135183
if err != nil {
136184
return "", fmt.Errorf("failed to create request: %w", err)

cmd/publisher/auth/dns.go

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

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

0 commit comments

Comments
 (0)