-
Notifications
You must be signed in to change notification settings - Fork 204
Remote verification #238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Remote verification #238
Conversation
#206) Closes [#22191](github/copilot#22191) This PR implements cryptographically secure 128-bit token generation for domain ownership verification in the MCP Registry. The package provides a single, focused function for generating base64url-encoded tokens suitable for both DNS TXT record and HTTP-01 challenge verification methods. ## Motivation and Context The MCP Registry requires a robust domain verification system to ensure that server publishers actually own the domains they claim. This package provides the foundational token generation capability needed for: 1. **DNS TXT Record Verification**: Publishers add `mcp-verify=<token>` to their domain's DNS 2. **HTTP-01 Web Challenge**: Publishers serve tokens at `https://domain/.well-known/mcp-challenge/<token>` The implementation follows security best practices with 128-bit entropy and base64url encoding for maximum compatibility with both DNS and HTTP standards. ## How Has This Been Tested? - **Unit tests**: Comprehensive test suite covering token generation, uniqueness, entropy validation, and error handling - **Security tests**: Verified 128-bit entropy, cryptographically secure randomness, and proper base64url encoding - **Compatibility tests**: Ensured tokens are safe for both DNS TXT records and HTTP URLs - **Performance tests**: Benchmarked token generation (~196ns/op on Apple M4 Max) - **Integration tests**: Verified the package builds and integrates correctly with the larger registry codebase - **Linting**: Passed all Go linting tools (go vet, gofmt, golangci-lint) ## Breaking Changes No breaking changes. This is a new package that doesn't affect existing functionality. ## Types of changes - [ ] 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) - [ ] Documentation update ## Checklist - [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 **Design Decisions:** - **Simplified API**: Based on PR feedback, removed `ValidateTokenFormat` and `TokenInfo` struct to keep the API minimal and focused on core functionality - **String comparison**: Token validation is handled through simple string comparison rather than format validation, as suggested in code review - **128-bit entropy**: Chosen to provide cryptographically secure randomness while maintaining reasonable token length (22 characters) - **Base64url encoding**: Ensures tokens are safe for both DNS TXT records and HTTP URLs without escaping **Security Considerations:** - Uses `crypto/rand` for cryptographically secure random number generation - Tokens should be treated as single-use and time-limited - Always use HTTPS when transmitting tokens - 2^128 possible values make collision probability negligible **Performance:** - Token generation is extremely fast (~196ns/op) and suitable for real-time web applications - No external dependencies --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Trent Jones <57182095+trent-j@users.noreply.github.com> Co-authored-by: Thomas Sickert <80130182+thomas-sickert@users.noreply.github.com>
Closes [#22190](github/copilot#22190) <!-- Provide a brief summary of your changes --> ## Motivation and Context This PR implements DNS record verification functionality for the MCP Registry's server name verification system. This feature enables domain owners to prove ownership of their domains by adding specific TXT records to their DNS, which is required for publishing MCP servers under domain-scoped namespaces (e.g., com.example/my-server). This prevents domain impersonation and ensures only legitimate domain owners can publish under their domains. The implementation follows industry best practices used by certificate authorities and cloud services for domain ownership verification, providing a secure and reliable method for continuous domain validation. ## How Has This Been Tested? - Comprehensive unit tests: Added extensive test coverage for all DNS verification functions including success cases, failure scenarios, timeout handling, and retry logic - Mock DNS resolver: Implemented a complete mock DNS resolver system for reliable testing without external dependencies - Token generation and validation: Added tests for cryptographically secure token generation and format validation Error handling: Tested various error conditions including DNS failures, timeouts, and invalid inputs - Integration scenarios: Tested with both mock and real DNS configurations - Performance benchmarks: Added benchmark tests for token generation and validation functions - Example CLI tool: Created a dns-verify example application for manual testing and demonstration ## Breaking Changes <!-- Will users need to update their code or configurations? --> ## 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) - [ ] 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 - Cryptographically secure tokens: 128-bit random tokens using crypto/rand with base64url encoding - Robust DNS resolution: Configurable DNS resolvers with support for secure resolvers (8.8.8.8, 1.1.1.1) - Retry logic: Exponential backoff for transient DNS failures with configurable timeouts and retry limits - Comprehensive error handling: Detailed error types with proper error wrapping and retry logic - Security considerations: Uses secure DNS resolvers, validates token formats, and implements proper timeout handling - Extensive documentation: Added detailed README with usage examples, security considerations, and integration guides --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Trent Jones <57182095+trent-j@users.noreply.github.com>
<!-- Provide a brief summary of your changes --> ## Motivation and Context <!-- Why is this change needed? What problem does it solve? --> ## How Has This Been Tested? <!-- Have you tested this in a real application? Which scenarios were tested? --> ## Breaking Changes <!-- Will users need to update their code or configurations? --> ## 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) - [ ] 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: Ameya Phansalkar <aphansal123@github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ameya Phansalkar <aphansal123@github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: adam jones <domdomegg@users.noreply.github.com>
@claude read this and PR #221, then:
|
Claude finished @domdomegg's task —— View job Remote Verification PR AnalysisThis PR implements a domain ownership verification system for the MCP Registry, allowing publishers to prove they own a domain before publishing MCP servers under that domain's namespace (e.g., What This PR DoesCore Problem SolvedWithout verification, malicious actors could publish MCP servers under domains they don't own - someone could claim Key Features Implemented
Files Added/Modified (28 total)
Current Blockers to MergingAfter thorough analysis, I found NO major blockers: ✅ Security Status: CLEAN
✅ CI Status: PASSING
|
@@ -117,7 +147,7 @@ | |||
|
|||
// Fetch the document at the cursor to get its sort values | |||
var cursorDoc model.Server | |||
err := db.collection.FindOne(ctx, bson.M{"id": cursor}).Decode(&cursorDoc) | |||
err := db.serverCollection.FindOne(ctx, bson.M{"id": cursor}).Decode(&cursorDoc) |
Check failure
Code scanning / CodeQL
Database query built from user-controlled sources High
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 15 days ago
To fully mitigate the risk, the code should convert the cursor
string to a UUID type and use that value in the query filter, rather than passing the raw string. This ensures that only valid UUIDs are used in the query and prevents any possibility of NoSQL injection via type confusion or malformed input. The fix should be applied in internal/database/mongo.go
, specifically in the block where the cursor is validated and used in the query filter. The change involves parsing the cursor to a UUID and using the parsed value in the query filter for both the FindOne
and pagination logic.
-
Copy modified lines R144-R145 -
Copy modified line R151 -
Copy modified line R159
@@ -143,3 +143,4 @@ | ||
// Validate that the cursor is a valid UUID | ||
if _, err := uuid.Parse(cursor); err != nil { | ||
parsedCursor, err := uuid.Parse(cursor) | ||
if err != nil { | ||
return nil, "", fmt.Errorf("invalid cursor format: %w", err) | ||
@@ -149,3 +150,3 @@ | ||
var cursorDoc model.Server | ||
err := db.serverCollection.FindOne(ctx, bson.M{"id": cursor}).Decode(&cursorDoc) | ||
err = db.serverCollection.FindOne(ctx, bson.M{"id": parsedCursor.String()}).Decode(&cursorDoc) | ||
if err != nil { | ||
@@ -157,3 +158,3 @@ | ||
// Use the cursor document's ID to paginate (records with ID > cursor's ID) | ||
mongoFilter["id"] = bson.M{"$gt": cursor} | ||
mongoFilter["id"] = bson.M{"$gt": parsedCursor.String()} | ||
} |
@@ -138,7 +168,7 @@ | |||
} | |||
|
|||
// Execute find operation with options | |||
mongoCursor, err := db.collection.Find(ctx, mongoFilter, findOptions) | |||
mongoCursor, err := db.serverCollection.Find(ctx, mongoFilter, findOptions) |
Check failure
Code scanning / CodeQL
Database query built from user-controlled sources High
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 15 days ago
To fix the problem, ensure that the cursor
value used in the MongoDB query is strictly a valid UUID string. After parsing the cursor with uuid.Parse(cursor)
, use the canonical string representation (uuid.String()
) for the query filter. This guarantees that only valid UUIDs are used and prevents any malformed or malicious input from reaching the database query. The change should be made in internal/database/mongo.go
, specifically in the block where the cursor is validated and used to build the query filter. No new imports are needed, as github.com/google/uuid
is already imported.
-
Copy modified lines R143-R145 -
Copy modified line R148 -
Copy modified line R152 -
Copy modified line R160
@@ -142,6 +142,8 @@ | ||
if cursor != "" { | ||
// Validate that the cursor is a valid UUID | ||
if _, err := uuid.Parse(cursor); err != nil { | ||
// Validate that the cursor is a valid UUID and use its canonical string representation | ||
parsedUUID, err := uuid.Parse(cursor) | ||
if err != nil { | ||
return nil, "", fmt.Errorf("invalid cursor format: %w", err) | ||
} | ||
canonicalCursor := parsedUUID.String() | ||
|
||
@@ -149,3 +151,3 @@ | ||
var cursorDoc model.Server | ||
err := db.serverCollection.FindOne(ctx, bson.M{"id": cursor}).Decode(&cursorDoc) | ||
err = db.serverCollection.FindOne(ctx, bson.M{"id": canonicalCursor}).Decode(&cursorDoc) | ||
if err != nil { | ||
@@ -157,3 +159,3 @@ | ||
// Use the cursor document's ID to paginate (records with ID > cursor's ID) | ||
mongoFilter["id"] = bson.M{"$gt": cursor} | ||
mongoFilter["id"] = bson.M{"$gt": canonicalCursor} | ||
} |
@@ -195,7 +225,7 @@ | |||
} | |||
|
|||
var existingEntry model.ServerDetail | |||
err := db.collection.FindOne(ctx, filter).Decode(&existingEntry) | |||
err := db.serverCollection.FindOne(ctx, filter).Decode(&existingEntry) |
Check failure
Code scanning / CodeQL
Database query built from user-controlled sources High
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 15 days ago
To fix the problem, we should ensure that serverDetail.Name
is strictly a string and does not contain any characters or structures that could alter the query semantics. The best way to do this is to validate the type and format of serverDetail.Name
immediately after unmarshaling the request body in internal/api/handlers/v0/publish.go
. Specifically, we should check that serverDetail.Name
is a non-empty string and does not contain any suspicious characters (such as $
or .
) that could be interpreted specially by MongoDB. This validation should be performed before passing serverDetail
to the registry service and ultimately to the database layer.
The required changes are:
- In
internal/api/handlers/v0/publish.go
, after unmarshalingserverDetail
, add validation to ensureserverDetail.Name
is a string and does not contain MongoDB special characters ($
,.
). - Return an error if the validation fails.
No changes are needed in the database layer, as the filter construction is otherwise safe if the input is validated.
-
Copy modified lines R56-R60
@@ -55,2 +55,7 @@ | ||
} | ||
// Prevent NoSQL injection: Name must not contain '$' or '.' (MongoDB special chars) | ||
if strings.Contains(serverDetail.Name, "$") || strings.Contains(serverDetail.Name, ".") { | ||
http.Error(w, "Name contains invalid characters", http.StatusBadRequest) | ||
return | ||
} | ||
|
} | ||
|
||
opts := options.Update().SetUpsert(true) | ||
_, err := db.verificationCollection.UpdateOne(ctx, domainFilter, update, opts) |
Check failure
Code scanning / CodeQL
Database query built from user-controlled sources High
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 15 days ago
To fix this issue, we should ensure that the domain
value used in the MongoDB query filter is strictly validated and sanitized before being used. The best way to do this is to add a validation step in the StoreVerificationToken
and GetVerificationTokens
methods in internal/database/mongo.go
to ensure that the domain
string is a valid domain name and does not contain any characters that could be interpreted as MongoDB operators or otherwise manipulate the query. This can be done by reusing the normalizeDomain
function from internal/api/handlers/v0/verification.go
or by implementing a similar domain validation function within internal/database/mongo.go
. Since we cannot import from the handler package, we should implement a local domain validation function in mongo.go
that ensures the domain is a valid hostname (e.g., using a regular expression or the net/url
and net
packages). This function should be called at the start of both StoreVerificationToken
and GetVerificationTokens
, and should return an error if the domain is invalid.
-
Copy modified lines R8-R9 -
Copy modified lines R343-R368 -
Copy modified lines R371-R373 -
Copy modified lines R400-R402
@@ -7,2 +7,4 @@ | ||
"log" | ||
"net" | ||
"regexp" | ||
"time" | ||
@@ -340,4 +342,33 @@ | ||
|
||
// validateDomain ensures the domain is a valid hostname (RFC 1035/1123) and does not contain dangerous characters. | ||
func validateDomain(domain string) error { | ||
// Domain must be non-empty and a valid hostname | ||
if domain == "" { | ||
return errors.New("domain cannot be empty") | ||
} | ||
// Use a regular expression to match valid hostnames (letters, digits, hyphens, dots) | ||
// This is a simple check; for stricter validation, use net.ParseIP or more advanced logic if needed | ||
var validDomain = regexp.MustCompile(`^(?i)[a-z0-9][a-z0-9\-\.]{0,253}[a-z0-9]$`) | ||
if !validDomain.MatchString(domain) { | ||
return errors.New("invalid domain format") | ||
} | ||
// Prevent domains with consecutive dots or starting/ending with a dot or hyphen | ||
if domain[0] == '.' || domain[len(domain)-1] == '.' || domain[0] == '-' || domain[len(domain)-1] == '-' { | ||
return errors.New("invalid domain format") | ||
} | ||
if len(domain) > 255 { | ||
return errors.New("domain too long") | ||
} | ||
// Optionally, check if it's a valid IP address (disallow if not wanted) | ||
if net.ParseIP(domain) != nil { | ||
return errors.New("IP addresses are not allowed as domains") | ||
} | ||
return nil | ||
} | ||
|
||
// StoreVerificationToken atomically stores a verification token for a domain if the token is unique | ||
func (db *MongoDB) StoreVerificationToken(ctx context.Context, domain string, token *model.VerificationToken) error { | ||
if err := validateDomain(domain); err != nil { | ||
return fmt.Errorf("invalid domain: %w", err) | ||
} | ||
domainFilter := bson.M{"domain": domain} | ||
@@ -368,2 +399,5 @@ | ||
func (db *MongoDB) GetVerificationTokens(ctx context.Context, domain string) (*model.VerificationTokens, error) { | ||
if err := validateDomain(domain); err != nil { | ||
return nil, fmt.Errorf("invalid domain: %w", err) | ||
} | ||
filter := bson.M{ |
} | ||
|
||
var domainVerification model.DomainVerification | ||
err := db.verificationCollection.FindOne(ctx, filter).Decode(&domainVerification) |
Check failure
Code scanning / CodeQL
Database query built from user-controlled sources High
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 15 days ago
To fix the problem, we should add strict validation to ensure that the domain
parameter is a valid domain name before it is used in any database query. This can be achieved by introducing a validation function that checks the normalized domain against a regular expression for valid domain names (e.g., only letters, digits, hyphens, and dots, with appropriate length limits). This validation should be performed immediately after normalization in the handler, and the request should be rejected if the domain is invalid. This change should be made in internal/api/handlers/v0/verification.go
, in the GetDomainStatusHandler
function, after the call to normalizeDomain
. The rest of the data flow can remain unchanged, as the domain will be guaranteed to be safe for use in the database query.
-
Copy modified lines R14-R24 -
Copy modified lines R149-R153
@@ -13,2 +13,13 @@ | ||
|
||
import ( | ||
"regexp" | ||
) | ||
|
||
// validateDomain checks if the domain is a valid DNS hostname (letters, digits, hyphens, dots) | ||
var domainRegexp = regexp.MustCompile(`^(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$`) | ||
|
||
func validateDomain(domain string) bool { | ||
return domainRegexp.MatchString(domain) | ||
} | ||
|
||
// normalizeDomain extracts and cleans the domain from a URL or domain string | ||
@@ -137,2 +148,7 @@ | ||
return | ||
} | ||
// Validate the normalized domain | ||
if !validateDomain(normalizedDomain) { | ||
http.Error(w, "Invalid domain name", http.StatusBadRequest) | ||
return | ||
} |
After digging in a bit more, I think this PR needs substantial work to get it into a good state. Things don't seem to be glued up correctly, and the code is starting to become quite messy and verbose for what it does. I think key issues missing here are:
|
…hip (#282) Add HTTP-based authentication to prove domain ownership, similar to #238 DNS authentication but using HTTPS well-known URIs instead of DNS TXT records. Fixes #280 ## Motivation and Context Some organizations don't have easy access to DNS records, but can easily deploy web assets. They would therefore prefer to be able to complete a HTTP challenge rather than a DNS one to authenticate to the registry. This would in some way mirror ACME challenges: where you can do a DNS-01 or a HTTP-01 challenge. ## How Has This Been Tested? - Tested locally by stubbing out the HTTP getter - Added unit tests ## Breaking Changes None ## 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) - [ ] 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 I concern I had when building this that it might expose us to SSRF attacks. I think on reflection this is not a significant problem: - We only GET a fixed file `.well-known/mcp-registry-auth` (redirects disallowed) after sanitizing the domain, and then also don't expose the response to the user. This means the user shouldn't be able to hit anything very interesting. - The information they might get is what domains/servers exist in the network. Our infrastructure is already all open-source, so attackers aren't getting any additional real info by prodding at the environment this way.
I sincerely apologize for the disruption. This PR was accidentally closed due to an unintended git history rewrite operation that broke the connection between branches. The operation has been reverted and I'm now recreating the affected PRs.
Original PR: #221
Remote verification