diff --git a/.gitignore b/.gitignore index 6c4ab8c..5ac207e 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,8 @@ Godeps.json tmp/ # Keep go.mod and go.sum tracked -# (do not ignore them) \ No newline at end of file +# (do not ignore them) + +# Ignore local CLI tool binary +k-mosaic-cli +cmd/k-mosaic-cli/k-mosaic-cli \ No newline at end of file diff --git a/BenchmarkReport.md b/BenchmarkReport.md index 0e431d0..a78a863 100644 --- a/BenchmarkReport.md +++ b/BenchmarkReport.md @@ -1,6 +1,6 @@ # kMOSAIC Go Benchmark Report -**Date:** December 29, 2025 +**Date:** December 31, 2025 **Platform:** macOS (darwin/arm64) **CPU:** Apple M2 Pro **Go Version:** Latest stable @@ -15,12 +15,12 @@ This report presents a comprehensive performance analysis comparing kMOSAIC's tw | Metric | MOS-128 | MOS-256 | Slowdown Factor | | --------------- | -------- | -------- | --------------- | -| KEM KeyGen | 6.04 ms | 22.25 ms | **3.68×** | -| KEM Encapsulate | 0.30 ms | 0.93 ms | **3.07×** | -| KEM Decapsulate | 0.34 ms | 0.99 ms | **2.94×** | -| Sign KeyGen | 6.11 ms | 22.31 ms | **3.65×** | -| Sign | 0.012 ms | 0.022 ms | **1.86×** | -| Verify | 2.36 ms | 9.11 ms | **3.86×** | +| KEM KeyGen | 6.29 ms | 22.43 ms | **3.56×** | +| KEM Encapsulate | 0.32 ms | 0.95 ms | **2.99×** | +| KEM Decapsulate | 0.38 ms | 1.06 ms | **2.80×** | +| Sign KeyGen | 6.22 ms | 22.49 ms | **3.62×** | +| Sign | 0.012 ms | 0.022 ms | **1.82×** | +| Verify | 2.44 ms | 9.13 ms | **3.74×** | --- @@ -32,12 +32,12 @@ This report presents a comprehensive performance analysis comparing kMOSAIC's tw | Level | Time (ns/op) | Time (ms) | Memory (B/op) | Allocations | | ------- | ------------ | ------------ | ---------------------- | ----------- | -| MOS-128 | 6,041,387 | **6.04 ms** | 3,703,413 (~3.53 MB) | 314 | -| MOS-256 | 22,250,110 | **22.25 ms** | 14,269,288 (~13.61 MB) | 422 | +| MOS-128 | 6,294,439 | **6.29 ms** | 3,703,685 (~3.53 MB) | 317 | +| MOS-256 | 22,426,476 | **22.43 ms** | 14,269,239 (~13.61 MB) | 424 | **Analysis:** -- MOS-256 key generation is **3.68× slower** than MOS-128 +- MOS-256 key generation is **3.56× slower** than MOS-128 - Memory consumption increases by **3.85×** (from ~3.5 MB to ~13.6 MB) - The higher cost stems from larger lattice dimensions, tensor sizes, and expander graph parameters @@ -45,12 +45,12 @@ This report presents a comprehensive performance analysis comparing kMOSAIC's tw | Level | Time (ns/op) | Time (ms) | Memory (B/op) | Allocations | | ------- | ------------ | ----------- | ----------------- | ----------- | -| MOS-128 | 304,337 | **0.30 ms** | 63,132 (~62 KB) | 89 | -| MOS-256 | 934,400 | **0.93 ms** | 121,369 (~118 KB) | 110 | +| MOS-128 | 316,474 | **0.32 ms** | 64,527 (~63 KB) | 121 | +| MOS-256 | 948,806 | **0.95 ms** | 121,371 (~118 KB) | 110 | **Analysis:** -- MOS-256 encapsulation is **3.07× slower** than MOS-128 +- MOS-256 encapsulation is **3.00× slower** than MOS-128 - Memory usage nearly doubles (1.92×) - Allocation count remains low, indicating efficient allocation patterns @@ -58,12 +58,12 @@ This report presents a comprehensive performance analysis comparing kMOSAIC's tw | Level | Time (ns/op) | Time (ms) | Memory (B/op) | Allocations | | ------- | ------------ | ----------- | ----------------- | ----------- | -| MOS-128 | 336,870 | **0.34 ms** | 97,561 (~95 KB) | 122 | -| MOS-256 | 991,434 | **0.99 ms** | 182,938 (~179 KB) | 143 | +| MOS-128 | 376,690 | **0.38 ms** | 112,156 (~109 KB) | 185 | +| MOS-256 | 1,055,179 | **1.06 ms** | 182,942 (~179 KB) | 143 | **Analysis:** -- MOS-256 decapsulation is **2.94× slower** than MOS-128 +- MOS-256 decapsulation is **2.80× slower** than MOS-128 - Decapsulation is slightly slower than encapsulation (~10% overhead) at both levels - Memory overhead ratio is 1.87× @@ -71,8 +71,8 @@ This report presents a comprehensive performance analysis comparing kMOSAIC's tw | Level | Time (ns/op) | Time (ms) | Memory (B/op) | Allocations | | ------- | ------------ | ----------- | ----------------- | ----------- | -| MOS-128 | 308,167 | **0.31 ms** | 63,709 (~62 KB) | 99 | -| MOS-256 | 953,193 | **0.95 ms** | 121,954 (~119 KB) | 120 | +| MOS-128 | 320,506 | **0.32 ms** | 66,210 (~64 KB) | 129 | +| MOS-256 | 936,247 | **0.94 ms** | 121,951 (~119 KB) | 120 | **Analysis:** @@ -83,8 +83,8 @@ This report presents a comprehensive performance analysis comparing kMOSAIC's tw | Level | Time (ns/op) | Time (ms) | Memory (B/op) | Allocations | | ------- | ------------ | ----------- | ----------------- | ----------- | -| MOS-128 | 333,909 | **0.33 ms** | 97,862 (~96 KB) | 129 | -| MOS-256 | 1,011,805 | **1.01 ms** | 183,248 (~179 KB) | 150 | +| MOS-128 | 407,390 | **0.41 ms** | 113,621 (~111 KB) | 192 | +| MOS-256 | 987,145 | **0.99 ms** | 183,241 (~179 KB) | 150 | **Analysis:** @@ -99,8 +99,8 @@ This report presents a comprehensive performance analysis comparing kMOSAIC's tw | Level | Time (ns/op) | Time (ms) | Memory (B/op) | Allocations | | ------- | ------------ | ------------ | ---------------------- | ----------- | -| MOS-128 | 6,113,639 | **6.11 ms** | 3,703,573 (~3.53 MB) | 315 | -| MOS-256 | 22,311,797 | **22.31 ms** | 14,269,156 (~13.61 MB) | 421 | +| MOS-128 | 6,217,396 | **6.22 ms** | 3,703,625 (~3.53 MB) | 317 | +| MOS-256 | 22,494,461 | **22.49 ms** | 14,269,639 (~13.61 MB) | 423 | **Analysis:** @@ -111,13 +111,20 @@ This report presents a comprehensive performance analysis comparing kMOSAIC's tw | Level | Time (ns/op) | Time (μs) | Memory (B/op) | Allocations | | ------- | ------------ | ------------ | ----------------- | ----------- | -| MOS-128 | 11,657 | **11.66 μs** | 7,348 (~7.2 KB) | 14 | -| MOS-256 | 21,632 | **21.63 μs** | 23,739 (~23.2 KB) | 17 | +| MOS-128 | 12,066 | **12.07 μs** | 7,348 (~7.2 KB) | 14 | +| MOS-256 | 21,945 | **21.95 μs** | 21,739 (~21.2 KB) | 17 | + +#### 2.3 Verify + +| Level | Time (ns/op) | Time (ms) | Memory (B/op) | Allocations | +| ------- | ------------ | ----------- | -------------------- | ----------- | +| MOS-128 | 2,441,008 | **2.44 ms** | 1,695,938 (~1.62 MB) | 9 | +| MOS-256 | 9,132,425 | **9.13 ms** | 6,684,872 (~6.38 MB) | 9 | **Analysis:** - Signing is **very fast** at both security levels -- MOS-256 is only **1.86× slower** than MOS-128 (best ratio among all operations) +- MOS-256 is only **1.82× slower** than MOS-128 (best ratio among all operations) - Very low memory footprint compared to other operations - Can sign **~85,800 messages/second** at MOS-128 level - Can sign **~46,200 messages/second** at MOS-256 level @@ -132,7 +139,7 @@ This report presents a comprehensive performance analysis comparing kMOSAIC's tw **Analysis:** - Verification is approximately **200× slower** than signing at both security levels -- MOS-256 verification is **3.86× slower** than MOS-128 +- MOS-256 verification is **3.74× slower** than MOS-128 - Memory consumption is significant due to tensor operations during verification - Allocation count is minimal (9), indicating bulk memory operations @@ -177,19 +184,19 @@ These benchmarks measure complete cryptographic workflows including key generati Operation MOS-128 MOS-256 Ratio ───────────────────────────────────────────────────────────── KEM KeyGen █████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 6.04 ms - ████████████████████░░░░░░░░░░░░░░ 22.25 ms 3.68× + ████████████████████░░░░░░░░░░░░░░ 22.43 ms 3.56× -KEM Encap █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.30 ms - ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.93 ms 3.07× +KEM Encap █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.32 ms + ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.95 ms 3.00× -KEM Decap █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.34 ms - ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.99 ms 2.94× +KEM Decap █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.38 ms + ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 1.06 ms 2.80× Sign ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.01 ms - ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.02 ms 1.86× + ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.02 ms 1.82× Verify ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 2.36 ms - █████████░░░░░░░░░░░░░░░░░░░░░░░░░ 9.11 ms 3.86× + █████████░░░░░░░░░░░░░░░░░░░░░░░░░ 9.13 ms 3.74× ``` ### Memory Consumption Comparison @@ -265,30 +272,30 @@ goarch: arm64 pkg: github.com/BackendStack21/k-mosaic-go/test cpu: Apple M2 Pro -BenchmarkKEM_GenerateKeyPair_MOS128-10 194 6,041,387 ns/op 3,703,413 B/op 314 allocs/op -BenchmarkKEM_Encapsulate_MOS128-10 3832 304,337 ns/op 63,132 B/op 89 allocs/op -BenchmarkKEM_Decapsulate_MOS128-10 3445 336,870 ns/op 97,561 B/op 122 allocs/op -BenchmarkKEM_Encrypt_MOS128-10 3849 308,167 ns/op 63,709 B/op 99 allocs/op -BenchmarkKEM_Decrypt_MOS128-10 3439 333,909 ns/op 97,862 B/op 129 allocs/op - -BenchmarkKEM_GenerateKeyPair_MOS256-10 49 22,250,110 ns/op 14,269,288 B/op 422 allocs/op -BenchmarkKEM_Encapsulate_MOS256-10 1294 934,400 ns/op 121,369 B/op 110 allocs/op -BenchmarkKEM_Decapsulate_MOS256-10 1202 991,434 ns/op 182,938 B/op 143 allocs/op -BenchmarkKEM_Encrypt_MOS256-10 1273 953,193 ns/op 121,954 B/op 120 allocs/op -BenchmarkKEM_Decrypt_MOS256-10 1209 1,011,805 ns/op 183,248 B/op 150 allocs/op - -BenchmarkSign_GenerateKeyPair_MOS128-10 195 6,113,639 ns/op 3,703,573 B/op 315 allocs/op -BenchmarkSign_Sign_MOS128-10 102409 11,657 ns/op 7,348 B/op 14 allocs/op -BenchmarkSign_Verify_MOS128-10 507 2,360,853 ns/op 1,695,942 B/op 9 allocs/op - -BenchmarkSign_GenerateKeyPair_MOS256-10 49 22,311,797 ns/op 14,269,156 B/op 421 allocs/op -BenchmarkSign_Sign_MOS256-10 55612 21,632 ns/op 23,739 B/op 17 allocs/op -BenchmarkSign_Verify_MOS256-10 130 9,109,609 ns/op 6,684,872 B/op 9 allocs/op - -BenchmarkKEM_FullRoundTrip_MOS128-10 174 6,957,989 ns/op 3,865,525 B/op 544 allocs/op -BenchmarkKEM_FullRoundTrip_MOS256-10 45 24,377,706 ns/op 14,574,141 B/op 691 allocs/op -BenchmarkSign_FullRoundTrip_MOS128-10 136 8,543,678 ns/op 5,408,387 B/op 341 allocs/op -BenchmarkSign_FullRoundTrip_MOS256-10 36 32,230,172 ns/op 20,980,555 B/op 455 allocs/op +BenchmarkKEM_GenerateKeyPair_MOS128-10 183 6,294,439 ns/op 3,703,685 B/op 317 allocs/op +BenchmarkKEM_Encapsulate_MOS128-10 3772 316,474 ns/op 64,527 B/op 121 allocs/op +BenchmarkKEM_Decapsulate_MOS128-10 3138 376,690 ns/op 112,156 B/op 185 allocs/op +BenchmarkKEM_Encrypt_MOS128-10 3553 320,506 ns/op 66,210 B/op 129 allocs/op +BenchmarkKEM_Decrypt_MOS128-10 3057 407,390 ns/op 113,621 B/op 192 allocs/op + +BenchmarkKEM_GenerateKeyPair_MOS256-10 50 22,426,476 ns/op 14,269,239 B/op 424 allocs/op +BenchmarkKEM_Encapsulate_MOS256-10 1263 948,806 ns/op 121,371 B/op 110 allocs/op +BenchmarkKEM_Decapsulate_MOS256-10 1131 1,055,179 ns/op 182,942 B/op 143 allocs/op +BenchmarkKEM_Encrypt_MOS256-10 1290 936,247 ns/op 121,951 B/op 120 allocs/op +BenchmarkKEM_Decrypt_MOS256-10 1195 987,145 ns/op 183,241 B/op 150 allocs/op + +BenchmarkSign_GenerateKeyPair_MOS128-10 193 6,217,396 ns/op 3,703,625 B/op 317 allocs/op +BenchmarkSign_Sign_MOS128-10 98024 12,066 ns/op 7,348 B/op 14 allocs/op +BenchmarkSign_Verify_MOS128-10 494 2,441,008 ns/op 1,695,938 B/op 9 allocs/op + +BenchmarkSign_GenerateKeyPair_MOS256-10 49 22,494,461 ns/op 14,269,639 B/op 423 allocs/op +BenchmarkSign_Sign_MOS256-10 53791 21,945 ns/op 21,739 B/op 17 allocs/op +BenchmarkSign_Verify_MOS256-10 130 9,132,425 ns/op 6,684,872 B/op 9 allocs/op + +BenchmarkKEM_FullRoundTrip_MOS128-10 171 7,014,679 ns/op 3,883,791 B/op 639 allocs/op +BenchmarkKEM_FullRoundTrip_MOS256-10 44 24,670,982 ns/op 14,601,994 B/op 787 allocs/op +BenchmarkSign_FullRoundTrip_MOS128-10 136 8,734,303 ns/op 5,408,413 B/op 344 allocs/op +BenchmarkSign_FullRoundTrip_MOS256-10 36 32,496,609 ns/op 20,981,376 B/op 460 allocs/op ``` --- diff --git a/cmd/k-mosaic-cli/main.go b/cmd/k-mosaic-cli/main.go index 0aac5b2..699d25a 100644 --- a/cmd/k-mosaic-cli/main.go +++ b/cmd/k-mosaic-cli/main.go @@ -3,7 +3,9 @@ package main import ( "bytes" + "crypto/rand" "encoding/base64" + "encoding/binary" "encoding/hex" "encoding/json" "fmt" @@ -13,12 +15,15 @@ import ( "time" kmosaic "github.com/BackendStack21/k-mosaic-go" + "github.com/BackendStack21/k-mosaic-go/entanglement" "github.com/BackendStack21/k-mosaic-go/kem" + "github.com/BackendStack21/k-mosaic-go/problems/slss" "github.com/BackendStack21/k-mosaic-go/sign" + "github.com/BackendStack21/k-mosaic-go/utils" ) const ( - version = "1.0.0" + version = "1.0.1" appName = "k-mosaic-cli" ) @@ -164,12 +169,18 @@ func handleKEM(args []string) { kemKeygen(args[1:]) case "encapsulate", "encap": kemEncapsulate(args[1:]) + case "encapsulate-deterministic", "encap-det": + kemEncapsulateDet(args[1:]) case "decapsulate", "decap": kemDecapsulate(args[1:]) case "encrypt", "enc": kemEncrypt(args[1:]) case "decrypt", "dec": kemDecrypt(args[1:]) + case "slss-debug": + kemSLSSDebug(args[1:]) + case "pk-inspect": + kemPKInspect(args[1:]) case "help", "--help", "-h": printKEMUsage() default: @@ -228,12 +239,18 @@ func kemKeygen(args []string) { // Serialize key pair pkBytes := kem.SerializePublicKey(&kp.PublicKey) - skBytes := kem.SerializeSecretKey(&kp.SecretKey) + + // Convert secret key to JSON format + skJSON, err := secretKeyToJSON(&kp.SecretKey) + if err != nil { + fmt.Fprintf(os.Stderr, "Error serializing secret key: %v\n", err) + os.Exit(1) + } export := KEMKeyPairExport{ SecurityLevel: string(config.SecurityLevel), PublicKey: encodeBytes(pkBytes, config.OutputFormat), - SecretKey: encodeBytes(skBytes, config.OutputFormat), + SecretKey: base64.StdEncoding.EncodeToString([]byte(skJSON)), CreatedAt: time.Now().UTC().Format(time.RFC3339), } @@ -248,7 +265,7 @@ func kemKeygen(args []string) { if config.Verbose { fmt.Fprintf(os.Stderr, "Generated KEM key pair with security level: %s\n", config.SecurityLevel) fmt.Fprintf(os.Stderr, "Public key size: %d bytes\n", len(pkBytes)) - fmt.Fprintf(os.Stderr, "Secret key size: %d bytes\n", len(skBytes)) + fmt.Fprintf(os.Stderr, "Secret key size: %d bytes (JSON)\n", len(skJSON)) } } @@ -309,6 +326,160 @@ func kemEncapsulate(args []string) { } } +func kemEncapsulateDet(args []string) { + config := parseConfig(args) + pkFile := getArg(args, "--public-key", "-pk") + ephemeralHex := getArg(args, "--ephemeral-secret", "-es") + + if pkFile == "" { + fmt.Fprintf(os.Stderr, "Error: --public-key is required\n") + os.Exit(1) + } + + if ephemeralHex == "" { + fmt.Fprintf(os.Stderr, "Error: --ephemeral-secret is required (hex 64 chars)\n") + os.Exit(1) + } + + ephemeral, err := hex.DecodeString(ephemeralHex) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid ephemeral-secret hex: %v\n", err) + os.Exit(1) + } + if len(ephemeral) != 32 { + fmt.Fprintf(os.Stderr, "Ephemeral secret must be exactly 32 bytes\n") + os.Exit(1) + } + + // Load public key + pkData, err := loadKeyFromFile(pkFile, "public_key") + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading public key: %v\n", err) + os.Exit(1) + } + + pk, err := kem.DeserializePublicKey(pkData) + if err != nil { + fmt.Fprintf(os.Stderr, "Error deserializing public key: %v\n", err) + os.Exit(1) + } + + start := time.Now() + result, err := kem.EncapsulateDeterministic(pk, ephemeral) + elapsed := time.Since(start) + + if err != nil { + fmt.Fprintf(os.Stderr, "Error encapsulating deterministically: %v\n", err) + os.Exit(1) + } + + if config.Timing { + fmt.Fprintf(os.Stderr, "Deterministic encapsulation took: %v\n", elapsed) + } + + ctBytes := kem.SerializeCiphertext(&result.Ciphertext) + + export := EncapsulationExport{ + Ciphertext: encodeBytes(ctBytes, config.OutputFormat), + SharedSecret: encodeBytes(result.SharedSecret, config.OutputFormat), + } + + output, err := json.MarshalIndent(export, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "Error marshaling output: %v\n", err) + os.Exit(1) + } + + writeOutput(output, config.OutputFile) + + if config.Verbose { + fmt.Fprintf(os.Stderr, "Encapsulation (deterministic) successful\n") + fmt.Fprintf(os.Stderr, "Ciphertext size: %d bytes\n", len(ctBytes)) + fmt.Fprintf(os.Stderr, "Shared secret size: %d bytes\n", len(result.SharedSecret)) + } +} + +func kemPKInspect(args []string) { + config := parseConfig(args) + pkFile := getArg(args, "--public-key", "-pk") + + if pkFile == "" { + fmt.Fprintf(os.Stderr, "Error: --public-key is required\n") + os.Exit(1) + } + + pkData, err := loadKeyFromFile(pkFile, "public_key") + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading public key: %v\n", err) + os.Exit(1) + } + + off := 0 + if len(pkData) < 4 { + fmt.Fprintf(os.Stderr, "Invalid public key: too short to read level\n") + os.Exit(1) + } + levelLen := int(binary.LittleEndian.Uint32(pkData[off:])) + off += 4 + levelLen + if off+4 > len(pkData) { + fmt.Fprintf(os.Stderr, "Invalid public key: truncated\n") + os.Exit(1) + } + slssLen := int(binary.LittleEndian.Uint32(pkData[off:])) + off += 4 + if off+slssLen > len(pkData) { + fmt.Fprintf(os.Stderr, "Invalid public key: SLSS truncated\n") + os.Exit(1) + } + slssBytes := pkData[off : off+slssLen] + off += slssLen + if off+4 > len(pkData) { + fmt.Fprintf(os.Stderr, "Invalid public key: truncated after SLSS\n") + os.Exit(1) + } + tddLen := int(binary.LittleEndian.Uint32(pkData[off:])) + off += 4 + if off+tddLen > len(pkData) { + fmt.Fprintf(os.Stderr, "Invalid public key: TDD truncated\n") + os.Exit(1) + } + tddBytes := pkData[off : off+tddLen] + off += tddLen + if off+4 > len(pkData) { + fmt.Fprintf(os.Stderr, "Invalid public key: truncated after TDD\n") + os.Exit(1) + } + egrwLen := int(binary.LittleEndian.Uint32(pkData[off:])) + off += 4 + if off+egrwLen > len(pkData) { + fmt.Fprintf(os.Stderr, "Invalid public key: EGRW truncated\n") + os.Exit(1) + } + egrwBytes := pkData[off : off+egrwLen] + off += egrwLen + if off+32 > len(pkData) { + fmt.Fprintf(os.Stderr, "Invalid public key: binding truncated\n") + os.Exit(1) + } + embedded := pkData[off : off+32] + + computed := entanglement.ComputeBinding(slssBytes, tddBytes, egrwBytes) + // Compute component hashes for comparison + slssHash := utils.SHA3256(slssBytes) + tddHash := utils.SHA3256(tddBytes) + egrwHash := utils.SHA3256(egrwBytes) + + out := map[string]string{ + "embedded_binding": encodeBytes(embedded, config.OutputFormat), + "computed_binding": encodeBytes(computed, config.OutputFormat), + "slss_hash": encodeBytes(slssHash, config.OutputFormat), + "tdd_hash": encodeBytes(tddHash, config.OutputFormat), + "egrw_hash": encodeBytes(egrwHash, config.OutputFormat), + } + j, _ := json.MarshalIndent(out, "", " ") + writeOutput(j, config.OutputFile) +} + func kemDecapsulate(args []string) { config := parseConfig(args) skFile := getArg(args, "--secret-key", "-sk") @@ -320,8 +491,8 @@ func kemDecapsulate(args []string) { os.Exit(1) } - // Load keys - skData, err := loadKeyFromFile(skFile, "secret_key") + // Load secret key (handles JSON format) + sk, err := loadSecretKeyFromFile(skFile) if err != nil { fmt.Fprintf(os.Stderr, "Error loading secret key: %v\n", err) os.Exit(1) @@ -339,12 +510,6 @@ func kemDecapsulate(args []string) { os.Exit(1) } - sk, err := kem.DeserializeSecretKey(skData) - if err != nil { - fmt.Fprintf(os.Stderr, "Error deserializing secret key: %v\n", err) - os.Exit(1) - } - pk, err := kem.DeserializePublicKey(pkData) if err != nil { fmt.Fprintf(os.Stderr, "Error deserializing public key: %v\n", err) @@ -467,6 +632,86 @@ func kemEncrypt(args []string) { } } +func kemSLSSDebug(args []string) { + config := parseConfig(args) + pkFile := getArg(args, "--public-key", "-pk") + randomHex := getArg(args, "--randomness", "-r") + messageHex := getArg(args, "--message", "-m") + + if pkFile == "" { + fmt.Fprintf(os.Stderr, "Error: --public-key is required\n") + os.Exit(1) + } + + pkData, err := loadKeyFromFile(pkFile, "public_key") + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading public key: %v\n", err) + os.Exit(1) + } + + pk, err := kem.DeserializePublicKey(pkData) + if err != nil { + fmt.Fprintf(os.Stderr, "Error deserializing public key: %v\n", err) + os.Exit(1) + } + + // Parse randomness from hex or use random + var randomness []byte + if randomHex != "" { + randomness, err = hex.DecodeString(randomHex) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid randomness hex: %v\n", err) + os.Exit(1) + } + if len(randomness) < 32 { + fmt.Fprintf(os.Stderr, "Randomness must be at least 32 bytes (hex length >= 64)\n") + os.Exit(1) + } + } else { + randomness = make([]byte, 32) + if _, err := rand.Read(randomness); err != nil { + fmt.Fprintf(os.Stderr, "Error generating randomness: %v\n", err) + os.Exit(1) + } + } + + // Parse message (default 32 zero bytes) + var message []byte + if messageHex != "" { + message, err = hex.DecodeString(messageHex) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid message hex: %v\n", err) + os.Exit(1) + } + } else { + message = make([]byte, 32) + } + + ct, debug, err := slss.DebugEncrypt(pk.SLSS, message, pk.Params.SLSS, randomness) + if err != nil { + fmt.Fprintf(os.Stderr, "Error during SLSS debug encrypt: %v\n", err) + os.Exit(1) + } + + out := map[string]any{ + "r_indices": debug.RIndices, + "r_values": debug.RValues, + "e1_head": debug.E1Head, + "e2_head": debug.E2Head, + "u_head": debug.UHead, + "v_head": debug.VHead, + "u_len": len(ct.U), + "v_len": len(ct.V), + } + + j, _ := json.MarshalIndent(out, "", " ") + writeOutput(j, config.OutputFile) + + if config.Verbose { + fmt.Fprintf(os.Stderr, "SLSS debug completed\n") + } +} + func kemDecrypt(args []string) { config := parseConfig(args) skFile := getArg(args, "--secret-key", "-sk") @@ -478,8 +723,8 @@ func kemDecrypt(args []string) { os.Exit(1) } - // Load keys - skData, err := loadKeyFromFile(skFile, "secret_key") + // Load secret key (handles JSON format) + sk, err := loadSecretKeyFromFile(skFile) if err != nil { fmt.Fprintf(os.Stderr, "Error loading secret key: %v\n", err) os.Exit(1) @@ -497,12 +742,6 @@ func kemDecrypt(args []string) { os.Exit(1) } - sk, err := kem.DeserializeSecretKey(skData) - if err != nil { - fmt.Fprintf(os.Stderr, "Error deserializing secret key: %v\n", err) - os.Exit(1) - } - pk, err := kem.DeserializePublicKey(pkData) if err != nil { fmt.Fprintf(os.Stderr, "Error deserializing public key: %v\n", err) @@ -616,12 +855,18 @@ func signKeygen(args []string) { // Serialize key pair pkBytes := sign.SerializePublicKey(&kp.PublicKey) - skBytes := sign.SerializeSecretKey(&kp.SecretKey) + + // Convert secret key to JSON format + skJSON, err := signSecretKeyToJSON(&kp.SecretKey) + if err != nil { + fmt.Fprintf(os.Stderr, "Error serializing secret key: %v\n", err) + os.Exit(1) + } export := SignKeyPairExport{ SecurityLevel: string(config.SecurityLevel), PublicKey: encodeBytes(pkBytes, config.OutputFormat), - SecretKey: encodeBytes(skBytes, config.OutputFormat), + SecretKey: base64.StdEncoding.EncodeToString([]byte(skJSON)), CreatedAt: time.Now().UTC().Format(time.RFC3339), } @@ -636,7 +881,7 @@ func signKeygen(args []string) { if config.Verbose { fmt.Fprintf(os.Stderr, "Generated signature key pair with security level: %s\n", config.SecurityLevel) fmt.Fprintf(os.Stderr, "Public key size: %d bytes\n", len(pkBytes)) - fmt.Fprintf(os.Stderr, "Secret key size: %d bytes\n", len(skBytes)) + fmt.Fprintf(os.Stderr, "Secret key size: %d bytes (JSON)\n", len(skJSON)) } } @@ -673,8 +918,8 @@ func signSign(args []string) { } } - // Load keys - skData, err := loadKeyFromFile(skFile, "secret_key") + // Load secret key (handles JSON format) + sk, err := loadSignSecretKeyFromFile(skFile) if err != nil { fmt.Fprintf(os.Stderr, "Error loading secret key: %v\n", err) os.Exit(1) @@ -686,12 +931,6 @@ func signSign(args []string) { os.Exit(1) } - sk, err := sign.DeserializeSecretKey(skData) - if err != nil { - fmt.Fprintf(os.Stderr, "Error deserializing secret key: %v\n", err) - os.Exit(1) - } - pk, err := sign.DeserializePublicKey(pkData) if err != nil { fmt.Fprintf(os.Stderr, "Error deserializing public key: %v\n", err) @@ -1042,6 +1281,200 @@ func hasFlag(args []string, long, short string) bool { return false } +// secretKeyToJSON converts a secret key to JSON format. +func secretKeyToJSON(sk *kmosaic.MOSAICSecretKey) (string, error) { + // Create JSON structure with lowercase a,b,c for TDD factors + skJSON := map[string]interface{}{ + "slss": map[string]interface{}{ + "s": sk.SLSS.S, + }, + "tdd": map[string]interface{}{ + "factors": map[string]interface{}{ + "a": sk.TDD.Factors.A, + "b": sk.TDD.Factors.B, + "c": sk.TDD.Factors.C, + }, + }, + "egrw": map[string]interface{}{ + "walk": sk.EGRW.Walk, + }, + "seed": sk.Seed, + "publicKeyHash": sk.PublicKeyHash, + } + + jsonBytes, err := json.Marshal(skJSON) + if err != nil { + return "", err + } + return string(jsonBytes), nil +} + +// signSecretKeyToJSON converts a signature secret key to JSON format. +func signSecretKeyToJSON(sk *kmosaic.MOSAICSignSecretKey) (string, error) { + // Create JSON structure with lowercase a,b,c for TDD factors + skJSON := map[string]interface{}{ + "slss": map[string]interface{}{ + "s": sk.SLSS.S, + }, + "tdd": map[string]interface{}{ + "factors": map[string]interface{}{ + "a": sk.TDD.Factors.A, + "b": sk.TDD.Factors.B, + "c": sk.TDD.Factors.C, + }, + }, + "egrw": map[string]interface{}{ + "walk": sk.EGRW.Walk, + }, + "seed": sk.Seed, + "publicKeyHash": sk.PublicKeyHash, + } + + jsonBytes, err := json.Marshal(skJSON) + if err != nil { + return "", err + } + return string(jsonBytes), nil +} + +// secretKeyFromJSON converts JSON format secret key to MOSAICSecretKey +func secretKeyFromJSON(jsonStr string) (*kmosaic.MOSAICSecretKey, error) { + var skJSON map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &skJSON); err != nil { + return nil, fmt.Errorf("failed to parse secret key JSON: %w", err) + } + + sk := &kmosaic.MOSAICSecretKey{} + + // Parse SLSS + if slss, ok := skJSON["slss"].(map[string]interface{}); ok { + if sArray, ok := slss["s"].([]interface{}); ok { + sk.SLSS.S = make([]int8, len(sArray)) + for i, v := range sArray { + if num, ok := v.(float64); ok { + sk.SLSS.S[i] = int8(num) + } + } + } + } + + // Parse TDD + if tdd, ok := skJSON["tdd"].(map[string]interface{}); ok { + if factors, ok := tdd["factors"].(map[string]interface{}); ok { + // Parse factor a + if aArray, ok := factors["a"].([]interface{}); ok { + sk.TDD.Factors.A = make([][]int32, len(aArray)) + for i, v := range aArray { + if vec, ok := v.([]interface{}); ok { + sk.TDD.Factors.A[i] = make([]int32, len(vec)) + for j, val := range vec { + if num, ok := val.(float64); ok { + sk.TDD.Factors.A[i][j] = int32(num) + } + } + } + } + } + // Parse factor b + if bArray, ok := factors["b"].([]interface{}); ok { + sk.TDD.Factors.B = make([][]int32, len(bArray)) + for i, v := range bArray { + if vec, ok := v.([]interface{}); ok { + sk.TDD.Factors.B[i] = make([]int32, len(vec)) + for j, val := range vec { + if num, ok := val.(float64); ok { + sk.TDD.Factors.B[i][j] = int32(num) + } + } + } + } + } + // Parse factor c + if cArray, ok := factors["c"].([]interface{}); ok { + sk.TDD.Factors.C = make([][]int32, len(cArray)) + for i, v := range cArray { + if vec, ok := v.([]interface{}); ok { + sk.TDD.Factors.C[i] = make([]int32, len(vec)) + for j, val := range vec { + if num, ok := val.(float64); ok { + sk.TDD.Factors.C[i][j] = int32(num) + } + } + } + } + } + } + } + + // Parse EGRW + if egrw, ok := skJSON["egrw"].(map[string]interface{}); ok { + if walkArray, ok := egrw["walk"].([]interface{}); ok { + sk.EGRW.Walk = make([]int, len(walkArray)) + for i, v := range walkArray { + if num, ok := v.(float64); ok { + sk.EGRW.Walk[i] = int(num) + } + } + } + } + + // Parse seed (supports both base64 string and number array) + if seedStr, ok := skJSON["seed"].(string); ok { + // Base64 encoded string (from json.Marshal on []byte) + decoded, err := base64.StdEncoding.DecodeString(seedStr) + if err == nil { + sk.Seed = decoded + } + } else if seedArray, ok := skJSON["seed"].([]interface{}); ok { + // Array of numbers + sk.Seed = make([]byte, len(seedArray)) + for i, v := range seedArray { + if num, ok := v.(float64); ok { + sk.Seed[i] = byte(num) + } + } + } + + // Parse publicKeyHash (supports both base64 string and number array) + if hashStr, ok := skJSON["publicKeyHash"].(string); ok { + // Base64 encoded string (from json.Marshal on []byte) + decoded, err := base64.StdEncoding.DecodeString(hashStr) + if err == nil { + sk.PublicKeyHash = decoded + } + } else if hashArray, ok := skJSON["publicKeyHash"].([]interface{}); ok { + // Array of numbers + sk.PublicKeyHash = make([]byte, len(hashArray)) + for i, v := range hashArray { + if num, ok := v.(float64); ok { + sk.PublicKeyHash[i] = byte(num) + } + } + } + + return sk, nil +} + +// signSecretKeyFromJSON converts JSON format secret key to MOSAICSignSecretKey +func signSecretKeyFromJSON(jsonStr string) (*kmosaic.MOSAICSignSecretKey, error) { + // Reuse the same parsing logic + sk, err := secretKeyFromJSON(jsonStr) + if err != nil { + return nil, err + } + + // Convert to sign secret key + signSK := &kmosaic.MOSAICSignSecretKey{ + SLSS: sk.SLSS, + TDD: sk.TDD, + EGRW: sk.EGRW, + Seed: sk.Seed, + PublicKeyHash: sk.PublicKeyHash, + } + + return signSK, nil +} + func encodeBytes(data []byte, format OutputFormat) string { switch format { case FormatHex: @@ -1065,6 +1498,70 @@ func decodeString(s string) ([]byte, error) { return nil, fmt.Errorf("unable to decode string") } +// loadSecretKeyFromFile loads a secret key from a file, handling both JSON and binary formats +func loadSecretKeyFromFile(filename string) (*kmosaic.MOSAICSecretKey, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + // Try to parse file as JSON keypair + var jsonData map[string]interface{} + if err := json.Unmarshal(data, &jsonData); err == nil { + // JSON format - extract secret_key field + if val, ok := jsonData["secret_key"]; ok { + if strVal, ok := val.(string); ok { + // Decode base64 + skBytes, err := base64.StdEncoding.DecodeString(strVal) + if err != nil { + return nil, fmt.Errorf("failed to decode secret key: %w", err) + } + // Try to parse as JSON (new format) + if sk, err := secretKeyFromJSON(string(skBytes)); err == nil { + return sk, nil + } + // Fall back to binary format + return kem.DeserializeSecretKey(skBytes) + } + } + } + + // Try raw binary + return kem.DeserializeSecretKey(data) +} + +// loadSignSecretKeyFromFile loads a signature secret key from a file +func loadSignSecretKeyFromFile(filename string) (*kmosaic.MOSAICSignSecretKey, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + // Try to parse file as JSON keypair + var jsonData map[string]interface{} + if err := json.Unmarshal(data, &jsonData); err == nil { + // JSON format - extract secret_key field + if val, ok := jsonData["secret_key"]; ok { + if strVal, ok := val.(string); ok { + // Decode base64 + skBytes, err := base64.StdEncoding.DecodeString(strVal) + if err != nil { + return nil, fmt.Errorf("failed to decode secret key: %w", err) + } + // Try to parse as JSON (new format) + if sk, err := signSecretKeyFromJSON(string(skBytes)); err == nil { + return sk, nil + } + // Fall back to binary format + return sign.DeserializeSecretKey(skBytes) + } + } + } + + // Try raw binary + return sign.DeserializeSecretKey(data) +} + func loadKeyFromFile(filename, keyField string) ([]byte, error) { data, err := os.ReadFile(filename) if err != nil { diff --git a/cmd/k-mosaic-cli/main_test.go b/cmd/k-mosaic-cli/main_test.go index 7266f97..661f96b 100644 --- a/cmd/k-mosaic-cli/main_test.go +++ b/cmd/k-mosaic-cli/main_test.go @@ -1,8 +1,11 @@ package main_test import ( + "bytes" "context" + "encoding/base64" "encoding/json" + "os" "os/exec" "path/filepath" "strings" @@ -10,6 +13,28 @@ import ( "time" ) +// Helper types for unmarshaling JSON responses +type kepExport struct { + SecurityLevel string `json:"security_level"` + PublicKey string `json:"public_key"` + SecretKey string `json:"secret_key"` + CreatedAt string `json:"created_at"` +} + +type encapsulationExport struct { + Ciphertext string `json:"ciphertext"` + SharedSecret string `json:"shared_secret"` +} + +type signatureExport struct { + Message string `json:"message"` + Signature string `json:"signature"` +} + +type encryptedExport struct { + Ciphertext string `json:"ciphertext"` +} + // runCLI executes the k-mosaic-cli via `go run ./cmd/k-mosaic-cli` from the repository root. func runCLI(t *testing.T, timeout time.Duration, args ...string) (stdout string, stderr string, err error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) @@ -25,6 +50,21 @@ func runCLI(t *testing.T, timeout time.Duration, args ...string) (stdout string, return string(out), "", nil } +// runCLIWithStdin runs CLI with stdin input +func runCLIWithStdin(t *testing.T, timeout time.Duration, stdin string, args ...string) (stdout string, stderr string, err error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + cmdArgs := append([]string{"run", "./cmd/k-mosaic-cli"}, args...) + cmd := exec.CommandContext(ctx, "go", cmdArgs...) + cmd.Dir = filepath.Join("..", "..") + cmd.Stdin = bytes.NewReader([]byte(stdin)) + out, err := cmd.CombinedOutput() + if err != nil { + return string(out), string(out), err + } + return string(out), "", nil +} + func TestHelpAndVersion(t *testing.T) { stdout, _, err := runCLI(t, 10*time.Second, "help") if err != nil { @@ -109,3 +149,668 @@ func TestSignKeygenSignVerify(t *testing.T) { t.Fatalf("signature reported invalid: %v", res) } } + +// ============================================================================ +// KEM Encapsulate/Decapsulate Tests +// ============================================================================ + +func TestKEMEncapsulateDecapsulate(t *testing.T) { + dir := t.TempDir() + kpFile := filepath.Join(dir, "kem_kp.json") + encapFile := filepath.Join(dir, "encap.json") + + // Keygen + _, stderr, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "128", "--output", kpFile) + if err != nil { + t.Fatalf("kem keygen failed: %v, stderr: %s", err, stderr) + } + + // Encapsulate (output to file, not stdout) + _, stderr, err = runCLI(t, 20*time.Second, "kem", "encapsulate", "--public-key", kpFile, "--output", encapFile) + if err != nil { + t.Fatalf("kem encapsulate failed: %v, stderr: %s", err, stderr) + } + + // Read encapsulation from file + encapData, err := os.ReadFile(encapFile) + if err != nil { + t.Fatalf("failed to read encapsulation file: %v", err) + } + + var encap encapsulationExport + if err := json.Unmarshal(encapData, &encap); err != nil { + t.Fatalf("unable to parse encapsulation output as json: %v, out: %s", err, string(encapData)) + } + if encap.Ciphertext == "" || encap.SharedSecret == "" { + t.Fatalf("encapsulation missing ciphertext or shared_secret: %v", encap) + } + + // Decapsulate + decapOut, stderr, err := runCLI(t, 20*time.Second, "kem", "decapsulate", "--secret-key", kpFile, "--public-key", kpFile, "--ciphertext", encapFile) + if err != nil { + t.Fatalf("kem decapsulate failed: %v, stderr: %s", err, stderr) + } + + var decap map[string]interface{} + if err := json.Unmarshal([]byte(decapOut), &decap); err != nil { + t.Fatalf("unable to parse decapsulate output as json: %v, out: %s", err, decapOut) + } + + // Verify shared secrets match + if decap["shared_secret"] != encap.SharedSecret { + t.Fatalf("shared secrets don't match: encapsulate=%s, decapsulate=%s", encap.SharedSecret, decap["shared_secret"]) + } +} + +func TestKEMEncapsulateDeterministic(t *testing.T) { + dir := t.TempDir() + kpFile := filepath.Join(dir, "kem_kp.json") + + // Keygen + _, stderr, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "128", "--output", kpFile) + if err != nil { + t.Fatalf("kem keygen failed: %v, stderr: %s", err, stderr) + } + + // Generate ephemeral secret (32 bytes = 64 hex chars) + ephemeralSecret := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + // Encapsulate deterministic + encapOut, stderr, err := runCLI(t, 20*time.Second, "kem", "encapsulate-deterministic", + "--public-key", kpFile, "--ephemeral-secret", ephemeralSecret) + if err != nil { + t.Fatalf("kem encapsulate-deterministic failed: %v, stderr: %s", err, stderr) + } + + var encap encapsulationExport + if err := json.Unmarshal([]byte(encapOut), &encap); err != nil { + t.Fatalf("unable to parse deterministic encapsulation as json: %v, out: %s", err, encapOut) + } + if encap.Ciphertext == "" || encap.SharedSecret == "" { + t.Fatalf("deterministic encapsulation missing required fields: %v", encap) + } + + // Second run with same ephemeral secret should produce same result + encapOut2, stderr, err := runCLI(t, 20*time.Second, "kem", "encapsulate-deterministic", + "--public-key", kpFile, "--ephemeral-secret", ephemeralSecret) + if err != nil { + t.Fatalf("second kem encapsulate-deterministic failed: %v, stderr: %s", err, stderr) + } + + var encap2 encapsulationExport + if err := json.Unmarshal([]byte(encapOut2), &encap2); err != nil { + t.Fatalf("unable to parse second deterministic encapsulation as json: %v, out: %s", err, encapOut2) + } + + if encap.SharedSecret != encap2.SharedSecret { + t.Fatalf("deterministic encapsulation not reproducible: first=%s, second=%s", encap.SharedSecret, encap2.SharedSecret) + } +} + +// ============================================================================ +// KEM SLSS Debug and PK-Inspect Tests +// ============================================================================ + +func TestKEMSLSSDebug(t *testing.T) { + dir := t.TempDir() + kpFile := filepath.Join(dir, "kem_kp.json") + + // Keygen + _, stderr, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "128", "--output", kpFile) + if err != nil { + t.Fatalf("kem keygen failed: %v, stderr: %s", err, stderr) + } + + // SLSS Debug + debugOut, stderr, err := runCLI(t, 20*time.Second, "kem", "slss-debug", "--public-key", kpFile) + if err != nil { + t.Fatalf("kem slss-debug failed: %v, stderr: %s", err, stderr) + } + + var debugInfo map[string]interface{} + if err := json.Unmarshal([]byte(debugOut), &debugInfo); err != nil { + t.Fatalf("unable to parse slss-debug output as json: %v, out: %s", err, debugOut) + } + + // Verify expected debug fields exist + expectedFields := []string{"r_indices", "r_values", "e1_head", "e2_head", "u_head", "v_head", "u_len", "v_len"} + for _, field := range expectedFields { + if _, ok := debugInfo[field]; !ok { + t.Fatalf("slss-debug output missing field '%s': %v", field, debugInfo) + } + } +} + +func TestKEMPKInspect(t *testing.T) { + dir := t.TempDir() + kpFile := filepath.Join(dir, "kem_kp.json") + + // Keygen + _, stderr, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "128", "--output", kpFile) + if err != nil { + t.Fatalf("kem keygen failed: %v, stderr: %s", err, stderr) + } + + // PK-Inspect + inspectOut, stderr, err := runCLI(t, 20*time.Second, "kem", "pk-inspect", "--public-key", kpFile) + if err != nil { + t.Fatalf("kem pk-inspect failed: %v, stderr: %s", err, stderr) + } + + var inspectInfo map[string]interface{} + if err := json.Unmarshal([]byte(inspectOut), &inspectInfo); err != nil { + t.Fatalf("unable to parse pk-inspect output as json: %v, out: %s", err, inspectOut) + } + + // Verify expected inspection fields exist + expectedFields := []string{"embedded_binding", "computed_binding", "slss_hash", "tdd_hash", "egrw_hash"} + for _, field := range expectedFields { + if _, ok := inspectInfo[field]; !ok { + t.Fatalf("pk-inspect output missing field '%s': %v", field, inspectInfo) + } + } +} + +// ============================================================================ +// Output Format Tests +// ============================================================================ + +func TestOutputFormatHex(t *testing.T) { + // Keygen with hex format + stdout, stderr, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "128", "--format", "hex") + if err != nil { + t.Fatalf("kem keygen with hex format failed: %v, stderr: %s", err, stderr) + } + + var kp kepExport + if err := json.Unmarshal([]byte(stdout), &kp); err != nil { + t.Fatalf("unable to parse keygen output as json: %v, out: %s", err, stdout) + } + + // Verify hex encoding - public key and secret key should be valid hex strings + // (Note: hex format may embed the key data, just verify the strings are non-empty and valid JSON was produced) + if kp.PublicKey == "" { + t.Fatalf("public key is empty") + } + if kp.SecretKey == "" { + t.Fatalf("secret key is empty") + } +} + +func TestOutputFormatBase64(t *testing.T) { + // Keygen with base64 format + stdout, stderr, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "128", "--format", "base64") + if err != nil { + t.Fatalf("kem keygen with base64 format failed: %v, stderr: %s", err, stderr) + } + + var kp kepExport + if err := json.Unmarshal([]byte(stdout), &kp); err != nil { + t.Fatalf("unable to parse keygen output as json: %v, out: %s", err, stdout) + } + + // Verify base64 encoding (should be decodable as base64) + if _, err := base64.StdEncoding.DecodeString(kp.PublicKey); err != nil { + t.Fatalf("public key is not valid base64: %v", err) + } + if _, err := base64.StdEncoding.DecodeString(kp.SecretKey); err != nil { + t.Fatalf("secret key is not valid base64: %v", err) + } +} + +func TestOutputFormatJSON(t *testing.T) { + // Keygen with json format (should be nested JSON structure) + stdout, stderr, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "128", "--format", "json") + if err != nil { + t.Fatalf("kem keygen with json format failed: %v, stderr: %s", err, stderr) + } + + var kp kepExport + if err := json.Unmarshal([]byte(stdout), &kp); err != nil { + t.Fatalf("unable to parse keygen output as json: %v, out: %s", err, stdout) + } + + // JSON format should still be decodable + if kp.PublicKey == "" || kp.SecretKey == "" { + t.Fatalf("keygen with json format missing keys: %v", kp) + } +} + +// ============================================================================ +// Flag Behavior Tests (Timing, Verbose) +// ============================================================================ + +func TestTimingFlag(t *testing.T) { + // Keygen with timing flag - timing output is mixed in stdout + stdout, _, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "128", "--timing") + if err != nil { + t.Fatalf("kem keygen with timing flag failed: %v", err) + } + + // Just verify output is not empty - timing and JSON are mixed in output + if strings.TrimSpace(stdout) == "" { + t.Fatalf("keygen with timing flag produced no output") + } +} + +func TestVerboseFlag(t *testing.T) { + // Keygen with verbose flag + stdout, _, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "128", "--verbose") + if err != nil { + t.Fatalf("kem keygen with verbose flag failed: %v", err) + } + + // Just verify we got output + if strings.TrimSpace(stdout) == "" { + t.Fatalf("keygen with verbose flag produced no output") + } +} + +// ============================================================================ +// Security Level 256 Tests +// ============================================================================ + +func TestKEMLevel256(t *testing.T) { + dir := t.TempDir() + kpFile := filepath.Join(dir, "kem_kp_256.json") + + // Keygen with level 256 + _, _, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "256", "--output", kpFile) + if err != nil { + t.Fatalf("kem keygen level 256 failed: %v", err) + } + + // Verify file was created + data, err := os.ReadFile(kpFile) + if err != nil { + t.Fatalf("failed to read keygen output file: %v", err) + } + + var kp kepExport + if err := json.Unmarshal(data, &kp); err != nil { + t.Fatalf("unable to parse keygen output as json: %v", err) + } + + if !strings.Contains(kp.SecurityLevel, "256") { + t.Fatalf("keygen level 256 not reflected in security_level: %s", kp.SecurityLevel) + } + + // Test encrypt with level 256 + message := "Test message for level 256" + + encryptOut, _, err := runCLI(t, 20*time.Second, "kem", "encrypt", "--public-key", kpFile, "--message", message) + if err != nil { + t.Fatalf("kem encrypt level 256 failed: %v", err) + } + + // Verify we got encrypted output + if strings.TrimSpace(encryptOut) == "" { + t.Fatalf("encrypt produced no output") + } +} + +func TestSignLevel256(t *testing.T) { + dir := t.TempDir() + kpFile := filepath.Join(dir, "sign_kp_256.json") + + // Keygen with level 256 + _, _, err := runCLI(t, 20*time.Second, "sign", "keygen", "--level", "256", "--output", kpFile) + if err != nil { + t.Fatalf("sign keygen level 256 failed: %v", err) + } + + // Verify file was created + data, err := os.ReadFile(kpFile) + if err != nil { + t.Fatalf("failed to read keygen output file: %v", err) + } + + var kp kepExport + if err := json.Unmarshal(data, &kp); err != nil { + t.Fatalf("unable to parse keygen output as json: %v", err) + } + + if !strings.Contains(kp.SecurityLevel, "256") { + t.Fatalf("keygen level 256 not reflected in security_level: %s", kp.SecurityLevel) + } +} + +// ============================================================================ +// Stdin Input Tests +// ============================================================================ + +func TestKEMEncryptStdinMessage(t *testing.T) { + dir := t.TempDir() + kpFile := filepath.Join(dir, "kem_kp.json") + + // Keygen + _, stderr, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "128", "--output", kpFile) + if err != nil { + t.Fatalf("kem keygen failed: %v, stderr: %s", err, stderr) + } + + // Encrypt with stdin message + message := "Message from stdin" + encryptOut, stderr, err := runCLIWithStdin(t, 20*time.Second, message, "kem", "encrypt", "--public-key", kpFile) + if err != nil { + t.Fatalf("kem encrypt with stdin failed: %v, stderr: %s", err, stderr) + } + + var enc encryptedExport + if err := json.Unmarshal([]byte(encryptOut), &enc); err != nil { + t.Fatalf("unable to parse encrypt output as json: %v, out: %s", err, encryptOut) + } + if enc.Ciphertext == "" { + t.Fatalf("encrypt output missing ciphertext: %v", enc) + } +} + +func TestSignSignStdinMessage(t *testing.T) { + dir := t.TempDir() + kpFile := filepath.Join(dir, "sign_kp.json") + + // Keygen + _, stderr, err := runCLI(t, 20*time.Second, "sign", "keygen", "--level", "128", "--output", kpFile) + if err != nil { + t.Fatalf("sign keygen failed: %v, stderr: %s", err, stderr) + } + + // Sign with stdin message + message := "Message to sign from stdin" + signOut, stderr, err := runCLIWithStdin(t, 20*time.Second, message, "sign", "sign", "--secret-key", kpFile, "--public-key", kpFile) + if err != nil { + t.Fatalf("sign with stdin failed: %v, stderr: %s", err, stderr) + } + + var sig signatureExport + if err := json.Unmarshal([]byte(signOut), &sig); err != nil { + t.Fatalf("unable to parse signature output as json: %v, out: %s", err, signOut) + } + if sig.Signature == "" { + t.Fatalf("signature output missing signature: %v", sig) + } +} + +// ============================================================================ +// File Input Tests +// ============================================================================ + +func TestKEMEncryptFileInput(t *testing.T) { + dir := t.TempDir() + kpFile := filepath.Join(dir, "kem_kp.json") + msgFile := filepath.Join(dir, "message.txt") + + // Create message file + message := "Message from file input" + if err := os.WriteFile(msgFile, []byte(message), 0644); err != nil { + t.Fatalf("failed to create message file: %v", err) + } + + // Keygen + _, stderr, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "128", "--output", kpFile) + if err != nil { + t.Fatalf("kem keygen failed: %v, stderr: %s", err, stderr) + } + + // Encrypt with file input + encryptOut, stderr, err := runCLI(t, 20*time.Second, "kem", "encrypt", "--public-key", kpFile, "--input", msgFile) + if err != nil { + t.Fatalf("kem encrypt with file input failed: %v, stderr: %s", err, stderr) + } + + var enc encryptedExport + if err := json.Unmarshal([]byte(encryptOut), &enc); err != nil { + t.Fatalf("unable to parse encrypt output as json: %v, out: %s", err, encryptOut) + } + if enc.Ciphertext == "" { + t.Fatalf("encrypt output missing ciphertext: %v", enc) + } +} + +func TestSignSignFileInput(t *testing.T) { + dir := t.TempDir() + kpFile := filepath.Join(dir, "sign_kp.json") + msgFile := filepath.Join(dir, "message.txt") + + // Create message file + message := "Message to sign from file" + if err := os.WriteFile(msgFile, []byte(message), 0644); err != nil { + t.Fatalf("failed to create message file: %v", err) + } + + // Keygen + _, stderr, err := runCLI(t, 20*time.Second, "sign", "keygen", "--level", "128", "--output", kpFile) + if err != nil { + t.Fatalf("sign keygen failed: %v, stderr: %s", err, stderr) + } + + // Sign with file input + signOut, stderr, err := runCLI(t, 20*time.Second, "sign", "sign", "--secret-key", kpFile, "--public-key", kpFile, "--input", msgFile) + if err != nil { + t.Fatalf("sign with file input failed: %v, stderr: %s", err, stderr) + } + + var sig signatureExport + if err := json.Unmarshal([]byte(signOut), &sig); err != nil { + t.Fatalf("unable to parse signature output as json: %v, out: %s", err, signOut) + } + if sig.Signature == "" { + t.Fatalf("signature output missing signature: %v", sig) + } +} + +// ============================================================================ +// Output File Tests +// ============================================================================ + +func TestKEMKeygenOutputFile(t *testing.T) { + dir := t.TempDir() + kpFile := filepath.Join(dir, "kem_kp.json") + + // Keygen with output file + _, stderr, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "128", "--output", kpFile) + if err != nil { + t.Fatalf("kem keygen failed: %v, stderr: %s", err, stderr) + } + + // Verify file was created and contains valid JSON + content, err := os.ReadFile(kpFile) + if err != nil { + t.Fatalf("failed to read output file: %v", err) + } + + var kp kepExport + if err := json.Unmarshal(content, &kp); err != nil { + t.Fatalf("output file does not contain valid JSON: %v", err) + } + if kp.PublicKey == "" || kp.SecretKey == "" { + t.Fatalf("output file missing keys: %v", kp) + } +} + +func TestKEMEncryptOutputFile(t *testing.T) { + dir := t.TempDir() + kpFile := filepath.Join(dir, "kem_kp.json") + ctFile := filepath.Join(dir, "ciphertext.json") + + // Keygen + _, stderr, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "128", "--output", kpFile) + if err != nil { + t.Fatalf("kem keygen failed: %v, stderr: %s", err, stderr) + } + + // Encrypt with output file + _, stderr, err = runCLI(t, 20*time.Second, "kem", "encrypt", "--public-key", kpFile, "--message", "test", "--output", ctFile) + if err != nil { + t.Fatalf("kem encrypt failed: %v, stderr: %s", err, stderr) + } + + // Verify file was created and contains valid ciphertext + content, err := os.ReadFile(ctFile) + if err != nil { + t.Fatalf("failed to read ciphertext file: %v", err) + } + + var enc encryptedExport + if err := json.Unmarshal(content, &enc); err != nil { + t.Fatalf("ciphertext file does not contain valid JSON: %v", err) + } + if enc.Ciphertext == "" { + t.Fatalf("ciphertext file missing ciphertext: %v", enc) + } +} + +// ============================================================================ +// Error Handling and Edge Cases +// ============================================================================ + +func TestMissingRequiredFlag(t *testing.T) { + // Encrypt without required --public-key flag + _, _, err := runCLI(t, 20*time.Second, "kem", "encrypt", "--message", "test") + if err == nil { + t.Fatalf("expected encrypt without public-key to fail, but it succeeded") + } +} + +func TestInvalidSecurityLevel(t *testing.T) { + // Keygen with invalid security level + _, _, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "512") + if err == nil { + t.Logf("Warning: CLI accepted invalid security level 512. This may be expected behavior.") + } +} + +func TestSignVerifyInvalidSignature(t *testing.T) { + dir := t.TempDir() + kpFile := filepath.Join(dir, "sign_kp.json") + sigFile := filepath.Join(dir, "sig.json") + + // Keygen + _, _, err := runCLI(t, 20*time.Second, "sign", "keygen", "--level", "128", "--output", kpFile) + if err != nil { + t.Fatalf("sign keygen failed: %v", err) + } + + // Create a signature file with invalid signature + invalidSig := map[string]string{ + "message": "test message", + "signature": "0000000000000000000000000000000000000000000000000000000000000000", + } + sigData, _ := json.Marshal(invalidSig) + if err := os.WriteFile(sigFile, sigData, 0644); err != nil { + t.Fatalf("failed to create signature file: %v", err) + } + + // Verify with invalid signature - may return error message instead of JSON + stdout, _, _ := runCLI(t, 20*time.Second, "sign", "verify", "--public-key", kpFile, "--message", "test message", "--signature", sigFile) + + // The CLI returns error messages for invalid signatures, not JSON + // Just verify we got some response indicating an error + if strings.Contains(stdout, "Error") || strings.Contains(stdout, "error") { + // This is expected - invalid signature produces an error + return + } + + var res map[string]interface{} + if err := json.Unmarshal([]byte(stdout), &res); err != nil { + // JSON parsing failed, which is OK - error message was returned instead + return + } + + // If we did get JSON, verify the valid field is false + valid, ok := res["valid"].(bool) + if ok && !valid { + // This is also OK + return + } + + t.Logf("Verify returned: %s", stdout) +} + +// ============================================================================ +// Benchmark Command Tests +// ============================================================================ + +func TestBenchmarkCommand(t *testing.T) { + // Run benchmark with default iterations + benchOut, stderr, err := runCLI(t, 60*time.Second, "benchmark", "--level", "128", "--iterations", "2") + if err != nil { + t.Fatalf("benchmark command failed: %v, stderr: %s, out: %s", err, stderr, benchOut) + } + + // Verify output contains expected benchmark sections + expectedSections := []string{"KEM", "Sign", "KeyGen", "Encapsulate", "Decapsulate", "Encrypt", "Decrypt"} + for _, section := range expectedSections { + if !strings.Contains(benchOut, section) { + t.Fatalf("benchmark output missing expected section '%s': %s", section, benchOut) + } + } +} + +func TestBenchmarkLevel256(t *testing.T) { + // Run benchmark with level 256 + benchOut, stderr, err := runCLI(t, 60*time.Second, "benchmark", "--level", "256", "--iterations", "1") + if err != nil { + t.Fatalf("benchmark level 256 failed: %v, stderr: %s", err, stderr) + } + + // Verify output is not empty + if strings.TrimSpace(benchOut) == "" { + t.Fatalf("benchmark output is empty") + } +} + +// ============================================================================ +// Short Command Aliases Tests +// ============================================================================ + +func TestKEMEncapsulateAlias(t *testing.T) { + dir := t.TempDir() + kpFile := filepath.Join(dir, "kem_kp.json") + + // Keygen + _, stderr, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "128", "--output", kpFile) + if err != nil { + t.Fatalf("kem keygen failed: %v, stderr: %s", err, stderr) + } + + // Encapsulate using short alias 'encap' + encapOut, stderr, err := runCLI(t, 20*time.Second, "kem", "encap", "--public-key", kpFile) + if err != nil { + t.Fatalf("kem encap (alias) failed: %v, stderr: %s", err, stderr) + } + + var encap encapsulationExport + if err := json.Unmarshal([]byte(encapOut), &encap); err != nil { + t.Fatalf("unable to parse encapsulation output as json: %v, out: %s", err, encapOut) + } +} + +func TestKEMDecapsulateAlias(t *testing.T) { + dir := t.TempDir() + kpFile := filepath.Join(dir, "kem_kp.json") + encapFile := filepath.Join(dir, "encap.json") + + // Keygen + _, stderr, err := runCLI(t, 20*time.Second, "kem", "keygen", "--level", "128", "--output", kpFile) + if err != nil { + t.Fatalf("kem keygen failed: %v, stderr: %s", err, stderr) + } + + // Encapsulate + _, _, err = runCLI(t, 20*time.Second, "kem", "encapsulate", "--public-key", kpFile, "--output", encapFile) + if err != nil { + t.Fatalf("kem encapsulate failed: %v, stderr: %s", err, err) + } + + // Decapsulate using short alias 'decap' + decapOut, stderr, err := runCLI(t, 20*time.Second, "kem", "decap", "--secret-key", kpFile, "--public-key", kpFile, "--ciphertext", encapFile) + if err != nil { + t.Fatalf("kem decap (alias) failed: %v, stderr: %s", err, stderr) + } + + var decap map[string]interface{} + if err := json.Unmarshal([]byte(decapOut), &decap); err != nil { + t.Fatalf("unable to parse decapsulate output as json: %v, out: %s", err, decapOut) + } +} diff --git a/entanglement/entanglement.go b/entanglement/entanglement.go index 85bcf4d..d9d9c7f 100644 --- a/entanglement/entanglement.go +++ b/entanglement/entanglement.go @@ -2,7 +2,10 @@ package entanglement import ( + "bytes" + "encoding/binary" "errors" + "strconv" "github.com/BackendStack21/k-mosaic-go/utils" ) @@ -99,7 +102,7 @@ func SecretShareDeterministic(secret []byte, n int, seed []byte) ([][]byte, erro // Generate n-1 deterministic shares for i := 0; i < n-1; i++ { - domain := DomainShare + "-" + string(rune('0'+i)) + domain := DomainShare + "-" + strconv.Itoa(i) shareSeed := utils.HashWithDomain(domain, seed) shares[i] = utils.Shake256(shareSeed, len(secret)) } @@ -120,8 +123,15 @@ func SecretShareDeterministic(secret []byte, n int, seed []byte) ([][]byte, erro // ComputeBinding computes the cross-component binding hash. // It binds the public keys of the three components (SLSS, TDD, EGRW) together. +// Uses three-layer binding with per-component domain separation for defense-in-depth. func ComputeBinding(slssBytes, tddBytes, egrwBytes []byte) []byte { - return utils.HashWithDomain(DomainBind, utils.HashConcat(slssBytes, tddBytes, egrwBytes)) + // Three-layer binding with domain separation + slssHash := utils.HashWithDomain(DomainBind+"-slss", slssBytes) + tddHash := utils.HashWithDomain(DomainBind+"-tdd", tddBytes) + egrwHash := utils.HashWithDomain(DomainBind+"-egrw", egrwBytes) + + // Final binding combines all three + return utils.HashWithDomain(DomainBind+"-final", utils.HashConcat(slssHash, tddHash, egrwHash)) } // BindingCommitment represents a commitment with its opening. @@ -170,48 +180,166 @@ type NIZKProof struct { // In this simplified implementation, it acts as a binding of all inputs. // A full implementation would use a Sigma protocol with Fiat-Shamir transform. func GenerateNIZKProof(secret []byte, shares [][]byte, ciphertextHashes [][]byte, seed []byte) []byte { - // Simplified NIZK: hash of all components - proofData := append([]byte{}, seed...) - for _, share := range shares { - proofData = append(proofData, share...) + // Structured NIZK (Fiat-Shamir) proof generation + if len(shares) != 3 || len(ciphertextHashes) != 3 { + return nil + } + + commitments := make([][]byte, 3) + commitRandomness := make([][]byte, 3) + for i := 0; i < 3; i++ { + domain := DomainNIZK + "-commit-" + strconv.Itoa(i) + domainHash := utils.HashWithDomain(domain, seed) + + r := utils.SHA3256(domainHash) + commitRandomness[i] = r + + concatInput := utils.HashConcat(shares[i], r, ciphertextHashes[i]) + + commitments[i] = utils.HashWithDomain(DomainNIZK+"-com", concatInput) + } + + secretMsg := utils.HashWithDomain(DomainNIZK+"-msg", secret) + + challengeInput := utils.HashConcat(secretMsg, commitments[0], commitments[1], commitments[2], ciphertextHashes[0], ciphertextHashes[1], ciphertextHashes[2]) + + challenge := utils.SHA3256(challengeInput) + + responses := make([][]byte, 3) + for i := 0; i < 3; i++ { + domain := DomainNIZK + "-mask-" + strconv.Itoa(i) + domainHash := utils.HashWithDomain(domain, challenge) + + fullMask := utils.SHA3256(domainHash) + + mask := fullMask[:len(shares[i])] + + resp := make([]byte, len(shares[i])+32) + for j := 0; j < len(shares[i]); j++ { + resp[j] = shares[i][j] ^ mask[j] + } + copy(resp[len(shares[i]):], commitRandomness[i]) + responses[i] = resp } - for _, hash := range ciphertextHashes { - proofData = append(proofData, hash...) + + parts := [][]byte{challenge, commitments[0], commitments[1], commitments[2], responses[0], responses[1], responses[2]} + + buf := &bytes.Buffer{} + _ = binary.Write(buf, binary.LittleEndian, uint32(len(parts))) + for _, p := range parts { + _ = binary.Write(buf, binary.LittleEndian, uint32(len(p))) + buf.Write(p) } - return utils.HashWithDomain(DomainNIZK, proofData) + + result := buf.Bytes() + return result } // VerifyNIZKProof verifies a NIZK proof. // It verifies the proof is correctly bound to the ciphertext hashes and binding. -func VerifyNIZKProof(proof []byte, ciphertextHashes [][]byte, binding []byte) bool { - // Check proof has correct length - if len(proof) != 32 { +func VerifyNIZKProof(proof []byte, ciphertextHashes [][]byte, message []byte) bool { + // Validate ciphertext hashes first before accessing + if len(ciphertextHashes) != 3 { return false } - // Verify proof is not all zeros (trivial forgery attempt) - allZero := true - for _, b := range proof { - if b != 0 { - allZero = false - break + for _, h := range ciphertextHashes { + if len(h) != 32 { + return false } } - if allZero { + + // Legacy compact proof (32 bytes): accept if non-zero + if len(proof) == 32 { + for _, b := range proof { + if b != 0 { + return true + } + } return false } - // Verify binding is present and correct length - if len(binding) != 32 { + // Structured proof parsing + if len(proof) < 4 { + return false + } + off := 0 + numParts := int(binary.LittleEndian.Uint32(proof[off:])) + off += 4 + if numParts != 7 { return false } + parts := make([][]byte, numParts) + const MaxProofPartSize = 1024 * 1024 // 1MB maximum part size - // Verify all ciphertext hashes are present and correct length - if len(ciphertextHashes) != 3 { + for i := 0; i < numParts; i++ { + if off+4 > len(proof) { + return false + } + partLen := int(binary.LittleEndian.Uint32(proof[off:])) + off += 4 + + // Validate part size to prevent DoS via memory exhaustion + if partLen < 0 || partLen > MaxProofPartSize { + return false + } + if off+partLen > len(proof) { + return false + } + + parts[i] = make([]byte, partLen) + copy(parts[i], proof[off:off+partLen]) + off += partLen + } + + challenge := parts[0] + + // Validate challenge length (must be 32 bytes for SHA3-256) + if len(challenge) != 32 { return false } - for _, h := range ciphertextHashes { - if len(h) != 32 { + + commitments := parts[1:4] + responses := parts[4:7] + + // Recompute challenge + secretMsg := utils.HashWithDomain(DomainNIZK+"-msg", message) + + expectedChallengeInput := utils.HashConcat(secretMsg, commitments[0], commitments[1], commitments[2], ciphertextHashes[0], ciphertextHashes[1], ciphertextHashes[2]) + + expectedChallenge := utils.SHA3256(expectedChallengeInput) + + if !utils.ConstantTimeEqual(challenge, expectedChallenge) { + return false + } + + // Verify each response + for i := 0; i < 3; i++ { + resp := responses[i] + if len(resp) < 32 { + return false + } + shareLen := len(resp) - 32 + + commitRandomness := resp[shareLen:] + + domain := DomainNIZK + "-mask-" + strconv.Itoa(i) + domainHash := utils.HashWithDomain(domain, challenge) + + fullMask := utils.SHA3256(domainHash) + + mask := fullMask[:shareLen] + + share := make([]byte, shareLen) + for j := 0; j < shareLen; j++ { + share[j] = resp[j] ^ mask[j] + } + + concatInput := utils.HashConcat(share, commitRandomness, ciphertextHashes[i]) + + expectedCom := utils.HashWithDomain(DomainNIZK+"-com", concatInput) + + if !utils.ConstantTimeEqual(expectedCom, commitments[i]) { return false } } @@ -219,16 +347,12 @@ func VerifyNIZKProof(proof []byte, ciphertextHashes [][]byte, binding []byte) bo return true } -// SerializeNIZKProof serializes the proof to bytes. +// SerializeNIZKProof returns the serialized proof as-is. func SerializeNIZKProof(proof []byte) []byte { return proof } -// DeserializeNIZKProof deserializes bytes to a proof. -// Returns nil if data is not exactly 32 bytes (expected proof length). +// DeserializeNIZKProof returns the proof bytes; no parsing here (verification parses structure). func DeserializeNIZKProof(data []byte) []byte { - if len(data) != 32 { - return nil - } return data } diff --git a/entanglement/entanglement_test.go b/entanglement/entanglement_test.go index e7d434f..9faaf8b 100644 --- a/entanglement/entanglement_test.go +++ b/entanglement/entanglement_test.go @@ -102,7 +102,7 @@ func TestBinding(t *testing.T) { func TestNIZKProof(t *testing.T) { // Mock data for NIZK - must match security requirements secret := []byte("secret") - shares := [][]byte{[]byte("share1"), []byte("share2")} + shares := [][]byte{[]byte("share1"), []byte("share2"), []byte("share3")} // NIZK requires exactly 3 ciphertext hashes, each 32 bytes hashes := [][]byte{ make([]byte, 32), @@ -115,38 +115,35 @@ func TestNIZKProof(t *testing.T) { hashes[i][j] = byte(i*32 + j) } } - // Binding must be 32 bytes - binding := make([]byte, 32) - for i := range binding { - binding[i] = byte(i + 100) - } seed := []byte("seed") proof := GenerateNIZKProof(secret, shares, hashes, seed) - if len(proof) != 32 { - t.Errorf("GenerateNIZKProof returned wrong length: %d", len(proof)) + if len(proof) == 0 { + t.Fatalf("GenerateNIZKProof returned empty proof") } - if !VerifyNIZKProof(proof, hashes, binding) { - t.Error("VerifyNIZKProof failed") + if !VerifyNIZKProof(proof, hashes, secret) { + t.Error("VerifyNIZKProof failed for structured proof") } - // Test serialization + // Test serialization roundtrip (identity) serialized := SerializeNIZKProof(proof) - // DeserializeNIZKProof is not implemented/exported in the snippet I saw, - // but SerializeNIZKProof returns []byte so it's likely identity or simple copy. if !bytes.Equal(proof, serialized) { t.Error("Serialization roundtrip failed") } // Test that invalid inputs are rejected - if VerifyNIZKProof(proof, hashes, nil) { - t.Error("VerifyNIZKProof should reject nil binding") + if VerifyNIZKProof(make([]byte, 32), hashes, secret) { + t.Error("VerifyNIZKProof should reject all-zero compact proof") } - if VerifyNIZKProof(proof, hashes[:2], binding) { + if VerifyNIZKProof(proof, hashes[:2], secret) { t.Error("VerifyNIZKProof should reject wrong number of hashes") } - if VerifyNIZKProof(make([]byte, 32), hashes, binding) { - t.Error("VerifyNIZKProof should reject all-zero proof") + // Tamper a single byte in proof and expect rejection + proofTampered := make([]byte, len(proof)) + copy(proofTampered, proof) + proofTampered[len(proofTampered)-1] ^= 0xFF + if VerifyNIZKProof(proofTampered, hashes, secret) { + t.Error("VerifyNIZKProof should reject tampered proof") } } diff --git a/kem/kem.go b/kem/kem.go index fa78f71..78828a3 100644 --- a/kem/kem.go +++ b/kem/kem.go @@ -2,8 +2,11 @@ package kem import ( + "crypto/aes" + "crypto/cipher" "encoding/binary" "errors" + "fmt" "sync" kmosaic "github.com/BackendStack21/k-mosaic-go" @@ -180,12 +183,6 @@ func EncapsulateDeterministic(pk *kmosaic.MOSAICPublicKey, ephemeralSecret []byt }() wg.Wait() - // Zeroize shares and randomness after use - for _, share := range shares { - utils.Zeroize(share) - } - utils.Zeroize(randomness) - if err1 != nil { return nil, err1 } @@ -196,7 +193,7 @@ func EncapsulateDeterministic(pk *kmosaic.MOSAICPublicKey, ephemeralSecret []byt return nil, err3 } - // Generate NIZK proof + // Generate NIZK proof (must happen BEFORE zeroizing shares and randomness) ciphertextHashes := [][]byte{ utils.SHA3256(serializeSLSSCiphertext(c1)), utils.SHA3256(serializeTDDCiphertext(c2)), @@ -209,6 +206,12 @@ func EncapsulateDeterministic(pk *kmosaic.MOSAICPublicKey, ephemeralSecret []byt utils.HashWithDomain(DomainSLSS+"-nizk", randomness), ) + // Zeroize shares and randomness after NIZK proof generation + for _, share := range shares { + utils.Zeroize(share) + } + utils.Zeroize(randomness) + ciphertext := kmosaic.MOSAICCiphertext{ C1: *c1, C2: *c2, @@ -233,16 +236,6 @@ func EncapsulateDeterministic(pk *kmosaic.MOSAICPublicKey, ephemeralSecret []byt func Decapsulate(sk *kmosaic.MOSAICSecretKey, pk *kmosaic.MOSAICPublicKey, ct *kmosaic.MOSAICCiphertext) ([]byte, error) { params := pk.Params - // Verify NIZK proof - ciphertextHashes := [][]byte{ - utils.SHA3256(serializeSLSSCiphertext(&ct.C1)), - utils.SHA3256(serializeTDDCiphertext(&ct.C2)), - utils.SHA3256(serializeEGRWCiphertext(&ct.C3)), - } - if !entanglement.VerifyNIZKProof(ct.Proof, ciphertextHashes, pk.Binding) { - return implicitReject(sk, ct), nil - } - // Decrypt shares in parallel var wg sync.WaitGroup var m1, m2, m3 []byte @@ -281,14 +274,34 @@ func Decapsulate(sk *kmosaic.MOSAICSecretKey, pk *kmosaic.MOSAICPublicKey, ct *k return implicitReject(sk, ct), nil } - // Compare ciphertexts + // Compare ciphertexts in constant time; do NOT return early to avoid timing leaks originalCT := SerializeCiphertext(ct) reEncCT := SerializeCiphertext(&reEncResult.Ciphertext) + validDecapsulation := 1 if !utils.ConstantTimeEqual(originalCT, reEncCT) { - return implicitReject(sk, ct), nil + validDecapsulation = 0 + } + + // Verify NIZK proof using recovered ephemeral secret (post-reconstruction) + ciphertextHashes := [][]byte{ + utils.SHA3256(serializeSLSSCiphertext(&ct.C1)), + utils.SHA3256(serializeTDDCiphertext(&ct.C2)), + utils.SHA3256(serializeEGRWCiphertext(&ct.C3)), + } + if !entanglement.VerifyNIZKProof(ct.Proof, ciphertextHashes, ephemeralSecret) { + validDecapsulation = 0 } - return reEncResult.SharedSecret, nil + // Constant-time select between correct shared secret and implicit reject + correctSecret := reEncResult.SharedSecret + rejectSecret := implicitReject(sk, ct) + result := utils.ConstantTimeSelect(validDecapsulation, correctSecret, rejectSecret) + + // Zeroize temporary secrets + utils.Zeroize(rejectSecret) + utils.Zeroize(correctSecret) + + return result, nil } // implicitReject returns a deterministic but unpredictable rejection value. @@ -311,16 +324,16 @@ func Encrypt(pk *kmosaic.MOSAICPublicKey, plaintext []byte) (*kmosaic.EncryptedM nonce := utils.Shake256(utils.HashWithDomain(DomainNonce, result.SharedSecret), 12) defer utils.Zeroize(encKey) - // Simple XOR encryption (in production, use AES-GCM) - keystream := utils.Shake256(utils.HashConcat(encKey, nonce), len(plaintext)+16) - defer utils.Zeroize(keystream) - encrypted := make([]byte, len(plaintext)+16) - for i := 0; i < len(plaintext); i++ { - encrypted[i] = plaintext[i] ^ keystream[i] + // AEAD encryption using AES-256-GCM + block, err := aes.NewCipher(encKey) + if err != nil { + return nil, err } - // Add authentication tag - tag := utils.SHA3256(utils.HashConcat(encKey, plaintext)) - copy(encrypted[len(plaintext):], tag[:16]) + aead, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + encrypted := aead.Seal(nil, nonce, plaintext, nil) return &kmosaic.EncryptedMessage{ Ciphertext: result.Ciphertext, @@ -336,74 +349,81 @@ func Decrypt(sk *kmosaic.MOSAICSecretKey, pk *kmosaic.MOSAICPublicKey, em *kmosa return nil, err } - // Derive encryption key + // Derive encryption key and nonce encKey := utils.Shake256(utils.HashWithDomain(DomainEncKey, sharedSecret), 32) + nonce := utils.Shake256(utils.HashWithDomain(DomainNonce, sharedSecret), 12) defer utils.Zeroize(encKey) - // Decrypt - if len(em.Encrypted) < 16 { + // AEAD decryption using AES-256-GCM + if len(em.Encrypted) < 1 { return nil, errors.New("ciphertext too short") } - keystream := utils.Shake256(utils.HashConcat(encKey, em.Nonce), len(em.Encrypted)) - defer utils.Zeroize(keystream) - plaintextLen := len(em.Encrypted) - 16 - plaintext := make([]byte, plaintextLen) - for i := 0; i < plaintextLen; i++ { - plaintext[i] = em.Encrypted[i] ^ keystream[i] + block, err := aes.NewCipher(encKey) + if err != nil { + return nil, err } - - // Verify tag - expectedTag := utils.SHA3256(utils.HashConcat(encKey, plaintext)) - if !utils.ConstantTimeEqual(em.Encrypted[plaintextLen:], expectedTag[:16]) { + aead, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + plaintext, err := aead.Open(nil, nonce, em.Encrypted, nil) + if err != nil { return nil, errors.New("authentication failed") } - return plaintext, nil } // SerializePublicKey serializes a public key. +// Format: [level_len:4][level_string][slss_len:4][slss_data][tdd_len:4][tdd_data][egrw_len:4][egrw_data][binding:32] func SerializePublicKey(pk *kmosaic.MOSAICPublicKey) []byte { slssBytes := slss.SerializePublicKey(pk.SLSS) tddBytes := tdd.SerializePublicKey(pk.TDD) egrwBytes := egrw.SerializePublicKey(pk.EGRW) - // Serialize params (security level as string) - levelBytes := []byte(pk.Params.Level) + // Serialize security level as string + levelStr := string(pk.Params.Level) + levelBytes := []byte(levelStr) - result := make([]byte, 0, 16+len(slssBytes)+len(tddBytes)+len(egrwBytes)+len(pk.Binding)+len(levelBytes)) + result := make([]byte, 0, 16+len(levelBytes)+len(slssBytes)+len(tddBytes)+len(egrwBytes)+32) - // Length prefixes + // Length prefixes buffer lenBuf := make([]byte, 4) - // Security level + // Security level string binary.LittleEndian.PutUint32(lenBuf, uint32(len(levelBytes))) result = append(result, lenBuf...) result = append(result, levelBytes...) + // SLSS component binary.LittleEndian.PutUint32(lenBuf, uint32(len(slssBytes))) result = append(result, lenBuf...) result = append(result, slssBytes...) + // TDD component binary.LittleEndian.PutUint32(lenBuf, uint32(len(tddBytes))) result = append(result, lenBuf...) result = append(result, tddBytes...) + // EGRW component binary.LittleEndian.PutUint32(lenBuf, uint32(len(egrwBytes))) result = append(result, lenBuf...) result = append(result, egrwBytes...) + // Binding (fixed 32 bytes, no length prefix) result = append(result, pk.Binding...) return result } // SerializeCiphertext serializes a ciphertext. +// Format: [c1_len:4][c1_data][c2_len:4][c2_data][c3_len:4][c3_data][proof_data] +// Note: proof has no length prefix - it extends to end of buffer func SerializeCiphertext(ct *kmosaic.MOSAICCiphertext) []byte { c1Bytes := serializeSLSSCiphertext(&ct.C1) c2Bytes := serializeTDDCiphertext(&ct.C2) c3Bytes := serializeEGRWCiphertext(&ct.C3) - result := make([]byte, 0, 16+len(c1Bytes)+len(c2Bytes)+len(c3Bytes)+len(ct.Proof)) + result := make([]byte, 0, 12+len(c1Bytes)+len(c2Bytes)+len(c3Bytes)+len(ct.Proof)) lenBuf := make([]byte, 4) binary.LittleEndian.PutUint32(lenBuf, uint32(len(c1Bytes))) @@ -418,22 +438,24 @@ func SerializeCiphertext(ct *kmosaic.MOSAICCiphertext) []byte { result = append(result, lenBuf...) result = append(result, c3Bytes...) - binary.LittleEndian.PutUint32(lenBuf, uint32(len(ct.Proof))) - result = append(result, lenBuf...) + // Proof has no length prefix - just append to end (matching Node implementation) result = append(result, ct.Proof...) return result } func serializeSLSSCiphertext(ct *kmosaic.SLSSCiphertext) []byte { - result := make([]byte, 8+len(ct.U)*4+len(ct.V)*4) - binary.LittleEndian.PutUint32(result[0:], uint32(len(ct.U))) + uBytes := len(ct.U) * 4 + vBytes := len(ct.V) * 4 + result := make([]byte, 8+uBytes+vBytes) + // Write byte lengths (not element counts) + binary.LittleEndian.PutUint32(result[0:], uint32(uBytes)) offset := 4 for i, v := range ct.U { binary.LittleEndian.PutUint32(result[offset+i*4:], uint32(v)) } - offset += len(ct.U) * 4 - binary.LittleEndian.PutUint32(result[offset:], uint32(len(ct.V))) + offset += uBytes + binary.LittleEndian.PutUint32(result[offset:], uint32(vBytes)) offset += 4 for i, v := range ct.V { binary.LittleEndian.PutUint32(result[offset+i*4:], uint32(v)) @@ -442,8 +464,10 @@ func serializeSLSSCiphertext(ct *kmosaic.SLSSCiphertext) []byte { } func serializeTDDCiphertext(ct *kmosaic.TDDCiphertext) []byte { - result := make([]byte, 4+len(ct.Data)*4) - binary.LittleEndian.PutUint32(result[0:], uint32(len(ct.Data))) + dataBytes := len(ct.Data) * 4 + result := make([]byte, 4+dataBytes) + // Write byte length (not element count) + binary.LittleEndian.PutUint32(result[0:], uint32(dataBytes)) for i, v := range ct.Data { binary.LittleEndian.PutUint32(result[4+i*4:], uint32(v)) } @@ -522,6 +546,7 @@ func SerializeSecretKey(sk *kmosaic.MOSAICSecretKey) []byte { } // DeserializePublicKey deserializes bytes to a public key. +// Format: [level_len:4][level_string][slss_len:4][slss_data][tdd_len:4][tdd_data][egrw_len:4][egrw_data][binding:32] func DeserializePublicKey(data []byte) (*kmosaic.MOSAICPublicKey, error) { if len(data) < 16 { return nil, errors.New("invalid public key data: too short") @@ -530,19 +555,22 @@ func DeserializePublicKey(data []byte) (*kmosaic.MOSAICPublicKey, error) { offset := 0 pk := &kmosaic.MOSAICPublicKey{} - // Read security level + // Read security level string levelLen := int(binary.LittleEndian.Uint32(data[offset:])) offset += 4 if offset+levelLen > len(data) { - return nil, errors.New("invalid public key: security level truncated") + return nil, errors.New("invalid public key: level string truncated") } - level := kmosaic.SecurityLevel(data[offset : offset+levelLen]) + levelStr := string(data[offset : offset+levelLen]) + offset += levelLen + + // Get params from level string + level := kmosaic.SecurityLevel(levelStr) params, err := core.GetParams(level) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid public key: unknown security level %q: %w", levelStr, err) } pk.Params = params - offset += levelLen // Read SLSS public key slssLen := int(binary.LittleEndian.Uint32(data[offset:])) @@ -583,14 +611,23 @@ func DeserializePublicKey(data []byte) (*kmosaic.MOSAICPublicKey, error) { pk.EGRW = *egrwPK offset += egrwLen - // Rest is binding (32 bytes) + // Read binding (fixed 32 bytes, no length prefix) if offset+32 > len(data) { return nil, errors.New("invalid public key: binding truncated") } pk.Binding = make([]byte, 32) copy(pk.Binding, data[offset:offset+32]) - // Cross-field consistency checks + // Validate binding to prevent component substitution attacks + slssBytes := slss.SerializePublicKey(pk.SLSS) + tddBytes := tdd.SerializePublicKey(pk.TDD) + egrwBytes := egrw.SerializePublicKey(pk.EGRW) + expectedBinding := entanglement.ComputeBinding(slssBytes, tddBytes, egrwBytes) + if !utils.ConstantTimeEqual(pk.Binding, expectedBinding) { + return nil, errors.New("invalid public key: binding mismatch") + } + + // Validate consistency with params expectedSLSSALen := params.SLSS.M * params.SLSS.N if len(pk.SLSS.A) != expectedSLSSALen { return nil, errors.New("invalid public key: SLSS.A length mismatch with params") @@ -733,8 +770,10 @@ func DeserializeSecretKey(data []byte) (*kmosaic.MOSAICSecretKey, error) { } // DeserializeCiphertext deserializes bytes to a ciphertext. +// Format: [c1_len:4][c1_data][c2_len:4][c2_data][c3_len:4][c3_data][proof_data] +// Note: proof has no length prefix - it's everything remaining in buffer func DeserializeCiphertext(data []byte) (*kmosaic.MOSAICCiphertext, error) { - if len(data) < 16 { + if len(data) < 12 { return nil, errors.New("invalid ciphertext data: too short") } @@ -780,14 +819,9 @@ func DeserializeCiphertext(data []byte) (*kmosaic.MOSAICCiphertext, error) { ct.C3 = *c3 offset += c3Len - // Read proof - proofLen := int(binary.LittleEndian.Uint32(data[offset:])) - offset += 4 - if offset+proofLen > len(data) { - return nil, errors.New("invalid ciphertext: proof truncated") - } - ct.Proof = make([]byte, proofLen) - copy(ct.Proof, data[offset:offset+proofLen]) + // Read proof - everything remaining in buffer + ct.Proof = make([]byte, len(data)-offset) + copy(ct.Proof, data[offset:]) return ct, nil } @@ -799,9 +833,14 @@ func deserializeSLSSCiphertext(data []byte) (*kmosaic.SLSSCiphertext, error) { ct := &kmosaic.SLSSCiphertext{} offset := 0 - uLen := int(binary.LittleEndian.Uint32(data[offset:])) + // Read byte length (not element count) + uBytes := int(binary.LittleEndian.Uint32(data[offset:])) offset += 4 - if uLen < 0 || uLen > (len(data)-offset)/4 { + if uBytes%4 != 0 { + return nil, errors.New("invalid SLSS ciphertext: U length not multiple of 4") + } + uLen := uBytes / 4 + if offset+uBytes > len(data) { return nil, errors.New("invalid SLSS ciphertext: U truncated") } ct.U = make([]int32, uLen) @@ -813,9 +852,14 @@ func deserializeSLSSCiphertext(data []byte) (*kmosaic.SLSSCiphertext, error) { if offset+4 > len(data) { return nil, errors.New("invalid SLSS ciphertext: missing V length") } - vLen := int(binary.LittleEndian.Uint32(data[offset:])) + // Read byte length (not element count) + vBytes := int(binary.LittleEndian.Uint32(data[offset:])) offset += 4 - if vLen < 0 || vLen > (len(data)-offset)/4 { + if vBytes%4 != 0 { + return nil, errors.New("invalid SLSS ciphertext: V length not multiple of 4") + } + vLen := vBytes / 4 + if offset+vBytes > len(data) { return nil, errors.New("invalid SLSS ciphertext: V truncated") } ct.V = make([]int32, vLen) @@ -832,8 +876,13 @@ func deserializeTDDCiphertext(data []byte) (*kmosaic.TDDCiphertext, error) { return nil, errors.New("invalid TDD ciphertext") } ct := &kmosaic.TDDCiphertext{} - dataLen := int(binary.LittleEndian.Uint32(data[0:])) - if dataLen < 0 || dataLen > (len(data)-4)/4 { + // Read byte length (not element count) + dataBytes := int(binary.LittleEndian.Uint32(data[0:])) + if dataBytes%4 != 0 { + return nil, errors.New("invalid TDD ciphertext: data length not multiple of 4") + } + dataLen := dataBytes / 4 + if 4+dataBytes > len(data) { return nil, errors.New("invalid TDD ciphertext: data truncated") } ct.Data = make([]int32, dataLen) @@ -855,40 +904,38 @@ func deserializeEGRWCiphertext(data []byte) (*kmosaic.EGRWCiphertext, error) { } // SerializeEncryptedMessage serializes an encrypted message. +// Format: [kemCt_len:4][kemCt_data][aes_encrypted_data] +// Note: nonce is not stored - it's derived from shared secret during decryption func SerializeEncryptedMessage(em *kmosaic.EncryptedMessage) []byte { ctBytes := SerializeCiphertext(&em.Ciphertext) - result := make([]byte, 0, 12+len(ctBytes)+len(em.Encrypted)+len(em.Nonce)) + result := make([]byte, 0, 4+len(ctBytes)+len(em.Encrypted)) lenBuf := make([]byte, 4) - // Ciphertext + // KEM Ciphertext length and data binary.LittleEndian.PutUint32(lenBuf, uint32(len(ctBytes))) result = append(result, lenBuf...) result = append(result, ctBytes...) - // Encrypted payload - binary.LittleEndian.PutUint32(lenBuf, uint32(len(em.Encrypted))) - result = append(result, lenBuf...) + // AES-GCM encrypted payload (includes auth tag) + // No length prefix - everything remaining is encrypted data result = append(result, em.Encrypted...) - // Nonce - binary.LittleEndian.PutUint32(lenBuf, uint32(len(em.Nonce))) - result = append(result, lenBuf...) - result = append(result, em.Nonce...) - return result } // DeserializeEncryptedMessage deserializes bytes to an encrypted message. +// Format: [kemCt_len:4][kemCt_data][aes_encrypted_data] +// Note: nonce is not stored - it will be derived from shared secret during decryption func DeserializeEncryptedMessage(data []byte) (*kmosaic.EncryptedMessage, error) { - if len(data) < 12 { + if len(data) < 4 { return nil, errors.New("invalid encrypted message: too short") } em := &kmosaic.EncryptedMessage{} offset := 0 - // Read ciphertext + // Read KEM ciphertext ctLen := int(binary.LittleEndian.Uint32(data[offset:])) offset += 4 if offset+ctLen > len(data) { @@ -901,24 +948,12 @@ func DeserializeEncryptedMessage(data []byte) (*kmosaic.EncryptedMessage, error) em.Ciphertext = *ct offset += ctLen - // Read encrypted payload - encLen := int(binary.LittleEndian.Uint32(data[offset:])) - offset += 4 - if offset+encLen > len(data) { - return nil, errors.New("invalid encrypted message: encrypted payload truncated") - } - em.Encrypted = make([]byte, encLen) - copy(em.Encrypted, data[offset:offset+encLen]) - offset += encLen + // Read AES-GCM encrypted payload - everything remaining + em.Encrypted = make([]byte, len(data)-offset) + copy(em.Encrypted, data[offset:]) - // Read nonce - nonceLen := int(binary.LittleEndian.Uint32(data[offset:])) - offset += 4 - if offset+nonceLen > len(data) { - return nil, errors.New("invalid encrypted message: nonce truncated") - } - em.Nonce = make([]byte, nonceLen) - copy(em.Nonce, data[offset:offset+nonceLen]) + // Nonce will be derived from shared secret during decryption (not stored) + em.Nonce = nil return em, nil } diff --git a/kem/kem_test.go b/kem/kem_test.go index 750163a..eea789e 100644 --- a/kem/kem_test.go +++ b/kem/kem_test.go @@ -311,3 +311,25 @@ func TestDeserializeErrors(t *testing.T) { t.Error("DeserializePublicKey should fail with invalid security level") } } + +func TestDeserializePublicKeyRejectsInvalidBinding(t *testing.T) { + kp, err := GenerateKeyPair(kmosaic.MOS_128) + if err != nil { + t.Fatalf("GenerateKeyPair failed: %v", err) + } + + serialized := SerializePublicKey(&kp.PublicKey) + if len(serialized) < 40 { + t.Fatal("serialized public key unexpectedly short") + } + + // Tamper with binding (last byte) + bad := make([]byte, len(serialized)) + copy(bad, serialized) + bad[len(bad)-1] ^= 0xFF + + _, err = DeserializePublicKey(bad) + if err == nil { + t.Error("DeserializePublicKey should reject invalid binding") + } +} diff --git a/mosaic.go b/mosaic.go index 797d9a4..e7c2388 100644 --- a/mosaic.go +++ b/mosaic.go @@ -9,7 +9,7 @@ package kmosaic // Users can also import specific sub-packages directly for more control. // Version of the kMOSAIC Go implementation. -const Version = "1.0.1" +const Version = "1.0.2" // API summary: // diff --git a/problems/slss/slss.go b/problems/slss/slss.go index dbee053..45f1144 100644 --- a/problems/slss/slss.go +++ b/problems/slss/slss.go @@ -132,7 +132,7 @@ func matTVecMul(A []int32, v []int32, m, n, q int) []int32 { func vecAdd(a, b []int32, q int) []int32 { result := make([]int32, len(a)) for i := range a { - result[i] = fastMod(int64(a[i])+int64(b[i]), q) + result[i] = mod(int64(a[i])+int64(b[i]), q) } return result } @@ -369,6 +369,86 @@ func KeyGen(params kmosaic.SLSSParams, seed []byte) (*kmosaic.SLSSKeyPair, error }, nil } +// DebugInfo for SLSS encryption internals +type SlssDebugInfo struct { + RIndices []int `json:"r_indices"` + RValues []int8 `json:"r_values"` + E1Head []int32 `json:"e1_head"` + E2Head []int32 `json:"e2_head"` + UHead []int32 `json:"u_head"` + VHead []int32 `json:"v_head"` +} + +// DebugEncrypt performs SLSS encryption but also returns internal debug information +func DebugEncrypt(pk kmosaic.SLSSPublicKey, message []byte, params kmosaic.SLSSParams, randomness []byte) (*kmosaic.SLSSCiphertext, *SlssDebugInfo, error) { + if len(randomness) < 32 { + return nil, nil, errors.New("randomness must be at least 32 bytes") + } + if len(message) > utils.MaxMessageSize { + return nil, nil, errors.New("message exceeds maximum allowed size") + } + + n, m, q, sigma := params.N, params.M, params.Q, params.Sigma + + // Sample ephemeral values (with domain separation as in Encrypt) + rSeed := utils.HashWithDomain(DomainEphemeral, randomness) + e1Seed := utils.HashWithDomain(DomainError1, randomness) + e2Seed := utils.HashWithDomain(DomainError2, randomness) + + r := sampleSparseVector(rSeed, m, params.W) + e1 := sampleError(e1Seed, n, sigma) + e2 := sampleError(e2Seed, len(message)*8, sigma) + + // Convert r to int32 for matTVecMul + rInt32 := make([]int32, len(r)) + for i, v := range r { + rInt32[i] = int32(v) + } + + ATr := matTVecMul(pk.A, rInt32, m, n, q) + u := vecAdd(ATr, e1, q) + + tTr := innerProduct(pk.T, r, q) + encoded := encodeMessage(message, q) + + v := make([]int32, len(encoded)) + for i := range encoded { + val := int64(tTr) + int64(e2[i%len(e2)]) + int64(encoded[i]) + v[i] = mod(val, q) + } + + // Collect debug info (small heads) + debug := &SlssDebugInfo{} + // Record non-zero indices and values for r (first up to 16) + for i := 0; i < len(r) && len(debug.RIndices) < 16; i++ { + if r[i] != 0 { + debug.RIndices = append(debug.RIndices, i) + debug.RValues = append(debug.RValues, r[i]) + } + } + // Heads of error and ciphertext components + k := func(arr []int32, n int) []int32 { + if len(arr) < n { + n = len(arr) + } + out := make([]int32, n) + copy(out, arr[:n]) + return out + } + debug.E1Head = k(e1, 8) + debug.E2Head = k(e2, 8) + debug.UHead = k(u, 8) + debug.VHead = k(v, 8) + + // Zeroize ephemeral values + utils.ZeroizeInt32(ATr) + utils.ZeroizeInt32(e1) + utils.ZeroizeInt32(e2) + utils.ZeroizeInt32(encoded) + + return &kmosaic.SLSSCiphertext{U: u, V: v}, debug, nil +} + // Encrypt encrypts a message fragment using SLSS func Encrypt(pk kmosaic.SLSSPublicKey, message []byte, params kmosaic.SLSSParams, randomness []byte) (*kmosaic.SLSSCiphertext, error) { if len(randomness) < 32 { @@ -445,14 +525,16 @@ func SerializePublicKey(pk kmosaic.SLSSPublicKey) []byte { tBytes := len(pk.T) * 4 result := make([]byte, 8+aBytes+tBytes) - binary.LittleEndian.PutUint32(result[0:], uint32(len(pk.A))) + // Write byte length (not element count) + binary.LittleEndian.PutUint32(result[0:], uint32(aBytes)) offset := 4 for i, v := range pk.A { binary.LittleEndian.PutUint32(result[offset+i*4:], uint32(v)) } offset += aBytes - binary.LittleEndian.PutUint32(result[offset:], uint32(len(pk.T))) + // Write byte length (not element count) + binary.LittleEndian.PutUint32(result[offset:], uint32(tBytes)) offset += 4 for i, v := range pk.T { binary.LittleEndian.PutUint32(result[offset+i*4:], uint32(v)) @@ -470,16 +552,16 @@ func DeserializePublicKey(data []byte) (*kmosaic.SLSSPublicKey, error) { pk := &kmosaic.SLSSPublicKey{} offset := 0 - rawA := binary.LittleEndian.Uint32(data[offset:]) - if rawA > uint32(utils.MaxMatrixElements) { + // Read byte length (not element count) + aBytes := int(binary.LittleEndian.Uint32(data[offset:])) + if aBytes%4 != 0 { + return nil, errors.New("invalid SLSS public key: A length not multiple of 4") + } + aLen := aBytes / 4 + if aLen > utils.MaxMatrixElements { return nil, errors.New("invalid SLSS public key: A length exceeds limit") } - aLen := int(rawA) offset += 4 - aBytes, err := utils.SafeMultiply(aLen, 4) - if err != nil { - return nil, errors.New("invalid SLSS public key: A length overflow") - } if offset+aBytes > len(data) { return nil, errors.New("invalid SLSS public key: A data truncated") } @@ -492,16 +574,16 @@ func DeserializePublicKey(data []byte) (*kmosaic.SLSSPublicKey, error) { if offset+4 > len(data) { return nil, errors.New("invalid SLSS public key: missing T length") } - rawT := binary.LittleEndian.Uint32(data[offset:]) - if rawT > uint32(utils.MaxVectorLength) { + // Read byte length (not element count) + tBytes := int(binary.LittleEndian.Uint32(data[offset:])) + if tBytes%4 != 0 { + return nil, errors.New("invalid SLSS public key: T length not multiple of 4") + } + tLen := tBytes / 4 + if tLen > utils.MaxVectorLength { return nil, errors.New("invalid SLSS public key: T length exceeds limit") } - tLen := int(rawT) offset += 4 - tBytes, err := utils.SafeMultiply(tLen, 4) - if err != nil { - return nil, errors.New("invalid SLSS public key: T length overflow") - } if offset+tBytes > len(data) { return nil, errors.New("invalid SLSS public key: T data truncated") } diff --git a/problems/tdd/tdd.go b/problems/tdd/tdd.go index 606bd33..386f8f6 100644 --- a/problems/tdd/tdd.go +++ b/problems/tdd/tdd.go @@ -356,8 +356,10 @@ func Decrypt(ct *kmosaic.TDDCiphertext, sk kmosaic.TDDSecretKey, pk kmosaic.TDDP // SerializePublicKey serializes TDD public key func SerializePublicKey(pk kmosaic.TDDPublicKey) []byte { - result := make([]byte, 4+len(pk.T)*4) - binary.LittleEndian.PutUint32(result[0:], uint32(len(pk.T))) + tBytes := len(pk.T) * 4 + result := make([]byte, 4+tBytes) + // Write byte length (not element count) + binary.LittleEndian.PutUint32(result[0:], uint32(tBytes)) for i, v := range pk.T { binary.LittleEndian.PutUint32(result[4+i*4:], uint32(v)) } @@ -371,16 +373,16 @@ func DeserializePublicKey(data []byte) (*kmosaic.TDDPublicKey, error) { } pk := &kmosaic.TDDPublicKey{} - raw := binary.LittleEndian.Uint32(data[0:]) - if raw > uint32(utils.MaxTensorElements) { - return nil, errors.New("invalid TDD public key: T length exceeds limit") + // Read byte length (not element count) + tBytes := int(binary.LittleEndian.Uint32(data[0:])) + if tBytes%4 != 0 { + return nil, errors.New("invalid TDD public key: T length not multiple of 4") } - tLen := int(raw) - requiredBytes, err := utils.SafeMultiply(tLen, 4) - if err != nil { - return nil, errors.New("invalid TDD public key: T length overflow") + tLen := tBytes / 4 + if tLen > utils.MaxTensorElements { + return nil, errors.New("invalid TDD public key: T length exceeds limit") } - if 4+requiredBytes > len(data) { + if 4+tBytes > len(data) { return nil, errors.New("invalid TDD public key: T data truncated") } pk.T = make([]int32, tLen) diff --git a/test/integration_test.go b/test/integration_test.go new file mode 100644 index 0000000..5d6aff1 --- /dev/null +++ b/test/integration_test.go @@ -0,0 +1,507 @@ +// Package test provides integration tests for kMOSAIC implementation. +// These tests verify cross-component integration and protocol compliance. +package test + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "testing" + + kmosaic "github.com/BackendStack21/k-mosaic-go" + "github.com/BackendStack21/k-mosaic-go/kem" + "github.com/BackendStack21/k-mosaic-go/sign" +) + +// TestKEMRoundtrip tests key generation, encapsulation, and decapsulation. +func TestKEMRoundtrip(t *testing.T) { + levels := []kmosaic.SecurityLevel{kmosaic.MOS_128, kmosaic.MOS_256} + + for _, level := range levels { + t.Run(string(level), func(t *testing.T) { + // Generate key pair + kp, err := kem.GenerateKeyPair(level) + if err != nil { + t.Fatalf("GenerateKeyPair failed: %v", err) + } + + // Encapsulate + result, err := kem.Encapsulate(&kp.PublicKey) + if err != nil { + t.Fatalf("Encapsulate failed: %v", err) + } + + if len(result.SharedSecret) != 32 { + t.Errorf("SharedSecret length = %d, want 32", len(result.SharedSecret)) + } + + // Decapsulate + recoveredSecret, err := kem.Decapsulate(&kp.SecretKey, &kp.PublicKey, &result.Ciphertext) + if err != nil { + t.Fatalf("Decapsulate failed: %v", err) + } + + // Verify secrets match + if !bytes.Equal(result.SharedSecret, recoveredSecret) { + t.Error("Shared secrets do not match") + } + }) + } +} + +// TestKEMSerialization tests public key and ciphertext serialization. +func TestKEMSerialization(t *testing.T) { + levels := []kmosaic.SecurityLevel{kmosaic.MOS_128, kmosaic.MOS_256} + + for _, level := range levels { + t.Run(string(level), func(t *testing.T) { + // Generate key pair + kp, err := kem.GenerateKeyPair(level) + if err != nil { + t.Fatalf("GenerateKeyPair failed: %v", err) + } + + // Serialize public key + pkBytes := kem.SerializePublicKey(&kp.PublicKey) + + // Deserialize public key + pk2, err := kem.DeserializePublicKey(pkBytes) + if err != nil { + t.Fatalf("DeserializePublicKey failed: %v", err) + } + + // Verify params match + if pk2.Params.Level != level { + t.Errorf("Level mismatch: got %s, want %s", pk2.Params.Level, level) + } + + // Verify binding matches + if !bytes.Equal(kp.PublicKey.Binding, pk2.Binding) { + t.Error("Binding mismatch after serialization") + } + + // Encapsulate with original key + result, err := kem.Encapsulate(&kp.PublicKey) + if err != nil { + t.Fatalf("Encapsulate failed: %v", err) + } + + // Serialize ciphertext + ctBytes := kem.SerializeCiphertext(&result.Ciphertext) + + // Deserialize ciphertext + ct2, err := kem.DeserializeCiphertext(ctBytes) + if err != nil { + t.Fatalf("DeserializeCiphertext failed: %v", err) + } + + // Decapsulate with deserialized ciphertext + recoveredSecret, err := kem.Decapsulate(&kp.SecretKey, &kp.PublicKey, ct2) + if err != nil { + t.Fatalf("Decapsulate with deserialized ciphertext failed: %v", err) + } + + // Verify secrets match + if !bytes.Equal(result.SharedSecret, recoveredSecret) { + t.Error("Shared secrets do not match after serialization roundtrip") + } + }) + } +} + +// TestKEMInvalidCiphertext tests implicit rejection on invalid ciphertext. +func TestKEMInvalidCiphertext(t *testing.T) { + kp, err := kem.GenerateKeyPair(kmosaic.MOS_128) + if err != nil { + t.Fatalf("GenerateKeyPair failed: %v", err) + } + + // Create valid ciphertext + result, err := kem.Encapsulate(&kp.PublicKey) + if err != nil { + t.Fatalf("Encapsulate failed: %v", err) + } + + // Tamper with ciphertext (modify first byte of proof) + tamperedCT := result.Ciphertext + if len(tamperedCT.Proof) > 0 { + tamperedCT.Proof[0] ^= 0xFF + } + + // Decapsulate tampered ciphertext + rejectedSecret, err := kem.Decapsulate(&kp.SecretKey, &kp.PublicKey, &tamperedCT) + if err != nil { + t.Fatalf("Decapsulate with tampered ciphertext failed: %v", err) + } + + // Verify rejected secret differs from correct secret + if bytes.Equal(result.SharedSecret, rejectedSecret) { + t.Error("Tampered ciphertext produced same shared secret (implicit rejection failed)") + } + + // Verify rejected secret is deterministic + rejectedSecret2, err := kem.Decapsulate(&kp.SecretKey, &kp.PublicKey, &tamperedCT) + if err != nil { + t.Fatalf("Second decapsulate with tampered ciphertext failed: %v", err) + } + + if !bytes.Equal(rejectedSecret, rejectedSecret2) { + t.Error("Implicit rejection not deterministic") + } +} + +// TestSignRoundtrip tests signature generation and verification. +func TestSignRoundtrip(t *testing.T) { + levels := []kmosaic.SecurityLevel{kmosaic.MOS_128, kmosaic.MOS_256} + + for _, level := range levels { + t.Run(string(level), func(t *testing.T) { + // Generate signing key pair + kp, err := sign.GenerateKeyPair(level) + if err != nil { + t.Fatalf("GenerateKeyPair failed: %v", err) + } + + // Message to sign + message := []byte("Hello, kMOSAIC! This is a test message for digital signatures.") + + // Sign message + sig, err := sign.Sign(&kp.SecretKey, &kp.PublicKey, message) + if err != nil { + t.Fatalf("Sign failed: %v", err) + } + + // Verify signature + valid := sign.Verify(&kp.PublicKey, message, sig) + + if !valid { + t.Error("Valid signature rejected") + } + + // Tamper with message + tamperedMessage := append([]byte{}, message...) + tamperedMessage[0] ^= 0xFF + + // Verify should fail on tampered message + valid = sign.Verify(&kp.PublicKey, tamperedMessage, sig) + + if valid { + t.Error("Tampered message signature accepted") + } + }) + } +} + +// TestHybridEncryption tests full encryption/decryption flow. +func TestHybridEncryption(t *testing.T) { + kp, err := kem.GenerateKeyPair(kmosaic.MOS_128) + if err != nil { + t.Fatalf("GenerateKeyPair failed: %v", err) + } + + // Test various message sizes + testCases := []struct { + name string + size int + }{ + {"empty", 0}, + {"small", 16}, + {"medium", 1024}, + {"large", 64 * 1024}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create test message + plaintext := make([]byte, tc.size) + for i := range plaintext { + plaintext[i] = byte(i % 256) + } + + // Encrypt + em, err := kem.Encrypt(&kp.PublicKey, plaintext) + if err != nil { + t.Fatalf("Encrypt failed: %v", err) + } + + // Decrypt + decrypted, err := kem.Decrypt(&kp.SecretKey, &kp.PublicKey, em) + if err != nil { + t.Fatalf("Decrypt failed: %v", err) + } + + // Verify plaintext matches + if !bytes.Equal(plaintext, decrypted) { + t.Error("Decrypted plaintext does not match original") + } + }) + } +} + +// TestCLICommands tests CLI command integration. +func TestCLICommands(t *testing.T) { + // Build CLI if not already built + cliPath := filepath.Join("..", "cmd", "k-mosaic-cli", "k-mosaic-cli") + if _, err := os.Stat(cliPath); os.IsNotExist(err) { + // Try to build + cmd := exec.Command("go", "build", "-o", cliPath, "./cmd/k-mosaic-cli") + if err := cmd.Run(); err != nil { + t.Skipf("Cannot build CLI: %v", err) + } + } + + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "kmosaic-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Test key generation + t.Run("keygen", func(t *testing.T) { + keyFile := filepath.Join(tmpDir, "test") + + cmd := exec.Command(cliPath, "kem", "keygen", "--level", "MOS-128", "--output", keyFile) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("keygen failed: %v\nOutput: %s", err, output) + } + + // Check file exists (output is JSON with both keys) + if _, err := os.Stat(keyFile); err != nil { + t.Errorf("Key file not created: %v", err) + } + }) + + // Test encryption/decryption + t.Run("encrypt-decrypt", func(t *testing.T) { + // Generate keys first + cmd := exec.Command(cliPath, "kem", "keygen", "--level", "MOS-128", "--output", filepath.Join(tmpDir, "enc-test")) + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("keygen failed: %v\nOutput: %s", err, output) + } + + // Create test file + plaintext := []byte("This is a test message for CLI encryption.") + plaintextPath := filepath.Join(tmpDir, "plaintext.txt") + if err := os.WriteFile(plaintextPath, plaintext, 0644); err != nil { + t.Fatalf("Failed to write plaintext file: %v", err) + } + + // Encrypt + encryptedPath := filepath.Join(tmpDir, "encrypted.bin") + cmd = exec.Command(cliPath, "kem", "encrypt", + "--public-key", filepath.Join(tmpDir, "enc-test"), + "--input", plaintextPath, + "--output", encryptedPath) + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("encrypt failed: %v\nOutput: %s", err, output) + } + + // Decrypt + decryptedPath := filepath.Join(tmpDir, "decrypted.txt") + cmd = exec.Command(cliPath, "kem", "decrypt", + "--secret-key", filepath.Join(tmpDir, "enc-test"), + "--public-key", filepath.Join(tmpDir, "enc-test"), + "--ciphertext", encryptedPath, + "--output", decryptedPath) + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("decrypt failed: %v\nOutput: %s", err, output) + } + + // Verify decrypted matches original + decrypted, err := os.ReadFile(decryptedPath) + if err != nil { + t.Fatalf("Failed to read decrypted file: %v", err) + } + + if !bytes.Equal(plaintext, decrypted) { + t.Error("Decrypted content does not match original") + } + }) + + // Test signing/verification + t.Run("sign-verify", func(t *testing.T) { + // Generate signing keys + cmd := exec.Command(cliPath, "sign", "keygen", "--level", "MOS-128", "--output", filepath.Join(tmpDir, "sign-test")) + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("sign keygen failed: %v\nOutput: %s", err, output) + } + + // Create test file + message := []byte("This is a test message for CLI signing.") + messagePath := filepath.Join(tmpDir, "message.txt") + if err := os.WriteFile(messagePath, message, 0644); err != nil { + t.Fatalf("Failed to write message file: %v", err) + } + + // Sign + sigPath := filepath.Join(tmpDir, "message.sig") + cmd = exec.Command(cliPath, "sign", "sign", + "--secret-key", filepath.Join(tmpDir, "sign-test"), + "--public-key", filepath.Join(tmpDir, "sign-test"), + "--input", messagePath, + "--output", sigPath) + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("sign failed: %v\nOutput: %s", err, output) + } + + // Verify + cmd = exec.Command(cliPath, "sign", "verify", + "--public-key", filepath.Join(tmpDir, "sign-test"), + "--input", messagePath, + "--signature", sigPath) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("verify failed: %v\nOutput: %s", err, output) + } + + if !bytes.Contains(output, []byte("valid")) && !bytes.Contains(output, []byte("Valid")) { + t.Errorf("Verification did not report valid signature. Output: %s", output) + } + }) +} + +// TestBindingValidation tests that binding validation prevents component substitution. +func TestBindingValidation(t *testing.T) { + // Generate two different key pairs + kp1, err := kem.GenerateKeyPair(kmosaic.MOS_128) + if err != nil { + t.Fatalf("GenerateKeyPair 1 failed: %v", err) + } + + kp2, err := kem.GenerateKeyPair(kmosaic.MOS_128) + if err != nil { + t.Fatalf("GenerateKeyPair 2 failed: %v", err) + } + + // Create hybrid public key with mismatched components + hybridPK := kp1.PublicKey + hybridPK.SLSS = kp2.PublicKey.SLSS // Substitute SLSS component + + // Serialize and attempt to deserialize + pkBytes := kem.SerializePublicKey(&hybridPK) + _, err = kem.DeserializePublicKey(pkBytes) + + // Should fail binding validation + if err == nil { + t.Error("Hybrid public key with substituted component should fail validation") + } else if !bytes.Contains([]byte(err.Error()), []byte("binding")) { + t.Errorf("Expected binding error, got: %v", err) + } +} + +// BenchmarkKEM benchmarks KEM operations. +func BenchmarkKEM(b *testing.B) { + kp, err := kem.GenerateKeyPair(kmosaic.MOS_128) + if err != nil { + b.Fatalf("GenerateKeyPair failed: %v", err) + } + + b.Run("Encapsulate", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := kem.Encapsulate(&kp.PublicKey) + if err != nil { + b.Fatal(err) + } + } + }) + + result, _ := kem.Encapsulate(&kp.PublicKey) + + b.Run("Decapsulate", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := kem.Decapsulate(&kp.SecretKey, &kp.PublicKey, &result.Ciphertext) + if err != nil { + b.Fatal(err) + } + } + }) +} + +// BenchmarkSign benchmarks signature operations. +func BenchmarkSign(b *testing.B) { + kp, err := sign.GenerateKeyPair(kmosaic.MOS_128) + if err != nil { + b.Fatalf("GenerateKeyPair failed: %v", err) + } + + message := []byte("Benchmark message for signing performance test") + + b.Run("Sign", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := sign.Sign(&kp.SecretKey, &kp.PublicKey, message) + if err != nil { + b.Fatal(err) + } + } + }) + + sig, _ := sign.Sign(&kp.SecretKey, &kp.PublicKey, message) + + b.Run("Verify", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + valid := sign.Verify(&kp.PublicKey, message, sig) + if !valid { + b.Fatal("signature verification failed") + } + } + }) +} + +// TestDeterministicNIZK tests NIZK proof generation and validation with fixed seed +// This allows reproducible debugging of NIZK proofs for cross-implementation comparison +func TestDeterministicNIZK(t *testing.T) { + // Fixed seed for deterministic NIZK proof generation + fixedSeed := []byte{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + } + + t.Run("MOS-128", func(t *testing.T) { + testDeterministicNIZKLevel(t, kmosaic.MOS_128, fixedSeed) + }) + + t.Run("MOS-256", func(t *testing.T) { + testDeterministicNIZKLevel(t, kmosaic.MOS_256, fixedSeed) + }) +} + +func testDeterministicNIZKLevel(t *testing.T, level kmosaic.SecurityLevel, fixedSeed []byte) { + // Generate key pair + kp, err := kem.GenerateKeyPair(level) + if err != nil { + t.Fatalf("GenerateKeyPair failed: %v", err) + } + + // Use fixed ephemeral secret for deterministic output + // This ensures the NIZK proof is reproducible for debugging + result, err := kem.EncapsulateDeterministic(&kp.PublicKey, fixedSeed) + if err != nil { + t.Fatalf("EncapsulateDeterministic failed: %v", err) + } + + // Verify decapsulation works + recoveredSecret, err := kem.Decapsulate(&kp.SecretKey, &kp.PublicKey, &result.Ciphertext) + if err != nil { + t.Fatalf("Decapsulate failed: %v", err) + } + + if !bytes.Equal(result.SharedSecret, recoveredSecret) { + t.Error("Shared secrets do not match after roundtrip") + } + + // Write proof data for debugging + if os.Getenv("DEBUG_NIZK") != "" { + t.Logf("Level: %s", level) + t.Logf("Ciphertext: %x", result.Ciphertext) + t.Logf("SharedSecret: %x", result.SharedSecret) + } +} diff --git a/types.go b/types.go index d746d53..ab2dd77 100644 --- a/types.go +++ b/types.go @@ -28,33 +28,33 @@ const ( // SLSSParams contains parameters for the Sparse Lattice Subset Sum problem. type SLSSParams struct { - N int // Lattice dimension - M int // Number of equations - Q int // Prime modulus - W int // Sparsity weight - Sigma float64 // Error standard deviation + N int `json:"n"` // Lattice dimension + M int `json:"m"` // Number of equations + Q int `json:"q"` // Prime modulus + W int `json:"w"` // Sparsity weight + Sigma float64 `json:"sigma"` // Error standard deviation } // TDDParams contains parameters for the Tensor Decomposition Distinguishing problem. type TDDParams struct { - N int // Tensor dimension - R int // Tensor rank - Q int // Modulus - Sigma float64 // Noise standard deviation + N int `json:"n"` // Tensor dimension + R int `json:"r"` // Tensor rank + Q int `json:"q"` // Modulus + Sigma float64 `json:"sigma"` // Noise standard deviation } // EGRWParams contains parameters for the Expander Graph Random Walk problem. type EGRWParams struct { - P int // Prime for SL(2, Z_p) - K int // Walk length + P int `json:"p"` // Prime for SL(2, Z_p) + K int `json:"k"` // Walk length } // MOSAICParams contains the complete parameter set for a security level. type MOSAICParams struct { - Level SecurityLevel - SLSS SLSSParams - TDD TDDParams - EGRW EGRWParams + Level SecurityLevel `json:"level"` + SLSS SLSSParams `json:"slss"` + TDD TDDParams `json:"tdd"` + EGRW EGRWParams `json:"egrw"` } // =============================================================================