diff --git a/docs/content/about/configuration.md b/docs/content/about/configuration.md
index d95ea99bd..f7efd2cc9 100644
--- a/docs/content/about/configuration.md
+++ b/docs/content/about/configuration.md
@@ -690,7 +690,7 @@ Default `signingalgorithms`:
 Additional notes on `rootcertbundle`:
 
 - The public key of this certificate will be automatically added to the list of known keys.
-- The public key will be identified by it's [RFC7638 Thumbprint](https://datatracker.ietf.org/doc/html/rfc7638).
+- The public key will be identified by its JWK Thumbprint. See [RFC 7638](https://datatracker.ietf.org/doc/html/rfc7638) and [RFC 8037](https://datatracker.ietf.org/doc/html/rfc8037) for reference.
 
 For more information about Token based authentication configuration, see the
 [specification](../spec/auth/token.md).
diff --git a/registry/auth/token/accesscontroller.go b/registry/auth/token/accesscontroller.go
index f24a6d0a7..b86cb3673 100644
--- a/registry/auth/token/accesscontroller.go
+++ b/registry/auth/token/accesscontroller.go
@@ -351,7 +351,7 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
 	rootPool := x509.NewCertPool()
 	for _, rootCert := range rootCerts {
 		rootPool.AddCert(rootCert)
-		if key := GetRFC7638Thumbprint(rootCert.PublicKey); key != "" {
+		if key := GetJWKThumbprint(rootCert.PublicKey); key != "" {
 			trustedKeys[key] = rootCert.PublicKey
 		}
 	}
diff --git a/registry/auth/token/util.go b/registry/auth/token/util.go
index 29b8811fd..88079cce7 100644
--- a/registry/auth/token/util.go
+++ b/registry/auth/token/util.go
@@ -3,6 +3,7 @@ package token
 import (
 	"crypto"
 	"crypto/ecdsa"
+	"crypto/ed25519"
 	"crypto/rsa"
 	"crypto/sha256"
 	"encoding/base64"
@@ -54,15 +55,26 @@ func hashAndEncode(payload string) string {
 	return base64.RawURLEncoding.EncodeToString(shasum[:])
 }
 
+// Deprecated: use GetJWKThumbprint instead.
+func GetRFC7638Thumbprint(publickey crypto.PublicKey) string {
+	return getJWKThumbprint(publickey, true)
+}
+
+// GetJWKThumbprint calculates the JWK thumbprint of a public key.
+// The current implementation uses SHA256, but this algorithm may change
+// in the future as cryptographic best practices evolve. It returns an
+// empty string if the public key type is not supported.
+func GetJWKThumbprint(publickey crypto.PublicKey) string {
+	return getJWKThumbprint(publickey, false)
+}
+
 // RFC7638 states in section 3 sub 1 that the keys in the JSON object payload
 // are required to be ordered lexicographical order. Golang does not guarantee
 // order of keys[0]
 // [0]: https://groups.google.com/g/golang-dev/c/zBQwhm3VfvU
 //
 // The payloads are small enough to create the JSON strings manually
-func GetRFC7638Thumbprint(publickey crypto.PublicKey) string {
-	var payload string
-
+func getJWKThumbprint(publickey crypto.PublicKey, skipED25519 bool) string {
 	switch pubkey := publickey.(type) {
 	case *rsa.PublicKey:
 		e_big := big.NewInt(int64(pubkey.E)).Bytes()
@@ -70,17 +82,22 @@ func GetRFC7638Thumbprint(publickey crypto.PublicKey) string {
 		e := base64.RawURLEncoding.EncodeToString(e_big)
 		n := base64.RawURLEncoding.EncodeToString(pubkey.N.Bytes())
 
-		payload = fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`, e, n)
+		return hashAndEncode(fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`, e, n))
 	case *ecdsa.PublicKey:
 		params := pubkey.Params()
 		crv := params.Name
 		x := base64.RawURLEncoding.EncodeToString(params.Gx.Bytes())
 		y := base64.RawURLEncoding.EncodeToString(params.Gy.Bytes())
 
-		payload = fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`, crv, x, y)
+		return hashAndEncode(fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`, crv, x, y))
+	case ed25519.PublicKey:
+		if skipED25519 {
+			return ""
+		}
+		x := base64.RawURLEncoding.EncodeToString(pubkey)
+
+		return hashAndEncode(fmt.Sprintf(`{"crv":"Ed25519","kty":"OTP","x":"%s"}`, x))
 	default:
 		return ""
 	}
-
-	return hashAndEncode(payload)
 }