IPNS.dev: Keyless Pinning Service

IPNS.dev: Keyless Pinning Service

Pin Content to IPNS Trustlessly and Securely!

By: sshmatrix & 0xc0de4c0ffee for NameSys

Client v1.0-beta: ipns.dev | pin.namesys.xyz | ipns.eth

GitHub: @namesys/ipns-client

Abstract

IPNS.dev is a no-nonsense, free, open-source, autonomous, trustless, keyless and secure IPNS Pinning Service. Users of IPNS.dev do not need to share their IPNS keys with service providers, and can securely and ‘keylessly’ publish to IPFS network with anonymity. To our knowledge, this is the first such service and public good in existence.

Motivation

IPNS.dev is a no-nonsense, free, open-source, autonomous, trustless, keyless and secure IPNS pinning service. Users of IPNS have so far required to either share their private keys with scarce service providers such as dWebServices.xyz or publish privately to their IPFS nodes. IPNS.dev is a service which removes this severe security flaw and accessibility issue by employing a “keyless” interface to the IPFS network. Users of IPNS.dev do not need to share their IPNS keys with service providers, and can securely and ‘keylessly’ publish to IPFS network with anonymity. Users’ IPNS keys are generated deterministically from their Ethereum wallet signatures only during an update, and the content is then pinned and published on w3name publishing infrastructure deployed on Cloudflare. To our knowledge, this is the first such service and public good in existence.

By providing a trustless, keyless, and secure IPNS pinning service, IPNS.dev provides users with absolute control over their IPNS content while prioritising privacy and security. IPNS.dev could reshape how IPFS content is shared and accessed, making it more accessible to those who truly hold the ‘not your keys, not your coin data’ ethos dear. It also sets a new standard for IPFS services to incorporate IPNS in their hosting schemes, potentially expanding the reach of IPFS network. IPNS.dev has the potential to make secure data publishing more accessible and contribute to the decentralisation aspect of the IPFS network.

 

Specification

a) Keyname

keyname is an identifier for an IPNS key, such that

let keyname = 'keyname'

b) Password

password is an optional string value used to salt the key derivation function (HKDF),

let password = "horse staple battery"

c) Chain-agnostic Identifiers

Chain-agnostic CAIP-02: Blockchain ID Specification and CAIP-10: Account ID Specification schemes are used to generate blockchain and address identifiers caip02 and caip10 respectively,

let caip02 =
      `eip155:<EVM_CHAINID>` ||
      `cosmos:<HUB_ID_NAME>` ||
      `bip122:<16BYTE_HASH>`;

let caip10 = `${caip02}:<ADDR_CHECKSUM>`;

d) Info

info is CAIP-10 and Keyname string formatted as:

let info = `${caip10}:${keyname}`;

e) Message

Deterministic message to be signed by the wallet provider,

let message = `Requesting Signature To Generate IPNS Key\n\nOrigin: ${keyname}\nKey Type: ed25519\nExtradata: ${extradata}\nSigned By: ${caip10}`

such that

// EXTRADATA
bytes32 extradata = keccak256(
            abi.encodePacked(
              keccak256(
                abi.encodePacked(password)
              ),
              ETH_ADDR_CHECKSUM
              )
            );

f) Signature

RFC-6979 compatible (ECDSA) deterministic signature calculated by the wallet provider using native keypair,

let signature = wallet.signMessage(message);

g) Salt

salt is SHA-256 hash of the info, optional password and last 32 bytes of signature string formatted as:

let salt = await sha256(`${info}:${password?password:""}:${signature.slice(68)}`);

where, signature.slice(68) are the last 32 bytes of the deterministic ECDSA-derived Ethereum signature.

h) Key Derivation Function (KDF)

