Skip to content

Commit

Permalink
v2 only
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed Aug 2, 2023
1 parent 547704f commit 7178407
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 95 deletions.
49 changes: 27 additions & 22 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ export const namespace = '/ipns/'
export const namespaceLength = namespace.length

export class IPNSRecord {
readonly protobuf: IpnsEntry
readonly pb: IpnsEntry
private readonly data: any

constructor (pb: IpnsEntry) {
this.protobuf = pb
this.pb = pb

if (pb.data == null) {
throw errCode(new Error('Record data is missing'), ERRORS.ERR_INVALID_RECORD_DATA)
Expand Down Expand Up @@ -71,21 +71,21 @@ export class IPNSRecord {
}
}

export interface IPNSEntryData {
Value: Uint8Array
Validity: Uint8Array
ValidityType: IpnsEntry.ValidityType
Sequence: bigint
TTL: bigint
}

export interface IDKeys {
routingPubKey: Key
pkKey: Key
routingKey: Key
ipnsKey: Key
}

export interface CreateOptions {
v1Compatible?: boolean
}

const defaultCreateOptions: CreateOptions = {
v1Compatible: true
}

/**
* Creates a new ipns entry and signs it with the given private key.
* The ipns entry validity should follow the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
Expand All @@ -95,15 +95,16 @@ export interface IDKeys {
* @param {Uint8Array} value - value to be stored in the record.
* @param {number | bigint} seq - number representing the current version of the record.
* @param {number} lifetime - lifetime of the record (in milliseconds).
* @param {CreateOptions} options - additional create options.
*/
export const create = async (peerId: PeerId, value: Uint8Array, seq: number | bigint, lifetime: number): Promise<IPNSRecord> => {
export const create = async (peerId: PeerId, value: Uint8Array, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
// Validity in ISOString with nanoseconds precision and validity type EOL
const expirationDate = new NanoDate(Date.now() + Number(lifetime))
const validityType = IpnsEntry.ValidityType.EOL
const [ms, ns] = lifetime.toString().split('.')
const lifetimeNs = (BigInt(ms) * BigInt(100000)) + BigInt(ns ?? '0')

return _create(peerId, value, seq, validityType, expirationDate, lifetimeNs)
return _create(peerId, value, seq, validityType, expirationDate, lifetimeNs, options)
}

/**
Expand All @@ -114,18 +115,19 @@ export const create = async (peerId: PeerId, value: Uint8Array, seq: number | bi
* @param {Uint8Array} value - value to be stored in the record.
* @param {number | bigint} seq - number representing the current version of the record.
* @param {string} expiration - expiration datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
* @param {CreateOptions} options - additional create options.
*/
export const createWithExpiration = async (peerId: PeerId, value: Uint8Array, seq: number | bigint, expiration: string): Promise<IPNSRecord> => {
export const createWithExpiration = async (peerId: PeerId, value: Uint8Array, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
const expirationDate = NanoDate.fromString(expiration)
const validityType = IpnsEntry.ValidityType.EOL

const ttlMs = expirationDate.toDate().getTime() - Date.now()
const ttlNs = (BigInt(ttlMs) * BigInt(100000)) + BigInt(expirationDate.getNano())

return _create(peerId, value, seq, validityType, expirationDate, ttlNs)
return _create(peerId, value, seq, validityType, expirationDate, ttlNs, options)
}

const _create = async (peerId: PeerId, value: Uint8Array, seq: number | bigint, validityType: IpnsEntry.ValidityType, expirationDate: NanoDate, ttl: bigint): Promise<IPNSRecord> => {
const _create = async (peerId: PeerId, value: Uint8Array, seq: number | bigint, validityType: IpnsEntry.ValidityType, expirationDate: NanoDate, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
seq = BigInt(seq)
const isoValidity = uint8ArrayFromString(expirationDate.toString())

Expand All @@ -134,22 +136,25 @@ const _create = async (peerId: PeerId, value: Uint8Array, seq: number | bigint,
}

const privateKey = await unmarshalPrivateKey(peerId.privateKey)
const signatureV1 = await signLegacyV1(privateKey, value, validityType, isoValidity)
const data = createCborData(value, isoValidity, validityType, seq, ttl)
const sigData = ipnsEntryDataForV2Sig(data)
const signatureV2 = await privateKey.sign(sigData)

const entry: IpnsEntry = {
value,
signature: signatureV1,
validityType,
validity: isoValidity,
sequence: seq,
ttl,
signatureV2,
data
}

if (options.v1Compatible === true) {
const signatureV1 = await signLegacyV1(privateKey, value, validityType, isoValidity)
entry.value = value
entry.validity = isoValidity
entry.validityType = validityType
entry.signature = signatureV1
entry.sequence = seq
entry.ttl = ttl
}

// if we cannot derive the public key from the PeerId (e.g. RSA PeerIDs),
// we have to embed it in the IPNS record
if (peerId.publicKey != null) {
Expand Down
31 changes: 4 additions & 27 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import * as ERRORS from './errors.js'
import { IpnsEntry } from './pb/ipns.js'
import { IPNSRecord } from './index.js'
import type { IPNSEntryData } from './index.js'
import type { PublicKey } from '@libp2p/interface-keys'
import type { PeerId } from '@libp2p/interface-peer-id'

Expand Down Expand Up @@ -76,15 +75,15 @@ export const extractPublicKey = async (peerId: PeerId, entry: IPNSRecord): Promi

let pubKey: PublicKey | undefined

if (entry.protobuf.pubKey != null) {
if (entry.pb.pubKey != null) {
try {
pubKey = unmarshalPublicKey(entry.protobuf.pubKey)
pubKey = unmarshalPublicKey(entry.pb.pubKey)
} catch (err) {
log.error(err)
throw err
}

const otherId = await peerIdFromKeys(entry.protobuf.pubKey)
const otherId = await peerIdFromKeys(entry.pb.pubKey)

if (!otherId.equals(peerId)) {
throw errCode(new Error('Embedded public key did not match PeerID'), ERRORS.ERR_INVALID_EMBEDDED_KEY)
Expand Down Expand Up @@ -119,7 +118,7 @@ export const ipnsEntryDataForV2Sig = (data: Uint8Array): Uint8Array => {
}

export const marshal = (obj: IPNSRecord): Uint8Array => {
return IpnsEntry.encode(obj.protobuf)
return IpnsEntry.encode(obj.pb)
}

export const unmarshal = (buf: Uint8Array): IPNSRecord => {
Expand Down Expand Up @@ -168,25 +167,3 @@ export const createCborData = (value: Uint8Array, validity: Uint8Array, validity

return cborg.encode(data)
}

export const parseCborData = (buf: Uint8Array): IPNSEntryData => {
const data = cborg.decode(buf)

if (data.ValidityType === 0) {
data.ValidityType = IpnsEntry.ValidityType.EOL
} else {
throw errCode(new Error('Unknown validity type'), ERRORS.ERR_UNRECOGNIZED_VALIDITY)
}

if (Number.isInteger(data.Sequence)) {
// sequence must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range
data.Sequence = BigInt(data.Sequence)
}

if (Number.isInteger(data.TTL)) {
// ttl must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range
data.TTL = BigInt(data.TTL)
}

return data
}
43 changes: 30 additions & 13 deletions src/validator.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { logger } from '@libp2p/logger'
import * as cborg from 'cborg'
import errCode from 'err-code'
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import * as ERRORS from './errors.js'
import { IpnsEntry } from './pb/ipns.js'
import { parseRFC3339, extractPublicKey, ipnsEntryDataForV2Sig, unmarshal, peerIdFromRoutingKey, parseCborData } from './utils.js'
import { parseRFC3339, extractPublicKey, ipnsEntryDataForV2Sig, unmarshal, peerIdFromRoutingKey } from './utils.js'
import type { IPNSRecord } from './index.js'
import type { ValidateFn } from '@libp2p/interface-dht'
import type { PublicKey } from '@libp2p/interface-keys'
Expand All @@ -20,23 +21,23 @@ const MAX_RECORD_SIZE = 1024 * 10
* Validates the given IPNS entry against the given public key
*/
export const validate = async (publicKey: PublicKey, entry: IPNSRecord): Promise<void> => {
const { value, validityType, validity } = entry.protobuf
const { value, validityType, validity } = entry.pb

// Ensure Signature V2 and Data are present and not empty.
if ((entry.protobuf.signatureV2 == null) || (entry.protobuf.data == null)) {
if ((entry.pb.signatureV2 == null) || (entry.pb.data == null)) {
throw errCode(new Error('missing data or signatureV2'), ERRORS.ERR_SIGNATURE_VERIFICATION)
}

// If Signature V1 is present, ensure that CBOR data matches Protobuf data.
if (entry.protobuf.signature != null || entry.value != null) {
if (entry.pb.signature != null || entry.pb.value != null) {
validateCborDataMatchesPbData(entry)
}

// Validate Signature V2
let isValid
try {
const signature = entry.protobuf.signatureV2
const dataForSignature = ipnsEntryDataForV2Sig(entry.protobuf.data)
const signature = entry.pb.signatureV2
const dataForSignature = ipnsEntryDataForV2Sig(entry.pb.data)
isValid = await publicKey.verify(dataForSignature, signature)
} catch (err) {
isValid = false
Expand Down Expand Up @@ -70,29 +71,45 @@ export const validate = async (publicKey: PublicKey, entry: IPNSRecord): Promise
}

const validateCborDataMatchesPbData = (entry: IPNSRecord): void => {
if (entry.protobuf.data == null) {
if (entry.pb.data == null) {
throw errCode(new Error('Record data is missing'), ERRORS.ERR_INVALID_RECORD_DATA)
}

const data = parseCborData(entry.protobuf.data)
const data = cborg.decode(entry.pb.data)

if (!uint8ArrayEquals(data.Value, entry.protobuf.value ?? new Uint8Array(0))) {
if (!uint8ArrayEquals(data.Value, entry.pb.value ?? new Uint8Array(0))) {
throw errCode(new Error('Field "value" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION)
}

if (!uint8ArrayEquals(data.Validity, entry.protobuf.validity ?? new Uint8Array(0))) {
if (!uint8ArrayEquals(data.Validity, entry.pb.validity ?? new Uint8Array(0))) {
throw errCode(new Error('Field "validity" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION)
}

if (data.ValidityType !== entry.protobuf.validityType) {
if (data.ValidityType === 0) {
data.ValidityType = IpnsEntry.ValidityType.EOL
} else {
throw errCode(new Error('Unknown validity type'), ERRORS.ERR_UNRECOGNIZED_VALIDITY)
}

if (data.ValidityType !== entry.pb.validityType) {
throw errCode(new Error('Field "validityType" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION)
}

if (data.Sequence !== entry.protobuf.sequence) {
if (Number.isInteger(data.Sequence)) {
// sequence must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range
data.Sequence = BigInt(data.Sequence)
}

if (data.Sequence !== entry.pb.sequence) {
throw errCode(new Error('Field "sequence" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION)
}

if (data.TTL !== entry.protobuf.ttl) {
if (Number.isInteger(data.TTL)) {
// ttl must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range
data.TTL = BigInt(data.TTL)
}

if (data.TTL !== entry.pb.ttl) {
throw errCode(new Error('Field "ttl" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION)
}
}
Expand Down
Loading

0 comments on commit 7178407

Please sign in to comment.