| 1 | // Copyright (c) blackshirt. All rights reserved. |
| 2 | // Use of this source code is governed by an MIT license |
| 3 | // that can be found in the LICENSE file. |
| 4 | module slhdsa |
| 5 | |
| 6 | // PrivateKey represents SLH-DSA keypair. |
| 7 | pub struct PrivateKey { |
| 8 | key &C.EVP_PKEY |
| 9 | } |
| 10 | |
| 11 | // new creates a new SLH-DSA PrivateKey based on the supplied options. |
| 12 | // By default, it will create a `SLH-DSA-SHA2-128s` key with random generator. |
| 13 | // See `enum Kind` for all of availables choice's type (kind) of key. |
| 14 | // Its also support to generate keys based on the seed or private bytes through |
| 15 | // provided options. See available options in `KeyOpts`. |
| 16 | // Usage example: |
| 17 | // ```v |
| 18 | // pk1 := slhdsa.PrivateKey.new()!; println(pk1) |
| 19 | // pk2 := slhdsa.PrivateKey.new(kind: .sha2_128s)!; println(pk2) |
| 20 | // ``` |
| 21 | pub fn PrivateKey.new(opt KeyOpts) !PrivateKey { |
| 22 | match opt.flag { |
| 23 | // default random generated key, its recommended way |
| 24 | 0 { |
| 25 | kind := opt.kind.long_name() |
| 26 | key := C.EVP_PKEY_Q_keygen(0, 0, kind) |
| 27 | if key == 0 { |
| 28 | return error('EVP_PKEY_Q_keygen failed') |
| 29 | } |
| 30 | return PrivateKey{ |
| 31 | key: key |
| 32 | } |
| 33 | } |
| 34 | // use seed bytes, usually for testing purposes |
| 35 | 1 { |
| 36 | // if you dont provides seed bytes, it will discarded, and use random one |
| 37 | return PrivateKey.from_seed(opt.seed, opt.kind)! |
| 38 | } |
| 39 | // use private bytes, usually for testing purposes |
| 40 | 2 { |
| 41 | // if you dont provides private bytes, it will discarded, and use random one |
| 42 | return PrivateKey.from_bytes(opt.priv, opt.kind)! |
| 43 | } |
| 44 | else { |
| 45 | return error('Unsupported flag') |
| 46 | } |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | // sign signs the message using this key under desired options in opt. |
| 51 | pub fn (pv PrivateKey) sign(msg []u8, opt SignerOpts) ![]u8 { |
| 52 | if msg.len == 0 { |
| 53 | return error('Zero length of messages') |
| 54 | } |
| 55 | if opt.context.len > 255 { |
| 56 | return error('The context string size was over than 255') |
| 57 | } |
| 58 | out := slhdsa_do_sign(pv.key, msg, opt)! |
| 59 | return out |
| 60 | } |
| 61 | |
| 62 | // verify verifies signature for the message msg under provided options. |
| 63 | // Its possible because of under the hood, private key is a key pair. |
| 64 | pub fn (pv PrivateKey) verify(sig []u8, msg []u8, opt SignerOpts) !bool { |
| 65 | if opt.context.len > 255 { |
| 66 | return error('The context string size was over than 255') |
| 67 | } |
| 68 | return slhdsa_do_verify(pv.key, sig, msg, opt)! |
| 69 | } |
| 70 | |
| 71 | const default_bioread_bufsize = 4096 |
| 72 | |
| 73 | // dump_key represents PrivateKey in human readable string. |
| 74 | pub fn (pv PrivateKey) dump_key() !string { |
| 75 | bo := C.BIO_new(C.BIO_s_mem()) |
| 76 | if bo == 0 { |
| 77 | C.BIO_free_all(bo) |
| 78 | return error('BIO_new failed') |
| 79 | } |
| 80 | n := C.EVP_PKEY_print_private(bo, pv.key, 2, 0) |
| 81 | if n <= 0 { |
| 82 | C.BIO_free_all(bo) |
| 83 | return error('print private failed') |
| 84 | } |
| 85 | size := usize(0) |
| 86 | buf := []u8{len: default_bioread_bufsize} |
| 87 | m := C.BIO_read_ex(bo, buf.data, buf.len, &size) |
| 88 | |
| 89 | if m <= 0 { |
| 90 | unsafe { buf.free() } |
| 91 | C.BIO_free_all(bo) |
| 92 | return error('BIO_read_ex failed') |
| 93 | } |
| 94 | output := buf[..size].bytestr() |
| 95 | |
| 96 | // Cleans up and return the result |
| 97 | unsafe { buf.free() } |
| 98 | C.BIO_free_all(bo) |
| 99 | |
| 100 | return output |
| 101 | } |
| 102 | |
| 103 | // public_key gets the public part of this private key as a PublicKey. |
| 104 | pub fn (pv PrivateKey) public_key() !PublicKey { |
| 105 | pbkey := C.EVP_PKEY_dup(pv.key) |
| 106 | // we clears out the private bits from the key |
| 107 | // by setting it into null |
| 108 | n := C.EVP_PKEY_set_octet_string_param(pbkey, c'priv', 0, 0) |
| 109 | if n <= 0 { |
| 110 | C.EVP_PKEY_free(pbkey) |
| 111 | return error('EVP_PKEY_set_octet_string_param') |
| 112 | } |
| 113 | |
| 114 | return PublicKey{ |
| 115 | key: pbkey |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | // free releases memory occupied by this key. |
| 120 | pub fn (mut pv PrivateKey) free() { |
| 121 | C.EVP_PKEY_free(pv.key) |
| 122 | } |
| 123 | |
| 124 | // PublicKey represents public key part from the key. |
| 125 | pub struct PublicKey { |
| 126 | key &C.EVP_PKEY |
| 127 | } |
| 128 | |
| 129 | // verify verifies signature sig for messages msg under options provided. |
| 130 | pub fn (pb PublicKey) verify(sig []u8, msg []u8, opt SignerOpts) !bool { |
| 131 | if opt.context.len > 255 { |
| 132 | return error('The context string size was over than 255') |
| 133 | } |
| 134 | return slhdsa_do_verify(pb.key, sig, msg, opt)! |
| 135 | } |
| 136 | |
| 137 | // dump_key dumps this public key as a human readable string. |
| 138 | pub fn (pb PublicKey) dump_key() !string { |
| 139 | bo := C.BIO_new(C.BIO_s_mem()) |
| 140 | n := C.EVP_PKEY_print_public(bo, pb.key, 2, 0) |
| 141 | if n <= 0 { |
| 142 | C.BIO_free_all(bo) |
| 143 | return error('EVP_PKEY_print_public failed') |
| 144 | } |
| 145 | size := usize(0) |
| 146 | mut m := C.BIO_read_ex(bo, 0, default_bioread_bufsize, &size) |
| 147 | mut buf := []u8{len: int(size)} |
| 148 | m = C.BIO_read_ex(bo, buf.data, buf.len, &size) |
| 149 | if m <= 0 { |
| 150 | C.BIO_free_all(bo) |
| 151 | return error('BIO_read_ex failed') |
| 152 | } |
| 153 | |
| 154 | output := buf[..size].bytestr() |
| 155 | // Cleans up |
| 156 | unsafe { buf.free() } |
| 157 | C.BIO_free_all(bo) |
| 158 | |
| 159 | return output |
| 160 | } |
| 161 | |
| 162 | // free releases memory occupied by this public key |
| 163 | pub fn (mut pb PublicKey) free() { |
| 164 | C.EVP_PKEY_free(pb.key) |
| 165 | } |
| 166 | |
| 167 | // the biggest supported size of seed was 32 bytes length, where private key contains 4*32 bytes |
| 168 | const default_privkey_buffer = 128 |
| 169 | |
| 170 | fn (pv PrivateKey) bytes() ![]u8 { |
| 171 | priv_len := usize(0) |
| 172 | priv := []u8{len: default_privkey_buffer} |
| 173 | |
| 174 | n := C.EVP_PKEY_get_octet_string_param(pv.key, c'priv', priv.data, priv.len, &priv_len) |
| 175 | if n <= 0 { |
| 176 | return error('EVP_PKEY_get_octet_string_param failed') |
| 177 | } |
| 178 | out := priv[..int(priv_len)].clone() |
| 179 | unsafe { priv.free() } |
| 180 | return out |
| 181 | } |
| 182 | |
| 183 | // the biggest public key size was 64 bytes length, where public key |
| 184 | // was contains 2*n, so we relaxed it to 2*72 bytes |
| 185 | const default_pubkey_buffer = 144 |
| 186 | |
| 187 | fn (pv PrivateKey) public_bytes() ![]u8 { |
| 188 | pblen := usize(0) |
| 189 | buf := []u8{len: default_pubkey_buffer} |
| 190 | n := C.EVP_PKEY_get_octet_string_param(pv.key, c'pub', buf.data, buf.len, &pblen) |
| 191 | if n <= 0 { |
| 192 | return error('EVP_PKEY_get_octet_string_param failed') |
| 193 | } |
| 194 | |
| 195 | out := buf[..pblen].clone() |
| 196 | unsafe { buf.free() } |
| 197 | |
| 198 | return out |
| 199 | } |
| 200 | |
| 201 | // Signing and verifying Helpers |
| 202 | // |
| 203 | // slhdsa_do_sign performs signing msg with SLH-DSA key. |
| 204 | @[inline] |
| 205 | fn slhdsa_do_sign(key &C.EVP_PKEY, msg []u8, opt SignerOpts) ![]u8 { |
| 206 | sctx := C.EVP_PKEY_CTX_new_from_pkey(0, key, 0) |
| 207 | if sctx == 0 { |
| 208 | C.EVP_PKEY_CTX_free(sctx) |
| 209 | return error('EVP_PKEY_CTX_new_from_pkey failed') |
| 210 | } |
| 211 | |
| 212 | // gets algo name from key |
| 213 | algo_name := key_type_name(key) |
| 214 | sig_alg := C.EVP_SIGNATURE_fetch(0, algo_name, 0) |
| 215 | if sig_alg == 0 { |
| 216 | C.EVP_SIGNATURE_free(sig_alg) |
| 217 | C.EVP_PKEY_CTX_free(sctx) |
| 218 | return error('EVP_SIGNATURE_fetch failed') |
| 219 | } |
| 220 | |
| 221 | param_bld := C.OSSL_PARAM_BLD_new() |
| 222 | if param_bld == 0 { |
| 223 | C.OSSL_PARAM_BLD_free(param_bld) |
| 224 | C.EVP_SIGNATURE_free(sig_alg) |
| 225 | C.EVP_PKEY_CTX_free(sctx) |
| 226 | return error('OSSL_PARAM_BLD_new failed') |
| 227 | } |
| 228 | |
| 229 | // writes `context-string` params into context key generator. |
| 230 | // OSSL_PARAM_octet_string("context-string", (unsigned char *)"A context string", 33), |
| 231 | cs := C.OSSL_PARAM_BLD_push_octet_string(param_bld, c'context-string', opt.context.str, |
| 232 | opt.context.len) |
| 233 | if cs <= 0 { |
| 234 | C.OSSL_PARAM_BLD_free(param_bld) |
| 235 | C.EVP_SIGNATURE_free(sig_alg) |
| 236 | C.EVP_PKEY_CTX_free(sctx) |
| 237 | return error('OSSL_PARAM_BLD_push_octet_string context-string flag FAILED') |
| 238 | } |
| 239 | |
| 240 | // write `message-encoding` flag |
| 241 | me := C.OSSL_PARAM_BLD_push_int(param_bld, c'message-encoding', opt.encoding) |
| 242 | if me <= 0 { |
| 243 | C.OSSL_PARAM_BLD_free(param_bld) |
| 244 | C.EVP_SIGNATURE_free(sig_alg) |
| 245 | C.EVP_PKEY_CTX_free(sctx) |
| 246 | return error('OSSL_PARAM_BLD_push_int message-encoding flag FAILED') |
| 247 | } |
| 248 | |
| 249 | // handle entropy testing |
| 250 | // `test-entropy flag only handled when `encoding` flag was set into 0 value |
| 251 | if opt.encoding == 0 { |
| 252 | te := C.OSSL_PARAM_BLD_push_octet_string(param_bld, c'test-entropy', opt.entropy.data, |
| 253 | opt.entropy.len) |
| 254 | if te <= 0 { |
| 255 | C.OSSL_PARAM_BLD_free(param_bld) |
| 256 | C.EVP_SIGNATURE_free(sig_alg) |
| 257 | C.EVP_PKEY_CTX_free(sctx) |
| 258 | return error('OSSL_PARAM_BLD_push_octet_string test-entropy flag failed') |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | // write `deterministic` flag, its getting ignored if "test-entropy" is set. |
| 263 | dt := C.OSSL_PARAM_BLD_push_int(param_bld, c'deterministic', opt.deterministic) |
| 264 | if dt <= 0 { |
| 265 | C.OSSL_PARAM_BLD_free(param_bld) |
| 266 | C.EVP_SIGNATURE_free(sig_alg) |
| 267 | C.EVP_PKEY_CTX_free(sctx) |
| 268 | return error('OSSL_PARAM_BLD_push_int deterministic flag FAILED') |
| 269 | } |
| 270 | // build params |
| 271 | params := C.OSSL_PARAM_BLD_to_param(param_bld) |
| 272 | |
| 273 | // Then init the context with this params |
| 274 | x := C.EVP_PKEY_sign_message_init(sctx, sig_alg, params) |
| 275 | if x <= 0 { |
| 276 | C.OSSL_PARAM_BLD_free(param_bld) |
| 277 | C.OSSL_PARAM_free(params) |
| 278 | C.EVP_SIGNATURE_free(sig_alg) |
| 279 | C.EVP_PKEY_CTX_free(sctx) |
| 280 | return error('EVP_PKEY_sign_message_init failed') |
| 281 | } |
| 282 | |
| 283 | sig_len := usize(C.EVP_PKEY_size(key)) |
| 284 | buf := []u8{len: int(sig_len)} |
| 285 | // Do signing the msg and updates the sig_len. |
| 286 | m := C.EVP_PKEY_sign(sctx, buf.data, &sig_len, msg.data, msg.len) |
| 287 | if m <= 0 { |
| 288 | C.OSSL_PARAM_BLD_free(param_bld) |
| 289 | C.OSSL_PARAM_free(params) |
| 290 | C.EVP_SIGNATURE_free(sig_alg) |
| 291 | C.EVP_PKEY_CTX_free(sctx) |
| 292 | return error('EVP_PKEY_sign failed') |
| 293 | } |
| 294 | |
| 295 | // return the copy of the sig |
| 296 | sig := buf[..sig_len].clone() |
| 297 | |
| 298 | // cleans up |
| 299 | unsafe { buf.free() } |
| 300 | C.OSSL_PARAM_BLD_free(param_bld) |
| 301 | C.OSSL_PARAM_free(params) |
| 302 | C.EVP_SIGNATURE_free(sig_alg) |
| 303 | C.EVP_PKEY_CTX_free(sctx) |
| 304 | |
| 305 | return sig |
| 306 | } |
| 307 | |
| 308 | // slhdsa_do_verify performs verifying of provided signature in sign for the msg message. |
| 309 | @[inline] |
| 310 | fn slhdsa_do_verify(key &C.EVP_PKEY, sign []u8, msg []u8, opt SignerOpts) !bool { |
| 311 | sctx := C.EVP_PKEY_CTX_new_from_pkey(0, key, 0) |
| 312 | if sctx == 0 { |
| 313 | C.EVP_PKEY_CTX_free(sctx) |
| 314 | return error('EVP_PKEY_CTX_new_from_pkey failed') |
| 315 | } |
| 316 | |
| 317 | // get the algo signature name from the key |
| 318 | algo_name := key_type_name(key) |
| 319 | sig_alg := C.EVP_SIGNATURE_fetch(0, algo_name, 0) |
| 320 | if sig_alg == 0 { |
| 321 | C.EVP_SIGNATURE_free(sig_alg) |
| 322 | C.EVP_PKEY_CTX_free(sctx) |
| 323 | return error('EVP_SIGNATURE_fetch failed') |
| 324 | } |
| 325 | |
| 326 | param_bld := C.OSSL_PARAM_BLD_new() |
| 327 | if param_bld == 0 { |
| 328 | C.OSSL_PARAM_BLD_free(param_bld) |
| 329 | C.EVP_SIGNATURE_free(sig_alg) |
| 330 | C.EVP_PKEY_CTX_free(sctx) |
| 331 | return error('OSSL_PARAM_BLD_new failed') |
| 332 | } |
| 333 | |
| 334 | // handle context option |
| 335 | if opt.context.len > 0 { |
| 336 | o := C.OSSL_PARAM_BLD_push_octet_string(param_bld, c'context-string', opt.context.str, |
| 337 | opt.context.len) |
| 338 | if o <= 0 { |
| 339 | C.OSSL_PARAM_BLD_free(param_bld) |
| 340 | C.EVP_SIGNATURE_free(sig_alg) |
| 341 | C.EVP_PKEY_CTX_free(sctx) |
| 342 | return error('OSSL_PARAM_BLD_push failed') |
| 343 | } |
| 344 | } |
| 345 | // for testing |
| 346 | if opt.encoding == 0 { |
| 347 | oo := C.OSSL_PARAM_BLD_push_int(param_bld, c'message-encoding', opt.encoding) |
| 348 | if oo <= 0 { |
| 349 | C.OSSL_PARAM_BLD_free(param_bld) |
| 350 | C.EVP_SIGNATURE_free(sig_alg) |
| 351 | C.EVP_PKEY_CTX_free(sctx) |
| 352 | return error('OSSL_PARAM_BLD_push FAILED') |
| 353 | } |
| 354 | } |
| 355 | // build params |
| 356 | params := C.OSSL_PARAM_BLD_to_param(param_bld) |
| 357 | n := C.EVP_PKEY_verify_message_init(sctx, sig_alg, params) |
| 358 | if n <= 0 { |
| 359 | C.OSSL_PARAM_BLD_free(param_bld) |
| 360 | C.OSSL_PARAM_free(params) |
| 361 | C.EVP_SIGNATURE_free(sig_alg) |
| 362 | C.EVP_PKEY_CTX_free(sctx) |
| 363 | return error('OSSL_PARAM_BLD_push FAILED') |
| 364 | } |
| 365 | |
| 366 | // do verifying of the signature. |
| 367 | m := C.EVP_PKEY_verify(sctx, sign.data, sign.len, msg.data, msg.len) |
| 368 | |
| 369 | // Cleans up |
| 370 | C.EVP_SIGNATURE_free(sig_alg) |
| 371 | C.EVP_PKEY_CTX_free(sctx) |
| 372 | C.OSSL_PARAM_BLD_free(param_bld) |
| 373 | C.OSSL_PARAM_free(params) |
| 374 | |
| 375 | return m == 1 |
| 376 | } |
| 377 | |