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 to42
,let dkLen = 42;
FIPS 186-4 B.4.1 requires hashkey length to be
>= n + 8
, wheren = 32
is the bytelength of the finaled25519
private key, such that42 >= 32 + 8
. -
hashToPrivateScalar()
function is FIPS 186-4 B.4.1 implementation to convert HKDF-derived hashkey to valided25519
keypair. This function is implemented in JavaScript library@noble/ed25519
ashashToPrivateScalar()
.let hashKey = hkdf(sha256, inputKey, salt, info, dkLen = 42); let privKey = ed25519.utils.hashToPrivateScalar(hashKey); let pubKey = ed25519.utils.getPublicKey(privKey);
The resulting
privKey
andpubKey
is theed25519
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
andpassword
in trusted and secure clients.
References
- RFC-6979: Deterministic Usage of the DSA and ECDSA
- RFC-5869: HKDF (HMAC-based Extract-and-Expand Key Derivation Function)
- CAIP-02: Blockchain ID Specification
- CAIP-10: Account ID Specification
- Digital Signature Standard (DSS), FIPS 186-4 B.4.1
- ERC-191: Signed Data Standard
- EIP-155: Simple Replay Attack Protection
- ECDSA Signature Standard
- @noble/hashes
- @noble/secp256k1