Verified Commit cc53e86e authored by Samuel Elliott's avatar Samuel Elliott
Browse files

Allow verifying the public key after verifying signed messages

parent f7754221
Pipeline #808 failed with stages
in 1 minute and 31 seconds
......@@ -38,6 +38,13 @@ describe('attached signing', () => {
expect(data.toString()).toBe(INPUT_STRING);
});
test('verify doesn\'t require sender public key', async () => {
const data = await verify(SIGNED);
expect(data.toString()).toBe(INPUT_STRING);
expect(data.public_key).toStrictEqual(KEYPAIR.publicKey);
});
test('verify stream', async () => {
const stream = new VerifyStream(KEYPAIR.publicKey);
const result: Buffer[] = [];
......@@ -53,6 +60,22 @@ describe('attached signing', () => {
expect(Buffer.concat(result).toString()).toBe(INPUT_STRING);
});
test('verify stream doesn\'t require sender public key', async () => {
const stream = new VerifyStream();
const result: Buffer[] = [];
await new Promise((rs, rj) => {
stream.on('error', rj);
stream.on('end', rs);
stream.on('data', chunk => result.push(chunk));
stream.end(SIGNED);
});
expect(Buffer.concat(result).toString()).toBe(INPUT_STRING);
expect(stream.public_key).toStrictEqual(KEYPAIR.publicKey);
});
test('verify with wrong public key fails', () => {
const public_key = new Uint8Array(KEYPAIR.publicKey);
public_key[0] = 0;
......@@ -66,6 +89,7 @@ describe('attached signing', () => {
describe('detached signing', () => {
test('sign detached', () => {
const signed = signDetached(INPUT_STRING, KEYPAIR);
expect(signed).toStrictEqual(DETACHED_SIGNATURE);
});
......@@ -73,6 +97,12 @@ describe('detached signing', () => {
await verifyDetached(DETACHED_SIGNATURE, INPUT_STRING, KEYPAIR.publicKey);
});
test('verify detached doesn\'t require sender public key', async () => {
const result = await verifyDetached(DETACHED_SIGNATURE, INPUT_STRING);
expect(result.public_key).toStrictEqual(KEYPAIR.publicKey);
});
test('verify detached with wrong public key fails', () => {
const public_key = KEYPAIR.publicKey;
public_key[0] = 0;
......
......@@ -18,14 +18,14 @@ export default class SignedMessageHeader extends Header {
static debug_fix_nonce: Buffer | null = null;
/** The sender's Ed25519 public key */
readonly public_key: Buffer;
readonly public_key: Uint8Array;
/** Random data for this message */
readonly nonce: Buffer;
readonly nonce: Uint8Array;
/** `true` if this is an attached signature header, `false` if this is a detached signature header */
readonly attached: boolean;
private _encoded_data: [Buffer, Buffer] | null = null;
constructor(public_key: Buffer, nonce: Buffer, attached = true) {
constructor(public_key: Uint8Array, nonce: Uint8Array, attached = true) {
super();
this.public_key = public_key;
this.nonce = nonce;
......@@ -50,14 +50,14 @@ export default class SignedMessageHeader extends Header {
static create(public_key: Uint8Array, attached = true): SignedMessageHeader {
const nonce = this.debug_fix_nonce ?? crypto.randomBytes(32);
return new this(Buffer.from(public_key), nonce, attached);
return new this(public_key, nonce, attached);
}
encode() {
return SignedMessageHeader.encodeHeader(this.public_key, this.nonce, this.attached);
}
static encodeHeader(public_key: Buffer, nonce: Buffer, attached: boolean): [Buffer, Buffer] {
static encodeHeader(public_key: Uint8Array, nonce: Uint8Array, attached: boolean): [Buffer, Buffer] {
const data = [
'saltpack',
[2, 0],
......@@ -73,7 +73,7 @@ export default class SignedMessageHeader extends Header {
return [header_hash, Buffer.from(msgpack.encode(encoded))];
}
static decode(encoded: Buffer, unwrapped = false) {
static decode(encoded: Uint8Array, unwrapped = false) {
const [header_hash, data] = super.decode1(encoded, unwrapped);
if (data[2] !== MessageType.ATTACHED_SIGNING &&
......
......@@ -96,7 +96,11 @@ export class SignStream extends Transform {
}
}
export async function verify(signed: Uint8Array, public_key: Uint8Array): Promise<Buffer> {
export interface VerifyResult extends Buffer {
public_key: Uint8Array;
}
export async function verify(signed: Uint8Array, public_key?: Uint8Array | null): Promise<VerifyResult> {
const stream = new Readable();
stream.push(signed);
stream.push(null);
......@@ -110,6 +114,10 @@ export async function verify(signed: Uint8Array, public_key: Uint8Array): Promis
const header_data = items.shift() as any;
const header = SignedMessageHeader.decode(header_data, true);
if (public_key && !Buffer.from(header.public_key).equals(public_key)) {
throw new Error('Sender public key doesn\'t match');
}
let output = Buffer.alloc(0);
for (const i in items) {
......@@ -117,7 +125,7 @@ export async function verify(signed: Uint8Array, public_key: Uint8Array): Promis
const final = items.length === (parseInt(i) + 1);
const payload = SignedMessagePayload.decode(message, true);
payload.verify(header, public_key, BigInt(i));
payload.verify(header, header.public_key, BigInt(i));
if (payload.final && !final) {
throw new Error('Found payload with invalid final flag, message extended?');
......@@ -133,24 +141,32 @@ export async function verify(signed: Uint8Array, public_key: Uint8Array): Promis
throw new Error('No signed payloads, message truncated?');
}
return output;
return Object.assign(output, {
public_key: new Uint8Array(header.public_key),
});
}
export class VerifyStream extends Transform {
private readonly _public_key: Uint8Array | null;
private decoder = new msgpack.Decoder(undefined!, undefined);
private header_data: SignedMessageHeader | null = null;
private last_payload: SignedMessagePayload | null = null;
private payload_index = BigInt(-1);
private i = 0;
constructor(readonly public_key: Uint8Array) {
constructor(public_key?: Uint8Array | null) {
super();
this._public_key = public_key ?? null;
}
get header() {
if (!this.header_data) throw new Error('Header hasn\'t been decoded yet');
return this.header_data;
}
get public_key() {
return this.header.public_key;
}
_transform(data: Buffer, encoding: string, callback: TransformCallback) {
this.decoder.appendBuffer(data);
......@@ -178,22 +194,26 @@ export class VerifyStream extends Transform {
if (!this.header_data) {
const header = SignedMessageHeader.decode(data as any, true);
if (this._public_key && !Buffer.from(header.public_key).equals(this._public_key)) {
throw new Error('Sender public key doesn\'t match');
}
this.header_data = header;
// @ts-expect-error
header.public_key = new Uint8Array(header.public_key);
} else {
this.payload_index++;
if (this.last_payload) {
if (this.last_payload.final) {
const err = new Error('Found payload with invalid final flag, message extended?');
this.emit('error', err);
throw err;
throw new Error('Found payload with invalid final flag, message extended?');
}
this.push(this.last_payload.data);
}
const payload = SignedMessagePayload.decode(data, true);
payload.verify(this.header, this.public_key, this.payload_index);
payload.verify(this.header, this.header.public_key, this.payload_index);
this.last_payload = payload;
}
......@@ -229,7 +249,13 @@ export function signDetached(data: Uint8Array | string, keypair: tweetnacl.SignK
]);
}
export async function verifyDetached(signature: Uint8Array, data: Uint8Array | string, public_key: Uint8Array) {
export interface VerifyDetachedResult {
public_key: Uint8Array;
}
export async function verifyDetached(
signature: Uint8Array, data: Uint8Array | string, public_key?: Uint8Array | null
): Promise<VerifyDetachedResult> {
const stream = new Readable();
stream.push(signature);
stream.push(null);
......@@ -244,5 +270,13 @@ export async function verifyDetached(signature: Uint8Array, data: Uint8Array | s
const header = SignedMessageHeader.decode(header_data, true);
header.verifyDetached(signature_data, Buffer.from(data), public_key);
if (public_key && !Buffer.from(header.public_key).equals(public_key)) {
throw new Error('Sender public key doesn\'t match');
}
header.verifyDetached(signature_data, Buffer.from(data), header.public_key);
return {
public_key: new Uint8Array(header.public_key),
};
}
import {encrypt, decrypt, EncryptStream, DecryptStream, DecryptResult} from './encryption';
import {sign, verify, SignStream, VerifyStream, signDetached, verifyDetached} from './signing';
import {
sign, verify, SignStream, VerifyStream, VerifyResult, signDetached, verifyDetached, VerifyDetachedResult,
} from './signing';
import {signcrypt, designcrypt, SigncryptStream, DesigncryptStream, DesigncryptResult} from './signcryption';
import {
armor, dearmor, ArmorStream, DearmorStream, Options as ArmorOptions, MessageType, ArmorHeaderInfo, DearmorResult,
......@@ -72,7 +74,7 @@ export async function verifyArmored(signed: string, public_key: Uint8Array): Pro
});
}
export type DearmorAndVerifyResult = DearmorResult & Buffer;
export type DearmorAndVerifyResult = DearmorResult & VerifyResult;
export class SignAndArmorStream extends Pumpify {
constructor(keypair: tweetnacl.SignKeyPair, armor_options?: Partial<ArmorOptions>) {
......@@ -88,7 +90,7 @@ export class DearmorAndVerifyStream extends Pumpify {
readonly dearmor: DearmorStream;
readonly verify: VerifyStream;
constructor(public_key: Uint8Array, armor_options?: Partial<ArmorOptions>) {
constructor(public_key?: Uint8Array | null, armor_options?: Partial<ArmorOptions>) {
const dearmor = new DearmorStream(armor_options);
const verify = new VerifyStream(public_key);
......@@ -101,6 +103,9 @@ export class DearmorAndVerifyStream extends Pumpify {
get info(): ArmorHeaderInfo {
return this.dearmor.info;
}
get public_key(): Uint8Array {
return this.verify.public_key;
}
}
export async function signDetachedAndArmor(data: Uint8Array | string, keypair: tweetnacl.SignKeyPair) {
......@@ -111,15 +116,16 @@ export async function verifyDetachedArmored(
signed: string, data: Uint8Array | string, public_key: Uint8Array
): Promise<DearmorAndVerifyDetachedResult> {
const dearmored = dearmor(signed);
await verifyDetached(dearmored, data, public_key);
const result = await verifyDetached(dearmored, data, public_key);
return {
remaining: dearmored.remaining,
header_info: dearmored.header_info,
public_key: result.public_key,
};
}
export interface DearmorAndVerifyDetachedResult {
export interface DearmorAndVerifyDetachedResult extends VerifyDetachedResult {
remaining: Buffer;
header_info: ArmorHeaderInfo;
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment