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.
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,2 @@
1
+ require 'certificate-transparency'
2
+ require 'certificate-transparency/client'
@@ -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