Skip to content

Fix legacy skull texture profile normalization#4960

Open
itslilscorp wants to merge 2 commits into
ViaVersion:masterfrom
itslilscorp:Legacy-Skull-Normalization-Changes
Open

Fix legacy skull texture profile normalization#4960
itslilscorp wants to merge 2 commits into
ViaVersion:masterfrom
itslilscorp:Legacy-Skull-Normalization-Changes

Conversation

@itslilscorp

Copy link
Copy Markdown
Contributor

What changed

This PR normalizes legacy skull texture profile data in two cases:

  • Restores safe missing Base64 padding for textures profile values during the 1.15.2 -> 1.16 skull owner rewrite path.
  • Drops empty texture signatures when converting profile data, while preserving non-empty signatures.

Why

Some legacy skulls from older servers contain texture values that older authlib versions accepted, but newer clients reject with:

Could not decode textures payload
Input byte array has wrong 4-byte ending unit

In testing, affected values were missing Base64 padding, for example length 179 with mod4=3, while working values were length 180 with padding.

Modern clients can also spam the client's log with signature verification errors when a texture property contains an empty signature string:

Failed to verify signature on property Property[name=textures, ..., signature=]
java.security.SignatureException: Bad signature length: got 0 but was expecting 512

An empty string is not a valid signature. Treating it as absent preserves unsigned texture behavior and avoids the invalid-signature path.

Scope

The Base64 normalization is limited to skull profile textures values crossing the 1.15.2 -> 1.16 rewrite path, covering both skull items and skull block entities.

Valid signatures and already valid texture values are preserved.

AI was used to help investigate the issue and prepare the patch. The behavior was reproduced and the resulting change was manually tested.

Normalize legacy skull texture profile data when translating to newer
versions.

Some legacy skulls can contain texture values with missing Base64 padding.
Older authlib versions tolerated this, but newer clients reject the payload
during skin loading. Normalize only the textures property by removing
whitespace and restoring safe missing padding during the 1.15.2->1.16 skull
owner rewrite path.

Also omit empty texture signatures when converting profile data. An empty
signature is not a valid signature and causes modern clients to attempt
verification and log errors; treating it as absent preserves unsigned texture
behavior.
@kennytv

kennytv commented Jun 7, 2026

Copy link
Copy Markdown
Member

Do you have example commands/chunk data/inventory data for such skulls to test with?

Move fix from 1.16 to 1.18 since that is where the issue actually begins. Previously, a player using 1.18 playing on a 1.17 server could still bump into this issue.
@itslilscorp

Copy link
Copy Markdown
Contributor Author

Just a heads up, most of my testing was done on a 1.12 server swapping back and forth between a 1.12 and 26.1 client, but the issue is present on clients 1.18+.

Texture Signature Issue

I was able to test this with this command:

/give @p minecraft:skull 1 3 {SkullOwner:{Id:"dcfab6a7-8467-4338-8787-af7245c3d31e",Properties:{textures:[{Value:"ewogICJ0aW1lc3RhbXAiIDogMTc4MDg1MzU4MDI2NSwKICAicHJvZmlsZUlkIiA6ICJkY2ZhYjZhNzg0Njc0MzM4ODc4N2FmNzI0NWMzZDMxZSIsCiAgInByb2ZpbGVOYW1lIiA6ICJJdHNMaWxTY29ycCIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS8zMTcwNWE1YWE3ZDgwMDYwM2E2OWM3MmZlOTMxYWRhZWQ3MDJlNTE4NjJhNjE2ZTcwY2Q4NzU2M2I3ZTY5YmI2IgogICAgfSwKICAgICJDQVBFIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS9kYmMyMWUyMjI1MjhlMzBkYzg4NDQ1MzE0ZjdiZTZmZjEyZDNhZWViYzNjMTkyMDU0ZmJhN2UzYjNmOGM3N2IxIgogICAgfQogIH0KfQ==",Signature:""}]}}}

In both versions, the skull renders properly as expected, but on 26.1 without this patch, I receive this error in my console:

