I am creating an internal application that will be used to generate and manage self-signed certificates and certificate authorities. Its primary use will be for generating certificates used in SSL decryption by my clients' firewalls.
My goal is to have a deterministic RNG that could be fed into either RSA key generation (rsa.GenerateKey
) or Certificate generation (x509.CreateCertificate
) which both accept io.Reader
interfaces for their source of randomness. I want to be able to recover a private key (or even a certificate) using a 256-bit mnemonic.
The initial entropy comes from Bip39-compliant CSPRNG (whatever system random device is available) which creates the mnemonic as well as the 512-bit PBKDF2-SHA512 seed (which is split into 2 256-bit keys, one used for RSA key, one for cert).
Below is a simple implementation of a ChaCha20-based cipher. My question is this:
Since this will be SOLELY used for generating the private key and certificate, and not in ANY type of ongoing communication or other data streams, and I want only the mnemonic to be used for generation and recovery, I am using both a static nonce (12 \x00
bytes) and a zeroized source buffer. If I do not zeroize the buffer, it is still perfectly deterministic of course, but the output values change based on the buffer size, which I don't see as desirable.
Given that the plaintext and nonce is known (all \x00
), is this concerning for security?
Thank you!
package rng
import (
"github.com/sirupsen/logrus"
"golang.org/x/crypto/chacha20"
)
/*
* Simple seedable RNG implementation using ChaCha20 as a backend
* Performance is far from optimal as the buffer is zeroed out before each iteration
* This is to guarantee that byte generation is independent of the buffer size
*/
type Gen struct {
buffer []byte // stores the ChaCha output buffer
i int // index of where data reading should start in the buffer
cipher *chacha20.Cipher // underlying ChaCha cipher
}
func NewGenerator(key, nonce []byte, bufsize int) (*Gen, error) {
// bounds restriction
key = key[:chacha20.KeySize]
nonce = nonce[:chacha20.NonceSize]
// create the underlying chacha20 cipher instance
cipher, err := chacha20.NewUnauthenticatedCipher(key, nonce)
if err != nil {
logrus.Error("Could not create cipher: %v", err)
return nil, err
}
// create the generator with the desired buffer size
g := &Gen{
buffer: make([]byte, bufsize),
i: 0,
cipher: cipher,
}
// initialize the generator
g.next()
return g, nil
}
func (g *Gen) next() {
zeroize(g.buffer)
g.cipher.XORKeyStream(g.buffer, g.buffer)
g.i = 0
}
func (g *Gen) getBytes(n int, dst []byte) int {
max := cap(g.buffer) - g.i
if n >= max {
n = max
defer g.next()
} else {
defer func() { g.i += n }()
}
return copy(dst, g.buffer[g.i:g.i+n])
}
func (g *Gen) Read(dst []byte) (int, error) {
// total size of the destination buffer and a counter to keep track of the number of bytes
size := len(dst)
ctr := 0
for size > 0 {
// retrieve bytes from the buffer, it will automatically re-generate if needed
i := g.getBytes(size, dst[ctr:])
ctr += i
size -= i
}
return ctr, nil
}
func zeroize(buf []byte) int {
// set all values to 0 for the buffer
size := len(buf)
for i := 0; i < size; i++ {
buf[i] = 0
}
return size
}