C# - ssh-keygen으로 생성한 ecdsa 유형의 Public Key 파일 해석
지난 글에서 ssh-keygen으로 생성한 공개키 파일을 해석해 봤는데요,
C# - ssh-keygen으로 생성한 Public Key 파일 해석과 fingerprint 값(md5, sha256) 생성
; https://www.sysnet.pe.kr/2/0/13739
근래에는 RSA보다는 (키 길이도 짧아 연산 속도에 부담이 없는) Elliptic Curve 암호화 방식을 더 선호합니다.
Visual Studio - Cross Platform / "Authentication Type: Private Key"로 접속하는 방법
; https://www.sysnet.pe.kr/2/0/13733
테스트를 위해 하나 만들면,
// 여기서는 테스트를 쉽게 하기 위해 암호를 생략하도록 (-N 옵션의 빈 문자열) 지정했습니다. (현업에서는 암호 사용을 권장합니다.)
C:\temp> ssh-keygen -N "" -m pem -t ecdsa -f test_ecdsa
...[생략]...
C:\temp> type test_ecdsa
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIEYJmh+sBdW+5ZjpqsHFJRiHwXuJ3IJFUhyEndpG1BVdoAoGCCqGSM49
AwEHoUQDQgAEtLUV5PkGQHC1pIDe35fjtJn6zYWC/63FE6/vFwTKS8knZMf17x5K
jAW98bl1T6nJMiPGYPPr/1dfyL09AQUvnA==
-----END EC PRIVATE KEY-----
C:\temp> type test_ecdsa.pub
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLS1FeT5BkBwtaSA3t+X47SZ+s2Fgv+txROv7xcEykvJJ2TH9e8eSowFvfG5dU+pyTIjxmDz6/9XX8i9PQEFL5w= kevin@THE12
보는 바와 같이 이런 경우 공개키가 "ssh-rsa"가 아닌 "ecdsa-sha2-nistp256"로 시작합니다. 그렇긴 한데,
구조 자체는 이전의 해석 방식을 그대로 따르는데요, 단지 필드의 의미가 좀 변경됩니다.
이에 대한 정보를 다음의 경로에서 찾아냈는데요,
SSH public key: C# parsing library
; https://formats.kaitai.io/ssh_public_key/csharp.html
SSH public key: format specification
; https://formats.kaitai.io/ssh_public_key/
openssh-portable/sshkey.c (위의 글을 작성한 이가 참조했다는 openssh 소스코드)
; https://github.com/openssh/openssh-portable/blob/b4d4eda6/sshkey.c#L1970
대충 다음과 같이 해석할 수 있습니다.
string "algorithm_name"
string "curve_name"
int(byte[]) Q[length(Q.X) + length(Q.Y) + 1]
실제로 아래는 지난 글의 코드를 조금 수정해 "ecdsa-sha2-nistp256"로 시작하는 공개키를 읽도록 한 것입니다.
using System.Security.Cryptography;
using System.Text;
namespace ConsoleApp1;
internal class Program
{
static void Main(string[] args)
{
string text = File.ReadAllText("test_ecdsa.pub");
{
string[] sshKeys = text.Split(' ');
if (sshKeys.Length != 3)
{
throw new Exception("Invalid SSH Key");
}
string keyType = sshKeys[0];
string comment = sshKeys[2];
byte[] bytesEncoded = Convert.FromBase64String(sshKeys[1]);
if (keyType != "ecdsa-sha2-nistp256")
{
throw new NotSupportedException($"Unsupported key type: {keyType}");
}
(string algorithmName, string curveName, byte[] ecKey) = DecodeSSHPublicKey(bytesEncoded);
}
}
private static (string algorithmName, string curveName, byte[] eccKey) DecodeSSHPublicKey(byte[] bytesEncoded)
{
string algorithmName;
string curveName;
byte[] eccKey;
using (var stream = new MemoryStream(bytesEncoded))
using (var reader = new BinaryReader(stream))
{
int algorithmLength = reader.ReadInt32BE();
algorithmName = Encoding.ASCII.GetString(reader.ReadBytes(algorithmLength));
int curveNameLength = reader.ReadInt32BE();
curveName = Encoding.ASCII.GetString(reader.ReadBytes(curveNameLength));
int publicKeyLength = reader.ReadInt32BE();
eccKey = reader.ReadBytes(publicKeyLength);
}
return (algorithmName, curveName, eccKey);
}
}
public static class BinaryReaderExtension
{
public static Int32 ReadInt32BE(this BinaryReader reader)
{
byte[] bytes = new byte[4];
bytes[3] = (byte)reader.ReadByte();
bytes[2] = (byte)reader.ReadByte();
bytes[1] = (byte)reader.ReadByte();
bytes[0] = (byte)reader.ReadByte();
return BitConverter.ToInt32(bytes);
}
}
자, 이렇게 해서 Elliptic Curve 방식의 공개키를 구했는데요, 이제 이것을 관련 타입에서 어떻게 초기화할 수 있을까요? 닷넷에서 Elliptic Curve 키는 ECParameters를 통해 초기화하는데요,
ECParameters ecparams = new ECParameters
{
Curve = ECCurve.NamedCurves.nistP256,
Q = new ECPoint
{
X = [...x_coord_byte_buffer...],
Y = [...y_coord_byte_buffer...],
}
};
위와 같이 Q.X, Q.Y에만 초기화를 하면, 마치 RSA에서 공개키를 초기화하는 것과 같습니다. 그런데, 여기서 재미있는 건 "ecdsa-sha2-nistp256"로 구한 ecKey 버퍼의 크기가 65바이트라는 점입니다.
보통 Q.X, Q.Y가 동일한 크기를 가지므로 정확히 2로 나눠떨어져야 하는데, 생뚱맞게 1바이트가 더 큽니다. 이것의 원인은, Q.X, Q.Y를 합한 바이트 배열을 압축 유형으로 표현하는 것까지 지원하기 위해 선행 바이트에 그 여부를 표시하기 때문입니다.
압축 유형인 경우에는 0x02, 0x03이 되는데요, 이것은 다음의 공식으로 구해집니다.
// Algorithm for elliptic curve point compression
// AlexHag/ECC-DSA-DH
byte prefix = (byte)((yCoord[yCoord.Length - 1] & 1) == 0 ? 0x02: 0x03);
반면, Uncompressed 유형인 경우에는 고정값 0x04가 선행됩니다. (여기서는 compressed 유형은 생략할 텐데,
Algorithm for elliptic curve point compression 글을 참고하시면 decompressed 하는 방법을 알 수 있으니 참고하시고.)
따라서, ecKey 바이트 배열을 이용해 ECParameters를 다음과 같이 초기화할 수 있고,
(string algorithmName, string curveName, byte[] ecKey) = DecodeSSHPublicKey(bytesEncoded);
int keyLengthInBytes= (ecKey.Length - 1) / 2;
int keyLength = keyLengthInBytes * 8;
{
if (ecKey[0] != 0x04)
{
throw new NotSupportedException("Only uncompressed keys are supported");
}
byte[] xCoord = ecKey.Skip(1).Take(keyLengthInBytes).ToArray();
byte[] yCoord = ecKey.Skip(keyLengthInBytes + 1).ToArray();
ECParameters ecparams = new ECParameters
{
Curve = ECCurve.CreateFromFriendlyName(curveName),
Q = new ECPoint
{
X = xCoord,
Y = yCoord,
}
};
}
이후 키가 정상적인지 테스트 여부를 ECDiffieHellman.ImportParameters 등의 메서드를 사용해 확인할 수 있습니다.
ECDiffieHellman ecdh = ECDiffieHellman.Create(ecParam); // ecdh.ImportParameters(ecParam);
// 또는 서명을 검증하기 위해,
ECDsa ecdsa = ECDsa.Create(ecParam); // ecdsa.ImportParameters(ecParam);
// bool verified = ecdsa.VerifyData(plainTextBuffer, signedBuffer, HashAlgorithmName.SHA256);
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]