Skip to content

Commit 8791079

Browse files
committed
feat: add server name scoping support in DNS TXT records (#481)
Implements optional n=<pattern> parameter in DNS TXT records to allow fine-grained permission scoping for server names. This enables DNS controllers to delegate limited namespaces to different teams while maintaining security isolation. Key changes: - Added DNSAuthRecord struct to support name patterns alongside public keys - Collect all valid DNS records instead of stopping at first match - Validate name patterns must start with reverse domain to prevent cross-domain privilege escalation - Added comprehensive tests for all scoping scenarios Example usage: v=MCPv1; k=ed25519; p=<key>; n=com.example/team-foo-* This change addresses Microsoft's requirement for granular permission control within path portions while preserving existing behavior for records without the n= parameter. Fixes #481
1 parent e079d38 commit 8791079

File tree

3 files changed

+568
-30
lines changed

3 files changed

+568
-30
lines changed

DNS_SCOPING_TEST_GUIDE.md

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
# DNS TXT Name Scoping - Testing Guide
2+
3+
This guide provides step-by-step instructions for testing the new DNS TXT name scoping feature that allows fine-grained permission control through the optional `n=<pattern>` parameter.
4+
5+
## Prerequisites
6+
7+
- Go 1.24.x installed
8+
- Access to DNS management for a test domain (or use mock testing)
9+
- Ed25519 key pair for signing
10+
11+
## 1. Unit Testing
12+
13+
### Run the DNS auth handler tests
14+
```bash
15+
# Run only DNS authentication tests
16+
go test ./internal/api/handlers/v0/auth -v -run TestDNSAuthHandler
17+
18+
# Run the new name pattern scoping tests specifically
19+
go test ./internal/api/handlers/v0/auth -v -run TestDNSAuthHandler_NamePatternScoping
20+
```
21+
22+
### Expected output
23+
All tests should pass, including:
24+
- Backward compatibility tests (records without `n=` parameter)
25+
- Specific name pattern tests
26+
- Multiple keys with different patterns
27+
- Edge cases (spaces, complex paths, etc.)
28+
29+
## 2. Integration Testing with Local Server
30+
31+
### Step 1: Start the local development environment
32+
```bash
33+
# Start the full stack with Docker
34+
make dev-compose
35+
36+
# Or run locally without Docker
37+
make build
38+
make dev-local
39+
```
40+
41+
### Step 2: Generate test Ed25519 key pairs
42+
```bash
43+
# Generate a key pair using OpenSSL
44+
openssl genpkey -algorithm ed25519 -out private1.pem
45+
openssl pkey -in private1.pem -pubout -out public1.pem
46+
47+
# Extract the base64-encoded public key
48+
openssl pkey -in private1.pem -pubout -outform DER | tail -c +13 | base64
49+
```
50+
51+
### Step 3: Create test DNS TXT records
52+
53+
For testing, you can either:
54+
55+
#### Option A: Use actual DNS (if you have a domain)
56+
Add these TXT records to your domain:
57+
58+
```bash
59+
# Traditional wildcard (backward compatible)
60+
"v=MCPv1; k=ed25519; p=YOUR_PUBLIC_KEY_BASE64"
61+
62+
# Scoped to specific pattern
63+
"v=MCPv1; k=ed25519; p=YOUR_PUBLIC_KEY_BASE64; n=com.yourdomain/team-alpha-*"
64+
65+
# Another scoped pattern
66+
"v=MCPv1; k=ed25519; p=ANOTHER_PUBLIC_KEY_BASE64; n=com.yourdomain/team-beta-*"
67+
```
68+
69+
#### Option B: Use mock testing (recommended for development)
70+
The test suite includes a mock DNS resolver that simulates DNS responses.
71+
72+
## 3. API Testing
73+
74+
### Step 1: Create a test script for authentication
75+
Create a file `test_dns_auth.go`:
76+
77+
```go
78+
package main
79+
80+
import (
81+
"crypto/ed25519"
82+
"encoding/base64"
83+
"encoding/hex"
84+
"encoding/json"
85+
"fmt"
86+
"net/http"
87+
"strings"
88+
"time"
89+
)
90+
91+
func main() {
92+
// Your test configuration
93+
domain := "example.com"
94+
registryURL := "http://localhost:8080"
95+
96+
// Generate timestamp
97+
timestamp := time.Now().UTC().Format(time.RFC3339)
98+
99+
// Sign the timestamp with your private key
100+
privateKeyBytes, _ := base64.StdEncoding.DecodeString("YOUR_PRIVATE_KEY_BASE64")
101+
privateKey := ed25519.PrivateKey(privateKeyBytes)
102+
signature := ed25519.Sign(privateKey, []byte(timestamp))
103+
signedTimestamp := hex.EncodeToString(signature)
104+
105+
// Create the request
106+
payload := map[string]string{
107+
"domain": domain,
108+
"timestamp": timestamp,
109+
"signed_timestamp": signedTimestamp,
110+
}
111+
112+
jsonData, _ := json.Marshal(payload)
113+
114+
// Send request to registry
115+
resp, err := http.Post(
116+
registryURL+"/v0/auth/dns",
117+
"application/json",
118+
strings.NewReader(string(jsonData)),
119+
)
120+
121+
if err != nil {
122+
panic(err)
123+
}
124+
defer resp.Body.Close()
125+
126+
// Parse response
127+
var result map[string]interface{}
128+
json.NewDecoder(resp.Body).Decode(&result)
129+
130+
fmt.Printf("Response: %+v\n", result)
131+
132+
// The JWT token will contain the scoped permissions
133+
if token, ok := result["registry_token"].(string); ok {
134+
// Decode and inspect the JWT claims to verify permissions
135+
fmt.Printf("Token received: %s...\n", token[:50])
136+
}
137+
}
138+
```
139+
140+
### Step 2: Test different scenarios
141+
142+
#### Test 1: Backward compatibility (no n= parameter)
143+
```bash
144+
# DNS TXT record:
145+
"v=MCPv1; k=ed25519; p=YOUR_KEY"
146+
147+
# Expected permissions in JWT:
148+
# - com.example/*
149+
# - com.example.*
150+
```
151+
152+
#### Test 2: Specific team scope
153+
```bash
154+
# DNS TXT record:
155+
"v=MCPv1; k=ed25519; p=YOUR_KEY; n=com.example/team-foo-*"
156+
157+
# Expected permissions in JWT:
158+
# - com.example/team-foo-*
159+
```
160+
161+
#### Test 3: Multiple keys with different scopes
162+
```bash
163+
# DNS TXT records:
164+
"v=MCPv1; k=ed25519; p=KEY1; n=com.example/app1-*"
165+
"v=MCPv1; k=ed25519; p=KEY2; n=com.example/app2-*"
166+
167+
# Sign with KEY1, expect permission: com.example/app1-*
168+
# Sign with KEY2, expect permission: com.example/app2-*
169+
```
170+
171+
## 4. Testing with the Publisher CLI
172+
173+
### Step 1: Build the publisher tool
174+
```bash
175+
make publisher
176+
```
177+
178+
### Step 2: Create a test server.json
179+
```json
180+
{
181+
"name": "com.example/team-foo-server",
182+
"description": "Test server with scoped permissions",
183+
"version": "1.0.0"
184+
}
185+
```
186+
187+
### Step 3: Attempt to publish with different scopes
188+
189+
#### With matching scope (should succeed):
190+
```bash
191+
# DNS TXT: n=com.example/team-foo-*
192+
./bin/mcp-publisher publish server.json
193+
# Should succeed as the pattern matches
194+
```
195+
196+
#### With non-matching scope (should fail):
197+
```bash
198+
# DNS TXT: n=com.example/team-bar-*
199+
# Trying to publish: com.example/team-foo-server
200+
./bin/mcp-publisher publish server.json
201+
# Should fail with permission denied
202+
```
203+
204+
## 5. Verification Checklist
205+
206+
- [ ] **Backward Compatibility**: Records without `n=` grant wildcard permissions
207+
- [ ] **Exact Scoping**: Records with `n=com.example/foo-*` only allow matching patterns
208+
- [ ] **Multiple Keys**: Different keys can have different scopes
209+
- [ ] **Pattern Matching**: Wildcards in patterns work correctly
210+
- [ ] **Edge Cases**:
211+
- [ ] Spaces in `n=` value are trimmed
212+
- [ ] Empty `n=` defaults to wildcard
213+
- [ ] Complex paths like `com.example/services/auth/*` work
214+
- [ ] Explicit `n=*` behaves like no `n=` parameter
215+
216+
## 6. JWT Token Inspection
217+
218+
To verify the permissions in the JWT token:
219+
220+
```bash
221+
# Use jwt.io or a JWT decoder library to inspect the token
222+
# Look for the "permissions" claim:
223+
{
224+
"permissions": [
225+
{
226+
"action": "publish",
227+
"resource": "com.example/team-foo-*"
228+
}
229+
]
230+
}
231+
```
232+
233+
## 7. Common Test Scenarios
234+
235+
### Scenario A: Enterprise Team Isolation
236+
```bash
237+
# Team Alpha's TXT record
238+
"v=MCPv1; k=ed25519; p=ALPHA_KEY; n=com.microsoft/team-alpha-*"
239+
240+
# Team Beta's TXT record
241+
"v=MCPv1; k=ed25519; p=BETA_KEY; n=com.microsoft/team-beta-*"
242+
243+
# Result: Each team can only publish to their namespace
244+
```
245+
246+
### Scenario B: Gradual Migration
247+
```bash
248+
# Start with wildcard
249+
"v=MCPv1; k=ed25519; p=OLD_KEY"
250+
251+
# Add scoped key alongside
252+
"v=MCPv1; k=ed25519; p=OLD_KEY"
253+
"v=MCPv1; k=ed25519; p=NEW_KEY; n=com.example/prod-*"
254+
255+
# Both keys work, with different permission levels
256+
```
257+
258+
### Scenario C: Subdomain Scoping
259+
```bash
260+
# Scope to specific subdomain path
261+
"v=MCPv1; k=ed25519; p=KEY; n=com.example.api/*"
262+
263+
# Only allows: com.example.api/service1, com.example.api/service2, etc.
264+
```
265+
266+
## Troubleshooting
267+
268+
### Issue: "no valid MCP public keys found"
269+
- Verify TXT record format exactly matches the pattern
270+
- Check base64 encoding of public key
271+
- Ensure no extra spaces in the record
272+
273+
### Issue: "signature verification failed"
274+
- Confirm timestamp is within ±15 seconds
275+
- Verify private key matches public key in TXT record
276+
- Check signature is hex-encoded
277+
278+
### Issue: Permission denied despite correct pattern
279+
- Inspect JWT token to see actual permissions
280+
- Verify the pattern in `n=` matches exactly
281+
- Check for typos in the resource pattern
282+
283+
## Security Considerations
284+
285+
1. **Key Rotation**: The system supports multiple TXT records for zero-downtime rotation
286+
2. **Timestamp Window**: 15-second window prevents replay attacks
287+
3. **Pattern Validation**: Patterns are used as-is, ensure they're correctly formatted
288+
4. **DNS Propagation**: Allow time for DNS changes to propagate (usually 5-60 minutes)
289+
290+
## Additional Resources
291+
292+
- [Original Issue #481](https://github.com/modelcontextprotocol/registry/issues/481)
293+
- [Pull Request #490](https://github.com/modelcontextprotocol/registry/pull/490)
294+
- [DNS Authentication Documentation](../docs/guides/publishing/auth-dns.md)

0 commit comments

Comments
 (0)