使用Go语言确定性地生成具有自定义io.Reader的RSA私钥。

huangapple go评论82阅读模式
英文:

Using Go deterministicly generate RSA Private Key with custom io.Reader

问题

由于某些原因,我需要生成无限数量的RSA公钥/私钥对。请注意,这不是用于高度安全的用途,所以请不要告诉我不要这样做,是的,我知道这不是理想的做法。所谓的“无限”是指我需要一个未知数量的密钥对,可能是数十亿到数万亿个,而且在使用之前无法创建它们。

由于这将占用无限的空间和时间来生成,所以我需要在运行时生成它们。

然而,我还需要对于给定的输入,生成相同的密钥对。这意味着我需要根据输入确定性地重新创建RSA密钥。

我正在使用Go语言,通常可以使用以下代码创建密钥对:

k, err := rsa.GenerateKey(rand.Reader, 2048)

当然,问题在于rand.Reader是由crypto/rand提供的,因此无法对其进行种子初始化。

我认为可以通过提供自己的读取器实现来实现我的目标。我查看了GenerateKey的源代码,并注意到它正在寻找素数,因此我实现了自己的读取器,以便我可以控制返回的“随机”素数,从而在需要时生成相同的密钥:

type Reader struct {
	data   []byte
	sum    int
	primes []int
}

func NewReader(toRead string) *Reader {
	primes := sieveOfEratosthenes(10_000_000)
	return &Reader{[]byte(toRead), 0, primes}
}

func (r *Reader) Read(p []byte) (n int, err error) {
	r.sum = r.sum + 1

	if r.sum >= 100_000 {
		return r.primes[rand.Intn(len(r.primes))], io.EOF
	}

	return r.primes[rand.Intn(len(r.primes))], nil
}

func sieveOfEratosthenes(N int) (primes []int) {
	b := make([]bool, N)
	for i := 2; i < N; i++ {
		if b[i] == true {
			continue
		}
		primes = append(primes, i)
		for k := i * i; k < N; k += i {
			b[k] = true
		}
	}
	return
}

然后可以这样调用生成密钥:

k, err := rsa.GenerateKey(NewReader(""), 2048)

这段代码可以编译通过,但在运行时由于空指针而崩溃。我对Go语言相当熟悉,但对于这个RSA的实现超出了我的理解范围。我正在寻找更好的方法来实现这一目标,或者也许是让我的读取器正常工作的方法。

请注意,我在这里的唯一硬性要求是能够为给定的输入生成相同的密钥,并且使用rsa.GenerateKey或与之兼容的替代方法。输入可以是任何内容,只要输出的密钥相同即可。

这是一个演示我目前进展的Go Playground链接:https://go.dev/play/p/jd1nAoPR5aD

英文:

For reasons probably best left unanswered I need to generate infinite RSA public/private keys. Note this is not being used for anything highly secure, so please don't tell me not to do this, yes I know its not ideal. By "infinite" I mean I need a unknown number of them think billions to trillions, and creating them before being used is not possible.

Since this would consume infinite space and take infinite time to generate I need to do it at runtime.

However I also need to have for a given input the same key pair. This means I need to deterministically recreate the RSA key given the input.

I am using Go and normally you create keys using the following,

k, err := rsa.GenerateKey(rand.Reader, 2048)

Of course the catch is that rand.Reader is supplied by crypto/rand and as such there is no way to seed this.

I thought that it would be possible to provide my own reader implementation to achieve my goal. I looked through the source of GenerateKey and noted that it is looking for prime numbers, so I implemented my own reader, such that I could control the "random" primes returned, allowing me to generate the same key when required,

type Reader struct {
	data   []byte
	sum    int
	primes []int
}

func NewReader(toRead string) *Reader {
	primes := sieveOfEratosthenes(10_000_000)
	return &amp;Reader{[]byte(toRead), 0, primes}
}

func (r *Reader) Read(p []byte) (n int, err error) {
	r.sum = r.sum + 1

	if r.sum &gt;= 100_000 {
		return r.primes[rand.Intn(len(r.primes))], io.EOF
	}

	return r.primes[rand.Intn(len(r.primes))], nil
}

func sieveOfEratosthenes(N int) (primes []int) {
	b := make([]bool, N)
	for i := 2; i &lt; N; i++ {
		if b[i] == true {
			continue
		}
		primes = append(primes, i)
		for k := i * i; k &lt; N; k += i {
			b[k] = true
		}
	}
	return
}

I can then call into generate key like so

k, err := rsa.GenerateKey(NewReader(&quot;&quot;), 2048)

Which compiles, but crashes at runtime due to nil pointers. I am fairly comfortable with Go, but the implementation of RSA for this is beyond my understanding. Looking for either a better way to achieve this, or perhaps what I need to do to get my reader working.

