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

Implement sha and rand #2

Merged
merged 25 commits into from
Mar 23, 2022
Merged

Implement sha and rand #2

merged 25 commits into from
Mar 23, 2022

Conversation

qmuntal
Copy link
Contributor

@qmuntal qmuntal commented Mar 15, 2022

This PR has the following changes (review commit by commit):

  • ae520e3: Implements a tool to generate syscall wrappers without having golang.org/x/sys as runtime dependency, which will facilitate vendoring this package into our Go fork. Read inline documentation for more context.
  • b520613: Implement all SHA algorithms except SHA224, which is not supported by BCrypt.
  • ab022d0: Implement random number generator.
  • ca9c71c: Add GH Actions test workflow

go.mod Show resolved Hide resolved
cmd/mksyscall/main.go Outdated Show resolved Hide resolved
internal/bcrypt/zsyscall_windows.go Show resolved Hide resolved
internal/bcrypt/zsyscall_windows.go Show resolved Hide resolved
cmd/mksyscall/main.go Show resolved Hide resolved
cng/sha.go Show resolved Hide resolved
cng/rand.go Outdated
if len(b) == 0 {
return 0, nil
}
err := bcrypt.GenRandom(0, &b[0], uint32(len(b)), bcrypt.USE_SYSTEM_PREFERRED_RNG)
Copy link
Member

Choose a reason for hiding this comment

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

The USE_SYSTEM_PREFERRED_RNG flag isn't supported on Vista so that puts a lower bound on where our impl would be supported. That seems fine but should note that in our docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are getting old. One could think that Vista is still a mainstream version, but it's no longer supported by MSFT since 2017 and the last Go version that supported it was Go 1.10 (see Go minimum requirements).

cng/rand.go Show resolved Hide resolved
cng/rand_test.go Show resolved Hide resolved
@qmuntal
Copy link
Contributor Author

qmuntal commented Mar 18, 2022

@jaredpar you might want to review c18affd, which I added after your last review.

It reduces boilerplate and makes the code safer by delegating to the autogenerated code the []byte -> *byte+length arguments transform.

@qmuntal
Copy link
Contributor Author

qmuntal commented Mar 18, 2022

I've filled an upstream proposal so we can delegate UTF16 string encoding to the autogenerated code: golang/go#51786

HASH_HANDLE HANDLE
)

//sys GetProperty(hObject HANDLE, pszProperty *uint16, pbOutput []byte, pcbResult *uint32, dwFlags uint32) (s error) = bcrypt.BCryptGetProperty
Copy link
Member

Choose a reason for hiding this comment

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

Aren't we missing the cbOutput uint32 parameter here?

Suggested change
//sys GetProperty(hObject HANDLE, pszProperty *uint16, pbOutput []byte, pcbResult *uint32, dwFlags uint32) (s error) = bcrypt.BCryptGetProperty
//sys GetProperty(hObject HANDLE, pszProperty *uint16, pbOutput []byte, cbOutput uint32, pcbResult *uint32, dwFlags uint32) (s error) = bcrypt.BCryptGetProperty

https://docs.microsoft.com/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptgetproperty

Noticed this when looking at the getUint32 function and didn't see an input size

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sys definitions do not need to exactly match Win32 API's. When mkwinsyscall (the tool that autogenerates the wrappers) sees a slice, in this case pbOutput []byte, it transforms it into two parameters: the first element pointer followed by the length of the slice. This is how GetProperty is expanded:

func GetProperty(hObject HANDLE, pszProperty *uint16, pbOutput []byte, pcbResult *uint32, dwFlags uint32) (s error) {
var _p0 *byte
if len(pbOutput) > 0 {
_p0 = &pbOutput[0]
}
r0, _, _ := syscall.Syscall6(procBCryptGetProperty.Addr(), 6, uintptr(hObject), uintptr(unsafe.Pointer(pszProperty)), uintptr(unsafe.Pointer(_p0)), uintptr(len(pbOutput)), uintptr(unsafe.Pointer(pcbResult)), uintptr(dwFlags))
if r0 != 0 {
s = syscall.Errno(r0)
}
return
}

This not only reduces boilerplate code but is also safer, as the slice is only deferred when it is not empty. The alternative is the define the //sys signature directly the pointer to the first element and the length, which leaves to the caller casting the slice to a pointer, increasing the likelihood of a runtime panic.

Copy link
Member

Choose a reason for hiding this comment

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

How do you define a //sys signature where you can use the length pointer parameter? Do you just use a *byte* instead of []byte` in that case? There are some windows APIs where the length pointer parameter is used for both input and output (IIRC).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In that case you can't rely on mksyscall doing the []byte -> *byte+len transform, but you can still explicitly define the pointer and length parameters as follows:

GetProperty(hObject HANDLE, pszProperty *uint16, pbOutput *byte, cbOutput *uint32, pcbResult *uint32, dwFlags uint32) (s error) = bcrypt.BCryptGetProperty

The call it in this way:

func getBuf(h bcrypt.HANDLE, name string, buf []byte) (uint32, error) {
	var discard uint32
        var _p0 *byte
        if len(buf ) > 0 {
                // We can't pass &buf[0] directly to GetProperty, as it would panic when len(0) == 0!
		_p0 = &buf[0]
	}
        n := len(buf) // n will wold the input and output length!
	err := bcrypt.GetProperty(h, utf16PtrFromString(name), _p0 , &n, &discard, 0)
	return prop, err
}

cng/sha.go Outdated Show resolved Hide resolved
cng/sha.go Show resolved Hide resolved
cng/sha.go Show resolved Hide resolved
Copy link
Member

@dagood dagood left a comment

Choose a reason for hiding this comment

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

A few comments on cmd/mksyscall/main.go itself before getting to the rest.

cmd/mksyscall/main.go Outdated Show resolved Hide resolved
cmd/mksyscall/main.go Outdated Show resolved Hide resolved
cmd/mksyscall/main.go Outdated Show resolved Hide resolved
cmd/mksyscall/main.go Outdated Show resolved Hide resolved
cmd/mksyscall/main.go Outdated Show resolved Hide resolved
cng/rand.go Outdated Show resolved Hide resolved
cng/rand.go Outdated

