mirror of
https://github.com/theupdateframework/python-tuf
synced 2026-05-24 10:08:28 +00:00
Merge branch 'repository-tools' of github.com:theupdateframework/tuf into repository-tools
This commit is contained in:
commit
47ab2bef05
65 changed files with 10130 additions and 2379 deletions
81
METADATA.md
Normal file
81
METADATA.md
Normal 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.
|
||||
62
README.md
62
README.md
|
|
@ -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
69
SECURITY.md
Normal 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).
|
||||
BIN
docs/images/libtuf-diagram.png
Normal file
BIN
docs/images/libtuf-diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
BIN
docs/latex/tuf-client-spec.pdf.old
Normal file
BIN
docs/latex/tuf-client-spec.pdf.old
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
4
setup.py
4
setup.py
|
|
@ -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
123
tests/unit/test_ed25519_keys.py
Executable 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()
|
||||
|
|
@ -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),
|
||||
|
||||
|
|
|
|||
|
|
@ -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
193
tests/unit/test_keys.py
Executable 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()
|
||||
|
|
@ -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
198
tests/unit/test_pycrypto_keys.py
Executable 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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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
349
tuf/README.md
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
#Repository Management
|
||||

|
||||
## 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 role’s 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
|
||||
```
|
||||
|
|
@ -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
0
ed25519/__init__.py → tuf/_vendor/__init__.py
Executable file → Normal file
1
tuf/_vendor/ed25519/.gitignore
vendored
Normal file
1
tuf/_vendor/ed25519/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
*.pyc
|
||||
28
tuf/_vendor/ed25519/.travis.yml
Normal file
28
tuf/_vendor/ed25519/.travis.yml
Normal 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
|
||||
0
tuf/_vendor/ed25519/__init__.py
Normal file
0
tuf/_vendor/ed25519/__init__.py
Normal file
161
tuf/_vendor/ed25519/ed25519.py
Normal file
161
tuf/_vendor/ed25519/ed25519.py
Normal 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")
|
||||
8
tuf/_vendor/ed25519/runtests.sh
Executable file
8
tuf/_vendor/ed25519/runtests.sh
Executable 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
|
||||
32
tuf/_vendor/ed25519/science.py
Normal file
32
tuf/_vendor/ed25519/science.py
Normal 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,
|
||||
))
|
||||
11
tuf/_vendor/ed25519/setup.py
Normal file
11
tuf/_vendor/ed25519/setup.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from setuptools import setup
|
||||
|
||||
|
||||
setup(
|
||||
name="ed25519",
|
||||
version="1.0",
|
||||
|
||||
py_modules="ed25519",
|
||||
|
||||
zip_safe=False,
|
||||
)
|
||||
|
|
@ -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)
|
||||
48
tuf/_vendor/ed25519/signfast.py
Normal file
48
tuf/_vendor/ed25519/signfast.py
Normal 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)
|
||||
5
tuf/_vendor/ed25519/tox.ini
Normal file
5
tuf/_vendor/ed25519/tox.ini
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[tox]
|
||||
envlist = py26,py27,pypy,py32,py33
|
||||
|
||||
[testenv]
|
||||
commands = ./runtests.sh
|
||||
172
tuf/client/README.md
Normal file
172
tuf/client/README.md
Normal 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
|
||||
|
|
@ -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
35
tuf/conf.py
35
tuf/conf.py
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
411
tuf/ed25519_keys.py
Executable 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
425
tuf/evp.py
Executable 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)
|
||||
361
tuf/formats.py
361
tuf/formats.py
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
42
tuf/keydb.py
42
tuf/keydb.py
|
|
@ -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
1244
tuf/keys.py
Executable file
File diff suppressed because it is too large
Load diff
4339
tuf/libtuf.py
Executable file
4339
tuf/libtuf.py
Executable file
File diff suppressed because it is too large
Load diff
|
|
@ -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
895
tuf/pycrypto_keys.py
Executable 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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
156
tuf/roledb.py
156
tuf/roledb.py
|
|
@ -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 == '':
|
||||
|
|
|
|||
664
tuf/rsa_key.py
664
tuf/rsa_key.py
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
17
tuf/sig.py
17
tuf/sig.py
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
21
tuf/util.py
21
tuf/util.py
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue