Skip to content

v1.3.0

Latest

Choose a tag to compare

@gouguoyin gouguoyin released this 19 Oct 12:05
· 3 commits to master since this release

English

✨ Features

  • Add ASN.1 ciphertext and signature formats to SM2: Sm2CipherMode gains ASN1C1C2C3 and ASN1C1C3C2, so ciphertext can be encoded as the DER SEQUENCE { x INTEGER, y INTEGER, C2 OCTET STRING, C3 OCTET STRING } that BouncyCastle, Java and most national-crypto gateways produce, instead of only the raw C1 || C2 || C3 / C1 || C3 || C2 byte layout; Sm2SingMode (ASN1 as the default, Bytes) and SetSingMode are added so a signature is either a DER SEQUENCE { r, s } or a fixed 64-byte r || s; both formats are parsed with cryptobyte, so a truncated or non-DER payload returns an error instead of being silently misread, and NewSm2KeyPair keeps ASN1 as the default (by @Web-Coke in #40)
  • Extend the morse alphabet to the full ITU-R M.1677-1 table and the international extended characters: adds ¡, ¿, å/Å, ä/Ä, é/É, ñ/Ñ, ó/Ó, ö/Ö and ú/Ú with the codes published by codigomorse.org, documents which ITU section each group comes from, and moves / into the punctuation group; note that two existing mappings change — < moves from .--.- to .--.-- because .--.- now belongs to å, and \n becomes .-..-.., which also separates it from \r; text encoded with v1.2.3 decodes differently for those two characters (by @htlsmilework in #42)

🐛 Fixes

  • Fix ascii85 decoding truncating its output when z shortcuts are present: encoding/ascii85 emits a single z for an all-zero four-byte group and the encoder here does the same, but the decoder recomputed the destination length from the character count at a strict 5:4 ratio and truncated the standard library's correct answer to it, so five zero bytes round-tripped to two, and eight zero bytes to one, without any error; z is now excluded from the ratio and counted as four bytes, and the destination is sized for the expansion a shortcut implies (by @youdie006 in #44)
  • Fix ascii85 decoding failing for four or more zero groups: the u padding still used len(src)%5 after the length fix above, so with z shortcuts present the final group could be left with a single character, which encoding/ascii85 rejects — sixteen zero bytes, encoded as zzzz, did not decode at all; both decoders now pad by the non-z length (by @youdie006 in #47)
  • Fix base58 and base62 streams producing different output from the bulk API: both alphabets treat the input as a single big number, so it cannot be split into independent chunks, yet the stream encoder encoded every 8 bytes separately and the stream decoder decoded every 1024 bytes separately — Encode.FromFile(...).ByBase58() and ByBase62() returned a different result than FromBytes() for the same data, and that result did not decode back; the stream encoder now buffers the input and encodes it once in Close, as its documentation already described, and the decoder reads all of the encoded input and decodes it once, releasing its buffers as the decoded data is consumed (by @Shaisolaris in #48)
  • Fix key pairs rejecting keys generated by other tools and panicking on a key of the wrong algorithm: a key was only accepted when it was byte for byte the output of GenKeyPair, i.e. a PUBLIC KEY/PRIVATE KEY PEM block holding PKIX or PKCS#8 DER; the base64 and raw material printed by most SM2 and Ed25519 generators failed with invalid public key: <nil> or an unintelligible ASN.1 error, and an RSA key passed to an Ed25519 key pair panicked because the result of the type assertion was never checked (#41); every Parse*, Format* and Set* method now goes through a shared selector that tries the input as PEM, base64 (all four alphabets, with or without padding), hex and bare key material, and lets the algorithm-specific parser pick the interpretation that works — Ed25519 accepts PKIX/PKCS#8 DER, a 32-byte seed and a 64-byte private key, SM2 accepts PKIX/PKCS#8 DER, a 65-byte uncompressed point and a 32-byte scalar, RSA accepts PKIX/PKCS#1 DER; SM2 key formatting now uses a local SPKI/PKCS#8 encoder because crypto/x509 has no SM2 support, so the output still parses in OpenSSL and Java; Set*Key accept PEM and normalize it, a block of another algorithm or direction is rejected before parsing with UnsupportedKeyFormatError ("the parsed key is an RSA key, not an Ed25519 key"), the error messages no longer print <nil>, Compress*Key use pem.Decode instead of string surgery so optional headers are handled, and the SM2 private scalar is validated to be in [1, n-1] — d = 0 used to produce a key without a public point (by @gouguolei in 012269d)

⚡ Performance

  • Derive the morse reverse alphabet once instead of scanning it per symbol: StdDecoder.Decode ranged over the whole StdAlphabet until it found a matching code, making decoding O(n × 89) where it should be O(n) and spending 53% of a CPU profile in map iteration; the inverse map is now built once in init(), the way base45 and base91 already derive their decode tables, cutting the geomean by 89.21% (4096 characters: 1222µs → 103µs) with allocations unchanged; the scan also made decoding non-deterministic, because seven codes are shared by an upper- and lower-case pair (Å/å for .--.-, and likewise for ä, é, ñ, ó, ö, ú) and ranging over a map picks a random winner, so .--.- decoded to Å 62% of the time and encoding-then-decoding those characters was not lossless; the inverse map resolves such collisions to the lower-case form, which is the only one Encode can produce (by @darakanoit in #46)

🔧 Refactors

  • Route key serialization through package-level helpers: marshalPKIXPublicKey and marshalPKCS8PrivateKey are now variables wrapping crypto/x509, and marshalSm2PublicKey/marshalSm2PrivateKey wrap the local SM2 encoder, so the error branches of Format*Key can be simulated in tests, since crypto/x509 never fails for a key that has already been parsed into its typed form; RSA, Ed25519 and SM2 formatting all use them (by @gouguolei in e314c99 6ad47b4 0769ea0 2c9ad5a)
  • Simplify the RSA stream verifier and drop unused stream buffers: rsa.StreamVerifier.verify returns only an error instead of a valid flag that was always true when the error was nil, unicode.StreamEncoder and unicode.StreamDecoder lose their unused fixed [512]byte buffers, and the morse error message uses a plain string literal instead of fmt.Sprintf with no verbs (by @gouguolei in 68dad92)
  • Fix an inverted SetSingMode assertion and initialize the SM2 default explicitly: the clamp test expected SingMode to stay ASN1 after SetSingMode(Bytes), which cannot happen because the setter simply assigns, and NewSm2KeyPair relied on the zero value being ASN1; the constructor now sets SingMode: ASN1 and the test asserts both directions (by @gouguolei in 079efab)
  • Clean up and extend the test suite: removes unused variables and duplicated cases from 24 test files, adds key_format_test.go and key_test.go covering the key material helpers, every UnsupportedKeyFormatError message and the rejection paths of the public API, and covers the SM2 DER and private scalar range branches, raising keypair statement coverage to 97.7% (by @gouguolei in 372209d 12c3e11)

🚀 CI

  • Add a golangci-lint workflow and configuration: lint.yml runs golangci-lint-action@v9 pinned to v2.15.0 on pushes and pull requests to master with contents: read; .golangci.yml enables the standard set plus misspell, unconvert, unparam and whitespace, disables the opinionated staticcheck style checks (ST1000–ST1022, QF1001) and excludes (io.Seeker).Seek from errcheck because rewinding a stream is best-effort by design; the READMEs drop the Go Report Card badge in favour of the workflow status badge (by @gouguolei in 1487675 0517244)

📝 Docs

  • Serve the README images from the repository instead of the docs site: the logo, the GVP and G-Star banners and the JetBrains logo now point at assets/, so the English, Chinese and Japanese READMEs no longer depend on dongle.go-pkg.com (by @gouguolei in 19bffd5)

中文

✨ 新功能

  • 新增 SM2 的 ASN.1 密文与签名格式:Sm2CipherMode 新增 ASN1C1C2C3 和 ASN1C1C3C2,密文可以是 DER 编码的 SEQUENCE { x INTEGER, y INTEGER, C2 OCTET STRING, C3 OCTET STRING },即 BouncyCastle、Java 与多数国密网关的输出格式,不再局限于 C1 || C2 || C3、C1 || C3 || C2 的裸字节排列;新增 Sm2SingMode(默认 ASN1,可选 Bytes)与 SetSingMode,签名既可以是 DER 的 SEQUENCE { r, s },也可以是固定 64 字节的 r || s;两种格式均使用 cryptobyte 解析,数据被截断或不是合法 DER 时直接返回错误,而不是被悄悄误读;NewSm2KeyPair 保持 ASN1 为默认值(by @Web-Coke in #40)
  • 将摩斯电码字母表补齐到 ITU-R M.1677-1 全表及国际扩展字符:新增 ¡、¿、å/Å、ä/Ä、é/É、ñ/Ñ、ó/Ó、ö/Ö、ú/Ú,编码取自 codigomorse.org,并标注每组字符对应的 ITU 条款来源,同时把 / 归入标点分组;注意有两个已有映射发生变化:< 由 .--.- 改为 .--.--(.--.- 现已分配给 å),\n 改为 .-..-..,不再与 \r 共用同一编码;用 v1.2.3 编码的这两类字符,解码结果会随之变化(by @htlsmilework in #42)

🐛 修复

  • 修复 ascii85 解码在存在 z 缩写时截断输出的问题:encoding/ascii85 会把全零的四字节分组编码成单个 z,本库的编码器同样如此,但解码器按严格的 5:4 比例用字符数重算目标长度,并把标准库给出的正确结果截断到该长度,导致五个零字节往返后只剩两个、八个零字节只剩一个,且不报任何错误;现在 z 不再计入该比例而按四字节计算,目标缓冲区也按缩写所隐含的展开量分配(by @youdie006 in #44)
  • 修复 ascii85 解码在四个及以上零分组时失败的问题:上述长度修复之后,u 补齐仍使用 len(src)%5,存在 z 缩写时最后一组可能只剩一个字符,而 encoding/ascii85 会拒绝这种输入——十六个零字节(编码为 zzzz)因此完全无法解码;现在两个解码器都按非 z 的长度补齐(by @youdie006 in #47)
  • 修复 base58、base62 流式编解码结果与批量接口不一致的问题:两种编码都把输入视为一个大整数,无法拆成互相独立的块,但流式编码器每 8 字节编一次、流式解码器每 1024 字节解一次,导致 Encode.FromFile(...).ByBase58()、ByBase62() 与同样数据的 FromBytes() 结果不同,且该结果无法解码回原文;现在流式编码器按文档承诺缓存输入并在 Close 时一次性编码,解码器读取全部输入后一次性解码,并在消费过程中释放缓冲区(by @Shaisolaris in #48)
  • 修复密钥对拒绝其他工具生成的密钥、并在算法不匹配时 panic 的问题:此前只有当密钥与 GenKeyPair 的输出逐字节一致(即 PUBLIC KEY/PRIVATE KEY PEM 块中承载 PKIX 或 PKCS#8 DER)时才会被接受;多数 SM2、Ed25519 生成器输出的 base64 或原始密钥材料会失败于 invalid public key: <nil> 或难以理解的 ASN.1 错误,把 RSA 密钥传给 Ed25519 密钥对则会因类型断言结果未检查而 panic(#41);现在所有 Parse*、Format*、Set* 方法统一经过一个选择器,依次尝试 PEM、base64(四种字母表、可带或不带填充)、十六进制与原始密钥材料,由各算法自己的解析器决定哪种解释成立——Ed25519 接受 PKIX/PKCS#8 DER、32 字节种子和 64 字节私钥,SM2 接受 PKIX/PKCS#8 DER、65 字节非压缩点与 32 字节标量,RSA 接受 PKIX/PKCS#1 DER;SM2 的密钥格式化改用本地 SPKI/PKCS#8 编码器(crypto/x509 不支持 SM2),输出仍可被 OpenSSL、Java 解析;Set*Key 接受 PEM 并归一化输出,算法或方向不符的 PEM 块在解析前即以 UnsupportedKeyFormatError 拒绝(提示 the parsed key is an RSA key, not an Ed25519 key),错误信息不再打印 <nil>,Compress*Key 改用 pem.Decode 而不再做字符串裁剪,因此可处理可选的块头,SM2 私钥标量也会校验落在 [1, n-1] 区间——此前 d = 0 会产生没有公钥点的密钥(by @gouguolei in 012269d)

⚡ 性能优化

  • 摩斯解码只构建一次反向字母表,不再逐字符扫描:StdDecoder.Decode 为每个符号遍历整张 StdAlphabet 直到命中,使解码成为 O(n × 89) 而非 O(n),CPU 采样中有 53% 的时间耗在 map 遍历上;现在按 base45、base91 已有的做法在 init() 中一次性构建反查表,几何平均耗时下降 89.21%(4096 字符:1222µs → 103µs),内存分配次数不变;这次扫描还让解码结果不确定——有七个编码由大小写成对共享(.--.- 同时属于 Å 和 å,ä、é、ñ、ó、ö、ú 同理),而 map 遍历顺序随机,导致 .--.- 有 62% 的概率解成 Å,这些字符的编码再解码并不无损;反查表把这类冲突统一解析为小写形式,这也是 Encode 唯一能产生的结果(by @darakanoit in #46)

🔧 重构

  • 将密钥序列化统一收敛到包级函数变量:marshalPKIXPublicKey、marshalPKCS8PrivateKey 现在是包装 crypto/x509 的变量,marshalSm2PublicKey、marshalSm2PrivateKey 包装本地的 SM2 编码器,从而可以在测试中模拟 Format*Key 的错误分支——对于已经解析成具体类型的密钥,crypto/x509 永远不会失败;RSA、Ed25519、SM2 的格式化均改为调用它们(by @gouguolei in e314c99 6ad47b4 0769ea0 2c9ad5a)
  • 简化 RSA 流式验签并移除未使用的流式缓冲区:rsa.StreamVerifier.verify 只返回 error,不再返回一个在无错误时恒为 true 的 valid;unicode.StreamEncoder、unicode.StreamDecoder 去掉未使用的固定 [512]byte 缓冲区;摩斯电码的错误信息改用字符串字面量,不再用没有占位符的 fmt.Sprintf(by @gouguolei in 68dad92)
  • 修正 SetSingMode 的反向断言并显式初始化 SM2 默认值:clamp 测试在调用 SetSingMode(Bytes) 后期望 SingMode 仍为 ASN1,这与 setter 直接赋值的实现相矛盾,而 NewSm2KeyPair 之前依赖零值恰好等于 ASN1;现在构造函数显式设置 SingMode: ASN1,测试同时断言两个方向(by @gouguolei in 079efab)
  • 清理并补充测试:从 24 个测试文件中移除未使用的变量与重复用例,新增 key_format_test.go、key_test.go 覆盖密钥材料辅助函数、各种 UnsupportedKeyFormatError 提示及公开 API 的拒绝路径,并覆盖 SM2 的 DER 与私钥标量区间分支,keypair 语句覆盖率提升至 97.7%(by @gouguolei in 372209d 12c3e11)

🚀 持续集成

  • 新增 golangci-lint 工作流与配置:lint.yml 在向 master 推送和发起 PR 时运行固定为 v2.15.0 的 golangci-lint-action@v9,权限仅 contents: read;.golangci.yml 在标准规则集之外启用 misspell、unconvert、unparam、whitespace,关闭过于主观的 staticcheck 风格检查(ST1000–ST1022、QF1001),并为 errcheck 排除 (io.Seeker).Seek(回卷流按设计是尽力而为);README 以该工作流的状态徽章替换 Go Report Card 徽章(by @gouguolei in 1487675 0517244)

📝 文档

  • README 图片改为从仓库读取,不再依赖文档站:Logo、GVP 与 G-Star 横幅、JetBrains 图标均指向 assets/,中文、英文、日文三个 README 不再依赖 dongle.go-pkg.com(by @gouguolei in 19bffd5)

What's Changed

  • add ASN.1 support by @Web-Coke in #40
  • fix(morse): Add some code from codigomorse.org and correct some repetitive code by @htlsmilework in #42
  • Decode the ascii85 z shortcut without truncating by @youdie006 in #44
  • perf(morse): Derive the reverse alphabet once instead of scanning it per symbol by @darakanoit in #46
  • Pad the ascii85 partial group without counting z shortcuts by @youdie006 in #47
  • fix(coding): encode and decode base58 and base62 streams as a whole by @Shaisolaris in #48
  • fix(keypair): correct SingMode test assertion and add explicit default by @gouguolei in 079efab
  • fix(keypair): accept externally generated keys instead of panicking by @gouguolei in 012269d
  • test(keypair): cover error paths and key material helpers by @gouguolei in 12c3e11
  • docs(readme): replace old badge with golangci-lint by @gouguolei in 0517244
  • docs(readme): replace old badge with golangci-lint by @gouguolei in 1487675
  • test: Clean up redundant content and unused variables in the test code by @gouguolei in 372209d
  • test(sm2): DER encoding error branch covering key formatting by @gouguolei in 6ad47b4
  • refactor(keypair): Extract x509 Marshall function as a variable to support testing simulation by @gouguolei in e314c99
  • refactor(crypto): Replace the ed25519 key serialization function with a local implementation by @gouguolei in 0769ea0
  • refactor(rsa): Replace the standard library key serialization function with a custom implementation by @gouguolei in 2c9ad5a
  • refactor(crypto/rsa): Simplify function signatures and clean up redundant code by @gouguolei in 68dad92
  • docs(readme): Replace the golangci lit badge with the GitHub Actions status badge by @gouguolei in e6f8039
  • docs(readme): Replace the image resource reference from remote URL to local assets path by @gouguolei in 19bffd5