{"id":"0c3eca59cce2dcccc12b7ed079473a5720100e62c009e80c260b165b54529661","pubkey":"78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d","created_at":1772115101,"kind":30817,"tags":[["d","cryptographic-identity-proofs"],["title","Cryptographic Identity Proofs"],["k","30509"]],"content":"NIP-C1\n======\n\nCryptographic Identity Proofs\n-----------------------------\n\n`draft` `optional`\n\n## Abstract\n\nThis 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.\n\n## Supported Key Types\n\n| Kind  | Type     | Description                                        |\n| ----- | -------- | -------------------------------------------------- |\n| 30509 | APK Cert | SHA-256 of the DER-encoded APK signing certificate |\n\nFuture versions may add support for additional types such as OpenPGP.\n\n## Event Format\n\nA cryptographic identity proof is published as a parameterized replaceable event:\n\n```jsonc\n{\n  \"kind\": 30509,\n  \"pubkey\": \"<nostr-pubkey-hex>\",\n  \"created_at\": <timestamp>,\n  \"tags\": [\n    [\"d\", \"<apk_certificate_hash>\"],\n    [\"signature\", \"<signature-base64>\"],\n    [\"expiry\", \"<unix-timestamp>\"]\n  ],\n  \"content\": \"\"\n}\n```\n\n### Tag Definitions\n\n| Tag         | Description                                                                          |\n| ----------- | ------------------------------------------------------------------------------------ |\n| `d`         | SHA-256 of the DER-encoded APK signing certificate, 64 lowercase hex                 |\n| `signature` | Signature in base64 (RFC 4648, with padding, no whitespace)                          |\n| `expiry`    | Expiry timestamp as Unix seconds (base-10 digits); MUST be greater than `created_at` |\n| `revoked`   | If present, this identity proof has been revoked (optional reason as value)          |\n\n## Identity Fingerprint\n\nThe identity fingerprint (`apk_certificate_hash`) is the SHA-256 hash of the full DER-encoded X.509 signing certificate, lowercase hex.\n\nThis 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.\n\nSPKIFP (SHA-256 of the SubjectPublicKeyInfo) was not used because Android identifies signers by certificate, not by bare public key.\n\n## Proof Message\n\nThe signature is computed over the following message:\n\n```\nVerifying at <created_at> until <expiry> that I control the following Nostr public key: <pubkey>\n```\n\nWhere:\n\n- `<created_at>` is the Unix timestamp (seconds), base-10 ASCII digits, and MUST equal the event's `created_at` field\n- `<expiry>` is the Unix timestamp (seconds), base-10 ASCII digits, and MUST equal the `expiry` tag\n- `<pubkey>` is the 64-character lowercase hex Nostr public key, and MUST equal the event `pubkey`\n\nThe signed message MUST be encoded as UTF-8 bytes exactly as shown (no trailing newline). Implementations MUST NOT add or remove whitespace.\n\n## Signature Algorithm\n\nVerifiers extract the public key from the certificate and determine the algorithm from the key type:\n\n| Certificate Key Type | Verification                            |\n| -------------------- | --------------------------------------- |\n| RSA                  | PKCS#1 v1.5 with SHA-256                |\n| ECDSA                | ECDSA over SHA-256, ASN.1 DER signature |\n\nSignatures are computed over the SHA-256 hash of the proof message.\n\n> **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`.\n\n## Verification Requirements\n\nVerifiers MUST:\n\n1. Compute SHA-256 of the DER-encoded certificate and confirm it matches the `d` tag\n2. Extract the public key from the certificate, determine key type, and verify signature accordingly\n3. Confirm `expiry > created_at` and current time < `expiry`\n4. 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\n\n## Example\n\n```jsonc\n{\n  \"kind\": 30509,\n  \"id\": \"b38336ac9191a55c6b07505e6ed55c7b1a405c7124260ad462911f3f17a5c9eb\",\n  \"pubkey\": \"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11\",\n  \"created_at\": 1772114325,\n  \"tags\": [\n    [\n      \"d\",\n      \"e0382ce13f09f4a4f969b95b351ede3b52f1d8946896db0bba85b9f255ae9693\"\n    ],\n    [\n      \"signature\",\n      \"MEYCIQC9TcEv8sQllSjmneoNY56EZKNEmtFNcMuiToEPd9ZCBwIhAPXblbB5LmEJSpN9Wp78Z5R2MhAmPSbr/KyMe6zi8AQR\"\n    ],\n    [\n      \"expiry\",\n      \"1803650325\"\n    ]\n  ],\n  \"content\": \"\",\n  \"sig\": \"879beaeb6228a36c071a9d8476961fbba62409cfa1fdbe36e899ce4d71eff2814334fea7cbf64df3f8a885c049bd766b45d8785fd3c87def590421c58fac5d56\"\n}\n```\n\nThe signed message for this example is:\n\n```\nVerifying at 1772114325 until 1803650325 that I control the following Nostr public key: 726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11\n```\n\n## Creating a Proof\n\nExport private key from Java keystore:\n\n```bash\nkeytool -importkeystore -srckeystore example.keystore -destkeystore example.p12 -deststoretype pkcs12\nopenssl pkcs12 -in example.p12 -nocerts -noenc -out privatekey.pem\n```\n\nExtract the certificate and compute the certificate hash (for `d` tag):\n\n```bash\nopenssl pkcs12 -in example.p12 -nokeys -out cert.pem\nopenssl x509 -in cert.pem -outform der | sha256sum | cut -d' ' -f1\n```\n\nExtract public key for signing:\n\n```bash\nopenssl x509 -in cert.pem -pubkey -noout > pubkey.pem\n```\n\nSign the proof message:\n\n```bash\nCREATED_AT=$(date +%s)\nEXPIRY=$(date -d \"+1 year\" +%s)\nPUBKEY=\"78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d\"\necho -n \"Verifying at ${CREATED_AT} until ${EXPIRY} that I control the following Nostr public key: ${PUBKEY}\" \\\n  | openssl dgst -sha256 -sign privatekey.pem | openssl base64 -A\n```\n\n## Verifying Against an APK\n\nExtract certificate hash from APK (must match `d` tag):\n\n```bash\napksigner verify --print-certs -v app.apk 2>&1 | grep -m1 'certificate SHA-256' | cut -d: -f2 | tr -d ' '\n```\n\nExtract public key for signature verification:\n\n```bash\napksigner verify --print-certs-pem app.apk 2>&1 \\\n  | sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' \\\n  | openssl x509 -pubkey -noout > pubkey.pem\n```\n\nVerify signature:\n\n```bash\necho -n \"Verifying at ${CREATED_AT} until ${EXPIRY} that I control the following Nostr public key: ${PUBKEY}\" \\\n  | openssl dgst -sha256 -verify pubkey.pem -signature <(echo \"${SIGNATURE}\" | base64 -d)\n```\n\n## Revocation\n\nTo revoke, publish a new event with the `revoked` tag (reason optional but recommended):\n\n```jsonc\n[\"d\", \"<apk_certificate_hash>\"],\n[\"revoked\", \"key-compromised\"]  // or: key-retired, superseded\n```\n\nStandard replaceable-event semantics apply, except: **`key-compromised` is permanent**. Relays MUST reject updates after a `key-compromised` revocation; clients MUST ignore them.\n\n## Security Considerations\n\n1. **Signature verification required** — Clients MUST verify signatures before trusting identity claims.\n2. **Certificate binding** — The certificate hash MUST match the `d` tag.\n3. **Replay prevention** — Both `created_at` and `expiry` are bound into the signed message.\n4. **Revocation checking** — Clients MUST check all relays in the user's relay list for revocation before trusting a proof.\n","sig":"c5b5ff5b85502fe26663aa6f0cb2ba1002ea81b35d1b4d0b222c663ddc07432f19b1f45bfa044ebd1ed9a037efd38f26ff4672c047232bd435452ff95499640b"}