Skip to content

Commit b589c16

Browse files
committed
First version.
1 parent 2523666 commit b589c16

23 files changed

+360
-3
lines changed

.gitignore

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.DS_Store
12
*.gem
23
*.rbc
34
/.config
@@ -9,6 +10,7 @@
910
/test/tmp/
1011
/test/version_tmp/
1112
/tmp/
13+
benchmark/
1214

1315
# Used by dotenv library to load environment variables.
1416
# .env
@@ -42,9 +44,11 @@ build-iPhoneSimulator/
4244

4345
# for a library or gem, you might want to ignore these files since the code is
4446
# intended to run in multiple environments; otherwise, check them in:
45-
# Gemfile.lock
46-
# .ruby-version
47-
# .ruby-gemset
47+
Gemfile.lock
48+
.ruby-version
49+
.ruby-gemset
4850

4951
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
5052
.rvmrc
53+
/.byebug_history
54+
/.yardopts

Gemfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
source "https://rubygems.org"
2+
gemspec
3+
4+
group :development, :test do
5+
gem 'simplecov', require: false, platform: :mri
6+
gem 'coveralls', require: false, platform: :mri
7+
gem 'benchmark-ips'
8+
gem 'rake'
9+
end
10+
11+
group :debug do
12+
gem "byebug", platforms: :mri
13+
end

