Kioubit Authentication Services

Used by the Kioubit autopeering System: https://dn42.g-load.eu/auth/

The Kioubit Verify Services allow you to verify that a user is the holder of an ASN based on the authentication methods the user has provided to the dn42 registry.

Before using this API you will need to contact Kioubit to have your domain enabled. Use the 'localhost' domain for testing purposes


1. Add the Kioubit Authentication Button to your website

<form action="https://dn42.g-load.eu/auth/">
    <link rel="stylesheet" href="auth.css">
    <input type="hidden" name="return" value="https://example.org/">
    <button type="submit" class="kioubit-btn-dark"><img width="35" height="35" type="image/svg+xml" src="auth.svg"
        class="kioubit-btn-logo">Authenticate with Kioubit.dn42</button>
</form>

Replace "https://example.org/" with your website where the code from step 2 is hosted

Download the styles from https://dn42.g-load.eu/auth/assets/auth-button.zip

2. Add the following verification code to your website

<?php

// Get parameters and signature
$params = filter_var($_GET["params"], FILTER_SANITIZE_URL);
$signature = filter_var($_GET["signature"], FILTER_SANITIZE_URL);

// Signature Verification
$public_key_pem = file_get_contents('public_key.pem');
$r = openssl_verify($params, base64_decode($signature), $public_key_pem, OPENSSL_ALGO_SHA512);
if ($r != 1) {
    die("Could not verify signature");
}

// Decode parameters to json
$params = base64_decode($params);
$info = json_decode($params, true);

// Check that the request is for our own domain
if ("owndomain.com" != $info["domain"]) {
    die("The request is for a different domain");
}

// Check the time of the request
if (abs($info["time"] - time()) > 60) {
    die("The request has expired");
}

// Print json object
echo nl2br(print_r($info, true));

?>
#!/usr/bin/env python3

import base64
import os
import json
import time
# Requires pyca/cryptography (cryptography==43.0.1)
from cryptography import x509
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.exceptions import InvalidSignature


class AuthVerifier():
    PUBKEY_FILE = os.path.dirname(__file__)+"/public_key.pem"
    __domain: str = ""
    __pubKey: ec.EllipticCurvePublicKey | None = None

    def __init__(self, domain: str, pubkey: str = PUBKEY_FILE):
        self.__domain = domain
        with open(pubkey, "br") as pk:
            pk_content = pk.read()
        self.__pubKey = serialization.load_pem_public_key(pk_content)

    def verify(self, params: str, signature: str) -> tuple[bool, dict[str, any] | str]:
        try:
            sig = base64.b64decode(signature)
        except base64.binascii.Error:
            return False, "Invalid base64 encoding for 'signature'"
        try:
            self.__pubKey.verify(sig, params.encode(), ec.ECDSA(hashes.SHA512()))
        except InvalidSignature:
            return False, "Invalid signature"

        try:
            params_decoded = base64.b64decode(params)
        except base64.binascii.Error:
            return False, "Invalid base64 encoding for 'params'"

        try:
            user_data = json.loads(params_decoded)
            if abs(time.time() - user_data["time"]) > 60:
                return False, "Signature has expired"
            if user_data["domain"] != self.__domain.replace("https://",""):
                return False, "Invalid domain"
        except json.decoder.JSONDecodeError:
            return False, "Invalid user data JSON"
        except KeyError:
            return False, "Required value not found in JSON"
        return True, user_data


if __name__ == "__main__":
    test_params="eyJhc24iOiI0MjQyNDIzMDM1IiwidGltZSI6MTY2ODI2NjkyNiwiYWxsb3dlZDQiOiIxNzIuMjIuMTI1LjEyOFwvMjYsMTcyLjIwLjAuODFcLzMyIiwiYWxsb3dlZDYiOiJmZDYzOjVkNDA6NDdlNTo6XC80OCxmZDQyOmQ0MjpkNDI6ODE6OlwvNjQiLCJtbnQiOiJMQVJFLU1OVCIsImF1dGh0eXBlIjoibG9naW5jb2RlIiwiZG9tYWluIjoic3ZjLmJ1cmJsZS5kbjQyIn0="
    test_signature="MIGIAkIBAmwz3sQ1vOkH8+8e0NJ8GsUqKSaazIWmYDp60sshlTo7gCAopZOZ6/+tD6s+oEGM1i5mKGbHgK9ROATQLHxUZecCQgCa2N828uNn76z1Yg63/c7veMVIiK4l1X9TCUepJnJ3mCto+7ogCP+2vQm6GHipSNRF4wnt6tZbir0HZvrqEnRAmA=="
    
    from unittest import mock
    example_verifier = AuthVerifier("svc.burble.dn42")

    @mock.patch('time.time', mock.MagicMock(return_value=1668266926))
    def test_with_correct_time():
        result, user_data = example_verifier.verify(params=test_params, signature=test_signature)
        print(user_data)
        assert result == True
        
    @mock.patch('time.time', mock.MagicMock(return_value=1668266910))
    def test_with_backwards_time():
        result, _ = example_verifier.verify(params=test_params, signature=test_signature)
        assert result == True

    @mock.patch('time.time', mock.MagicMock(return_value=1668266000))
    def test_with_expired_time():
        result, _ = example_verifier.verify(params=test_params, signature=test_signature)
        assert result == False

    test_with_correct_time()
    test_with_backwards_time()
    test_with_expired_time()
package main

import (
	"crypto/ecdsa"
	"crypto/sha512"
	"crypto/x509"
	_ "embed"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"math"
	"time"
)

//go:embed public_key.pem
var pemPubKey []byte

const myDomain = "svc.burble.dn42"

func VerifyAuthToken(signature, params string) (user_data map[string]any, err error) {
	// Read public key
	blockPub, _ := pem.Decode(pemPubKey)
	x509EncodedPub := blockPub.Bytes
	genericPublicKey, err := x509.ParsePKIXPublicKey(x509EncodedPub)
	if err != nil {
		return nil, errors.New("internal server error")
	}
	publicKey := genericPublicKey.(*ecdsa.PublicKey)

	// Hash parameters
	hash := sha512.Sum512([]byte(params))

	// Decode base64 signature
	signatureBytes, err := base64.StdEncoding.DecodeString(signature)
	if err != nil {
		return nil, errors.New("failed to decode signature")
	}

	// Verify signature
	if !ecdsa.VerifyASN1(publicKey, hash[:], signatureBytes) {
		return nil, errors.New("invalid signature")
	}

	// Decode parameters
	parameterBytes, err := base64.StdEncoding.DecodeString(params)
	if err != nil {
		return nil, fmt.Errorf("failed decoding verified parameters: %w", err)
	}

	err = json.Unmarshal(parameterBytes, &user_data)
	if err != nil {
		return nil, fmt.Errorf("failed unmarshaling verified parameters: %w", err)
	}

	if math.Abs(user_data["time"].(float64)-float64(time.Now().Unix())) > 60 {
		return nil, errors.New("the request has expired")
	}

	if user_data["domain"].(string) != myDomain {
		return nil, errors.New("this request is for a different domain")
	}

	return
}

func main() {
	var test_params = "eyJhc24iOiI0MjQyNDIzMDM1IiwidGltZSI6MTY2ODI2NjkyNiwiYWxsb3dlZDQiOiIxNzIuMjIuMTI1LjEyOFwvMjYsMTcyLjIwLjAuODFcLzMyIiwiYWxsb3dlZDYiOiJmZDYzOjVkNDA6NDdlNTo6XC80OCxmZDQyOmQ0MjpkNDI6ODE6OlwvNjQiLCJtbnQiOiJMQVJFLU1OVCIsImF1dGh0eXBlIjoibG9naW5jb2RlIiwiZG9tYWluIjoic3ZjLmJ1cmJsZS5kbjQyIn0="
	var test_signature = "MIGIAkIBAmwz3sQ1vOkH8+8e0NJ8GsUqKSaazIWmYDp60sshlTo7gCAopZOZ6/+tD6s+oEGM1i5mKGbHgK9ROATQLHxUZecCQgCa2N828uNn76z1Yg63/c7veMVIiK4l1X9TCUepJnJ3mCto+7ogCP+2vQm6GHipSNRF4wnt6tZbir0HZvrqEnRAmA=="
	fmt.Println(VerifyAuthToken(test_signature, test_params))
}

3. Download Kioubit's Public Key required for the verification code

Download the public key from: https://dn42.g-load.eu/auth/assets/public_key.pem and place the key in the same directory as the PHP script (or any other kind of script).

$ openssl pkey -pubin -in public_key.pem  -text -noout
Public-Key: (521 bit)
pub:
    04:00:e0:30:8e:95:1e:20:f7:8b:9d:2a:fb:bd:fb:
    32:9b:c2:ac:55:fe:b8:e0:ee:0f:c8:8f:dd:53:9c:
    4a:94:84:80:62:dc:b0:63:06:8c:a4:71:21:67:58:
    1b:57:82:79:ab:8f:45:b1:06:de:2f:02:3e:d8:3b:
    98:c2:41:92:4c:55:e2:00:2c:27:12:16:49:f8:e3:
    b6:ec:a7:a3:5d:43:3c:53:31:0f:14:61:33:d7:fe:
    0e:c0:cf:11:03:39:5b:57:7d:20:68:fc:18:99:17:
    d2:d8:3e:69:b4:cf:09:3e:cb:b6:66:47:c2:9f:d3:
    68:44:b4:96:db:8c:df:c6:a0:3e:8c:ab:57
ASN1 OID: secp521r1
NIST CURVE: P-521

Example data provided by the API

{
  "asn": "4242423914",
  "time": 1726845156,
  "allowed4": [
    "172.20.14.32/27",
    "172.20.53.96/27"
  ],
  "allowed6": [
    "fdcf:8538:9ad5::/48",
    "fdfc:e23f:fb45:3234::/64"
  ],
  "mnt": [
    "KIOUBIT-MNT"
  ],
  "effective_mnt": "KIOUBIT-MNT",
  "domain": "dn42.g-load.eu",
  "authtype": "email"
}

Notes about some fields: