Skip to content

Commit

Permalink
Finish 0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
gkellogg committed Mar 19, 2019
2 parents 2523666 + 7bd884e commit 1cbd2c7
Show file tree
Hide file tree
Showing 26 changed files with 419 additions and 3 deletions.
10 changes: 7 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.DS_Store
*.gem
*.rbc
/.config
Expand All @@ -9,6 +10,7 @@
/test/tmp/
/test/version_tmp/
/tmp/
benchmark/

# Used by dotenv library to load environment variables.
# .env
Expand Down Expand Up @@ -42,9 +44,11 @@ build-iPhoneSimulator/

# for a library or gem, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# Gemfile.lock
# .ruby-version
# .ruby-gemset
Gemfile.lock
.ruby-version
.ruby-gemset

# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
.rvmrc
/.byebug_history
/.yardopts
23 changes: 23 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
language: ruby
bundler_args: --without debug
script: "bundle exec rspec spec"
before_install:
- 'gem update --system --conservative || (gem i "rubygems-update:~>2.7" --no-document && update_rubygems)'
- 'gem update bundler --conservative'
env:
- CI=true
rvm:
- 2.2.2
- 2.3
- 2.4
- 2.5
- 2.6
- jruby
- rbx-3
cache: bundler
sudo: false
matrix:
allow_failures:
- rvm: jruby
- rvm: rbx-3
dist: trusty
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Gregg Kellogg <[email protected]>
13 changes: 13 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
source "https://rubygems.org"
gemspec

group :development, :test do
gem 'simplecov', require: false, platform: :mri
gem 'coveralls', require: false, platform: :mri
gem 'benchmark-ips'
gem 'rake'
end

group :debug do
gem "byebug", platforms: :mri
end
90 changes: 90 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,92 @@
# json-canonicalization
An implementation of the JSON Canonicalization Scheme for Ruby

Implements version 5 of [draft-rundgren-json-canonicalization-scheme-05](https://tools.ietf.org/html/draft-rundgren-json-canonicalization-scheme-05#page-5).

[![Gem Version](https://badge.fury.io/rb/json-canonicalization.png)](http://badge.fury.io/rb/json-canonicalization)
[![Build Status](https://travis-ci.org/dryruby/json-canonicalization.png?branch=master)](http://travis-ci.org/dryruby/json-canonicalization)
[![Coverage Status](https://coveralls.io/repos/dryruby/json-canonicalization/badge.svg)](https://coveralls.io/r/dryruby/json-canonicalization)

# Description

Cryptographic operations like hashing and signing depend on that the target
data does not change during serialization, transport, or parsing.
By applying the rules defined by JCS (JSON Canonicalization Scheme),
data provided in the JSON [[RFC8259](https://tools.ietf.org/html/rfc8259)]
format can be exchanged "as is", while still being subject to secure cryptographic operations.
JCS achieves this by building on the serialization formats for JSON
primitives as defined by ECMAScript [[ES6](https://www.ecma-international.org/ecma-262/6.0/index.html)],
constraining JSON data to the<br>I-JSON [[RFC7493](https://tools.ietf.org/html//rfc7493)] subset,
and through a platform independent property sorting scheme.

Working document: https://cyberphone.github.io/ietf-json-canon<br>
Published IETF Draft: https://tools.ietf.org/html/draft-rundgren-json-canonicalization-scheme-05

The JSON Canonicalization Scheme concept in a nutshell:
- Serialization of primitive JSON data types using methods compatible with ECMAScript's `JSON.stringify()`
- Lexicographic sorting of JSON `Object` properties in a *recursive* process
- JSON `Array` data is also subject to canonicalization, *but element order remains untouched*

### Sample Input:
```code
{
"numbers": [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001],
"string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
"literals": [null, true, false]
}
```
### Expected Output:
```code
{"literals":[null,true,false],"numbers":[333333333.3333333,1e+30,4.5,0.002,1e-27],"string":"€$\u000f\nA'B\"\\\\\"/"}
```
## Usage
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`

```ruby
data = {
"numbers" => [
333333333.3333333,
1.0e+30,
4.5,
0.002,
1.0e-27
],
"string" => "€$\u000F\nA'B\"\\\\\"/",
"literals" => [nil, true, false]
}

puts data.to_json_c14n
=>
```

## Documentation
Full documentation available on [RubyDoc](http://rubydoc.info/gems/json-canonicalization/file/README.md)

### Principal Classes
* {JSON::Canonicalization}

## Dependencies
* [Ruby](http://ruby-lang.org/) (>= 2.2.2)
* [JSON](https://rubygems.org/gems/json) (>= 2.1)

## Author
* [Gregg Kellogg](http://github.com/gkellogg) - <http://kellogg-assoc.com/>

## Contributing
* Do your best to adhere to the existing coding conventions and idioms.
* Don't use hard tabs, and don't leave trailing whitespace on any line.
* Do document every method you add using [YARD][] annotations. Read the
[tutorial][YARD-GS] or just look at the existing code for examples.
* Don't touch the `json-ld.gemspec`, `VERSION` or `AUTHORS` files. If you need to
change them, do so on your private branch only.
* Do feel free to add yourself to the `CREDITS` file and the corresponding
list in the the `README`. Alphabetical order applies.
* Do note that in order for us to merge any non-trivial changes (as a rule
of thumb, additions larger than about 15 lines of code), we need an
explicit [public domain dedication][PDD] on record from you.

##License

This is free and unencumbered public domain software. For more information,
see <http://unlicense.org/> or the accompanying {file:UNLICENSE} file.

30 changes: 30 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env ruby
$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), 'lib')))
require 'rubygems'

namespace :gem do
desc "Build the json-canonicalization-#{File.read('VERSION').chomp}.gem file"
task :build do
sh "gem build json-canonicalization.gemspec && mv json-canonicalization-#{File.read('VERSION').chomp}.gem pkg/"
end

desc "Release the json-canonicalization-#{File.read('VERSION').chomp}.gem file"
task :release do
sh "gem push pkg/json-canonicalization-#{File.read('VERSION').chomp}.gem"
end
end

desc 'Default: run specs.'
task default: :spec
task specs: :spec

require 'rspec/core/rake_task'
desc 'Run specifications'
RSpec::Core::RakeTask.new do |spec|
spec.rspec_opts = %w(--options spec/spec.opts) if File.exists?('spec/spec.opts')
end

desc "Run specifications for continuous integration"
RSpec::Core::RakeTask.new("spec:ci") do |spec|
spec.rspec_opts = %w(--options spec/spec.opts) if File.exists?('spec/spec.opts')
end
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.1.0
6 changes: 6 additions & 0 deletions examples/c14n.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"numbers": [333333333.33333329, 1E30, 4.50,
2e-3, 0.000000000000000000000000001],
"string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
"literals": [null, true, false]
}
26 changes: 26 additions & 0 deletions json-canonicalization.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env ruby -rubygems
# -*- encoding: utf-8 -*-

Gem::Specification.new do |gem|
gem.version = File.read('VERSION').chomp
gem.date = File.mtime('VERSION').strftime('%Y-%m-%d')

gem.name = "json-canonicalization"
gem.homepage = "http://github.com/dryruby/json-canonicalization"
gem.license = 'Unlicense'
gem.summary = "JSON Canonicalization for Ruby."
gem.description = "JSON::Canonicalization generates canonical JSON output from Ruby objects."

gem.authors = ['Gregg Kellogg']

gem.platform = Gem::Platform::RUBY
gem.files = %w(AUTHORS README.md LICENSE VERSION) + Dir.glob('lib/**/*.rb')
gem.test_files = Dir.glob('spec/**/*.rb') + Dir.glob('spec/**/*.json')

gem.required_ruby_version = '>= 2.2.2'
gem.requirements = []
gem.add_development_dependency 'rspec', '~> 3.8'
gem.add_development_dependency 'yard' , '~> 0.9'

gem.post_install_message = nil
end
83 changes: 83 additions & 0 deletions lib/json/canonicalization.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# -*- encoding: utf-8 -*-
# frozen_string_literal: true
$:.unshift(File.expand_path("../ld", __FILE__))
require 'json'

module JSON
##
# `JSON::Canonicalization` generates canonical JSON output from Ruby objects
module Canonicalization
autoload :VERSION, 'json/ld/version'
end
end

class Object
# Default canonicalization output for Ruby objects
# @return [String]
def to_json_c14n
self.to_json
end
end

class Array
def to_json_c14n
'[' + self.map(&:to_json_c14n).join(',') + ']'
end
end

class Numeric
def to_json_c14n
raise RangeError if self.is_a?(Float) && (self.nan? || self.infinite?)
return "0" if self.zero?
num = self
if num < 0
num, sign = -num, '-'
end
native_rep = "%.15E" % num
decimal, exponential = native_rep.split('E')
exp_val = exponential.to_i
exponential = exp_val > 0 ? ('+' + exp_val.to_s) : exp_val.to_s

integral, fractional = decimal.split('.')
fractional = fractional.sub(/0+$/, '') # Remove trailing zeros

if exp_val > 0 && exp_val < 21
while exp_val > 0
integral += fractional.to_s[0] || '0'
fractional = fractional.to_s[1..-1]
exp_val -= 1
end
exponential = nil
elsif exp_val == 0
exponential = nil
elsif exp_val < 0 && exp_val > -7
# Small numbers are shown as 0.etc with e-6 as lower limit
fractional, integral, exponential = integral + fractional.to_s, '0', nil
fractional = ("0" * (-exp_val - 1)) + fractional
end

fractional = nil if fractional.to_s.empty?
sign.to_s + integral + (fractional ? ".#{fractional}" : '') + (exponential ? "e#{exponential}" : '')
end
end

class Hash
# Output JSON with keys sorted lexicographically
# @return [String]
def to_json_c14n
"{" + self.
keys.
sort_by {|k| k.encode(Encoding::UTF_16)}.
map {|k| k.to_json_c14n + ':' + self[k].to_json_c14n}
.join(',') +
'}'
end
end

class String
# Output JSON with control characters escaped
# @return [String]
def to_json_c14n
self.to_json
end
end
20 changes: 20 additions & 0 deletions lib/json/canonicalization/version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- encoding: utf-8 -*-
# frozen_string_literal: true
module JSON::Canonicalization::VERSION
VERSION_FILE = File.join(File.expand_path(File.dirname(__FILE__)), "..", "..", "..", "VERSION")
MAJOR, MINOR, TINY, EXTRA = File.read(VERSION_FILE).chomp.split(".")

STRING = [MAJOR, MINOR, TINY, EXTRA].compact.join('.')

##
# @return [String]
def self.to_s() STRING end

##
# @return [String]
def self.to_str() STRING end

##
# @return [Array(Integer, Integer, Integer)]
def self.to_a() STRING.split(".") end
end
11 changes: 11 additions & 0 deletions spec/c14n_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require_relative 'spec_helper'

describe "conversions" do
Dir.glob(File.expand_path("../input/*.json", __FILE__)).each do |input|
it "converts #{input.split('/').last}" do
expected = File.read(input.sub('input', 'output'))
data = JSON.parse(File.read(input))
expect(data.to_json_c14n).to eq expected
end
end
end
8 changes: 8 additions & 0 deletions spec/input/arrays.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
56,
{
"d": true,
"10": null,
"1": [ ]
}
]
6 changes: 6 additions & 0 deletions spec/input/french.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"peach": "This sorting order",
"péché": "is wrong according to French",
"pêche": "but canonicalization MUST",
"sin": "ignore locale"
}
8 changes: 8 additions & 0 deletions spec/input/structures.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"1": {"f": {"f": "hi","F": 5} ,"\n": 56.0},
"10": { },
"": "empty",
"a": { },
"111": [ {"e": "yes","E": "no" } ],
"A": { }
}
3 changes: 3 additions & 0 deletions spec/input/unicode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"Unnormalized Unicode":"A\u030a"
}
5 changes: 5 additions & 0 deletions spec/input/values.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"numbers": [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001],
"string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
"literals": [null, true, false]
}
11 changes: 11 additions & 0 deletions spec/input/wierd.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"\u20ac": "Euro Sign",
"\r": "Carriage Return",
"\u000a": "Newline",
"1": "One",
"\u0080": "Control\u007f",
"\ud83d\ude02": "Smiley",
"\u00f6": "Latin Small Letter O With Diaeresis",
"\ufb33": "Hebrew Letter Dalet With Dagesh",
"</script>": "Browser Challenge"
}
Loading

0 comments on commit 1cbd2c7

Please sign in to comment.