工作笔记 - ASN.1密钥结构和编码研究

概述

为什么想起要研究这个问题?是因为笔者在工作学习中,遇到了这样一种情况。就是在程序中,使用的非对称加密的密钥和签名验证操作时,可能需要不同的密钥,非常不方便。搞清楚了这个问题,就可以基于相同的密钥类型,在不同的使用方式之间进行转换和传输,这样可以大大简化系统开发和应用的配置管理。

一个完整的公钥加密的密码学操作,通常包括了密钥对的生成、密钥协商、内容加密、签名和验证等环节。Nodejs的Crypto库中,有很多相关的实现和操作,但感觉都比较杂乱,这可能是缺乏统一规划和功能模块逐步完善所造成的(当然相比其他的语言和平台,已经非常简单方便了)。在其中,笔者发现ECDH这个对象的实现和应用,是最简洁优雅的,可以非常方便的完成密钥对的生成,密钥协商等操作。而且对于一个可配置的环境,其密钥的保存和加载也是非常方便的。

但到了签名和验证操作环节,却发现了一些问题。就是ECDH虽然能够使用标准的ECC曲线和密钥对生成,但这个密钥,却不能直接用于签名和验证。首先ECDH主要是为密钥协商计算而设计,本身没有相关签名验证相关的操作实现。这并不是一个太大的问题,因为nodejs也提供了相关签名验证的相关对象和方法。主要的问题是,ECDH的密钥对,不能直接在签名验证操作中使用,这就给程序的配置、相关密钥的管理和维护带来了很多不便。

我们希望实现的效果是,既然都使用同一种类型的非对称加密算法,甚至选择了相同的ECC曲线类型,那相关操作,都应当可以围绕这同一套配置方式来展开。本文研究的目的,就是希望找到使用一种配置,可以支持加密解密和签名验证操作的方式。

初看起来,这个目的,和本文的标题是有一些出入的,但实际上,实现这种效果的核心,就是在对ASN1结构的理解和应用之上的。这也是笔者在相关的研究和实现之后,才突然理解到的,希望也能够将这个理解和认识,分享给感兴趣的读者。

ECDH的基本应用

我们先来看看,一般在Nodejs中,如何使用ECDH对象,来进行相关的非对称加密的操作。 为了方便讨论,本文中使用的ECC都是SECP384R1,无论是ECDH操作和签名加密操作。

在ECDH方面,简要的操作过程和代码示例如下:

js 复制代码
const CURVE = "secp384r1";
const echdA = createECDH(CURVE);
echdA.generateKeys();
let publicKeyA = echdA.getPublicKey();
let privateKeyA = echdA.getPrivateKey();

const echdB = createECDH(CURVE);
echdB.generateKeys();
let publicKeyB = echdB.getPublicKey();

let keyAB= echdA.computeSecret(publicKeyB);
console.log("KeyAB:", keyAB);

let keyBA= echdB.computeSecret(publicKeyA);
console.log("KeyBA:", keyBA);

const echdC = createECDH(CURVE);
echdC.setPrivateKey(privateKeyA);

let keyCB= echdC.computeSecret(publicKeyB);
console.log("KeyCA:", keyCB);

我们在代码中,可以看到,ECDH的使用非常简单方便:

  • 先使用createECDH函数和曲线名称作为参数,创建ecdh对象
  • 使用这个对象的generateKeys()方法,可以创建密钥对
  • 在实例对象创建和密钥对生成之后,就可以使用其getPublicKey和getPrivateKey方法,导出Buffer类型的公钥和私钥
  • 当然也可以指定导出的格式,如base64或者hex字符串,方便存储和传输
  • 可以使用ecdh实例的computeSecret()方法,配合另一个同类型的公钥作为参数,来计算协商的密钥,后续可以用于加密解密
  • 如果不使用临时生成的密钥对,也可以在创建ecdh实例后,可以使用setPrivate来设置私钥,这个操作在基于配置的应用程序运行中,是非常有用的

但就如前面所提到的,ecdh本身,并没有进一步加密解密和签名验证的功能。虽然,它协商计算出来的密钥,可以直接用于对称加密和解密(如用于createCipherIV函数),因为nodejs的cipher和decipher对象,是可以直接接受纯二进制密钥的。但是,它导出的私钥和公钥,却不能直接用于签名和验证,因为nodejs crypto的相关操作,并不简单的直接使用纯密钥内容,而是使用密钥对象。而ecdh导出的密钥信息,是无法直接来构造相关的密钥对象的,其实是需要进行转换和编码的,这就是本文要研究的问题的核心。

所以,在继续之前,我们再来看看,nodejs中是如何进行签名和验证操作的。

签名和验证操作

在nodejs中,使用相同的ECC算法和曲线参数,进行签名和验证操作,大致的过程和代码示例如下:

js 复制代码
import {
    generateKeyPairSync, createPrivateKey, createPublicKey,
    createSign, createVerify, createECDH
} from "crypto";
        
const CURVE =  "secp384r1";
let { publicKey, privateKey }  = generateKeyPairSync("ec", {  namedCurve: CURVE });

const otext = "China.中国";
const sign = createSign("SHA256")
             .update(otext)
             .end()
             .sign( privateKey)
             .toString("base64");

console.log("Sign:", sign);

let keyString = publicKey.export({ format: "der", type: "spki" }).toString("base64");
console.log("PublicKey:", keyString);

let publicKey2 = createPublicKey({
    key: Buffer.from(keyString,"base64"),
    format: "der",
    type: "spki"
})

const veri = createVerify("SHA256")
            .update(otext)
            .end()
            .verify(publicKey2, Buffer.from(sign,"base64"));

console.log("Verify:", veri); // should true

const veri2 = createVerify("SHA256")
            .update(otext+"x")
            .end()
            .verify(publicKey2, Buffer.from(sign,"base64"));

console.log("Verify2:", veri2); // should false for content changed

这个过程大体如下:

  • 签名方,使用密钥对生成函数,指定曲线类型,生成密钥对,包括了私钥和公钥对象实例
  • 签名方调用createSign方法,创建一个签名器,可以加载签名内容,并使用私钥进行签名
  • 签名结果默认是buffer,当然一般导出为base64进行传输
  • 公钥对象,也可以按照设置,导出为一个DER的base64形式,来进行发布
  • 验证方,可以获取发布的公钥信息,并还原成为公钥对象
  • 验证方,可以调用创建一个createVerify方法,创建一个验证器实例
  • 验证器加载验证内容,并且使用签名公钥对象和签名信息,来进行验证
  • 验证的返回结果是true和false,表示验证是否成功
  • SHA256表示,在签名前先进行摘要计算,从而可以有效控制签名信息的规模

这段代码的问题在于,签名使用的私钥,是临时生成的。但在真实的应用场景中,很可能是需要使用一个配置好的密钥,来加载使用。当然,也可以直接方便的导出密钥,然后在后面加载使用,就和代码中公钥的导出和传输类似。

很自然的,笔者就构想,既然都使用相同的曲线和配置,createPrivateKey方法,应该也可以加载ECDH导出的私钥,并在签名和验证中使用。但是现实情况是,虽然是相同的曲线和密钥信息,ECDH导出的是私钥的原始信息(buffer),是无法直接在createPrivateKey中使用的,它使用的是ASN1标准编码后的信息。

所以,本文问题的核心,就是将私钥的原始信息,转换成为ASN1标准编码格式,并且可以用于创建签名使用的私钥对象。我们会在下一章节中,着重讨论这个问题。

ASN.1编码和格式

在开始之前,笔者认为需要先了解一下ASN1编码和相关的基本知识和概念。由于这个内容比较复杂,笔者并不打算非常非常深入的探讨,只是初步了解,能够有助于问题的理解和简单应用即可。

ASN.1,即Abstract Syntax Notation One,抽象语法标记一,是一种用于描述数据结构的标准表示法,广泛应用于通信协议中的数据编码和传输。它基于二进制格式,提供了一种与平台、编程语言无关的方式,来定义复杂数据结构。它还支持多种编码规则(如BER、DER、PER等),能够将抽象描述转换为具体的二进制或文本格式。我们可以理解,它是一种信息编码的事实标准,在密码学应用中的应用也非常广泛,比如密钥和数字证书等的编码。

遗憾的是,这个编码标准,基本是为计算机系统和软件程序而设计的,虽然严谨高效,但对于人类的解读是非常不友好的,所以在程序调试过程中,是不太直观的,我们会在后面的分析中看到。

除了ASN.1之外,笔者认为还需要了解一些其他相关的概念。

  • DER

Distinguished Encoding Rules,可辨识编码规则。它是一种ASN1编码规则,要求唯一确定的编码,常用于密钥和数字证书(如X.509),信息安全等场景。本文默认的密钥导入导出的编码都是DER。密钥的DER就是一种带编码的二进制格式。

  • PEM

Privacy-Enhanced Mail,隐私增强邮件。密钥导出可选择PEM格式,这时导出的密钥可能类似于下面的形式:

-----BEGIN PUBLIC KEY----- MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEVNL13u4QD7QQmxNrgKfTF7xtjICDxMU/ xfYFcbO2/sSf3L3xPbvufYKBcSSZ54gYb2ZLJ+iGEuP1Vg7dH37ghJauYn9dE2lc h9zfdZ9jYDAhYZ7trAr/TMJX/MQdWAna -----END PUBLIC KEY-----

显然,它是一种文本化结构化的信息封装方式。其实封装的内容,就是密钥DER信息的base64编码。PEM通常用于使用文件存储和加载密钥信息。

  • SEC1

在导入导出密钥时,可以选择一个"类型"的参数。参数可以选择SEC1。

SEC1是密码学中定义椭圆曲线私钥/公钥格式的一种标准,属于Standards for Efficient Cryptography (SEC) 系列规范之一,它主要用于标准化椭圆曲线密码学(ECC,Elliptic Curve Cryptography)密钥的存储和传输格式。

  • PKCS#1

Public-Key Cryptography Standards #1,它是RSA实验室制定的标准,定义RSA密钥的格式和加密/签名算法。本文中都使用ECC密钥,所以不会使用PKCS1类型导出公钥。

  • PKSI#8

Public-Key Cryptography Standards #8, 是 RSA 实验室制定的一种通用私钥封装标准,属于 PKCS 系列规范 的一部分。它定义了如何存储和传输私钥(包括 RSA、ECC、DSA 等多种算法),并支持对私钥进行加密保护,适用于更广泛的密钥管理场景。理论上可以使用PKSI8导出私钥,但笔者使用的不多,通常都使用SEC1。

  • SPKI

SPKI(Subject Public Key Info,主题公钥信息)是密码学中用于存储和传输公钥的一种标准格式,通常与 X.509 证书 和 PKI(公钥基础设施) 相关。它定义了如何编码公钥及其关联的算法标识符(如 RSA、ECC),广泛应用于 TLS/SSL、数字证书、密钥交换等场景。导出和导入公钥的时候,我们使用SPKI格式。

简单总结一下。在导入导出密钥时,format(格式)指定了密钥编码的形式,如二进制(DER)和文本(PEM),type(类型)指定了ASN1编码方式,包括SPKI用于公钥导出,SEC1或者PKSI8用于私钥的导出。

在了解了相关的基本概念和知识之后,下一章节我们来分析一下ASN的结构,和如何应用在密钥转换和构造的操作当中。

ASN.1 结构解析

前面的内容中,我们已经了解, ASN1信息结构,才是本文主题讨论的核心,其实也是很多兼容性和错误产生的原因。因为如果没法搞清楚ASN编码的结构,就无法从ECDH的密钥结构转换成为标准的用于签名验证的密钥结构。为了解决这一问题,笔者使用了反推的结构分析方式,在结合了一些AI对于ASN编码和结构的解释,过程如下。

  • 标准编码

我们先使用以下代码,导出标准的ECC私钥的DER数据。为了方便数据的分析和观察(比如相同的内容片段搜索和长度对齐),这里使用了hex格式。

js 复制代码
const {
generateKeyPairSync 
} = require("crypto");

// 生成密钥对
let { publicKey, privateKey}  = generateKeyPairSync("ec", { 
    namedCurve: "secp384r1"
});

// 导出私钥
let vkey = privateKey.export({ 
    format: "der", 
    type: "sec1" ,
});
console.log(vkey.byteLength,vkey.toString("hex"));

// 私钥
3081a40201010430
40c5e99e9d45563273ba581a8830b47fd80d34cb3529fc47
0df995378ab176afc0df0b72f8ad0a2262ea757ae340092c
a00706052b81040022a16403620004
d87b7a43f9ce72cfdb76c4464dbf30a42650dd8ed401c4c6
5db98ad9017e4e58eb1770571e8f9aac5f68679024648a19
475330826c1cbc18f192a9107eeacb5d536c6852cee56d5f
21b604a1a4647c0ebe457a67d99ebce7c1359d6140ca78d9

// 再运行一次,得到新的密钥 
3081a40201010430
e76c6526c98c1c3d7de2337744e7304e464cde1a8b614911
ced92f336f7a2ee87ed2a9a70c688199e008a64b6ac7fab4
a00706052b81040022a16403620004
fb31331e0ddc9512b9953f8876aebfc6b99aa85da58879c5
1fd1a583db787a78b0c3dad5b0f10d37ac4c85cb7000f9c7
750a0e00a5e76111669ed8e94537d14fc5684b000f851f6a
3ebf4df73d8599a26d355da9bcf9fc1d537f550165fcbdbc

这就是一个标准的ECC密钥对生成和密钥导出的操作,这个操作我们可以得到一个167字节的二进制数据(后面我们会看到为什么是167),在nodejs中的类型是buffer。 为了方便观察和分析,我们将其使用hex字符串来表示。我们已经知道,这个信息大体上是一个ASN.1编码的结构。这个编码本身是一个比较复杂的,但如果只是在工程上的应用,我们只需要理解它的构成和结构,而无需更深刻的理解其基础原理。

  • 结构解析

如上面的代码所示,我们可以多运行几次以上的代码,就应该可以发现其中的规律。即这个编码中是由某种固定结构组成的,其中有动态变化的部分(如第二和第四部分),也有固定的内容(如第一和第三部分)。结合一些资料查找和编码原理的分析,我们可以了解到,对于secp384r1这种曲线,导出的私钥(der格式)编码结构如下:

js 复制代码
// 标准的secp384r1导出der内容,SEC版本,共167个字节
03字节-序列编码	
3081a4

03字节-版本编码	
020101

02字节-私钥形式长度	
0430 

48字节-私钥		
8c03042cab6ffa7dc83403ed008cf45be9c2797af012769c
5b58534f27a5f91416c2ba4e88c04f12545b22e5f54cf55c

09字节-OID		
a00706052b81040022

05字节-附加公钥和参数	
a164036200

01字节-公钥不压缩	
04

96字节-公钥内容,其实包括了X和Y	
583c1207234d0f4775892782c71936a697345a3b65828793
6569609c09fc226c9d19b4c40906e111a717a041ae8f9d79
327d6892086aba02b37aaf5d691077c8443b9382b3235825
3c0bb3be8b783c607dc1be681d1c37e6783baa96e96d5504

基于以上解构和分析,可以总结一下,一个完整的DER格式的secp381r1私钥,是由以下部分构成:

js 复制代码
序列和长度: 3081A4, 序列,长度162字节
├── 版本编码: 020101, 版本编码,1字节,版本1
├── 私钥长度: 0430, 私钥本体OctetString,48字节
├── 私钥: ....(48字节)
├── SECP384R1 OID: A0 07 0605 2b81040022, 曲线Tag,7字节,1.3.132.0.34的编码
├── 公钥参数: A164, 公钥Tag,100字节  
└── 公钥: 036200, BitString类型, 98字节,无padding
    └──公钥内容: ... 1+96字节,非压缩, EC点位 (04 || X || Y)
    
    
  • 私钥重构

在了解了这个密钥的结构之后,我们就可以比较方便的从一个私钥,来重构这个编码。我们可以从一个ECDH导出的私钥内容开始,然后计算它对应的公钥,随后将私钥和公钥填入到这个框架之中,就可以还原这个密钥完整的DER结构了。

这部分的实例代码,笔者想要先省略一下,因为后面我们有更好的方法,但基本概念相同。

  • 简化重构

看到这里,相信有的读者会有几个疑问。就是在重构的时候,为什么需要完整的私钥和公钥信息呢? 这样可能需要在外部计算一遍公钥。难道不是理论上而言,每个私钥都有对应的公钥,只需要加载私钥内容,这个密钥对象其实就是已经包含了公钥或者可以推导出来吗?

确实是这样的,理论上我们可以直接导出和导入只包括私钥的结构。公钥可以在使用的时候进行计算。但笔者查找了资料,没有发现对于私钥对象,可以只导出私钥内容的操作方法。它们的解释是,同时包含公钥的内容,可以简化计算并且提高兼容性。但很多材料都提到,可以使用只包含私钥内容的DER数据来创建私钥。

但是,笔者在构造DER结构时,直接使用没有包括公钥部分的内容(结构树中公钥参数和内容的部分),来创建私钥的时候,是失败的。失败的信息提到了某种编码结构长度错误的问题。看来是不能简单的去除公钥,就可以直接导入密钥的。

这个问题的解决,还得从ASN的编码着手。经过对GPT的请教,它指出了问题的可能,是在第一部分,即序列编码这一块。由于缺少了公钥这部分参数和内容,整个结构的长度缩短了,序列的编码(原来是 81A4, A4代表后面序列的长度为164字节,加上序列一共167)也应该修改。我们看到,调整后的序列长度只有 3+2+48+9=62个字节,即0x3E。由于这个数值小于128,可以直接表示(大于128需要用两个字节表示,在前面加0x81),所以序列这部分的内容就是"303E"。

最后经过整理和尝试,只包括私钥内容的DER结构构造和私钥对象创建的完整的示例代码内容如下:

js 复制代码
// 不带公钥的私钥 DER构造
const keyBuf = Buffer.concat([
    Buffer.from([0x30, 0x3E]), // sequnce - 带公钥: [0x30, 0x81, 0xA4]
    Buffer.from([0x02, 0x01, 0x01]), // version
    Buffer.from([0x04, 0x30]), // private key length
    Buffer.from(keyHex, "hex"), // 私钥的实际内容,48字节
    Buffer.from([0xA0, 0x07]), // oid tag 和 length
    Buffer.from([0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22]) // oid   1.3.132.0.34
    // Buffer.from([0xA1, 0x64, 0x03, 0x62, 0x00, 0x04]), // 附加公钥参数
    // pkey1 // 公钥内容, 96字节
]); 

// 基于DER信息创建私钥和公钥
const vkey2 = createPrivateKey({ key: keyBuf, format: "der",  type: "sec1" });
const pkey2 = createPublicKey({ key: vkey2});

// use to sign and verify.... 

// 简化形式, 使用模板hex字符串
const keyBuf = Buffer.from("303E0201010430"+keyHex+"A00706052B81040022","hex");

至此,我们已经基本上能够比较清晰明确的了解到,在使用相同的曲线和参数的情况下,如何将ECDH导出的二进制简单的私钥内容,使用ASN1的结构来进行编码,并且构建可以用于签名和验证操作的密钥对象了。

扩展内容

在研究这个问题的过程中,笔者发现了一些有趣的扩展内容,觉得比较有趣,可以和读者分享一下。这里大力感谢各种AI给的分析和帮助。

  • 一些TAG标识

根据上面的示例,我们可以总结在ASN编码中,涉及到一些特定的标识如下:

30 - 序列

02 - 整数

03 - BitString

04 - OctetString

06 - OID标识,后面通常接编码长度

8n - 字节长度声明,如81是一个字节等

A0 - 曲线Tag和参数

A1 - 密钥Tag

  • OID

在前面的内容中,我们提到了secp384r1的OID编码,是1.3.132.0.34。 但编码后却是 "A0 07 06 05 2B 81 04 00 22",这是怎么来的呢?分析如下:

A0 是构造标签

07 后续内容长度,7字节

06 OIDTag,表示后面的信息是OID

05 OID的长度,5字节

2B 前两个数值合并 1x40+3= 43 -> 0x2B

8104 132大于128,加前位0x81,余数是132-128 = 4,表示为0x04

00 表示为0x00 以及 34 表示为0x22

类似的:

SECP256R1的OID编码是:1.2.840.10045.3.1.7, ASN编码是: 06 08 2A 86 48 CE 3D 03 01 07

SECP256R1的OID编码是:1.3.132.0.10, ASN编码是: 06 05 2B 81 04 00 0A

  • Base128编码

OID使用一种Base128编码的方式,我们以SECP256R1的OID编码 1.2.840.10045.3.1.7 来说明:

头两位 1.2,编码成为 1x40+2=42,即0x2A

840: 840/128= 6余72,即 0x86 0x48

10045: 10045/128= 78余61,由于后面还有字节,需要设置第一位为1,(0x4E | 0x80) = 0xCE, 0x3D

3.1.7,简单编码成 030107

  • 公钥参数

虽然不影响私钥构造和导入,但在完整的结构中,包括了公钥和公钥参数,即A164036200,它是这样构成的:

A1 上下文标签(这里是一个公钥)

64 后续长度,100字节

03 标签,后续是一个bitstring位串

62 位串的长度是 98字节

00 未使用位数,为0,即无padding

04 公钥未压缩

  • SEQUENCE头结构

ASN.1 DER 编码是一种结构化编码,在其开始的30,表示这是一个SEQUENCE结构,有序集合,可以包括多个子元素。然后会跟随一个序列长度的编码。此处为 81A4,即164个字节。

为什么是81,其实这是一个定义长度的方式,它使用0x8N,来表示长度所占用的字节数。如果长度小于127,则直接使用长度数值(文中示例为64个字节,直接表示为 3E),否则计算长度编码。 81的意思是,长度信息占了一个字节,后续A4表示164。比如如果长度为800字节(0x0320),长度占两个字节,应该编码成为 820320。

  • ECC密钥曲线类型

从本文中,我们就可以很容易理解,使用createPrivateKey创建密钥对象时,为什么不需要指定曲线和算法了,因为所使用的DER信息,已经包括了OID的内容,当然也就包括了曲线类型了。

这些曲线类型都有不同的名称,如SECP256R1等等,这些命名的大致意思是:

SEC: Standards for Efficient Cryptography,高效密码学(标准组织)制定的标准曲线。

P: Prime Field,素数域

384: 密钥长度为384位,即48字节, 曲线的基域素数p和阶n均为384位,提供约192位安全强度

R1: 随机曲线,版本1

K1: Koblitz曲线,某一个人为选择的固定曲线,区块链常用

在很多情况下,相同的曲线可能有不同的名字,但技术上其实是等价的,比如Prime256v1其实就是SECP256R1,Prime384V1就是SECP384R1。

一般情况下,我们熟悉的加解密算法,都使用32字节的密钥和256位的算法。实际上nodejs crypto中,支持多种类型的曲线,常见的包括secp256k1,secp256r1,secp384r1, secp521r1等等。本来笔者倾向于使用secp245k1的,但后来发现这个算法,并不是标准的NIST推荐的设置,只是由于它是比特币系统选择的主要算法,它的理由是标准的secp256r1可能存在一些问题。所以笔者索性就选择了更安全和标准的secp384r1,来作为标准算法。

公钥结构

虽然不是本文要讨论的重点和核心,但使用类似的方式,我们也可以进行公钥的处理。SECP384R1公钥的DER格式为120字节,其中后96个字节,就是实际的公钥内容。它的整个结构如下:

js 复制代码
3076 外层sequence, 118个字节
3010 内层sequence, 16字节
0607 2a8648ce3d0201 ecPublicKey的OID, 7字节,编码1.2.840.10045.2.1
0605 2b81040022 OID,5字节,编码1.3.132.0.34
03620004 公钥参数,0x03是bitstring,0x62表示后续98个字节,0x04表示非压缩
...公钥(96字节)...

SQ结构如下:
SubjectPublicKeyInfo: 3076
├── AlgorithmIdentifier: 3010
│   ├── ecPublicKey (OID): 0607 2a8648ce3d0201
│   └── secp384r1 (OID):  0605 2b81040022
└── subjectPublicKey (BIT STRING):  036300
    └── Uncompressed EC point (04 || X || Y)

小结

本文从一个如何使用ECDH所产生的密钥,来进行签名验证的操作的需求入手,分析了如何将其产生的私钥,按照创建签名私钥对象所要求的ASN.1格式进行转换和编码的操作。并探讨了ASN1编码和相关格式和类型,以及这个过程中的一些问题和技术细节。

相关推荐
fmingzh3 分钟前
NVIDIA高级辅助驾驶安全与技术读后感
人工智能·安全·自动驾驶
ss27314 分钟前
基于Springboot + vue + 爬虫实现的高考志愿智能推荐系统
spring boot·后端·高考
拉不动的猪18 分钟前
前端常见数组分析
前端·javascript·面试
Blossom.1181 小时前
量子网络:构建未来通信的超高速“高速公路”
网络·opencv·算法·安全·机器学习·密码学·量子计算
专注API从业者1 小时前
《Go 语言高并发爬虫开发:淘宝商品 API 实时采集与 ETL 数据处理管道》
开发语言·后端·爬虫·golang
落——枫1 小时前
网络安全知识点3
安全·web安全
Asthenia04121 小时前
Netty writeAndFlush与Pipeline深入分析
后端
欧先生^_^2 小时前
Scala语法基础
开发语言·后端·scala
柴郡猫^O^2 小时前
OSCP - Proving Grounds - Wpwn
安全·网络安全·安全性测试
安全系统学习2 小时前
网络安全之红队LLM的大模型自动化越狱
运维·人工智能·安全·web安全·机器学习·php