Skip to content

Commit 0c38a5e

Browse files
authored
Use github.com/distribution/reference for auth checking: (#284)
## Description This library has no third-party dependencies and handles all the necessary tasks. ## Why is this needed Fixes: # ## How Has This Been Tested? ## How are existing users impacted? What migration steps/scripts do we need? ## Checklist: I have: - [ ] updated the documentation and/or roadmap (if required) - [ ] added unit or e2e tests - [ ] provided instructions on how to upgrade
2 parents 9b3e399 + 9a46245 commit 0c38a5e

File tree

3 files changed

+122
-556
lines changed

3 files changed

+122
-556
lines changed

images/hook-bootkit/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ toolchain go1.24.1
66

77
require (
88
github.com/cenkalti/backoff/v4 v4.3.0
9+
github.com/distribution/reference v0.6.0
910
github.com/docker/docker v28.3.2+incompatible
1011
github.com/go-logr/logr v1.4.3
1112
github.com/go-logr/zerologr v1.2.3
@@ -18,7 +19,6 @@ require (
1819
github.com/containerd/errdefs v1.0.0 // indirect
1920
github.com/containerd/errdefs/pkg v0.3.0 // indirect
2021
github.com/containerd/log v0.1.0 // indirect
21-
github.com/distribution/reference v0.6.0 // indirect
2222
github.com/docker/go-connections v0.5.0 // indirect
2323
github.com/docker/go-units v0.5.0 // indirect
2424
github.com/felixge/httpsnoop v1.0.4 // indirect

images/hook-bootkit/registry.go

Lines changed: 9 additions & 214 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package main
22

33
import (
4-
"net/url"
5-
"strconv"
6-
"strings"
7-
4+
"github.com/distribution/reference"
85
"golang.org/x/text/unicode/norm"
96
)
107

@@ -17,219 +14,17 @@ func useAuth(imageRef, registryHost string) bool {
1714
return false
1815
}
1916

20-
imageHost := extractRegistryHostname(imageRef)
21-
configHost := normalizeRegistryHostname(registryHost)
17+
pnn, err := reference.ParseNormalizedNamed(imageRef)
18+
if err != nil {
19+
return false
20+
}
21+
imageHost := reference.Domain(pnn)
2222

2323
// Apply Unicode normalization to prevent homograph attacks
2424
// Use NFC (Canonical Decomposition followed by Canonical Composition)
2525
// to ensure consistent Unicode representation
26-
imageHost = norm.NFC.String(imageHost)
27-
configHost = norm.NFC.String(configHost)
28-
29-
return imageHost == configHost
30-
}
31-
32-
// extractRegistryHostname extracts the registry hostname from an image reference.
33-
// Examples:
34-
// - "registry.example.com/namespace/image:tag" -> "registry.example.com"
35-
// - "registry.example.com:5000/image" -> "registry.example.com:5000"
36-
// - "localhost:5000/image" -> "localhost:5000"
37-
// - "image" -> "docker.io" (Docker Hub default)
38-
// - "ubuntu:20.04" -> "docker.io" (Docker Hub default)
39-
func extractRegistryHostname(imageRef string) string {
40-
if imageRef == "" {
41-
return ""
42-
}
43-
44-
// Split the image reference by '/' to get the potential registry part
45-
parts := strings.Split(imageRef, "/")
46-
if len(parts) == 1 {
47-
// Single part means it's a Docker Hub image (e.g., "ubuntu", "ubuntu:20.04")
48-
return "docker.io"
49-
}
50-
51-
// The first part might be the registry hostname
52-
firstPart := parts[0]
53-
54-
// Check if the first part looks like a registry hostname
55-
// We need to distinguish between:
56-
// - Registry hostnames (registry.example.com, localhost:5000, 192.168.1.1:5000, [::1]:5000)
57-
// - Docker Hub images with tags (ubuntu:20.04, myapp:v1.2.3)
58-
// - Docker Hub usernames (username/image)
59-
if isRegistryHostname(firstPart) {
60-
return firstPart
61-
}
62-
63-
// If the first part doesn't look like a hostname, assume it's Docker Hub
64-
// Examples: "library/ubuntu", "username/image", "ubuntu:20.04"
65-
return "docker.io"
66-
}
67-
68-
// normalizeRegistryHostname normalizes a registry hostname for comparison.
69-
// It handles various formats that might be provided in configuration.
70-
// Examples:
71-
// - "https://registry.example.com" -> "registry.example.com"
72-
// - "http://localhost:5000" -> "localhost:5000"
73-
// - "registry.example.com:443" -> "registry.example.com:443"
74-
// - "registry.example.com" -> "registry.example.com"
75-
func normalizeRegistryHostname(registryHost string) string {
76-
if registryHost == "" {
77-
return ""
78-
}
79-
80-
// Handle URL schemes (https:// or http://)
81-
if strings.HasPrefix(registryHost, "https://") || strings.HasPrefix(registryHost, "http://") {
82-
parsed, err := url.Parse(registryHost)
83-
if err != nil {
84-
// If parsing fails, strip the scheme manually
85-
registryHost = strings.TrimPrefix(registryHost, "https://")
86-
registryHost = strings.TrimPrefix(registryHost, "http://")
87-
} else {
88-
registryHost = parsed.Host
89-
}
90-
}
91-
92-
// Remove any trailing path components
93-
if idx := strings.Index(registryHost, "/"); idx != -1 {
94-
registryHost = registryHost[:idx]
95-
}
96-
97-
return registryHost
98-
}
99-
100-
// isRegistryHostname determines if a string represents a registry hostname rather than
101-
// a Docker Hub image name with tag. This function handles various edge cases:
102-
// - IPv6 addresses in brackets: [::1]:5000, [2001:db8::1]:5000
103-
// - IPv4 addresses with ports: 192.168.1.1:5000
104-
// - Hostnames with ports: registry.example.com:5000, localhost:5000
105-
// - Hostnames with dots: registry.example.com, sub.domain.com
106-
// - Known registry patterns: localhost, 127.0.0.1
107-
// - Excludes Docker Hub image:tag patterns: ubuntu:20.04, myapp:v1.2.3.
108-
func isRegistryHostname(part string) bool {
109-
if part == "" {
110-
return false
111-
}
112-
113-
// Handle IPv6 addresses in brackets [::1], [2001:db8::1], [::1]:5000, or [2001:db8::1]:5000
114-
if strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]") {
115-
return true
116-
}
117-
if strings.HasPrefix(part, "[") && strings.Contains(part, "]:") {
118-
return true
119-
}
120-
121-
// Check for localhost (with or without port)
122-
if part == "localhost" {
123-
return true
124-
}
125-
if strings.HasPrefix(part, "localhost:") {
126-
portStr := part[len("localhost:"):]
127-
if portStr == "" {
128-
return false
129-
}
130-
port, err := strconv.Atoi(portStr)
131-
if err != nil || port < 1 || port > 65535 {
132-
return false
133-
}
134-
return true
135-
}
136-
137-
// Check for IP addresses (IPv4) with optional port
138-
if isIPv4WithOptionalPort(part) {
139-
return true
140-
}
141-
142-
// Check if it contains a dot (indicating a domain)
143-
if strings.Contains(part, ".") {
144-
// Make sure it's not just a single dot or other invalid patterns
145-
if part == "." || part == ".." || strings.HasPrefix(part, ".") || strings.HasSuffix(part, ".") {
146-
return false
147-
}
148-
149-
// Additional check: if it contains a colon, make sure it's likely a port, not a tag
150-
if strings.Contains(part, ":") {
151-
return isHostnameWithPort(part)
152-
}
153-
154-
// Basic validation: should have at least one character before and after dot
155-
dotParts := strings.Split(part, ".")
156-
for _, dotPart := range dotParts {
157-
if len(dotPart) == 0 {
158-
return false
159-
}
160-
}
161-
162-
return true
163-
}
164-
165-
// If it contains a colon but no dot, it could be:
166-
// 1. A hostname with port (localhost:5000) - already handled above
167-
// 2. A Docker image with tag (ubuntu:20.04) - should return false
168-
// 3. An IPv4 address with port (1.2.3.4:5000) - already handled above
169-
// At this point, assume it's a Docker image with tag
170-
return false
171-
}
172-
173-
// isIPv4WithOptionalPort checks if the string is an IPv4 address with optional port.
174-
func isIPv4WithOptionalPort(part string) bool {
175-
// Split by colon to separate potential IP and port
176-
host := part
177-
if colonIndex := strings.LastIndex(part, ":"); colonIndex != -1 {
178-
host = part[:colonIndex]
179-
portStr := part[colonIndex+1:]
180-
// Validate port number (1-65535)
181-
if portStr == "" || len(portStr) > 5 {
182-
return false
183-
}
184-
port, err := strconv.Atoi(portStr)
185-
if err != nil || port < 1 || port > 65535 {
186-
return false
187-
}
188-
}
189-
190-
// Basic IPv4 validation: check for pattern like x.x.x.x
191-
parts := strings.Split(host, ".")
192-
if len(parts) != 4 {
193-
return false
194-
}
195-
196-
for _, octet := range parts {
197-
if octet == "" || len(octet) > 3 {
198-
return false
199-
}
200-
// Check if octet contains only digits
201-
for _, r := range octet {
202-
if r < '0' || r > '9' {
203-
return false
204-
}
205-
}
206-
}
207-
208-
return true
209-
}
210-
211-
// isHostnameWithPort checks if a string with both dots and colons represents
212-
// a hostname with port rather than a Docker image with tag.
213-
func isHostnameWithPort(part string) bool {
214-
// Find the last colon (potential port separator)
215-
colonIndex := strings.LastIndex(part, ":")
216-
if colonIndex == -1 {
217-
return true // No colon, just a hostname with dots
218-
}
219-
220-
portStr := part[colonIndex+1:]
221-
hostname := part[:colonIndex]
222-
223-
// Port should be numeric and within the valid range (1-65535)
224-
if len(portStr) == 0 || len(portStr) > 5 {
225-
return false
226-
}
227-
228-
port, err := strconv.Atoi(portStr)
229-
if err != nil || port < 1 || port > 65535 {
230-
return false
231-
}
26+
imageH := norm.NFC.String(imageHost)
27+
registryH := norm.NFC.String(registryHost)
23228

233-
// Hostname part should still contain dots for this to be a registry hostname
234-
return strings.Contains(hostname, ".")
29+
return imageH == registryH
23530
}

0 commit comments

Comments
 (0)