certificate-transparency 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|