HMAC-Based KDF hkdf(sha256, inputKey, salt, info, dkLen = 42) is used to derive the 42 bytes long hashkey with inputs,

  • inputKey is SHA-256 hash of signature bytes,

    let inputKey = await sha256(hexToBytes(signature.slice(2)));
    
  • info is same as defined before, i.e.

    let info = `${caip10}:${keyname}`;
    
  • salt is same as defined before, i.e.

    let salt = await sha256(`${info}:${password?password:""}:${signature.slice(68)}`);
    
  • dkLen (Derived Key Length) is set to 42,

    let dkLen = 42;
    

    FIPS 186-4 B.4.1 requires hashkey length to be >= n + 8, where n = 32 is the bytelength of the final ed25519 private key, such that 42 >= 32 + 8.

  • hashToPrivateScalar() function is FIPS 186-4 B.4.1 implementation to convert HKDF-derived hashkey to valid ed25519 keypair. This function is implemented in JavaScript library @noble/ed25519 as hashToPrivateScalar().

    let hashKey = hkdf(sha256, inputKey, salt, info, dkLen = 42);
    let privKey = ed25519.utils.hashToPrivateScalar(hashKey);
    let pubKey = ed25519.utils.getPublicKey(privKey);
    

    The resulting privKey and pubKey is the ed25519 deterministic keypair that can interact with IPFS network.

Implementation Requirements

  • Connected Ethereum wallet Signer MUST be EIP-191 and RFC-6979 compatible.
  • The message MUST be string formatted as
Requesting Signature To Generate IPNS Key\n\nOrigin: ${keyname}\nKey Type: ed25519\nExtradata: ${extradata}\nSigned By: ${caip10}
  • HKDF inputKey MUST be generated as the SHA-256 hash of 65 bytes long signature.
  • HKDF salt MUST be generated as SHA-256 hash of string
${info}:${password?password:""}:${signature.slice(68)}
  • HKDF Derived Key Length (dkLen) MUST be 42.
  • HKDF info MUST be string formatted as
${caip10}:${keyname}

TS Example

import * as ed25519 from '@noble/ed25519'
import {hkdf} from '@noble/hashes/hkdf'
import {sha256} from '@noble/hashes/sha256'
import {ethers} from 'ethers'

let wallet = new ethers.Wallet(PRIVATE_KEY, provider)
let keyname = "keyname"
let chainId = wallet.getChainId(); // get ChainID from connected wallet
let address = wallet.getAddress(); // get Address from wallet
let caip10 = `eip155:${chainId}:${address}`;
let message = `Requesting Signature To Generate IPNS Key\n\nOrigin: ${keyname}\nKey Type: ed25519\nExtradata: ${extradata}\nSigned By: ${caip10}`
let signature = wallet.signMessage(message); // request Signature from wallet
let password = "horse staple battery"

/**
 * @param   keyname Key identifier
 * @param    caip10 CAIP identifier for the blockchain account
 * @param signature Deterministic signature from X-wallet provider
 * @param  password Optional password
 * @returns Deterministic private/public keypairs as hex strings
 * Hex-encoded
 * [ed25519.priv, ed25519.pub]
 */
export async function KEYGEN(
  keyname: string,
  caip10: string,
  signature: string,
  password: string | undefined
): Promise<[
  string, string
]> {
  if (signature.length < 64)
    throw new Error('SIGNATURE TOO SHORT; LENGTH SHOULD BE 65 BYTES')
  let inputKey = sha256(
    ed25519.utils.hexToBytes(
      signature.toLowerCase().startsWith('0x') ? signature.slice(2) : signature
    )
  )
  let info = `${caip10}:${keyname}`
  let salt = sha256(`${info}:${password ? password : ''}:${signature.slice(-64)}`)
  let hashKey = hkdf(sha256, inputKey, salt, info, 42)
  let ed25519priv = ed25519.utils.hashToPrivateScalar(hashKey).toString(16).padStart(64, "0") // ed25519 Private Key
  let ed25519pub = ed25519.utils.bytesToHex(await ed25519.getPublicKey(ed25519priv)) // ed25519 Public Key
  return [ // Hex-encoded [ed25519.priv, ed25519.pub]
    ed25519priv, ed25519pub
  ]
}

Security Considerations

  • Users SHOULD always verify the integrity and authenticity of their client before signing the message.
  • Users SHOULD ensure that they only input their keyname and password in trusted and secure clients.

References

2 Likes