microsoft/go-crypto-winnative

Do you have a function to return an rsa.PrivateKey from a CERT_CONTEXT?

Closed this issue · 4 comments

@qmuntal I've found some Go code to unmarshal an RSA public key, but I need similar code to unmarshal the output of NCryptExportKey to an rsa.PrivateKey instance. I am a novice in this area so some guidance would be appreciated!

I think I have adapted the code I found correctly.
I will look at what you are building here and see if I can replace any of my own cert native interop calls with use of your module.
My draft PR for adding Always Encrypted support to the go-mssqldb driver is at microsoft/go-mssqldb#116

func unmarshalRSA(buf []byte) (*rsa.PrivateKey, error) {
	// BCRYPT_RSA_BLOB -- https://learn.microsoft.com/windows/win32/api/bcrypt/ns-bcrypt-bcrypt_rsakey_blob
	header := struct {
		Magic         uint32
		BitLength     uint32
		PublicExpSize uint32
		ModulusSize   uint32
		Prime1Size    uint32
		Prime2Size    uint32
	}{}

	r := bytes.NewReader(buf)
	if err := binary.Read(r, binary.LittleEndian, &header); err != nil {
		return nil, err
	}

	if header.Magic != 0x33415352 { // "RSA3" BCRYPT_RSAFULLPRIVATE_MAGIC
		return nil, fmt.Errorf("invalid header magic %x", header.Magic)
	}

	if header.PublicExpSize > 8 {
		return nil, fmt.Errorf("unsupported public exponent size (%d bits)", header.PublicExpSize*8)
	}

	// the exponent is in BigEndian format, so read the data into the right place in the buffer
	exp := make([]byte, 8)
	n, err := r.Read(exp[8-header.PublicExpSize:])

	if err != nil {
		return nil, fmt.Errorf("failed to read public exponent %w", err)
	}

	if n != int(header.PublicExpSize) {
		return nil, fmt.Errorf("failed to read correct public exponent size, read %d expected %d", n, int(header.PublicExpSize))
	}

	mod := make([]byte, header.ModulusSize)
	n, err = r.Read(mod)

	if err != nil {
		return nil, fmt.Errorf("failed to read modulus %w", err)
	}

	if n != int(header.ModulusSize) {
		return nil, fmt.Errorf("failed to read correct modulus size, read %d expected %d", n, int(header.ModulusSize))
	}

	pk := &rsa.PrivateKey{
		PublicKey: rsa.PublicKey{
			N: new(big.Int).SetBytes(mod),
			E: int(binary.BigEndian.Uint64(exp)),
		},
		D:      new(big.Int),
		Primes: make([]*big.Int, 2),
	}
	prime := make([]byte, header.Prime1Size)
	n, err = r.Read(prime)
	if err != nil {
		return nil, fmt.Errorf("failed to read prime1 %w", err)
	}
	pk.Primes[0] = new(big.Int).SetBytes(prime)
	prime = make([]byte, header.Prime2Size)
	n, err = r.Read(prime)
	if err != nil {
		return nil, fmt.Errorf("failed to read prime2 %w", err)
	}
	pk.Primes[1] = new(big.Int).SetBytes(prime)
	expBytes := make([]byte, 2*header.Prime1Size+header.Prime2Size+header.ModulusSize)
	n, err = r.Read(expBytes)
	if err != nil {
		return nil, fmt.Errorf("Unable to read PrivateExponent %w", err)
	}
	pk.D = new(big.Int).SetBytes(expBytes[2*header.Prime1Size+header.Prime2Size:])
	return pk, nil
}

@qmuntal I've found some Go code to unmarshal an RSA public key, but I need similar code to unmarshal the output of NCryptExportKey to an rsa.PrivateKey instance.

We are doing something similar in GeneratKeyRSA:

hdr, data, err := exportRSAKey(hkey, true)
if err != nil {
return bad(err)
}
if hdr.Magic != bcrypt.RSAFULLPRIVATE_MAGIC || hdr.BitLength != uint32(bits) {
return bad(errors.New("crypto/rsa: exported key is corrupted"))
}
consumeBigInt := func(size uint32) BigInt {
b := data[:size]
data = data[size:]
return b
}
E = consumeBigInt(hdr.PublicExpSize)
N = consumeBigInt(hdr.ModulusSize)
P = consumeBigInt(hdr.Prime1Size)
Q = consumeBigInt(hdr.Prime2Size)
Dp = consumeBigInt(hdr.Prime1Size)
Dq = consumeBigInt(hdr.Prime2Size)
Qinv = consumeBigInt(hdr.Prime1Size)
D = consumeBigInt(hdr.ModulusSize)
return

exportRSAKey returns the rsa blob header as a typed struct and the rest of the payload from where to take the key components. The consumeBigInt converts each segment of the blob into the corresponding big integer chunk, which can be converted into a big.Int using new(big.Int).SetBytes(b).

I think I have adapted the code I found correctly.

You code looks good, although using bytes.NewReader forces you to allocate more than necessary, why not directly slice the blob directly, as in consumeBigInt?.

Also, you are not setting the precomputed values, which are present in the blob between the second prime number and the private exponent (D).

thx I put a todo in my code to revisit it.

Long term - will there be a centralized push within the Microsoft org to consume a package like yours instead of writing home-grown Windows cert store interop code? The Go windows standard library stops short of full functionality.

Long term - will there be a centralized push within the Microsoft org to consume a package like yours instead of writing home-grown Windows cert store interop code? The Go windows standard library stops short of full functionality.

I'm not aware of any effort heading on that direction, and I doubt go-crypto-winnative will ever become a generic cert store interop. Would be nice, anyway.