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 (

 * 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

    return g, nil

func (g *Gen) next() {
    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


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy