{"openapi":"3.1.0","info":{"title":"ecdsa.com Signature API","version":"1.0.0","description":"Free public REST API for ECDSA signature verification, DER ⇄ raw signature conversion, JWT (ES256/ES384/ES512) decoding and verification, X.509 certificate decoding and TLS signature-health grades.\n\nNo API keys. Rate limit: 60 requests/minute per IP (the health endpoint additionally 10/minute), enforced per server instance. CORS is open (Access-Control-Allow-Origin: *).\n\nNever send private keys. Requests containing a PEM PRIVATE KEY block or a JWK with a \"d\" member are rejected with code private_key_rejected. For anything secret, use the in-browser tools at https://ecdsa.com — they run entirely client-side.","contact":{"url":"https://ecdsa.com/developers"}},"servers":[{"url":"https://ecdsa.com/api/v1"}],"paths":{"/":{"get":{"operationId":"getApiIndex","summary":"API self-description","description":"Version, endpoint list, rate limits and documentation links.","responses":{"200":{"description":"Self-description of the API.","headers":{"X-RateLimit-Limit":{"description":"Requests allowed per minute per IP for this endpoint.","schema":{"type":"string"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object"}}}},"429":{"description":"Rate limit exceeded. Carries a Retry-After header (seconds).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/verify":{"post":{"operationId":"verifySignature","summary":"Verify an ECDSA signature","description":"Verifies a signature over a message (or a precomputed digest) against a public key. Curves: P-256, P-384, P-521, secp256k1. Signature formats auto-detected: ASN.1 DER or raw r‖s, in hex, base64 or base64url. Public key formats: PEM SPKI (\"PUBLIC KEY\"), EC JWK, or a bare SEC1 point in hex/base64. A signature that simply does not verify returns valid: false with HTTP 200; only unparseable input yields 400.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyRequest"},"example":{"publicKey":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4+8TCP3Hop/Acb8L7reksO9flyt1\nsUhvEqUs7a45RVDRDtAIZdTnOJcAhTCQvdzUBO4KUyxSRcZm6ro+2RGfSQ==\n-----END PUBLIC KEY-----","message":"The quick brown fox jumps over the lazy dog","signature":"3045022100df542bb71e0e3019086f1b1ba51f1aa0ec5e6afca05c3f5a7f4f02f5b7c39d24022030c59f1ac6d8c6893fb2a5ac8dfd9388d31ebd3382510dcfb7f28f9182eb1131"}}}},"responses":{"200":{"description":"Verification outcome.","headers":{"X-RateLimit-Limit":{"description":"Requests allowed per minute per IP for this endpoint.","schema":{"type":"string"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyResponse"}}}},"400":{"description":"Invalid input. Also returned with code private_key_rejected when the request contains private-key material (PEM PRIVATE KEY block or a JWK with \"d\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Request body larger than 100KB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded. Carries a Retry-After header (seconds).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/convert":{"post":{"operationId":"convertSignature","summary":"Convert a signature between DER and raw r‖s","description":"Decodes a signature (DER or raw, auto-detected; hex/base64/base64url) and returns both encodings plus r, s and the high-S flag. normalize: \"low-s\" replaces a high-S s with n − s. Warnings list DER canonicality deviations (BER lengths, missing 0x00 pads, trailing bytes) and curve-ambiguity notes.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConvertRequest"},"example":{"signature":"3045022100df542bb71e0e3019086f1b1ba51f1aa0ec5e6afca05c3f5a7f4f02f5b7c39d24022030c59f1ac6d8c6893fb2a5ac8dfd9388d31ebd3382510dcfb7f28f9182eb1131"}}}},"responses":{"200":{"description":"Both encodings of the signature.","headers":{"X-RateLimit-Limit":{"description":"Requests allowed per minute per IP for this endpoint.","schema":{"type":"string"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConvertResponse"}}}},"400":{"description":"Invalid input. Also returned with code private_key_rejected when the request contains private-key material (PEM PRIVATE KEY block or a JWK with \"d\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Request body larger than 100KB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded. Carries a Retry-After header (seconds).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/jwt/decode":{"post":{"operationId":"decodeJwt","summary":"Decode a JWT (no signature check)","description":"Decodes a compact JWS token: header, payload, enriched exp/iat/nbf time claims and a classification of the algorithm. The signature is NOT verified — the response carries verified: false explicitly. Use /jwt/verify to check ES* signatures.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JwtDecodeRequest"},"example":{"token":"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImNvcHBlci1leGFtcGxlLTEifQ.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlLmNvbSIsInN1YiI6InVzZXJfNDIiLCJhdWQiOiJlY2RzYSIsImlhdCI6MTc4NzE0MDgwMCwiZXhwIjo0MDcwOTA4ODAwfQ.fQpTQxkahGRlZp-U71-xNk68mpZH23dhn9QoysMLmZfBx_e-vd9sCgh7sJ4eXu0-WbQSNqzmVPyUKErCJx4DHw"}}}},"responses":{"200":{"description":"Decoded token.","headers":{"X-RateLimit-Limit":{"description":"Requests allowed per minute per IP for this endpoint.","schema":{"type":"string"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JwtDecodeResponse"}}}},"400":{"description":"Invalid input. Also returned with code private_key_rejected when the request contains private-key material (PEM PRIVATE KEY block or a JWK with \"d\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Request body larger than 100KB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded. Carries a Retry-After header (seconds).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/jwt/verify":{"post":{"operationId":"verifyJwt","summary":"Verify an ES256/ES384/ES512 JWT signature","description":"Verifies the token's ECDSA signature against a public key (PEM SPKI, EC JWK, or bare SEC1 point). Non-ECDSA algorithms (HS*, RS*, PS*, EdDSA, ES256K, none) return 400 with code unsupported_algorithm. A signature that fails verification returns valid: false with HTTP 200. Time claims are noted in explanations but do not affect valid.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JwtVerifyRequest"},"example":{"token":"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImNvcHBlci1leGFtcGxlLTEifQ.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlLmNvbSIsInN1YiI6InVzZXJfNDIiLCJhdWQiOiJlY2RzYSIsImlhdCI6MTc4NzE0MDgwMCwiZXhwIjo0MDcwOTA4ODAwfQ.fQpTQxkahGRlZp-U71-xNk68mpZH23dhn9QoysMLmZfBx_e-vd9sCgh7sJ4eXu0-WbQSNqzmVPyUKErCJx4DHw","publicKey":"{\n  \"kty\": \"EC\",\n  \"crv\": \"P-256\",\n  \"x\": \"A4KIXkvTVuOzkR5Mzgsy8n_VSSIlephF1ViMxDYTUx4\",\n  \"y\": \"Py8AMAM_6yWsOTbv8eHCvKDfObpZmTHy5P4Jz30iMuU\",\n  \"kid\": \"copper-example-1\"\n}"}}}},"responses":{"200":{"description":"Verification outcome.","headers":{"X-RateLimit-Limit":{"description":"Requests allowed per minute per IP for this endpoint.","schema":{"type":"string"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JwtVerifyResponse"}}}},"400":{"description":"Invalid input. Also returned with code private_key_rejected when the request contains private-key material (PEM PRIVATE KEY block or a JWK with \"d\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Request body larger than 100KB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded. Carries a Retry-After header (seconds).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/cert/decode":{"post":{"operationId":"decodeCertificate","summary":"Decode an X.509 certificate or chain","description":"Accepts PEM (one or more CERTIFICATE blocks — a whole chain works) or a single DER certificate as base64/hex. Returns a structured summary per certificate — subject, issuer, validity, key and signature algorithms, SAN, key usage, fingerprint — plus assessments (weak hashes, expiry, CA/Browser Forum lifetime limits, post-quantum notes).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertDecodeRequest"},"example":{"certificate":"-----BEGIN CERTIFICATE-----\n…\n-----END CERTIFICATE-----"}}}},"responses":{"200":{"description":"Decoded certificates with assessments.","headers":{"X-RateLimit-Limit":{"description":"Requests allowed per minute per IP for this endpoint.","schema":{"type":"string"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertDecodeResponse"}}}},"400":{"description":"Invalid input. Also returned with code private_key_rejected when the request contains private-key material (PEM PRIVATE KEY block or a JWK with \"d\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"Request body larger than 100KB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded. Carries a Retry-After header (seconds).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/health/{domain}":{"get":{"operationId":"getDomainHealth","summary":"TLS signature-health grade for a domain","description":"Connects to the domain on port 443, examines the served certificate chain, key and signature algorithms, protocol versions and lifetimes, and returns a letter grade with findings. Results are cached for 1 hour. Only public DNS hostnames are accepted — IP addresses, single-label names and private/reserved addresses are refused. Extra rate limit: 10 requests/minute per IP. A fresh scan can take several seconds.","parameters":[{"name":"domain","in":"path","required":true,"description":"Public DNS hostname, e.g. \"github.com\".","schema":{"type":"string"},"example":"github.com"}],"responses":{"200":{"description":"Health grade and findings.","headers":{"X-RateLimit-Limit":{"description":"Requests allowed per minute per IP for this endpoint.","schema":{"type":"string"}},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"string"}},"X-RateLimit-Reset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}},"400":{"description":"Invalid domain, DNS failure, or a private/reserved address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded. Carries a Retry-After header (seconds).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The domain was unreachable or the TLS handshake failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"The scan timed out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/openapi.json":{"get":{"operationId":"getOpenApi","summary":"This specification","responses":{"200":{"description":"OpenAPI 3.1 document.","content":{"application/json":{"schema":{"type":"object"}}}},"429":{"description":"Rate limit exceeded. Carries a Retry-After header (seconds).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Machine-readable error code: invalid_json, payload_too_large, missing_field, invalid_input, private_key_rejected, unsupported_algorithm, invalid_domain, scan_dns_error, scan_private_address, scan_timeout, scan_unreachable, scan_tls_error, rate_limited, internal_error."},"message":{"type":"string","description":"Human-readable explanation."}}}}},"VerifyRequest":{"type":"object","required":["publicKey","message","signature"],"properties":{"publicKey":{"type":"string","description":"PEM \"PUBLIC KEY\" (SPKI), EC JWK JSON, or a bare SEC1 point in hex/base64."},"message":{"type":"string","description":"The signed message (or its digest — see messageIsDigest)."},"signature":{"type":"string","description":"DER or raw r‖s signature in hex, base64 or base64url (auto-detected)."},"hash":{"type":"string","enum":["SHA-256","SHA-384","SHA-512"],"default":"SHA-256","description":"Digest applied to the message (ignored when messageIsDigest is true)."},"curve":{"type":"string","enum":["auto","P-256","P-384","P-521","secp256k1"],"default":"auto","description":"Force a curve, or auto to infer it from the key."},"messageEncoding":{"type":"string","enum":["utf8","hex","base64"],"default":"utf8","description":"How the message string encodes bytes."},"messageIsDigest":{"type":"boolean","default":false,"description":"When true, message bytes are used as the digest directly (no hashing)."}}},"VerifyResponse":{"type":"object","required":["valid","curve","signatureFormat","details"],"properties":{"valid":{"type":"boolean"},"curve":{"type":["string","null"],"description":"Curve the signature verified on (or the single candidate when it did not)."},"curvesTried":{"type":"array","items":{"type":"string"}},"keySource":{"type":"string","enum":["pem-spki","jwk","raw-point"]},"signatureFormat":{"type":"string","enum":["der","raw"]},"details":{"type":"object","required":["r","s","highS"],"properties":{"r":{"type":"string","description":"r as hex."},"s":{"type":"string","description":"s as hex."},"highS":{"type":"boolean","description":"s > n/2 (non-canonical for Bitcoin & co)."}}},"notes":{"type":"array","items":{"type":"string"},"description":"Per-curve diagnostics."}}},"ConvertRequest":{"type":"object","required":["signature"],"properties":{"signature":{"type":"string","description":"DER or raw r‖s signature in hex, base64 or base64url (auto-detected)."},"curve":{"type":"string","enum":["P-256","P-384","P-521","secp256k1"],"description":"Curve for sizing and the high-S check; inferred from the length when omitted."},"normalize":{"type":"string","enum":["low-s"],"description":"Replace a high-S s with its low-S equivalent n − s."}}},"ConvertResponse":{"type":"object","required":["inputFormat","curve","der","raw","r","s","highS","warnings"],"properties":{"inputFormat":{"type":"string","enum":["der","raw"]},"curve":{"type":"string"},"der":{"type":"object","required":["hex","base64"],"properties":{"hex":{"type":"string"},"base64":{"type":"string"}}},"raw":{"type":"object","required":["hex","base64","base64url"],"properties":{"hex":{"type":"string"},"base64":{"type":"string"},"base64url":{"type":"string"}}},"r":{"type":"string","description":"r as hex, zero-padded to the curve's component size."},"s":{"type":"string","description":"s as hex (after normalization when requested)."},"highS":{"type":"boolean","description":"Whether the returned s is high-S."},"warnings":{"type":"array","items":{"type":"string"}}}},"JwtDecodeRequest":{"type":"object","required":["token"],"properties":{"token":{"type":"string","description":"Compact JWS token. A leading \"Bearer \" is stripped."}}},"JwtDecodeResponse":{"type":"object","required":["header","payload","timeClaims","algInfo","verified"],"properties":{"header":{"type":"object"},"payload":{"type":"object"},"signaturePresent":{"type":"boolean"},"signatureBytes":{"type":"integer"},"timeClaims":{"type":"object","properties":{"exp":{"$ref":"#/components/schemas/TimeClaim"},"iat":{"$ref":"#/components/schemas/TimeClaim"},"nbf":{"$ref":"#/components/schemas/TimeClaim"},"status":{"type":["object","null"],"properties":{"kind":{"type":"string","enum":["expired","not-yet-valid","valid"]},"detail":{"type":"string"}}}}},"algInfo":{"type":"object","description":"Classification of the header's alg value.","properties":{"alg":{"type":["string","null"]},"kind":{"type":"string"},"curve":{"type":"string"},"hash":{"type":"string"},"verifiableHere":{"type":"boolean"},"summary":{"type":"string"}}},"verified":{"type":"boolean","const":false,"description":"Always false — this endpoint never verifies."},"note":{"type":"string"}}},"TimeClaim":{"type":["object","null"],"description":"null when the claim is absent; {error} when malformed.","properties":{"value":{"type":"number","description":"Seconds since the epoch."},"utc":{"type":"string"},"relative":{"type":"string","description":"e.g. \"in 72 years\", \"3 minutes ago\"."},"error":{"type":"string"}}},"JwtVerifyRequest":{"type":"object","required":["token","publicKey"],"properties":{"token":{"type":"string","description":"Compact JWS token with alg ES256, ES384 or ES512."},"publicKey":{"type":"string","description":"PEM \"PUBLIC KEY\" (SPKI), EC JWK JSON, or a bare SEC1 point in hex/base64."}}},"JwtVerifyResponse":{"type":"object","required":["valid","alg","explanations"],"properties":{"valid":{"type":"boolean"},"alg":{"type":"string","enum":["ES256","ES384","ES512"]},"explanations":{"type":"array","items":{"type":"string"}}}},"CertDecodeRequest":{"type":"object","required":["certificate"],"properties":{"certificate":{"type":"string","description":"PEM with one or more CERTIFICATE blocks (chains welcome), or one DER certificate as base64/hex."}}},"CertDecodeResponse":{"type":"object","required":["certificates","notes"],"properties":{"certificates":{"type":"array","items":{"$ref":"#/components/schemas/CertificateSummary"}},"notes":{"type":"array","items":{"type":"string"},"description":"Per-block problems and skipped-block notes."}}},"CertificateSummary":{"type":"object","properties":{"subject":{"type":"string"},"issuer":{"type":"string"},"selfSigned":{"type":"boolean"},"serialNumber":{"type":"string","description":"Colon-separated hex."},"version":{"type":["integer","null"]},"notBefore":{"type":"string","format":"date-time"},"notAfter":{"type":"string","format":"date-time"},"validityStatus":{"type":"string","enum":["valid","expiring","expired","not-yet-valid"]},"daysRemaining":{"type":"integer"},"lifetimeDays":{"type":"integer"},"publicKeyLabel":{"type":"string"},"signatureLabel":{"type":"string"},"san":{"type":"array","items":{"type":"string"}},"keyUsage":{"type":"array","items":{"type":"string"}},"extendedKeyUsage":{"type":"array","items":{"type":"string"}},"basicConstraints":{"type":["object","null"],"properties":{"ca":{"type":"boolean"},"pathLength":{"type":"integer"}}},"subjectKeyId":{"type":["string","null"]},"authorityKeyId":{"type":["string","null"]},"fingerprintSha256":{"type":"string"},"assessments":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["ok","err","warn","info"]},"title":{"type":"string"},"body":{"type":"string"}}}}}},"HealthResponse":{"type":"object","required":["domain","grade","findings","scannedAt","cached"],"properties":{"domain":{"type":"string"},"grade":{"type":"object","required":["letter","score"],"properties":{"letter":{"type":"string","enum":["A+","A","B","C","D","F"]},"score":{"type":"integer","minimum":0,"maximum":100}}},"findings":{"type":"array","items":{"type":"object","required":["id","severity","title","detail"],"properties":{"id":{"type":"string"},"severity":{"type":"string","enum":["critical","warn","info","ok"]},"title":{"type":"string"},"detail":{"type":"string"}}}},"scannedAt":{"type":"string","format":"date-time"},"cached":{"type":"boolean","description":"Best-effort: true when the result came from the 1-hour cache."},"cacheTtlSeconds":{"type":"integer","const":3600},"reportUrl":{"type":"string","description":"Human-readable report for the same domain."}}}}}}