Merge branch 'repository-tools' of github.com:theupdateframework/tuf into repository-tools

This commit is contained in:
Santiago Torres 2014-01-21 17:55:47 -05:00
commit 47ab2bef05
65 changed files with 10130 additions and 2379 deletions

81
METADATA.md Normal file
View file

@ -0,0 +1,81 @@
# Metadata
Metadata files provide information that clients can use to make update decisions. Different metadata files provide different information. The various metadata files are signed by different roles. The concept of roles allows TUF to only trust information that a role is trusted to provide.
The signed metadata files always include the time they were created and their expiration dates. This ensures that outdated metadata will be detected and that clients can refuse to accept metadata older than that which they've already seen.
All TUF metadata uses a subset of the JSON object format. When calculating the digest of an object, we use the [Canonical JSON](http://wiki.laptop.org/go/Canonical_JSON) format. Implementation-level detail about the metadata can be found in the [spec](docs/tuf-spec.txt).
There are four required top-level roles and one optional top-level role, each with their own metadata file.
Required:
* Root
* Targets
* Release
* Timestamp
Optional:
* Mirrors
There may also be any number of delegated target roles.
## Root Metadata (root.txt)
Signed by: Root role.
Specifies the other top-level roles. When specifying these roles, the trusted keys for each role are listed along with the minimum number of those keys which are required to sign the role's metadata. We call this number the signature threshold.
See [example](http://mirror1.poly.edu/test-pypi/metadata/root.txt).
## Targets Metadata (targets.txt)
Signed by: Targets role.
The targets.txt metadata file lists hashes and sizes of target files. Target files are the actual files that clients are intending to download (for example, the software updates they are trying to obtain).
This file can optionally define other roles to which it delegates trust. Delegating trust means that the delegated role is trusted for some or all of the target files available from the repository. When delegated roles are specified, they are specified in a similar way to how the Root role specifies the top-level roles: the trusted keys and signature threshold for each role is given. Additionally, one or more patterns are specified which indicate the target file paths for which clients should trust each delegated role.
See [example](http://mirror1.poly.edu/test-pypi/metadata/targets.txt).
## Delegated Targets Metadata (targets/foo.txt)
Signed by: A delegated targets role.
The metadata files provided by delegated targets roles follow exactly the same format as the metadata file provided by the top-level Targets role.
The location of the metadata file for each delegated target role is based on the delegation ancestry of the role. If the top-level Targets role defines a role named foo, then the delegated target role's full name would be targets/foo and its metadata file will be available on the repository at the path targets/foo.txt (this is relative to the base directory from which all metadata is available). This path is just the full name of the role followed by a file extension.
If this delegated role foo further delegates to a role bar, then the result is a role whose full name is targets/foo/bar and whose signed metadata file is made available on the repository at targets/foo/bar.txt.
See [example](http://mirror1.poly.edu/test-pypi/metadata/targets/unclaimed.txt).
## Release Metadata (release.txt)
Signed by: Release role.
The release.txt metadata file lists hashes and sizes of all metadata files other than timestamp.txt. This file ensures that clients will see a consistent view of the files on the repository. That is, metadata files (and thus target file) that existed on the repository at different times cannot be combined and presented to clients by an attacker.
See [example](http://mirror1.poly.edu/test-pypi/metadata/release.txt).
## Timestamp Metadata (timestamp.txt)
Signed by: Timestamp role.
The timestamp.txt metadata file lists the hashes and size of the release.txt file. This is the first and potentially only file that needs to be downloaded when clients poll for the existence of updates. This file is frequently resigned and has a short expiration date, thus allowing clients to quickly detect if they are being prevented from obtaining the most recent metadata. An online key is generally used to automatically resign this file at regular intervals.
There are two primary reasons that the timestamp.txt file doesn't contain all of the information that the release.txt file does.
* The timestamp.txt file is downloaded very frequently and so should be kept as small as possible, especially considering that the release.txt file grows in size in proportion to the number of delegated target roles.
* As the Timestamp role's key is an online key and thus at high risk, separate keys should be used for signing the release.txt metadata file so that the Release role's keys can be kept offline and thus more secure.
See [example](http://mirror1.poly.edu/test-pypi/metadata/timestamp.txt).
## Mirrors Metadata (mirrors.txt)
Optionally signed by: Mirrors role.
The mirrors.txt file provides an optional way to provide mirror list updates to TUF clients. Mirror lists can alternatively be provided directly by the software update system and obtained in any way the system sees fit, including being hard coded if that is what an applications wants to do.
No example available. At the time of writing, this hasn't been implemented in TUF. Currently mirrors are specified by the client code.

View file

@ -15,9 +15,8 @@ completely new software.
Three major classes of software update systems are:
* **Application updaters** which are used by applications use to update
themselves. For example, Firefox updates itself through its own application
updater.
* **Application updaters** which are used by applications to update themselves.
For example, Firefox updates itself through its own application updater.
* **Library package managers** such as those offered by many programming
languages for installing additional libraries. These are systems such as
@ -30,8 +29,63 @@ YaST are examples of these.
## Our Approach
There are literally thousands of different software update systems in common
use today. (In fact the average Windows user has about two dozen different
use today. (In fact the average Windows user has about [two dozen](http://secunia.com/gfx/pdf/Secunia_RSA_Software_Portfolio_Security_Exposure.pdf) different
software updaters on their machine!)
We are building a library that can be universally (and in most cases
transparently) used to secure software update systems.
## Overview
At the highest level, TUF simply provides applications with a secure method of obtaining files and knowing when new versions of files are available. We call these files, the ones that are supposed to be downloaded, "target files". The most common need for these abilities is in software update systems and that's what we had in mind when creating TUF.
On the surface, this all sounds simple. Securely obtaining updates just means:
* Knowing when an update exists.
* Downloading the updated file.
The problem is that this is only simple when there are no malicious parties involved. If an attacker is trying to interfere with these seemingly simple steps, there is plenty they can do.
## Background
Let's assume you take the approach that most systems do (at least, the ones that even try to be secure). You download both the file you want and a cryptographic signature of the file. You already know which key you trust to make the signature. You check that the signature is correct and was made by this trusted key. All seems well, right? Wrong. You are still at risk in many ways, including:
* An attacker keeps giving you the same file, so you never realize there is an update.
* An attacker gives you an older, insecure version of a file that you already have, so you download that one and blindly use it thinking it's newer.
* An attacker gives you a newer version of a file you have but it's not the newest one. It's newer to you, but it may be insecure and exploitable by the attacker.
* An attacker compromises the key used to sign these files and now you download a malicious file that is properly signed.
These are just some of the attacks software update systems are vulnerable to when only using signed files.
See [Security](SECURITY.md) for a full list of attacks and updater weaknesses TUF is designed to prevent.
The following papers provide detailed information on securing software updater systems, TUF's design and implementation details, attacks on package managers, and package management security:
* [Survivable Key Compromise in Software Update Systems](docs/papers/survivable-key-compromise-ccs2010.pdf?raw=true)
* [A Look In the Mirror: Attacks on Package Managers](docs/papers/package-management-security-tr08-02.pdf?raw=true)
* [Package Management Security](docs/papers/attacks-on-package-managers-ccs2008.pdf?raw=true)
##What TUF Does
In order to securely download and verify target files, TUF requires a few extra files to exist on a repository. These are called metadata files. TUF metadata files contain additional information, including information about which keys are trusted, the cryptographic hashes of files, signatures on the metadata, metadata version numbers, and the date after which the metadata should be considered expired.
When a software update system using TUF wants to check for updates, it asks TUF to do the work. That is, your software update system never has to deal with this additional metadata or understand what's going on underneath. If TUF reports back that there are updates available, your software update system can then ask TUF to download these files. TUF downloads them and checks them against the TUF metadata that it also downloads from the repository. If the downloaded target files are trustworthy, TUF hands them over to your software update system.
See [Metadata](METADATA.md) for more information and examples.
TUF specification document is also available:
* [The Update Framework Specification](docs/tuf-spec.txt?raw=true)
##Using TUF
TUF has four major classes of users: clients, for whom TUF is largely transparent; mirrors, who will (in most cases) have nothing at all to do with TUF; upstream servers, who will largely be responsible for care and feeding of repositories; and integrators, who do the work of putting TUF into existing projects.
* [Creating a Repository](tuf/README.md)
* [Low-level Integration](tuf/client/README.md)
* [High-level Integration](tuf/interposition/README.md)

69
SECURITY.md Normal file
View file

@ -0,0 +1,69 @@
#Security
Generally, a software update system is secure if it can be sure that it knows about the latest available updates in a timely manner, any files it downloads are the correct files, and no harm results from checking or downloading files. The details of making this happen are complicated by various attacks that can be carried out against software update systems.
## Attacks and Weaknesses
The following are some of the known attacks on software update systems, including weaknesses that make attacks possible. In order to design a secure software update framework, these need to be understood and protected against. Some of these issues are or can be related depending on the design and implementation of a software update system.
* **Arbitrary software installation**. An attacker installs anything they want on the client system. That is, an attacker can provide arbitrary files in response to download requests and the files will not be detected as illegitimate.
* **Rollback attacks**. An attacker presents a software update system with older files than those the client has already seen, causing the client to use files older than those the client knows about.
* **Indefinite freeze attacks**. An attacker continues to present a software update system with the same files the client has already seen. The result is that the client does not know that new files are available.
* **Endless data attacks**. An attacker responds to a file download request with an endless stream of data, causing harm to clients (e.g. a disk partition filling up or memory exhaustion).
* **Slow retrieval attacks**. An attacker responds to clients with a very slow stream of data that essentially results in the client never continuing the update process.
* **Extraneous dependencies attacks**. An attacker indicates to clients that in order to install the software they wanted, they also need to install unrelated software. This unrelated software can be from a trusted source but may have known vulnerabilities that are exploitable by the attacker.
* **Mix-and-match attacks**. An attacker presents clients with a view of a repository that includes files that never existed together on the repository at the same time. This can result in, for example, outdated versions of dependencies being installed.
* **Wrong software installation**. An attacker provides a client with a trusted file that is not the one the client wanted.
* **Malicious mirrors preventing updates**. An attacker in control of one repository mirror is able to prevent users from obtaining updates from other, good mirrors.
* **Vulnerability to key compromises**. At attacker who is able to compromise a single key or less than a given threshold of keys can compromise clients. This includes relying on a single online key (such as only being protected by SSL) or a single offline key (such as most software update systems use to sign files).
##Design Concepts
The design and implementation of TUF aims to be secure against all of the above attacks. A few general ideas drive much of the security of TUF.
For the details of how TUF conveys the information discussed below, see the [Metadata documentation](METADATA.md).
## Trust
Trusting downloaded files really means trusting that the files were provided by some trusted party. Two frequently overlooked aspects of trust in a secure software update system are:
* Trust should not be granted forever. Trust should expire if it is not renewed.
* Compartmentalized trust. A trusted party should only be trusted for files that it is supposed to provide.
## Mitigated Key Risk
Cryptographic signatures are a necessary component in securing a software update system. The safety of the keys that are used to create these signatures affects the security of clients. Rather than incorrectly assume that private keys are always safe from compromise, a secure software update system must strive to keep clients as safe as possible even when compromises happen.
Keeping clients safe despite dangers to keys involves:
* Fast and secure key replacement and revocation.
* Minimally trusting keys that are at high risk. Keys that are kept online or used in an automated fashion shouldn't pose immediate risk to clients if compromised.
* Supporting the use of multiple keys with threshold/quorum signatures trust.
## Integrity
File integrity is important both with respect to single files as well as collections of files. It's fairly obvious that clients must verify that individual downloaded files are correct. Not as obvious but still very important is the need for clients to be certain that their entire view of a repository is correct. For example, if a trusted party is providing two files, a software update system should see the latest versions of both of those files, not just one of the files and not versions of the two files that were never provided together.
## Freshness
As software updates often fix security bugs, it is important that software update systems be able to obtain the latest versions of files that are available. An attacker may want to trick a client into installing outdated versions of software or even just convince a client that no updates are available.
Ensuring freshness means to:
* Never accept files older than those that have been seen previously.
* Recognize when there may be a problem obtaining updates.
Note that it won't always be possible for a client to successfully update if an attacker is responding to their requests. However, a client should be able to recognize that updates may exist that they haven't been able to obtain.
## Implementation Safety
In addition to a secure design, TUF also works to be secure against implementation vulnerabilities including those common to software update systems. In some cases this is assisted by the inclusion of additional information in metadata. For example, knowing the expected size of a target file that is to be downloaded allows TUF to limit the amount of data it will download when retrieving the file. As a result, TUF is secure against endless data attacks (discussed above).

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

View file

@ -1,8 +1,35 @@
% tuf_client_spec.tex
\documentclass{letter}
\usepackage{listings}
\documentclass{article}
\setlength\parindent{0pt}
\lstset{frameshape={ynn}{nny}{nnn}{yyn}, showstringspaces=false, basicstyle=\ttfamily}
\usepackage{listings}
\usepackage{hyperref}
\usepackage{color}
\usepackage{textcomp}
\definecolor{listinggray}{gray}{0.9}
\definecolor{lbcolor}{rgb}{0.9,0.9,0.9}
\lstset{
backgroundcolor=\color{lbcolor},
tabsize=4,
rulecolor=,
language=matlab,
basicstyle=\scriptsize,
upquote=true,
aboveskip={1.5\baselineskip},
columns=fixed,
showstringspaces=false,
extendedchars=true,
breaklines=true,
prebreak = \raisebox{0ex}[0ex][0ex]{\ensuremath{\hookleftarrow}},
frame=single,
showtabs=false,
showspaces=false,
showstringspaces=false,
identifierstyle=\ttfamily,
keywordstyle=\color[rgb]{0,0,1},
commentstyle=\color[rgb]{0.133,0.545,0.133},
stringstyle=\color[rgb]{0.627,0.126,0.941},
}
\begin{document}
%--------------------------------- Header --------------------------------------
@ -44,7 +71,23 @@ functional TUF system in which to do so.
\subsection{Setting up the Repository}
You'll need to run the following steps to set the stage:
\lstinputlisting[language=csh]{tufsetup.sh}
\begin{lstlisting}
#! /bin/sh
# create the relevant directories
mkdir tufdemo
cd tufdemo
mkdir demorepo
mkdir demoproject
# add a file to the project
echo "#! /usr/bin/env python" > demoproject/helloworld.py
echo "print 'hello, world!'" >> demoproject/helloworld.py
# run the quickstart script
quickstart.py -t 1 -k keystore -l demorepo -r demoproject
\end{lstlisting}
This will prompt you for a password for your keystore and an expiration date.
Choosing your expiration date is something of a balancing act: on the one hand,
@ -72,7 +115,14 @@ TUF isn't designed as a replacement for package managers so it doesn't provide
a mechanism with which to perform the initial installation of our demo project's
metadata. To do that, open up another terminal and run the following:
\lstinputlisting[language=csh]{tufclientsetup.sh}
\begin{lstlisting}
#! /bin/sh
mkdir democlient
cp -r demorepo/meta democlient/cur
cp -r democlient/cur democlient/prev
\end{lstlisting}
Once we've installed our metadata, getting the software is a simple matter of
running the demonstration client, found with TUF's source at

View file

@ -1,8 +1,35 @@
% tuf_repo_spec.tex
% tuf_server_spec.tex
\documentclass{article}
\setlength\parindent{0pt}
\usepackage{listings}
\lstset{frameshape={ynn}{nny}{nnn}{yyn}, showstringspaces=false, basicstyle=\ttfamily}
\usepackage{hyperref}
\usepackage{color}
\usepackage{textcomp}
\definecolor{listinggray}{gray}{0.9}
\definecolor{lbcolor}{rgb}{0.9,0.9,0.9}
\lstset{
backgroundcolor=\color{lbcolor},
tabsize=4,
rulecolor=,
language=matlab,
basicstyle=\scriptsize,
upquote=true,
aboveskip={1.5\baselineskip},
columns=fixed,
showstringspaces=false,
extendedchars=true,
breaklines=true,
prebreak = \raisebox{0ex}[0ex][0ex]{\ensuremath{\hookleftarrow}},
frame=single,
showtabs=false,
showspaces=false,
showstringspaces=false,
identifierstyle=\ttfamily,
keywordstyle=\color[rgb]{0,0,1},
commentstyle=\color[rgb]{0.133,0.545,0.133},
stringstyle=\color[rgb]{0.627,0.126,0.941},
}
\begin{document}
%--------------------------------- Header --------------------------------------

Binary file not shown.

View file

@ -1,4 +1,4 @@
TUF: The Update Framework
The Update Framework Specification
1. Introduction
@ -680,9 +680,10 @@
5.1. The client application
Note: At any point in the following process there is a problem (e.g. only
expired metadata can be retrieved), the software update system using the
framework must decide how to proceed.
Note: If at any point in the following process there is a problem (e.g., only
expired metadata can be retrieved), the Root file is downloaded and the process
starts over. Optionally, the software update system using the framework can
decide how to proceed rather than automatically downloading a new Root file.
The client code instructs the framework to check for updates. The framework
downloads the timestamp.txt file from a mirror and checks that the file is

View file

@ -1,104 +0,0 @@
import hashlib
b = 256
q = 2**255 - 19
l = 2**252 + 27742317777372353535851937790883648493
def H(m):
return hashlib.sha512(m).digest()
def expmod(b,e,m):
if e == 0: return 1
t = expmod(b,e/2,m)**2 % m
if e & 1: t = (t*b) % m
return t
def inv(x):
return expmod(x,q-2,q)
d = -121665 * inv(121666)
I = expmod(2,(q-1)/4,q)
def xrecover(y):
xx = (y*y-1) * inv(d*y*y+1)
x = expmod(xx,(q+3)/8,q)
if (x*x - xx) % q != 0: x = (x*I) % q
if x % 2 != 0: x = q-x
return x
By = 4 * inv(5)
Bx = xrecover(By)
B = [Bx % q,By % q]
def edwards(P,Q):
x1 = P[0]
y1 = P[1]
x2 = Q[0]
y2 = Q[1]
x3 = (x1*y2+x2*y1) * inv(1+d*x1*x2*y1*y2)
y3 = (y1*y2+x1*x2) * inv(1-d*x1*x2*y1*y2)
return [x3 % q,y3 % q]
def scalarmult(P,e):
if e == 0: return [0,1]
Q = scalarmult(P,e/2)
Q = edwards(Q,Q)
if e & 1: Q = edwards(Q,P)
return Q
def encodeint(y):
bits = [(y >> i) & 1 for i in range(b)]
return ''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b/8)])
def encodepoint(P):
x = P[0]
y = P[1]
bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1]
return ''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b/8)])
def bit(h,i):
return (ord(h[i/8]) >> (i%8)) & 1
def publickey(sk):
h = H(sk)
a = 2**(b-2) + sum(2**i * bit(h,i) for i in range(3,b-2))
A = scalarmult(B,a)
return encodepoint(A)
def Hint(m):
h = H(m)
return sum(2**i * bit(h,i) for i in range(2*b))
def signature(m,sk,pk):
h = H(sk)
a = 2**(b-2) + sum(2**i * bit(h,i) for i in range(3,b-2))
r = Hint(''.join([h[i] for i in range(b/8,b/4)]) + m)
R = scalarmult(B,r)
S = (r + Hint(encodepoint(R) + pk + m) * a) % l
return encodepoint(R) + encodeint(S)
def isoncurve(P):
x = P[0]
y = P[1]
return (-x*x + y*y - 1 - d*x*x*y*y) % q == 0
def decodeint(s):
return sum(2**i * bit(s,i) for i in range(0,b))
def decodepoint(s):
y = sum(2**i * bit(s,i) for i in range(0,b-1))
x = xrecover(y)
if x & 1 != bit(s,b-1): x = q-x
P = [x,y]
if not isoncurve(P): raise Exception("decoding point that is not on curve")
return P
def checkvalid(s,m,pk):
if len(s) != b/4: raise Exception("signature length is wrong")
if len(pk) != b/8: raise Exception("public-key length is wrong")
R = decodepoint(s[0:b/8])
A = decodepoint(pk)
S = decodeint(s[b/8:b/4])
h = Hint(encodepoint(R) + pk + m)
if scalarmult(B,S) != edwards(R,scalarmult(A,h)):
raise Exception("signature does not pass verification")

View file

@ -69,6 +69,7 @@
url='https://www.updateframework.com',
install_requires=['pycrypto>=2.6'],
packages=[
'ed25519',
'tuf',
'tuf.client',
'tuf.compatibility',
@ -82,6 +83,7 @@
'tuf/repo/quickstart.py',
'tuf/pushtools/push.py',
'tuf/pushtools/receivetools/receive.py',
'tuf/repo/signercli.py'
'tuf/repo/signercli.py',
'tuf/client/basic_client.py'
]
)

123
tests/unit/test_ed25519_keys.py Executable file
View file

@ -0,0 +1,123 @@
"""
<Program Name>
test_ed25519_keys.py
<Author>
Vladimir Diaz
<Started>
October 11, 2013.
<Copyright>
See LICENSE for licensing information.
<Purpose>
Test cases for test_ed25519_keys.py.
"""
import unittest
import logging
import tuf
import tuf.log
import tuf.formats
import tuf.ed25519_keys as ed25519
logger = logging.getLogger('tuf.test_ed25519_keys')
public, private = ed25519.generate_public_and_private()
FORMAT_ERROR_MSG = 'tuf.FormatError raised. Check object\'s format.'
class TestEd25519_keys(unittest.TestCase):
def setUp(self):
pass
def test_generate_public_and_private(self):
pub, priv = ed25519.generate_public_and_private()
# Check format of 'pub' and 'priv'.
self.assertEqual(True, tuf.formats.ED25519PUBLIC_SCHEMA.matches(pub))
self.assertEqual(True, tuf.formats.ED25519SEED_SCHEMA.matches(priv))
# Check for invalid argument.
self.assertRaises(tuf.FormatError,
ed25519.generate_public_and_private, 'True')
self.assertRaises(tuf.FormatError,
ed25519.generate_public_and_private, 2048)
def test_create_signature(self):
global public
global private
data = 'The quick brown fox jumps over the lazy dog'
signature, method = ed25519.create_signature(public, private, data)
# Verify format of returned values.
self.assertEqual(True,
tuf.formats.ED25519SIGNATURE_SCHEMA.matches(signature))
self.assertEqual(True, tuf.formats.NAME_SCHEMA.matches(method))
self.assertEqual('ed25519', method)
# Check for improperly formatted argument.
self.assertRaises(tuf.FormatError,
ed25519.create_signature, 123, private, data)
self.assertRaises(tuf.FormatError,
ed25519.create_signature, public, 123, data)
# Check for invalid 'data'.
self.assertRaises(tuf.CryptoError,
ed25519.create_signature, public, private, 123)
def test_verify_signature(self):
global public
global private
data = 'The quick brown fox jumps over the lazy dog'
signature, method = ed25519.create_signature(public, private, data)
valid_signature = ed25519.verify_signature(public, method, signature, data)
self.assertEqual(True, valid_signature)
# Check for improperly formatted arguments.
self.assertRaises(tuf.FormatError, ed25519.verify_signature, 123, method,
signature, data)
# Signature method improperly formatted.
self.assertRaises(tuf.FormatError, ed25519.verify_signature, public, 123,
signature, data)
# Signature not a string.
self.assertRaises(tuf.FormatError, ed25519.verify_signature, public, method,
123, data)
# Invalid signature length, which must be exactly 64 bytes..
self.assertRaises(tuf.FormatError, ed25519.verify_signature, public, method,
'bad_signature', data)
# Check for invalid signature and data.
# Mismatched data.
self.assertEqual(False, ed25519.verify_signature(public, method,
signature, '123'))
# Mismatched signature.
bad_signature = 'a'*64
self.assertEqual(False, ed25519.verify_signature(public, method,
bad_signature, data))
# Generated signature created with different data.
new_signature, method = ed25519.create_signature(public, private,
'mismatched data')
self.assertEqual(False, ed25519.verify_signature(public, method,
new_signature, data))
# Run the unit tests.
if __name__ == '__main__':
unittest.main()

View file

@ -76,7 +76,7 @@ def test_schemas(self):
'NAME_SCHEMA': (tuf.formats.NAME_SCHEMA, 'Marty McFly'),
'TOGGLE_SCHEMA': (tuf.formats.TOGGLE_SCHEMA, True),
'BOOLEAN_SCHEMA': (tuf.formats.BOOLEAN_SCHEMA, True),
'THRESHOLD_SCHEMA': (tuf.formats.THRESHOLD_SCHEMA, 1),

View file

@ -13,7 +13,6 @@
<Purpose>
Unit test for 'keydb.py'.
"""
import unittest
@ -21,7 +20,7 @@
import tuf
import tuf.formats
import tuf.rsa_key
import tuf.keys
import tuf.keydb
import tuf.log
@ -31,7 +30,7 @@
# Generate the three keys to use in our test cases.
KEYS = []
for junk in range(3):
KEYS.append(tuf.rsa_key.generate(2048))
KEYS.append(tuf.keys.generate_rsa_key(2048))
@ -89,7 +88,7 @@ def test_get_key(self):
def test_add_rsakey(self):
def test_add_key(self):
# Test conditions using valid 'keyid' arguments.
rsakey = KEYS[0]
keyid = KEYS[0]['keyid']
@ -97,9 +96,9 @@ def test_add_rsakey(self):
keyid2 = KEYS[1]['keyid']
rsakey3 = KEYS[2]
keyid3 = KEYS[2]['keyid']
self.assertEqual(None, tuf.keydb.add_rsakey(rsakey, keyid))
self.assertEqual(None, tuf.keydb.add_rsakey(rsakey2, keyid2))
self.assertEqual(None, tuf.keydb.add_rsakey(rsakey3))
self.assertEqual(None, tuf.keydb.add_key(rsakey, keyid))
self.assertEqual(None, tuf.keydb.add_key(rsakey2, keyid2))
self.assertEqual(None, tuf.keydb.add_key(rsakey3))
self.assertEqual(rsakey, tuf.keydb.get_key(keyid))
self.assertEqual(rsakey2, tuf.keydb.get_key(keyid2))
@ -109,26 +108,26 @@ def test_add_rsakey(self):
tuf.keydb.clear_keydb()
rsakey3['keytype'] = 'bad_keytype'
self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, None, keyid)
self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, '', keyid)
self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, ['123'], keyid)
self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, {'a': 'b'}, keyid)
self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, rsakey, {'keyid': ''})
self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, rsakey, 123)
self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, rsakey, False)
self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, rsakey, ['keyid'])
self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, rsakey3, keyid3)
self.assertRaises(tuf.FormatError, tuf.keydb.add_key, None, keyid)
self.assertRaises(tuf.FormatError, tuf.keydb.add_key, '', keyid)
self.assertRaises(tuf.FormatError, tuf.keydb.add_key, ['123'], keyid)
self.assertRaises(tuf.FormatError, tuf.keydb.add_key, {'a': 'b'}, keyid)
self.assertRaises(tuf.FormatError, tuf.keydb.add_key, rsakey, {'keyid': ''})
self.assertRaises(tuf.FormatError, tuf.keydb.add_key, rsakey, 123)
self.assertRaises(tuf.FormatError, tuf.keydb.add_key, rsakey, False)
self.assertRaises(tuf.FormatError, tuf.keydb.add_key, rsakey, ['keyid'])
self.assertRaises(tuf.FormatError, tuf.keydb.add_key, rsakey3, keyid3)
rsakey3['keytype'] = 'rsa'
# Test conditions where keyid does not match the rsakey.
self.assertRaises(tuf.Error, tuf.keydb.add_rsakey, rsakey, keyid2)
self.assertRaises(tuf.Error, tuf.keydb.add_rsakey, rsakey2, keyid)
self.assertRaises(tuf.Error, tuf.keydb.add_key, rsakey, keyid2)
self.assertRaises(tuf.Error, tuf.keydb.add_key, rsakey2, keyid)
# Test conditions using keyids that have already been added.
tuf.keydb.add_rsakey(rsakey, keyid)
tuf.keydb.add_rsakey(rsakey2, keyid2)
self.assertRaises(tuf.KeyAlreadyExistsError, tuf.keydb.add_rsakey, rsakey)
self.assertRaises(tuf.KeyAlreadyExistsError, tuf.keydb.add_rsakey, rsakey2)
tuf.keydb.add_key(rsakey, keyid)
tuf.keydb.add_key(rsakey2, keyid2)
self.assertRaises(tuf.KeyAlreadyExistsError, tuf.keydb.add_key, rsakey)
self.assertRaises(tuf.KeyAlreadyExistsError, tuf.keydb.add_key, rsakey2)
@ -140,12 +139,13 @@ def test_remove_key(self):
keyid2 = KEYS[1]['keyid']
rsakey3 = KEYS[2]
keyid3 = KEYS[2]['keyid']
tuf.keydb.add_rsakey(rsakey, keyid)
tuf.keydb.add_rsakey(rsakey2, keyid2)
tuf.keydb.add_rsakey(rsakey3, keyid3)
tuf.keydb.add_key(rsakey, keyid)
tuf.keydb.add_key(rsakey2, keyid2)
tuf.keydb.add_key(rsakey3, keyid3)
self.assertEqual(None, tuf.keydb.remove_key(keyid))
self.assertEqual(None, tuf.keydb.remove_key(keyid2))
# Ensure the keys were actually removed.
self.assertRaises(tuf.UnknownKeyError, tuf.keydb.get_key, keyid)
self.assertRaises(tuf.UnknownKeyError, tuf.keydb.get_key, keyid2)

193
tests/unit/test_keys.py Executable file
View file

@ -0,0 +1,193 @@
"""
<Program Name>
test_keys.py
<Author>
Vladimir Diaz
<Started>
October 10, 2013.
<Copyright>
See LICENSE for licensing information.
<Purpose>
Test cases for test_keys.py.
TODO: test case for ed25519 key generation and refactor.
"""
import unittest
import logging
import tuf
import tuf.log
import tuf.formats
import tuf.keys
logger = logging.getLogger('tuf.test_keys')
KEYS = tuf.keys
FORMAT_ERROR_MSG = 'tuf.FormatError was raised! Check object\'s format.'
DATA = 'SOME DATA REQUIRING AUTHENTICITY.'
rsakey_dict = KEYS.generate_rsa_key()
temp_key_info_vals = rsakey_dict.values()
temp_key_vals = rsakey_dict['keyval'].values()
class TestKeys(unittest.TestCase):
def setUp(self):
rsakey_dict['keytype']=temp_key_info_vals[0]
rsakey_dict['keyid']=temp_key_info_vals[1]
rsakey_dict['keyval']=temp_key_info_vals[2]
rsakey_dict['keyval']['public']=temp_key_vals[0]
rsakey_dict['keyval']['private']=temp_key_vals[1]
def test_generate_rsa_key(self):
_rsakey_dict = KEYS.generate_rsa_key()
# Check if the format of the object returned by generate() corresponds
# to RSAKEY_SCHEMA format.
self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(_rsakey_dict),
FORMAT_ERROR_MSG)
# Passing a bit value that is <2048 to generate() - should raise
# 'tuf.FormatError'.
self.assertRaises(tuf.FormatError, KEYS.generate_rsa_key, 555)
# Passing a string instead of integer for a bit value.
self.assertRaises(tuf.FormatError, KEYS.generate_rsa_key, 'bits')
# NOTE if random bit value >=2048 (not 4096) is passed generate(bits)
# does not raise any errors and returns a valid key.
self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(KEYS.generate_rsa_key(2048)))
self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(KEYS.generate_rsa_key(4096)))
def test_format_keyval_to_metadata(self):
keyvalue = rsakey_dict['keyval']
keytype = rsakey_dict['keytype']
key_meta = KEYS.format_keyval_to_metadata(keytype, keyvalue)
# Check if the format of the object returned by this function corresponds
# to KEY_SCHEMA format.
self.assertEqual(None,
tuf.formats.KEY_SCHEMA.check_match(key_meta),
FORMAT_ERROR_MSG)
key_meta = KEYS.format_keyval_to_metadata(keytype, keyvalue, private=True)
# Check if the format of the object returned by this function corresponds
# to KEY_SCHEMA format.
self.assertEqual(None, tuf.formats.KEY_SCHEMA.check_match(key_meta),
FORMAT_ERROR_MSG)
# Supplying a 'bad' keyvalue.
self.assertRaises(tuf.FormatError, KEYS.format_keyval_to_metadata,
'bad_keytype', keyvalue)
del keyvalue['public']
self.assertRaises(tuf.FormatError, KEYS.format_keyval_to_metadata,
keytype, keyvalue)
def test_format_metadata_to_key(self):
# Reconfiguring rsakey_dict to conform to KEY_SCHEMA
# i.e. {keytype: 'rsa', keyval: {public: pub_key, private: priv_key}}
#keyid = rsakey_dict['keyid']
del rsakey_dict['keyid']
rsakey_dict_from_meta = KEYS.format_metadata_to_key(rsakey_dict)
# Check if the format of the object returned by this function corresponds
# to RSAKEY_SCHEMA format.
self.assertEqual(None,
tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict_from_meta),
FORMAT_ERROR_MSG)
# Supplying a wrong number of arguments.
self.assertRaises(TypeError, KEYS.format_metadata_to_key)
args = (rsakey_dict, rsakey_dict)
self.assertRaises(TypeError, KEYS.format_metadata_to_key, *args)
# Supplying a malformed argument to the function - should get FormatError
del rsakey_dict['keyval']
self.assertRaises(tuf.FormatError, KEYS.format_metadata_to_key,
rsakey_dict)
def test_helper_get_keyid(self):
keytype = rsakey_dict['keytype']
keyvalue = rsakey_dict['keyval']
# Check format of 'keytype'.
self.assertEqual(None, tuf.formats.KEYTYPE_SCHEMA.check_match(keytype),
FORMAT_ERROR_MSG)
# Check format of 'keyvalue'.
self.assertEqual(None, tuf.formats.KEYVAL_SCHEMA.check_match(keyvalue),
FORMAT_ERROR_MSG)
keyid = KEYS._get_keyid(keytype, keyvalue)
# Check format of 'keyid' - the output of '_get_keyid()' function.
self.assertEqual(None, tuf.formats.KEYID_SCHEMA.check_match(keyid),
FORMAT_ERROR_MSG)
def test_create_signature(self):
# Creating a signature for 'DATA'.
signature = KEYS.create_signature(rsakey_dict, DATA)
# Check format of output.
self.assertEqual(None,
tuf.formats.SIGNATURE_SCHEMA.check_match(signature),
FORMAT_ERROR_MSG)
# Removing private key from 'rsakey_dict' - should raise a TypeError.
rsakey_dict['keyval']['private'] = ''
args = (rsakey_dict, DATA)
self.assertRaises(TypeError, KEYS.create_signature, *args)
# Supplying an incorrect number of arguments.
self.assertRaises(TypeError, KEYS.create_signature)
def test_verify_signature(self):
# Creating a signature 'signature' of 'DATA' to be verified.
signature = KEYS.create_signature(rsakey_dict, DATA)
# Verifying the 'signature' of 'DATA'.
verified = KEYS.verify_signature(rsakey_dict, signature, DATA)
self.assertTrue(verified, "Incorrect signature.")
# Testing an invalid 'signature'. Same 'signature' is passed, with
# 'DATA' different than the original 'DATA' that was used
# in creating the 'signature'. Function should return 'False'.
# Modifying 'DATA'.
_DATA = '1111'+DATA+'1111'
# Verifying the 'signature' of modified '_DATA'.
verified = KEYS.verify_signature(rsakey_dict, signature, _DATA)
self.assertFalse(verified,
'Returned \'True\' on an incorrect signature.')
# Modifying 'signature' to pass an incorrect method since only
# 'PyCrypto-PKCS#1 PSS'
# is accepted.
signature['method'] = 'Biff'
args = (rsakey_dict, signature, DATA)
self.assertRaises(tuf.UnknownMethodError, KEYS.verify_signature, *args)
# Passing incorrect number of arguments.
self.assertRaises(TypeError, KEYS.verify_signature)
# Run the unit tests.
if __name__ == '__main__':
unittest.main()

View file

@ -13,7 +13,6 @@
<Purpose>
Unit test for keystore.py.
"""
import unittest
@ -25,7 +24,7 @@
import tuf
import tuf.repo.keystore
import tuf.rsa_key
import tuf.keys
import tuf.formats
import tuf.util
import tuf.log
@ -56,7 +55,7 @@
for i in range(3):
# Populating the original 'RSAKEYS' and 'PASSWDS' lists.
RSAKEYS.append(tuf.rsa_key.generate())
RSAKEYS.append(tuf.keys.generate_rsa_key())
PASSWDS.append('passwd_'+str(i))
# Saving original copies of 'RSAKEYS' and 'PASSWDS' to temp variables
@ -350,6 +349,7 @@ def tearDownModule():
tuf.repo.keystore.clear_keystore()
# Run the unit tests.
if __name__ == '__main__':
unittest.main()

198
tests/unit/test_pycrypto_keys.py Executable file
View file

@ -0,0 +1,198 @@
"""
<Program Name>
test_pycrypto_keys.py
<Author>
Vladimir Diaz
<Started>
October 10, 2013.
<Copyright>
See LICENSE for licensing information.
<Purpose>
Test cases for test_pycrypto_keys.py.
"""
import unittest
import logging
import tuf
import tuf.log
import tuf.formats
import tuf.pycrypto_keys as pycrypto
logger = logging.getLogger('tuf.test_pycrypto_keys')
public_rsa, private_rsa = pycrypto.generate_rsa_public_and_private()
FORMAT_ERROR_MSG = 'tuf.FormatError raised. Check object\'s format.'
class TestPycrypto_keys(unittest.TestCase):
def setUp(self):
pass
def test_generate_rsa_public_and_private(self):
pub, priv = pycrypto.generate_rsa_public_and_private()
# Check format of 'pub' and 'priv'.
self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(pub),
FORMAT_ERROR_MSG)
self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(priv),
FORMAT_ERROR_MSG)
# Check for invalid bits argument. bit >= 2048 and a multiple of 256.
self.assertRaises(tuf.FormatError,
pycrypto.generate_rsa_public_and_private, 1024)
self.assertRaises(ValueError,
pycrypto.generate_rsa_public_and_private, 2049)
self.assertRaises(tuf.FormatError,
pycrypto.generate_rsa_public_and_private, '2048')
def test_create_rsa_signature(self):
global private_rsa
data = 'The quick brown fox jumps over the lazy dog'
signature, method = pycrypto.create_rsa_signature(private_rsa, data)
# Verify format of returned values.
self.assertNotEqual(None, signature)
self.assertEqual(None, tuf.formats.NAME_SCHEMA.check_match(method),
FORMAT_ERROR_MSG)
self.assertEqual('RSASSA-PSS', method)
# Check for improperly formatted argument.
self.assertRaises(tuf.FormatError,
pycrypto.create_rsa_signature, 123, data)
# Check for invalid 'data'.
self.assertRaises(tuf.CryptoError,
pycrypto.create_rsa_signature, private_rsa, 123)
def test_verify_rsa_signature(self):
global public_rsa
global private_rsa
data = 'The quick brown fox jumps over the lazy dog'
signature, method = pycrypto.create_rsa_signature(private_rsa, data)
valid_signature = pycrypto.verify_rsa_signature(signature, method, public_rsa,
data)
self.assertEqual(True, valid_signature)
# Check for improperly formatted arguments.
self.assertRaises(tuf.FormatError, pycrypto.verify_rsa_signature, signature,
123, public_rsa, data)
self.assertRaises(tuf.FormatError, pycrypto.verify_rsa_signature, signature,
method, 123, data)
self.assertRaises(tuf.FormatError, pycrypto.verify_rsa_signature, 123, method,
public_rsa, data)
# Check for invalid signature and data.
self.assertRaises(tuf.CryptoError, pycrypto.verify_rsa_signature, signature,
method, public_rsa, 123)
self.assertEqual(False, pycrypto.verify_rsa_signature(signature, method,
public_rsa, 'mismatched data'))
mismatched_signature, method = pycrypto.create_rsa_signature(private_rsa,
'mismatched data')
self.assertEqual(False, pycrypto.verify_rsa_signature(mismatched_signature,
method, public_rsa, data))
def test_create_rsa_encrypted_pem(self):
global public_rsa
global private_rsa
passphrase = 'pw'
# Check format of 'public_rsa'.
self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(public_rsa),
FORMAT_ERROR_MSG)
# Check format of 'passphrase'.
self.assertEqual(None, tuf.formats.PASSWORD_SCHEMA.check_match(passphrase),
FORMAT_ERROR_MSG)
# Generate the encrypted PEM string of 'public_rsa'.
pem_rsakey = pycrypto.create_rsa_encrypted_pem(private_rsa, passphrase)
# Check format of 'pem_rsakey'.
self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(pem_rsakey),
FORMAT_ERROR_MSG)
# Check for invalid arguments.
self.assertRaises(tuf.FormatError,
pycrypto.create_rsa_encrypted_pem, 1, passphrase)
self.assertRaises(tuf.FormatError,
pycrypto.create_rsa_encrypted_pem, private_rsa, ['pw'])
def test_create_rsa_public_and_private_from_encrypted_pem(self):
global private_rsa
passphrase = 'pw'
# Generate the encrypted PEM string of 'private_rsa'.
pem_rsakey = pycrypto.create_rsa_encrypted_pem(private_rsa, passphrase)
# Check format of 'passphrase'.
self.assertEqual(None, tuf.formats.PASSWORD_SCHEMA.check_match(passphrase),
FORMAT_ERROR_MSG)
# Decrypt 'pem_rsakey' and verify the decrypted object is properly
# formatted.
public_decrypted, private_decrypted = \
pycrypto.create_rsa_public_and_private_from_encrypted_pem(pem_rsakey,
passphrase)
self.assertEqual(None,
tuf.formats.PEMRSA_SCHEMA.check_match(public_decrypted),
FORMAT_ERROR_MSG)
self.assertEqual(None,
tuf.formats.PEMRSA_SCHEMA.check_match(private_decrypted),
FORMAT_ERROR_MSG)
# Does 'public_decrypted' and 'private_decrypted' match the originals?
self.assertEqual(public_rsa, public_decrypted)
self.assertEqual(private_rsa, private_decrypted)
# Attempt decryption of 'pem_rsakey' using an incorrect passphrase.
self.assertRaises(tuf.CryptoError,
pycrypto.create_rsa_public_and_private_from_encrypted_pem,
pem_rsakey, 'bad_pw')
# Check for non-encrypted PEM strings.
# create_rsa_public_and_private_from_encrypted_pem()
# returns a tuple of tuf.formats.PEMRSA_SCHEMA objects if the PEM formatted
# string is not actually encrypted but still a valid PEM string.
pub, priv = pycrypto.create_rsa_public_and_private_from_encrypted_pem(
private_rsa, passphrase)
self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(pub),
FORMAT_ERROR_MSG)
self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(priv),
FORMAT_ERROR_MSG)
# Check for invalid arguments.
self.assertRaises(tuf.FormatError,
pycrypto.create_rsa_public_and_private_from_encrypted_pem,
123, passphrase)
self.assertRaises(tuf.FormatError,
pycrypto.create_rsa_public_and_private_from_encrypted_pem,
pem_rsakey, ['pw'])
self.assertRaises(tuf.CryptoError,
pycrypto.create_rsa_public_and_private_from_encrypted_pem,
'invalid_pem', passphrase)
# Run the unit tests.
if __name__ == '__main__':
unittest.main()

View file

@ -18,7 +18,6 @@
Given that all message prompts don't change - this will work pretty well
for running quickstart without having to manually enter input to prompts
every time you want to run quickstart.
"""
import os

View file

@ -13,7 +13,6 @@
<Purpose>
Unit test for 'roledb.py'.
"""
@ -22,7 +21,7 @@
import tuf
import tuf.formats
import tuf.rsa_key
import tuf.keys
import tuf.roledb
import tuf.log
@ -32,7 +31,7 @@
# Generate the three keys to use in our test cases.
KEYS = []
for junk in range(3):
KEYS.append(tuf.rsa_key.generate(2048))
KEYS.append(tuf.keys.generate_rsa_key(2048))

View file

@ -1,258 +0,0 @@
"""
<Program Name>
test_rsa_key.py
<Author>
Konstantin Andrianov
<Started>
April 24, 2012.
<Copyright>
See LICENSE for licensing information.
<Purpose>
Test cases for rsa_key.py.
<Notes>
I'm using 'global rsakey_dict' - there is no harm in doing so since
in order to modify the global variable in any method, python requires
explicit indication to modify i.e. declaring 'global' in each method
that modifies the global variable 'rsakey_dict'.
"""
import unittest
import logging
import tuf
import tuf.log
import tuf.formats
import tuf.rsa_key
logger = logging.getLogger('tuf.test_rsa_key')
RSA_KEY = tuf.rsa_key
FORMAT_ERROR_MSG = 'tuf.FormatError was raised! Check object\'s format.'
DATA = 'SOME DATA REQUIRING AUTHENTICITY.'
rsakey_dict = RSA_KEY.generate()
temp_key_info_vals = rsakey_dict.values()
temp_key_vals = rsakey_dict['keyval'].values()
class TestRsa_key(unittest.TestCase):
def setUp(self):
rsakey_dict['keytype']=temp_key_info_vals[0]
rsakey_dict['keyid']=temp_key_info_vals[1]
rsakey_dict['keyval']=temp_key_info_vals[2]
rsakey_dict['keyval']['public']=temp_key_vals[0]
rsakey_dict['keyval']['private']=temp_key_vals[1]
def test_generate(self):
_rsakey_dict = RSA_KEY.generate()
# Check if the format of the object returned by generate() corresponds
# to RSAKEY_SCHEMA format.
self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(_rsakey_dict),
FORMAT_ERROR_MSG)
# Passing a bit value that is <2048 to generate() - should raise
# 'tuf.FormatError'.
self.assertRaises(tuf.FormatError, RSA_KEY.generate, 555)
# Passing a string instead of integer for a bit value.
self.assertRaises(tuf.FormatError, RSA_KEY.generate, 'bits')
# NOTE if random bit value >=2048 (not 4096) is passed generate(bits)
# does not raise any errors and returns a valid key.
self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(RSA_KEY.generate(2048)))
self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(RSA_KEY.generate(4096)))
def test_create_in_metadata_format(self):
key_value = rsakey_dict['keyval']
key_meta = RSA_KEY.create_in_metadata_format(key_value)
# Check if the format of the object returned by this function corresponds
# to KEY_SCHEMA format.
self.assertEqual(None,
tuf.formats.KEY_SCHEMA.check_match(key_meta),
FORMAT_ERROR_MSG)
key_meta = RSA_KEY.create_in_metadata_format(key_value, private=True)
# Check if the format of the object returned by this function corresponds
# to KEY_SCHEMA format.
self.assertEqual(None, tuf.formats.KEY_SCHEMA.check_match(key_meta),
FORMAT_ERROR_MSG)
# Supplying a 'bad' key_value.
del key_value['public']
self.assertRaises(tuf.FormatError, RSA_KEY.create_in_metadata_format,
key_value)
def test_create_from_metadata_format(self):
# Reconfiguring rsakey_dict to conform to KEY_SCHEMA
# i.e. {keytype: 'rsa', keyval: {public: pub_key, private: priv_key}}
#keyid = rsakey_dict['keyid']
del rsakey_dict['keyid']
rsakey_dict_from_meta = RSA_KEY.create_from_metadata_format(rsakey_dict)
# Check if the format of the object returned by this function corresponds
# to RSAKEY_SCHEMA format.
self.assertEqual(None,
tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict_from_meta),
FORMAT_ERROR_MSG)
# Supplying a wrong number of arguments.
self.assertRaises(TypeError, RSA_KEY.create_from_metadata_format)
args = (rsakey_dict, rsakey_dict)
self.assertRaises(TypeError, RSA_KEY.create_from_metadata_format, *args)
# Supplying a malformed argument to the function - should get FormatError
del rsakey_dict['keyval']
self.assertRaises(tuf.FormatError, RSA_KEY.create_from_metadata_format,
rsakey_dict)
def test_helper_get_keyid(self):
key_value = rsakey_dict['keyval']
# Check format of 'key_value'.
self.assertEqual(None, tuf.formats.KEYVAL_SCHEMA.check_match(key_value),
FORMAT_ERROR_MSG)
keyid = RSA_KEY._get_keyid(key_value)
# Check format of 'keyid' - the output of '_get_keyid()' function.
self.assertEqual(None, tuf.formats.KEYID_SCHEMA.check_match(keyid),
FORMAT_ERROR_MSG)
def test_createsignature(self):
# Creating a signature for 'DATA'.
signature = RSA_KEY.create_signature(rsakey_dict, DATA)
# Check format of output.
self.assertEqual(None,
tuf.formats.SIGNATURE_SCHEMA.check_match(signature),
FORMAT_ERROR_MSG)
# Removing private key from 'rsakey_dict' - should raise a TypeError.
rsakey_dict['keyval']['private'] = ''
args = (rsakey_dict, DATA)
self.assertRaises(TypeError, RSA_KEY.create_signature, *args)
# Supplying an incorrect number of arguments.
self.assertRaises(TypeError, RSA_KEY.create_signature)
def test_verify_signature(self):
# Creating a signature 'signature' of 'DATA' to be verified.
signature = RSA_KEY.create_signature(rsakey_dict, DATA)
# Verifying the 'signature' of 'DATA'.
verified = RSA_KEY.verify_signature(rsakey_dict, signature, DATA)
self.assertTrue(verified, "Incorrect signature.")
# Testing an invalid 'signature'. Same 'signature' is passed, with
# 'DATA' different than the original 'DATA' that was used
# in creating the 'signature'. Function should return 'False'.
# Modifying 'DATA'.
_DATA = '1111'+DATA+'1111'
# Verifying the 'signature' of modified '_DATA'.
verified = RSA_KEY.verify_signature(rsakey_dict, signature, _DATA)
self.assertFalse(verified,
'Returned \'True\' on an incorrect signature.')
# Modifying 'signature' to pass an incorrect method since only
# 'PyCrypto-PKCS#1 PSS'
# is accepted.
signature['method'] = 'Biff'
args = (rsakey_dict, signature, DATA)
self.assertRaises(tuf.UnknownMethodError, RSA_KEY.verify_signature, *args)
# Passing incorrect number of arguments.
self.assertRaises(TypeError,RSA_KEY.verify_signature)
def test_create_encrypted_pem(self):
passphrase = 'pw'
# Check format of 'rsakey_dict'.
self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict),
FORMAT_ERROR_MSG)
# Check format of 'passphrase'.
self.assertEqual(None, tuf.formats.PASSWORD_SCHEMA.check_match(passphrase),
FORMAT_ERROR_MSG)
# Generate the encrypted PEM string of 'rsakey_dict'.
pem_rsakey = tuf.rsa_key.create_encrypted_pem(rsakey_dict, passphrase)
# Check for invalid arguments.
self.assertRaises(tuf.FormatError,
tuf.rsa_key.create_encrypted_pem, 'Biff', passphrase)
self.assertRaises(tuf.FormatError,
tuf.rsa_key.create_encrypted_pem, rsakey_dict, ['pw'])
def test_create_from_encrypted_pem(self):
passphrase = 'pw'
# Check format of 'rsakey_dict'.
self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict),
FORMAT_ERROR_MSG)
# Check format of 'passphrase'.
self.assertEqual(None, tuf.formats.PASSWORD_SCHEMA.check_match(passphrase),
FORMAT_ERROR_MSG)
# Generate the encrypted PEM string of 'rsakey_dict'.
pem_rsakey = tuf.rsa_key.create_encrypted_pem(rsakey_dict, passphrase)
# Decrypt 'pem_rsakey' and verify the decrypted object is properly
# formatted.
decrypted_rsakey = tuf.rsa_key.create_from_encrypted_pem(pem_rsakey,
passphrase)
self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(decrypted_rsakey),
FORMAT_ERROR_MSG)
# Does 'decrypted_rsakey' match the original 'rsakey_dict'.
self.assertEqual(rsakey_dict, decrypted_rsakey)
# Attempt decryption of 'pem_rsakey' using an incorrect passphrase.
self.assertRaises(tuf.CryptoError,
tuf.rsa_key.create_from_encrypted_pem, pem_rsakey,
'bad_pw')
# Check for non-encrypted PEM string. create_from_encrypted_pem()/PyCrypto
# returns a tuf.formats.RSAKEY_SCHEMA object if PEM formatted string is
# not actually encrypted but still a valid PEM string.
non_encrypted_private_key = rsakey_dict['keyval']['private']
decrypted_non_encrypted = tuf.rsa_key.create_from_encrypted_pem(
non_encrypted_private_key, passphrase)
self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(
decrypted_non_encrypted), FORMAT_ERROR_MSG)
# Check for invalid arguments.
self.assertRaises(tuf.FormatError,
tuf.rsa_key.create_from_encrypted_pem, 123, passphrase)
self.assertRaises(tuf.FormatError,
tuf.rsa_key.create_from_encrypted_pem, pem_rsakey, ['pw'])
self.assertRaises(tuf.CryptoError,
tuf.rsa_key.create_from_encrypted_pem, 'invalid_pem',
passphrase)
# Run the unit tests.
if __name__ == '__main__':
unittest.main()

View file

@ -14,10 +14,8 @@
<Purpose>
Test cases for for sig.py.
"""
import unittest
import logging
@ -26,7 +24,7 @@
import tuf.formats
import tuf.keydb
import tuf.roledb
import tuf.rsa_key
import tuf.keys
import tuf.sig
logger = logging.getLogger('tuf.test_sig')
@ -34,7 +32,7 @@
# Setup the keys to use in our test cases.
KEYS = []
for _ in range(3):
KEYS.append(tuf.rsa_key.generate(2048))
KEYS.append(tuf.keys.generate_rsa_key(2048))
@ -55,7 +53,7 @@ def test_get_signature_status_no_role(self):
signable['signatures'].append(tuf.sig.generate_rsa_signature(
signable['signed'], KEYS[0]))
tuf.keydb.add_rsakey(KEYS[0])
tuf.keydb.add_key(KEYS[0])
# No specific role we're considering.
sig_status = tuf.sig.get_signature_status(signable, None)
@ -82,7 +80,7 @@ def test_get_signature_status_bad_sig(self):
signable['signed'], KEYS[0]))
signable['signed'] += 'signature no longer matches signed data'
tuf.keydb.add_rsakey(KEYS[0])
tuf.keydb.add_key(KEYS[0])
threshold = 1
roleinfo = tuf.formats.make_role_metadata(
[KEYS[0]['keyid']], threshold)
@ -112,7 +110,7 @@ def test_get_signature_status_unknown_method(self):
signable['signed'], KEYS[0]))
signable['signatures'][0]['method'] = 'fake-sig-method'
tuf.keydb.add_rsakey(KEYS[0])
tuf.keydb.add_key(KEYS[0])
threshold = 1
roleinfo = tuf.formats.make_role_metadata(
[KEYS[0]['keyid']], threshold)
@ -142,7 +140,7 @@ def test_get_signature_status_single_key(self):
signable['signatures'].append(tuf.sig.generate_rsa_signature(
signable['signed'], KEYS[0]))
tuf.keydb.add_rsakey(KEYS[0])
tuf.keydb.add_key(KEYS[0])
threshold = 1
roleinfo = tuf.formats.make_role_metadata(
[KEYS[0]['keyid']], threshold)
@ -171,7 +169,7 @@ def test_get_signature_status_below_threshold(self):
signable['signatures'].append(tuf.sig.generate_rsa_signature(
signable['signed'], KEYS[0]))
tuf.keydb.add_rsakey(KEYS[0])
tuf.keydb.add_key(KEYS[0])
threshold = 2
roleinfo = tuf.formats.make_role_metadata(
[KEYS[0]['keyid'],
@ -205,8 +203,8 @@ def test_get_signature_status_below_threshold_unrecognized_sigs(self):
signable['signatures'].append(tuf.sig.generate_rsa_signature(
signable['signed'], KEYS[2]))
tuf.keydb.add_rsakey(KEYS[0])
tuf.keydb.add_rsakey(KEYS[1])
tuf.keydb.add_key(KEYS[0])
tuf.keydb.add_key(KEYS[1])
threshold = 2
roleinfo = tuf.formats.make_role_metadata(
[KEYS[0]['keyid'],
@ -242,8 +240,8 @@ def test_get_signature_status_below_threshold_unauthorized_sigs(self):
signable['signatures'].append(tuf.sig.generate_rsa_signature(
signable['signed'], KEYS[1]))
tuf.keydb.add_rsakey(KEYS[0])
tuf.keydb.add_rsakey(KEYS[1])
tuf.keydb.add_key(KEYS[0])
tuf.keydb.add_key(KEYS[1])
threshold = 2
roleinfo = tuf.formats.make_role_metadata(
[KEYS[0]['keyid'], KEYS[2]['keyid']], threshold)
@ -278,7 +276,7 @@ def test_check_signatures_no_role(self):
signable['signatures'].append(tuf.sig.generate_rsa_signature(
signable['signed'], KEYS[0]))
tuf.keydb.add_rsakey(KEYS[0])
tuf.keydb.add_key(KEYS[0])
# No specific role we're considering. It's invalid to use the
# function tuf.sig.verify() without a role specified because
@ -295,7 +293,7 @@ def test_verify_single_key(self):
signable['signatures'].append(tuf.sig.generate_rsa_signature(
signable['signed'], KEYS[0]))
tuf.keydb.add_rsakey(KEYS[0])
tuf.keydb.add_key(KEYS[0])
threshold = 1
roleinfo = tuf.formats.make_role_metadata(
[KEYS[0]['keyid']], threshold)
@ -321,8 +319,8 @@ def test_verify_unrecognized_sig(self):
signable['signatures'].append(tuf.sig.generate_rsa_signature(
signable['signed'], KEYS[2]))
tuf.keydb.add_rsakey(KEYS[0])
tuf.keydb.add_rsakey(KEYS[1])
tuf.keydb.add_key(KEYS[0])
tuf.keydb.add_key(KEYS[1])
threshold = 2
roleinfo = tuf.formats.make_role_metadata(
[KEYS[0]['keyid'], KEYS[1]['keyid']], threshold)
@ -363,7 +361,7 @@ def test_may_need_new_keys(self):
signable['signatures'].append(tuf.sig.generate_rsa_signature(
signable['signed'], KEYS[0]))
tuf.keydb.add_rsakey(KEYS[1])
tuf.keydb.add_key(KEYS[1])
threshold = 1
roleinfo = tuf.formats.make_role_metadata(
[KEYS[1]['keyid']], threshold)

349
tuf/README.md Normal file
View file

@ -0,0 +1,349 @@
#Repository Management
![Repo Tools Diagram 1](https://raw.github.com/theupdateframework/tuf/repository-tools/docs/images/libtuf-diagram.png)
## Create TUF Repository
The **tuf.libtuf** module can be used to create a TUF repository. It may either be imported into a Python module
or used interactively in a Python interpreter.
```Bash
$ python
Python 2.7.3 (default, Sep 26 2013, 20:08:41)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from tuf.libtuf import *
>>> repository = load_repository("path/to/repository")
```
The **tuf.interposition** package and **tuf.client.updater** module assist in integrating TUF with a software updater.
### Keys
#### Create RSA Keys
```python
from tuf.libtuf import *
# Generate and write the first of two root keys for the TUF repository.
# The following function creates an RSA key pair, where the private key is saved to
# "path/to/root_key" and the public key to "path/to/root_key.pub".
generate_and_write_rsa_keypair("path/to/root_key", bits=2048, password="password")
# If the key length is unspecified, it defaults to 3072 bits. A length of less
# than 2048 bits raises an exception. A password may be supplied as an
# argument, otherwise a user prompt is presented.
generate_and_write_rsa_keypair("path/to/root_key2")
Enter a password for the RSA key:
Confirm:
```
The following four key files should now exist:
1. root_key
2. root_key.pub
3. root_key2
4. root_key2.pub
### Import RSA Keys
```python
from tuf.libtuf import *
# Import an existing public key.
public_root_key = import_rsa_publickey_from_file("path/to/root_key.pub")
# Import an existing private key. Importing a private key requires a password, whereas
# importing a public key does not.
private_root_key = import_rsa_privatekey_from_file("path/to/root_key")
Enter a password for the encrypted RSA key:
```
import_rsa_privatekey_from_file() raises a "tuf.CryptoError" exception if the key/password
is invalid.
### Create and Import ED25519 Keys
```Python
from tuf.libtuf import *
# Generate and write an ed25519 key pair. The private key is saved encrypted.
# A 'password' argument may be supplied, otherwise a prompt is presented.
generate_and_write_ed25519_keypair('path/to/ed25519_key')
Enter a password for the ED25519 key:
Confirm:
# Import the ed25519 public key just created . . .
public_ed25519_key = import_ed25519_publickey_from_file('path/to/ed25519_key.pub')
# and its corresponding private key.
private_ed25519_key = import_ed25519_privatekey_from_file('path/to/ed25519_key')
Enter a password for the encrypted ED25519 key:
```
### Create a new Repository
#### Create Root
```python
# Continuing from the previous section . . .
# Create a new Repository object that holds the file path to the repository and the four
# top-level role objects (Root, Targets, Release, Timestamp). Metadata files are created when
# repository.write() is called. The repository directory is created if it does not exist.
repository = create_new_repository("path/to/repository/")
# The Repository instance, 'repository', initially contains top-level Metadata objects.
# Add one of the public keys, created in the previous section, to the root role. Metadata is
# considered valid if it is signed by the public key's corresponding private key.
repository.root.add_key(public_root_key)
# Role keys (i.e., the key's keyid) may be queried. Other attributes include: signing_keys, version,
# signatures, expiration, threshold, delegations (Targets role), and compressions.
repository.root.keys
['b23514431a53676595922e955c2d547293da4a7917e3ca243a175e72bbf718df']
# Add a second public key to the root role. Although previously generated and saved to a file,
# the second public key must be imported before it can added to a role.
public_root_key2 = import_rsa_publickey_from_file("path/to/root_key2.pub")
repository.root.add_key(public_root_key2)
# Threshold of each role defaults to 1. Users may change the threshold value, but libtuf.py
# validates thresholds and warns users. Set the threshold of the root role to 2,
# which means the root metadata file is considered valid if it contains at least two valid
# signatures.
repository.root.threshold = 2
private_root_key2 = import_rsa_privatekey_from_file("path/to/root_key2", password="password")
# Load the root signing keys to the repository, which write() uses to sign the root metadata.
# The load_signing_key() method SHOULD warn when the key is NOT explicitly allowed to
# sign for it.
repository.root.load_signing_key(private_root_key)
repository.root.load_signing_key(private_root_key2)
# Print the number of valid signatures and public/private keys of the repository's metadata.
repository.status()
'root' role contains 2 / 2 signatures.
'targets' role contains 0 / 1 public keys.
try:
repository.write()
# An exception is raised here by write() because the other top-level roles (targets, release,
# and timestamp) have not been configured with keys. Another option is to call
# repository.write_partial() and generate metadata that may contain an invalid threshold of
# signatures, required public keys, etc. write_partial() allows multiple repository maintainers to
# independently sign metadata and generate them separately. load_repository() can load partially
# written metadata.q
except tuf.Error, e:
print e
Not enough signatures for 'path/to/repository/metadata.staged/targets.txt'
# In the next section, update the other top-level roles and create a repository with valid metadata.
```
#### Create Timestamp, Release, Targets
```python
# Continuing from the previous section . . .
# Generate keys for the remaining top-level roles. The root keys have been set above.
# The password argument may be omitted if a password prompt is needed.
generate_and_write_rsa_keypair("path/to/targets_key", password="password")
generate_and_write_rsa_keypair("path/to/release_key", password="password")
generate_and_write_rsa_keypair("path/to/timestamp_key", password="password")
# Add the public keys of the remaining top-level roles.
repository.targets.add_key(import_rsa_publickey_from_file("path/to/targets_key.pub"))
repository.release.add_key(import_rsa_publickey_from_file("path/to/release_key.pub"))
repository.timestamp.add_key(import_rsa_publickey_from_file("path/to/timestamp_key.pub"))
# Import the signing keys of the remaining top-level roles. Prompt for passwords.
private_targets_key = import_rsa_privatekey_from_file("path/to/targets_key")
Enter a password for the encrypted RSA key:
private_release_key = import_rsa_privatekey_from_file("path/to/release_key")
Enter a password for the encrypted RSA key:
private_timestamp_key = import_rsa_privatekey_from_file("path/to/timestamp_key")
Enter a password for the encrypted RSA key:
# Load the signing keys of the remaining roles so that valid signatures are generated when
# repository.write() is called.
repository.targets.load_signing_key(private_targets_key)
repository.release.load_signing_key(private_release_key)
repository.timestamp.load_signing_key(private_timestamp_key)
# Optionally set the expiration date of the timestamp role. By default, roles are set to expire
# as follows: root(1 year), targets(3 months), release(1 week), timestamp(1 day).
repository.timestamp.expiration = "2014-10-28 12:08:00"
# Metadata files may also be compressed. Only "gz" is currently supported.
repository.targets.compressions = ["gz"]
repository.release.compressions = ["gz"]
# Write all metadata to "path/to/repository/metadata.staged/". The common case is to crawl the
# filesystem for all delegated roles in "path/to/repository/metadata.staged/targets/".
repository.write()
```
### Targets
#### Add Target Files
```Bash
# Create and save target files to the targets directory of the repository.
$ cd path/to/repository/targets/
$ echo 'file1' > file1.txt
$ echo 'file2' > file2.txt
$ echo 'file3' > file3.txt
$ mkdir django; echo 'file4' > django/file4.txt
```
```python
from tuf.libtuf import *
# Load the repository created in the previous section. This repository so far contains metadata for
# the top-level roles, but no targets.
repository = load_repository("path/to/repository/")
# Get a list of file paths in a directory, even those in sub-directories.
# This must be relative to an existing directory in the repository, raise an exception.
list_of_targets = repository.get_filepaths_in_directory("path/to/repository/targets/",
recursive_walk=False, followlinks=True)
# Add the list of target paths to the metadata of the Targets role. Any target file paths
# that may already exist are NOT replaced. add_targets() does not create or move target files.
repository.targets.add_targets(list_of_targets)
# Individual target files may also be added.
repository.targets.add_target("path/to/repository/targets/file3.txt")
# The private key of the updated targets metadata must be loaded before it can be signed and
# written (Note the load_repository() call above).
private_targets_key = import_rsa_privatekey_from_file("path/to/targets_key")
Enter a password for the encrypted RSA key:
repository.targets.load_signing_key(private_targets_key)
# Due to the load_repository(), we must also load the private keys of the other top-level roles
# to generate a valid set of metadata.
private_root_key = import_rsa_privatekey_from_file("path/to/root_key")
Enter a password for the encrypted RSA key:
private_root_key2 = import_rsa_privatekey_from_file("path/to/root_key2")
Enter a password for the encrypted RSA key:
private_release_key = import_rsa_privatekey_from_file("path/to/release_key")
Enter a password for the encrypted RSA key:
private_timestamp_key = import_rsa_privatekey_from_file("path/to/timestamp_key")
Enter a password for the encrypted RSA key:
repository.root.load_signing_key(private_root_key)
repository.root.load_signing_key(private_root_key2)
repository.release.load_signing_key(private_release_key)
repository.timestamp.load_signing_key(private_timestamp_key)
# Generate new versions of all the top-level metadata.
repository.write()
```
#### Remove Target Files
```python
# Continuing from the previous section . . .
# Remove a target file listed in the "targets" metadata. The target file is not actually deleted
# from the file system.
repository.targets.remove_target("path/to/repository/targets/file3.txt")
# repository.write() creates any new metadata files, updates those that have changed, and any that
# need updating to make a new "release" (new release.txt and timestamp.txt).
repository.write()
```
### Delegations
```python
# Continuing from the previous section . . .
# Generate a key for a new delegated role named "unclaimed".
generate_and_write_rsa_keypair("path/to/unclaimed_key", bits=2048, password="password")
public_unclaimed_key = import_rsa_publickey_from_file("path/to/unclaimed_key.pub")
# Make a delegation from "targets" to "targets/unclaimed", initially containing zero targets.
# The delegated roles full name is not expected.
# delegate(rolename, list_of_public_keys, list_of_file_paths, threshold,
# restricted_paths, path_hash_prefixes)
repository.targets.delegate("unclaimed", [public_unclaimed_key], [])
# Load the private key of "targets/unclaimed" so that signatures are later added and valid
# metadata is created.
private_unclaimed_key = import_rsa_privatekey_from_file("path/to/unclaimed_key")
Enter a password for the encrypted RSA key:
repository.targets.unclaimed.load_signing_key(private_unclaimed_key)
# Update an attribute of the unclaimed role.
repository.targets('unclaimed').version = 2
# Delegations may also be nested. Create the delegated role "targets/unclaimed/django",
# where it initially contains zero targets and future targets are restricted to a
# particular directory.
repository.targets('unclaimed').delegate("django", [public_unclaimed_key], [],
restricted_paths=["path/to/repository/targets/django/"])
repository.targets('unclaimed')('django').load_signing_key(private_unclaimed_key)
repository.targets('unclaimed')('django').add_target("path/to/repository/targets/django/file4.txt")
repository.targets('unclaimed')('django').compressions = ["gz"]
# Write the metadata of "targets/unclaimed", "targets/unclaimed/django", root, targets, release,
# and timestamp.
repository.write()
```
#### Revoke Delegated Role
```python
# Continuing from the previous section . . .
# Create a delegated role that will be revoked in the next step.
repository.targets('unclaimed').delegate("flask", [public_unclaimed_key], [])
# Revoke "targets/unclaimed/flask" and write the metadata of all remaining roles.
repository.targets('unclaimed').revoke("flask")
repository.write()
```
```bash
# Copy the staged metadata directory changes to the live repository.
$ cp -r "path/to/repository/metadata.staged/" "path/to/repository/metadata/"
```
## Client Setup and Repository Trial
### Using TUF Within an Example Client Updater
```python
from tuf.libtuf import *
# The following function creates a directory structure that a client
# downloading new software using TUF (via tuf/client/updater.py) will expect.
# The root.txt metadata file must exist, and also the directories that hold the metadata files
# downloaded from a repository. Software updaters integrating with TUF may use this
# directory to store TUF updates saved on the client side. create_tuf_client_directory()
# moves metadata from "path/to/repository/metadata" to "path/to/client/". The repository
# in "path/to/repository/" is the repository created in the "Create TUF Repository" section.
create_tuf_client_directory("path/to/repository/", "path/to/client/")
```
#### Test TUF Locally
```Bash
# Run the local TUF repository server.
$ cd "path/to/repository/"; python -m SimpleHTTPServer 8001
# Retrieve targets from the TUF repository and save them to "path/to/client/". The
# basic_client.py module is available in "tuf/client/".
# In a different command-line prompt . . .
$ cd "path/to/client/"
$ ls
metadata/
$ basic_client.py --repo http://localhost:8001
$ ls . targets/ targets/django/
.:
metadata targets tuf.log
targets/:
django file1.txt file2.txt
targets/django/:
file4.txt
```

View file

@ -18,7 +18,6 @@
The names chosen for TUF Exception classes should end in
'Error' except where there is a good reason not to, and
provide that reason in those cases.
"""
import urlparse
@ -117,6 +116,14 @@ class RepositoryError(Error):
class InsufficientKeysError(Error):
"""Indicate that metadata role lacks a threshold of pubic or private keys."""
pass
class ForbiddenTargetError(RepositoryError):
"""Indicate that a role signed for a target that it was not delegated to."""
pass
@ -165,7 +172,7 @@ class CryptoError(Error):
class BadSignatureError(CryptoError):
"""Indicate that some metadata file had a bad signature."""
"""Indicate that some metadata file has a bad signature."""
def __init__(self, metadata_role_name):
self.metadata_role_name = metadata_role_name
@ -285,6 +292,13 @@ class InvalidNameError(Error):
class UnsignedMetadataError(Error):
"""Indicate metadata object with insufficient threshold of signatures."""
class NoWorkingMirrorError(Error):
"""An updater will throw this exception in case it could not download a
metadata or target file.

0
ed25519/__init__.py → tuf/_vendor/__init__.py Executable file → Normal file
View file

1
tuf/_vendor/ed25519/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.pyc

View file

@ -0,0 +1,28 @@
language: python
python: 2.7
env:
- TOXENV=py26
- TOXENV=py27
#- TOXENV=py32
#- TOXENV=py33
- TOXENV=pypy
install:
# Add the PyPy repository
- "if [[ $TOXENV == 'pypy' ]]; then sudo add-apt-repository -y ppa:pypy/ppa; fi"
# Upgrade PyPy
- "if [[ $TOXENV == 'pypy' ]]; then sudo apt-get -y install pypy; fi"
# This is required because we need to get rid of the Travis installed PyPy
# or it'll take precedence over the PPA installed one.
- "if [[ $TOXENV == 'pypy' ]]; then sudo rm -rf /usr/local/pypy/bin; fi"
- pip install tox
script:
- tox
notifications:
irc:
channels:
- "irc.freenode.org#cryptography-dev"
use_notice: true
skip_join: true

View file

View file

@ -0,0 +1,161 @@
import hashlib
b = 256
q = 2 ** 255 - 19
l = 2 ** 252 + 27742317777372353535851937790883648493
def H(m):
return hashlib.sha512(m).digest()
def pow2(x, p):
"""== pow(x, 2**p, q)"""
while p > 0:
x = x * x % q
p -= 1
return x
def inv(z):
"""$= z^{-1} \mod q$, for z != 0"""
# Adapted from curve25519_athlon.c in djb's Curve25519.
z2 = z * z % q # 2
z9 = pow2(z2, 2) * z % q # 9
z11 = z9 * z2 % q # 11
z2_5_0 = (z11*z11)%q * z9 % q # 31 == 2^5 - 2^0
z2_10_0 = pow2(z2_5_0, 5) * z2_5_0 % q # 2^10 - 2^0
z2_20_0 = pow2(z2_10_0, 10) * z2_10_0 % q # ...
z2_40_0 = pow2(z2_20_0, 20) * z2_20_0 % q
z2_50_0 = pow2(z2_40_0, 10) * z2_10_0 % q
z2_100_0 = pow2(z2_50_0, 50) * z2_50_0 % q
z2_200_0 = pow2(z2_100_0, 100) * z2_100_0 % q
z2_250_0 = pow2(z2_200_0, 50) * z2_50_0 % q # 2^250 - 2^0
return pow2(z2_250_0, 5) * z11 % q # 2^255 - 2^5 + 11 = q - 2
d = -121665 * inv(121666)
I = pow(2, (q - 1) / 4, q)
def xrecover(y):
xx = (y * y - 1) * inv(d * y * y + 1)
x = pow(xx, (q + 3) / 8, q)
if (x * x - xx) % q != 0:
x = (x * I) % q
if x % 2 != 0:
x = q-x
return x
By = 4 * inv(5)
Bx = xrecover(By)
B = (Bx % q, By % q)
def edwards(P, Q):
x1, y1 = P
x2, y2 = Q
x3 = (x1 * y2 + x2 * y1) * inv(1 + d * x1 * x2 * y1 * y2)
y3 = (y1 * y2 + x1 * x2) * inv(1 - d * x1 * x2 * y1 * y2)
return (x3 % q, y3 % q)
def scalarmult(P, e):
if e == 0:
return (0, 1)
Q = scalarmult(P, e / 2)
Q = edwards(Q, Q)
if e & 1:
Q = edwards(Q, P)
return Q
def encodeint(y):
bits = [(y >> i) & 1 for i in range(b)]
return ''.join([
chr(sum([bits[i * 8 + j] << j for j in range(8)]))
for i in range(b/8)
])
def encodepoint(P):
x = P[0]
y = P[1]
bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1]
return ''.join([
chr(sum([bits[i * 8 + j] << j for j in range(8)]))
for i in range(b/8)
])
def bit(h, i):
return (ord(h[i / 8]) >> (i % 8)) & 1
def publickey(sk):
h = H(sk)
a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2))
A = scalarmult(B, a)
return encodepoint(A)
def Hint(m):
h = H(m)
return sum(2 ** i * bit(h, i) for i in range(2 * b))
def signature(m, sk, pk):
h = H(sk)
a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2))
r = Hint(''.join([h[j] for j in range(b / 8, b / 4)]) + m)
R = scalarmult(B, r)
S = (r + Hint(encodepoint(R) + pk + m) * a) % l
return encodepoint(R) + encodeint(S)
def isoncurve(P):
x, y = P
return (-x * x + y * y - 1 - d * x * x * y * y) % q == 0
def decodeint(s):
return sum(2 ** i * bit(s, i) for i in range(0, b))
def decodepoint(s):
y = sum(2 ** i * bit(s, i) for i in range(0, b - 1))
x = xrecover(y)
if x & 1 != bit(s, b-1):
x = q-x
P = (x, y)
if not isoncurve(P):
raise Exception("decoding point that is not on curve")
return P
def checkvalid(s, m, pk):
if len(s) != b / 4:
raise Exception("signature length is wrong")
if len(pk) != b / 8:
raise Exception("public-key length is wrong")
R = decodepoint(s[:b / 8])
A = decodepoint(pk)
S = decodeint(s[b / 8:b / 4])
h = Hint(encodepoint(R) + pk + m)
if scalarmult(B, S) != edwards(R, scalarmult(A, h)):
raise Exception("signature does not pass verification")

View file

@ -0,0 +1,8 @@
#!/bin/sh
set -e
python -u signfast.py < sign.input
if [[ $TEST == 'slow' ]]; then
python -u sign.py < sign.input
fi

View file

@ -0,0 +1,32 @@
import os
import timeit
import ed25519
seed = os.urandom(32)
data = "The quick brown fox jumps over the lazy dog"
private_key = seed
public_key = ed25519.publickey(seed)
signature = ed25519.signature(data, private_key, public_key)
print('Time generate')
print(timeit.timeit("ed25519.publickey(seed)",
setup="from __main__ import ed25519, seed",
number=10,
))
print('\nTime create signature')
print(timeit.timeit("ed25519.signature(data, private_key, public_key)",
setup="from __main__ import ed25519, data, private_key, public_key",
number=10,
))
print('\nTime verify signature')
print(timeit.timeit("ed25519.checkvalid(signature, data, public_key)",
setup="from __main__ import ed25519, signature, data, public_key",
number=10,
))

View file

@ -0,0 +1,11 @@
from setuptools import setup
setup(
name="ed25519",
version="1.0",
py_modules="ed25519",
zip_safe=False,
)

View file

@ -1,3 +1,5 @@
from __future__ import print_function
import sys
import binascii
import ed25519
@ -16,6 +18,7 @@
while 1:
line = sys.stdin.readline()
if not line: break
print(".", end="")
x = line.split(':')
sk = binascii.unhexlify(x[0][0:64])
pk = ed25519.publickey(sk)

View file

@ -0,0 +1,48 @@
from __future__ import print_function
import sys
import binascii
import ed25519
# examples of inputs: see sign.input
# should produce no output: python sign.py < sign.input
# warning: currently 37 seconds/line on a fast machine
# fields on each input line: sk, pk, m, sm
# each field hex
# each field colon-terminated
# sk includes pk at end
# sm includes m at end
MAX = 10
i = 0
while 1:
if i >= MAX:
break
i += 1
line = sys.stdin.readline()
if not line: break
print(".", end="")
x = line.split(':')
sk = binascii.unhexlify(x[0][0:64])
pk = ed25519.publickey(sk)
m = binascii.unhexlify(x[2])
s = ed25519.signature(m,sk,pk)
ed25519.checkvalid(s,m,pk)
forgedsuccess = 0
try:
if len(m) == 0:
forgedm = "x"
else:
forgedmlen = len(m)
forgedm = ''.join([chr(ord(m[i])+(i==forgedmlen-1)) for i in range(forgedmlen)])
ed25519.checkvalid(s,forgedm,pk)
forgedsuccess = 1
except:
pass
assert not forgedsuccess
assert x[0] == binascii.hexlify(sk + pk)
assert x[1] == binascii.hexlify(pk)
assert x[3] == binascii.hexlify(s + m)

View file

@ -0,0 +1,5 @@
[tox]
envlist = py26,py27,pypy,py32,py33
[testenv]
commands = ./runtests.sh

172
tuf/client/README.md Normal file
View file

@ -0,0 +1,172 @@
#updater.py
**updater.py** is intended as the only TUF module that software update
systems need to utilize for a low-level integration. It provides a single
class representing an updater that includes methods to download, install, and
verify metadata or target files in a secure manner. Importing
**tuf.client.updater.py** and instantiating its main class is all that is
required by the client prior to a TUF update request. The importation and
instantiation steps allow TUF to load all of the required metadata files
and set the repository mirror information.
The **tuf.libtuf** module can be used to create a TUF repository. See
[tuf/README](../README.md) for more information on creating TUF repositories.
The **tuf.interposition** package can also assist in integrating TUF with a
software updater. See [tuf/interposition/README](../interposition/README.md)
for more information on interposing Python urllib calls with TUF.
## Overview of the Update Process
1. The software update system instructs TUF to check for updates.
2. TUF downloads and verifies timestamp.txt.
3. If timestamp.txt indicates that release.txt has changed, TUF downloads and
verifies release.txt.
4. TUF determines which metadata files listed in release.txt differ from those
described in the last release.txt that TUF has seen. If root.txt has changed,
the update process starts over using the new root.txt.
5. TUF provides the software update system with a list of available files
according to targets.txt.
6. The software update system instructs TUF to download a specific target
file.
7. TUF downloads and verifies the file and then makes the file available to
the software update system.
## Example Client
### Refresh TUF Metadata and Download Target Files
```Python
# The client first imports the 'updater.py' module, the only module the
# client is required to import. The client will utilize a single class
# from this module.
import tuf.client.updater
# The only other module the client interacts with is 'tuf.conf'. The
# client accesses this module solely to set the repository directory.
# This directory will hold the files downloaded from a remote repository.
tuf.conf.repository_directory = 'path/to/local_repository'
# Next, the client creates a dictionary object containing the repository
# mirrors. The client may download content from any one of these mirrors.
# In the example below, a single mirror named 'mirror1' is defined. The
# mirror is located at 'http://localhost:8001', and all of the metadata
# and targets files can be found in the 'metadata' and 'targets' directory,
# respectively. If the client wishes to only download target files from
# specific directories on the mirror, the 'confined_target_dirs' field
# should be set. In the example, the client has chosen '', which is
# interpreted as no confinement. In other words, the client can download
# targets from any directory or subdirectories. If the client had chosen
# 'targets1/', they would have been confined to the '/targets/targets1/'
# directory on the 'http://localhost:8001' mirror.
repository_mirrors = {'mirror1': {'url_prefix': 'http://localhost:8001',
'metadata_path': 'metadata',
'targets_path': 'targets',
'confined_target_dirs': ['']}}
# The updater may now be instantiated. The Updater class of 'updater.py'
# is called with two arguments. The first argument assigns a name to this
# particular updater and the second argument the repository mirrors defined
# above.
updater = tuf.client.updater.Updater('updater', repository_mirrors)
# The client calls the refresh() method to ensure it has the latest
# copies of the top-level metadata files (i.e., Root, Targets, Release,
# Timestamp).
updater.refresh()
# The target file information of all the repository targets is determined next.
# Since all_targets() downloads the target files of every role, all role
# metadata is updated.
targets = updater.all_targets()
# Among these targets, determine the ones that have changed since the client's
# last refresh(). A target is considered updated if it does not exist in
# 'destination_directory' (current directory) or the target located there has
# changed.
destination_directory = '.'
updated_targets = updater.updated_targets(targets, destination_directory)
# Lastly, attempt to download each target among those that have changed.
# The updated target files are saved locally to 'destination_directory'.
for target in updated_targets:
updater.download_target(target, destination_directory)
# Remove any files from the destination directory that are no longer being
# tracked. For example, a target file from a previous release that has since
# been removed on the remote repository.
updater.remove_obsolete_targets(destination_directory)
```
### Download Target Files of a Role
```Python
# Example demonstrating an update that only downloads the targets of
# a specific role (i.e., 'targets/django').
# Refresh the metadata of the top-level roles (i.e., Root, Targets, Release, Timestamp).
updater.refresh()
# Update the 'targets/django' role, and determine the target files that have changed.
# targets_of_role() refreshes the minimum metadata needed to download the target files
# of the specified role (e.g., R1->R4->R5, where R2 and R3 are excluded).
targets_of_django = updater.targets_of_role('targets/django')
updated_targets = updater.updated_targets(targets_of_django, destination_directory)
for target in updated_targets:
updater.download_target(target, destination_directory)
```
### Download Specific Target File
```Python
# Example demonstrating an update that downloads a specific target.
# Refresh the metadata of the top-level roles (i.e., Root, Targets, Release, Timestamp).
updater.refresh()
# target() updates role metadata when required.
target = updater.target('LICENSE.txt')
updated_target = updater.updated_targets([target], destination_directory)
for target in updated_target:
updater.download_target(target, destination_directory)
```
###A Simple Integration Example with basic_client.py
```Bash
# Assume a simple TUF repository has been setup with 'tuf.libtuf.py'.
$ basic_client.py --repo http://localhost:8001
# Metadata and target files are silently updated. An exception is only raised if an error,
# or attack, is detected. Inspect 'tuf.log' for the outcome of the update process.
$ cat tuf.log
[2013-12-16 16:17:05,267 UTC] [tuf.download] [INFO][_download_file:726@download.py]
Downloading: http://localhost:8001/metadata/timestamp.txt
[2013-12-16 16:17:05,269 UTC] [tuf.download] [WARNING][_check_content_length:589@download.py]
reported_length (545) < required_length (2048)
[2013-12-16 16:17:05,269 UTC] [tuf.download] [WARNING][_check_downloaded_length:656@download.py]
Downloaded 545 bytes, but expected 2048 bytes. There is a difference of 1503 bytes!
[2013-12-16 16:17:05,611 UTC] [tuf.download] [INFO][_download_file:726@download.py]
Downloading: http://localhost:8001/metadata/release.txt
[2013-12-16 16:17:05,612 UTC] [tuf.client.updater] [INFO][_check_hashes:636@updater.py]
The file\'s sha256 hash is correct: 782675fadd650eeb2926d33c401b5896caacf4fd6766498baf2bce2f3b739db4
[2013-12-16 16:17:05,951 UTC] [tuf.download] [INFO][_download_file:726@download.py]
Downloading: http://localhost:8001/metadata/targets.txt
[2013-12-16 16:17:05,952 UTC] [tuf.client.updater] [INFO][_check_hashes:636@updater.py]
The file\'s sha256 hash is correct: a5019c28a1595c43a14cad2b6252c4d1db472dd6412a9204181ad6d61b1dd69a
[2013-12-16 16:17:06,299 UTC] [tuf.download] [INFO][_download_file:726@download.py]
Downloading: http://localhost:8001/targets/file1.txt
[2013-12-16 16:17:06,303 UTC] [tuf.client.updater] [INFO][_check_hashes:636@updater.py]
The file's sha256 hash is correct: ecdc5536f73bdae8816f0ea40726ef5e9b810d914493075903bb90623d97b1d8

View file

@ -211,7 +211,7 @@ def parse_options():
# the current directory.
try:
update_client(repository_mirror)
except (tuf.RepositoryError, tuf.ExpiredMetadataError), e:
except (tuf.NoWorkingMirrorError, tuf.RepositoryError), e:
sys.stderr.write('Error: '+str(e)+'\n')
sys.exit(1)

File diff suppressed because it is too large Load diff

View file

@ -12,11 +12,12 @@
See LICENSE for licensing information.
<Purpose>
A central location for TUF configuration settings.
A central location for TUF configuration settings. Example options include
setting the destination of temporary files and downloaded content, the maximum
length of downloaded metadata (unknown file attributes), download behavior,
and cryptography libraries clients wish to use.
"""
# Set a directory that should be used for all temporary files. If this
# is None, then the system default will be used. The system default
# will also be used if a directory path set here is invalid or
@ -41,6 +42,12 @@
# default but sane upper bound for the number of bytes required to download it.
DEFAULT_TIMESTAMP_REQUIRED_LENGTH = 16384 #bytes
# The Root role may be updated without knowing its hash if top-level metadata
# cannot be safely downloaded (e.g., keys may have been revoked, thus requiring
# a new Root file that includes the updated keys). Set a default upper bound
# for the maximum total bytes that may be downloaded for Root metadata.
DEFAULT_ROOT_REQUIRED_LENGTH = 512000 #bytes
# Set a timeout value in seconds (float) for non-blocking socket operations.
SOCKET_TIMEOUT = 1 #seconds
@ -62,6 +69,26 @@
# computational restrictions. A strong user password is still important.
# Modifying the number of iterations will result in a new derived key+PBDKF2
# combination if the key is loaded and re-saved, overriding any previous
# iteration setting used by the old '<keyid>.key'.
# iteration setting used in the old '<keyid>' key file.
# https://en.wikipedia.org/wiki/PBKDF2
PBKDF2_ITERATIONS = 100000
# The user client may set the specific cryptography library used by The Update
# Framework updater, or the software updater integrating TUF.
# Supported RSA cryptography libraries: ['pycrypto']
RSA_CRYPTO_LIBRARY = 'pycrypto'
# Supported ed25519 cryptography libraries: ['pynacl', 'ed25519']
ED25519_CRYPTO_LIBRARY = 'ed25519'
# General purpose cryptography. Algorithms and functions that fall under general
# purpose include AES, PBKDF2, cryptographically strong random number
# generators, and cryptographic hash functions. The majority of the general
# cryptography is needed by the repository and developer tools.
# RSA_CRYPTO_LIBRARY and ED25519_CRYPTO_LIBRARY are needed on the client side
# of the software updater.
GENERAL_CRYPTO_LIBRARY = 'pycrypto'
# The algorithm in HASH_ALGORITHMS are chosen by the repository tool to generate
# the digests listed in metadata.
REPOSITORY_HASH_ALGORITHMS = ['sha224', 'sha256']

View file

@ -19,7 +19,6 @@
file-like object that will automatically destroys itself once closed. Note
that the file-like object, 'tuf.util.TempFile', is returned by the
'_download_file()' function.
"""
# Induce "true division" (http://www.python.org/dev/peps/pep-0238/).
@ -106,7 +105,6 @@ def __start_clock(self):
<Returns>
None.
"""
# We must have reset the clock before this.
@ -139,7 +137,6 @@ def __stop_clock_and_check_speed(self, data_length):
<Returns>
None.
"""
# We use (platform-specific) wall time, so it will be imprecise sometimes.
@ -201,7 +198,6 @@ def read(self, size):
<Returns>
Received data up to 'size' bytes.
"""
# We should never try to specify a negative size.
@ -426,7 +422,6 @@ def _open_connection(url):
<Returns>
File-like object.
"""
# urllib2.Request produces a Request object that allows for a finer control
@ -478,7 +473,6 @@ def _download_fixed_amount_of_data(connection, temp_file, required_length):
total_downloaded:
The total number of bytes we have downloaded for the desired file and
which should be equal to 'required_length'.
"""
# Keep track of total bytes downloaded.
@ -538,7 +532,6 @@ def _get_content_length(connection):
reported_length:
The total number of bytes reported by server. If the process fails, we
return None; otherwise we would return a nonnegative integer.
"""
try:
@ -559,7 +552,7 @@ def _get_content_length(connection):
def _check_content_length(reported_length, required_length):
def _check_content_length(reported_length, required_length, strict_length=True):
"""
<Purpose>
A helper function that checks whether the length reported by server is
@ -572,6 +565,10 @@ def _check_content_length(reported_length, required_length):
required_length:
The total number of bytes obtained from (possibly default) metadata.
strict_length:
Boolean that indicates whether the required length of the file is an
exact match, or an upper limit (e.g., downloading a Timestamp file).
<Side Effects>
No known side effects.
@ -580,21 +577,33 @@ def _check_content_length(reported_length, required_length):
<Returns>
None.
"""
logger.debug('The server reported a length of '+repr(reported_length)+' bytes.')
comparison_result = None
try:
if reported_length < required_length:
logger.warn('reported_length ('+str(reported_length)+
') < required_length ('+str(required_length)+')')
comparison_result = 'less than'
elif reported_length > required_length:
logger.warn('reported_length ('+str(reported_length)+
') > required_length ('+str(required_length)+')')
comparison_result = 'greater than'
else:
logger.debug('reported_length ('+str(reported_length)+
') == required_length ('+str(required_length)+')')
comparison_result = 'equal to'
except:
logger.exception('Could not check reported and required lengths!')
logger.exception('Could not check reported and required lengths.')
if strict_length:
message = 'The reported length is '+comparison_result+' the required '+\
'length of '+repr(required_length)+' bytes.'
logger.debug(message)
else:
message = 'The reported length is '+comparison_result+' the upper limit '+\
'of '+repr(required_length)+' bytes.'
logger.debug(message)
@ -612,8 +621,11 @@ def _check_downloaded_length(total_downloaded, required_length,
The total number of bytes supposedly downloaded for the file in question.
required_length:
The total number of bytes expected of the file as seen from its (possibly
default) metadata.
The total number of bytes expected of the file as seen from its metadata.
The Timestamp role is always downloaded without a known file length, and
the Root role when the client cannot download any of the required
top-level roles. In both cases, 'required_length' is actually an upper
limit on the length of the downloaded file.
STRICT_REQUIRED_LENGTH:
A Boolean indicator used to signal whether we should perform strict
@ -630,30 +642,33 @@ def _check_downloaded_length(total_downloaded, required_length,
<Returns>
None.
"""
if total_downloaded == required_length:
logger.debug('total_downloaded == required_length == '+
str(required_length))
logger.info('Downloaded '+str(total_downloaded)+' bytes out of the '+\
'expected '+str(required_length)+ ' bytes.')
else:
difference_in_bytes = abs(total_downloaded-required_length)
message = 'Downloaded '+str(total_downloaded)+' bytes, but expected '+\
str(required_length)+' bytes. There is a difference of '+\
str(difference_in_bytes)+' bytes!'
# What we downloaded is not equal to the required length, but did we ask
# for strict checking of required length?
if STRICT_REQUIRED_LENGTH:
if STRICT_REQUIRED_LENGTH:
message = 'Downloaded '+str(total_downloaded)+' bytes, but expected '+\
str(required_length)+' bytes. There is a difference of '+\
str(difference_in_bytes)+' bytes.'
# This must be due to a programming error, and must never happen!
logger.error(message)
raise tuf.DownloadLengthMismatchError(required_length, total_downloaded)
else:
message = 'Downloaded '+str(total_downloaded)+' bytes out of an upper '+\
'limit of '+str(required_length)+' bytes.'
# We specifically disabled strict checking of required length, but we
# will log a warning anyway. This is useful when we wish to download the
# timestamp metadata, for which we have no signed metadata; so, we must
# guess a reasonable required_length for it.
logger.warn(message)
# Timestamp or Root metadata, for which we have no signed metadata; so,
# we must guess a reasonable required_length for it.
logger.info(message)
@ -711,7 +726,6 @@ def _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True):
<Returns>
A 'tuf.util.TempFile' file-like object which points to the contents of
'url'.
"""
# Do all of the arguments have the appropriate format?
@ -748,7 +762,8 @@ def _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True):
reported_length = _get_content_length(connection)
# Then, we check whether the required length matches the reported length.
_check_content_length(reported_length, required_length)
_check_content_length(reported_length, required_length,
STRICT_REQUIRED_LENGTH)
# Download the contents of the URL, up to the required length, to a
# temporary file, and get the total number of downloaded bytes.
@ -773,8 +788,3 @@ def _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True):
# Restore previously saved values or functions.
httplib.HTTPConnection.response_class = previous_http_response_class
socket.setdefaulttimeout(previous_socket_timeout)

View file

@ -1,624 +0,0 @@
"""
<Program Name>
ed25519_key.py
<Author>
Vladimir Diaz <vladimir.v.diaz@gmail.com>
<Started>
September 24, 2013.
<Copyright>
See LICENSE for licensing information.
<Purpose>
The goal of this module is to support ed25519 signatures. ed25519 is an
elliptic-curve public key signature scheme, its main strength being small
signatures (64 bytes) and small public keys (32 bytes).
http://ed25519.cr.yp.to/
'tuf/ed25519_key.py' calls 'ed25519/ed25519.py', which is the pure Python
implementation of ed25519 provided by the author:
http://ed25519.cr.yp.to/software.html
Optionally, ed25519 cryptographic operations may be executed by PyNaCl, which
provides Python bindings to the NaCl library and is much faster than the pure
python implementation. PyNaCl relies on the C library, libsodium.
https://github.com/dstufft/pynacl
https://github.com/jedisct1/libsodium
http://nacl.cr.yp.to/
The ed25519-related functions included here are generate(), create_signature()
and verify_signature(). The 'ed25519' and PyNaCl (i.e., 'nacl') modules used
by ed25519_key.py generate the actual ed25519 keys and the functions listed
above can be viewed as an easy-to-use public interface. Additional functions
contained here include format_keyval_to_metadata() and
format_metadata_to_key(). These last two functions produce or use
ed25519 keys compatible with the key structures listed in TUF Metadata files.
The generate() function returns a dictionary containing all the information
needed of ed25519 keys, such as public/private keys and a keyID identifier.
create_signature() and verify_signature() are supplemental functions used for
generating ed25519 signatures and verifying them.
Key IDs are used as identifiers for keys (e.g., RSA key). They are the
hexadecimal representation of the hash of key object (specifically, the key
object containing only the public key). Review 'ed25519_key.py' and the
'_get_keyid()' function to see precisely how keyids are generated. One may
get the keyid of a key object by simply accessing the dictionary's 'keyid'
key (i.e., ed25519_key_dict['keyid']).
"""
# Help with Python 3 compatability, where the print statement is a function, an
# implicit relative import is invalid, and the '/' operator performs true
# division. Example: print 'hello world' raises a 'SyntaxError' exception.
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
# Required for hexadecimal conversions. Signatures and public/private keys are
# hexlified.
import binascii
# Generate OS-specific randomness (os.urandom) suitable for cryptographic use.
# http://docs.python.org/2/library/os.html#miscellaneous-functions
import os
import tuf
# Import the python implementation of the ed25519 algorithm that is provided by
# the author. Note: This implementation is very slow and does not include
# protection against side-channel attacks according to the author. Verifying
# signatures can take approximately 9 seconds on an intel core 2 duo @
# 2.2 ghz x 2). Optionally, the PyNaCl module may be used to speed up ed25519
# cryptographic operations.
# http://ed25519.cr.yp.to/software.html
# Try to import PyNaCl. The functions found in this module provide the option
# of using PyNaCl over the slower implementation of ed25519.
try:
import nacl.signing
import nacl.encoding
except (ImportError, IOError):
message = 'The PyNacl library and/or its dependencies cannot be imported.'
raise tuf.UnsupportedLibraryError(message)
# The pure Python implementation of ed25519.
import ed25519.ed25519
# Digest objects needed to generate hashes.
import tuf.hash
# Perform object format-checking.
import tuf.formats
# The default hash algorithm to use when generating KeyIDs.
_KEY_ID_HASH_ALGORITHM = 'sha256'
# Supported ed25519 signing methods. 'ed25519-python' is the pure Python
# implementation signing method. 'ed25519-pynacl' (i.e., 'nacl' module) is the
# (libsodium+Python bindings) implementation signing method.
_SUPPORTED_ED25519_SIGNING_METHODS = ['ed25519-python', 'ed25519-pynacl']
def generate(use_pynacl=False):
"""
<Purpose>
Generate an ed25519 seed key ('sk') and public key ('pk').
In addition, a keyid used as an identifier for ed25519 keys is generated.
The object returned conforms to 'tuf.formats.ED25519KEY_SCHEMA' and has the
form:
{'keytype': 'ed25519',
'keyid': keyid,
'keyval': {'public': '876f5584a9db99b8546c0d8608d6...',
'private': 'bf7336055c7638276efe9afe039...'}}
The public and private keys are strings. An ed25519 seed key is a random
32-byte value and public key 32 bytes, although both are hexlified to 64
bytes.
>>> ed25519_key = generate()
>>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key)
True
>>> len(ed25519_key['keyval']['public'])
64
>>> len(ed25519_key['keyval']['private'])
64
>>> ed25519_key_pynacl = generate(use_pynacl=True)
>>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key_pynacl)
True
>>> len(ed25519_key_pynacl['keyval']['public'])
64
>>> len(ed25519_key_pynacl['keyval']['private'])
64
<Arguments>
use_pynacl:
True, if the ed25519 keys should be generated with PyNaCl. False, if the
keys should be generated with the pure Python implementation of ed25519
(much slower).
<Exceptions>
NotImplementedError, if a randomness source is not found.
<Side Effects>
The ed25519 keys are generated by first creating a random 32-byte value
'sk' with os.urandom() and then calling ed25519's ed25519.25519.publickey(sk)
or PyNaCl's nacl.signing.SigningKey().
<Returns>
A dictionary containing the ed25519 keys and other identifying information.
Conforms to 'tuf.formats.ED25519KEY_SCHEMA'.
"""
# Begin building the ed25519 key dictionary.
ed25519_key_dict = {}
keytype = 'ed25519'
# Generate ed25519's seed key by calling os.urandom(). The random bytes
# returned should be suitable for cryptographic use and is OS-specific.
# Raise 'NotImplementedError' if a randomness source is not found.
# ed25519 seed keys are fixed at 32 bytes (256-bit keys).
# http://blog.mozilla.org/warner/2011/11/29/ed25519-keys/
seed = os.urandom(32)
public = None
if use_pynacl:
# Generate the public key. PyNaCl (i.e., 'nacl' module) performs
# the actual key generation.
nacl_key = nacl.signing.SigningKey(seed)
public = str(nacl_key.verify_key)
# Use the pure Python implementation of ed25519.
else:
public = ed25519.ed25519.publickey(seed)
# Generate the keyid for the ed25519 key dict. 'key_value' corresponds to the
# 'keyval' entry of the 'ED25519KEY_SCHEMA' dictionary. The seed (private)
# key information is not included in the generation of the 'keyid' identifier.
key_value = {'public': binascii.hexlify(public),
'private': ''}
keyid = _get_keyid(key_value)
# Build the 'ed25519_key_dict' dictionary. Update 'key_value' with the
# ed25519 seed key prior to adding 'key_value' to 'ed25519_key_dict'.
key_value['private'] = binascii.hexlify(seed)
ed25519_key_dict['keytype'] = keytype
ed25519_key_dict['keyid'] = keyid
ed25519_key_dict['keyval'] = key_value
return ed25519_key_dict
def format_keyval_to_metadata(key_value, private=False):
"""
<Purpose>
Return a dictionary conformant to 'tuf.formats.KEY_SCHEMA'.
If 'private' is True, include the private key. The dictionary
returned has the form:
{'keytype': 'ed25519',
'keyval': {'public': '876f5584a9db99b8546c0d8608d6...',
'private': 'bf7336055c7638276efe9afe039...'}}
or if 'private' is False:
{'keytype': 'ed25519',
'keyval': {'public': '876f5584a9db99b8546c0d8608d6...',
'private': ''}}
The private and public keys are 32 bytes, although hexlified to 64 bytes.
ed25519 keys are stored in Metadata files (e.g., root.txt) in the format
returned by this function.
>>> ed25519_key = generate()
>>> key_val = ed25519_key['keyval']
>>> ed25519_metadata = format_keyval_to_metadata(key_val, private=True)
>>> tuf.formats.KEY_SCHEMA.matches(ed25519_metadata)
True
<Arguments>
key_value:
A dictionary containing a seed and public ed25519 key.
'key_value' is of the form:
{'public': '876f5584a9db99b8546c0d8608d6...',
'private': 'bf7336055c7638276efe9afe039...'}
conformat to 'tuf.formats.KEYVAL_SCHEMA'.
private:
Indicates if the private key should be included in the
returned dictionary.
<Exceptions>
tuf.FormatError, if 'key_value' does not conform to
'tuf.formats.KEYVAL_SCHEMA'.
<Side Effects>
None.
<Returns>
A 'KEY_SCHEMA' dictionary.
"""
# Does 'key_value' have the correct format?
# This check will ensure 'key_value' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.KEYVAL_SCHEMA.check_match(key_value)
if private is True and len(key_value['private']):
return {'keytype': 'ed25519', 'keyval': key_value}
else:
public_key_value = {'public': key_value['public'], 'private': ''}
return {'keytype': 'ed25519', 'keyval': public_key_value}
def format_metadata_to_key(key_metadata):
"""
<Purpose>
Construct an ed25519 key dictionary (i.e., tuf.formats.ED25519KEY_SCHEMA)
from 'key_metadata'. The dict returned by this function has the exact
format as the dict returned by generate(). It is of the form:
{'keytype': 'ed25519',
'keyid': keyid,
'keyval': {'public': '876f5584a9db99b8546c0d8608d6...',
'private': 'bf7336055c7638276efe9afe039...'}}
The public and private keys are 32-byte strings, although hexlified to 64
bytes.
ed25519 key dictionaries in 'ED25519KEY_SCHEMA' format should be used by
modules storing a collection of keys, such as a keydb keystore.
ed25519 keys as stored in metadata files use a different format, so this
function should be called if an ed25519 key is extracted from one of these
metadata files and needs converting. Generate() creates an entirely
new key and returns it in the format appropriate for 'keydb.py' and
'keystore.py'.
>>> ed25519_key = generate()
>>> key_val = ed25519_key['keyval']
>>> ed25519_metadata = format_keyval_to_metadata(key_val, private=True)
>>> ed25519_key_2 = format_metadata_to_key(ed25519_metadata)
>>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key_2)
True
>>> ed25519_key == ed25519_key_2
True
<Arguments>
key_metadata:
The ed25519 key dictionary as stored in Metadata files, conforming to
'tuf.formats.KEY_SCHEMA'. It has the form:
{'keytype': 'ed25519',
'keyval': {'public': '876f5584a9db99b8546c0d8608d6...',
'private': 'bf7336055c7638276efe9afe039...'}}
<Exceptions>
tuf.FormatError, if 'key_metadata' does not conform to
'tuf.formats.KEY_SCHEMA'.
<Side Effects>
None.
<Returns>
A dictionary containing the ed25519 keys and other identifying information.
"""
# Does 'key_metadata' have the correct format?
# This check will ensure 'key_metadata' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.KEY_SCHEMA.check_match(key_metadata)
# Construct the dictionary to be returned.
ed25519_key_dict = {}
keytype = 'ed25519'
key_value = key_metadata['keyval']
# Convert 'key_value' to 'tuf.formats.KEY_SCHEMA' and generate its hash
# The hash is in hexdigest form. _get_keyid() ensures the private key
# information is not included.
keyid = _get_keyid(key_value)
# We now have all the required key values. Build 'ed25519_key_dict'.
ed25519_key_dict['keytype'] = keytype
ed25519_key_dict['keyid'] = keyid
ed25519_key_dict['keyval'] = key_value
return ed25519_key_dict
def _get_keyid(key_value):
"""Return the keyid for 'key_value'."""
# 'keyid' will be generated from an object conformant to 'KEY_SCHEMA',
# which is the format Metadata files (e.g., root.txt) store keys.
# 'format_keyval_to_metadata()' returns the object needed by _get_keyid().
ed25519_key_meta = format_keyval_to_metadata(key_value, private=False)
# Convert the ed25519 key to JSON Canonical format suitable for adding
# to digest objects.
ed25519_key_update_data = tuf.formats.encode_canonical(ed25519_key_meta)
# Create a digest object and call update(), using the JSON
# canonical format of 'ed25519_key_meta' as the update data.
digest_object = tuf.hash.digest(_KEY_ID_HASH_ALGORITHM)
digest_object.update(ed25519_key_update_data)
# 'keyid' becomes the hexadecimal representation of the hash.
keyid = digest_object.hexdigest()
return keyid
def create_signature(ed25519_key_dict, data, use_pynacl=False):
"""
<Purpose>
Return a signature dictionary of the form:
{'keyid': 'a0469d9491e3c0b42dd41fe3455359dbacb3306b6e8fb59...',
'method': 'ed25519-python',
'sig': '4b3829671b2c6b90034518a918d2447c722474c878c2431dd...'}
Note: 'method' may also be 'ed25519-pynacl', if the signature was created
by the 'nacl' module.
The signing process will use the public and seed key
ed25519_key_dict['keyval']['private'],
ed25519_key_dict['keyval']['public']
and 'data' to generate the signature.
>>> ed25519_key_dict = generate()
>>> data = 'The quick brown fox jumps over the lazy dog.'
>>> signature = create_signature(ed25519_key_dict, data)
>>> tuf.formats.SIGNATURE_SCHEMA.matches(signature)
True
>>> len(signature['sig'])
128
>>> signature_pynacl = create_signature(ed25519_key_dict, data, True)
>>> tuf.formats.SIGNATURE_SCHEMA.matches(signature_pynacl)
True
>>> len(signature_pynacl['sig'])
128
<Arguments>
ed25519_key_dict:
A dictionary containing the ed25519 keys and other identifying information.
'ed25519_key_dict' has the form:
{'keytype': 'ed25519',
'keyid': keyid,
'keyval': {'public': '876f5584a9db99b8546c0d8608d6...',
'private': 'bf7336055c7638276efe9afe039...'}}
The public and private keys are 32-byte strings, although hexlified to 64
bytes.
data:
Data object used by create_signature() to generate the signature.
use_pynacl:
True, if the ed25519 signature should be generated with PyNaCl. False,
if the signature should be generated with the pure Python implementation
of ed25519 (much slower).
<Exceptions>
TypeError, if a private key is not defined for 'ed25519_key_dict'.
tuf.FormatError, if an incorrect format is found for 'ed25519_key_dict'.
tuf.CryptoError, if a signature cannot be created.
<Side Effects>
ed25519.ed25519.signature() or nacl.signing.SigningKey.sign() called to
generate the actual signature.
<Returns>
A signature dictionary conformat to 'tuf.format.SIGNATURE_SCHEMA'.
ed25519 signatures are 64 bytes, however, the hexlified signature
(128 bytes) is stored in the dictionary returned.
"""
# Does 'ed25519_key_dict' have the correct format?
# This check will ensure 'ed25519_key_dict' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.ED25519KEY_SCHEMA.check_match(ed25519_key_dict)
# Signing the 'data' object requires a seed and public key.
# 'ed25519.ed25519.py' generates the actual 64-byte signature in pure Python.
# nacl.signing.SigningKey.sign() generates the signature if 'use_pynacl'
# is True.
signature = {}
private_key = ed25519_key_dict['keyval']['private']
public_key = ed25519_key_dict['keyval']['public']
private_key = binascii.unhexlify(private_key)
public_key = binascii.unhexlify(public_key)
keyid = ed25519_key_dict['keyid']
method = None
sig = None
# Verify the signature, but only if the private key has been set. The private
# key is a NULL string if unset. Although it may be clearer to explicit check
# that 'private_key' is not '', we can/should check for a value and not
# compare identities with the 'is' keyword.
if len(private_key):
if use_pynacl:
method = 'ed25519-pynacl'
try:
nacl_key = nacl.signing.SigningKey(private_key)
nacl_sig = nacl_key.sign(data)
sig = nacl_sig.signature
except (ValueError, nacl.signing.CryptoError):
message = 'An "ed25519-pynacl" signature could not be created.'
raise tuf.CryptoError(message)
# Generate an "ed25519-python" (i.e., pure python implementation) signature.
else:
# ed25519.ed25519.signature() requires both the seed and public keys.
# It calculates the SHA512 of the seed key, which is 32 bytes.
method = 'ed25519-python'
try:
sig = ed25519.ed25519.signature(data, private_key, public_key)
except Exception, e:
message = 'An "ed25519-python" signature could not be generated.'
raise tuf.CryptoError(message)
# Raise an exception since the private key is not defined.
else:
message = 'The required private key is not defined for "ed25519_key_dict".'
raise TypeError(message)
# Build the signature dictionary to be returned.
# The hexadecimal representation of 'sig' is stored in the signature.
signature['keyid'] = keyid
signature['method'] = method
signature['sig'] = binascii.hexlify(sig)
return signature
def verify_signature(ed25519_key_dict, signature, data, use_pynacl=False):
"""
<Purpose>
Determine whether the seed key belonging to 'ed25519_key_dict' produced
'signature'. verify_signature() will use the public key found in
'ed25519_key_dict', the 'method' and 'sig' objects contained in 'signature',
and 'data' to complete the verification. Type-checking performed on both
'ed25519_key_dict' and 'signature'.
>>> ed25519_key_dict = generate()
>>> data = 'The quick brown fox jumps over the lazy dog.'
>>> signature = create_signature(ed25519_key_dict, data)
>>> verify_signature(ed25519_key_dict, signature, data)
True
>>> verify_signature(ed25519_key_dict, signature, data, True)
True
>>> bad_data = 'The sly brown fox jumps over the lazy dog.'
>>> bad_signature = create_signature(ed25519_key_dict, bad_data)
>>> verify_signature(ed25519_key_dict, bad_signature, data, True)
False
<Arguments>
ed25519_key_dict:
A dictionary containing the ed25519 keys and other identifying
information. 'ed25519_key_dict' has the form:
{'keytype': 'ed25519',
'keyid': 'a0469d9491e3c0b42dd41fe3455359dbacb3306b6e8fb59...',
'keyval': {'public': '876f5584a9db99b8546c0d8608d6...',
'private': 'bf7336055c7638276efe9afe039...'}}
The public and private keys are 32-byte strings, although hexlified to
64 bytes.
signature:
The signature dictionary produced by tuf.ed25519_key.create_signature().
'signature' has the form:
{'keyid': 'a0469d9491e3c0b42dd41fe3455359dbacb3306b6e8fb59...',
'method': 'ed25519-python',
'sig': '4b3829671b2c6b90034518a918d2447c722474c878c2431dd...'}
Conformant to 'tuf.formats.SIGNATURE_SCHEMA'.
data:
Data object used by tuf.ed25519_key.create_signature() to generate
'signature'. 'data' is needed here to verify the signature.
use_pynacl:
True, if the ed25519 signature should be verified with PyNaCl. False,
if the signature should be verified with the pure Python implementation
of ed25519 (much slower).
<Exceptions>
tuf.UnknownMethodError. Raised if the signing method used by
'signature' is not one supported by tuf.ed25519_key.create_signature().
tuf.FormatError. Raised if either 'ed25519_key_dict'
or 'signature' do not match their respective tuf.formats schema.
'ed25519_key_dict' must conform to 'tuf.formats.ED25519KEY_SCHEMA'.
'signature' must conform to 'tuf.formats.SIGNATURE_SCHEMA'.
<Side Effects>
ed25519.ed25519.checkvalid() called to do the actual verification.
nacl.signing.VerifyKey.verify() called if 'use_pynacl' is True.
<Returns>
Boolean. True if the signature is valid, False otherwise.
"""
# Does 'ed25519_key_dict' have the correct format?
# This check will ensure 'ed25519_key_dict' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.ED25519KEY_SCHEMA.check_match(ed25519_key_dict)
# Does 'signature' have the correct format?
tuf.formats.SIGNATURE_SCHEMA.check_match(signature)
# Using the public key belonging to 'ed25519_key_dict'
# (i.e., ed25519_key_dict['keyval']['public']), verify whether 'signature'
# was produced by ed25519_key_dict's corresponding seed key
# ed25519_key_dict['keyval']['private']. Before returning the Boolean result,
# ensure 'ed25519-python' or 'ed25519-pynacl' was used as the signing method.
method = signature['method']
sig = signature['sig']
sig = binascii.unhexlify(sig)
public = ed25519_key_dict['keyval']['public']
public = binascii.unhexlify(public)
valid_signature = False
if method in _SUPPORTED_ED25519_SIGNING_METHODS:
if use_pynacl:
try:
nacl_verify_key = nacl.signing.VerifyKey(public)
nacl_message = nacl_verify_key.verify(data, sig)
if nacl_message == data:
valid_signature = True
except nacl.signing.BadSignatureError:
pass
# Verify signature with 'ed25519-python' (i.e., pure Python implementation).
else:
try:
ed25519.ed25519.checkvalid(sig, data, public)
valid_signature = True
# The pure Python implementation raises 'Exception' if 'signature' is
# invalid.
except Exception, e:
pass
else:
message = 'Unsupported ed25519 signing method: '+repr(method)+'.\n'+ \
'Supported methods: '+repr(_SUPPORTED_ED25519_SIGNING_METHODS)+'.'
raise tuf.UnknownMethodError(message)
return valid_signature
if __name__ == '__main__':
# The interactive sessions of the documentation strings can
# be tested by running 'ed25519_key.py' as a standalone module.
# python -B ed25519_key.py
import doctest
doctest.testmod()

411
tuf/ed25519_keys.py Executable file
View file

@ -0,0 +1,411 @@
"""
<Program Name>
ed25519_keys.py
<Author>
Vladimir Diaz <vladimir.v.diaz@gmail.com>
<Started>
September 24, 2013.
<Copyright>
See LICENSE for licensing information.
<Purpose>
The goal of this module is to support ed25519 signatures. ed25519 is an
elliptic-curve public key signature scheme, its main strength being small
signatures (64 bytes) and small public keys (32 bytes).
http://ed25519.cr.yp.to/
'tuf/ed25519_keys.py' calls 'tuf._vendor.ed25519/ed25519.py', which is the
pure Python implementation of ed25519 optimized for a faster runtime.
The Python reference implementation is concise, but very slow (verifying
signatures takes ~9 seconds on an Intel core 2 duo @ 2.2 ghz x 2). The
optimized version can verify signatures in ~2 seconds.
http://ed25519.cr.yp.to/software.html
https://github.com/pyca/ed25519
Optionally, ed25519 cryptographic operations may be executed by PyNaCl, which
is a Python binding to the NaCl library and is faster than the pure python
implementation. Verifying signatures can take approximately 0.0009 seconds.
PyNaCl relies on the libsodium C library.
https://github.com/pyca/pynacl
https://github.com/jedisct1/libsodium
http://nacl.cr.yp.to/
The ed25519-related functions included here are generate(), create_signature()
and verify_signature(). The 'ed25519' and PyNaCl (i.e., 'nacl') modules used
by ed25519_keys.py generate the actual ed25519 keys and the functions listed
above can be viewed as an easy-to-use public interface.
"""
# Help with Python 3 compatibility, where the print statement is a function, an
# implicit relative import is invalid, and the '/' operator performs true
# division. Example: print 'hello world' raises a 'SyntaxError' exception.
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
# 'binascii' required for hexadecimal conversions. Signatures and
# public/private keys are hexlified.
import binascii
# 'os' required to generate OS-specific randomness (os.urandom) suitable for
# cryptographic use.
# http://docs.python.org/2/library/os.html#miscellaneous-functions
import os
# Import the python implementation of the ed25519 algorithm provided by pyca,
# which is an optimized version of the one provided by ed25519's authors.
# Note: The pure Python version do not include protection against side-channel
# attacks. Verifying signatures can take approximately 2 seconds on a intel
# core 2 duo @ 2.2 ghz x 2). Optionally, the PyNaCl module may be used to
# speed up ed25519 cryptographic operations.
# http://ed25519.cr.yp.to/software.html
# https://github.com/pyca/ed25519
# https://github.com/pyca/pynacl
#
# PyNaCl's 'cffi' dependency may thrown an 'IOError' exception when
# importing 'nacl.signing'.
try:
import nacl.signing
import nacl.encoding
except (ImportError, IOError):
pass
# The optimized pure Python implementation of ed25519 provided by TUF. If
# PyNaCl cannot be imported and an attempt to use is made in this module, a
# 'tuf.UnsupportedLibraryError' exception is raised.
import tuf._vendor.ed25519.ed25519
import tuf
# Digest objects needed to generate hashes.
import tuf.hash
# Perform object format-checking.
import tuf.formats
# Supported ed25519 signing method: 'ed25519'. The pure Python
# implementation (i.e., 'tuf._vendor.ed25519.ed25519') and PyNaCl
# (i.e., 'nacl', libsodium+Python bindgs) modules are currently supported in
# the creationg of 'ed25519' signatures. Previously, a distinction was made
# between signatures made by the pure Python implementation and PyNaCl.
_SUPPORTED_ED25519_SIGNING_METHODS = ['ed25519',]
def generate_public_and_private(use_pynacl=False):
"""
<Purpose>
Generate a pair of ed25519 public and private keys.
The public and private keys returned conform to
'tuf.formats.ED25519PULIC_SCHEMA' and 'tuf.formats.ED25519SEED_SCHEMA',
respectively, and have the form:
'\xa2F\x99\xe0\x86\x80%\xc8\xee\x11\xb95T\xd9\...'
An ed25519 seed key is a random 32-byte string. Public keys are also 32
bytes.
>>> public, private = generate_public_and_private(use_pynacl=False)
>>> tuf.formats.ED25519PUBLIC_SCHEMA.matches(public)
True
>>> tuf.formats.ED25519SEED_SCHEMA.matches(private)
True
>>> public, private = generate_public_and_private(use_pynacl=True)
>>> tuf.formats.ED25519PUBLIC_SCHEMA.matches(public)
True
>>> tuf.formats.ED25519SEED_SCHEMA.matches(private)
True
<Arguments>
use_pynacl:
True, if the ed25519 keys should be generated with PyNaCl. False, if the
keys should be generated with the pure Python implementation of ed25519
(slower).
<Exceptions>
tuf.FormatError, if 'use_pynacl' is not a Boolean.
tuf.UnsupportedLibraryError, if the PyNaCl ('nacl') module is unavailable
and 'use_pynacl' is True.
NotImplementedError, if a randomness source is not found by 'os.urandom'.
<Side Effects>
The ed25519 keys are generated by first creating a random 32-byte seed
with os.urandom() and then calling ed25519's
ed25519.25519.publickey(seed) or PyNaCl's nacl.signing.SigningKey().
<Returns>
A (public, private) tuple that conform to 'tuf.formats.ED25519PUBLIC_SCHEMA'
and 'tuf.formats.ED25519SEED_SCHEMA', respectively.
"""
# Does 'use_pynacl' have the correct format?
# This check will ensure 'use_pynacl' conforms to 'tuf.formats.BOOLEAN_SCHEMA'.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.BOOLEAN_SCHEMA.check_match(use_pynacl)
# Generate ed25519's seed key by calling os.urandom(). The random bytes
# returned should be suitable for cryptographic use and is OS-specific.
# Raise 'NotImplementedError' if a randomness source is not found.
# ed25519 seed keys are fixed at 32 bytes (256-bit keys).
# http://blog.mozilla.org/warner/2011/11/29/ed25519-keys/
seed = os.urandom(32)
public = None
if use_pynacl:
# Generate the public key. PyNaCl (i.e., 'nacl' module) performs
# the actual key generation.
try:
nacl_key = nacl.signing.SigningKey(seed)
public = str(nacl_key.verify_key)
except NameError:
message = 'The PyNaCl library and/or its dependencies unavailable.'
raise tuf.UnsupportedLibraryError(message)
# Use the pure Python implementation of ed25519.
else:
public = tuf._vendor.ed25519.ed25519.publickey(seed)
return public, seed
def create_signature(public_key, private_key, data, use_pynacl=False):
"""
<Purpose>
Return a (signature, method) tuple, where the method is 'ed25519' and
generated by either the pure python implemenation, or by PyNaCl
(i.e., 'nacl'). The signature returns conforms to
'tuf.formats.ED25519SIGNATURE_SCHEMA', and has the form:
'\xae\xd7\x9f\xaf\x95{bP\x9e\xa8YO Z\x86\x9d...'
A signature is a 64-byte string.
>>> public, private = generate_public_and_private(use_pynacl=False)
>>> data = 'The quick brown fox jumps over the lazy dog'
>>> signature, method = \
create_signature(public, private, data, use_pynacl=False)
>>> tuf.formats.ED25519SIGNATURE_SCHEMA.matches(signature)
True
>>> method == 'ed25519'
True
>>> signature, method = \
create_signature(public, private, data, use_pynacl=True)
>>> tuf.formats.ED25519SIGNATURE_SCHEMA.matches(signature)
True
>>> method == 'ed25519'
True
<Arguments>
public:
The ed25519 public key, which is a 32-byte string.
private:
The ed25519 private key, which is a 32-byte string.
data:
Data object used by create_signature() to generate the signature.
use_pynacl:
True, if the ed25519 signature should be generated with PyNaCl. False,
if the signature should be generated with the pure Python implementation
of ed25519 (much slower).
<Exceptions>
tuf.FormatError, if the arguments are improperly formatted.
tuf.CryptoError, if a signature cannot be created.
<Side Effects>
tuf._vendor.ed25519.ed25519.signature() or nacl.signing.SigningKey.sign()
called to generate the actual signature.
<Returns>
A signature dictionary conformat to 'tuf.format.SIGNATURE_SCHEMA'.
ed25519 signatures are 64 bytes, however, the hexlified signature is
stored in the dictionary returned.
"""
# Does 'public_key' have the correct format?
# This check will ensure 'public_key' conforms to
# 'tuf.formats.ED25519PUBLIC_SCHEMA', which must have length 32 bytes.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.ED25519PUBLIC_SCHEMA.check_match(public_key)
# Is 'private_key' properly formatted?
tuf.formats.ED25519SEED_SCHEMA.check_match(private_key)
# Is 'use_pynacl' properly formatted?
tuf.formats.BOOLEAN_SCHEMA.check_match(use_pynacl)
# Signing the 'data' object requires a seed and public key.
# 'tuf._vendor.ed25519.ed25519.py' generates the actual 64-byte signature in
# pure Python. nacl.signing.SigningKey.sign() generates the signature if
# 'use_pynacl' is True.
public = public_key
private = private_key
method = None
signature = None
# The private and public keys have been validated above by 'tuf.formats' and
# should be 32-byte strings.
if use_pynacl:
method = 'ed25519'
try:
nacl_key = nacl.signing.SigningKey(private)
nacl_sig = nacl_key.sign(data)
signature = nacl_sig.signature
except NameError:
message = 'The PyNaCl library and/or its dependencies unavailable.'
raise tuf.UnsupportedLibraryError(message)
except (ValueError, nacl.signing.CryptoError):
message = 'An "ed25519" signature could not be created with PyNaCl.'
raise tuf.CryptoError(message)
# Generate an "ed25519" signature with the pure python implementation.
else:
# tuf._vendor.ed25519.ed25519.signature() requires both the seed and
# public keys. It calculates the SHA512 of the seed key, which is 32 bytes.
method = 'ed25519'
try:
signature = tuf._vendor.ed25519.ed25519.signature(data, private, public)
# 'Exception' raised by ed25519.py for any exception that may occur.
except Exception, e:
message = 'An "ed25519" signature could not be generated in pure Python.'
raise tuf.CryptoError(message)
return signature, method
def verify_signature(public_key, method, signature, data, use_pynacl=False):
"""
<Purpose>
Determine whether the private key corresponding to 'public_key' produced
'signature'. verify_signature() will use the public key, the 'method' and
'sig', and 'data' arguments to complete the verification.
>>> public, private = generate_public_and_private(use_pynacl=False)
>>> data = 'The quick brown fox jumps over the lazy dog'
>>> signature, method = \
create_signature(public, private, data, use_pynacl=False)
>>> verify_signature(public, method, signature, data, use_pynacl=False)
True
>>> verify_signature(public, method, signature, data, use_pynacl=True)
True
>>> bad_data = 'The sly brown fox jumps over the lazy dog'
>>> bad_signature, method = \
create_signature(public, private, bad_data, use_pynacl=False)
>>> verify_signature(public, method, bad_signature, data, use_pynacl=False)
False
<Arguments>
public_key:
The public key is a 32-byte string.
method:
'ed25519' signature method generated by either the pure python
implementation (i.e., 'tuf._vendor.ed25519.ed25519.py') or PyNacl
(i.e., 'nacl').
signature:
The signature is a 64-byte string.
data:
Data object used by tuf.ed25519_keys.create_signature() to generate
'signature'. 'data' is needed here to verify the signature.
use_pynacl:
True, if the ed25519 signature should be verified by PyNaCl. False,
if the signature should be verified with the pure Python implementation
of ed25519 (slower).
<Exceptions>
tuf.UnknownMethodError. Raised if the signing method used by
'signature' is not one supported by tuf.ed25519_keys.create_signature().
tuf.FormatError. Raised if the arguments are improperly formatted.
<Side Effects>
tuf._vendor.ed25519.ed25519.checkvalid() called to do the actual
verification. nacl.signing.VerifyKey.verify() called if 'use_pynacl' is
True.
<Returns>
Boolean. True if the signature is valid, False otherwise.
"""
# Does 'public_key' have the correct format?
# This check will ensure 'public_key' conforms to
# 'tuf.formats.ED25519PUBLIC_SCHEMA', which must have length 32 bytes.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.ED25519PUBLIC_SCHEMA.check_match(public_key)
# Is 'method' properly formatted?
tuf.formats.NAME_SCHEMA.check_match(method)
# Is 'signature' properly formatted?
tuf.formats.ED25519SIGNATURE_SCHEMA.check_match(signature)
# Is 'use_pynacl' properly formatted?
tuf.formats.BOOLEAN_SCHEMA.check_match(use_pynacl)
# Verify 'signature'. Before returning the Boolean result,
# ensure 'ed25519' was used as the signing method.
# Raise 'tuf.UnsupportedLibraryError' if 'use_pynacl' is True but 'nacl' is
# unavailable.
public = public_key
valid_signature = False
if method in _SUPPORTED_ED25519_SIGNING_METHODS:
if use_pynacl:
try:
nacl_verify_key = nacl.signing.VerifyKey(public)
nacl_message = nacl_verify_key.verify(data, signature)
if nacl_message == data:
valid_signature = True
except NameError:
message = 'The PyNaCl library and/or its dependencies unavailable.'
raise tuf.UnsupportedLibraryError(message)
except nacl.signing.BadSignatureError:
pass
# Verify 'ed25519' signature with pure Python implementation.
else:
try:
tuf._vendor.ed25519.ed25519.checkvalid(signature, data, public)
valid_signature = True
# The pure Python implementation raises 'Exception' if 'signature' is
# invalid.
except Exception, e:
pass
else:
message = 'Unsupported ed25519 signing method: '+repr(method)+'.\n'+ \
'Supported methods: '+repr(_SUPPORTED_ED25519_SIGNING_METHODS)+'.'
raise tuf.UnknownMethodError(message)
return valid_signature
if __name__ == '__main__':
# The interactive sessions of the documentation strings can
# be tested by running 'ed25519_keys.py' as a standalone module.
# python -B ed25519_keys.py
import doctest
doctest.testmod()

425
tuf/evp.py Executable file
View file

@ -0,0 +1,425 @@
"""
<Program Name>
evp.py
<Author>
Vladimir Diaz <vladimir.v.diaz@gmail.com>
<Started>
October 2013.
<Copyright>
See LICENSE for licensing information.
<Purpose>
The goal of this module is to support public-key cryptography using
the RSA algorithm. The RSA-related functions provided include
generate(), create_signature(), and verify_signature(). The 'evpy' package
used by 'rsa_key.py' generates the actual RSA keys and the functions listed
above can be viewed as an easy-to-use public interface. Additional functions
contained here include create_in_metadata_format() and
create_from_metadata_format(). These last two functions produce or use RSA
keys compatible with the key structures listed in TUF Metadata files.
The generate() function returns a dictionary containing all the information
needed of RSA keys, such as public and private keys, keyIDs, and an iden-
fier. create_signature() and verify_signature() are supplemental functions
used for generating RSA signatures and verifying them.
Key IDs are used as identifiers for keys (e.g., RSA key). They are the
hexadecimal representation of the hash of key object (specifically, the key
object containing only the public key). See 'rsa_key.py' and the
'_get_keyid()' function to see precisely how keyids are generated. One may
get the keyid of a key object by simply accessing the dictionary's 'keyid'
key (i.e., rsakey['keyid']).
"""
# Required for hexadecimal conversions.
import binascii
# Needed to generate, sign, and verify RSA keys.
import evpy.signature
import evpy.envelope
# Digest objects needed to generate hashes.
import tuf.hash
# Perform object format-checking.
import tuf.formats
_KEY_ID_HASH_ALGORITHM = 'sha256'
# Recommended RSA key sizes: http://www.rsa.com/rsalabs/node.asp?id=2004
# According to the document above, revised May 6, 2003, RSA keys of
# size 3072 provide security through 2031 and beyond.
_DEFAULT_RSA_KEY_BITS = 3072
def generate(bits=_DEFAULT_RSA_KEY_BITS):
"""
<Purpose>
Generate public and private RSA keys, with modulus length 'bits'.
In addition, a keyid used as an identifier for RSA keys is generated.
The object returned conforms to tuf.formats.RSAKEY_SCHEMA and as the form:
{'keytype': 'rsa',
'keyid': keyid,
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
The public and private keys are in PEM format and stored as strings.
<Arguments>
bits:
The key size, or key length, of the RSA key.
<Exceptions>
tuf.CryptoError, if an exception occurs after calling evpy.envelope.keygen().
tuf.FormatError, if 'bits' does not contain the correct format.
<Side Effects>
The RSA keys are generated by calling evpy.envelope.keygen().
<Returns>
A dictionary containing the RSA keys and other identifying information.
"""
# Does 'bits' have the correct format?
# This check will ensure 'bits' conforms to 'tuf.formats.RSAKEYBITS_SCHEMA'.
# 'bits' must be an integer object, with a minimum value of 2048.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.RSAKEYBITS_SCHEMA.check_match(bits)
# Begin building the RSA key dictionary.
rsakey_dict = {}
keytype = 'rsa'
# Generate the public and private keys. 'public_key' and 'private_key'
# will both be strings containing RSA keys in PEM format.
# The evpy.envelope module performs the actual key generation. The
# evpy.envelope.keygen() function returns a (public, private) tuple.
try:
public_key, private_key = evpy.envelope.keygen(bits, pem=True)
except (evpy.envelope.EnvelopeError, evpy.envelope.KeygenError, MemoryError), e:
raise tuf.CryptoError(e)
# Generate the keyid for the RSA key. 'key_value' corresponds to the
# 'keyval' entry of the RSAKEY_SCHEMA dictionary.
key_value = {'public': public_key,
'private': ''}
keyid = _get_keyid(key_value)
# Build the 'rsakey_dict' dictionary.
# Update 'key_value' with the RSA private key prior to adding
# 'key_value' to 'rsakey_dict'.
key_value['private'] = private_key
rsakey_dict['keytype'] = keytype
rsakey_dict['keyid'] = keyid
rsakey_dict['keyval'] = key_value
return rsakey_dict
def create_in_metadata_format(key_value, private=False):
"""
<Purpose>
Return a dictionary conformant to tuf.formats.KEY_SCHEMA.
If 'private' is True, include the private key. The dictionary
returned has the form:
{'keytype': 'rsa',
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
or
{'keytype': 'rsa',
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': ''}} if 'private' is False.
The private and public keys are in PEM format.
RSA keys are stored in Metadata files (e.g., root.txt) in the format
returned by this function.
<Arguments>
key_value:
A dictionary containing a private and public RSA key.
'key_value' is of the form:
{'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}},
conformat to tuf.formats.KEYVAL_SCHEMA.
private:
Indicates if the private key should be included in the
returned dictionary.
<Exceptions>
tuf.FormatError, if 'key_value' does not conform to
tuf.formats.KEYVAL_SCHEMA.
<Side Effects>
None.
<Returns>
An KEY_SCHEMA dictionary.
"""
# Does 'key_value' have the correct format?
# This check will ensure 'key_value' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.KEYVAL_SCHEMA.check_match(key_value)
if private and key_value['private']:
return {'keytype': 'rsa', 'keyval': key_value}
else:
public_key_value = {'public': key_value['public'], 'private': ''}
return {'keytype': 'rsa', 'keyval': public_key_value}
def create_from_metadata_format(key_metadata):
"""
<Purpose>
Construct an RSA key dictionary (i.e., tuf.formats.RSAKEY_SCHEMA)
from 'key_metadata'. The dict returned by this function has the exact
format as the dict returned by generate(). It is of the form:
{'keytype': 'rsa',
'keyid': keyid,
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
The public and private keys are in PEM format and stored as strings.
RSA key dictionaries in RSAKEY_SCHEMA format should be used by
modules storing a collection of keys, such as a keydb and keystore.
RSA keys as stored in metadata files use a different format, so this
function should be called if an RSA key is extracted from one of these
metadata files and needs converting. Generate() creates an entirely
new key and returns it in the format appropriate for keydb and keystore.
<Arguments>
key_metadata:
The RSA key dictionary as stored in Metadata files, conforming to
tuf.formats.KEY_SCHEMA.
<Exceptions>
tuf.FormatError, if 'key_metadata' does not conform to
tuf.formats.KEY_SCHEMA.
<Side Effects>
None.
<Returns>
A dictionary containing the RSA keys and other identifying information.
"""
# Does 'key_metadata' have the correct format?
# This check will ensure 'key_metadata' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.KEY_SCHEMA.check_match(key_metadata)
# Construct the dictionary to be returned.
rsakey_dict = {}
keytype = 'rsa'
key_value = key_metadata['keyval']
keyid = _get_keyid(key_value)
# We now have all the required key values.
# Build 'rsakey_dict'.
rsakey_dict['keytype'] = keytype
rsakey_dict['keyid'] = keyid
rsakey_dict['keyval'] = key_value
return rsakey_dict
def _get_keyid(key_value):
"""Return the keyid for 'key_value'."""
# 'keyid' will be generated from an object conformant to KEY_SCHEMA,
# which is the format Metadata files (e.g., root.txt) store keys.
# 'create_in_metadata_format()' returns the object needed by _get_keyid().
rsakey_meta = create_in_metadata_format(key_value, private=False)
# Convert the RSA key to JSON Canonical format suitable for adding
# to digest objects.
rsakey_update_data = tuf.formats.encode_canonical(rsakey_meta)
# Create a digest object and call update(), using the JSON
# canonical format of 'rskey_meta' as the update data.
digest_object = tuf.hash.digest(_KEY_ID_HASH_ALGORITHM)
digest_object.update(rsakey_update_data)
# 'keyid' becomes the hexadecimal representation of the hash.
keyid = digest_object.hexdigest()
return keyid
def create_signature(rsakey_dict, data):
"""
<Purpose>
Return a signature dictionary of the form:
{'keyid': keyid, 'method': 'evp', 'sig': sig}.
The signing process will use the private key
rsakey_dict['keyval']['private'] and 'data' to generate the signature.
<Arguments>
rsakey_dict:
A dictionary containing the RSA keys and other identifying information.
'rsakey_dict' has the form:
{'keytype': 'rsa',
'keyid': keyid,
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
The public and private keys are in PEM format and stored as strings.
data:
Data object used by create_signature() to generate the signature.
<Exceptions>
TypeError, if a private key is not defined for 'rsakey_dict'.
tuf.FormatError, if an incorrect format is found for the
'rsakey_dict' object.
<Side Effects>
evpy.signature.sign() called to perform the actual signing.
<Returns>
A signature dictionary conformat to tuf.format.SIGNATURE_SCHEMA.
"""
# Does 'rsakey_dict' have the correct format?
# This check will ensure 'rsakey_dict' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict)
# Signing the 'data' object requires a private key.
# The 'evp' (i.e., evpy) signing method is the only method
# currently supported.
signature = {}
private_key = rsakey_dict['keyval']['private']
keyid = rsakey_dict['keyid']
method = 'evp'
if private_key:
sig = evpy.signature.sign(data, key=private_key)
else:
raise TypeError('The required private key is not defined for rsakey_dict.')
# Build the signature dictionary to be returned.
# The hexadecimal representation of 'sig' is stored in the signature.
signature['keyid'] = keyid
signature['method'] = method
signature['sig'] = binascii.hexlify(sig)
return signature
def verify_signature(rsakey_dict, signature, data):
"""
<Purpose>
Determine whether the private key belonging to 'rsakey_dict' produced
'signature'. verify_signature() will use the public key found in
'rsakey_dict', the 'method' and 'sig' objects contained in 'signature',
and 'data' to complete the verification. Type-checking performed on both
'rsakey_dict' and 'signature'.
<Arguments>
rsakey_dict:
A dictionary containing the RSA keys and other identifying information.
'rsakey_dict' has the form:
{'keytype': 'rsa',
'keyid': keyid,
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
The public and private keys are in PEM format and stored as strings.
signature:
The signature dictionary produced by tuf.rsa_key.create_signature().
'signature' has the form:
{'keyid': keyid, 'method': 'method', 'sig': sig}. Conformant to
tuf.formats.SIGNATURE_SCHEMA.
data:
Data object used by tuf.rsa_key.create_signature() to generate
'signature'. 'data' is needed here to verify the signature.
<Exceptions>
tuf.UnknownMethodError. Raised if the signing method used by
'signature' is not one supported by tuf.rsa_key.create_signature().
tuf.FormatError. Raised if either 'rsakey_dict'
or 'signature' do not match their respective tuf.formats schema.
'rsakey_dict' must conform to tuf.formats.RSAKEY_SCHEMA.
'signature' must conform to tuf.formats.SIGNATURE_SCHEMA.
<Side Effects>
evpy.signature_verify() called to do the actual verification.
<Returns>
Boolean.
"""
# Does 'rsakey_dict' have the correct format?
# This check will ensure 'rsakey_dict' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict)
# Does 'signature' have the correct format?
tuf.formats.SIGNATURE_SCHEMA.check_match(signature)
# Using the public key belonging to 'rsakey_dict'
# (i.e., rsakey_dict['keyval']['public']), verify whether 'signature'
# was produced by rsakey_dict's corresponding private key
# rsakey_dict['keyval']['private']. Before returning the Boolean result,
# ensure 'evp' was used as the signing method.
method = signature['method']
sig = signature['sig']
public_key = rsakey_dict['keyval']['public']
if method != 'evp':
raise tuf.UnknownMethodError(method)
return evpy.signature.verify(data, binascii.unhexlify(sig), key=public_key)

View file

@ -57,10 +57,8 @@
Example:
signable_object = make_signable(unsigned_object)
"""
import binascii
import calendar
import re
@ -76,15 +74,19 @@
# easily backwards compatible with clients that are already deployed.
# A date in 'YYYY-MM-DD HH:MM:SS UTC' format.
# TODO: Support timestamps according to the ISO 8601 standard.
TIME_SCHEMA = SCHEMA.RegularExpression(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC')
# A date in 'YYYY-MM-DD HH:MM:SS UTC' format.
DATETIME_SCHEMA = SCHEMA.RegularExpression(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')
# A hexadecimal value in '23432df87ab..' format.
HASH_SCHEMA = SCHEMA.RegularExpression(r'[a-fA-F0-9]+')
# A dict in {'sha256': '23432df87ab..', 'sha512': '34324abc34df..', ...} format.
HASHDICT_SCHEMA = SCHEMA.DictOf(
key_schema=SCHEMA.AnyString(),
value_schema=HASH_SCHEMA)
key_schema = SCHEMA.AnyString(),
value_schema = HASH_SCHEMA)
# A hexadecimal value in '23432df87ab..' format.
HEX_SCHEMA = SCHEMA.RegularExpression(r'[a-fA-F0-9]+')
@ -93,7 +95,7 @@
KEYID_SCHEMA = HASH_SCHEMA
KEYIDS_SCHEMA = SCHEMA.ListOf(KEYID_SCHEMA)
# The method used for a generated signature (e.g., 'evp').
# The method used for a generated signature (e.g., 'RSASSA-PSS').
SIG_METHOD_SCHEMA = SCHEMA.AnyString()
# A relative file path (e.g., 'metadata/root/').
@ -109,14 +111,14 @@
# A dictionary holding version information.
VERSION_SCHEMA = SCHEMA.Object(
object_name='version',
major=SCHEMA.Integer(lo=0),
minor=SCHEMA.Integer(lo=0),
fix=SCHEMA.Integer(lo=0))
object_name = 'VERSION_SCHEMA',
major = SCHEMA.Integer(lo=0),
minor = SCHEMA.Integer(lo=0),
fix = SCHEMA.Integer(lo=0))
# An integer representing the numbered version of a metadata file.
# Must be 1, or greater.
METADATAVERSION_SCHEMA = SCHEMA.Integer(lo=1)
METADATAVERSION_SCHEMA = SCHEMA.Integer(lo=0)
# An integer representing length. Must be 0, or greater.
LENGTH_SCHEMA = SCHEMA.Integer(lo=0)
@ -127,9 +129,20 @@
# A string representing a named object.
NAME_SCHEMA = SCHEMA.AnyString()
NAMES_SCHEMA = SCHEMA.ListOf(NAME_SCHEMA)
# Supported hash algorithms.
HASHALGORITHMS_SCHEMA = SCHEMA.ListOf(SCHEMA.OneOf(
[SCHEMA.String('md5'), SCHEMA.String('sha1'),
SCHEMA.String('sha224'), SCHEMA.String('sha256'),
SCHEMA.String('sha384'), SCHEMA.String('sha512')]))
# The contents of an encrypted TUF key. Encrypted TUF keys are saved to files
# in this format.
ENCRYPTEDKEY_SCHEMA = SCHEMA.AnyString()
# A value that is either True or False, on or off, etc.
TOGGLE_SCHEMA = SCHEMA.Boolean()
BOOLEAN_SCHEMA = SCHEMA.Boolean()
# A role's threshold value (i.e., the minimum number
# of signatures required to sign a metadata file).
@ -142,6 +155,12 @@
# The minimum number of bits for an RSA key. Must be 2048 bits and greater.
RSAKEYBITS_SCHEMA = SCHEMA.Integer(lo=2048)
# The number of bins used to delegate to hashed roles.
NUMBINS_SCHEMA = SCHEMA.Integer(lo=16)
# A PyCrypto signature.
PYCRYPTOSIGNATURE_SCHEMA = SCHEMA.AnyString()
# An RSA key in PEM format.
PEMRSA_SCHEMA = SCHEMA.AnyString()
@ -155,50 +174,76 @@
# key identifier ('rsa', 233df889cb). For RSA keys, the key value is a pair of
# public and private keys in PEM Format stored as strings.
KEYVAL_SCHEMA = SCHEMA.Object(
object_name='keyval',
public=SCHEMA.AnyString(),
private=SCHEMA.AnyString())
object_name = 'KEYVAL_SCHEMA',
public = SCHEMA.AnyString(),
private = SCHEMA.AnyString())
# A generic key. All TUF keys should be saved to metadata files in this format.
# Supported TUF key types.
KEYTYPE_SCHEMA = SCHEMA.OneOf(
[SCHEMA.String('rsa'), SCHEMA.String('ed25519')])
# A generic TUF key. All TUF keys should be saved to metadata files in this
# format.
KEY_SCHEMA = SCHEMA.Object(
object_name='key',
keytype=SCHEMA.AnyString(),
keyval=KEYVAL_SCHEMA)
object_name = 'KEY_SCHEMA',
keytype = SCHEMA.AnyString(),
keyval = KEYVAL_SCHEMA)
# An RSA key.
# A TUF key object. This schema simplifies validation of keys that may be
# one of the supported key types.
# Supported key types: 'rsa', 'ed25519'.
ANYKEY_SCHEMA = SCHEMA.Object(
object_name = 'ANYKEY_SCHEMA',
keytype = KEYTYPE_SCHEMA,
keyid = KEYID_SCHEMA,
keyval = KEYVAL_SCHEMA)
# A list of TUF key objects.
ANYKEYLIST_SCHEMA = SCHEMA.ListOf(ANYKEY_SCHEMA)
# An RSA TUF key.
RSAKEY_SCHEMA = SCHEMA.Object(
object_name='rsakey',
keytype=SCHEMA.String('rsa'),
keyid=KEYID_SCHEMA,
keyval=KEYVAL_SCHEMA)
object_name = 'RSAKEY_SCHEMA',
keytype = SCHEMA.String('rsa'),
keyid = KEYID_SCHEMA,
keyval = KEYVAL_SCHEMA)
# An ed25519 key.
# An ED25519 raw public key, which must be 32 bytes.
ED25519PUBLIC_SCHEMA = SCHEMA.LengthString(32)
# An ED25519 raw seed key, which must be 32 bytes.
ED25519SEED_SCHEMA = SCHEMA.LengthString(32)
# An ED25519 raw signature, which must be 64 bytes.
ED25519SIGNATURE_SCHEMA = SCHEMA.LengthString(64)
# An ed25519 TUF key.
ED25519KEY_SCHEMA = SCHEMA.Object(
object_name='ed25519key',
keytype=SCHEMA.String('ed25519'),
keyid=KEYID_SCHEMA,
keyval=KEYVAL_SCHEMA)
object_name = 'ED25519KEY_SCHEMA',
keytype = SCHEMA.String('ed25519'),
keyid = KEYID_SCHEMA,
keyval = KEYVAL_SCHEMA)
# Info that describes both metadata and target files.
# This schema allows the storage of multiple hashes for the same file
# (e.g., sha256 and sha512 may be computed for the same file and stored).
FILEINFO_SCHEMA = SCHEMA.Object(
object_name='fileinfo',
length=LENGTH_SCHEMA,
hashes=HASHDICT_SCHEMA,
custom=SCHEMA.Optional(SCHEMA.Object()))
object_name = 'FILEINFO_SCHEMA',
length = LENGTH_SCHEMA,
hashes = HASHDICT_SCHEMA,
custom = SCHEMA.Optional(SCHEMA.Object()))
# A dict holding the information for a particular file. The keys hold the
# relative file path and the values the relevant file information.
FILEDICT_SCHEMA = SCHEMA.DictOf(
key_schema=RELPATH_SCHEMA,
value_schema=FILEINFO_SCHEMA)
key_schema = RELPATH_SCHEMA,
value_schema = FILEINFO_SCHEMA)
# A dict holding a target file.
TARGETFILE_SCHEMA = SCHEMA.Object(
object_name='targetfile',
filepath=RELPATH_SCHEMA,
fileinfo=FILEINFO_SCHEMA)
object_name = 'TARGETFILE_SCHEMA',
filepath = RELPATH_SCHEMA,
fileinfo = FILEINFO_SCHEMA)
TARGETFILES_SCHEMA = SCHEMA.ListOf(TARGETFILE_SCHEMA)
# A single signature of an object. Indicates the signature, the id of the
@ -210,10 +255,13 @@
# one can imagine that maybe a key wants to sign multiple times with different
# signature methods.
SIGNATURE_SCHEMA = SCHEMA.Object(
object_name='signature',
keyid=KEYID_SCHEMA,
method=SIG_METHOD_SCHEMA,
sig=HEX_SCHEMA)
object_name = 'SIGNATURE_SCHEMA',
keyid = KEYID_SCHEMA,
method = SIG_METHOD_SCHEMA,
sig = HEX_SCHEMA)
# List of SIGNATURE_SCHEMA.
SIGNATURES_SCHEMA = SCHEMA.ListOf(SIGNATURE_SCHEMA)
# A schema holding the result of checking the signatures of a particular
# 'SIGNABLE_SCHEMA' role.
@ -221,31 +269,31 @@
# valid? This SCHEMA holds this information. See 'sig.py' for
# more information.
SIGNATURESTATUS_SCHEMA = SCHEMA.Object(
object_name='signaturestatus',
threshold=SCHEMA.Integer(),
good_sigs=SCHEMA.ListOf(KEYID_SCHEMA),
bad_sigs=SCHEMA.ListOf(KEYID_SCHEMA),
unknown_sigs=SCHEMA.ListOf(KEYID_SCHEMA),
untrusted_sigs=SCHEMA.ListOf(KEYID_SCHEMA),
unknown_method_sigs=SCHEMA.ListOf(KEYID_SCHEMA))
object_name = 'SIGNATURESTATUS_SCHEMA',
threshold = SCHEMA.Integer(),
good_sigs = KEYIDS_SCHEMA,
bad_sigs = KEYIDS_SCHEMA,
unknown_sigs = KEYIDS_SCHEMA,
untrusted_sigs = KEYIDS_SCHEMA,
unknown_method_sigs = KEYIDS_SCHEMA)
# A signable object. Holds the signing role and its associated signatures.
SIGNABLE_SCHEMA = SCHEMA.Object(
object_name='signable',
signed=SCHEMA.Any(),
signatures=SCHEMA.ListOf(SIGNATURE_SCHEMA))
object_name = 'SIGNABLE_SCHEMA',
signed = SCHEMA.Any(),
signatures = SCHEMA.ListOf(SIGNATURE_SCHEMA))
# A dict where the dict keys hold a keyid and the dict values a key object.
KEYDICT_SCHEMA = SCHEMA.DictOf(
key_schema=KEYID_SCHEMA,
value_schema=KEY_SCHEMA)
key_schema = KEYID_SCHEMA,
value_schema = KEY_SCHEMA)
# The format used by the key database to store keys. The dict keys hold a key
# identifier and the dict values any object. The key database should store
# key objects in the values (e.g., 'RSAKEY_SCHEMA', 'DSAKEY_SCHEMA').
KEYDB_SCHEMA = SCHEMA.DictOf(
key_schema=KEYID_SCHEMA,
value_schema=SCHEMA.Any())
key_schema = KEYID_SCHEMA,
value_schema = SCHEMA.Any())
# The format of the resulting "scp config dict" after extraction from the
# push configuration file (i.e., push.cfg). In the case of a config file
@ -255,18 +303,18 @@
# 'remote_directory' entries. See 'tuf/pushtools/pushtoolslib.py' and
# 'tuf/pushtools/push.py'.
SCPCONFIG_SCHEMA = SCHEMA.Object(
object_name='scp_config',
general=SCHEMA.Object(
object_name='[general]',
transfer_module=SCHEMA.String('scp'),
metadata_path=PATH_SCHEMA,
targets_directory=PATH_SCHEMA),
object_name = 'SCPCONFIG_SCHEMA',
general = SCHEMA.Object(
object_name = '[general]',
transfer_module = SCHEMA.String('scp'),
metadata_path = PATH_SCHEMA,
targets_directory = PATH_SCHEMA),
scp=SCHEMA.Object(
object_name='[scp]',
host=URL_SCHEMA,
user=NAME_SCHEMA,
identity_file=PATH_SCHEMA,
remote_directory=PATH_SCHEMA))
object_name = '[scp]',
host = URL_SCHEMA,
user = NAME_SCHEMA,
identity_file = PATH_SCHEMA,
remote_directory = PATH_SCHEMA))
# The format of the resulting "receive config dict" after extraction from the
# receive configuration file (i.e., receive.cfg). The receive config file
@ -275,101 +323,136 @@
# 'backup_directory' entries.
# see 'tuf/pushtools/pushtoolslib.py' and 'tuf/pushtools/receive/receive.py'
RECEIVECONFIG_SCHEMA = SCHEMA.Object(
object_name='receive_config',
general=SCHEMA.Object(
object_name='[general]',
pushroots=SCHEMA.ListOf(PATH_SCHEMA),
repository_directory=PATH_SCHEMA,
metadata_directory=PATH_SCHEMA,
targets_directory=PATH_SCHEMA,
backup_directory=PATH_SCHEMA))
object_name = 'RECEIVECONFIG_SCHEMA', general=SCHEMA.Object(
object_name = '[general]',
pushroots = SCHEMA.ListOf(PATH_SCHEMA),
repository_directory = PATH_SCHEMA,
metadata_directory = PATH_SCHEMA,
targets_directory = PATH_SCHEMA,
backup_directory = PATH_SCHEMA))
# A path hash prefix is a hexadecimal string.
PATH_HASH_PREFIX_SCHEMA = HEX_SCHEMA
# A list of path hash prefixes.
PATH_HASH_PREFIXES_SCHEMA = SCHEMA.ListOf(PATH_HASH_PREFIX_SCHEMA)
# Role object in {'keyids': [keydids..], 'name': 'ABC', 'threshold': 1,
# 'paths':[filepaths..]} # format.
# 'paths':[filepaths..]} format.
ROLE_SCHEMA = SCHEMA.Object(
object_name='role',
keyids=SCHEMA.ListOf(KEYID_SCHEMA),
name=SCHEMA.Optional(ROLENAME_SCHEMA),
threshold=THRESHOLD_SCHEMA,
paths=SCHEMA.Optional(RELPATHS_SCHEMA),
path_hash_prefixes=SCHEMA.Optional(PATH_HASH_PREFIXES_SCHEMA))
object_name = 'ROLE_SCHEMA',
name = SCHEMA.Optional(ROLENAME_SCHEMA),
keyids = KEYIDS_SCHEMA,
threshold = THRESHOLD_SCHEMA,
paths = SCHEMA.Optional(RELPATHS_SCHEMA),
path_hash_prefixes = SCHEMA.Optional(PATH_HASH_PREFIXES_SCHEMA))
# A dict of roles where the dict keys are role names and the dict values holding
# the role data/information.
ROLEDICT_SCHEMA = SCHEMA.DictOf(
key_schema=ROLENAME_SCHEMA,
value_schema=ROLE_SCHEMA)
key_schema = ROLENAME_SCHEMA,
value_schema = ROLE_SCHEMA)
# Like ROLEDICT_SCHEMA, except that ROLE_SCHEMA instances are stored in order.
ROLELIST_SCHEMA = SCHEMA.ListOf(ROLE_SCHEMA)
# The root: indicates root keys and top-level roles.
# The delegated roles of a Targets role (a parent).
DELEGATIONS_SCHEMA = SCHEMA.Object(
keys = KEYDICT_SCHEMA,
roles = ROLELIST_SCHEMA)
# The number of seconds before metadata expires. The minimum is 86400 seconds
# (= 1 day). This schema is used for the initial expiration date. Repository
# maintainers may later modify this value (TIME_SCHEMA).
EXPIRATION_SCHEMA = SCHEMA.Integer(lo=86400)
# Supported compression extension (e.g., 'gz').
COMPRESSION_SCHEMA = SCHEMA.OneOf([SCHEMA.String(''), SCHEMA.String('gz')])
# List of supported compression extensions.
COMPRESSIONS_SCHEMA = SCHEMA.ListOf(
SCHEMA.OneOf([SCHEMA.String(''), SCHEMA.String('gz')]))
# tuf.roledb
ROLEDB_SCHEMA = SCHEMA.Object(
object_name = 'ROLEDB_SCHEMA',
keyids = KEYIDS_SCHEMA,
signing_keyids = SCHEMA.Optional(KEYIDS_SCHEMA),
threshold = THRESHOLD_SCHEMA,
version = SCHEMA.Optional(METADATAVERSION_SCHEMA),
expires = SCHEMA.Optional(SCHEMA.OneOf([EXPIRATION_SCHEMA, TIME_SCHEMA])),
signatures = SCHEMA.Optional(SIGNATURES_SCHEMA),
compressions = SCHEMA.Optional(COMPRESSIONS_SCHEMA),
paths = SCHEMA.Optional(RELPATHS_SCHEMA),
path_hash_prefixes = SCHEMA.Optional(PATH_HASH_PREFIXES_SCHEMA),
delegations = SCHEMA.Optional(DELEGATIONS_SCHEMA),
partial_loaded = SCHEMA.Optional(BOOLEAN_SCHEMA))
# Root role: indicates root keys and top-level roles.
ROOT_SCHEMA = SCHEMA.Object(
object_name='root',
_type=SCHEMA.String('Root'),
version=METADATAVERSION_SCHEMA,
expires=TIME_SCHEMA,
keys=KEYDICT_SCHEMA,
roles=ROLEDICT_SCHEMA)
object_name = 'ROOT_SCHEMA',
_type = SCHEMA.String('Root'),
version = METADATAVERSION_SCHEMA,
consistent_snapshots = BOOLEAN_SCHEMA,
expires = TIME_SCHEMA,
keys = KEYDICT_SCHEMA,
roles = ROLEDICT_SCHEMA)
# Targets. Indicates targets and delegates target paths to other roles.
# Targets role: Indicates targets and delegates target paths to other roles.
TARGETS_SCHEMA = SCHEMA.Object(
object_name='targets',
_type=SCHEMA.String('Targets'),
version=METADATAVERSION_SCHEMA,
expires=TIME_SCHEMA,
targets=FILEDICT_SCHEMA,
delegations=SCHEMA.Optional(SCHEMA.Object(
keys=KEYDICT_SCHEMA,
roles=ROLELIST_SCHEMA)))
object_name = 'TARGETS_SCHEMA',
_type = SCHEMA.String('Targets'),
version = METADATAVERSION_SCHEMA,
expires = TIME_SCHEMA,
targets = FILEDICT_SCHEMA,
delegations = SCHEMA.Optional(DELEGATIONS_SCHEMA))
# A Release: indicates the latest versions of all metadata (except timestamp).
# Release role: indicates the latest versions of all metadata (except timestamp).
RELEASE_SCHEMA = SCHEMA.Object(
object_name='release',
_type=SCHEMA.String('Release'),
version=METADATAVERSION_SCHEMA,
expires=TIME_SCHEMA,
meta=FILEDICT_SCHEMA)
object_name = 'RELEASE_SCHEMA',
_type = SCHEMA.String('Release'),
version = METADATAVERSION_SCHEMA,
expires = TIME_SCHEMA,
meta = FILEDICT_SCHEMA)
# A Timestamp: indicates the latest version of the release file.
# Timestamp role: indicates the latest version of the release file.
TIMESTAMP_SCHEMA = SCHEMA.Object(
object_name='timestamp',
_type=SCHEMA.String('Timestamp'),
version=METADATAVERSION_SCHEMA,
expires=TIME_SCHEMA,
meta=FILEDICT_SCHEMA)
object_name = 'TIMESTAMP_SCHEMA',
_type = SCHEMA.String('Timestamp'),
version = METADATAVERSION_SCHEMA,
expires = TIME_SCHEMA,
meta = FILEDICT_SCHEMA)
# A schema containing information a repository mirror may require,
# such as a url, the path of the directory metadata files, etc.
MIRROR_SCHEMA = SCHEMA.Object(
object_name='mirror',
url_prefix=URL_SCHEMA,
metadata_path=RELPATH_SCHEMA,
targets_path=RELPATH_SCHEMA,
confined_target_dirs=RELPATHS_SCHEMA,
custom=SCHEMA.Optional(SCHEMA.Object()))
object_name = 'MIRROR_SCHEMA',
url_prefix = URL_SCHEMA,
metadata_path = RELPATH_SCHEMA,
targets_path = RELPATH_SCHEMA,
confined_target_dirs = RELPATHS_SCHEMA,
custom = SCHEMA.Optional(SCHEMA.Object()))
# A dictionary of mirrors where the dict keys hold the mirror's name and
# and the dict values the mirror's data (i.e., 'MIRROR_SCHEMA').
# The repository class of 'updater.py' accepts dictionaries
# of this type provided by the TUF client.
MIRRORDICT_SCHEMA = SCHEMA.DictOf(
key_schema=SCHEMA.AnyString(),
value_schema=MIRROR_SCHEMA)
key_schema = SCHEMA.AnyString(),
value_schema = MIRROR_SCHEMA)
# A Mirrorlist: indicates all the live mirrors, and what documents they
# serve.
MIRRORLIST_SCHEMA = SCHEMA.Object(
object_name='mirrorlist',
_type=SCHEMA.String('Mirrors'),
version=METADATAVERSION_SCHEMA,
expires=TIME_SCHEMA,
mirrors=SCHEMA.ListOf(MIRROR_SCHEMA))
object_name = 'MIRRORLIST_SCHEMA',
_type = SCHEMA.String('Mirrors'),
version = METADATAVERSION_SCHEMA,
expires = TIME_SCHEMA,
mirrors = SCHEMA.ListOf(MIRROR_SCHEMA))
# Any of the role schemas (e.g., TIMESTAMP_SCHEMA, RELEASE_SCHEMA, etc.)
ANYROLE_SCHEMA = SCHEMA.OneOf([ROOT_SCHEMA, TARGETS_SCHEMA, RELEASE_SCHEMA,
TIMESTAMP_SCHEMA, MIRROR_SCHEMA])
@ -383,7 +466,6 @@ class MetaFile(object):
and ReleaseFile all inherit from MetaFile. The
__eq__, __ne__, perform 'equal' and 'not equal' comparisons
between Metadata File objects.
"""
info = None
@ -401,7 +483,6 @@ def __getattr__(self, name):
Allow all metafile objects to have their interesting attributes
referred to directly without the info dict. The info dict is just
to be able to do the __eq__ comparison generically.
"""
if name in self.info:
@ -450,12 +531,13 @@ def make_metadata(version, expiration_date, filedict):
class RootFile(MetaFile):
def __init__(self, version, expires, keys, roles):
def __init__(self, version, expires, keys, roles, consistent_snapshots):
self.info = {}
self.info['version'] = version
self.info['expires'] = expires
self.info['keys'] = keys
self.info['roles'] = roles
self.info['consistent_snapshots'] = consistent_snapshots
@staticmethod
@ -468,21 +550,20 @@ def from_metadata(object):
expires = parse_time(object['expires'])
keys = object['keys']
roles = object['roles']
consistent_snapshots = object['consistent_snapshots']
return RootFile(version, expires, keys, roles)
return RootFile(version, expires, keys, roles, consistent_snapshots)
@staticmethod
def make_metadata(version, expiration_seconds, keydict, roledict):
# Is 'expiration_seconds' properly formatted?
# Raise 'tuf.FormatError' if not.
LENGTH_SCHEMA.check_match(expiration_seconds)
def make_metadata(version, expiration_date, keydict, roledict,
consistent_snapshots):
result = {'_type' : 'Root'}
result['version'] = version
result['expires'] = format_time(time.time() + expiration_seconds)
result['expires'] = expiration_date
result['keys'] = keydict
result['roles'] = roledict
result['consistent_snapshots'] = consistent_snapshots
# Is 'result' a Root metadata file?
# Raise 'tuf.FormatError' if not.
@ -645,7 +726,6 @@ def format_time(timestamp):
<Returns>
A string in 'YYYY-MM-DD HH:MM:SS UTC' format.
"""
try:
@ -677,7 +757,6 @@ def parse_time(string):
<Returns>
A timestamp (e.g., 499137660).
"""
# Is 'string' properly formatted?
@ -715,7 +794,6 @@ def format_base64(data):
<Returns>
A base64-encoded string.
"""
try:
@ -746,7 +824,6 @@ def parse_base64(base64_string):
<Returns>
A byte string representing the parsed based64 encoding of
'base64_string'.
"""
if not isinstance(base64_string, basestring):
@ -791,7 +868,6 @@ def make_signable(object):
<Returns>
A dict in 'SIGNABLE_SCHEMA' format.
"""
if not isinstance(object, dict) or 'signed' not in object:
@ -832,7 +908,6 @@ def make_fileinfo(length, hashes, custom=None):
<Returns>
A dictionary conformant to 'FILEINFO_SCHEMA', representing the file
information of a metadata or target file.
"""
fileinfo = {'length' : length, 'hashes' : hashes}
@ -889,7 +964,6 @@ def make_role_metadata(keyids, threshold, name=None, paths=None,
<Returns>
A properly formatted role meta dict, conforming to
'ROLE_SCHEMA'.
"""
role_meta = {}
@ -950,7 +1024,6 @@ def get_role_class(expected_rolename):
The class corresponding to 'expected_rolename'.
E.g., 'Release' as an argument to this function causes
'ReleaseFile' to be returned.
"""
# Does 'expected_rolename' have the correct type?
@ -993,7 +1066,6 @@ def expected_meta_rolename(meta_rolename):
<Returns>
A string (e.g., 'Root', 'Targets').
"""
# Does 'meta_rolename' have the correct type?
@ -1033,7 +1105,6 @@ def check_signable_object_format(object):
<Returns>
A string representing the signing role (e.g., 'root', 'targets').
The role string is returned with characters all lower case.
"""
# Does 'object' have the correct type?
@ -1077,7 +1148,6 @@ def _canonical_string_encoder(string):
<Returns>
A string with the canonical-encoded 'string' embedded.
"""
string = '"%s"' % re.sub(r'(["\\])', r'\\\1', string)
@ -1182,7 +1252,6 @@ def encode_canonical(object, output_function=None):
<Returns>
A string representing the 'object' encoded in canonical JSON form.
"""
result = None

View file

@ -285,6 +285,24 @@ def configure(filename="tuf.interposition.json",
def refresh(configurations):
"""Refresh the top-level metadata for previously read configurations."""
# Get the updater and refresh its top-level metadata. In the majority of
# integrations, a software updater integrating TUF with interposition will
# usually only require an initial refresh() (i.e., when configure() is
# called). A series of target file requests may then occur, which are all
# referenced by the latest top-level metadata updated by configure().
# Although interposition was designed to remain transparent, for software
# updaters that require an explicit refresh of top-level metadata, this
# method is provided.
for configuration in configurations.itervalues():
__updater_controller.refresh(configuration)
def deconfigure(configurations):
"""Remove TUF interposition for previously read configurations."""

View file

@ -45,7 +45,7 @@ def __init__(self, hostname, port, repository_directory, repository_mirrors,
def __repr__(self):
MESSAGE = "Configuration(netloc={network_location})"
MESSAGE = "network location: {network_location}"
return MESSAGE.format(network_location=self.network_location)

View file

@ -50,6 +50,18 @@ def __init__(self, configuration):
self.switch_context()
self.updater = tuf.client.updater.Updater(self.configuration.hostname,
self.configuration.repository_mirrors)
# Update the client's top-level metadata. The download_target() method does
# not automatically refresh top-level prior to retrieving target files and
# their associated Targets metadata, so update the top-level
# metadata here.
Logger.info('Refreshing top-level metadata for interposed '+repr(configuration))
self.updater.refresh()
def refresh(self):
"""Refresh top-level metadata"""
self.updater.refresh()
def cleanup(self):
@ -66,11 +78,14 @@ def download_target(self, target_filepath):
# download file into a temporary directory shared over runtime
destination_directory = self.tempdir
filename = os.path.join(destination_directory, target_filepath)
self.switch_context() # switch TUF context
self.updater.refresh() # update TUF client repository metadata
# then, update target at filepath
# Switch TUF context.
self.switch_context()
# Locate the fileinfo of 'target_filepath'. updater.target() searches
# Targets metadata in order of trust, according to the currently trusted
# release. To prevent consecutive target file requests from referring to
# different releases, top-level metadata is not automatically refreshed.
targets = [self.updater.target(target_filepath)]
# TODO: targets are always updated if destination directory is new, right?
@ -254,15 +269,37 @@ def __check_configuration_on_add(self, configuration):
def add(self, configuration):
"""Add an Updater based on the given Configuration."""
UPDATER_ADDED_MESSAGE = "Updater added for {configuration}."
repository_mirror_hostnames = self.__check_configuration_on_add(configuration)
# If all is well, build and store an Updater, and remember hostnames.
Logger.info('Adding updater for interposed '+repr(configuration))
self.__updaters[configuration.hostname] = Updater(configuration)
self.__repository_mirror_hostnames.update(repository_mirror_hostnames)
def refresh(self, configuration):
"""Refresh the top-level metadata of the given Configuration."""
assert isinstance(configuration, Configuration)
repository_mirror_hostnames = configuration.get_repository_mirror_hostnames()
assert configuration.hostname in self.__updaters
assert repository_mirror_hostnames.issubset(self.__repository_mirror_hostnames)
# Get the updater and refresh its top-level metadata. In the majority of
# integrations, a software updater integrating TUF with interposition will
# usually only require an initial refresh() (i.e., when configure() is
# called). A series of target file requests may then occur, which are all
# referenced by the latest top-level metadata updated by configure().
# Although interposition was designed to remain transparent, for software
# updaters that require an explicit refresh of top-level metadata, this
# method is provided.
Logger.info('Refreshing top-level metadata for '+ repr(configuration))
updater = self.__updaters.get(configuration.hostname)
updater.refresh()
Logger.info(UPDATER_ADDED_MESSAGE.format(configuration=configuration))
def get(self, url):
@ -273,7 +310,7 @@ def get(self, url):
GENERIC_WARNING_MESSAGE = "No updater or interposition for url={url}"
DIFFERENT_NETLOC_MESSAGE = "We have an updater for netloc={netloc1} but not for netlocs={netloc2}"
HOSTNAME_FOUND_MESSAGE = "Found updater for hostname={hostname}"
HOSTNAME_FOUND_MESSAGE = "Found updater for interposed network location: {netloc}"
HOSTNAME_NOT_FOUND_MESSAGE = "No updater for hostname={hostname}"
updater = None
@ -298,7 +335,7 @@ def get(self, url):
# Ensure that the updater is meant for this (hostname, port).
if updater.configuration.network_location in network_locations:
Logger.info(HOSTNAME_FOUND_MESSAGE.format(hostname=hostname))
Logger.info(HOSTNAME_FOUND_MESSAGE.format(netloc=network_location))
# Raises an exception in case we do not recognize how to
# transform this URL for TUF. In that case, there will be no
# updater for this URL.
@ -324,7 +361,7 @@ def get(self, url):
def remove(self, configuration):
"""Remove an Updater matching the given Configuration."""
UPDATER_REMOVED_MESSAGE = "Updater removed for {configuration}."
UPDATER_REMOVED_MESSAGE = "Updater removed for interposed {configuration}."
assert isinstance(configuration, Configuration)

View file

@ -25,15 +25,17 @@
and the '_get_keyid()' function to learn precisely how keyids are generated.
One may get the keyid of a key object by simply accessing the dictionary's
'keyid' key (i.e., rsakey['keyid']).
"""
import logging
import copy
import tuf
import tuf.formats
import tuf.rsa_key
import tuf.keys
# List of strings representing the key types supported by TUF.
_SUPPORTED_KEY_TYPES = ['rsa', 'ed25519']
# See 'log.py' to learn how logging is handled in TUF.
logger = logging.getLogger('tuf.keydb')
@ -62,13 +64,12 @@ def create_keydb_from_root_metadata(root_metadata):
<Side Effects>
A function to add the key to the database is called. In the case of RSA
keys, this function is add_rsakey().
keys, this function is add_key().
The old keydb key database is replaced.
<Returns>
None.
"""
# Does 'root_metadata' have the correct format?
@ -84,13 +85,13 @@ def create_keydb_from_root_metadata(root_metadata):
# them to 'RSAKEY_SCHEMA' if their type is 'rsa', and then
# adding them the database. Duplicates are avoided.
for keyid, key_metadata in root_metadata['keys'].items():
if key_metadata['keytype'] == 'rsa':
if key_metadata['keytype'] in _SUPPORTED_KEY_TYPES:
# 'key_metadata' is stored in 'KEY_SCHEMA' format. Call
# create_from_metadata_format() to get the key in 'RSAKEY_SCHEMA'
# format, which is the format expected by 'add_rsakey()'.
rsakey_dict = tuf.rsa_key.create_from_metadata_format(key_metadata)
# format, which is the format expected by 'add_key()'.
key_dict = tuf.keys.format_metadata_to_key(key_metadata)
try:
add_rsakey(rsakey_dict, keyid)
add_key(key_dict, keyid)
# 'tuf.Error' raised if keyid does not match the keyid for 'rsakey_dict'.
except tuf.Error, e:
logger.error(e)
@ -105,7 +106,7 @@ def create_keydb_from_root_metadata(root_metadata):
def add_rsakey(rsakey_dict, keyid=None):
def add_key(key_dict, keyid=None):
"""
<Purpose>
Add 'rsakey_dict' to the key database while avoiding duplicates.
@ -113,8 +114,8 @@ def add_rsakey(rsakey_dict, keyid=None):
and raise an exception if it is not.
<Arguments>
rsakey_dict:
A dictionary conformant to 'tuf.formats.RSAKEY_SCHEMA'.
key_dict:
A dictionary conformant to 'tuf.formats.ANYKEY_SCHEMA'.
It has the form:
{'keytype': 'rsa',
'keyid': keyid,
@ -138,15 +139,13 @@ def add_rsakey(rsakey_dict, keyid=None):
<Returns>
None.
"""
# Does 'rsakey_dict' have the correct format?
# This check will ensure 'rsakey_dict' has the appropriate number of objects
# and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError if the check fails.
tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict)
tuf.formats.ANYKEY_SCHEMA.check_match(key_dict)
# Does 'keyid' have the correct format?
if keyid is not None:
@ -154,16 +153,16 @@ def add_rsakey(rsakey_dict, keyid=None):
tuf.formats.KEYID_SCHEMA.check_match(keyid)
# Check if the keyid found in 'rsakey_dict' matches 'keyid'.
if keyid != rsakey_dict['keyid']:
raise tuf.Error('Incorrect keyid '+rsakey_dict['keyid']+' expected '+keyid)
if keyid != key_dict['keyid']:
raise tuf.Error('Incorrect keyid '+key_dict['keyid']+' expected '+keyid)
# Check if the keyid belonging to 'rsakey_dict' is not already
# available in the key database before returning.
keyid = rsakey_dict['keyid']
keyid = key_dict['keyid']
if keyid in _keydb_dict:
raise tuf.KeyAlreadyExistsError('Key: '+keyid)
_keydb_dict[keyid] = rsakey_dict
_keydb_dict[keyid] = copy.deepcopy(key_dict)
@ -190,7 +189,6 @@ def get_key(keyid):
<Returns>
The key matching 'keyid'. In the case of RSA keys, a dictionary conformant
to 'tuf.formats.RSAKEY_SCHEMA' is returned.
"""
# Does 'keyid' have the correct format?
@ -201,7 +199,7 @@ def get_key(keyid):
# Return the key belonging to 'keyid', if found in the key database.
try:
return _keydb_dict[keyid]
return copy.deepcopy(_keydb_dict[keyid])
except KeyError:
raise tuf.UnknownKeyError('Key: '+keyid)
@ -229,7 +227,6 @@ def remove_key(keyid):
<Returns>
None.
"""
# Does 'keyid' have the correct format?
@ -264,7 +261,6 @@ def clear_keydb():
<Returns>
None.
"""
_keydb_dict.clear()

1244
tuf/keys.py Executable file

File diff suppressed because it is too large Load diff

4339
tuf/libtuf.py Executable file

File diff suppressed because it is too large Load diff

View file

@ -77,7 +77,7 @@
# Example format for '_FORMAT_STRING':
# [2013-08-13 15:21:18,068 UTC] [tuf] [INFO][_update_metadata:851@updater.py]
_FORMAT_STRING = '[%(asctime)s UTC] [%(name)s] [%(levelname)s]'+\
'[%(funcName)s:%(lineno)s@%(filename)s] %(message)s'
'[%(funcName)s:%(lineno)s@%(filename)s]\n%(message)s\n'
# Ask all Formatter instances to talk GMT. Set the 'converter' attribute of
# 'logging.Formatter' so that all formatters use Greenwich Mean Time.

895
tuf/pycrypto_keys.py Executable file
View file

@ -0,0 +1,895 @@
"""
<Program Name>
pycrypto_keys.py
<Author>
Vladimir Diaz <vladimir.v.diaz@gmail.com>
<Started>
October 7, 2013.
<Copyright>
See LICENSE for licensing information.
<Purpose>
The goal of this module is to support public-key and general-purpose
cryptography through the PyCrypto library. The RSA-related functions provided:
generate_rsa_public_and_private()
create_rsa_signature()
verify_rsa_signature()
create_rsa_encrypted_pem()
create_rsa_public_and_private_from_encrypted_pem()
The general-purpose functions include:
encrypt_key()
decrypt_key()
PyCrypto (i.e., the 'Crypto' package) performs the actual cryptographic
operations and the functions listed above can be viewed as the easy-to-use
public interface.
https://github.com/dlitz/pycrypto
https://en.wikipedia.org/wiki/RSA_(algorithm)
https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
https://en.wikipedia.org/wiki/3des
https://en.wikipedia.org/wiki/PBKDF
TUF key files are encrypted with the AES-256-CTR-Mode symmetric key
algorithm. User passwords are strengthened with PBKDF2, currently set to
100,000 passphrase iterations. The previous evpy implementation used 1,000
iterations.
PEM-encrypted RSA key files use the Triple Data Encryption Algorithm (3DES)
and Cipher-block chaining (CBC) for the mode of operation. Password-Based Key
Derivation Function 1 (PBKF1) + MD5.
"""
import os
import binascii
import json
# Crypto.PublicKey (i.e., PyCrypto's public-key cryptography modules) supports
# algorithms like the Digital Signature Algorithm (DSA) and the ElGamal
# encryption system. 'Crypto.PublicKey.RSA' is needed here to generate, sign,
# and verify RSA keys.
import Crypto.PublicKey.RSA
# PyCrypto requires 'Crypto.Hash' hash objects to generate PKCS#1 PSS
# signatures (i.e., Crypto.Signature.PKCS1_PSS).
import Crypto.Hash.SHA256
# RSA's probabilistic signature scheme with appendix (RSASSA-PSS).
# PKCS#1 v1.5 is available for compatibility with existing applications, but
# RSASSA-PSS is encouraged for newer applications. RSASSA-PSS generates
# a random salt to ensure the signature generated is probabilistic rather than
# deterministic (e.g., PKCS#1 v1.5).
# http://en.wikipedia.org/wiki/RSA-PSS#Schemes
# https://tools.ietf.org/html/rfc3447#section-8.1
import Crypto.Signature.PKCS1_PSS
# Import PyCrypto's Key Derivation Function (KDF) module. 'keystore.py'
# needs this module to derive a secret key according to the Password-Based
# Key Derivation Function 2 specification. The derived key is used as the
# symmetric key to encrypt TUF key information. PyCrypto's implementation:
# Crypto.Protocol.KDF.PBKDF2(). PKCS#5 v2.0 PBKDF2 specification:
# http://tools.ietf.org/html/rfc2898#section-5.2
import Crypto.Protocol.KDF
# PyCrypto's AES implementation. AES is a symmetric key algorithm that
# operates on fixed block sizes of 128-bits.
# https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
import Crypto.Cipher.AES
# 'Crypto.Random' is a cryptographically strong version of Python's standard
# "random" module. Random bits of data is needed for salts and
# initialization vectors suitable for the encryption algorithms used in
# 'pycrypto_keys.py'.
import Crypto.Random
# The mode of operation is presently set to CTR (CounTeR Mode) for symmetric
# block encryption (AES-256, where the symmetric key is 256 bits). PyCrypto
# provides a callable stateful block counter that can update successive blocks
# when needed. The initial random block, or initialization vector (IV), can
# be set to begin the process of incrementing the 128-bit blocks and allowing
# the AES algorithm to perform cipher block operations on them.
import Crypto.Util.Counter
# Import the TUF package and TUF-defined exceptions in __init__.py.
import tuf
# Digest objects needed to generate hashes.
import tuf.hash
# Perform object format-checking.
import tuf.formats
# Extract the cryptography library settings.
import tuf.conf
# Import key files containing json data.
import tuf.util
# Recommended RSA key sizes:
# http://www.emc.com/emc-plus/rsa-labs/historical/twirl-and-rsa-key-size.htm#table1
# According to the document above, revised May 6, 2003, RSA keys of
# size 3072 provide security through 2031 and beyond.
_DEFAULT_RSA_KEY_BITS = 3072
# The delimiter symbol used to separate the different sections
# of encrypted files (i.e., salt, iterations, hmac, IV, ciphertext).
# This delimiter is arbitrarily chosen and should not occur in
# the hexadecimal representations of the fields it is separating.
_ENCRYPTION_DELIMITER = '@@@@'
# AES key size. Default key size = 32 bytes = AES-256.
_AES_KEY_SIZE = 32
# Default salt size, in bytes. A 128-bit salt (i.e., a random sequence of data
# to protect against attacks that use precomputed rainbow tables to crack
# password hashes) is generated for PBKDF2.
_SALT_SIZE = 16
# Default PBKDF2 passphrase iterations. The current "good enough" number
# of passphrase iterations. We recommend that important keys, such as root,
# be kept offline. 'tuf.conf.PBKDF2_ITERATIONS' should increase as CPU
# speeds increase, set here at 100,000 iterations by default (in 2013).
# Repository maintainers may opt to modify the default setting according to
# their security needs and computational restrictions. A strong user password
# is still important. Modifying the number of iterations will result in a new
# derived key+PBDKF2 combination if the key is loaded and re-saved, overriding
# any previous iteration setting used by the old '<keyid>.key'.
# https://en.wikipedia.org/wiki/PBKDF2
_PBKDF2_ITERATIONS = tuf.conf.PBKDF2_ITERATIONS
def generate_rsa_public_and_private(bits=_DEFAULT_RSA_KEY_BITS):
"""
<Purpose>
Generate public and private RSA keys with modulus length 'bits'.
The public and private keys returned conform to 'tuf.formats.PEMRSA_SCHEMA'
and have the form:
'-----BEGIN RSA PUBLIC KEY----- ...'
or
'-----BEGIN RSA PRIVATE KEY----- ...'
The public and private keys are returned as strings in PEM format.
Although PyCrypto sets a 1024-bit minimum key size,
generate_rsa_public_and_private() enforces a minimum key size of 2048 bits.
If 'bits' is unspecified, a 3072-bit RSA key is generated, which is the key
size recommended by TUF.
>>> public, private = generate_rsa_public_and_private(2048)
>>> tuf.formats.PEMRSA_SCHEMA.matches(public)
True
>>> tuf.formats.PEMRSA_SCHEMA.matches(private)
True
<Arguments>
bits:
The key size, or key length, of the RSA key. 'bits' must be 2048, or
greater, and a multiple of 256.
<Exceptions>
tuf.FormatError, if 'bits' does not contain the correct format.
ValueError, if an exception occurs in the RSA key generation routine.
'bits' must be a multiple of 256. The 'ValueError' exception is raised by
the PyCrypto key generation function.
<Side Effects>
The RSA keys are generated by PyCrypto's Crypto.PublicKey.RSA.generate().
<Returns>
A (public, private) tuple containing the RSA keys in PEM format.
"""
# Does 'bits' have the correct format?
# This check will ensure 'bits' conforms to 'tuf.formats.RSAKEYBITS_SCHEMA'.
# 'bits' must be an integer object, with a minimum value of 2048.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.RSAKEYBITS_SCHEMA.check_match(bits)
# Generate the public and private RSA keys. The PyCrypto module performs
# the actual key generation. Raise 'ValueError' if 'bits' is less than 1024
# or not a multiple of 256, although a 2048-bit minimum is enforced by
# tuf.formats.RSAKEYBITS_SCHEMA.check_match().
rsa_key_object = Crypto.PublicKey.RSA.generate(bits)
# Extract the public & private halves of the RSA key and generate their
# PEM-formatted representations. Return the key pair as a (public, private)
# tuple, where each RSA is a string in PEM format.
private = rsa_key_object.exportKey(format='PEM')
rsa_pubkey = rsa_key_object.publickey()
public = rsa_pubkey.exportKey(format='PEM')
return public, private
def create_rsa_signature(private_key, data):
"""
<Purpose>
Generate an RSASSA-PSS signature. The signature, and the method (signature
algorithm) used, is returned as a (signature, method) tuple.
The signing process will use 'private_key' and 'data' to generate the
signature.
RFC3447 - RSASSA-PSS
http://www.ietf.org/rfc/rfc3447.txt
>>> public, private = generate_rsa_public_and_private(2048)
>>> data = 'The quick brown fox jumps over the lazy dog'
>>> signature, method = create_rsa_signature(private, data)
>>> tuf.formats.NAME_SCHEMA.matches(method)
True
>>> method == 'RSASSA-PSS'
True
>>> tuf.formats.PYCRYPTOSIGNATURE_SCHEMA.matches(method)
True
<Arguments>
private_key:
The private RSA key, a string in PEM format.
data:
Data object used by create_rsa_signature() to generate the signature.
<Exceptions>
tuf.FormatError, if 'private_key' is improperly formatted.
TypeError, if 'private_key' is unset.
tuf.CryptoError, if the signature cannot be generated.
<Side Effects>
PyCrypto's 'Crypto.Signature.PKCS1_PSS' called to generate the signature.
<Returns>
A (signature, method) tuple, where the signature is a string and the method
is 'RSASSA-PSS'.
"""
# Does 'private_key' have the correct format?
# This check will ensure 'private_key' conforms to 'tuf.formats.PEMRSA_SCHEMA'.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.PEMRSA_SCHEMA.check_match(private_key)
# Signing the 'data' object requires a private key.
# The 'RSASSA-PSS' (i.e., PyCrypto module) signing method is the
# only method currently supported.
method = 'RSASSA-PSS'
signature = None
# Verify the signature, but only if the private key has been set. The private
# key is a NULL string if unset. Although it may be clearer to explicit check
# that 'private_key' is not '', we can/should check for a value and not
# compare identities with the 'is' keyword.
if len(private_key):
# Calculate the SHA256 hash of 'data' and generate the hash's PKCS1-PSS
# signature.
try:
rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key)
sha256_object = Crypto.Hash.SHA256.new(data)
pkcs1_pss_signer = Crypto.Signature.PKCS1_PSS.new(rsa_key_object)
signature = pkcs1_pss_signer.sign(sha256_object)
except (ValueError, IndexError, TypeError), e:
message = 'An RSA signature could not be generated.'
raise tuf.CryptoError(message)
else:
raise TypeError('The required private key is unset.')
return signature, method
def verify_rsa_signature(signature, signature_method, public_key, data):
"""
<Purpose>
Determine whether the corresponding private key of 'public_key' produced
'signature'. verify_signature() will use the public key, signature method,
and 'data' to complete the verification.
>>> public, private = generate_rsa_public_and_private(2048)
>>> data = 'The quick brown fox jumps over the lazy dog'
>>> signature, method = create_rsa_signature(private, data)
>>> verify_rsa_signature(signature, method, public, data)
True
>>> verify_rsa_signature(signature, method, public, 'bad_data')
False
<Arguments>
signature:
An RSASSA PSS signature as a string. This is the signature returned
by create_rsa_signature().
signature_method:
A string that indicates the signature algorithm used to generate
'signature'. 'RSASSA-PSS' is currently supported.
public_key:
The RSA public key, a string in PEM format.
data:
Data object used by tuf.keys.create_signature() to generate
'signature'. 'data' is needed here to verify the signature.
<Exceptions>
tuf.UnknownMethodError. Raised if the signing method used by
'signature' is not one supported by tuf.keys.create_signature().
tuf.FormatError. Raised if 'signature', 'signature_method', or 'public_key'
is improperly formatted.
<Side Effects>
Crypto.Signature.PKCS1_PSS.verify() called to do the actual verification.
<Returns>
Boolean. True if the signature is valid, False otherwise.
"""
# Does 'public_key' have the correct format?
# This check will ensure 'public_key' conforms to 'tuf.formats.PEMRSA_SCHEMA'.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.PEMRSA_SCHEMA.check_match(public_key)
# Does 'signature_method' have the correct format?
tuf.formats.NAME_SCHEMA.check_match(signature_method)
# Does 'signature' have the correct format?
tuf.formats.PYCRYPTOSIGNATURE_SCHEMA.check_match(signature)
# Verify whether the private key of 'public_key' produced the signature.
# Before returning the Boolean result, ensure 'RSASSA-PSS' was used
# as the signing method.
signature = signature
method = signature_method
public = public_key
valid_signature = False
# Verify the signature with PyCrypto if the signature method is valid, else
# raise 'tuf.UnknownMethodError'.
if method == 'RSASSA-PSS':
try:
rsa_key_object = Crypto.PublicKey.RSA.importKey(public_key)
pkcs1_pss_verifier = Crypto.Signature.PKCS1_PSS.new(rsa_key_object)
sha256_object = Crypto.Hash.SHA256.new(data)
valid_signature = pkcs1_pss_verifier.verify(sha256_object, signature)
except (ValueError, IndexError, TypeError), e:
message = 'The RSA signature could not be verified.'
raise tuf.CryptoError(message)
else:
raise tuf.UnknownMethodError(method)
return valid_signature
def create_rsa_encrypted_pem(private_key, passphrase):
"""
<Purpose>
Return a string in PEM format, where the private part of the RSA key is
encrypted. The private part of the RSA key is encrypted by the Triple
Data Encryption Algorithm (3DES) and Cipher-block chaining (CBC) for the
mode of operation. Password-Based Key Derivation Function 1 (PBKF1) + MD5
is used to strengthen 'passphrase'.
https://en.wikipedia.org/wiki/Triple_DES
https://en.wikipedia.org/wiki/PBKDF2
>>> public, private = generate_rsa_public_and_private(2048)
>>> passphrase = 'secret'
>>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase)
>>> tuf.formats.PEMRSA_SCHEMA.matches(encrypted_pem)
True
<Arguments>
private_key:
The private key string in PEM format.
passphrase:
The passphrase, or password, to encrypt the private part of the RSA
key. 'passphrase' is not used directly as the encryption key, a stronger
encryption key is derived from it.
<Exceptions>
tuf.FormatError, if the arguments are improperly formatted.
tuf.CryptoError, if an RSA key in encrypted PEM format cannot be created.
TypeError, 'private_key' is unset.
<Side Effects>
PyCrypto's Crypto.PublicKey.RSA.exportKey() called to perform the actual
generation of the PEM-formatted output.
<Returns>
A string in PEM format, where the private RSA key is encrypted.
Conforms to 'tuf.formats.PEMRSA_SCHEMA'.
"""
# Does 'private_key' have the correct format?
# This check will ensure 'private_key' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.PEMRSA_SCHEMA.check_match(private_key)
# Does 'passphrase' have the correct format?
tuf.formats.PASSWORD_SCHEMA.check_match(passphrase)
# 'private_key' is in PEM format and unencrypted. The extracted key will be
# imported and converted to PyCrypto's RSA key object
# (i.e., Crypto.PublicKey.RSA). Use PyCrypto's exportKey method, with a
# passphrase specified, to create the string. PyCrypto uses PBKDF1+MD5 to
# strengthen 'passphrase', and 3DES with CBC mode for encryption.
# 'private_key' may still be a NULL string after the tuf.formats check.
if len(private_key):
try:
rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key)
encrypted_pem = rsa_key_object.exportKey(format='PEM',
passphrase=passphrase)
except (ValueError, IndexError, TypeError), e:
message = 'An encrypted RSA key in PEM format could not be generated.'
raise tuf.CryptoError(message)
else:
raise TypeError('The required private key is unset.')
return encrypted_pem
def create_rsa_public_and_private_from_encrypted_pem(encrypted_pem, passphrase):
"""
<Purpose>
Generate public and private RSA keys from an encrypted PEM.
The public and private keys returned conform to 'tuf.formats.PEMRSA_SCHEMA'
and have the form:
'-----BEGIN RSA PUBLIC KEY----- ...'
or
'-----BEGIN RSA PRIVATE KEY----- ...'
The public and private keys are returned as strings in PEM format.
The private key part of 'encrypted_pem' is encrypted. PyCrypto's importKey
method is used, where a passphrase is specified. PyCrypto uses PBKDF1+MD5
to strengthen 'passphrase', and 3DES with CBC mode for encryption/decryption.
Alternatively, key data may be encrypted with AES-CTR-Mode and the passphrase
strengthened with PBKDF2+SHA256. See 'keystore.py'.
>>> public, private = generate_rsa_public_and_private(2048)
>>> passphrase = 'secret'
>>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase)
>>> returned_public, returned_private = \
create_rsa_public_and_private_from_encrypted_pem(encrypted_pem, passphrase)
>>> tuf.formats.PEMRSA_SCHEMA.matches(returned_public)
True
>>> tuf.formats.PEMRSA_SCHEMA.matches(returned_private)
True
>>> public == returned_public
True
>>> private == returned_private
True
<Arguments>
encrypted_pem:
A byte string in PEM format, where the private key is encrypted. It has
the form:
'-----BEGIN RSA PRIVATE KEY-----\n
Proc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC ...'
passphrase:
The passphrase, or password, to decrypt the private part of the RSA
key. 'passphrase' is not directly used as the encryption key, instead
it is used to derive a stronger symmetric key.
<Exceptions>
tuf.FormatError, if the arguments are improperly formatted.
<Side Effects>
PyCrypto's 'Crypto.PublicKey.RSA.importKey()' called to perform the actual
conversion from an encrypted RSA private key.
<Returns>
A (public, private) tuple containing the RSA keys in PEM format.
"""
# Does 'encryped_pem' have the correct format?
# This check will ensure 'encrypted_pem' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.PEMRSA_SCHEMA.check_match(encrypted_pem)
# Does 'passphrase' have the correct format?
tuf.formats.PASSWORD_SCHEMA.check_match(passphrase)
try:
rsa_key_object = Crypto.PublicKey.RSA.importKey(encrypted_pem, passphrase)
except (ValueError, IndexError, TypeError), e:
message = 'An RSA key object could not be generated from the encrypted '+\
'PEM string.'
# Raise 'tuf.CryptoError' instead of PyCrypto's exception to avoid
# revealing sensitive error, such as a decryption error due to an
# invalid passphrase.
raise tuf.CryptoError(message)
# Extract the public and private halves of the RSA key and generate their
# PEM-formatted representations. The dictionary returned contains the
# private and public RSA keys in PEM format, as strings.
private = rsa_key_object.exportKey(format='PEM')
rsa_pubkey = rsa_key_object.publickey()
public = rsa_pubkey.exportKey(format='PEM')
return public, private
def encrypt_key(key_object, password):
"""
<Purpose>
Return a string containing 'key_object' in encrypted form. Encrypted strings
may be safely saved to a file. The corresponding decrypt_key() function can
be applied to the encrypted string to restore the original key object.
'key_object' is a TUF key (e.g., RSAKEY_SCHEMA, ED25519KEY_SCHEMA). This
function calls the PyCrypto library to perform the encryption and derive
a suitable encryption key.
Whereas an encrypted PEM file uses the Triple Data Encryption Algorithm
(3DES), the Cipher-block chaining (CBC) mode of operation, and the Password
Based Key Derivation Function 1 (PBKF1) + MD5 to strengthen 'password',
encrypted TUF keys use AES-256-CTR-Mode and passwords strengthened with
PBKDF2-HMAC-SHA256 (100K iterations by default, but may be overriden in
'tuf.conf.PBKDF2_ITERATIONS' by the user).
http://en.wikipedia.org/wiki/Advanced_Encryption_Standard
http://en.wikipedia.org/wiki/CTR_mode#Counter_.28CTR.29
https://en.wikipedia.org/wiki/PBKDF2
>>> ed25519_key = {'keytype': 'ed25519', \
'keyid': \
'd62247f817883f593cf6c66a5a55292488d457bcf638ae03207dbbba9dbe457d', \
'keyval': {'public': \
'74addb5ad544a4306b34741bc1175a3613a8d7dc69ff64724243efdec0e301ad', \
'private': \
'1f26964cc8d4f7ee5f3c5da2fbb7ab35811169573ac367b860a537e47789f8c4'}}
>>> passphrase = 'secret'
>>> encrypted_key = encrypt_key(ed25519_key, passphrase)
>>> tuf.formats.ENCRYPTEDKEY_SCHEMA.matches(encrypted_key)
True
<Arguments>
key_object:
The TUF key object that should contain the private portion of the ED25519
key.
password:
The password, or passphrase, to encrypt the private part of the RSA
key. 'password' is not used directly as the encryption key, a stronger
encryption key is derived from it.
<Exceptions>
tuf.FormatError, if any of the arguments are improperly formatted or
'key_object' does not contain the private portion of the key.
tuf.CryptoError, if an ED25519 key in encrypted TUF format cannot be
created.
<Side Effects>
PyCrypto cryptographic operations called to perform the actual encryption of
'key_object'. 'password' used to derive a suitable encryption key.
<Returns>
An encrypted string in 'tuf.formats.ENCRYPTEDKEY_SCHEMA' format.
"""
# Do the arguments have the correct format?
# Ensure the arguments have the appropriate number of objects and object
# types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.ANYKEY_SCHEMA.check_match(key_object)
# Does 'password' have the correct format?
tuf.formats.PASSWORD_SCHEMA.check_match(password)
# Ensure the private portion of the key is included in 'key_object'.
if not key_object['keyval']['private']:
message = 'Key object does not contain a private part.'
raise tuf.FormatError(message)
# Derive a key (i.e., an appropriate encryption key and not the
# user's password) from the given 'password'. Strengthen 'password' with
# PBKDF2-HMAC-SHA256 (100K iterations by default, but may be overriden in
# 'tuf.conf.PBKDF2_ITERATIONS' by the user).
salt, iterations, derived_key = _generate_derived_key(password)
# Store the derived key info in a dictionary, the object expected
# by the non-public _encrypt() routine.
derived_key_information = {'salt': salt, 'iterations': iterations,
'derived_key': derived_key}
# Convert the key object to json string format and encrypt it with the
# derived key.
encrypted_key = _encrypt(json.dumps(key_object), derived_key_information)
return encrypted_key
def decrypt_key(encrypted_key, password):
"""
<Purpose>
Return a string containing 'encrypted_key' in non-encrypted form.
The decrypt_key() function can be applied to the encrypted string to restore
the original key object, a TUF key (e.g., RSAKEY_SCHEMA, ED25519KEY_SCHEMA).
This function calls the appropriate cryptography module (e.g.,
pycrypto_keys.py) to perform the decryption.
Encrypted TUF keys use AES-256-CTR-Mode and passwords strengthened with
PBKDF2-HMAC-SHA256 (100K iterations be default, but may be overriden in
'tuf.conf.py' by the user).
http://en.wikipedia.org/wiki/Advanced_Encryption_Standard
http://en.wikipedia.org/wiki/CTR_mode#Counter_.28CTR.29
https://en.wikipedia.org/wiki/PBKDF2
>>> ed25519_key = {'keytype': 'ed25519', \
'keyid': \
'd62247f817883f593cf6c66a5a55292488d457bcf638ae03207dbbba9dbe457d', \
'keyval': {'public': \
'74addb5ad544a4306b34741bc1175a3613a8d7dc69ff64724243efdec0e301ad', \
'private': \
'1f26964cc8d4f7ee5f3c5da2fbb7ab35811169573ac367b860a537e47789f8c4'}}
>>> passphrase = 'secret'
>>> encrypted_key = encrypt_key(ed25519_key, passphrase)
>>> decrypted_key = decrypt_key(encrypted_key, passphrase)
>>> tuf.formats.ED25519KEY_SCHEMA.matches(decrypted_key)
True
>>> decrypted_key == ed25519_key
True
<Arguments>
encrypted_key:
An encrypted TUF key (additional data is also included, such as salt,
number of password iterations used for the derived encryption key, etc)
of the form 'tuf.formats.ENCRYPTEDKEY_SCHEMA'. 'encrypted_key' should
have been generated with encrypted_key().
password:
The password, or passphrase, to encrypt the private part of the RSA
key. 'password' is not used directly as the encryption key, a stronger
encryption key is derived from it.
<Exceptions>
tuf.FormatError, if the arguments are improperly formatted.
tuf.CryptoError, if a TUF key cannot be decrypted from 'encrypted_key'.
<Side Effects>
The PyCrypto library called to perform the actual decryption of
'encrypted_key'. The key derivation data stored in 'encrypted_key' is used
to re-derive the encryption/decryption key.
<Returns>
The decrypted key object in 'tuf.formats.ANYKEY_SCHEMA' format.
"""
# Do the arguments have the correct format?
# Ensure the arguments have the appropriate number of objects and object
# types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.ENCRYPTEDKEY_SCHEMA.check_match(encrypted_key)
# Does 'password' have the correct format?
tuf.formats.PASSWORD_SCHEMA.check_match(password)
# Decrypt 'encrypted_key', using 'password' (and additional key derivation
# data like salts and password iterations) to re-derive the decryption key.
json_data = _decrypt(encrypted_key, password)
key_object = tuf.util.load_json_string(json_data)
return key_object
def _generate_derived_key(password, salt=None, iterations=None):
"""
Generate a derived key by feeding 'password' to the Password-Based Key
Derivation Function (PBKDF2). PyCrypto's PBKDF2 implementation is
currently used. 'salt' may be specified so that a previous derived key
may be regenerated.
"""
if salt is None:
salt = Crypto.Random.new().read(_SALT_SIZE)
if iterations is None:
iterations = _PBKDF2_ITERATIONS
def pseudorandom_function(password, salt):
"""
PyCrypto's PBKDF2() expects a callable function for its optional
'prf' argument. 'prf' is set to HMAC-SHA1 (in PyCrypto's PBKDF2 function)
by default. 'pseudorandom_function' instead sets 'prf' to HMAC-SHA256.
"""
return Crypto.Hash.HMAC.new(password, salt, Crypto.Hash.SHA256).digest()
# 'dkLen' is the desired key length. 'count' is the number of password
# iterations performed by PBKDF2. 'prf' is a pseudorandom function, which
# must be callable.
derived_key = Crypto.Protocol.KDF.PBKDF2(password, salt,
dkLen=_AES_KEY_SIZE,
count=iterations,
prf=pseudorandom_function)
return salt, iterations, derived_key
def _encrypt(key_data, derived_key_information):
"""
Encrypt 'key_data' using the Advanced Encryption Standard (AES-256) algorithm.
'derived_key_information' should contain a key strengthened by PBKDF2. The
key size is 256 bits and AES's mode of operation is set to CTR (CounTeR Mode).
The HMAC of the ciphertext is generated to ensure the ciphertext has not been
modified.
'key_data' is the JSON string representation of the key. In the case
of RSA keys, this format would be 'tuf.formats.RSAKEY_SCHEMA':
{'keytype': 'rsa',
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
'derived_key_information' is a dictionary of the form:
{'salt': '...',
'derived_key': '...',
'iterations': '...'}
'tuf.CryptoError' raised if the encryption fails.
"""
# Generate a random initialization vector (IV). The 'iv' is treated as the
# initial counter block to a stateful counter block function (i.e.,
# PyCrypto's 'Crypto.Util.Counter'). The AES block cipher operates on 128-bit
# blocks, so generate a random 16-byte initialization block. PyCrypto expects
# the initial value of the stateful counter to be an integer.
# Follow the provably secure encrypt-then-MAC approach, which affords the
# ability to verify ciphertext without needing to decrypt it and preventing
# an attacker from feeding the block cipher malicious data. Modes like GCM
# provide both encryption and authentication, whereas CTR only provides
# encryption.
iv = Crypto.Random.new().read(16)
stateful_counter_128bit_blocks = Crypto.Util.Counter.new(128,
initial_value=long(iv.encode('hex'), 16))
symmetric_key = derived_key_information['derived_key']
aes_cipher = Crypto.Cipher.AES.new(symmetric_key,
Crypto.Cipher.AES.MODE_CTR,
counter=stateful_counter_128bit_blocks)
# Use AES-256 to encrypt 'key_data'. The key size determines how many cycle
# repetitions are performed by AES, 14 cycles for 256-bit keys.
try:
ciphertext = aes_cipher.encrypt(key_data)
# Raise generic exception message to avoid revealing sensitive information,
# such as invalid passwords, encryption keys, etc., that an attacker can use
# to his advantage.
except Exception, e:
message = 'The key data cannot be encrypted.'
raise tuf.CryptoError(message)
# Generate the hmac of the ciphertext to ensure it has not been modified.
# The decryption routine may verify a ciphertext without having to perform
# a decryption operation.
salt = derived_key_information['salt']
hmac_object = Crypto.Hash.HMAC.new(symmetric_key, ciphertext,
Crypto.Hash.SHA256)
hmac = hmac_object.hexdigest()
# Store the number of PBKDF2 iterations used to derive the symmetric key so
# that the decryption routine can regenerate the symmetric key successfully.
# The pbkdf2 iterations are allowed to vary for the keys loaded and saved.
iterations = derived_key_information['iterations']
# Return the salt, iterations, hmac, initialization vector, and ciphertext
# as a single string. These five values are delimited by
# '_ENCRYPTION_DELIMITER' to make extraction easier. This delimiter is
# arbitrarily chosen and should not occur in the hexadecimal representations
# of the fields it is separating.
return binascii.hexlify(salt) + _ENCRYPTION_DELIMITER + \
binascii.hexlify(str(iterations)) + _ENCRYPTION_DELIMITER + \
binascii.hexlify(hmac) + _ENCRYPTION_DELIMITER + \
binascii.hexlify(iv) + _ENCRYPTION_DELIMITER + \
binascii.hexlify(ciphertext)
def _decrypt(file_contents, password):
"""
The corresponding decryption routine for _encrypt().
'tuf.CryptoError' raised if the decryption fails.
"""
# Extract the salt, iterations, hmac, initialization vector, and ciphertext
# from 'file_contents'. These five values are delimited by
# '_ENCRYPTION_DELIMITER'. This delimiter is arbitrarily chosen and should
# not occur in the hexadecimal representations of the fields it is separating.
salt, iterations, hmac, iv, ciphertext = \
file_contents.split(_ENCRYPTION_DELIMITER)
# Ensure we have the expected raw data for the delimited cryptographic data.
salt = binascii.unhexlify(salt)
iterations = int(binascii.unhexlify(iterations))
hmac = binascii.unhexlify(hmac)
iv = binascii.unhexlify(iv)
ciphertext = binascii.unhexlify(ciphertext)
# Generate derived key from 'password'. The salt and iterations are specified
# so that the expected derived key is regenerated correctly. Discard the old
# "salt" and "iterations" values, as we only need the old derived key.
junk_old_salt, junk_old_iterations, derived_key = \
_generate_derived_key(password, salt, iterations)
# Verify the hmac to ensure the ciphertext is valid and has not been altered.
# See the encryption routine for why we use the encrypt-then-MAC approach.
generated_hmac_object = Crypto.Hash.HMAC.new(derived_key, ciphertext,
Crypto.Hash.SHA256)
generated_hmac = generated_hmac_object.hexdigest()
if generated_hmac != hmac:
raise tuf.CryptoError('Decryption failed.')
# The following decryption routine assumes 'ciphertext' was encrypted with
# AES-256.
stateful_counter_128bit_blocks = Crypto.Util.Counter.new(128,
initial_value=long(iv.encode('hex'), 16))
aes_cipher = Crypto.Cipher.AES.new(derived_key,
Crypto.Cipher.AES.MODE_CTR,
counter=stateful_counter_128bit_blocks)
try:
key_plaintext = aes_cipher.decrypt(ciphertext)
# Raise generic exception message to avoid revealing sensitive information,
# such as invalid passwords, encryption keys, etc., that an attacker can
# use to his advantage.
except Exception, e:
raise tuf.CryptoError('Decryption failed.')
return key_plaintext
if __name__ == '__main__':
# The interactive sessions of the documentation strings can
# be tested by running 'pycrypto_keys.py' as a standalone module:
# $ python pycrypto_keys.py
import doctest
doctest.testmod()

View file

@ -34,7 +34,6 @@
algorithm. User passwords are strengthened with PBKDF2, currently set to
100,000 passphrase iterations. The previous evpy implementation used 1,000
iterations.
"""
import os
@ -68,7 +67,7 @@
# the AES algorithm to perform cipher block operations on them.
import Crypto.Util.Counter
import tuf.rsa_key
import tuf.keys
import tuf.util
import tuf.conf
@ -104,6 +103,9 @@
# https://en.wikipedia.org/wiki/PBKDF2
_PBKDF2_ITERATIONS = tuf.conf.PBKDF2_ITERATIONS
#
_SUPPORTED_KEY_TYPES = ['rsa', 'ed25519']
# A user password is read and a derived key generated. The derived key returned
# by the key derivation function (PBKDF2) is saved in '_derived_keys', along
# with the salt and iterations used, which has the form:
@ -159,7 +161,6 @@ def add_rsakey(rsakey_dict, password, keyid=None):
<Returns>
None.
"""
# Does 'rsakey_dict' have the correct format?
@ -235,7 +236,6 @@ def load_keystore_from_keyfiles(directory_name, keyids, passwords):
<Returns>
A list containing the keyids of the loaded keys.
"""
# Does 'directory_name' have the correct format?
@ -286,11 +286,11 @@ def load_keystore_from_keyfiles(directory_name, keyids, passwords):
# Create the key based on its key type. RSA keys currently
# supported.
if keydata['keytype'] == 'rsa':
if keydata['keytype'] in _SUPPORTED_KEY_TYPES:
# 'keydata' is stored in KEY_SCHEMA format. Call
# create_from_metadata_format() to get the key in RSAKEY_SCHEMA
# format_metadata_to_key() to get the key in RSAKEY_SCHEMA
# format, which is the format expected by 'add_rsakey()'.
rsa_key = tuf.rsa_key.create_from_metadata_format(keydata)
rsa_key = tuf.keys.format_metadata_to_key(keydata)
# Ensure the keyid for 'rsa_key' is one of the keys specified in
# 'keyids'. If not, do not load the key.
@ -343,7 +343,6 @@ def save_keystore_to_keyfiles(directory_name):
<Returns>
None.
"""
# Does 'directory_name' have the correct format?
@ -365,9 +364,11 @@ def save_keystore_to_keyfiles(directory_name):
file_object = open(basefilename, 'w')
# Determine the appropriate format to save the key based on its key type.
if key['keytype'] == 'rsa':
if key['keytype'] in _SUPPORTED_KEY_TYPES:
keytype = key['keytype']
keyval = key['keyval']
key_metadata_format = \
tuf.rsa_key.create_in_metadata_format(key['keyval'], private=True)
tuf.keys.format_keyval_to_metadata(keytype, keyval, private=True)
else:
logger.warn('The keystore has a key with an unrecognized key type.')
continue
@ -402,7 +403,6 @@ def clear_keystore():
<Returns>
None.
"""
_keystore.clear()
@ -442,7 +442,6 @@ def change_password(keyid, old_password, new_password):
<Returns>
None.
"""
# Does 'keyid' have the correct format?
@ -506,7 +505,6 @@ def get_key(keyid):
<Returns>
The key belonging to 'keyid' (e.g., RSA key).
"""
# Does 'keyid' have the correct format?
@ -530,7 +528,6 @@ def _generate_derived_key(password, salt=None, iterations=None):
Derivation Function (PBKDF2). PyCrypto's PBKDF2 implementation is
currently used. 'salt' may be specified so that a previous derived key
may be regenerated.
"""
if salt is None:
@ -584,7 +581,6 @@ def _encrypt(key_data, derived_key_information):
'iterations': '...'}
'tuf.CryptoError' raised if the encryption fails.
"""
# Generate a random initialization vector (IV). The 'iv' is treated as the
@ -650,7 +646,6 @@ def _decrypt(file_contents, password):
The corresponding decryption routine for _encrypt().
'tuf.CryptoError' raised if the decryption fails.
"""
# Extract the salt, iterations, hmac, initialization vector, and ciphertext

View file

@ -46,7 +46,6 @@
<Options>
See the parse_options() function for the full list of supported options.
"""
import os
@ -92,7 +91,6 @@ def _get_password(prompt='Password: ', confirm=False):
is True, the user is asked to enter the previously
entered password once again. If they match, the
password is returned to the caller.
"""
while True:
@ -131,7 +129,6 @@ def _get_metadata_directory():
returned to the caller. 'tuf.FormatError' is raised
if the directory is not properly formatted, and 'tuf.Error'
if it does not exist.
"""
metadata_directory = _prompt('\nEnter the metadata directory: ', str)
@ -151,7 +148,6 @@ def _list_keyids(keystore_directory, metadata_directory):
It is assumed the directory arguments exist and have been validated by
the caller. The keyids are listed without the '.key' extension,
along with their associated roles.
"""
# Determine the 'root.txt' filename. This metadata file is needed
@ -233,7 +229,6 @@ def _get_keyids(keystore_directory):
key files are stored in encrypted form, the user is asked
to enter the password that was used to encrypt the key
file.
"""
# The keyids list containing the keys loaded.
@ -288,7 +283,6 @@ def _get_all_config_keyids(config_filepath, keystore_directory):
loaded_keyids = {'root': [1233d3d, 598djdks, ..],
'release': [sdfsd323, sdsd9090s, ..]
...}
"""
# Save the 'load_keystore_from_keyfiles' function call.
@ -338,7 +332,6 @@ def _get_role_config_keyids(config_filepath, keystore_directory, role):
<Exceptions>
tuf.Error, if the required keys could not be loaded.
"""
# Save the 'load_keystore_from_keyfiles' function call.
@ -409,7 +402,6 @@ def _get_metadata_version(metadata_filename):
'metadata_filename' does not exist, return a version value of 1.
Raise 'tuf.RepositoryError' if 'metadata_filename' cannot be read or
validated.
"""
# If 'metadata_filename' does not exist on the repository, this means
@ -442,7 +434,6 @@ def _get_metadata_expiration():
<Exceptions>
tuf.RepositoryError, if the entered expiration date is invalid.
"""
message = '\nCurrent time: '+tuf.formats.format_time(time.time())+'.\n'+\
@ -487,7 +478,6 @@ def change_password(keystore_directory):
<Returns>
None.
"""
# Save the 'load_keystore_from_keyfiles' function call.
@ -563,7 +553,6 @@ def generate_rsa_key(keystore_directory):
<Returns>
None.
"""
# Save a reference to the generate_and_save_rsa_key() function.
@ -612,7 +601,6 @@ def list_signing_keys(keystore_directory):
<Returns>
None.
"""
# Verify the 'keystore_directory' argument.
@ -654,7 +642,6 @@ def dump_key(keystore_directory):
<Returns>
None.
"""
# Save the 'load_keystore_from_keyfiles' function call.
@ -704,8 +691,11 @@ def dump_key(keystore_directory):
# Retrieve the key metadata according to the keytype.
if key['keytype'] == 'rsa':
key_metadata = tuf.rsa_key.create_in_metadata_format(key['keyval'],
private=show_private)
keytype = key['keytype']
keyval = key['keyval']
key_metadata = tuf.keys.format_keyval_to_metadata(keytype, keyval,
#key_metadata = tuf.keys.create_in_metadata_format(keytype, keyval,
private=show_private)
else:
message = 'The keystore contains an invalid key type.'
raise tuf.RepositoryError(message)
@ -737,7 +727,6 @@ def make_root_metadata(keystore_directory):
<Returns>
None.
"""
# Verify the 'keystore_directory' argument.
@ -804,7 +793,6 @@ def make_targets_metadata(keystore_directory):
<Returns>
None.
"""
# Verify the 'keystore_directory' argument.
@ -886,7 +874,6 @@ def make_release_metadata(keystore_directory):
<Returns>
None.
"""
# Verify the 'keystore_directory' argument.
@ -951,7 +938,6 @@ def make_timestamp_metadata(keystore_directory):
<Returns>
None.
"""
# Verify the 'keystore_directory' argument.
@ -1017,7 +1003,6 @@ def sign_metadata_file(keystore_directory):
<Returns>
None.
"""
# Verify the 'keystore_directory' argument.
@ -1084,7 +1069,6 @@ def make_delegation(keystore_directory):
<Returns>
None.
"""
# Verify the 'keystore_directory' argument.
@ -1154,7 +1138,6 @@ def _load_parent_role(metadata_directory, keystore_directory, targets_roles):
list of known targets roles and asked to enter the parent role to load.
Ensure the parent role is loaded properly and return a string containing
the parent role's full rolename and a list of keyids belonging to the parent.
"""
# 'load_key' is a reference to the 'load_keystore_from_keyfiles function'.
@ -1210,7 +1193,6 @@ def _get_delegated_role(keystore_directory, metadata_directory):
a list of keyids available in the keystore and asked to enter the keyid
belonging to the delegated role. Return a string containing
the delegated role's full rolename and its keyids.
"""
# Retrieve the delegated rolename from the user (e.g., 'role1').
@ -1240,7 +1222,6 @@ def _make_delegated_metadata(metadata_directory, delegated_targets,
role. Determine the target files from the paths in 'delegated_targets'
and the other information needed to generate the targets metadata file for
delegated_role'. Return the delegated paths to the caller.
"""
repository_directory, junk = os.path.split(metadata_directory)
@ -1336,7 +1317,6 @@ def _update_parent_metadata(metadata_directory, delegated_role,
metadata file is updated with the key and role information belonging
to the newly added delegated role. Finally, the metadata file
is signed and written to the metadata directory.
"""
# According to the specification, the 'paths' and 'path_hash_prefixes'
@ -1376,8 +1356,10 @@ def _update_parent_metadata(metadata_directory, delegated_role,
# Retrieve the key belonging to 'delegated_keyid' from the keystore.
role_key = tuf.repo.keystore.get_key(delegated_keyid)
if role_key['keytype'] == 'rsa':
keytype = role_key['keytype']
keyval = role_key['keyval']
keys[delegated_keyid] = tuf.rsa_key.create_in_metadata_format(keyval)
keys[delegated_keyid] = tuf.keys.format_keyval_to_metadata(keytype, keyval)
#keys[delegated_keyid] = tuf.keys.create_in_metadata_format(keytype, keyval)
else:
message = 'Invalid keytype encountered: '+delegated_keyid+'\n'
raise tuf.RepositoryError(message)
@ -1450,7 +1432,6 @@ def process_option(options):
<Returns>
None.
"""
# Determine which option was chosen and call its corresponding
@ -1506,7 +1487,6 @@ def parse_options():
<Returns>
The options object returned by the parser's parse_args() method.
"""
usage = 'usage: %prog [option] <keystore_directory>'

View file

@ -16,18 +16,18 @@
These functions contain code that can extract or create needed repository
data, such as the extraction of role and keyid information from a config file,
and the generation of actual metadata content.
"""
import gzip
import os
import ConfigParser
import logging
import time
import tuf
import tuf.formats
import tuf.hash
import tuf.rsa_key
import tuf.keys
import tuf.repo.keystore
import tuf.sig
import tuf.util
@ -81,7 +81,6 @@ def read_config_file(filename):
<Returns>
A dictionary containing the data loaded from the configuration file.
"""
# Does 'filename' have the correct format?
@ -151,7 +150,6 @@ def get_metadata_file_info(filename):
A dictionary conformant to 'tuf.formats.FILEINFO_SCHEMA'. This
dictionary contains the length, hashes, and custom data about
the 'filename' metadata file.
"""
# Does 'filename' have the correct format?
@ -204,7 +202,6 @@ def get_metadata_filenames(metadata_directory=None):
<Returns>
A dictionary containing the expected filenames of the top-level
metadata files, such as 'root.txt' and 'release.txt'.
"""
if metadata_directory is None:
@ -255,7 +252,6 @@ def generate_root_metadata(config_filepath, version):
<Returns>
A root 'signable' object conformant to 'tuf.formats.SIGNABLE_SCHEMA'.
"""
# Does 'config_filepath' have the correct format?
@ -290,8 +286,10 @@ def generate_root_metadata(config_filepath, version):
keyid = key['keyid']
# This appears to be a new keyid. Let's generate the key for it.
if keyid not in keydict:
if key['keytype'] == 'rsa':
keydict[keyid] = tuf.rsa_key.create_in_metadata_format(key['keyval'])
if key['keytype'] in ['rsa', 'ed25519']:
keytype = key['keytype']
keyval = key['keyval']
keydict[keyid] = tuf.keys.format_keyval_to_metadata(keytype, keyval)
# This is not a recognized key. Raise an exception.
else:
raise tuf.Error('Unsupported keytype: '+keyid)
@ -312,7 +310,8 @@ def generate_root_metadata(config_filepath, version):
3600 * 24 * expiration['days'])
# Generate the root metadata object.
root_metadata = tuf.formats.RootFile.make_metadata(version, expiration_seconds,
expiration_date = tuf.formats.format_time(time.time()+expiration_seconds)
root_metadata = tuf.formats.RootFile.make_metadata(version, expiration_date,
keydict, roledict)
# Note: make_signable() returns the following dictionary:
@ -364,7 +363,6 @@ def generate_targets_metadata(repository_directory, target_files, version,
<Returns>
A targets 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'.
"""
# Do the arguments have the correct format.
@ -433,7 +431,6 @@ def generate_release_metadata(metadata_directory, version, expiration_date):
<Returns>
The release 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'.
"""
# Does 'metadata_directory' have the correct format?
@ -510,7 +507,6 @@ def generate_timestamp_metadata(release_filename, version,
<Returns>
A timestamp 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'.
"""
# Do the arguments have the correct format?
@ -575,7 +571,6 @@ def write_metadata_file(metadata, filename, compression=None):
<Returns>
The path to the written metadata file.
"""
# Are the arguments properly formatted?
@ -645,7 +640,6 @@ def read_metadata_file(filename):
<Returns>
The metadata object.
"""
return tuf.util.load_json_file(filename)
@ -684,7 +678,6 @@ def sign_metadata(metadata, keyids, filename):
<Returns>
A signable object conformant to 'tuf.formats.SIGNABLE_SCHEMA'.
"""
# Does 'keyids' and 'filename' have the correct format?
@ -767,7 +760,6 @@ def generate_and_save_rsa_key(keystore_directory, password,
'keyid': keyid,
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
"""
# Are the arguments correctly formatted?
@ -778,7 +770,7 @@ def generate_and_save_rsa_key(keystore_directory, password,
keystore_directory = check_directory(keystore_directory)
# tuf.FormatError or tuf.CryptoError raised.
rsakey = tuf.rsa_key.generate(bits)
rsakey = tuf.keys.generate_rsa_key(bits)
logger.info('Generated a new key: '+rsakey['keyid'])
@ -820,7 +812,6 @@ def check_directory(directory):
<Returns>
The normalized absolutized path of 'directory'.
"""
# Does 'directory' have the correct format?
@ -868,7 +859,6 @@ def get_target_keyids(metadata_directory):
A dictionary containing the role information extracted from the
metadata.
Ex: {'targets':[keyid1, ...], 'targets/role1':[keyid], ...}
"""
# Does 'metadata_directory' have the correct format?
@ -964,7 +954,6 @@ def build_config_file(config_file_directory, timeout, role_info):
<Returns>
The normalized absolutized path of the saved configuration file.
"""
# Do the arguments have the correct format?
@ -1054,7 +1043,6 @@ def build_root_file(config_filepath, root_keyids, metadata_directory, version):
<Returns>
The path for the written root metadata file.
"""
# Do the arguments have the correct format?
@ -1116,7 +1104,6 @@ def build_targets_file(target_paths, targets_keyids, metadata_directory,
<Returns>
The path for the written targets metadata file.
"""
# Do the arguments have the correct format?
@ -1207,7 +1194,6 @@ def build_release_file(release_keyids, metadata_directory,
<Returns>
The path for the written release metadata file.
"""
# Do the arguments have the correct format?
@ -1282,7 +1268,6 @@ def build_timestamp_file(timestamp_keyids, metadata_directory,
<Returns>
The path for the written timestamp metadata file.
"""
# Do the arguments have the correct format?
@ -1370,7 +1355,6 @@ def build_delegated_role_file(delegated_targets_directory, delegated_keyids,
<Returns>
The path for the written targets metadata file.
"""
# Do the arguments have the correct format?
@ -1432,7 +1416,6 @@ def find_delegated_role(roles, delegated_role):
<Returns>
None, if the role with the given name does not exist, or its unique index
in the list of roles.
"""
# Check argument types.
@ -1487,7 +1470,6 @@ def accept_any_file(full_target_path):
<Returns>
True.
"""
return True
@ -1524,7 +1506,6 @@ def get_targets(files_directory, recursive_walk=False, followlinks=True,
<Returns>
A list of absolute paths to target files in the given files_directory.
"""
targets = []
@ -1546,8 +1527,3 @@ def get_targets(files_directory, recursive_walk=False, followlinks=True,
del dirnames[:]
return targets

View file

@ -23,13 +23,20 @@
The role database is a dictionary conformant to 'tuf.formats.ROLEDICT_SCHEMA'
and has the form:
{'rolename': {'keyids': ['34345df32093bd12...'],
'threshold': 1
'paths': ['path/to/role.txt']}}
'signatures': ['abcd3452...'],
'paths': ['path/to/role.txt'],
'path_hash_prefixes': ['ab34df13'],
'delegations': {'keys': {}, 'roles': {}}}
The 'name', 'paths', 'path_hash_prefixes', and 'delegations' dict keys are
optional.
"""
import logging
import copy
import tuf
import tuf.formats
@ -66,7 +73,6 @@ def create_roledb_from_root_metadata(root_metadata):
<Returns>
None.
"""
# Does 'root_metadata' have the correct object format?
@ -81,6 +87,17 @@ def create_roledb_from_root_metadata(root_metadata):
# Iterate through the roles found in 'root_metadata'
# and add them to '_roledb_dict'. Duplicates are avoided.
for rolename, roleinfo in root_metadata['roles'].items():
if rolename == 'root':
roleinfo['version'] = root_metadata['version']
roleinfo['expires'] = root_metadata['expires']
roleinfo['signatures'] = []
roleinfo['signing_keyids'] = []
roleinfo['compressions'] = ['']
roleinfo['partial_loaded'] = False
if rolename.startswith('targets'):
roleinfo['delegations'] = {'keys': {}, 'roles': []}
try:
add_role(rolename, roleinfo)
# tuf.Error raised if the parent role of 'rolename' does not exist.
@ -104,10 +121,17 @@ def add_role(rolename, roleinfo, require_parent=True):
roleinfo:
An object representing the role associated with 'rolename', conformant to
ROLE_SCHEMA. 'roleinfo' has the form:
ROLEDB_SCHEMA. 'roleinfo' has the form:
{'keyids': ['34345df32093bd12...'],
'threshold': 1}
'threshold': 1,
'signatures': ['ab23dfc32']
'paths': ['path/to/target1', 'path/to/target2', ...],
'path_hash_prefixes': ['a324fcd...', ...],
'delegations': {'keys': }
The 'paths', 'path_hash_prefixes', and 'delegations' dict keys are
optional.
The 'target' role has an additional 'paths' key. Its value is a list of
strings representing the path of the target file(s).
@ -128,7 +152,6 @@ def add_role(rolename, roleinfo, require_parent=True):
<Returns>
None.
"""
# Does 'rolename' have the correct object format?
@ -137,10 +160,10 @@ def add_role(rolename, roleinfo, require_parent=True):
tuf.formats.ROLENAME_SCHEMA.check_match(rolename)
# Does 'roleinfo' have the correct object format?
tuf.formats.ROLE_SCHEMA.check_match(roleinfo)
tuf.formats.ROLEDB_SCHEMA.check_match(roleinfo)
# Does 'require_parent' have the correct format?
tuf.formats.TOGGLE_SCHEMA.check_match(require_parent)
tuf.formats.BOOLEAN_SCHEMA.check_match(require_parent)
# Raises tuf.InvalidNameError.
_validate_rolename(rolename)
@ -157,12 +180,71 @@ def add_role(rolename, roleinfo, require_parent=True):
if parent_role not in _roledb_dict:
raise tuf.Error('Parent role does not exist: '+parent_role)
_roledb_dict[rolename] = roleinfo
_roledb_dict[rolename] = copy.deepcopy(roleinfo)
def update_roleinfo(rolename, roleinfo):
"""
<Purpose>
<Arguments>
rolename:
An object representing the role's name, conformant to 'ROLENAME_SCHEMA'
(e.g., 'root', 'release', 'timestamp').
roleinfo:
An object representing the role associated with 'rolename', conformant to
ROLEDB_SCHEMA. 'roleinfo' has the form:
{'name': 'role_name',
'keyids': ['34345df32093bd12...'],
'threshold': 1,
'paths': ['path/to/target1', 'path/to/target2', ...],
'path_hash_prefixes': ['a324fcd...', ...]}
The 'name', 'paths', and 'path_hash_prefixes' dict keys are optional.
The 'target' role has an additional 'paths' key. Its value is a list of
strings representing the path of the target file(s).
<Exceptions>
tuf.FormatError, if 'rolename' or 'roleinfo' does not have the correct
object format.
tuf.UnknownRoleError, if 'rolename' cannot be found in the role database.
tuf.InvalidNameError, if 'rolename' is improperly formatted.
<Side Effects>
The role database is modified.
<Returns>
None.
"""
# Does 'rolename' have the correct object format?
# This check will ensure 'rolename' has the appropriate number of objects
# and object types, and that all dict keys are properly named.
tuf.formats.ROLENAME_SCHEMA.check_match(rolename)
# Does 'roleinfo' have the correct object format?
tuf.formats.ROLEDB_SCHEMA.check_match(roleinfo)
# Raises tuf.InvalidNameError.
_validate_rolename(rolename)
if rolename not in _roledb_dict:
raise tuf.UnknownRoleError('Role does not exist: '+rolename)
_roledb_dict[rolename] = copy.deepcopy(roleinfo)
def get_parent_rolename(rolename):
"""
<Purpose>
@ -187,7 +269,6 @@ def get_parent_rolename(rolename):
<Returns>
A string representing the name of the parent role.
"""
# Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
@ -228,7 +309,6 @@ def get_all_parent_roles(rolename):
<Returns>
A list containing all the parent roles.
"""
# Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
@ -279,7 +359,6 @@ def role_exists(rolename):
<Returns>
Boolean. True if 'rolename' is found in the role database, False otherwise.
"""
# Raise tuf.FormatError, tuf.InvalidNameError.
@ -318,7 +397,6 @@ def remove_role(rolename):
<Returns>
None.
"""
# Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
@ -358,7 +436,6 @@ def remove_delegated_roles(rolename):
<Returns>
None.
"""
# Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
@ -390,7 +467,6 @@ def get_rolenames():
<Returns>
A list of rolenames.
"""
return _roledb_dict.keys()
@ -399,6 +475,47 @@ def get_rolenames():
def get_roleinfo(rolename):
"""
<Purpose>
Return the roleinfo of 'rolename'.
{'keyids': ['34345df32093bd12...'],
'threshold': 1,
'signatures': ['ab453bdf...', ...],
'paths': ['path/to/target1', 'path/to/target2', ...],
'path_hash_prefixes': ['a324fcd...', ...],
'delegations': {'keys': {}, 'roles': []}}
The 'signatures', 'paths', 'path_hash_prefixes', and 'delegations' dict keys
are optional.
<Arguments>
rolename:
An object representing the role's name, conformant to 'ROLENAME_SCHEMA'
(e.g., 'root', 'release', 'timestamp').
<Exceptions>
tuf.FormatError, if 'rolename' is improperly formatted.
tuf.UnknownRoleError, if 'rolename' does not exist.
<Side Effects>
None.
<Returns>
The roleinfo of 'rolename'.
"""
# Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
_check_rolename(rolename)
return copy.deepcopy(_roledb_dict[rolename])
def get_role_keyids(rolename):
"""
<Purpose>
@ -426,7 +543,6 @@ def get_role_keyids(rolename):
<Returns>
A list of keyids.
"""
# Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
@ -462,7 +578,6 @@ def get_role_threshold(rolename):
<Returns>
A threshold integer value.
"""
# Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
@ -498,7 +613,6 @@ def get_role_paths(rolename):
<Returns>
A list of paths.
"""
# Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
@ -540,8 +654,7 @@ def get_delegated_rolenames(rolename):
<Returns>
A list of rolenames. Note that the rolenames are *NOT* sorted by order of
delegation!
delegation.
"""
# Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError.
@ -578,7 +691,6 @@ def clear_roledb():
<Returns>
None.
"""
_roledb_dict.clear()
@ -593,7 +705,6 @@ def _check_rolename(rolename):
'tuf.formats.ROLENAME_SCHEMA', tuf.UnknownRoleError if 'rolename' is not
found in the role database, or tuf.InvalidNameError if 'rolename' is
not formatted correctly.
"""
# Does 'rolename' have the correct object format?
@ -616,7 +727,6 @@ def _validate_rolename(rolename):
Raise tuf.InvalidNameError if 'rolename' is not formatted correctly.
It is assumed 'rolename' has been checked against 'ROLENAME_SCHEMA'
prior to calling this function.
"""
if rolename == '':

View file

@ -1,664 +0,0 @@
"""
<Program Name>
rsa_key.py
<Author>
Vladimir Diaz <vladimir.v.diaz@gmail.com>
<Started>
March 9, 2012. Based on a previous version of this module by Geremy Condra.
<Copyright>
See LICENSE for licensing information.
<Purpose>
The goal of this module is to support public-key cryptography using the RSA
algorithm. The RSA-related functions provided include generate(),
create_signature(), and verify_signature(). The create_encrypted_pem() and
create_from_encrypted_pem() functions are optional, and may be used save a
generated RSA key to a file. The 'PyCrypto' package used by 'rsa_key.py'
generates the actual RSA keys and the functions listed above can be viewed
as an easy-to-use public interface. Additional functions contained here
include create_in_metadata_format() and create_from_metadata_format(). These
last two functions produce or use RSA keys compatible with the key structures
listed in TUF Metadata files. The generate() function returns a dictionary
containing all the information needed of RSA keys, such as public and private=
keys, keyIDs, and an idenfier. create_signature() and verify_signature() are
supplemental functions used for generating RSA signatures and verifying them.
https://en.wikipedia.org/wiki/RSA_(algorithm)
Key IDs are used as identifiers for keys (e.g., RSA key). They are the
hexadecimal representation of the hash of key object (specifically, the key
object containing only the public key). Review 'rsa_key.py' and the
'_get_keyid()' function to see precisely how keyids are generated. One may
get the keyid of a key object by simply accessing the dictionary's 'keyid'
key (i.e., rsakey['keyid']).
"""
# Required for hexadecimal conversions. Signatures are hexlified.
import binascii
# Crypto.PublicKey (i.e., PyCrypto public-key cryptography) provides algorithms
# such as Digital Signature Algorithm (DSA) and the ElGamal encryption system.
# 'Crypto.PublicKey.RSA' is needed here to generate, sign, and verify RSA keys.
import Crypto.PublicKey.RSA
# PyCrypto requires 'Crypto.Hash' hash objects to generate PKCS#1 PSS
# signatures (i.e., Crypto.Signature.PKCS1_PSS).
import Crypto.Hash.SHA256
# RSA's probabilistic signature scheme with appendix (RSASSA-PSS).
# PKCS#1 v1.5 is provided for compatability with existing applications, but
# RSASSA-PSS is encouraged for newer applications. RSASSA-PSS generates
# a random salt to ensure the signature generated is probabilistic rather than
# deterministic, like PKCS#1 v1.5.
# http://en.wikipedia.org/wiki/RSA-PSS#Schemes
# https://tools.ietf.org/html/rfc3447#section-8.1
import Crypto.Signature.PKCS1_PSS
import tuf
# Digest objects needed to generate hashes.
import tuf.hash
# Perform object format-checking.
import tuf.formats
_KEY_ID_HASH_ALGORITHM = 'sha256'
# Recommended RSA key sizes:
# http://www.emc.com/emc-plus/rsa-labs/historical/twirl-and-rsa-key-size.htm#table1
# According to the document above, revised May 6, 2003, RSA keys of
# size 3072 provide security through 2031 and beyond.
_DEFAULT_RSA_KEY_BITS = 3072
def generate(bits=_DEFAULT_RSA_KEY_BITS):
"""
<Purpose>
Generate public and private RSA keys, with modulus length 'bits'.
In addition, a keyid used as an identifier for RSA keys is generated.
The object returned conforms to 'tuf.formats.RSAKEY_SCHEMA' and as the form:
{'keytype': 'rsa',
'keyid': keyid,
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
The public and private keys are in PEM format and stored as strings.
Although the crytography library called sets a 1024-bit minimum key size,
generate() enforces a minimum key size of 2048 bits. If 'bits' is
unspecified, a 3072-bit RSA key is generated, which is the key size
recommended by TUF.
<Arguments>
bits:
The key size, or key length, of the RSA key. 'bits' must be 2048, or
greater, and a multiple of 256.
<Exceptions>
ValueError, if an exception occurs after calling the RSA key generation
routine. 'bits' must be a multiple of 256. The 'ValueError' exception is
raised by the key generation function of the cryptography library called.
tuf.FormatError, if 'bits' does not contain the correct format.
<Side Effects>
The RSA keys are generated by calling PyCrypto's
Crypto.PublicKey.RSA.generate().
<Returns>
A dictionary containing the RSA keys and other identifying information.
"""
# Does 'bits' have the correct format?
# This check will ensure 'bits' conforms to 'tuf.formats.RSAKEYBITS_SCHEMA'.
# 'bits' must be an integer object, with a minimum value of 2048.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.RSAKEYBITS_SCHEMA.check_match(bits)
# Begin building the RSA key dictionary.
rsakey_dict = {}
keytype = 'rsa'
# Generate the public and private RSA keys. The PyCrypto module performs
# the actual key generation. Raise 'ValueError' if 'bits' is less than 1024
# or not a multiple of 256, although a 2048-bit minimum is enforced by
# tuf.formats.RSAKEYBITS_SCHEMA.check_match().
rsa_key_object = Crypto.PublicKey.RSA.generate(bits)
# Extract the public & private halves of the RSA key and generate their
# PEM-formatted representations. The dictionary returned contains the
# private and public RSA keys in PEM format, as strings.
private_key_pem = rsa_key_object.exportKey(format='PEM')
rsa_pubkey = rsa_key_object.publickey()
public_key_pem = rsa_pubkey.exportKey(format='PEM')
# Generate the keyid for the RSA key. 'key_value' corresponds to the
# 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key
# information is not included in the generation of the 'keyid' identifier.
key_value = {'public': public_key_pem,
'private': ''}
keyid = _get_keyid(key_value)
# Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA
# private key prior to adding 'key_value' to 'rsakey_dict'.
key_value['private'] = private_key_pem
rsakey_dict['keytype'] = keytype
rsakey_dict['keyid'] = keyid
rsakey_dict['keyval'] = key_value
return rsakey_dict
def create_in_metadata_format(key_value, private=False):
"""
<Purpose>
Return a dictionary conformant to 'tuf.formats.KEY_SCHEMA'.
If 'private' is True, include the private key. The dictionary
returned has the form:
{'keytype': 'rsa',
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
or if 'private' is False:
{'keytype': 'rsa',
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': ''}}
The private and public keys are in PEM format.
RSA keys are stored in Metadata files (e.g., root.txt) in the format
returned by this function.
<Arguments>
key_value:
A dictionary containing a private and public RSA key.
'key_value' is of the form:
{'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}},
conformat to 'tuf.formats.KEYVAL_SCHEMA'.
private:
Indicates if the private key should be included in the
returned dictionary.
<Exceptions>
tuf.FormatError, if 'key_value' does not conform to
'tuf.formats.KEYVAL_SCHEMA'.
<Side Effects>
None.
<Returns>
An 'KEY_SCHEMA' dictionary.
"""
# Does 'key_value' have the correct format?
# This check will ensure 'key_value' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.KEYVAL_SCHEMA.check_match(key_value)
if private is True and key_value['private']:
return {'keytype': 'rsa', 'keyval': key_value}
else:
public_key_value = {'public': key_value['public'], 'private': ''}
return {'keytype': 'rsa', 'keyval': public_key_value}
def create_from_metadata_format(key_metadata):
"""
<Purpose>
Construct an RSA key dictionary (i.e., tuf.formats.RSAKEY_SCHEMA)
from 'key_metadata'. The dict returned by this function has the exact
format as the dict returned by generate(). It is of the form:
{'keytype': 'rsa',
'keyid': keyid,
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
The public and private keys are in PEM format and stored as strings.
RSA key dictionaries in RSAKEY_SCHEMA format should be used by
modules storing a collection of keys, such as a keydb and keystore.
RSA keys as stored in metadata files use a different format, so this
function should be called if an RSA key is extracted from one of these
metadata files and needs converting. Generate() creates an entirely
new key and returns it in the format appropriate for 'keydb.py' and
'keystore.py'.
<Arguments>
key_metadata:
The RSA key dictionary as stored in Metadata files, conforming to
'tuf.formats.KEY_SCHEMA'. It has the form:
{'keytype': '...',
'keyval': {'public': '...',
'private': '...'}}
<Exceptions>
tuf.FormatError, if 'key_metadata' does not conform to
'tuf.formats.KEY_SCHEMA'.
<Side Effects>
None.
<Returns>
A dictionary containing the RSA keys and other identifying information.
"""
# Does 'key_metadata' have the correct format?
# This check will ensure 'key_metadata' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.KEY_SCHEMA.check_match(key_metadata)
# Construct the dictionary to be returned.
rsakey_dict = {}
keytype = 'rsa'
key_value = key_metadata['keyval']
# Convert 'key_value' to 'tuf.formats.KEY_SCHEMA' and generate its hash
# The hash is in hexdigest form.
keyid = _get_keyid(key_value)
# We now have all the required key values. Build 'rsakey_dict'.
rsakey_dict['keytype'] = keytype
rsakey_dict['keyid'] = keyid
rsakey_dict['keyval'] = key_value
return rsakey_dict
def _get_keyid(key_value):
"""Return the keyid for 'key_value'."""
# 'keyid' will be generated from an object conformant to KEY_SCHEMA,
# which is the format Metadata files (e.g., root.txt) store keys.
# 'create_in_metadata_format()' returns the object needed by _get_keyid().
rsakey_meta = create_in_metadata_format(key_value, private=False)
# Convert the RSA key to JSON Canonical format suitable for adding
# to digest objects.
rsakey_update_data = tuf.formats.encode_canonical(rsakey_meta)
# Create a digest object and call update(), using the JSON
# canonical format of 'rskey_meta' as the update data.
digest_object = tuf.hash.digest(_KEY_ID_HASH_ALGORITHM)
digest_object.update(rsakey_update_data)
# 'keyid' becomes the hexadecimal representation of the hash.
keyid = digest_object.hexdigest()
return keyid
def create_signature(rsakey_dict, data):
"""
<Purpose>
Return a signature dictionary of the form:
{'keyid': keyid,
'method': 'PyCrypto-PKCS#1 PPS',
'sig': sig}.
The signing process will use the private key
rsakey_dict['keyval']['private'] and 'data' to generate the signature.
RFC3447 - RSASSA-PSS
http://www.ietf.org/rfc/rfc3447.txt
<Arguments>
rsakey_dict:
A dictionary containing the RSA keys and other identifying information.
'rsakey_dict' has the form:
{'keytype': 'rsa',
'keyid': keyid,
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
The public and private keys are in PEM format and stored as strings.
data:
Data object used by create_signature() to generate the signature.
<Exceptions>
TypeError, if a private key is not defined for 'rsakey_dict'.
tuf.FormatError, if an incorrect format is found for the
'rsakey_dict' object.
<Side Effects>
PyCrypto's 'Crypto.Signature.PKCS1_PSS' called to perform the actual
signing.
<Returns>
A signature dictionary conformat to 'tuf.format.SIGNATURE_SCHEMA'.
"""
# Does 'rsakey_dict' have the correct format?
# This check will ensure 'rsakey_dict' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict)
# Signing the 'data' object requires a private key.
# The 'PyCrypto-PKCS#1 PSS' (i.e., PyCrypto module) signing method is the
# only method currently supported.
signature = {}
private_key = rsakey_dict['keyval']['private']
keyid = rsakey_dict['keyid']
method = 'PyCrypto-PKCS#1 PSS'
sig = None
# Verify the signature, but only if the private key has been set. The private
# key is a NULL string if unset. Although it may be clearer to explicit check
# that 'private_key' is not '', we can/should check for a value and not
# compare identities with the 'is' keyword.
if len(private_key):
# Calculate the SHA256 hash of 'data' and generate the hash's PKCS1-PSS
# signature.
try:
rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key)
sha256_object = Crypto.Hash.SHA256.new(data)
pkcs1_pss_signer = Crypto.Signature.PKCS1_PSS.new(rsa_key_object)
sig = pkcs1_pss_signer.sign(sha256_object)
except (ValueError, IndexError, TypeError), e:
message = 'An RSA signature could not be generated.'
raise tuf.CryptoError(message)
else:
raise TypeError('The required private key is not defined for "rsakey_dict".')
# Build the signature dictionary to be returned.
# The hexadecimal representation of 'sig' is stored in the signature.
signature['keyid'] = keyid
signature['method'] = method
signature['sig'] = binascii.hexlify(sig)
return signature
def verify_signature(rsakey_dict, signature, data):
"""
<Purpose>
Determine whether the private key belonging to 'rsakey_dict' produced
'signature'. verify_signature() will use the public key found in
'rsakey_dict', the 'method' and 'sig' objects contained in 'signature',
and 'data' to complete the verification. Type-checking performed on both
'rsakey_dict' and 'signature'.
<Arguments>
rsakey_dict:
A dictionary containing the RSA keys and other identifying information.
'rsakey_dict' has the form:
{'keytype': 'rsa',
'keyid': keyid,
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
The public and private keys are in PEM format and stored as strings.
signature:
The signature dictionary produced by tuf.rsa_key.create_signature().
'signature' has the form:
{'keyid': keyid, 'method': 'method', 'sig': sig}. Conformant to
'tuf.formats.SIGNATURE_SCHEMA'.
data:
Data object used by tuf.rsa_key.create_signature() to generate
'signature'. 'data' is needed here to verify the signature.
<Exceptions>
tuf.UnknownMethodError. Raised if the signing method used by
'signature' is not one supported by tuf.rsa_key.create_signature().
tuf.FormatError. Raised if either 'rsakey_dict'
or 'signature' do not match their respective tuf.formats schema.
'rsakey_dict' must conform to 'tuf.formats.RSAKEY_SCHEMA'.
'signature' must conform to 'tuf.formats.SIGNATURE_SCHEMA'.
<Side Effects>
Crypto.Signature.PKCS1_PSS.verify() called to do the actual verification.
<Returns>
Boolean. True if the signature is valid, False otherwise.
"""
# Does 'rsakey_dict' have the correct format?
# This check will ensure 'rsakey_dict' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict)
# Does 'signature' have the correct format?
tuf.formats.SIGNATURE_SCHEMA.check_match(signature)
# Using the public key belonging to 'rsakey_dict'
# (i.e., rsakey_dict['keyval']['public']), verify whether 'signature'
# was produced by rsakey_dict's corresponding private key
# rsakey_dict['keyval']['private']. Before returning the Boolean result,
# ensure 'PyCrypto-PKCS#1 PSS' was used as the signing method.
method = signature['method']
sig = signature['sig']
public_key = rsakey_dict['keyval']['public']
valid_signature = False
if method == 'PyCrypto-PKCS#1 PSS':
try:
rsa_key_object = Crypto.PublicKey.RSA.importKey(public_key)
pkcs1_pss_verifier = Crypto.Signature.PKCS1_PSS.new(rsa_key_object)
sha256_object = Crypto.Hash.SHA256.new(data)
# The metadata stores signatures in hex. Unhexlify and verify the
# signature.
signature = binascii.unhexlify(sig)
valid_signature = pkcs1_pss_verifier.verify(sha256_object, signature)
except (ValueError, IndexError, TypeError), e:
message = 'The RSA signature could not be verified.'
raise tuf.CryptoError(message)
else:
raise tuf.UnknownMethodError(method)
return valid_signature
def create_encrypted_pem(rsakey_dict, passphrase):
"""
<Purpose>
Return a string in PEM format, where the private part of the RSA key is
encrypted. The private part of the RSA key is encrypted by the Triple
Data Encryption Algorithm (3DES) and Cipher-block chaining (CBC) for the
mode of operation. Password-Based Key Derivation Function 1 (PBKF1) + MD5
is used to strengthen 'passphrase'.
https://en.wikipedia.org/wiki/Triple_DES
https://en.wikipedia.org/wiki/PBKDF2
<Arguments>
rsakey_dict:
A dictionary containing the RSA keys and other identifying information.
'rsakey_dict' has the form:
{'keytype': 'rsa',
'keyid': keyid,
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
The public and private keys are in PEM format and stored as strings.
passphrase:
The passphrase, or password, to encrypt the private part of the RSA
key. 'passphrase' is not used directly as the encryption key, a stronger
encryption key is derived from it.
<Exceptions>
TypeError, if a private key is not defined for 'rsakey_dict'.
tuf.FormatError, if an incorrect format is found for 'rsakey_dict'.
<Side Effects>
PyCrypto's Crypto.PublicKey.RSA.exportKey() called to perform the actual
generation of the PEM-formatted output.
<Returns>
A string in PEM format, where the private RSA key is encrypted.
"""
# Does 'rsakey_dict' have the correct format?
# This check will ensure 'rsakey_dict' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict)
# Does 'signature' have the correct format?
tuf.formats.PASSWORD_SCHEMA.check_match(passphrase)
# Extract the private key from 'rsakey_dict', which is stored in PEM format
# and unencrypted. The extracted key will be imported and converted to
# PyCrypto's RSA key object (i.e., Crypto.PublicKey.RSA).Use PyCrypto's
# exportKey method, with a passphrase specified, to create the string.
# PyCrypto uses PBKDF1+MD5 to strengthen 'passphrase', and 3DES with CBC mode
# for encryption.
private_key = rsakey_dict['keyval']['private']
try:
rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key)
rsakey_pem_encrypted = rsa_key_object.exportKey(format='PEM',
passphrase=passphrase)
except (ValueError, IndexError, TypeError), e:
message = 'An encrypted RSA key in PEM format could not be generated.'
raise tuf.CryptoError(message)
return rsakey_pem_encrypted
def create_from_encrypted_pem(encrypted_pem, passphrase):
"""
<Purpose>
Return an RSA key in 'tuf.formats.RSAKEY_SCHEMA' format, which has the
form:
{'keytype': 'rsa',
'keyid': keyid,
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
The RSAKEY_SCHEMA object is generated from a byte string in PEM format,
where the private part of the RSA key is encrypted. PyCrypto's importKey
method is used, where a passphrase is specified. PyCrypto uses PBKDF1+MD5
to strengthen 'passphrase', and 3DES with CBC mode for encryption/decryption.
Alternatively, key data may be encrypted with AES-CTR-Mode and the passphrase
strengthened with PBKDF2+SHA256. See 'keystore.py'.
<Arguments>
encrypted_pem:
A byte string in PEM format, where the private key is encrypted. It has
the form:
'-----BEGIN RSA PRIVATE KEY-----\n
Proc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC ...'
passphrase:
The passphrase, or password, to decrypt the private part of the RSA
key. 'passphrase' is not directly used as the encryption key, instead
it is used to derive a stronger symmetric key.
<Exceptions>
TypeError, if a private key is not defined for 'rsakey_dict'.
tuf.FormatError, if an incorrect format is found for the
'rsakey_dict' object.
<Side Effects>
PyCrypto's 'Crypto.PublicKey.RSA.importKey()' called to perform the actual
conversion from an encrypted RSA private key.
<Returns>
A dictionary in 'tuf.formats.RSAKEY_SCHEMA' format.
"""
# Does 'encryped_pem' have the correct format?
# This check will ensure 'encrypted_pem' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.PEMRSA_SCHEMA.check_match(encrypted_pem)
# Does 'passphrase' have the correct format?
tuf.formats.PASSWORD_SCHEMA.check_match(passphrase)
keytype = 'rsa'
rsakey_dict = {}
try:
rsa_key_object = Crypto.PublicKey.RSA.importKey(encrypted_pem, passphrase)
except (ValueError, IndexError, TypeError), e:
message = 'An RSA key object could not be generated from the encrypted '+\
'PEM string.'
# Raise 'tuf.CryptoError' instead of PyCrypto's exception to avoid
# revealing sensitive error, such as a decryption error due to an
# invalid passphrase.
raise tuf.CryptoError(message)
# Extract the public & private halves of the RSA key and generate their
# PEM-formatted representations. The dictionary returned contains the
# private and public RSA keys in PEM format, as strings.
private_key_pem = rsa_key_object.exportKey(format='PEM')
rsa_pubkey = rsa_key_object.publickey()
public_key_pem = rsa_pubkey.exportKey(format='PEM')
# Generate the keyid for the RSA key. 'key_value' corresponds to the
# 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key
# information is not included in the generation of the 'keyid' identifier.
key_value = {'public': public_key_pem,
'private': ''}
keyid = _get_keyid(key_value)
# Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA
# private key prior to adding 'key_value' to 'rsakey_dict'.
key_value['private'] = private_key_pem
rsakey_dict['keytype'] = keytype
rsakey_dict['keyid'] = keyid
rsakey_dict['keyval'] = key_value
return rsakey_dict

View file

@ -38,7 +38,6 @@
criteria. See 'tuf.formats.py' and the rest of this module for extensive
examples. Anything related to the checking of TUF objects and their formats
can be found in 'formats.py'.
"""
@ -55,7 +54,6 @@ class Schema:
that are encodable in JSON. 'Schema' is the base class for
the other classes defined in this module. All derived classes
should implement check_match().
"""
def matches(self, object):
@ -64,7 +62,6 @@ def matches(self, object):
Return True if 'object' matches this schema, False if it doesn't.
If the caller wishes to signal an error on a failed match, check_match()
should be called, which will raise a 'tuf.FormatError' exception.
"""
try:
@ -82,7 +79,6 @@ def check_match(self, object):
implement check_match(). If 'object' matches the schema, check_match()
should simply return. If 'object' does not match the schema,
'tuf.FormatError' should be raised.
"""
raise NotImplementedError()
@ -110,7 +106,6 @@ class Any(Schema):
True
>>> schema.matches([1, 'list'])
True
"""
def __init__(self):
@ -143,7 +138,6 @@ class String(Schema):
True
>>> schema.matches('Not hi')
False
"""
def __init__(self, string):
@ -187,7 +181,6 @@ class AnyString(Schema):
True
>>> schema.matches({})
False
"""
def __init__(self):
@ -202,6 +195,48 @@ def check_match(self, object):
class LengthString(Schema):
"""
<Purpose>
Matches any string of a specified length. The argument object
must be a string. At instantiation, the string length is set
and any future comparisons are checked against this internal
string value length.
Supported methods include
matches(): returns a Boolean result.
check_match(): raises 'tuf.FormatError' on a mismatch.
<Example Use>
>>> schema = LengthString(5)
>>> schema.matches('Hello')
True
>>> schema.matches('Hi')
False
"""
def __init__(self, length):
if isinstance(length, bool) or not isinstance(length, (int, long)):
# We need to check for bool as a special case, since bool
# is for historical reasons a subtype of int.
raise tuf.FormatError('Got '+repr(length)+' instead of an integer.')
self._string_length = length
def check_match(self, object):
if not isinstance(object, basestring):
raise tuf.FormatError('Expected a string but got '+repr(object))
if len(object) != self._string_length:
raise tuf.FormatError('Expected a string of length '+
repr(self._string_length))
class OneOf(Schema):
"""
<Purpose>
@ -229,7 +264,6 @@ class OneOf(Schema):
True
>>> schema.matches(['Hi'])
False
"""
def __init__(self, alternatives):
@ -275,7 +309,6 @@ class AllOf(Schema):
False
>>> schema.matches('a')
True
"""
def __init__(self, required_schemas):
@ -314,7 +347,6 @@ class Boolean(Schema):
True
>>> schema.matches(11)
False
"""
def __init__(self):
@ -367,7 +399,6 @@ class ListOf(Schema):
True
>>> schema.matches([3]*11)
False
"""
def __init__(self, schema, min_count=0, max_count=sys.maxint, list_name='list'):
@ -380,7 +411,6 @@ def __init__(self, schema, min_count=0, max_count=sys.maxint, list_name='list'):
min_count: The minimum number of sub-schema in 'schema'.
max_count: The maximum number of sub-schema in 'schema'.
list_name: A string identifier for the ListOf object.
"""
if not isinstance(schema, Schema):
@ -443,7 +473,6 @@ class Integer(Schema):
True
>>> Integer(lo=10, hi=30).matches(5)
False
"""
def __init__(self, lo= -sys.maxint, hi=sys.maxint):
@ -454,7 +483,6 @@ def __init__(self, lo= -sys.maxint, hi=sys.maxint):
<Arguments>
lo: The minimum value the int object argument can be.
hi: The maximum value the int object argument can be.
"""
self._lo = lo
@ -468,7 +496,7 @@ def check_match(self, object):
raise tuf.FormatError('Got '+repr(object)+' instead of an integer.')
elif not (self._lo <= object <= self._hi):
int_range = '['+repr(self._lo)+','+repr(self._hi)+'].'
int_range = '['+repr(self._lo)+', '+repr(self._hi)+'].'
raise tuf.FormatError(repr(object)+' not in range '+int_range)
@ -502,7 +530,6 @@ class DictOf(Schema):
False
>>> schema.matches({'a': ['x', 'y'], 'e' : ['', ''], 'd' : ['a', 'b']})
False
"""
def __init__(self, key_schema, value_schema):
@ -513,7 +540,6 @@ def __init__(self, key_schema, value_schema):
<Arguments>
key_schema: The dictionary's key.
value_schema: The dictionary's value.
"""
if not isinstance(key_schema, Schema):
@ -564,7 +590,6 @@ class Optional(Schema):
False
>>> schema.matches({'k1': 'X'})
True
"""
def __init__(self, schema):
@ -604,7 +629,6 @@ class Object(Schema):
False
>>> schema.matches({'a':'ZYYY'})
False
"""
def __init__(self, object_name='object', **required):
@ -616,7 +640,6 @@ def __init__(self, object_name='object', **required):
object_name: A string identifier for the object argument.
A variable number of keyword arguments is accepted.
"""
# Ensure valid arguments.
@ -713,7 +736,6 @@ class Struct(Schema):
False
>>> schema.matches(['X', 3, 'A'])
False
"""
def __init__(self, sub_schemas, optional_schemas=[], allow_more=False,
@ -727,7 +749,6 @@ def __init__(self, sub_schemas, optional_schemas=[], allow_more=False,
optional_schemas: The optional list of schemas.
allow_more: Specifies that an optional list of types is allowed.
struct_name: A string identifier for the Struct object.
"""
# Ensure each item of the list contains the expected object type.
@ -792,7 +813,6 @@ class RegularExpression(Schema):
False
>>> schema.matches([33, 'Hello'])
False
"""
def __init__(self, pattern=None, modifiers=0, re_object=None, re_name=None):
@ -805,7 +825,6 @@ def __init__(self, pattern=None, modifiers=0, re_object=None, re_name=None):
modifiers: Flags to use when compiling the pattern.
re_object: A compiled regular expression object.
re_name: Identifier for the regular expression object.
"""
if not isinstance(pattern, basestring):

View file

@ -33,7 +33,6 @@
that will determine if a role still has a sufficient number of valid keys.
If a caller needs to update the signatures of a 'signable' object, there
is also a function for that.
"""
import tuf
@ -76,7 +75,6 @@ def get_signature_status(signable, role=None):
<Returns>
A dictionary representing the status of the signatures in 'signable'.
Conformant to tuf.formats.SIGNATURESTATUS_SCHEMA.
"""
# Does 'signable' have the correct format?
@ -106,9 +104,6 @@ def get_signature_status(signable, role=None):
signed = signable['signed']
signatures = signable['signatures']
# 'signed' needed in canonical JSON format.
data = tuf.formats.encode_canonical(signed)
# Iterate through the signatures and enumerate the signature_status fields.
# (i.e., good_sigs, bad_sigs, etc.).
for signature in signatures:
@ -125,7 +120,7 @@ def get_signature_status(signable, role=None):
# Identify key using an unknown key signing method.
try:
valid_sig = tuf.rsa_key.verify_signature(key, signature, data)
valid_sig = tuf.keys.verify_signature(key, signature, signed)
except tuf.UnknownMethodError:
unknown_method_sigs.append(keyid)
continue
@ -201,7 +196,6 @@ def verify(signable, role):
<Returns>
Boolean. True if the number of good signatures >= the role's threshold,
False otherwise.
"""
# Retrieve the signature status. tuf.sig.get_signature_status() raises
@ -243,10 +237,8 @@ def may_need_new_keys(signature_status):
<Returns>
Boolean.
"""
# Does 'signature_status' have the correct format?
# This check will ensure 'signature_status' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
@ -281,11 +273,11 @@ def generate_rsa_signature(signed, rsakey_dict):
<Arguments>
signed:
The data used by 'tuf.rsa_key.create_signature()' to generate signatures.
The data used by 'tuf.keys.create_signature()' to generate signatures.
It is stored in the 'signed' field of 'signable'.
rsakey_dict:
The RSA key, a tuf.formats.RSAKEY_SCHEMA dictionary.
The RSA key, a 'tuf.formats.RSAKEY_SCHEMA' dictionary.
Used here to produce 'keyid', 'method', and 'sig'.
<Exceptions>
@ -300,7 +292,6 @@ def generate_rsa_signature(signed, rsakey_dict):
Signature dictionary conformant to tuf.formats.SIGNATURE_SCHEMA.
Has the form:
{'keyid': keyid, 'method': 'evp', 'sig': sig}
"""
# We need 'signed' in canonical JSON format to generate
@ -309,6 +300,6 @@ def generate_rsa_signature(signed, rsakey_dict):
# Generate the RSA signature.
# Raises tuf.FormatError and TypeError.
signature = tuf.rsa_key.create_signature(rsakey_dict, signed)
signature = tuf.keys.create_signature(rsakey_dict, signed)
return signature

View file

@ -14,7 +14,6 @@
<Purpose>
To provide a quick repository structure to be used in conjunction with
test modules like test_updater.py for instance.
"""
import os
@ -24,7 +23,6 @@
import tempfile
import tuf.formats
import tuf.rsa_key as rsa_key
import tuf.repo.keystore as keystore
import tuf.repo.signerlib as signerlib
import tuf.repo.signercli as signercli
@ -276,7 +274,6 @@ def create_repositories():
<Return>
A dictionary of all repositories, with the following keys:
(main_repository, client_repository, server_repository)
"""
# Ensure the keyids for the required roles are loaded. Role keyids are

View file

@ -15,7 +15,6 @@
Provides an array of various methods for unit testing. Use it instead of
actual unittest module. This module builds on unittest module.
Specifically, Modified_TestCase is a derived class from unittest.TestCase.
"""
import os
@ -27,7 +26,7 @@
import string
import ConfigParser
import tuf.rsa_key as rsa_key
import tuf.keys
import tuf.repo.keystore as keystore
# Modify the number of iterations (from the higher default count) so the unit
@ -100,7 +99,6 @@ def setUp():
random_string(length=7):
Generate a 'length' long string of random characters.
"""
# List of all top level roles.
@ -222,6 +220,7 @@ def make_temp_config_file(self, suffix='', directory=None, config_dict={}, expir
dictionary in it using ConfigParser.
It then returns the temp file path, dictionary tuple.
"""
config = ConfigParser.RawConfigParser()
if not config_dict:
# Using the fact that empty sequences are false.
@ -288,7 +287,6 @@ def make_temp_directory_with_data_files(self, _current_dir=None,
Returns:
('/tmp/tmp_dir_Test_random/', [targets/tmp_random1.txt,
targets/tmp_random2.txt, targets/more_targets/tmp_random3.txt])
"""
if not _current_dir:
@ -385,7 +383,7 @@ def generate_rsakey():
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
"""
rsakey = rsa_key.generate()
rsakey = tuf.keys.generate_rsa_key()
keyid = rsakey['keyid']
Modified_TestCase.rsa_keyids.append(keyid)
password = Modified_TestCase.random_string()

View file

@ -3,30 +3,36 @@
import timeit
import tuf
from tuf.ed25519_key import *
from tuf.ed25519_keys import *
use_pynacl = False
if '--pynacl' in sys.argv:
use_pynacl = True
print('Time generate()')
print(timeit.timeit('generate(use_pynacl)',
setup='from __main__ import generate, use_pynacl',
print('Time generate_public_and_private()')
print(timeit.timeit('generate_public_and_private(use_pynacl)',
setup='from __main__ import generate_public_and_private, \
use_pynacl',
number=1))
print('\nTime create_signature()')
print(timeit.timeit('create_signature(ed25519_key, data, use_pynacl)',
setup='from __main__ import generate, create_signature, \
print(timeit.timeit('create_signature(public, private, data, use_pynacl)',
setup='from __main__ import generate_public_and_private, \
create_signature, \
use_pynacl; \
ed25519_key = generate(use_pynacl);\
public, private = \
generate_public_and_private(use_pynacl); \
data = "The quick brown fox jumps over the lazy dog"',
number=1))
print('\nTime verify_signature()')
print(timeit.timeit('verify_signature(ed25519_key, signature, data, use_pynacl)',
setup='from __main__ import generate, create_signature, \
verify_signature, use_pynacl;\
ed25519_key = generate(use_pynacl);\
print(timeit.timeit('verify_signature(public, method, signature, data, use_pynacl)',
setup='from __main__ import generate_public_and_private, \
create_signature, \
verify_signature, use_pynacl; \
public, private = \
generate_public_and_private(use_pynacl); \
data = "The quick brown fox jumps over the lazy dog";\
signature = create_signature(ed25519_key, data, use_pynacl)',
signature, method = \
create_signature(public, private, data, use_pynacl)',
number=1))

View file

@ -340,7 +340,7 @@ def close_temp_file(self):
def get_file_details(filepath):
def get_file_details(filepath, hash_algorithms=['sha256']):
"""
<Purpose>
To get file's length and hash information. The hash is computed using the
@ -351,6 +351,8 @@ def get_file_details(filepath):
filepath:
Absolute file path of a file.
hash_algorithms:
<Exceptions>
tuf.FormatError: If hash of the file does not match HASHDICT_SCHEMA.
@ -363,6 +365,10 @@ def get_file_details(filepath):
# Making sure that the format of 'filepath' is a path string.
# 'tuf.FormatError' is raised on incorrect format.
tuf.formats.PATH_SCHEMA.check_match(filepath)
tuf.formats.HASHALGORITHMS_SCHEMA.check_match(hash_algorithms)
# The returned file hashes of 'filepath'.
file_hashes = {}
# Does the path exists?
if not os.path.exists(filepath):
@ -373,14 +379,15 @@ def get_file_details(filepath):
file_length = os.path.getsize(filepath)
# Obtaining hash of the file.
digest_object = tuf.hash.digest_filename(filepath, algorithm='sha256')
file_hash = {'sha256' : digest_object.hexdigest()}
for algorithm in hash_algorithms:
digest_object = tuf.hash.digest_filename(filepath, algorithm)
file_hashes.update({algorithm: digest_object.hexdigest()})
# Performing a format check to ensure 'file_hash' corresponds HASHDICT_SCHEMA.
# Raise 'tuf.FormatError' if there is a mismatch.
tuf.formats.HASHDICT_SCHEMA.check_match(file_hash)
tuf.formats.HASHDICT_SCHEMA.check_match(file_hashes)
return file_length, file_hash
return file_length, file_hashes
@ -547,7 +554,7 @@ def load_json_file(filepath):
Deserialize a JSON object from a file containing the object.
<Arguments>
data:
filepath:
Absolute path of JSON file.
<Exceptions>
@ -579,5 +586,3 @@ def load_json_file(filepath):
return json.load(fileobject)
finally:
fileobject.close()