Note, the only hard requirement's I have here are being able to generate the same key for a given input, and use rsa.GenerateKey or a drop in compatible replacement. The input can be anything really, so long as I get the same key as the output.

Here is a Go playground link demonstrating where I am currently https://go.dev/play/p/jd1nAoPR5aD

答案1

得分: 3

Read方法没有按预期工作。它没有用随机字节填充输入的p字节切片。如果你查看crypto/rand.Read方法在Unix系统上的实现,它会将输入的字节切片传递给另一个读取器。所以基本上你需要用随机数填充字节切片。例如:

func (r *Reader) Read(p []byte) (n int, err error) {
        i := 0
        b := p

        for i < len(b) {
                if len(b) < 4 {
                        b[0] = 7
                        b = b[1:]
                } else {
                        binary.LittleEndian.PutUint32(b, uint32(rand.Intn(len(r.primes))))
                        b = b[4:]
                }
        }

        return len(p), nil
}

这是链接到playground。


更新

正如Erwin的答案中提到的,有一个名为MaybeReadRand的函数,有50%的几率从rand读取器中读取1个字节,使得该函数不确定性。但你可以通过在Read方法中添加if语句来绕过这个问题:如果输入切片的长度为1,则忽略一切并返回。否则,用质数填充输入切片:

func (r *Reader) Read(p []byte) (n int, err error) {
	i := 0
	b := p

	if len(p) == 1 {
		println("maybeReadRand")
		return 1, nil
	}

	for i < len(b) {
		if len(b) < 4 {
			b[0] = 7
			b = b[1:]
		} else {
			binary.LittleEndian.PutUint32(b, uint32(r.primes[r.i]))
			r.i++
			b = b[4:]
		}
	}

	return len(p), nil
}

在这个代码片段中,我创建了两个密钥,它们都是相等的。

英文:

The Read method is not doing what is expected. It does not fill the input p byte slice with random bytes. If you look at the implementation for Unix of crypto/rand.Read method, it passes the input byte slice into another reader. So basically you what you need to fill the byte slice with random numbers. For example:

func (r *Reader) Read(p []byte) (n int, err error) {
        i := 0
        b := p

        for i &lt; len(b) {
                if len(b) &lt; 4 {
                        b[0] = 7
                        b = b[1:]
                } else {
                        binary.LittleEndian.PutUint32(b, uint32(rand.Intn(len(r.primes))))
                        b = b[4:]
                }
        }

        return len(p), nil
}

Here is the link to playground.


UPD

As mentioned in the answer by Erwin, there is a function called MaybeReadRand that with 50% chance read 1 byte from rand reader to make this function nondeterministic. But you can get around by adding if statement in Read method: if length of the input slice is 1, then ignore everything and return. Otherwise, feed the input slice with prime numbers:

func (r *Reader) Read(p []byte) (n int, err error) {
	i := 0
	b := p

	if len(p) == 1 {
		println(&quot;maybeReadRand&quot;)
		return 1, nil
	}

	for i &lt; len(b) {
		if len(b) &lt; 4 {
			b[0] = 7
			b = b[1:]
		} else {
			binary.LittleEndian.PutUint32(b, uint32(r.primes[r.i]))
			r.i++
			b = b[4:]
		}
	}

	return len(p), nil
}

In this snippet I am creating 2 keys, and both of them are equal.

答案2

得分: 0

有人在这里发布了一个答案,我正要把它授予他,因为它给了我解决问题所需的最后一部分。

简而言之,我没有正确阅读读取操作的方式,我需要填充输入切片。将读取操作更改为以下内容:


func (r *Reader) Read(p []byte) (n int, err error) {
    i := 0
    b := p

    for i < len(b) {
        if len(b) < 4 {
            b[0] = 7
            b = b[1:]
        } else {
            binary.LittleEndian.PutUint32(b, uint32(rand.Intn(len(r.primes))))
            b = b[4:]
        }
    }

    return len(p), nil
}

解决了崩溃问题,而且由于现在使用了math/rand,我可以为质数列表设置种子,以确保它是确定性的。一切都解决了,不需要任何第三方的东西。太棒了。

对于那位发布这个答案的人,如果你再次发布,我将把最佳答案授予你。

英文:

Someone posted an answer here, and I was about to award it to them as it got me the final piece I needed to resolve the question.

In short, I didn't read how the read was being called properly and I needed to populate the input slice. Changing the read to the below,


func (r *Reader) Read(p []byte) (n int, err error) {
	i := 0
	b := p

	for i &lt; len(b) {
		if len(b) &lt; 4 {
			b[0] = 7
			b = b[1:]
		} else {
			binary.LittleEndian.PutUint32(b, uint32(rand.Intn(len(r.primes))))
			b = b[4:]
		}
	}

	return len(p), nil
}

