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

Cryptographic Identity Proofs

Published Oct 7, 2026
kind 30509 · APK Cert

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

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>"],
    ["cert", "<certificate-der-base64>"],       // OPTIONAL
    ["delegation", "<delegate-pubkey-hex>"]     // OPTIONAL
  ],
  "content": ""
}

Tag Definitions

TagDescription
dSHA-256 of the DER certificate, 64 lowercase hex
signatureCertificate-key signature, padded RFC 4648 base64 without whitespace
expiryUnix seconds; MUST be greater than created_at
certExact DER certificate, padded RFC 4648 base64 without whitespace (optional)
delegationAuthorized NIP-82 publisher, 64 lowercase hex (optional)
revokedRevokes 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'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
RSARSASSA-PKCS1-v1_5 with SHA-256
ECDSAECDSA 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 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

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:

  1. Verify the Nostr event ID and signature and enforce tag cardinality.
  2. Confirm expiry > created_at and current time is before expiry.
  3. Decode cert, if present, or obtain the certificate externally.
  4. Compute SHA-256 of the exact DER bytes and match d.
  5. Extract the certificate public key and verify signature over 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_hash value 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 a tag 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: 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 -out cert.der
openssl dgst -sha256 -r cert.der | cut -d' ' -f1

Encode the optional self-contained cert tag:

openssl x509 -in cert.pem -outform der | openssl base64 -A

Sign 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 -A

Verifying 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.pem

Then 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.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 replacement with the same d and a revoked tag:

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

The 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

  1. Clients MUST verify both the Nostr event signature and certificate-key

signature before trusting an active proof.

  1. The certificate hash MUST match d.
  2. created_at and expiry are bound into the proof message.
  3. Delegation MUST be checked against the current proof at verification time.
  4. Clients MUST reject malformed, duplicate, unsupported, expired, revoked, or

out-of-scope claims.