From 32a9fee3e6812895c2758c9e692c7ca0ea384173 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Wed, 3 Mar 2021 13:33:56 +0100 Subject: [PATCH] Replace /signature-verification-key endpoint with extension-specific ones /signature-verification-key.{der|pem}. Fix Content-Disposition header --- CHANGELOG.md | 3 ++- README.md | 6 +++++- http/skinsystem.go | 22 ++++++++++++++++++---- http/skinsystem_test.go | 37 +++++++++++++++++++++++++++++++++---- 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dca8f1..6557699 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `/profile/{username}` endpoint, which returns a profile and its textures, equivalent of the Mojang's [UUID -> Profile + Skin/Cape endpoint](https://wiki.vg/Mojang_API#UUID_-.3E_Profile_.2B_Skin.2FCape). -- `/signature-verification-key` endpoint, which returns the public key in `DER` format for signature verification. +- `/signature-verification-key.der` and `/signature-verification-key.pem` endpoints, which returns the public key in + `DER` or `PEM` formats for signature verification. ### Fixed - [#28](https://github.com/elyby/chrly/issues/28): Added handling of corrupted data from the Mojang's username to UUID diff --git a/README.md b/README.md index 66d94b1..8fde0f7 100644 --- a/README.md +++ b/README.md @@ -280,11 +280,15 @@ Note that this endpoint will try to use the UUID for the stored profile in the d to the situation where the user is available in the database but has no textures, which caused them to be retrieved from the Mojang's API. -#### `GET /signature-verification-key` +#### `GET /signature-verification-key.der` This endpoint returns a public key that can be used to verify textures signatures. The key is provided in `DER` format, so it can be used directly in the Authlib, without modifying the signature checking algorithm. +#### `GET /signature-verification-key.pem` + +The same endpoint as the previous one, except that it returns the key in `PEM` format. + #### `GET /textures/signed/{username}` Actually, this is the [Ely.by](https://ely.by)'s feature called diff --git a/http/skinsystem.go b/http/skinsystem.go index 15a593d..c586a49 100644 --- a/http/skinsystem.go +++ b/http/skinsystem.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "encoding/base64" "encoding/json" + "encoding/pem" "github.com/elyby/chrly/utils" "io" "net/http" @@ -71,7 +72,8 @@ func (ctx *Skinsystem) Handler() *mux.Router { router.HandleFunc("/skins", ctx.skinGetHandler).Methods(http.MethodGet) router.HandleFunc("/cloaks", ctx.capeGetHandler).Methods(http.MethodGet) // Utils - router.HandleFunc("/signature-verification-key", ctx.signatureVerificationKeyHandler).Methods(http.MethodGet) + router.HandleFunc("/signature-verification-key.der", ctx.signatureVerificationKeyHandler).Methods(http.MethodGet) + router.HandleFunc("/signature-verification-key.pem", ctx.signatureVerificationKeyHandler).Methods(http.MethodGet) return router } @@ -244,9 +246,21 @@ func (ctx *Skinsystem) signatureVerificationKeyHandler(response http.ResponseWri panic(err) } - _, _ = response.Write(asn1Bytes) - response.Header().Set("Content-Type", "application/octet-stream") - response.Header().Set("Content-Disposition", "attachment; filename=\"yggdrasil_session_pubkey.der\"") + if strings.HasSuffix(request.URL.Path, ".pem") { + publicKeyBlock := pem.Block{ + Type: "PUBLIC KEY", + Bytes: asn1Bytes, + } + + publicKeyPemBytes := pem.EncodeToMemory(&publicKeyBlock) + + response.Header().Set("Content-Disposition", "attachment; filename=\"yggdrasil_session_pubkey.pem\"") + _, _ = response.Write(publicKeyPemBytes) + } else { + response.Header().Set("Content-Type", "application/octet-stream") + response.Header().Set("Content-Disposition", "attachment; filename=\"yggdrasil_session_pubkey.der\"") + _, _ = response.Write(asn1Bytes) + } } // TODO: in v5 should be extracted into some ProfileProvider interface, diff --git a/http/skinsystem_test.go b/http/skinsystem_test.go index 615516e..151d84f 100644 --- a/http/skinsystem_test.go +++ b/http/skinsystem_test.go @@ -11,6 +11,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "strings" "testing" "time" @@ -1083,9 +1084,18 @@ func (suite *skinsystemTestSuite) TestProfile() { * Get profile tests cases * ***************************/ -var signingKeyTestsCases = []*skinsystemTestCase{ +type signingKeyTestCase struct { + Name string + KeyFormat string + BeforeTest func(suite *skinsystemTestSuite) + PanicErr string + AfterTest func(suite *skinsystemTestSuite, response *http.Response) +} + +var signingKeyTestsCases = []*signingKeyTestCase{ { - Name: "Get public key", + Name: "Get public key in DER format", + KeyFormat: "DER", BeforeTest: func(suite *skinsystemTestSuite) { pubPem, _ := pem.Decode([]byte("-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnm\nUDlzHBQH3DpYef5WCO32TDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQ==\n-----END PUBLIC KEY-----")) publicKey, _ := x509.ParsePKIXPublicKey(pubPem.Bytes) @@ -1095,12 +1105,31 @@ var signingKeyTestsCases = []*skinsystemTestCase{ AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { suite.Equal(200, response.StatusCode) suite.Equal("application/octet-stream", response.Header.Get("Content-Type")) + suite.Equal("attachment; filename=\"yggdrasil_session_pubkey.der\"", response.Header.Get("Content-Disposition")) body, _ := ioutil.ReadAll(response.Body) suite.Equal([]byte{48, 92, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 75, 0, 48, 72, 2, 65, 0, 214, 212, 165, 80, 153, 144, 194, 169, 126, 246, 25, 211, 197, 183, 150, 233, 157, 1, 166, 49, 44, 25, 230, 80, 57, 115, 28, 20, 7, 220, 58, 88, 121, 254, 86, 8, 237, 246, 76, 53, 58, 125, 226, 9, 231, 192, 52, 148, 12, 176, 130, 214, 120, 195, 8, 182, 116, 97, 206, 207, 253, 97, 2, 247, 2, 3, 1, 0, 1}, body) }, }, { - Name: "Error while obtaining public key", + Name: "Get public key in PEM format", + KeyFormat: "PEM", + BeforeTest: func(suite *skinsystemTestSuite) { + pubPem, _ := pem.Decode([]byte("-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnm\nUDlzHBQH3DpYef5WCO32TDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQ==\n-----END PUBLIC KEY-----")) + publicKey, _ := x509.ParsePKIXPublicKey(pubPem.Bytes) + + suite.TexturesSigner.On("GetPublicKey").Return(publicKey, nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(200, response.StatusCode) + suite.Equal("text/plain; charset=utf-8", response.Header.Get("Content-Type")) + suite.Equal("attachment; filename=\"yggdrasil_session_pubkey.pem\"", response.Header.Get("Content-Disposition")) + body, _ := ioutil.ReadAll(response.Body) + suite.Equal("-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnm\nUDlzHBQH3DpYef5WCO32TDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQ==\n-----END PUBLIC KEY-----\n", string(body)) + }, + }, + { + Name: "Error while obtaining public key", + KeyFormat: "DER", BeforeTest: func(suite *skinsystemTestSuite) { suite.TexturesSigner.On("GetPublicKey").Return(nil, errors.New("textures signer error")) }, @@ -1113,7 +1142,7 @@ func (suite *skinsystemTestSuite) TestSignatureVerificationKey() { suite.RunSubTest(testCase.Name, func() { testCase.BeforeTest(suite) - req := httptest.NewRequest("GET", "http://chrly/signature-verification-key", nil) + req := httptest.NewRequest("GET", "http://chrly/signature-verification-key."+strings.ToLower(testCase.KeyFormat), nil) w := httptest.NewRecorder() if testCase.PanicErr != "" {