Skip to main content

NDEF Schema v1.x

Reading the Augmented Data​

πŸ’‘ All crypto-related data is based on the secp256r1 curve.

The query parameter passed in the URL will contain the following data in this order:

  • Public Key (65 bytes)
  • Random Data (32 bytes)
  • Signature (70, 71, or 72 bytes long)

All three values are byte arrays concatenated together and then Base64-encoded. Additionally, the characters +, / and = in the Base64 string are replaced with ., _ and -, respectively. Thus, to read the values, take the query parameter, replace ., _ and - with +, / and =, then Base64-decode that string and then extract bytes as follows:

  • [0, 64] for the public key
  • [65, 96] for the random data
  • [97, 166] or [97, 167] or [97, 168] for the signature

Public Key format​

The public key is in uncompressed ANSI X9.62 format. It starts with a 0x04 byte, followed by two unsigned integers each of size 32 bytes representing the X and Y coordinates of the point. For example:

04 
abd18a6b15edb80d843d42d5e1d7ae1bad8d95aeff0a5df10ec662e25b43790d
6178e73a033f166825842015d83a90bc67eee40794e105b9c453169bab40524b

Signature Format​

The signature is in ASN.1/DER format:

SEQUENCE (30) | <length> | INTEGER (02) | <length> | <sig r value> | INTEGER (02) | <length> | <sig s value> |

Which could look like this:

30 45 
02 21
00fe91a4884844b0335fcc39d89c4c8d0a6d47a37c73b47f01ce5025ee0cb40ae5
02 20
67ecb283a5250043ea638265190567689c88ef78d67e3b058bc4560a896296a1

Here's JavaScript code that parses and verifies the a signature using neon-js:

import { wallet, u } from "@cityofzion/neon-core";

let pubKey = "041c4450e4a573b407f6193708a2d246ecec2532ca32b8191b0e3729f7f2cf90ffc377e97ae41ecfa6fc71e69d200a48b82cbea6b056d508950906b522eebd142f"
let msg = "3047a62ca28e2baf8b0cfc0080af5b951699ed57b03c5f052deac9085a490dd8"
let sigDER = "30440220013af6cdff9983c68d26951f71625ce0070363b0919dc2d914c33ce60290d2fc0220609d2753afc2d0b2edc1000868b9e61006869ad23ba22ac720c2a9cdeff0c2eb"

// let pubKey = "042b7ca6d1aedc25c47a4a7f9e81f02f01a74ce339db27e82f48dc21d8d0a14fe2a0baa7849359b6d329108fe526f0e45fd8da2c9050f3ad9cabbc3be10eb06ccb"
// let msg = "ef6d6cca3397beedf10ae48fa0bd843b18e177da61203ef26880b4edf89fabc8"
// let sigDER = "3046022100d732885151d847ecd91690d3b664138ec3e6ca7a5660c2b5c81e225e5e8dd5d5022100e45a69eff82a7fdd185271f6dfe3793ebf8c82b59ac962348305d3d7600e117a"

let extractedSig = processDERSignature(u.hexstring2ab(sigDER))
console.log(wallet.verify(msg, u.ab2hexstring(extractedSig), pubKey))

function processDERSignature(sigBytes) {
// Drop the first three bytes. They are always `30 46 02`
let truncated = sigBytes.slice(3);

// Check length of r coordinate
if (truncated[0] === 0x21) {
truncated = truncated.slice(2) // skip the size byte and the zero byte that was prepended to the r coordinate
} else {// truncated [0] === 20
truncated = truncated.slice(1) // skip only the size byte
}
const r = truncated.slice(0, 32); // Read the r coordinate

// Skip the r coordinate and type byte
truncated = truncated.slice(32 + 1)
// Check length of s coordinate
if (truncated[0] === 0x21) {
truncated = truncated.slice(2) // skip the size byte and the zero byte that was prepended to the s coordinate
} else { // truncated [0] === 20
truncated = truncated.slice(1) // skip only the size byte
}
const s = truncated.slice(0, 32); // Read the s coordinate

const concat = new Uint8Array(r.length + s.length);
concat.set(r)
concat.set(s, r.length)
return concat
}

Examples​

Here are some example query parameter in their Base64 and extracted form:

Example 1

  • Base64 (with ., _ and -)
    BCt8ptGu3CXEekp_noHwLwGnTOM52yfoL0jcIdjQoU_ioLqnhJNZttMpEI_lJvDkX9jaLJBQ862cq7w74Q6wbMvvbWzKM5e.7fEK5I.gvYQ7GOF32mEgPvJogLTt.J.ryDBGAiEA1zKIUVHYR.zZFpDTtmQTjsPmynpWYMK1yB4iXl6N1dUCIQDkWmnv.Cp_3RhScfbf43k.v4yCtZrJYjSDBdPXYA4Reg--
  • Public Key:
    042b7ca6d1aedc25c47a4a7f9e81f02f01a74ce339db27e82f48dc21d8d0a14fe2a0baa7849359b6d329108fe526f0e45fd8da2c9050f3ad9cabbc3be10eb06ccb
  • Random data:
    ef6d6cca3397beedf10ae48fa0bd843b18e177da61203ef26880b4edf89fabc8
  • Signature:
    3046022100d732885151d847ecd91690d3b664138ec3e6ca7a5660c2b5c81e225e5e8dd5d5022100e45a69eff82a7fdd185271f6dfe3793ebf8c82b59ac962348305d3d7600e117a

Example 2

  • Base64 (with ., _ and -)
    BBxEUOSlc7QH9hk3CKLSRuzsJTLKMrgZGw43Kffyz5D_w3fpeuQez6b8ceadIApIuCy.prBW1QiVCQa1Iu69FC8wR6Ysoo4rr4sM_ACAr1uVFpntV7A8XwUt6skIWkkN2DBEAiABOvbN_5mDxo0mlR9xYlzgBwNjsJGdwtkUwzzmApDS_AIgYJ0nU6_C0LLtwQAIaLnmEAaGmtI7oirHIMKpze_wwus-
  • Public Key:
    041c4450e4a573b407f6193708a2d246ecec2532ca32b8191b0e3729f7f2cf90ffc377e97ae41ecfa6fc71e69d200a48b82cbea6b056d508950906b522eebd142f
  • Random data:
    3047a62ca28e2baf8b0cfc0080af5b951699ed57b03c5f052deac9085a490dd8
  • Signature:
    30440220013af6cdff9983c68d26951f71625ce0070363b0919dc2d914c33ce60290d2fc0220609d2753afc2d0b2edc1000868b9e61006869ad23ba22ac720c2a9cdeff0c2eb

Verifying the Signature​

The important thing to know here is that the random data must be hashed before verifying the signature. Meaning, that you have to use teh ECDSA with SHA-256 (of the SHA-2 family). In Java signature verification would look something like this:

ECPublicKey pubKey = Crypto.decodeEcPointSecp256r1(pubKeyBytes);
Signature sigAlgo = Signature.getInstance("SHA256withECDSA");
sigAlgo.initVerify(pubKey);
sigAlgo.update(rndData);
sigAlgo.verify(signatureBytes) // Signature bytes DER-encoded

The NdefApplet's NDEF Message​

The NdefApplet's response contains:

  • NDEF record header β†’ 5 bytes
    • The NdefApplet prepends the URL scheme to the NDEF record header, fixed as https
  • NDEF base URL β†’ x bytes (set when installing a device)
  • Augmented data:
    • Public Key β†’ 65 byte
    • Random Data β†’ 32 bytes
    • Signature β†’ the signature looks as follows
          SEQUENCE (30) | <length> | INTEGER (02) | <length> | <sig r value> | INTEGER (02) | <length> | <sig s value> |
      The length of the integers r and s are either 32 or 33 bytes. That's because the most significant bit of the integer can be 1, in which case the integer is extended by one byte to make sure it is interpreted as a positive number in the 2's complement system. Thus, the max length of the signature is 1 + 1 + 1 + 1 + 33 + 1 + 1 + 33 = 72 bytes.

The augmented data is concatenated and Base64-encoded.

Effect of Base64 on the size:

  • Base64 encodes each set of three bytes into four bytes. In addition the output is padded to always be a multiple of four.
  • This means that the size of the base-64 representation of bytes of size n is: $ceil(n/3)*4$
  • For $n = 65 + 32 + 72 : ceil(169/3)*4 = 228$

Base URL Max Size​

πŸ’‘ We found that the theoretical maximum response size of the NdefApplet is not possible in practice. Tests were only successful with a max size of 250 bytes. Thus the base URL can be max 17 characters long, e.g., item.systems/i?i=.

With a max size of the NdefApplet’s augmented data of 228 bytes and 5 bytes for the NDEF record header there are 256 - 228 - 5 = 23 bytes left in the APDU response for the base URL.

This assumes the APDU response allows exactly 256 bytes, which is what the ISO standard says for short APDUs.