feat(crypto): key derivation, one-time addresses, and key images
Add ECDH shared secret, ephemeral key derivation (DerivePublicKey, DeriveSecretKey), and key image generation/validation to the CGo bridge. Tests verify ECDH commutativity, output scanning round-trip, and key image determinism. Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
a68926c45c
commit
9821ea4d21
5 changed files with 283 additions and 0 deletions
|
|
@ -39,4 +39,62 @@ int cn_check_key(const uint8_t pub[32]) {
|
|||
return crypto::check_key(pk) ? 0 : 1;
|
||||
}
|
||||
|
||||
// ── Key Derivation ────────────────────────────────────────
|
||||
|
||||
int cn_generate_key_derivation(const uint8_t pub[32], const uint8_t sec[32],
|
||||
uint8_t derivation[32]) {
|
||||
crypto::public_key pk;
|
||||
crypto::secret_key sk;
|
||||
crypto::key_derivation kd;
|
||||
memcpy(&pk, pub, 32);
|
||||
memcpy(&sk, sec, 32);
|
||||
bool ok = crypto::generate_key_derivation(pk, sk, kd);
|
||||
if (!ok) return 1;
|
||||
memcpy(derivation, &kd, 32);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cn_derive_public_key(const uint8_t derivation[32], uint64_t index,
|
||||
const uint8_t base[32], uint8_t derived[32]) {
|
||||
crypto::key_derivation kd;
|
||||
crypto::public_key base_pk, derived_pk;
|
||||
memcpy(&kd, derivation, 32);
|
||||
memcpy(&base_pk, base, 32);
|
||||
bool ok = crypto::derive_public_key(kd, index, base_pk, derived_pk);
|
||||
if (!ok) return 1;
|
||||
memcpy(derived, &derived_pk, 32);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cn_derive_secret_key(const uint8_t derivation[32], uint64_t index,
|
||||
const uint8_t base[32], uint8_t derived[32]) {
|
||||
crypto::key_derivation kd;
|
||||
crypto::secret_key base_sk, derived_sk;
|
||||
memcpy(&kd, derivation, 32);
|
||||
memcpy(&base_sk, base, 32);
|
||||
crypto::derive_secret_key(kd, index, base_sk, derived_sk);
|
||||
memcpy(derived, &derived_sk, 32);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── Key Images ────────────────────────────────────────────
|
||||
|
||||
int cn_generate_key_image(const uint8_t pub[32], const uint8_t sec[32],
|
||||
uint8_t image[32]) {
|
||||
crypto::public_key pk;
|
||||
crypto::secret_key sk;
|
||||
crypto::key_image ki;
|
||||
memcpy(&pk, pub, 32);
|
||||
memcpy(&sk, sec, 32);
|
||||
crypto::generate_key_image(pk, sk, ki);
|
||||
memcpy(image, &ki, 32);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cn_validate_key_image(const uint8_t image[32]) {
|
||||
crypto::key_image ki;
|
||||
memcpy(&ki, image, 32);
|
||||
return crypto::validate_key_image(ki) ? 0 : 1;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,19 @@ int cn_generate_keys(uint8_t pub[32], uint8_t sec[32]);
|
|||
int cn_secret_to_public(const uint8_t sec[32], uint8_t pub[32]);
|
||||
int cn_check_key(const uint8_t pub[32]);
|
||||
|
||||
// ── Key Derivation ────────────────────────────────────────
|
||||
int cn_generate_key_derivation(const uint8_t pub[32], const uint8_t sec[32],
|
||||
uint8_t derivation[32]);
|
||||
int cn_derive_public_key(const uint8_t derivation[32], uint64_t index,
|
||||
const uint8_t base[32], uint8_t derived[32]);
|
||||
int cn_derive_secret_key(const uint8_t derivation[32], uint64_t index,
|
||||
const uint8_t base[32], uint8_t derived[32]);
|
||||
|
||||
// ── Key Images ────────────────────────────────────────────
|
||||
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]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -66,3 +66,138 @@ func TestGenerateKeys_Good_Unique(t *testing.T) {
|
|||
t.Fatal("two GenerateKeys calls returned identical public keys")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Key Derivation ────────────────────────────────────────
|
||||
|
||||
func TestKeyDerivation_Good_Roundtrip(t *testing.T) {
|
||||
// Alice and Bob generate key pairs; shared derivation must match.
|
||||
pubA, secA, err := crypto.GenerateKeys()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeys (Alice): %v", err)
|
||||
}
|
||||
pubB, secB, err := crypto.GenerateKeys()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeys (Bob): %v", err)
|
||||
}
|
||||
|
||||
// D = secA * pubB must equal secB * pubA (ECDH commutativity).
|
||||
dAB, err := crypto.GenerateKeyDerivation(pubB, secA)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyDerivation(pubB, secA): %v", err)
|
||||
}
|
||||
dBA, err := crypto.GenerateKeyDerivation(pubA, secB)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyDerivation(pubA, secB): %v", err)
|
||||
}
|
||||
if dAB != dBA {
|
||||
t.Fatalf("ECDH mismatch:\n dAB: %x\n dBA: %x", dAB, dBA)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDerivePublicKey_Good_OutputScanning(t *testing.T) {
|
||||
// Simulate one-time address generation and scanning.
|
||||
// Sender knows (txPub, recipientPub). Recipient knows (txPub, recipientSec).
|
||||
recipientPub, recipientSec, err := crypto.GenerateKeys()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeys (recipient): %v", err)
|
||||
}
|
||||
txPub, txSec, err := crypto.GenerateKeys()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeys (tx): %v", err)
|
||||
}
|
||||
|
||||
// Sender derives ephemeral output key.
|
||||
derivSender, err := crypto.GenerateKeyDerivation(recipientPub, txSec)
|
||||
if err != nil {
|
||||
t.Fatalf("sender derivation: %v", err)
|
||||
}
|
||||
ephPub, err := crypto.DerivePublicKey(derivSender, 0, recipientPub)
|
||||
if err != nil {
|
||||
t.Fatalf("DerivePublicKey: %v", err)
|
||||
}
|
||||
if !crypto.CheckKey(ephPub) {
|
||||
t.Fatal("ephemeral public key failed CheckKey")
|
||||
}
|
||||
|
||||
// Recipient re-derives and must get the same key.
|
||||
derivRecipient, err := crypto.GenerateKeyDerivation(txPub, recipientSec)
|
||||
if err != nil {
|
||||
t.Fatalf("recipient derivation: %v", err)
|
||||
}
|
||||
ephPub2, err := crypto.DerivePublicKey(derivRecipient, 0, recipientPub)
|
||||
if err != nil {
|
||||
t.Fatalf("DerivePublicKey (recipient): %v", err)
|
||||
}
|
||||
if ephPub != ephPub2 {
|
||||
t.Fatalf("output scanning mismatch:\n sender: %x\n recipient: %x", ephPub, ephPub2)
|
||||
}
|
||||
|
||||
// Recipient derives the secret key for spending.
|
||||
ephSec, err := crypto.DeriveSecretKey(derivRecipient, 0, recipientSec)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveSecretKey: %v", err)
|
||||
}
|
||||
|
||||
// Verify: ephSec → ephPub must match.
|
||||
ephPub3, err := crypto.SecretToPublic(ephSec)
|
||||
if err != nil {
|
||||
t.Fatalf("SecretToPublic(ephSec): %v", err)
|
||||
}
|
||||
if ephPub != ephPub3 {
|
||||
t.Fatalf("ephemeral key pair mismatch:\n derived pub: %x\n sec→pub: %x", ephPub, ephPub3)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Key Images ────────────────────────────────────────────
|
||||
|
||||
func TestKeyImage_Good_Roundtrip(t *testing.T) {
|
||||
pub, sec, err := crypto.GenerateKeys()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeys: %v", err)
|
||||
}
|
||||
|
||||
ki, err := crypto.GenerateKeyImage(pub, sec)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyImage: %v", err)
|
||||
}
|
||||
|
||||
var zero [32]byte
|
||||
if ki == zero {
|
||||
t.Fatal("key image is zero")
|
||||
}
|
||||
|
||||
if !crypto.ValidateKeyImage(ki) {
|
||||
t.Fatal("generated key image failed validation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyImage_Good_Deterministic(t *testing.T) {
|
||||
pub, sec, err := crypto.GenerateKeys()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeys: %v", err)
|
||||
}
|
||||
|
||||
ki1, err := crypto.GenerateKeyImage(pub, sec)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyImage (1): %v", err)
|
||||
}
|
||||
ki2, err := crypto.GenerateKeyImage(pub, sec)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyImage (2): %v", err)
|
||||
}
|
||||
|
||||
if ki1 != ki2 {
|
||||
t.Fatalf("key image not deterministic:\n ki1: %x\n ki2: %x", ki1, ki2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyImage_Bad_Invalid(t *testing.T) {
|
||||
// 0xFF...FF is not a valid curve point, so not a valid key image.
|
||||
bad := [32]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}
|
||||
if crypto.ValidateKeyImage(bad) {
|
||||
t.Fatal("0xFF...FF should fail ValidateKeyImage")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,3 +41,47 @@ func SecretToPublic(sec [32]byte) ([32]byte, error) {
|
|||
func CheckKey(pub [32]byte) bool {
|
||||
return C.cn_check_key((*C.uint8_t)(unsafe.Pointer(&pub[0]))) == 0
|
||||
}
|
||||
|
||||
// GenerateKeyDerivation computes the ECDH shared secret (key derivation).
|
||||
func GenerateKeyDerivation(pub [32]byte, sec [32]byte) ([32]byte, error) {
|
||||
var d [32]byte
|
||||
rc := C.cn_generate_key_derivation(
|
||||
(*C.uint8_t)(unsafe.Pointer(&pub[0])),
|
||||
(*C.uint8_t)(unsafe.Pointer(&sec[0])),
|
||||
(*C.uint8_t)(unsafe.Pointer(&d[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
return d, fmt.Errorf("crypto: generate_key_derivation failed")
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// DerivePublicKey derives an ephemeral public key for a transaction output.
|
||||
func DerivePublicKey(derivation [32]byte, index uint64, base [32]byte) ([32]byte, error) {
|
||||
var derived [32]byte
|
||||
rc := C.cn_derive_public_key(
|
||||
(*C.uint8_t)(unsafe.Pointer(&derivation[0])),
|
||||
C.uint64_t(index),
|
||||
(*C.uint8_t)(unsafe.Pointer(&base[0])),
|
||||
(*C.uint8_t)(unsafe.Pointer(&derived[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
return derived, fmt.Errorf("crypto: derive_public_key failed")
|
||||
}
|
||||
return derived, nil
|
||||
}
|
||||
|
||||
// DeriveSecretKey derives the ephemeral secret key for a received output.
|
||||
func DeriveSecretKey(derivation [32]byte, index uint64, base [32]byte) ([32]byte, error) {
|
||||
var derived [32]byte
|
||||
rc := C.cn_derive_secret_key(
|
||||
(*C.uint8_t)(unsafe.Pointer(&derivation[0])),
|
||||
C.uint64_t(index),
|
||||
(*C.uint8_t)(unsafe.Pointer(&base[0])),
|
||||
(*C.uint8_t)(unsafe.Pointer(&derived[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
return derived, fmt.Errorf("crypto: derive_secret_key failed")
|
||||
}
|
||||
return derived, nil
|
||||
}
|
||||
|
|
|
|||
33
crypto/keyimage.go
Normal file
33
crypto/keyimage.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// SPDX-Licence-Identifier: EUPL-1.2
|
||||
|
||||
package crypto
|
||||
|
||||
/*
|
||||
#include "bridge.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// GenerateKeyImage computes the key image for a public/secret key pair.
|
||||
// The key image is used for double-spend detection in ring signatures.
|
||||
func GenerateKeyImage(pub [32]byte, sec [32]byte) ([32]byte, error) {
|
||||
var ki [32]byte
|
||||
rc := C.cn_generate_key_image(
|
||||
(*C.uint8_t)(unsafe.Pointer(&pub[0])),
|
||||
(*C.uint8_t)(unsafe.Pointer(&sec[0])),
|
||||
(*C.uint8_t)(unsafe.Pointer(&ki[0])),
|
||||
)
|
||||
if rc != 0 {
|
||||
return ki, fmt.Errorf("crypto: generate_key_image failed")
|
||||
}
|
||||
return ki, nil
|
||||
}
|
||||
|
||||
// ValidateKeyImage checks that a key image is a valid curve point of the correct order.
|
||||
func ValidateKeyImage(ki [32]byte) bool {
|
||||
return C.cn_validate_key_image((*C.uint8_t)(unsafe.Pointer(&ki[0]))) == 0
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue