Skip to content

Commit

Permalink
Add support for embedded (non-detached) signatures
Browse files Browse the repository at this point in the history
This adds API functions sign.PrivateKey.signEmbed() and
sign.PublicKey.open() that behave like NIST's crypto_sign() and
crypto_sign_open() functions.
  • Loading branch information
tniessen committed Dec 2, 2024
1 parent b7cbfb4 commit 0a0c6ba
Show file tree
Hide file tree
Showing 7 changed files with 348 additions and 8 deletions.
20 changes: 18 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ above).
Returns an `ArrayBuffer` containing the key material. The key can later be
imported using `new sign.PublicKey(name, bytes)`.

#### `publicKey.open(signedMessage)`

Verifies that the signature embedded in the `signedMessage` is correct using
this public key. The `signedMessage` must be a `BufferSource`. Returns a
`Promise` that resolves to the message if the signature is valid and that is
rejected otherwise.

#### `publicKey.verify(message, signature)`

Verifies that the given `signature` is correct for the given `message` using
Expand All @@ -181,12 +188,21 @@ imported using `new sign.PrivateKey(name, bytes)`.

#### `privateKey.sign(message)`

Computes a signature for the given `message` using this private key. The
`message` must be a `BufferSource`. Returns a `Promise` that resolves to an
Computes a detached signature for the given `message` using this private key.
The `message` must be a `BufferSource`. Returns a `Promise` that resolves to an
`ArrayBuffer`, which is the signature.

The size of the signature is at most `privateKey.algorithm.signatureSize`.

#### `privateKey.signEmbed(message)`

Signs the given `message` using this private key and embeds both into a single
signed message. The `message` must be a `BufferSource`. Returns a `Promise` that
resolves to an `ArrayBuffer`, which is the signed message.

The size of the signed message is at most the byte length of the `message` plus
`privateKey.algorithm.signatureSize`.

## Classic API

The classic API is compatible with [node-mceliece-nist][]. It uses Node.js
Expand Down
10 changes: 10 additions & 0 deletions native/algorithm.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,18 @@ typedef int (*signature_fn)(uint8_t* sig, size_t* siglen,
const uint8_t* m, size_t mlen,
const uint8_t* sk);

typedef int (*sign_fn)(uint8_t* sm, size_t* smlen,
const uint8_t* m, size_t mlen,
const uint8_t* sk);

typedef int (*verify_fn)(const uint8_t* sig, size_t siglen,
const uint8_t* m, size_t mlen,
const uint8_t* pk);

typedef int (*open_fn)(uint8_t* m, size_t* mlen,
const uint8_t* sm, size_t smlen,
const uint8_t* pk);

struct Algorithm {
std::string id;
std::string description;
Expand All @@ -49,7 +57,9 @@ struct Algorithm {
size_t seedSize;
keypair_fn keypair;
signature_fn signature;
sign_fn sign;
verify_fn verify;
open_fn open;
};

} // namespace sign
Expand Down
172 changes: 172 additions & 0 deletions native/node_pqclean.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1358,6 +1358,68 @@ class SignatureWorker : public Napi::AsyncWorker {
size_t signatureSize;
};

class EmbeddedSignatureWorker : public Napi::AsyncWorker {
public:
static Napi::Value Q(Napi::Env env, const AsymmetricKey<pqclean::sign::Algorithm>::Ptr& privateKey,
std::unique_ptr<unsigned char[]>&& message, size_t messageSize) {
EmbeddedSignatureWorker* worker = new EmbeddedSignatureWorker(env, privateKey, std::move(message), messageSize);
worker->Queue();
return worker->deferred.Promise();
}

protected:
void Execute() override {
auto impl = privateKey->algorithm();

const size_t maxSignedMessageSize = messageSize + impl->signatureSize;
signedMessageSize = maxSignedMessageSize;
int r = impl->sign(&signedMessage[0], &signedMessageSize, &message[0], messageSize, privateKey->material().data());
if (r != 0) {
return SetError("signEmbed operation failed");
}

NAPI_CHECK(signedMessageSize <= maxSignedMessageSize, "PQClean:EmbeddedSignatureWorker",
"Actual signature size must not exceed maximum signature size.");
}

virtual void OnOK() override {
Napi::Env env = Env();

// TODO: avoid new allocation / copying
auto result = Napi::ArrayBuffer::New(env, signedMessageSize);
std::copy(&this->signedMessage[0],
&this->signedMessage[0] + signedMessageSize,
reinterpret_cast<unsigned char*>(result.Data()));

deferred.Resolve(result);
}

virtual void OnError(const Napi::Error& e) override {
deferred.Reject(e.Value());
}

private:
EmbeddedSignatureWorker(Napi::Env env, const AsymmetricKey<pqclean::sign::Algorithm>::Ptr& privateKey,
std::unique_ptr<unsigned char[]>&& message, size_t messageSize)
: Napi::AsyncWorker(env),
deferred(Napi::Promise::Deferred::New(env)),
privateKey(privateKey),
message(std::move(message)),
messageSize(messageSize),
signedMessage(new unsigned char[messageSize + privateKey->algorithm()->signatureSize]) {}

Napi::Promise::Deferred deferred;

// Inputs:
AsymmetricKey<pqclean::sign::Algorithm>::Ptr privateKey;
std::unique_ptr<unsigned char[]> message;
size_t messageSize;

// Output:
std::unique_ptr<unsigned char[]> signedMessage;
size_t signedMessageSize;
};

class VerificationWorker : public Napi::AsyncWorker {
public:
static Napi::Value Q(Napi::Env env, const AsymmetricKey<pqclean::sign::Algorithm>::Ptr& publicKey,
Expand Down Expand Up @@ -1407,6 +1469,70 @@ class VerificationWorker : public Napi::AsyncWorker {
bool ok;
};

class OpenWorker : public Napi::AsyncWorker {
public:
static Napi::Value Q(Napi::Env env, const AsymmetricKey<pqclean::sign::Algorithm>::Ptr& publicKey,
std::unique_ptr<unsigned char[]>&& signedMessage, size_t signedMessageSize) {
OpenWorker* worker = new OpenWorker(env, publicKey, std::move(signedMessage), signedMessageSize);
worker->Queue();
return worker->deferred.Promise();
}

protected:
void Execute() override {
auto impl = publicKey->algorithm();

messageSize = signedMessageSize;

// TODO: can we distinguish verification errors from other internal errors?
bool ok = 0 == impl->open(&message[0], &messageSize,
&signedMessage[0], signedMessageSize,
publicKey->material().data());
if (!ok) {
return SetError("signature verification failed");
}

NAPI_CHECK(messageSize < signedMessageSize, "PQClean:OpenWorker",
"Embedded message size must be less than signed message size.");
}

virtual void OnOK() override {
Napi::Env env = Env();

// TODO: avoid new allocation / copying
auto result = Napi::ArrayBuffer::New(env, messageSize);
std::copy(&this->message[0], &this->message[0] + messageSize,
reinterpret_cast<unsigned char*>(result.Data()));

deferred.Resolve(result);
}

virtual void OnError(const Napi::Error& e) override {
deferred.Reject(e.Value());
}

private:
OpenWorker(Napi::Env env, const AsymmetricKey<pqclean::sign::Algorithm>::Ptr& publicKey,
std::unique_ptr<unsigned char[]>&& signedMessage, size_t signedMessageSize)
: Napi::AsyncWorker(env),
deferred(Napi::Promise::Deferred::New(env)),
publicKey(publicKey),
signedMessage(std::move(signedMessage)),
signedMessageSize(signedMessageSize),
message(new unsigned char[signedMessageSize]) {}

Napi::Promise::Deferred deferred;

// Inputs:
AsymmetricKey<pqclean::sign::Algorithm>::Ptr publicKey;
std::unique_ptr<unsigned char[]> signedMessage;
size_t signedMessageSize;

// Outputs:
std::unique_ptr<unsigned char[]> message;
size_t messageSize;
};

class SignPublicKey : public Napi::ObjectWrap<SignPublicKey> {
public:
SignPublicKey(const Napi::CallbackInfo& info)
Expand Down Expand Up @@ -1500,6 +1626,28 @@ class SignPublicKey : public Napi::ObjectWrap<SignPublicKey> {
return VerificationWorker::Q(env, key, std::move(messageCopy), message.byteLength, std::move(signatureCopy), signature.byteLength);
}

Napi::Value Open(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();

if (info.Length() != 1) {
Napi::TypeError::New(env, "Wrong number of arguments")
.ThrowAsJavaScriptException();
return env.Undefined();
}

ArrayBufferSlice signedMessage;
if (!GetBufferSourceAsSlice(info[0], &signedMessage)) {
Napi::TypeError::New(env, "First argument must be a BufferSource")
.ThrowAsJavaScriptException();
return env.Undefined();
}

std::unique_ptr<unsigned char[]> signedMessageCopy = std::make_unique<unsigned char[]>(signedMessage.byteLength);
std::copy(signedMessage.data, signedMessage.data + signedMessage.byteLength, &signedMessageCopy[0]);

return OpenWorker::Q(env, key, std::move(signedMessageCopy), signedMessage.byteLength);
}

Napi::Value Export(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();

Expand Down Expand Up @@ -1600,6 +1748,28 @@ class SignPrivateKey : public Napi::ObjectWrap<SignPrivateKey> {
return SignatureWorker::Q(env, key, std::move(messageCopy), message.byteLength);
}

Napi::Value SignEmbed(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();

if (info.Length() != 1) {
Napi::TypeError::New(env, "Wrong number of arguments")
.ThrowAsJavaScriptException();
return env.Undefined();
}

ArrayBufferSlice message;
if (!GetBufferSourceAsSlice(info[0], &message)) {
Napi::TypeError::New(env, "First argument must be a BufferSource")
.ThrowAsJavaScriptException();
return env.Undefined();
}

std::unique_ptr<unsigned char[]> messageCopy = std::make_unique<unsigned char[]>(message.byteLength);
std::copy(message.data, message.data + message.byteLength, &messageCopy[0]);

return EmbeddedSignatureWorker::Q(env, key, std::move(messageCopy), message.byteLength);
}

Napi::Value Export(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();

Expand Down Expand Up @@ -1656,6 +1826,7 @@ Napi::Object InitKeyCentricSign(Napi::Env env, AddonData* addonData) {
auto publicKeyClass = SignPublicKey::DefineClass(env, "PQCleanSignPublicKey", {
Napi::ObjectWrap<SignPublicKey>::InstanceAccessor("algorithm", &SignPublicKey::GetAlgorithm, nullptr, napi_enumerable),
Napi::ObjectWrap<SignPublicKey>::InstanceMethod("verify", &SignPublicKey::Verify),
Napi::ObjectWrap<SignPublicKey>::InstanceMethod("open", &SignPublicKey::Open),
Napi::ObjectWrap<SignPublicKey>::InstanceMethod("export", &SignPublicKey::Export)
});
obj.DefineProperty(Napi::PropertyDescriptor::Value("PublicKey", publicKeyClass, napi_enumerable));
Expand All @@ -1666,6 +1837,7 @@ Napi::Object InitKeyCentricSign(Napi::Env env, AddonData* addonData) {
auto privateKeyClass = SignPrivateKey::DefineClass(env, "PQCleanSignPrivateKey", {
Napi::ObjectWrap<SignPrivateKey>::InstanceAccessor("algorithm", &SignPrivateKey::GetAlgorithm, nullptr, napi_enumerable),
Napi::ObjectWrap<SignPrivateKey>::InstanceMethod("sign", &SignPrivateKey::Sign),
Napi::ObjectWrap<SignPrivateKey>::InstanceMethod("signEmbed", &SignPrivateKey::SignEmbed),
Napi::ObjectWrap<SignPrivateKey>::InstanceMethod("export", &SignPrivateKey::Export)
});
obj.DefineProperty(Napi::PropertyDescriptor::Value("PrivateKey", privateKeyClass, napi_enumerable));
Expand Down
32 changes: 28 additions & 4 deletions scripts/build-wasm.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ async function readApi(name, type) {
.map(([key, value]) => [key.substring(prefix.length), value]));
}

function functionName(name, type, fn) {
if (type === 'sign' && fn === 'sign') return `${ns(name)}crypto_${type}`;
return `${ns(name)}crypto_${type}_${fn}`;
}

async function readAlgorithms(names, type, props, functions) {
return Promise.all(names.map(async (name) => {
const api = await readApi(name, type);
Expand All @@ -35,7 +40,7 @@ async function readAlgorithms(names, type, props, functions) {
privateKeySize: api.SECRETKEYBYTES,
...props(api)
},
functions: Object.fromEntries(functions.map((fn) => [fn, `${ns(name)}crypto_${type}_${fn}`]))
functions: Object.fromEntries(functions.map((fn) => [fn, functionName(name, type, fn)]))
};
}));
}
Expand All @@ -47,20 +52,20 @@ const kemAlgorithms = await readAlgorithms(kemNames, 'kem', (api) => ({

const signAlgorithms = await readAlgorithms(signNames, 'sign', (api) => ({
signatureSize: api.BYTES
}), ['keypair', 'signature', 'verify']);
}), ['keypair', 'signature', 'sign', 'verify', 'open']);

await writeFile(`${buildDir}/algorithms.json`, JSON.stringify({
kem: kemAlgorithms,
sign: signAlgorithms
}, null, 2));

const functions = (names, type, ...fns) => names.map((name) => fns.map((fn) => `${ns(name)}crypto_${type}_${fn}`))
const functions = (names, type, ...fns) => names.map((name) => fns.map((fn) => functionName(name, type, fn)))
.flat();

const wantedExports = JSON.stringify([
'malloc', 'free',
...functions(kemNames, 'kem', 'keypair', 'enc', 'dec'),
...functions(signNames, 'sign', 'keypair', 'signature', 'verify')
...functions(signNames, 'sign', 'keypair', 'signature', 'sign', 'verify', 'open')
].map((e) => `_${e}`));

const sources = async (dir, filter) => (await readdir(`${depDir}/${dir}`))
Expand All @@ -85,6 +90,15 @@ int ${fn}(uint8_t *sig, size_t *siglen, const uint8_t *m, size_t mlen, const uin
return _fn_symbol_${fn}(sig, siglen, m, mlen, sk);
}
#endif`).join('\n')}
${functions(signNames, 'sign', 'sign').map((fn) => `#ifdef ${fn}
static inline int _fn_symbol_${fn}(uint8_t *sig, size_t *siglen, const uint8_t *m, size_t mlen, const uint8_t *sk) {
return ${fn}(sig, siglen, m, mlen, sk);
}
#undef ${fn}
int ${fn}(uint8_t *sig, size_t *siglen, const uint8_t *m, size_t mlen, const uint8_t *sk) {
return _fn_symbol_${fn}(sig, siglen, m, mlen, sk);
}
#endif`).join('\n')}
${functions(signNames, 'sign', 'verify').map((fn) => `#ifdef ${fn}
static inline int _fn_symbol_${fn}(const uint8_t *sig, size_t siglen, const uint8_t *m, size_t mlen, const uint8_t *pk) {
return ${fn}(sig, siglen, m, mlen, pk);
Expand All @@ -93,6 +107,16 @@ static inline int _fn_symbol_${fn}(const uint8_t *sig, size_t siglen, const uint
int ${fn}(const uint8_t *sig, size_t siglen, const uint8_t *m, size_t mlen, const uint8_t *pk) {
return _fn_symbol_${fn}(sig, siglen, m, mlen, pk);
}
#endif
`).join('\n')}
${functions(signNames, 'sign', 'open').map((fn) => `#ifdef ${fn}
static inline int _fn_symbol_${fn}(uint8_t *sig, size_t *siglen, const uint8_t *m, size_t mlen, const uint8_t *sk) {
return ${fn}(sig, siglen, m, mlen, sk);
}
#undef ${fn}
int ${fn}(uint8_t *sig, size_t *siglen, const uint8_t *m, size_t mlen, const uint8_t *sk) {
return _fn_symbol_${fn}(sig, siglen, m, mlen, sk);
}
#endif`).join('\n')}
`;

Expand Down
Loading

0 comments on commit 0a0c6ba

Please sign in to comment.