Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add size enforcement and tests #37

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions boxer.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ type boxMaker struct {
counterparty *rsa.PublicKey
}

const maxBoxSize = 4 * 1024 * 1024

func NewBoxer(key *rsa.PrivateKey, counterparty *rsa.PublicKey) boxMaker {
return boxMaker{
key: key,
Expand Down Expand Up @@ -172,6 +170,10 @@ func (boxer boxMaker) DecodeUnverified(b64 string) (*Box, error) {
return nil, fmt.Errorf("decoding base64: %w", err)
}

if len(data) > V0MaxSize {
return nil, fmt.Errorf("data too big, is %d, max is %d", len(data), V0MaxSize)
}

return boxer.DecodeRawUnverified(data)
}

Expand Down Expand Up @@ -199,14 +201,18 @@ func (boxer boxMaker) DecodePngUnverified(r io.Reader) (*Box, error) {
return nil, fmt.Errorf("decoding png: %w", err)
}

if data.Len() > maxBoxSize {
if data.Len() > V0MaxSize {
return nil, errors.New("looks to be larger than max box size")
}

return boxer.DecodeRawUnverified(data.Bytes())
}

func (boxer boxMaker) DecodeRaw(data []byte) (*Box, error) {
if len(data) > V0MaxSize {
return nil, fmt.Errorf("data too big, is %d, max is %d", len(data), V0MaxSize)
}

var outer outerBox
if err := msgpack.Unmarshal(data, &outer); err != nil {
return nil, fmt.Errorf("unmarshalling outer: %w", err)
Expand Down
90 changes: 90 additions & 0 deletions cross_language_tests/boxer_cross_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,93 @@ func TestBoxerRuby(t *testing.T) {
})
}
}

func TestBoxerMaxSize(t *testing.T) {
t.Parallel()

//
// Setup keys and similar.
//
aliceKey, err := krypto.RsaRandomKey()
require.NoError(t, err)
var alicePubPem bytes.Buffer
require.NoError(t, krypto.RsaPublicKeyToPem(aliceKey, &alicePubPem))

bobKey, err := krypto.RsaRandomKey()
require.NoError(t, err)
var bobPem bytes.Buffer
require.NoError(t, krypto.RsaPrivateKeyToPem(bobKey, &bobPem))

malloryKey, err := krypto.RsaRandomKey()
require.NoError(t, err)
var malloryPem bytes.Buffer
require.NoError(t, krypto.RsaPrivateKeyToPem(malloryKey, &malloryPem))

aliceBoxer := krypto.NewBoxer(aliceKey, bobKey.Public().(*rsa.PublicKey))

tooBigBytes := mkrand(t, krypto.V0MaxSize+1)
tooBigBytesB64 := base64.StdEncoding.EncodeToString(tooBigBytes)

t.Run("max size enforced in go", func(t *testing.T) {
t.Parallel()

_, err = aliceBoxer.Decode(tooBigBytesB64)
require.ErrorContains(t, err, "data too big")

_, err = aliceBoxer.DecodeUnverified(tooBigBytesB64)
require.ErrorContains(t, err, "data too big")
})

t.Run("max size enforced in ruby", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()

responseTo := ulid.New()
ciphertext, err := aliceBoxer.Encode(responseTo, tooBigBytes)
require.NoError(t, err)

var png bytes.Buffer
pngFile := path.Join(dir, ulid.New()+".png")
require.NoError(t, krypto.ToPngNoMaxSize(&png, tooBigBytes))
require.NoError(t, os.WriteFile(pngFile, png.Bytes(), 0644))

tests := []boxerCrossTestCase{
{Key: bobPem.Bytes(), Counterparty: alicePubPem.Bytes(), Ciphertext: ciphertext, cmd: "decode"},
{Key: bobPem.Bytes(), Counterparty: alicePubPem.Bytes(), Ciphertext: ciphertext, cmd: "decodeunverified"},
{Key: bobPem.Bytes(), Ciphertext: ciphertext, cmd: "decodeunverified"},
{Key: bobPem.Bytes(), Counterparty: alicePubPem.Bytes(), PngFile: pngFile, cmd: "decodepng"},
}

for _, tt := range tests {
tt := tt

t.Run("", func(t *testing.T) {
t.Parallel()

if runtime.GOOS == "windows" && tt.cmd == "decodepng" {
t.Skip("skip png decode test on windows because ruby library chunky_png is looking for CRLF png signature")
}

testfile := path.Join(dir, ulid.New()+".msgpack")
rubyout := path.Join(dir, ulid.New()+"ruby-out")

//
// Setup
//
b, err := msgpack.Marshal(tt)
require.NoError(t, err)
require.NoError(t, os.WriteFile(testfile, []byte(base64.StdEncoding.EncodeToString(b)), 0644))

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

//#nosec G204 -- No taint on hardcoded input
cmd := exec.CommandContext(ctx, "ruby", boxerRB, tt.cmd, testfile, rubyout)
out, err := cmd.CombinedOutput()

require.Error(t, err)
require.Contains(t, string(out), "box too large", "actual out: ", string(out))
})
}
})
}
34 changes: 34 additions & 0 deletions cross_language_tests/challenge_cross_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,40 @@ func TestChallenge_GoGenerate_RubyRespond(t *testing.T) {
}
}