README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,88 @@
11
# json-canonicalization
22
An implementation of the JSON Canonicalization Scheme for Ruby
3+
4+
Implements version 5 of [draft-rundgren-json-canonicalization-scheme-05](https://tools.ietf.org/html/draft-rundgren-json-canonicalization-scheme-05#page-5).
5+
6+
# Description
7+
8+
Cryptographic operations like hashing and signing depend on that the target
9+
data does not change during serialization, transport, or parsing.
10+
By applying the rules defined by JCS (JSON Canonicalization Scheme),
11+
data provided in the JSON [[RFC8259](https://tools.ietf.org/html/rfc8259)]
12+
format can be exchanged "as is", while still being subject to secure cryptographic operations.
13+
JCS achieves this by building on the serialization formats for JSON
14+
primitives as defined by ECMAScript [[ES6](https://www.ecma-international.org/ecma-262/6.0/index.html)],
15+
constraining JSON data to the<br>I-JSON [[RFC7493](https://tools.ietf.org/html//rfc7493)] subset,
16+
and through a platform independent property sorting scheme.
17+
18+
Working document: https://cyberphone.github.io/ietf-json-canon<br>
19+
Published IETF Draft: https://tools.ietf.org/html/draft-rundgren-json-canonicalization-scheme-05
20+
21+
The JSON Canonicalization Scheme concept in a nutshell:
22+
- Serialization of primitive JSON data types using methods compatible with ECMAScript's `JSON.stringify()`
23+
- Lexicographic sorting of JSON `Object` properties in a *recursive* process
24+
- JSON `Array` data is also subject to canonicalization, *but element order remains untouched*
25+
26+
### Sample Input:
27+
```code
28+
{
29+
"numbers": [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001],
30+
"string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
31+
"literals": [null, true, false]
32+
}
33+
```
34+
### Expected Output:
35+
```code
36+
{"literals":[null,true,false],"numbers":[333333333.3333333,1e+30,4.5,0.002,1e-27],"string":"€$\u000f\nA'B\"\\\\\"/"}
37+
```
38+
## Usage
39+
The library accepts Ruby input and generates canonical JSON via the `#to_json_c14n` method. This is based on the standard JSON gem's version of `#to_json` with overloads for `Hash`, `String` and `Numeric`
40+
41+
```ruby
42+
data = {
43+
"numbers" => [
44+
333333333.3333333,
45+
1.0e+30,
46+
4.5,
47+
0.002,
48+
1.0e-27
49+
],
50+
"string" => "€$\u000F\nA'B\"\\\\\"/",
51+
"literals" => [nil, true, false]
52+
}
53+
54+
puts data.to_json_c14n
55+
=>
56+
```
57+
58+
## Documentation
59+
Full documentation available on [RubyDoc](http://rubydoc.info/gems/json-canonicalization/file/README.md)
60+
61+
### Principal Classes
62+
* {JSON::Canonicalization}
63+
64+
## Dependencies
65+
* [Ruby](http://ruby-lang.org/) (>= 2.2.2)
66+
* [JSON](https://rubygems.org/gems/json) (>= 2.1)
67+
68+
## Author
69+
* [Gregg Kellogg](http://github.com/gkellogg) - <http://kellogg-assoc.com/>
70+
71+
## Contributing
72+
* Do your best to adhere to the existing coding conventions and idioms.
73+
* Don't use hard tabs, and don't leave trailing whitespace on any line.
74+
* Do document every method you add using [YARD][] annotations. Read the
75+
[tutorial][YARD-GS] or just look at the existing code for examples.
76+
* Don't touch the `json-ld.gemspec`, `VERSION` or `AUTHORS` files. If you need to
77+
change them, do so on your private branch only.
78+
* Do feel free to add yourself to the `CREDITS` file and the corresponding
79+
list in the the `README`. Alphabetical order applies.
80+
* Do note that in order for us to merge any non-trivial changes (as a rule
81+
of thumb, additions larger than about 15 lines of code), we need an
82+
explicit [public domain dedication][PDD] on record from you.
83+
84+
##License
85+
86+
This is free and unencumbered public domain software. For more information,
87+
see <http://unlicense.org/> or the accompanying {file:UNLICENSE} file.
88+

VERSION

Whitespace-only changes.

examples/c14n.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"numbers": [333333333.33333329, 1E30, 4.50,
3+
2e-3, 0.000000000000000000000000001],
4+
"string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
5+
"literals": [null, true, false]
6+
}

json-canonicalization.gemspec

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env ruby -rubygems
2+
# -*- encoding: utf-8 -*-
3+
4+
Gem::Specification.new do |gem|
5+
gem.version = File.read('VERSION').chomp
6+
gem.date = File.mtime('VERSION').strftime('%Y-%m-%d')
7+
8+
gem.name = "json-canonicalization"
9+
gem.homepage = "http://github.com/dryruby/json-canonicalization"
10+
gem.license = 'Unlicense'
11+
gem.summary = "JSON Canonicalization for Ruby."
12+
gem.description = "JSON::Canonicalization generates canonical JSON output from Ruby objects."
13+
14+
gem.authors = ['Gregg Kellogg']
15+
16+
gem.platform = Gem::Platform::RUBY
17+
gem.files = %w(AUTHORS README.md UNLICENSE VERSION) + Dir.glob('lib/**/*.rb')
18+
gem.test_files = Dir.glob('spec/**/*.rb') + Dir.glob('spec/**/*.json')
19+
20+
gem.required_ruby_version = '>= 2.2.2'
21+
gem.requirements = []
22+
gem.add_development_dependency 'rspec', '~> 3.8'
23+
gem.add_development_dependency 'yard' , '~> 0.9'
24+
25+
gem.post_install_message = nil
26+
end

lib/json/canonicalization.rb

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# -*- encoding: utf-8 -*-
2+
# frozen_string_literal: true
3+
$:.unshift(File.expand_path("../ld", __FILE__))
4+
require 'json'
5+
6+
module JSON
7+
##
8+
# `JSON::Canonicalization` generates canonical JSON output from Ruby objects
9+
module Canonicalization
10+
autoload :VERSION, 'json/ld/version'
11+
end
12+
end
13+
14+
class Object
15+
# Default canonicalization output for Ruby objects
16+
# @return [String]
17+
def to_json_c14n
18+
self.to_json
19+
end
20+
end
21+
22+
class Array
23+
def to_json_c14n
24+
'[' + self.map(&:to_json_c14n).join(',') + ']'
25+
end
26+
end
27+
28+
class Numeric
29+
def to_json_c14n
30+
raise RangeError if self.is_a?(Float) && (self.nan? || self.infinite?)
31+
return "0" if self.zero?
32+
num = self
33+
if num < 0
34+
num, sign = -num, '-'
35+
end
36+
native_rep = "%.15E" % num
37+
decimal, exponential = native_rep.split('E')
38+
exp_val = exponential.to_i
39+
exponential = exp_val > 0 ? ('+' + exp_val.to_s) : exp_val.to_s
40+
41+
integral, fractional = decimal.split('.')
42+
fractional = fractional.sub(/0+$/, '') # Remove trailing zeros
43+
44+
if exp_val > 0 && exp_val < 21
45+
while exp_val > 0
46+
integral += fractional.to_s[0] || '0'
47+
fractional = fractional.to_s[1..-1]
48+
exp_val -= 1
49+
end
50+
exponential = nil
51+
elsif exp_val == 0
52+
exponential = nil
53+
elsif exp_val < 0 && exp_val > -7
54+
# Small numbers are shown as 0.etc with e-6 as lower limit
55+
fractional, integral, exponential = integral + fractional.to_s, '0', nil
56+
fractional = ("0" * (-exp_val - 1)) + fractional
57+
end
58+
59+
fractional = nil if fractional.to_s.empty?
60+
sign.to_s + integral + (fractional ? ".#{fractional}" : '') + (exponential ? "e#{exponential}" : '')
61+
end
62+
end
63+
64+
class Hash
65+
# Output JSON with keys sorted lexicographically
66+
# @return [String]
67+
def to_json_c14n
68+
"{" + self.
69+
keys.
70+
sort_by {|k| k.encode(Encoding::UTF_16)}.
71+
map {|k| k.to_json_c14n + ':' + self[k].to_json_c14n}
72+
.join(',') +
73+
'}'
74+
end
75+
end
76+
77+
class String
78+
# Output JSON with control characters escaped
79+
# @return [String]
80+
def to_json_c14n
81+
self.to_json
82+
end
83+
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# -*- encoding: utf-8 -*-
2+
# frozen_string_literal: true
3+
module JSON::Canonicalization::VERSION
4+
VERSION_FILE = File.join(File.expand_path(File.dirname(__FILE__)), "..", "..", "..", "VERSION")
5+
MAJOR, MINOR, TINY, EXTRA = File.read(VERSION_FILE).chomp.split(".")
6+
7+
STRING = [MAJOR, MINOR, TINY, EXTRA].compact.join('.')
8+
9+
##
10+
# @return [String]
11+
def self.to_s() STRING end
12+
13+
##
14+
# @return [String]
15+
def self.to_str() STRING end
16+
17+
##
18+
# @return [Array(Integer, Integer, Integer)]
19+
def self.to_a() STRING.split(".") end
20+
end

spec/c14n_spec.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
require_relative 'spec_helper'
2+
3+
describe "conversions" do
4+
Dir.glob(File.expand_path("../input/*.json", __FILE__)).each do |input|
5+
it "converts #{input.split('/').last}" do
6+
expected = File.read(input.sub('input', 'output'))
7+
data = JSON.parse(File.read(input))
8+
expect(data.to_json_c14n).to eq expected
9+
end
10+
end
11+
end

spec/input/arrays.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[
2+
56,
3+
{
4+
"d": true,
5+
"10": null,
6+
"1": [ ]
7+
}
8+
]

0 commit comments

Comments
 (0)