certificate-transparency-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,79 @@
1
+ This is a Ruby client library for interacting with
2
+ [RFC6962](http://tools.ietf.org/html/rfc6962) [Certificate
3
+ Transparency](http://www.certificate-transparency.org/) servers. It
4
+ aims to provide a complete interface for retrieving and validating tree
5
+ heads, entries, SCTs, as well as submitting certificates and precerts to a
6
+ log.
7
+
8
+ At present, it is not feature complete, however what is released is well
9
+ tested, heavily documented, and should be ready for production use.
10
+
11
+
12
+ # Installation
13
+
14
+ It's a gem:
15
+
16
+ gem install certificate-transparency-client
17
+
18
+ There's also the wonders of [the Gemfile](http://bundler.io):
19
+
20
+ gem 'certificate-transparency-client'
21
+
22
+ If you're the sturdy type that likes to run from git:
23
+
24
+ rake build; gem install pkg/certificate-transparency-client-<whatever>.gem
25
+
26
+ Or, if you've eschewed the convenience of Rubygems entirely, then you
27
+ presumably know what to do already.
28
+
29
+
30
+ # Usage
31
+
32
+ To get started, instantiate a new instance of {CT::Client}:
33
+
34
+ require 'certificate-transparency-client'
35
+
36
+ ct = CT::Client.new "https://ct.example.org"
37
+
38
+ The URL provided should be the "base" URL for the log; that is, everything
39
+ immediately preceding the `/ct/v1/<blah>` parts of the URL when making a
40
+ complete request.
41
+
42
+ If you only provide a URL, you can retrieve things and submit entries, but
43
+ if you provide a public key, {CT::Client} will also validate signatures for
44
+ you:
45
+
46
+ ct = CT::Client.new "https://ct.example.org",
47
+ :public_key => "<native or base64 key>"
48
+
49
+ To discover what you can do with an instance of {CT::Client}, see the API
50
+ docs for the {CT::Client} class.
51
+
52
+
53
+ # Contributing
54
+
55
+ Bug reports should be sent to the [Github issue
56
+ tracker](https://github.com/mpalmer/certificate-transparency-client/issues),
57
+ or [e-mailed](mailto:theshed+certificate-transparency-client@hezmatt.org).
58
+ Patches can be sent as a Github pull request, or
59
+ [e-mailed](mailto:theshed+certificate-transparency-client@hezmatt.org).
60
+
61
+
62
+ # Licence
63
+
64
+ Unless otherwise stated, everything in this repo is covered by the following
65
+ copyright notice:
66
+
67
+ Copyright (C) 2014,2015 Matt Palmer <matt@hezmatt.org>
68
+
69
+ This program is free software: you can redistribute it and/or modify it
70
+ under the terms of the GNU General Public License version 3, as
71
+ published by the Free Software Foundation.
72
+
73
+ This program is distributed in the hope that it will be useful,
74
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
75
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
76
+ GNU General Public License for more details.
77
+
78
+ You should have received a copy of the GNU General Public License
79
+ 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-client"
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 = "A client for RFC6962 Certificate Transparency log servers"
16
+
17
+ s.authors = ["Matt Palmer"]
18
+ s.email = ["theshed+certificate-transparency-client@hezmatt.org"]
19
+ s.homepage = "http://theshed.hezmatt.org/certificate-transparency-client"
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,27 @@
1
+ require 'openssl'
2
+
3
+ # Interact with a Certificate Transparency server.
4
+ #
5
+ class CertificateTransparency::Client
6
+ def initialize(url, opts = {})
7
+ unless opts.is_a? Hash
8
+ raise ArgumentError,
9
+ "Must pass a hash of options as second argument"
10
+ end
11
+
12
+ if opts[:public_key]
13
+ begin
14
+ @pubkey = if opts[:public_key].valid_encoding? && opts[:public_key] =~ /^[A-Za-z0-9+\/]+=*$/
15
+ OpenSSL::PKey::EC.new(opts[:public_key].unpack("m").first)
16
+ else
17
+ OpenSSL::PKey::EC.new(opts[:public_key])
18
+ end
19
+ rescue OpenSSL::PKey::ECError
20
+ raise ArgumentError,
21
+ "Invalid public key"
22
+ end
23
+ end
24
+
25
+ @url = URI(url)
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ module CertificateTransparency
2
+ # RFC6962 s3.1
3
+ LogEntryType = {
4
+ :x509_entry => 0,
5
+ :precert_entry => 1
6
+ }
7
+
8
+ # RFC6962 s3.4
9
+ MerkleLeafType = {
10
+ :timestamped_entry => 0
11
+ }
12
+
13
+ # RFC6962 s3.2
14
+ SignatureType = {
15
+ :certificate_timestamp => 0,
16
+ :tree_hash => 1
17
+ }
18
+
19
+ # RFC6962 s3.2
20
+ Version = {
21
+ :v1 => 0
22
+ }
23
+ end
@@ -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).to_i
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
@@ -0,0 +1,2 @@
1
+ require 'certificate-transparency'
2
+ require 'certificate-transparency/client'
@@ -0,0 +1,14 @@
1
+ # The base module of everything related to Certificate Transparency.
2
+ module CertificateTransparency; end
3
+
4
+ unless Kernel.const_defined?(:CT)
5
+ #:nodoc:
6
+ CT = CertificateTransparency
7
+ end
8
+
9
+ require 'certificate-transparency/extensions/string'
10
+ require 'certificate-transparency/extensions/time'
11
+
12
+ require 'certificate-transparency/constants'
13
+
14
+ require 'certificate-transparency/signed_tree_head'
@@ -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
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'