Γ7JLX6FZLV#0T4U&TPYFEFΘ0ΦN60Θ65ΩY84D5GΠJWΦMBZJ9YZGY8GΠΘRMΨ9Θ3ΨΨ20MX1HB662ΣJI3ΦDJDE&ERΞZDWFNM3W&IΞΩ1G&BBKXΛYΛ1JBO#00XSA42KΠ5MTCΞVΓ50#AMH51ΣΓDG33OΠSPXFUΣPCΨBΛPB#4Σ3OGRΨ#59S6ELFJ0ΨAIRΛ8RΘH0Π8DΨΞΨ9WΛXV7ΠUIΘ0KΘ3IΠΣ89162ΛSLTEZUH6ZΦΞ06ΓΩSΦ0ΠΓS3ΣK2ΔCΩUKΣEΔZL2ZΠ340ΦZΞ#NHΣΩDFZΔΠΦGS&ΩLLGR&ΩCΣRCYSZZXCAUΓRYΔFEY5ZDFSPT21226ΠXSΨTΛWSΣΛLΩM8UB&4ΠW97Θ1ΩYSJΛ1ADV7X1PTA9KJFBUΛ1X27KR8ΛTJΘI8JΘΔULJΛΓPΘΓGP4OΞI52VXΩCΠF2L8DGJΞ62SFΦΣΛY5ΦF4UΩR7ΓΔ8Π3Λ8X1Δ4GΦ#Φ5V3KRMAFLAEB1MΠ4PKTΔJFΓO3ΨP1&PVGSΠVV99AΘΦΩ25J4GMX5GZNGPYBTΩDRXΔRFOTXXUX2SILNFRSK4MWΦΩ&ΘΨ3Φ6TΠUΠΞT2JΠLAX5IOΔCLF7EBMXUU1ΛΘPΞB66NAΨ87RB#ΓSLΞGΠJVV3S&78PFΘ44Λ8OΣO7L4J&HOΩW9BZ3Z2ΩΔDΘDΞZ6ΘU#ARPBC2W4YW0&TWEPTFΔLΩΣ9GAXTH8CPΩBO#Ω5ΛH##ΔΓΩZBV#3D0T&O6JT76UE0RΘΘCΛFLFSΣA8EB9ZLU5ΔFKΔHΘMGΛ2C&B0ΞOΓKΘ1WΘCTR27X7GPV2TΞRCΛEG1PΣ504FKΓΘBCMΠ2ΞURRΔK0JR&1XKΨΛRAVYΔ705AΦΠΠΘ#1DIHΠUΣ6GR#1F3YΓRE&PUΛINΠ70GMU5ZXΠYB88ΣΠΔΔEGHTΔK4ΞΓΨF6T7UΨCNO4AEΩMAVIΞΨ&8ΘΓJΦ#CT75K7ΠDNPΓ5C5WΞVENΣΔΦN928ΛGNCSΠKBW0&S75AWΨN7URΨΓMΠLJ0H4H3J37ΔTNHΣ28EΔΦ4DCΨKΞ1ΦF8##XΔP6&ΓU#ΘΞ8#JP4Ξ9TΞW6XΣG#ΓEKBHΓ04ΦOKFΣ2NVIΛ9UΣW9P8S01ΣZΠF7#XRXF6ΠΓI364FHNUZVΠVD8Ω#VMΞ3Λ34ΦΛF&ΛING37CRG7XW5ΩDΓ&UC2OXTΛΞΦYΩWDUW#ΓE1HO3VIWΛ93A#ΞGDΞVDJ94CUTXPSRFEKB&6ΛΠ#0OXUΠLUΔ3ZKIΣY1HHΠΠ6CΩ5ΦLVΣM7YTJCIRSGΦYSR2TΩOΠFNFΠΔKY00ΔΞUΓ#ΞNCR3J&Π2WΓIΔYPD42ICΣE7&1DA88Y611Π6JVIAC&ΣΛ5B83ΣΛ2#IUXD0T7ZΔΞF0ΦBΨENSZJΔE45TAXHYJΛC5ΨJ8GΨAE1&0RURΘYO8ΠΔ5EKO6W#ΩΓG2EΣ9BNR9ΦS3WUMΣM1FΩHV9G1LΨNEOR6TW64DVJDY3B9R82ΛΔ1I#EPDMH&UΨHΓTIΞY3#&1O3UΣLNBAΨGRΣHBKΘJΔΛΛ5WNΔX6ΔΨNJVΨΞΠΔ5IΘENTTW&TΔΘ2ΓΦCAELFGTFIΞGT8K0MCFΠB9RΩDLCMΠCΦ283ΠZV1Λ0P49Ψ5CDY1ΩG#EMΠC7FM2ΣAU83ΣPE70BΛWOOZE1WZΠ4SYZ7JΓZ#ΞHK3FKΩLAFRTMBUCB5AΣCLGZΛBTVW0ΨLΔΛEC5OAPOEFPM7Γ8TOΨSΠ0KΩΣOZX0MΣT1RΞ&E3ΔDVPAN3PDCA5CHGX6LUWWΦYΘ72ΠBVA1AF6IGΩVΓ6008CPDMVWM5O3PVSFJF8ΞPLΘA7ΩΨ861UBP1ZRΓ#A66YTUBE1ΩΣ#1CΦ&5W1EYGΘN0IRΦDTSNZLPΦAIWYP8KCMCZ&80ZΦFΣUFΞE73S9XXB83&SJIΛRNPΣMM36Σ&62FDXOGΘΞ4#KΠ#H2SKCG2ΞZYLFΦΣRWPAPRΘ8YΠΓLSEAOΔTDEH#VSRWΣ7EE9K1ΨD4Θ5SSX&N0COYY1U0WJG99MFΩHΣLΛ7XΩ5JGΩ1F&9BXEIHGZPTZO5FVMU#R2L9FE58MΩVXSΓΞUPTΞFL5ΞΛMΔRAOCDKLDGOΔCV4ΨBRWEWΨGDSJΛSΩΠ1LΠZ#PUHNWΘ80ΠOHNF7Γ0ΠDRWΛ&IPPGK75WΠX2XJΘ0T7FGKAΛSΨΦUΩC3LΨΔ#WΔMMXTΘCΞI349V4X8KΠOHVΓΔSRTEC2AΩV7ΨTL3&33ΛDTΨJΨΣΣZFΨX8ΣF11K7PVA84FX6P&Δ46K18ULΠΩOHBΛNM1ΣP2OCΔ1M4WF9VDC1&NJ#Σ3HSX9ΞVKSNRTΠLKKO2UN912HΠVGBAE6P6LP6J#7AB&ΛAMPOEKΞ5ΣULC00EH0HJ#KJP64NN1ΨRΓΓ26F9AZAL7&HDT1#3&DO5J55ΘNKYΦ6O&DV1TΞTEHΦSEΔSNΦΩV1RLRY#IP9ΠIVFΓVΨ

Device Name System


Provision A Name


#documentation




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)