Reputation: 3088
I have some server that provides access to data by cryptographic API. I need to write client in C# that can create requests to server and read responses from.
For doing it I need to create public and private RSA keys and convert them to bytes array. I had the working example in java:
java.security.KeyPairjava.security.KeyPair keypair = keyGen.genKeyPair();
byte[] pubKeyBytes = keypair.getPublic().getEncoded();
byte[] privKeyBytes = keypair.getPrivate().getEncoded();
I tried to do the same with C# in .NET:
RSACryptoServiceProvider keyPair = new RSACryptoServiceProvider(2048);
var publicKey = keyPair.ExportParameters(false);
var privateKey = keyPair.ExportParameters(true);
And I don't know how to do it. I have D, Dp, DQ, InverseQ, Modulus, Exponent as properties of publicKey and privateKey, but in java sample those key looks like single united keys. Which one of D, Dp, DQ, InverseQ, Modulus, Exponent I should use for my task? What the way to do the same as in java example, but in C#?
Upvotes: 1
Views: 1446
Reputation: 33256
According to https://docs.oracle.com/javase/7/docs/api/java/security/Key.html#getFormat() the default for a public key encoding is X.509 SubjectPublicKeyInfo and for a private key is PKCS#8 PrivateKeyInfo.
There are a number of questions (like Correctly create RSACryptoServiceProvider from public key) on creating RSAParameters from a SubjectPublicKeyInfo, but not as many for the reverse.
If you're creating your key via RSACryptoServiceProvider then the new key will always have an exponent value of 0x010001, which means the only variable sized piece of data that you have to contend with is the modulus value. The reason that this is important is that a SubjectPublicKeyInfo is (almost always) encoded in DER (defined by ITU-T X.690), which uses length-prefixed values. The ASN.1 (ITU-T X.680) is defined in RFC 5280 as
SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING }
The encoded value for the AlgorithmIdentifier for RSA is
30 0D 06 09 2A 86 48 86 F7 0D 01 01 01 05 00
(aka SEQUENCE(OID("1.2.840.113549.1.1.1"), NULL))
The value for subjectPublicKey
depends on the algorithm. For RSA it's RSAPublicKey
, defined in RFC 3447 as
RSAPublicKey ::= SEQUENCE {
modulus INTEGER, -- n
publicExponent INTEGER -- e }
The encoding for an INTEGER is 02 (then the length) then the signed big-endian value. So, assuming that your Exponent value is 01 00 01
the encoded value is 02 03 01 00 01
. The modulus length depends on the size of your key.
int modulusBytes = parameters.Modulus.Length;
if (parameters.Modulus[0] >= 0x80)
modulusBytes++;
RSACryptoServiceProvider should always create keys that need the extra byte, but technically keys could exist which don't. The reason we need it is that parameters.Modulus is an UNsigned big-endian encoding, and if the high bit is set then we would be encoding a negative number into the RSAPublicKey. We fix that by inserting an 00 byte to keep the sign bit clear.
The length bytes for the modulus are slightly tricky. If the modulus is representable in 127 bytes or fewer (RSA-1015 or smaller) then you just use one byte for that value. Otherwise you need the smallest number of bytes to report the number, plus one. That extra byte (the first one, actually) says how many bytes the length is. So 128-255 is one byte, 81
. 256-65535 is two, so 82
.
We then need to wrap that into a BIT STRING value, which is easy (if we ignore the hard parts, since they're not relevant here). And then wrap everything else up in a SEQUENCE, which is easy.
Quick and dirty, only works on a 2048-bit key with exponent=0x010001:
private static byte[] s_prefix =
{
0x30, 0x82, 0x01, 0x22,
0x30, 0x0D,
0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01,
0x05, 0x00,
0x03, 0x82, 0x01, 0x0F,
0x00,
0x30, 0x82, 0x01, 0x0A,
0x02, 0x82, 0x01, 0x01, 0x00
};
private static byte[] s_suffix = { 0x02, 0x03, 0x01, 0x00, 0x01 };
private static byte[] MakeSubjectPublicInfoEasy2048(RSA rsa)
{
if (rsa.KeySize != 2048)
throw new ArgumentException(nameof(rsa));
RSAParameters rsaParameters = rsa.ExportParameters(false);
if (Convert.ToBase64String(rsaParameters.Exponent) != "AQAB")
{
throw new ArgumentException(nameof(rsa));
}
return s_prefix.Concat(rsaParameters.Modulus).Concat(s_suffix).ToArray();
}
Or, for a general-purpose response (that creates a lot of temporary byte[]s):
private static byte[] MakeTagLengthValue(byte tag, byte[] value, int index = 0, int length = -1)
{
if (length == -1)
{
length = value.Length - index;
}
byte[] data;
if (length < 0x80)
{
data = new byte[length + 2];
data[1] = (byte)length;
}
else if (length <= 0xFF)
{
data = new byte[length + 3];
data[1] = 0x81;
data[2] = (byte)length;
}
else if (length <= 0xFFFF)
{
data = new byte[length + 4];
data[1] = 0x82;
data[2] = (byte)(length >> 8);
data[3] = unchecked((byte)length);
}
else
{
throw new InvalidOperationException("Continue the pattern");
}
data[0] = tag;
int dataOffset = data.Length - length;
Buffer.BlockCopy(value, index, data, dataOffset, length);
return data;
}
private static byte[] MakeInteger(byte[] unsignedBigEndianValue)
{
if (unsignedBigEndianValue[0] >= 0x80)
{
byte[] tmp = new byte[unsignedBigEndianValue.Length + 1];
Buffer.BlockCopy(unsignedBigEndianValue, 0, tmp, 1, unsignedBigEndianValue.Length);
return MakeTagLengthValue(0x02, tmp);
}
for (int i = 0; i < unsignedBigEndianValue.Length; i++)
{
if (unsignedBigEndianValue[i] != 0)
{
if (unsignedBigEndianValue[i] >= 0x80)
{
i--;
}
return MakeTagLengthValue(0x02, unsignedBigEndianValue, i);
}
}
// All bytes were 0, encode 0.
return MakeTagLengthValue(0x02, unsignedBigEndianValue, 0, 1);
}
private static byte[] MakeSequence(params byte[][] data)
{
return MakeTagLengthValue(0x30, data.SelectMany(a => a).ToArray());
}
private static byte[] MakeBitString(byte[] data)
{
byte[] tmp = new byte[data.Length + 1];
// Insert a 0x00 byte for the unused bit count value
Buffer.BlockCopy(data, 0, tmp, 1, data.Length);
return MakeTagLengthValue(0x03, tmp);
}
private static byte[] s_rsaAlgorithmId = new byte[] { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
private static byte[] ExportSubjectPublicKeyInfo(RSA rsa)
{
RSAParameters parameters = rsa.ExportParameters(false);
return MakeSequence(
s_rsaAlgorithmId,
MakeBitString(
MakeSequence(
MakeInteger(parameters.Modulus),
MakeInteger(parameters.Exponent))));
}
You shouldn't really need the encoded private key. But, if you really do, you need the general-purpose approach because there's a lot of room for variability in the private key data.
PrivateKeyInfo
is defined in RFC 5208 as
PrivateKeyInfo ::= SEQUENCE {
version Version,
privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
privateKey PrivateKey,
attributes [0] IMPLICIT Attributes OPTIONAL }
Version ::= INTEGER
PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
PrivateKey ::= OCTET STRING
Attributes ::= SET OF Attribute
It also says the current version number is 0.
The octet string of the private key is defined by the algorithm. For RSA we see in RFC 3447, along with RSAPublicKey
:
RSAPrivateKey ::= SEQUENCE {
version Version,
modulus INTEGER, -- n
publicExponent INTEGER, -- e
privateExponent INTEGER, -- d
prime1 INTEGER, -- p
prime2 INTEGER, -- q
exponent1 INTEGER, -- d mod (p-1)
exponent2 INTEGER, -- d mod (q-1)
coefficient INTEGER, -- (inverse of q) mod p
otherPrimeInfos OtherPrimeInfos OPTIONAL }
Ignore otherPrimeInfos
. It doesn't, and shouldn't ever, apply. Therefore the version number to use is 0.
Taking utility methods already defined, we get the rest by
private static byte[] MakeOctetString(byte[] data)
{
return MakeTagLengthValue(0x04, data);
}
private static byte[] s_integerZero = new byte[] { 0x02, 0x01, 0x00 };
private static byte[] ExportPrivateKeyInfo(RSA rsa)
{
RSAParameters parameters = rsa.ExportParameters(true);
return MakeSequence(
s_integerZero,
s_rsaAlgorithmId,
MakeOctetString(
MakeSequence(
s_integerZero,
MakeInteger(parameters.Modulus),
MakeInteger(parameters.Exponent),
MakeInteger(parameters.D),
MakeInteger(parameters.P),
MakeInteger(parameters.Q),
MakeInteger(parameters.DP),
MakeInteger(parameters.DQ),
MakeInteger(parameters.InverseQ))));
}
Making all of this easier is on the feature roadmap for .NET Core (https://github.com/dotnet/corefx/issues/20414 - doesn't say export, but where there's an import there's usually an export :))
Save your output to a file and you can check it with openssl rsa -inform der -pubin -text -in pub.key
and openssl rsa -inform der -text -in priv.key
Upvotes: 2
Reputation: 2889
You need to use the ExportCspBlob
method:
RSACryptoServiceProvider keyPair = new RSACryptoServiceProvider(2048);
var publicKey = keyPair.ExportCspBlob(false);
var privateKey = keyPair.ExportCspBlob(true);
ExportParameters
exports the specific parameters from which the keys themselves can be calculated. For more information about those parameters, see the wiki article.
Upvotes: 0