Skip to content
This repository has been archived by the owner on Jun 19, 2023. It is now read-only.

Fix noise prologue generation #18

Merged
merged 9 commits into from
Aug 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@
"@libp2p/logger": "^2.0.0",
"@libp2p/multistream-select": "^3.0.0",
"@libp2p/peer-id": "^1.1.15",
"@multiformats/multiaddr": "../js-multiaddr/",
"@multiformats/multiaddr": "file:../js-multiaddr",
"abortable-iterator": "^4.0.2",
"err-code": "^3.0.1",
"it-merge": "^1.0.4",
"multiformats": "^9.7.1",
"multihashes": "^4.0.3",
"p-defer": "^4.0.0",
"socket.io-client": "^4.1.2",
"timeout-abort-controller": "^3.0.0",
Expand Down
14 changes: 7 additions & 7 deletions src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { WebRTCStream } from './stream';
import { select as msselect, handle as mshandle } from '@libp2p/multistream-select';
import { Duplex } from 'it-stream-types';
import { Uint8ArrayList } from 'uint8arraylist';
import { dataChannelError, operationAborted, overStreamLimit } from './error';

const log = logger('libp2p:webrtc:connection');

Expand Down Expand Up @@ -73,8 +74,7 @@ export class WebRTCConnection implements ic.Connection {

await Promise.race([openPromise.promise, abortPromise.promise]);
if (controller.signal.aborted) {
// TODO: Better errors
throw Error(controller.signal.reason);
throw operationAborted('prior to a new stream incoming.', controller.signal.reason);
}

let rawStream = new WebRTCStream({
Expand Down Expand Up @@ -162,8 +162,8 @@ export class WebRTCConnection implements ic.Connection {
}

options.signal.onabort = () => {
openError = operationAborted('.', options.signal?.reason || 'aborted');
log.trace(`[stream: ${label}] abort called - ${options.signal?.reason}`);
openError = new Error(options.signal?.reason || 'aborted');
abortedPromise.resolve();
};

Expand All @@ -174,8 +174,8 @@ export class WebRTCConnection implements ic.Connection {
openPromise.resolve();
};
channel.onerror = (_evt) => {
log.trace(`[stream: ${label}] data channel error: ${(_evt as RTCErrorEvent).error}`);
openError = new Error(`data channel error`);
openError = dataChannelError(label, (_evt as RTCErrorEvent).error.message);
log.trace(openError.message);
abortedPromise.resolve();
};

Expand Down Expand Up @@ -216,8 +216,8 @@ export class WebRTCConnection implements ic.Connection {
let protocol = stream.stat.protocol!;
let direction = stream.stat.direction;
if (this.countStream(protocol, direction) === this.findStreamLimit(protocol, direction)) {
log(`${direction} stream limit reached for protocol - ${protocol}`);
let err = new Error(`${direction} stream limit reached for protocol - ${protocol}`);
let err = overStreamLimit(direction, protocol);
log(err.message);
stream.abort(err);
throw err;
}
Expand Down
104 changes: 95 additions & 9 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,99 @@
import { default as createError } from 'err-code';
import { Direction } from '@libp2p/interface-connection';

export class WebRTCTransportError extends Error {
constructor(msg: string) {
super(msg);
this.name = 'WebRTCTransportError';
}
constructor(msg: string) {
super('WebRTC transport error: ' + msg);
this.name = 'WebRTCTransportError';
}
}

export enum codes {
ERR_ALREADY_ABORTED = 'ERR_ALREADY_ABORTED',
ERR_DATA_CHANNEL = 'ERR_DATA_CHANNEL',
ERR_INVALID_MULTIADDR = 'ERR_INVALID_MULTIADDR',
ERR_INVALID_PARAMETERS = 'ERR_INVALID_PARAMETERS',
ERR_HASH_NOT_SUPPORTED = 'ERR_HASH_NOT_SUPPORTED',
ERR_NOT_IMPLEMENTED = 'ERR_NOT_IMPLEMENTED',
ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS = 'ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS',
ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS = 'ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS',
}

export class InvalidArgumentError extends WebRTCTransportError {
constructor(msg: string) {
super(msg);
this.name = 'WebRTC/InvalidArgumentError';
}
}
constructor(msg: string) {
super('There was a problem with a provided argument: ' + msg);
this.name = 'WebRTC/InvalidArgumentError';
}
}

export function unsupportedHashAlgorithm(algorithm: string) {
return createError(new UnsupportedHashAlgorithmError(algorithm), codes.ERR_HASH_NOT_SUPPORTED);
}

export class UnsupportedHashAlgorithmError extends WebRTCTransportError {
constructor(algo: string) {
let msg = `unsupported hash algorithm: ${algo}`;
super(msg);
this.name = 'WebRTC/UnsupportedHashAlgorithmError';
}
}

export function invalidArgument(msg: string) {
return createError(new InvalidArgumentError(msg), codes.ERR_INVALID_PARAMETERS);
}

export class UnimplementedError extends WebRTCTransportError {
constructor(methodName: string) {
super('A method (' + methodName + ') was called though it has been intentionally left unimplemented.');
this.name = 'WebRTC/UnimplementedError';
}
}

export function unimplemented(methodName: string) {
return createError(new UnimplementedError(methodName), codes.ERR_NOT_IMPLEMENTED);
}

export class InappropriateMultiaddrError extends WebRTCTransportError {
constructor(msg: string) {
super('There was a problem with the Multiaddr which was passed in: ' + msg);
this.name = 'WebRTC/InappropriateMultiaddrError';
}
}

export function inappropriateMultiaddr(msg: string) {
return createError(new InappropriateMultiaddrError(msg), codes.ERR_INVALID_MULTIADDR);
}

export class OperationAbortedError extends WebRTCTransportError {
constructor(context: string, abortReason: string) {
super(`Signalled to abort because (${abortReason}})${context}`);
this.name = 'WebRTC/OperationAbortedError';
}
}

export function operationAborted(context: string, reason: string) {
return createError(new OperationAbortedError(context, reason), codes.ERR_ALREADY_ABORTED);
}

export class DataChannelError extends WebRTCTransportError {
constructor(streamLabel: string, errorMessage: string) {
super(`[stream: ${streamLabel}] data channel error: ${errorMessage}`);
this.name = 'WebRTC/DataChannelError';
}
}

export function dataChannelError(streamLabel: string, msg: string) {
return createError(new OperationAbortedError(streamLabel, msg), codes.ERR_DATA_CHANNEL);
}

export class StreamingLimitationError extends WebRTCTransportError {
constructor(msg: string) {
super(msg);
this.name = 'WebRTC/StreamingLimitationError';
}
}

export function overStreamLimit(dir: Direction, proto: string) {
let code = dir == 'inbound' ? codes.ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS : codes.ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS;
return createError(new StreamingLimitationError(`${dir} stream limit reached for protocol - ${proto}`), code);
}
60 changes: 45 additions & 15 deletions src/sdp.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { InvalidArgumentError } from './error.js'
import { inappropriateMultiaddr, invalidArgument, unsupportedHashAlgorithm } from './error.js';
import { logger } from '@libp2p/logger';
import { Multiaddr } from '@multiformats/multiaddr';
import * as multihashes from 'multihashes';
import { bases } from 'multiformats/basics';

const log = logger('libp2p:webrtc:sdp');

const mbdecoder = (function () {
const decoders = Object.values(bases).map((b) => b.decoder);
let acc = decoders[0].or(decoders[1]);
decoders.slice(2).forEach((d) => (acc = acc.or(d)));
return acc;
})();

const CERTHASH_CODE: number = 466;
const ANSWER_SDP_FORMAT: string = `
v=0
Expand Down Expand Up @@ -37,28 +46,52 @@ function ip(ma: Multiaddr): string {
function port(ma: Multiaddr): number {
return ma.toOptions().port;
}
function certhash(ma: Multiaddr): string {

export function certhash(ma: Multiaddr): string {
let tups = ma.stringTuples();
let certhash_value = tups
.filter((tup) => tup[0] == CERTHASH_CODE)
.map((tup) => tup[1])[0];
let certhash_value = tups.filter((tup) => tup[0] == CERTHASH_CODE).map((tup) => tup[1])[0];
if (certhash_value) {
return certhash_value;
} else {
throw new Error("Couldn't find a certhash component of multiaddr:" + ma.toString());
throw inappropriateMultiaddr("Couldn't find a certhash component of multiaddr:" + ma.toString());
}
}

function certhashToFingerprint(ma: Multiaddr): string {
let certhash_value = certhash(ma);
// certhash_value is a multibase encoded multihash encoded string
let mbdecoded = mbdecoder.decode(certhash_value);
let mhdecoded = multihashes.decode(mbdecoded);
let prefix = '';
switch (mhdecoded.name) {
case 'md5':
prefix = 'md5';
break;
case 'sha2-256':
prefix = 'sha-256';
break;
case 'sha2-512':
prefix = 'sha-512';
break;
default:
throw unsupportedHashAlgorithm(mhdecoded.name);
}

let fp = mhdecoded.digest.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
fp = fp.match(/.{1,2}/g)!.join(':');

return `${prefix} ${fp}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Certhash contains a space?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This writes the fingerprint in SDP format <hash algorithm> <fingerprint>.

}

function ma2sdp(ma: Multiaddr, ufrag: string): string {
return ANSWER_SDP_FORMAT
.replace('%s', ipv(ma))
return ANSWER_SDP_FORMAT.replace('%s', ipv(ma))
.replace('%s', ip(ma))
.replace('%s', ipv(ma))
.replace('%s', ip(ma))
.replace('%d', port(ma).toString())
.replace('%s', ufrag)
.replace('%s', ufrag)
.replace('%s', certhash(ma));
.replace('%s', certhashToFingerprint(ma));
}

export function fromMultiAddr(ma: Multiaddr, ufrag: string): RTCSessionDescriptionInit {
Expand All @@ -70,12 +103,9 @@ export function fromMultiAddr(ma: Multiaddr, ufrag: string): RTCSessionDescripti

export function munge(desc: RTCSessionDescriptionInit, ufrag: string): RTCSessionDescriptionInit {
if (desc.sdp) {
desc.sdp = desc.sdp
.replace(/\na=ice-ufrag:[^\n]*\n/, '\na=ice-ufrag:' + ufrag + '\n')
.replace(/\na=ice-pwd:[^\n]*\n/, '\na=ice-pwd:' + ufrag + '\n')
;
return desc;
desc.sdp = desc.sdp.replace(/\na=ice-ufrag:[^\n]*\n/, '\na=ice-ufrag:' + ufrag + '\n').replace(/\na=ice-pwd:[^\n]*\n/, '\na=ice-pwd:' + ufrag + '\n');
John-LittleBearLabs marked this conversation as resolved.
Show resolved Hide resolved
return desc;
} else {
throw new InvalidArgumentError("Can't munge a missing SDP");
throw invalidArgument("Can't munge a missing SDP");
}
}
53 changes: 48 additions & 5 deletions src/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ import { logger } from '@libp2p/logger';
import { Multiaddr } from '@multiformats/multiaddr';
import { v4 as genUuid } from 'uuid';
import defer, { DeferredPromise } from 'p-defer';
import { base64 } from 'multiformats/bases/base64';
import { fromString as uint8arrayFromString } from 'uint8arrays/from-string';
import { concat } from 'uint8arrays/concat';
import * as multihashes from 'multihashes';
import { inappropriateMultiaddr, unimplemented, invalidArgument, unsupportedHashAlgorithm } from './error';

const log = logger('libp2p:webrtc:transport');
const utf8 = new TextEncoder();

export class WebRTCTransport implements Transport, Initializable {
private componentsPromise: DeferredPromise<void> = defer();
Expand All @@ -31,7 +35,7 @@ export class WebRTCTransport implements Transport, Initializable {
}

createListener(options: CreateListenerOptions): Listener {
throw new Error('TODO - replace with an exception more appropriate to the fact that this will not be implemented.');
throw unimplemented('WebRTCTransport.createListener');
}

filter(multiaddrs: Multiaddr[]): Multiaddr[] {
Expand Down Expand Up @@ -87,15 +91,15 @@ export class WebRTCTransport implements Transport, Initializable {
let myPeerId = this.components!.getPeerId();
let rps = ma.getPeerId();
if (!rps) {
throw new Error('TODO Do we really need a peer ID ?');
throw inappropriateMultiaddr("we need to have the remote's PeerId");
}
let theirPeerId = p.peerIdFromString(rps);

// do noise handshake
//set the Noise Prologue to libp2p-webrtc-noise:<FINGERPRINTS> before starting the actual Noise handshake.
// <FINGERPRINTS> is the concatenation of the of the two TLS fingerprints of A and B in their multihash byte representation, sorted in ascending order.
let fingerprintsPrologue = [myPeerId.multihash, theirPeerId.multihash].sort().join('');
let noise = new Noise(myPeerId.privateKey, undefined, stablelib, utf8.encode(fingerprintsPrologue));
let fingerprintsPrologue = this.generateNoisePrologue(peerConnection, ma);
let noise = new Noise(myPeerId.privateKey, undefined, stablelib, fingerprintsPrologue);
let wrappedChannel = new WebRTCStream({ channel: handshakeDataChannel, stat: { direction: 'outbound', timeline: { open: 1 } } });
let wrappedDuplex = {
...wrappedChannel,
Expand All @@ -120,4 +124,43 @@ export class WebRTCTransport implements Transport, Initializable {
remotePeer: theirPeerId,
});
}

private generateNoisePrologue(pc: RTCPeerConnection, ma: Multiaddr): Uint8Array {
let remoteCerthash = sdp.certhash(ma);
if (!remoteCerthash) {
throw inappropriateMultiaddr('no remote tls fingerprint in multiaddr');
}
let remote = base64.decode(remoteCerthash);
if (pc.getConfiguration().certificates?.length === 0) {
throw invalidArgument('no local certificate');
}
let localCert = pc.getConfiguration().certificates![0];
if (localCert.getFingerprints().length === 0) {
throw invalidArgument('no fingerprint on local certificate');
}

let localFingerprint = localCert.getFingerprints()[0];
let localFpString = localFingerprint.value!.replaceAll(':', '');
let localFpArray = uint8arrayFromString(localFpString, 'hex');
let local: Uint8Array;
switch (localFingerprint.algorithm!) {
case 'md5':
local = multihashes.encode(localFpArray, multihashes.names['md5']);
break;
case 'sha-256':
local = multihashes.encode(localFpArray, multihashes.names['sha2-256']);
break;
case 'sha-512':
local = multihashes.encode(localFpArray, multihashes.names['sha2-512']);
break;
default:
throw unsupportedHashAlgorithm(localFingerprint.algorithm || 'none');
}

let prefix = uint8arrayFromString('libp2p-webrtc-noise:');
let fps = [local, remote].sort();

let result = concat([prefix, ...fps]);
return result;
}
}
2 changes: 1 addition & 1 deletion test/connection.browser.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-env mocha */

import {createConnectionPair, echoHandler} from "./util";
import {createConnectionPair, echoHandler} from "../test/util.js";
import { expect } from 'aegir/chai';
import { pipe } from 'it-pipe';
import all from 'it-all';
Expand Down
6 changes: 3 additions & 3 deletions test/sdp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ a=mid:0
a=ice-options:ice2
a=ice-ufrag:MyUserFragment
a=ice-pwd:MyUserFragment
a=fingerprint:mTXVsdGliYXNlIGlzIGF3ZXNvbWUhIFxvLw
a=fingerprint:sha-256 b9:2e:11:cf:23:ff:da:31:bb:bb:5c:0a:9d:d9:0e:20:07:e2:bb:61:2f:1f:94:cf:e5:2e:0e:05:5c:4e:8a:88
a=setup:actpass
a=sctp-port:5000
a=max-message-size:100000
`;

describe('SDP creation', () => {
it('handles simple blue sky easily enough', async () => {
let ma = new Multiaddr('/ip4/192.168.0.152/udp/2345/webrtc/certhash/zYAjKoNbau5KiqmHPmSxYCvn66dA1vLmwbt');
let ma = new Multiaddr('/ip4/192.168.0.152/udp/2345/webrtc/certhash/uEiC5LhHPI__aMbu7XAqd2Q4gB-K7YS8flM_lLg4FXE6KiA');
let ufrag = 'MyUserFragment';
let sdp = underTest.fromMultiAddr(ma, ufrag);
expect(sdp.sdp).to.equal(an_sdp);
Expand All @@ -42,7 +42,7 @@ a=mid:0
a=ice-options:ice2
a=ice-ufrag:someotheruserfragmentstring
a=ice-pwd:someotheruserfragmentstring
a=fingerprint:mTXVsdGliYXNlIGlzIGF3ZXNvbWUhIFxvLw
a=fingerprint:sha-256 b9:2e:11:cf:23:ff:da:31:bb:bb:5c:0a:9d:d9:0e:20:07:e2:bb:61:2f:1f:94:cf:e5:2e:0e:05:5c:4e:8a:88
a=setup:actpass
a=sctp-port:5000
a=max-message-size:100000
Expand Down
Loading