NIPs by PolleramaCommunity NIPs, surfaced by trustConnect
npub10r8xl2njyep...

Cryptographic Identity Proofs

Published Feb 26, 2026
kind 30509

NIP-C1

Cryptographic Identity Proofs

draft optional

Abstract

This NIP defines a standard for linking cryptographic identities to Nostr profiles. It allows users to prove they control a specific public key, enabling verification against external sources such as signed software artifacts.

Supported Key Types

KindTypeDescription
30509APK CertSHA-256 of the DER-encoded APK signing certificate

Future versions may add support for additional types such as OpenPGP.

Event Format

A cryptographic identity proof is published as a parameterized replaceable event:

{
  "kind": 30509,
  "pubkey": "<nostr-pubkey-hex>",
  "created_at": <timestamp>,
  "tags": [
    ["d", "<apk_certificate_hash>"],
    ["signature", "<signature-base64>"],
    ["expiry", "<unix-timestamp>"]
  ],
  "content": ""
}

Tag Definitions

TagDescription
dSHA-256 of the DER-encoded APK signing certificate, 64 lowercase hex
signatureSignature in base64 (RFC 4648, with padding, no whitespace)
expiryExpiry timestamp as Unix seconds (base-10 digits); MUST be greater than created_at
revokedIf present, this identity proof has been revoked (optional reason as value)

Identity Fingerprint

The identity fingerprint (apk_certificate_hash) is the SHA-256 hash of the full DER-encoded X.509 signing certificate, lowercase hex.

This matches the identifier used natively by Android and apksigner, and aligns directly with the apk_certificate_hash field in Software Release events (NIP-82). No SPKI extraction step is required anywhere in the stack.

SPKIFP (SHA-256 of the SubjectPublicKeyInfo) was not used because Android identifies signers by certificate, not by bare public key.

Proof Message

The signature is computed over the following message:

Verifying at <created_at> until <expiry> that I control the following Nostr public key: <pubkey>

Where:

  • <created_at> is the Unix timestamp (seconds), base-10 ASCII digits, and MUST equal the event's created_at field
  • <expiry> is the Unix timestamp (seconds), base-10 ASCII digits, and MUST equal the expiry tag
  • <pubkey> is the 64-character lowercase hex Nostr public key, and MUST equal the event pubkey

The signed message MUST be encoded as UTF-8 bytes exactly as shown (no trailing newline). Implementations MUST NOT add or remove whitespace.

Signature Algorithm

Verifiers extract the public key from the certificate and determine the algorithm from the key type:

Certificate Key TypeVerification
RSAPKCS#1 v1.5 with SHA-256
ECDSAECDSA over SHA-256, ASN.1 DER signature

Signatures are computed over the SHA-256 hash of the proof message.

Note: These algorithms cover the vast majority of Android APK signing certificates. Future revisions may add an algorithm tag if additional schemes (e.g., RSA-PSS, Ed25519) see significant adoption. The public key is always extracted from the certificate whose hash is in d.

Verification Requirements

Verifiers MUST:

  1. Compute SHA-256 of the DER-encoded certificate and confirm it matches the d tag
  2. Extract the public key from the certificate, determine key type, and verify signature accordingly
  3. Confirm expiry > created_at and current time < expiry
  4. Query all relays in the user's relay list for events with the same d tag and pubkey, and confirm no version has a revoked tag

Example

{
  "kind": 30509,
  "id": "b38336ac9191a55c6b07505e6ed55c7b1a405c7124260ad462911f3f17a5c9eb",
  "pubkey": "726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11",
  "created_at": 1772114325,
  "tags": [
    [
      "d",
      "e0382ce13f09f4a4f969b95b351ede3b52f1d8946896db0bba85b9f255ae9693"
    ],
    [
      "signature",
      "MEYCIQC9TcEv8sQllSjmneoNY56EZKNEmtFNcMuiToEPd9ZCBwIhAPXblbB5LmEJSpN9Wp78Z5R2MhAmPSbr/KyMe6zi8AQR"
    ],
    [
      "expiry",
      "1803650325"
    ]
  ],
  "content": "",
  "sig": "879beaeb6228a36c071a9d8476961fbba62409cfa1fdbe36e899ce4d71eff2814334fea7cbf64df3f8a885c049bd766b45d8785fd3c87def590421c58fac5d56"
}

The signed message for this example is:

Verifying at 1772114325 until 1803650325 that I control the following Nostr public key: 726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11

Creating a Proof

Export private key from Java keystore:

keytool -importkeystore -srckeystore example.keystore -destkeystore example.p12 -deststoretype pkcs12
openssl pkcs12 -in example.p12 -nocerts -noenc -out privatekey.pem

Extract the certificate and compute the certificate hash (for d tag):

openssl pkcs12 -in example.p12 -nokeys -out cert.pem
openssl x509 -in cert.pem -outform der | sha256sum | cut -d' ' -f1

Extract public key for signing:

openssl x509 -in cert.pem -pubkey -noout > pubkey.pem

Sign the proof message:

CREATED_AT=$(date +%s)
EXPIRY=$(date -d "+1 year" +%s)
PUBKEY="78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d"
echo -n "Verifying at ${CREATED_AT} until ${EXPIRY} that I control the following Nostr public key: ${PUBKEY}" \
  | openssl dgst -sha256 -sign privatekey.pem | openssl base64 -A

Verifying Against an APK

Extract certificate hash from APK (must match d tag):

apksigner verify --print-certs -v app.apk 2>&1 | grep -m1 'certificate SHA-256' | cut -d: -f2 | tr -d ' '

Extract public key for signature verification:

apksigner verify --print-certs-pem app.apk 2>&1 \
  | sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' \
  | openssl x509 -pubkey -noout > pubkey.pem

Verify signature:

echo -n "Verifying at ${CREATED_AT} until ${EXPIRY} that I control the following Nostr public key: ${PUBKEY}" \
  | openssl dgst -sha256 -verify pubkey.pem -signature <(echo "${SIGNATURE}" | base64 -d)

Revocation

To revoke, publish a new event with the revoked tag (reason optional but recommended):

["d", "<apk_certificate_hash>"],
["revoked", "key-compromised"]  // or: key-retired, superseded

Standard replaceable-event semantics apply, except: **key-compromised is permanent**. Relays MUST reject updates after a key-compromised revocation; clients MUST ignore them.

Security Considerations

  1. Signature verification required — Clients MUST verify signatures before trusting identity claims.
  2. Certificate binding — The certificate hash MUST match the d tag.
  3. Replay prevention — Both created_at and expiry are bound into the signed message.
  4. Revocation checking — Clients MUST check all relays in the user's relay list for revocation before trusting a proof.