Console Output
[15:19:18] [Worker-Main-21/ERROR]: Failed to verify signature on property Property[name=textures, value=ewogICJ0aW1lc3RhbXAiIDogMTc4MDg1MzU4MDI2NSwKICAicHJvZmlsZUlkIiA6ICJkY2ZhYjZhNzg0Njc0MzM4ODc4N2FmNzI0NWMzZDMxZSIsCiAgInByb2ZpbGVOYW1lIiA6ICJJdHNMaWxTY29ycCIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS8zMTcwNWE1YWE3ZDgwMDYwM2E2OWM3MmZlOTMxYWRhZWQ3MDJlNTE4NjJhNjE2ZTcwY2Q4NzU2M2I3ZTY5YmI2IgogICAgfSwKICAgICJDQVBFIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS9kYmMyMWUyMjI1MjhlMzBkYzg4NDQ1MzE0ZjdiZTZmZjEyZDNhZWViYzNjMTkyMDU0ZmJhN2UzYjNmOGM3N2IxIgogICAgfQogIH0KfQ==, signature=]
java.security.SignatureException: Bad signature length: got 0 but was expecting 512
	at java.base/sun.security.rsa.RSASignature.engineVerify(RSASignature.java:212)
	at java.base/java.security.Signature$Delegate.engineVerify(Signature.java:1434)
	at java.base/java.security.Signature.verify(Signature.java:802)
	at knot//com.mojang.authlib.yggdrasil.YggdrasilServicesKeyInfo.validateProperty(YggdrasilServicesKeyInfo.java:165)
	at knot//com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService.lambda$getPropertySignatureState$0(YggdrasilMinecraftSessionService.java:185)
	at java.base/java.util.stream.MatchOps$1MatchSink.accept(MatchOps.java:90)
	at java.base/java.util.AbstractList$RandomAccessSpliterator.tryAdvance(AbstractList.java:708)
	at java.base/java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:147)
	at java.base/java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:588)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:574)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:560)
	at java.base/java.util.stream.MatchOps$MatchOp.evaluateSequential(MatchOps.java:230)
	at java.base/java.util.stream.MatchOps$MatchOp.evaluateSequential(MatchOps.java:196)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:265)
	at java.base/java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:678)
	at knot//com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService.getPropertySignatureState(YggdrasilMinecraftSessionService.java:185)
	at knot//com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService.unpackTextures(YggdrasilMinecraftSessionService.java:128)
	at knot//net.minecraft.client.resources.SkinManager$1.lambda$load$0(SkinManager.java:60)
	at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1789)
	at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1781)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:511)
	at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1450)
	at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:2019)
	at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:187)
[15:19:18] [Worker-Main-21/ERROR]: Failed to verify signature on property Property[name=textures, value=ewogICJ0aW1lc3RhbXAiIDogMTc4MDg1MzU4MDI2NSwKICAicHJvZmlsZUlkIiA6ICJkY2ZhYjZhNzg0Njc0MzM4ODc4N2FmNzI0NWMzZDMxZSIsCiAgInByb2ZpbGVOYW1lIiA6ICJJdHNMaWxTY29ycCIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS8zMTcwNWE1YWE3ZDgwMDYwM2E2OWM3MmZlOTMxYWRhZWQ3MDJlNTE4NjJhNjE2ZTcwY2Q4NzU2M2I3ZTY5YmI2IgogICAgfSwKICAgICJDQVBFIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS9kYmMyMWUyMjI1MjhlMzBkYzg4NDQ1MzE0ZjdiZTZmZjEyZDNhZWViYzNjMTkyMDU0ZmJhN2UzYjNmOGM3N2IxIgogICAgfQogIH0KfQ==, signature=]
java.security.SignatureException: Bad signature length: got 0 but was expecting 512
	at java.base/sun.security.rsa.RSASignature.engineVerify(RSASignature.java:212)
	at java.base/java.security.Signature$Delegate.engineVerify(Signature.java:1434)
	at java.base/java.security.Signature.verify(Signature.java:802)
	at knot//com.mojang.authlib.yggdrasil.YggdrasilServicesKeyInfo.validateProperty(YggdrasilServicesKeyInfo.java:165)
	at knot//com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService.lambda$getPropertySignatureState$0(YggdrasilMinecraftSessionService.java:185)
	at java.base/java.util.stream.MatchOps$1MatchSink.accept(MatchOps.java:90)
	at java.base/java.util.AbstractList$RandomAccessSpliterator.tryAdvance(AbstractList.java:708)
	at java.base/java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:147)
	at java.base/java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:588)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:574)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:560)
	at java.base/java.util.stream.MatchOps$MatchOp.evaluateSequential(MatchOps.java:230)
	at java.base/java.util.stream.MatchOps$MatchOp.evaluateSequential(MatchOps.java:196)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:265)
	at java.base/java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:678)
	at knot//com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService.getPropertySignatureState(YggdrasilMinecraftSessionService.java:185)
	at knot//com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService.unpackTextures(YggdrasilMinecraftSessionService.java:128)
	at knot//net.minecraft.client.resources.SkinManager$1.lambda$load$0(SkinManager.java:60)
	at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1789)
	at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1781)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:511)
	at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1450)
	at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:2019)
	at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:187)
