@@ -40,6 +40,12 @@ func (r *DefaultDNSResolver) LookupTXT(ctx context.Context, name string) ([]stri
4040 return (& net.Resolver {}).LookupTXT (ctx , name )
4141}
4242
43+ // DNSAuthRecord represents a DNS TXT authentication record with optional name pattern
44+ type DNSAuthRecord struct {
45+ PublicKey ed25519.PublicKey
46+ NamePattern string // Defaults to "*" for wildcard access
47+ }
48+
4349// DNSAuthHandler handles DNS-based authentication
4450type DNSAuthHandler struct {
4551 config * config.Config
@@ -120,29 +126,29 @@ func (h *DNSAuthHandler) ExchangeToken(ctx context.Context, domain, timestamp, s
120126 return nil , fmt .Errorf ("failed to lookup DNS TXT records: %w" , err )
121127 }
122128
123- // Parse public keys from TXT records
124- publicKeys := h .parsePublicKeysFromTXT (txtRecords )
129+ // Parse auth records from TXT records
130+ authRecords := h .parseAuthRecordsFromTXT (txtRecords )
125131
126- if len (publicKeys ) == 0 {
132+ if len (authRecords ) == 0 {
127133 return nil , fmt .Errorf ("no valid MCP public keys found in DNS TXT records" )
128134 }
129135
130- // Verify signature with any of the public keys
136+ // Verify signature with any of the auth records
131137 messageBytes := []byte (timestamp )
132- signatureValid := false
133- for _ , publicKey := range publicKeys {
134- if ed25519 .Verify (publicKey , messageBytes , signature ) {
135- signatureValid = true
138+ var validRecord * DNSAuthRecord
139+ for i , record := range authRecords {
140+ if ed25519 .Verify (record . PublicKey , messageBytes , signature ) {
141+ validRecord = & authRecords [ i ]
136142 break
137143 }
138144 }
139145
140- if ! signatureValid {
146+ if validRecord == nil {
141147 return nil , fmt .Errorf ("signature verification failed" )
142148 }
143149
144- // Build permissions for domain and subdomains
145- permissions := h .buildPermissions (domain )
150+ // Build permissions using the name pattern from the valid record
151+ permissions := h .buildPermissions (domain , validRecord . NamePattern )
146152
147153 // Create JWT claims
148154 jwtClaims := auth.JWTClaims {
@@ -160,14 +166,16 @@ func (h *DNSAuthHandler) ExchangeToken(ctx context.Context, domain, timestamp, s
160166 return tokenResponse , nil
161167}
162168
163- // parsePublicKeysFromTXT parses Ed25519 public keys from DNS TXT records
164- func (h * DNSAuthHandler ) parsePublicKeysFromTXT (txtRecords []string ) []ed25519.PublicKey {
165- var publicKeys []ed25519.PublicKey
166- mcpPattern := regexp .MustCompile (`v=MCPv1;\s*k=ed25519;\s*p=([A-Za-z0-9+/=]+)` )
169+ // parseAuthRecordsFromTXT parses DNS authentication records from TXT records
170+ // Supports optional n=<pattern> parameter for name scoping
171+ func (h * DNSAuthHandler ) parseAuthRecordsFromTXT (txtRecords []string ) []DNSAuthRecord {
172+ var authRecords []DNSAuthRecord
173+ // Updated pattern to capture optional n=<pattern> parameter
174+ mcpPattern := regexp .MustCompile (`v=MCPv1;\s*k=ed25519;\s*p=([A-Za-z0-9+/=]+)(?:;\s*n=([^;]+))?` )
167175
168176 for _ , record := range txtRecords {
169177 matches := mcpPattern .FindStringSubmatch (record )
170- if len (matches ) = = 2 {
178+ if len (matches ) > = 2 {
171179 // Decode base64 public key
172180 publicKeyBytes , err := base64 .StdEncoding .DecodeString (matches [1 ])
173181 if err != nil {
@@ -178,29 +186,52 @@ func (h *DNSAuthHandler) parsePublicKeysFromTXT(txtRecords []string) []ed25519.P
178186 continue // Skip invalid key sizes
179187 }
180188
181- publicKeys = append (publicKeys , ed25519 .PublicKey (publicKeyBytes ))
189+ // Extract name pattern or default to wildcard
190+ namePattern := "*"
191+ if len (matches ) > 2 && matches [2 ] != "" {
192+ namePattern = strings .TrimSpace (matches [2 ])
193+ }
194+
195+ authRecords = append (authRecords , DNSAuthRecord {
196+ PublicKey : ed25519 .PublicKey (publicKeyBytes ),
197+ NamePattern : namePattern ,
198+ })
182199 }
183200 }
184201
185- return publicKeys
202+ return authRecords
186203}
187204
188- // buildPermissions builds permissions for a domain and its subdomains using reverse DNS notation
189- func (h * DNSAuthHandler ) buildPermissions (domain string ) []auth.Permission {
205+ // buildPermissions builds permissions based on domain and name pattern
206+ // namePattern defaults to "*" for wildcard access (backward compatible)
207+ func (h * DNSAuthHandler ) buildPermissions (domain string , namePattern string ) []auth.Permission {
190208 reverseDomain := reverseString (domain )
191209
210+ // If namePattern is "*", grant traditional wildcard permissions
211+ if namePattern == "*" {
212+ permissions := []auth.Permission {
213+ // Grant permissions for the exact domain (e.g., com.example/*)
214+ {
215+ Action : auth .PermissionActionPublish ,
216+ ResourcePattern : fmt .Sprintf ("%s/*" , reverseDomain ),
217+ },
218+ // DNS implies a hierarchy where subdomains are treated as part of the parent domain,
219+ // therefore we grant permissions for all subdomains (e.g., com.example.*)
220+ // This is in line with other DNS-based authentication methods e.g. ACME DNS-01 challenges
221+ {
222+ Action : auth .PermissionActionPublish ,
223+ ResourcePattern : fmt .Sprintf ("%s.*" , reverseDomain ),
224+ },
225+ }
226+ return permissions
227+ }
228+
229+ // For specific name patterns, grant permission only for the specified pattern
230+ // This allows DNS controllers to scope permissions to specific prefixes
192231 permissions := []auth.Permission {
193- // Grant permissions for the exact domain (e.g., com.example/*)
194- {
195- Action : auth .PermissionActionPublish ,
196- ResourcePattern : fmt .Sprintf ("%s/*" , reverseDomain ),
197- },
198- // DNS implies a hierarchy where subdomains are treated as part of the parent domain,
199- // therefore we grant permissions for all subdomains (e.g., com.example.*)
200- // This is in line with other DNS-based authentication methods e.g. ACME DNS-01 challenges
201232 {
202233 Action : auth .PermissionActionPublish ,
203- ResourcePattern : fmt . Sprintf ( "%s.*" , reverseDomain ) ,
234+ ResourcePattern : namePattern ,
204235 },
205236 }
206237
0 commit comments