func (randReader) Read(b []byte) (int, error) {
// BCryptGenRandom only accepts 2**32-1 bytes at a time, so truncate.
inputLen := uint32(len(b))
Copy link
Member

Choose a reason for hiding this comment

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

This looks like it made the function break for values >= 1<<32 when you try to use it like a reader--here's a test I wrote to try repeated reads like I think you'd normally do with a reader (based on https://pkg.go.dev/io#Reader) (with iter just there to prevent infinite loop):

func TestRandReadAllBig(t *testing.T) {
	b := make([]byte, 1<<33+60)
	bb := b
	var iter int
	for iter = 0; len(bb) > 0 && iter <= 9000; iter++ {
		n, err := RandReader.Read(bb)
		if n > 0 {
			bb = bb[n:]
		}
		if err != nil {
			if err == io.EOF {
				break
			}
			t.Fatal(err)
		}
		t.Logf("Iter %v, got %v\n", iter, n)
	}
	t.Logf("Done. %v\n", iter)
}
=== RUN   TestRandReadAllBig
    rand_test.go:40: Iter 0, got 60
    rand_test.go:40: Iter 1, got 0
    rand_test.go:40: Iter 2, got 0
    rand_test.go:40: Iter 3, got 0
...

(io.ReadFull runs into the problem too--but it simply hangs, because it doesn't have an iter limit.)

Should this func actually do something more like this?

// BCryptGenRandom only accepts 2**32-1 bytes at a time, so limit it.
inputLen := len(b)
const maxGenRandomLen = 2<<32 - 1
if inputLen > maxGenRandomLen {
	inputLen = maxGenRandomLen
}
if inputLen == 0 {
	return 0, io.EOF
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm aware that it is breaking for values => 1<<32, but I wanted to keep consistency with Go crypto/rand's implementation: https://github.com/golang/go/blob/7eaad60737bc507596c56cec4951b089596ccc9e/src/crypto/rand/rand_windows.go#L19.

What I hadn't seen is that io.ReadFull hangs. I can reproduce it even in Go crypto/rand... I think this deserved at least a CVE and and best a security bounty if we find a service that is passing in buffers whose length is provided by the user.

@dagood @jaredpar thoughts? Do you see the CVE as I do? Is there a MSFT standard procedure for reporting? Should we go for the bounty?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I fixed it in 7b9094c and 310966f.

Copy link
Member

@dagood dagood Mar 21, 2022

Choose a reason for hiding this comment

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

A few somewhat related issues (didn't see anything closer):

cng/sha.go Outdated Show resolved Hide resolved
internal/sysdll/sys_windows.go Show resolved Hide resolved
cmd/mksyscall/main.go Outdated Show resolved Hide resolved
cmd/mksyscall/main.go Outdated Show resolved Hide resolved
cng/cng.go Outdated Show resolved Hide resolved
cng/cng.go Outdated
if v > maxULong {
return maxULong, true
}
return uint32(v), false
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, -1 yields 0xffffffff, rather than what I'd expect (0). Should the func handle negative numbers, or maybe just panic because that isn't something we expect to ever happen?

cng/rand.go Outdated
const flags = bcrypt.USE_SYSTEM_PREFERRED_RNG
err := bcrypt.GenRandom(0, b[:inputLen], flags)
if err == nil && truncated {
err = bcrypt.GenRandom(0, b[inputLen:], flags)
Copy link
Member

Choose a reason for hiding this comment

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

I think this solves it for the 1<<32+60 case, but not e.g. 1<<34. In that case the first call fills up the first ~1<<32 positions in the slice, this second call fills up the next ~1<<32 positions, but the remaining positions aren't filled.

func TestRandBigger(t *testing.T) {
	b := make([]byte, 1<<34-1)
	c, err := io.ReadFull(RandReader, b)
	if err != nil {
		return
	}
	t.Logf("Claimed read %x of %x bytes\n", c, len(b))
	var i int
	for i = len(b) - 1; i > 0; i-- {
		if b[i] != 0 {
			break
		}
	}
	t.Logf("Last non0  = %x\n", i)
	t.Logf("len(b)     = %x\n", len(b))
	t.Logf("Got/wanted = %v\n", float64(i)/float64(len(b)))
}

Without seeded rand, I guess we can't technically determine if this means the func isn't working properly 😄, but when I run this locally, I get:

    rand_test.go:42: Claimed read 3ffffffff of 3ffffffff bytes
    rand_test.go:49: Last non0  = fffffffe
    rand_test.go:50: len(b)     = 3ffffffff
    rand_test.go:51: Got/wanted = 0.2499999998981366

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm overcomplicating the randReader.Read implementation. rand.Reader.Read is no guaranteed to return the length if the incoming slice, this guarantee only applies to rand.Read, as ianlancetaylor explains in this comment. Therefore there is no need to call bcrypt.GenRandom more times if the input buffer lengths is clamped, rand.Read will do that for us.

cng/sha.go Outdated
inputLen, truncated := ulong(len(p))
err := bcrypt.HashData(h.ctx, p[:inputLen], 0)
if err == nil && truncated {
err = bcrypt.HashData(h.ctx, p[inputLen:], 0)
Copy link
Member

Choose a reason for hiding this comment

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

This also doesn't seem to work for values that need more than two iterations:

func TestSHAHugeWriteCollide(t *testing.T) {
	a := make([]byte, 1<<33+32)
	a[len(a)-5] = 5
	t.Logf("%v\n", prettySHA256Sum(t, a)) // 8e706dbcf4ec73f4f06c177146145b502d1a0f7dc42ad967469ec0727fcdcd9c

	b := make([]byte, 1<<33+32)
	b[len(a)-5] = 6
	t.Logf("%v\n", prettySHA256Sum(t, b)) // 8e706dbcf4ec73f4f06c177146145b502d1a0f7dc42ad967469ec0727fcdcd9c
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

New attempt: fe562dd

if err != nil {
// hash.Hash interface mandates Write should never return an error.
panic(err)
}
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to move this check into the loop just after err is set so the loop condition can be simplified down to for n < len(p) {?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IMO having the error check in the loop condition makes the loop content cleaner and easier to read, but it could be just me 😛.

It is also kind of idiomatic.

Copy link
Member

Choose a reason for hiding this comment

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

Interesting, for me, having the error handling anywhere but immediately after where the error is generated makes me immediately want to confirm where the error's going, so I end up taking a much closer look. Here, flow travels upwards to the loop condition and then down afterwards, so I find myself skipping around. Vs. what I think of as normal (when there's no loop involved):

for n < len(p) {
	nn := clamp32(len(p[n:]))
	err = bcrypt.HashData(h.ctx, p[n:n+nn], 0)
	if err != nil {
		// hash.Hash interface mandates Write should never return an error.
		panic(err)
	}
	n += nn
}
runtime.KeepAlive(h)
return len(p), nil

But, as an idiom, I guess I'll get used to it. Maybe this has to do with review, focusing on "how can this fail" rather than "what does this do when successful".

cng/rand.go Outdated
if len(b) == 0 {
return 0, nil
}
n := clamp32(len(b))
Copy link
Member

Choose a reason for hiding this comment

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

Seems all uses of clam32 are on []byte length. Feels like a place where generics could be useful in the future because would allow for a flexible len method.

func len32[T any](slice []T) int32 {
	v := len(slice)
	if v > math.MaxInt32 {
		return math.MaxInt32
	}
	return int32(v)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would be cool, but then we won't be able to use this backend in pre-go1.18 toolchains. Anyway, I like the idea of accepting an slice and calling in len32 + using math.Uint32, I'll take that bits!

@qmuntal qmuntal merged commit 20e03f8 into main Mar 23, 2022
@qmuntal qmuntal deleted the dev/qmuntal/base branch March 23, 2022 09:26
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.

3 participants