0
$\begingroup$

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 Bip39 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
}
$\endgroup$
2
  • 2
    $\begingroup$ The best buffer is no buffer. You can just zeroize the destination and then use XORStream on that. Use a local single byte buffer for next(), you may need to set that to zero after use. Beware that if anything changes in the key pair generation then your scheme will fail; this is not limited to the DRBG / bytewise CSPRNG that is used. $\endgroup$
    – Maarten Bodewes
    Aug 2, 2022 at 10:45
  • $\begingroup$ @MaartenBodewes thank you for the advice! The fact that ANY minor change can affect the entire system is what made me want to go with a published standard like Bip39. The "best buffer is no buffer" was also a great recommendation. $\endgroup$
    – Goodies
    Aug 3, 2022 at 4:30

1 Answer 1

3
$\begingroup$

Given that the plaintext and nonce is known (all \x00), is this concerning for security?

TL;DR: no.

It's not a concern that plaintext and nonce are known. Neither is it a concern that the plaintext is constant. In theory, the fact that the nonce is a known constant could be a security concern, because it allows amortizing the cost of attack by key search across several instances of the problem with different keys. In such so-called multi-targets attack, the expected work for finding a key decreases with the number of keys targeted (at worse, linearly). The attack enumerate keys, runs the CSPRNG with that key an the common nounce/plaintext, does whatever the output is used for (such as generating a public key), and tests if the outcome is among the multiple targets. This final test has marginal cost, and has cost that grows marginally with the number of targets, hence the speedup.

But in the context, since the chacha20 key is 256-bit, and assuming it's near full entropy, even a billion keys will not eat 30 bits of security, thus there remains aplenty.

Disclaimer: I did not review the code: crypto-SE is not for code review, and I'm not fluent with that language anyway.

$\endgroup$
1
  • $\begingroup$ Thank you. The code was to provide context should anyone desire it, but I greatly appreciate your insight. I want to avoid the cardinal sin of "making my own crypto" while still making my own application which safely utilizes it. $\endgroup$
    – Goodies
    Aug 3, 2022 at 4:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.