{"id":13032,"date":"2026-08-28T13:25:05","date_gmt":"2026-08-28T11:25:05","guid":{"rendered":"https:\/\/www.lukaswojcik.com\/blog\/toolbox\/web-bot-auth-signature-inspector\/"},"modified":"2026-08-28T13:25:05","modified_gmt":"2026-08-28T11:25:05","slug":"web-bot-auth-signature-inspector","status":"publish","type":"page","link":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/web-bot-auth-signature-inspector\/","title":{"rendered":"Web Bot Auth Signature Inspector and Crawler Range Checker"},"content":{"rendered":"<div class=\"gtm-analyser-container\" style=\"background: var(--bg-panel, #1e1e24); padding: 25px; border-radius: 8px; border: 1px solid var(--border, #2a2a35);\">\n<p style=\"color: var(--text-secondary, #a0a0b0); margin-bottom: 20px;\">An agentic browser looks like a person in a report because it is a genuine browser engine. Two mechanisms answer the question of who is actually asking, and they cost very different things to run. One is a signature the agent brings along; this rebuilds the exact bytes that were signed, checks the key thumbprint against the keyid and verifies the signature. The other is a list of addresses the operator publishes; this counts what such a list covers and tests whether an address falls inside it.<\/p>\n<div style=\"margin-bottom: 14px;\">\n        <label for=\"wb-header\" style=\"color: var(--text-secondary, #a0a0b0); display: block; font-size: 0.85rem; margin-bottom: 5px;\">Request headers: Signature-Agent, Signature-Input, Signature<\/label><br \/>\n        <textarea id=\"wb-header\" rows=\"7\" class=\"form-control\" style=\"width: 100%; padding: 10px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px; box-sizing: border-box; font-family: monospace; font-size: 0.82rem;\"><\/textarea>\n    <\/div>\n<div style=\"display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; margin-bottom: 14px;\">\n<div>\n            <label for=\"wb-authority\" style=\"color: var(--text-secondary, #a0a0b0); display: block; font-size: 0.85rem; margin-bottom: 5px;\">Authority the request went to<\/label><br \/>\n            <input id=\"wb-authority\" type=\"text\" value=\"www.example.com\" class=\"form-control\" style=\"width: 100%; padding: 10px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px; box-sizing: border-box; font-family: monospace;\">\n        <\/div>\n<div>\n            <label for=\"wb-adresse\" style=\"color: var(--text-secondary, #a0a0b0); display: block; font-size: 0.85rem; margin-bottom: 5px;\">Address to test<\/label><br \/>\n            <input id=\"wb-adresse\" type=\"text\" value=\"192.0.2.45\" class=\"form-control\" style=\"width: 100%; padding: 10px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px; box-sizing: border-box; font-family: monospace;\">\n        <\/div>\n<\/p><\/div>\n<div style=\"display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; margin-bottom: 14px;\">\n<div>\n            <label for=\"wb-jwk\" style=\"color: var(--text-secondary, #a0a0b0); display: block; font-size: 0.85rem; margin-bottom: 5px;\">Public key as JWK, or a JWKS<\/label><br \/>\n            <textarea id=\"wb-jwk\" rows=\"5\" class=\"form-control\" style=\"width: 100%; padding: 10px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px; box-sizing: border-box; font-family: monospace; font-size: 0.82rem;\"><\/textarea>\n        <\/div>\n<div>\n            <label for=\"wb-praefixe\" style=\"color: var(--text-secondary, #a0a0b0); display: block; font-size: 0.85rem; margin-bottom: 5px;\">Published prefixes, one per line<\/label><br \/>\n            <textarea id=\"wb-praefixe\" rows=\"5\" class=\"form-control\" style=\"width: 100%; padding: 10px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px; box-sizing: border-box; font-family: monospace; font-size: 0.82rem;\"><\/textarea>\n        <\/div>\n<\/p><\/div>\n<p style=\"color: var(--text-secondary, #a0a0b0); font-size: 0.82rem; margin-bottom: 16px;\">Everything runs in the browser; no header, key or address is transmitted or stored. The example carries fixed timestamps so its signature stays verifiable &#8211; which is why it also reports as expired. That split is the useful part: cryptographically sound and currently valid are two separate findings.<\/p>\n<p>    <button id=\"wb-btn\" class=\"button\" style=\"background: var(--accent, #7ee787); color: var(--on-accent, #0b1114); border: none; padding: 12px 24px; border-radius: 6px; font-weight: 700; cursor: pointer;\">Inspect<\/button><\/p>\n<div id=\"wb-ausgabe\" style=\"margin-top: 22px;\"><\/div>\n<\/div>\n<p><script>\n(function () {\n    'use strict';<\/p>\n<p>    const T = {\"einleitung\":\"An agentic browser looks like a person in a report because it is a genuine browser engine. Two mechanisms answer the question of who is actually asking, and they cost very different things to run. One is a signature the agent brings along; this rebuilds the exact bytes that were signed, checks the key thumbprint against the keyid and verifies the signature. The other is a list of addresses the operator publishes; this counts what such a list covers and tests whether an address falls inside it.\",\"label_header\":\"Request headers: Signature-Agent, Signature-Input, Signature\",\"label_authority\":\"Authority the request went to\",\"label_adresse\":\"Address to test\",\"label_jwk\":\"Public key as JWK, or a JWKS\",\"label_praefixe\":\"Published prefixes, one per line\",\"datenschutz\":\"Everything runs in the browser; no header, key or address is transmitted or stored. The example carries fixed timestamps so its signature stays verifiable - which is why it also reports as expired. That split is the useful part: cryptographically sound and currently valid are two separate findings.\",\"knopf\":\"Inspect\",\"beispiel_header\":\"Signature-Agent: \\u0022https:\\\/\\\/signer.example.com\\u0022\\nSignature-Input: sig1=(\\u0022@authority\\u0022 \\u0022signature-agent\\u0022);created=1787000000;expires=1787003600;keyid=\\u0022ulMr0bd_FrTSdSiYtpOpPZ0UrGNldXBNi9wDof-iKpc\\u0022;alg=\\u0022ed25519\\u0022;tag=\\u0022web-bot-auth\\u0022;nonce=\\u0022bHctYmVpc3BpZWwtbm9uY2U\\u0022\\nSignature: sig1=:HPpAewTOKvc4nY\\\/dONz6R848m3D8Zf8poPQMu6bNGByXPzztOSHT7tqjx5\\\/THiWk7r\\\/u0lKzn7wPvdPsK3NaAg==:\",\"beispiel_jwk\":\"{\\u0022crv\\u0022:\\u0022Ed25519\\u0022,\\u0022kty\\u0022:\\u0022OKP\\u0022,\\u0022x\\u0022:\\u0022AYQxRK7FjZW-qXcFpYAer7j-nC9VPsKM-5oCT7Vb3kw\\u0022}\",\"beispiel_praefixe\":\"192.0.2.0\\\/24\\n198.51.100.0\\\/24\\n203.0.113.0\\\/25\\n203.0.113.128\\\/25\\n2001:db8::\\\/32\",\"h_agent\":\"Who claims to be asking\",\"zeile_agent\":\"Signature-Agent host\",\"zeile_verzeichnis\":\"Where its key set should sit\",\"wert_keiner\":\"none\",\"fehler_kein_signature_input\":\"No Signature-Input header found. Without it there is nothing to rebuild - the covered components and the parameters live in that header alone.\",\"fehler_kein_signature\":\"No Signature header found. The base can still be rebuilt, but nothing can be verified against it.\",\"fehler_signature_input_unlesbar\":\"The Signature-Input header does not parse as a structured field dictionary.\",\"fehler_signature_unlesbar\":\"The Signature header does not parse as a structured field dictionary.\",\"h_signatur\":\"Signature\",\"zeile_komponenten\":\"Covered components\",\"zeile_keyid\":\"keyid\",\"zeile_alg\":\"alg\",\"zeile_tag\":\"tag\",\"zeile_lebensdauer\":\"Validity window\",\"zeile_signaturlaenge\":\"Signature length\",\"einheit_sekunden\":\"seconds\",\"einheit_bytes\":\"bytes\",\"h_basis\":\"Signature base, rebuilt\",\"basis_fehlend\":\"No value could be resolved for:\",\"pruefung_laeuft\":\"Checking the key...\",\"zeile_abdruck\":\"Key thumbprint, RFC 7638\",\"zeile_abdruck_urteil\":\"Against the keyid\",\"abdruck_kein_jwk\":\"no key supplied\",\"abdruck_kein_okp\":\"not an OKP key with a crv and an x\",\"abdruck_kein_subtle\":\"this browser offers no WebCrypto\",\"abdruck_passt\":\"matches - this key is the one the signature names\",\"abdruck_passt_nicht\":\"does not match the keyid: a different key\",\"abdruck_ohne_keyid\":\"the signature names no keyid to compare against\",\"zeile_signaturpruefung\":\"Signature check\",\"signatur_gueltig\":\"valid for this base and this key\",\"signatur_ungueltig\":\"does not verify\",\"pruefung_kein_subtle\":\"this browser offers no WebCrypto\",\"pruefung_kein_ed25519\":\"this browser has no Ed25519 in WebCrypto; the thumbprint above was still checked\",\"pruefung_unvollstaendig\":\"a key or a signature is missing\",\"h_befunde\":\"Findings\",\"befund_kein_tag\":\"The tag is not web-bot-auth. A signature without that tag may be an HTTP message signature for something else entirely.\",\"befund_keyid_fehlt\":\"No keyid. Without it a verifier cannot tell which key out of a set to fetch.\",\"befund_alg_fremd\":\"The alg names something other than ed25519. Web Bot Auth is specified around Ed25519.\",\"befund_created_fehlt\":\"No created parameter, so the age of the signature cannot be established.\",\"befund_kein_ablauf\":\"No expires parameter. A signature without an end is replayable for as long as the key lives.\",\"befund_abgelaufen\":\"The validity window has passed. The signature may still verify cryptographically - that is a separate question from whether it should be accepted now.\",\"befund_noch_nicht_gueltig\":\"The created timestamp lies in the future, which usually means a clock is off rather than an attack.\",\"befund_fenster_weit\":\"The validity window is longer than an hour. The wider it is, the longer a captured signature stays reusable.\",\"befund_authority_ungedeckt\":\"@authority is not covered. The same signature would then be valid against a different host - which is the one thing the covered set has to prevent.\",\"befund_agent_ungedeckt\":\"A Signature-Agent header is present but not covered by the signature. It can be swapped without breaking anything, so the claimed origin of the key is not protected.\",\"befund_agent_fehlt\":\"No Signature-Agent header. The key set has to be located some other way.\",\"befund_nonce_fehlt\":\"No nonce. Replay within the validity window then has to be caught elsewhere.\",\"befund_laenge_ungewoehnlich\":\"The signature is not 64 bytes. An Ed25519 signature is always exactly that.\",\"befund_signatur_fehlt\":\"No signature bytes for this label were found in the Signature header.\",\"befund_basis_unvollstaendig\":\"At least one covered component could not be resolved from the input, so the base above is incomplete and a check against it would fail for that reason alone.\",\"h_netz\":\"The address proof\",\"zeile_praefixe\":\"Prefixes read\",\"zeile_v4adressen\":\"IPv4 addresses covered\",\"zeile_v6\":\"IPv6 addresses covered\",\"zeile_ungueltig\":\"Not readable as a prefix\",\"zeile_treffer\":\"Address inside the list\",\"treffer_ja\":\"yes, in\",\"treffer_nein\":\"no\",\"treffer_unlesbar\":\"the address does not parse\",\"h_vergleich\":\"What separates the two proofs\",\"vergleich_pflege\":\"An address list has to be refetched whenever the operator changes it, and an operator can multiply its pool overnight. A key proof needs no such upkeep: the key set is fetched once and cached against the keyid.\",\"vergleich_beweis\":\"An address proves that a request came from a network the operator controls. A signature proves that whoever holds the private key produced this exact request. The second is a statement about the request, the first only about the path it took.\",\"vergleich_absicht\":\"Neither proves intent. A verified signature says who is asking, not what they will do with the answer - and that is where the mechanism stops.\",\"tausender\":\",\"};<\/p>\n<p>    \/\/ =========================================================================\n    \/\/ Strukturierte Felder, RFC 8941\n    \/\/ =========================================================================\n    \/\/\n    \/\/ Gebraucht wird nur der Ausschnitt, den RFC 9421 benutzt: ein Woerterbuch,\n    \/\/ dessen Werte innere Listen aus Zeichenketten oder einzelne Bytefolgen\n    \/\/ sind, jeweils mit Parametern. Ein vollstaendiger Parser waere hier\n    \/\/ Ballast; was fehlt, wird als Fehler gemeldet statt stillschweigend\n    \/\/ falsch gedeutet.<\/p>\n<p>    function Leser(text) {\n        this.s = String(text == null ? '' : text);\n        this.i = 0;\n    }\n    Leser.prototype.rest = function () { return this.s.slice(this.i); };\n    Leser.prototype.ende = function () { return this.i >= this.s.length; };\n    Leser.prototype.zeichen = function () { return this.s.charAt(this.i); };\n    Leser.prototype.ows = function () {\n        while (!this.ende() && (this.zeichen() === ' ' || this.zeichen() === '\\t')) { this.i++; }\n    };<\/p>\n<p>    function istZiffer(c) { return c >= '0' && c <= '9'; }\n    function istBuchstabe(c) {\n        return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');\n    }\n\n    \/** Zeichenkette mit Anfuehrungszeichen, Fluchtzeichen nur vor \" und Rueckstrich. *\/\n    function leseZeichenkette(l) {\n        if (l.zeichen() !== '\"') { throw new Error('erwartet: Zeichenkette'); }\n        l.i++;\n        var raus = '';\n        while (!l.ende()) {\n            var c = l.s.charAt(l.i++);\n            if (c === '\\\\') {\n                if (l.ende()) { throw new Error('Fluchtzeichen am Ende'); }\n                var n = l.s.charAt(l.i++);\n                if (n !== '\"' &#038;&#038; n !== '\\\\') { throw new Error('unerlaubtes Fluchtzeichen'); }\n                raus += n;\n            } else if (c === '\"') {\n                return raus;\n            } else {\n                raus += c;\n            }\n        }\n        throw new Error('Zeichenkette nicht geschlossen');\n    }\n\n    function leseToken(l) {\n        var start = l.i;\n        while (!l.ende() &#038;&#038; \/[A-Za-z0-9:\\\/!#$%&#038;'*+\\-.^_`|~]\/.test(l.zeichen())) { l.i++; }\n        if (l.i === start) { throw new Error('erwartet: Token'); }\n        return l.s.slice(start, l.i);\n    }\n\n    function leseZahl(l) {\n        var start = l.i;\n        if (l.zeichen() === '-') { l.i++; }\n        while (!l.ende() &#038;&#038; (istZiffer(l.zeichen()) || l.zeichen() === '.')) { l.i++; }\n        var roh = l.s.slice(start, l.i);\n        if (!\/^-?\\d+(\\.\\d+)?$\/.test(roh)) { throw new Error('erwartet: Zahl'); }\n        return Number(roh);\n    }\n\n    function leseBytefolge(l) {\n        if (l.zeichen() !== ':') { throw new Error('erwartet: Bytefolge'); }\n        l.i++;\n        var start = l.i;\n        while (!l.ende() &#038;&#038; l.zeichen() !== ':') { l.i++; }\n        if (l.ende()) { throw new Error('Bytefolge nicht geschlossen'); }\n        var roh = l.s.slice(start, l.i);\n        l.i++;\n        return { typ: 'bytes', wert: roh };\n    }\n\n    function leseWert(l) {\n        var c = l.zeichen();\n        if (c === '\"') { return { typ: 'text', wert: leseZeichenkette(l) }; }\n        if (c === ':') { return leseBytefolge(l); }\n        if (c === '?') {\n            l.i++;\n            var b = l.s.charAt(l.i++);\n            if (b !== '0' &#038;&#038; b !== '1') { throw new Error('erwartet: Wahrheitswert'); }\n            return { typ: 'bool', wert: b === '1' };\n        }\n        if (istZiffer(c) || c === '-') { return { typ: 'zahl', wert: leseZahl(l) }; }\n        return { typ: 'token', wert: leseToken(l) };\n    }\n\n    \/** Parameter: ;name=wert oder ;name (dann true). Reihenfolge bleibt erhalten. *\/\n    function leseParameter(l) {\n        var liste = [];\n        while (!l.ende() &#038;&#038; l.zeichen() === ';') {\n            l.i++;\n            l.ows();\n            var name = leseSchluessel(l);\n            var wert = { typ: 'bool', wert: true };\n            if (!l.ende() &#038;&#038; l.zeichen() === '=') {\n                l.i++;\n                wert = leseWert(l);\n            }\n            liste.push({ name: name, wert: wert });\n        }\n        return liste;\n    }\n\n    function leseSchluessel(l) {\n        var start = l.i;\n        while (!l.ende() &#038;&#038; \/[a-z0-9_\\-.*]\/.test(l.zeichen())) { l.i++; }\n        if (l.i === start) { throw new Error('erwartet: Schluessel'); }\n        return l.s.slice(start, l.i);\n    }\n\n    function leseInnereListe(l) {\n        if (l.zeichen() !== '(') { throw new Error('erwartet: innere Liste'); }\n        l.i++;\n        var eintraege = [];\n        for (;;) {\n            l.ows();\n            if (l.ende()) { throw new Error('innere Liste nicht geschlossen'); }\n            if (l.zeichen() === ')') { l.i++; break; }\n            var w = leseWert(l);\n            var p = leseParameter(l);\n            eintraege.push({ wert: w, parameter: p });\n        }\n        return eintraege;\n    }\n\n    \/** Woerterbuch: schluessel=wert, durch Komma getrennt. *\/\n    function leseWoerterbuch(text) {\n        var l = new Leser(text);\n        var raus = [];\n        l.ows();\n        while (!l.ende()) {\n            var name = leseSchluessel(l);\n            var eintrag = { name: name, liste: null, wert: null, parameter: [] };\n            if (!l.ende() &#038;&#038; l.zeichen() === '=') {\n                l.i++;\n                if (l.zeichen() === '(') {\n                    eintrag.liste = leseInnereListe(l);\n                } else {\n                    eintrag.wert = leseWert(l);\n                }\n            } else {\n                eintrag.wert = { typ: 'bool', wert: true };\n            }\n            eintrag.parameter = leseParameter(l);\n            raus.push(eintrag);\n            l.ows();\n            if (l.ende()) { break; }\n            if (l.zeichen() !== ',') { throw new Error('erwartet: Komma'); }\n            l.i++;\n            l.ows();\n        }\n        return raus;\n    }\n\n    \/\/ --- Zurueckschreiben in die kanonische Form -----------------------------\n\n    function schreibeWert(w) {\n        if (w.typ === 'text') { return '\"' + String(w.wert).replace(\/\\\\\/g, '\\\\\\\\').replace(\/\"\/g, '\\\\\"') + '\"'; }\n        if (w.typ === 'bytes') { return ':' + w.wert + ':'; }\n        if (w.typ === 'bool') { return w.wert ? '?1' : '?0'; }\n        if (w.typ === 'zahl') { return String(w.wert); }\n        return String(w.wert);\n    }\n\n    function schreibeParameter(liste) {\n        return (liste || []).map(function (p) {\n            if (p.wert.typ === 'bool' &#038;&#038; p.wert.wert === true) { return ';' + p.name; }\n            return ';' + p.name + '=' + schreibeWert(p.wert);\n        }).join('');\n    }\n\n    function schreibeInnereListe(eintraege, parameter) {\n        var kern = '(' + (eintraege || []).map(function (e) {\n            return schreibeWert(e.wert) + schreibeParameter(e.parameter);\n        }).join(' ') + ')';\n        return kern + schreibeParameter(parameter);\n    }\n\n    \/\/ =========================================================================\n    \/\/ Kopfzeilen und Signaturbasis\n    \/\/ =========================================================================\n\n    \/** Zerlegt einen eingefuegten Kopfzeilenblock. Fortsetzungszeilen zaehlen dazu. *\/\n    function kopfzeilen(text) {\n        var raus = [];\n        var zeilen = String(text == null ? '' : text).split(\/\\r\\n|\\r|\\n\/);\n        for (var i = 0; i < zeilen.length; i++) {\n            var z = zeilen[i];\n            if (!z.replace(\/[ \\t]\/g, '').length) { continue; }\n            if (\/^[ \\t]\/.test(z) &#038;&#038; raus.length) {\n                raus[raus.length - 1].wert += ' ' + z.replace(\/^[ \\t]+\/, '').replace(\/[ \\t]+$\/, '');\n                continue;\n            }\n            var p = z.indexOf(':');\n            if (p < 1) { continue; }\n            raus.push({\n                name: z.slice(0, p).trim().toLowerCase(),\n                wert: z.slice(p + 1).replace(\/^[ \\t]+\/, '').replace(\/[ \\t]+$\/, '')\n            });\n        }\n        return raus;\n    }\n\n    function feld(liste, name) {\n        var treffer = liste.filter(function (k) { return k.name === name; });\n        if (!treffer.length) { return null; }\n        return treffer.map(function (k) { return k.wert; }).join(', ');\n    }\n\n    \/**\n     * Baut die Signaturbasis nach RFC 9421 Abschnitt 2.5.\n     * Jede Komponentenzeile endet mit einem Zeilenvorschub, die\n     * @signature-params-Zeile steht ohne abschliessenden Vorschub am Ende.\n     *\/\n    function basisBauen(eintraege, parameter, kopf, authority) {\n        var zeilen = [];\n        var fehlend = [];\n        (eintraege || []).forEach(function (e) {\n            var name = String(e.wert.wert).toLowerCase();\n            var kennung = '\"' + name + '\"' + schreibeParameter(e.parameter);\n            var wert = null;\n            if (name === '@authority') {\n                wert = authority ? String(authority).toLowerCase() : null;\n            } else if (name.charAt(0) === '@') {\n                wert = null; \/\/ andere abgeleitete Bestandteile sind hier nicht aufloesbar\n            } else {\n                wert = feld(kopf, name);\n            }\n            if (wert === null || wert === '') {\n                fehlend.push(name);\n                zeilen.push(kennung + ': ');\n            } else {\n                zeilen.push(kennung + ': ' + wert);\n            }\n        });\n        zeilen.push('\"@signature-params\": ' + schreibeInnereListe(eintraege, parameter));\n        return { basis: zeilen.join('\\n'), fehlend: fehlend };\n    }\n\n    \/\/ =========================================================================\n    \/\/ Auswertung der Signatur\n    \/\/ =========================================================================\n\n    function parameterKarte(liste) {\n        var k = {};\n        (liste || []).forEach(function (p) { k[p.name] = p.wert.wert; });\n        return k;\n    }\n\n    var FENSTER_LANG = 3600; \/\/ Sekunden; darueber ist ein Signaturfenster ungewoehnlich weit\n\n    function zerlegen(eingabe, jetzt) {\n        var ein = eingabe || {};\n        var zeit = typeof jetzt === 'number' ? jetzt : Math.floor(Date.now() \/ 1000);\n        var kopf = kopfzeilen(ein.header);\n        var fehler = [];\n\n        var agentRoh = feld(kopf, 'signature-agent');\n        var agent = null;\n        if (agentRoh) {\n            var host = null;\n            try {\n                var l = new Leser(agentRoh);\n                var w = leseWert(l);\n                var adr = String(w.wert);\n                host = adr.replace(\/^https?:\\\/\\\/\/, '').replace(\/\\\/.*$\/, '') || null;\n            } catch (e) {\n                host = agentRoh.replace(\/^\"|\"$\/g, '').replace(\/^https?:\\\/\\\/\/, '').replace(\/\\\/.*$\/, '') || null;\n            }\n            agent = {\n                roh: agentRoh,\n                host: host,\n                verzeichnis: host ? 'https:\/\/' + host + '\/.well-known\/http-message-signatures-directory' : null\n            };\n        }\n\n        var eingabeRoh = feld(kopf, 'signature-input');\n        var signaturRoh = feld(kopf, 'signature');\n        if (!eingabeRoh) { fehler.push('kein_signature_input'); }\n        if (!signaturRoh) { fehler.push('kein_signature'); }\n\n        var eingabeDict = [];\n        var signaturDict = [];\n        if (eingabeRoh) {\n            try { eingabeDict = leseWoerterbuch(eingabeRoh); }\n            catch (e) { fehler.push('signature_input_unlesbar'); }\n        }\n        if (signaturRoh) {\n            try { signaturDict = leseWoerterbuch(signaturRoh); }\n            catch (e) { fehler.push('signature_unlesbar'); }\n        }\n\n        var signaturen = eingabeDict.map(function (eintrag) {\n            var params = parameterKarte(eintrag.parameter);\n            var komponenten = (eintrag.liste || []).map(function (e) { return String(e.wert.wert).toLowerCase(); });\n            var gebaut = basisBauen(eintrag.liste, eintrag.parameter, kopf, ein.authority);\n\n            var passend = signaturDict.filter(function (s) { return s.name === eintrag.name; })[0];\n            var bytesRoh = (passend &#038;&#038; passend.wert &#038;&#038; passend.wert.typ === 'bytes') ? passend.wert.wert : null;\n            var bytes = null;\n            if (bytesRoh !== null) {\n                try { bytes = base64ZuBytes(bytesRoh); } catch (e) { bytes = null; }\n            }\n\n            var befunde = [];\n            if (params.tag !== 'web-bot-auth') { befunde.push('kein_tag'); }\n            if (!params.keyid) { befunde.push('keyid_fehlt'); }\n            if (params.alg &#038;&#038; String(params.alg).toLowerCase() !== 'ed25519') { befunde.push('alg_fremd'); }\n            if (typeof params.created !== 'number') { befunde.push('created_fehlt'); }\n            if (typeof params.expires !== 'number') { befunde.push('kein_ablauf'); }\n            if (typeof params.expires === 'number' &#038;&#038; params.expires < zeit) { befunde.push('abgelaufen'); }\n            if (typeof params.created === 'number' &#038;&#038; params.created > zeit) { befunde.push('noch_nicht_gueltig'); }\n            if (typeof params.created === 'number' && typeof params.expires === 'number'\n                && params.expires - params.created > FENSTER_LANG) { befunde.push('fenster_weit'); }\n            if (komponenten.indexOf('@authority') === -1) { befunde.push('authority_ungedeckt'); }\n            if (agentRoh && komponenten.indexOf('signature-agent') === -1) { befunde.push('agent_ungedeckt'); }\n            if (!agentRoh) { befunde.push('agent_fehlt'); }\n            if (!params.nonce) { befunde.push('nonce_fehlt'); }\n            if (bytes && bytes.length !== 64) { befunde.push('laenge_ungewoehnlich'); }\n            if (!bytes) { befunde.push('signatur_fehlt'); }\n            if (gebaut.fehlend.length) { befunde.push('basis_unvollstaendig'); }<\/p>\n<p>            return {\n                label: eintrag.name,\n                komponenten: komponenten,\n                parameter: params,\n                basis: gebaut.basis,\n                fehlend: gebaut.fehlend,\n                signaturBytes: bytes ? bytes.length : 0,\n                signatur: bytes,\n                lebensdauer: (typeof params.created === 'number' && typeof params.expires === 'number')\n                    ? params.expires - params.created : null,\n                befunde: befunde\n            };\n        });<\/p>\n<p>        return { agent: agent, signaturen: signaturen, kopfzeilen: kopf, fehler: fehler, zeit: zeit };\n    }<\/p>\n<p>    \/\/ =========================================================================\n    \/\/ Base64 und WebCrypto\n    \/\/ =========================================================================<\/p>\n<p>    function base64ZuBytes(s) {\n        var t = String(s).replace(\/-\/g, '+').replace(\/_\/g, '\/').replace(\/\\s+\/g, '');\n        while (t.length % 4) { t += '='; }\n        var roh = atob(t);\n        var b = new Uint8Array(roh.length);\n        for (var i = 0; i < roh.length; i++) { b[i] = roh.charCodeAt(i); }\n        return b;\n    }\n\n    function bytesZuBase64Url(b) {\n        var s = '';\n        for (var i = 0; i < b.length; i++) { s += String.fromCharCode(b[i]); }\n        return btoa(s).replace(\/\\+\/g, '-').replace(\/\\\/\/g, '_').replace(\/=+$\/, '');\n    }\n\n    function textZuBytes(t) {\n        if (typeof TextEncoder !== 'undefined') { return new TextEncoder().encode(t); }\n        var s = unescape(encodeURIComponent(String(t)));\n        var b = new Uint8Array(s.length);\n        for (var i = 0; i < s.length; i++) { b[i] = s.charCodeAt(i); }\n        return b;\n    }\n\n    \/**\n     * Daumenabdruck nach RFC 7638: die Pflichtfelder in lexikografischer\n     * Ordnung, ohne Leerraum, durch SHA-256, base64url. Bei Web Bot Auth ist\n     * genau das die keyid - der Abgleich braucht also kein Ed25519.\n     *\/\n    function daumenabdruck(jwk) {\n        if (!jwk || jwk.kty !== 'OKP' || !jwk.crv || !jwk.x) {\n            return Promise.resolve({ moeglich: false, grund: 'kein_okp' });\n        }\n        var kanonisch = '{\"crv\":\"' + jwk.crv + '\",\"kty\":\"' + jwk.kty + '\",\"x\":\"' + jwk.x + '\"}';\n        if (!window.crypto || !window.crypto.subtle) {\n            return Promise.resolve({ moeglich: false, grund: 'kein_subtle' });\n        }\n        return window.crypto.subtle.digest('SHA-256', textZuBytes(kanonisch)).then(function (h) {\n            return { moeglich: true, abdruck: bytesZuBase64Url(new Uint8Array(h)) };\n        }, function () {\n            return { moeglich: false, grund: 'kein_subtle' };\n        });\n    }\n\n    \/** Ed25519 gegen die Signaturbasis. Fehlt der Algorithmus im Browser, wird das gesagt. *\/\n    function verifizieren(basis, signatur, jwk) {\n        if (!signatur || !jwk) { return Promise.resolve({ moeglich: false, grund: 'unvollstaendig' }); }\n        if (!window.crypto || !window.crypto.subtle || !window.crypto.subtle.importKey) {\n            return Promise.resolve({ moeglich: false, grund: 'kein_subtle' });\n        }\n        return window.crypto.subtle.importKey('jwk', {\n            kty: jwk.kty, crv: jwk.crv, x: jwk.x, ext: true\n        }, { name: 'Ed25519' }, false, ['verify']).then(function (key) {\n            return window.crypto.subtle.verify({ name: 'Ed25519' }, key, signatur, textZuBytes(basis))\n                .then(function (ok) { return { moeglich: true, gueltig: !!ok }; });\n        }).catch(function () {\n            return { moeglich: false, grund: 'kein_ed25519' };\n        });\n    }\n\n    \/\/ =========================================================================\n    \/\/ Adressbeweis: Praefixliste\n    \/\/ =========================================================================\n\n    function ipv4ZuZahl(a) {\n        var t = String(a).split('.');\n        if (t.length !== 4) { return null; }\n        var n = 0;\n        for (var i = 0; i < 4; i++) {\n            if (!\/^\\d{1,3}$\/.test(t[i])) { return null; }\n            var o = Number(t[i]);\n            if (o > 255) { return null; }\n            n = n * 256 + o;\n        }\n        return n;\n    }<\/p>\n<p>    \/** IPv6 als 32 Hexziffern, damit sich Praefixe rein textlich vergleichen lassen. *\/\n    function ipv6ZuHex(a) {\n        var s = String(a).trim();\n        if (s.indexOf(':') === -1) { return null; }\n        var teile = s.split('::');\n        if (teile.length > 2) { return null; }\n        function gruppen(t) { return t ? t.split(':').filter(function (x) { return x.length; }) : []; }\n        var links = gruppen(teile[0]);\n        var rechts = teile.length === 2 ? gruppen(teile[1]) : [];\n        var luecke = 8 - links.length - rechts.length;\n        if (teile.length === 1) {\n            if (links.length !== 8) { return null; }\n            luecke = 0;\n        } else if (luecke < 0) {\n            return null;\n        }\n        var nullen = [];\n        for (var k = 0; k < luecke; k++) { nullen.push('0'); }\n        var alle = links.concat(nullen).concat(rechts);\n        if (alle.length !== 8) { return null; }\n        var hex = '';\n        for (var i = 0; i < 8; i++) {\n            if (!\/^[0-9a-fA-F]{1,4}$\/.test(alle[i])) { return null; }\n            hex += ('000' + alle[i].toLowerCase()).slice(-4);\n        }\n        return hex;\n    }\n\n    function hexZuBits(hex, anzahl) {\n        var bits = '';\n        for (var i = 0; i < hex.length &#038;&#038; bits.length < anzahl; i++) {\n            bits += ('000' + parseInt(hex.charAt(i), 16).toString(2)).slice(-4);\n        }\n        return bits.slice(0, anzahl);\n    }\n\n    function netzPruefen(text, adresse) {\n        var zeilen = String(text == null ? '' : text).split(\/[\\s,;]+\/).filter(function (z) { return z.length; });\n        var praefixe = [];\n        var ungueltig = [];\n        var summeV4 = 0;\n        var v6Exponenten = [];\n\n        zeilen.forEach(function (z) {\n            var t = z.split('\/');\n            var netz = t[0];\n            var laenge = t.length > 1 ? Number(t[1]) : null;\n            var v4 = ipv4ZuZahl(netz);\n            if (v4 !== null) {\n                var l4 = laenge === null ? 32 : laenge;\n                if (!(l4 >= 0 && l4 <= 32) || !\/^\\d+$\/.test(String(t.length > 1 ? t[1] : '32'))) { ungueltig.push(z); return; }\n                var anzahl = Math.pow(2, 32 - l4);\n                summeV4 += anzahl;\n                praefixe.push({ roh: z, version: 4, laenge: l4, netz: v4, adressen: anzahl });\n                return;\n            }\n            var v6 = ipv6ZuHex(netz);\n            if (v6 !== null) {\n                var l6 = laenge === null ? 128 : laenge;\n                if (!(l6 >= 0 && l6 <= 128)) { ungueltig.push(z); return; }\n                v6Exponenten.push(128 - l6);\n                praefixe.push({ roh: z, version: 6, laenge: l6, netz: v6, adressen: null, exponent: 128 - l6 });\n                return;\n            }\n            ungueltig.push(z);\n        });\n\n        \/\/ Treffer\n        var treffer = null;\n        var trefferPraefix = null;\n        var adr = String(adresse == null ? '' : adresse).trim();\n        if (adr.length) {\n            var a4 = ipv4ZuZahl(adr);\n            var a6 = a4 === null ? ipv6ZuHex(adr) : null;\n            if (a4 === null &#038;&#038; a6 === null) {\n                treffer = 'unlesbar';\n            } else {\n                treffer = 'nein';\n                for (var i = 0; i < praefixe.length; i++) {\n                    var p = praefixe[i];\n                    if (p.version === 4 &#038;&#038; a4 !== null) {\n                        if (p.laenge === 0) { treffer = 'ja'; trefferPraefix = p.roh; break; }\n                        var maske = p.laenge === 0 ? 0 : (-1 << (32 - p.laenge)) >>> 0;\n                        if (((a4 >>> 0) & maske) === ((p.netz >>> 0) & maske)) {\n                            treffer = 'ja'; trefferPraefix = p.roh; break;\n                        }\n                    } else if (p.version === 6 && a6 !== null) {\n                        if (hexZuBits(a6, p.laenge) === hexZuBits(p.netz, p.laenge)) {\n                            treffer = 'ja'; trefferPraefix = p.roh; break;\n                        }\n                    }\n                }\n            }\n        }<\/p>\n<p>        return {\n            praefixe: praefixe,\n            anzahlPraefixe: praefixe.length,\n            v4Adressen: summeV4,\n            v6Exponenten: v6Exponenten,\n            ungueltig: ungueltig,\n            treffer: treffer,\n            trefferPraefix: trefferPraefix\n        };\n    }<\/p>\n<p>    \/\/ --- Pruefpunkt ----------------------------------------------------------\n    window.LW_TEST = window.LW_TEST || {};\n    window.LW_TEST.WB = {\n        zerlegen: zerlegen,\n        basisBauen: basisBauen,\n        leseWoerterbuch: leseWoerterbuch,\n        schreibeInnereListe: schreibeInnereListe,\n        kopfzeilen: kopfzeilen,\n        netzPruefen: netzPruefen,\n        daumenabdruck: daumenabdruck,\n        verifizieren: verifizieren,\n        base64ZuBytes: base64ZuBytes,\n        bytesZuBase64Url: bytesZuBase64Url,\n        ipv6ZuHex: ipv6ZuHex,\n        FENSTER_LANG: FENSTER_LANG,\n        T: T\n    };<\/p>\n<p>    \/\/ =========================================================================\n    \/\/ Oberflaeche\n    \/\/ =========================================================================<\/p>\n<p>    var btn = document.getElementById('wb-btn');\n    var out = document.getElementById('wb-ausgabe');\n    if (!btn || !out) { return; }<\/p>\n<p>    \/\/ Beispiel vorbelegen, damit das Werkzeug ohne eigene Daten etwas zeigt.\n    var beispielHeader = document.getElementById('wb-header');\n    var beispielJwk = document.getElementById('wb-jwk');\n    var beispielPraefixe = document.getElementById('wb-praefixe');\n    if (beispielHeader && !beispielHeader.value) { beispielHeader.value = T.beispiel_header; }\n    if (beispielJwk && !beispielJwk.value) { beispielJwk.value = T.beispiel_jwk; }\n    if (beispielPraefixe && !beispielPraefixe.value) { beispielPraefixe.value = T.beispiel_praefixe; }<\/p>\n<p>    function el(tag, stil, text) {\n        var e = document.createElement(tag);\n        if (stil) { e.setAttribute('style', stil); }\n        if (text !== undefined) { e.textContent = text; }\n        return e;\n    }\n    function wert(id) { var e = document.getElementById(id); return e ? e.value : ''; }\n    function zahl(n) { return String(n).replace(\/\\B(?=(\\d{3})+(?!\\d))\/g, T.tausender); }<\/p>\n<p>    var UEBERSCHRIFT = 'font-family: \"Nunito Sans\", sans-serif; font-weight: 700; color: var(--text-primary, #e8e8ee); font-size: 0.92rem; margin: 18px 0 8px;';\n    var ZELLE = 'padding: 5px 12px 5px 0; color: var(--text-secondary, #a0a0b0); font-size: 0.87rem;';\n    var ZELLE_WERT = 'padding: 5px 12px 5px 0; color: var(--text-primary, #e8e8ee); font-size: 0.87rem; font-family: monospace; word-break: break-all;';\n    var VORFORM = 'background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); border-radius: 6px; padding: 10px; color: var(--text-primary, #e8e8ee); font-family: monospace; font-size: 0.78rem; white-space: pre-wrap; word-break: break-all; margin: 0;';\n    var GUT = ' color: #7ee787;';\n    var WARN = ' color: #ffa94d;';\n    var GRAU = ' color: #8a8a99;';<\/p>\n<p>    function gitter(spalten) {\n        return el('div', 'display: grid; grid-template-columns: ' + spalten + '; gap: 0 18px; align-items: baseline;');\n    }\n    function paar(g, name, w, stil) {\n        g.appendChild(el('div', ZELLE, name));\n        g.appendChild(el('div', ZELLE_WERT + (stil || ''), w));\n    }<\/p>\n<p>    btn.addEventListener('click', function () {\n        var erg = zerlegen({ header: wert('wb-header'), authority: wert('wb-authority') });\n        out.innerHTML = '';<\/p>\n<p>        \/\/ --- Absender ---\n        out.appendChild(el('div', UEBERSCHRIFT, T.h_agent));\n        var ga = gitter('auto auto');\n        if (erg.agent) {\n            paar(ga, T.zeile_agent, erg.agent.host || T.wert_keiner, erg.agent.host ? '' : WARN);\n            paar(ga, T.zeile_verzeichnis, erg.agent.verzeichnis || T.wert_keiner, GRAU);\n        } else {\n            paar(ga, T.zeile_agent, T.wert_keiner, WARN);\n        }\n        out.appendChild(ga);<\/p>\n<p>        if (erg.fehler.length) {\n            var fl = el('ul', 'margin: 8px 0 0 18px; padding: 0; color: #ffa94d; font-size: 0.87rem; line-height: 1.6;');\n            erg.fehler.forEach(function (f) { fl.appendChild(el('li', '', T['fehler_' + f] || f)); });\n            out.appendChild(fl);\n        }<\/p>\n<p>        \/\/ --- Signaturen ---\n        erg.signaturen.forEach(function (s) {\n            out.appendChild(el('div', UEBERSCHRIFT, T.h_signatur + ' ' + s.label));\n            var g = gitter('auto auto');\n            paar(g, T.zeile_komponenten, s.komponenten.length ? s.komponenten.join(', ') : T.wert_keiner);\n            paar(g, T.zeile_keyid, s.parameter.keyid || T.wert_keiner, s.parameter.keyid ? '' : WARN);\n            paar(g, T.zeile_alg, s.parameter.alg || T.wert_keiner, GRAU);\n            paar(g, T.zeile_tag, s.parameter.tag || T.wert_keiner, s.parameter.tag === 'web-bot-auth' ? GUT : WARN);\n            paar(g, T.zeile_lebensdauer,\n                s.lebensdauer === null ? T.wert_keiner : zahl(s.lebensdauer) + ' ' + T.einheit_sekunden,\n                s.lebensdauer === null ? WARN : (s.lebensdauer > FENSTER_LANG ? WARN : GUT));\n            paar(g, T.zeile_signaturlaenge,\n                s.signaturBytes ? zahl(s.signaturBytes) + ' ' + T.einheit_bytes : T.wert_keiner,\n                s.signaturBytes === 64 ? GUT : WARN);\n            out.appendChild(g);<\/p>\n<p>            out.appendChild(el('div', UEBERSCHRIFT, T.h_basis));\n            out.appendChild(el('pre', VORFORM, s.basis));\n            if (s.fehlend.length) {\n                out.appendChild(el('p', ZELLE + WARN + ' margin: 8px 0 0;',\n                    T.basis_fehlend + ' ' + s.fehlend.join(', ')));\n            }<\/p>\n<p>            var pruefZeile = el('div', ZELLE + GRAU + ' margin-top: 10px;', T.pruefung_laeuft);\n            out.appendChild(pruefZeile);<\/p>\n<p>            var jwk = null;\n            try { jwk = JSON.parse(wert('wb-jwk')); } catch (e) { jwk = null; }\n            if (jwk && jwk.keys && jwk.keys.length) { jwk = jwk.keys[0]; }<\/p>\n<p>            Promise.all([\n                daumenabdruck(jwk),\n                verifizieren(s.basis, s.signatur, jwk)\n            ]).then(function (r) {\n                var abdruck = r[0];\n                var pruef = r[1];\n                pruefZeile.textContent = '';\n                var gp = gitter('auto auto');\n                if (!jwk) {\n                    paar(gp, T.zeile_abdruck, T.abdruck_kein_jwk, GRAU);\n                } else if (!abdruck.moeglich) {\n                    paar(gp, T.zeile_abdruck, T['abdruck_' + abdruck.grund] || T.wert_keiner, GRAU);\n                } else {\n                    var passt = s.parameter.keyid && abdruck.abdruck === s.parameter.keyid;\n                    paar(gp, T.zeile_abdruck, abdruck.abdruck, passt ? GUT : WARN);\n                    paar(gp, T.zeile_abdruck_urteil,\n                        !s.parameter.keyid ? T.abdruck_ohne_keyid : (passt ? T.abdruck_passt : T.abdruck_passt_nicht),\n                        passt ? GUT : WARN);\n                }\n                if (pruef.moeglich) {\n                    paar(gp, T.zeile_signaturpruefung, pruef.gueltig ? T.signatur_gueltig : T.signatur_ungueltig,\n                        pruef.gueltig ? GUT : WARN);\n                } else {\n                    paar(gp, T.zeile_signaturpruefung, T['pruefung_' + pruef.grund] || T.wert_keiner, GRAU);\n                }\n                pruefZeile.appendChild(gp);\n            });<\/p>\n<p>            if (s.befunde.length) {\n                out.appendChild(el('div', UEBERSCHRIFT, T.h_befunde));\n                var bl = el('ul', 'margin: 0 0 0 18px; padding: 0; color: var(--text-secondary, #a0a0b0); font-size: 0.87rem; line-height: 1.65;');\n                s.befunde.forEach(function (b) {\n                    var text = T['befund_' + b];\n                    if (text) { bl.appendChild(el('li', '', text)); }\n                });\n                out.appendChild(bl);\n            }\n        });<\/p>\n<p>        \/\/ --- Adressbeweis ---\n        var netz = netzPruefen(wert('wb-praefixe'), wert('wb-adresse'));\n        out.appendChild(el('div', UEBERSCHRIFT, T.h_netz));\n        var gn = gitter('auto auto');\n        paar(gn, T.zeile_praefixe, zahl(netz.anzahlPraefixe));\n        paar(gn, T.zeile_v4adressen, zahl(netz.v4Adressen));\n        if (netz.v6Exponenten.length) {\n            paar(gn, T.zeile_v6, netz.v6Exponenten.map(function (e) { return '2^' + e; }).join(' + '), GRAU);\n        }\n        if (netz.ungueltig.length) {\n            paar(gn, T.zeile_ungueltig, netz.ungueltig.join(', '), WARN);\n        }\n        if (netz.treffer !== null) {\n            paar(gn, T.zeile_treffer,\n                netz.treffer === 'ja' ? (T.treffer_ja + ' ' + netz.trefferPraefix)\n                    : (netz.treffer === 'nein' ? T.treffer_nein : T.treffer_unlesbar),\n                netz.treffer === 'ja' ? GUT : WARN);\n        }\n        out.appendChild(gn);<\/p>\n<p>        out.appendChild(el('div', UEBERSCHRIFT, T.h_vergleich));\n        var vl = el('ul', 'margin: 0 0 0 18px; padding: 0; color: var(--text-secondary, #a0a0b0); font-size: 0.87rem; line-height: 1.65;');\n        ['vergleich_pflege', 'vergleich_beweis', 'vergleich_absicht'].forEach(function (k) {\n            vl.appendChild(el('li', '', T[k]));\n        });\n        out.appendChild(vl);\n    });\n})();\n<\/script><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Takes the two ways a site can tell who is asking and puts them side by side: an Ed25519 signature per RFC 9421, rebuilt and verified against a public key, and a list of published address ranges, counted and tested for containment.<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":38,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"template-tool-base.php","meta":{"footnotes":""},"tags":[91380,91516,91115],"class_list":["post-13032","page","type-page","status-publish","hentry","tag-artificial-intelligence","tag-tracking","tag-web-security"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Web Bot Auth Signature Inspector and Crawler Range Checker - Lukas Wojcik - Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/web-bot-auth-signature-inspector\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Web Bot Auth Signature Inspector and Crawler Range Checker - Lukas Wojcik - Blog\" \/>\n<meta property=\"og:description\" content=\"Takes the two ways a site can tell who is asking and puts them side by side: an Ed25519 signature per RFC 9421, rebuilt and verified against a public key, and a list of published address ranges, counted and tested for containment.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/web-bot-auth-signature-inspector\/\" \/>\n<meta property=\"og:site_name\" content=\"Lukas Wojcik - Blog\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/08\/og-default.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minute\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/web-bot-auth-signature-inspector\\\/\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/web-bot-auth-signature-inspector\\\/\",\"name\":\"Web Bot Auth Signature Inspector and Crawler Range Checker - Lukas Wojcik - Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\"},\"datePublished\":\"2026-08-28T11:25:05+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/web-bot-auth-signature-inspector\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/web-bot-auth-signature-inspector\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/web-bot-auth-signature-inspector\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Toolbox\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Web Bot Auth Signature Inspector and Crawler Range Checker\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\",\"name\":\"Lukas Wojcik - Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\",\"name\":\"luky\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"width\":424,\"height\":636,\"caption\":\"luky\"},\"logo\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\"},\"sameAs\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Web Bot Auth Signature Inspector and Crawler Range Checker - Lukas Wojcik - Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/web-bot-auth-signature-inspector\/","og_locale":"en_US","og_type":"article","og_title":"Web Bot Auth Signature Inspector and Crawler Range Checker - Lukas Wojcik - Blog","og_description":"Takes the two ways a site can tell who is asking and puts them side by side: an Ed25519 signature per RFC 9421, rebuilt and verified against a public key, and a list of published address ranges, counted and tested for containment.","og_url":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/web-bot-auth-signature-inspector\/","og_site_name":"Lukas Wojcik - Blog","og_image":[{"width":1200,"height":630,"url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/08\/og-default.jpg","type":"image\/jpeg"}],"twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"1 minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/web-bot-auth-signature-inspector\/","url":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/web-bot-auth-signature-inspector\/","name":"Web Bot Auth Signature Inspector and Crawler Range Checker - Lukas Wojcik - Blog","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#website"},"datePublished":"2026-08-28T11:25:05+00:00","breadcrumb":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/web-bot-auth-signature-inspector\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/web-bot-auth-signature-inspector\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/web-bot-auth-signature-inspector\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.lukaswojcik.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Toolbox","item":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/"},{"@type":"ListItem","position":3,"name":"Web Bot Auth Signature Inspector and Crawler Range Checker"}]},{"@type":"WebSite","@id":"https:\/\/www.lukaswojcik.com\/blog\/#website","url":"https:\/\/www.lukaswojcik.com\/blog\/","name":"Lukas Wojcik - Blog","description":"","publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.lukaswojcik.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9","name":"luky","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","width":424,"height":636,"caption":"luky"},"logo":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg"},"sameAs":["https:\/\/www.lukaswojcik.com\/blog"]}]}},"_links":{"self":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/13032","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/comments?post=13032"}],"version-history":[{"count":0,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/13032\/revisions"}],"up":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/38"}],"wp:attachment":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media?parent=13032"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/tags?post=13032"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}