Cryptographic Identity Proofs
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
| Kind | Type | Description |
|---|---|---|
| 30509 | APK Cert | SHA-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
| Tag | Description |
|---|---|
d | SHA-256 of the DER-encoded APK signing certificate, 64 lowercase hex |
signature | Signature in base64 (RFC 4648, with padding, no whitespace) |
expiry | Expiry timestamp as Unix seconds (base-10 digits); MUST be greater than created_at |
revoked | If 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'screated_atfield<expiry>is the Unix timestamp (seconds), base-10 ASCII digits, and MUST equal theexpirytag<pubkey>is the 64-character lowercase hex Nostr public key, and MUST equal the eventpubkey
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 Type | Verification |
|---|---|
| RSA | PKCS#1 v1.5 with SHA-256 |
| ECDSA | ECDSA 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
algorithmtag if additional schemes (e.g., RSA-PSS, Ed25519) see significant adoption. The public key is always extracted from the certificate whose hash is ind.
Verification Requirements
Verifiers MUST:
- Compute SHA-256 of the DER-encoded certificate and confirm it matches the
dtag - Extract the public key from the certificate, determine key type, and verify signature accordingly
- Confirm
expiry > created_atand current time <expiry - Query all relays in the user's relay list for events with the same
dtag andpubkey, and confirm no version has arevokedtag
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: 726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11Creating 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.pemExtract 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' ' -f1Extract public key for signing:
openssl x509 -in cert.pem -pubkey -noout > pubkey.pemSign 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 -AVerifying 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.pemVerify 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, supersededStandard replaceable-event semantics apply, except: **key-compromised is permanent**. Relays MUST reject updates after a key-compromised revocation; clients MUST ignore them.
Security Considerations
- Signature verification required — Clients MUST verify signatures before trusting identity claims.
- Certificate binding — The certificate hash MUST match the
dtag. - Replay prevention — Both
created_atandexpiryare bound into the signed message. - Revocation checking — Clients MUST check all relays in the user's relay list for revocation before trusting a proof.