1

C#生成putty格式的ppk文件(支持passphrase) - 波斯马

 1 year ago
source link: https://www.cnblogs.com/bossma/p/16428919.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

C#生成putty格式的ppk文件(支持passphrase)

2022国家级护网行动即将开启,根据阿里云给出的安全建议,需要将登陆Linux的方式改为密钥对方式。我这里使用的远程工具是自己开发的,能够同时管理Windows和Linux,但是以前不支持密钥对的登陆方式,所以需要改造一下。

护网行动是什么?护网行动从2016年开始,是一场由公安部组织的网络安全攻防演练,目的是针对全国范围的真实网络目标为对象的实战攻防活动,旨在发现、暴露和解决安全问题,检验我国各大企事业单位、部属机关的网络安全防护水平和应急处置能力。护网行动每年举办一次,为期2-3周。

我使用的远程工具 RDManager:https://blog.bossma.cn/tools/new-version-of-rdmanager-replace-poderosa-with-putty/,这个工具访问Linux使用了putty,putty的密钥对登陆方式使用的是自有格式的ppk文件,但是阿里云上下载的是pem格式的密钥文件,所以需要将pem格式转换为ppk格式。

putty本身提供了一个工具,可以将其他格式的密钥文件转换为自有的ppk文件,这个工具的名字是puttygen。在linux上可以通过命令进行转换,在Windows上则必须使用GUI工具手动操作,这多有不便。我期望的是能通过编程的方式进行这个转换,这样只需要在RDManger中上传pem文件,就可以自动转换为putty的ppk格式的文件,不需要再去使用puttygen。

首先查了下有没有现成的轮子,经过多次寻找,在Github上找到了一个项目:pem2ppk (https://github.com/akira345/pem2ppk),这个项目看名字就知道很贴合我的需求,它的主要功能就是读取pem文件,然后输出为ppk文件。我最终的解决方案主体也是从此而来。不过这个程序有两个问题:

  • 1、不是所有的pem文件都能转换成功,网上也是有人说成功了,有人说不行。
  • 2、不支持对密钥进行加密,别人拿走了这个ppk文件就可以直接使用。puttygen是有这个功能的。

除此之外,很难再找到比较贴合需求的资料了。怎么办?其实这个Github项目的很大一部分代码来源于另一篇文章:https://antonymale.co.uk/generating-putty-key-files.html,作者提到可以去看putty的源码。

受此启发,我也可以去看putty的源码,然后将相关处理翻译为C#的实现,这样应该是可以解决问题的。

putty的源码官网上就可以下载到,不过我看的是一个几年前的版本:https://github.com/KasperDeng/putty,这个版本和新版本的主要逻辑都是一样的,搞懂C语言的若干函数和数据类型就很容易理解,而且旧版本更原始,没有那么多的抽象,反而更容易理解。

输出ppk内容不正确的问题

这个问题主要是由于填充(padding)使用不当造成的,pem2ppk项目在输出密钥的各个属性时都使用了前置填充,而putty并不是固定的都加了填充。

看putty的代码实现:https://github.com/KasperDeng/putty/blob/037a4ccb6e731fafc4cc77c0d16f80552fd69dce/putty-src/sshrsa.c#L654

    dlen = (bignum_bitcount(rsa->private_exponent) + 8) / 8;
    plen = (bignum_bitcount(rsa->p) + 8) / 8;
    qlen = (bignum_bitcount(rsa->q) + 8) / 8;
    ulen = (bignum_bitcount(rsa->iqmp) + 8) / 8;
    bloblen = 16 + dlen + plen + qlen + ulen;

这段代码是计算密钥的各个属性的值的字节数,然后用于初始化一个大的字节数组,将这些数据写进去。bignum_bitcount是计算值的比特位数,除以8就是得到字节数,为什么还要加8呢?这是因为C语言中除法的结果是向下取整的,比如数学计算结果是1.5,那么C语言中得到的就是1,为了不让任何一个比特丢失,所以这里加了一个8,预留好充足的空间。

再来看pem2ppk中的实现:https://github.com/akira345/pem2ppk/blob/d2baee08064953280984607d1e4ae1183127e5ad/PEM2PPK/PuttyKeyFileGenerator.cs#L24

private const int prefixSize = 4;
private const int paddedPrefixSize = prefixSize + 1;
byte[] publicBuffer = new byte[3 + keyType.Length + paddedPrefixSize + keyParameters.Exponent.Length +
                                           paddedPrefixSize + keyParameters.Modulus.Length + 1];

这里keyParameters.Exponent和keyParameters.Modulus是公钥的两个属性,可以看到前边加了一个固定的长度paddedPrefixSize,这个paddedPrefixSize=prefixSize + 1,这里边的1就对应putty中的+8逻辑。

不过固定+1是有问题的,可以想一下C#和C语言在处理这些属性值时的差别。

在putty中如果数据比特数不能被8整除,那么+8之后再整除就可以得到正确的字节数,否则就会少1个字节;如果数据能被8整除,那么+8就会多1个空的字节,这个多的字节就是padding了。所以能被8整除的时候才会有这个padding。

在C#中开始处理的时候就已经都是字节了,所以C#中不需要处理位数不能被8整除的问题,但是需要在能被8整除的时候增加一个空字节,C#中如何判断数据的位数能被8整除呢?可以认为数据首byte的最高位是1的时候,比特数就能被8整数,此时最小二进制数是10000000,比它小的数就可以被舍弃掉至少1位。10000000也就是128,因此凡是大于等于这个数的都是能被8整数的,也就是需要padding的。

所以可以这样判断是否需要增加padding:https://gist.github.com/bosima/ee6630d30b533c7d7b2743a849e9b9d0#file-puttykeyfilegenerator-cs-L190

	private static bool CheckIsNeddPadding(byte[] bytes)
	{
		return bytes[0] >= 128;
	}

	private static int GetPrefixSize(byte[] bytes)
	{
		return CheckIsNeddPadding(bytes) ? paddedPrefixSize : prefixSize;
	}

实现ppk加密

pem2ppk项目中没有对key进行加密的实现,网上也没有找到C#的源代码可以实现这个功能。但是这个功能很关键,在RDManager中所有的密码都是加密处理的,这样服务器账号落盘的时候安全性才能有比较好的保障,但是阿里云导出的pem是没有加密的,虽然puttygen也可以给pem加密,但是还不是不能将加密以编程的方式集成到RDManager中。

解决这个问题的方式还是搬运putty的实现方式,将C语言的实现转换为C#的实现。其中有两个关键的处理:一是要在计算Private-MAC的值时给私钥增加padding,二是使用AES256进行加密处理。至于putty为什么要这样处理,我没有研究,只是照搬过来。

主要看下AES256加密的处理,有些参数很关键:

byte[] passKey = new byte[40];
...
byte[] iv = new byte[16];
byte[] aesKey = new byte[32];
Buffer.BlockCopy(passKey, 0, aesKey, 0, 32);
using (RijndaelManaged rijalg = new RijndaelManaged())
{
	rijalg.BlockSize = 128;
	rijalg.KeySize = 256;
	rijalg.Padding = PaddingMode.None;
	rijalg.Mode = CipherMode.CBC;
	rijalg.Key = aesKey;
	rijalg.IV = iv;

	ICryptoTransform encryptor = rijalg.CreateEncryptor(rijalg.Key, rijalg.IV);
	return encryptor.TransformFinalBlock(bytes, 0, bytes.Length);
}
  • iv是长度为16的字节数组,里边都是默认值0。
  • aeskey是一个长度为32的字节数组,不过计算的时候准备的是长度为40的字节数组,需要截一下。
  • Padding需要设置为PaddingMode.None,默认的不是这个。

其它就没什么好说的了。

为了不那么单调,来一张RDManger的使用界面:

73642-20220630224509805-1112395507.png

以上就是本文的主要内容了。

完整代码在Github,欢迎访问:https://gist.github.com/bosima/ee6630d30b533c7d7b2743a849e9b9d0


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK