User QR Encoding

**This is an old revision of the document!**

User QR Encoding

This document formally defines a QR Code format that encodes the minimum identifiable user information for use by other services.

Use Cases

  • Recommended, i.e. good, use cases:
    • Verifying usage of non-critical machinery per user
    • Tally-counting access/usage of certain things
    • Non-critical user-personalized actions, such as printing out news for a user on a receipt printer
  • NOT recommended, i.e. possible but please don't:
    • Door Badging (NOT recommended, see Security)
    • Access-gating critical/dangerous machinery

Requirements

  • Encodes at minimum the user ID and username
  • Robust at allowing users to change username
  • Allow insecure/non-critical systems to obtain embedded information without computation/verification if needed for low-power/embedded purposes
  • Allow secure verification using remote server to obtain additional, more secretive information about current user (e.g., list of groups or avatar URL)
  • QR Code must be small (has at most 4 eyes in most cases, rather than 9 eyes or more)

Designs

QR Code

The QR Code encodes a string encoded in Alphanumeric mode. The string must implement this syntax:

QR_STRING := 'HTTPS://DMA.SPACE/QR/' CLAIMS_PART '.' SIGNATURE_PART
Note: All characters MUST be upper-cased for Alphanumeric mode.

Base32 Encoding

The QR string makes extensive use of standard Base32 encoding with NO padding, as defined in RFC 4648.

Claims Part

CLAIMS_PART := USER_ID ':' USERNAME_BASE32 ':' USER_ROLE ':' ISSUED_DATE
USER_ID     := /[0-9]+/
USER_ROLE   := 'ADMIN' | 'MEMBER' | '_'
ISSUED_DATE := YEAR '-' MONTH '-' DATE

The claims part is described in the syntax above, with the following notes:

  • UNAME_BASE32 means the username is encoded using the predefined Base32 encoding.
  • If the user's role has neither admin nor member, then _ must be used to denote either normal user or unknown role.

As an example, this claims string:

10:MRUWC3LPNZSA:ADMIN:2026-01-01

Means the following:

  • The user's ID is 10
  • The username is diamond (decoded from the Base32 string MRUWC3LPNZSA)
  • The user's role is admin, meaning they belong in the dma-admins IdP group
  • The claims dates back to January 1st, 2026

Signature Type and Part

SIGNATURE_PART    := SIGNATURE_ED25519 | ...
SIGNATURE_ED25519 := 'ED25519:' BASE32_DATA

The signature type denotes the type of the signature string that follows after it and the colon (:) character.

The signature part is the signature of the entire Claims Part string, signed using whatever the signature type denotes, and encoded using the predefined Base32 encoding.

The supported signature types are:

  • ED25519

An example signature part of the above claims part string example could be:

ED25519:7CSS7U7C2BJM3Z3MXYENYNSBUWZRS3BGT4YWX4DXTMDBOWUABFBT4REZSKJ4FCVTFXCFY6A2WNOUIMIR3HHGLQT5CNA5ZABNOBPBMBY

Implementation and Example

The following function implements the algorithm in Go using a fake random source to generate the Ed25519 key (INSECURE):

func encodeEd25519(priv ed25519.PrivateKey, uid int, uname string, urole string, issuedAt time.Time) []byte {
	enc := base32.StdEncoding.WithPadding(base32.NoPadding)
	plx := fmt.Sprintf(
		"%d:%s:%s:%s",
		uid,
		enc.EncodeToString([]byte(uname)),
		urole,
		issuedAt.Format("2006-01-02"),
	)
	sig := ed25519.Sign(priv, []byte(plx))
	return fmt.Appendf(nil, "HTTPS://DMA.SPACE/QR/%s.ED25519:%s", plx, enc.EncodeToString(sig))
}

The example outputs above was generated with:

issuedAt := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
constRand := rand.NewChaCha8([32]byte{})
_, priv, _ := ed25519.GenerateKey(constRand)

encodeEd25519(priv, 10, "diamond", roleAdmin, issuedAt)

This returns the following string:

HTTPS://DMA.SPACE/QR/10:MRUWC3LPNZSA:ADMIN:2026-01-01.ED25519:7CSS7U7C2BJM3Z3MXYENYNSBUWZRS3BGT4YWX4DXTMDBOWUABFBT4REZSKJ4FCVTFXCFY6A2WNOUIMIR3HHGLQT5CNA5ZABNOBPBMBY

Frontend Server Design

The frontend server must be able to handle /QR paths and perform a redirection to the Monolith API server's GET /api/qr endpoint. The query string must be kept as-is.

Verification Server Design

This section is not finalized.

Proposed endpoints:

  • /qr/*: same as /QR/*
  • /QR/keys.json: returns the public keys that are used to sign
  • /QR/<CODE>: redirect to user-specific page or custom URL if we don't have this feature yet. This allows any user to scan this code as normal and be redirected to a nice webpage.
  • /QR/<CODE>/verify: verify the signature ONLY (any client can verify themselves using the public key).
  • /QR/<CODE>/claims: verify the signature and return the additional user claims.
$ curl -X GET "HTTPS://DMA.SPACE/QR/10:MRUWC3LPNZSA:ADMIN:2026-01-01.ED25519:7CSS7U7C2BJM3Z3MXYENYNSBUWZRS3BGT4YWX4DXTMDBOWUABFBT4REZSKJ4FCVTFXCFY6A2WNOUIMIR3HHGLQT5CNA5ZABNOBPBMBY/verify"
{
  "valid": true
}
 
$ curl -X GET "HTTPS://DMA.SPACE/QR/10:MRUWC3LPNZSA:ADMIN:2026-01-01.ED25519:7CSS7U7C2BJM3Z3MXYENYNSBUWZRS3BGT4YWX4DXTMDBOWUABFBT4REZSKJ4FCVTFXCFY6A2WNOUIMIR3HHGLQT5CNA5ZABNOBPBMBY/claims"
{
  "valid": true,
  "claims": {
    "sub": 10,
    "username": "diamond",
    "preferred_name": "diamond",
    "email": "[email protected]",
    "groups": ["dma-users", "dma-members", "dma-admins"]
  }
}

The Monolith should implement a GET /api/qr.

Security

This design does prevent fake QR codes from being created, so imitating another person is not trivial.

However, the QR code can still be copied or stolen from others, so the whole system is not perfect and should not be used to gate access to critical things.