[15:19:18] [Worker-Main-21/WARN]: Profile contained invalid signature for textures property (profile id: dcfab6a7-8467-4338-8787-af7245c3d31e)
[15:19:18] [Render thread/INFO]: [CHAT] [@: Given [Head] * 1 to ItsLilScorp]

The error no longer shows up with this patch.

Base64 Padding Issue

I first noticed this issue on an external 1.12 server while using ViaFabricPlus, but it is replicable on 1.18 using that server's built-in ViaVersion. I've listed the server and location in case you would like to see for yourself. Note that, in my testing, if I loaded the texture properly on 1.12, it would get cached and correctly load across other versions as well until I restarted my PC. Very annoying for testing.

Server Info

IP: main.mcparks.us
Location: /warp rise
Turn around and you will see it here:

2026-06-07_15 37 49

This is the 1.12 data for one of the impacted skulls in question:

{Owner:{Id:"23f58a2e-f6a6-b008-2c1c-768cd0898fe3",Properties:{textures:[{Value:"eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzA2ZDI3YjgzNGRiOGE4Mzc0ZDQxYmUzYWI5NjNlZWZiZjc1NWIwZGMwYzljOGViMTg3MDJlYWZkODMwZTQxZiJ9fX0="}]}},Rot:0b,x:-344,y:66,z:-531,id:"minecraft:skull",SkullType:3b}

On newer versions without this patch, this skull gets translated to this:

{id:"minecraft:player_head",count:1,components:{"minecraft:profile":{id:[I;785466496,-92448722,1080509230,264963125],properties:[{name:"textures",value:"eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMmE2M2M0NGJmMWE4NDg5NGQwNDQ1N2NjNzhhMzdlYjkwNWE3NmQzNjlhZmViNzdmOGM4ZmZhYzMwMmYxMjMyIn19fQ="}]}}}

While accurate to the original data, the base64 data is broken since it is missing an extra =. This can cause the skull to not properly load its texture when moving between these versions. The console also complained about this issue.

Console Output
Could not decode textures payload
java.lang.IllegalArgumentException: Input byte array has wrong 4-byte ending unit
    at java.base/java.util.Base64$Decoder.decode0(Base64.java:842)
    at java.base/java.util.Base64$Decoder.decode(Base64.java:570)
    at java.base/java.util.Base64$Decoder.decode(Base64.java:593)
    at knot//com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService.unpackTextures(YggdrasilMinecraftSessionService.java:132)
    at knot//net.minecraft.client.resources.SkinManager$1.lambda$load$0(SkinManager.java:60)

This patch fixes this making the skull get translated to this:

{id:"minecraft:player_head",count:1,components:{"minecraft:profile":{id:[I;785466496,-92448722,1080509230,264963125],properties:[{name:"textures",value:"eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMmE2M2M0NGJmMWE4NDg5NGQwNDQ1N2NjNzhhMzdlYjkwNWE3NmQzNjlhZmViNzdmOGM4ZmZhYzMwMmYxMjMyIn19fQ=="}]}}}

This fixes the base64 issue and correctly allows the skull to load on the server.

The Problem

I could not find a way to replicate this issue on a localhost server. No matter what I did to generate the exact same skull, including give commands as well as downloading the original server world, the skulls just loaded as expected. So, I'm not sure what on that server is causing broken skulls to load sometimes and not load other times, I just know that it's solved here.

Also, this was originally implemented into the 1.15.2 -> 1.16 conversion, but I have moved it to 1.17.1 -> 1.18 since the issue affects 1.18+ clients and the prior implementation could let the issue slip through when playing on 1.16-1.17.1 servers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants