<!-- 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>
147 lines
4.1 KiB
Go
147 lines
4.1 KiB
Go
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())
|
|
}
|
|
}
|