diff --git a/README-pq.md b/README-pq.md new file mode 100644 index 0000000..b6f6590 --- /dev/null +++ b/README-pq.md @@ -0,0 +1,96 @@ +# Post-Quantum Wrappers for go-wolfssl + +This document describes the ML-KEM and ML-DSA wrappers added by the +`feat/mlkem-mldsa-wrappers` branch. + +## Primitives added + +| File | Algorithm | NIST standard | wolfSSL API | +|------|-----------|---------------|-------------| +| `mlkem.go` | ML-KEM-512 / ML-KEM-768 / ML-KEM-1024 | FIPS 203 | `wc_MlKemKey_*` | +| `dilithium.go` | ML-DSA-44 / ML-DSA-65 / ML-DSA-87 | FIPS 204 | `wc_dilithium_*` | + +## Build prerequisites + +### ML-KEM only (no experimental flag required on recent wolfSSL) + +``` +./configure --enable-mlkem +make +sudo make install +``` + +The symbol `WOLFSSL_HAVE_MLKEM` must be present in `wolfssl/options.h`. +`mlkem.go` is compiled unconditionally; when the symbol is absent every +function returns an error at runtime (the `#ifndef` stubs return `-174`). + +### ML-DSA (requires `--enable-experimental` on wolfSSL master) + +``` +./configure --enable-mlkem --enable-dilithium +make +sudo make install +``` + +`dilithium.go` and `dilithium_test.go` carry the `//go:build dilithium` +constraint and are only compiled when you pass `-tags dilithium`. This +ensures the package builds cleanly against a standard wolfSSL installation +that does not have Dilithium enabled. + +``` +# Build with ML-DSA support +go build -tags dilithium . + +# Run all tests (ML-KEM + ML-DSA) +go test -tags dilithium -count=1 ./... + +# Run only ML-KEM tests (no special wolfSSL build needed) +go test -count=1 ./... +``` + +## Sample usage + +### ML-KEM key exchange + +```go +import wolfssl "github.com/wolfssl/go-wolfssl" + +// Key generation +pub, priv, err := wolfssl.MlKemGenerateKey(wolfssl.MlKemLevel768) + +// Encapsulate (sender side) +ciphertext, sharedSecretA, err := wolfssl.MlKemEncapsulate(wolfssl.MlKemLevel768, pub) + +// Decapsulate (recipient side) +sharedSecretB, err := wolfssl.MlKemDecapsulate(wolfssl.MlKemLevel768, priv, ciphertext) + +// sharedSecretA == sharedSecretB +``` + +Available levels: `MlKemLevel512`, `MlKemLevel768`, `MlKemLevel1024`. + +Size constants: `MLKEM_512_PUB_SIZE`, `MLKEM_512_PRIV_SIZE`, +`MLKEM_512_CIPHERTEXT_SIZE`, `MLKEM_512_SHARED_SIZE` (and likewise for 768 +and 1024). + +### ML-DSA sign / verify + +```go +// +build dilithium + +import wolfssl "github.com/wolfssl/go-wolfssl" + +// Key generation +pub, priv, err := wolfssl.MlDsaGenerateKey(wolfssl.MlDsaLevel65) + +// Sign +sig, err := wolfssl.MlDsaSign(wolfssl.MlDsaLevel65, priv, []byte("hello")) + +// Verify +ok, err := wolfssl.MlDsaVerify(wolfssl.MlDsaLevel65, pub, []byte("hello"), sig) +// ok == true +``` + +Available levels: `MlDsaLevel44` (ML-DSA-44), `MlDsaLevel65` (ML-DSA-65), +`MlDsaLevel87` (ML-DSA-87). + diff --git a/aes.go b/aes.go index 37803b7..cfaf3ae 100644 --- a/aes.go +++ b/aes.go @@ -21,8 +21,8 @@ package wolfSSL -// #cgo CFLAGS: -g -Wall -I/usr/include -I/usr/include/wolfssl -I/usr/local/include -I/usr/local/include/wolfssl -// #cgo LDFLAGS: -L/usr/local/lib -lwolfssl +// #cgo CFLAGS: -g -Wall -I/usr/include -I/usr/include/wolfssl +// #cgo LDFLAGS: -L/usr/lib -lwolfssl // #include // #include // #include diff --git a/dilithium.go b/dilithium.go new file mode 100644 index 0000000..08b0d24 --- /dev/null +++ b/dilithium.go @@ -0,0 +1,242 @@ +//go:build dilithium + +/* dilithium.go + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +// ML-DSA (CRYSTALS-Dilithium) wrappers. +// +// Build tag: this file is only compiled when the "dilithium" build tag is +// provided, because the system wolfSSL must have been built with +// --enable-dilithium --enable-experimental. +// +// To build and test: +// +// go build -tags dilithium . +// go test -tags dilithium -count=1 ./... + +package wolfSSL + +// #cgo CFLAGS: -g -Wall -I/usr/include -I/usr/include/wolfssl -I/usr/local/include -I/usr/local/include/wolfssl +// #cgo LDFLAGS: -L/usr/local/lib -lwolfssl +// #include +// #include +// #include +// #include +import "C" +import ( + "errors" + "unsafe" +) + +// ML-DSA size constants. Level numbers follow the NIST naming: +// - ML-DSA-44 corresponds to wolfSSL level 2 (DILITHIUM_LEVEL2_*) +// - ML-DSA-65 corresponds to wolfSSL level 3 (DILITHIUM_LEVEL3_*) +// - ML-DSA-87 corresponds to wolfSSL level 5 (DILITHIUM_LEVEL5_*) +const ( + MLDSA_44_PUB_SIZE = int(C.ML_DSA_LEVEL2_PUB_KEY_SIZE) + MLDSA_44_PRIV_SIZE = int(C.ML_DSA_LEVEL2_PRV_KEY_SIZE) + MLDSA_44_SIG_SIZE = int(C.ML_DSA_LEVEL2_SIG_SIZE) + + MLDSA_65_PUB_SIZE = int(C.ML_DSA_LEVEL3_PUB_KEY_SIZE) + MLDSA_65_PRIV_SIZE = int(C.ML_DSA_LEVEL3_PRV_KEY_SIZE) + MLDSA_65_SIG_SIZE = int(C.ML_DSA_LEVEL3_SIG_SIZE) + + MLDSA_87_PUB_SIZE = int(C.ML_DSA_LEVEL5_PUB_KEY_SIZE) + MLDSA_87_PRIV_SIZE = int(C.ML_DSA_LEVEL5_PRV_KEY_SIZE) + MLDSA_87_SIG_SIZE = int(C.ML_DSA_LEVEL5_SIG_SIZE) +) + +// MlDsaLevel selects the ML-DSA security level. +// wolfSSL uses byte values 2, 3, 5 for its internal level representation. +type MlDsaLevel byte + +const ( + MlDsaLevel44 MlDsaLevel = 2 // ML-DSA-44 + MlDsaLevel65 MlDsaLevel = 3 // ML-DSA-65 + MlDsaLevel87 MlDsaLevel = 5 // ML-DSA-87 +) + +func mldsaSizes(level MlDsaLevel) (pub, priv, sig int, err error) { + switch level { + case MlDsaLevel44: + return MLDSA_44_PUB_SIZE, MLDSA_44_PRIV_SIZE, MLDSA_44_SIG_SIZE, nil + case MlDsaLevel65: + return MLDSA_65_PUB_SIZE, MLDSA_65_PRIV_SIZE, MLDSA_65_SIG_SIZE, nil + case MlDsaLevel87: + return MLDSA_87_PUB_SIZE, MLDSA_87_PRIV_SIZE, MLDSA_87_SIG_SIZE, nil + default: + return 0, 0, 0, errors.New("wolfSSL: unknown MlDsaLevel") + } +} + +// dilithiumNewKey allocates and initialises a dilithium_key at the given level. +func dilithiumNewKey(level MlDsaLevel) (*C.dilithium_key, error) { + key := (*C.dilithium_key)(C.calloc(1, C.sizeof_dilithium_key)) + if key == nil { + return nil, errors.New("wolfSSL: calloc dilithium_key failed") + } + if ret := C.wc_dilithium_init(key); ret != 0 { + C.free(unsafe.Pointer(key)) + return nil, errors.New("wolfSSL: wc_dilithium_init failed") + } + if ret := C.wc_dilithium_set_level(key, C.byte(level)); ret != 0 { + C.wc_dilithium_free(key) + C.free(unsafe.Pointer(key)) + return nil, errors.New("wolfSSL: wc_dilithium_set_level failed") + } + return key, nil +} + +func dilithiumFreeKey(key *C.dilithium_key) { + if key != nil { + C.wc_dilithium_free(key) + C.free(unsafe.Pointer(key)) + } +} + +// MlDsaGenerateKey generates an ML-DSA key pair. +func MlDsaGenerateKey(level MlDsaLevel) (publicKey, privateKey []byte, err error) { + pubSz, privSz, _, err := mldsaSizes(level) + if err != nil { + return nil, nil, err + } + + key, err := dilithiumNewKey(level) + if err != nil { + return nil, nil, err + } + defer dilithiumFreeKey(key) + + rng := C.wc_rng_new(nil, 0, nil) + if rng == nil { + return nil, nil, errors.New("wolfSSL: wc_rng_new failed") + } + defer C.wc_rng_free(rng) + + if ret := C.wc_dilithium_make_key(key, rng); ret != 0 { + return nil, nil, errors.New("wolfSSL: wc_dilithium_make_key failed") + } + + pubBuf := make([]byte, pubSz) + pubLen := C.word32(pubSz) + if ret := C.wc_dilithium_export_public(key, + (*C.byte)(&pubBuf[0]), &pubLen); ret != 0 { + return nil, nil, errors.New("wolfSSL: wc_dilithium_export_public failed") + } + pubBuf = pubBuf[:pubLen] + + privBuf := make([]byte, privSz) + privLen := C.word32(privSz) + if ret := C.wc_dilithium_export_private(key, + (*C.byte)(&privBuf[0]), &privLen); ret != 0 { + return nil, nil, errors.New("wolfSSL: wc_dilithium_export_private failed") + } + privBuf = privBuf[:privLen] + + return pubBuf, privBuf, nil +} + +// MlDsaSign signs msg with privateKey and returns the signature. +func MlDsaSign(level MlDsaLevel, privateKey, msg []byte) (signature []byte, err error) { + _, privSz, sigSz, err := mldsaSizes(level) + if err != nil { + return nil, err + } + if len(privateKey) != privSz { + return nil, errors.New("wolfSSL: MlDsaSign: wrong private key length") + } + if len(msg) == 0 { + return nil, errors.New("wolfSSL: MlDsaSign: empty message") + } + + key, err := dilithiumNewKey(level) + if err != nil { + return nil, err + } + defer dilithiumFreeKey(key) + + if ret := C.wc_dilithium_import_private( + (*C.byte)(&privateKey[0]), C.word32(privSz), key); ret != 0 { + return nil, errors.New("wolfSSL: wc_dilithium_import_private failed") + } + + rng := C.wc_rng_new(nil, 0, nil) + if rng == nil { + return nil, errors.New("wolfSSL: wc_rng_new failed") + } + defer C.wc_rng_free(rng) + + sigBuf := make([]byte, sigSz) + sigLen := C.word32(sigSz) + // Use the FIPS-204 context API with ctx=NULL, ctxLen=0 (empty context). + ret := C.wc_dilithium_sign_ctx_msg( + nil, 0, + (*C.byte)(&msg[0]), C.word32(len(msg)), + (*C.byte)(&sigBuf[0]), &sigLen, + key, rng) + if ret != 0 { + return nil, errors.New("wolfSSL: wc_dilithium_sign_ctx_msg failed") + } + + return sigBuf[:sigLen], nil +} + +// MlDsaVerify verifies signature over msg using publicKey. +// Returns (true, nil) on valid signature, (false, nil) on invalid. +func MlDsaVerify(level MlDsaLevel, publicKey, msg, signature []byte) (bool, error) { + pubSz, _, _, err := mldsaSizes(level) + if err != nil { + return false, err + } + if len(publicKey) != pubSz { + return false, errors.New("wolfSSL: MlDsaVerify: wrong public key length") + } + if len(msg) == 0 { + return false, errors.New("wolfSSL: MlDsaVerify: empty message") + } + if len(signature) == 0 { + return false, errors.New("wolfSSL: MlDsaVerify: empty signature") + } + + key, err := dilithiumNewKey(level) + if err != nil { + return false, err + } + defer dilithiumFreeKey(key) + + if ret := C.wc_dilithium_import_public( + (*C.byte)(&publicKey[0]), C.word32(pubSz), key); ret != 0 { + return false, errors.New("wolfSSL: wc_dilithium_import_public failed") + } + + var res C.int + ret := C.wc_dilithium_verify_ctx_msg( + (*C.byte)(&signature[0]), C.word32(len(signature)), + nil, 0, + (*C.byte)(&msg[0]), C.word32(len(msg)), + &res, key) + if ret != 0 { + // A non-zero return typically means the signature is invalid. + return false, nil + } + + return res == 1, nil +} diff --git a/dilithium_test.go b/dilithium_test.go new file mode 100644 index 0000000..005be51 --- /dev/null +++ b/dilithium_test.go @@ -0,0 +1,105 @@ +//go:build dilithium + +/* dilithium_test.go + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package wolfSSL + +import ( + "bytes" + "testing" +) + +func testMlDsaLevel(t *testing.T, level MlDsaLevel) { + t.Helper() + + pub, priv, err := MlDsaGenerateKey(level) + if err != nil { + t.Fatalf("MlDsaGenerateKey: %v", err) + } + + msg := []byte("wolfSSL ML-DSA test message") + + sig, err := MlDsaSign(level, priv, msg) + if err != nil { + t.Fatalf("MlDsaSign: %v", err) + } + + // Valid signature should verify. + ok, err := MlDsaVerify(level, pub, msg, sig) + if err != nil { + t.Fatalf("MlDsaVerify (valid): %v", err) + } + if !ok { + t.Error("expected valid signature to verify successfully") + } + + // Tampered message should not verify. + tampered := append([]byte(nil), msg...) + tampered[0] ^= 0xff + ok, err = MlDsaVerify(level, pub, tampered, sig) + if err != nil { + t.Logf("MlDsaVerify (tampered msg) returned error: %v", err) + } + if ok { + t.Error("expected tampered message to fail verification") + } + + // Tampered signature should not verify. + badSig := append([]byte(nil), sig...) + badSig[0] ^= 0xff + ok, err = MlDsaVerify(level, pub, msg, badSig) + if err != nil { + t.Logf("MlDsaVerify (bad sig) returned error: %v", err) + } + if ok { + t.Error("expected tampered signature to fail verification") + } + + // Signature from a different key should not verify. + pub2, _, err := MlDsaGenerateKey(level) + if err != nil { + t.Fatalf("MlDsaGenerateKey (key2): %v", err) + } + ok, err = MlDsaVerify(level, pub2, msg, sig) + if err != nil { + t.Logf("MlDsaVerify (wrong key) returned error: %v", err) + } + if ok { + t.Error("expected signature to fail with a different public key") + } + + // Size assertions. + pubSz, privSz, sigSz, _ := mldsaSizes(level) + if !bytes.Equal(pub[:pubSz], pub) { + t.Errorf("public key length mismatch: got %d, want %d", len(pub), pubSz) + } + if !bytes.Equal(priv[:privSz], priv) { + t.Errorf("private key length mismatch: got %d, want %d", len(priv), privSz) + } + if len(sig) > sigSz { + t.Errorf("signature length %d exceeds max %d", len(sig), sigSz) + } +} + +func TestMlDsa44(t *testing.T) { testMlDsaLevel(t, MlDsaLevel44) } +func TestMlDsa65(t *testing.T) { testMlDsaLevel(t, MlDsaLevel65) } +func TestMlDsa87(t *testing.T) { testMlDsaLevel(t, MlDsaLevel87) } diff --git a/mlkem.go b/mlkem.go new file mode 100644 index 0000000..211b598 --- /dev/null +++ b/mlkem.go @@ -0,0 +1,259 @@ +/* mlkem.go + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +// ML-KEM (CRYSTALS-Kyber) wrappers. +// +// Requires wolfSSL built with --enable-mlkem (and optionally +// --enable-experimental if using a pre-release wolfSSL). +// The WOLFSSL_HAVE_MLKEM symbol must be present in wolfssl/options.h. + +package wolfSSL + +// #cgo CFLAGS: -g -Wall -I/usr/include -I/usr/include/wolfssl -I/usr/local/include -I/usr/local/include/wolfssl +// #cgo LDFLAGS: -L/usr/local/lib -lwolfssl +// #include +// #include +// #ifdef WOLFSSL_HAVE_MLKEM +// #include +// #endif +// +// /* Stub out the entire API when wolfSSL was built without ML-KEM support. */ +// #ifndef WOLFSSL_HAVE_MLKEM +// #define WC_ML_KEM_512 0 +// #define WC_ML_KEM_768 1 +// #define WC_ML_KEM_1024 2 +// #define WC_ML_KEM_512_PUBLIC_KEY_SIZE 1 +// #define WC_ML_KEM_512_PRIVATE_KEY_SIZE 1 +// #define WC_ML_KEM_512_CIPHER_TEXT_SIZE 1 +// #define WC_ML_KEM_768_PUBLIC_KEY_SIZE 1 +// #define WC_ML_KEM_768_PRIVATE_KEY_SIZE 1 +// #define WC_ML_KEM_768_CIPHER_TEXT_SIZE 1 +// #define WC_ML_KEM_1024_PUBLIC_KEY_SIZE 1 +// #define WC_ML_KEM_1024_PRIVATE_KEY_SIZE 1 +// #define WC_ML_KEM_1024_CIPHER_TEXT_SIZE 1 +// #define WC_ML_KEM_SS_SZ 1 +// struct MlKemKey { int dummy; }; +// typedef struct MlKemKey MlKemKey; +// static MlKemKey* wc_MlKemKey_New(int type, void* heap, int devId) +// { (void)type; (void)heap; (void)devId; return NULL; } +// static int wc_MlKemKey_Delete(MlKemKey* key, MlKemKey** key_p) +// { (void)key; (void)key_p; return -174; } +// static int wc_MlKemKey_Free(MlKemKey* key) +// { (void)key; return -174; } +// static int wc_MlKemKey_MakeKey(MlKemKey* key, WC_RNG* rng) +// { (void)key; (void)rng; return -174; } +// static int wc_MlKemKey_EncodePublicKey(MlKemKey* key, unsigned char* out, word32 len) +// { (void)key; (void)out; (void)len; return -174; } +// static int wc_MlKemKey_EncodePrivateKey(MlKemKey* key, unsigned char* out, word32 len) +// { (void)key; (void)out; (void)len; return -174; } +// static int wc_MlKemKey_DecodePublicKey(MlKemKey* key, const unsigned char* in, word32 len) +// { (void)key; (void)in; (void)len; return -174; } +// static int wc_MlKemKey_DecodePrivateKey(MlKemKey* key, const unsigned char* in, word32 len) +// { (void)key; (void)in; (void)len; return -174; } +// static int wc_MlKemKey_Encapsulate(MlKemKey* key, unsigned char* ct, unsigned char* ss, WC_RNG* rng) +// { (void)key; (void)ct; (void)ss; (void)rng; return -174; } +// static int wc_MlKemKey_Decapsulate(MlKemKey* key, unsigned char* ss, const unsigned char* ct, word32 ctSz) +// { (void)key; (void)ss; (void)ct; (void)ctSz; return -174; } +// #endif +import "C" +import ( + "errors" +) + +// ML-KEM size constants (raw byte lengths, not DER-encoded). +const ( + MLKEM_512_PUB_SIZE = int(C.WC_ML_KEM_512_PUBLIC_KEY_SIZE) + MLKEM_512_PRIV_SIZE = int(C.WC_ML_KEM_512_PRIVATE_KEY_SIZE) + MLKEM_512_CIPHERTEXT_SIZE = int(C.WC_ML_KEM_512_CIPHER_TEXT_SIZE) + MLKEM_512_SHARED_SIZE = int(C.WC_ML_KEM_SS_SZ) + + MLKEM_768_PUB_SIZE = int(C.WC_ML_KEM_768_PUBLIC_KEY_SIZE) + MLKEM_768_PRIV_SIZE = int(C.WC_ML_KEM_768_PRIVATE_KEY_SIZE) + MLKEM_768_CIPHERTEXT_SIZE = int(C.WC_ML_KEM_768_CIPHER_TEXT_SIZE) + MLKEM_768_SHARED_SIZE = int(C.WC_ML_KEM_SS_SZ) + + MLKEM_1024_PUB_SIZE = int(C.WC_ML_KEM_1024_PUBLIC_KEY_SIZE) + MLKEM_1024_PRIV_SIZE = int(C.WC_ML_KEM_1024_PRIVATE_KEY_SIZE) + MLKEM_1024_CIPHERTEXT_SIZE = int(C.WC_ML_KEM_1024_CIPHER_TEXT_SIZE) + MLKEM_1024_SHARED_SIZE = int(C.WC_ML_KEM_SS_SZ) +) + +// MlKemLevel selects between the three ML-KEM parameter sets. +type MlKemLevel int + +const ( + MlKemLevel512 MlKemLevel = C.WC_ML_KEM_512 + MlKemLevel768 MlKemLevel = C.WC_ML_KEM_768 + MlKemLevel1024 MlKemLevel = C.WC_ML_KEM_1024 +) + +// pubPrivSizes returns (pubSz, privSz, ctSz, ssSz) for the given level. +func mlkemSizes(level MlKemLevel) (pub, priv, ct, ss int, err error) { + switch level { + case MlKemLevel512: + return MLKEM_512_PUB_SIZE, MLKEM_512_PRIV_SIZE, MLKEM_512_CIPHERTEXT_SIZE, MLKEM_512_SHARED_SIZE, nil + case MlKemLevel768: + return MLKEM_768_PUB_SIZE, MLKEM_768_PRIV_SIZE, MLKEM_768_CIPHERTEXT_SIZE, MLKEM_768_SHARED_SIZE, nil + case MlKemLevel1024: + return MLKEM_1024_PUB_SIZE, MLKEM_1024_PRIV_SIZE, MLKEM_1024_CIPHERTEXT_SIZE, MLKEM_1024_SHARED_SIZE, nil + default: + return 0, 0, 0, 0, errors.New("wolfSSL: unknown MlKemLevel") + } +} + +// mlkemNewKey allocates a new MlKemKey via wolfSSL's own allocator. +func mlkemNewKey(level MlKemLevel) (*C.MlKemKey, error) { + key := C.wc_MlKemKey_New(C.int(level), nil, C.int(C.INVALID_DEVID)) + if key == nil { + return nil, errors.New("wolfSSL: wc_MlKemKey_New failed (WOLFSSL_HAVE_MLKEM not enabled?)") + } + return key, nil +} + +func mlkemFreeKey(key *C.MlKemKey) { + C.wc_MlKemKey_Delete(key, nil) +} + +// mlkemNewRng allocates and initialises a WC_RNG via wolfSSL's own allocator. +func mlkemNewRng() (*C.WC_RNG, error) { + rng := C.wc_rng_new(nil, 0, nil) + if rng == nil { + return nil, errors.New("wolfSSL: wc_rng_new failed") + } + return rng, nil +} + +// MlKemGenerateKey generates an ML-KEM key pair at the requested security +// level. publicKey and privateKey are raw (non-DER) byte slices whose lengths +// match the MLKEM_*_PUB_SIZE / MLKEM_*_PRIV_SIZE constants. +func MlKemGenerateKey(level MlKemLevel) (publicKey, privateKey []byte, err error) { + pubSz, privSz, _, _, err := mlkemSizes(level) + if err != nil { + return nil, nil, err + } + + key, err := mlkemNewKey(level) + if err != nil { + return nil, nil, err + } + defer mlkemFreeKey(key) + + rng, err := mlkemNewRng() + if err != nil { + return nil, nil, err + } + defer C.wc_rng_free(rng) + + if ret := C.wc_MlKemKey_MakeKey(key, rng); ret != 0 { + return nil, nil, errors.New("wolfSSL: wc_MlKemKey_MakeKey failed") + } + + pubBuf := make([]byte, pubSz) + if ret := C.wc_MlKemKey_EncodePublicKey(key, + (*C.uchar)(&pubBuf[0]), C.word32(pubSz)); ret != 0 { + return nil, nil, errors.New("wolfSSL: wc_MlKemKey_EncodePublicKey failed") + } + + privBuf := make([]byte, privSz) + if ret := C.wc_MlKemKey_EncodePrivateKey(key, + (*C.uchar)(&privBuf[0]), C.word32(privSz)); ret != 0 { + return nil, nil, errors.New("wolfSSL: wc_MlKemKey_EncodePrivateKey failed") + } + + return pubBuf, privBuf, nil +} + +// MlKemEncapsulate generates a shared secret and encapsulates it under +// publicKey. Returns (ciphertext, sharedSecret, error). +func MlKemEncapsulate(level MlKemLevel, publicKey []byte) (ciphertext, sharedSecret []byte, err error) { + pubSz, _, ctSz, ssSz, err := mlkemSizes(level) + if err != nil { + return nil, nil, err + } + if len(publicKey) != pubSz { + return nil, nil, errors.New("wolfSSL: MlKemEncapsulate: wrong public key length") + } + + key, err := mlkemNewKey(level) + if err != nil { + return nil, nil, err + } + defer mlkemFreeKey(key) + + if ret := C.wc_MlKemKey_DecodePublicKey(key, + (*C.uchar)(&publicKey[0]), C.word32(pubSz)); ret != 0 { + return nil, nil, errors.New("wolfSSL: wc_MlKemKey_DecodePublicKey failed") + } + + rng, err := mlkemNewRng() + if err != nil { + return nil, nil, err + } + defer C.wc_rng_free(rng) + + ctBuf := make([]byte, ctSz) + ssBuf := make([]byte, ssSz) + + if ret := C.wc_MlKemKey_Encapsulate(key, + (*C.uchar)(&ctBuf[0]), + (*C.uchar)(&ssBuf[0]), + rng); ret != 0 { + return nil, nil, errors.New("wolfSSL: wc_MlKemKey_Encapsulate failed") + } + + return ctBuf, ssBuf, nil +} + +// MlKemDecapsulate recovers the shared secret from ciphertext using +// privateKey. +func MlKemDecapsulate(level MlKemLevel, privateKey, ciphertext []byte) (sharedSecret []byte, err error) { + _, privSz, ctSz, ssSz, err := mlkemSizes(level) + if err != nil { + return nil, err + } + if len(privateKey) != privSz { + return nil, errors.New("wolfSSL: MlKemDecapsulate: wrong private key length") + } + if len(ciphertext) != ctSz { + return nil, errors.New("wolfSSL: MlKemDecapsulate: wrong ciphertext length") + } + + key, err := mlkemNewKey(level) + if err != nil { + return nil, err + } + defer mlkemFreeKey(key) + + if ret := C.wc_MlKemKey_DecodePrivateKey(key, + (*C.uchar)(&privateKey[0]), C.word32(privSz)); ret != 0 { + return nil, errors.New("wolfSSL: wc_MlKemKey_DecodePrivateKey failed") + } + + ssBuf := make([]byte, ssSz) + if ret := C.wc_MlKemKey_Decapsulate(key, + (*C.uchar)(&ssBuf[0]), + (*C.uchar)(&ciphertext[0]), + C.word32(ctSz)); ret != 0 { + return nil, errors.New("wolfSSL: wc_MlKemKey_Decapsulate failed") + } + + return ssBuf, nil +} diff --git a/mlkem_test.go b/mlkem_test.go new file mode 100644 index 0000000..6e53e02 --- /dev/null +++ b/mlkem_test.go @@ -0,0 +1,116 @@ +/* mlkem_test.go + * + * Copyright (C) 2006-2025 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +package wolfSSL + +import ( + "bytes" + "testing" +) + +// testMlKemLevel runs the full keygen → encap → decap round-trip for one level. +func testMlKemLevel(t *testing.T, level MlKemLevel, pubSz, privSz, ctSz, ssSz int) { + t.Helper() + + pubA, privA, err := MlKemGenerateKey(level) + if err != nil { + t.Fatalf("MlKemGenerateKey: %v", err) + } + if len(pubA) != pubSz { + t.Errorf("public key length: got %d, want %d", len(pubA), pubSz) + } + if len(privA) != privSz { + t.Errorf("private key length: got %d, want %d", len(privA), privSz) + } + + ct, ssEnc, err := MlKemEncapsulate(level, pubA) + if err != nil { + t.Fatalf("MlKemEncapsulate: %v", err) + } + if len(ct) != ctSz { + t.Errorf("ciphertext length: got %d, want %d", len(ct), ctSz) + } + if len(ssEnc) != ssSz { + t.Errorf("encap shared secret length: got %d, want %d", len(ssEnc), ssSz) + } + + ssDec, err := MlKemDecapsulate(level, privA, ct) + if err != nil { + t.Fatalf("MlKemDecapsulate: %v", err) + } + if len(ssDec) != ssSz { + t.Errorf("decap shared secret length: got %d, want %d", len(ssDec), ssSz) + } + + if !bytes.Equal(ssEnc, ssDec) { + t.Error("shared secrets do not match after round-trip") + } +} + +func TestMlKem512(t *testing.T) { + testMlKemLevel(t, MlKemLevel512, + MLKEM_512_PUB_SIZE, MLKEM_512_PRIV_SIZE, + MLKEM_512_CIPHERTEXT_SIZE, MLKEM_512_SHARED_SIZE) +} + +func TestMlKem768(t *testing.T) { + testMlKemLevel(t, MlKemLevel768, + MLKEM_768_PUB_SIZE, MLKEM_768_PRIV_SIZE, + MLKEM_768_CIPHERTEXT_SIZE, MLKEM_768_SHARED_SIZE) +} + +func TestMlKem1024(t *testing.T) { + testMlKemLevel(t, MlKemLevel1024, + MLKEM_1024_PUB_SIZE, MLKEM_1024_PRIV_SIZE, + MLKEM_1024_CIPHERTEXT_SIZE, MLKEM_1024_SHARED_SIZE) +} + +// TestMlKemWrongKey: encapsulate to key A, decapsulate with key B. +// Under ML-KEM implicit rejection semantics the decapsulation always +// "succeeds" (returns no error) but produces a different shared secret. +func TestMlKemWrongKey(t *testing.T) { + level := MlKemLevel768 + + pubA, _, err := MlKemGenerateKey(level) + if err != nil { + t.Fatalf("keygen A: %v", err) + } + _, privB, err := MlKemGenerateKey(level) + if err != nil { + t.Fatalf("keygen B: %v", err) + } + + ct, ssA, err := MlKemEncapsulate(level, pubA) + if err != nil { + t.Fatalf("encapsulate: %v", err) + } + + ssB, err := MlKemDecapsulate(level, privB, ct) + if err != nil { + // Some wolfSSL builds may return an error; that is also acceptable. + t.Logf("MlKemDecapsulate with wrong key returned error (acceptable): %v", err) + return + } + + if bytes.Equal(ssA, ssB) { + t.Error("shared secrets should differ when decapsulating with the wrong key") + } +} diff --git a/wolftls/conn.go b/wolftls/conn.go index 84e5015..3f121ee 100644 --- a/wolftls/conn.go +++ b/wolftls/conn.go @@ -21,8 +21,8 @@ package wolftls -// #cgo CFLAGS: -g -Wall -I/usr/include -I/usr/include/wolfssl -I/usr/local/include -I/usr/local/include/wolfssl -// #cgo LDFLAGS: -L/usr/local/lib -lwolfssl -lm +// #cgo CFLAGS: -g -Wall -I/usr/include -I/usr/include/wolfssl +// #cgo LDFLAGS: -L/usr/lib -lwolfssl -lm import "C" import ( diff --git a/wolfx509/certgen_wolfcrypt.go b/wolfx509/certgen_wolfcrypt.go index 2c8f366..558c11d 100644 --- a/wolfx509/certgen_wolfcrypt.go +++ b/wolfx509/certgen_wolfcrypt.go @@ -21,8 +21,8 @@ package wolfx509 -// #cgo CFLAGS: -g -Wall -DWC_CTC_NAME_SIZE=256 -I/usr/include -I/usr/include/wolfssl -I/usr/local/include -I/usr/local/include/wolfssl -// #cgo LDFLAGS: -L/usr/local/lib -lwolfssl -lm +// #cgo CFLAGS: -g -Wall -DWC_CTC_NAME_SIZE=256 -I/usr/include -I/usr/include/wolfssl +// #cgo LDFLAGS: -L/usr/lib -lwolfssl -lm // #include // #include // #include