resolves the crashes, and since it is now using math/rand I can set the seed for the list of primes allowing me to ensure it is deterministic. Everything solved without needing anything from 3rd parties. Excellent.

To whoever posted that, if you post again I will award you the accepted answer.

答案3

得分: 0

你做出了一个奇怪的假设。rand.Reader返回的是完全随机的数据,而不是素数。rsa.GenerateKey在内部使用这些数据来生成非常可能是素数的数字,但它并不依赖于读取器中的任何特定模式。事实上,它更喜欢没有模式(而“是素数”显然是一种模式)。

当然,你可以自由地向rsa.GenerateKey提供rand.Reader或者莎士比亚作品的摘录,但rsa.GenerateKey不会利用这一点。

因此,运行埃拉托斯特尼筛法是浪费时间(真的)。

如果rsa.GenerateKey允许你确定性地生成密钥,那么你可以直接使用使用种子的伪随机数生成器直接生成字节。

但事实并非如此。看一下源代码,rsa.go的第287行

func GenerateMultiPrimeKey(random io.Reader, nprimes int, bits int) (*PrivateKey, error) {
	randutil.MaybeReadByte(random) // <------- 这一行很重要

函数GenerateMultiPrimeKey是由GenerateKey直接调用的。

那么randutil.MaybeReadByte是做什么的呢?文档很清楚:

> MaybeReadByte以约50%的概率从r中读取一个字节。这用于确保调用者不依赖于不保证的行为,例如假设rsa.GenerateKey在给定的随机流中是确定性的。

简而言之,它确保不可能确定性地生成特定的RSA密钥

如果你仍然需要这样做,你需要将GenerateMultiPrimeKey的源代码复制到你自己的库中,并删除randutil.MaybeReadByte这一行。或者你需要重新思考你的要求,比你目前的思维层次更高,并看看是否有其他实现你要求的不同方式(可以参考“XY问题”)。

英文:

You have made a strange assumption. rand.Reader returns completely random data. It doesn't return prime numbers. rsa.GenerateKey uses the data internally to come up with -very probable- prime numbers. But it doesn't depend on any particular pattern in the reader. In fact, it prefers there to be no pattern (and "being a prime number" is definitely a pattern)

Sure, you're free to feed rsa.GenerateKeym, or excerpts from Shakespeare's works, but rsa.GenerateKey is not going to make use of the fact that it is.

So running a sieve of Eratosthenes is a waste of time (literally).

If rsa.GenerateKey allowed you to deterministically generate keys, then you could directly generate bytes using a pseudo-random number generator that uses a seed, directly.

But, that's not the case. Look at the source code, line 287 of rsa.go:

func GenerateMultiPrimeKey(random io.Reader, nprimes int, bits int) (*PrivateKey, error) {
	randutil.MaybeReadByte(random) // &lt;------- this line here is the important one

The function GenerateMultiPrimeKey is directly invoked by GenerateKey.

Now what does randutil.MaybeReadByte do? The documentation is clear:

> MaybeReadByte reads a single byte from r with ~50% probability. This
> is used to ensure that callers do not depend on non-guaranteed
> behaviour, e.g. assuming that rsa.GenerateKey is deterministic w.r.t.
> a given random stream.

In short, it ensures that it is not possible to deterministically generate a particular RSA key.

If you still needed to do that, you would need to copy the source code of GenerateMultiPrimeKey to your own library, and remove the line randutil.MaybeReadByte. Or you'd need to rethink what your requirement really is, a level higher than you're currently thinking, and see if there is a different way to achieve your requirement. (Have a look at the "XY problem")

答案4

得分: 0

如果你需要一个确定性随机的io.Reader,可以使用标准库来实现。以下是示例代码:

import (
	"io"
	"math/rand"
)

func main() {
	const seed = 1
	var myRand = rand.New(rand.NewSource(seed))
}

rand.NewSource函数创建一个确定性的随机种子源,rand.New函数将其封装为更具功能的*Rand对象,该对象包含了io.Reader的实现。

你也可以通过调用myRand.Seed(seed)来重置*Rand对象。

英文:

If you need a deterministic-random io.Reader, here's how to do it with the standard library

import (
	&quot;io&quot;
	&quot;math/rand&quot;
)

func main() {
	const seed = 1
	var myRand = rand.New(rand.NewSource(seed))
}

rand.NewSource creates a deterministic seeded random source, and rand.New wraps it to create a more featureful *Rand object, which includes an implementation of io.Reader.

You can also reset the *Rand by calling myRand.Seed(seed).

huangapple
  • 本文由 发表于 2022年12月21日 07:40:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/74869997.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定