func TestChallenge_MaxSize(t *testing.T) {
t.Parallel()

tooBigBytes := mkrand(t, krypto.V0MaxSize+1)

t.Run("max size enforced in go", func(t *testing.T) {
t.Parallel()
_, err := challenge.UnmarshalChallenge(tooBigBytes)
require.ErrorContains(t, err, "exceeds max size", "should get an error due to size")
})

t.Run("max size enforced in ruby", func(t *testing.T) {
t.Parallel()

dir := t.TempDir()
rubyPrivateSigningKey := ecdsaKey(t)

rubyChallengeCmdData := rubyChallengeCmd{
RubyPrivateSigningKey: privateEcKeyToPem(t, rubyPrivateSigningKey),
}

out, err := rubyChallengeExec("generate", dir, rubyChallengeCmdData)
require.NoError(t, err, string(out))

rubyChallengeCmdData = rubyChallengeCmd{
ResponsePack: tooBigBytes,
}

out, err = rubyChallengeExec("open_response_png", dir, rubyChallengeCmdData)
require.Error(t, err, string(out))
require.Contains(t, string(out), "response too large", "should get an error due to size")
})
}

// #nosec G306 -- Need readable files
func rubyChallengeExec(rubyCmd, dir string, inputData rubyChallengeCmd) ([]byte, error) {
testCaseBytes, err := msgpack.Marshal(inputData)
Expand Down
4 changes: 4 additions & 0 deletions lib/krypto/boxer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ def decode_unverified(data)
end

def decode(data, verify: true, raw: false, png: false)
if data.size > MAX_CHALLENGE_SIZE
raise "box too large"
end

data = unpng(data) if png
data = Base64.strict_decode64(data) unless raw || png
outer = Outer.new(MessagePack.unpack(data).slice(*OUTER_FIELDS.map(&:to_s)))
Expand Down
7 changes: 7 additions & 0 deletions lib/krypto/challenge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
require "openssl"

module Krypto

MAX_CHALLENGE_SIZE = 4 * 1024 * 1024
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved

class Challenge
def self.generate(signing_key, challenge_id, challenge_data, request_data, timestamp: Time.now)
private_encryption_key = RbNaCl::PrivateKey.generate
Expand All @@ -29,6 +32,10 @@ def self.generate(signing_key, challenge_id, challenge_data, request_data, times
end

def self.unmarshal(data, png: false, base64: true)
if data.size > MAX_CHALLENGE_SIZE
raise "challenge too large"
end

data = ::Krypto::Png.decode_blob(data) if png
data = Base64.strict_decode64(data) if base64
OuterChallenge.new(MessagePack.unpack(data).slice(*OUTER_CHALLENGE_FIELDS.map(&:to_s)))
Expand Down
4 changes: 4 additions & 0 deletions lib/krypto/challenge_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
module Krypto
class ChallengeResponse
def self.unmarshal(data, png: false, base64: true)
if data.size > MAX_CHALLENGE_SIZE
raise "response too large"
end

data = ::Krypto::Png.decode_blob(data) if png
data = Base64.strict_decode64(data) if base64

Expand Down
4 changes: 4 additions & 0 deletions pkg/challenge/challenge.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ func (o *OuterChallenge) RespondPng(signer crypto.Signer, signer2 crypto.Signer,
}

func UnmarshalChallenge(outerChallengeBytes []byte) (*OuterChallenge, error) {
if len(outerChallengeBytes) > krypto.V0MaxSize {
return nil, fmt.Errorf("challenge exceeds max size: %d, max is %d", len(outerChallengeBytes), krypto.V0MaxSize)
}

var outerChallenge OuterChallenge
if err := msgpack.Unmarshal(outerChallengeBytes, &outerChallenge); err != nil {
return nil, err
Expand Down
4 changes: 4 additions & 0 deletions pkg/challenge/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ type InnerResponse struct {
}

func UnmarshalResponse(outerResponseBytes []byte) (*OuterResponse, error) {
if len(outerResponseBytes) > krypto.V0MaxSize {
return nil, fmt.Errorf("response to large: is %d, max is %d", len(outerResponseBytes), krypto.V0MaxSize)
}

var outerResponse OuterResponse
if err := msgpack.Unmarshal(outerResponseBytes, &outerResponse); err != nil {
return nil, err
Expand Down
20 changes: 12 additions & 8 deletions png.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,10 @@ const (
pixelsInHeader = 2
alphaValue = 0xFF

v0MaxSize = 1 << 24
V0MaxSize = 4 * 1024 * 1024
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
)

func ToPng(w io.Writer, data []byte) error {
dataSize := len(data)

if dataSize > v0MaxSize {
return fmt.Errorf("data too big: %d is bigger than %d", dataSize, v0MaxSize)
}

func ToPngNoMaxSize(w io.Writer, data []byte) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have this function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the original ToPng() check the size before generating the PNG and I needed a way to make a PNG that was too large in order to test that receiving party had the same size limit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? The size check is before any png detection, right? Just send it too much data and check the error type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels really wrong to bake in functionality we don't want to have a test for if len(data) > V0MaxSize

Copy link
Contributor Author

@James-Pickett James-Pickett Apr 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated it to just send too much data. Though the test is not "perfect" due to the b64 encoding in various places that changes the size of the data. I don't think I could untangle that with out a bit of a refactor. Which leads me to wonder, do we still need the "boxer" logic at all since v2 relies only on the challenge logic.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I wonder how many v1 clients are left. Maybe we should rip it out soon

pixelCount := divCeil(len(data), usableBytesPerPixel)
pixelCount = pixelCount + pixelsInHeader + 1

Expand Down Expand Up @@ -65,6 +59,16 @@ func ToPng(w io.Writer, data []byte) error {
return encoder.Encode(w, img)
}

func ToPng(w io.Writer, data []byte) error {
dataSize := len(data)

if dataSize > V0MaxSize {
return fmt.Errorf("data too big: %d is bigger than %d", dataSize, V0MaxSize)
}

return ToPngNoMaxSize(w, data)
}

func FromPng(r io.Reader, w io.Writer) error {
imgRaw, _, err := image.Decode(r)
if err != nil {
Expand Down
Loading