IPNS.eth: Keyless Pinning Service by NameSys


 

IPNS.eth: Keyless Pinning Service

Pin Content to IPNS Trustlessly and Securely!

 
Clients: ipns.eth | ipns.dev | pin.namesys.xyz

Abstract

IPNS.eth is a no-nonsense, free, open-source, autonomous, trustless, keyless and secure IPNS Pinning Service. Users of IPNS.eth 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

It’s autonomous, trustless, keyless and secure, and it is the first of its kind. Users of IPNS have so far required to either share their private keys with service providers such as 1W3.io or dWebServices.xyz, or publish privately from their IPFS nodes. IPNS.eth is the first service which removes this severe security flaw and accessibility issue by employing a “keyless” interface to the IPFS network. Users’ IPNS keys are generated deterministically from their Ethereum wallet signatures only during an update, and the content is then pinned and published on NameSys’s fork of w3name publishing infrastructure deployed on Cloudflare.

 

Showcase

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

7 Likes

Excellent, we are eager to explore this opportunity and consider integrating it into WebHash.eth AKA (1W3.eth), provided it operates in a permissionless manner. We have been searching for a service of this nature for quite some time, and are hopeful that this could be a promising solution.

2 Likes

Code is open-source as always. Anyone can integrate it into their frontend and publish to w3name’s open API: GitHub - namesys-eth/ipns-eth-client: IPNS.eth Pinning Service Client | https://ipns.eth.limo

:rocket:

1 Like

I have a concern regarding the creation of IPNS records using the w3name service. Recently, there have been inconsistencies with web3.storage’s operations. Initially, they announced plans to discontinue the w3name service, only to later reverse this decision and keep it active. Given this history of sudden changes, I remain uncertain about the service’s long-term reliability. This track record of web3.storage altering their services raises concerns for us.

1 Like

We have forked w3name’s open-source code and are running our own publishing service starting from v1. Whether w3name continues running or not is irrelevant

2 Likes

Excellent :rocket:

1 Like

This is fantastic to see!

As I understood it, the issue with IPNS is that you either have to regularly sign new records with short TTLs, or sign records with long TTLs - which could then be returned as valid results long after you’d like to replace them with newer records. How are you handling this?

1 Like

w3name stack comes with an IPNS republisher script which rebroadcasts every 24 hours. We are running a fork of it as well. We are also playing with faster/slower rebroadcast intervals to see what breaks.

So the script retains a copy of your IPNS keys?

No, re-broadcast doesn’t require your keys, only signature (and re-publisher service). Keys are only generated during revisions and not retained anywhere.

The idea is to initially publish with long TTLs and when an update is made, revision is ‘spammed’ across the network by the re-publisher script. The network effects in the end determine how fast you get your new records.

1 Like