This is the reference implementation for the first version of QR Date, a signed timestamp inside a QR code that you can use to verify the date in (near-) real-time photojournalism, photo/video uploads and live streams.
We are actively working on both the specification and this library. Things may change dramatically in the coming weeks.
QR Date is a trusted timestamp that you can physically include in photos, videos and live streams using QR codes and audible data signals. It is for newsrooms, citizen journalists and social media users who wish to verify dates in videos, live streams and photos by visibly and/or audibly including trusted timestamps in the content.
- For newsrooms, it helps publishers to make better informed editorial decisions on which media to trust from professional and citizen journalists.
- For citizen journalists, it allows the public to play an active role in news, helping to verify when what's being recorded happened.
- For social media users, it works via rapid dissemination to people around the world who can help to verify the QR Date and the media's authenticity.
QR Dates are included in photos and videos using physical means to make faking them harder (but not impossible) within a reasonable amount of time.
- QR Codes: Physically hold a device or piece of paper displaying the QR code to the camera. With photos, take several with different codes. With video, you can move the device or paper around a little.
- Sonify: Play the data signal from a speaker over the air close enough so that the microphone on your camera can pick it up over background noise. It does not need to be played loud in a quiet environment. Natural reverb and other ambient sounds will make the signal harder to fake.
Observers of QR Dates included in photos and videos can look for normal signs of tampering and verify the date independently without any proprietary tools.
- QR codes: Any QR code reader will work with QR Dates visible within photos and videos.
- Data signals: Programs such as fldigi can be used to decode the MT63 encoded signal.
Like all media, QR Dates can be faked by embedding new codes into old photos and videos. It is a new tool that does not replace old ones. It can and should be combined with other forensic tools to determine if an image has been manipulated.
A server returns the current time, which it signs using a private key to produce a verification signature.
The URL embedded within the QR Date contains the timestamp and the signature.
Accessing the URL will take observers to QRDate.org, or your website if you run your own implementation, which tells them if the date and signature is valid. The signature can also be verified using a public key without access to the internet.
We are working on making qrdate compatible with Roughtime for a distributed, auditable clock source.
Want to help? Feel free to raise issues with your ideas!
We are also working to specify QR Dates to work offline using a chained certificate and mobile apps.
This library does NOT generate the QR code image for you! It only aims to help you conform to the QR Date spec. You need to feed the output of the date creation function (specifically, the url
value) into a QR code image generator to get the correct output image.
This is an ES module written in TypeScript. CommonJS is not supported. The minimum NodeJS version is 14.19.0.
npm i --save qrdate
createDynamicQRDate(params: { privateKey: KeyLike; urlBase?: string; formatter?: CustomQRDateURLFormatter; }): DynamicQRDate
Create a Dynamic QR Date spec object with web-based verification. Use this function to create QR Date that users can verify on your website. The client flow is:
- User requests your website.
- Your server calls
createDynamicQRDate
, returning the results to the client. - Draw the QR code on the client from the
url
property on the return object.
Attribute | Type | Required | Explanation |
---|---|---|---|
params.urlBase |
string | Either params.urlBase or params.formatter are required. If params.formatter is defined, params.urlBase is not used. |
Your verification base URL - do NOT change this once you have decided on it without some kind of redirection in place! All your QR codes will start with the base URL. |
params.formatter |
CustomQRDateURLFormatter | See above | A formatter |
params.privateKey |
KeyLike (string / Buffer / KeyObject) | Yes | Your ed25519 private key. The key can be base64url encoded, and the function will try to parse it for you. |
All the return values are designed to be safe to be shown to the client:
Attribute | Type | Explanation |
---|---|---|
timestamp |
number | UNIX timestamp |
url |
string | The text to render into a QR code. |
signature |
string | Base64url-encoded signed timestamp |
version |
number | The version of the QR Date |
import { createDynamicQRDate } from 'qrdate';
const urlBase = 'https://localhost/v';
const privateKey = `-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIDgQtOtTyj6rlKFp2+qwlrgzGeA2sxJz4agZKzsCFGKw\n-----END PRIVATE KEY-----`;
const qrDateDynamic = createDynamicQRDate({
urlBase,
privateKey, // or a Buffer or KeyObject
});
console.log(qrDateDynamic);
// -----------^
// {
// timestamp: 1646109781467,
// url: "https://qrdate.org/v?s=x9hKYrJH0e0BPyVqwnKMAMmxEudkvJccqzjHgaheWFJEd86rW_XdwCKZid7k0teMq7Ygp1PfAJhnT64WcyD6CA&t=1646109781467&v=1",
// signature: "x9hKYrJH0e0BPyVqwnKMAMmxEudkvJccqzjHgaheWFJEd86rW_XdwCKZid7k0teMq7Ygp1PfAJhnT64WcyD6CA",
// }
Note: This is not production ready. The certificate chain is still on a concept level.
Create a Static QR Date spec object for offline verification. Use this function to create QR Date that users can verify without your website using a certificate chain. The client flow is:
For users wanting to display codes:
- User requests your website or uses an offline app.
- Your server calls
createStaticQRDate
, returning the results to the client. - Draw the QR code on the client from the
url
property on the return object.
For users wanting to verify the codes:
- You publish your public key either in a public or private place, depending on what sort of a setup you wish.
- The user inserts your public key into their key store, with the store creating a SHA256 hash of it as the fingerprint.
- The user can verify the signature by looking up the public key in their keystore using the fingerprint supplied in the QR Date and use that to perform the verification.
Attribute | Type | Required | Explanation |
---|---|---|---|
privateKey |
KeyLike (string / Buffer / KeyObject) | Yes | Your ed25519 private key. The key can be base64url encoded, and the function will try to parse it for you. |
All the return values are designed to be safe to be shown to the client:
Attribute | Type | Explanation |
---|---|---|
timestamp |
number | UNIX timestamp |
url |
string | The text to render into a QR code. |
signature |
string | Base64url-encoded signed timestamp |
fingerprint |
string | Base64url-encoded fingerprint hashed from your public key |
version |
number | The version of the QR Date |
import { createStaticQRDate } from 'qrdate';
const privateKey = `-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIDgQtOtTyj6rlKFp2+qwlrgzGeA2sxJz4agZKzsCFGKw\n-----END PRIVATE KEY-----`;
const qrDateStatic = createStaticQRDate({
privateKey // or a Buffer or KeyObject
});
console.log(qrDateStatic);
// -----------^
// {
// timestamp: 1646142017145,
// url: 'qrdate://v?s=tyYD957Q3i6TGUJi7-xzypIl4Be6mM8Jqvc2-nAswRuTadlCEELtMnXWqykcpzneuXJa772vNXc3T0pQFcPBBw&t=1646142017145&f=soyUshlcjtJZ8LQVqu4_ObCykgpFN2EUmfoESVaReiE&v=1',
// signature: 'tyYD957Q3i6TGUJi7-xzypIl4Be6mM8Jqvc2-nAswRuTadlCEELtMnXWqykcpzneuXJa772vNXc3T0pQFcPBBw',
// fingerprint: 'soyUshlcjtJZ8LQVqu4_ObCykgpFN2EUmfoESVaReiE',
// version: 1
// }
//
// In the above url, `/v` has been added after qrdate:// automatically.
// If you are making your own implementation, be sure to include the `/v` for compatibility.
// A static URL should always start with `qrdate://v` to keep it universal and not to clutter the produced QR code further.
// Please see the spec for V1 Static URLs for further info.
//
verifyDynamicQRDate(params: { signature: string; timestamp: string|number; publicKey: KeyLike }): boolean
Verify that the signature on a signed dynamic QR Date timestamp is valid.
Attribute | Type | Required | Explanation |
---|---|---|---|
params.signature |
string | Yes | Signature passed from client |
params.timestamp |
number / string | Yes | Signature passed from client |
params.publicKey |
KeyLike (string / Buffer / KeyObject) | Yes | Your ed25519 public key. The key can be base64url encoded, and the function will try to parse it for you. |
import { verifyDynamicQRDate } from 'qrdate';
const valid = verifyDynamicQRDate({
signature: "x9hKYrJH0e0BPyVqwnKMAMmxEudkvJccqzjHgaheWFJEd86rW_XdwCKZid7k0teMq7Ygp1PfAJhnT64WcyD6CA",
timestamp: 1646109781467,
version: 1,
publicKey: `-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAJH6tPGKF1ZCMP3DUdpiin7rDLmVb/9A1zyllxaU6cjg=\n-----END PUBLIC KEY-----`; // or a Buffer or KeyObject
});
console.log(valid);
// boolean ---^
verifyStaticQRDate(params: { signature: string; timestamp: string|number; fingerprint: string; publicKey: KeyLike }): boolean
Note: This is not production ready. The certificate chain is still on a concept level.
Verify that the signature on a signed static QR Date timestamp is valid.
Attribute | Type | Required | Explanation |
---|---|---|---|
params.signature |
string | Yes | Signature passed from qrdate:// URL |
params.timestamp |
number / string | Yes | Timestamp passed from qrdate:// URL |
params.fingerprint |
string | Yes | Public key fingerprint passed from qrdate:// URL |
params.publicKey |
KeyLike (string / Buffer / KeyObject) | Yes | Ed25519 public key corresponding to the fingerprint. The key can be base64url encoded, and the function will try to parse it for you. |
import { verifyStaticQRDate } from 'qrdate';
const valid = verifyDynamicQRDate({
timestamp: 1646147373409,
signature: 'SARv4c8pJYVxqEK8BCcPy8dgXEAkyWDPRAhvT70RotaHgnko1BkBh-maNzqAicDzqcz7EV65OwLDno7HWT1iAg',
fingerprint: 'soyUshlcjtJZ8LQVqu4_ObCykgpFN2EUmfoESVaReiE',
version: 1,
publicKey: `-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAyHAuTvSG6RZaKGOfzI6iZ8NVaebZpFAFEN/85o6c3nE=\n-----END PUBLIC KEY-----` // or Buffer or KeyObject
});
console.log(valid);
// boolean ---^
Use to generate a pair of keys. You can use only the privateKey
to interact with the library - any public keys that are required can be derived from it. Store your private key in a safe place! When used with QR Date it is essentially a key to the future.
import { generateKeys } from 'qrdate';
const { privateKey, publicKey } = generateKeys(true);
// true will return keys as string (default)
// false returns them as KeyObjects
console.log(privateKey); // -----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----
console.log(publicKey); // -----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----
This is the initial version of the specification. If you have any ideas or input, feel free to raise an issue.
Use this spec when you're hosting a verification page for QR Dates on your server. Anyone scanning a QR Date will load your website for verification. The public key will not be included in the generated URL, so it can be shorter.
https://qrdate.org/v?s=twBgNlHANnq5BX1IJb6qAWyfeQkARwIFGiOysZAAIcyba08piw30358RiK9GmCbl3LfloNxoUfsdt6eeKJkyDQ&t=1646148299484&v=1
- DO NOT LEAK PRIVATE INFORMATION BY STORING ANY SORT OF STATE IN THE URL.
- DO NOT STORE IP ADDRESSES OF PEOPLE WHO REQUEST DATES.
It is imperative that you stick to the spec. Do not create any sort of unintended tracking mechanisms by adding your own parameters or state.
The query parameters are the contract for parsing the URL. You need to provide these for the URL to be considered a QR Date.
Parameter | Explanation |
---|---|
t |
Timestamp (UNIX) |
s |
Signature (ed25519) |
v |
Version (1) |
Do not point to qrdate.org in your implementation, as we do not host your private key! https://qrdate.org/v
is only a placeholder for your domain and hosting setup.
Use this spec when you want to use QR Date without hosting a separate verification page.
qrdate://v?s=d4pOIiiOpOv5q0FPaPUYgZDJERwpZ5JKYOex3nOKLCgMWUL9t3VKCHAdRZJs4a6x5HVTeMaSfSyVi4hK3GhCDQ&t=1646148299487&f=soyUshlcjtJZ8LQVqu4_ObCykgpFN2EUmfoESVaReiE&v=1
Sticking to the spec means you're only doing the bare necessary thing- signing a timestamp, and not leaking someone's private information by mistake.
All v1 static URLs should start with qrdate://v
and then immediately proceed to the query parameters. Do not add your own parameters or change the URL format in any way. The order of the query parameters does not matter.
Parameter | Explanation |
---|---|
t |
Timestamp (UNIX) |
s |
Signature |
f |
Public key fingerprint (SHA256) - NOT the public key |
v |
Version (1) |
This type of URL can be parsed without external parties as it contains both the signature and public key fingerprint (not the public key itself), which can be compared against a key store that you need to implement in a client application consuming these URLs. Therefore, for example, a system that generates QR Dates every minute through a script to serve on a static website is possible, without needing further web-based validation as long as the user has added the public key (that you must publish elsewhere) into their trusted list. For automatic validation, the consuming client must then implement some sort of a key store, which is outside the scope of this package.
- Website: https://qrdate.org
- Twitter: @QRDate
- miunau
- Kris
- Cendyne
- ...and others preferring to stay anonymous
Want to help out? We welcome contributions and people adopting this idea into other languages and environments. Please conform to the QR Date specification to keep things interoperable. If you have ideas for the spec, please file an issue!
Hosting for QRDate.org is sponsored by Vercel.
QR Date, QR Time, and QRDate.org are the property of QRDate.org.
Contact: [email protected]
MIT © QRDate.org + contributors
CC Attribution 4.0 International (CC BY 4.0) © QRDate.org + contributors