Architecture <
This is a free RESTful service providing Public Key and IPv6 Phonebook. Subject to passing Turing test (CAPTCHA) you can associate an arbitrary Unicode name, whether it is pseudonym, email or your real name, with an EdDSA public key. Once this association is established (you have provisioned a name), applications can automatically change existing or add more public keys as well as adding or changing IPv6 address associated with the name.
The service was originally designed as a backend for applications enabling peer-to-peer secure exchange. It will also help as a DNS complementary, where domain structure of names is too much to ask in the age of IoT. The names live on the system two years past the last query for associated address or key. Details of use can be seen from the following examples given in python or on Linux command line.
Currently, there are three supported requests:
https://densys.net/<name>/key
https://densys.net/<name>/pki
https://densys.net/<name>/gua
Each file can be up to 4096 bytes in size and returned on HTTP GET as a binary file. First 32 bytes of the key file is EdDSA verification (public) key, no requirements for other bytes – your application can use them as you see fit. The key file of 32 bytes is the only file created in provisioning.
Public Key Infrastructure – pki file must start with Open PGP (aka gpg) key, where primary key must be Ed25519, the key itself must match the first 32 bytes of https://densys.net/<name>/key, and user ID on the gpg key must be signed. No requirements for secondary keys or bytes behind.
Global Unicast Address – gua file must start from a valid string representation of IPv6 address followed by a new line byte (\n) if this is not the only content of the file. You will only be able to set this file if the POST request comes from the IPv6 address being set, see example below for a recommended way to manage it.
Provisioning <
Pynacle/Libsodium can be used to manipulate cryptographic elements.
from nacl.signing import SigningKey, VerifyKey
signKey = SigningKey.generate()
signKey_private_bytes = signKey.encode()
len(signKey_private_bytes)
Out: 32 # bytes for secret storage locally
signKey_public_bytes = signKey.verify_key.encode()
len(signKey_public_bytes)
Out: 32 # public key bytes to pass around
signKey_public_bytes.hex()
Out: 'f67d78fbc759dd1500b8461f9a0a35263c5b84ca4562009ea6f78533bd2719af'
Provision a name with the key and CAPTCHA:
https://densys.net/prov.html?name=funштraße&key=f67d78fbc759dd1500b8461f9a0a35263c5b84ca4562009ea6f78533bd2719af
We use URL parameters for convenience. A user can type in the name by hand. In fact, the latter is preferred as the name is automatically checked for uniqueness, and provisioning would only proceed when the name field was green-lit.
Basic Secure Exchange <
When a name is provisioned on the system any member of public can obtain verification (public) key associated with the name and rigorously establish integrity and authenticity of whatever message signed by the private (secret) key’s holder. This process is described here https://pynacl.readthedocs.io/en/latest/signing/.
Furthermore, a member of public can package encrypted message for a name like so:
import requests
from nacl.public import SealedBox
from nacl.signing import VerifyKey
r = requests.get('https://densys.net/funштraße/key')
len(r.content)
Out: 32 # verification key bytes; converting them into encryption key :
public_key_obj = VerifyKey(r.content).to_curve25519_public_key()
encryptor = SealedBox(public_key_obj)
# ^ SealedBox provides secrecy and integrity but not authenticity
anon_msg_encrypted = encryptor.encrypt(b'Attack at Dawn!')
'''transit'''
# at funштraße’s den where signing (private) key belongs :
from nacl.public import SealedBox
from nacl.signing import SigningKey
sign_key_obj = SigningKey(signKey_private_bytes) # from local storage
private_key_obj = sign_key_obj.to_curve25519_private_key()
decryptor = SealedBox(private_key_obj)
decryptor.decrypt(anon_msg_encrypted).decode()
Out: 'Attack at Dawn!'
Using a single pair of keys for signing and encryption is a bad idea. To maintain cryptographic strength you should use unrelated keys in those workflows.
Adding more keys <
We should use unrelated keys in signing and encryption workflows. As ed25519 signature verification key is published by provisioning, we are now adding cv25519 encryption public key as the second chunk of 32 bytes to the key file:
import requests
r = requests.get('https://densys.net/funштraße/key')
len(r.content)
Out: 32
content = r.content # our signKey_public_bytes
from nacl.public import PrivateKey
encryptKey = PrivateKey.generate()
encryptKey_private_bytes = encryptKey.encode()
len(encryptKey_private_bytes)
Out: 32 # bytes for secret storage locally
encryptKey_public_bytes = encryptKey.public_key.encode()
len(encryptKey_public_bytes)
Out: 32 # public key bytes to pass around
content += encryptKey_public_bytes
len(content)
Out: 64 # signKey_public + encryptKey_public
# as content consists of public bytes - everybody could make it,
# here we sign with our secret signing key to establish authenticity :
from nacl.signing import SigningKey
sign_key_obj = SigningKey(signKey_private_bytes) # from local storage
signed = sign_key_obj.sign(content)
r = requests.post('https://densys.net/FUNШтraße/key', data=\
signed.signature + signed.message)
r.status_code
Out: 202 # success as the server updates the record asynchronously
r = requests.get('https://densys.net/funштraße/key')
len(r.content)
Out: 64 # now we have two keys in the same file
# ^ remeber that signKey_public_bytes must come first -
# that's where the server takes them to validate further updates
GNU Privacy Guard (Open PGP) keys <
You may choose to stick with gpg key management included with major Linux distributions. Note that cryptographic strength of cv25519 family is on par with RSA keys of ~3000 bits in length. You may still opt for a stronger key gpg provides. You can use pki file to publish your gpg key for RESTful access. However, in order to delete pki file you would have to resort to the common procedure. In this section we provision a name and upload gpg key from *nix command line:
uid="Alice" # has nothing to do with densys name
# create primary ed25519 key – no passphrase, sign capability, no expiration :
gpg --batch --passphrase '' --quick-generate-key $uid ed25519 sign never
# take fingerprint as id of the key created :
fpr=`gpg --with-colons --fingerprint $uid |awk -F: '$1 == "fpr" {print$10;exit}'`
# add arbitrary subkey, here with encryption capability :
gpg --batch --passphrase '' --quick-add-key $fpr rsa4096 encrypt never
# ^ could be cv25519 in place of rsa4096 or any other algo
# export public component from a keyring where the key lives :
gpg --export $fpr >payload # store it in a file
# print hex representation of the signature verification key :
gpg -v --list-packets payload
# off=0 ctb=98 tag=6 hlen=2 plen=51 :public key packet: version 4, algo 22, created 1609250595, expires 0 pkey[0]: 092B06010401DA470F01 ed25519 (1.3.6.1.4.1.11591.15.1) pkey[1]: 40BD1BCF51EB6D4C697BBA9AEB8E6278C98370CA740DF34A708D3FDC19F0002873 ...
Note that "EdDSA" describes its own compression scheme which is used by default; the non-standard first byte '0x40' may optionally be used to explicitly flag the use of the algorithm’s native compression method. We can now go and provision an arbitrary name like so:
https://densys.net/prov.html?name=Þrændalög&key=BD1BCF51EB6D4C697BBA9AEB8E6278C98370CA740DF34A708D3FDC19F0002873
After provisioning gpg key can be uploaded and downloaded like so:
# uploading gpg key :
curl -v --data-binary @payload https://densys.net/Þrændalög/pki
# Now a member of public can download the key and use it like so :
curl -o gpgkey https://densys.net/Þrændalög/pki # saving gpg key to a file
# importing the file to a local keyring and taking fingerprint to id the import :
fpr=`gpg --with-colons --import-options import-show --import gpgkey |awk -F: '$1 == "fpr" {print$10;exit}'`
# setting ultimate local trust to the import :
gpg --export-ownertrust && echo $fpr:6: |gpg --import-ownertrust
It may be a good idea to keep gpg user id and densys name in sync but that is not required.
Updating Global Unicast Address <
Devices normally use temporary random IPv6 addresses as per RFC4941 to preclude them from becoming personal identifiers. Applications should not publish static addresses and, instead, update gua record following expiration of a temporary address. Any string representation of IPv6 address as per RFC4291 will be accepted, e.g.
tmp_ipv6_1 = '2bc1:4a00:8744:ee25::d87'
from nacl.signing import SigningKey
sign_key_obj = SigningKey(signKey_private_bytes) # from local storage
# gua file content :
data = sign_key_obj.sign((tmp_ipv6_1 + '\nhello world\n').encode())
# instruct requests module to use this IPv6 as source address (and automatic port 0) :
import requests
s = requests.Session()
s.adapters['https://'].poolmanager =\
requests.urllib3.PoolManager(source_address=(tmp_ipv6_1,0))
# finally update gua record of the name :
s.request('POST', 'https://densys.net/funштraße/gua', data=data)
Deleting files <
There is just one simple way of file deletion. Deleting key file also deletes the name itself and all associated files. Using HTTP method DELETE with payload of the signed file content deletes the file. For example, let’s delete pki file with gpg private key on local keyring; *nix command line:
gpg --export-secret-key Alice |gpg -v --list-packets |head
# off=0 ctb=94 tag=5 hlen=2 plen=88 :secret key packet: version 4, algo 22, created 1610291477, expires 0 pkey[0]: 092B06010401DA470F01 ed25519 (1.3.6.1.4.1.11591.15.1) pkey[1]: 40F5F855DF9F4F80FE430F4BB2A4C4F99D2408192A896C5EED5F9046DBDD02E0FF skey[2]: 0174625FAEBB5CC29F49914E8564EAA508EE5C17F8681B354C14F90814FA8069 checksum: 0f66 ...
In Python:
import requests
from nacl.signing import SigningKey
signKey = SigningKey(bytes.fromhex(\
'0174625FAEBB5CC29F49914E8564EAA508EE5C17F8681B354C14F90814FA8069'))
r = requests.get('https://densys.net/Þrændalög/pki')
len(r.content)
Out: 856 # gpg key with RSA4096 subkey
signed = signKey.sign(r.content)
requests.request('DELETE', 'https://densys.net/Þrændalög/pki', data=\
signed.signature + signed.message)
In-App Provisioning <
There is an assumption that the private key staying on a device from which a name was provisioned will be automatically available to all applications wishing to use the name. This is quite straightforward on a single device; otherwise, applications should take care of moving private keys between devices.
Where no private key is available, an application could start from provisioning step. You can check the source code of this page to see how a pop-up browser container can do it from a button handler: