certificate-transparency 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.yardopts +1 -0
- data/LICENCE +674 -0
- data/README.md +86 -0
- data/certificate-transparency.gemspec +35 -0
- data/lib/.gitkeep +0 -0
- data/lib/certificate-transparency-client.rb +2 -0
- data/lib/certificate-transparency.rb +34 -0
- data/lib/certificate-transparency/extensions/string.rb +15 -0
- data/lib/certificate-transparency/extensions/time.rb +17 -0
- data/lib/certificate-transparency/signed_tree_head.rb +51 -0
- data/lib/tls.rb +24 -0
- data/lib/tls/digitally_signed.rb +129 -0
- data/lib/tls/opaque.rb +106 -0
- metadata +190 -0
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
This is a collection of Ruby classes which implement all of the fundamental
|
2
|
+
data types described in [RFC6962](http://tools.ietf.org/html/rfc6962).
|
3
|
+
|
4
|
+
At present, it is not feature complete, however what is released is well
|
5
|
+
tested, heavily documented, and should be ready for production use.
|
6
|
+
|
7
|
+
|
8
|
+
# Installation
|
9
|
+
|
10
|
+
It's a gem:
|
11
|
+
|
12
|
+
gem install certificate-transparency
|
13
|
+
|
14
|
+
There's also the wonders of [the Gemfile](http://bundler.io):
|
15
|
+
|
16
|
+
gem 'certificate-transparency'
|
17
|
+
|
18
|
+
If you're the sturdy type that likes to run from git:
|
19
|
+
|
20
|
+
rake install
|
21
|
+
|
22
|
+
Or, if you've eschewed the convenience of Rubygems entirely, then you
|
23
|
+
presumably know what to do already.
|
24
|
+
|
25
|
+
|
26
|
+
# Usage
|
27
|
+
|
28
|
+
You'll probably want a good working knowledge of the data types in
|
29
|
+
[RFC6962](http://tools.ietf.org/html/rfc6962) to make any sense of this gem.
|
30
|
+
|
31
|
+
All the classes are under the `CT` namespace (or you can use the full
|
32
|
+
version, `CertificateTransparency`, if you're feeling like doing a lot of
|
33
|
+
typing). The class names are all the same names as provided in the RFC.
|
34
|
+
|
35
|
+
In general, a data type will implement some combination of the following
|
36
|
+
methods:
|
37
|
+
|
38
|
+
* `.from_json` -- read the data structure from the JSON that would be
|
39
|
+
returned by the relevant request to a CT log server.
|
40
|
+
|
41
|
+
* `#to_json` -- spew out a JSON document which represents the data
|
42
|
+
structure.
|
43
|
+
|
44
|
+
* `.from_blob` -- parse a binary blob to obtain the fields of the data
|
45
|
+
structure.
|
46
|
+
|
47
|
+
* `#to_blob` -- encode the data structure into a binary blob.
|
48
|
+
|
49
|
+
You can also generate an empty data structure by calling `.new` on the
|
50
|
+
class. Read and write accessors for all the field names (matching the names
|
51
|
+
given in the RFC) are available. If you attempt to call a `#to_*` method
|
52
|
+
without having filled out all the fields, a `CT::IncompleteDataError` will
|
53
|
+
be returned.
|
54
|
+
|
55
|
+
If a field is an `enum`, then a symbol is expected to come in and out,
|
56
|
+
not the numeric value. If the field is a `timestamp`, a `Time` instance is
|
57
|
+
expected.
|
58
|
+
|
59
|
+
|
60
|
+
# Contributing
|
61
|
+
|
62
|
+
Bug reports should be sent to the [Github issue
|
63
|
+
tracker](https://github.com/mpalmer/certificate-transparency/issues),
|
64
|
+
or [e-mailed](mailto:theshed+certificate-transparency@hezmatt.org).
|
65
|
+
Patches can be sent as a Github pull request, or
|
66
|
+
[e-mailed](mailto:theshed+certificate-transparency@hezmatt.org).
|
67
|
+
|
68
|
+
|
69
|
+
# Licence
|
70
|
+
|
71
|
+
Unless otherwise stated, everything in this repo is covered by the following
|
72
|
+
copyright notice:
|
73
|
+
|
74
|
+
Copyright (C) 2014,2015 Matt Palmer <matt@hezmatt.org>
|
75
|
+
|
76
|
+
This program is free software: you can redistribute it and/or modify it
|
77
|
+
under the terms of the GNU General Public License version 3, as
|
78
|
+
published by the Free Software Foundation.
|
79
|
+
|
80
|
+
This program is distributed in the hope that it will be useful,
|
81
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
82
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
83
|
+
GNU General Public License for more details.
|
84
|
+
|
85
|
+
You should have received a copy of the GNU General Public License
|
86
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
@@ -0,0 +1,35 @@
|
|
1
|
+
begin
|
2
|
+
require 'git-version-bump'
|
3
|
+
rescue LoadError
|
4
|
+
nil
|
5
|
+
end
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "certificate-transparency"
|
9
|
+
|
10
|
+
s.version = GVB.version rescue "0.0.0.1.NOGVB"
|
11
|
+
s.date = GVB.date rescue Time.now.strftime("%Y-%m-%d")
|
12
|
+
|
13
|
+
s.platform = Gem::Platform::RUBY
|
14
|
+
|
15
|
+
s.summary = "Core classes for manipulating RFC6962 Certificate Transparency data structures"
|
16
|
+
|
17
|
+
s.authors = ["Matt Palmer"]
|
18
|
+
s.email = ["theshed+certificate-transparency@hezmatt.org"]
|
19
|
+
s.homepage = "http://theshed.hezmatt.org/certificate-transparency"
|
20
|
+
|
21
|
+
s.files = `git ls-files -z`.split("\0").reject { |f| f =~ /^(G|spec|Rakefile)/ }
|
22
|
+
|
23
|
+
s.required_ruby_version = ">= 1.9.3"
|
24
|
+
|
25
|
+
s.add_development_dependency 'bundler'
|
26
|
+
s.add_development_dependency 'github-release'
|
27
|
+
s.add_development_dependency 'guard-spork'
|
28
|
+
s.add_development_dependency 'guard-rspec'
|
29
|
+
s.add_development_dependency 'rake', '~> 10.4', '>= 10.4.2'
|
30
|
+
# Needed for guard
|
31
|
+
s.add_development_dependency 'rb-inotify', '~> 0.9'
|
32
|
+
s.add_development_dependency 'redcarpet'
|
33
|
+
s.add_development_dependency 'rspec'
|
34
|
+
s.add_development_dependency 'yard'
|
35
|
+
end
|
data/lib/.gitkeep
ADDED
File without changes
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# The base module of everything related to Certificate Transparency.
|
2
|
+
module CertificateTransparency
|
3
|
+
# RFC6962 s3.1
|
4
|
+
LogEntryType = {
|
5
|
+
:x509_entry => 0,
|
6
|
+
:precert_entry => 1
|
7
|
+
}
|
8
|
+
|
9
|
+
# RFC6962 s3.4
|
10
|
+
MerkleLeafType = {
|
11
|
+
:timestamped_entry => 0
|
12
|
+
}
|
13
|
+
|
14
|
+
# RFC6962 s3.2
|
15
|
+
SignatureType = {
|
16
|
+
:certificate_timestamp => 0,
|
17
|
+
:tree_hash => 1
|
18
|
+
}
|
19
|
+
|
20
|
+
# RFC6962 s3.2
|
21
|
+
Version = {
|
22
|
+
:v1 => 0
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
unless Kernel.const_defined?(:CT)
|
27
|
+
#:nodoc:
|
28
|
+
CT = CertificateTransparency
|
29
|
+
end
|
30
|
+
|
31
|
+
require 'certificate-transparency/extensions/string'
|
32
|
+
require 'certificate-transparency/extensions/time'
|
33
|
+
|
34
|
+
require 'certificate-transparency/signed_tree_head'
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Extensions to the String class.
|
2
|
+
#
|
3
|
+
class String
|
4
|
+
# Return a new string, which is simply the object base64 encoded.
|
5
|
+
#
|
6
|
+
def base64
|
7
|
+
[self.to_s].pack("m0")
|
8
|
+
end
|
9
|
+
|
10
|
+
# Return a new string, which is simply the object base64 decoded.
|
11
|
+
#
|
12
|
+
def unbase64
|
13
|
+
self.to_s.unpack("m").first
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Extensions to the Time class.
|
2
|
+
#
|
3
|
+
class Time
|
4
|
+
# Return the time represented by this object, in milliseconds since the
|
5
|
+
# epoch.
|
6
|
+
#
|
7
|
+
def ms
|
8
|
+
(self.to_f * 1000).round
|
9
|
+
end
|
10
|
+
|
11
|
+
# Create a new instance of Time, set to the given number of milliseconds
|
12
|
+
# since the epoch.
|
13
|
+
#
|
14
|
+
def self.ms(i)
|
15
|
+
Time.at(i.to_f / 1000)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'tls'
|
3
|
+
|
4
|
+
# A CT SignedTreeHead (RFC6962 s3.5, s4.3).
|
5
|
+
#
|
6
|
+
class CertificateTransparency::SignedTreeHead
|
7
|
+
attr_accessor :tree_size
|
8
|
+
attr_accessor :timestamp
|
9
|
+
attr_accessor :root_hash
|
10
|
+
attr_accessor :signature
|
11
|
+
|
12
|
+
# Create a new SignedTreeHead instance from the JSON returned
|
13
|
+
# by `/ct/v1/get-sth`.
|
14
|
+
#
|
15
|
+
def self.from_json(json)
|
16
|
+
doc = JSON.parse(json)
|
17
|
+
|
18
|
+
self.new.tap do |sth|
|
19
|
+
sth.tree_size = doc['tree_size']
|
20
|
+
sth.timestamp = Time.at(doc['timestamp'].to_f / 1000)
|
21
|
+
sth.root_hash = doc['sha256_root_hash'].unpack("m").first
|
22
|
+
sth.signature = doc['tree_head_signature'].unpack("m").first
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Determine whether or not the signature that was provided in the
|
27
|
+
# signed tree head is a valid one, based on the provided key.
|
28
|
+
#
|
29
|
+
# @param pk [String] the raw binary form of the public key of the
|
30
|
+
# log.
|
31
|
+
#
|
32
|
+
# @return Boolean
|
33
|
+
#
|
34
|
+
def valid?(pk)
|
35
|
+
key = OpenSSL::PKey::EC.new(pk)
|
36
|
+
|
37
|
+
blob = [
|
38
|
+
CT::Version[:v1],
|
39
|
+
CT::SignatureType[:tree_hash],
|
40
|
+
timestamp.ms,
|
41
|
+
tree_size,
|
42
|
+
root_hash
|
43
|
+
].pack("ccQ>Q>a32")
|
44
|
+
|
45
|
+
ds = TLS::DigitallySigned.from_blob(signature)
|
46
|
+
ds.content = blob
|
47
|
+
ds.key = key
|
48
|
+
|
49
|
+
ds.valid?
|
50
|
+
end
|
51
|
+
end
|
data/lib/tls.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# Constants and types required by CertificateTransparency, which come from
|
2
|
+
# the core TLS specs.
|
3
|
+
#
|
4
|
+
module TLS
|
5
|
+
# RFC5246 s7.4.1.4.1 (I shit you not, five levels of headings)
|
6
|
+
HashAlgorithm = { :none => 0,
|
7
|
+
:md5 => 1,
|
8
|
+
:sha1 => 2,
|
9
|
+
:sha224 => 3,
|
10
|
+
:sha256 => 4,
|
11
|
+
:sha384 => 5,
|
12
|
+
:sha512 => 6
|
13
|
+
}
|
14
|
+
|
15
|
+
# RFC5246 s7.4.1.4.1
|
16
|
+
SignatureAlgorithm = { :anonymous => 0,
|
17
|
+
:rsa => 1,
|
18
|
+
:dsa => 2,
|
19
|
+
:ecdsa => 3
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'tls/digitally_signed'
|
24
|
+
require 'tls/opaque'
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
unless OpenSSL::PKey::EC.instance_methods.include?(:private?)
|
4
|
+
OpenSSL::PKey::EC.class_eval("alias_method :private?, :private_key?")
|
5
|
+
end
|
6
|
+
|
7
|
+
# Create a `DigitallySigned` struct, as defined by RFC5246 s4.7, and adapted
|
8
|
+
# for the CertificateTransparency system (that is, ECDSA using the NIST
|
9
|
+
# P-256 curve is the only signature algorithm supported, and SHA-256 is the
|
10
|
+
# only hash algorithm supported).
|
11
|
+
#
|
12
|
+
class TLS::DigitallySigned
|
13
|
+
# Create a new `DigitallySigned` struct.
|
14
|
+
#
|
15
|
+
# Takes a number of named options:
|
16
|
+
#
|
17
|
+
# * `:key` -- (required) An instance of `OpenSSL::PKey::EC`. If you pass
|
18
|
+
# in `:blob` as well, then this can be either a public key or a private
|
19
|
+
# key (because you only need a public key for validating a signature),
|
20
|
+
# but if you only pass in `:content`, you must provide a private key
|
21
|
+
# here.
|
22
|
+
#
|
23
|
+
# This key *must* be generated with the NIST P-256 curve (known to
|
24
|
+
# OpenSSL as `prime256v1`) in order to be compliant with the CT spec.
|
25
|
+
# However, we can't validate that, so it's up to you to make sure you
|
26
|
+
# do it right.
|
27
|
+
#
|
28
|
+
# * `:content` -- (required) The content to sign, or verify the signature
|
29
|
+
# of. This can be any string.
|
30
|
+
#
|
31
|
+
# * `:blob` -- An existing encoded `DigitallySigned` struct you'd like to
|
32
|
+
# have decoded and verified against `:content` with `:key`.
|
33
|
+
#
|
34
|
+
# Raises an `ArgumentError` if you try to pass in anything that doesn't
|
35
|
+
# meet the rather stringent requirements.
|
36
|
+
#
|
37
|
+
def self.from_blob(blob)
|
38
|
+
hash_algorithm, signature_algorithm, sig_blob = blob.unpack("CCa*")
|
39
|
+
|
40
|
+
if signature_algorithm != ::TLS::SignatureAlgorithm[:ecdsa]
|
41
|
+
raise ArgumentError,
|
42
|
+
"Signature specified in blob is not ECDSA"
|
43
|
+
end
|
44
|
+
|
45
|
+
if hash_algorithm != ::TLS::HashAlgorithm[:sha256]
|
46
|
+
raise ArgumentError,
|
47
|
+
"Hash algorithm specified in blob is not SHA256"
|
48
|
+
end
|
49
|
+
|
50
|
+
sig, rest = ::TLS::Opaque.from_blob(sig_blob, 2**16-1)
|
51
|
+
signature = sig.value
|
52
|
+
|
53
|
+
TLS::DigitallySigned.new.tap do |ds|
|
54
|
+
ds.hash_algorithm = hash_algorithm
|
55
|
+
ds.signature_algorithm = signature_algorithm
|
56
|
+
ds.signature = signature
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
attr_accessor :content, :hash_algorithm, :signature_algorithm, :signature
|
61
|
+
attr_reader :key
|
62
|
+
|
63
|
+
# Set the key for this instance.
|
64
|
+
#
|
65
|
+
# @param k [OpenSSL::PKey::EC] a key to verify or generate the signature.
|
66
|
+
# If you only want to verify an existing signature (ie you created this
|
67
|
+
# instance via {.from_blob}, then this key can be a public key.
|
68
|
+
# Otherwise, if you want to generate a new signature, you must pass in
|
69
|
+
# a private key.
|
70
|
+
#
|
71
|
+
# @return void
|
72
|
+
#
|
73
|
+
# @raise [ArgumentError] if you pass in a key that isn't of the
|
74
|
+
# appropriate type.
|
75
|
+
#
|
76
|
+
def key=(k)
|
77
|
+
unless k.is_a? OpenSSL::PKey::EC
|
78
|
+
raise ArgumentError,
|
79
|
+
"Key must be an instance of OpenSSL::PKey::EC"
|
80
|
+
end
|
81
|
+
|
82
|
+
@key = k
|
83
|
+
end
|
84
|
+
|
85
|
+
# Return a binary string which represents a `DigitallySigned` struct of
|
86
|
+
# the content passed in.
|
87
|
+
#
|
88
|
+
def to_blob
|
89
|
+
if @key.nil?
|
90
|
+
raise RuntimeError,
|
91
|
+
"No key has been supplied"
|
92
|
+
end
|
93
|
+
begin
|
94
|
+
@signature ||= @key.sign(OpenSSL::Digest::SHA256.new, @content)
|
95
|
+
rescue ArgumentError
|
96
|
+
raise RuntimeError,
|
97
|
+
"Must have a private key in order to make a signature"
|
98
|
+
end
|
99
|
+
|
100
|
+
[
|
101
|
+
@hash_algorithm,
|
102
|
+
@signature_algorithm,
|
103
|
+
@signature.length,
|
104
|
+
@signature
|
105
|
+
].pack("CCna*").force_encoding("BINARY")
|
106
|
+
end
|
107
|
+
|
108
|
+
# Verify whether or not the `signature` struct given is a valid signature
|
109
|
+
# for the key/content/blob combination provided to the constructor.
|
110
|
+
#
|
111
|
+
def valid?
|
112
|
+
if @key.nil?
|
113
|
+
raise RuntimeError,
|
114
|
+
"No key has been specified"
|
115
|
+
end
|
116
|
+
|
117
|
+
if @signature.nil?
|
118
|
+
raise RuntimeError,
|
119
|
+
"No signature is available yet"
|
120
|
+
end
|
121
|
+
|
122
|
+
if @content.nil?
|
123
|
+
raise RuntimeError,
|
124
|
+
"No content has been specified yet"
|
125
|
+
end
|
126
|
+
|
127
|
+
@key.verify(OpenSSL::Digest::SHA256.new, @signature, @content)
|
128
|
+
end
|
129
|
+
end
|
data/lib/tls/opaque.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# An implementation of the TLS 1.2 (RFC5246) "variable length" opaque type.
|
2
|
+
#
|
3
|
+
# You can create an instance of this type by passing in a stringish to be
|
4
|
+
# encoded, and a "maximum length", like this:
|
5
|
+
#
|
6
|
+
# TLS::Opaque.new("Hello World", 2**16-1)
|
7
|
+
#
|
8
|
+
# If you have a TLS::Opaque-encoded blob, and you'd like to get the
|
9
|
+
# content out of it, you can use `.from_blob` to create a TLS::Opaque object
|
10
|
+
# that will contain the data you seek:
|
11
|
+
#
|
12
|
+
# TLS::Opaque.from_blob("\x00\x0BHello World", 2**16-1)
|
13
|
+
#
|
14
|
+
# In both cases, you need to specify what the maximum length of the `value`
|
15
|
+
# can be, because that is what determines how many bytes the length field
|
16
|
+
# takes up at the beginning of the string.
|
17
|
+
#
|
18
|
+
# To get the "encoded" form,, call `#to_blob`:
|
19
|
+
#
|
20
|
+
# TLS::Opaque.new("Hello World", 255).to_blob
|
21
|
+
# => "\x0BHello World"
|
22
|
+
#
|
23
|
+
# Or, to get the string itself out, call `#value`:
|
24
|
+
#
|
25
|
+
# TLS::Opaque.from_blob("\x0BHello World", 255)[0].value
|
26
|
+
# => "Hello World"
|
27
|
+
#
|
28
|
+
# Passing in a value or blob which is longer than the maximum length
|
29
|
+
# specified will result in `ArgumentError` being thrown.
|
30
|
+
#
|
31
|
+
class TLS::Opaque
|
32
|
+
attr_reader :value
|
33
|
+
|
34
|
+
# Parse out an opaque string from a blob, as well as returning
|
35
|
+
# any remaining data. The `maxlen` parameter is required to
|
36
|
+
# know how many octets at the beginning of the string to read to
|
37
|
+
# determine the length of the opaque string.
|
38
|
+
#
|
39
|
+
# Returns a two-element array, `[TLS::Opaque, String]`, being a
|
40
|
+
# `TLS::Opaque` instance retrieved from the blob provided, and a `String`
|
41
|
+
# containing any remainder of the blob that wasn't considered part of the
|
42
|
+
# `TLS::Opaque`. This second element will *always* be a string, but it
|
43
|
+
# may be an empty string, if the `TLS::Opaque` instance was the entire
|
44
|
+
# blob.
|
45
|
+
#
|
46
|
+
# This method will raise `ArgumentError` if the length encoded at the
|
47
|
+
# beginning of `blob` is longer than the data in `blob`, or if it is
|
48
|
+
# larger than `maxlen`.
|
49
|
+
#
|
50
|
+
def self.from_blob(blob, maxlen)
|
51
|
+
len_bytes = lenlen(maxlen)
|
52
|
+
|
53
|
+
len = blob[0..len_bytes-1].split('').inject(0) do |total, c|
|
54
|
+
total * 256 + c.ord
|
55
|
+
end
|
56
|
+
|
57
|
+
if len > maxlen
|
58
|
+
raise ArgumentError,
|
59
|
+
"Encoded length (#{len}) is greater than maxlen (#{maxlen})"
|
60
|
+
end
|
61
|
+
|
62
|
+
if len > blob[len_bytes..-1].length
|
63
|
+
raise ArgumentError,
|
64
|
+
"Encoded length (#{len}) is greater than the number of bytes available"
|
65
|
+
end
|
66
|
+
|
67
|
+
[TLS::Opaque.new(blob[len_bytes..(len_bytes+len-1)], maxlen),
|
68
|
+
blob[(len_bytes+len)..-1]
|
69
|
+
]
|
70
|
+
end
|
71
|
+
|
72
|
+
def initialize(str, maxlen)
|
73
|
+
unless maxlen.is_a? Integer
|
74
|
+
raise ArgumentError,
|
75
|
+
"maxlen must be an Integer"
|
76
|
+
end
|
77
|
+
|
78
|
+
if str.length > maxlen
|
79
|
+
raise ArgumentError,
|
80
|
+
"value given is longer than maxlen (#{maxlen})"
|
81
|
+
end
|
82
|
+
|
83
|
+
@maxlen = maxlen
|
84
|
+
@value = str
|
85
|
+
end
|
86
|
+
|
87
|
+
# Return an encoded Opaque.
|
88
|
+
#
|
89
|
+
def to_blob
|
90
|
+
len = value.length
|
91
|
+
params = []
|
92
|
+
self.class.lenlen(@maxlen).times do
|
93
|
+
params.unshift(len % 256)
|
94
|
+
len /= 256
|
95
|
+
end
|
96
|
+
|
97
|
+
params << value
|
98
|
+
|
99
|
+
params.pack("C#{self.class.lenlen(@maxlen)}a*")
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
def self.lenlen(len)
|
104
|
+
(Math.log2(len).ceil / 8.0).ceil
|
105
|
+
end
|
106
|
+
end
|