Cryptographic Identity Proofs
NIP-C1
Cryptographic Identity Proofs
draft optional
Abstract
This NIP links a Nostr pubkey to an APK signing certificate by proving control of both keys. An optional embedded certificate makes the proof self-contained. An optional delegate pubkey lets CI/CD publish narrowly scoped NIP-82 events without access to the main Nostr key.
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>"],
["cert", "<certificate-der-base64>"], // OPTIONAL
["delegation", "<delegate-pubkey-hex>"] // OPTIONAL
],
"content": ""
}Tag Definitions
| Tag | Description |
|---|---|
d | SHA-256 of the DER certificate, 64 lowercase hex |
signature | Certificate-key signature, padded RFC 4648 base64 without whitespace |
expiry | Unix seconds; MUST be greater than created_at |
cert | Exact DER certificate, padded RFC 4648 base64 without whitespace (optional) |
delegation | Authorized NIP-82 publisher, 64 lowercase hex (optional) |
revoked | Revokes the proof; value MAY contain a reason |
An active proof MUST contain exactly one d, signature, and expiry tag, and at most one cert and delegation tag.
Certificate
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 Asset 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.
When cert is present, it MUST decode to exactly one DER-encoded X.509 certificate whose SHA-256 hash equals d. Otherwise a verifier MAY obtain the certificate from an APK, keystore export, or any other source: d commits to the exact bytes, so the source need not be trusted.
An embedded certificate proves the certificate-key-to-Nostr-key relationship without downloading an APK. It does not by itself prove that the certificate signed a particular APK; that association comes from the APK or a verified NIP-82 asset.
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 | RSASSA-PKCS1-v1_5 with SHA-256 |
| ECDSA | ECDSA with SHA-256; ASN.1 DER signature |
The signing operation hashes the exact proof message once with SHA-256. Implementations MUST NOT pre-hash the message when using an API that also performs SHA-256.
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
To find the current proof, verifiers MUST query the relays in the author's relay list for kind 30509 events with the expected pubkey and d, then resolve the newest event using NIP-01 ordering (created_at, then lowest event ID). Older versions MAY have been discarded by relays.
If the current event has a revoked tag, the proof is revoked. Otherwise verifiers MUST:
- Verify the Nostr event ID and signature and enforce tag cardinality.
- Confirm
expiry > created_atand current time is beforeexpiry. - Decode
cert, if present, or obtain the certificate externally. - Compute SHA-256 of the exact DER bytes and match
d. - Extract the certificate public key and verify
signatureover the proof
message.
CI/CD Delegation
A current, active proof containing delegation authorizes that pubkey to sign the following NIP-82 events on behalf of the proof's main pubkey:
- A kind 3063 APK asset only when every
apk_certificate_hashvalue is
covered by a current, active kind 30509 proof from the same main pubkey that delegates to the event signer.
- A kind 32267 application only when it is backed by a kind 3063 APK asset
authorized for that delegate by the preceding rule and signed by that same delegate.
- A kind 30063 release only when its
atag is
32267:<release-signer-pubkey>:<app-id>, the referenced kind 32267 is authored by that same pubkey, and every linked kind 3063 asset is authored by that same pubkey and authorized for that delegate by the preceding rule. A delegate publishes a complete triple under the delegate key. It does not attach a release to the owner's 32267.
Authorization is certificate-scoped, not app-scoped: it covers any application signed by that certificate. It does not authorize kind 30509 or any other event kind.
The Nostr event signature authorizes delegation by committing to all event tags. The certificate signature binds only the certificate to the main pubkey.
Delegation Rotation
Replacing the kind 30509 event with a fresh valid proof changes or removes the delegate. Delegation is evaluated against the current proof, so rotation, expiry, or revocation invalidates the old delegate's past and future events. Assets and releases that must remain trusted MUST be republished by the main or new delegate key.
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 -out cert.der
openssl dgst -sha256 -r cert.der | cut -d' ' -f1Encode the optional self-contained cert tag:
openssl x509 -in cert.pem -outform der | openssl base64 -ASign the proof message:
CREATED_AT=$(date +%s)
# One year in seconds; avoids GNU-specific `date -d`.
EXPIRY=$((CREATED_AT + 31536000))
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 a Self-Contained Proof
Decode cert, confirm d, and extract the public key:
echo "${CERT}" | openssl base64 -d -A > cert.der
openssl dgst -sha256 -r cert.der
openssl x509 -inform der -in cert.der -pubkey -noout > pubkey.pemThen verify signature using the command below.
Verifying Against an APK
When cert is absent, extract the certificate hash from the APK:
apksigner verify --print-certs -v app.apk 2>&1 | grep -m1 'certificate SHA-256' | cut -d: -f2 | tr -d ' 'Extract its public key:
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 replacement with the same d and a revoked tag:
["d", "<apk_certificate_hash>"],
["revoked", "key-compromised"] // or: key-retired, supersededThe Nostr event signature is sufficient to authorize revocation; a revocation event MUST omit signature, expiry, cert, and delegation. Standard addressable-event replacement applies. Reactivation requires a newer active proof with a fresh certificate-key signature.
Security Considerations
- Clients MUST verify both the Nostr event signature and certificate-key
signature before trusting an active proof.
- The certificate hash MUST match
d. created_atandexpiryare bound into the proof message.- Delegation MUST be checked against the current proof at verification time.
- Clients MUST reject malformed, duplicate, unsupported, expired, revoked, or
out-of-scope claims.