feat(crypto): standard and ring signature (NLSAG) generation/verification

Standard signature sign/verify round-trip with negative tests (wrong key,
wrong message). Ring signature (NLSAG) uses flat buffer C API to avoid
double-pointer indirection across CGo boundary. Round-trip with 4-member
ring and wrong-message negative test. All 15 tests pass with race detector.

Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
Claude 2026-02-20 18:30:53 +00:00
parent 9821ea4d21
commit f022e61da9
No known key found for this signature in database
GPG key ID: AF404715446AEB41
4 changed files with 286 additions and 0 deletions

View file

@ -5,6 +5,7 @@
#include "bridge.h"
#include <cstring>
#include <vector>
#include "crypto.h"
#include "hash-ops.h"
@ -97,4 +98,78 @@ int cn_validate_key_image(const uint8_t image[32]) {
return crypto::validate_key_image(ki) ? 0 : 1;
}
// ── Standard Signatures ──────────────────────────────────
int cn_generate_signature(const uint8_t hash[32], const uint8_t pub[32],
const uint8_t sec[32], uint8_t sig[64]) {
crypto::hash h;
crypto::public_key pk;
crypto::secret_key sk;
crypto::signature s;
memcpy(&h, hash, 32);
memcpy(&pk, pub, 32);
memcpy(&sk, sec, 32);
crypto::generate_signature(h, pk, sk, s);
memcpy(sig, &s, 64);
return 0;
}
int cn_check_signature(const uint8_t hash[32], const uint8_t pub[32],
const uint8_t sig[64]) {
crypto::hash h;
crypto::public_key pk;
crypto::signature s;
memcpy(&h, hash, 32);
memcpy(&pk, pub, 32);
memcpy(&s, sig, 64);
return crypto::check_signature(h, pk, s) ? 0 : 1;
}
// ── Ring Signatures (NLSAG) ─────────────────────────────
int cn_generate_ring_signature(const uint8_t hash[32], const uint8_t image[32],
const uint8_t *pubs, size_t pubs_count,
const uint8_t sec[32], size_t sec_index,
uint8_t *sigs) {
crypto::hash h;
crypto::key_image ki;
crypto::secret_key sk;
memcpy(&h, hash, 32);
memcpy(&ki, image, 32);
memcpy(&sk, sec, 32);
// Reconstruct pointer array from flat buffer.
std::vector<const crypto::public_key*> pk_ptrs(pubs_count);
std::vector<crypto::public_key> pk_storage(pubs_count);
for (size_t i = 0; i < pubs_count; i++) {
memcpy(&pk_storage[i], pubs + i * 32, 32);
pk_ptrs[i] = &pk_storage[i];
}
std::vector<crypto::signature> sig_vec(pubs_count);
crypto::generate_ring_signature(h, ki, pk_ptrs.data(), pubs_count,
sk, sec_index, sig_vec.data());
memcpy(sigs, sig_vec.data(), pubs_count * 64);
return 0;
}
int cn_check_ring_signature(const uint8_t hash[32], const uint8_t image[32],
const uint8_t *pubs, size_t pubs_count,
const uint8_t *sigs) {
crypto::hash h;
crypto::key_image ki;
memcpy(&h, hash, 32);
memcpy(&ki, image, 32);
std::vector<const crypto::public_key*> pk_ptrs(pubs_count);
std::vector<crypto::public_key> pk_storage(pubs_count);
for (size_t i = 0; i < pubs_count; i++) {
memcpy(&pk_storage[i], pubs + i * 32, 32);
pk_ptrs[i] = &pk_storage[i];
}
auto* sig_ptr = reinterpret_cast<const crypto::signature*>(sigs);
return crypto::check_ring_signature(h, ki, pk_ptrs.data(), pubs_count, sig_ptr) ? 0 : 1;
}
} // extern "C"

View file

@ -32,6 +32,21 @@ int cn_generate_key_image(const uint8_t pub[32], const uint8_t sec[32],
uint8_t image[32]);
int cn_validate_key_image(const uint8_t image[32]);
// ── Standard Signatures ───────────────────────────────────
int cn_generate_signature(const uint8_t hash[32], const uint8_t pub[32],
const uint8_t sec[32], uint8_t sig[64]);
int cn_check_signature(const uint8_t hash[32], const uint8_t pub[32],
const uint8_t sig[64]);
// ── Ring Signatures (NLSAG) ──────────────────────────────
int cn_generate_ring_signature(const uint8_t hash[32], const uint8_t image[32],
const uint8_t *pubs, size_t pubs_count,
const uint8_t sec[32], size_t sec_index,
uint8_t *sigs);
int cn_check_ring_signature(const uint8_t hash[32], const uint8_t image[32],
const uint8_t *pubs, size_t pubs_count,
const uint8_t *sigs);
#ifdef __cplusplus
}
#endif

View file

@ -201,3 +201,100 @@ func TestKeyImage_Bad_Invalid(t *testing.T) {
t.Fatal("0xFF...FF should fail ValidateKeyImage")
}
}
// ── Standard Signatures ──────────────────────────────────
func TestSignature_Good_Roundtrip(t *testing.T) {
pub, sec, _ := crypto.GenerateKeys()
// Sign a message hash.
msg := crypto.FastHash([]byte("test message"))
sig, err := crypto.GenerateSignature(msg, pub, sec)
if err != nil {
t.Fatalf("GenerateSignature: %v", err)
}
// Verify with correct key.
if !crypto.CheckSignature(msg, pub, sig) {
t.Fatal("valid signature failed verification")
}
}
func TestSignature_Bad_WrongKey(t *testing.T) {
pub, sec, _ := crypto.GenerateKeys()
pub2, _, _ := crypto.GenerateKeys()
msg := crypto.FastHash([]byte("test"))
sig, _ := crypto.GenerateSignature(msg, pub, sec)
// Verify with wrong public key should fail.
if crypto.CheckSignature(msg, pub2, sig) {
t.Fatal("signature verified with wrong public key")
}
}
func TestSignature_Bad_WrongMessage(t *testing.T) {
pub, sec, _ := crypto.GenerateKeys()
msg1 := crypto.FastHash([]byte("message 1"))
msg2 := crypto.FastHash([]byte("message 2"))
sig, _ := crypto.GenerateSignature(msg1, pub, sec)
if crypto.CheckSignature(msg2, pub, sig) {
t.Fatal("signature verified with wrong message")
}
}
// ── Ring Signatures (NLSAG) ─────────────────────────────
func TestRingSignature_Good_Roundtrip(t *testing.T) {
// Create a ring of 4 public keys. The real signer is at index 1.
ringSize := 4
realIndex := 1
pubs := make([][32]byte, ringSize)
var realSec [32]byte
for i := 0; i < ringSize; i++ {
pub, sec, _ := crypto.GenerateKeys()
pubs[i] = pub
if i == realIndex {
realSec = sec
}
}
// Generate key image for the real key.
ki, _ := crypto.GenerateKeyImage(pubs[realIndex], realSec)
// Sign.
msg := crypto.FastHash([]byte("ring sig test"))
sigs, err := crypto.GenerateRingSignature(msg, ki, pubs, realSec, realIndex)
if err != nil {
t.Fatalf("GenerateRingSignature: %v", err)
}
// Verify.
if !crypto.CheckRingSignature(msg, ki, pubs, sigs) {
t.Fatal("valid ring signature failed verification")
}
}
func TestRingSignature_Bad_WrongMessage(t *testing.T) {
pubs := make([][32]byte, 3)
var sec [32]byte
for i := range pubs {
pub, s, _ := crypto.GenerateKeys()
pubs[i] = pub
if i == 0 {
sec = s
}
}
ki, _ := crypto.GenerateKeyImage(pubs[0], sec)
msg1 := crypto.FastHash([]byte("msg1"))
msg2 := crypto.FastHash([]byte("msg2"))
sigs, _ := crypto.GenerateRingSignature(msg1, ki, pubs, sec, 0)
if crypto.CheckRingSignature(msg2, ki, pubs, sigs) {
t.Fatal("ring signature verified with wrong message")
}
}

99
crypto/signature.go Normal file
View file

@ -0,0 +1,99 @@
// SPDX-Licence-Identifier: EUPL-1.2
package crypto
/*
#include "bridge.h"
*/
import "C"
import (
"fmt"
"unsafe"
)
// GenerateSignature creates a standard (non-ring) signature.
func GenerateSignature(hash [32]byte, pub [32]byte, sec [32]byte) ([64]byte, error) {
var sig [64]byte
rc := C.cn_generate_signature(
(*C.uint8_t)(unsafe.Pointer(&hash[0])),
(*C.uint8_t)(unsafe.Pointer(&pub[0])),
(*C.uint8_t)(unsafe.Pointer(&sec[0])),
(*C.uint8_t)(unsafe.Pointer(&sig[0])),
)
if rc != 0 {
return sig, fmt.Errorf("crypto: generate_signature failed")
}
return sig, nil
}
// CheckSignature verifies a standard signature.
func CheckSignature(hash [32]byte, pub [32]byte, sig [64]byte) bool {
return C.cn_check_signature(
(*C.uint8_t)(unsafe.Pointer(&hash[0])),
(*C.uint8_t)(unsafe.Pointer(&pub[0])),
(*C.uint8_t)(unsafe.Pointer(&sig[0])),
) == 0
}
// GenerateRingSignature creates a ring signature using the given key ring.
// pubs contains the public keys of all ring members.
// sec is the secret key of the actual signer at position secIndex.
// Returns one signature pair per ring member.
func GenerateRingSignature(hash [32]byte, image [32]byte, pubs [][32]byte,
sec [32]byte, secIndex int) ([][64]byte, error) {
n := len(pubs)
flatPubs := make([]byte, n*32)
for i, p := range pubs {
copy(flatPubs[i*32:], p[:])
}
flatSigs := make([]byte, n*64)
rc := C.cn_generate_ring_signature(
(*C.uint8_t)(unsafe.Pointer(&hash[0])),
(*C.uint8_t)(unsafe.Pointer(&image[0])),
(*C.uint8_t)(unsafe.Pointer(&flatPubs[0])),
C.size_t(n),
(*C.uint8_t)(unsafe.Pointer(&sec[0])),
C.size_t(secIndex),
(*C.uint8_t)(unsafe.Pointer(&flatSigs[0])),
)
if rc != 0 {
return nil, fmt.Errorf("crypto: generate_ring_signature failed")
}
sigs := make([][64]byte, n)
for i := range sigs {
copy(sigs[i][:], flatSigs[i*64:])
}
return sigs, nil
}
// CheckRingSignature verifies a ring signature.
func CheckRingSignature(hash [32]byte, image [32]byte, pubs [][32]byte,
sigs [][64]byte) bool {
n := len(pubs)
if len(sigs) != n {
return false
}
flatPubs := make([]byte, n*32)
for i, p := range pubs {
copy(flatPubs[i*32:], p[:])
}
flatSigs := make([]byte, n*64)
for i, s := range sigs {
copy(flatSigs[i*64:], s[:])
}
return C.cn_check_ring_signature(
(*C.uint8_t)(unsafe.Pointer(&hash[0])),
(*C.uint8_t)(unsafe.Pointer(&image[0])),
(*C.uint8_t)(unsafe.Pointer(&flatPubs[0])),
C.size_t(n),
(*C.uint8_t)(unsafe.Pointer(&flatSigs[0])),
) == 0
}