microsoft / go-crypto-winnative

Go crypto backend for Windows using CNG
MIT License
28 stars 3 forks source link

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

Closed shueybubbles closed 1 year ago

shueybubbles commented 1 year ago

@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!

shueybubbles commented 1 year ago

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 https://github.com/microsoft/go-mssqldb/pull/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 commented 1 year ago

@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:

https://github.com/microsoft/go-crypto-winnative/blob/6eb98854418e078c1c45384e1263b5f967028868/cng/rsa.go#L61-L81

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).

shueybubbles commented 1 year ago

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.

qmuntal commented 1 year ago

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.